----------------------------------------------------------------------- -- 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