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