TradeSkillMaster/Core/UI/Elements/Element.lua

576 lines
17 KiB
Lua

-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Base UI Element Class.
-- This the base class for all other UI element classes.
-- @classmod Element
local _, TSM = ...
local Element = TSM.Include("LibTSMClass").DefineClass("Element", nil, "ABSTRACT")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local Analytics = TSM.Include("Util.Analytics")
local Tooltip = TSM.Include("UI.Tooltip")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(Element)
TSM.UI.Element = Element
local private = {}
local ANCHOR_REL_PARENT = newproxy()
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function Element.__init(self, frame)
self._tags = {}
self._frame = frame
self._scripts = {}
self._baseElementCache = nil
self._parent = nil
self._context = nil
self._acquired = nil
self._tooltip = nil
self._width = nil
self._height = nil
self._margin = { left = 0, right = 0, top = 0, bottom = 0 }
self._padding = { left = 0, right = 0, top = 0, bottom = 0 }
self._relativeLevel = nil
self._anchors = {}
end
function Element.__tostring(self)
local parentId = self._parent and self._parent._id
return self.__class.__name..":"..(parentId and (parentId..".") or "")..(self._id or "?")
end
function Element.SetId(self, id)
-- should only be called by core UI code before acquiring the element
assert(not self._acquired)
self._id = id or tostring(self)
end
function Element.SetTags(self, ...)
-- should only be called by core UI code before acquiring the element
assert(not self._acquired)
assert(#self._tags == 0)
for i = 1, select("#", ...) do
local tag = select(i, ...)
tinsert(self._tags, tag)
end
end
function Element.Acquire(self)
assert(not self._acquired)
self._acquired = true
self:Show()
end
function Element.Release(self)
assert(self._acquired)
local frame = self:_GetBaseFrame()
-- clear the OnLeave script before hiding the frame (otherwise it'll get called)
if self._scripts.OnLeave then
frame:SetScript("OnLeave", nil)
self._scripts.OnLeave = nil
end
if self._tooltip and Tooltip.IsVisible(frame) then
-- hide the tooltip
Tooltip.Hide()
end
self:Hide()
frame:ClearAllPoints()
frame:SetParent(nil)
frame:SetScale(1)
-- clear scripts
for script in pairs(self._scripts) do
frame:SetScript(script, nil)
end
wipe(self._tags)
wipe(self._scripts)
self._baseElementCache = nil
self._parent = nil
self._context = nil
self._acquired = nil
self._tooltip = nil
self._width = nil
self._height = nil
self._margin.left = 0
self._margin.right = 0
self._margin.top = 0
self._margin.bottom = 0
self._padding.left = 0
self._padding.right = 0
self._padding.top = 0
self._padding.bottom = 0
self._relativeLevel = nil
wipe(self._anchors)
UIElements.Recycle(self)
end
--- Shows the element.
-- @tparam Element self The element object
function Element.Show(self)
self:_GetBaseFrame():Show()
return self
end
--- Hides the element.
-- @tparam Element self The element object
function Element.Hide(self)
self:_GetBaseFrame():Hide()
return self
end
--- Returns whether or not the element is visible.
-- @tparam Element self The element object
-- @treturn boolean Whether or not the element is currently visible
function Element.IsVisible(self)
return self:_GetBaseFrame():IsVisible()
end
--- Sets the width of the element.
-- @tparam Element self The element object
-- @tparam ?number width The width of the element, or nil to have an undefined width
-- @treturn Element The element object
function Element.SetWidth(self, width)
assert(width == nil or type(width) == "number")
self._width = width
return self
end
--- Sets the height of the element.
-- @tparam Element self The element object
-- @tparam ?number height The height of the element, or nil to have an undefined height
-- @treturn Element The element object
function Element.SetHeight(self, height)
assert(height == nil or type(height) == "number")
self._height = height
return self
end
--- Sets the width and height of the element.
-- @tparam Element self The element object
-- @tparam ?number width The width of the element, or nil to have an undefined width
-- @tparam ?number height The height of the element, or nil to have an undefined height
-- @treturn Element The element object
function Element.SetSize(self, width, height)
self:SetWidth(width)
self:SetHeight(height)
return self
end
--- Sets the padding of the element.
-- @tparam Element self The element object
-- @tparam number left The left padding value if all arguments are passed or the value of all sides if a single argument is passed
-- @tparam[opt] number right The right padding value if all arguments are passed
-- @tparam[opt] number top The top padding value if all arguments are passed
-- @tparam[opt] number bottom The bottom padding value if all arguments are passed
-- @treturn Element The element object
function Element.SetPadding(self, left, right, top, bottom)
if not right and not top and not bottom then
right = left
top = left
bottom = left
end
assert(type(left) == "number" and type(right) == "number" and type(top) == "number" and type(bottom) == "number")
self._padding.left = left
self._padding.right = right
self._padding.top = top
self._padding.bottom = bottom
return self
end
--- Sets the margin of the element.
-- @tparam Element self The element object
-- @tparam number left The left margin value if all arguments are passed or the value of all sides if a single argument is passed
-- @tparam[opt] number right The right margin value if all arguments are passed
-- @tparam[opt] number top The top margin value if all arguments are passed
-- @tparam[opt] number bottom The bottom margin value if all arguments are passed
-- @treturn Element The element object
function Element.SetMargin(self, left, right, top, bottom)
if not right and not top and not bottom then
right = left
top = left
bottom = left
end
assert(type(left) == "number" and type(right) == "number" and type(top) == "number" and type(bottom) == "number")
self._margin.left = left
self._margin.right = right
self._margin.top = top
self._margin.bottom = bottom
return self
end
--- Sets the relative level of this element with regards to its parent.
-- @tparam Element self The element object
-- @tparam number level The relative level of this element
-- @treturn Element The element object
function Element.SetRelativeLevel(self, level)
self._relativeLevel = level
return self
end
--- Wipes the element's anchors.
-- @treturn Element The element object
function Element.WipeAnchors(self)
wipe(self._anchors)
return self
end
--- Adds an anchor to the element.
-- @tparam Element self The element object
-- @param ... The anchor arguments (following WoW's SetPoint() arguments)
-- @treturn Element The element object
function Element.AddAnchor(self, ...)
local numArgs = select("#", ...)
local point, relFrame, relPoint, x, y = nil, nil, nil, nil, nil
if numArgs == 1 then
point = ...
elseif numArgs == 2 then
point, relFrame = ...
elseif numArgs == 3 then
local arg2 = select(2, ...)
if type(arg2) == "number" then
point, x, y = ...
else
point, relFrame, relPoint = ...
end
elseif numArgs == 4 then
point, relFrame, x, y = ...
elseif numArgs == 5 then
point, relFrame, relPoint, x, y = ...
else
error("Invalid anchor")
end
tinsert(self._anchors, point)
tinsert(self._anchors, relFrame or ANCHOR_REL_PARENT)
tinsert(self._anchors, relPoint or point)
tinsert(self._anchors, x or 0)
tinsert(self._anchors, y or 0)
return self
end
--- Gets the top-most element in the tree.
-- @tparam Element self The element object
-- @treturn Element The top-most element object
function Element.GetBaseElement(self)
if not self._baseElementCache then
local element = self
local parent = element:GetParentElement()
while parent do
local temp = element
element = parent
parent = temp:GetParentElement()
end
self._baseElementCache = element
end
return self._baseElementCache
end
--- Gets the parent element's base frame.
-- @tparam Element self The element object
-- @treturn Element The parent element's base frame
function Element.GetParent(self)
return self:GetParentElement():_GetBaseFrame()
end
--- Gets the parent element.
-- @tparam Element self The element object
-- @treturn Element The parent element object
function Element.GetParentElement(self)
return self._parent
end
--- Gets another element in the tree by relative path.
-- The path consists of element ids separated by `.`. `__parent` may also be used to indicate the parent element.
-- @tparam Element self The element object
-- @tparam string path The relative path to the element
-- @treturn Element The desired element
function Element.GetElement(self, path)
-- First try to find the element as a child of self
local result = private.GetElementHelper(self, path)
if not result then
Analytics.Action("GET_ELEMENT_FAIL", tostring(self), path)
end
-- TODO: is this needed?
result = result or private.GetElementHelper(self:GetBaseElement(), path)
return result
end
--- Sets the tooltip of the element.
-- @tparam Element self The element object
-- @param tooltip The value passed to @{Tooltip.Show} when the user hovers over the element, or nil to clear it
-- @treturn Element The element object
function Element.SetTooltip(self, tooltip)
self._tooltip = tooltip
if tooltip then
-- setting OnEnter/OnLeave will implicitly enable the mouse, so make sure it's previously been enabled
assert(self:_GetBaseFrame():IsMouseEnabled())
self:SetScript("OnEnter", private.OnEnter)
self:SetScript("OnLeave", private.OnLeave)
else
self:SetScript("OnEnter", nil)
self:SetScript("OnLeave", nil)
end
return self
end
--- Shows a tooltip on the element.
-- @tparam Element self The element object
-- @param tooltip The value passed to @{Tooltip.Show} when the user hovers over the element
-- @tparam ?boolean noWrapping Disables wrapping of text lines
-- @tparam[opt=0] number xOffset An extra x offset to apply to the anchor of the tooltip
-- @treturn Element The element object
function Element.ShowTooltip(self, tooltip, noWrapping, xOffset)
Tooltip.Show(self:_GetBaseFrame(), tooltip, noWrapping, xOffset)
return self
end
--- Sets the context value of the element.
-- @tparam Element self The element object
-- @param context The context value
-- @treturn Element The element object
function Element.SetContext(self, context)
self._context = context
return self
end
--- Gets the context value from the element.
-- @tparam Element self The element object
-- @return The context value
function Element.GetContext(self)
return self._context
end
--- Registers a script handler.
-- @tparam Element self The element object
-- @tparam string script The script to register for
-- @tparam function handler The script handler which will be called with the element object followed by any arguments to
-- the script
-- @treturn Element The element object
function Element.SetScript(self, script, handler)
self._scripts[script] = handler
if handler then
ScriptWrapper.Set(self:_GetBaseFrame(), script, handler, self)
else
ScriptWrapper.Clear(self:_GetBaseFrame(), script)
end
return self
end
--- Sets a script to propagate to the parent element.
-- @tparam Element self The element object
-- @tparam string script The script to propagate
-- @treturn Element The element object
function Element.PropagateScript(self, script)
self._scripts[script] = "__PROPAGATE"
ScriptWrapper.SetPropagate(self:_GetBaseFrame(), script, self)
return self
end
function Element.Draw(self)
assert(self._acquired)
local frame = self:_GetBaseFrame()
local numAnchors = self:_GetNumAnchors()
if numAnchors > 0 then
frame:ClearAllPoints()
for i = 1, numAnchors do
local point, relFrame, relPoint, x, y = self:_GetAnchor(i)
if relFrame == ANCHOR_REL_PARENT then
relFrame = frame:GetParent()
elseif type(relFrame) == "string" then
-- this is a relative element
relFrame = self:GetParentElement():GetElement(relFrame):_GetBaseFrame()
end
frame:SetPoint(point, relFrame, relPoint, x, y)
end
end
local width = self._width
if width then
self:_SetDimension("WIDTH", width)
end
local height = self._height
if height then
self:_SetDimension("HEIGHT", height)
end
local relativeLevel = self._relativeLevel
if relativeLevel then
frame:SetFrameLevel(frame:GetParent():GetFrameLevel() + relativeLevel)
end
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function Element._GetNumAnchors(self)
assert(#self._anchors % 5 == 0)
return #self._anchors / 5
end
function Element._GetAnchor(self, index)
index = (index - 1) * 5 + 1
assert(index < #self._anchors)
return unpack(self._anchors, index, index + 4)
end
function Element._SetParentElement(self, parent)
self._parent = parent
self:_ClearBaseElementCache()
end
function Element._ClearBaseElementCache(self)
self._baseElementCache = nil
end
function Element._GetMinimumDimension(self, dimension)
if dimension == "WIDTH" then
local width = self._width
return width or 0, width == nil
elseif dimension == "HEIGHT" then
local height = self._height
return height or 0, height == nil
else
error("Invalid dimension: " .. tostring(dimension))
end
end
function Element._GetPreferredDimension(self, dimension)
if dimension == "WIDTH" then
return nil
elseif dimension == "HEIGHT" then
return nil
else
error("Invalid dimension: " .. tostring(dimension))
end
end
function Element._GetDimension(self, dimension)
if dimension == "WIDTH" then
return self:_GetBaseFrame():GetWidth()
elseif dimension == "HEIGHT" then
return self:_GetBaseFrame():GetHeight()
else
error("Invalid dimension: " .. tostring(dimension))
end
end
function Element._SetDimension(self, dimension, ...)
if dimension == "WIDTH" then
self:_GetBaseFrame():SetWidth(...)
elseif dimension == "HEIGHT" then
self:_GetBaseFrame():SetHeight(...)
else
error("Invalid dimension: " .. tostring(dimension))
end
end
function Element._GetBaseFrame(self)
return self._frame
end
function Element._GetPadding(self, side)
return self._padding[strlower(side)]
end
function Element._GetPaddingAnchorOffsets(self, anchor)
local xPart, yPart = private.SplitAnchor(anchor)
local x = xPart and ((xPart == "LEFT" and 1 or -1) * self:_GetPadding(xPart)) or 0
local y = yPart and ((yPart == "BOTTOM" and 1 or -1) * self:_GetPadding(yPart)) or 0
return x, y
end
function Element._GetMargin(self, side)
return self._margin[strlower(side)]
end
function Element._GetMarginAnchorOffsets(self, anchor)
local xPart, yPart = private.SplitAnchor(anchor)
local x = xPart and ((xPart == "LEFT" and 1 or -1) * self:_GetMargin(xPart)) or 0
local y = yPart and ((yPart == "BOTTOM" and 1 or -1) * self:_GetMargin(yPart)) or 0
return x, y
end
-- ============================================================================
-- Helper Functions
-- ============================================================================
function private.GetElementHelper(element, path)
local numParts = select("#", strsplit(".", path))
local partIndex = 1
while partIndex <= numParts do
local part = select(partIndex, strsplit(".", path))
if part == "__parent" then
local parentElement = element:GetParentElement()
if not parentElement then
error(format("Element (%s) has no parent", tostring(element._id)))
end
element = parentElement
elseif part == "__base" then
local baseElement = element:GetBaseElement()
if not baseElement then
error(format("Element (%s) has no base element", tostring(element._id)))
end
element = baseElement
else
local found = false
for _, child in ipairs(element._children) do
if child._id == part then
element = child
found = true
break
end
end
if not found then
element = nil
break
end
end
partIndex = partIndex + 1
end
return element
end
function private.SplitAnchor(anchor)
if anchor == "BOTTOMLEFT" then
return "LEFT", "BOTTOM"
elseif anchor == "BOTTOM" then
return nil, "BOTTOM"
elseif anchor == "BOTTOMRIGHT" then
return "RIGHT", "BOTTOM"
elseif anchor == "RIGHT" then
return "RIGHT", nil
elseif anchor == "TOPRIGHT" then
return "RIGHT", "TOP"
elseif anchor == "TOP" then
return nil, "TOP"
elseif anchor == "TOPLEFT" then
return "LEFT", "TOP"
elseif anchor == "LEFT" then
return "LEFT", nil
else
error("Invalid anchor: "..tostring(anchor))
end
end
function private.OnEnter(element)
element:ShowTooltip(element._tooltip)
end
function private.OnLeave(element)
Tooltip.Hide()
end