You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
402 lines
12 KiB
402 lines
12 KiB
-----------------------------------------------------------------------
|
|
-- Upvalued Lua API.
|
|
-----------------------------------------------------------------------
|
|
-- Functions
|
|
local date = _G.date
|
|
local error = _G.error
|
|
local type = _G.type
|
|
|
|
-- Libraries
|
|
local table = _G.table
|
|
|
|
-----------------------------------------------------------------------
|
|
-- Library namespace.
|
|
-----------------------------------------------------------------------
|
|
local LibStub = _G.LibStub
|
|
local MAJOR = "LibTextDump-1.0"
|
|
|
|
_G.assert(LibStub, MAJOR .. " requires LibStub")
|
|
|
|
local MINOR = 4 -- Should be manually increased
|
|
local lib, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
|
|
|
|
if not lib then
|
|
return
|
|
end -- No upgrade needed
|
|
|
|
-----------------------------------------------------------------------
|
|
-- Migrations.
|
|
-----------------------------------------------------------------------
|
|
lib.prototype = lib.prototype or {}
|
|
lib.metatable = lib.metatable or { __index = lib.prototype }
|
|
|
|
lib.buffers = lib.buffers or {}
|
|
lib.frames = lib.frames or {}
|
|
|
|
lib.num_frames = lib.num_frames or 0
|
|
|
|
-----------------------------------------------------------------------
|
|
-- Constants and upvalues.
|
|
-----------------------------------------------------------------------
|
|
local prototype = lib.prototype
|
|
local metatable = lib.metatable
|
|
|
|
local buffers = lib.buffers
|
|
local frames = lib.frames
|
|
|
|
local METHOD_USAGE_FORMAT = MAJOR .. ":%s() - %s."
|
|
|
|
local DEFAULT_FRAME_WIDTH = 750
|
|
local DEFAULT_FRAME_HEIGHT = 600
|
|
|
|
-----------------------------------------------------------------------
|
|
-- Helper functions.
|
|
-----------------------------------------------------------------------
|
|
local function round(number, places)
|
|
local mult = 10 ^ (places or 0)
|
|
return _G.floor(number * mult + 0.5) / mult
|
|
end
|
|
|
|
local function NewInstance(width, height, useFauxScroll)
|
|
lib.num_frames = lib.num_frames + 1
|
|
|
|
local frameName = ("%s_CopyFrame%d"):format(MAJOR, lib.num_frames)
|
|
local copyFrame = _G.CreateFrame("Frame", frameName, _G.UIParent, "UIPanelDialogTemplate")
|
|
copyFrame:SetSize(width, height)
|
|
copyFrame:SetPoint("CENTER", _G.UIParent, "CENTER")
|
|
copyFrame:SetFrameStrata("DIALOG")
|
|
copyFrame:EnableMouse(true)
|
|
copyFrame:SetMovable(true)
|
|
copyFrame:SetToplevel(true)
|
|
copyFrame:Hide()
|
|
|
|
copyFrame.title = copyFrame.Title
|
|
|
|
local titleBackground = _G[frameName.."TitleBG"]
|
|
local dragFrame = _G.CreateFrame("Frame", nil, copyFrame)
|
|
dragFrame:SetPoint("TOPLEFT", titleBackground, 16, 0)
|
|
dragFrame:SetPoint("BOTTOMRIGHT", titleBackground)
|
|
dragFrame:EnableMouse(true)
|
|
|
|
dragFrame:SetScript("OnMouseDown", function(self, button)
|
|
copyFrame:StartMoving()
|
|
end)
|
|
|
|
dragFrame:SetScript("OnMouseUp", function(self, button)
|
|
copyFrame:StopMovingOrSizing()
|
|
end)
|
|
|
|
local scrollArea
|
|
|
|
if useFauxScroll then
|
|
scrollArea = _G.CreateFrame("ScrollFrame", ("%sScroll"):format(frameName), copyFrame, "FauxScrollFrameTemplate")
|
|
|
|
function scrollArea:Update(start, wrappedLines, maxDisplayLines, lineHeight)
|
|
local lineIndex = start - 1
|
|
local linesToDisplay = 0
|
|
|
|
repeat
|
|
lineIndex = lineIndex + 1
|
|
linesToDisplay = linesToDisplay + (wrappedLines[lineIndex] or 0)
|
|
until linesToDisplay > maxDisplayLines or not wrappedLines[lineIndex]
|
|
|
|
local stop = lineIndex - 1
|
|
|
|
self:Show()
|
|
|
|
local name = self:GetName()
|
|
local scrollBar = _G[name .. "ScrollBar"]
|
|
|
|
scrollBar:SetStepsPerPage(linesToDisplay - 1)
|
|
|
|
-- This block should only be run when the buffer is changed.
|
|
-- Possible variations in linesToDisplay from scroll to scroll will affect the height of the scroll frame.
|
|
-- This will then result in inconsistent scrolling behaviour.
|
|
if lineHeight then
|
|
local scrollChildFrame = _G[name .. "ScrollChildFrame"]
|
|
|
|
local scrollFrameHeight = (wrappedLines.all - linesToDisplay) * lineHeight
|
|
local scrollChildHeight = wrappedLines.all * lineHeight
|
|
|
|
if scrollFrameHeight < 0 then
|
|
scrollFrameHeight = 0
|
|
end
|
|
|
|
self.height = scrollFrameHeight
|
|
|
|
scrollChildFrame:Show()
|
|
scrollChildFrame:SetHeight(scrollChildHeight)
|
|
|
|
scrollBar:SetMinMaxValues(0, scrollFrameHeight)
|
|
scrollBar:SetValueStep(lineHeight)
|
|
end
|
|
|
|
-- Arrow button handling
|
|
local scrollUpButton = _G[name .. "ScrollBarScrollUpButton"]
|
|
local scrollDownButton = _G[name .. "ScrollBarScrollDownButton"]
|
|
|
|
if scrollBar:GetValue() == 0 then
|
|
scrollUpButton:Disable()
|
|
else
|
|
scrollUpButton:Enable()
|
|
end
|
|
|
|
if scrollBar:GetValue() - self.height == 0 then
|
|
scrollDownButton:Disable()
|
|
else
|
|
scrollDownButton:Enable()
|
|
end
|
|
|
|
return start, stop
|
|
end
|
|
|
|
-- lineDummy is used to get the height of a line *after* word wrap to calculate the proper scroll position of the FauxScrollFrame.
|
|
local lineDummy = copyFrame:CreateFontString()
|
|
lineDummy:SetJustifyH("LEFT")
|
|
lineDummy:SetNonSpaceWrap(true)
|
|
lineDummy:SetFontObject("ChatFontNormal")
|
|
lineDummy:SetPoint("TOPLEFT", 5, 100)
|
|
lineDummy:SetPoint("BOTTOMRIGHT", copyFrame, "TOPRIGHT", -28, 0)
|
|
lineDummy:Hide()
|
|
|
|
copyFrame.lineDummy = lineDummy
|
|
else
|
|
scrollArea = _G.CreateFrame("ScrollFrame", ("%sScroll"):format(frameName), copyFrame, "UIPanelScrollFrameTemplate")
|
|
|
|
scrollArea:SetScript("OnMouseWheel", function(self, delta)
|
|
_G.ScrollFrameTemplate_OnMouseWheel(self, delta, self.ScrollBar)
|
|
end)
|
|
|
|
scrollArea.ScrollBar:SetScript("OnMouseWheel", function(self, delta)
|
|
_G.ScrollFrameTemplate_OnMouseWheel(self, delta, self)
|
|
end)
|
|
end
|
|
|
|
local dialogBackground = _G[frameName.."DialogBG"]
|
|
scrollArea:SetPoint("TOPLEFT", dialogBackground, 5, -7)
|
|
scrollArea:SetPoint("BOTTOMRIGHT", dialogBackground, -25, 5)
|
|
|
|
copyFrame.scrollArea = scrollArea
|
|
|
|
|
|
local editBox = _G.CreateFrame("EditBox", nil, copyFrame)
|
|
editBox:SetMultiLine(true)
|
|
editBox:SetMaxLetters(0)
|
|
editBox:EnableMouse(true)
|
|
editBox:SetAutoFocus(false)
|
|
editBox:SetFontObject("ChatFontNormal")
|
|
|
|
editBox:SetScript("OnEscapePressed", function()
|
|
copyFrame:Hide()
|
|
end)
|
|
|
|
copyFrame.edit_box = editBox
|
|
|
|
-- While using a standard scroll frame, editBox will be positioned automatcialy when set as its scrollChild.
|
|
-- With the faux scroll, we have to position it ourself.
|
|
if useFauxScroll then
|
|
editBox:SetAllPoints(scrollArea)
|
|
else
|
|
editBox:SetSize(scrollArea:GetSize())
|
|
scrollArea:SetScrollChild(editBox)
|
|
end
|
|
|
|
local highlightButton = _G.CreateFrame("Button", nil, copyFrame)
|
|
highlightButton:SetSize(16, 16)
|
|
highlightButton:SetPoint("TOPLEFT", titleBackground)
|
|
|
|
highlightButton:SetScript("OnMouseUp", function(self, button)
|
|
self.texture:ClearAllPoints()
|
|
self.texture:SetAllPoints(self)
|
|
|
|
editBox:HighlightText(0)
|
|
editBox:SetFocus()
|
|
|
|
copyFrame:RegisterEvent("PLAYER_LOGOUT")
|
|
end)
|
|
|
|
highlightButton:SetScript("OnMouseDown", function(self, button)
|
|
self.texture:ClearAllPoints()
|
|
self.texture:SetPoint("RIGHT", self, "RIGHT", 1, -1)
|
|
end)
|
|
|
|
highlightButton:SetScript("OnEnter", function(self)
|
|
self.texture:SetVertexColor(0.75, 0.75, 0.75)
|
|
end)
|
|
|
|
highlightButton:SetScript("OnLeave", function(self)
|
|
self.texture:SetVertexColor(1, 1, 1)
|
|
end)
|
|
|
|
local highlightIcon = highlightButton:CreateTexture()
|
|
highlightIcon:SetAllPoints()
|
|
highlightIcon:SetTexture([[Interface\BUTTONS\UI-GuildButton-PublicNote-Up]])
|
|
highlightButton.texture = highlightIcon
|
|
|
|
local instance = _G.setmetatable({}, metatable)
|
|
frames[instance] = copyFrame
|
|
buffers[instance] = {
|
|
wrappedLines = {}
|
|
}
|
|
|
|
return instance
|
|
end
|
|
|
|
|
|
-----------------------------------------------------------------------
|
|
-- Library methods.
|
|
-----------------------------------------------------------------------
|
|
--- Create a new dump frame.
|
|
-- @param frameTitle The title text of the frame.
|
|
-- @param width (optional) The width of the frame.
|
|
-- @param height (optional) The height of the frame.
|
|
-- @param save (optional) A function that will be called when the copy button is clicked.
|
|
-- @return A handle for the dump frame.
|
|
function lib:New(frameTitle, width, height, save)
|
|
local titleType = type(frameTitle)
|
|
if titleType ~= "nil" and titleType ~= "string" then
|
|
error(METHOD_USAGE_FORMAT:format("New", "frame title must be nil or a string."), 2)
|
|
end
|
|
|
|
local widthType = type(width)
|
|
if widthType ~= "nil" and widthType ~= "number" then
|
|
error(METHOD_USAGE_FORMAT:format("New", "frame width must be nil or a number."))
|
|
end
|
|
|
|
local heightType = type(height)
|
|
if heightType ~= "nil" and heightType ~= "number" then
|
|
error(METHOD_USAGE_FORMAT:format("New", "frame height must be nil or a number."))
|
|
end
|
|
|
|
local saveType = type(save)
|
|
if saveType ~= "nil" and saveType ~= "function" then
|
|
error(METHOD_USAGE_FORMAT:format("New", "save must be nil or a function."))
|
|
end
|
|
|
|
local instance = NewInstance(width or DEFAULT_FRAME_WIDTH, height or DEFAULT_FRAME_HEIGHT, not not save)
|
|
local frame = frames[instance]
|
|
frame.title:SetText(frameTitle)
|
|
|
|
if save then
|
|
frame:SetScript("OnEvent", function(event, ...)
|
|
buffers[instance].wrappedLines = nil
|
|
save(buffers[instance])
|
|
end)
|
|
end
|
|
|
|
return instance
|
|
end
|
|
|
|
|
|
-----------------------------------------------------------------------
|
|
-- Instance methods.
|
|
-----------------------------------------------------------------------
|
|
function prototype:AddLine(text, dateFormat)
|
|
self:InsertLine(#buffers[self] + 1, text, dateFormat)
|
|
|
|
if lib.frames[self]:IsVisible() then
|
|
self:Display()
|
|
end
|
|
end
|
|
|
|
function prototype:Clear()
|
|
table.wipe(buffers[self])
|
|
buffers[self].wrappedLines = {}
|
|
end
|
|
|
|
function prototype:Display(separator)
|
|
local frame = frames[self]
|
|
|
|
if frame.UpdateText then
|
|
frame:UpdateText("Display")
|
|
else
|
|
local displayText = self:String(separator)
|
|
|
|
if displayText == "" then
|
|
error(METHOD_USAGE_FORMAT:format("Display", "buffer must be non-empty"), 2)
|
|
end
|
|
|
|
frame.edit_box:SetText(displayText)
|
|
frame.edit_box:SetCursorPosition(0)
|
|
end
|
|
|
|
frame:Show()
|
|
end
|
|
|
|
function prototype:InsertLine(position, text, dateFormat)
|
|
if type(position) ~= "number" then
|
|
error(METHOD_USAGE_FORMAT:format("InsertLine", "position must be a number."))
|
|
end
|
|
|
|
if type(text) ~= "string" or text == "" then
|
|
error(METHOD_USAGE_FORMAT:format("InsertLine", "text must be a non-empty string."), 2)
|
|
end
|
|
|
|
local buffer = buffers[self]
|
|
|
|
if dateFormat and dateFormat ~= "" then
|
|
table.insert(buffer, position, ("[%s] %s"):format(date(dateFormat), text))
|
|
else
|
|
table.insert(buffer, position, text)
|
|
end
|
|
|
|
local lineDummy = frames[self].lineDummy
|
|
|
|
if lineDummy then
|
|
lineDummy:SetText(buffer[position])
|
|
table.insert(buffer.wrappedLines, position, lineDummy:GetNumLines())
|
|
buffer.wrappedLines.all = (buffer.wrappedLines.all or 0) + buffer.wrappedLines[position]
|
|
end
|
|
end
|
|
|
|
function prototype:Lines()
|
|
return #buffers[self]
|
|
end
|
|
|
|
function prototype:String(separator)
|
|
local separatorType = type(separator)
|
|
|
|
if separatorType ~= "nil" and separatorType ~= "string" then
|
|
error(METHOD_USAGE_FORMAT:format("String", "separator must be nil or a string."), 2)
|
|
end
|
|
|
|
separator = separator or "\n"
|
|
|
|
local buffer = buffers[self]
|
|
local frame = frames[self]
|
|
local lineDummy = frame.lineDummy
|
|
|
|
if lineDummy then
|
|
local _, lineHeight = lineDummy:GetFont()
|
|
local maxDisplayLines = round(frame.edit_box:GetHeight() / lineHeight)
|
|
local allWrappedLines = buffer.wrappedLines.all
|
|
local offset = 1
|
|
local start, stop = frame.scrollArea:Update(offset, buffer.wrappedLines, maxDisplayLines, lineHeight)
|
|
|
|
function frame:UpdateText()
|
|
local newWrappedLines = buffer.wrappedLines.all
|
|
|
|
if newWrappedLines > allWrappedLines then
|
|
allWrappedLines = newWrappedLines
|
|
start, stop = frame.scrollArea:Update(offset, buffer.wrappedLines, maxDisplayLines, lineHeight)
|
|
else
|
|
start, stop = frame.scrollArea:Update(offset, buffer.wrappedLines, maxDisplayLines)
|
|
end
|
|
|
|
frame.edit_box:SetText(table.concat(buffer, separator, start, stop))
|
|
end
|
|
|
|
frame.scrollArea:SetScript("OnVerticalScroll", function(scrollArea, value)
|
|
local scrollbar = scrollArea.ScrollBar
|
|
local _, scrollMax = scrollbar:GetMinMaxValues()
|
|
local scrollPer = round(value / scrollMax, 2)
|
|
offset = round((1 - scrollPer) * 1 + scrollPer * #buffer)
|
|
|
|
frame:UpdateText()
|
|
end)
|
|
|
|
return table.concat(buffer, separator, start, stop)
|
|
else
|
|
return table.concat(buffer, separator)
|
|
end
|
|
end
|
|
|