local MAJOR = "LibQTip-1.0" local MINOR = 42 -- Should be manually increased assert(LibStub, MAJOR .. " requires LibStub") local lib, oldminor = LibStub:NewLibrary(MAJOR, MINOR) if not lib then return end -- No upgrade needed ------------------------------------------------------------------------------ -- Upvalued globals ------------------------------------------------------------------------------ local _G = getfenv(0) local type = type local select = select local error = error local pairs, ipairs = pairs, ipairs local tonumber, tostring = tonumber, tostring local strfind = string.find local math = math local next = next local min, max = math.min, math.max local setmetatable = setmetatable local tinsert, tremove = tinsert, tremove local wipe = wipe local CreateFrame = CreateFrame local UIParent = UIParent ------------------------------------------------------------------------------ -- Tables and locals ------------------------------------------------------------------------------ lib.frameMetatable = lib.frameMetatable or { __index = CreateFrame("Frame") } lib.tipPrototype = lib.tipPrototype or setmetatable({}, lib.frameMetatable) lib.tipMetatable = lib.tipMetatable or { __index = lib.tipPrototype } lib.providerPrototype = lib.providerPrototype or {} lib.providerMetatable = lib.providerMetatable or { __index = lib.providerPrototype } lib.cellPrototype = lib.cellPrototype or setmetatable({}, lib.frameMetatable) lib.cellMetatable = lib.cellMetatable or { __index = lib.cellPrototype } lib.activeTooltips = lib.activeTooltips or {} lib.tooltipHeap = lib.tooltipHeap or {} lib.frameHeap = lib.frameHeap or {} lib.tableHeap = lib.tableHeap or {} local tipPrototype = lib.tipPrototype local tipMetatable = lib.tipMetatable local providerPrototype = lib.providerPrototype local providerMetatable = lib.providerMetatable local cellPrototype = lib.cellPrototype local cellMetatable = lib.cellMetatable local activeTooltips = lib.activeTooltips ------------------------------------------------------------------------------ -- Private methods for Caches and Tooltip ------------------------------------------------------------------------------ local AcquireTooltip, ReleaseTooltip local AcquireCell, ReleaseCell local AcquireTable, ReleaseTable local InitializeTooltip, SetTooltipSize, ResetTooltipSize, FixCellSizes local ClearTooltipScripts local SetFrameScript, ClearFrameScripts ------------------------------------------------------------------------------ -- Cache debugging. ------------------------------------------------------------------------------ -- @debug @ local usedTables, usedFrames, usedTooltips = 0, 0, 0 --@end-debug@ ------------------------------------------------------------------------------ -- Internal constants to tweak the layout ------------------------------------------------------------------------------ local TOOLTIP_PADDING = 10 local CELL_MARGIN_H = 6 local CELL_MARGIN_V = 3 ------------------------------------------------------------------------------ -- Public library API ------------------------------------------------------------------------------ --- Create or retrieve the tooltip with the given key. -- If additional arguments are passed, they are passed to :SetColumnLayout for the acquired tooltip. -- @name LibQTip:Acquire(key[, numColumns, column1Justification, column2justification, ...]) -- @param key string or table - the tooltip key. Any value that can be used as a table key is accepted though you should try to provide unique keys to avoid conflicts. -- Numbers and booleans should be avoided and strings should be carefully chosen to avoid namespace clashes - no "MyTooltip" - you have been warned! -- @return tooltip Frame object - the acquired tooltip. -- @usage Acquire a tooltip with at least 5 columns, justification : left, center, left, left, left --
local tip = LibStub('LibQTip-1.0'):Acquire('MyFooBarTooltip', 5, "LEFT", "CENTER")function lib:Acquire(key, ...) if key == nil then error("attempt to use a nil key", 2) end local tooltip = activeTooltips[key] if not tooltip then tooltip = AcquireTooltip() 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 lib:Release(tooltip) local key = tooltip and tooltip.key if not key or activeTooltips[key] ~= tooltip then return end ReleaseTooltip(tooltip) activeTooltips[key] = nil end function lib:IsAcquired(key) if key == nil then error("attempt to use a nil key", 2) end return not not activeTooltips[key] end function lib:IterateTooltips() return pairs(activeTooltips) end ------------------------------------------------------------------------------ -- Frame cache ------------------------------------------------------------------------------ local frameHeap = lib.frameHeap local function AcquireFrame(parent) local frame = tremove(frameHeap) or CreateFrame("Frame") frame:SetParent(parent) --[===[@debug@ usedFrames = usedFrames + 1 --@end-debug@]===] return frame end local function ReleaseFrame(frame) frame:Hide() frame:SetParent(nil) frame:ClearAllPoints() frame:SetBackdrop(nil) ClearFrameScripts(frame) tinsert(frameHeap, frame) --[===[@debug@ usedFrames = usedFrames - 1 --@end-debug@]===] end ------------------------------------------------------------------------------ -- Dirty layout handler ------------------------------------------------------------------------------ lib.layoutCleaner = lib.layoutCleaner or CreateFrame('Frame') local layoutCleaner = lib.layoutCleaner layoutCleaner.registry = layoutCleaner.registry or {} function layoutCleaner:RegisterForCleanup(tooltip) self.registry[tooltip] = true self:Show() end function layoutCleaner:CleanupLayouts() self:Hide() for tooltip in pairs(self.registry) do FixCellSizes(tooltip) end wipe(self.registry) end layoutCleaner:SetScript('OnUpdate', layoutCleaner.CleanupLayouts) ------------------------------------------------------------------------------ -- CellProvider and Cell ------------------------------------------------------------------------------ function providerPrototype:AcquireCell() local cell = tremove(self.heap) if not cell then cell = setmetatable(CreateFrame("Frame", nil, UIParent), 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 function lib: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 lib.LabelProvider then lib.LabelProvider, lib.LabelPrototype = lib:CreateCellProvider() end local labelProvider = lib.LabelProvider local labelPrototype = lib.LabelPrototype function labelPrototype:InitializeCell() self.fontString = self:CreateFontString() self.fontString:SetFontObject(_G.GameTooltipText) end function labelPrototype:SetupCell(tooltip, value, justification, font, l_pad, r_pad, max_width, min_width, ...) local fs = self.fontString local line = tooltip.lines[self._line] -- detatch fs from cell for size calculations fs:ClearAllPoints() fs:SetFontObject(font or (line.is_header and tooltip:GetHeaderFont() or tooltip:GetFont())) fs:SetJustifyH(justification) fs:SetText(tostring(value)) l_pad = l_pad or 0 r_pad = r_pad or 0 local width = fs:GetStringWidth() + l_pad + r_pad if max_width and min_width and (max_width < min_width) then error("maximum width cannot be lower than minimum width: " .. tostring(max_width) .. " < " .. tostring(min_width), 2) end if max_width and (max_width < (l_pad + r_pad)) then error("maximum width cannot be lower than the sum of paddings: " .. tostring(max_width) .. " < " .. tostring(l_pad) .. " + " .. tostring(r_pad), 2) end if min_width and width < min_width then width = min_width end if max_width and max_width < width then width = max_width end fs:SetWidth(width - (l_pad + r_pad)) -- Use GetHeight() instead of GetStringHeight() so lines which are longer than width will wrap. local height = fs:GetHeight() -- reanchor fs to cell fs:SetWidth(0) fs:SetPoint("TOPLEFT", self, "TOPLEFT", l_pad, 0) fs:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", -r_pad, 0) --~ fs:SetPoint("TOPRIGHT", self, "TOPRIGHT", -r_pad, 0) self._paddingL = l_pad self._paddingR = r_pad return width, height end function labelPrototype:getContentHeight() local fs = self.fontString fs:SetWidth(self:GetWidth() - (self._paddingL + self._paddingR)) local height = self.fontString:GetHeight() fs:SetWidth(0) return height end function labelPrototype:GetPosition() return self._line, self._column end ------------------------------------------------------------------------------ -- Tooltip cache ------------------------------------------------------------------------------ local tooltipHeap = lib.tooltipHeap -- Returns a tooltip function AcquireTooltip() local tooltip = tremove(tooltipHeap) if not tooltip then tooltip = CreateFrame("Frame", nil, UIParent) local scrollFrame = CreateFrame("ScrollFrame", nil, tooltip) scrollFrame:SetPoint("TOP", tooltip, "TOP", 0, -TOOLTIP_PADDING) scrollFrame:SetPoint("BOTTOM", tooltip, "BOTTOM", 0, TOOLTIP_PADDING) scrollFrame:SetPoint("LEFT", tooltip, "LEFT", TOOLTIP_PADDING, 0) scrollFrame:SetPoint("RIGHT", tooltip, "RIGHT", -TOOLTIP_PADDING, 0) tooltip.scrollFrame = scrollFrame local scrollChild = CreateFrame("Frame", nil, tooltip.scrollFrame) scrollFrame:SetScrollChild(scrollChild) tooltip.scrollChild = scrollChild setmetatable(tooltip, tipMetatable) end --[===[@debug@ usedTooltips = usedTooltips + 1 --@end-debug@]===] return tooltip end -- Cleans the tooltip and stores it in the cache function ReleaseTooltip(tooltip) if tooltip.releasing then return end tooltip.releasing = true tooltip:Hide() if tooltip.OnRelease then local success, errorMessage = pcall(tooltip.OnRelease, tooltip) if not success then geterrorhandler()(errorMessage) end tooltip.OnRelease = nil end tooltip.releasing = nil tooltip.key = nil tooltip.step = nil ClearTooltipScripts(tooltip) tooltip:SetAutoHideDelay(nil) tooltip:ClearAllPoints() tooltip:Clear() if tooltip.slider then tooltip.slider:SetValue(0) tooltip.slider:Hide() tooltip.scrollFrame:SetPoint("RIGHT", tooltip, "RIGHT", -TOOLTIP_PADDING, 0) tooltip:EnableMouseWheel(false) end for i, column in ipairs(tooltip.columns) do tooltip.columns[i] = ReleaseFrame(column) end tooltip.columns = ReleaseTable(tooltip.columns) tooltip.lines = ReleaseTable(tooltip.lines) tooltip.colspans = ReleaseTable(tooltip.colspans) layoutCleaner.registry[tooltip] = nil tinsert(tooltipHeap, tooltip) --[===[@debug@ usedTooltips = usedTooltips - 1 --@end-debug@]===] end ------------------------------------------------------------------------------ -- Cell 'cache' (just a wrapper to the provider's cache) ------------------------------------------------------------------------------ -- Returns a cell for the given tooltip from the given provider function AcquireCell(tooltip, provider) local cell = provider:AcquireCell(tooltip) cell:SetParent(tooltip.scrollChild) cell:SetFrameLevel(tooltip.scrollChild:GetFrameLevel() + 3) cell._provider = provider return cell end -- Cleans the cell hands it to its provider for storing function ReleaseCell(cell) if cell.fontString then cell.fontString:SetFontObject(cell._font) if cell.r then cell.fontString:SetTextColor(cell.r, cell.g, cell.b) end end cell._font = nil cell._justification = nil cell._colSpan = nil cell._line = nil cell._column = nil cell:Hide() cell:ClearAllPoints() cell:SetParent(nil) cell:SetBackdrop(nil) ClearFrameScripts(cell) cell._provider:ReleaseCell(cell) cell._provider = nil end ------------------------------------------------------------------------------ -- Table cache ------------------------------------------------------------------------------ local tableHeap = lib.tableHeap -- Returns a table function AcquireTable() local tbl = tremove(tableHeap) or {} --[===[@debug@ usedTables = usedTables + 1 --@end-debug@]===] return tbl end -- Cleans the table and stores it in the cache function ReleaseTable(table) wipe(table) tinsert(tableHeap, table) --[===[@debug@ usedTables = usedTables - 1 --@end-debug@]===] end ------------------------------------------------------------------------------ -- Tooltip prototype ------------------------------------------------------------------------------ function InitializeTooltip(tooltip, key) ---------------------------------------------------------------------- -- (Re)set frame settings ---------------------------------------------------------------------- local backdrop = GameTooltip:GetBackdrop() tooltip:SetBackdrop(backdrop) if backdrop then tooltip:SetBackdropColor(GameTooltip:GetBackdropColor()) tooltip:SetBackdropBorderColor(GameTooltip:GetBackdropBorderColor()) end tooltip:SetScale(GameTooltip:GetScale()) tooltip:SetAlpha(1) tooltip:SetFrameStrata("TOOLTIP") tooltip:SetClampedToScreen(false) ---------------------------------------------------------------------- -- Internal data. Since it's possible to Acquire twice without calling -- release, check for pre-existence. ---------------------------------------------------------------------- tooltip.key = key tooltip.columns = tooltip.columns or AcquireTable() tooltip.lines = tooltip.lines or AcquireTable() tooltip.colspans = tooltip.colspans or AcquireTable() tooltip.regularFont = GameTooltipText tooltip.headerFont = GameTooltipHeaderText tooltip.labelProvider = labelProvider tooltip.cell_margin_h = tooltip.cell_margin_h or CELL_MARGIN_H tooltip.cell_margin_v = tooltip.cell_margin_v or CELL_MARGIN_V ---------------------------------------------------------------------- -- Finishing procedures ---------------------------------------------------------------------- tooltip:SetAutoHideDelay(nil) tooltip:Hide() ResetTooltipSize(tooltip) end function tipPrototype:SetDefaultProvider(myProvider) if not myProvider then return end self.labelProvider = myProvider end function tipPrototype:GetDefaultProvider() return self.labelProvider end local function checkJustification(justification, level, silent) if justification ~= "LEFT" and justification ~= "CENTER" and justification ~= "RIGHT" then if silent then return false end error("invalid justification, must one of LEFT, CENTER or RIGHT, not: " .. tostring(justification), level + 1) end return true 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 = self.columns[colNum] or AcquireFrame(self.scrollChild) column:SetFrameLevel(self.scrollChild:GetFrameLevel() + 1) column.justification = justification column.width = 0 column:SetWidth(1) column:SetPoint("TOP", self.scrollChild) column:SetPoint("BOTTOM", self.scrollChild) if colNum > 1 then local h_margin = self.cell_margin_h or CELL_MARGIN_H column:SetPoint("LEFT", self.columns[colNum - 1], "RIGHT", h_margin, 0) SetTooltipSize(self, self.width + h_margin, self.height) else column:SetPoint("LEFT", self.scrollChild) end column:Show() self.columns[colNum] = column return colNum end ------------------------------------------------------------------------------ -- Convenient methods ------------------------------------------------------------------------------ function tipPrototype:Release() lib:Release(self) end function tipPrototype:IsAcquiredBy(key) return key ~= nil and self.key == key end ------------------------------------------------------------------------------ -- Script hooks ------------------------------------------------------------------------------ local RawSetScript = lib.frameMetatable.__index.SetScript function ClearTooltipScripts(tooltip) if tooltip.scripts then for scriptType in pairs(tooltip.scripts) do RawSetScript(tooltip, scriptType, nil) end tooltip.scripts = ReleaseTable(tooltip.scripts) end end function tipPrototype:SetScript(scriptType, handler) RawSetScript(self, scriptType, handler) if handler then if not self.scripts then self.scripts = AcquireTable() end self.scripts[scriptType] = true elseif self.scripts then self.scripts[scriptType] = nil end end -- That might break some addons ; those addons were breaking other -- addons' tooltip though. function tipPrototype:HookScript() geterrorhandler()(":HookScript is not allowed on LibQTip tooltips") end ------------------------------------------------------------------------------ -- Scrollbar data and functions ------------------------------------------------------------------------------ local sliderBackdrop = { ["bgFile"] = [[Interface\Buttons\UI-SliderBar-Background]], ["edgeFile"] = [[Interface\Buttons\UI-SliderBar-Border]], ["tile"] = true, ["edgeSize"] = 8, ["tileSize"] = 8, ["insets"] = { ["left"] = 3, ["right"] = 3, ["top"] = 3, ["bottom"] = 3, }, } local function slider_OnValueChanged(self) self.scrollFrame:SetVerticalScroll(self:GetValue()) end local function tooltip_OnMouseWheel(self, delta) local slider = self.slider local currentValue = slider:GetValue() local minValue, maxValue = slider:GetMinMaxValues() local stepValue = self.step or 10 if delta < 0 and currentValue < maxValue then slider:SetValue(min(maxValue, currentValue + stepValue)) elseif delta > 0 and currentValue > minValue then slider:SetValue(max(minValue, currentValue - stepValue)) end end -- Set the step size for the scroll bar function tipPrototype:SetScrollStep(step) self.step = step end -- will resize the tooltip to fit the screen and show a scrollbar if needed function tipPrototype:UpdateScrolling(maxheight) self:SetClampedToScreen(false) -- all data is in the tooltip; fix colspan width and prevent the layout cleaner from messing up the tooltip later FixCellSizes(self) layoutCleaner.registry[self] = nil local scale = self:GetScale() local topside = self:GetTop() local bottomside = self:GetBottom() local screensize = UIParent:GetHeight() / scale local tipsize = (topside - bottomside) -- if the tooltip would be too high, limit its height and show the slider if bottomside < 0 or topside > screensize or (maxheight and tipsize > maxheight) then local shrink = (bottomside < 0 and (5 - bottomside) or 0) + (topside > screensize and (topside - screensize + 5) or 0) if maxheight and tipsize - shrink > maxheight then shrink = tipsize - maxheight end self:SetHeight(2 * TOOLTIP_PADDING + self.height - shrink) self:SetWidth(2 * TOOLTIP_PADDING + self.width + 20) self.scrollFrame:SetPoint("RIGHT", self, "RIGHT", -(TOOLTIP_PADDING + 20), 0) if not self.slider then local slider = CreateFrame("Slider", nil, self) self.slider = slider slider:SetOrientation("VERTICAL") slider:SetPoint("TOPRIGHT", self, "TOPRIGHT", -TOOLTIP_PADDING, -TOOLTIP_PADDING) slider:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", -TOOLTIP_PADDING, TOOLTIP_PADDING) slider:SetBackdrop(sliderBackdrop) slider:SetThumbTexture([[Interface\Buttons\UI-SliderBar-Button-Vertical]]) slider:SetMinMaxValues(0, 1) slider:SetValueStep(1) slider:SetWidth(12) slider.scrollFrame = self.scrollFrame slider:SetScript("OnValueChanged", slider_OnValueChanged) slider:SetValue(0) end self.slider:SetMinMaxValues(0, shrink) self.slider:Show() self:EnableMouseWheel(true) self:SetScript("OnMouseWheel", tooltip_OnMouseWheel) else self:SetHeight(2 * TOOLTIP_PADDING + self.height) self:SetWidth(2 * TOOLTIP_PADDING + self.width) self.scrollFrame:SetPoint("RIGHT", self, "RIGHT", -TOOLTIP_PADDING, 0) if self.slider then self.slider:SetValue(0) self.slider:Hide() self:EnableMouseWheel(false) self:SetScript("OnMouseWheel", nil) end end end ------------------------------------------------------------------------------ -- Tooltip methods for changing its contents. ------------------------------------------------------------------------------ function tipPrototype:Clear() for i, line in ipairs(self.lines) do for j, cell in pairs(line.cells) do if cell then ReleaseCell(cell) end end ReleaseTable(line.cells) line.cells = nil line.is_header = nil ReleaseFrame(line) self.lines[i] = nil end for i, column in ipairs(self.columns) do column.width = 0 column:SetWidth(1) end wipe(self.colspans) self.cell_margin_h = nil self.cell_margin_v = nil ResetTooltipSize(self) end function tipPrototype:SetCellMarginH(size) if #self.lines > 0 then error("Unable to set horizontal margin while the tooltip has lines.", 2) end if not size or type(size) ~= "number" or size < 0 then error("Margin size must be a positive number or zero.", 2) end self.cell_margin_h = size end function tipPrototype:SetCellMarginV(size) if #self.lines > 0 then error("Unable to set vertical margin while the tooltip has lines.", 2) end if not size or type(size) ~= "number" or size < 0 then error("Margin size must be a positive number or zero.", 2) end self.cell_margin_v = size end function SetTooltipSize(tooltip, width, height) tooltip:SetHeight(2 * TOOLTIP_PADDING + height) tooltip.scrollChild:SetHeight(height) tooltip.height = height tooltip:SetWidth(2 * TOOLTIP_PADDING + width) tooltip.scrollChild:SetWidth(width) tooltip.width = width end -- Add 2 pixels to height so dangling letters (g, y, p, j, etc) are not clipped. function ResetTooltipSize(tooltip) local h_margin = tooltip.cell_margin_h or CELL_MARGIN_H SetTooltipSize(tooltip, max(0, (h_margin * (#tooltip.columns - 1)) + (h_margin / 2)), 2) end local function EnlargeColumn(tooltip, column, width) if width > column.width then SetTooltipSize(tooltip, tooltip.width + width - column.width, tooltip.height) column.width = width column:SetWidth(width) end end local function ResizeLine(tooltip, line, height) SetTooltipSize(tooltip, tooltip.width, tooltip.height + height - line.height) line.height = height line:SetHeight(height) end function FixCellSizes(tooltip) local columns = tooltip.columns local colspans = tooltip.colspans local lines = tooltip.lines -- resize columns to make room for the colspans local h_margin = tooltip.cell_margin_h or CELL_MARGIN_H while next(colspans) do local maxNeedCols local maxNeedWidthPerCol = 0 -- calculate the colspan with the highest additional width need per column for colRange, width in pairs(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 - h_margin end width = width - columns[right].width if width <= 0 then colspans[colRange] = nil else width = width / (right - left + 1) if width > maxNeedWidthPerCol then maxNeedCols = colRange maxNeedWidthPerCol = width end end end -- resize all columns for that colspan if maxNeedCols then local left, right = maxNeedCols:match("^(%d+)%-(%d+)$") for col = left, right do EnlargeColumn(tooltip, columns[col], columns[col].width + maxNeedWidthPerCol) end colspans[maxNeedCols] = nil end end --now that the cell width is set, recalculate the rows' height for _, line in ipairs(lines) do if #(line.cells) > 0 then local lineheight = 0 for _, cell in pairs(line.cells) do if cell then lineheight = max(lineheight, cell:getContentHeight()) end end if lineheight > 0 then ResizeLine(tooltip, line, lineheight) end end end end local function _SetCell(tooltip, lineNum, colNum, value, font, justification, colSpan, provider, ...) local line = tooltip.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(cell) end return lineNum, colNum end font = font or (line.is_header and tooltip.headerFont or tooltip.regularFont) -- Check previous cell local cell local prevCell = cells[colNum] if prevCell then -- There is a cell here justification = justification or prevCell._justification colSpan = colSpan or prevCell._colSpan -- Clear the currently marked colspan for i = colNum + 1, colNum + prevCell._colSpan - 1 do cells[i] = nil end if provider == nil or prevCell._provider == provider then -- Reuse existing cell cell = prevCell provider = cell._provider else -- A new cell is required cells[colNum] = ReleaseCell(prevCell) end elseif prevCell == nil then -- Creating a new cell, using meaningful defaults. provider = provider or tooltip.labelProvider justification = justification or tooltip.columns[colNum].justification or "LEFT" colSpan = colSpan or 1 else error("overlapping cells at column " .. colNum, 3) end local tooltipWidth = #tooltip.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 = 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 then ReleaseCell(cell) elseif cell == false then error("overlapping cells at column " .. i, 3) end cells[i] = false end -- Create the cell if not cell then cell = AcquireCell(tooltip, provider) cells[colNum] = cell end -- Anchor the cell cell:SetPoint("LEFT", tooltip.columns[colNum]) cell:SetPoint("RIGHT", tooltip.columns[rightColNum]) cell:SetPoint("TOP", line) cell:SetPoint("BOTTOM", line) -- Store the cell settings directly into the cell -- That's a bit risky but is really cheap compared to other ways to do it cell._font, cell._justification, cell._colSpan, cell._line, cell._column = font, justification, colSpan, lineNum, colNum -- Setup the cell content local width, height = cell:SetupCell(tooltip, value, justification, font, ...) cell:Show() if colSpan > 1 then -- Postpone width changes until the tooltip is shown local colRange = colNum .. "-" .. rightColNum tooltip.colspans[colRange] = max(tooltip.colspans[colRange] or 0, width) layoutCleaner:RegisterForCleanup(tooltip) else -- Enlarge the column and tooltip if need be EnlargeColumn(tooltip, tooltip.columns[colNum], width) end -- Enlarge the line and tooltip if need be if height > line.height then SetTooltipSize(tooltip, tooltip.width, tooltip.height + height - line.height) line.height = height line:SetHeight(height) end if rightColNum < tooltipWidth then return lineNum, rightColNum + 1 else return lineNum, nil end end do local function CreateLine(tooltip, font, ...) if #tooltip.columns == 0 then error("column layout should be defined before adding line", 3) end local lineNum = #tooltip.lines + 1 local line = tooltip.lines[lineNum] or AcquireFrame(tooltip.scrollChild) line:SetFrameLevel(tooltip.scrollChild:GetFrameLevel() + 2) line:SetPoint('LEFT', tooltip.scrollChild) line:SetPoint('RIGHT', tooltip.scrollChild) if lineNum > 1 then local v_margin = tooltip.cell_margin_v or CELL_MARGIN_V line:SetPoint('TOP', tooltip.lines[lineNum - 1], 'BOTTOM', 0, -v_margin) SetTooltipSize(tooltip, tooltip.width, tooltip.height + v_margin) else line:SetPoint('TOP', tooltip.scrollChild) end tooltip.lines[lineNum] = line line.cells = line.cells or AcquireTable() line.height = 0 line:SetHeight(1) line:Show() local colNum = 1 for i = 1, #tooltip.columns do local value = select(i, ...) if value ~= nil then lineNum, colNum = _SetCell(tooltip, lineNum, i, value, font, nil, 1, tooltip.labelProvider) end end return lineNum, colNum end function tipPrototype:AddLine(...) return CreateLine(self, self.regularFont, ...) end function tipPrototype:AddHeader(...) local line, col = CreateLine(self, self.headerFont, ...) self.lines[line].is_header = true return line, col end end -- do-block local GenericBackdrop = { bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", } function tipPrototype:AddSeparator(height, r, g, b, a) local lineNum, colNum = self:AddLine() local line = self.lines[lineNum] local color = _G.NORMAL_FONT_COLOR height = height or 1 SetTooltipSize(self, self.width, self.height + height) line.height = height line:SetHeight(height) line:SetBackdrop(GenericBackdrop) line:SetBackdropColor(r or color.r, g or color.g, b or color.b, a or 1) return lineNum, colNum end function tipPrototype:SetCellColor(lineNum, colNum, r, g, b, a) local cell = self.lines[lineNum].cells[colNum] if cell then local sr, sg, sb, sa = self:GetBackdropColor() cell:SetBackdrop(GenericBackdrop) cell:SetBackdropColor(r or sr, g or sg, b or sb, a or sa) end end function tipPrototype:SetColumnColor(colNum, r, g, b, a) local column = self.columns[colNum] if column then local sr, sg, sb, sa = self:GetBackdropColor() column:SetBackdrop(GenericBackdrop) column:SetBackdropColor(r or sr, g or sg, b or sb, a or sa) end end function tipPrototype:SetLineColor(lineNum, r, g, b, a) local line = self.lines[lineNum] if line then local sr, sg, sb, sa = self:GetBackdropColor() line:SetBackdrop(GenericBackdrop) line:SetBackdropColor(r or sr, g or sg, b or sb, a or sa) end end function tipPrototype:SetCellTextColor(lineNum, colNum, r, g, b, a) local line = self.lines[lineNum] local column = self.columns[colNum] if not line or not column then return end local cell = self.lines[lineNum].cells[colNum] if cell then if not cell.fontString then error("cell's label provider did not assign a fontString field", 2) end if not cell.r then cell.r, cell.g, cell.b = cell.fontString:GetTextColor() end local font = (line.is_header and self.headerFont or self.regularFont) local sr, sg, sb, sa = font:GetTextColor() cell.fontString:SetTextColor(r or sr, g or sg, b or sb, a or sa) end end function tipPrototype:SetColumnTextColor(colNum, r, g, b, a) if not self.columns[colNum] then return end for line_index = 1, #self.lines do self:SetCellTextColor(line_index, colNum, r, g, b, a) end end function tipPrototype:SetLineTextColor(lineNum, r, g, b, a) local line = self.lines[lineNum] if not line then return end for cell_index = 1, #line.cells do self:SetCellTextColor(lineNum, line.cells[cell_index]._column, r, g, b, a) end end do local function checkFont(font, level, silent) local bad = false if not font then bad = true elseif type(font) == "string" then local ref = _G[font] if not ref or type(ref) ~= 'table' or type(ref.IsObjectType) ~= 'function' or not ref:IsObjectType("Font") then bad = true end elseif type(font) ~= 'table' or type(font.IsObjectType) ~= 'function' or not font:IsObjectType("Font") then bad = true end if bad then if silent then return false end error("font must be a Font instance or a string matching the name of a global Font instance, not: " .. tostring(font), level + 1) end return true end function tipPrototype:SetFont(font) local is_string = type(font) == "string" checkFont(font, 2) self.regularFont = is_string and _G[font] or font end function tipPrototype:SetHeaderFont(font) local is_string = type(font) == "string" checkFont(font, 2) self.headerFont = is_string and _G[font] or font end -- TODO: fixed argument positions / remove checks for performance? 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 return _SetCell(self, lineNum, colNum, value, font, justification, colSpan, provider, select(i, ...)) end end -- do-block function tipPrototype:GetFont() return self.regularFont end function tipPrototype:GetHeaderFont() return self.headerFont end function tipPrototype:GetLineCount() return #self.lines end function tipPrototype:GetColumnCount() return #self.columns end ------------------------------------------------------------------------------ -- Frame Scripts ------------------------------------------------------------------------------ local highlight = CreateFrame("Frame", nil, UIParent) highlight:SetFrameStrata("TOOLTIP") highlight:Hide() highlight._texture = highlight:CreateTexture(nil, "OVERLAY") highlight._texture:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight") highlight._texture:SetBlendMode("ADD") highlight._texture:SetAllPoints(highlight) local scripts = { OnEnter = function(frame, ...) highlight:SetParent(frame) highlight:SetAllPoints(frame) highlight:Show() if frame._OnEnter_func then frame:_OnEnter_func(frame._OnEnter_arg, ...) end end, OnLeave = function(frame, ...) highlight:Hide() highlight:ClearAllPoints() highlight:SetParent(nil) if frame._OnLeave_func then frame:_OnLeave_func(frame._OnLeave_arg, ...) end end, OnMouseDown = function(frame, ...) frame:_OnMouseDown_func(frame._OnMouseDown_arg, ...) end, OnMouseUp = function(frame, ...) frame:_OnMouseUp_func(frame._OnMouseUp_arg, ...) end, OnReceiveDrag = function(frame, ...) frame:_OnReceiveDrag_func(frame._OnReceiveDrag_arg, ...) end, } function SetFrameScript(frame, script, func, arg) if not scripts[script] then return end frame["_" .. script .. "_func"] = func frame["_" .. script .. "_arg"] = arg if script == "OnMouseDown" or script == "OnMouseUp" or script == "OnReceiveDrag" then if func then frame:SetScript(script, scripts[script]) else frame:SetScript(script, nil) end end -- if at least one script is set, set the OnEnter/OnLeave scripts for the highlight if frame._OnEnter_func or frame._OnLeave_func or frame._OnMouseDown_func or frame._OnMouseUp_func or frame._OnReceiveDrag_func then frame:EnableMouse(true) frame:SetScript("OnEnter", scripts.OnEnter) frame:SetScript("OnLeave", scripts.OnLeave) else frame:EnableMouse(false) frame:SetScript("OnEnter", nil) frame:SetScript("OnLeave", nil) end end function ClearFrameScripts(frame) if frame._OnEnter_func or frame._OnLeave_func or frame._OnMouseDown_func or frame._OnMouseUp_func or frame._OnReceiveDrag_func then frame:EnableMouse(false) frame:SetScript("OnEnter", nil) frame._OnEnter_func = nil frame._OnEnter_arg = nil frame:SetScript("OnLeave", nil) frame._OnLeave_func = nil frame._OnLeave_arg = nil frame:SetScript("OnReceiveDrag", nil) frame._OnReceiveDrag_func = nil frame._OnReceiveDrag_arg = nil frame:SetScript("OnMouseDown", nil) frame._OnMouseDown_func = nil frame._OnMouseDown_arg = nil frame:SetScript("OnMouseUp", nil) frame._OnMouseUp_func = nil frame._OnMouseUp_arg = nil end end function tipPrototype:SetLineScript(lineNum, script, func, arg) SetFrameScript(self.lines[lineNum], script, func, arg) end function tipPrototype:SetColumnScript(colNum, script, func, arg) SetFrameScript(self.columns[colNum], script, func, arg) end function tipPrototype:SetCellScript(lineNum, colNum, script, func, arg) local cell = self.lines[lineNum].cells[colNum] if cell then SetFrameScript(cell, script, func, arg) end end ------------------------------------------------------------------------------ -- Auto-hiding feature ------------------------------------------------------------------------------ -- Script of the auto-hiding child frame local function AutoHideTimerFrame_OnUpdate(self, elapsed) self.checkElapsed = self.checkElapsed + elapsed if self.checkElapsed > 0.1 then if self.parent:IsMouseOver() or (self.alternateFrame and self.alternateFrame:IsMouseOver()) then self.elapsed = 0 else self.elapsed = self.elapsed + self.checkElapsed if self.elapsed >= self.delay then lib:Release(self.parent) end end self.checkElapsed = 0 end end -- Usage: -- :SetAutoHideDelay(0.25) => hides after 0.25sec outside of the tooltip -- :SetAutoHideDelay(0.25, someFrame) => hides after 0.25sec outside of both the tooltip and someFrame -- :SetAutoHideDelay() => disable auto-hiding (default) function tipPrototype:SetAutoHideDelay(delay, alternateFrame) local timerFrame = self.autoHideTimerFrame delay = tonumber(delay) or 0 if delay > 0 then if not timerFrame then timerFrame = AcquireFrame(self) timerFrame:SetScript("OnUpdate", AutoHideTimerFrame_OnUpdate) self.autoHideTimerFrame = timerFrame end timerFrame.parent = self timerFrame.checkElapsed = 0 timerFrame.elapsed = 0 timerFrame.delay = delay timerFrame.alternateFrame = alternateFrame timerFrame:Show() elseif timerFrame then self.autoHideTimerFrame = nil timerFrame.alternateFrame = nil timerFrame:SetScript("OnUpdate", nil) ReleaseFrame(timerFrame) end end ------------------------------------------------------------------------------ -- "Smart" Anchoring ------------------------------------------------------------------------------ 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 ------------------------------------------------------------------------------ -- Debug slashcmds ------------------------------------------------------------------------------ -- @debug @ local print = print local function PrintStats() local tipCache = tostring(#tooltipHeap) local frameCache = tostring(#frameHeap) local tableCache = tostring(#tableHeap) local header = false print("Tooltips used: " .. usedTooltips .. ", Cached: " .. tipCache .. ", Total: " .. tipCache + usedTooltips) print("Frames used: " .. usedFrames .. ", Cached: " .. frameCache .. ", Total: " .. frameCache + usedFrames) print("Tables used: " .. usedTables .. ", Cached: " .. tableCache .. ", Total: " .. tableCache + usedTables) for k, v in pairs(activeTooltips) do if not header then print("Active tooltips:") header = true end print("- " .. k) end end SLASH_LibQTip1 = "/qtip" SlashCmdList["LibQTip"] = PrintStats --@end-debug@