--[[ Copyright (c) 2008, LibTooltip Development Team All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Redistribution of a stand alone version is strictly prohibited without prior written authorization from the Lead of the LibTooltip Development Team. * Neither the name of the LibTooltip Development Team nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --]] assert(LibStub, "LibTooltip-1.0 requires LibStub") local MAJOR, MINOR = "LibTooltip-1.0", 2 local LibTooltip, oldminor = LibStub:NewLibrary(MAJOR, MINOR) if not LibTooltip then return end -- No upgrade needed -- Internal constants to tweak the layout local TOOLTIP_PADDING = 10 local CELL_MARGIN = 3 local bgFrame = { edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", tile = true, tileSize = 16, edgeSize = 16, insets = {left = 5, right = 5, top = 5, bottom = 5} } ------------------------------------------------------------------------------ -- Tables and locals ------------------------------------------------------------------------------ LibTooltip.frameMetatable = LibTooltip.frameMetatable or {__index = CreateFrame("Frame")} LibTooltip.tipPrototype = LibTooltip.tipPrototype or setmetatable({}, LibTooltip.frameMetatable) LibTooltip.tipMetatable = LibTooltip.tipMetatable or {__index = LibTooltip.tipPrototype} LibTooltip.providerPrototype = LibTooltip.providerPrototype or {} LibTooltip.providerMetatable = LibTooltip.providerMetatable or {__index = LibTooltip.providerPrototype} LibTooltip.cellPrototype = LibTooltip.cellPrototype or setmetatable({}, LibTooltip.frameMetatable) LibTooltip.cellMetatable = LibTooltip.cellMetatable or { __index = LibTooltip.cellPrototype } LibTooltip.activeTooltips = LibTooltip.activeTooltips or {} LibTooltip.tooltipHeap = LibTooltip.tooltipHeap or {} LibTooltip.frameHeap = LibTooltip.frameHeap or {} local tipPrototype = LibTooltip.tipPrototype local tipMetatable = LibTooltip.tipMetatable local providerPrototype = LibTooltip.providerPrototype local providerMetatable = LibTooltip.providerMetatable local cellPrototype = LibTooltip.cellPrototype local cellMetatable = LibTooltip.cellMetatable local activeTooltips = LibTooltip.activeTooltips local tooltipHeap = LibTooltip.tooltipHeap local frameHeap = LibTooltip.frameHeap -- Tooltip private methods local InitializeTooltip, FinalizeTooltip, ResetTooltipSize, ResizeColspans local AcquireCell, ReleaseCell ------------------------------------------------------------------------------ -- Public library API ------------------------------------------------------------------------------ function LibTooltip:Acquire(key, ...) if key == nil then error("attempt to use a nil key", 2) end local tooltip = activeTooltips[key] if not tooltip then tooltip = tremove(tooltipHeap) or setmetatable(CreateFrame("Frame", nil, UIParent), tipMetatable) InitializeTooltip(tooltip, key) activeTooltips[key] = tooltip end if select('#', ...) > 0 then -- Here we catch any error to properly report it for the calling code local ok, msg = pcall(tooltip.SetColumnLayout, tooltip, ...) if not ok then error(msg, 2) end end return tooltip end function LibTooltip:IsAcquired(key) if key == nil then error("attempt to use a nil key", 2) end return not not activeTooltips[key] end function LibTooltip:Release(tooltip) local key = tooltip and tooltip.key if not key or activeTooltips[key] ~= tooltip then return end tooltip:Hide() FinalizeTooltip(tooltip) tinsert(tooltipHeap, tooltip) activeTooltips[key] = nil end function LibTooltip:IterateTooltips() return pairs(activeTooltips) end ------------------------------------------------------------------------------ -- Frame heap ------------------------------------------------------------------------------ local function AcquireFrame(parent) local frame = tremove(frameHeap) or CreateFrame("Frame") frame:SetParent(parent) return frame end local function ReleaseFrame(frame) frame:Hide() frame:SetParent(nil) frame:ClearAllPoints() tinsert(frameHeap, frame) end ------------------------------------------------------------------------------ -- CellProvider and Cell ------------------------------------------------------------------------------ -- Provider prototype function providerPrototype:AcquireCell(tooltip) local cell = tremove(self.heap) if not cell then cell = setmetatable(CreateFrame("Frame", nil, tooltip), self.cellMetatable) if type(cell.InitializeCell) == 'function' then cell:InitializeCell() end end self.cells[cell] = true return cell end function providerPrototype:ReleaseCell(cell) if not self.cells[cell] then return end if type(cell.ReleaseCell) == 'function' then cell:ReleaseCell() end self.cells[cell] = nil tinsert(self.heap, cell) end function providerPrototype:GetCellPrototype() return self.cellPrototype, self.cellMetatable end function providerPrototype:IterateCells() return pairs(self.cells) end -- Cell provider factory function LibTooltip:CreateCellProvider(baseProvider) local cellBaseMetatable, cellBasePrototype if baseProvider and baseProvider.GetCellPrototype then cellBasePrototype, cellBaseMetatable = baseProvider:GetCellPrototype() else cellBaseMetatable = cellMetatable end local cellPrototype = setmetatable({}, cellBaseMetatable) local cellProvider = setmetatable({}, providerMetatable) cellProvider.heap = {} cellProvider.cells = {} cellProvider.cellPrototype = cellPrototype cellProvider.cellMetatable = { __index = cellPrototype } return cellProvider, cellPrototype, cellBasePrototype end ------------------------------------------------------------------------------ -- Basic label provider ------------------------------------------------------------------------------ if not LibTooltip.LabelProvider then LibTooltip.LabelProvider, LibTooltip.LabelPrototype = LibTooltip:CreateCellProvider() end local labelProvider = LibTooltip.LabelProvider local labelPrototype = LibTooltip.LabelPrototype function labelPrototype:InitializeCell() self.fontString = self:CreateFontString() self.fontString:SetAllPoints(self) self.fontString:SetFontObject(GameTooltipText) end function labelPrototype:SetupCell(tooltip, value, justification, font, ...) local fs = self.fontString fs:SetFontObject(font or tooltip:GetFont()) fs:SetJustifyH(justification) fs:SetText(tostring(value)) fs:Show() return fs:GetStringWidth(), fs:GetStringHeight() end ------------------------------------------------------------------------------ -- Helpers ------------------------------------------------------------------------------ local function checkFont(font, level, silent) if not font or type(font) ~= 'table' or type(font.IsObjectType) ~= 'function' or not font:IsObjectType("Font") then if silent then return false else error("font must be Font instance, not: "..tostring(font), level+1) end end return true end local function checkJustification(justification, level, silent) if justification ~= "LEFT" and justification ~= "CENTER" and justification ~= "RIGHT" then if silent then return false else error("invalid justification, must one of LEFT, CENTER or RIGHT, not: "..tostring(justification), level+1) end end return true end ------------------------------------------------------------------------------ -- Tooltip prototype ------------------------------------------------------------------------------ function InitializeTooltip(self, key) -- (Re)set frame settings self:SetBackdrop(bgFrame) self:SetBackdropColor(0.09, 0.09, 0.09) self:SetBackdropBorderColor(1, 1, 1) self:SetAlpha(0.9) self:SetScale(1.0) self:SetFrameStrata("TOOLTIP") self:SetClampedToScreen(false) -- Our data self.key = key self.columns = self.columns or {} self.lines = self.lines or {} self.colspans = self.colspans or {} self.regularFont = GameTooltipText self.headerFont = GameTooltipHeaderText self:SetScript('OnShow', ResizeColspans) ResetTooltipSize(self) end function tipPrototype:SetColumnLayout(numColumns, ...) if type(numColumns) ~= "number" or numColumns < 1 then error("number of columns must be a positive number, not: "..tostring(numColumns), 2) end for i = 1, numColumns do local justification = select(i, ...) or "LEFT" checkJustification(justification, 2) if self.columns[i] then self.columns[i].justification = justification else self:AddColumn(justification) end end end function tipPrototype:AddColumn(justification) justification = justification or "LEFT" checkJustification(justification, 2) local colNum = #self.columns + 1 local column = AcquireFrame(self) column.justification = justification column.width = 0 column:SetWidth(0) column:SetPoint("TOP", self, "TOP", 0, -TOOLTIP_PADDING) column:SetPoint("BOTTOM", self, "BOTTOM", 0, TOOLTIP_PADDING) if colNum > 1 then column:SetPoint("LEFT", self.columns[colNum-1], "RIGHT", CELL_MARGIN, 0) self.width = self.width + CELL_MARGIN self:SetWidth(self.width) else column:SetPoint("LEFT", self, "LEFT", TOOLTIP_PADDING, 0) end column:Show() self.columns[colNum] = column return colNum end function FinalizeTooltip(self) self:Clear() for i, column in ipairs(self.columns) do column:Hide() ReleaseFrame(column) self.columns[i] = nil end end function ResetTooltipSize(self) self.width = 2*TOOLTIP_PADDING + math.max(0, CELL_MARGIN * (#self.columns - 1)) self.height = 2*TOOLTIP_PADDING self:SetWidth(self.width) self:SetHeight(self.height) end function tipPrototype:Clear() for i, line in ipairs(self.lines) do for j, cell in ipairs(line.cells) do ReleaseCell(self, cell) line.cells[j] = nil end line:Hide() ReleaseFrame(line) self.lines[i] = nil end for i, column in ipairs(self.columns) do column.width = 0 column:SetWidth(0) end for k in pairs(self.colspans) do self.colspans[k] = nil end ResetTooltipSize(self) end function tipPrototype:SetFont(font) checkFont(font, 2) self.regularFont = font end function tipPrototype:GetFont() return self.regularFont end function tipPrototype:SetHeaderFont(font) checkFont(font, 2) self.headerFont = font end function tipPrototype:GetHeaderFont() return self.headerFont end local function EnlargeColumn(self, column, width) if width > column.width then self.width = self.width + width - column.width self:SetWidth(self.width) column.width = width column:SetWidth(width) end end function ResizeColspans(self) if not self:IsShown() then return end local columns = self.columns for colRange, width in pairs(self.colspans) do local left, right = colRange:match("(%d)%-(%d)") left, right = tonumber(left), tonumber(right) for col = left, right-1 do width = width - columns[col].width - CELL_MARGIN end EnlargeColumn(self, columns[right], width) self.colspans[colRange] = nil end end function AcquireCell(self, provider) local cell = provider:AcquireCell(self) cell:SetParent(self) cell:SetFrameLevel(self:GetFrameLevel()+1) return cell end function ReleaseCell(self, cell) if cell and cell:GetParent() == self then cell:Hide() cell:SetParent(nil) cell:ClearAllPoints() cell._provider:ReleaseCell(cell) end end local function _SetCell(self, lineNum, colNum, value, font, justification, colSpan, provider, ...) local line = self.lines[lineNum] local cells = line.cells -- Unset: be quick if value == nil then local cell = cells[colNum] if cell then for i = colNum, colNum + cell._colSpan - 1 do cells[i] = nil end ReleaseCell(self, cell) end return lineNum, colNum end -- Check previous cell local cell local prevCell = cells[colNum] if prevCell == false then error("overlapping cells at column "..colNum, 3) elseif prevCell then -- There is a cell here font = font or prevCell._font justification = justification or prevCell._justification colSpan = colSpan or prevCell._colSpan if provider == nil or prevCell._provider == provider then -- Reuse existing cell cell = prevCell provider = cell._provider else -- A new cell is required ReleaseCell(self, prevCell) cells[colNum] = nil end else -- Creating a new cell, use meaning full defaults provider = provider or labelProvider font = font or self.regularFont justification = justification or self.columns[colNum].justification or "LEFT" colSpan = colSpan or 1 end local tooltipWidth = #self.columns local rightColNum if colSpan > 0 then rightColNum = colNum + colSpan - 1 if rightColNum > tooltipWidth then error("ColSpan too big, cell extends beyond right-most column", 3) end else -- Zero or negative: count back from right-most columns rightColNum = math.max(colNum, tooltipWidth + colSpan) -- Update colspan to its effective value colSpan = 1 + rightColNum - colNum end -- Cleanup colspans for i = colNum + 1, rightColNum do local cell = cells[i] if cell == false then error("overlapping cells at column "..i, 3) elseif cell then ReleaseCell(self, cell) end cells[i] = false end -- Create the cell and anchor it if not cell then cell = AcquireCell(self, provider) cell:SetPoint("LEFT", self.columns[colNum], "LEFT", 0, 0) cell:SetPoint("TOP", line, "TOP", 0, 0) cell:SetPoint("BOTTOM", line, "BOTTOM", 0, 0) cells[colNum] = cell end cell:SetPoint("RIGHT", self.columns[rightColNum], "RIGHT", 0, 0) -- Store the cell settings directly into the cell -- That's a bit risky but is is really cheap compared to other ways to do it cell._provider, cell._font, cell._justification, cell._colSpan = provider, font, justification, colSpan -- Setup the cell content local width, height = cell:SetupCell(tooltip, value, justification, font, ...) -- Enforce cell size cell:SetWidth(width) cell:SetHeight(height) cell:Show() if colSpan > 1 then -- Postpone width changes until the tooltip is shown local colRange = colNum.."-"..rightColNum self.colspans[colRange] = math.max(self.colspans[colRange] or 0, width) else -- Enlarge the column and tooltip if need be EnlargeColumn(self, self.columns[colNum], width) end -- Enlarge the line and tooltip if need be if height > line.height then self.height = self.height + height - line.height self:SetHeight(self.height) line.height = height line:SetHeight(height) end if rightColNum < tooltipWidth then return lineNum, rightColNum+1 else return lineNum, nil end end local function CreateLine(self, font, ...) local line = AcquireFrame(self) local lineNum = #self.lines + 1 line:SetPoint('LEFT', self, 'LEFT', TOOLTIP_PADDING, 0) line:SetPoint('RIGHT', self, 'RIGHT', -TOOLTIP_PADDING, 0) if lineNum > 1 then line:SetPoint('TOP', self.lines[lineNum-1], 'BOTTOM', 0, -CELL_MARGIN) self.height = self.height + CELL_MARGIN self:SetHeight(self.height) else line:SetPoint('TOP', self, 'TOP', 0, -TOOLTIP_PADDING) end self.lines[lineNum] = line line.cells = line.cells or {} line.height = 0 line:SetHeight(0) line:Show() local colNum = 1 for i = 1, #self.columns do local value = select(i, ...) if value ~= nil then lineNum, colNum = _SetCell(self, lineNum, i, value, font, nil, 1, labelProvider) end end return lineNum, colNum end function tipPrototype:AddLine(...) local lineNum, colNum = CreateLine(self, self.regularFont, ...) ResizeColspans(self) return lineNum, colNum end function tipPrototype:AddHeader(...) local lineNum, colNum = CreateLine(self, self.headerFont, ...) ResizeColspans(self) return lineNum, colNum end function tipPrototype:SetCell(lineNum, colNum, value, ...) -- Mandatory argument checking if type(lineNum) ~= "number" then error("line number must be a number, not: "..tostring(lineNum), 2) elseif lineNum < 1 or lineNum > #self.lines then error("line number out of range: "..tostring(lineNum), 2) elseif type(colNum) ~= "number" then error("column number must be a number, not: "..tostring(colNum), 2) elseif colNum < 1 or colNum > #self.columns then error("column number out of range: "..tostring(colNum), 2) end -- Variable argument checking local font, justification, colSpan, provider local i, arg = 1, ... if arg == nil or checkFont(arg, 2, true) then i, font, arg = 2, ... end if arg == nil or checkJustification(arg, 2, true) then i, justification, arg = i+1, select(i, ...) end if arg == nil or type(arg) == 'number' then i, colSpan, arg = i+1, select(i, ...) end if arg == nil or type(arg) == 'table' and type(arg.AcquireCell) == 'function' then i, provider = i+1, arg end lineNum, colNum = _SetCell(self, lineNum, colNum, value, font, justification, colSpan, provider, select(i, ...)) ResizeColspans(self) return lineNum, colNum end function tipPrototype:GetLineCount() return #self.lines end function tipPrototype:GetColumnCount() return #self.columns end ------------------------------------------------------------------------------ -- "Smart" Anchoring (work in progress) ------------------------------------------------------------------------------ local function GetTipAnchor(frame) local x,y = frame:GetCenter() if not x or not y then return "TOPLEFT", "BOTTOMLEFT" end local hhalf = (x > UIParent:GetWidth()*2/3) and "RIGHT" or (x < UIParent:GetWidth()/3) and "LEFT" or "" local vhalf = (y > UIParent:GetHeight()/2) and "TOP" or "BOTTOM" return vhalf..hhalf, frame, (vhalf == "TOP" and "BOTTOM" or "TOP")..hhalf end function tipPrototype:SmartAnchorTo(frame) if not frame then error("Invalid frame provided.", 2) end self:ClearAllPoints() self:SetClampedToScreen(true) self:SetPoint(GetTipAnchor(frame)) end