WoWInterface SVN NeedyGreedy

[/] [trunk/] [libs/] [LibQTip-1.0/] [LibQTip-1.0.lua] - Rev 75

Go to most recent revision | Compare with Previous | Blame | View Log

local MAJOR = "LibQTip-1.0"
local MINOR = 31 -- 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 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 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, LayoutColspans
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
-- <pre>local tip = LibStub('LibQTip-1.0'):Acquire('MyFooBarTooltip', 5, "LEFT", "CENTER")</pre>
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
                LayoutColspans(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(GameTooltipText)
end

function labelPrototype:SetupCell(tooltip, value, justification, font, l_pad, r_pad, max_width, min_width, ...)
        local fs = self.fontString
        fs:SetFontObject(font 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

        fs:SetPoint("TOPLEFT", self, "TOPLEFT", l_pad, 0)
        fs:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", -r_pad, 0)

        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 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)
        fs:Show()

        -- Use GetHeight() instead of GetStringHeight() so lines which are longer than width will wrap.
        return width, fs:GetHeight()
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)
        tooltip:SetAutoHideDelay(nil)
        tooltip:Hide()
        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)
                tooltip:SetScript("OnMouseWheel", nil)
        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)
        cell:Hide()
        cell:ClearAllPoints()
        cell:SetParent(nil)
        cell:SetBackdrop(nil)
        ClearFrameScripts(cell)
        cell._font, cell._justification, cell._colSpan, cell._line, cell._column = nil

        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
        ----------------------------------------------------------------------
        tooltip:SetBackdrop(GameTooltip:GetBackdrop())
        tooltip:SetBackdropColor(GameTooltip:GetBackdropColor())
        tooltip:SetBackdropBorderColor(GameTooltip:GetBackdropBorderColor())
        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

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

        if delta < 0 and currentValue < maxValue then
                slider:SetValue(min(maxValue, currentValue + 10))
        elseif delta > 0 and currentValue > minValue then
                slider:SetValue(max(minValue, currentValue - 10))
        end
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
        LayoutColspans(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) / scale

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

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
                end
                error("font must be Font instance, not: "..tostring(font), level + 1)
        end
        return true
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

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

function LayoutColspans(tooltip)
        local columns = tooltip.columns

        for colRange, width in pairs(tooltip.colspans) do
                local h_margin = tooltip.cell_margin_h or CELL_MARGIN_H
                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
                EnlargeColumn(tooltip, columns[right], width)
        end
        wipe(tooltip.colspans)
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

        -- Check previous cell
        local cell
        local prevCell = cells[colNum]

        if prevCell then
                -- There is a cell here
                font = font or prevCell._font
                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
                font = font or tooltip.regularFont
                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

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(...)
        return CreateLine(self, self.headerFont, ...)
end

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

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

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

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" 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 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 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("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@]===]

Go to most recent revision | Compare with Previous | Blame