initial commit
This commit is contained in:
575
Core/UI/Elements/Element.lua
Normal file
575
Core/UI/Elements/Element.lua
Normal file
@@ -0,0 +1,575 @@
|
||||
-- ------------------------------------------------------------------------------ --
|
||||
-- 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
|
||||
Reference in New Issue
Block a user