initial commit

This commit is contained in:
Gitea
2020-11-13 14:13:12 -05:00
commit 05df49ff60
368 changed files with 128754 additions and 0 deletions

View File

@@ -0,0 +1,436 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- ActionButton UI Element Class.
-- An action button is a button which uses specific background textures and has a pressed state. It is a subclass of the
-- @{Text} class.
-- @classmod ActionButton
local _, TSM = ...
local ActionButton = TSM.Include("LibTSMClass").DefineClass("ActionButton", TSM.UI.Text)
local NineSlice = TSM.Include("Util.NineSlice")
local Vararg = TSM.Include("Util.Vararg")
local Event = TSM.Include("Util.Event")
local Theme = TSM.Include("Util.Theme")
local Color = TSM.Include("Util.Color")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(ActionButton)
TSM.UI.ActionButton = ActionButton
local private = {}
local ICON_PADDING = 2
local CLICK_COOLDOWN = 0.2
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function ActionButton.__init(self, name, isSecure)
local frame = UIElements.CreateFrame(self, "Button", name, nil, isSecure and "SecureActionButtonTemplate" or nil)
ScriptWrapper.Set(frame, isSecure and "PostClick" or "OnClick", private.OnClick, self)
ScriptWrapper.Set(frame, "OnMouseDown", private.OnMouseDown, self)
ScriptWrapper.Set(frame, "OnEnter", private.OnEnter, self)
ScriptWrapper.Set(frame, "OnLeave", private.OnLeave, self)
self.__super:__init(frame)
self._nineSlice = NineSlice.New(frame)
-- create the icon
frame.icon = frame:CreateTexture(nil, "ARTWORK")
frame.icon:SetPoint("RIGHT", frame.text, "LEFT", -ICON_PADDING, 0)
Event.Register("MODIFIER_STATE_CHANGED", function()
if self:IsVisible() and next(self._modifierText) then
self:Draw()
if GameTooltip:IsOwned(self:_GetBaseFrame()) then
self:ShowTooltip(self._tooltip)
end
end
end)
self._iconTexturePack = nil
self._pressed = nil
self._disabled = false
self._locked = false
self._lockedColor = nil
self._justifyH = "CENTER"
self._font = "BODY_BODY2_MEDIUM"
self._defaultText = ""
self._modifierText = {}
self._onClickHandler = nil
self._onEnterHandler = nil
self._onLeaveHandler = nil
self._clickCooldown = nil
self._clickCooldownDisabled = false
self._defaultNoBackground = false
self._isMouseDown = false
self._manualRequired = false
end
function ActionButton.Release(self)
self._iconTexturePack = nil
self._pressed = nil
self._disabled = false
self._locked = false
self._lockedColor = nil
self._defaultText = ""
wipe(self._modifierText)
self._onClickHandler = nil
self._onEnterHandler = nil
self._onLeaveHandler = nil
self._clickCooldown = nil
self._clickCooldownDisabled = false
self._defaultNoBackground = false
self._manualRequired = false
self._isMouseDown = false
local frame = self:_GetBaseFrame()
ScriptWrapper.Clear(frame, "OnUpdate")
frame:Enable()
frame:RegisterForClicks("LeftButtonUp")
frame:UnlockHighlight()
self.__super:Release()
self._justifyH = "CENTER"
self._font = "BODY_BODY2_MEDIUM"
end
--- Sets the icon that shows within the button.
-- @tparam ActionButton self The action button object
-- @tparam[opt=nil] string texturePack A texture pack string to set the icon and its size to
-- @treturn ActionButton The action button object
function ActionButton.SetIcon(self, texturePack)
if texturePack then
assert(TSM.UI.TexturePacks.IsValid(texturePack))
self._iconTexturePack = texturePack
else
self._iconTexturePack = nil
end
return self
end
--- Set the text.
-- @tparam ActionButton self The action button object
-- @tparam ?string|number text The text
-- @treturn ActionButton The action button object
function ActionButton.SetText(self, text)
self.__super:SetText(text)
self._defaultText = self:GetText()
return self
end
--- Sets a script handler.
-- @see Element.SetScript
-- @tparam ActionButton self The action button object
-- @tparam string script The script to register for (currently only supports `OnClick`)
-- @tparam function handler The script handler which will be called with the action button object followed by any
-- arguments to the script
-- @treturn ActionButton The action button object
function ActionButton.SetScript(self, script, handler)
if script == "OnClick" then
self._onClickHandler = handler
elseif script == "OnEnter" then
self._onEnterHandler = handler
elseif script == "OnLeave" then
self._onLeaveHandler = handler
elseif script == "OnMouseDown" or script == "OnMouseUp" then
self.__super:SetScript(script, handler)
else
error("Unknown ActionButton script: "..tostring(script))
end
return self
end
--- Sets a script to propagate to the parent element.
-- @tparam ActionButton self The action button object
-- @tparam string script The script to propagate
-- @treturn ActionButton The action button object
function ActionButton.PropagateScript(self, script)
if script == "OnMouseDown" or script == "OnMouseUp" then
self.__super:PropagateScript(script)
else
error("Cannot propagate ActionButton script: "..tostring(script))
end
return self
end
--- Set whether or not the action button is disabled.
-- @tparam ActionButton self The action button object
-- @tparam boolean disabled Whether or not the action button should be disabled
-- @treturn ActionButton The action button object
function ActionButton.SetDisabled(self, disabled)
self._disabled = disabled
self:_UpdateDisabled()
return self
end
--- Set whether or not the action button is pressed.
-- @tparam ActionButton self The action button object
-- @tparam boolean locked Whether or not to lock the action button's highlight
-- @tparam[opt=nil] string color The locked highlight color as a theme color key
-- @treturn ActionButton The action button object
function ActionButton.SetHighlightLocked(self, locked, color)
self._locked = locked
self._lockedColor = color
if locked then
self:_GetBaseFrame():LockHighlight()
else
self:_GetBaseFrame():UnlockHighlight()
end
return self
end
--- Set whether or not the action button is pressed.
-- @tparam ActionButton self The action button object
-- @tparam boolean pressed Whether or not the action button should be pressed
-- @treturn ActionButton The action button object
function ActionButton.SetPressed(self, pressed)
self._pressed = pressed and private.GetModifierKey(IsShiftKeyDown(), IsControlKeyDown(), IsAltKeyDown()) or nil
self:_UpdateDisabled()
return self
end
--- Disables the default click cooldown to allow the button to be spammed (i.e. for macro-able buttons).
-- @tparam ActionButton self The action button object
-- @treturn ActionButton The action button object
function ActionButton.DisableClickCooldown(self)
self._clickCooldownDisabled = true
return self
end
function ActionButton.SetDefaultNoBackground(self)
self._defaultNoBackground = true
return self
end
function ActionButton.SetModifierText(self, text, ...)
local key = private.GetModifierKey(private.ParseModifiers(...))
assert(key and key ~= "NONE")
self._modifierText[key] = text
return self
end
--- Set whether a manual click (vs. a macro) is required.
-- @tparam ActionButton self The action button object
-- @tparam boolean required Whether or not a manual click is required
-- @treturn ActionButton The action button object
function ActionButton.SetRequireManualClick(self, required)
self._manualRequired = required
return self
end
--- Click on the action button.
-- @tparam ActionButton self The action button object
function ActionButton.Click(self)
local frame = self:_GetBaseFrame()
if frame:IsEnabled() and frame:IsVisible() then
private.OnClick(self)
end
end
function ActionButton.Draw(self)
local maxRank, maxRankKey, numMaxRank = nil, nil, nil
local currentModifier = self._pressed or private.GetModifierKey(IsShiftKeyDown(), IsControlKeyDown(), IsAltKeyDown())
local currentShift, currentControl, currentAlt = private.ParseModifiers(strsplit("-", currentModifier))
for key in pairs(self._modifierText) do
local hasShift, hasControl, hasAlt = private.ParseModifiers(strsplit("-", key))
if (not hasShift or currentShift) and (not hasControl or currentControl) and (not hasAlt or currentAlt) then
-- this key matches the current state
local rank = select("#", strsplit("-", key))
if not maxRank or rank > maxRank then
maxRank = rank
numMaxRank = 1
maxRankKey = key
elseif rank == maxRank then
numMaxRank = numMaxRank + 1
end
end
end
if maxRank then
assert(numMaxRank == 1)
self.__super:SetText(self._modifierText[maxRankKey])
else
self.__super:SetText(self._defaultText)
end
self.__super:Draw()
local frame = self:_GetBaseFrame()
-- set nine-slice and text color depending on the state
local textColor, nineSliceTheme, nineSliceColor = nil, nil, nil
if self._pressed or self._clickCooldown then
textColor = Color.GetFullBlack()
nineSliceTheme = "rounded"
nineSliceColor = Theme.GetColor("INDICATOR")
elseif self._disabled then
textColor = Theme.GetColor("ACTIVE_BG_ALT")
nineSliceTheme = "global"
nineSliceColor = Theme.GetColor("ACTIVE_BG")
elseif self._locked then
textColor = Color.GetFullBlack()
nineSliceTheme = "rounded"
nineSliceColor = Theme.GetColor(self._lockedColor or "ACTIVE_BG+HOVER")
elseif frame:IsMouseOver() then
textColor = Theme.GetColor("TEXT")
nineSliceTheme = "rounded"
nineSliceColor = Theme.GetColor("ACTIVE_BG+HOVER")
else
textColor = self:_GetTextColor()
if not self._defaultNoBackground then
nineSliceTheme = "rounded"
nineSliceColor = Theme.GetColor("ACTIVE_BG")
end
end
frame.text:SetTextColor(textColor:GetFractionalRGBA())
if nineSliceTheme then
self._nineSlice:SetStyle(nineSliceTheme)
self._nineSlice:SetVertexColor(nineSliceColor:GetFractionalRGBA())
else
self._nineSlice:Hide()
end
if self._iconTexturePack then
TSM.UI.TexturePacks.SetTextureAndSize(frame.icon, self._iconTexturePack)
frame.icon:Show()
frame.icon:SetVertexColor(textColor:GetFractionalRGBA())
local xOffset = self:GetText() ~= "" and ((TSM.UI.TexturePacks.GetWidth(self._iconTexturePack) + ICON_PADDING) / 2) or (self:_GetDimension("WIDTH") / 2)
frame.text:ClearAllPoints()
frame.text:SetPoint("TOP", xOffset, 0)
frame.text:SetPoint("BOTTOM", xOffset, 0)
frame.text:SetWidth(frame.text:GetStringWidth())
else
frame.icon:Hide()
frame.text:ClearAllPoints()
frame.text:SetPoint("TOPLEFT")
frame.text:SetPoint("BOTTOMRIGHT")
end
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function ActionButton._UpdateDisabled(self)
local frame = self:_GetBaseFrame()
if self._disabled or self._pressed or self._clickCooldown then
frame:Disable()
else
frame:Enable()
end
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.OnClick(self)
if not self._acquired then
return
end
if self._manualRequired and not self._isMouseDown then
return
end
self._isMouseDown = false
if not self._clickCooldownDisabled then
self._clickCooldown = CLICK_COOLDOWN
self:_UpdateDisabled()
ScriptWrapper.Set(self:_GetBaseFrame(), "OnUpdate", private.OnUpdate, self)
end
self:Draw()
if self._onClickHandler then
self:_onClickHandler()
end
end
function private.OnMouseDown(self)
self._isMouseDown = true
end
function private.OnEnter(self)
if self._onEnterHandler then
self:_onEnterHandler()
end
if self._disabled or self._pressed or self._clickCooldown then
return
end
self:Draw()
end
function private.OnLeave(self)
if not self:IsVisible() then
return
end
if self._onLeaveHandler then
self:_onLeaveHandler()
end
if self._disabled or self._pressed or self._clickCooldown then
return
end
self:Draw()
end
function private.OnUpdate(self, elapsed)
self._clickCooldown = self._clickCooldown - elapsed
if self._clickCooldown <= 0 then
ScriptWrapper.Clear(self:_GetBaseFrame(), "OnUpdate")
self._clickCooldown = nil
self:_UpdateDisabled()
self:Draw()
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GetModifierKey(hasShift, hasControl, hasAlt)
if hasShift and hasControl and hasAlt then
return "SHIFT-CTRL-ALT"
elseif hasShift and hasControl then
return "SHIFT-CTRL"
elseif hasShift and hasAlt then
return "SHIFT-ALT"
elseif hasShift then
return "SHIFT"
elseif hasControl and hasAlt then
return "CTRL-ALT"
elseif hasControl then
return "CTRL"
elseif hasAlt then
return "ALT"
else
return "NONE"
end
end
function private.ParseModifiers(...)
local hasShift, hasControl, hasAlt, hasNone = false, false, false, false
for _, modifier in Vararg.Iterator(...) do
if modifier == "SHIFT" then
assert(not hasShift and not hasNone)
hasShift = true
elseif modifier == "CTRL" then
assert(not hasControl and not hasNone)
hasControl = true
elseif modifier == "ALT" then
assert(not hasAlt and not hasNone)
hasAlt = true
elseif modifier == "NONE" then
assert(not hasShift and not hasControl and not hasAlt and not hasNone)
hasNone = true
else
error("Invalid modifier: "..tostring(modifier))
end
end
return hasShift, hasControl, hasAlt
end

View File

@@ -0,0 +1,82 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- AlphaAnimatedFrame UI Element Class.
-- An alpha animated frame is a frame which allows for animating its alpha. It is a subclass of the @{Frame} class.
-- @classmod AlphaAnimatedFrame
local _, TSM = ...
local AlphaAnimatedFrame = TSM.Include("LibTSMClass").DefineClass("AlphaAnimatedFrame", TSM.UI.Frame)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(AlphaAnimatedFrame)
TSM.UI.AlphaAnimatedFrame = AlphaAnimatedFrame
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function AlphaAnimatedFrame.__init(self)
self.__super:__init()
local frame = self:_GetBaseFrame()
self._ag = frame:CreateAnimationGroup()
self._ag:SetLooping("BOUNCE")
self._alpha = self._ag:CreateAnimation("Alpha")
end
function AlphaAnimatedFrame.Acquire(self)
self._alpha:SetFromAlpha(1)
self._alpha:SetToAlpha(1)
self._alpha:SetDuration(1)
self.__super:Acquire()
end
function AlphaAnimatedFrame.Release(self)
self._ag:Stop()
self.__super:Release()
end
--- Sets the range of the alpha animation.
-- @tparam AlphaAnimatedFrame self The alpha animated frame object
-- @tparam number fromAlpha The initial alpha value (usually 1)
-- @tparam number toAlpha The end alpha value (between 0 and 1 inclusive)
-- @treturn AlphaAnimatedFrame The alpha animated frame object
function AlphaAnimatedFrame.SetRange(self, fromAlpha, toAlpha)
self._alpha:SetFromAlpha(fromAlpha)
self._alpha:SetToAlpha(toAlpha)
return self
end
--- Sets the duration of the animation.
-- @tparam AlphaAnimatedFrame self The alpha animated frame object
-- @tparam number duration The duration in seconds
-- @treturn AlphaAnimatedFrame The alpha animated frame object
function AlphaAnimatedFrame.SetDuration(self, duration)
self._alpha:SetDuration(duration)
return self
end
--- Sets whether or not the animation is playing.
-- @tparam AlphaAnimatedFrame self The alpha animated frame object
-- @tparam boolean play Whether the animation should be playing or not
-- @treturn AlphaAnimatedFrame The alpha animated frame object
function AlphaAnimatedFrame.SetPlaying(self, play)
if play then
self._ag:Play()
else
self._ag:Stop()
end
return self
end
--- Gets whether or not the animation is playing.
-- @tparam AlphaAnimatedFrame self The alpha animated frame object
-- @treturn boolean Whether the animation is playing
function AlphaAnimatedFrame.IsPlaying(self)
return self._ag:IsPlaying()
end

View File

@@ -0,0 +1,682 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- ApplicationFrame UI Element Class.
-- An application frame is the base frame of all of the TSM UIs. It is a subclass of the @{Frame} class.
-- @classmod ApplicationFrame
local _, TSM = ...
local L = TSM.Include("Locale").GetTable()
local Math = TSM.Include("Util.Math")
local TempTable = TSM.Include("Util.TempTable")
local Color = TSM.Include("Util.Color")
local NineSlice = TSM.Include("Util.NineSlice")
local Table = TSM.Include("Util.Table")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local Theme = TSM.Include("Util.Theme")
local Tooltip = TSM.Include("UI.Tooltip")
local UIElements = TSM.Include("UI.UIElements")
local ApplicationFrame = TSM.Include("LibTSMClass").DefineClass("ApplicationFrame", TSM.UI.Frame)
UIElements.Register(ApplicationFrame)
TSM.UI.ApplicationFrame = ApplicationFrame
local private = {
menuDialogContext = {},
}
local SECONDS_PER_HOUR = 60 * 60
local SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR
local CONTENT_FRAME_OFFSET = 8
local DIALOG_RELATIVE_LEVEL = 18
local HEADER_HEIGHT = 40
local MIN_SCALE = 0.3
local DIALOG_OPACITY_PCT = 65
local MIN_ON_SCREEN_PX = 50
local function NoOp() end
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function ApplicationFrame.__init(self)
self.__super:__init()
self._contentFrame = nil
self._contextTable = nil
self._defaultContextTable = nil
self._isScaling = nil
self._protected = nil
self._minWidth = 0
self._minHeight = 0
self._dialogStack = {}
local frame = self:_GetBaseFrame()
local globalFrameName = tostring(frame)
_G[globalFrameName] = frame
-- insert our frames before other addons (i.e. Skillet) to avoid conflicts
tinsert(UISpecialFrames, 1, globalFrameName)
self._nineSlice = NineSlice.New(frame)
self._nineSlice:SetStyle("outerFrame")
frame.resizeIcon = frame:CreateTexture(nil, "ARTWORK")
frame.resizeIcon:SetPoint("BOTTOMRIGHT")
TSM.UI.TexturePacks.SetTextureAndSize(frame.resizeIcon, "iconPack.14x14/Resize")
frame.resizeBtn = CreateFrame("Button", nil, frame)
frame.resizeBtn:SetAllPoints(frame.resizeIcon)
frame.resizeBtn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
ScriptWrapper.Set(frame.resizeBtn, "OnEnter", private.ResizeButtonOnEnter, self)
ScriptWrapper.Set(frame.resizeBtn, "OnLeave", private.ResizeButtonOnLeave, self)
ScriptWrapper.Set(frame.resizeBtn, "OnMouseDown", private.ResizeButtonOnMouseDown, self)
ScriptWrapper.Set(frame.resizeBtn, "OnMouseUp", private.ResizeButtonOnMouseUp, self)
ScriptWrapper.Set(frame.resizeBtn, "OnClick", private.ResizeButtonOnClick, self)
Theme.RegisterChangeCallback(function()
if self:IsVisible() then
self:Draw()
end
end)
end
function ApplicationFrame.Acquire(self)
self:AddChildNoLayout(UIElements.New("Frame", "titleFrame")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddAnchor("TOPLEFT", 8, -8)
:AddAnchor("TOPRIGHT", -8, -8)
:SetBackgroundColor("FRAME_BG")
:AddChild(UIElements.New("Texture", "icon")
:SetMargin(0, 16, 0, 0)
:SetTextureAndSize("uiFrames.SmallLogo")
)
:AddChild(UIElements.New("Text", "title")
:AddAnchor("CENTER")
:SetWidth("AUTO")
:SetFont("BODY_BODY2_BOLD")
:SetTextColor("TEXT_ALT")
)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Button", "closeBtn")
:SetBackgroundAndSize("iconPack.24x24/Close/Default")
:SetScript("OnClick", private.CloseButtonOnClick)
)
)
self.__super:Acquire()
local frame = self:_GetBaseFrame()
frame:EnableMouse(true)
frame:SetMovable(true)
frame:SetResizable(true)
frame:RegisterForDrag("LeftButton")
self:SetScript("OnDragStart", private.FrameOnDragStart)
self:SetScript("OnDragStop", private.FrameOnDragStop)
end
function ApplicationFrame.Release(self)
if self._protected then
tinsert(UISpecialFrames, 1, tostring(self:_GetBaseFrame()))
end
self._contentFrame = nil
self._contextTable = nil
self._defaultContextTable = nil
self:_GetBaseFrame():SetMinResize(0, 0)
self:_GetBaseFrame():SetMaxResize(0, 0)
self._isScaling = nil
self._protected = nil
self._minWidth = 0
self._minHeight = 0
self.__super:Release()
end
--- Adds player gold text to the title frame.
-- @tparam ApplicationFrame self The application frame object
-- @treturn ApplicationFrame The application frame object
function ApplicationFrame.AddPlayerGold(self)
local titleFrame = self:GetElement("titleFrame")
titleFrame:AddChildBeforeById(titleFrame:HasChildById("switchBtn") and "switchBtn" or "closeBtn", UIElements.New("PlayerGoldText", "playerGold")
:SetWidth("AUTO")
:SetMargin(0, 8, 0, 0)
)
return self
end
--- Adds the app status icon to the title frame.
-- @tparam ApplicationFrame self The application frame object
-- @treturn ApplicationFrame The application frame object
function ApplicationFrame.AddAppStatusIcon(self)
local color, texture = nil, nil
local appUpdateAge = time() - TSM.GetAppUpdateTime()
local auctionDBRealmTime, auctionDBRegionTime = TSM.AuctionDB.GetAppDataUpdateTimes()
local auctionDBRealmAge = time() - auctionDBRealmTime
local auctionDBRegionAge = time() - auctionDBRegionTime
if appUpdateAge >= 2 * SECONDS_PER_DAY or auctionDBRealmAge > 2 * SECONDS_PER_DAY or auctionDBRegionAge > 2 * SECONDS_PER_DAY then
color = "RED"
texture = "iconPack.14x14/Attention"
elseif appUpdateAge >= 2 * SECONDS_PER_HOUR or auctionDBRealmAge >= 4 * SECONDS_PER_HOUR then
color = "YELLOW"
texture = "iconPack.14x14/Attention"
else
color = "GREEN"
texture = "iconPack.14x14/Checkmark/Circle"
end
local titleFrame = self:GetElement("titleFrame")
titleFrame:AddChildBeforeById("playerGold", UIElements.New("Button", "playerGold")
:SetBackgroundAndSize(TSM.UI.TexturePacks.GetColoredKey(texture, Theme.GetFeedbackColor(color)))
:SetMargin(0, 8, 0, 0)
:SetTooltip(private.GetAppStatusTooltip)
)
return self
end
--- Adds a switch button to the title frame.
-- @tparam ApplicationFrame self The application frame object
-- @tparam function onClickHandler The handler for the OnClick script for the button
-- @treturn ApplicationFrame The application frame object
function ApplicationFrame.AddSwitchButton(self, onClickHandler)
local titleFrame = self:GetElement("titleFrame")
titleFrame:AddChildBeforeById("closeBtn", UIElements.New("ActionButton", "switchBtn")
:SetSize(95, 20)
:SetMargin(0, 8, 0, 0)
:SetFont("BODY_BODY3_MEDIUM")
:SetText(L["WOW UI"])
:SetScript("OnClick", onClickHandler)
)
return self
end
function ApplicationFrame.SetProtected(self, protected)
self._protected = protected
local globalFrameName = tostring(self:_GetBaseFrame())
if protected then
Table.RemoveByValue(UISpecialFrames, globalFrameName)
else
if not Table.KeyByValue(UISpecialFrames, globalFrameName) then
-- insert our frames before other addons (i.e. Skillet) to avoid conflicts
tinsert(UISpecialFrames, 1, globalFrameName)
end
end
return self
end
--- Sets the title text.
-- @tparam ApplicationFrame self The application frame object
-- @tparam string title The title text
-- @treturn ApplicationFrame The application frame object
function ApplicationFrame.SetTitle(self, title)
local titleFrame = self:GetElement("titleFrame")
titleFrame:GetElement("title"):SetText(title)
titleFrame:Draw()
return self
end
--- Sets the content frame.
-- @tparam ApplicationFrame self The application frame object
-- @tparam Frame frame The frame's content frame
-- @treturn ApplicationFrame The application frame object
function ApplicationFrame.SetContentFrame(self, frame)
assert(frame:__isa(TSM.UI.Frame))
frame:WipeAnchors()
frame:AddAnchor("TOPLEFT", CONTENT_FRAME_OFFSET, -HEADER_HEIGHT)
frame:AddAnchor("BOTTOMRIGHT", -CONTENT_FRAME_OFFSET, CONTENT_FRAME_OFFSET)
frame:SetPadding(2)
frame:SetBorderColor("ACTIVE_BG", 2)
self._contentFrame = frame
self:AddChildNoLayout(frame)
return self
end
--- Sets the context table.
-- This table can be used to preserve position and size information across lifecycles of the application frame and even
-- WoW sessions if it's within the settings DB.
-- @tparam ApplicationFrame self The application frame object
-- @tparam table tbl The context table
-- @tparam table defaultTbl Default values (required attributes: `width`, `height`, `centerX`, `centerY`)
-- @treturn ApplicationFrame The application frame object
function ApplicationFrame.SetContextTable(self, tbl, defaultTbl)
assert(defaultTbl.width > 0 and defaultTbl.height > 0)
assert(defaultTbl.centerX and defaultTbl.centerY)
tbl.width = tbl.width or defaultTbl.width
tbl.height = tbl.height or defaultTbl.height
tbl.centerX = tbl.centerX or defaultTbl.centerX
tbl.centerY = tbl.centerY or defaultTbl.centerY
tbl.scale = tbl.scale or defaultTbl.scale
self._contextTable = tbl
self._defaultContextTable = defaultTbl
return self
end
--- Sets the context table from a settings object.
-- @tparam ApplicationFrame self The application frame object
-- @tparam Settings settings The settings object
-- @tparam string key The setting key
-- @treturn ApplicationFrame The application frame object
function ApplicationFrame.SetSettingsContext(self, settings, key)
return self:SetContextTable(settings[key], settings:GetDefaultReadOnly(key))
end
--- Sets the minimum size the application frame can be resized to.
-- @tparam ApplicationFrame self The application frame object
-- @tparam number minWidth The minimum width
-- @tparam number minHeight The minimum height
-- @treturn ApplicationFrame The application frame object
function ApplicationFrame.SetMinResize(self, minWidth, minHeight)
self._minWidth = minWidth
self._minHeight = minHeight
return self
end
--- Shows a dialog frame.
-- @tparam ApplicationFrame self The application frame object
-- @tparam Element frame The element to show in a dialog
-- @param context The context to set on the dialog frame
function ApplicationFrame.ShowDialogFrame(self, frame, context)
local dialogFrame = UIElements.New("Frame", "_dialog_"..random())
:SetRelativeLevel(DIALOG_RELATIVE_LEVEL * (#self._dialogStack + 1))
:SetBackgroundColor(Color.GetFullBlack():GetOpacity(DIALOG_OPACITY_PCT))
:AddAnchor("TOPLEFT")
:AddAnchor("BOTTOMRIGHT")
:SetMouseEnabled(true)
:SetMouseWheelEnabled(true)
:SetContext(context)
:SetScript("OnMouseWheel", NoOp)
:SetScript("OnMouseUp", private.DialogOnMouseUp)
:SetScript("OnHide", private.DialogOnHide)
:AddChildNoLayout(frame)
tinsert(self._dialogStack, dialogFrame)
self._contentFrame:AddChildNoLayout(dialogFrame)
dialogFrame:Show()
dialogFrame:Draw()
end
--- Show a confirmation dialog.
-- @tparam ApplicationFrame self The application frame object
-- @tparam string title The title of the dialog
-- @tparam string subTitle The sub-title of the dialog
-- @tparam function callback The callback for when the dialog is closed
-- @tparam[opt] varag ... Arguments to pass to the callback
function ApplicationFrame.ShowConfirmationDialog(self, title, subTitle, callback, ...)
local context = TempTable.Acquire(...)
context.callback = callback
local frame = UIElements.New("Frame", "frame")
:SetLayout("VERTICAL")
:SetSize(328, 158)
:SetPadding(12, 12, 8, 12)
:AddAnchor("CENTER")
:SetBackgroundColor("FRAME_BG", true)
:SetMouseEnabled(true)
:AddChild(UIElements.New("Frame", "header")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddChild(UIElements.New("Text", "title")
:SetHeight(20)
:SetMargin(32, 8, 0, 0)
:SetFont("BODY_BODY2_BOLD")
:SetJustifyH("CENTER")
:SetText(title)
)
:AddChild(UIElements.New("Button", "closeBtn")
:SetBackgroundAndSize("iconPack.24x24/Close/Default")
:SetScript("OnClick", private.DialogCancelBtnOnClick)
)
)
:AddChild(UIElements.New("Text", "desc")
:SetMargin(0, 0, 16, 16)
:SetFont("BODY_BODY3")
:SetJustifyH("LEFT")
:SetJustifyV("TOP")
:SetText(subTitle)
)
:AddChild(UIElements.New("ActionButton", "confirmBtn")
:SetHeight(24)
:SetText(L["Confirm"])
:SetScript("OnClick", private.DialogConfirmBtnOnClick)
)
self:ShowDialogFrame(frame, context)
end
--- Show a dialog triggered by a "more" button.
-- @tparam ApplicationFrame self The application frame object
-- @tparam Button moreBtn The "more" button
-- @tparam function iter A dialog menu row iterator with the following fields: `index, text, callback`
function ApplicationFrame.ShowMoreButtonDialog(self, moreBtn, iter)
local frame = UIElements.New("PopupFrame", "moreDialog")
:SetLayout("VERTICAL")
:SetWidth(200)
:SetPadding(0, 0, 8, 4)
:AddAnchor("TOPRIGHT", moreBtn:_GetBaseFrame(), "BOTTOM", 22, -16)
local numRows = 0
for i, text, callback in iter do
frame:AddChild(UIElements.New("Button", "row"..i)
:SetHeight(20)
:SetFont("BODY_BODY2_MEDIUM")
:SetText(text)
:SetScript("OnClick", callback)
)
numRows = numRows + 1
end
frame:SetHeight(12 + numRows * 20)
self:ShowDialogFrame(frame)
end
--- Show a menu dialog.
-- @tparam ApplicationFrame self The application frame object
-- @tparam string frame The frame to anchor the dialog to
-- @tparam function iter A menu row iterator with the following fields: `index, text, subIter`
-- @param context Context to pass to the iter / subIter
-- @tparam function clickCallback The function to be called when a menu row is clicked
-- @tparam boolean flip Flip the anchor to the other side
function ApplicationFrame.ShowMenuDialog(self, frame, iter, context, clickCallback, flip)
wipe(private.menuDialogContext)
private.menuDialogContext.context = context
private.menuDialogContext.clickCallback = clickCallback
self:ShowDialogFrame(private.CreateMenuDialogFrame("_menuDialog", iter)
:AddAnchor(flip and "TOPRIGHT" or "TOPLEFT", frame, flip and "BOTTOMRIGHT" or "BOTTOMLEFT", 2, -4)
)
end
--- Hides the current dialog.
-- @tparam ApplicationFrame self The application frame object
function ApplicationFrame.HideDialog(self)
local dialogFrame = tremove(self._dialogStack)
if not dialogFrame then
return
end
dialogFrame:GetParentElement():RemoveChild(dialogFrame)
dialogFrame:Hide()
dialogFrame:Release()
end
function ApplicationFrame.Draw(self)
local frame = self:_GetBaseFrame()
frame:SetToplevel(true)
frame:Raise()
self._nineSlice:SetVertexColor(Theme.GetColor("FRAME_BG"):GetFractionalRGBA())
-- update the size if it's less than the set min size
assert(self._minWidth > 0 and self._minHeight > 0)
self._contextTable.width = max(self._contextTable.width, self._minWidth)
self._contextTable.height = max(self._contextTable.height, self._minHeight)
self._contextTable.scale = max(self._contextTable.scale, MIN_SCALE)
-- set the frame size from the contextTable
self:SetScale(self._contextTable.scale)
self:SetSize(self._contextTable.width, self._contextTable.height)
-- make sure at least 50px of the frame is on the screen and offset by at least 1 scaled pixel to fix some rendering issues
local maxAbsCenterX = (UIParent:GetWidth() / self._contextTable.scale + self._contextTable.width) / 2 - MIN_ON_SCREEN_PX
local maxAbsCenterY = (UIParent:GetHeight() / self._contextTable.scale + self._contextTable.height) / 2 - MIN_ON_SCREEN_PX
local effectiveScale = UIParent:GetEffectiveScale()
if self._contextTable.centerX < 0 then
self._contextTable.centerX = min(max(self._contextTable.centerX, -maxAbsCenterX), -effectiveScale)
else
self._contextTable.centerX = max(min(self._contextTable.centerX, maxAbsCenterX), effectiveScale)
end
if self._contextTable.centerY < 0 then
self._contextTable.centerY = min(max(self._contextTable.centerY, -maxAbsCenterY), -effectiveScale)
else
self._contextTable.centerY = max(min(self._contextTable.centerY, maxAbsCenterY), effectiveScale)
end
-- adjust the position of the frame based on the UI scale to make rendering more consistent
self._contextTable.centerX = Math.Round(self._contextTable.centerX, effectiveScale)
self._contextTable.centerY = Math.Round(self._contextTable.centerY, effectiveScale)
-- set the frame position from the contextTable
self:WipeAnchors()
self:AddAnchor("CENTER", self._contextTable.centerX, self._contextTable.centerY)
self.__super:Draw()
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function ApplicationFrame._SavePositionAndSize(self, wasScaling)
local frame = self:_GetBaseFrame()
local parentFrame = frame:GetParent()
local width = frame:GetWidth()
local height = frame:GetHeight()
if wasScaling then
-- the anchor is in our old frame's scale, so convert the parent measurements to our old scale and then the resuslt to our new scale
local scaleAdjustment = width / self._contextTable.width
local frameLeftOffset = frame:GetLeft() - parentFrame:GetLeft() / self._contextTable.scale
self._contextTable.centerX = (frameLeftOffset - (parentFrame:GetWidth() / self._contextTable.scale - width) / 2) / scaleAdjustment
local frameBottomOffset = frame:GetBottom() - parentFrame:GetBottom() / self._contextTable.scale
self._contextTable.centerY = (frameBottomOffset - (parentFrame:GetHeight() / self._contextTable.scale - height) / 2) / scaleAdjustment
self._contextTable.scale = self._contextTable.scale * scaleAdjustment
else
self._contextTable.width = width
self._contextTable.height = height
-- the anchor is in our frame's scale, so convert the parent measurements to our scale
local frameLeftOffset = frame:GetLeft() - parentFrame:GetLeft() / self._contextTable.scale
self._contextTable.centerX = (frameLeftOffset - (parentFrame:GetWidth() / self._contextTable.scale - width) / 2)
local frameBottomOffset = frame:GetBottom() - parentFrame:GetBottom() / self._contextTable.scale
self._contextTable.centerY = (frameBottomOffset - (parentFrame:GetHeight() / self._contextTable.scale - height) / 2)
end
end
function ApplicationFrame._SetResizing(self, resizing)
if resizing then
self:GetElement("titleFrame"):Hide()
self._contentFrame:_GetBaseFrame():SetAlpha(0)
self._contentFrame:_GetBaseFrame():SetFrameStrata("LOW")
self._contentFrame:Draw()
else
self:GetElement("titleFrame"):Show()
self._contentFrame:_GetBaseFrame():SetAlpha(1)
self._contentFrame:_GetBaseFrame():SetFrameStrata(self._strata)
end
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.CloseButtonOnClick(button)
button:GetElement("__parent.__parent"):Hide()
end
function private.ResizeButtonOnEnter(self)
local tooltip = L["Click and drag to resize this window."].."\n"..
L["Hold SHIFT while dragging to scale the window instead."].."\n"..
L["Right-Click to reset the window size, scale, and position to their defaults."]
Tooltip.Show(self:_GetBaseFrame().resizeBtn, tooltip, true)
end
function private.ResizeButtonOnLeave(self)
Tooltip.Hide()
end
function private.ResizeButtonOnMouseDown(self, mouseButton)
if mouseButton ~= "LeftButton" then
return
end
self._isScaling = IsShiftKeyDown()
local frame = self:_GetBaseFrame()
local width = frame:GetWidth()
local height = frame:GetHeight()
if self._isScaling then
local minWidth = width * MIN_SCALE / self._contextTable.scale
local minHeight = height * MIN_SCALE / self._contextTable.scale
frame:SetMinResize(minWidth, minHeight)
frame:SetMaxResize(width * 10, height * 10)
else
frame:SetMinResize(self._minWidth, self._minHeight)
frame:SetMaxResize(width * 10, height * 10)
end
self:_SetResizing(true)
frame:StartSizing("BOTTOMRIGHT")
-- force updating the size here, to prevent using cached values from previously opened application frames
frame:SetWidth(width)
frame:SetHeight(height)
end
function private.ResizeButtonOnMouseUp(self, mouseButton)
if mouseButton ~= "LeftButton" then
return
end
self:_GetBaseFrame():StopMovingOrSizing()
self:_SetResizing(false)
self:_SavePositionAndSize(self._isScaling)
self._isScaling = nil
self:Draw()
end
function private.ResizeButtonOnClick(self, mouseButton)
if mouseButton ~= "RightButton" then
return
end
self._contextTable.scale = self._defaultContextTable.scale
self._contextTable.width = self._defaultContextTable.width
self._contextTable.height = self._defaultContextTable.height
self._contextTable.centerX = self._defaultContextTable.centerX
self._contextTable.centerY = self._defaultContextTable.centerY
self:Draw()
end
function private.FrameOnDragStart(self)
self:_GetBaseFrame():StartMoving()
end
function private.FrameOnDragStop(self)
self:_GetBaseFrame():StopMovingOrSizing()
self:_SavePositionAndSize()
self:Draw()
end
function private.DialogOnMouseUp(dialog)
local self = dialog:GetParentElement():GetParentElement()
self:HideDialog()
end
function private.DialogOnHide(dialog)
local context = dialog:GetContext()
if context then
TempTable.Release(context)
end
end
function private.DialogCancelBtnOnClick(button)
local self = button:GetBaseElement()
self:HideDialog()
end
function private.DialogConfirmBtnOnClick(button)
local self = button:GetBaseElement()
local dialogFrame = button:GetParentElement():GetParentElement()
local context = dialogFrame:GetContext()
dialogFrame:SetContext(nil)
self:HideDialog()
context.callback(TempTable.UnpackAndRelease(context))
end
function private.CreateMenuDialogFrame(id, iter)
local frame = UIElements.New("Frame", id)
:SetLayout("VERTICAL")
:SetWidth(180)
:SetPadding(2)
:SetBackgroundColor("PRIMARY_BG_ALT")
:SetBorderColor("ACTIVE_BG_ALT")
local numRows = 0
for i, text, subIter in iter, private.menuDialogContext.context do
frame:AddChild(UIElements.New("Frame", "row"..i)
:SetLayout("HORIZONTAL")
:SetHeight(21)
:SetContext(subIter)
:AddChild(UIElements.New("Button", "btn")
:SetHeight(21)
:SetPadding(8, 0, 0, 0)
:SetFont("BODY_BODY3_MEDIUM")
:SetBackground("PRIMARY_BG_ALT")
:SetHighlightEnabled(true)
:SetJustifyH("LEFT")
:SetIcon(subIter and "iconPack.12x12/Chevron/Right" or nil, subIter and "RIGHT" or nil)
:SetText(text)
:SetContext(i)
:SetScript("OnEnter", subIter and private.MenuDialogRowSubIterOnEnter or private.MenuDialogRowDefaultOnEnter)
:SetScript("OnClick", not subIter and private.MenuDialogRowOnClick or nil)
)
)
numRows = numRows + 1
end
frame:SetHeight(4 + numRows * 21)
return frame
end
function private.MenuDialogRowOnClick(button)
local path = TempTable.Acquire()
tinsert(path, button:GetContext())
local parentFrame = button:GetParentElement():GetParentElement():GetParentElement()
local self = parentFrame:GetBaseElement()
while parentFrame:GetParentElement() ~= self._contentFrame do
local selectedButton = parentFrame:GetContext():GetElement("btn")
tinsert(path, 1, selectedButton:GetContext())
parentFrame = parentFrame:GetParentElement()
end
private.menuDialogContext.clickCallback(button, private.menuDialogContext.context, TempTable.UnpackAndRelease(path))
end
function private.MenuDialogRowDefaultOnEnter(button)
local frame = button:GetParentElement():GetParentElement()
if frame:HasChildById("subFrame") then
local subFrame = frame:GetElement("subFrame")
local prevRow = frame:GetContext()
frame:SetContext(nil)
if prevRow then
prevRow:GetElement("btn"):SetHighlightLocked(false)
end
frame:RemoveChild(subFrame)
subFrame:Release()
frame:Draw()
end
end
function private.MenuDialogRowSubIterOnEnter(button)
private.MenuDialogRowDefaultOnEnter(button)
button:SetHighlightLocked(true)
local row = button:GetParentElement()
local frame = row:GetParentElement()
frame:SetContext(row)
local subFrame = private.CreateMenuDialogFrame("subFrame", row:GetContext())
:AddAnchor("TOPLEFT", button:_GetBaseFrame(), "TOPRIGHT", 4, 2)
frame:AddChildNoLayout(subFrame)
subFrame:Draw()
end
function private.GetAppStatusTooltip()
local tooltipLines = TempTable.Acquire()
tinsert(tooltipLines, format(L["TSM Desktop App Status (%s)"], TSM.GetRegion().."-"..GetRealmName()))
local appUpdateAge = time() - TSM.GetAppUpdateTime()
if appUpdateAge < 2 * SECONDS_PER_HOUR then
tinsert(tooltipLines, Theme.GetFeedbackColor("GREEN"):ColorText(format(L["App Synced %s Ago"], SecondsToTime(appUpdateAge))))
elseif appUpdateAge < 2 * SECONDS_PER_DAY then
tinsert(tooltipLines, Theme.GetFeedbackColor("YELLOW"):ColorText(format(L["App Synced %s Ago"], SecondsToTime(appUpdateAge))))
else
tinsert(tooltipLines, Theme.GetFeedbackColor("RED"):ColorText(L["App Not Synced"]))
end
local auctionDBRealmTime, auctionDBRegionTime = TSM.AuctionDB.GetAppDataUpdateTimes()
local auctionDBRealmAge = time() - auctionDBRealmTime
local auctionDBRegionAge = time() - auctionDBRegionTime
if auctionDBRealmAge < 4 * SECONDS_PER_HOUR then
tinsert(tooltipLines, Theme.GetFeedbackColor("GREEN"):ColorText(format(L["AuctionDB Realm Data is %s Old"], SecondsToTime(auctionDBRealmAge))))
elseif auctionDBRealmAge < 2 * SECONDS_PER_DAY then
tinsert(tooltipLines, Theme.GetFeedbackColor("YELLOW"):ColorText(format(L["AuctionDB Realm Data is %s Old"], SecondsToTime(auctionDBRealmAge))))
else
tinsert(tooltipLines, Theme.GetFeedbackColor("RED"):ColorText(L["No AuctionDB Realm Data"]))
end
if auctionDBRegionAge < 2 * SECONDS_PER_DAY then
tinsert(tooltipLines, Theme.GetFeedbackColor("GREEN"):ColorText(format(L["AuctionDB Region Data is %s Old"], SecondsToTime(auctionDBRegionAge))))
else
tinsert(tooltipLines, Theme.GetFeedbackColor("RED"):ColorText(L["No AuctionDB Region Data"]))
end
return strjoin("\n", TempTable.UnpackAndRelease(tooltipLines)), true, 16
end

View File

@@ -0,0 +1,189 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- ApplicationGroupTree UI Element Class.
-- An application group tree displays the group tree in a way which allows the user to select any number of them. This
-- element is used wherever the user needs to select groups to perform some action on. It is a subclass of the
-- @{GroupTree} class.
-- @classmod ApplicationGroupTree
local _, TSM = ...
local TempTable = TSM.Include("Util.TempTable")
local UIElements = TSM.Include("UI.UIElements")
local ApplicationGroupTree = TSM.Include("LibTSMClass").DefineClass("ApplicationGroupTree", TSM.UI.GroupTree)
UIElements.Register(ApplicationGroupTree)
TSM.UI.ApplicationGroupTree = ApplicationGroupTree
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function ApplicationGroupTree.__init(self)
self.__super:__init()
self._selectedGroupsChangedHandler = nil
end
function ApplicationGroupTree.Release(self)
self._selectedGroupsChangedHandler = nil
self.__super:Release()
end
--- Registers a script handler.
-- @tparam ApplicationGroupTree self The application group tree object
-- @tparam string script The script to register for (supported scripts: `OnGroupSelectionChanged`)
-- @tparam function handler The script handler which will be called with the application group tree object followed by
-- any arguments to the script
-- @treturn ApplicationGroupTree The application group tree object
function ApplicationGroupTree.SetScript(self, script, handler)
if script == "OnGroupSelectionChanged" then
self._selectedGroupsChangedHandler = handler
else
error("Unknown ApplicationGroupTree script: "..tostring(script))
end
return self
end
--- Iterates through the selected groups.
-- @tparam ApplicationGroupTree self The application group tree object
-- @return Iterator with the following fields: `index, groupPath`
function ApplicationGroupTree.SelectedGroupsIterator(self)
local groups = TempTable.Acquire()
for _, groupPath in ipairs(self._allData) do
if self:_IsSelected(groupPath) then
tinsert(groups, groupPath)
end
end
return TempTable.Iterator(groups)
end
--- Sets the context table.
-- This table can be used to preserve selection state across lifecycles of the application group tree and even WoW
-- sessions if it's within the settings DB.
-- @see GroupTree.SetContextTable
-- @tparam ApplicationGroupTree self The application group tree object
-- @tparam table tbl The context table
-- @tparam table defaultTbl The default table (required fields: `unselected` OR `selected`, `collapsed`)
-- @treturn ApplicationGroupTree The application group tree object
function ApplicationGroupTree.SetContextTable(self, tbl, defaultTbl)
if defaultTbl.unselected then
assert(type(defaultTbl.unselected) == "table" and not defaultTbl.selected)
tbl.unselected = tbl.unselected or CopyTable(defaultTbl.unselected)
tbl.selected = nil
else
assert(type(defaultTbl.selected) == "table" and not defaultTbl.unselected)
tbl.selected = tbl.selected or CopyTable(defaultTbl.selected)
tbl.unselected = nil
end
self.__super:SetContextTable(tbl, defaultTbl)
return self
end
--- Gets whether or not a group is currently selected.
-- @tparam ApplicationGroupTree self The application group tree object
-- @tparam string groupPath The group to check
-- @treturn boolean Whether or not the group is selected
function ApplicationGroupTree.IsGroupSelected(self, groupPath)
return self:_IsSelected(groupPath)
end
--- Gets whether or not a group is currently selected.
-- @tparam ApplicationGroupTree self The application group tree object
-- @tparam string groupPath The group to set the selected state of
-- @tparam boolean selected Whether or not the group should be selected
-- @treturn ApplicationGroupTree The application group tree object
function ApplicationGroupTree.SetGroupSelected(self, groupPath, selected)
self:_SetSelected(groupPath, selected)
return self
end
--- Gets whether or not the selection is cleared.
-- @tparam ApplicationGroupTree self The application group tree object
-- @tparam[opt=false] boolean updateData Whether or not to update the data first
-- @treturn boolean Whether or not the selection is cleared
function ApplicationGroupTree.IsSelectionCleared(self, updateData)
if updateData then
self:_UpdateData()
end
for _, groupPath in ipairs(self._searchStr == "" and self._allData or self._data) do
if self:_IsSelected(groupPath) then
return false
end
end
return true
end
--- Toggle the selection state of the application group tree.
-- @tparam ApplicationGroupTree self The application group tree object
-- @treturn ApplicationGroupTree The application group tree object
function ApplicationGroupTree.ToggleSelectAll(self)
local isCleared = self:IsSelectionCleared()
for _, groupPath in ipairs(self._searchStr == "" and self._allData or self._data) do
self:_SetSelected(groupPath, isCleared)
end
self:Draw()
if self._selectedGroupsChangedHandler then
self:_selectedGroupsChangedHandler()
end
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function ApplicationGroupTree._UpdateData(self)
self.__super:_UpdateData()
-- remove data which is no longer present from _contextTable
local selectedGroups = TempTable.Acquire()
for _, groupPath in ipairs(self._allData) do
if self:_IsSelected(groupPath) then
selectedGroups[groupPath] = true
end
end
wipe(self._contextTable.selected or self._contextTable.unselected)
for _, groupPath in ipairs(self._allData) do
self:_SetSelected(groupPath, selectedGroups[groupPath])
end
TempTable.Release(selectedGroups)
end
function ApplicationGroupTree._IsSelected(self, data)
if self._contextTable.unselected then
return not self._contextTable.unselected[data]
else
return self._contextTable.selected[data]
end
end
function ApplicationGroupTree._SetSelected(self, data, selected)
if self._contextTable.unselected then
self._contextTable.unselected[data] = not selected or nil
else
self._contextTable.selected[data] = selected or nil
end
end
function ApplicationGroupTree._HandleRowClick(self, data, mouseButton)
if mouseButton == "RightButton" then
self.__super:_HandleRowClick(data, mouseButton)
return
end
self:_SetSelected(data, not self:_IsSelected(data))
-- also set the selection for all child groups to the same as this group
for _, groupPath in ipairs(self._allData) do
if TSM.Groups.Path.IsChild(groupPath, data) and data ~= TSM.CONST.ROOT_GROUP_PATH then
self:_SetSelected(groupPath, self:_IsSelected(data))
end
end
self:Draw()
if self._selectedGroupsChangedHandler then
self:_selectedGroupsChangedHandler()
end
end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,245 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Base Dropdown UI Element Class.
-- The base dropdown class is an abstract class which provides shared functionality between the @{SelectionDropdown} and
-- @{MultiselectionDropdown} classes. It is a subclass of the @{Text} class.
-- @classmod BaseDropdown
local _, TSM = ...
local NineSlice = TSM.Include("Util.NineSlice")
local Color = TSM.Include("Util.Color")
local Theme = TSM.Include("Util.Theme")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local BaseDropdown = TSM.Include("LibTSMClass").DefineClass("BaseDropdown", TSM.UI.Text, "ABSTRACT")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(BaseDropdown)
TSM.UI.BaseDropdown = BaseDropdown
local private = {}
local EXPANDER_SIZE = 18
local TEXT_PADDING = 8
local EXPANDER_PADDING = 8
-- ============================================================================
-- Meta Class Methods
-- ============================================================================
function BaseDropdown.__init(self)
local frame = UIElements.CreateFrame(self, "Button", nil, nil, nil)
self.__super:__init(frame)
self._nineSlice = NineSlice.New(frame)
ScriptWrapper.Set(frame, "OnClick", private.FrameOnClick, self)
frame.arrow = frame:CreateTexture(nil, "ARTWORK")
self._widthText = UIElements.CreateFontString(self, frame)
self._widthText:Hide()
self._font = "BODY_BODY2"
self._hintText = ""
self._items = {}
self._itemKeyLookup = {}
self._disabled = false
self._isOpen = false
self._onSelectionChangedHandler = nil
end
function BaseDropdown.Release(self)
self._hintText = ""
wipe(self._items)
wipe(self._itemKeyLookup)
self._disabled = false
self._isOpen = false
self._onSelectionChangedHandler = nil
self:_GetBaseFrame():Enable()
self.__super:Release()
self._font = "BODY_BODY2"
end
-- ============================================================================
-- Public Class Methods
-- ============================================================================
--- Sets the hint text which is shown when there's no selection.
-- @tparam BaseDropdown self The dropdown object
-- @tparam string text The hint text string
-- @treturn BaseDropdown The dropdown object
function BaseDropdown.SetHintText(self, text)
self._hintText = text
return self
end
--- Add an item to be shown in the dropdown dialog list.
-- @tparam BaseDropdown self The dropdown object
-- @tparam string item The item to add to the list (localized string)
-- @tparam[opt] string|number itemKey The internal representation of the item (if not specified will be the index)
-- @treturn BaseDropdown The dropdown object
function BaseDropdown.AddItem(self, item, itemKey)
tinsert(self._items, item)
self._itemKeyLookup[item] = itemKey or #self._items
return self
end
--- Set the items to show in the dropdown dialog list.
-- @tparam BaseDropdown self The dropdown object
-- @tparam table items A list of items to be shown in the dropdown list
-- @tparam[opt] table itemKeys A list of keys which go with the item at the corresponding index in the items list
-- @treturn BaseDropdown The dropdown object
function BaseDropdown.SetItems(self, items, itemKeys)
wipe(self._items)
wipe(self._itemKeyLookup)
assert(not itemKeys or #itemKeys == #items)
for i, item in ipairs(items) do
self:AddItem(item, itemKeys and itemKeys[i])
end
return self
end
--- Set whether or not the dropdown is disabled.
-- @tparam BaseDropdown self The dropdown object
-- @tparam boolean disabled Whether or not to disable the dropdown
-- @treturn BaseDropdown The dropdown object
function BaseDropdown.SetDisabled(self, disabled)
self._disabled = disabled
if disabled then
self:_GetBaseFrame():Disable()
else
self:_GetBaseFrame():Enable()
end
return self
end
--- Registers a script handler.
-- @tparam BaseDropdown self The dropdown object
-- @tparam string script The script to register for (supported scripts: `OnSelectionChanged`)
-- @tparam function handler The script handler which will be called with the dropdown object followed by any arguments
-- to the script
-- @treturn BaseDropdown The dropdown object
function BaseDropdown.SetScript(self, script, handler)
if script == "OnSelectionChanged" then
self._onSelectionChangedHandler = handler
else
error("Invalid BaseDropdown script: "..tostring(script))
end
return self
end
--- Sets whether or not the dropdown is open.
-- @tparam BaseDropdown self The dropdown object
-- @tparam boolean open Whether or not the dropdown is open
-- @treturn BaseDropdown The dropdown object
function BaseDropdown.SetOpen(self, open)
assert(type(open) == "boolean")
if open == self._isOpen then
return self
end
self._isOpen = open
if open then
local width, height = self:_GetDialogSize()
local dialogFrame = UIElements.New("Frame", "dropdown")
:SetLayout("VERTICAL")
:SetContext(self)
:AddAnchor("TOPLEFT", self:_GetBaseFrame(), "BOTTOMLEFT", 0, -4)
:SetPadding(0, 0, 4, 4)
:SetBackgroundColor("ACTIVE_BG", true)
:SetSize(max(width, self:_GetDimension("WIDTH")), height)
:SetScript("OnHide", private.DialogOnHide)
self:_AddDialogChildren(dialogFrame)
dialogFrame:GetElement("list"):SetScript("OnSelectionChanged", private.ListOnSelectionChanged)
self:GetBaseElement():ShowDialogFrame(dialogFrame)
else
self:GetBaseElement():HideDialog()
end
return self
end
function BaseDropdown.SetText(self)
error("BaseDropdown does not support this method")
end
function BaseDropdown.SetTextColor(self, color)
error("BaseDropdown does not support this method")
end
function BaseDropdown.Draw(self)
self.__super:SetText(self:_GetCurrentSelectionString())
self.__super:Draw()
local frame = self:_GetBaseFrame()
TSM.UI.TexturePacks.SetTexture(frame.arrow, "iconPack.18x18/Chevron/Down")
local frameHeight = frame:GetHeight()
local paddingX = EXPANDER_PADDING
local paddingY = (frameHeight - EXPANDER_SIZE) / 2
frame.text:ClearAllPoints()
frame.text:SetPoint("TOPLEFT", TEXT_PADDING, 0)
frame.text:SetPoint("BOTTOMRIGHT", -EXPANDER_SIZE, 0)
frame.arrow:ClearAllPoints()
frame.arrow:SetPoint("BOTTOMLEFT", frame.text, "BOTTOMRIGHT", -paddingX, paddingY)
frame.arrow:SetPoint("TOPRIGHT", -paddingX, -paddingY)
-- set textures and text color depending on the state
self._nineSlice:SetStyle("rounded")
local textColor = self:_GetTextColor()
frame.text:SetTextColor(textColor:GetFractionalRGBA())
self._nineSlice:SetVertexColor(Theme.GetColor(self._disabled and "PRIMARY_BG_ALT" or "ACTIVE_BG"):GetFractionalRGBA())
frame.arrow:SetVertexColor(textColor:GetFractionalRGBA())
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function BaseDropdown._GetTextColor(self)
local color = Theme.GetColor(self._disabled and "PRIMARY_BG_ALT" or "ACTIVE_BG")
-- the text color should have maximum contrast with the dropdown color, so set it to white/black based on the dropdown color
if color:IsLight() then
-- the dropdown is light, so set the text to black
return Color.GetFullBlack():GetTint(self._disabled and "-DISABLED" or 0)
else
-- the dropdown is dark, so set the text to white
return Color.GetFullWhite():GetTint(self._disabled and "+DISABLED" or 0)
end
end
function BaseDropdown._GetDialogSize(self)
local maxStringWidth = 100 -- no smaller than 100
self._widthText:Show()
self._widthText:SetFont(Theme.GetFont(self._font):GetWowFont())
for _, item in ipairs(self._items) do
self._widthText:SetText(item)
maxStringWidth = max(maxStringWidth, self._widthText:GetUnboundedStringWidth())
end
self._widthText:Hide()
return maxStringWidth + Theme.GetColSpacing() * 2, 8 + max(16, min(8, #self._items) * 20)
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.FrameOnClick(self)
self:SetOpen(true)
end
function private.ListOnSelectionChanged(dropdownList, selection)
local self = dropdownList:GetParentElement():GetContext()
self:_OnListSelectionChanged(dropdownList, selection)
self:Draw()
end
function private.DialogOnHide(frame)
local self = frame:GetContext()
self._isOpen = false
end

View File

@@ -0,0 +1,584 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Base Input UI Element Class.
-- The base input class is an abstract class which provides shared functionality between the @{Input} and
-- @{MultiLineInput} classes. It is a subclass of the @{Element} class.
-- @classmod BaseInput
local _, TSM = ...
local L = TSM.Include("Locale").GetTable()
local NineSlice = TSM.Include("Util.NineSlice")
local Log = TSM.Include("Util.Log")
local Color = TSM.Include("Util.Color")
local Theme = TSM.Include("Util.Theme")
local Delay = TSM.Include("Util.Delay")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local CustomPrice = TSM.Include("Service.CustomPrice")
local BaseInput = TSM.Include("LibTSMClass").DefineClass("BaseInput", TSM.UI.Element, "ABSTRACT")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(BaseInput)
TSM.UI.BaseInput = BaseInput
local private = {}
local BORDER_THICKNESS = 1
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function BaseInput.__init(self, frame)
self.__super:__init(frame)
self._borderNineSlice = NineSlice.New(frame)
self._borderNineSlice:Hide()
self._backgroundNineSlice = NineSlice.New(frame, 1)
self._backgroundNineSlice:Hide()
self._editBox:SetShadowColor(0, 0, 0, 0)
self._editBox:SetAutoFocus(false)
ScriptWrapper.Set(self._editBox, "OnEscapePressed", private.OnEscapePressed, self)
ScriptWrapper.Set(self._editBox, "OnTabPressed", private.OnTabPressed, self)
ScriptWrapper.Set(self._editBox, "OnEditFocusGained", private.OnEditFocusGained, self)
ScriptWrapper.Set(self._editBox, "OnEditFocusLost", private.OnEditFocusLost, self)
ScriptWrapper.Set(self._editBox, "OnChar", self._OnChar, self)
self._lostFocusDelayName = "INPUT_LOST_FOCUS_"..tostring(frame)
self._backgroundColor = "ACTIVE_BG_ALT"
self._borderColor = nil
self._value = ""
self._escValue = nil
self._justifyH = "LEFT"
self._justifyV = "MIDDLE"
self._font = "BODY_BODY2"
self._pasteMode = nil
self._validateFunc = nil
self._validateContext = nil
self._settingTable = nil
self._settingKey = nil
self._disabled = false
self._isValid = true
self._tabPrevPath = nil
self._tabNextPath = nil
self._onValueChangedHandler = nil
self._onEnterPressedHandler = nil
self._onValidationChangedHandler = nil
self._onFocusLostHandler = nil
self._pasteChars = {}
end
function BaseInput.Acquire(self)
self.__super:Acquire()
ScriptWrapper.Set(self._editBox, "OnEnterPressed", private.OnEnterPressed, self)
ScriptWrapper.Set(self._editBox, "OnTextChanged", private.OnTextChanged, self)
end
function BaseInput.Release(self)
Delay.Cancel(self._lostFocusDelayName)
ScriptWrapper.Clear(self._editBox, "OnEnterPressed")
ScriptWrapper.Clear(self._editBox, "OnTextChanged")
self._editBox:SetText("")
self._editBox:ClearFocus()
self._editBox:Enable()
self._editBox:EnableMouse(true)
self._editBox:EnableKeyboard(true)
self._editBox:HighlightText(0, 0)
self._editBox:SetHitRectInsets(0, 0, 0, 0)
self._editBox:SetMaxLetters(2147483647)
self._editBox:SetMaxBytes(2147483647)
self._backgroundColor = "ACTIVE_BG_ALT"
self._borderColor = nil
self._value = ""
self._escValue = nil
self._justifyH = "LEFT"
self._justifyV = "MIDDLE"
self._font = "BODY_BODY2"
self._pasteMode = nil
self._validateFunc = nil
self._validateContext = nil
self._settingTable = nil
self._settingKey = nil
self._disabled = false
self._isValid = true
self._tabPrevPath = nil
self._tabNextPath = nil
self._onValueChangedHandler = nil
self._onEnterPressedHandler = nil
self._onValidationChangedHandler = nil
self._onFocusLostHandler = nil
wipe(self._pasteChars)
self.__super:Release()
end
--- Sets the background of the input.
-- @tparam BaseInput self The input object
-- @tparam ?string|nil color The background color as a theme color key or nil
-- @treturn BaseInput The input object
function BaseInput.SetBackgroundColor(self, color)
assert(color == nil or Theme.GetColor(color))
self._backgroundColor = color
return self
end
--- Sets the border of the input.
-- @tparam BaseInput self The input object
-- @tparam ?string|nil color The border color as a theme color key or nil
-- @treturn BaseInput The input object
function BaseInput.SetBorderColor(self, color)
assert(color == nil or Theme.GetColor(color))
self._borderColor = color
return self
end
--- Sets the horizontal justification of the input.
-- @tparam BaseInput self The input object
-- @tparam string justifyH The horizontal justification (either "LEFT", "CENTER" or "RIGHT")
-- @treturn BaseInput The input object
function BaseInput.SetJustifyH(self, justifyH)
assert(justifyH == "LEFT" or justifyH == "CENTER" or justifyH == "RIGHT")
self._justifyH = justifyH
return self
end
--- Sets the vertical justification of the input.
-- @tparam BaseInput self The input object
-- @tparam string justifyV The vertical justification (either "TOP", "MIDDLE" or "BOTTOM")
-- @treturn BaseInput The input object
function BaseInput.SetJustifyV(self, justifyV)
assert(justifyV == "TOP" or justifyV == "MIDDLE" or justifyV == "BOTTOM")
self._justifyV = justifyV
return self
end
--- Sets the font.
-- @tparam BaseInput self The input object
-- @tparam string font The font key
-- @treturn BaseInput The input object
function BaseInput.SetFont(self, font)
assert(Theme.GetFont(font))
self._font = font
return self
end
--- Sets the path of the inputs to jump to when tab (or shift-tab to go backwards) is pressed.
-- @tparam BaseInput self The input object
-- @tparam string prevPath The path to the previous input (for shift-tab)
-- @tparam string nextPath The path to the next input (for tab)
-- @treturn BaseInput The input object
function BaseInput.SetTabPaths(self, prevPath, nextPath)
self._tabPrevPath = prevPath
self._tabNextPath = nextPath
return self
end
--- Set the highlight to all or some of the input's text.
-- @tparam BaseInput self The input object
-- @tparam number starting The position at which to start the highlight
-- @tparam number ending The position at which to stop the highlight
-- @treturn BaseInput The input object
function BaseInput.HighlightText(self, starting, ending)
if starting and ending then
self._editBox:HighlightText(starting, ending)
else
self._editBox:HighlightText()
end
return self
end
--- Sets the current value.
-- @tparam BaseInput self The input object
-- @tparam string value The value
-- @treturn BaseInput The input object
function BaseInput.SetValue(self, value)
if type(value) == "number" then
value = tostring(value)
end
assert(type(value) == "string")
if self:_SetValueHelper(value, true) then
self._escValue = self._value
else
self._escValue = nil
end
return self
end
--- Sets whether or not the input is disabled.
-- @tparam BaseInput self The input object
-- @tparam boolean disabled Whether or not the input is disabled
-- @treturn BaseInput The input object
function BaseInput.SetDisabled(self, disabled)
self._disabled = disabled
if disabled then
self._editBox:Disable()
else
self._editBox:Enable()
end
return self
end
--- Sets the function to use to validate the input text.
-- @tparam BaseInput self The input object
-- @tparam ?string|function validateFunc A function which returns true if the passed text is valid
-- or false and an error message if not, or one of the following strings for built in validate
-- functions: "CUSTOM_PRICE"
-- @param[opt=nil] context Extra context to pass to the validate function. For the built-in
-- "CUSTOM_PRICE" function, this is optionally a list of bad sources. For the built-in "NUMBER"
-- function, this must be a string such as "0:1000" to specify the min and max values.
-- @treturn BaseInput The input object
function BaseInput.SetValidateFunc(self, validateFunc, context)
if type(validateFunc) == "function" then
self._validateFunc = validateFunc
self._validateContext = context
elseif validateFunc == "CUSTOM_PRICE" then
assert(context == nil or type(context) == "table")
self._validateFunc = private.CustomPriceValidateFunc
self._validateContext = context
elseif validateFunc == "NUMBER" then
local minVal, maxVal, extra = strsplit(":", context)
assert(tonumber(minVal) <= tonumber(maxVal) and not extra)
self._validateFunc = private.NumberValidateFunc
self._validateContext = context
else
error("Invalid validateFunc: "..tostring(validateFunc))
end
return self
end
--- Returns the input's focus state.
-- @tparam BaseInput self The input object
function BaseInput.HasFocus(self)
return self._editBox:HasFocus()
end
--- Sets whether or not this input is focused.
-- @tparam BaseInput self The input object
-- @tparam boolean focused Whether or not this input is focused
-- @treturn BaseInput The input object
function BaseInput.SetFocused(self, focused)
if focused then
self._editBox:SetFocus()
else
self._editBox:ClearFocus()
end
return self
end
--- Clears the highlight.
-- @tparam BaseInput self The input object
-- @treturn BaseInput The input object
function BaseInput.ClearHighlight(self)
self._editBox:HighlightText(0, 0)
return self
end
--- Set the maximum number of letters for the input's entered text.
-- @tparam BaseInput self The input object
-- @tparam number number The number of letters for entered text
-- @treturn BaseInput The input object
function BaseInput.SetMaxLetters(self, number)
self._editBox:SetMaxLetters(number)
return self
end
--- Gets the input value.
-- @tparam BaseInput self The input object
-- @treturn string The input value
function BaseInput.GetValue(self)
return self._ignoreEnter and self._value or strtrim(self._value)
end
--- Registers a script handler.
-- @tparam BaseInput self The input object
-- @tparam string script The script to register for
-- @tparam[opt=nil] function handler The script handler which should be called
-- @treturn BaseInput The element object
function BaseInput.SetScript(self, script, handler)
if script == "OnValueChanged" then
self._onValueChangedHandler = handler
elseif script == "OnEnterPressed" then
self._onEnterPressedHandler = handler
elseif script == "OnValidationChanged" then
self._onValidationChangedHandler = handler
elseif script == "OnFocusLost" then
self._onFocusLostHandler = handler
else
error("Invalid base input script: "..tostring(script))
end
return self
end
--- Sets the setting info.
-- This method is used to have the value of the input automatically correspond with the value of a field in a table.
-- This is useful for inputs which are tied directly to settings.
-- @tparam BaseInput self The input object
-- @tparam table tbl The table which the field to set belongs to
-- @tparam string key The key into the table to be set based on the input state
-- @treturn BaseInput The input object
function BaseInput.SetSettingInfo(self, tbl, key)
assert(self._value == "")
self._settingTable = tbl
self._settingKey = key
self:SetValue(tbl[key])
return self
end
--- Get the current validation state.
-- @tparam BaseInput self The input object
-- @treturn boolean The current valiation state
function BaseInput.IsValid(self)
return self._isValid
end
--- Sets the input into paste mode for supporting the pasting of large strings.
-- @tparam BaseInput self The input object
-- @treturn BaseInput The input object
function BaseInput.SetPasteMode(self)
self._pasteMode = true
ScriptWrapper.Clear(self._editBox, "OnTextChanged")
self._editBox:SetMaxBytes(1)
return self
end
function BaseInput.Draw(self)
self.__super:Draw()
self:_DrawBackgroundAndBorder()
-- set the font
self._editBox:SetFont(Theme.GetFont(self._font):GetWowFont())
-- set the justification
self._editBox:SetJustifyH(self._justifyH)
self._editBox:SetJustifyV(self._justifyV)
-- set the text color
self._editBox:SetTextColor(self:_GetTextColor():GetFractionalRGBA())
-- set the highlight color
self._editBox:SetHighlightColor(Theme.GetColor("TEXT%HIGHLIGHT"):GetFractionalRGBA())
if not self._editBox:HasFocus() then
-- set the text
self._editBox:SetText(self._value)
end
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function BaseInput._GetTextColor(self, tint)
local color = Theme.GetColor(self._disabled and "PRIMARY_BG_ALT" or self._backgroundColor)
-- the text color should have maximum contrast with the input color, so set it to white/black based on the input color
if color:IsLight() then
-- the input is light, so set the text to black
return Color.GetFullBlack():GetTint(self._disabled and "-DISABLED" or tint or 0)
else
-- the input is dark, so set the text to white
return Color.GetFullWhite():GetTint(self._disabled and "+DISABLED" or tint or 0)
end
end
function BaseInput._SetValueHelper(self, value, noCallback)
if not self._validateFunc or self:_validateFunc(strtrim(value), self._validateContext) then
self._value = value
if self._settingTable then
if type(self._settingTable[self._settingKey]) == "number" then
value = tonumber(value)
assert(value)
end
self._settingTable[self._settingKey] = value
end
if not noCallback and self._onValueChangedHandler then
self:_onValueChangedHandler()
end
if not self._isValid then
self._isValid = true
self:_DrawBackgroundAndBorder()
if self._onValidationChangedHandler then
self:_onValidationChangedHandler()
end
end
return true
else
if self._isValid then
self._isValid = false
self:_DrawBackgroundAndBorder()
if self._onValidationChangedHandler then
self:_onValidationChangedHandler()
end
end
return false
end
end
function BaseInput._DrawBackgroundAndBorder(self)
assert(self._backgroundColor)
self._backgroundNineSlice:SetStyle("rounded", (self._borderColor or not self._isValid) and BORDER_THICKNESS or nil)
self._backgroundNineSlice:SetVertexColor(Theme.GetColor(self._disabled and "PRIMARY_BG_ALT" or self._backgroundColor):GetFractionalRGBA())
if self._borderColor or not self._isValid then
self._borderNineSlice:SetStyle("rounded")
self._borderNineSlice:SetVertexColor((not self._isValid and Theme.GetFeedbackColor("RED") or Theme.GetColor(self._borderColor)):GetFractionalRGBA())
else
self._borderNineSlice:Hide()
end
end
function BaseInput._OnChar(self, c)
-- can be overridden
if not self._pasteMode then
return
end
tinsert(self._pasteChars, c)
ScriptWrapper.Set(self._editBox, "OnUpdate", private.OnUpdate, self)
end
function BaseInput._OnTextChanged(self, value)
-- can be overridden
end
function BaseInput._ShouldKeepFocus(self)
-- can be overridden
return false
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.OnEscapePressed(self)
if self._escValue then
self._value = self._escValue
assert(self:_SetValueHelper(self._escValue))
end
self:SetFocused(false)
self:HighlightText(0, 0)
self:Draw()
end
function private.OnTabPressed(self)
local isValid, err = true, nil
if self._validateFunc then
local value = strtrim(self._editBox:GetText())
isValid, err = self:_validateFunc(value, self._validateContext)
end
if not isValid and err then
-- TODO: better way to show the error message?
Log.PrintUser(err)
end
self:SetFocused(false)
self:HighlightText(0, 0)
if self._tabPrevPath and IsShiftKeyDown() then
self:GetElement(self._tabPrevPath):SetFocused(true)
elseif self._tabNextPath and not IsShiftKeyDown() then
self:GetElement(self._tabNextPath):SetFocused(true)
end
end
function private.OnEnterPressed(self)
local isValid, err = true, nil
if self._validateFunc then
local value = strtrim(self._editBox:GetText())
isValid, err = self:_validateFunc(value, self._validateContext)
end
if not isValid and err then
-- TODO: better way to show the error message?
Log.PrintUser(err)
end
if isValid then
self:SetFocused(false)
self:HighlightText(0, 0)
if self._onEnterPressedHandler then
self:_onEnterPressedHandler()
end
end
end
function private.OnEditFocusGained(self)
Delay.Cancel(self._lostFocusDelayName)
self:Draw()
self:HighlightText()
end
function private.OnEditFocusLost(self)
if self:_ShouldKeepFocus() then
self:SetFocused(true)
return
end
if self._isValid then
self._escValue = self._value
end
self:HighlightText(0, 0)
self:Draw()
if not self._isValid then
self._isValid = true
self:_DrawBackgroundAndBorder()
if self._onValidationChangedHandler then
self:_onValidationChangedHandler()
end
end
-- wait until the next frame before calling the handler
Delay.AfterFrame(self._lostFocusDelayName, 0, private.OnFocusLost, nil, self)
end
function private.OnFocusLost(self)
if self:HasFocus() then
return
end
if self._onFocusLostHandler then
self:_onFocusLostHandler()
end
end
function private.OnTextChanged(self, isUserInput)
if not isUserInput then
return
end
local value = self._editBox:GetText()
self:_SetValueHelper(value)
self:_OnTextChanged(value)
end
function private.OnUpdate(self)
ScriptWrapper.Clear(self._editBox, "OnUpdate")
local value = table.concat(self._pasteChars)
wipe(self._pasteChars)
self:_SetValueHelper(value)
self:_OnTextChanged(value)
end
-- ============================================================================
-- Built In Validate Functions
-- ============================================================================
function private.CustomPriceValidateFunc(_, value, badSources)
local isValid, err = CustomPrice.Validate(value, badSources)
if not isValid then
return false, L["Invalid custom price."].." "..err
end
return true
end
function private.NumberValidateFunc(input, value, range)
local minValue, maxValue = strsplit(":", range)
minValue = tonumber(minValue)
maxValue = tonumber(maxValue)
value = tonumber(value)
if not value then
return false, L["Invalid numeric value."]
elseif value < minValue or value > maxValue then
return false, format(L["Value must be between %d and %d."], minValue, maxValue)
end
return true
end

285
Core/UI/Elements/Button.lua Normal file
View File

@@ -0,0 +1,285 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Button UI Element Class.
-- A button is a clickable element which has text drawn over top of it. It is a subclass of the @{Text} class.
-- @classmod Button
local _, TSM = ...
local Button = TSM.Include("LibTSMClass").DefineClass("Button", TSM.UI.Text)
local Theme = TSM.Include("Util.Theme")
local ItemInfo = TSM.Include("Service.ItemInfo")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(Button)
TSM.UI.Button = Button
local ICON_SPACING = 4
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function Button.__init(self)
local frame = UIElements.CreateFrame(self, "Button")
self.__super:__init(frame)
frame.backgroundTexture = frame:CreateTexture(nil, "BACKGROUND")
-- create the highlight
frame.highlight = frame:CreateTexture(nil, "HIGHLIGHT")
frame.highlight:SetAllPoints()
frame.highlight:SetBlendMode("BLEND")
frame:SetHighlightTexture(frame.highlight)
-- create the icon
frame.icon = frame:CreateTexture(nil, "ARTWORK")
self._font = "BODY_BODY1"
self._justifyH = "CENTER"
self._background = nil
self._iconTexturePack = nil
self._iconPosition = nil
self._highlightEnabled = false
end
function Button.Acquire(self)
self:_GetBaseFrame():Enable()
self:_GetBaseFrame():RegisterForClicks("LeftButtonUp")
self:_GetBaseFrame():SetHitRectInsets(0, 0, 0, 0)
self.__super:Acquire()
end
function Button.Release(self)
local frame = self:_GetBaseFrame()
frame:UnlockHighlight()
self._background = nil
self._iconTexturePack = nil
self._iconPosition = nil
self._highlightEnabled = false
self.__super:Release()
self._font = "BODY_BODY1"
self._justifyH = "CENTER"
end
--- Sets the background of the button.
-- @tparam Button self The button object
-- @tparam ?string|number|nil background Either a texture pack string, itemString, WoW file id, theme color key, or nil
-- @treturn Button The button object
function Button.SetBackground(self, background)
assert(background == nil or type(background) == "string" or type(background) == "number")
self._background = background
return self
end
--- Sets the background and size of the button based on a texture pack string.
-- @tparam Button self The button object
-- @tparam string texturePack A texture pack string to set the background to and base the size on
-- @treturn Button The button object
function Button.SetBackgroundAndSize(self, texturePack)
self:SetBackground(texturePack)
self:SetSize(TSM.UI.TexturePacks.GetSize(texturePack))
return self
end
--- Sets whether or not the highlight is enabled.
-- @tparam Button self The button object
-- @tparam boolean enabled Whether or not the highlight is enabled
-- @treturn Button The button object
function Button.SetHighlightEnabled(self, enabled)
self._highlightEnabled = enabled
return self
end
--- Sets the icon that shows within the button.
-- @tparam Button self The button object
-- @tparam[opt=nil] string texturePack A texture pack string to set the icon and its size to
-- @tparam[opt=nil] string position The positin of the icon
-- @treturn Button The button object
function Button.SetIcon(self, texturePack, position)
if texturePack or position then
assert(TSM.UI.TexturePacks.IsValid(texturePack))
assert(position == "LEFT" or position == "LEFT_NO_TEXT" or position == "CENTER" or position == "RIGHT")
self._iconTexturePack = texturePack
self._iconPosition = position
else
self._iconTexturePack = nil
self._iconPosition = nil
end
return self
end
--- Set whether or not the button is disabled.
-- @tparam Button self The button object
-- @tparam boolean disabled Whether or not the button should be disabled
-- @treturn Button The button object
function Button.SetDisabled(self, disabled)
if disabled then
self:_GetBaseFrame():Disable()
else
self:_GetBaseFrame():Enable()
end
return self
end
--- Registers the button for drag events.
-- @tparam Button self The button object
-- @tparam string button The mouse button to register for drag events from
-- @treturn Button The button object
function Button.RegisterForDrag(self, button)
self:_GetBaseFrame():RegisterForDrag(button)
return self
end
--- Click on the button.
-- @tparam Button self The button object
function Button.Click(self)
self:_GetBaseFrame():Click()
end
--- Enable right-click events for the button.
-- @tparam Button self The button object
-- @treturn Button The button object
function Button.EnableRightClick(self)
self:_GetBaseFrame():RegisterForClicks("LeftButtonUp", "RightButtonUp")
return self
end
--- Set the hit rectangle insets for the button.
-- @tparam Button self The button object
-- @tparam number left How much the left side of the hit rectangle is inset
-- @tparam number right How much the right side of the hit rectangle is inset
-- @tparam number top How much the top side of the hit rectangle is inset
-- @tparam number bottom How much the bottom side of the hit rectangle is inset
-- @treturn Button The button object
function Button.SetHitRectInsets(self, left, right, top, bottom)
self:_GetBaseFrame():SetHitRectInsets(left, right, top, bottom)
return self
end
--- Set whether or not to lock the button's highlight.
-- @tparam Button self The action button object
-- @tparam boolean locked Whether or not to lock the action button's highlight
-- @treturn Button The action button object
function Button.SetHighlightLocked(self, locked)
if locked then
self:_GetBaseFrame():LockHighlight()
else
self:_GetBaseFrame():UnlockHighlight()
end
return self
end
function Button.Draw(self)
local frame = self:_GetBaseFrame()
frame.text:Show()
self.__super:Draw()
frame.backgroundTexture:SetTexture(nil)
frame.backgroundTexture:SetTexCoord(0, 1, 0, 1)
frame.backgroundTexture:SetVertexColor(1, 1, 1, 1)
if self._background == nil then
frame.backgroundTexture:Hide()
elseif type(self._background) == "string" and TSM.UI.TexturePacks.IsValid(self._background) then
-- this is a texture pack
frame.backgroundTexture:Show()
frame.backgroundTexture:ClearAllPoints()
frame.backgroundTexture:SetPoint("CENTER")
TSM.UI.TexturePacks.SetTextureAndSize(frame.backgroundTexture, self._background)
elseif type(self._background) == "string" and strmatch(self._background, "^[ip]:%d+") then
-- this is an itemString
frame.backgroundTexture:Show()
frame.backgroundTexture:ClearAllPoints()
frame.backgroundTexture:SetAllPoints()
frame.backgroundTexture:SetTexture(ItemInfo.GetTexture(self._background))
elseif type(self._background) == "string" then
-- this is a theme color key
frame.backgroundTexture:Show()
frame.backgroundTexture:ClearAllPoints()
frame.backgroundTexture:SetAllPoints()
frame.backgroundTexture:SetColorTexture(Theme.GetColor(self._background):GetFractionalRGBA())
elseif type(self._background) == "number" then
-- this is a wow file id
frame.backgroundTexture:Show()
frame.backgroundTexture:ClearAllPoints()
frame.backgroundTexture:SetAllPoints()
frame.backgroundTexture:SetTexture(self._background)
else
error("Invalid background: "..tostring(self._background))
end
-- set the text color
local textColor = frame:IsEnabled() and self:_GetTextColor() or Theme.GetColor("ACTIVE_BG_ALT")
frame.text:SetTextColor(textColor:GetFractionalRGBA())
-- set the highlight texture
if self._highlightEnabled then
frame.highlight:SetColorTexture(Theme.GetColor(self._background):GetTint("+HOVER"):GetFractionalRGBA())
else
frame.highlight:SetColorTexture(0, 0, 0, 0)
end
if self._iconTexturePack then
TSM.UI.TexturePacks.SetTextureAndSize(frame.icon, self._iconTexturePack)
frame.icon:Show()
frame.icon:ClearAllPoints()
frame.icon:SetVertexColor(textColor:GetFractionalRGBA())
local iconWidth = TSM.UI.TexturePacks.GetWidth(self._iconTexturePack) + ICON_SPACING
if self._iconPosition == "LEFT" then
frame.icon:SetPoint("RIGHT", frame.text, "LEFT", -ICON_SPACING, 0)
frame.text:ClearAllPoints()
if self._justifyH == "CENTER" then
local xOffset = iconWidth / 2
frame.text:SetPoint("TOP", xOffset, -self:_GetPadding("TOP"))
frame.text:SetPoint("BOTTOM", xOffset, self:_GetPadding("BOTTOM"))
frame.text:SetWidth(frame.text:GetStringWidth())
elseif self._justifyH == "LEFT" then
frame.text:SetPoint("TOPLEFT", iconWidth + self:_GetPadding("LEFT"), -self:_GetPadding("TOP"))
frame.text:SetPoint("BOTTOMRIGHT", -self:_GetPadding("RIGHT"), self:_GetPadding("BOTTOM"))
else
error("Unsupported justifyH: "..tostring(self._justifyH))
end
elseif self._iconPosition == "LEFT_NO_TEXT" then
frame.icon:SetPoint("LEFT", self:_GetPadding("LEFT"), 0)
frame.text:ClearAllPoints()
frame.text:Hide()
elseif self._iconPosition == "CENTER" then
frame.icon:SetPoint("CENTER")
frame.text:ClearAllPoints()
frame.text:Hide()
elseif self._iconPosition == "RIGHT" then
frame.icon:SetPoint("RIGHT", -self:_GetPadding("RIGHT"), 0)
local xOffset = iconWidth
frame.text:ClearAllPoints()
-- TODO: support non-left-aligned text
frame.text:SetPoint("TOPLEFT", self:_GetPadding("LEFT"), -self:_GetPadding("TOP"))
frame.text:SetPoint("BOTTOMRIGHT", -xOffset, self:_GetPadding("BOTTOM"))
else
error("Invalid iconPosition: "..tostring(self._iconPosition))
end
else
frame.icon:Hide()
frame.text:ClearAllPoints()
frame.text:SetPoint("TOPLEFT", self:_GetPadding("LEFT"), -self:_GetPadding("TOP"))
frame.text:SetPoint("BOTTOMRIGHT", -self:_GetPadding("RIGHT"), self:_GetPadding("BOTTOM"))
end
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function Button._GetMinimumDimension(self, dimension)
if dimension == "WIDTH" and self._autoWidth then
return self:GetStringWidth() + (self._iconTexturePack and TSM.UI.TexturePacks.GetWidth(self._iconTexturePack) or 0)
else
return self.__super:_GetMinimumDimension(dimension)
end
end

View File

@@ -0,0 +1,245 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Checkbox UI Element Class.
-- This is a simple checkbox element with an attached description text. It is a subclass of the @{Text} class.
-- @classmod Checkbox
local _, TSM = ...
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local Theme = TSM.Include("Util.Theme")
local UIElements = TSM.Include("UI.UIElements")
local Checkbox = TSM.Include("LibTSMClass").DefineClass("Checkbox", TSM.UI.Text)
UIElements.Register(Checkbox)
TSM.UI.Checkbox = Checkbox
local private = {}
local THEME_TEXTURES = {
RADIO = {
checked = "iconPack.Misc/Radio/Checked",
unchecked = "iconPack.Misc/Radio/Unchecked",
},
CHECK = {
checked = "iconPack.Misc/Checkbox/Checked",
unchecked = "iconPack.Misc/Checkbox/Unchecked",
},
}
local CHECKBOX_SPACING = 4
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function Checkbox.__init(self)
local frame = UIElements.CreateFrame(self, "Button")
self.__super:__init(frame)
ScriptWrapper.Set(frame, "OnClick", private.FrameOnClick, self)
-- create the text and check texture
frame.text = UIElements.CreateFontString(self, frame)
frame.text:SetJustifyV("MIDDLE")
frame.check = frame:CreateTexture()
self._position = "LEFT"
self._theme = "CHECK"
self._font = "BODY_BODY3"
self._disabled = false
self._value = false
self._onValueChangedHandler = nil
self._settingTable = nil
self._settingKey = nil
end
function Checkbox.Release(self)
self._position = "LEFT"
self._theme = "CHECK"
self._disabled = false
self._value = false
self._onValueChangedHandler = nil
self._settingTable = nil
self._settingKey = nil
self.__super:Release()
self._font = "BODY_BODY3"
end
--- Sets the position of the checkbox relative to the text.
-- This method can be used to set the checkbox to be either on the left or right side of the text.
-- @tparam Checkbox self The checkbox object
-- @tparam string position The position of the checkbox relative to the text
-- @treturn Checkbox The checkbox object
function Checkbox.SetCheckboxPosition(self, position)
if position == "LEFT" or position == "RIGHT" then
self._position = position
else
error("Invalid checkbox position: "..tostring(position))
end
return self
end
--- Sets the checkbox theme
-- @tparam Checkbox self The checkbox object
-- @tparam string theme Either "RADIO" or "CHECK"
-- @treturn Checkbox The checkbox object
function Checkbox.SetTheme(self, theme)
assert(THEME_TEXTURES[theme])
self._theme = theme
return self
end
--- Sets whether or not the checkbox is disabled.
-- @tparam Checkbox self The checkbox object
-- @tparam boolean disabled Whether or not the checkbox is disabled
-- @treturn Checkbox The checkbox object
function Checkbox.SetDisabled(self, disabled)
self._disabled = disabled
return self
end
--- Sets the text string.
-- @tparam Checkbox self The checkbox object
-- @tparam string text The text string to be displayed
-- @treturn Checkbox The checkbox object
function Checkbox.SetText(self, text)
self._textStr = text
return self
end
--- Gets the text string.
-- @tparam Checkbox self The checkbox object
-- @treturn string The text string
function Checkbox.GetText(self)
return self._textStr
end
--- Sets a formatted text string.
-- @tparam Checkbox self The checkbox object
-- @tparam vararg ... The format string and arguments
-- @treturn Checkbox The checkbox object
function Checkbox.SetFormattedText(self, ...)
self._textStr = format(...)
return self
end
--- Sets whether or not the checkbox is checked.
-- @tparam Checkbox self The checkbox object
-- @tparam boolean value Whether or not the checkbox is checked
-- @tparam[opt=false] boolean silent If true, will not trigger the `OnValueChanged` script
-- @treturn Checkbox The checkbox object
function Checkbox.SetChecked(self, value, silent)
self._value = value and true or false
if self._onValueChangedHandler and not silent then
self:_onValueChangedHandler(value)
end
return self
end
--- Sets the setting info.
-- This method is used to have the state of the checkbox automatically correspond with the boolean state of a field in
-- a table. This is useful for checkboxes which are tied directly to settings.
-- @tparam Checkbox self The checkbox object
-- @tparam table tbl The table which the field to set belongs to
-- @tparam string key The key into the table to be set based on the checkbox state
-- @treturn Checkbox The checkbox object
function Checkbox.SetSettingInfo(self, tbl, key)
self._settingTable = tbl
self._settingKey = key
self:SetChecked(tbl[key])
return self
end
--- Gets the checked state.
-- @tparam Checkbox self The checkbox object
-- @treturn boolean Whether or not the checkbox is checked
function Checkbox.IsChecked(self)
return self._value
end
--- Registers a script handler.
-- @tparam Checkbox self The checkbox object
-- @tparam string script The script to register for (supported scripts: `OnValueChanged`)
-- @tparam function handler The script handler which will be called with the checkbox object followed by any arguments
-- to the script
-- @treturn Checkbox The checkbox object
function Checkbox.SetScript(self, script, handler)
if script == "OnValueChanged" then
self._onValueChangedHandler = handler
elseif script == "OnEnter" or script == "OnLeave" then
return self.__super:SetScript(script, handler)
else
error("Unknown Checkbox script: "..tostring(script))
end
return self
end
function Checkbox.Draw(self)
self.__super:Draw()
local frame = self:_GetBaseFrame()
if self._disabled then
frame.text:SetTextColor(Theme.GetColor("TEXT_DISABLED"):GetFractionalRGBA())
else
frame.text:SetTextColor(self:_GetTextColor():GetFractionalRGBA())
end
TSM.UI.TexturePacks.SetTextureAndSize(frame.check, THEME_TEXTURES[self._theme][self._value and "checked" or "unchecked"])
frame.text:ClearAllPoints()
frame.check:ClearAllPoints()
if self._position == "LEFT" then
frame.check:SetPoint("LEFT")
frame.text:SetJustifyH("LEFT")
frame.text:SetPoint("LEFT", frame.check, "RIGHT", CHECKBOX_SPACING, 0)
frame.text:SetPoint("TOPRIGHT")
frame.text:SetPoint("BOTTOMRIGHT")
elseif self._position == "RIGHT" then
frame.check:SetPoint("RIGHT")
frame.text:SetJustifyH("RIGHT")
frame.text:SetPoint("BOTTOMLEFT")
frame.text:SetPoint("TOPLEFT")
frame.text:SetPoint("RIGHT", frame.check, "LEFT", -CHECKBOX_SPACING, 0)
else
error("Invalid position: "..tostring(self._position))
end
if self._disabled then
frame.check:SetAlpha(0.3)
self:_GetBaseFrame():Disable()
else
frame.check:SetAlpha(1)
self:_GetBaseFrame():Enable()
end
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function Checkbox._GetMinimumDimension(self, dimension)
if dimension == "WIDTH" and self._autoWidth then
local checkboxWidth = TSM.UI.TexturePacks.GetWidth(THEME_TEXTURES[self._theme].checked)
return self:GetStringWidth() + CHECKBOX_SPACING + checkboxWidth, nil
else
return self.__super:_GetMinimumDimension(dimension)
end
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.FrameOnClick(self)
local value = not self._value
if self._settingTable and self._settingKey then
self._settingTable[self._settingKey] = value
end
self:SetChecked(value)
self:Draw()
end

View File

@@ -0,0 +1,135 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Collapsible Container UI Element Class.
-- An collapsible container is a container which can be collapsed to a single heading line. It is a subclass of the @{Frame} class.
-- @classmod CollapsibleContainer
local _, TSM = ...
local CollapsibleContainer = TSM.Include("LibTSMClass").DefineClass("CollapsibleContainer", TSM.UI.Frame)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(CollapsibleContainer)
TSM.UI.CollapsibleContainer = CollapsibleContainer
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function CollapsibleContainer.__init(self)
self.__super:__init()
self._headingText = ""
self._contextTbl = nil
self._contextKey = nil
end
function CollapsibleContainer.Acquire(self)
self.__super:Acquire()
self:SetBackgroundColor("PRIMARY_BG_ALT", true)
self.__super:SetLayout("VERTICAL")
self.__super:SetPadding(12, 12, 8, 8)
self.__super:AddChild(UIElements.New("Frame", "heading")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:AddChild(UIElements.New("Button", "expander")
:SetMargin(0, 4, 0, 0)
:SetScript("OnClick", private.OnExpanderClick)
)
:AddChild(UIElements.New("Text", "text")
:SetFont("BODY_BODY1_BOLD")
)
)
self.__super:AddChild(UIElements.New("Frame", "content"))
end
function CollapsibleContainer.Release(self)
self._headingText = ""
self._contextTbl = nil
self._contextKey = nil
self.__super:Release()
end
--- Sets the context table and key where to store the collapsed state.
-- @tparam CollapsibleContainer self The collapsible container object
-- @tparam table tbl The table
-- @tparam string key The key
-- @treturn CollapsibleContainer The collapsible container object
function CollapsibleContainer.SetContextTable(self, tbl, key)
assert(type(tbl) == "table" and type(key) == "string")
self._contextTbl = tbl
self._contextKey = key
if self._contextTbl[self._contextKey] then
self:GetElement("content"):Hide()
else
self:GetElement("content"):Show()
end
return self
end
--- Set the heading text.
-- @tparam CollapsibleContainer self The collapsible container object
-- @tparam ?string|number headingText The heading text
-- @treturn CollapsibleContainer The collapsible container object
function CollapsibleContainer.SetHeadingText(self, headingText)
assert(type(headingText) == "string" or type(headingText) == "number")
self._headingText = headingText
return self
end
function CollapsibleContainer.SetPadding(self, left, right, top, bottom)
error("CollapsibleContainer doesn't support this method")
end
function CollapsibleContainer.SetLayout(self, layout)
self:GetElement("content"):SetLayout(layout)
return self
end
function CollapsibleContainer.AddChild(self, child)
self:GetElement("content"):AddChild(child)
return self
end
function CollapsibleContainer.AddChildIf(self, condition, child)
self:GetElement("content"):AddChildIf(condition, child)
return self
end
function CollapsibleContainer.AddChildrenWithFunction(self, func, ...)
self:GetElement("content"):AddChildrenWithFunction(func, ...)
return self
end
function CollapsibleContainer.AddChildBeforeById(self, beforeId, child)
self:GetElement("content"):AddChildBeforeById(beforeId, child)
return self
end
function CollapsibleContainer.Draw(self)
self:GetElement("heading.text"):SetText(self._headingText)
self:GetElement("heading.expander"):SetBackgroundAndSize(self._contextTbl[self._contextKey] and "iconPack.18x18/Caret/Right" or "iconPack.18x18/Caret/Down")
self.__super:Draw()
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.OnExpanderClick(button)
local self = button:GetParentElement():GetParentElement()
self._contextTbl[self._contextKey] = not self._contextTbl[self._contextKey]
if self._contextTbl[self._contextKey] then
self:GetElement("content"):Hide()
else
self:GetElement("content"):Show()
end
-- TODO: is there a better way to notify the elements up the stack that our size has changed?
self:GetBaseElement():Draw()
end

View File

@@ -0,0 +1,254 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Commodity List UI Element Class.
-- The element used to show the details of a selected commodity in shopping. It is a subclass of the @{ScrollingTable} class.
-- @classmod CommodityList
local _, TSM = ...
local L = TSM.Include("Locale").GetTable()
local Money = TSM.Include("Util.Money")
local Math = TSM.Include("Util.Math")
local Theme = TSM.Include("Util.Theme")
local UIElements = TSM.Include("UI.UIElements")
local CommodityList = TSM.Include("LibTSMClass").DefineClass("CommodityList", TSM.UI.ScrollingTable)
UIElements.Register(CommodityList)
TSM.UI.CommodityList = CommodityList
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function CommodityList.__init(self)
self.__super:__init()
self._row = nil
self._marketValueFunc = nil
self._alertThreshold = math.huge
end
function CommodityList.Acquire(self)
self._headerHidden = true
self.__super:Acquire()
self:GetScrollingTableInfo()
:NewColumn("warning")
:SetWidth(12)
:SetIconSize(12)
:SetIconHoverEnabled(true)
:SetIconFunction(private.GetWarningIcon)
:SetJustifyH("CENTER")
:SetFont("BODY_BODY3")
:Commit()
:NewColumn("itemBuyout")
:SetFont("TABLE_TABLE1")
:SetJustifyH("LEFT")
:SetTextFunction(private.GetItemBuyoutText)
:DisableHiding()
:Commit()
:NewColumn("quantity")
:SetWidth(60)
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetTextFunction(private.GetQuantityText)
:DisableHiding()
:Commit()
:NewColumn("pct")
:SetWidth(50)
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetTextFunction(private.GetPercentText)
:DisableHiding()
:Commit()
:Commit()
end
function CommodityList.Release(self)
self._row = nil
self._marketValueFunc = nil
self._alertThreshold = math.huge
self.__super:Release()
end
function CommodityList.GetTotalQuantity(self, maxIndex)
local totalQuantity = 0
for _, index in ipairs(self._data) do
if index > maxIndex then
break
end
local subRow = self:_GetSubRow(index)
local _, numOwnerItems = subRow:GetOwnerInfo()
local quantityAvailable = subRow:GetQuantities() - numOwnerItems
totalQuantity = totalQuantity + quantityAvailable
end
return totalQuantity
end
--- Sets the result row.
-- @tparam CommodityList self The commodity list object
-- @tparam table row The row to set
-- @treturn CommodityList The commodity list object
function CommodityList.SetData(self, row)
self._row = row
self:UpdateData()
return self
end
--- Sets the selected quantity.
-- @tparam CommodityList self The commodity list object
-- @tparam number quantity The selected quantity
-- @treturn CommodityList The commodity list object
function CommodityList.SelectQuantity(self, quantity)
local maxIndex = nil
for _, index in ipairs(self._data) do
local subRow = self:_GetSubRow(index)
local _, numOwnerItems = subRow:GetOwnerInfo()
local quantityAvailable = subRow:GetQuantities() - numOwnerItems
maxIndex = index
quantity = quantity - quantityAvailable
if quantity <= 0 then
break
end
end
self:SetSelection(maxIndex)
return self
end
--- Sets the market value function.
-- @tparam CommodityList self The commodity list object
-- @tparam function func The function to call with the ResultSubRow to get the market value
-- @treturn CommodityList The commodity list object
function CommodityList.SetMarketValueFunction(self, func)
self._marketValueFunc = func
return self
end
--- Sets the alert threshold.
-- @tparam CommodityList self The commodity list object
-- @tparam number threshold The item buyout above which the alert icon should be shown
-- @treturn CommodityList The commodity list object
function CommodityList.SetAlertThreshold(self, threshold)
self._alertThreshold = threshold or math.huge
return self
end
function CommodityList.SetSelection(self, selection)
self.__super:SetSelection(selection and self:_SanitizeSelectionIndex(selection) or nil)
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function CommodityList._HandleRowClick(self, data, mouseButton)
local index = self:_SanitizeSelectionIndex(data)
if not index then
return
end
self.__super:_HandleRowClick(index, mouseButton)
end
function CommodityList._SanitizeSelectionIndex(self, selectedIndex)
-- select the highest subrow which isn't the player's auction and isn't above the selection
local highestIndex = nil
for index, subRow in self._row:SubRowIterator() do
if subRow:GetQuantities() - select(2, subRow:GetOwnerInfo()) ~= 0 then
highestIndex = index
end
if index == selectedIndex then
break
end
end
return highestIndex
end
function CommodityList._GetSubRow(self, index)
return self._row._subRows[index]
end
function CommodityList._UpdateData(self)
wipe(self._data)
if not self._row then
return
end
for index in self._row:SubRowIterator() do
tinsert(self._data, index)
end
end
function CommodityList._IsSelected(self, data)
if data > (self._selection or 0) then
return false
end
local subRow = self:_GetSubRow(data)
local _, numOwnerItems = subRow:GetOwnerInfo()
local quantityAvailable = subRow:GetQuantities() - numOwnerItems
return quantityAvailable > 0
end
function CommodityList._GetMarketValuePct(self, row)
assert(row:IsSubRow())
if not self._marketValueFunc then
-- no market value function was set
return nil, nil
end
local marketValue = self._marketValueFunc(row) or 0
if marketValue == 0 then
-- this item doesn't have a market value
return nil, nil
end
local _, itemBuyout = row:GetBuyouts()
return itemBuyout > 0 and Math.Round(100 * itemBuyout / marketValue) or nil
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GetWarningIcon(self, index)
local subRow = self:_GetSubRow(index)
assert(subRow)
local _, itemBuyout = subRow:GetBuyouts()
if itemBuyout < self._alertThreshold then
return
end
return "iconPack.12x12/Attention", L["This price is above your confirmation alert threshold."]
end
function private.GetItemBuyoutText(self, index)
local _, itemBuyout = self:_GetSubRow(index):GetBuyouts()
return Money.ToString(itemBuyout, nil, "OPT_83_NO_COPPER")
end
function private.GetPercentText(self, index)
local pct = self:_GetMarketValuePct(self:_GetSubRow(index))
if not pct then
return "---"
end
local pctColor = Theme.GetAuctionPercentColor(pct)
if pct > 999 then
pct = ">999"
end
return pctColor:ColorText(pct.."%")
end
function private.GetQuantityText(self, index)
local subRow = self:_GetSubRow(index)
local _, numOwnerItems = subRow:GetOwnerInfo()
local totalQuantity = subRow:GetQuantities()
local quantityAvailable = totalQuantity - numOwnerItems
if quantityAvailable == 0 then
return Theme.GetColor("INDICATOR_ALT"):ColorText(totalQuantity)
else
return quantityAvailable
end
end

View File

@@ -0,0 +1,207 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Container UI Element Class.
-- A container is an abstract element class which simply contains other elements. It is a subclass of the @{Element} class.
-- @classmod Container
local _, TSM = ...
local TempTable = TSM.Include("Util.TempTable")
local Table = TSM.Include("Util.Table")
local Container = TSM.Include("LibTSMClass").DefineClass("Container", TSM.UI.Element, "ABSTRACT")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(Container)
TSM.UI.Container = Container
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function Container.__init(self, frame)
self.__super:__init(frame)
self._children = {}
self._layoutChildren = {}
self._noLayoutChildren = {}
end
function Container.Release(self)
self:ReleaseAllChildren()
self.__super:Release()
end
--- Release all child elements.
-- @tparam Container self The container object
function Container.ReleaseAllChildren(self)
for _, child in ipairs(self._children) do
child:Release()
end
wipe(self._children)
wipe(self._layoutChildren)
wipe(self._noLayoutChildren)
end
--- Add a child element.
-- @tparam Container self The container object
-- @tparam Element child The child element
-- @treturn Container The container object
function Container.AddChild(self, child)
self:_AddChildHelper(child, true)
return self
end
--- Add a child element when the required condition is true.
-- @tparam Container self The container object
-- @tparam boolean condition The required condition
-- @tparam Element child The child element
-- @treturn Container The container object
function Container.AddChildIf(self, condition, child)
if not condition then
child:Release()
return self
end
self:_AddChildHelper(child, true)
return self
end
--- Add a child element before another one.
-- @tparam Container self The container object
-- @tparam string beforeId The id of the child element to add this one before
-- @tparam Element child The child element
-- @treturn Container The container object
function Container.AddChildBeforeById(self, beforeId, child)
self:_AddChildHelper(child, true, beforeId)
return self
end
--- Add child elements using a function.
-- @tparam Container self The container object
-- @tparam function func The function to call and pass this container object
-- @tparam vararg ... Additional arguments to pass to the function
-- @treturn Container The container object
function Container.AddChildrenWithFunction(self, func, ...)
func(self, ...)
return self
end
--- Add a child element which is not involved in layout.
-- The layout of this child must be explicitly done by the application code.
-- @tparam Container self The container object
-- @tparam Element child The child element
-- @treturn Container The container object
function Container.AddChildNoLayout(self, child)
self:_AddChildHelper(child, false)
return self
end
--- Remove a child element.
-- @tparam Container self The container object
-- @tparam Element child The child element to remove
function Container.RemoveChild(self, child)
assert(child:__isa(TSM.UI.Element) and child:_GetBaseFrame():GetParent())
child:_GetBaseFrame():SetParent(nil)
Table.RemoveByValue(self._children, child)
Table.RemoveByValue(self._layoutChildren, child)
Table.RemoveByValue(self._noLayoutChildren, child)
child:_SetParentElement(nil)
end
function Container.HasChildById(self, childId)
for _, child in ipairs(self._children) do
if child._id == childId then
return true
end
end
return false
end
--- Gets the number of child elements involved in layout.
-- @tparam Container self The container object
-- @treturn number The number of elements
function Container.GetNumLayoutChildren(self)
local count = 0
for _ in self:LayoutChildrenIterator() do
count = count + 1
end
return count
end
--- Iterates through the child elements involved in layout.
-- @tparam Container self The container object
-- @return An iterator with the following fields: `index, child`
function Container.LayoutChildrenIterator(self)
local children = TempTable.Acquire()
for _, child in ipairs(self._layoutChildren) do
if child:IsVisible() then
tinsert(children, child)
end
end
return TempTable.Iterator(children)
end
--- Shows all child elements.
-- @tparam Container self The container object
function Container.ShowAllChildren(self)
for _, child in ipairs(self._layoutChildren) do
if not child:IsVisible() then
child:Show()
end
end
end
function Container.Draw(self)
self.__super:Draw()
for _, child in ipairs(self._children) do
child:Draw()
end
end
-- ============================================================================
-- Container - Private Class Methods
-- ============================================================================
function Container._AddChildHelper(self, child, layout, beforeId)
assert(child:__isa(TSM.UI.Element) and not child:_GetBaseFrame():GetParent())
child:_GetBaseFrame():SetParent(self:_GetBaseFrame())
tinsert(self._children, private.GetElementInsertIndex(self._children, beforeId), child)
if layout then
tinsert(self._layoutChildren, private.GetElementInsertIndex(self._layoutChildren, beforeId), child)
else
tinsert(self._noLayoutChildren, private.GetElementInsertIndex(self._noLayoutChildren, beforeId), child)
end
child:_SetParentElement(self)
child:Show()
end
function Container._ClearBaseElementCache(self)
self.__super:_ClearBaseElementCache()
for _, child in ipairs(self._children) do
child:_ClearBaseElementCache()
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GetElementInsertIndex(tbl, beforeId)
if not beforeId then
return #tbl + 1
end
for i, element in ipairs(tbl) do
if element._id == beforeId then
return i
end
end
error("Invalid beforeId: "..tostring(beforeId))
end

View File

@@ -0,0 +1,141 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Crafting Mat List UI Element Class.
-- The element used to show the mats for a specific craft in the Crafting UI. It is a subclass of the @{ScrollingTable} class.
-- @classmod CraftingMatList
local _, TSM = ...
local CraftingMatList = TSM.Include("LibTSMClass").DefineClass("CraftingMatList", TSM.UI.ScrollingTable)
local ItemString = TSM.Include("Util.ItemString")
local Theme = TSM.Include("Util.Theme")
local Inventory = TSM.Include("Service.Inventory")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(CraftingMatList)
TSM.UI.CraftingMatList = CraftingMatList
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function CraftingMatList.__init(self)
self.__super:__init()
self._spellId = nil
self._rowHoverEnabled = false
end
function CraftingMatList.Acquire(self)
self._headerHidden = true
self.__super:Acquire()
self:SetSelectionDisabled(true)
self:GetScrollingTableInfo()
:NewColumn("check")
:SetWidth(14)
:SetIconSize(14)
:SetIconFunction(private.GetCheck)
:Commit()
:NewColumn("item")
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetIconSize(12)
:SetIconFunction(private.GetItemIcon)
:SetTextFunction(private.GetItemText)
:SetTooltipFunction(private.GetItemTooltip)
:Commit()
:NewColumn("qty")
:SetAutoWidth()
:SetFont("TABLE_TABLE1")
:SetJustifyH("CENTER")
:SetTextFunction(private.GetQty)
:Commit()
:Commit()
end
function CraftingMatList.Release(self)
self._spellId = nil
self.__super:Release()
end
function CraftingMatList.SetScript(self, script, handler)
error("Unknown CraftingMatList script: "..tostring(script))
return self
end
--- Sets the crafting recipe to display materials for.
-- @tparam CraftingMatList self The crafting mat list object
-- @tparam number spellId The spellId for the recipe
-- @treturn CraftingMatList The crafting mat list object
function CraftingMatList.SetRecipe(self, spellId)
self._spellId = spellId
self:_UpdateData()
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function CraftingMatList._UpdateData(self)
wipe(self._data)
if not self._spellId then
return
end
for i = 1, TSM.Crafting.ProfessionUtil.GetNumMats(self._spellId) do
tinsert(self._data, i)
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GetCheck(self, index)
local itemLink, _, _, quantity = TSM.Crafting.ProfessionUtil.GetMatInfo(self._spellId, index)
local itemString = ItemString.Get(itemLink)
local bagQuantity = Inventory.GetBagQuantity(itemString)
if not TSM.IsWowClassic() then
bagQuantity = bagQuantity + Inventory.GetReagentBankQuantity(itemString) + Inventory.GetBankQuantity(itemString)
end
if bagQuantity >= quantity then
return TSM.UI.TexturePacks.GetColoredKey("iconPack.14x14/Checkmark/Default", Theme.GetFeedbackColor("GREEN"))
else
return TSM.UI.TexturePacks.GetColoredKey("iconPack.14x14/Close/Default", Theme.GetFeedbackColor("RED"))
end
end
function private.GetItemIcon(self, index)
local _, _, texture = TSM.Crafting.ProfessionUtil.GetMatInfo(self._spellId, index)
return texture
end
function private.GetItemText(self, index)
local itemLink = TSM.Crafting.ProfessionUtil.GetMatInfo(self._spellId, index)
local itemString = ItemString.Get(itemLink)
return TSM.UI.GetColoredItemName(itemString) or Theme.GetFeedbackColor("RED"):ColorText("?")
end
function private.GetItemTooltip(self, index)
local itemLink = TSM.Crafting.ProfessionUtil.GetMatInfo(self._spellId, index)
return ItemString.Get(itemLink)
end
function private.GetQty(self, index)
local itemLink, _, _, quantity = TSM.Crafting.ProfessionUtil.GetMatInfo(self._spellId, index)
local itemString = ItemString.Get(itemLink)
local bagQuantity = Inventory.GetBagQuantity(itemString)
if not TSM.IsWowClassic() then
bagQuantity = bagQuantity + Inventory.GetReagentBankQuantity(itemString) + Inventory.GetBankQuantity(itemString)
end
local color = bagQuantity >= quantity and Theme.GetFeedbackColor("GREEN") or Theme.GetFeedbackColor("RED")
return color:ColorText(format("%d / %d", bagQuantity, quantity))
end

View File

@@ -0,0 +1,498 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Crafting Queue List UI Element Class.
-- The element used to show the queue in the Crafting UI. It is a subclass of the @{ScrollingTable} class.
-- @classmod CraftingQueueList
local _, TSM = ...
local L = TSM.Include("Locale").GetTable()
local TempTable = TSM.Include("Util.TempTable")
local Money = TSM.Include("Util.Money")
local Theme = TSM.Include("Util.Theme")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local ItemInfo = TSM.Include("Service.ItemInfo")
local Inventory = TSM.Include("Service.Inventory")
local CraftingQueueList = TSM.Include("LibTSMClass").DefineClass("CraftingQueueList", TSM.UI.ScrollingTable)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(CraftingQueueList)
TSM.UI.CraftingQueueList = CraftingQueueList
local private = {
categoryOrder = {},
sortSelf = nil,
sortProfitCache = {},
}
local CATEGORY_SEP = "\001"
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function CraftingQueueList.__init(self)
self.__super:__init()
self._collapsed = {}
self._query = nil
self._numCraftableCache = {}
self._onRowMouseDownHandler = nil
end
function CraftingQueueList.Acquire(self)
self._headerHidden = true
self.__super:Acquire()
self:SetSelectionDisabled(true)
self:GetScrollingTableInfo()
:NewColumn("name")
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetIconSize(12)
:SetExpanderStateFunction(private.GetExpanderState)
:SetIconFunction(private.GetItemIcon)
:SetIconHoverEnabled(true)
:SetIconClickHandler(private.OnItemIconClick)
:SetTextFunction(private.GetItemText)
:SetTooltipFunction(private.GetItemTooltip)
:SetActionIconInfo(1, 12, private.GetDeleteIcon, true)
:SetActionIconClickHandler(private.OnDeleteIconClick)
:Commit()
:NewColumn("qty")
:SetAutoWidth()
:SetFont("TABLE_TABLE1")
:SetJustifyH("CENTER")
:SetTextFunction(private.GetQty)
:SetActionIconInfo(1, 12, private.GetEditIcon, true)
:SetActionIconClickHandler(private.OnEditIconClick)
:Commit()
:Commit()
end
function CraftingQueueList.Release(self)
self._onRowMouseDownHandler = nil
wipe(self._numCraftableCache)
wipe(self._collapsed)
if self._query then
self._query:Release()
self._query = nil
end
for _, row in ipairs(self._rows) do
ScriptWrapper.Clear(row._frame, "OnDoubleClick")
ScriptWrapper.Clear(row._frame, "OnMouseDown")
for _, button in pairs(row._buttons) do
ScriptWrapper.Clear(button, "OnMouseDown")
end
end
self.__super:Release()
end
--- Gets the data of the first row.
-- @tparam CraftingMatList self The crafting queue list object
-- @treturn CraftingQueueList The crafting queue list object
function CraftingQueueList.GetFirstData(self)
for _, data in ipairs(self._data) do
if type(data) ~= "string" then
return data
end
end
end
--- Registers a script handler.
-- @tparam CraftingQueueList self The crafting queue list object
-- @tparam string script The script to register for (supported scripts: `OnRowClick`, `OnRowMouseDown`)
-- @tparam function handler The script handler which will be called with the crafting queue list object followed by any
-- arguments to the script
-- @treturn CraftingQueueList The crafting queue list object
function CraftingQueueList.SetScript(self, script, handler)
if script == "OnRowMouseDown" then
self._onRowMouseDownHandler = handler
else
self.__super:SetScript(script, handler)
end
return self
end
--- Sets the @{DatabaseQuery} source for this list.
-- This query is used to populate the entries in the crafting queue list.
-- @tparam CraftingQueueList self The crafting queue list object
-- @tparam DatabaseQuery query The query object
-- @treturn CraftingQueueList The crafting queue list object
function CraftingQueueList.SetQuery(self, query)
if self._query then
self._query:Release()
end
self._query = query
self._query:SetUpdateCallback(private.QueryUpdateCallback, self)
self:_UpdateData()
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function CraftingQueueList._GetTableRow(self, isHeader)
local row = self.__super:_GetTableRow(isHeader)
if not isHeader then
ScriptWrapper.Set(row._frame, "OnMouseDown", private.RowOnMouseDown, row)
ScriptWrapper.Set(row._frame, "OnDoubleClick", private.RowOnDoubleClick, row)
for _, button in pairs(row._buttons) do
ScriptWrapper.Set(button, "OnMouseDown", private.RowOnMouseDown, row)
end
end
return row
end
function CraftingQueueList._UpdateData(self)
wipe(self._data)
if not self._query then
return
end
local categories = TempTable.Acquire()
wipe(self._numCraftableCache)
wipe(private.sortProfitCache)
for _, row in self._query:Iterator() do
local rawCategory = strjoin(CATEGORY_SEP, row:GetFields("profession", "players"))
local category = strlower(rawCategory)
if not categories[category] then
tinsert(categories, category)
end
categories[category] = rawCategory
if not self._collapsed[rawCategory] then
local spellId = row:GetField("spellId")
self._numCraftableCache[row] = TSM.Crafting.ProfessionUtil.GetNumCraftableFromDB(spellId)
private.sortProfitCache[spellId] = TSM.Crafting.Cost.GetProfitBySpellId(spellId)
tinsert(self._data, row)
end
end
sort(categories, private.CategorySortComparator)
wipe(private.categoryOrder)
for i, category in ipairs(categories) do
private.categoryOrder[category] = i
tinsert(self._data, categories[category])
end
TempTable.Release(categories)
private.sortSelf = self
sort(self._data, private.DataSortComparator)
private.sortSelf = nil
end
function CraftingQueueList._SetCollapsed(self, data, collapsed)
self._collapsed[data] = collapsed or nil
end
function CraftingQueueList._HandleRowClick(self, data, mouseButton)
if type(data) == "string" then
self:_SetCollapsed(data, not self._collapsed[data])
self:UpdateData(true)
else
local currentRow
for _, row in ipairs(self._rows) do
if row:GetData() == data then
currentRow = row
break
end
end
if currentRow._texts.qty:IsMouseOver(0, 0, 0, 12) then
private.OnEditIconClick(self, data, 1)
else
self.__super:_HandleRowClick(data, mouseButton)
end
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.RowOnMouseDown(row, mouseButton)
local data = row:GetData()
if type(data) == "string" then
return
end
local self = row._scrollingTable
if self._onRowMouseDownHandler then
self:_onRowMouseDownHandler(data, mouseButton)
end
end
function private.RowOnDoubleClick(row, mouseButton)
local self = row._scrollingTable
self:_HandleRowClick(row:GetData(), mouseButton)
end
function private.GetExpanderState(self, data)
if type(data) == "string" then
return true, not self._collapsed[data], 0
else
return false, false, 0
end
end
function private.GetItemIcon(self, data)
if type(data) == "string" then
return
end
local spellId = data:GetField("spellId")
local itemString = TSM.Crafting.GetItemString(spellId)
local texture, tooltip = nil, nil
if itemString then
texture = ItemInfo.GetTexture(itemString)
tooltip = itemString
else
texture = select(3, TSM.Crafting.ProfessionUtil.GetResultInfo(spellId))
if TSM.Crafting.ProfessionState.IsClassicCrafting() then
tooltip = "craft:"..(TSM.Crafting.ProfessionScanner.GetIndexBySpellId(spellId) or spellId)
else
tooltip = "enchant:"..spellId
end
return
end
return texture, tooltip
end
function private.OnItemIconClick(self, data, mouseButton)
self:_HandleRowClick(data, mouseButton)
end
function private.GetItemText(self, data)
if type(data) == "string" then
local profession, players = strsplit(CATEGORY_SEP, data)
local isValid = private.PlayersContains(players, UnitName("player")) and strlower(profession) == strlower(TSM.Crafting.ProfessionUtil.GetCurrentProfessionName() or "")
local text = Theme.GetColor("INDICATOR"):ColorText(profession.." ("..players..")")
if isValid then
return text
else
return text.." "..TSM.UI.TexturePacks.GetTextureLink("iconPack.12x12/Attention")
end
else
local spellId = data:GetField("spellId")
local itemString = TSM.Crafting.GetItemString(spellId)
return itemString and TSM.UI.GetColoredItemName(itemString) or GetSpellInfo(spellId) or "?"
end
end
function private.GetItemTooltip(self, data)
if type(data) == "string" then
local profession, players = strsplit(CATEGORY_SEP, data)
if not private.PlayersContains(players, UnitName("player")) then
return L["You are not on one of the listed characters."]
elseif strlower(profession) ~= strlower(TSM.Crafting.ProfessionUtil.GetCurrentProfessionName() or "") then
return L["This profession is not open."]
end
return
end
local spellId = data:GetField("spellId")
local numQueued = data:GetField("num")
local itemString = TSM.Crafting.GetItemString(spellId)
local name = itemString and TSM.UI.GetColoredItemName(itemString) or GetSpellInfo(spellId) or "?"
local tooltipLines = TempTable.Acquire()
tinsert(tooltipLines, name.." (x"..numQueued..")")
local numResult = TSM.Crafting.GetNumResult(spellId)
local profit = TSM.Crafting.Cost.GetProfitBySpellId(spellId)
local profitStr = profit and Money.ToString(profit * numResult, Theme.GetFeedbackColor(profit >= 0 and "GREEN" or "RED"):GetTextColorPrefix()) or "---"
local totalProfitStr = profit and Money.ToString(profit * numResult * numQueued, Theme.GetFeedbackColor(profit >= 0 and "GREEN" or "RED"):GetTextColorPrefix()) or "---"
tinsert(tooltipLines, L["Profit (Total)"]..": "..profitStr.." ("..totalProfitStr..")")
for _, matItemString, quantity in TSM.Crafting.MatIterator(spellId) do
local numHave = Inventory.GetBagQuantity(matItemString)
if not TSM.IsWowClassic() then
numHave = numHave + Inventory.GetReagentBankQuantity(matItemString) + Inventory.GetBankQuantity(matItemString)
end
local numNeed = quantity * numQueued
local color = Theme.GetFeedbackColor(numHave >= numNeed and "GREEN" or "RED")
tinsert(tooltipLines, color:ColorText(numHave.."/"..numNeed).." - "..(ItemInfo.GetName(matItemString) or "?"))
end
if TSM.Crafting.ProfessionUtil.GetRemainingCooldown(spellId) then
tinsert(tooltipLines, Theme.GetFeedbackColor("RED"):ColorText(L["On Cooldown"]))
end
return strjoin("\n", TempTable.UnpackAndRelease(tooltipLines)), true, true
end
function private.GetDeleteIcon(self, data, iconIndex)
assert(iconIndex == 1)
if type(data) == "string" then
return false
end
return true, "iconPack.12x12/Close/Default", true
end
function private.OnDeleteIconClick(self, data, iconIndex)
assert(iconIndex == 1 and type(data) ~= "string")
TSM.Crafting.Queue.SetNum(data:GetField("spellId"), 0)
end
function private.GetEditIcon(self, data, iconIndex)
assert(iconIndex == 1)
if type(data) == "string" then
return false
end
return true, "iconPack.12x12/Edit", true
end
function private.OnEditIconClick(self, data, iconIndex)
assert(iconIndex == 1 and type(data) ~= "string")
local currentRow = nil
for _, row in ipairs(self._rows) do
if row:GetData() == data then
currentRow = row
break
end
end
local name = private.GetItemText(self, data)
local texture, tooltip = private.GetItemIcon(self, data)
local dialogFrame = UIElements.New("Frame", "qty")
:SetLayout("HORIZONTAL")
:AddAnchor("LEFT", currentRow._frame, Theme.GetColSpacing() / 2, 0)
:AddAnchor("RIGHT", currentRow._frame, -Theme.GetColSpacing(), 0)
:SetHeight(20)
:SetContext(self)
:SetBackgroundColor("PRIMARY_BG")
:SetScript("OnHide", private.DialogOnHide)
:AddChild(UIElements.New("Button", "icon")
:SetSize(12, 12)
:SetMargin(16, 4, 0, 0)
:SetBackground(texture)
:SetTooltip(tooltip)
)
:AddChild(UIElements.New("Text", "name")
:SetWidth("AUTO")
:SetFont("ITEM_BODY3")
:SetText(name)
)
:AddChild(UIElements.New("Spacer", "spacer"))
:AddChild(UIElements.New("Input", "input")
:SetWidth(75)
:SetBackgroundColor("ACTIVE_BG")
:SetJustifyH("CENTER")
:SetContext(currentRow:GetData():GetField("spellId"))
:SetSubAddEnabled(true)
:SetValidateFunc("NUMBER", "1:9999")
:SetValue(currentRow:GetData():GetField("num"))
:SetScript("OnFocusLost", private.QtyInputOnFocusLost)
)
local baseFrame = self:GetBaseElement()
baseFrame:ShowDialogFrame(dialogFrame)
dialogFrame:GetElement("input"):SetFocused(true)
end
function private.DialogOnHide(frame)
local input = frame:GetElement("input")
TSM.Crafting.Queue.SetNum(input:GetContext(), tonumber(input:GetValue()))
frame:GetContext():Draw()
end
function private.QtyInputOnFocusLost(input)
input:GetBaseElement():HideDialog()
end
function private.GetQty(self, data)
if type(data) == "string" then
return ""
end
local numQueued = data:GetFields("num")
local numCraftable = min(self._numCraftableCache[data], numQueued)
local onCooldown = TSM.Crafting.ProfessionUtil.GetRemainingCooldown(data:GetField("spellId"))
local color = Theme.GetFeedbackColor(((numCraftable == 0 or onCooldown) and "RED") or (numCraftable < numQueued and "YELLOW") or "GREEN")
return color:ColorText(format("%s / %s", numCraftable, numQueued))
end
function private.PlayersContains(players, player)
players = strlower(players)
player = strlower(player)
return players == player or strmatch(players, "^"..player..",") or strmatch(players, ","..player..",") or strmatch(players, ","..player.."$")
end
function private.CategorySortComparator(a, b)
local aProfession, aPlayers = strsplit(CATEGORY_SEP, a)
local bProfession, bPlayers = strsplit(CATEGORY_SEP, b)
if aProfession ~= bProfession then
local currentProfession = TSM.Crafting.ProfessionUtil.GetCurrentProfessionName()
currentProfession = strlower(currentProfession or "")
if aProfession == currentProfession then
return true
elseif bProfession == currentProfession then
return false
else
return aProfession < bProfession
end
end
local playerName = UnitName("player")
local aContainsPlayer = private.PlayersContains(aPlayers, playerName)
local bContainsPlayer = private.PlayersContains(bPlayers, playerName)
if aContainsPlayer and not bContainsPlayer then
return true
elseif bContainsPlayer and not aContainsPlayer then
return false
else
return aPlayers < bPlayers
end
end
function private.DataSortComparator(a, b)
-- sort by category
local aCategory, bCategory = nil, nil
if type(a) == "string" and type(b) == "string" then
return private.categoryOrder[strlower(a)] < private.categoryOrder[strlower(b)]
elseif type(a) == "string" then
aCategory = strlower(a)
bCategory = strlower(strjoin(CATEGORY_SEP, b:GetFields("profession", "players")))
if aCategory == bCategory then
return true
end
elseif type(b) == "string" then
aCategory = strlower(strjoin(CATEGORY_SEP, a:GetFields("profession", "players")))
bCategory = strlower(b)
if aCategory == bCategory then
return false
end
else
aCategory = strlower(strjoin(CATEGORY_SEP, a:GetFields("profession", "players")))
bCategory = strlower(strjoin(CATEGORY_SEP, b:GetFields("profession", "players")))
end
if aCategory ~= bCategory then
return private.categoryOrder[aCategory] < private.categoryOrder[bCategory]
end
-- sort spells within a category
local aSpellId = a:GetField("spellId")
local bSpellId = b:GetField("spellId")
local aNumCraftable = private.sortSelf._numCraftableCache[a]
local bNumCraftable = private.sortSelf._numCraftableCache[b]
local aNumQueued = a:GetField("num")
local bNumQueued = b:GetField("num")
local aCanCraftAll = aNumCraftable >= aNumQueued
local bCanCraftAll = bNumCraftable >= bNumQueued
if aCanCraftAll and not bCanCraftAll then
return true
elseif not aCanCraftAll and bCanCraftAll then
return false
end
local aCanCraftSome = aNumCraftable > 0
local bCanCraftSome = bNumCraftable > 0
if aCanCraftSome and not bCanCraftSome then
return true
elseif not aCanCraftSome and bCanCraftSome then
return false
end
local aProfit = private.sortProfitCache[aSpellId]
local bProfit = private.sortProfitCache[bSpellId]
if aProfit and not bProfit then
return true
elseif not aProfit and bProfit then
return false
end
if aProfit ~= bProfit then
return aProfit > bProfit
end
return aSpellId < bSpellId
end
function private.QueryUpdateCallback(_, _, self)
self:UpdateData(true)
end

View File

@@ -0,0 +1,281 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Divided Container UI Element Class.
-- A divided container is a container with two children with a divider between them. It is a subclass of the @{Frame} class.
-- @classmod DividedContainer
local _, TSM = ...
local UIElements = TSM.Include("UI.UIElements")
local DividedContainer = TSM.Include("LibTSMClass").DefineClass("DividedContainer", TSM.UI.Frame)
UIElements.Register(DividedContainer)
TSM.UI.DividedContainer = DividedContainer
local private = {}
local DIVIDER_SIZE = 2
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function DividedContainer.__init(self)
self.__super:__init()
self._leftChild = nil
self._rightChild = nil
self._resizeStartX = nil
self._resizeOffset = 0
self._contextTable = nil
self._defaultContextTable = nil
self._minLeftWidth = nil
self._minRightWidth = nil
end
function DividedContainer.Acquire(self)
self.__super:AddChildNoLayout(UIElements.New("Frame", "leftEmpty")
:AddAnchor("TOPLEFT")
:AddAnchor("BOTTOMRIGHT", "divider", "BOTTOMLEFT")
)
self.__super:AddChild(UIElements.New("Button", "divider")
:SetSize(DIVIDER_SIZE, nil)
:SetHitRectInsets(-2, -2, 0, 0)
:SetRelativeLevel(2)
:EnableRightClick()
:SetScript("OnMouseDown", private.HandleOnMouseDown)
:SetScript("OnMouseUp", private.HandleOnMouseUp)
:SetScript("OnClick", private.HandleOnClick)
:SetScript("OnUpdate", private.HandleOnUpdate)
)
self.__super:AddChildNoLayout(UIElements.New("Frame", "rightEmpty")
:AddAnchor("TOPLEFT", "divider", "TOPRIGHT")
:AddAnchor("BOTTOMRIGHT")
)
self.__super:Acquire()
self.__super:SetLayout("HORIZONTAL")
end
function DividedContainer.Release(self)
self._isVertical = false
self._leftChild = nil
self._rightChild = nil
self._resizeStartX = nil
self._resizeOffset = 0
self._contextTable = nil
self._defaultContextTable = nil
self._minLeftWidth = nil
self._minRightWidth = nil
self.__super:Release()
end
function DividedContainer.SetVertical(self)
assert(not self._leftChild and not self._rightChild and not self._isVertical)
self._isVertical = true
self:GetElement("leftEmpty")
:WipeAnchors()
:AddAnchor("TOPLEFT")
:AddAnchor("BOTTOMRIGHT", "divider", "TOPRIGHT")
self:GetElement("divider")
:SetSize(nil, DIVIDER_SIZE)
:SetHitRectInsets(0, 0, -2, -2)
self:GetElement("rightEmpty")
:WipeAnchors()
:AddAnchor("TOPLEFT", "divider", "BOTTOMLEFT")
:AddAnchor("BOTTOMRIGHT")
self.__super:SetLayout("VERTICAL")
return self
end
function DividedContainer.SetLayout(self, layout)
error("DividedContainer doesn't support this method")
end
function DividedContainer.AddChild(self, child)
error("DividedContainer doesn't support this method")
end
function DividedContainer.AddChildBeforeById(self, beforeId, child)
error("DividedContainer doesn't support this method")
end
--- Sets the context table.
-- This table can be used to preserve the divider position across lifecycles of the divided container and even WoW
-- sessions if it's within the settings DB. The position is stored as the width of the left child element.
-- @tparam DividedContainer self The divided container object
-- @tparam table tbl The context table
-- @tparam table defaultTbl The default table (required fields: `leftWidth`)
-- @treturn DividedContainer The divided container object
function DividedContainer.SetContextTable(self, tbl, defaultTbl)
assert(defaultTbl.leftWidth > 0)
tbl.leftWidth = tbl.leftWidth or defaultTbl.leftWidth
self._contextTable = tbl
self._defaultContextTable = defaultTbl
return self
end
--- Sets the context table from a settings object.
-- @tparam DividedContainer self The divided container object
-- @tparam Settings settings The settings object
-- @tparam string key The setting key
-- @treturn DividedContainer The divided container object
function DividedContainer.SetSettingsContext(self, settings, key)
return self:SetContextTable(settings[key], settings:GetDefaultReadOnly(key))
end
--- Sets the minimum width of the child element.
-- @tparam DividedContainer self The divided container object
-- @tparam number minLeftWidth The minimum width of the left child element
-- @tparam number minRightWidth The minimum width of the right child element
-- @treturn DividedContainer The divided container object
function DividedContainer.SetMinWidth(self, minLeftWidth, minRightWidth)
self._minLeftWidth = minLeftWidth
self._minRightWidth = minRightWidth
return self
end
--- Sets the left child element.
-- @tparam DividedContainer self The divided container object
-- @tparam Element child The left child element
-- @treturn DividedContainer The divided container object
function DividedContainer.SetLeftChild(self, child)
assert(not self._isVertical and not self._leftChild and child)
self._leftChild = child
self.__super:AddChildBeforeById("divider", child)
return self
end
--- Sets the right child element.
-- @tparam DividedContainer self The divided container object
-- @tparam Element child The right child element
-- @treturn DividedContainer The divided container object
function DividedContainer.SetRightChild(self, child)
assert(not self._isVertical and not self._rightChild and child)
self._rightChild = child
self.__super:AddChild(child)
return self
end
--- Sets the top child element in vertical mode.
-- @tparam DividedContainer self The divided container object
-- @tparam Element child The top child element
-- @treturn DividedContainer The divided container object
function DividedContainer.SetTopChild(self, child)
assert(self._isVertical and not self._leftChild and child)
self._leftChild = child
self.__super:AddChildBeforeById("divider", child)
return self
end
--- Sets the bottom child element in vertical mode.
-- @tparam DividedContainer self The divided container object
-- @tparam Element child The bottom child element
-- @treturn DividedContainer The divided container object
function DividedContainer.SetBottomChild(self, child)
assert(self._isVertical and not self._rightChild and child)
self._rightChild = child
self.__super:AddChild(child)
return self
end
function DividedContainer.Draw(self)
assert(self._contextTable and self._minLeftWidth and self._minRightWidth)
self.__super.__super.__super:Draw()
self:GetElement("divider")
:SetBackground("ACTIVE_BG")
:SetHighlightEnabled(true)
local width = self:_GetDimension(self._isVertical and "HEIGHT" or "WIDTH") - DIVIDER_SIZE
local leftWidth = self._contextTable.leftWidth + self._resizeOffset
local rightWidth = width - leftWidth
if rightWidth < self._minRightWidth then
leftWidth = width - self._minRightWidth
assert(leftWidth >= self._minLeftWidth)
elseif leftWidth < self._minLeftWidth then
leftWidth = self._minLeftWidth
end
self._contextTable.leftWidth = leftWidth - self._resizeOffset
local leftChild = self._leftChild
local rightChild = self._rightChild
local leftEmpty = self:GetElement("leftEmpty")
local rightEmpty = self:GetElement("rightEmpty")
if self._isVertical then
leftEmpty:SetHeight(leftWidth)
leftChild:SetHeight(leftWidth)
else
leftEmpty:SetWidth(leftWidth)
leftChild:SetWidth(leftWidth)
end
if self._resizeStartX then
leftChild:_GetBaseFrame():SetAlpha(0)
leftChild:_GetBaseFrame():SetFrameStrata("LOW")
rightChild:_GetBaseFrame():SetAlpha(0)
rightChild:_GetBaseFrame():SetFrameStrata("LOW")
leftEmpty:Show()
rightEmpty:Show()
else
leftChild:_GetBaseFrame():SetAlpha(1)
leftChild:_GetBaseFrame():SetFrameStrata(self:_GetBaseFrame():GetFrameStrata())
rightChild:_GetBaseFrame():SetAlpha(1)
rightChild:_GetBaseFrame():SetFrameStrata(self:_GetBaseFrame():GetFrameStrata())
leftEmpty:Hide()
rightEmpty:Hide()
end
self.__super:Draw()
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.HandleOnUpdate(handle)
local self = handle:GetParentElement()
if self._resizeStartX then
if self._isVertical then
local currY = select(2, GetCursorPosition()) / self:_GetBaseFrame():GetEffectiveScale()
self._resizeOffset = self._resizeStartX - currY
else
local currX = GetCursorPosition() / self:_GetBaseFrame():GetEffectiveScale()
self._resizeOffset = currX - self._resizeStartX
end
self:Draw()
end
end
function private.HandleOnMouseDown(handle, mouseButton)
if mouseButton ~= "LeftButton" then
return
end
local self = handle:GetParentElement()
if self._isVertical then
self._resizeStartX = select(2, GetCursorPosition()) / self:_GetBaseFrame():GetEffectiveScale()
else
self._resizeStartX = GetCursorPosition() / self:_GetBaseFrame():GetEffectiveScale()
end
self._resizeOffset = 0
end
function private.HandleOnMouseUp(handle, mouseButton)
if mouseButton ~= "LeftButton" then
return
end
local self = handle:GetParentElement()
self._contextTable.leftWidth = max(self._contextTable.leftWidth + self._resizeOffset, self._minLeftWidth)
self._resizeOffset = 0
self._resizeStartX = nil
self:Draw()
end
function private.HandleOnClick(handle, mouseButton)
if mouseButton ~= "RightButton" then
return
end
local self = handle:GetParentElement()
self._contextTable.leftWidth = self._defaultContextTable.leftWidth
self:Draw()
end

View File

@@ -0,0 +1,195 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
local _, TSM = ...
local Color = TSM.Include("Util.Color")
local Theme = TSM.Include("Util.Theme")
local UIElements = TSM.Include("UI.UIElements")
local DropdownList = TSM.Include("LibTSMClass").DefineClass("DropdownList", TSM.UI.ScrollingTable)
UIElements.Register(DropdownList)
TSM.UI.DropdownList = DropdownList
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function DropdownList.__init(self)
self.__super:__init()
self._selectedItems = {}
self._multiselect = false
self._onSelectionChangedHandler = nil
end
function DropdownList.Acquire(self)
self._backgroundColor = "ACTIVE_BG"
self._headerHidden = true
self.__super:Acquire()
self:SetSelectionDisabled(true)
self:GetScrollingTableInfo()
:NewColumn("text")
:SetFont("BODY_BODY3")
:SetJustifyH("LEFT")
:SetTextFunction(private.GetText)
:SetIconSize(12)
:SetIconFunction(private.GetIcon)
:DisableHiding()
:Commit()
:Commit()
end
function DropdownList.Release(self)
wipe(self._selectedItems)
self._multiselect = false
self._onSelectionChangedHandler = nil
self.__super:Release()
end
function DropdownList.SetMultiselect(self, multiselect)
self._multiselect = multiselect
return self
end
function DropdownList.SetItems(self, items, selection, redraw)
wipe(self._data)
for _, item in ipairs(items) do
tinsert(self._data, item)
end
self:_SetSelectionHelper(selection)
if redraw then
self:Draw()
end
return self
end
function DropdownList.ItemIterator(self)
return private.ItemIterator, self, 0
end
function DropdownList.SetSelection(self, selection)
self:_SetSelectionHelper(selection)
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler(self._multiselect and self._selectedItems or selection)
end
return self
end
function DropdownList.GetSelection(self)
if self._multiselect then
return self._selectedItems
else
local selectedItem = next(self._selectedItems)
return selectedItem
end
end
function DropdownList.SelectAll(self)
assert(self._multiselect)
for _, data in ipairs(self._data) do
self._selectedItems[data] = true
end
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler(self._selectedItems)
end
self:Draw()
end
function DropdownList.DeselectAll(self)
assert(self._multiselect)
wipe(self._selectedItems)
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler(self._selectedItems)
end
self:Draw()
end
function DropdownList.SetScript(self, script, handler)
if script == "OnSelectionChanged" then
self._onSelectionChangedHandler = handler
else
error("Invalid DropdownList script: "..tostring(script))
end
return self
end
function DropdownList.Draw(self)
self.__super:Draw()
local textColor = nil
local color = Theme.GetColor(self._backgroundColor)
-- the text color should have maximum contrast with the background color, so set it to white/black based on the background color
if color:IsLight() then
-- the background is light, so set the text to black
textColor = Color.GetFullBlack()
else
-- the background is dark, so set the text to white
textColor = Color.GetFullWhite()
end
for _, row in ipairs(self._rows) do
row:SetTextColor(textColor)
end
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function DropdownList._SetSelectionHelper(self, selection)
wipe(self._selectedItems)
if selection then
if self._multiselect then
assert(type(selection) == "table")
for item, selected in pairs(selection) do
self._selectedItems[item] = selected
end
else
assert(type(selection) == "string" or type(selection) == "number")
self._selectedItems[selection] = true
end
end
end
function DropdownList._HandleRowClick(self, data)
if self._multiselect then
self._selectedItems[data] = not self._selectedItems[data] or nil
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler(self._selectedItems)
end
self:Draw()
else
self:SetSelection(data)
end
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.GetText(self, data)
return data
end
function private.GetIcon(self, data)
return self._multiselect and self._selectedItems[data] and "iconPack.12x12/Checkmark/Default" or ""
end
function private.ItemIterator(self, index)
index = index + 1
local item = self._data[index]
if not item then
return
end
return index, item, self._selectedItems[item]
end

View File

@@ -0,0 +1,179 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- EditableText UI Element Class.
-- A text element which has an editing state. It is a subclass of the @{Text} class.
-- @classmod EditableText
local _, TSM = ...
local Theme = TSM.Include("Util.Theme")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local ItemLinked = TSM.Include("Service.ItemLinked")
local UIElements = TSM.Include("UI.UIElements")
local EditableText = TSM.Include("LibTSMClass").DefineClass("EditableText", TSM.UI.Text)
UIElements.Register(EditableText)
TSM.UI.EditableText = EditableText
local private = {}
local STRING_RIGHT_PADDING = 16
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function EditableText.__init(self)
local frame = UIElements.CreateFrame(self, "EditBox")
self.__super:__init(frame)
frame:SetShadowColor(0, 0, 0, 0)
frame:SetAutoFocus(false)
ScriptWrapper.Set(frame, "OnEscapePressed", private.OnEscapePressed, self)
ScriptWrapper.Set(frame, "OnEnterPressed", private.OnEnterPressed, self)
ScriptWrapper.Set(frame, "OnEditFocusLost", private.OnEditFocusLost, self)
frame.text = UIElements.CreateFontString(self, frame)
frame.text:SetAllPoints()
local function ItemLinkedCallback(name, link)
if self._allowItemInsert == nil or not self:IsVisible() or not self._editing then
return
end
if self._allowItemInsert == true then
frame:Insert(link)
else
frame:Insert(name)
end
return true
end
ItemLinked.RegisterCallback(ItemLinkedCallback, -1)
self._editing = false
self._allowItemInsert = nil
self._onValueChangedHandler = nil
self._onEditingChangedHandler = nil
end
function EditableText.Release(self)
self:_GetBaseFrame():ClearFocus()
self:_GetBaseFrame():Disable()
self._editing = false
self._allowItemInsert = nil
self._onValueChangedHandler = nil
self._onEditingChangedHandler = nil
self.__super:Release()
end
--- Registers a script handler.
-- @tparam EditableText self The editable text object
-- @tparam string script The script to register for (supported scripts: `OnValueChanged`, `OnEditingChanged`)
-- @tparam function handler The script handler which will be called with the editable text object followed by any
-- arguments to the script
-- @treturn EditableText The editable text object
function EditableText.SetScript(self, script, handler)
if script == "OnValueChanged" then
self._onValueChangedHandler = handler
elseif script == "OnEditingChanged" then
self._onEditingChangedHandler = handler
elseif script == "OnEnter" or script == "OnLeave" or script == "OnMouseDown" then
self.__super:SetScript(script, handler)
else
error("Unknown EditableText script: "..tostring(script))
end
return self
end
--- Sets whether or not the text is currently being edited.
-- @tparam EditableText self The editable text object
-- @tparam boolean editing The editing state to set
-- @treturn EditableText The editable text object
function EditableText.SetEditing(self, editing)
self._editing = editing
if self._onEditingChangedHandler then
self:_onEditingChangedHandler(editing)
end
if self._autoWidth then
self:GetParentElement():Draw()
else
self:Draw()
end
return self
end
--- Allows inserting an item into the editable text by linking it while the editable text has focus.
-- @tparam EditableText self The editable text object
-- @tparam[opt=false] boolean insertLink Insert the link instead of the item name
-- @treturn EditableText The editable text object
function EditableText.AllowItemInsert(self, insertLink)
assert(insertLink == true or insertLink == false or insertLink == nil)
self._allowItemInsert = insertLink or false
return self
end
function EditableText.Draw(self)
self.__super:Draw()
local frame = self:_GetBaseFrame()
-- set the editbox font
frame:SetFont(Theme.GetFont(self._font):GetWowFont())
-- set the justification
frame:SetJustifyH(self._justifyH)
frame:SetJustifyV(self._justifyV)
-- set the text color
frame:SetTextColor(self:_GetTextColor():GetFractionalRGBA())
if self._editing then
frame:Enable()
frame:SetText(self._textStr)
frame:SetFocus()
frame:HighlightText(0, -1)
frame.text:Hide()
else
frame:SetText("")
frame:ClearFocus()
frame:HighlightText(0, 0)
frame:Disable()
frame.text:Show()
end
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function EditableText._GetPreferredDimension(self, dimension)
if dimension == "WIDTH" and self._autoWidth and not self._editing then
return self:GetStringWidth() + STRING_RIGHT_PADDING
else
return self.__super.__super:_GetPreferredDimension(dimension)
end
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.OnEscapePressed(self)
self:SetEditing(false)
end
function private.OnEnterPressed(self)
local newText = self:_GetBaseFrame():GetText()
self:SetEditing(false)
self:_onValueChangedHandler(newText)
end
function private.OnEditFocusLost(self)
self:SetEditing(false)
end

View 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

456
Core/UI/Elements/Frame.lua Normal file
View File

@@ -0,0 +1,456 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Frame UI Element Class.
-- A frame is a container which supports automated layout of its children. It also supports being the base element of a UI and anchoring/parenting directly to a WoW frame. It is a subclass of the @{Container} class.
-- @classmod Frame
local _, TSM = ...
local TempTable = TSM.Include("Util.TempTable")
local Table = TSM.Include("Util.Table")
local Color = TSM.Include("Util.Color")
local Theme = TSM.Include("Util.Theme")
local NineSlice = TSM.Include("Util.NineSlice")
local VALID_LAYOUTS = {
NONE = true,
HORIZONTAL = true,
VERTICAL = true,
FLOW = true,
}
local LAYOUT_CONTEXT = {
VERTICAL = {
primaryDimension = "HEIGHT",
secondaryDimension = "WIDTH",
sides = { primary = { "TOP", "BOTTOM" }, secondary = { "LEFT", "RIGHT" } },
},
HORIZONTAL = {
primaryDimension = "WIDTH",
secondaryDimension = "HEIGHT",
sides = { primary = { "LEFT", "RIGHT" }, secondary = { "TOP", "BOTTOM" } },
},
}
local Frame = TSM.Include("LibTSMClass").DefineClass("Frame", TSM.UI.Container)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(Frame)
TSM.UI.Frame = Frame
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function Frame.__init(self)
local frame = UIElements.CreateFrame(self, "Frame")
self.__super:__init(frame)
self._borderNineSlice = NineSlice.New(frame)
self._borderNineSlice:Hide()
self._backgroundNineSlice = NineSlice.New(frame, 1)
self._backgroundNineSlice:Hide()
self._layout = "NONE"
self._backgroundColor = nil
self._roundedCorners = false
self._borderColor = nil
self._borderSize = nil
self._expandWidth = false
self._strata = nil
self._scale = 1
end
function Frame.Release(self)
self._layout = "NONE"
self._backgroundColor = nil
self._roundedCorners = false
self._borderColor = nil
self._borderSize = nil
self._expandWidth = false
self._strata = nil
self._scale = 1
self._borderNineSlice:Hide()
self._backgroundNineSlice:Hide()
local frame = self:_GetBaseFrame()
frame:RegisterForDrag(nil)
frame:EnableMouse(false)
frame:SetMovable(false)
frame:EnableMouseWheel(false)
frame:SetHitRectInsets(0, 0, 0, 0)
self.__super:Release()
end
--- Sets the background of the frame.
-- @tparam Frame self The frame object
-- @tparam ?string|Color|nil color The background color as a theme color key, Color object, or nil
-- @tparam[opt=false] boolean roundedCorners Whether or not the corners should be rounded
-- @treturn Frame The frame object
function Frame.SetBackgroundColor(self, color, roundedCorners)
assert(color == nil or Color.IsInstance(color) or Theme.GetColor(color))
self._backgroundColor = color
self._roundedCorners = roundedCorners
return self
end
--- Sets the border color of the frame.
-- @tparam Frame self The frame object
-- @tparam ?string|nil color The border color as a theme color key or nil
-- @tparam[opt=1] ?number borderSize The border size
-- @treturn Frame The frame object
function Frame.SetBorderColor(self, color, borderSize)
assert(color == nil or Color.IsInstance(color) or Theme.GetColor(color))
self._borderColor = color
self._borderSize = borderSize or 1
return self
end
--- Sets the width of the frame.
-- @tparam Frame self The frame object
-- @tparam ?number|string width The width of the frame, "EXPAND" to set the width to expand to be
-- as large as possible, or nil to have an undefined width
-- @treturn Frame The frame object
function Frame.SetWidth(self, width)
if width == "EXPAND" then
self._expandWidth = true
else
self.__super:SetWidth(width)
end
return self
end
--- Sets the parent frame.
-- @tparam Frame self The frame object
-- @tparam frame parent The WoW frame to parent to
-- @treturn Frame The frame object
function Frame.SetParent(self, parent)
self:_GetBaseFrame():SetParent(parent)
return self
end
--- Sets the level of the frame.
-- @tparam Frame self The frame object
-- @tparam number level The frame level
-- @treturn Frame The frame object
function Frame.SetFrameLevel(self, level)
self:_GetBaseFrame():SetFrameLevel(level)
return self
end
--- Sets the strata of the frame.
-- @tparam Frame self The frame object
-- @tparam string strata The frame strata
-- @treturn Frame The frame object
function Frame.SetStrata(self, strata)
self._strata = strata
return self
end
--- Sets the scale of the frame.
-- @tparam Frame self The frame object
-- @tparam string scale The frame scale
-- @treturn Frame The frame object
function Frame.SetScale(self, scale)
self._scale = scale
return self
end
--- Sets the layout of the frame.
-- @tparam Frame self The frame object
-- @tparam string layout The frame layout (`NONE`, `HORIZONTAL`, `VERTICAL`, or `FLOW`)
-- @treturn Frame The frame object
function Frame.SetLayout(self, layout)
assert(VALID_LAYOUTS[layout], format("Invalid layout (%s)", tostring(layout)))
self._layout = layout
return self
end
--- Sets whether mouse interaction is enabled.
-- @tparam Frame self The frame object
-- @tparam boolean enabled Whether mouse interaction is enabled
-- @treturn Frame The frame object
function Frame.SetMouseEnabled(self, enabled)
self:_GetBaseFrame():EnableMouse(enabled)
return self
end
--- Sets whether mouse wheel interaction is enabled.
-- @tparam Frame self The frame object
-- @tparam boolean enabled Whether mouse wheel interaction is enabled
-- @treturn Frame The frame object
function Frame.SetMouseWheelEnabled(self, enabled)
self:_GetBaseFrame():EnableMouseWheel(enabled)
return self
end
--- Allows dragging of the frame.
-- @tparam Frame self The frame object
-- @tparam string button The button to support dragging with
-- @treturn Frame The frame object
function Frame.RegisterForDrag(self, button)
self:SetMouseEnabled(button and true or false)
self:_GetBaseFrame():RegisterForDrag(button)
return self
end
--- Gets whether the mouse is currently over the frame.
-- @tparam Frame self The frame object
-- @treturn boolean Whether or not the mouse is over the frame
function Frame.IsMouseOver(self)
return self:_GetBaseFrame():IsMouseOver()
end
--- Sets the hit rectangle insets.
-- @tparam Frame self The frame object
-- @tparam number left The left hit rectangle inset
-- @tparam number right The right hit rectangle inset
-- @tparam number top The top hit rectangle inset
-- @tparam number bottom The bottom hit rectangle inset
-- @treturn Frame The frame object
function Frame.SetHitRectInsets(self, left, right, top, bottom)
self:_GetBaseFrame():SetHitRectInsets(left, right, top, bottom)
return self
end
--- Makes the element movable and starts moving it.
-- @tparam Frame self The element object
function Frame.StartMoving(self)
self:_GetBaseFrame():SetMovable(true)
self:_GetBaseFrame():StartMoving()
return self
end
--- Stops moving the element, and makes it unmovable.
-- @tparam Frame self The element object
function Frame.StopMovingOrSizing(self)
self:_GetBaseFrame():StopMovingOrSizing()
self:_GetBaseFrame():SetMovable(false)
return self
end
function Frame.Draw(self)
local layout = self._layout
self.__super.__super:Draw()
local frame = self:_GetBaseFrame()
if self._backgroundColor then
self._backgroundNineSlice:SetStyle(self._roundedCorners and "rounded" or "solid", self._borderColor and self._borderSize or nil)
local color = Color.IsInstance(self._backgroundColor) and self._backgroundColor or Theme.GetColor(self._backgroundColor)
self._backgroundNineSlice:SetVertexColor(color:GetFractionalRGBA())
else
assert(not self._borderColor)
self._backgroundNineSlice:Hide()
end
if self._borderColor then
assert(self._backgroundColor)
self._borderNineSlice:SetStyle(self._roundedCorners and "rounded" or "solid")
local color = Color.IsInstance(self._borderColor) and self._borderColor or Theme.GetColor(self._borderColor)
self._borderNineSlice:SetVertexColor(color:GetFractionalRGBA())
else
self._borderNineSlice:Hide()
end
frame:SetScale(self._scale)
local strata = self._strata
if strata then
frame:SetFrameStrata(strata)
end
if layout == "NONE" then
-- pass
elseif layout == "FLOW" then
local width = self:_GetDimension("WIDTH")
local height = self:_GetDimension("HEIGHT") - self:_GetPadding("TOP") - self:_GetPadding("BOTTOM")
local rowHeight = 0
for _, child in self:LayoutChildrenIterator() do
child:_GetBaseFrame():ClearAllPoints()
local childPrimary = child:_GetMinimumDimension("WIDTH")
child:_SetDimension("WIDTH", childPrimary)
local childSecondary = child:_GetMinimumDimension("HEIGHT")
rowHeight = childSecondary + child:_GetMargin("BOTTOM") + child:_GetMargin("TOP")
child:_SetDimension("HEIGHT", childSecondary)
end
local xOffset = self:_GetPadding("LEFT")
-- calculate the Y offset to properly position stuff with the padding of this frame taken into account
local yOffset = -self:_GetPadding("TOP")
for _, child in self:LayoutChildrenIterator() do
local childFrame = child:_GetBaseFrame()
local childWidth = childFrame:GetWidth() + child:_GetMargin("LEFT") + child:_GetMargin("RIGHT")
if xOffset + childWidth + self:_GetPadding("RIGHT") > width then
-- move to the next row
xOffset = self:_GetPadding("LEFT")
yOffset = yOffset - rowHeight
end
local childYOffset = yOffset + (height - childFrame:GetHeight()) / 2 - child:_GetMargin("TOP")
childFrame:SetPoint("LEFT", xOffset + child:_GetMargin("LEFT"), childYOffset)
xOffset = xOffset + childWidth
end
else
local context = LAYOUT_CONTEXT[layout]
assert(context)
local primary = self:_GetDimension(context.primaryDimension) - self:_GetPadding(context.sides.primary[1]) - self:_GetPadding(context.sides.primary[2])
local secondary = self:_GetDimension(context.secondaryDimension) - self:_GetPadding(context.sides.secondary[1]) - self:_GetPadding(context.sides.secondary[2])
local expandChildren = TempTable.Acquire()
local preferredChildren = TempTable.Acquire()
for _, child in self:LayoutChildrenIterator() do
child:_GetBaseFrame():ClearAllPoints()
local childPrimary, childPrimaryCanExpand = child:_GetMinimumDimension(context.primaryDimension)
if childPrimaryCanExpand then
local childPreferredPrimary = child:_GetPreferredDimension(context.primaryDimension)
if childPreferredPrimary then
assert(childPreferredPrimary > childPrimary, "Invalid preferred dimension")
preferredChildren[child] = childPreferredPrimary
else
expandChildren[child] = childPrimary
end
else
child:_SetDimension(context.primaryDimension, childPrimary)
end
primary = primary - childPrimary - child:_GetMargin(context.sides.primary[1]) - child:_GetMargin(context.sides.primary[2])
local childSecondary, childSecondaryCanExpand = child:_GetMinimumDimension(context.secondaryDimension)
childSecondary = min(childSecondary, secondary)
if childSecondaryCanExpand and childSecondary < secondary then
childSecondary = secondary
end
child:_SetDimension(context.secondaryDimension, childSecondary - child:_GetMargin(context.sides.secondary[1]) - child:_GetMargin(context.sides.secondary[2]))
end
for child, preferredPrimary in pairs(preferredChildren) do
local childPrimary = min(primary, preferredPrimary)
child:_SetDimension(context.primaryDimension, childPrimary)
primary = primary - (childPrimary - child:_GetMinimumDimension(context.primaryDimension))
end
local numExpandChildren = Table.Count(expandChildren)
for child, childPrimary in pairs(expandChildren) do
childPrimary = max(childPrimary, childPrimary + primary / numExpandChildren)
child:_SetDimension(context.primaryDimension, childPrimary)
end
TempTable.Release(expandChildren)
TempTable.Release(preferredChildren)
if layout == "HORIZONTAL" then
local xOffset = self:_GetPadding("LEFT")
-- calculate the Y offset to properly position stuff with the padding of this frame taken into account
local yOffset = (self:_GetPadding("BOTTOM") - self:_GetPadding("TOP")) / 2
for _, child in self:LayoutChildrenIterator() do
local childFrame = child:_GetBaseFrame()
xOffset = xOffset + child:_GetMargin("LEFT")
local childYOffset = (child:_GetMargin("BOTTOM") - child:_GetMargin("TOP")) / 2
childFrame:SetPoint("LEFT", xOffset, childYOffset + yOffset)
xOffset = xOffset + childFrame:GetWidth() + child:_GetMargin("RIGHT")
end
elseif layout == "VERTICAL" then
local yOffset = -self:_GetPadding("TOP")
-- calculate the X offset to properly position stuff with the padding of this frame taken into account
local xOffset = (self:_GetPadding("LEFT") - self:_GetPadding("RIGHT")) / 2
for _, child in self:LayoutChildrenIterator() do
local childFrame = child:_GetBaseFrame()
yOffset = yOffset - child:_GetMargin("TOP")
local childXOffset = (child:_GetMargin("LEFT") - child:_GetMargin("RIGHT")) / 2
childFrame:SetPoint("TOP", childXOffset + xOffset, yOffset)
yOffset = yOffset - childFrame:GetHeight() - child:_GetMargin("BOTTOM")
end
else
error()
end
end
for _, child in self:LayoutChildrenIterator() do
child:Draw()
end
for _, child in ipairs(self._noLayoutChildren) do
child:Draw()
end
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function Frame._GetMinimumDimension(self, dimension)
assert(dimension == "WIDTH" or dimension == "HEIGHT")
local styleResult = nil
if dimension == "WIDTH" then
styleResult = self._width
elseif dimension == "HEIGHT" then
styleResult = self._height
else
error("Invalid dimension: "..tostring(dimension))
end
local layout = self._layout
local context = LAYOUT_CONTEXT[layout]
if styleResult then
return styleResult, false
elseif self:GetNumLayoutChildren() == 0 or layout == "NONE" then
return 0, true
elseif layout == "FLOW" then
-- calculate our minimum width which is the largest of the widths of the children
local minWidth = 0
for _, child in self:LayoutChildrenIterator() do
local childMin = child:_GetMinimumDimension("WIDTH")
childMin = childMin + child:_GetMargin("LEFT") + child:_GetMargin("RIGHT")
minWidth = max(minWidth, childMin)
end
minWidth = minWidth + self:_GetPadding("LEFT") + self:_GetPadding("RIGHT")
if dimension == "WIDTH" then
return minWidth, true
end
-- calculate the row height (all children should be the exact same height)
local rowHeight = nil
for _, child in self:LayoutChildrenIterator() do
local childMin, childCanExpand = child:_GetMinimumDimension("HEIGHT")
childMin = childMin + child:_GetMargin("TOP") + child:_GetMargin("BOTTOM")
rowHeight = rowHeight or childMin
assert(childMin == rowHeight and not childCanExpand)
end
rowHeight = rowHeight or 0
local parentElement = self:GetParentElement()
local parentWidth = parentElement:_GetDimension("WIDTH") - parentElement:_GetPadding("LEFT") - parentElement:_GetPadding("RIGHT")
if minWidth > parentWidth then
-- we won't fit, so just pretend we're a single row
return rowHeight, false
end
-- calculate our height based on our parent's width
local height = rowHeight
local currentRowWidth = 0
for _, child in self:LayoutChildrenIterator() do
local childWidth = child:_GetMinimumDimension("WIDTH") + child:_GetMargin("LEFT") + child:_GetMargin("RIGHT")
if currentRowWidth + childWidth > parentWidth then
-- this child will go on the next row
height = height + rowHeight
currentRowWidth = childWidth
else
-- this child fits on the current row
currentRowWidth = currentRowWidth + childWidth
end
end
return height, false
elseif context then
-- calculate the dimension based on the children
local sides = (dimension == context.primaryDimension) and context.sides.primary or context.sides.secondary
local result = 0
local canExpand = false
for _, child in self:LayoutChildrenIterator() do
local childMin, childCanExpand = child:_GetMinimumDimension(dimension)
childMin = childMin + child:_GetMargin(sides[1]) + child:_GetMargin(sides[2])
canExpand = canExpand or childCanExpand
if dimension == context.primaryDimension then
result = result + childMin
else
result = max(result, childMin)
end
end
result = result + self:_GetPadding(sides[1]) + self:_GetPadding(sides[2])
return result, self._expandWidth or canExpand
else
error(format("Invalid layout (%s)", tostring(layout)))
end
end

615
Core/UI/Elements/Graph.lua Normal file
View File

@@ -0,0 +1,615 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Graph UI Element Class.
-- The graph element allows for generating line graphs. It is a subclass of the @{Element} class.
-- @classmod Graph
local _, TSM = ...
local Math = TSM.Include("Util.Math")
local Theme = TSM.Include("Util.Theme")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local Graph = TSM.Include("LibTSMClass").DefineClass("Graph", TSM.UI.Element)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(Graph)
TSM.UI.Graph = Graph
local private = {}
local PLOT_X_LABEL_WIDTH = 48
local PLOT_X_LABEL_HEIGHT = 16
local PLOT_X_LABEL_MARGIN = 6
local PLOT_Y_LABEL_WIDTH = 48
local PLOT_Y_LABEL_HEIGHT = 16
local PLOT_Y_LABEL_MARGIN = 4
local PLOT_HIGHLIGHT_TEXT_WIDTH = 80
local PLOT_HIGHLIGHT_TEXT_HEIGHT = 16
local PLOT_X_EXTRA_HIT_RECT = 4
local PLOT_Y_MARGIN = 4
local LINE_THICKNESS = 1
local LINE_THICKNESS_RATIO = 16
local PLOT_MIN_X_LINE_SPACING = PLOT_X_LABEL_WIDTH * 1.5 + 8
local PLOT_MIN_Y_LINE_SPACING = PLOT_Y_LABEL_HEIGHT * 1.5 + 8
local HOVER_LINE_THICKNESS = 1
local MAX_FILL_ALPHA = 0.5
local SELECTION_ALPHA = 0.2
local MAX_PLOT_POINTS = 300
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function Graph.__init(self)
local frame = UIElements.CreateFrame(self, "Frame", nil, nil, TSM.IsShadowlands() and "BackdropTemplate" or nil)
self.__super:__init(frame)
frame:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" })
frame.plot = CreateFrame("Frame", nil, frame, nil)
frame.plot:SetPoint("BOTTOMLEFT", PLOT_Y_LABEL_WIDTH, PLOT_X_LABEL_HEIGHT)
frame.plot:SetPoint("TOPRIGHT", -PLOT_X_EXTRA_HIT_RECT, -PLOT_HIGHLIGHT_TEXT_HEIGHT - PLOT_Y_MARGIN)
frame.plot:SetHitRectInsets(-PLOT_X_EXTRA_HIT_RECT, -PLOT_X_EXTRA_HIT_RECT, 0, 0)
frame.plot:EnableMouse(true)
ScriptWrapper.Set(frame.plot, "OnEnter", private.PlotFrameOnEnter, self)
ScriptWrapper.Set(frame.plot, "OnLeave", private.PlotFrameOnLeave, self)
ScriptWrapper.Set(frame.plot, "OnMouseDown", private.PlotFrameOnMouseDown, self)
ScriptWrapper.Set(frame.plot, "OnMouseUp", private.PlotFrameOnMouseUp, self)
frame.plot.dot = frame.plot:CreateTexture(nil, "ARTWORK", nil, 3)
TSM.UI.TexturePacks.SetTextureAndSize(frame.plot.dot, "uiFrames.HighlightDot")
frame.plot.hoverLine = frame.plot:CreateTexture(nil, "ARTWORK", nil, 2)
frame.plot.hoverLine:SetWidth(HOVER_LINE_THICKNESS)
frame.plot.hoverLine:Hide()
frame.plot.hoverText = frame.plot:CreateFontString()
frame.plot.hoverText:SetSize(PLOT_HIGHLIGHT_TEXT_WIDTH, PLOT_HIGHLIGHT_TEXT_HEIGHT)
frame.plot.hoverText:Hide()
frame.plot.selectionBox = frame.plot:CreateTexture(nil, "ARTWORK", nil, 2)
frame.plot.selectionBox:Hide()
self._usedTextures = {}
self._freeTextures = {}
self._usedFontStrings = {}
self._freeFontStrings = {}
self._xValuesFiltered = {}
self._yLookup = {}
self._yValueFunc = nil
self._xFormatFunc = nil
self._yFormatFunc = nil
self._xStepFunc = nil
self._yStepFunc = nil
self._xMin = nil
self._xMax = nil
self._yMin = nil
self._yMax = nil
self._isMouseOver = false
self._selectionStartX = nil
self._zoomStart = nil
self._zoomEnd = nil
self._onZoomChanged = nil
self._onHoverUpdate = nil
end
function Graph.Release(self)
self:_ReleaseAllTextures()
self:_ReleaseAllFontStrings()
wipe(self._xValuesFiltered)
wipe(self._yLookup)
self._yValueFunc = nil
self._xFormatFunc = nil
self._yFormatFunc = nil
self._xStepFunc = nil
self._yStepFunc = nil
self._xMin = nil
self._xMax = nil
self._yMin = nil
self._yMax = nil
self._isMouseOver = false
self._selectionStartX = nil
self._zoomStart = nil
self._zoomEnd = nil
self._onZoomChanged = nil
self._onHoverUpdate = nil
self.__super:Release()
end
--- Sets the step size of the axes.
-- @tparam Graph self The graph object
-- @tparam function x A function which gets the next x-axis step value
-- @tparam function y A function which gets the next y-axis step value
-- @treturn Graph The graph object
function Graph.SetAxisStepFunctions(self, x, y)
self._xStepFunc = x
self._yStepFunc = y
return self
end
function Graph.SetXRange(self, xMin, xMax, stepInterval)
assert(xMin <= xMax)
self._xMin = xMin
self._xMax = xMax
self._xStepInterval = stepInterval
self._zoomStart = xMin
self._zoomEnd = xMax
return self
end
function Graph.SetZoom(self, zoomStart, zoomEnd)
self._zoomStart = zoomStart
self._zoomEnd = zoomEnd
return self
end
function Graph.GetZoom(self)
return self._zoomStart, self._zoomEnd
end
function Graph.GetXRange(self)
local yMin, yMax = nil, nil
for _, x in ipairs(self._xValuesFiltered) do
local y = self._yValueFunc(x)
yMin = min(yMin or math.huge, y)
yMax = max(yMax or -math.huge, y)
end
return self._xMin, self._xMax
end
function Graph.GetYRange(self)
local yMin, yMax = nil, nil
for _, x in ipairs(self._xValuesFiltered) do
local y = self._yValueFunc(x)
yMin = min(yMin or math.huge, y)
yMax = max(yMax or -math.huge, y)
end
return yMin, yMax
end
function Graph.SetYValueFunction(self, func)
self._yValueFunc = func
return self
end
--- Sets functions for formatting values.
-- @tparam Graph self The graph object
-- @tparam function xFormatFunc A function which is passed an x value and returns a formatted string
-- @tparam function yFormatFunc A function which is passed a y value and returns a formatted string
-- @treturn Graph The graph object
function Graph.SetFormatFunctions(self, xFormatFunc, yFormatFunc)
self._xFormatFunc = xFormatFunc
self._yFormatFunc = yFormatFunc
return self
end
--- Registers a script handler.
-- @tparam ScrollingTable self The graph object
-- @tparam string script The script to register for (supported scripts: `OnZoomChanged`)
-- @tparam function handler The script handler which will be called with the graph object followed by any
-- arguments to the script
-- @treturn Graph The graph object
function Graph.SetScript(self, script, handler)
if script == "OnZoomChanged" then
self._onZoomChanged = handler
elseif script == "OnHoverUpdate" then
self._onHoverUpdate = handler
else
error("Unknown Graph script: "..tostring(script))
end
return self
end
function Graph.Draw(self)
self.__super:Draw()
self:_ReleaseAllTextures()
self:_ReleaseAllFontStrings()
local frame = self:_GetBaseFrame()
frame:SetBackdropColor(Theme.GetColor("PRIMARY_BG"):GetFractionalRGBA())
local plot = frame.plot
plot.hoverText:SetFont(Theme.GetFont("TABLE_TABLE1"):GetWowFont())
plot.hoverText:SetTextColor(Theme.GetColor("INDICATOR_ALT"):GetFractionalRGBA())
local plotWidth = plot:GetWidth()
local plotHeight = plot:GetHeight()
-- update the filtered set of x values to show and the bounds of the plot data
self:_PopulateFilteredData(plotWidth)
-- calculate the min and max y values which should be shown
self._yMin, self._yMax = self._yStepFunc("RANGE", self._yMin, self._yMax, floor(plotHeight / PLOT_MIN_Y_LINE_SPACING))
if Math.IsNan(self._yMax) then
-- this happens when we're resizing the application frame
return
end
-- draw the y axis lines and labels
local prevYAxisOffset = -math.huge
local yAxisValue = self._yMin
while yAxisValue <= self._yMax do
local yAxisOffset = Math.Scale(yAxisValue, self._yMin, self._yMax, 0, plotHeight)
if not prevYAxisOffset or (yAxisOffset - prevYAxisOffset) >= PLOT_MIN_Y_LINE_SPACING then
self:_DrawYAxisLine(yAxisOffset, yAxisValue, plotWidth, plotHeight)
prevYAxisOffset = yAxisOffset
end
yAxisValue = self._yStepFunc("NEXT", yAxisValue, self._yMax)
end
-- draw the x axis lines and labels
local xSuggestedStep = Math.Scale(PLOT_MIN_X_LINE_SPACING, 0, plotWidth, 0, self._zoomEnd - self._zoomStart)
local prevXAxisOffset = -math.huge
local xAxisValue = self._xStepFunc(self._zoomStart, xSuggestedStep)
while xAxisValue <= self._zoomEnd do
local xAxisOffset = Math.Scale(xAxisValue, self._zoomStart, self._zoomEnd, 0, plotWidth)
if not prevXAxisOffset or (xAxisOffset - prevXAxisOffset) > PLOT_MIN_X_LINE_SPACING then
self:_DrawXAxisLine(xAxisOffset, xAxisValue, plotWidth, plotHeight, xSuggestedStep)
prevXAxisOffset = xAxisOffset
end
xAxisValue = self._xStepFunc(xAxisValue, xSuggestedStep)
end
-- draw all the lines
local color = nil
if self._isMouseOver or self._selectionStartX then
color = Theme.GetColor("INDICATOR_ALT")
elseif self._yLookup[self._xValuesFiltered[1]] <= self._yLookup[self._xValuesFiltered[#self._xValuesFiltered]] then
color = Theme.GetFeedbackColor("GREEN")
else
color = Theme.GetFeedbackColor("RED")
end
local xPrev, yPrev = nil, nil
for _, x in ipairs(self._xValuesFiltered) do
local y = self._yLookup[x]
local xCoord = Math.Scale(x, self._zoomStart, self._zoomEnd, 0, plotWidth)
local yCoord = Math.Scale(y, self._yMin, self._yMax, 0, plotHeight)
if xPrev then
self:_DrawFillLine(xPrev, yPrev, xCoord, yCoord, LINE_THICKNESS, plotHeight, color)
end
xPrev = xCoord
yPrev = yCoord
end
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function Graph._PopulateFilteredData(self, plotWidth)
wipe(self._xValuesFiltered)
wipe(self._yLookup)
self._yMin = math.huge
self._yMax = -math.huge
local minStep = Math.Ceil((self._zoomEnd - self._zoomStart) / min(plotWidth / 3, MAX_PLOT_POINTS), self._xStepInterval)
local x = self._zoomStart
while x <= self._zoomEnd do
local prevX = self._xValuesFiltered[#self._xValuesFiltered]
if not prevX or x == self._zoomEnd or (x - prevX > minStep and self._zoomEnd - x > minStep) then
-- this is either the first / last point or a middle point which is sufficiently far from the previous and last points
tinsert(self._xValuesFiltered, x)
local y = self._yValueFunc(x)
self._yMin = min(self._yMin, y)
self._yMax = max(self._yMax, y)
self._yLookup[x] = y
end
if x == self._zoomEnd then
break
end
x = min(x + minStep, self._zoomEnd)
end
end
function Graph._DrawYAxisLine(self, yOffset, yValue, plotWidth, plotHeight, ySuggestedStep)
local line = self:_AcquireLine("ARTWORK")
local thickness = LINE_THICKNESS
local textureHeight = thickness * LINE_THICKNESS_RATIO
-- trim the texture a bit on the left/right since it's not completely filled to the edges which is noticeable on long lines
line:SetTexCoord(0.1, 1, 0.1, 0, 0.9, 1, 0.9, 0)
line:SetPoint("BOTTOMLEFT", 0 - thickness / 2, yOffset - textureHeight / 2)
line:SetPoint("TOPRIGHT", line:GetParent(), "BOTTOMLEFT", plotWidth + thickness / 2, yOffset + textureHeight / 2)
line:SetVertexColor(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())
line:SetDrawLayer("BACKGROUND", 0)
local text = self:_AcquireFontString(Theme.GetFont("TABLE_TABLE1"))
text:SetJustifyH("RIGHT")
local textYOffset = 0
if PLOT_Y_LABEL_HEIGHT / 2 > yOffset then
text:SetJustifyV("BOTTOM")
textYOffset = max(PLOT_Y_LABEL_HEIGHT / 2 - yOffset, 0)
elseif yOffset + PLOT_Y_LABEL_HEIGHT / 2 > plotHeight then
text:SetJustifyV("TOP")
textYOffset = plotHeight - yOffset - PLOT_Y_LABEL_HEIGHT / 2
else
text:SetJustifyV("MIDDLE")
end
text:SetPoint("RIGHT", line, "LEFT", -PLOT_Y_LABEL_MARGIN, textYOffset)
text:SetSize(PLOT_Y_LABEL_WIDTH, PLOT_Y_LABEL_HEIGHT)
text:SetText(self._yFormatFunc(yValue, ySuggestedStep))
end
function Graph._DrawXAxisLine(self, xOffset, xValue, plotWidth, plotHeight, xSuggestedStep)
local line = self:_AcquireLine("ARTWORK")
local thickness = LINE_THICKNESS
local textureHeight = thickness * LINE_THICKNESS_RATIO
-- trim the texture a bit on the left/right since it's not completely filled to the edges which is noticeable on long lines
line:SetTexCoord(0.9, 1, 0.1, 1, 0.9, 0, 0.1, 0)
line:SetPoint("BOTTOMLEFT", xOffset - textureHeight / 2, thickness / 2)
line:SetPoint("TOPRIGHT", line:GetParent(), "BOTTOMLEFT", xOffset + textureHeight / 2, plotHeight + thickness / 2)
line:SetVertexColor(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())
line:SetDrawLayer("BACKGROUND", 0)
local text = self:_AcquireFontString(Theme.GetFont("BODY_BODY3_MEDIUM"))
text:ClearAllPoints()
text:SetJustifyV("TOP")
local textXOffset = 0
if PLOT_X_LABEL_WIDTH / 2 > xOffset then
text:SetJustifyH("LEFT")
textXOffset = max(PLOT_X_LABEL_WIDTH / 2 - xOffset, 0)
elseif xOffset + PLOT_X_LABEL_WIDTH / 2 > plotWidth then
text:SetJustifyH("RIGHT")
textXOffset = plotWidth - xOffset - PLOT_X_LABEL_WIDTH / 2
else
text:SetJustifyH("CENTER")
end
text:SetPoint("TOP", line, "BOTTOM", textXOffset, -PLOT_X_LABEL_MARGIN)
text:SetSize(PLOT_X_LABEL_WIDTH, PLOT_X_LABEL_HEIGHT)
text:SetText(self._xFormatFunc(xValue, xSuggestedStep))
end
function Graph._DrawFillLine(self, xFrom, yFrom, xTo, yTo, thickness, plotHeight, color)
assert(xFrom <= xTo)
local line = self:_AcquireLine("ARTWORK")
local textureHeight = thickness * LINE_THICKNESS_RATIO
local xDiff = xTo - xFrom
local yDiff = yTo - yFrom
local length = sqrt(xDiff * xDiff + yDiff * yDiff)
local sinValue = -yDiff / length
local cosValue = xDiff / length
local sinCosValue = sinValue * cosValue
local aspectRatio = length / textureHeight
local invAspectRatio = textureHeight / length
-- calculate and set tex coords
local LLx, LLy, ULx, ULy, URx, URy, LRx, LRy = nil, nil, nil, nil, nil, nil, nil, nil
if yDiff >= 0 then
LLx = invAspectRatio * sinCosValue
LLy = sinValue * sinValue
LRy = aspectRatio * sinCosValue
LRx = 1 - LLy
ULx = LLy
ULy = 1 - LRy
URx = 1 - LLx
URy = LRx
else
LLx = sinValue * sinValue
LLy = -aspectRatio * sinCosValue
LRx = 1 + invAspectRatio * sinCosValue
LRy = LLx
ULx = 1 - LRx
ULy = 1 - LLx
URy = 1 - LLy
URx = ULy
end
line:SetTexCoord(ULx, ULy, LLx, LLy, URx, URy, LRx, LRy)
-- calculate and set texture anchors
local xCenter = (xFrom + xTo) / 2
local yCenter = (yFrom + yTo) / 2
local halfWidth = (xDiff + invAspectRatio * abs(yDiff) + thickness) / 2
local halfHeight = (abs(yDiff) + invAspectRatio * xDiff + thickness) / 2
line:SetPoint("BOTTOMLEFT", xCenter - halfWidth, yCenter - halfHeight)
line:SetPoint("TOPRIGHT", line:GetParent(), "BOTTOMLEFT", xCenter + halfWidth, yCenter + halfHeight)
local minY = min(yFrom, yTo)
local maxY = max(yFrom, yTo)
local r, g, b, a = color:GetFractionalRGBA()
local barMaxAlpha = Math.Scale(minY, 0, plotHeight, 0, MAX_FILL_ALPHA * a)
local topMaxAlpha = Math.Scale(maxY, 0, plotHeight, 0, MAX_FILL_ALPHA * a)
line:SetVertexColor(r, g, b, a)
local fillTop = self:_AcquireTexture("ARTWORK", -1)
fillTop:SetTexture("Interface\\AddOns\\TradeSkillMaster\\Media\\triangle")
if yFrom < yTo then
fillTop:SetTexCoord(0, 0, 0, 1, 1, 0, 1, 1)
else
fillTop:SetTexCoord(1, 0, 1, 1, 0, 0, 0, 1)
end
fillTop:SetGradientAlpha("VERTICAL", r, g, b, barMaxAlpha, r, g, b, topMaxAlpha)
fillTop:SetPoint("BOTTOMLEFT", xFrom, minY)
fillTop:SetPoint("TOPRIGHT", fillTop:GetParent(), "BOTTOMLEFT", xTo, maxY)
local fillBar = self:_AcquireTexture("ARTWORK", -1)
fillBar:SetTexture("Interface\\Buttons\\WHITE8X8")
fillBar:SetGradientAlpha("VERTICAL", r, g, b, 0, r, g, b, barMaxAlpha)
fillBar:SetPoint("BOTTOMLEFT", xFrom, 0)
fillBar:SetPoint("TOPRIGHT", fillBar:GetParent(), "BOTTOMLEFT", xTo, minY)
return line
end
function Graph._AcquireLine(self, layer, subLayer)
local line = self:_AcquireTexture(layer, subLayer)
line:SetTexture("Interface\\AddOns\\TradeSkillMaster\\Media\\line.tga")
return line
end
function Graph._AcquireTexture(self, layer, subLayer)
local plot = self:_GetBaseFrame().plot
local result = tremove(self._freeTextures) or plot:CreateTexture()
tinsert(self._usedTextures, result)
result:SetParent(plot)
result:Show()
result:SetDrawLayer(layer, subLayer)
return result
end
function Graph._ReleaseAllTextures(self)
while #self._usedTextures > 0 do
local texture = tremove(self._usedTextures)
texture:SetTexture(nil)
texture:SetVertexColor(0, 0, 0, 0)
texture:SetTexCoord(0, 0, 0, 1, 1, 0, 1, 1)
texture:SetWidth(0)
texture:SetHeight(0)
texture:ClearAllPoints()
texture:Hide()
tinsert(self._freeTextures, texture)
end
end
function Graph._AcquireFontString(self, font)
local plot = self:_GetBaseFrame().plot
local result = tremove(self._freeFontStrings) or plot:CreateFontString()
tinsert(self._usedFontStrings, result)
result:SetParent(plot)
result:Show()
result:SetFont(font:GetWowFont())
result:SetTextColor(Theme.GetColor("TEXT"):GetFractionalRGBA())
return result
end
function Graph._ReleaseAllFontStrings(self)
while #self._usedFontStrings > 0 do
local fontString = tremove(self._usedFontStrings)
fontString:SetWidth(0)
fontString:SetHeight(0)
fontString:ClearAllPoints()
fontString:Hide()
tinsert(self._freeFontStrings, fontString)
end
end
function Graph._GetCursorClosestPoint(self)
local plotFrame = self:_GetBaseFrame().plot
local xPos = GetCursorPosition() / plotFrame:GetEffectiveScale()
local fromMin = plotFrame:GetLeft()
local fromMax = plotFrame:GetRight()
-- Convert the cursor position to be relative to the plotted x values
xPos = Math.Scale(Math.Bound(xPos, fromMin, fromMax), fromMin, fromMax, self._zoomStart, self._zoomEnd)
-- Find the closest point to the cursor (based on the x distance)
local closestX, closestY = nil, nil
for _, x in ipairs(self._xValuesFiltered) do
local y = self._yLookup[x]
local xDist = abs(x - xPos)
if not closestX or xDist < abs(closestX - xPos) then
closestX = x
closestY = y
end
end
assert(closestY)
return closestX, closestY
end
function Graph._XValueToPlotCoord(self, xValue)
local plotFrame = self:_GetBaseFrame().plot
return Math.Scale(xValue, self._zoomStart, self._zoomEnd, 0, plotFrame:GetWidth())
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.PlotFrameOnEnter(self)
self._isMouseOver = true
self:Draw()
local plotFrame = self:_GetBaseFrame().plot
ScriptWrapper.Set(plotFrame, "OnUpdate", private.PlotFrameOnUpdate, self)
end
function private.PlotFrameOnLeave(self)
self._isMouseOver = false
end
function private.PlotFrameOnUpdate(self)
local plotFrame = self:_GetBaseFrame().plot
local closestX, closestY = self:_GetCursorClosestPoint()
local xCoord = self:_XValueToPlotCoord(closestX)
local yCoord = Math.Scale(closestY, self._yMin, self._yMax, 0, plotFrame:GetHeight())
if self._isMouseOver then
plotFrame.dot:Show()
plotFrame.dot:ClearAllPoints()
plotFrame.dot:SetPoint("CENTER", plotFrame, "BOTTOMLEFT", xCoord, yCoord)
plotFrame.hoverLine:Show()
plotFrame.hoverLine:SetColorTexture(Theme.GetColor("INDICATOR_ALT"):GetFractionalRGBA())
plotFrame.hoverLine:ClearAllPoints()
plotFrame.hoverLine:SetPoint("TOP", plotFrame, "TOPLEFT", xCoord, 0)
plotFrame.hoverLine:SetPoint("BOTTOM", plotFrame, "BOTTOMLEFT", xCoord, 0)
plotFrame.hoverText:Show()
plotFrame.hoverText:SetWidth(1000)
plotFrame.hoverText:SetText(self._yFormatFunc(closestY, nil, true))
local textWidth = plotFrame.hoverText:GetStringWidth()
plotFrame.hoverText:SetWidth(textWidth)
plotFrame.hoverText:ClearAllPoints()
if xCoord - textWidth / 2 < 0 then
plotFrame.hoverText:SetPoint("BOTTOMLEFT", plotFrame, "TOPLEFT", 0, PLOT_Y_MARGIN)
elseif textWidth / 2 + xCoord > plotFrame:GetWidth() then
plotFrame.hoverText:SetPoint("BOTTOMRIGHT", plotFrame, "TOPRIGHT", 0, PLOT_Y_MARGIN)
else
plotFrame.hoverText:SetPoint("BOTTOM", plotFrame, "TOPLEFT", xCoord, PLOT_Y_MARGIN)
end
else
plotFrame.dot:Hide()
plotFrame.hoverLine:Hide()
plotFrame.hoverText:Hide()
end
if self._selectionStartX then
local startXCoord = self:_XValueToPlotCoord(self._selectionStartX)
local selectionMinX = min(startXCoord, xCoord)
local selectionMaxX = max(startXCoord, xCoord)
plotFrame.selectionBox:Show()
local r, g, b, a = Theme.GetColor("INDICATOR_ALT"):GetFractionalRGBA()
assert(a == 1)
plotFrame.selectionBox:SetColorTexture(r, g, b, SELECTION_ALPHA)
plotFrame.selectionBox:ClearAllPoints()
plotFrame.selectionBox:SetPoint("TOPLEFT", plotFrame, selectionMinX, 0)
plotFrame.selectionBox:SetPoint("BOTTOMRIGHT", plotFrame, "BOTTOMLEFT", selectionMaxX, 0)
else
plotFrame.selectionBox:Hide()
end
local isHovered = self._isMouseOver or self._selectionStartX
if not isHovered then
self:Draw()
ScriptWrapper.Clear(plotFrame, "OnUpdate")
end
if self._onHoverUpdate then
self:_onHoverUpdate(isHovered and closestX or nil)
end
end
function private.PlotFrameOnMouseDown(self, mouseButton)
if mouseButton ~= "LeftButton" then
return
end
assert(self._isMouseOver)
self._selectionStartX = self:_GetCursorClosestPoint()
end
function private.PlotFrameOnMouseUp(self, mouseButton)
if mouseButton ~= "LeftButton" then
return
end
local currentX = self:_GetCursorClosestPoint()
local startX = min(self._selectionStartX, currentX)
local endX = max(self._selectionStartX, currentX)
self._selectionStartX = nil
local plotFrame = self:_GetBaseFrame().plot
plotFrame.selectionBox:Hide()
if startX ~= endX and (startX ~= self._zoomStart or endX ~= self._zoomEnd) then
self._zoomStart = startX
self._zoomEnd = endX
self:Draw()
if self._onZoomChanged then
self:_onZoomChanged()
end
end
end

View File

@@ -0,0 +1,360 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Group Selector UI Element Class.
-- A group selector is an element which can be used to prompt the user to select a list of groups, usually for
-- filtering. It is a subclass of the @{Element} class.
-- @classmod GroupSelector
local _, TSM = ...
local L = TSM.Include("Locale").GetTable()
local Table = TSM.Include("Util.Table")
local Analytics = TSM.Include("Util.Analytics")
local Theme = TSM.Include("Util.Theme")
local NineSlice = TSM.Include("Util.NineSlice")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local UIElements = TSM.Include("UI.UIElements")
local GroupSelector = TSM.Include("LibTSMClass").DefineClass("GroupSelector", TSM.UI.Element)
UIElements.Register(GroupSelector)
TSM.UI.GroupSelector = GroupSelector
local private = {}
local TEXT_MARGIN = 8
local ICON_MARGIN = 8
local DEFAULT_CONTEXT = { selected = {}, collapsed = {} }
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function GroupSelector.__init(self)
local frame = UIElements.CreateFrame(self, "Button")
ScriptWrapper.Set(frame, "OnClick", private.OnClick, self)
self.__super:__init(frame)
frame.text = UIElements.CreateFontString(self, frame)
frame.text:SetPoint("TOPLEFT", TEXT_MARGIN, 0)
frame.text:SetPoint("BOTTOMRIGHT", -ICON_MARGIN - TSM.UI.TexturePacks.GetWidth("iconPack.18x18/Add/Default") - TEXT_MARGIN, 0)
frame.text:SetJustifyH("LEFT")
frame.text:SetJustifyV("MIDDLE")
frame.icon = frame:CreateTexture(nil, "ARTWORK")
frame.icon:SetPoint("RIGHT", -ICON_MARGIN, 0)
frame.iconBtn = CreateFrame("Button", nil, frame)
frame.iconBtn:SetAllPoints(frame.icon)
ScriptWrapper.Set(frame.iconBtn, "OnClick", private.OnIconClick, self)
self._nineSlice = NineSlice.New(frame)
self._groupTreeContext = CopyTable(DEFAULT_CONTEXT)
self._hintText = ""
self._selectedText = L["%d groups"]
self._singleSelection = nil
self._onSelectionChanged = nil
self._customQueryFunc = nil
self._showCreateNew = false
end
function GroupSelector.Release(self)
wipe(self._groupTreeContext.collapsed)
wipe(self._groupTreeContext.selected)
self._hintText = ""
self._selectedText = L["%d groups"]
self._singleSelection = nil
self._onSelectionChanged = nil
self._customQueryFunc = nil
self._showCreateNew = false
self.__super:Release()
end
--- Sets the hint text.
-- @tparam GroupSelector self The group selector object
-- @tparam string text The hint text
-- @treturn GroupSelector The group selector object
function GroupSelector.SetHintText(self, text)
assert(type(text) == "string")
self._hintText = text
return self
end
--- Sets the selected text.
-- @tparam GroupSelector self The group selector object
-- @tparam string text The selected text (with a %d formatter for the number of groups)
-- @treturn GroupSelector The group selector object
function GroupSelector.SetSelectedText(self, text)
assert(type(text) == "string" and strmatch(text, "%%d"))
self._selectedText = text
return self
end
--- Registers a script handler.
-- @tparam GroupSelector self The group selector object
-- @tparam string script The script to register for (supported scripts: `OnSelectionChanged`)
-- @tparam function handler The script handler which will be called with the group selector object followed by any
-- arguments to the script
-- @treturn GroupSelector The group selector object
function GroupSelector.SetScript(self, script, handler)
if script == "OnSelectionChanged" then
self._onSelectionChanged = handler
else
error("Unknown GroupSelector script: "..tostring(script))
end
return self
end
--- Sets a function to generate a custom query to use for the group tree
-- @tparam GroupSelector self The group selector object
-- @tparam function func A function to call to create the custom query (gets auto-released by the GroupTree)
-- @treturn GroupSelector The group selector object
function GroupSelector.SetCustomQueryFunc(self, func)
self._customQueryFunc = func
return self
end
--- Adds the "Create New Group" option to the group tree
-- @tparam GroupSelector self The group selector object
-- @treturn GroupSelector The group selector object
function GroupSelector.AddCreateNew(self)
self._showCreateNew = true
return self
end
--- Sets the selection to only handle single selection.
-- @tparam GroupSelector self The group selector object
-- @tparam boolean enabled The state of the single selection
-- @treturn GroupSelector The group selector object
function GroupSelector.SetSingleSelection(self, enabled)
self._singleSelection = enabled
return self
end
--- Returns the single selected group path.
-- @tparam GroupSelector self The group selector object
function GroupSelector.GetSelection(self)
assert(self._singleSelection)
return next(self._groupTreeContext.selected)
end
--- Sets the single selected group path.
-- @tparam GroupSelector self The group selector object
-- @tparam string|table selection The selected group(s) or nil if nothing should be selected
-- @treturn GroupSelector The group selector object
function GroupSelector.SetSelection(self, selection)
wipe(self._groupTreeContext.selected)
if not selection then
return self
end
if self._singleSelection then
self._groupTreeContext.selected[selection] = true
else
for groupPath in pairs(selection) do
self._groupTreeContext.selected[groupPath] = true
end
end
return self
end
--- Returns an iterator for all selected groups.
-- @tparam GroupSelector self The group selector object
-- @return An iterator which iterates over the selected groups and has the following values: `groupPath`
function GroupSelector.SelectedGroupIterator(self)
return pairs(self._groupTreeContext.selected)
end
--- Clears all selected groups.
-- @tparam GroupSelector self The group selector object
-- @tparam boolean silent Don't call the selection changed callback
-- @treturn GroupSelector The group selector object
function GroupSelector.ClearSelectedGroups(self, silent)
wipe(self._groupTreeContext.selected)
if not silent and self._onSelectionChanged then
self:_onSelectionChanged()
end
return self
end
function GroupSelector.Draw(self)
self.__super:Draw()
local frame = self:_GetBaseFrame()
frame.text:SetFont(Theme.GetFont("BODY_BODY2"):GetWowFont())
local numGroups = Table.Count(self._groupTreeContext.selected)
frame.text:SetText(numGroups == 0 and self._hintText or (self._singleSelection and TSM.Groups.Path.Format(next(self._groupTreeContext.selected)) or format(self._selectedText, numGroups)))
TSM.UI.TexturePacks.SetTextureAndSize(frame.icon, numGroups == 0 and "iconPack.18x18/Add/Default" or "iconPack.18x18/Close/Default")
self._nineSlice:SetStyle("rounded")
self._nineSlice:SetVertexColor(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function GroupSelector._CreateQuery(self)
local query = nil
if self._customQueryFunc then
query = self._customQueryFunc()
else
query = TSM.Groups.CreateQuery()
end
if self._singleSelection then
query:NotEqual("groupPath", TSM.CONST.ROOT_GROUP_PATH)
end
return query
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.OnClick(self)
self:GetBaseElement():ShowDialogFrame(UIElements.New("Frame", "frame", "DIALOG")
:SetLayout("VERTICAL")
:SetSize(464, 500)
:SetPadding(8)
:AddAnchor("CENTER")
:SetBackgroundColor("FRAME_BG", true)
:SetMouseEnabled(true)
:AddChild(UIElements.New("Frame", "header")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(0, 0, 0, 8)
:AddChild(UIElements.New("Text", "title")
:SetMargin(32, 8, 0, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetJustifyH("CENTER")
:SetText(L["Select Group"])
)
:AddChild(UIElements.New("Button", "closeBtn")
:SetBackgroundAndSize("iconPack.24x24/Close/Default")
:SetScript("OnClick", private.DialogCloseBtnOnClick)
)
)
:AddChild(UIElements.New("Frame", "container")
:SetLayout("VERTICAL")
:SetPadding(2)
:SetBackgroundColor("PRIMARY_BG")
:SetBorderColor("ACTIVE_BG")
:AddChild(UIElements.New("Frame", "header")
:SetLayout("HORIZONTAL")
:SetHeight(24)
:SetMargin(8)
:AddChild(UIElements.New("Input", "input")
:AllowItemInsert(true)
:SetIconTexture("iconPack.18x18/Search")
:SetClearButtonEnabled(true)
:SetHintText(L["Search Groups"])
:SetScript("OnValueChanged", private.DialogFilterOnValueChanged)
)
:AddChild(UIElements.New("Button", "expandAllBtn")
:SetSize(24, 24)
:SetMargin(8, 0, 0, 0)
:SetBackground("iconPack.18x18/Expand All")
:SetScript("OnClick", private.ExpandAllGroupsOnClick)
:SetTooltip(L["Expand / Collapse All Groups"])
)
:AddChildIf(not self._singleSelection, UIElements.New("Button", "selectAllBtn")
:SetSize(24, 24)
:SetMargin(8, 0, 0, 0)
:SetBackground("iconPack.18x18/Select All")
:SetScript("OnClick", private.SelectAllGroupsOnClick)
:SetTooltip(L["Select / Deselect All Groups"])
)
)
:AddChildIf(self._showCreateNew, UIElements.New("Button", "createGroup")
:SetHeight(24)
:SetMargin(8, 8, 0, 0)
:SetFont("BODY_BODY2_MEDIUM")
:SetJustifyH("LEFT")
:SetIcon("iconPack.14x14/Add/Circle", "LEFT")
:SetText(L["Create New Group"])
:SetScript("OnClick", private.CreateGroupOnClick)
)
:AddChild(UIElements.New(self._singleSelection and "SelectionGroupTree" or "ApplicationGroupTree", "groupTree")
:SetContext(self)
:SetContextTable(self._groupTreeContext, DEFAULT_CONTEXT)
:SetQuery(self:_CreateQuery())
)
)
:AddChild(UIElements.New("ActionButton", "groupBtn")
:SetHeight(24)
:SetMargin(0, 0, 8, 0)
:SetContext(self)
:SetText(L["Select Group"])
:SetScript("OnClick", private.DialogSelectOnClick)
)
)
end
function private.OnIconClick(self)
if Table.Count(self._groupTreeContext.selected) > 0 then
self:ClearSelectedGroups()
self:Draw()
if self._onSelectionChanged then
self:_onSelectionChanged()
end
else
private.OnClick(self)
end
end
function private.DialogCloseBtnOnClick(button)
local self = button:GetElement("__parent.__parent.groupBtn"):GetContext()
button:GetBaseElement():HideDialog()
self:Draw()
if self._onSelectionChanged then
self:_onSelectionChanged()
end
end
function private.DialogFilterOnValueChanged(input)
input:GetElement("__parent.__parent.groupTree")
:SetSearchString(strlower(input:GetValue()))
:Draw()
end
function private.ExpandAllGroupsOnClick(button)
button:GetElement("__parent.__parent.groupTree")
:ToggleExpandAll()
end
function private.SelectAllGroupsOnClick(button)
button:GetElement("__parent.__parent.groupTree")
:ToggleSelectAll()
end
function private.DialogSelectOnClick(button)
local self = button:GetContext()
button:GetBaseElement():HideDialog()
self:Draw()
if self._onSelectionChanged then
self:_onSelectionChanged()
end
end
function private.CreateGroupOnClick(button)
local newGroupPath = L["New Group"]
if TSM.Groups.Exists(newGroupPath) then
local num = 1
while TSM.Groups.Exists(newGroupPath.." "..num) do
num = num + 1
end
newGroupPath = newGroupPath.." "..num
end
TSM.Groups.Create(newGroupPath)
Analytics.Action("CREATED_GROUP", newGroupPath)
button:GetElement("__parent.groupTree")
:UpdateData()
:SetSelection(newGroupPath)
:Draw()
end

View File

@@ -0,0 +1,345 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- GroupTree UI Element Class.
-- A group tree is an abstract element which displays TSM groups. It is a subclass of the @{ScrollingTable} class.
-- @classmod GroupTree
local _, TSM = ...
local L = TSM.Include("Locale").GetTable()
local TempTable = TSM.Include("Util.TempTable")
local String = TSM.Include("Util.String")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local Theme = TSM.Include("Util.Theme")
local UIElements = TSM.Include("UI.UIElements")
local GroupTree = TSM.Include("LibTSMClass").DefineClass("GroupTree", TSM.UI.ScrollingTable, "ABSTRACT")
UIElements.Register(GroupTree)
TSM.UI.GroupTree = GroupTree
local private = {}
local EXPANDER_SPACING = 2
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function GroupTree.__init(self)
self.__super:__init()
self:SetRowHeight(24)
self._allData = {}
self._contextTable = nil
self._defaultContextTable = nil
self._hasChildrenLookup = {}
self._query = nil
self._searchStr = ""
self._moduleOperationFilter = nil
end
function GroupTree.Acquire(self)
self._headerHidden = true
self.__super:Acquire()
self:GetScrollingTableInfo()
:NewColumn("group")
:SetFont("BODY_BODY2")
:SetJustifyH("LEFT")
:SetTextFunction(private.GetGroupText)
:SetExpanderStateFunction(private.GetExpanderState)
:SetFlagStateFunction(private.GetFlagState)
:SetTooltipFunction(private.GetTooltip)
:Commit()
:Commit()
end
function GroupTree.Release(self)
wipe(self._allData)
if self._query then
self._query:Release()
self._query = nil
end
self._searchStr = ""
self._moduleOperationFilter = nil
self._contextTable = nil
self._defaultContextTable = nil
wipe(self._hasChildrenLookup)
for _, row in ipairs(self._rows) do
ScriptWrapper.Clear(row._frame, "OnDoubleClick")
end
self.__super:Release()
self:SetRowHeight(24)
end
--- Sets the context table.
-- This table can be used to preserve collapsed state across lifecycles of the group tree and even WoW sessions if it's
-- within the settings DB.
-- @tparam GroupTree self The group tree object
-- @tparam table tbl The context table
-- @tparam table defaultTbl The default table (required fields: `collapsed`)
-- @treturn GroupTree The group tree object
function GroupTree.SetContextTable(self, tbl, defaultTbl)
assert(type(defaultTbl.collapsed) == "table")
tbl.collapsed = tbl.collapsed or CopyTable(defaultTbl.collapsed)
self._contextTable = tbl
self._defaultContextTable = defaultTbl
return self
end
--- Sets the context table from a settings object.
-- @tparam GroupTree self The group tree object
-- @tparam Settings settings The settings object
-- @tparam string key The setting key
-- @treturn GroupTree The group tree object
function GroupTree.SetSettingsContext(self, settings, key)
return self:SetContextTable(settings[key], settings:GetDefaultReadOnly(key))
end
--- Sets the query used to populate the group tree.
-- @tparam GroupTree self The group tree object
-- @tparam DatabaseQuery query The database query object
-- @tparam[opt=nil] string moduleName The name of the module to filter visible groups to only ones with operations
-- @treturn GroupTree The group tree object
function GroupTree.SetQuery(self, query, moduleName)
assert(query)
if self._query then
self._query:Release()
end
self._query = query
self._query:SetUpdateCallback(private.QueryUpdateCallback, self)
self._moduleOperationFilter = moduleName
self:UpdateData()
return self
end
function GroupTree.SetScript(self, script, handler)
-- GroupTree doesn't support any scripts
error("Unknown GroupTree script: "..tostring(script))
return self
end
--- Sets the search string.
-- This search string is used to filter the groups which are displayed in the group tree.
-- @tparam GroupTree self The group tree object
-- @tparam string searchStr The search string which filters the displayed groups
-- @treturn GroupTree The group tree object
function GroupTree.SetSearchString(self, searchStr)
self._searchStr = String.Escape(searchStr)
self:UpdateData()
return self
end
--- Expand every group.
-- @tparam GroupTree self The application group tree object
-- @treturn GroupTree The application group tree object
function GroupTree.ExpandAll(self)
for _, groupPath in ipairs(self._allData) do
if groupPath ~= TSM.CONST.ROOT_GROUP_PATH and self._hasChildrenLookup[groupPath] and self._contextTable.collapsed[groupPath] then
self:_SetCollapsed(groupPath, false)
end
end
self:UpdateData(true)
return self
end
--- Collapse every group.
-- @tparam GroupTree self The application group tree object
-- @treturn GroupTree The application group tree object
function GroupTree.CollapseAll(self)
for _, groupPath in ipairs(self._allData) do
if groupPath ~= TSM.CONST.ROOT_GROUP_PATH and self._hasChildrenLookup[groupPath] and not self._contextTable.collapsed[groupPath] then
self:_SetCollapsed(groupPath, true)
end
end
self:UpdateData(true)
return self
end
--- Toggle the expand/collapse all state of the group tree.
-- @tparam GroupTree self The application group tree object
-- @treturn GroupTree The application group tree object
function GroupTree.ToggleExpandAll(self)
if next(self._contextTable.collapsed) then
-- at least one group is collapsed, so expand everything
self:ExpandAll()
else
-- nothing is collapsed, so collapse everything
self:CollapseAll()
end
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function GroupTree._GetTableRow(self, isHeader)
local row = self.__super:_GetTableRow(isHeader)
if not isHeader then
ScriptWrapper.Set(row._frame, "OnDoubleClick", private.RowOnDoubleClick, row)
end
return row
end
function GroupTree._CanResizeCols(self)
return false
end
function GroupTree._UpdateData(self)
-- update our groups list
wipe(self._hasChildrenLookup)
wipe(self._allData)
wipe(self._data)
local groups = TempTable.Acquire()
if self._moduleOperationFilter then
local shouldKeep = TempTable.Acquire()
for _, row in self._query:Iterator() do
local groupPath = row:GetField("groupPath")
shouldKeep[groupPath] = row:GetField("has"..self._moduleOperationFilter.."Operation")
if shouldKeep[groupPath] then
shouldKeep[TSM.CONST.ROOT_GROUP_PATH] = true
-- add all parent groups to the keep table as well
local checkPath = TSM.Groups.Path.GetParent(groupPath)
while checkPath and checkPath ~= TSM.CONST.ROOT_GROUP_PATH do
shouldKeep[checkPath] = true
checkPath = TSM.Groups.Path.GetParent(checkPath)
end
end
end
for _, row in self._query:Iterator() do
local groupPath = row:GetField("groupPath")
if shouldKeep[groupPath] then
tinsert(groups, groupPath)
end
end
TempTable.Release(shouldKeep)
else
for _, row in self._query:Iterator() do
tinsert(groups, row:GetField("groupPath"))
end
end
-- remove collapsed state for any groups which no longer exist or no longer have children
local pathExists = TempTable.Acquire()
for i, groupPath in ipairs(groups) do
pathExists[groupPath] = true
local nextGroupPath = groups[i + 1]
self._hasChildrenLookup[groupPath] = nextGroupPath and TSM.Groups.Path.IsChild(nextGroupPath, groupPath) or nil
end
for groupPath in pairs(self._contextTable.collapsed) do
if groupPath == TSM.CONST.ROOT_GROUP_PATH or not pathExists[groupPath] or not self._hasChildrenLookup[groupPath] then
self._contextTable.collapsed[groupPath] = nil
end
end
TempTable.Release(pathExists)
for _, groupPath in ipairs(groups) do
tinsert(self._allData, groupPath)
if self._searchStr ~= "" or not self:_IsGroupHidden(groupPath) then
local groupName = groupPath == TSM.CONST.ROOT_GROUP_PATH and L["Base Group"] or TSM.Groups.Path.GetName(groupPath)
if strmatch(strlower(groupName), self._searchStr) and (self._searchStr == "" or groupPath ~= TSM.CONST.ROOT_GROUP_PATH) then
tinsert(self._data, groupPath)
end
end
end
TempTable.Release(groups)
end
function GroupTree._IsGroupHidden(self, data)
if data == TSM.CONST.ROOT_GROUP_PATH then
return false
elseif self._contextTable.collapsed[TSM.CONST.ROOT_GROUP_PATH] then
return true
end
local parent = TSM.Groups.Path.GetParent(data)
while parent and parent ~= TSM.CONST.ROOT_GROUP_PATH do
if self._contextTable.collapsed[parent] then
return true
end
parent = TSM.Groups.Path.GetParent(parent)
end
return false
end
function GroupTree._SetCollapsed(self, data, collapsed)
self._contextTable.collapsed[data] = collapsed or nil
end
function GroupTree._IsSelected(self, data)
return false
end
function GroupTree._HandleRowClick(self, data, mouseButton)
if mouseButton == "RightButton" and self._searchStr == "" and data ~= TSM.CONST.ROOT_GROUP_PATH and self._hasChildrenLookup[data] then
self:_SetCollapsed(data, not self._contextTable.collapsed[data])
self:UpdateData(true)
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GetGroupText(self, data)
local groupName = data == TSM.CONST.ROOT_GROUP_PATH and L["Base Group"] or TSM.Groups.Path.GetName(data)
if data ~= TSM.CONST.ROOT_GROUP_PATH then
groupName = Theme.GetGroupColor(select('#', strsplit(TSM.CONST.GROUP_SEP, data))):ColorText(groupName)
end
return groupName
end
function private.GetExpanderState(self, data)
local indentWidth = nil
local searchIsActive = self._searchStr ~= ""
if data == TSM.CONST.ROOT_GROUP_PATH then
indentWidth = -TSM.UI.TexturePacks.GetWidth("iconPack.14x14/Caret/Right") + EXPANDER_SPACING
else
local level = select('#', strsplit(TSM.CONST.GROUP_SEP, data))
indentWidth = (searchIsActive and 0 or (level - 1)) * (TSM.UI.TexturePacks.GetWidth("iconPack.14x14/Caret/Right") + EXPANDER_SPACING)
end
return not searchIsActive and data ~= TSM.CONST.ROOT_GROUP_PATH and self._hasChildrenLookup[data], not self._contextTable.collapsed[data], nil, indentWidth, EXPANDER_SPACING, true
end
function private.GetFlagState(self, data, isMouseOver)
if data == TSM.CONST.ROOT_GROUP_PATH then
return true, Theme.GetColor("TEXT")
end
local level = select('#', strsplit(TSM.CONST.GROUP_SEP, data))
local levelColor = Theme.GetGroupColor(level)
local color = (self:_IsSelected(data) or isMouseOver) and levelColor or Theme.GetColor("PRIMARY_BG_ALT")
return true, color
end
function private.GetTooltip(self, data)
if self._searchStr == "" then
return nil
end
return TSM.Groups.Path.Format(data), true
end
function private.QueryUpdateCallback(_, _, self)
self:UpdateData(true)
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.RowOnDoubleClick(row, mouseButton)
if mouseButton ~= "LeftButton" then
return
end
local data = row:GetData()
local self = row._scrollingTable
assert(self._searchStr == "" and data ~= TSM.CONST.ROOT_GROUP_PATH and self._hasChildrenLookup[data])
self:_SetCollapsed(data, not self._contextTable.collapsed[data])
self:UpdateData(true)
end

351
Core/UI/Elements/Input.lua Normal file
View File

@@ -0,0 +1,351 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Input UI Element Class.
-- The input element allows the user to enter text. It is a subclass of the @{BaseInput} class.
-- @classmod Input
local _, TSM = ...
local Theme = TSM.Include("Util.Theme")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local ItemLinked = TSM.Include("Service.ItemLinked")
local UIElements = TSM.Include("UI.UIElements")
local Input = TSM.Include("LibTSMClass").DefineClass("Input", TSM.UI.BaseInput)
UIElements.Register(Input)
TSM.UI.Input = Input
local private = {}
local PADDING_LEFT = 8
local PADDING_RIGHT = 8
local PADDING_TOP_BOTTOM = 4
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function Input.__init(self)
local frame = UIElements.CreateFrame(self, "EditBox")
self._editBox = frame
self.__super:__init(frame)
self._hintText = UIElements.CreateFontString(self, frame)
self._hintText:SetFont(Theme.GetFont("BODY_BODY3"):GetWowFont())
self._hintText:SetJustifyH("LEFT")
self._hintText:SetJustifyV("MIDDLE")
self._hintText:SetPoint("TOPLEFT", PADDING_LEFT, 0)
self._hintText:SetPoint("BOTTOMRIGHT", -PADDING_RIGHT, 0)
self._icon = frame:CreateTexture(nil, "ARTWORK")
self._icon:SetPoint("RIGHT", -PADDING_RIGHT / 2, 0)
self._clearBtn = CreateFrame("Button", nil, frame)
self._clearBtn:SetAllPoints(self._icon)
ScriptWrapper.Set(self._clearBtn, "OnClick", private.ClearBtnOnClick, self)
self._subIcon = frame:CreateTexture(nil, "ARTWORK")
self._subIcon:SetPoint("LEFT", PADDING_LEFT / 2, 0)
TSM.UI.TexturePacks.SetTextureAndSize(self._subIcon, "iconPack.14x14/Subtract/Default")
self._subBtn = CreateFrame("Button", nil, frame)
self._subBtn:SetAllPoints(self._subIcon)
ScriptWrapper.Set(self._subBtn, "OnClick", private.SubBtnOnClick, self)
ScriptWrapper.SetPropagate(self._subBtn, "OnEnter")
ScriptWrapper.SetPropagate(self._subBtn, "OnLeave")
self._addIcon = frame:CreateTexture(nil, "ARTWORK")
self._addIcon:SetPoint("RIGHT", -PADDING_RIGHT / 2, 0)
TSM.UI.TexturePacks.SetTextureAndSize(self._addIcon, "iconPack.14x14/Add/Default")
self._addBtn = CreateFrame("Button", nil, frame)
self._addBtn:SetAllPoints(self._addIcon)
ScriptWrapper.Set(self._addBtn, "OnClick", private.AddBtnOnClick, self)
ScriptWrapper.SetPropagate(self._addBtn, "OnEnter")
ScriptWrapper.SetPropagate(self._addBtn, "OnLeave")
ScriptWrapper.Set(frame, "OnEnter", private.OnEnter, self)
ScriptWrapper.Set(frame, "OnLeave", private.OnLeave, self)
local function ItemLinkedCallback(name, link)
if self._allowItemInsert == nil or not self:IsVisible() or not self:HasFocus() then
return
end
if self._allowItemInsert == true then
self._editBox:Insert(link)
else
self._editBox:Insert(name)
end
return true
end
ItemLinked.RegisterCallback(ItemLinkedCallback, -1)
self._clearEnabled = false
self._subAddEnabled = false
self._iconTexture = nil
self._autoComplete = nil
self._allowItemInsert = nil
self._lostFocusOnButton = false
end
function Input.Release(self)
self._clearEnabled = false
self._subAddEnabled = false
self._iconTexture = nil
self._autoComplete = nil
self._allowItemInsert = nil
self._lostFocusOnButton = false
self._hintText:SetText("")
self.__super:Release()
end
--- Sets the horizontal justification of the hint text.
-- @tparam Input self The input object
-- @tparam string justifyH The horizontal justification (either "LEFT", "CENTER" or "RIGHT")
-- @treturn Input The input object
function Input.SetHintJustifyH(self, justifyH)
assert(justifyH == "LEFT" or justifyH == "CENTER" or justifyH == "RIGHT")
self._hintText:SetJustifyH(justifyH)
return self
end
--- Sets the vertical justification of the hint text.
-- @tparam Input self The input object
-- @tparam string justifyV The vertical justification (either "TOP", "MIDDLE" or "BOTTOM")
-- @treturn Input The input object
function Input.SetHintJustifyV(self, justifyV)
assert(justifyV == "TOP" or justifyV == "MIDDLE" or justifyV == "BOTTOM")
self._hintText:SetJustifyV(justifyV)
return self
end
--- Sets the auto complete table.
-- @tparam Input self The input object
-- @tparam table tbl A list of strings to auto-complete to
-- @treturn Input The input object
function Input.SetAutoComplete(self, tbl)
assert(type(tbl) == "table")
self._autoComplete = tbl
return self
end
--- Sets the hint text.
-- The hint text is shown when there's no other text in the input.
-- @tparam Input self The input object
-- @tparam string text The hint text
-- @treturn Input The input object
function Input.SetHintText(self, text)
self._hintText:SetText(text)
return self
end
--- Sets whether or not the clear button is enabled.
-- @tparam Input self The input object
-- @tparam boolean enabled Whether or not the clear button is enabled
-- @treturn Input The input object
function Input.SetClearButtonEnabled(self, enabled)
assert(type(enabled) == "boolean")
assert(not self._subAddEnabled)
self._clearEnabled = enabled
return self
end
--- Sets whether or not the sub/add buttons are enabled.
-- @tparam Input self The input object
-- @tparam boolean enabled Whether or not the sub/add buttons are enabled
-- @treturn Input The input object
function Input.SetSubAddEnabled(self, enabled)
assert(type(enabled) == "boolean")
assert(not self._clearEnabled and not self._iconTexture)
self._subAddEnabled = enabled
return self
end
--- Sets the icon texture.
-- @tparam Input self The input object
-- @tparam[opt=nil] string iconTexture The texture string to use for the icon texture
-- @treturn Input The input object
function Input.SetIconTexture(self, iconTexture)
assert(iconTexture == nil or TSM.UI.TexturePacks.IsValid(iconTexture))
assert(not self._subAddEnabled)
self._iconTexture = iconTexture
return self
end
--- Allows inserting an item into the input by linking it while the input has focus.
-- @tparam Input self The input object
-- @tparam[opt=false] boolean insertLink Insert the link instead of the item name
-- @treturn Input The input object
function Input.AllowItemInsert(self, insertLink)
assert(insertLink == true or insertLink == false or insertLink == nil)
self._allowItemInsert = insertLink or false
return self
end
function Input.Draw(self)
self.__super:Draw()
self:_UpdateIconsForValue(self._value)
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function Input._GetHintTextColor(self)
local color = Theme.GetColor(self._disabled and "PRIMARY_BG_ALT" or self._backgroundColor)
if color:IsLight() then
return self:_GetTextColor("+HOVER")
else
return self:_GetTextColor("-HOVER")
end
end
function Input._UpdateIconsForValue(self, value)
local frame = self:_GetBaseFrame()
local leftPadding, rightPadding = PADDING_LEFT, PADDING_RIGHT
-- set the hint text
if value == "" and self._hintText:GetText() ~= "" then
self._hintText:SetFont(Theme.GetFont(self._font):GetWowFont())
self._hintText:SetTextColor(self:_GetHintTextColor():GetFractionalRGBA())
self._hintText:Show()
else
self._hintText:Hide()
end
local showSubAdd = self._subAddEnabled and (frame:IsMouseOver() or frame:HasFocus())
if showSubAdd then
self._subIcon:Show()
self._subBtn:Show()
self._addIcon:Show()
self._addBtn:Show()
else
self._subIcon:Hide()
self._subBtn:Hide()
self._addIcon:Hide()
self._addBtn:Hide()
end
-- set the icon
local iconTexture = nil
if self._clearEnabled and value ~= "" then
self._clearBtn:Show()
iconTexture = TSM.UI.TexturePacks.GetColoredKey("iconPack.18x18/Close/Default", self:_GetTextColor())
else
self._clearBtn:Hide()
iconTexture = not frame:HasFocus() and self._iconTexture and TSM.UI.TexturePacks.GetColoredKey(self._iconTexture, self:_GetTextColor()) or nil
end
if iconTexture then
assert(not showSubAdd)
self._icon:Show()
TSM.UI.TexturePacks.SetTextureAndSize(self._icon, iconTexture)
rightPadding = rightPadding + TSM.UI.TexturePacks.GetWidth(iconTexture)
else
self._icon:Hide()
end
frame:SetTextInsets(leftPadding, rightPadding, PADDING_TOP_BOTTOM, PADDING_TOP_BOTTOM)
-- for some reason the text insets don't take effect right away, so on the next frame, we call GetTextInsets() which seems to fix things
ScriptWrapper.Set(frame, "OnUpdate", private.OnUpdate, self)
end
function Input._OnTextChanged(self, value)
self:_UpdateIconsForValue(value)
end
function Input._ShouldKeepFocus(self)
if not IsMouseButtonDown("LeftButton") then
return false
end
if self._clearBtn:IsVisible() and self._clearBtn:IsMouseOver() then
return true
elseif self._subBtn:IsVisible() and self._subBtn:IsMouseOver() then
return true
elseif self._addBtn:IsVisible() and self._addBtn:IsMouseOver() then
return true
else
return false
end
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function Input._OnChar(self, c)
self.__super:_OnChar(c)
if not self._autoComplete then
return
end
local frame = self:_GetBaseFrame()
local text = frame:GetText()
local match = nil
for _, k in ipairs(self._autoComplete) do
local start, ending = strfind(strlower(k), strlower(text), 1, true)
if start == 1 and ending and ending == #text then
match = k
break
end
end
if match and not IsControlKeyDown() then
local compStart = #text
frame:SetText(match)
self:HighlightText(compStart, #match)
frame:GetScript("OnTextChanged")(frame, true)
end
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.OnUpdate(self)
local frame = self:_GetBaseFrame()
ScriptWrapper.Clear(frame, "OnUpdate")
frame:GetTextInsets()
end
function private.ClearBtnOnClick(self)
assert(self:_SetValueHelper(""))
self._escValue = ""
self._editBox:SetText(self._value)
self:Draw()
end
function private.SubBtnOnClick(self)
local minVal = self._validateContext and strsplit(":", self._validateContext)
local value = tostring(max(tonumber(self:GetValue()) - (IsShiftKeyDown() and 10 or 1), minVal or -math.huge))
if self:_SetValueHelper(value) then
self._escValue = self._value
self:_GetBaseFrame():SetText(value)
self:_UpdateIconsForValue(value)
end
end
function private.AddBtnOnClick(self)
local _, maxVal = nil, nil
if self._validateContext then
_, maxVal = strsplit(":", self._validateContext)
end
local value = tostring(min(tonumber(self:GetValue()) + (IsShiftKeyDown() and 10 or 1), maxVal or math.huge))
if self:_SetValueHelper(value) then
self._escValue = self._value
self:_GetBaseFrame():SetText(value)
self:_UpdateIconsForValue(value)
end
end
function private.OnEnter(self)
self:_UpdateIconsForValue(self._value)
end
function private.OnLeave(self)
self:_UpdateIconsForValue(self._value)
end

View File

@@ -0,0 +1,287 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- ItemList UI Element Class.
-- This element is used for the item lists in the group UI. It is a subclass of the @{ScrollingTable} class.
-- @classmod ItemList
local _, TSM = ...
local ItemString = TSM.Include("Util.ItemString")
local Theme = TSM.Include("Util.Theme")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local ItemInfo = TSM.Include("Service.ItemInfo")
local ItemList = TSM.Include("LibTSMClass").DefineClass("ItemList", TSM.UI.ScrollingTable)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(ItemList)
TSM.UI.ItemList = ItemList
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function ItemList.__init(self)
self.__super:__init()
self._rightClickToggle = true
self._allData = {}
self._selectedItems = {}
self._category = {}
self._categoryCollapsed = {}
self._filterFunc = nil
self._onSelectionChangedHandler = nil
end
function ItemList.Acquire(self)
self._headerHidden = true
self.__super:Acquire()
self:SetSelectionDisabled(true)
self:GetScrollingTableInfo()
:NewColumn("item")
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetIconSize(12)
:SetExpanderStateFunction(private.GetExpanderState)
:SetCheckStateFunction(private.GetCheckState)
:SetIconFunction(private.GetItemIcon)
:SetTextFunction(private.GetItemText)
:SetTooltipFunction(private.GetItemTooltip)
:Commit()
:Commit()
end
function ItemList.Release(self)
wipe(self._allData)
wipe(self._selectedItems)
wipe(self._category)
wipe(self._categoryCollapsed)
self._filterFunc = nil
self._onSelectionChangedHandler = nil
for _, row in ipairs(self._rows) do
ScriptWrapper.Clear(row._frame, "OnDoubleClick")
end
self.__super:Release()
end
--- Sets the items.
-- @tparam ItemList self The item list object
-- @tparam table items Either a list of items or list of tables with a `header` field and sub-list of items
-- @tparam boolean redraw Whether or not to redraw the item list
-- @treturn ItemList The item list object
function ItemList.SetItems(self, items, redraw)
wipe(self._allData)
wipe(self._category)
wipe(self._categoryCollapsed)
for _, item in ipairs(items) do
if type(item) == "table" and next(item) then
assert(item.header)
tinsert(self._allData, item.header)
for _, subItem in ipairs(item) do
tinsert(self._allData, subItem)
self._category[subItem] = item.header
end
elseif type(item) ~= "table" then
tinsert(self._allData, item)
self._category[item] = ""
end
end
self:_UpdateData()
wipe(self._selectedItems)
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler()
end
if redraw then
-- scroll up to the top
self._vScrollbar:SetValue(0)
self:Draw()
end
return self
end
--- Sets a filter function.
-- @tparam ItemList self The item list object
-- @tparam function func A function which is passed an item and returns true if it should be filtered (not shown)
-- @treturn ItemList The item list object
function ItemList.SetFilterFunction(self, func)
self._filterFunc = func
self:_UpdateData()
return self
end
--- Gets whether or not an item is selected.
-- @tparam ItemList self The item list object
-- @tparam string item The item
-- @treturn boolean Whether or not the item is selected
function ItemList.IsItemSelected(self, item)
return tContains(self._data, item) and self._selectedItems[item]
end
--- Selects all items.
-- @tparam ItemList self The item list object
function ItemList.SelectAll(self)
for _, item in ipairs(self._data) do
if self._category[item] then
self._selectedItems[item] = true
end
end
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler()
end
self:Draw()
end
--- Deselects all items.
-- @tparam ItemList self The item list object
function ItemList.ClearSelection(self)
wipe(self._selectedItems)
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler()
end
self:Draw()
end
--- Toggle the selection state of the item list.
-- @tparam ItemList self The item list object
-- @treturn ItemList The item list object
function ItemList.ToggleSelectAll(self)
if self:GetNumSelected() == 0 then
self:SelectAll()
else
self:ClearSelection()
end
return self
end
--- Registers a script handler.
-- @tparam ItemList self The item list object
-- @tparam string script The script to register for (supported scripts: `OnSelectionChanged`)
-- @tparam function handler The script handler which will be called with the item list object followed by any arguments
-- to the script
-- @treturn ItemList The item list object
function ItemList.SetScript(self, script, handler)
if script == "OnSelectionChanged" then
self._onSelectionChangedHandler = handler
else
error("Unknown ItemList script: "..tostring(script))
end
return self
end
--- Gets the number of selected items.
-- @tparam ItemList self The item list object
-- @treturn number The number of selected items
function ItemList.GetNumSelected(self)
local num = 0
for _, item in ipairs(self._data) do
if self._selectedItems[item] then
num = num + 1
end
end
return num
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function ItemList._UpdateData(self)
wipe(self._data)
for _, data in ipairs(self._allData) do
if not self:_IsDataHidden(data) then
tinsert(self._data, data)
end
end
end
function ItemList._IsDataHidden(self, data)
if not self._category[data] then
return false
end
if self._categoryCollapsed[self._category[data]] then
return true
end
if self._filterFunc then
return self._filterFunc(data)
end
return false
end
function ItemList._GetTableRow(self, isHeader)
local row = self.__super:_GetTableRow(isHeader)
if not isHeader then
ScriptWrapper.Set(row._frame, "OnDoubleClick", private.RowOnDoubleClick, row)
end
return row
end
function ItemList._HandleRowClick(self, data)
if self._category[data] then
self._selectedItems[data] = not self._selectedItems[data]
else
if IsMouseButtonDown("RightButton") then
return
end
self._categoryCollapsed[data] = not self._categoryCollapsed[data]
self:_UpdateData()
end
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler()
end
self:Draw()
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.GetExpanderState(self, data)
local isHeading = not self._category[data]
return isHeading, not self._categoryCollapsed[data], isHeading and 0 or 1
end
function private.GetCheckState(self, data)
return self._category[data] and self._selectedItems[data]
end
function private.GetItemIcon(self, data)
if not self._category[data] then
return
end
return ItemInfo.GetTexture(data)
end
function private.GetItemText(self, data)
if self._category[data] then
return TSM.UI.GetColoredItemName(data) or Theme.GetFeedbackColor("RED"):ColorText("?")
else
return data
end
end
function private.GetItemTooltip(self, data)
if not self._category[data] then
return nil
end
return ItemString.Get(data)
end
function private.RowOnDoubleClick(row, mouseButton)
if mouseButton ~= "LeftButton" then
return
end
local self = row._scrollingTable
self:_HandleRowClick(row:GetData())
end

View File

@@ -0,0 +1,204 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- LargeApplicationFrame UI Element Class.
-- This is the base frame of the large TSM windows which have tabs along the top (i.e. MainUI, AuctionUI, CraftingUI).
-- It is a subclass of the @{ApplicationFrame} class.
-- @classmod LargeApplicationFrame
local _, TSM = ...
local LargeApplicationFrame = TSM.Include("LibTSMClass").DefineClass("LargeApplicationFrame", TSM.UI.ApplicationFrame)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(LargeApplicationFrame)
TSM.UI.LargeApplicationFrame = LargeApplicationFrame
local private = {}
local NAV_BAR_SPACING = 16
local NAV_BAR_HEIGHT = 24
local NAV_BAR_RELATIVE_LEVEL = 21
local NAV_BAR_TOP_OFFSET = -8
-- ============================================================================
-- Meta Class Methods
-- ============================================================================
function LargeApplicationFrame.__init(self)
self.__super:__init()
self._buttons = {}
self._selectedButton = nil
self._buttonIndex = {}
end
function LargeApplicationFrame.Acquire(self)
self:SetContentFrame(UIElements.New("Frame", "content")
:SetLayout("VERTICAL")
:SetBackgroundColor("FRAME_BG")
)
self.__super:Acquire()
end
function LargeApplicationFrame.Release(self)
wipe(self._buttons)
wipe(self._buttonIndex)
self._selectedButton = nil
self.__super:Release()
end
-- ============================================================================
-- Public Class Methods
-- ============================================================================
--- Sets the context table.
-- This table can be used to preserve position, size, and current page information across lifecycles of the frame and
-- even WoW sessions if it's within the settings DB.
-- @see ApplicationFrame.SetContextTable
-- @tparam LargeApplicationFrame self The large application frame object
-- @tparam table tbl The context table
-- @tparam table defaultTbl Default values (see @{ApplicationFrame.SetContextTable} for fields)
-- @treturn LargeApplicationFrame The large application frame object
function LargeApplicationFrame.SetContextTable(self, tbl, defaultTbl)
assert(defaultTbl.page)
tbl.page = tbl.page or defaultTbl.page
self.__super:SetContextTable(tbl, defaultTbl)
return self
end
--- Adds a top-level navigation button.
-- @tparam LargeApplicationFrame self The large application frame object
-- @tparam string text The button text
-- @tparam function drawCallback The function called when the button is clicked to get the corresponding content
-- @treturn LargeApplicationFrame The large application frame object
function LargeApplicationFrame.AddNavButton(self, text, drawCallback)
local button = UIElements.New("AlphaAnimatedFrame", "NavBar_"..text)
:SetRange(1, 0.3)
:SetDuration(1)
:SetLayout("HORIZONTAL")
:SetRelativeLevel(NAV_BAR_RELATIVE_LEVEL)
:SetContext(drawCallback)
:AddChild(UIElements.New("Button", "button")
:SetText(text)
:SetScript("OnEnter", private.OnNavBarButtonEnter)
:SetScript("OnLeave", private.OnNavBarButtonLeave)
:SetScript("OnClick", private.OnNavBarButtonClicked)
)
self:AddChildNoLayout(button)
tinsert(self._buttons, button)
self._buttonIndex[text] = #self._buttons
if self._buttonIndex[text] == self._contextTable.page then
self:SetSelectedNavButton(text)
end
return self
end
--- Set the selected nav button.
-- @tparam LargeApplicationFrame self The large application frame object
-- @tparam string buttonText The button text
-- @tparam boolean redraw Whether or not to redraw the frame
function LargeApplicationFrame.SetSelectedNavButton(self, buttonText, redraw)
if buttonText == self._selectedButton then
return
end
local index = self._buttonIndex[buttonText]
self._contextTable.page = index
self._selectedButton = buttonText
self._contentFrame:ReleaseAllChildren()
self._contentFrame:AddChild(self._buttons[index]:GetContext()(self))
if redraw then
self:Draw()
end
return self
end
--- Get the selected nav button.
-- @tparam LargeApplicationFrame self The large application frame object
-- @treturn string The text of the selected button
function LargeApplicationFrame.GetSelectedNavButton(self)
return self._selectedButton
end
--- Sets which nav button is pulsing.
-- @tparam LargeApplicationFrame self The large application frame object
-- @tparam ?string buttonText The button text or nil if no nav button should be pulsing
function LargeApplicationFrame.SetPulsingNavButton(self, buttonText)
local index = buttonText and self._buttonIndex[buttonText]
for i, button in ipairs(self._buttons) do
if not index or i ~= index then
button:SetPlaying(false)
elseif not button:IsPlaying() then
button:SetPlaying(true)
end
end
end
function LargeApplicationFrame.Draw(self)
self.__super:Draw()
for i, buttonFrame in ipairs(self._buttons) do
local button = buttonFrame:GetElement("button")
button:SetFont("BODY_BODY1_BOLD")
button:SetTextColor(i == self._contextTable.page and "INDICATOR" or "TEXT_ALT")
button:Draw()
buttonFrame:SetSize(button:GetStringWidth(), NAV_BAR_HEIGHT)
end
local offsetX = 104
for _, buttonFrame in ipairs(self._buttons) do
local buttonWidth = buttonFrame:GetElement("button"):GetStringWidth()
buttonFrame:SetSize(buttonWidth, NAV_BAR_HEIGHT)
buttonFrame:WipeAnchors()
buttonFrame:AddAnchor("TOPLEFT", offsetX, NAV_BAR_TOP_OFFSET)
offsetX = offsetX + buttonWidth + NAV_BAR_SPACING
-- draw the buttons again now that we know their dimensions
buttonFrame:Draw()
end
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function LargeApplicationFrame._SetResizing(self, resizing)
for _, button in ipairs(self._buttons) do
if resizing then
button:Hide()
else
button:Show()
end
end
self.__super:_SetResizing(resizing)
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.OnNavBarButtonEnter(button)
if button:GetBaseElement():GetSelectedNavButton() == button:GetText() then
return
end
button:SetTextColor("TEXT")
:Draw()
end
function private.OnNavBarButtonLeave(button)
if button:GetBaseElement():GetSelectedNavButton() == button:GetText() then
return
end
button:SetTextColor("TEXT_ALT")
:Draw()
end
function private.OnNavBarButtonClicked(button)
local self = button:GetParentElement():GetParentElement()
self:SetSelectedNavButton(button:GetText(), true)
end

View File

@@ -0,0 +1,301 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- ManagementGroupTree UI Element Class.
-- The management group tree allows for moving, adding, and deleting groups. It also only allows for a single group to
-- be selected. It is a subclass of the @{GroupTree} class.
-- @classmod ManagementGroupTree
local _, TSM = ...
local L = TSM.Include("Locale").GetTable()
local Analytics = TSM.Include("Util.Analytics")
local String = TSM.Include("Util.String")
local Theme = TSM.Include("Util.Theme")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local ManagementGroupTree = TSM.Include("LibTSMClass").DefineClass("ManagementGroupTree", TSM.UI.GroupTree)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(ManagementGroupTree)
TSM.UI.ManagementGroupTree = ManagementGroupTree
local private = {}
local DRAG_SCROLL_SPEED_FACTOR = 12
local MOVE_FRAME_PADDING = 8
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function ManagementGroupTree.__init(self)
self.__super:__init()
self._moveFrame = nil
self._selectedGroup = nil
self._onGroupSelectedHandler = nil
self._onNewGroupHandler = nil
self._scrollAmount = 0
end
function ManagementGroupTree.Acquire(self)
self._moveFrame = UIElements.New("Frame", self._id.."_MoveFrame")
:SetLayout("VERTICAL")
:SetHeight(20)
:SetStrata("TOOLTIP")
:SetBackgroundColor("PRIMARY_BG_ALT", true)
:SetBorderColor("INDICATOR")
:SetContext(self)
:AddChild(UIElements.New("Text", "text")
:SetFont("BODY_BODY3")
:SetJustifyH("CENTER")
)
self._moveFrame:SetParent(self:_GetBaseFrame())
self._moveFrame:Hide()
self._moveFrame:SetScript("OnShow", private.MoveFrameOnShow)
self._moveFrame:SetScript("OnUpdate", private.MoveFrameOnUpdate)
self.__super:Acquire()
self:GetScrollingTableInfo()
:GetColById("group")
:SetActionIconInfo(2, 14, private.GetActionIcon, true)
:SetActionIconClickHandler(private.OnActionIconClick)
:Commit()
:Commit()
end
function ManagementGroupTree.Release(self)
self._selectedGroup = nil
self._onGroupSelectedHandler = nil
self._onNewGroupHandler = nil
self._moveFrame:Release()
self._moveFrame = nil
for _, row in ipairs(self._rows) do
row._frame:RegisterForDrag()
ScriptWrapper.Clear(row._frame, "OnDragStart")
ScriptWrapper.Clear(row._frame, "OnDragStop")
for _, button in pairs(row._buttons) do
button:RegisterForDrag()
ScriptWrapper.Clear(button, "OnDragStart")
ScriptWrapper.Clear(button, "OnDragStop")
end
end
self.__super:Release()
end
--- Sets the selected group.
-- @tparam ManagementGroupTree self The management group tree object
-- @tparam string groupPath The selected group's path
-- @tparam boolean redraw Whether or not to redraw the management group tree
-- @treturn ManagementGroupTree The management group tree object
function ManagementGroupTree.SetSelectedGroup(self, groupPath, redraw)
self._selectedGroup = groupPath
if self._onGroupSelectedHandler then
self:_onGroupSelectedHandler(groupPath)
end
if redraw then
-- make sure this group is visible (its parent is expanded)
local parent = TSM.Groups.Path.GetParent(groupPath)
self._contextTable.collapsed[TSM.CONST.ROOT_GROUP_PATH] = nil
while parent and parent ~= TSM.CONST.ROOT_GROUP_PATH do
self._contextTable.collapsed[parent] = nil
parent = TSM.Groups.Path.GetParent(parent)
end
self:UpdateData(true)
self:_ScrollToData(self._selectedGroup)
end
return self
end
--- Registers a script handler.
-- @tparam ManagementGroupTree self The management group tree object
-- @tparam string script The script to register for (supported scripts: `OnGroupSelected`)
-- @tparam function handler The script handler which will be called with the management group tree object followed by
-- any arguments to the script
-- @treturn ManagementGroupTree The management group tree object
function ManagementGroupTree.SetScript(self, script, handler)
if script == "OnGroupSelected" then
self._onGroupSelectedHandler = handler
elseif script == "OnNewGroup" then
self._onNewGroupHandler = handler
else
error("Unknown ManagementGroupTree script: "..tostring(script))
end
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function ManagementGroupTree._GetTableRow(self, isHeader)
local row = self.__super:_GetTableRow(isHeader)
if not isHeader then
row._frame:RegisterForDrag("LeftButton")
ScriptWrapper.Set(row._frame, "OnDragStart", private.RowOnDragStart, row)
ScriptWrapper.Set(row._frame, "OnDragStop", private.RowOnDragStop, row)
for _, button in pairs(row._buttons) do
button:RegisterForDrag("LeftButton")
ScriptWrapper.Set(button, "OnDragStart", private.RowOnDragStart, row)
ScriptWrapper.Set(button, "OnDragStop", private.RowOnDragStop, row)
end
end
return row
end
function ManagementGroupTree._SetCollapsed(self, data, collapsed)
self.__super:_SetCollapsed(data, collapsed)
if collapsed and self._selectedGroup ~= data and strmatch(self._selectedGroup, "^"..String.Escape(data)) then
-- we collapsed a parent of the selected group, so select the group we just collapsed instead
self:SetSelectedGroup(data, true)
end
end
function ManagementGroupTree._IsSelected(self, data)
return data == self._selectedGroup
end
function ManagementGroupTree._HandleRowClick(self, data, mouseButton)
if mouseButton == "RightButton" then
self.__super:_HandleRowClick(data, mouseButton)
return
end
self:SetSelectedGroup(data, true)
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GetActionIcon(self, data, iconIndex, isMouseOver)
if iconIndex == 1 then
local texturePack = "iconPack.14x14/Add/Circle"
return true, isMouseOver and TSM.UI.TexturePacks.GetColoredKey(texturePack, Theme.GetColor("INDICATOR")) or texturePack
elseif iconIndex == 2 then
if data ~= TSM.CONST.ROOT_GROUP_PATH then
local texturePack = "iconPack.14x14/Delete"
return true, isMouseOver and TSM.UI.TexturePacks.GetColoredKey(texturePack, Theme.GetColor("INDICATOR")) or texturePack
else
return false, nil
end
else
error("Invalid index: "..tostring(iconIndex))
end
end
function private.OnActionIconClick(self, data, iconIndex)
if iconIndex == 1 then
local newGroupPath = TSM.Groups.Path.Join(data, L["New Group"])
if TSM.Groups.Exists(newGroupPath) then
local num = 1
while TSM.Groups.Exists(newGroupPath.." "..num) do
num = num + 1
end
newGroupPath = newGroupPath.." "..num
end
TSM.Groups.Create(newGroupPath)
Analytics.Action("CREATED_GROUP", newGroupPath)
self:SetSelectedGroup(newGroupPath, true)
if self._onNewGroupHandler then
self:_onNewGroupHandler()
end
elseif iconIndex == 2 then
local groupColor = Theme.GetGroupColor(select('#', strsplit(TSM.CONST.GROUP_SEP, data)))
self:GetBaseElement():ShowConfirmationDialog(L["Delete Group?"], format(L["Deleting this group (%s) will also remove any sub-groups attached to this group."], groupColor:ColorText(TSM.Groups.Path.GetName(data))), private.DeleteConfirmed, self, data)
else
error("Invalid index: "..tostring(iconIndex))
end
end
function private.DeleteConfirmed(self, data)
TSM.Groups.Delete(data)
Analytics.Action("DELETED_GROUP", data)
self:SetSelectedGroup(TSM.CONST.ROOT_GROUP_PATH, true)
end
function private.MoveFrameOnShow(frame)
local self = frame:GetContext()
self._scrollAmount = 0
end
function private.MoveFrameOnUpdate(frame)
local self = frame:GetContext()
local uiScale = UIParent:GetEffectiveScale()
local x, y = GetCursorPosition()
x = x / uiScale
y = y / uiScale
frame:_GetBaseFrame():SetPoint("CENTER", UIParent, "BOTTOMLEFT", x, y)
-- figure out if we're above or below the frame for scrolling while dragging
local top = self:_GetBaseFrame():GetTop()
local bottom = self:_GetBaseFrame():GetBottom()
if y > top then
self._scrollAmount = top - y
elseif y < bottom then
self._scrollAmount = bottom - y
else
self._scrollAmount = 0
end
self._vScrollbar:SetValue(self._vScrollbar:GetValue() + self._scrollAmount / DRAG_SCROLL_SPEED_FACTOR)
end
function private.RowOnDragStart(row)
local self = row._scrollingTable
local groupPath = row:GetData()
if groupPath == TSM.CONST.ROOT_GROUP_PATH then
-- don't do anything for the root group
return
end
local level = select('#', strsplit(TSM.CONST.GROUP_SEP, groupPath))
local levelColor = Theme.GetGroupColor(level)
self._dragGroupPath = groupPath
self._moveFrame:Show()
self._moveFrame:SetHeight(self._rowHeight)
local moveFrameText = self._moveFrame:GetElement("text")
moveFrameText:SetTextColor(levelColor)
moveFrameText:SetText(TSM.Groups.Path.GetName(groupPath))
moveFrameText:SetWidth(1000)
moveFrameText:Draw()
self._moveFrame:SetWidth(moveFrameText:GetStringWidth() + MOVE_FRAME_PADDING * 2)
self._moveFrame:Draw()
end
function private.RowOnDragStop(row)
local self = row._scrollingTable
local groupPath = row:GetData()
if groupPath == TSM.CONST.ROOT_GROUP_PATH then
-- don't do anything for the root group
return
end
self._moveFrame:Hide()
local destPath = nil
for _, targetRow in ipairs(self._rows) do
if targetRow:IsMouseOver() then
destPath = targetRow:GetData()
break
end
end
local oldPath = self._dragGroupPath
self._dragGroupPath = nil
if not destPath or destPath == oldPath or TSM.Groups.Path.IsChild(destPath, oldPath) then
return
end
local newPath = TSM.Groups.Path.Join(destPath, TSM.Groups.Path.GetName(oldPath))
if oldPath == newPath then
return
elseif TSM.Groups.Exists(newPath) then
return
end
TSM.Groups.Move(oldPath, newPath)
Analytics.Action("MOVED_GROUP", oldPath, newPath)
self:SetSelectedGroup(newPath, true)
end

View File

@@ -0,0 +1,170 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- MultiLineInput UI Element Class.
-- The input element allows the user to enter text. It is a subclass of the @{BaseInput} class.
-- @classmod MultiLineInput
local _, TSM = ...
local Theme = TSM.Include("Util.Theme")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local MultiLineInput = TSM.Include("LibTSMClass").DefineClass("MultiLineInput", TSM.UI.BaseInput)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(MultiLineInput)
TSM.UI.MultiLineInput = MultiLineInput
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function MultiLineInput.__init(self)
local frame = UIElements.CreateFrame(self, "ScrollFrame")
self._editBox = CreateFrame("EditBox", nil, frame)
self.__super:__init(frame)
frame:EnableMouseWheel(true)
frame:SetClipsChildren(true)
ScriptWrapper.Set(frame, "OnUpdate", private.FrameOnUpdate, self)
ScriptWrapper.Set(frame, "OnMouseWheel", private.FrameOnMouseWheel, self)
ScriptWrapper.Set(frame, "OnMouseUp", private.FrameOnMouseUp, self)
self._scrollbar = TSM.UI.Scrollbar.Create(frame)
ScriptWrapper.Set(self._scrollbar, "OnValueChanged", private.OnScrollbarValueChanged, self)
self._editBox:SetSpacing(4)
self._editBox:SetMultiLine(true)
self._editBox:SetTextInsets(8, 8, 4, 4)
frame:SetScrollChild(self._editBox)
ScriptWrapper.Set(self._editBox, "OnCursorChanged", private.OnCursorChanged, self)
ScriptWrapper.Set(self._editBox, "OnSizeChanged", private.OnSizeChanged, self)
self._scrollValue = 0
self._ignoreEnter = false
end
function MultiLineInput.Acquire(self)
self:SetBackgroundColor("ACTIVE_BG")
self:SetJustifyH("LEFT")
self:SetJustifyV("TOP")
self.__super:Acquire()
self._scrollValue = 0
self._ignoreEnter = false
self._scrollbar:SetValue(0)
end
function MultiLineInput.Draw(self)
self._editBox:SetWidth(self:_GetBaseFrame():GetWidth())
self.__super:Draw()
local maxScroll = self:_GetMaxScroll()
self._scrollbar:SetMinMaxValues(0, maxScroll)
self._scrollbar:SetValue(min(self._scrollValue, maxScroll))
self._scrollbar.thumb:SetHeight(TSM.UI.Scrollbar.GetLength(self._editBox:GetHeight(), self:_GetDimension("HEIGHT")))
end
--- Sets to ignore enter pressed scripts for the input multi-line input.
-- @tparam MultiLineInput self The multi-line input object
-- @treturn MultiLineInput The multi-line input object
function MultiLineInput.SetIgnoreEnter(self)
ScriptWrapper.Clear(self._editBox, "OnEnterPressed")
self._ignoreEnter = true
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function MultiLineInput._OnScrollValueChanged(self, value)
self:_GetBaseFrame():SetVerticalScroll(value)
self._scrollValue = value
end
function MultiLineInput._GetMaxScroll(self)
return max(self._editBox:GetHeight() - self:_GetDimension("HEIGHT"), 0)
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.OnCursorChanged(self, _, y, _, lineHeight)
y = abs(y)
local offset = self._scrollValue - y
if offset > 0 or offset < self:_GetDimension("HEIGHT") + lineHeight then
self._scrollbar:SetValue(y)
end
end
function private.OnSizeChanged(self, _, height)
local maxScroll = self:_GetMaxScroll()
self._scrollbar:SetMinMaxValues(0, maxScroll)
self._scrollbar:SetValue(min(self._scrollValue, maxScroll))
self._scrollbar.thumb:SetHeight(TSM.UI.Scrollbar.GetLength(self._editBox:GetHeight(), self:_GetDimension("HEIGHT")))
end
function private.OnScrollbarValueChanged(self, value)
value = max(min(value, self:_GetMaxScroll()), 0)
self:_OnScrollValueChanged(value)
end
function private.FrameOnUpdate(self)
if (self:_GetBaseFrame():IsMouseOver() and self:_GetMaxScroll() > 0) or self._scrollbar.dragging then
self._scrollbar:Show()
else
self._scrollbar:Hide()
end
end
function private.FrameOnMouseWheel(self, direction)
local parentScroll = nil
local parent = self:GetParentElement()
while parent do
if parent:__isa(TSM.UI.ScrollFrame) then
parentScroll = parent
break
else
parent = parent:GetParentElement()
end
end
if parentScroll then
local minValue, maxValue = self._scrollbar:GetMinMaxValues()
if direction > 0 then
if self._scrollbar:GetValue() == minValue then
local scrollAmount = min(parentScroll:_GetDimension("HEIGHT") / 3, Theme.GetMouseWheelScrollAmount())
parentScroll._scrollbar:SetValue(parentScroll._scrollbar:GetValue() + -1 * direction * scrollAmount)
else
local scrollAmount = min(self:_GetDimension("HEIGHT") / 3, Theme.GetMouseWheelScrollAmount())
self._scrollbar:SetValue(self._scrollbar:GetValue() + -1 * direction * scrollAmount)
end
else
if self._scrollbar:GetValue() == maxValue then
local scrollAmount = min(parentScroll:_GetDimension("HEIGHT") / 3, Theme.GetMouseWheelScrollAmount())
parentScroll._scrollbar:SetValue(parentScroll._scrollbar:GetValue() + -1 * direction * scrollAmount)
else
local scrollAmount = min(self:_GetDimension("HEIGHT") / 3, Theme.GetMouseWheelScrollAmount())
self._scrollbar:SetValue(self._scrollbar:GetValue() + -1 * direction * scrollAmount)
end
end
else
local scrollAmount = min(self:_GetDimension("HEIGHT") / 3, Theme.GetMouseWheelScrollAmount())
self._scrollbar:SetValue(self._scrollbar:GetValue() + -1 * direction * scrollAmount)
end
end
function private.FrameOnMouseUp(self)
self:SetFocused(true)
end

View File

@@ -0,0 +1,292 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Multiselection Dropdown UI Element Class.
-- A dropdown element allows the user to select from a dialog list. It is a subclass of the @{BaseDropdown} class.
-- @classmod MultiselectionDropdown
local _, TSM = ...
local L = TSM.Include("Locale").GetTable()
local Table = TSM.Include("Util.Table")
local Theme = TSM.Include("Util.Theme")
local MultiselectionDropdown = TSM.Include("LibTSMClass").DefineClass("MultiselectionDropdown", TSM.UI.BaseDropdown)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(MultiselectionDropdown)
TSM.UI.MultiselectionDropdown = MultiselectionDropdown
local private = {}
-- ============================================================================
-- Meta Class Methods
-- ============================================================================
function MultiselectionDropdown.__init(self)
self.__super:__init()
self._itemIsSelected = {}
self._settingTableDirect = nil
self._text = self:_GetBaseFrame():CreateFontString()
self._text:SetFont(Theme.GetFont("BODY_BODY3"):GetWowFont())
self._text:Hide()
self._noneSelectionText = L["None Selected"]
self._partialSelectionText = L["%d Selected"]
self._allSelectionText = L["All Selected"]
end
function MultiselectionDropdown.Release(self)
wipe(self._itemIsSelected)
self._settingTableDirect = nil
self._noneSelectionText = L["None Selected"]
self._partialSelectionText = L["%d Selected"]
self._allSelectionText = L["All Selected"]
self.__super:Release()
end
-- ============================================================================
-- Public Class Methods
-- ============================================================================
--- Set whether the item is selected.
-- @tparam MultiselectionDropdown self The dropdown object
-- @tparam string item The item
-- @tparam boolean selected Whether or not the item should be selected
-- @treturn MultiselectionDropdown The dropdown object
function MultiselectionDropdown.SetItemSelected(self, item, selected)
self:_SetItemSelectedHelper(item, selected)
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler()
end
return self
end
--- Set whether the item is selected by key.
-- @tparam MultiselectionDropdown self The dropdown object
-- @tparam string itemKey The key for the item
-- @tparam boolean selected Whether or not the item should be selected
-- @treturn MultiselectionDropdown The dropdown object
function MultiselectionDropdown.SetItemSelectedByKey(self, itemKey, selected)
self:SetItemSelected(Table.GetDistinctKey(self._itemKeyLookup, itemKey), selected)
return self
end
--- Set the selected items.
-- @tparam MultiselectionDropdown self The dropdown object
-- @tparam table selected A table where the keys are the items to be selected
-- @treturn MultiselectionDropdown The dropdown object
function MultiselectionDropdown.SetSelectedItems(self, selected)
wipe(self._itemIsSelected)
if self._settingTableDirect then
wipe(self._settingTableDirect)
end
for _, item in ipairs(self._items) do
self:_SetItemSelectedHelper(item, selected[item])
end
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler()
end
return self
end
--- Set the selected items.
-- @tparam MultiselectionDropdown self The dropdown object
-- @tparam table selected A table where the keys are the items to be selected
-- @treturn MultiselectionDropdown The dropdown object
function MultiselectionDropdown.SetSelectedItemKeys(self, selected)
wipe(self._itemIsSelected)
if self._settingTableDirect then
wipe(self._settingTableDirect)
end
for _, item in ipairs(self._items) do
self:_SetItemSelectedHelper(item, selected[self._itemKeyLookup[item]])
end
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler()
end
return self
end
--- Set the unselected items.
-- @tparam MultiselectionDropdown self The dropdown object
-- @tparam table unselected A table where the keys are the items which aren't selected
-- @treturn MultiselectionDropdown The dropdown object
function MultiselectionDropdown.SetUnselectedItemKeys(self, unselected)
wipe(self._itemIsSelected)
if self._settingTableDirect then
wipe(self._settingTableDirect)
end
for _, item in ipairs(self._items) do
self:_SetItemSelectedHelper(item, not unselected[self._itemKeyLookup[item]])
end
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler()
end
return self
end
--- Get the currently selected item.
-- @tparam MultiselectionDropdown self The dropdown object
-- @tparam string item The item
-- @treturn ?string The selected item
function MultiselectionDropdown.ItemIsSelected(self, item)
return self._itemIsSelected[item]
end
--- Get the currently selected item.
-- @tparam MultiselectionDropdown self The dropdown object
-- @tparam string|number itemKey The key for the item
-- @treturn boolean Whether or not the item is selected
function MultiselectionDropdown.ItemIsSelectedByKey(self, itemKey)
return self:ItemIsSelected(Table.GetDistinctKey(self._itemKeyLookup, itemKey))
end
--- Sets the setting info.
-- This method is used to have the selected keys of the dropdown automatically correspond with the value of a field in a
-- table. This is useful for dropdowns which are tied directly to settings.
-- @tparam MultiselectionDropdown self The dropdown object
-- @tparam table tbl The table which the field to set belongs to
-- @tparam string key The key into the table to be set based on the dropdown state
-- @treturn MultiselectionDropdown The dropdown object
function MultiselectionDropdown.SetSettingInfo(self, tbl, key)
local directTbl = tbl[key]
assert(type(directTbl) == "table")
-- this function wipes our settingTable, so set the selected items first
self:SetSelectedItemKeys(directTbl)
self._settingTableDirect = directTbl
return self
end
--- Populate the specified table with a list of selected items
-- @tparam MultiselectionDropdown self The dropdown object
-- @tparam table resultTbl The table to populate
function MultiselectionDropdown.GetSelectedItems(self, resultTbl)
for _, item in ipairs(self._items) do
if self:ItemIsSelected(item) then
tinsert(resultTbl, item)
end
end
end
--- Sets the selection text which is shown to summarize the current value.
-- @tparam BaseDropdown self The dropdown object
-- @tparam string noneText The selection text string when none are selected
-- @tparam string partialText The selection text string for a partial selection
-- @tparam string allText The selection text string when all are selected
-- @treturn BaseDropdown The dropdown object
function MultiselectionDropdown.SetSelectionText(self, noneText, partialText, allText)
assert(type(partialText) == "string" and type(partialText) == "string" and type(allText) == "string")
self._noneSelectionText = noneText
self._partialSelectionText = partialText
self._allSelectionText = allText
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function MultiselectionDropdown._GetDialogSize(self)
local width, height = self.__super:_GetDialogSize()
width = max(width + 12, 200) -- check icon, and big enough for select all / deselect all buttons
height = height + 26 -- header + line
return width, height
end
function MultiselectionDropdown._GetNumSelected(self)
local num = 0
for _, item in ipairs(self._items) do
if self:ItemIsSelected(item) then
num = num + 1
end
end
return num
end
function MultiselectionDropdown._AddDialogChildren(self, frame)
local numSelected = self:_GetNumSelected()
frame:AddChild(UIElements.New("Frame", "header")
:SetLayout("HORIZONTAL")
:SetPadding(8, 8, 2, 2)
:SetHeight(24)
:AddChild(UIElements.New("Button", "selectAll")
:SetWidth("AUTO")
:SetMargin(0, 8, 0, 0)
:SetFont("BODY_BODY2_BOLD")
:SetTextColor(numSelected == #self._items and "ACTIVE_BG_ALT" or "TEXT")
:SetDisabled(numSelected == #self._items)
:SetText(L["Select All"])
:SetScript("OnClick", private.SelectAllOnClick)
)
:AddChild(UIElements.New("Button", "deselectAll")
:SetWidth("AUTO")
:SetMargin(0, 8, 0, 0)
:SetFont("BODY_BODY2_BOLD")
:SetTextColor(numSelected == 0 and "ACTIVE_BG_ALT" or "TEXT")
:SetDisabled(numSelected == 0)
:SetText(L["Deselect All"])
:SetScript("OnClick", private.DeselectAllOnClick)
)
:AddChild(UIElements.New("Spacer", "spacer"))
)
frame:AddChild(UIElements.New("Texture", "line")
:SetHeight(2)
:SetTexture("ACTIVE_BG_ALT")
)
frame:AddChild(UIElements.New("DropdownList", "list")
:SetMultiselect(true)
:SetItems(self._items, self._itemIsSelected)
)
end
function MultiselectionDropdown._GetCurrentSelectionString(self)
local numSelected = self:_GetNumSelected()
local result = nil
if numSelected == 0 then
result = self._hintText ~= "" and self._hintText or self._noneSelectionText
elseif numSelected == #self._items then
result = self._allSelectionText.." ("..numSelected..")"
else
result = format(self._partialSelectionText, numSelected)
end
return result
end
function MultiselectionDropdown._OnListSelectionChanged(self, dropdownList, selection)
self:SetSelectedItems(selection)
local numSelected = self:_GetNumSelected()
dropdownList:GetElement("__parent.header.selectAll")
:SetTextColor(numSelected == #self._items and "ACTIVE_BG_ALT" or "TEXT")
:SetDisabled(numSelected == #self._items)
:Draw()
dropdownList:GetElement("__parent.header.deselectAll")
:SetTextColor(numSelected == 0 and "ACTIVE_BG_ALT" or "TEXT")
:SetDisabled(numSelected == 0)
:Draw()
end
function MultiselectionDropdown._SetItemSelectedHelper(self, item, selected)
self._itemIsSelected[item] = selected and true or nil
if self._settingTableDirect then
self._settingTableDirect[self._itemKeyLookup[item]] = self._itemIsSelected[item]
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.SelectAllOnClick(button)
button:GetElement("__parent.__parent.list"):SelectAll()
end
function private.DeselectAllOnClick(button)
button:GetElement("__parent.__parent.list"):DeselectAll()
end

View File

@@ -0,0 +1,183 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- My Auctions Scrolling Table Class.
-- A scrolling table containing the player's auctions. It is a subclass of the @{QueryScrollingTable} class.
-- @classmod MyAuctionsScrollingTable
local _, TSM = ...
local MyAuctionsScrollingTable = TSM.Include("LibTSMClass").DefineClass("MyAuctionsScrollingTable", TSM.UI.QueryScrollingTable)
local TempTable = TSM.Include("Util.TempTable")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(MyAuctionsScrollingTable)
TSM.UI.MyAuctionsScrollingTable = MyAuctionsScrollingTable
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function MyAuctionsScrollingTable.__init(self)
self.__super:__init()
self._selectionSortValue = nil
self._selectionAuctionId = nil
end
function MyAuctionsScrollingTable.Release(self)
self._selectionSortValue = nil
self._selectionAuctionId = nil
self.__super:Release()
end
--- Sets the selected record.
-- @tparam MyAuctionsScrollingTable self The my auctions scrolling table object
-- @param selection The selected record or nil to clear the selection
-- @tparam[opt=false] bool redraw Whether or not to redraw the scrolling table
-- @treturn MyAuctionsScrollingTable The my auctions scrolling table object
function MyAuctionsScrollingTable.SetSelection(self, selection, redraw)
self.__super:SetSelection(selection, redraw)
if self._selection then
local selectedRow = self:GetSelection()
local sortField = TSM.IsWowClassic() and "index" or self._tableInfo:_GetSortFieldById(self._sortCol)
self._selectionSortValue = selectedRow:GetField(sortField)
if type(self._selectionSortValue) == "string" then
self._selectionSortValue = strlower(self._selectionSortValue)
end
self._selectionAuctionId = selectedRow:GetField("auctionId")
else
self._selectionSortValue = nil
self._selectionAuctionId = nil
end
return self
end
--- Selects the next row.
-- @tparam MyAuctionsScrollingTable self The my auctions scrolling table object
-- @treturn MyAuctionsScrollingTable The my auctions scrolling table object
function MyAuctionsScrollingTable.SelectNextRow(self)
local newSelection = nil
for i = 1, #self._data - 1 do
if self._data[i] == self._selection then
for j = i + 1, #self._data do
if not self._selectionValidator or self:_selectionValidator(self._query:GetResultRowByUUID(self._data[j])) then
newSelection = self._data[j]
break
end
end
break
end
end
self:SetSelection(newSelection, true)
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function MyAuctionsScrollingTable._UpdateSortFromQuery(self)
if self._tableInfo:_IsSortEnabled() then
assert(not TSM.IsWowClassic())
local sortField, sortAscending = self._query:GetOrderBy(2)
if sortField then
self._sortCol = self._tableInfo:_GetIdBySortField(sortField)
self._sortAscending = sortAscending
else
self._sortCol = nil
self._sortAscending = nil
end
end
end
function MyAuctionsScrollingTable._UpdateData(self)
-- we need to fix up the data within the rows updated to avoid errors with trying to access old DatabaseQueryResultRows
local prevRowIndex = TempTable.Acquire()
local newRowData = TempTable.Acquire()
for i, row in ipairs(self._rows) do
if row:IsVisible() then
prevRowIndex[row:GetData()] = i
end
end
local prevSelection = self._selection
wipe(self._data)
self._selection = nil
for _, uuid in self._query:UUIDIterator() do
local row = self._query:GetResultRowByUUID(uuid)
if (uuid == prevSelection or (row:GetField("auctionId") == self._selectionAuctionId)) and not row:GetField("isPending") then
self._selection = uuid
end
if prevRowIndex[uuid] then
newRowData[prevRowIndex[uuid]] = uuid
end
tinsert(self._data, uuid)
end
for i, row in ipairs(self._rows) do
if row:IsVisible() then
if newRowData[i] then
row:SetData(newRowData[i])
else
row:ClearData()
end
end
end
TempTable.Release(prevRowIndex)
TempTable.Release(newRowData)
if prevSelection and not self._selection then
local newSelection = nil
-- try to select the next row based on the sorting
local sortField = TSM.IsWowClassic() and "index" or self._tableInfo:_GetSortFieldById(self._sortCol)
local sortAscending = not TSM.IsWowClassic() and self._sortAscending
for _, uuid in ipairs(self._data) do
local row = self._query:GetResultRowByUUID(uuid)
local sortValue = row:GetField(sortField)
if type(sortValue) == "string" then
sortValue = strlower(sortValue)
end
if (sortAscending and sortValue > self._selectionSortValue) or (not sortAscending and sortValue < self._selectionSortValue) then
if not self._selectionValidator or self:_selectionValidator(row) then
newSelection = uuid
break
end
elseif not TSM.IsWowClassic() and sortValue == self._selectionSortValue and row:GetField("auctionId") > self._selectionAuctionId then
if not self._selectionValidator or self:_selectionValidator(row) then
newSelection = uuid
break
end
end
end
-- select either the next row
self:SetSelection(newSelection)
end
if self._onDataUpdated then
self:_onDataUpdated()
end
end
function MyAuctionsScrollingTable._ToggleSort(self, id)
local sortField = self._tableInfo:_GetSortFieldById(id)
if not self._sortCol or not self._query or not sortField then
-- sorting disabled so ignore
return
end
if id == self._sortCol then
self._sortAscending = not self._sortAscending
else
self._sortCol = id
self._sortAscending = true
end
assert(not TSM.IsWowClassic())
self._query:ResetOrderBy()
:OrderBy("saleStatus", false)
:OrderBy(sortField, self._sortAscending)
:OrderBy("auctionId", true)
self:_UpdateData()
self:Draw()
end

View File

@@ -0,0 +1,288 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- OperationTree UI Element Class.
-- The operation tree is used to display operations grouped by module and allows for adding, duplicating, and deleting
-- them. Only one module is allowed to be expanded at a time. It is a subclass of the @{ScrollingTable} class.
-- @classmod OperationTree
local _, TSM = ...
local OperationTree = TSM.Include("LibTSMClass").DefineClass("OperationTree", TSM.UI.ScrollingTable)
local L = TSM.Include("Locale").GetTable()
local Theme = TSM.Include("Util.Theme")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(OperationTree)
TSM.UI.OperationTree = OperationTree
local private = {}
local DATA_SEP = "\001"
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function OperationTree.__init(self)
self.__super:__init()
self:SetRowHeight(28)
self._operationNameFilter = ""
self._selected = nil
self._expandedModule = nil
self._selectedOperation = nil
self._prevSelectedOperation = nil
self._onOperationSelectedHandler = nil
self._onOperationAddedHandler = nil
self._onOperationDeletedHandler = nil
end
function OperationTree.Acquire(self)
self._backgroundColor = "PRIMARY_BG_ALT"
self._headerHidden = true
self.__super:Acquire()
self:GetScrollingTableInfo()
:NewColumn("text")
:SetFont("BODY_BODY2_MEDIUM")
:SetJustifyH("LEFT")
:SetTextFunction(private.GetText)
:SetExpanderStateFunction(private.GetExpanderState)
:SetActionIconInfo(2, 14, private.GetActionIcon)
:SetActionIconClickHandler(private.OnActionIconClick)
:DisableHiding()
:Commit()
:Commit()
self:UpdateData()
end
function OperationTree.Release(self)
for _, row in ipairs(self._rows) do
ScriptWrapper.Clear(row._frame, "OnDoubleClick")
end
self._selected = nil
self._operationNameFilter = ""
self._expandedModule = nil
self._selectedOperation = nil
self._prevSelectedOperation = nil
self._onOperationSelectedHandler = nil
self._onOperationAddedHandler = nil
self._onOperationDeletedHandler = nil
self.__super:Release()
self:SetRowHeight(28)
end
--- Sets the operation name filter.
-- @tparam OperationTree self The operation tree object
-- @tparam string filter The filter string (any operations which don't match this are hidden)
function OperationTree.SetOperationNameFilter(self, filter)
self._operationNameFilter = filter
if filter == "" and self._prevSelectedOperation and not self._selectedOperation then
-- restore any previous selection if we don't have something selected
self:SetSelectedOperation(self:_SplitOperationKey(self._prevSelectedOperation))
self._prevSelectedOperation = nil
elseif filter ~= "" and self._selectedOperation then
local _, operationName = self:_SplitOperationKey(self._selectedOperation)
if not operationName or not strmatch(strlower(operationName), filter) then
-- save the current selection to restore after the filter is cleared and then clear the current selection
self._prevSelectedOperation = self._selectedOperation
self:SetSelectedOperation()
end
end
self:UpdateData(true)
end
--- Registers a script handler.
-- @tparam OperationTree self The operation tree object
-- @tparam string script The script to register for (supported scripts: `OnOperationSelected`, `OnOperationAdded`,
-- `OnOperationDeleted`)
-- @tparam function handler The script handler which will be called with the operation tree object followed by any
-- arguments to the script
-- @treturn OperationTree The operation tree object
function OperationTree.SetScript(self, script, handler)
if script == "OnOperationSelected" then
self._onOperationSelectedHandler = handler
elseif script == "OnOperationAdded" then
self._onOperationAddedHandler = handler
elseif script == "OnOperationDeleted" then
self._onOperationDeletedHandler = handler
else
error("Unknown OperationTree script: "..tostring(script))
end
return self
end
--- Sets the selected operation.
-- @tparam OperationTree self The operation tree object
-- @tparam string moduleName The name of the module which the operation belongs to
-- @tparam string operationName The name of the operation
-- @treturn OperationTree The operation tree object
function OperationTree.SetSelectedOperation(self, moduleName, operationName)
if moduleName and operationName then
self._selectedOperation = moduleName..DATA_SEP..operationName
self._expandedModule = moduleName
elseif moduleName then
self._selectedOperation = moduleName
self._expandedModule = moduleName
else
self._selectedOperation = nil
self._expandedModule = nil
end
self:UpdateData()
self.__super:SetSelection(self._selectedOperation, true)
if self._onOperationSelectedHandler then
self:_onOperationSelectedHandler(moduleName, operationName)
end
self:_ForceLastDataUpdate()
self:UpdateData(true)
return self
end
function OperationTree.SetSelection(self, data)
self:SetSelectedOperation(self:_SplitOperationKey(data))
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function OperationTree._GetTableRow(self, isHeader)
local row = self.__super:_GetTableRow(isHeader)
if not isHeader then
ScriptWrapper.Set(row._frame, "OnDoubleClick", private.RowOnDoubleClick, row)
end
return row
end
function OperationTree._IsDataHidden(self, data)
local moduleName, operationName = self:_SplitOperationKey(data)
if operationName and not strmatch(strlower(operationName), self._operationNameFilter) then
return true
elseif operationName and moduleName ~= self._expandedModule then
return true
end
return false
end
function OperationTree._SplitOperationKey(self, data)
local moduleName, operationName = strmatch(data, "([^"..DATA_SEP.."]+)"..DATA_SEP.."?(.*)")
operationName = operationName ~= "" and operationName or nil
return moduleName, operationName
end
function OperationTree._UpdateData(self)
wipe(self._data)
for _, moduleName in TSM.Operations.ModuleIterator() do
if not self:_IsDataHidden(moduleName) then
tinsert(self._data, moduleName)
end
for _, operationName in TSM.Operations.OperationIterator(moduleName) do
local data = moduleName..DATA_SEP..operationName
if not self:_IsDataHidden(data) then
tinsert(self._data, data)
end
end
end
end
function OperationTree._HandleRowClick(self)
self:Draw()
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.GetText(self, data)
local moduleName, operationName = self:_SplitOperationKey(data)
local color = Theme.GetColor(operationName and "TEXT" or "INDICATOR")
return color:ColorText(operationName or moduleName.." "..L["Operations"])
end
function private.GetExpanderState(self, data)
local moduleName, operationName = self:_SplitOperationKey(data)
return not operationName, self._expandedModule == moduleName, operationName and 1 or 0
end
function private.GetActionIcon(self, data, iconIndex, isMouseOver)
local _, operationName = self:_SplitOperationKey(data)
if iconIndex == 1 then
if operationName and data == self._selectedOperation then
local texturePack = "iconPack.14x14/Duplicate"
return true, isMouseOver and TSM.UI.TexturePacks.GetColoredKey(texturePack, Theme.GetColor("INDICATOR")) or texturePack
elseif operationName then
return false, nil
else
local texturePack = "iconPack.14x14/Add/Circle"
return true, isMouseOver and TSM.UI.TexturePacks.GetColoredKey(texturePack, Theme.GetColor("INDICATOR")) or texturePack
end
elseif iconIndex == 2 then
if operationName and data == self._selectedOperation then
local texturePack = "iconPack.14x14/Delete"
return true, isMouseOver and TSM.UI.TexturePacks.GetColoredKey(texturePack, Theme.GetColor("INDICATOR")) or texturePack
else
return false, nil
end
else
error("Invalid index: "..tostring(iconIndex))
end
end
function private.OnActionIconClick(self, data, iconIndex)
local moduleName, operationName = self:_SplitOperationKey(data)
if iconIndex == 1 then
if operationName then
-- duplicate
local num = 1
while TSM.Operations.Exists(moduleName, operationName.." "..num) do
num = num + 1
end
local newOperationName = operationName.." "..num
self:_onOperationAddedHandler(moduleName, newOperationName, operationName)
self:UpdateData()
self:SetSelectedOperation(moduleName, newOperationName)
else
-- add
operationName = "New Operation"
local num = 1
while TSM.Operations.Exists(moduleName, operationName.." "..num) do
num = num + 1
end
operationName = operationName .. " " .. num
self._expandedModule = moduleName
self:_onOperationAddedHandler(moduleName, operationName)
self:UpdateData()
self:SetSelectedOperation(moduleName, operationName)
end
self:Draw()
elseif iconIndex == 2 then
assert(operationName)
-- delete
self:_onOperationDeletedHandler(moduleName, operationName)
self:UpdateData(true)
else
error("Invalid index: "..tostring(iconIndex))
end
end
function private.RowOnDoubleClick(row, mouseButton)
if mouseButton ~= "LeftButton" then
return
end
local self = row._scrollingTable
local data = row:GetData()
local moduleName, operationName = self:_SplitOperationKey(data)
if operationName then
return
end
if moduleName == self._selectedOperation then
self:SetSelectedOperation()
else
self:SetSelectedOperation(moduleName, operationName)
end
end

View File

@@ -0,0 +1,186 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- OverlayApplicationFrame UI Element Class.
-- The overlay application frame is currently just used for the TaskListUI. It is a subclass of the @{Frame} class.
-- @classmod OverlayApplicationFrame
local _, TSM = ...
local Theme = TSM.Include("Util.Theme")
local UIElements = TSM.Include("UI.UIElements")
local OverlayApplicationFrame = TSM.Include("LibTSMClass").DefineClass("OverlayApplicationFrame", TSM.UI.Frame)
UIElements.Register(OverlayApplicationFrame)
TSM.UI.OverlayApplicationFrame = OverlayApplicationFrame
local private = {}
local TITLE_HEIGHT = 40
local CONTENT_PADDING_BOTTOM = 16
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function OverlayApplicationFrame.__init(self)
self.__super:__init()
self._contentFrame = nil
self._contextTable = nil
self._defaultContextTable = nil
Theme.RegisterChangeCallback(function()
if self:IsVisible() then
self:Draw()
end
end)
end
function OverlayApplicationFrame.Acquire(self)
local frame = self:_GetBaseFrame()
frame:EnableMouse(true)
frame:SetMovable(true)
frame:RegisterForDrag("LeftButton")
self:AddChildNoLayout(UIElements.New("Button", "closeBtn")
:AddAnchor("TOPRIGHT", -8, -11)
:SetBackgroundAndSize("iconPack.18x18/Close/Circle")
:SetScript("OnClick", private.CloseButtonOnClick)
)
self:AddChildNoLayout(UIElements.New("Button", "minimizeBtn")
:AddAnchor("TOPRIGHT", -26, -11)
:SetBackgroundAndSize("iconPack.18x18/Subtract/Circle")
:SetScript("OnClick", private.MinimizeBtnOnClick)
)
self:AddChildNoLayout(UIElements.New("Text", "title")
:SetHeight(24)
:SetFont("BODY_BODY1_BOLD")
:AddAnchor("TOPLEFT", 8, -8)
:AddAnchor("TOPRIGHT", -52, -8)
)
self:SetScript("OnDragStart", private.FrameOnDragStart)
self:SetScript("OnDragStop", private.FrameOnDragStop)
self.__super:Acquire()
end
function OverlayApplicationFrame.Release(self)
self._contentFrame = nil
self._contextTable = nil
self._defaultContextTable = nil
self:_GetBaseFrame():SetMinResize(0, 0)
self.__super:Release()
end
--- Sets the title text.
-- @tparam OverlayApplicationFrame self The overlay application frame object
-- @tparam string title The title text
-- @treturn OverlayApplicationFrame The overlay application frame object
function OverlayApplicationFrame.SetTitle(self, title)
self:GetElement("title"):SetText(title)
return self
end
--- Sets the content frame.
-- @tparam OverlayApplicationFrame self The overlay application frame object
-- @tparam Element frame The content frame
-- @treturn OverlayApplicationFrame The overlay application frame object
function OverlayApplicationFrame.SetContentFrame(self, frame)
frame:WipeAnchors()
frame:AddAnchor("TOPLEFT", 0, -TITLE_HEIGHT)
frame:AddAnchor("BOTTOMRIGHT", 0, CONTENT_PADDING_BOTTOM)
self._contentFrame = frame
self:AddChildNoLayout(frame)
return self
end
--- Sets the context table.
-- This table can be used to preserve position information across lifecycles of the frame and even WoW sessions if it's
-- within the settings DB.
-- @tparam OverlayApplicationFrame self The overlay application frame object
-- @tparam table tbl The context table
-- @tparam table defaultTbl The default values (required fields: `minimized`, `topRightX`, `topRightY`)
-- @treturn OverlayApplicationFrame The overlay application frame object
function OverlayApplicationFrame.SetContextTable(self, tbl, defaultTbl)
assert(defaultTbl.minimized ~= nil and defaultTbl.topRightX and defaultTbl.topRightY)
if tbl.minimized == nil then
tbl.minimized = defaultTbl.minimized
end
tbl.topRightX = tbl.topRightX or defaultTbl.topRightX
tbl.topRightY = tbl.topRightY or defaultTbl.topRightY
self._contextTable = tbl
self._defaultContextTable = defaultTbl
return self
end
--- Sets the context table from a settings object.
-- @tparam OverlayApplicationFrame self The overlay application frame object
-- @tparam Settings settings The settings object
-- @tparam string key The setting key
-- @treturn OverlayApplicationFrame The overlay application frame object
function OverlayApplicationFrame.SetSettingsContext(self, settings, key)
return self:SetContextTable(settings[key], settings:GetDefaultReadOnly(key))
end
function OverlayApplicationFrame.Draw(self)
if self._contextTable.minimized then
self:GetElement("minimizeBtn"):SetBackgroundAndSize("iconPack.18x18/Add/Circle")
self:GetElement("content"):Hide()
self:SetHeight(TITLE_HEIGHT)
else
self:GetElement("minimizeBtn"):SetBackgroundAndSize("iconPack.18x18/Subtract/Circle")
self:GetElement("content"):Show()
-- set the height of the frame based on the height of the children
local contentHeight, contentHeightExpandable = self:GetElement("content"):_GetMinimumDimension("HEIGHT")
assert(not contentHeightExpandable)
self:SetHeight(contentHeight + TITLE_HEIGHT + CONTENT_PADDING_BOTTOM)
end
-- make sure the frame is on the screen
self._contextTable.topRightX = max(min(self._contextTable.topRightX, 0), -UIParent:GetWidth() + 100)
self._contextTable.topRightY = max(min(self._contextTable.topRightY, 0), -UIParent:GetHeight() + 100)
-- set the frame position from the contextTable
self:WipeAnchors()
self:AddAnchor("TOPRIGHT", self._contextTable.topRightX, self._contextTable.topRightY)
self.__super:Draw()
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function OverlayApplicationFrame._SavePosition(self)
local frame = self:_GetBaseFrame()
local parentFrame = frame:GetParent()
self._contextTable.topRightX = frame:GetRight() - parentFrame:GetRight()
self._contextTable.topRightY = frame:GetTop() - parentFrame:GetTop()
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.FrameOnDragStart(self)
self:_GetBaseFrame():StartMoving()
end
function private.FrameOnDragStop(self)
local frame = self:_GetBaseFrame()
frame:StopMovingOrSizing()
self:_SavePosition()
end
function private.CloseButtonOnClick(button)
button:GetParentElement():Hide()
end
function private.MinimizeBtnOnClick(button)
local self = button:GetParentElement()
self._contextTable.minimized = not self._contextTable.minimized
self:Draw()
end

View File

@@ -0,0 +1,103 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- PlayerGoldText UI Element Class.
-- A text element which contains player gold info which automatically updates when the player's gold amount changes. It
-- is a subclass of the @{Text} class.
-- @classmod PlayerGoldText
local _, TSM = ...
local PlayerGoldText = TSM.Include("LibTSMClass").DefineClass("PlayerGoldText", TSM.UI.Text)
local L = TSM.Include("Locale").GetTable()
local TempTable = TSM.Include("Util.TempTable")
local Event = TSM.Include("Util.Event")
local Money = TSM.Include("Util.Money")
local Settings = TSM.Include("Service.Settings")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(PlayerGoldText)
TSM.UI.PlayerGoldText = PlayerGoldText
local private = {
registered = false,
elements = {},
}
-- ============================================================================
-- Meta Class Methods
-- ============================================================================
function PlayerGoldText.__init(self)
self.__super:__init()
self:_GetBaseFrame():EnableMouse(true)
if not private.registered then
Event.Register("PLAYER_MONEY", private.MoneyOnUpdate)
private.registered = true
end
self._justifyH = "RIGHT"
self._font = "TABLE_TABLE1"
end
function PlayerGoldText.Acquire(self)
private.elements[self] = true
self.__super:Acquire()
self:SetText(Money.ToString(TSM.db.global.appearanceOptions.showTotalMoney and private.GetTotalMoney() or GetMoney()))
self:SetTooltip(private.MoneyTooltipFunc)
end
function PlayerGoldText.Release(self)
private.elements[self] = nil
self.__super:Release()
self._justifyH = "RIGHT"
self._font = "TABLE_TABLE1"
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.MoneyOnUpdate()
for element in pairs(private.elements) do
element:SetText(Money.ToString(TSM.db.global.appearanceOptions.showTotalMoney and private.GetTotalMoney() or GetMoney()))
element:Draw()
end
end
function private.MoneyTooltipFunc()
local tooltipLines = TempTable.Acquire()
local playerMoney = TSM.db.sync.internalData.money
local total = playerMoney
tinsert(tooltipLines, strjoin(TSM.CONST.TOOLTIP_SEP, UnitName("player")..":", Money.ToString(playerMoney)))
local numPosted, numSold, postedGold, soldGold = TSM.MyAuctions.GetAuctionInfo()
if numPosted then
tinsert(tooltipLines, " "..strjoin(TSM.CONST.TOOLTIP_SEP, format(L["%s Sold Auctions"], numSold)..":", Money.ToString(soldGold)))
tinsert(tooltipLines, " "..strjoin(TSM.CONST.TOOLTIP_SEP, format(L["%s Posted Auctions"], numPosted)..":", Money.ToString(postedGold)))
end
for _, _, character, syncScopeKey in Settings.ConnectedFactionrealmAltCharacterIterator() do
local money = Settings.Get("sync", syncScopeKey, "internalData", "money")
if money > 0 then
tinsert(tooltipLines, strjoin(TSM.CONST.TOOLTIP_SEP, character..":", Money.ToString(money)))
total = total + money
end
end
tinsert(tooltipLines, 1, strjoin(TSM.CONST.TOOLTIP_SEP, L["Total Gold"]..":", Money.ToString(total)))
return strjoin("\n", TempTable.UnpackAndRelease(tooltipLines))
end
function private.GetTotalMoney()
local total = TSM.db.sync.internalData.money
for _, _, _, syncScopeKey in Settings.ConnectedFactionrealmAltCharacterIterator() do
local money = Settings.Get("sync", syncScopeKey, "internalData", "money")
if money > 0 then
total = total + money
end
end
return total
end

View File

@@ -0,0 +1,35 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- PopupFrame UI Element Class.
-- A popup frame which shows when clicking on a "more" button.
-- @classmod PopupFrame
local _, TSM = ...
local NineSlice = TSM.Include("Util.NineSlice")
local Theme = TSM.Include("Util.Theme")
local PopupFrame = TSM.Include("LibTSMClass").DefineClass("PopupFrame", TSM.UI.Frame)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(PopupFrame)
TSM.UI.PopupFrame = PopupFrame
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function PopupFrame.__init(self)
self.__super:__init()
self._nineSlice = NineSlice.New(self:_GetBaseFrame())
end
function PopupFrame.Draw(self)
self.__super:Draw()
self._nineSlice:SetStyle("popup")
-- TOOD: fix the texture color properly
self._nineSlice:SetPartVertexColor("center", Theme.GetColor("PRIMARY_BG_ALT", "duskwood"):GetFractionalRGBA())
end

View File

@@ -0,0 +1,615 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- ProfessionScrollingTable UI Element Class.
-- This is used to display the crafts within the currently-selected profession in the CraftingUI. It is a subclass of
-- the @{ScrollingTable} class.
-- @classmod ProfessionScrollingTable
local _, TSM = ...
local L = TSM.Include("Locale").GetTable()
local TempTable = TSM.Include("Util.TempTable")
local Money = TSM.Include("Util.Money")
local Theme = TSM.Include("Util.Theme")
local Log = TSM.Include("Util.Log")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local Event = TSM.Include("Util.Event")
local ItemInfo = TSM.Include("Service.ItemInfo")
local Tooltip = TSM.Include("UI.Tooltip")
local ProfessionScrollingTable = TSM.Include("LibTSMClass").DefineClass("ProfessionScrollingTable", TSM.UI.ScrollingTable)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(ProfessionScrollingTable)
TSM.UI.ProfessionScrollingTable = ProfessionScrollingTable
local private = {
activeElements = {},
categoryInfoCache = {
parent = {},
numIndents = {},
name = {},
currentSkillLevel = {},
maxSkillLevel = {},
},
}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function ProfessionScrollingTable.__init(self)
self.__super:__init()
self._query = nil
self._isSpellId = {}
self._favoritesContextTable = nil
end
function ProfessionScrollingTable.Acquire(self)
self.__super:Acquire()
self:GetScrollingTableInfo()
:SetMenuInfo(private.MenuIterator, private.MenuClickHandler)
:NewColumn("name")
:SetTitle(L["Recipe Name"])
:SetFont("ITEM_BODY3")
:SetJustifyH("LEFT")
:SetTextFunction(private.GetNameCellText)
:SetExpanderStateFunction(private.GetExpanderState)
:SetActionIconInfo(1, 14, private.GetFavoriteIcon, true)
:SetActionIconClickHandler(private.OnFavoriteIconClick)
:DisableHiding()
:Commit()
:NewColumn("qty")
:SetTitle(L["Craft"])
:SetFont("BODY_BODY3_MEDIUM")
:SetJustifyH("CENTER")
:SetTextFunction(private.GetQtyCellText)
:Commit()
if not TSM.IsWowClassic() then
self:GetScrollingTableInfo()
:NewColumn("rank")
:SetTitle(RANK)
:SetFont("BODY_BODY3_MEDIUM")
:SetJustifyH("CENTER")
:SetTextFunction(private.GetRankCellText)
:Commit()
end
self:GetScrollingTableInfo()
:NewColumn("craftingCost")
:SetTitle(L["Crafting Cost"])
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetTextFunction(private.GetCraftingCostCellText)
:Commit()
:NewColumn("itemValue")
:SetTitle(L["Item Value"])
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetTextFunction(private.GetItemValueCellIndex)
:Commit()
:NewColumn("profit")
:SetTitle(L["Profit"])
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetTextFunction(private.GetProfitCellText)
:Commit()
:NewColumn("profitPct")
:SetTitle("%")
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetTextFunction(private.GetProfitPctCellText)
:Commit()
:NewColumn("saleRate")
:SetTitleIcon("iconPack.14x14/SaleRate")
:SetFont("TABLE_TABLE1")
:SetJustifyH("RIGHT")
:SetTextFunction(private.GetSaleRateCellText)
:Commit()
:Commit()
if not next(private.activeElements) then
Event.Register("CHAT_MSG_SKILL", private.OnChatMsgSkill)
end
private.activeElements[self] = true
end
function ProfessionScrollingTable.Release(self)
private.activeElements[self] = nil
if not next(private.activeElements) then
Event.Unregister("CHAT_MSG_SKILL", private.OnChatMsgSkill)
end
if self._query then
self._query:SetUpdateCallback()
self._query = nil
end
wipe(self._isSpellId)
self._favoritesContextTable = nil
for _, row in ipairs(self._rows) do
ScriptWrapper.Clear(row._frame, "OnDoubleClick")
end
self.__super:Release()
end
-- ============================================================================
-- Public Class Methods
-- ============================================================================
--- Sets the @{DatabaseQuery} source for this table.
-- This query is used to populate the entries in the profession scrolling table.
-- @tparam ProfessionScrollingTable self The profession scrolling table object
-- @tparam DatabaseQuery query The query object
-- @tparam[opt=false] bool redraw Whether or not to redraw the scrolling table
-- @treturn ProfessionScrollingTable The profession scrolling table object
function ProfessionScrollingTable.SetQuery(self, query, redraw)
if query == self._query and not redraw then
return self
end
if self._query then
self._query:SetUpdateCallback()
end
self._query = query
self._query:SetUpdateCallback(private.QueryUpdateCallback, self)
self:_ForceLastDataUpdate()
self:UpdateData(redraw)
return self
end
--- Sets the context table to use to store favorite craft information.
-- @tparam ProfessionScrollingTable self The profession scrolling table object
-- @tparam table tbl The context table
-- @treturn ProfessionScrollingTable The profession scrolling table object
function ProfessionScrollingTable.SetFavoritesContext(self, tbl)
assert(type(tbl) == "table")
self._favoritesContextTable = tbl
return self
end
--- Sets the context table.
-- @tparam ProfessionScrollingTable self The profession scrolling table object
-- @tparam table tbl The context table
-- @tparam table defaultTbl The default table (required fields: `colWidth`, `colHidden`, `collapsed`)
-- @treturn ProfessionScrollingTable The profession scrolling table object
function ProfessionScrollingTable.SetContextTable(self, tbl, defaultTbl)
assert(type(defaultTbl.collapsed) == "table")
tbl.collapsed = tbl.collapsed or CopyTable(defaultTbl.collapsed)
self.__super:SetContextTable(tbl, defaultTbl)
return self
end
function ProfessionScrollingTable.IsSpellIdVisible(self, spellId)
if not self._isSpellId[spellId] then
-- this spellId isn't included in the query
return false
end
local categoryId = TSM.Crafting.ProfessionScanner.GetCategoryIdBySpellId(spellId)
return not self:_IsCategoryHidden(categoryId) and not self._contextTable.collapsed[categoryId]
end
function ProfessionScrollingTable.Draw(self)
if self._lastDataUpdate == nil then
self:_IgnoreLastDataUpdate()
end
self.__super:Draw()
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function ProfessionScrollingTable._ToggleCollapsed(self, categoryId)
self._contextTable.collapsed[categoryId] = not self._contextTable.collapsed[categoryId] or nil
if self._selection and not self:IsSpellIdVisible(self._selection) then
self:SetSelection(nil, true)
end
end
function ProfessionScrollingTable._GetTableRow(self, isHeader)
local row = self.__super:_GetTableRow(isHeader)
if not isHeader then
ScriptWrapper.Set(row._frame, "OnClick", private.RowOnClick, row)
ScriptWrapper.Set(row._frame, "OnDoubleClick", private.RowOnClick, row)
local rankBtn = row:_GetButton()
rankBtn:SetAllPoints(row._texts.rank)
ScriptWrapper.SetPropagate(rankBtn, "OnClick")
ScriptWrapper.Set(rankBtn, "OnEnter", private.RankOnEnter, row)
ScriptWrapper.Set(rankBtn, "OnLeave", private.RankOnLeave, row)
row._buttons.rank = rankBtn
end
return row
end
function ProfessionScrollingTable._UpdateData(self)
local currentCategoryPath = TempTable.Acquire()
local foundSelection = false
-- populate the data
wipe(self._data)
wipe(self._isSpellId)
for _, spellId in self._query:Iterator() do
if self._favoritesContextTable[spellId] then
local categoryId = -1
if categoryId ~= currentCategoryPath[#currentCategoryPath] then
-- this is a new category
local newCategoryPath = TempTable.Acquire()
tinsert(newCategoryPath, 1, categoryId)
-- create new category headers
if currentCategoryPath[1] ~= categoryId then
if not self:_IsCategoryHidden(categoryId) then
tinsert(self._data, categoryId)
end
end
TempTable.Release(currentCategoryPath)
currentCategoryPath = newCategoryPath
end
foundSelection = foundSelection or spellId == self:GetSelection()
if not self._contextTable.collapsed[categoryId] and not self:_IsCategoryHidden(categoryId) then
tinsert(self._data, spellId)
self._isSpellId[spellId] = true
end
end
end
for _, spellId, categoryId in self._query:Iterator() do
if not self._favoritesContextTable[spellId] then
if categoryId ~= currentCategoryPath[#currentCategoryPath] then
-- this is a new category
local newCategoryPath = TempTable.Acquire()
local currentCategoryId = categoryId
while currentCategoryId do
tinsert(newCategoryPath, 1, currentCategoryId)
currentCategoryId = private.CategoryGetParentCategoryId(currentCategoryId)
end
-- create new category headers
for i = 1, #newCategoryPath do
local newCategoryId = newCategoryPath[i]
if currentCategoryPath[i] ~= newCategoryId then
if not self:_IsCategoryHidden(newCategoryId) then
tinsert(self._data, newCategoryId)
end
end
end
TempTable.Release(currentCategoryPath)
currentCategoryPath = newCategoryPath
end
foundSelection = foundSelection or spellId == self:GetSelection()
if not self._contextTable.collapsed[categoryId] and not self:_IsCategoryHidden(categoryId) then
tinsert(self._data, spellId)
self._isSpellId[spellId] = true
end
end
end
TempTable.Release(currentCategoryPath)
if not foundSelection then
-- try to select the first visible spellId
local newSelection = nil
for _, data in ipairs(self._data) do
if not newSelection and self._isSpellId[data] then
newSelection = data
end
end
self:SetSelection(newSelection, true)
end
end
function ProfessionScrollingTable._IsCategoryHidden(self, categoryId)
if private.IsFavoriteCategory(categoryId) then
return false
end
local parent = private.CategoryGetParentCategoryId(categoryId)
while parent do
if self._contextTable.collapsed[parent] then
return true
end
parent = private.CategoryGetParentCategoryId(parent)
end
return false
end
function ProfessionScrollingTable._SetRowData(self, row, data)
local rank = self._isSpellId[data] and TSM.Crafting.ProfessionScanner.GetRankBySpellId(data) or -1
if rank == -1 then
row._buttons.rank:Hide()
else
row._buttons.rank:Show()
end
self.__super:_SetRowData(row, data)
end
function ProfessionScrollingTable._ToggleSort(self, id)
-- do nothing
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.PopulateCategoryInfoCache(categoryId)
-- numIndents always gets set, so use that to know whether or not this category is already cached
if not private.categoryInfoCache.numIndents[categoryId] then
local name, numIndents, parentCategoryId, currentSkillLevel, maxSkillLevel = TSM.Crafting.ProfessionUtil.GetCategoryInfo(categoryId)
private.categoryInfoCache.name[categoryId] = name
private.categoryInfoCache.numIndents[categoryId] = numIndents
private.categoryInfoCache.parent[categoryId] = parentCategoryId
private.categoryInfoCache.currentSkillLevel[categoryId] = currentSkillLevel
private.categoryInfoCache.maxSkillLevel[categoryId] = maxSkillLevel
end
end
function private.CategoryGetParentCategoryId(categoryId)
private.PopulateCategoryInfoCache(categoryId)
return private.categoryInfoCache.parent[categoryId]
end
function private.CategoryGetNumIndents(categoryId)
private.PopulateCategoryInfoCache(categoryId)
return private.categoryInfoCache.numIndents[categoryId]
end
function private.CategoryGetName(categoryId)
private.PopulateCategoryInfoCache(categoryId)
return private.categoryInfoCache.name[categoryId]
end
function private.CategoryGetSkillLevel(categoryId)
private.PopulateCategoryInfoCache(categoryId)
return private.categoryInfoCache.currentSkillLevel[categoryId], private.categoryInfoCache.maxSkillLevel[categoryId]
end
function private.IsFavoriteCategory(categoryId)
return categoryId == -1
end
function private.QueryUpdateCallback(_, _, self)
self:_ForceLastDataUpdate()
self:UpdateData(true)
end
function private.MenuIterator(self, prevIndex)
if prevIndex == "CREATE_GROUPS" then
-- we're done
return
else
return "CREATE_GROUPS", L["Create Groups from Table"]
end
end
function private.MenuClickHandler(self, index1, index2)
if index1 == "CREATE_GROUPS" then
assert(not index2)
self:GetBaseElement():HideDialog()
local numCreated, numAdded = 0, 0
for _, spellId in self._query:Iterator() do
local itemString = TSM.Crafting.GetItemString(spellId)
if itemString then
local groupPath = private.GetCategoryGroupPath(TSM.Crafting.ProfessionScanner.GetCategoryIdBySpellId(spellId))
if not TSM.Groups.Exists(groupPath) then
TSM.Groups.Create(groupPath)
numCreated = numCreated + 1
end
if not TSM.Groups.IsItemInGroup(itemString) and not ItemInfo.IsSoulbound(itemString) then
TSM.Groups.SetItemGroup(itemString, groupPath)
numAdded = numAdded + 1
end
end
end
Log.PrintfUser(L["%d groups were created and %d items were added from the table."], numCreated, numAdded)
else
error("Unexpected index1: "..tostring(index1))
end
end
function private.GetCategoryGroupPath(categoryId)
local parts = TempTable.Acquire()
while categoryId do
tinsert(parts, 1, private.categoryInfoCache.name[categoryId])
categoryId = private.categoryInfoCache.parent[categoryId]
end
tinsert(parts, 1, TSM.Crafting.ProfessionUtil.GetCurrentProfessionName())
return TSM.Groups.Path.Join(TempTable.UnpackAndRelease(parts))
end
function private.GetNameCellText(self, data)
if self._isSpellId[data] then
local name = TSM.Crafting.ProfessionScanner.GetNameBySpellId(data)
local color = nil
if TSM.Crafting.ProfessionUtil.IsGuildProfession() then
color = Theme.GetProfessionDifficultyColor("easy")
elseif TSM.Crafting.ProfessionUtil.IsNPCProfession() then
color = Theme.GetProfessionDifficultyColor("nodifficulty")
else
local difficulty = TSM.Crafting.ProfessionScanner.GetDifficultyBySpellId(data)
color = Theme.GetProfessionDifficultyColor(difficulty)
end
return color:ColorText(name)
else
-- this is a category
local name = nil
if private.IsFavoriteCategory(data) then
name = L["Favorited Patterns"]
else
local currentSkillLevel, maxSkillLevel = private.CategoryGetSkillLevel(data)
name = private.CategoryGetName(data)
if name and currentSkillLevel and maxSkillLevel then
name = name.." ("..currentSkillLevel.."/"..maxSkillLevel..")"
end
end
if not name then
-- happens if we're switching to another profession
return "?"
end
if private.IsFavoriteCategory(data) or private.CategoryGetNumIndents(data) == 0 then
return Theme.GetColor("INDICATOR"):ColorText(name)
else
return Theme.GetColor("INDICATOR_ALT"):ColorText(name)
end
end
end
function private.GetExpanderState(self, data)
local indentLevel = 0
if self._isSpellId[data] then
indentLevel = 2
elseif not private.IsFavoriteCategory(data) then
indentLevel = private.CategoryGetNumIndents(data) * 2
end
return not self._isSpellId[data], not self._contextTable.collapsed[data], -indentLevel
end
function private.GetFavoriteIcon(self, data, iconIndex, isMouseOver)
if iconIndex == 1 then
if not self._isSpellId[data] then
return false, nil, true
else
return true, self._favoritesContextTable[data] and "iconPack.12x12/Star/Filled" or "iconPack.12x12/Star/Unfilled", true
end
else
error("Invalid index: "..tostring(iconIndex))
end
end
function private.OnFavoriteIconClick(self, data, iconIndex)
if iconIndex == 1 then
if self._isSpellId[data] and private.IsPlayerProfession() then
self._favoritesContextTable[data] = not self._favoritesContextTable[data] or nil
self:_ForceLastDataUpdate()
self:UpdateData(true)
end
else
error("Invalid index: "..tostring(iconIndex))
end
end
function private.GetQtyCellText(self, data)
if not self._isSpellId[data] then
return ""
end
local num, numAll = TSM.Crafting.ProfessionUtil.GetNumCraftable(data)
if num == numAll then
if num > 0 then
return Theme.GetFeedbackColor("GREEN"):ColorText(num)
end
return tostring(num)
else
if num > 0 then
return Theme.GetFeedbackColor("GREEN"):ColorText(num.."-"..numAll)
elseif numAll > 0 then
return Theme.GetFeedbackColor("YELLOW"):ColorText(num.."-"..numAll)
else
return num.."-"..numAll
end
end
end
function private.GetRankCellText(self, data)
local rank = self._isSpellId[data] and TSM.Crafting.ProfessionScanner.GetRankBySpellId(data) or -1
if rank == -1 then
return ""
end
local filled = TSM.UI.TexturePacks.GetTextureLink("iconPack.14x14/Star/Filled")
local unfilled = TSM.UI.TexturePacks.GetTextureLink("iconPack.14x14/Star/Unfilled")
assert(rank >= 1 and rank <= 3)
return strrep(filled, rank)..strrep(unfilled, 3 - rank)
end
function private.GetCraftingCostCellText(self, data)
if not self._isSpellId[data] then
return ""
end
local craftingCost = TSM.Crafting.Cost.GetCostsBySpellId(data)
return craftingCost and Money.ToString(craftingCost) or ""
end
function private.GetItemValueCellIndex(self, data)
if not self._isSpellId[data] then
return ""
end
local _, craftedItemValue = TSM.Crafting.Cost.GetCostsBySpellId(data)
return craftedItemValue and Money.ToString(craftedItemValue) or ""
end
function private.GetProfitCellText(self, data, currentTitleIndex)
if not self._isSpellId[data] then
return ""
end
local _, _, profit = TSM.Crafting.Cost.GetCostsBySpellId(data)
local color = profit and Theme.GetFeedbackColor(profit >= 0 and "GREEN" or "RED")
return profit and Money.ToString(profit, color:GetTextColorPrefix()) or ""
end
function private.GetProfitPctCellText(self, data, currentTitleIndex)
if not self._isSpellId[data] then
return ""
end
local craftingCost, _, profit = TSM.Crafting.Cost.GetCostsBySpellId(data)
local color = profit and Theme.GetFeedbackColor(profit >= 0 and "GREEN" or "RED")
return profit and color:ColorText(floor(profit * 100 / craftingCost).."%") or ""
end
function private.GetSaleRateCellText(self, data)
return self._isSpellId[data] and TSM.Crafting.Cost.GetSaleRateBySpellId(data) or ""
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.RowOnClick(row, mouseButton)
local scrollingTable = row._scrollingTable
local data = row:GetData()
if mouseButton == "LeftButton" then
if scrollingTable._isSpellId[data] then
scrollingTable:SetSelection(data)
else
scrollingTable:_ToggleCollapsed(data)
end
scrollingTable:UpdateData(true)
if scrollingTable._isSpellId[data] then
row:SetHighlightState("selectedHover")
else
row:SetHighlightState("hover")
end
end
end
function private.RankOnEnter(row)
local data = row:GetData()
local rank = row._scrollingTable._isSpellId[data] and TSM.Crafting.ProfessionScanner.GetRankBySpellId(data) or -1
if rank > 0 and not TSM.IsWowClassic() then
assert(not Tooltip.IsVisible())
GameTooltip:SetOwner(row._buttons.rank, "ANCHOR_PRESERVE")
GameTooltip:ClearAllPoints()
GameTooltip:SetPoint("LEFT", row._buttons.rank, "RIGHT")
GameTooltip:SetRecipeRankInfo(data, rank)
GameTooltip:Show()
end
row._frame:GetScript("OnEnter")(row._frame)
end
function private.RankOnLeave(row)
Tooltip.Hide()
row._frame:GetScript("OnLeave")(row._frame)
end
function private.IsPlayerProfession()
return not (TSM.Crafting.ProfessionUtil.IsNPCProfession() or TSM.Crafting.ProfessionUtil.IsLinkedProfession() or TSM.Crafting.ProfessionUtil.IsGuildProfession())
end
function private.OnChatMsgSkill(_, msg)
if not strmatch(msg, TSM.Crafting.ProfessionUtil.GetCurrentProfessionName()) then
return
end
for self in pairs(private.activeElements) do
wipe(private.categoryInfoCache.numIndents)
self:_ForceLastDataUpdate()
self:_UpdateData(true)
end
end

View File

@@ -0,0 +1,168 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- ProgressBar UI Element Class.
-- The progress bar element is a left-to-right progress bar with an anaimated progress indicator and text. It is a
-- subclass of the @{Text} class.
-- @classmod ProgressBar
local _, TSM = ...
local Theme = TSM.Include("Util.Theme")
local UIElements = TSM.Include("UI.UIElements")
local ProgressBar = TSM.Include("LibTSMClass").DefineClass("ProgressBar", TSM.UI.Text)
UIElements.Register(ProgressBar)
TSM.UI.ProgressBar = ProgressBar
local PROGRESS_PADDING = 2
local PROGRESS_ICON_PADDING = 4
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function ProgressBar.__init(self)
local frame = UIElements.CreateFrame(self, "Frame")
self.__super:__init(frame)
self._bgLeft = frame:CreateTexture(nil, "BACKGROUND")
self._bgLeft:SetPoint("TOPLEFT")
self._bgLeft:SetPoint("BOTTOMLEFT")
TSM.UI.TexturePacks.SetTextureAndWidth(self._bgLeft, "uiFrames.LoadingBarLeft")
self._bgRight = frame:CreateTexture(nil, "BACKGROUND")
self._bgRight:SetPoint("TOPRIGHT")
self._bgRight:SetPoint("BOTTOMRIGHT")
TSM.UI.TexturePacks.SetTextureAndWidth(self._bgRight, "uiFrames.LoadingBarRight")
self._bgMiddle = frame:CreateTexture(nil, "BACKGROUND")
self._bgMiddle:SetPoint("TOPLEFT", self._bgLeft, "TOPRIGHT")
self._bgMiddle:SetPoint("BOTTOMRIGHT", self._bgRight, "BOTTOMLEFT")
TSM.UI.TexturePacks.SetTexture(self._bgMiddle, "uiFrames.LoadingBarMiddle")
-- create the progress textures
self._progressLeft = frame:CreateTexture(nil, "ARTWORK")
self._progressLeft:SetPoint("TOPLEFT", PROGRESS_PADDING, -PROGRESS_PADDING)
self._progressLeft:SetPoint("BOTTOMLEFT", PROGRESS_PADDING, PROGRESS_PADDING)
self._progressLeft:SetBlendMode("BLEND")
TSM.UI.TexturePacks.SetTexture(self._progressLeft, "uiFrames.LoadingBarLeft")
self._progressMiddle = frame:CreateTexture(nil, "ARTWORK")
self._progressMiddle:SetPoint("TOPLEFT", self._progressLeft, "TOPRIGHT")
self._progressMiddle:SetPoint("BOTTOMLEFT", self._progressLeft, "BOTTOMRIGHT")
self._progressMiddle:SetBlendMode("BLEND")
TSM.UI.TexturePacks.SetTexture(self._progressMiddle, "uiFrames.LoadingBarMiddle")
self._progressRight = frame:CreateTexture(nil, "ARTWORK")
self._progressRight:SetPoint("TOPLEFT", self._progressMiddle, "TOPRIGHT")
self._progressRight:SetPoint("BOTTOMLEFT", self._progressMiddle, "BOTTOMRIGHT")
self._progressRight:SetBlendMode("BLEND")
TSM.UI.TexturePacks.SetTexture(self._progressRight, "uiFrames.LoadingBarRight")
-- create the progress icon
frame.progressIcon = frame:CreateTexture(nil, "OVERLAY")
frame.progressIcon:SetPoint("RIGHT", frame.text, "LEFT", -PROGRESS_ICON_PADDING, 0)
frame.progressIcon:Hide()
frame.progressIcon.ag = frame.progressIcon:CreateAnimationGroup()
local spin = frame.progressIcon.ag:CreateAnimation("Rotation")
spin:SetDuration(2)
spin:SetDegrees(360)
frame.progressIcon.ag:SetLooping("REPEAT")
self._progress = 0
self._progressIconHidden = false
self._justifyH = "CENTER"
self._font = "BODY_BODY2_MEDIUM"
self._textColor = "INDICATOR"
end
function ProgressBar.Release(self)
self._progress = 0
self._progressIconHidden = false
self:_GetBaseFrame().progressIcon.ag:Stop()
self:_GetBaseFrame().progressIcon:Hide()
self.__super:Release()
self._justifyH = "CENTER"
self._font = "BODY_BODY2_MEDIUM"
self._textColor = "INDICATOR"
end
--- Sets the progress.
-- @tparam ProgressBar self The progress bar object
-- @tparam number progress The progress from a value of 0 to 1 (inclusive)
-- @tparam boolean isDone Whether or not the progress is finished
-- @treturn ProgressBar The progress bar object
function ProgressBar.SetProgress(self, progress, isDone)
self._progress = progress
return self
end
--- Sets whether or not the progress indicator is hidden.
-- @tparam ProgressBar self The progress bar object
-- @tparam boolean hidden Whether or not the progress indicator is hidden
-- @treturn ProgressBar The progress bar object
function ProgressBar.SetProgressIconHidden(self, hidden)
self._progressIconHidden = hidden
return self
end
function ProgressBar.Draw(self)
self.__super:Draw()
local frame = self:_GetBaseFrame()
self._bgLeft:SetVertexColor(Theme.GetColor("PRIMARY_BG"):GetFractionalRGBA())
self._bgMiddle:SetVertexColor(Theme.GetColor("PRIMARY_BG"):GetFractionalRGBA())
self._bgRight:SetVertexColor(Theme.GetColor("PRIMARY_BG"):GetFractionalRGBA())
local text = frame.text
text:ClearAllPoints()
text:SetWidth(self:_GetDimension("WIDTH"))
text:SetWidth(frame.text:GetStringWidth())
text:SetHeight(self:_GetDimension("HEIGHT"))
text:SetPoint("CENTER", self._progressIconHidden and 0 or ((TSM.UI.TexturePacks.GetWidth("iconPack.18x18/Running") + PROGRESS_ICON_PADDING) / 2), 0)
TSM.UI.TexturePacks.SetTextureAndSize(frame.progressIcon, "iconPack.18x18/Running")
frame.progressIcon:SetVertexColor(self:_GetTextColor():GetFractionalRGBA())
if self._progressIconHidden and frame.progressIcon:IsVisible() then
frame.progressIcon.ag:Stop()
frame.progressIcon:Hide()
elseif not self._progressIconHidden and not frame.progressIcon:IsVisible() then
frame.progressIcon:Show()
frame.progressIcon.ag:Play()
end
if self._progress == 0 then
self._progressLeft:Hide()
self._progressMiddle:Hide()
self._progressRight:Hide()
else
self._progressLeft:Show()
self._progressMiddle:Show()
self._progressRight:Show()
self._progressLeft:SetVertexColor(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())
self._progressMiddle:SetVertexColor(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())
self._progressRight:SetVertexColor(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())
local leftTextureWidth = TSM.UI.TexturePacks.GetWidth("uiFrames.LoadingBarLeft")
local rightTextureWidth = TSM.UI.TexturePacks.GetWidth("uiFrames.LoadingBarRight")
local maxProgressWidth = self:_GetDimension("WIDTH") - PROGRESS_PADDING * 2
local progressWidth = maxProgressWidth * self._progress
if progressWidth <= leftTextureWidth then
self._progressLeft:SetWidth(progressWidth)
self._progressMiddle:Hide()
self._progressRight:Hide()
elseif progressWidth < maxProgressWidth - rightTextureWidth then
self._progressLeft:SetWidth(leftTextureWidth)
self._progressMiddle:SetWidth(progressWidth - leftTextureWidth)
self._progressRight:Hide()
else
self._progressLeft:SetWidth(leftTextureWidth)
self._progressMiddle:SetWidth(progressWidth - leftTextureWidth - rightTextureWidth)
self._progressRight:SetWidth(rightTextureWidth)
end
end
end

View File

@@ -0,0 +1,258 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Query Scrolling Table UI ScrollingTable Class.
-- A query scrolling table contains a scrollable list of rows with a fixed set of columns. It is a subclass of the
-- @{ScrollingTable} class.
-- @classmod QueryScrollingTable
local _, TSM = ...
local QueryScrollingTable = TSM.Include("LibTSMClass").DefineClass("QueryScrollingTable", TSM.UI.ScrollingTable)
local TempTable = TSM.Include("Util.TempTable")
local Table = TSM.Include("Util.Table")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(QueryScrollingTable)
TSM.UI.QueryScrollingTable = QueryScrollingTable
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function QueryScrollingTable.__init(self)
self.__super:__init()
self._query = nil
self._sortCol = nil
self._sortAscending = nil
self._autoReleaseQuery = false
end
function QueryScrollingTable.Release(self)
if self._query then
self._query:SetUpdateCallback()
if self._autoReleaseQuery then
self._query:Release()
end
self._query = nil
end
self._sortCol = nil
self._sortAscending = nil
self._autoReleaseQuery = false
self.__super:Release()
end
--- Sets the @{DatabaseQuery} source for this table.
-- This query is used to populate the entries in the query scrolling table.
-- @tparam QueryScrollingTable self The query scrolling table object
-- @tparam DatabaseQuery query The query object
-- @tparam[opt=false] bool redraw Whether or not to redraw the scrolling table
-- @treturn QueryScrollingTable The query scrolling table object
function QueryScrollingTable.SetQuery(self, query, redraw)
if query == self._query and not redraw then
return self
end
if self._query then
self._query:SetUpdateCallback()
end
self._query = query
self._query:SetUpdateCallback(private.QueryUpdateCallback, self)
self:_UpdateSortFromQuery()
self:_ForceLastDataUpdate()
self:UpdateData(redraw)
return self
end
--- Sets whether or not the @{DatabaseQuery} is automatically released.
-- @tparam QueryScrollingTable self The query scrolling table object
-- @tparam bool autoRelease Whether or not to auto-release the query
-- @treturn QueryScrollingTable The query scrolling table object
function QueryScrollingTable.SetAutoReleaseQuery(self, autoRelease)
self._autoReleaseQuery = autoRelease
return self
end
--- Sets the selected record.
-- @tparam QueryScrollingTable self The query scrolling table object
-- @param selection The selected record or nil to clear the selection
-- @tparam[opt=false] bool redraw Whether or not to redraw the scrolling table
-- @treturn QueryScrollingTable The query scrolling table object
function QueryScrollingTable.SetSelection(self, selection, redraw)
if selection == self._selection then
return self
elseif selection and self._selectionValidator and not self:_selectionValidator(self._query:GetResultRowByUUID(selection)) then
return self
end
local index = nil
if selection then
index = Table.KeyByValue(self._data, selection)
assert(index)
end
self:_IgnoreLastDataUpdate()
self._selection = selection
if selection then
-- set the scroll so that the selection is visible if necessary
local rowHeight = self._rowHeight
local firstVisibleIndex = ceil(self._vScrollValue / rowHeight) + 1
local lastVisibleIndex = floor((self._vScrollValue + self:_GetDimension("HEIGHT")) / rowHeight)
if lastVisibleIndex > firstVisibleIndex and (index < firstVisibleIndex or index > lastVisibleIndex) then
self:_OnScrollValueChanged(min((index - 1) * rowHeight, self:_GetMaxScroll()))
end
end
for _, row in ipairs(self._rows) do
if not row:IsMouseOver() and row:IsVisible() and row:GetData() ~= selection then
row:SetHighlightState(nil)
elseif row:IsMouseOver() and row:IsVisible() then
row:SetHighlightState(row:GetData() == selection and "selectedHover" or "hover")
elseif row:IsVisible() and row:GetData() == selection then
row:SetHighlightState("selected")
end
end
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler()
end
if redraw then
self:Draw()
end
return self
end
--- Gets the currently selected row.
-- @tparam QueryScrollingTable self The scrolling table object
-- @return The selected row or nil if there's nothing selected
function QueryScrollingTable.GetSelection(self)
return self._selection and self._query:GetResultRowByUUID(self._selection) or nil
end
--- Registers a script handler.
-- @tparam QueryScrollingTable self The scrolling table object
-- @tparam string script The script to register for (supported scripts: `OnDataUpdated`, `OnSelectionChanged`, `OnRowClick`)
-- @tparam function handler The script handler which will be called with the scrolling table object followed by any
-- arguments to the script
-- @treturn QueryScrollingTable The scrolling table object
function QueryScrollingTable.SetScript(self, script, handler)
if script == "OnDataUpdated" then
self._onDataUpdated = handler
else
self.__super:SetScript(script, handler)
end
return self
end
function QueryScrollingTable.Draw(self)
self._query:SetUpdatesPaused(true)
if self._lastDataUpdate == nil then
self:_IgnoreLastDataUpdate()
end
self.__super:Draw()
self._header:SetSort(self._sortCol, self._sortAscending)
self._query:SetUpdatesPaused(false)
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function QueryScrollingTable._CreateScrollingTableInfo(self)
return TSM.UI.Util.QueryScrollingTableInfo()
end
function QueryScrollingTable._UpdateSortFromQuery(self)
if self._tableInfo:_IsSortEnabled() then
local sortField, sortAscending = self._query:GetLastOrderBy()
if sortField then
self._sortCol = self._tableInfo:_GetIdBySortField(sortField)
self._sortAscending = sortAscending
else
self._sortCol = nil
self._sortAscending = nil
end
end
end
function QueryScrollingTable._UpdateData(self)
-- we need to fix up the data within the rows updated to avoid errors with trying to access old DatabaseQueryResultRows
local prevRowIndex = TempTable.Acquire()
local newRowData = TempTable.Acquire()
for i, row in ipairs(self._rows) do
if row:IsVisible() then
prevRowIndex[row:GetData()] = i
end
end
local prevSelection = self._selection
wipe(self._data)
self._selection = nil
for _, uuid in self._query:UUIDIterator() do
if uuid == prevSelection then
self._selection = uuid
end
if prevRowIndex[uuid] then
newRowData[prevRowIndex[uuid]] = uuid
end
tinsert(self._data, uuid)
end
for i, row in ipairs(self._rows) do
if row:IsVisible() then
if newRowData[i] then
row:SetData(newRowData[i])
else
row:ClearData()
end
end
end
TempTable.Release(prevRowIndex)
TempTable.Release(newRowData)
if prevSelection and not self._selection then
-- select the first row since we weren't able to find the previously-selected row
self:SetSelection(self._data[1])
end
if self._onDataUpdated then
self:_onDataUpdated()
end
end
function QueryScrollingTable._ToggleSort(self, id)
local sortField = self._tableInfo:_GetSortFieldById(id)
if not self._sortCol or not self._query or not sortField then
-- sorting disabled so ignore
return
end
if id == self._sortCol then
self._sortAscending = not self._sortAscending
else
self._sortCol = id
self._sortAscending = true
end
self._query:UpdateLastOrderBy(sortField, self._sortAscending)
self:_UpdateData()
self:Draw()
end
function QueryScrollingTable._HandleRowClick(self, uuid, mouseButton)
if self._onRowClickHandler then
self:_onRowClickHandler(self._query:GetResultRowByUUID(uuid), mouseButton)
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.QueryUpdateCallback(_, uuid, self)
self:_SetLastDataUpdate(uuid)
if not uuid then
self:_UpdateData()
end
self:Draw()
end

View File

@@ -0,0 +1,237 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- ScrollFrame UI Element Class.
-- A scroll frame is a container which allows the content to be of unlimited (but fixed/static) height within a
-- scrollable window. It is a subclass of the @{Container} class.
-- @classmod ScrollFrame
local _, TSM = ...
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local Theme = TSM.Include("Util.Theme")
local NineSlice = TSM.Include("Util.NineSlice")
local ScrollFrame = TSM.Include("LibTSMClass").DefineClass("ScrollFrame", TSM.UI.Container)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(ScrollFrame)
TSM.UI.ScrollFrame = ScrollFrame
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function ScrollFrame.__init(self)
local frame = UIElements.CreateFrame(self, "ScrollFrame")
self.__super:__init(frame)
self._backgroundNineSlice = NineSlice.New(frame, 1)
self._backgroundNineSlice:Hide()
frame:EnableMouseWheel(true)
frame:SetClipsChildren(true)
ScriptWrapper.Set(frame, "OnUpdate", private.FrameOnUpdate, self)
ScriptWrapper.Set(frame, "OnMouseWheel", private.FrameOnMouseWheel, self)
self._scrollbar = TSM.UI.Scrollbar.Create(frame)
ScriptWrapper.Set(self._scrollbar, "OnValueChanged", private.OnScrollbarValueChanged, self)
self._content = CreateFrame("Frame", nil, frame)
self._content:SetPoint("TOPLEFT")
self._content:SetPoint("TOPRIGHT")
frame:SetScrollChild(self._content)
self._scrollValue = 0
self._onUpdateHandler = nil
self._backgroundColor = nil
end
function ScrollFrame.Acquire(self)
self.__super:Acquire()
self._scrollValue = 0
self._scrollbar:SetValue(0)
end
function ScrollFrame.Release(self)
self._onUpdateHandler = nil
self._backgroundColor = nil
self._backgroundNineSlice:Hide()
self.__super:Release()
end
--- Sets the background of the scroll frame.
-- @tparam ScrollFrame self The scroll frame object
-- @tparam ?string|nil color The background color as a theme color key or nil
-- @treturn ScrollFrame The scroll frame object
function ScrollFrame.SetBackgroundColor(self, color)
assert(color == nil or Theme.GetColor(color))
self._backgroundColor = color
return self
end
function ScrollFrame.SetScript(self, script, handler)
if script == "OnUpdate" then
self._onUpdateHandler = handler
else
self.__super:SetScript(script, handler)
end
return self
end
function ScrollFrame.Draw(self)
self.__super.__super:Draw()
if self._backgroundColor then
self._backgroundNineSlice:SetStyle("solid")
self._backgroundNineSlice:SetVertexColor(Theme.GetColor(self._backgroundColor):GetFractionalRGBA())
else
self._backgroundNineSlice:Hide()
end
local width = self:_GetDimension("WIDTH")
self._content:SetWidth(width)
width = width - self:_GetPadding("LEFT") - self:_GetPadding("RIGHT")
local totalHeight = self:_GetPadding("TOP") + self:_GetPadding("BOTTOM")
for _, child in self:LayoutChildrenIterator() do
child:_GetBaseFrame():SetParent(self._content)
child:_GetBaseFrame():ClearAllPoints()
-- set the height
local childHeight, childHeightCanExpand = child:_GetMinimumDimension("HEIGHT")
assert(not childHeightCanExpand, "Invalid height for child: "..tostring(child._id))
child:_SetDimension("HEIGHT", childHeight)
totalHeight = totalHeight + childHeight + child:_GetMargin("TOP") + child:_GetMargin("BOTTOM")
-- set the width
local childWidth, childWidthCanExpand = child:_GetMinimumDimension("WIDTH")
if childWidthCanExpand then
childWidth = max(childWidth, width - child:_GetMargin("LEFT") - child:_GetMargin("RIGHT"))
end
child:_SetDimension("WIDTH", childWidth)
end
self._content:SetHeight(totalHeight)
local maxScroll = self:_GetMaxScroll()
self._scrollbar:SetMinMaxValues(0, maxScroll)
self._scrollbar:SetValue(min(self._scrollValue, maxScroll))
self._scrollbar.thumb:SetHeight(TSM.UI.Scrollbar.GetLength(totalHeight, self:_GetDimension("HEIGHT")))
local yOffset = -1 * self:_GetPadding("TOP")
for _, child in self:LayoutChildrenIterator() do
local childFrame = child:_GetBaseFrame()
yOffset = yOffset - child:_GetMargin("TOP")
childFrame:SetPoint("TOPLEFT", child:_GetMargin("LEFT") + self:_GetPadding("LEFT"), yOffset)
yOffset = yOffset - childFrame:GetHeight() - child:_GetMargin("BOTTOM")
end
self.__super:Draw()
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function ScrollFrame._OnScrollValueChanged(self, value)
self:_GetBaseFrame():SetVerticalScroll(value)
self._scrollValue = value
end
function ScrollFrame._GetMaxScroll(self)
return max(self._content:GetHeight() - self:_GetDimension("HEIGHT"), 0)
end
function ScrollFrame._GetMinimumDimension(self, dimension)
local styleResult = nil
if dimension == "WIDTH" then
styleResult = self._width
elseif dimension == "HEIGHT" then
styleResult = self._height
else
error("Invalid dimension: "..tostring(dimension))
end
if styleResult then
return styleResult, false
elseif dimension == "HEIGHT" or self:GetNumLayoutChildren() == 0 then
-- regarding the first condition for this if statment, a scrollframe can be any height (including greater than
-- the height of the content if no scrolling is needed), so has no minimum and can always expand
return 0, true
else
-- we're trying to determine the width based on the max width of any of the children
local result = 0
local canExpand = false
for _, child in self:LayoutChildrenIterator() do
local childMin, childCanExpand = child:_GetMinimumDimension(dimension)
childMin = childMin + child:_GetMargin("LEFT") + child:_GetMargin("RIGHT")
canExpand = canExpand or childCanExpand
result = max(result, childMin)
end
result = result + self:_GetPadding("LEFT") + self:_GetPadding("RIGHT")
return result, canExpand
end
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.OnScrollbarValueChanged(self, value)
value = max(min(value, self:_GetMaxScroll()), 0)
self:_OnScrollValueChanged(value)
end
function private.FrameOnUpdate(self)
if (self:_GetBaseFrame():IsMouseOver() and self:_GetMaxScroll() > 0) or self._scrollbar.dragging then
self._scrollbar:Show()
else
self._scrollbar:Hide()
end
if self._onUpdateHandler then
self:_onUpdateHandler()
end
end
function private.FrameOnMouseWheel(self, direction)
local parentScroll = nil
local parent = self:GetParentElement()
while parent do
if parent:__isa(ScrollFrame) then
parentScroll = parent
break
else
parent = parent:GetParentElement()
end
end
if parentScroll then
local minValue, maxValue = self._scrollbar:GetMinMaxValues()
if direction > 0 then
if self._scrollbar:GetValue() == minValue then
local scrollAmount = min(parentScroll:_GetDimension("HEIGHT") / 3, Theme.GetMouseWheelScrollAmount())
parentScroll._scrollbar:SetValue(parentScroll._scrollbar:GetValue() + -1 * direction * scrollAmount)
else
local scrollAmount = min(self:_GetDimension("HEIGHT") / 3, Theme.GetMouseWheelScrollAmount())
self._scrollbar:SetValue(self._scrollbar:GetValue() + -1 * direction * scrollAmount)
end
else
if self._scrollbar:GetValue() == maxValue then
local scrollAmount = min(parentScroll:_GetDimension("HEIGHT") / 3, Theme.GetMouseWheelScrollAmount())
parentScroll._scrollbar:SetValue(parentScroll._scrollbar:GetValue() + -1 * direction * scrollAmount)
else
local scrollAmount = min(self:_GetDimension("HEIGHT") / 3, Theme.GetMouseWheelScrollAmount())
self._scrollbar:SetValue(self._scrollbar:GetValue() + -1 * direction * scrollAmount)
end
end
else
local scrollAmount = min(self:_GetDimension("HEIGHT") / 3, Theme.GetMouseWheelScrollAmount())
self._scrollbar:SetValue(self._scrollbar:GetValue() + -1 * direction * scrollAmount)
end
end

View File

@@ -0,0 +1,741 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Scrolling Table UI Element Class.
-- A scrolling table contains a scrollable list of rows with a fixed set of columns. It is a subclass of the @{Element}
-- class.
-- @classmod ScrollingTable
local _, TSM = ...
local ObjectPool = TSM.Include("Util.ObjectPool")
local Table = TSM.Include("Util.Table")
local Math = TSM.Include("Util.Math")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local Color = TSM.Include("Util.Color")
local Theme = TSM.Include("Util.Theme")
local ScrollingTable = TSM.Include("LibTSMClass").DefineClass("ScrollingTable", TSM.UI.Element, "ABSTRACT")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(ScrollingTable)
TSM.UI.ScrollingTable = ScrollingTable
local private = {
rowPool = ObjectPool.New("TABLE_ROWS", TSM.UI.Util.TableRow, 1),
}
local HEADER_HEIGHT = 22
local HEADER_LINE_HEIGHT = 2
local MORE_COL_WIDTH = 8
local FORCE_DATA_UPDATE = newproxy()
local IGNORE_DATA_UPDATE = newproxy()
local SCROLL_TO_DATA_TOTAL_TIME_S = 0.1
-- ============================================================================
-- Meta Class Methods
-- ============================================================================
function ScrollingTable.__init(self)
local frame = UIElements.CreateFrame(self, "Frame", nil, nil, TSM.IsShadowlands() and "BackdropTemplate" or nil)
self.__super:__init(frame)
frame:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8" })
self._lineTop = frame:CreateTexture(nil, "ARTWORK")
self._lineTop:SetPoint("TOPLEFT")
self._lineTop:SetPoint("TOPRIGHT")
self._lineTop:SetHeight(HEADER_LINE_HEIGHT)
self._lineTop:SetColorTexture(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())
self._lineBottom = frame:CreateTexture(nil, "ARTWORK")
self._lineBottom:SetPoint("TOPLEFT", 0, -HEADER_HEIGHT - HEADER_LINE_HEIGHT)
self._lineBottom:SetPoint("TOPRIGHT", 0, -HEADER_HEIGHT - HEADER_LINE_HEIGHT)
self._lineBottom:SetHeight(HEADER_LINE_HEIGHT)
self._lineBottom:SetColorTexture(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())
self._hScrollFrame = CreateFrame("ScrollFrame", nil, frame)
self._hScrollFrame:SetPoint("TOPLEFT")
self._hScrollFrame:SetPoint("BOTTOMRIGHT")
self._hScrollFrame:EnableMouseWheel(true)
self._hScrollFrame:SetClipsChildren(true)
ScriptWrapper.Set(self._hScrollFrame, "OnUpdate", private.HScrollFrameOnUpdate, self)
ScriptWrapper.Set(self._hScrollFrame, "OnMouseWheel", private.FrameOnMouseWheel, self)
self._hContent = CreateFrame("Frame", nil, self._hScrollFrame)
self._hContent:SetPoint("TOPLEFT")
self._hScrollFrame:SetScrollChild(self._hContent)
self._vScrollFrame = CreateFrame("ScrollFrame", nil, self._hContent)
self._vScrollFrame:SetPoint("TOPLEFT")
self._vScrollFrame:SetPoint("BOTTOMRIGHT")
self._vScrollFrame:EnableMouseWheel(true)
self._vScrollFrame:SetClipsChildren(true)
ScriptWrapper.Set(self._vScrollFrame, "OnUpdate", private.VScrollFrameOnUpdate, self)
ScriptWrapper.Set(self._vScrollFrame, "OnMouseWheel", private.FrameOnMouseWheel, self)
self._content = CreateFrame("Frame", nil, self._vScrollFrame)
self._content:SetPoint("TOPLEFT")
self._vScrollFrame:SetScrollChild(self._content)
self._hScrollbar = TSM.UI.Scrollbar.Create(frame, true)
self._vScrollbar = TSM.UI.Scrollbar.Create(frame)
self._rowHeight = 20
self._backgroundColor = "PRIMARY_BG"
self._rows = {}
self._data = {}
self._hScrollValue = 0
self._vScrollValue = 0
self._onSelectionChangedHandler = nil
self._onRowClickHandler = nil
self._selection = nil
self._selectionDisabled = nil
self._selectionValidator = nil
self._tableInfo = self:_CreateScrollingTableInfo()
self._header = nil
self._dataTranslationFunc = nil
self._contextTable = nil
self._defaultContextTable = nil
self._prevDataOffset = nil
self._lastDataUpdate = nil
self._rowHoverEnabled = true
self._headerHidden = false
self._targetScrollValue = nil
self._totalScrollDistance = nil
self._rightClickToggle = nil
Theme.RegisterChangeCallback(function()
if self:IsVisible() and self._header then
self._header:_LayoutHeaderRow()
end
end)
end
function ScrollingTable.Acquire(self)
self.__super:Acquire()
self._tableInfo:_Acquire(self)
self._hScrollFrame:SetHorizontalScroll(0)
self._hScrollValue = 0
self._vScrollValue = 0
ScriptWrapper.Set(self._vScrollbar, "OnValueChanged", private.OnVScrollbarValueChangedNoDraw, self)
-- don't want to cause this element to be drawn for this initial scrollbar change
self._vScrollbar:SetValue(0)
ScriptWrapper.Set(self._vScrollbar, "OnValueChanged", private.OnVScrollbarValueChanged, self)
ScriptWrapper.Set(self._hScrollbar, "OnValueChanged", private.OnHScrollbarValueChangedNoDraw, self)
-- don't want to cause this element to be drawn for this initial scrollbar change
self._hScrollbar:SetValue(0)
ScriptWrapper.Set(self._hScrollbar, "OnValueChanged", private.OnHScrollbarValueChanged, self)
end
function ScrollingTable.Release(self)
self._rowHeight = 20
self._backgroundColor = "PRIMARY_BG"
self._onSelectionChangedHandler = nil
self._onRowClickHandler = nil
self._selection = nil
self._selectionDisabled = nil
self._selectionValidator = nil
self._dataTranslationFunc = nil
self._contextTable = nil
self._defaultContextTable = nil
self._prevDataOffset = nil
self._lastDataUpdate = nil
if self._header then
self._header:Release()
private.rowPool:Recycle(self._header)
self._header = nil
end
for _, row in ipairs(self._rows) do
row:Release()
private.rowPool:Recycle(row)
end
wipe(self._rows)
self._tableInfo:_Release()
wipe(self._data)
self._headerHidden = false
self._targetScrollValue = nil
self._totalScrollDistance = nil
self.__super:Release()
end
-- ============================================================================
-- Public Class Methods
-- ============================================================================
--- Sets the background of the scrolling table.
-- @tparam ScrollingTable self The scrolling table object
-- @tparam number rowHeight The row height
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.SetRowHeight(self, rowHeight)
self._rowHeight = rowHeight
return self
end
--- Sets the background of the scrolling table.
-- @tparam ScrollingTable self The scrolling table object
-- @tparam boolean hidden Whether or not the header should be hidden
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.SetHeaderHidden(self, hidden)
self._headerHidden = hidden
return self
end
--- Sets the background of the scrolling table.
-- @tparam ScrollingTable self The scrolling table object
-- @tparam string color The background color as a theme color key
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.SetBackgroundColor(self, color)
assert(Theme.GetColor(color))
self._backgroundColor = color
return self
end
--- Sets the context table.
-- This table can be used to preserve the table configuration across lifecycles of the scrolling table and even WoW
-- sessions if it's within the settings DB.
-- @tparam ScrollingTable self The scrolling table object
-- @tparam table tbl The context table
-- @tparam table defaultTbl The default table (required fields: `colWidth`, `colHidden`)
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.SetContextTable(self, tbl, defaultTbl)
assert(type(defaultTbl.colWidth) == "table" and type(defaultTbl.colHidden) == "table")
tbl.colWidth = tbl.colWidth or CopyTable(defaultTbl.colWidth)
tbl.colHidden = tbl.colHidden or CopyTable(defaultTbl.colHidden)
self._contextTable = tbl
self._defaultContextTable = defaultTbl
self:_UpdateColsHidden()
return self
end
--- Sets the context table from a settings object.
-- @tparam ScrollingTable self The scrolling table object
-- @tparam Settings settings The settings object
-- @tparam string key The setting key
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.SetSettingsContext(self, settings, key)
return self:SetContextTable(settings[key], settings:GetDefaultReadOnly(key))
end
--- Forces an update of the data shown within the table.
-- @tparam ScrollingTable self The scrolling table object
-- @tparam[opt=false] bool redraw Whether or not to redraw the scrolling table
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.UpdateData(self, redraw)
self:_ForceLastDataUpdate()
self:_UpdateData()
if redraw then
self:Draw()
end
return self
end
--- Gets the ScrollingTableInfo object.
-- @tparam ScrollingTable self The scrolling table object
-- @treturn ScrollingTableInfo The scrolling table info object
function ScrollingTable.GetScrollingTableInfo(self)
return self._tableInfo
end
--- Commits the scrolling table info.
-- This should be called once the scrolling table info is completely set (retrieved via @{ScrollingTable.GetScrollingTableInfo}).
-- @tparam ScrollingTable self The scrolling table object
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.CommitTableInfo(self)
self:_UpdateColsHidden()
if self._header then
self._header:Release()
private.rowPool:Recycle(self._header)
self._header = nil
end
return self
end
--- Registers a script handler.
-- @tparam ScrollingTable self The scrolling table object
-- @tparam string script The script to register for (supported scripts: `OnSelectionChanged`, `OnRowClick`)
-- @tparam function handler The script handler which will be called with the scrolling table object followed by any
-- arguments to the script
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.SetScript(self, script, handler)
if script == "OnSelectionChanged" then
self._onSelectionChangedHandler = handler
elseif script == "OnRowClick" then
self._onRowClickHandler = handler
else
error("Unknown ScrollingTable script: "..tostring(script))
end
return self
end
--- Sets the selected row.
-- @tparam ScrollingTable self The scrolling table object
-- @param selection The selected row or nil to clear the selection
-- @tparam[opt=false] boolean noDraw Don't redraw the rows
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.SetSelection(self, selection, noDraw)
if selection == self._selection then
self:_JumpToData(selection)
return self
elseif selection and self._selectionValidator and not self:_selectionValidator(selection) then
return self
end
self:_IgnoreLastDataUpdate()
self._selection = selection
self:_JumpToData(selection)
if not noDraw then
for _, row in ipairs(self._rows) do
if not row:IsMouseOver() and row:IsVisible() and not self:_IsSelected(row:GetData()) then
row:SetHighlightState(nil)
elseif row:IsMouseOver() and row:IsVisible() and not self:_IsSelected(row:GetData()) then
row:SetHighlightState("hover")
elseif row:IsMouseOver() and row:IsVisible() and self:_IsSelected(row:GetData()) then
row:SetHighlightState(self._selectionDisabled and "hover" or "selectedHover")
elseif row:IsVisible() and self:_IsSelected(row:GetData()) then
row:SetHighlightState("selected")
end
end
end
if self._onSelectionChangedHandler then
self:_onSelectionChangedHandler()
end
return self
end
--- Gets the currently selected row.
-- @tparam ScrollingTable self The scrolling table object
-- @return The selected row or nil if there's nothing selected
function ScrollingTable.GetSelection(self)
return self._selection
end
--- Sets a selection validator function.
-- @tparam ScrollingTable self The scrolling table object
-- @tparam function validator A function which gets called with the scrolling table object and a row to validate
-- whether or not it's selectable (returns true if it is, false otherwise)
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.SetSelectionValidator(self, validator)
self._selectionValidator = validator
return self
end
--- Sets whether or not selection is disabled.
-- @tparam ScrollingTable self The scrolling table object
-- @tparam boolean disabled Whether or not to disable selection
-- @treturn ScrollingTable The scrolling table object
function ScrollingTable.SetSelectionDisabled(self, disabled)
self._selectionDisabled = disabled
return self
end
function ScrollingTable.Draw(self)
self.__super:Draw()
local frame = self:_GetBaseFrame()
local background = Theme.GetColor(self._backgroundColor)
frame:SetBackdropColor(background:GetFractionalRGBA())
if self:_CanResizeCols() then
self:_UpdateColsHidden()
end
if not self._header then
self._header = self:_GetTableRow(true)
self._header:SetBackgroundColor(Theme.GetColor("FRAME_BG"))
self._header:SetHeight(HEADER_HEIGHT)
end
-- update the scrollbar layout
if self._headerHidden then
self._vScrollbar:SetPoint("TOPRIGHT", -Theme.GetScrollbarMargin(), -Theme.GetScrollbarMargin())
else
self._vScrollbar:SetPoint("TOPRIGHT", -Theme.GetScrollbarMargin(), -Theme.GetScrollbarMargin() - HEADER_HEIGHT - HEADER_LINE_HEIGHT * 2)
end
local totalWidth = 0
if self:_CanResizeCols() then
-- add the "more" column
totalWidth = totalWidth + MORE_COL_WIDTH + Theme.GetColSpacing()
for colId, colWidth in pairs(self._contextTable.colWidth) do
if not self._contextTable.colHidden[colId] then
totalWidth = totalWidth + colWidth + Theme.GetColSpacing()
end
end
end
totalWidth = max(totalWidth, self:_GetDimension("WIDTH"))
self._hContent:SetHeight(self._hScrollFrame:GetHeight())
self._hContent:SetWidth(totalWidth)
self._content:SetWidth(self._hContent:GetWidth())
local rowHeight = self._rowHeight
local totalHeight = #self._data * rowHeight
local visibleHeight = self._vScrollFrame:GetHeight()
local visibleWidth = self._hScrollFrame:GetWidth()
local numVisibleRows = min(ceil(visibleHeight / rowHeight), #self._data)
local maxScroll = self:_GetMaxScroll()
local vScrollOffset = min(self._vScrollValue, maxScroll)
local hScrollOffset = min(self._hScrollValue, self:_GetMaxHScroll())
local dataOffset = floor(vScrollOffset / rowHeight)
self._vScrollbar.thumb:SetHeight(TSM.UI.Scrollbar.GetLength(totalHeight, visibleHeight))
self._vScrollbar:SetMinMaxValues(0, maxScroll)
self._vScrollbar:SetValue(vScrollOffset)
self._hScrollbar.thumb:SetWidth(TSM.UI.Scrollbar.GetLength(self._hContent:GetWidth(), visibleWidth))
self._hScrollbar:SetMinMaxValues(0, self:_GetMaxHScroll())
self._hScrollbar:SetValue(hScrollOffset)
self._content:SetHeight(numVisibleRows * rowHeight)
if self._headerHidden then
self._lineTop:Hide()
self._lineBottom:Hide()
self._header:SetHeight(0)
self._header:SetBackgroundColor(Color.GetTransparent())
self._vScrollFrame:SetPoint("TOPLEFT", 0, 0)
else
self._lineTop:SetColorTexture(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())
self._lineBottom:SetColorTexture(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())
self._lineTop:Show()
self._lineBottom:Show()
self._vScrollFrame:SetPoint("TOPLEFT", 0, -HEADER_HEIGHT - HEADER_LINE_HEIGHT * 2)
self._header:SetBackgroundColor(Theme.GetColor("FRAME_BG"))
self._header:SetHeight(HEADER_HEIGHT)
end
if Math.Round(vScrollOffset + visibleHeight) == totalHeight then
-- we are at the bottom
self._vScrollFrame:SetVerticalScroll(numVisibleRows * rowHeight - visibleHeight)
else
self._vScrollFrame:SetVerticalScroll(0)
end
self._hScrollFrame:SetHorizontalScroll(hScrollOffset)
while #self._rows < numVisibleRows do
local row = self:_GetTableRow(false)
row._frame:SetPoint("TOPLEFT", 0, -rowHeight * #self._rows)
row._frame:SetPoint("TOPRIGHT", 0, -rowHeight * #self._rows)
tinsert(self._rows, row)
end
local scrollDiff = dataOffset - (self._prevDataOffset or dataOffset)
self._prevDataOffset = dataOffset
if scrollDiff ~= 0 then
-- Shuffle the rows around to accomplish the scrolling so that the data only changes
-- for the minimal number of rows, which allows for better optimization
for _ = 1, abs(scrollDiff) do
if scrollDiff > 0 then
tinsert(self._rows, tremove(self._rows, 1))
else
tinsert(self._rows, 1, tremove(self._rows))
end
end
-- fix the points of all the rows
for i, row in ipairs(self._rows) do
row._frame:SetPoint("TOPLEFT", 0, -rowHeight * (i - 1))
row._frame:SetPoint("TOPRIGHT", 0, -rowHeight * (i - 1))
end
end
for i, row in ipairs(self._rows) do
local dataIndex = i + dataOffset
local data = self._data[dataIndex]
if i > numVisibleRows or not data then
row:SetVisible(false)
row:ClearData()
else
row:SetVisible(true)
self:_SetRowData(row, data)
row:SetBackgroundColor(background)
row:SetHeight(rowHeight)
end
end
self._lastDataUpdate = nil
self._header:SetHeaderData()
end
-- ============================================================================
-- ScrollingTable - Private Class Methods
-- ============================================================================
function ScrollingTable._CreateScrollingTableInfo(self)
return TSM.UI.Util.ScrollingTableInfo()
end
function ScrollingTable._GetTableRow(self, isHeader)
local row = private.rowPool:Get()
row:Acquire(self, isHeader)
return row
end
function ScrollingTable._SetRowData(self, row, data)
-- updating the row data is expensive, so only do it if necessary
local dataUpdated = row:GetData() ~= data or not self._lastDataUpdate or self._lastDataUpdate == FORCE_DATA_UPDATE or self._lastDataUpdate == data
local isMouseOver = row:IsMouseOver()
local isSelected = self:_IsSelected(data)
if not isMouseOver and isSelected then
row:SetHighlightState("selected", dataUpdated)
elseif isMouseOver and isSelected then
row:SetHighlightState(self._selectionDisabled and "hover" or "selectedHover", dataUpdated)
elseif isMouseOver and not isSelected then
row:SetHighlightState("hover", dataUpdated)
else
row:SetHighlightState(nil, dataUpdated)
end
if dataUpdated then
row:SetData(data)
end
end
function ScrollingTable._OnScrollValueChanged(self, value, noDraw)
self._vScrollValue = value
if not noDraw then
self:Draw()
end
end
function ScrollingTable._OnHScrollValueChanged(self, value, noDraw)
self._hScrollValue = value
if not noDraw then
self:Draw()
end
end
function ScrollingTable._GetMaxScroll(self)
return max(#self._data * self._rowHeight - self._vScrollFrame:GetHeight(), 0)
end
function ScrollingTable._GetMaxHScroll(self)
return max(self._hContent:GetWidth() - self._hScrollFrame:GetWidth(), 0)
end
function ScrollingTable._UpdateData(self)
error("Must be implemented by the child class")
end
function ScrollingTable._ToggleSort(self, id)
error("Must be implemented by the child class")
end
function ScrollingTable._IsSelected(self, data)
return data == self._selection
end
function ScrollingTable._HandleRowClick(self, data, mouseButton)
if self._onRowClickHandler then
self:_onRowClickHandler(data, mouseButton)
end
end
function ScrollingTable._GetColWidth(self, id)
return self._contextTable.colWidth[id]
end
function ScrollingTable._ResetColWidth(self, id)
local defaultWidth = self._defaultContextTable.colWidth[id]
local currentWidth = self._contextTable.colWidth[id]
assert(currentWidth and defaultWidth)
self._contextTable.colWidth[id] = defaultWidth
self._header:_LayoutHeaderRow()
for _, row in ipairs(self._rows) do
row:_LayoutDataRow()
end
self:Draw()
end
function ScrollingTable._SetColWidth(self, id, width, redraw)
assert(not self._contextTable.colWidthLocked)
local prevWidth = self._contextTable.colWidth[id]
assert(prevWidth)
if width == prevWidth and not redraw then
return
end
self._contextTable.colWidth[id] = width
for _, row in ipairs(self._rows) do
row:_LayoutDataRow()
end
if redraw then
self:Draw()
end
end
function ScrollingTable._IsColWidthLocked(self)
return self._contextTable.colWidthLocked
end
function ScrollingTable._ToogleColWidthLocked(self)
self._contextTable.colWidthLocked = not self._contextTable.colWidthLocked or nil
self._header:_LayoutHeaderRow()
self:Draw()
end
function ScrollingTable._CanResizeCols(self)
return self._contextTable and true or false
end
function ScrollingTable._ToggleColHide(self, id)
if not self._contextTable then
return
end
self._contextTable.colHidden[id] = not self._contextTable.colHidden[id] or nil
self:_UpdateColsHidden()
self._header:_LayoutHeaderRow()
for _, row in ipairs(self._rows) do
row:_LayoutDataRow()
end
self:Draw()
end
function ScrollingTable._ResetContext(self)
assert(self._contextTable)
if self._defaultContextTable.colWidth then
wipe(self._contextTable.colWidth)
for col, width in pairs(self._defaultContextTable.colWidth) do
self._contextTable.colWidth[col] = width
end
end
if self._defaultContextTable.colHidden then
wipe(self._contextTable.colHidden)
for col, hidden in pairs(self._defaultContextTable.colHidden) do
self._contextTable.colHidden[col] = hidden
end
self:_UpdateColsHidden()
end
self._header:_LayoutHeaderRow()
for _, row in ipairs(self._rows) do
row:_LayoutDataRow()
end
self:Draw()
end
function ScrollingTable._UpdateColsHidden(self)
for _, col in self:GetScrollingTableInfo():_ColIterator() do
local colId = col:_GetId()
if col:_CanHide() then
col:_SetHidden(self._contextTable and self._contextTable.colHidden[colId] and true or false)
elseif self._contextTable then
self._contextTable.colHidden[colId] = nil
end
end
end
function ScrollingTable._SetLastDataUpdate(self, value)
self._lastDataUpdate = value
end
function ScrollingTable._IgnoreLastDataUpdate(self)
self._lastDataUpdate = IGNORE_DATA_UPDATE
end
function ScrollingTable._ForceLastDataUpdate(self)
self._lastDataUpdate = FORCE_DATA_UPDATE
end
function ScrollingTable._ScrollToData(self, data)
local rowHeight = self._rowHeight
local visibleHeight = self._vScrollFrame:GetHeight()
local currentOffset = self._vScrollbar:GetValue()
local dataIndex = Table.KeyByValue(self._data, data)
-- if we are going to scroll up/down, we want to scroll such that the top of the passed row is in the visible area
-- by at least 1 row height
local scrollUpOffset = max(rowHeight * (dataIndex - 1) - rowHeight, 0)
local scrollDownOffset = min(rowHeight * dataIndex + rowHeight - visibleHeight, self:_GetMaxScroll())
if scrollUpOffset < currentOffset and scrollDownOffset > currentOffset then
-- it's impossible to scroll to the right place, so do nothing
elseif scrollUpOffset < currentOffset then
-- we need to scroll up
self._targetScrollValue = scrollUpOffset
self._totalScrollDistance = currentOffset - scrollUpOffset
elseif scrollDownOffset > currentOffset then
-- we need to scroll down
self._targetScrollValue = scrollDownOffset
self._totalScrollDistance = scrollDownOffset - currentOffset
else
-- the data is already in the visible area, so do nothing
end
end
function ScrollingTable._JumpToData(self, data)
if not data then
return
end
local index = Table.KeyByValue(self._data, data)
assert(index)
-- set the scroll so that the selection is visible if necessary
local rowHeight = self._rowHeight
local firstVisibleIndex = ceil(self._vScrollValue / rowHeight) + 1
local lastVisibleIndex = floor((self._vScrollValue + self:_GetDimension("HEIGHT")) / rowHeight)
if lastVisibleIndex > firstVisibleIndex and (index < firstVisibleIndex or index > lastVisibleIndex) then
self:_OnScrollValueChanged(min((index - 1) * rowHeight, self:_GetMaxScroll()))
end
end
-- ============================================================================
-- ScrollingTable - Local Script Handlers
-- ============================================================================
function private.OnHScrollbarValueChanged(self, value)
value = max(min(value, self:_GetMaxHScroll()), 0)
self:_OnHScrollValueChanged(value)
end
function private.OnVScrollbarValueChanged(self, value)
value = max(min(value, self:_GetMaxScroll()), 0)
self:_OnScrollValueChanged(value)
end
function private.OnHScrollbarValueChangedNoDraw(self, value)
value = max(min(value, self:_GetMaxHScroll()), 0)
self:_OnHScrollValueChanged(value, true)
end
function private.OnVScrollbarValueChangedNoDraw(self, value)
value = max(min(value, self:_GetMaxScroll()), 0)
self:_OnScrollValueChanged(value, true)
end
function private.HScrollFrameOnUpdate(self)
if (self._hScrollFrame:IsMouseOver() and self:_GetMaxHScroll() > 1) or self._hScrollbar.dragging then
self._hScrollbar:Show()
else
self._hScrollbar:Hide()
end
end
function private.VScrollFrameOnUpdate(self, elapsed)
elapsed = min(elapsed, 0.01)
if self._targetScrollValue then
local scrollValue = self._vScrollbar:GetValue()
local direction = scrollValue < self._targetScrollValue and 1 or -1
local newScrollValue = scrollValue + direction * self._totalScrollDistance * elapsed / SCROLL_TO_DATA_TOTAL_TIME_S
self._vScrollbar:SetValue(newScrollValue)
if direction * newScrollValue >= direction * self._targetScrollValue or newScrollValue <= 0 or newScrollValue >= self:_GetMaxScroll() then
-- we are done scrolling
self._targetScrollValue = nil
self._totalScrollDistance = nil
end
end
local rOffset = max(self._hContent:GetWidth() - self._hScrollFrame:GetWidth() - self._hScrollbar:GetValue(), 0)
if (self._vScrollFrame:IsMouseOver(0, 0, 0, -rOffset) and self:_GetMaxScroll() > 1) or self._vScrollbar.dragging then
self._vScrollbar:Show()
else
self._vScrollbar:Hide()
end
end
function private.FrameOnMouseWheel(self, direction)
local scrollAmount = -direction * Theme.GetMouseWheelScrollAmount()
if IsShiftKeyDown() and self._hScrollbar:IsVisible() then
-- scroll horizontally
self._hScrollbar:SetValue(self._hScrollbar:GetValue() + scrollAmount)
else
self._vScrollbar:SetValue(self._vScrollbar:GetValue() + scrollAmount)
end
end

View File

@@ -0,0 +1,165 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Search List UI Element Class.
-- A search list contains a list of recent or favorite searches. It is a subclass of the @{ScrollingTable} class.
-- @classmod SearchList
local _, TSM = ...
local SearchList = TSM.Include("LibTSMClass").DefineClass("SearchList", TSM.UI.ScrollingTable)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(SearchList)
TSM.UI.SearchList = SearchList
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function SearchList.__init(self)
self.__super:__init()
self._onRowClickHandler = nil
self._onFavoriteChangedHandler = nil
self._onEditClickHandler = nil
self._onDeleteHandler = nil
self._query = nil
self._editBtnHidden = false
end
function SearchList.Acquire(self)
self._headerHidden = true
self.__super:Acquire()
self:GetScrollingTableInfo()
:NewColumn("name")
:SetFont("BODY_BODY3_MEDIUM")
:SetJustifyH("LEFT")
:SetTextFunction(private.GetNameText)
:SetActionIconInfo(3, 18, private.GetActionIcon, true)
:SetActionIconClickHandler(private.OnActionIconClick)
:DisableHiding()
:Commit()
:Commit()
end
function SearchList.Release(self)
self._onRowClickHandler = nil
self._onFavoriteChangedHandler = nil
self._onEditClickHandler = nil
self._onDeleteHandler = nil
if self._query then
self._query:Release()
self._query = nil
end
self._editBtnHidden = false
self.__super:Release()
end
--- Sets whether or not the edit button is hidden.
-- @tparam SearchList self The search list object
-- @tparam boolean hidden Whether or not the edit button is hidden
-- @treturn SearchList The search list object
function SearchList.SetEditButtonHidden(self, hidden)
self._editBtnHidden = hidden
return self
end
--- Sets the @{DatabaseQuery} source for this list.
-- This query is used to populate the entries in the search list.
-- @tparam SearchList self The search list object
-- @tparam DatabaseQuery query The query object
-- @tparam[opt=false] bool redraw Whether or not to redraw the search list
-- @treturn SearchList The search list object
function SearchList.SetQuery(self, query, redraw)
if self._query then
self._query:Release()
end
self._query = query
self._query:SetUpdateCallback(private.QueryUpdateCallback, self)
self:UpdateData(redraw)
return self
end
--- Registers a script handler.
-- @tparam SearchList self The search list object
-- @tparam string script The script to register for (supported scripts: `OnRowClick`, `OnFavoriteChanged`,
-- `OnEditClick`, `OnDelete`)
-- @tparam function handler The script handler which will be called with the search list object followed by any
-- arguments to the script
-- @treturn SearchList The search list object
function SearchList.SetScript(self, script, handler)
if script == "OnRowClick" then
self._onRowClickHandler = handler
elseif script == "OnFavoriteChanged" then
self._onFavoriteChangedHandler = handler
elseif script == "OnEditClick" then
self._onEditClickHandler = handler
elseif script == "OnDelete" then
self._onDeleteHandler = handler
else
error("Unknown SearchList script: "..tostring(script))
end
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function SearchList._UpdateData(self)
wipe(self._data)
for _, row in self._query:Iterator() do
tinsert(self._data, row)
end
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.GetNameText(self, data)
return data:GetField("name")
end
function private.GetActionIcon(self, data, iconIndex, isMouseOver)
if iconIndex == 1 then
return true, data:GetField("isFavorite") and "iconPack.18x18/Star/Filled" or "iconPack.18x18/Star/Unfilled"
elseif iconIndex == 2 then
if self._editBtnHidden then
return false, nil
end
return true, "iconPack.18x18/Edit"
elseif iconIndex == 3 then
return true, "iconPack.18x18/Delete"
else
error("Invalid iconIndex: "..tostring(iconIndex))
end
end
function private.OnActionIconClick(self, data, iconIndex)
if iconIndex == 1 then
-- favorite
self:_onFavoriteChangedHandler(data, not data:GetField("isFavorite"))
elseif iconIndex == 2 then
-- edit
assert(not self._editBtnHidden)
self:_onEditClickHandler(data)
elseif iconIndex == 3 then
-- delete
self:_onDeleteHandler(data)
else
error("Invalid iconIndex: "..tostring(iconIndex))
end
end
function private.QueryUpdateCallback(_, _, self)
self:UpdateData(true)
end

View File

@@ -0,0 +1,61 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- SecureMacroActionButton UI Element Class.
-- A secure macro action button builds on top of WoW's `SecureActionButtonTemplate` to allow executing scripts which
-- addon buttons would otherwise be forbidden from running. It is a subclass of the @{ActionButton} class.
-- @classmod SecureMacroActionButton
local _, TSM = ...
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local SecureMacroActionButton = TSM.Include("LibTSMClass").DefineClass("SecureMacroActionButton", TSM.UI.ActionButton)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(SecureMacroActionButton)
TSM.UI.SecureMacroActionButton = SecureMacroActionButton
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function SecureMacroActionButton.__init(self, name)
self.__super:__init(name, true)
local frame = self:_GetBaseFrame()
frame:SetAttribute("type1", "macro")
frame:SetAttribute("macrotext1", "")
end
function SecureMacroActionButton.Release(self)
local frame = self:_GetBaseFrame()
ScriptWrapper.Clear(frame, "PreClick")
frame:SetAttribute("macrotext1", "")
self.__super:Release()
end
--- Registers a script handler.
-- @tparam SecureMacroActionButton self The secure macro action button object
-- @tparam string script The script to register for (supported scripts: `PreClick`)
-- @tparam function handler The script handler which will be called with the secure macro action button object followed
-- by any arguments to the script
-- @treturn SecureMacroActionButton The secure macro action button object
function SecureMacroActionButton.SetScript(self, script, handler)
if script == "PreClick" or script == "PostClick" then
self.__super.__super:SetScript(script, handler)
else
error("Unknown SecureActionButton script: "..tostring(script))
end
return self
end
--- Sets the macro text which clicking the button executes.
-- @tparam SecureMacroActionButton self The secure macro action button object
-- @tparam string text THe macro text
-- @treturn SecureMacroActionButton The secure macro action button object
function SecureMacroActionButton.SetMacroText(self, text)
self:_GetBaseFrame():SetAttribute("macrotext1", text)
return self
end

View File

@@ -0,0 +1,121 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Selection Dropdown UI Element Class.
-- A dropdown element allows the user to select from a dialog list. It is a subclass of the @{BaseDropdown} class.
-- @classmod SelectionDropdown
local _, TSM = ...
local Table = TSM.Include("Util.Table")
local SelectionDropdown = TSM.Include("LibTSMClass").DefineClass("SelectionDropdown", TSM.UI.BaseDropdown)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(SelectionDropdown)
TSM.UI.SelectionDropdown = SelectionDropdown
-- ============================================================================
-- Meta Class Methods
-- ============================================================================
function SelectionDropdown.__init(self)
self.__super:__init()
self._selectedItem = nil
self._settingTable = nil
self._settingKey = nil
end
function SelectionDropdown.Release(self)
self._selectedItem = nil
self._settingTable = nil
self._settingKey = nil
self.__super:Release()
end
-- ============================================================================
-- Public Class Methods
-- ============================================================================
--- Set the currently selected item.
-- @tparam SelectionDropdown self The dropdown object
-- @tparam ?string item The selected item or nil if nothing should be selected
-- @tparam[opt=false] boolean silent Don't call the OnSelectionChanged callback
-- @treturn SelectionDropdown The dropdown object
function SelectionDropdown.SetSelectedItem(self, item, silent)
self._selectedItem = item
if self._settingTable then
self._settingTable[self._settingKey] = self._itemKeyLookup[item]
end
if not silent and self._onSelectionChangedHandler then
self:_onSelectionChangedHandler()
end
return self
end
--- Set the currently selected item by key.
-- @tparam SelectionDropdown self The dropdown object
-- @tparam ?string itemKey The key for the selected item or nil if nothing should be selected
-- @tparam[opt=false] boolean silent Don't call the OnSelectionChanged callback
-- @treturn SelectionDropdown The dropdown object
function SelectionDropdown.SetSelectedItemByKey(self, itemKey, silent)
local item = itemKey and Table.GetDistinctKey(self._itemKeyLookup, itemKey) or nil
self:SetSelectedItem(item, silent)
return self
end
--- Get the currently selected item.
-- @tparam SelectionDropdown self The dropdown object
-- @treturn ?string The selected item
function SelectionDropdown.GetSelectedItem(self)
return self._selectedItem
end
--- Get the currently selected item.
-- @tparam SelectionDropdown self The dropdown object
-- @treturn ?string The selected item key
function SelectionDropdown.GetSelectedItemKey(self)
return self._selectedItem and self._itemKeyLookup[self._selectedItem] or nil
end
--- Sets the setting info.
-- This method is used to have the value of the dropdown automatically correspond with the value of a field in a table.
-- This is useful for dropdowns which are tied directly to settings.
-- @tparam SelectionDropdown self The dropdown object
-- @tparam table tbl The table which the field to set belongs to
-- @tparam string key The key into the table to be set based on the dropdown state
-- @treturn SelectionDropdown The dropdown object
function SelectionDropdown.SetSettingInfo(self, tbl, key)
self._settingTable = tbl
self._settingKey = key
if tbl then
self:SetSelectedItemByKey(tbl[key])
end
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function SelectionDropdown._AddDialogChildren(self, frame)
frame:AddChild(UIElements.New("DropdownList", "list")
:SetMultiselect(false)
:SetItems(self._items, self._selectedItem)
)
end
function SelectionDropdown._GetCurrentSelectionString(self)
return self._selectedItem or self._hintText
end
function SelectionDropdown._OnListSelectionChanged(self, _, selection)
self:SetOpen(false)
self:SetSelectedItem(selection)
end

View File

@@ -0,0 +1,117 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- SelectionGroupTree UI Element Class.
-- A selection group tree allows for selecting a single group within the tree. It is a subclass of the @{GroupTree} class.
-- @classmod SelectionGroupTree
local _, TSM = ...
local Table = TSM.Include("Util.Table")
local UIElements = TSM.Include("UI.UIElements")
local SelectionGroupTree = TSM.Include("LibTSMClass").DefineClass("SelectionGroupTree", TSM.UI.GroupTree)
UIElements.Register(SelectionGroupTree)
TSM.UI.SelectionGroupTree = SelectionGroupTree
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function SelectionGroupTree.__init(self)
self.__super:__init()
self._selectedGroup = TSM.CONST.ROOT_GROUP_PATH
self._selectedGroupChangedHandler = nil
end
function SelectionGroupTree.Release(self)
self._selectedGroupChangedHandler = nil
self._selectedGroup = TSM.CONST.ROOT_GROUP_PATH
self.__super:Release()
end
--- Sets the selected group path.
-- @tparam SelectionGroupTree self The selection group tree object
-- @tparam string groupPath The group path string to select
-- @treturn SelectionGroupTree The application group tree object
function SelectionGroupTree.SetSelection(self, groupPath)
assert(groupPath)
self._selectedGroup = groupPath
wipe(self._contextTable.selected)
self._contextTable.selected[groupPath] = true
local index = Table.KeyByValue(self._data, groupPath)
assert(index)
-- set the scroll so that the selection is visible if necessary
local rowHeight = self._rowHeight
local firstVisibleIndex = ceil(self._vScrollValue / rowHeight) + 1
local lastVisibleIndex = floor((self._vScrollValue + self:_GetDimension("HEIGHT")) / rowHeight)
if lastVisibleIndex > firstVisibleIndex and (index < firstVisibleIndex or index > lastVisibleIndex) then
self:_OnScrollValueChanged(min((index - 1) * rowHeight, self:_GetMaxScroll()))
end
return self
end
--- Gets the selected group path.
-- @tparam SelectionGroupTree self The selection group tree object
-- @treturn string The currently selected group path string
function SelectionGroupTree.GetSelection(self)
return self._selectedGroup
end
--- Sets the context table.
-- This table can be used to preserve selection state across lifecycles of the application group tree and even WoW
-- sessions if it's within the settings DB.
-- @see GroupTree.SetContextTable
-- @tparam SelectionGroupTree self The application group tree object
-- @tparam table tbl The context table
-- @tparam table defaultTbl The default table (required fields: `selected`, `collapsed`)
-- @treturn SelectionGroupTree The application group tree object
function SelectionGroupTree.SetContextTable(self, tbl, defaultTbl)
assert(type(defaultTbl.selected) == "table")
tbl.selected = tbl.selected or CopyTable(defaultTbl.selected)
self.__super:SetContextTable(tbl, defaultTbl)
return self
end
--- Registers a script handler.
-- @tparam SelectionGroupTree self The selection group tree object
-- @tparam string script The script to register for (supported scripts: `OnGroupSelectionChanged`)
-- @tparam function handler The script handler which will be called with the selection group tree object followed by any
-- arguments to the script
-- @treturn SelectionGroupTree The selection group tree object
function SelectionGroupTree.SetScript(self, script, handler)
if script == "OnGroupSelectionChanged" then
self._selectedGroupChangedHandler = handler
else
error("Unknown SelectionGroupTree script: "..tostring(script))
end
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function SelectionGroupTree._HandleRowClick(self, data, mouseButton)
if mouseButton == "RightButton" then
self.__super:_HandleRowClick(data, mouseButton)
return
end
self._selectedGroup = data
wipe(self._contextTable.selected)
self._contextTable.selected[data] = true
self:Draw()
if self._selectedGroupChangedHandler then
self:_selectedGroupChangedHandler(data)
end
end
function SelectionGroupTree._IsSelected(self, data)
return data == self._selectedGroup
end

View File

@@ -0,0 +1,101 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- SelectionList UI Element Class.
-- A selection list is a scrollable list of entries which allows selecting a single one. It is a subclass of the
-- @{ScrollingTable} class.
-- @classmod SelectionList
local _, TSM = ...
local Theme = TSM.Include("Util.Theme")
local SelectionList = TSM.Include("LibTSMClass").DefineClass("SelectionList", TSM.UI.ScrollingTable)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(SelectionList)
TSM.UI.SelectionList = SelectionList
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function SelectionList.__init(self)
self.__super:__init()
self._selectedEntry = nil
self._onEntrySelectedHandler = nil
end
function SelectionList.Acquire(self)
self._headerHidden = true
self.__super:Acquire()
self:SetSelectionDisabled(true)
self:GetScrollingTableInfo()
:NewColumn("text")
:SetFont("BODY_BODY2")
:SetJustifyH("LEFT")
:SetTextFunction(private.GetText)
:DisableHiding()
:Commit()
:Commit()
end
function SelectionList.Release(self)
self._selectedEntry = nil
self._onEntrySelectedHandler = nil
self.__super:Release()
end
--- Sets the entries.
-- @tparam SelectionList self The selection list object
-- @tparam table entries A list of entries
-- @tparam string selectedEntry The selected entry
-- @treturn SelectionList The selection list object
function SelectionList.SetEntries(self, entries, selectedEntry)
wipe(self._data)
for _, entry in ipairs(entries) do
tinsert(self._data, entry)
end
self._selectedEntry = selectedEntry
return self
end
--- Registers a script handler.
-- @tparam SelectionList self The selection list object
-- @tparam string script The script to register for (supported scripts: `OnEntrySelected`)
-- @tparam function handler The script handler which will be called with the selection list object followed by any
-- arguments to the script
-- @treturn SelectionList The selection list object
function SelectionList.SetScript(self, script, handler)
if script == "OnEntrySelected" then
self._onEntrySelectedHandler = handler
else
error("Unknown SelectionList script: "..tostring(script))
end
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function SelectionList._HandleRowClick(self, data)
if self._onEntrySelectedHandler then
self:_onEntrySelectedHandler(data)
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.GetText(self, data)
return Theme.GetColor(data == self._selectedEntry and "INDICATOR" or "TEXT"):ColorText(data)
end

View File

@@ -0,0 +1,250 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- SelectionScrollingTable UI Element Class.
-- A selection scrolling table is a scrolling table which allows for selecting and deselecting individual rows. It is a
-- subclass of the @{QueryScrollingTable} class.
-- @classmod SelectionScrollingTable
local _, TSM = ...
local Table = TSM.Include("Util.Table")
local TempTable = TSM.Include("Util.TempTable")
local UIElements = TSM.Include("UI.UIElements")
local SelectionScrollingTable = TSM.Include("LibTSMClass").DefineClass("SelectionScrollingTable", TSM.UI.QueryScrollingTable)
UIElements.Register(SelectionScrollingTable)
TSM.UI.SelectionScrollingTable = SelectionScrollingTable
local private = {
querySelectionScrollingTableLookup = {},
sortValuesTemp = {},
tempContextTable = {},
}
local TEMP_CONTEXT_TABLE_DEFAULT = {
colWidth = {
selected = 16,
},
colHidden = {},
}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function SelectionScrollingTable.__init(self)
self.__super:__init()
self._selectedData = {}
self._selectionEnabledFunc = nil
self._rightClickToggle = true
end
function SelectionScrollingTable.Acquire(self)
self.__super:Acquire()
-- temporarily set a context table so we can create the table columns (should be overridden later)
wipe(private.tempContextTable)
self.__super:SetContextTable(private.tempContextTable, TEMP_CONTEXT_TABLE_DEFAULT)
self:GetScrollingTableInfo()
:NewColumn("selected", true)
:SetTitleIcon("iconPack.14x14/Checkmark/Default")
:SetWidth(14)
:SetIconSize(12)
:SetFont("ITEM_BODY3")
:SetJustifyH("CENTER")
:SetIconInfo(nil, private.GetSelectedIcon)
:DisableHiding()
:Commit()
:Commit()
end
function SelectionScrollingTable.Release(self)
private.querySelectionScrollingTableLookup[self._query] = nil
wipe(self._selectedData)
self._selectionEnabledFunc = nil
self.__super:Release()
end
--- Sets the @{DatabaseQuery} source for this table.
-- This query is used to populate the entries in the selection scrolling table.
-- @tparam SelectionScrollingTable self The selection scrolling table object
-- @tparam DatabaseQuery query The query object
-- @tparam[opt=false] bool redraw Whether or not to redraw the selection scrolling table
-- @treturn SelectionScrollingTable The selection scrolling table object
function SelectionScrollingTable.SetQuery(self, query, redraw)
if self._query then
private.querySelectionScrollingTableLookup[self._query] = nil
end
private.querySelectionScrollingTableLookup[query] = self
self.__super:SetQuery(query, redraw)
return self
end
--- Selects all items.
-- @tparam SelectionScrollingTable self The selection scrolling table object
function SelectionScrollingTable.SelectAll(self)
for _, uuid in ipairs(self._data) do
self._selectedData[uuid] = true
end
self:_UpdateData()
self:Draw()
if self._onSelectionChangedHandler then
self._onSelectionChangedHandler(self)
end
end
--- Clear the selection.
-- @tparam SelectionScrollingTable self The selection scrolling table object
function SelectionScrollingTable.ClearSelection(self)
wipe(self._selectedData)
self:_UpdateData()
self:Draw()
if self._onSelectionChangedHandler then
self._onSelectionChangedHandler(self)
end
end
--- Sets a selection enabled function.
-- @tparam SelectionScrollingTable self The selection scrolling table object
-- @tparam function func A funciton which gets called with data to determine if it's selectable or not
-- @treturn SelectionScrollingTable The selection scrolling table object
function SelectionScrollingTable.SetIsSelectionEnabledFunc(self, func)
self._selectionEnabledFunc = func
return self
end
--- Toggles the selection of a record.
-- @tparam SelectionScrollingTable self The selection scrolling table object
-- @tparam ?table data The record to toggle the selection of
-- @treturn SelectionScrollingTable The selection scrolling table object
function SelectionScrollingTable.SetSelection(self, data)
if data and self._selectionValidator and not self:_selectionValidator(self._query:GetResultRowByUUID(data)) then
assert(not self._selectedData[data])
return self
end
self._selectedData[data] = not self._selectedData[data] or nil
for _, row in ipairs(self._rows) do
if row:GetData() == data then
self:_SetRowData(row, data)
break
end
end
if self._sortCol == "selected" then
self:_UpdateData()
self:Draw()
end
if self._onSelectionChangedHandler then
self._onSelectionChangedHandler(self)
end
return self
end
--- Gets whether or not all of the items are currently selected.
-- @tparam SelectionScrollingTable self The selection scrolling table object
-- @treturn boolean Whether or not all of the selection is selected
function SelectionScrollingTable.IsAllSelected(self)
for _, uuid in ipairs(self._data) do
if not self._selectedData[uuid] then
return false
end
end
return true
end
--- Gets whether or not the selection is currently cleared.
-- @tparam SelectionScrollingTable self The selection scrolling table object
-- @treturn boolean Whether or not the selection is cleared
function SelectionScrollingTable.IsSelectionCleared(self)
return not next(self._selectedData)
end
--- Gets the current selection table.
-- @tparam SelectionScrollingTable self The selection scrolling table object
-- @treturn table A table where the key is the data and the value is whether or not it's selected (only selected entries
-- are in the table)
function SelectionScrollingTable.SelectionIterator(self)
return private.SelectionIteratorHelper, self
end
--- Sets the context table.
-- This table can be used to preserve the table configuration across lifecycles of the scrolling table and even WoW
-- sessions if it's within the settings DB.
-- @tparam SelectionScrollingTable self The selection scrolling table object
-- @tparam table tbl The context table
-- @tparam table defaultTbl The default table (required fields: `colWidth`)
-- @treturn SelectionScrollingTable The selection scrolling table object
function SelectionScrollingTable.SetContextTable(self, tbl, defaultTbl)
assert(defaultTbl.colWidth.selected)
self.__super:SetContextTable(tbl, defaultTbl)
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function SelectionScrollingTable._IsSelected(self, data)
return self._selectedData[data] == 1
end
function SelectionScrollingTable._UpdateData(self)
self.__super:_UpdateData()
-- clear any old selection context
local hasData = TempTable.Acquire()
for _, data in ipairs(self._data) do
hasData[data] = true
end
for data in pairs(self._selectedData) do
if not hasData[data] then
self._selectedData[data] = nil
end
end
TempTable.Release(hasData)
if self._sortCol == "selected" then
local selectedValue = self._sortAscending and -1 or 1
for _, uuid in ipairs(self._data) do
private.sortValuesTemp[uuid] = self._selectedData[uuid] and selectedValue or 0
end
Table.SortWithValueLookup(self._data, private.sortValuesTemp)
wipe(private.sortValuesTemp)
end
end
function SelectionScrollingTable._ToggleSort(self, id)
if id ~= "selected" then
return self.__super:_ToggleSort(id)
end
if id == self._sortCol then
self._sortAscending = not self._sortAscending
else
self._sortCol = id
self._sortAscending = true
end
self:_UpdateData()
self:Draw()
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.SelectionIteratorHelper(self, uuid)
uuid = next(self._selectedData, uuid)
if not uuid then
return
end
return uuid, self._query:GetResultRowByUUID(uuid)
end
function private.GetSelectedIcon(row)
local self = private.querySelectionScrollingTableLookup[row:GetQuery()]
return self._selectedData[row:GetUUID()] and "iconPack.14x14/Checkmark/Default" or 0
end

View File

@@ -0,0 +1,105 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- SimpleTabGroup UI Element Class.
-- A simple table group uses text to denote tabs with the selected one colored differently. It is a subclass of the
-- @{ViewContainer} class.
-- @classmod SimpleTabGroup
local _, TSM = ...
local SimpleTabGroup = TSM.Include("LibTSMClass").DefineClass("SimpleTabGroup", TSM.UI.ViewContainer)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(SimpleTabGroup)
TSM.UI.SimpleTabGroup = SimpleTabGroup
local private = {}
local BUTTON_HEIGHT = 24
local BUTTON_PADDING_BOTTOM = 2
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function SimpleTabGroup.__init(self)
self.__super:__init()
self._buttons = {}
end
function SimpleTabGroup.Acquire(self)
self.__super.__super:AddChildNoLayout(UIElements.New("Frame", "buttons")
:SetLayout("HORIZONTAL")
:AddAnchor("TOPLEFT")
:AddAnchor("TOPRIGHT")
)
self.__super:Acquire()
end
function SimpleTabGroup.Release(self)
wipe(self._buttons)
self.__super:Release()
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function SimpleTabGroup._GetContentPadding(self, side)
if side == "TOP" then
return BUTTON_HEIGHT + BUTTON_PADDING_BOTTOM
end
return self.__super:_GetContentPadding(side)
end
function SimpleTabGroup.Draw(self)
self.__super.__super.__super:Draw()
local selectedPath = self:GetPath()
local buttons = self:GetElement("buttons")
buttons:SetHeight(BUTTON_HEIGHT + BUTTON_PADDING_BOTTOM)
buttons:ReleaseAllChildren()
for i, buttonPath in ipairs(self._pathsList) do
local isSelected = buttonPath == selectedPath
buttons:AddChild(UIElements.New("Button", self._id.."_Tab"..i)
:SetWidth("AUTO")
:SetMargin(8, 8, 0, BUTTON_PADDING_BOTTOM)
:SetJustifyH("LEFT")
:SetFont("BODY_BODY1_BOLD")
:SetTextColor(isSelected and "INDICATOR" or "TEXT_ALT")
:SetContext(self)
:SetText(buttonPath)
:SetScript("OnEnter", not isSelected and private.OnButtonEnter)
:SetScript("OnLeave", not isSelected and private.OnButtonLeave)
:SetScript("OnClick", private.OnButtonClicked)
)
end
self.__super:Draw()
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.OnButtonEnter(button)
button:SetTextColor("TEXT")
:Draw()
end
function private.OnButtonLeave(button)
button:SetTextColor("TEXT_ALT")
:Draw()
end
function private.OnButtonClicked(button)
local self = button:GetContext()
local path = button:GetText()
self:SetPath(path, true)
end

266
Core/UI/Elements/Slider.lua Normal file
View File

@@ -0,0 +1,266 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Slider UI Element Class.
-- A slider allows for selecting a numerical range. It is a subclass of the @{Element} class.
-- @classmod Slider
local _, TSM = ...
local Math = TSM.Include("Util.Math")
local NineSlice = TSM.Include("Util.NineSlice")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local Theme = TSM.Include("Util.Theme")
local Slider = TSM.Include("LibTSMClass").DefineClass("Slider", TSM.UI.Element)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(Slider)
TSM.UI.Slider = Slider
local private = {}
local THUMB_WIDTH = 8
local INPUT_WIDTH = 50
local INPUT_AREA_SPACE = 128
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function Slider.__init(self)
local frame = UIElements.CreateFrame(self, "Frame")
frame:EnableMouse(true)
ScriptWrapper.Set(frame, "OnMouseDown", private.FrameOnMouseDown, self)
ScriptWrapper.Set(frame, "OnMouseUp", private.FrameOnMouseUp, self)
ScriptWrapper.Set(frame, "OnUpdate", private.FrameOnUpdate, self)
self.__super:__init(frame)
-- create the textures
frame.barTexture = frame:CreateTexture(nil, "BACKGROUND", nil, 1)
frame.activeBarTexture = frame:CreateTexture(nil, "BACKGROUND", nil, 2)
frame.thumbTextureLeft = frame:CreateTexture(nil, "ARTWORK")
frame.thumbTextureRight = frame:CreateTexture(nil, "ARTWORK")
frame.inputLeft = CreateFrame("EditBox", nil, frame, nil)
frame.inputLeft:SetJustifyH("CENTER")
frame.inputLeft:SetWidth(INPUT_WIDTH)
frame.inputLeft:SetHeight(24)
frame.inputLeft:SetAutoFocus(false)
frame.inputLeft:SetNumeric(true)
ScriptWrapper.Set(frame.inputLeft, "OnEscapePressed", private.InputOnEscapePressed)
ScriptWrapper.Set(frame.inputLeft, "OnEnterPressed", private.LeftInputOnEnterPressed, self)
frame.dash = UIElements.CreateFontString(self, frame)
frame.dash:SetJustifyH("CENTER")
frame.dash:SetJustifyV("MIDDLE")
frame.dash:SetWidth(12)
frame.inputRight = CreateFrame("EditBox", nil, frame, nil)
frame.inputRight:SetJustifyH("CENTER")
frame.inputRight:SetWidth(INPUT_WIDTH)
frame.inputRight:SetHeight(24)
frame.inputRight:SetNumeric(true)
frame.inputRight:SetAutoFocus(false)
ScriptWrapper.Set(frame.inputRight, "OnEscapePressed", private.InputOnEscapePressed)
ScriptWrapper.Set(frame.inputRight, "OnEnterPressed", private.RightInputOnEnterPressed, self)
self._inputLeftNineSlice = NineSlice.New(frame.inputLeft)
self._inputRightNineSlice = NineSlice.New(frame.inputRight)
self._leftValue = nil
self._rightValue = nil
self._minValue = nil
self._maxValue = nil
self._dragging = nil
end
function Slider.Release(self)
self._leftValue = nil
self._rightValue = nil
self._minValue = nil
self._maxValue = nil
self._dragging = nil
self.__super:Release()
end
--- Set the extends of the possible range.
-- @tparam Slider self The slider object
-- @tparam number minValue The minimum value
-- @tparam number maxValue The maxmimum value
-- @treturn Slider The slider object
function Slider.SetRange(self, minValue, maxValue)
self._minValue = minValue
self._maxValue = maxValue
self._leftValue = minValue
self._rightValue = maxValue
return self
end
--- Sets the current value.
-- @tparam Slider self The slider object
-- @tparam number leftValue The lower end of the range
-- @tparam number rightValue The upper end of the range
-- @treturn Slider The slider object
function Slider.SetValue(self, leftValue, rightValue)
assert(leftValue < rightValue and leftValue >= self._minValue and rightValue <= self._maxValue)
self._leftValue = leftValue
self._rightValue = rightValue
return self
end
--- Gets the current value
-- @tparam Slider self The slider object
-- @treturn number The lower end of the range
-- @treturn number The upper end of the range
function Slider.GetValue(self)
return self._leftValue, self._rightValue
end
function Slider.Draw(self)
self.__super:Draw()
local frame = self:_GetBaseFrame()
local inputColor = Theme.GetColor("ACTIVE_BG")
self._inputLeftNineSlice:SetStyle("rounded")
self._inputRightNineSlice:SetStyle("rounded")
self._inputLeftNineSlice:SetVertexColor(inputColor:GetFractionalRGBA())
self._inputRightNineSlice:SetVertexColor(inputColor:GetFractionalRGBA())
local sliderHeight = self:_GetDimension("HEIGHT") / 2
local width = self:_GetDimension("WIDTH") - INPUT_AREA_SPACE
local leftPos = Math.Scale(self._leftValue, self._minValue, self._maxValue, 0, width - THUMB_WIDTH)
local rightPos = Math.Scale(self._rightValue, self._minValue, self._maxValue, 0, width - THUMB_WIDTH)
local fontPath, fontHeight = Theme.GetFont("BODY_BODY1"):GetWowFont()
local textColor = Theme.GetColor("TEXT")
-- wow renders the font slightly bigger than the designs would indicate, so subtract one from the font height
frame.inputRight:SetFont(fontPath, fontHeight)
frame.inputRight:SetTextColor(textColor:GetFractionalRGBA())
frame.inputRight:SetPoint("RIGHT", 0)
frame.inputRight:SetNumber(self._rightValue)
frame.dash:SetFont(fontPath, fontHeight)
frame.dash:SetTextColor(textColor:GetFractionalRGBA())
frame.dash:SetText("-")
frame.dash:SetPoint("RIGHT", frame.inputRight, "LEFT", 0, 0)
-- wow renders the font slightly bigger than the designs would indicate, so subtract one from the font height
frame.inputLeft:SetFont(fontPath, fontHeight)
frame.inputLeft:SetTextColor(textColor:GetFractionalRGBA())
frame.inputLeft:SetPoint("RIGHT", frame.dash, "LEFT", 0)
frame.inputLeft:SetNumber(self._leftValue)
frame.barTexture:ClearAllPoints()
frame.barTexture:SetPoint("LEFT", 0, 0)
frame.barTexture:SetPoint("RIGHT", frame.inputLeft, "LEFT", -16, 0)
frame.barTexture:SetHeight(sliderHeight / 3)
frame.barTexture:SetColorTexture(Theme.GetColor("FRAME_BG"):GetFractionalRGBA())
TSM.UI.TexturePacks.SetTextureAndSize(frame.thumbTextureLeft, "iconPack.14x14/Circle")
frame.thumbTextureLeft:SetPoint("LEFT", frame.barTexture, leftPos, 0)
TSM.UI.TexturePacks.SetTextureAndSize(frame.thumbTextureRight, "iconPack.14x14/Circle")
frame.thumbTextureRight:SetPoint("LEFT", frame.barTexture, rightPos, 0)
frame.activeBarTexture:SetPoint("LEFT", frame.thumbTextureLeft, "CENTER")
frame.activeBarTexture:SetPoint("RIGHT", frame.thumbTextureRight, "CENTER")
frame.activeBarTexture:SetHeight(sliderHeight / 3)
frame.activeBarTexture:SetColorTexture(Theme.GetColor("TEXT"):GetFractionalRGBA())
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function Slider._GetCursorPositionValue(self)
local frame = self:_GetBaseFrame()
local x = GetCursorPosition() / frame:GetEffectiveScale()
local left = frame:GetLeft() + THUMB_WIDTH / 2
local right = frame:GetRight() - THUMB_WIDTH - INPUT_AREA_SPACE * 2 / 2
x = min(max(x, left), right)
local value = Math.Scale(x, left, right, self._minValue, self._maxValue)
return min(max(Math.Round(value), self._minValue), self._maxValue)
end
function Slider._UpdateLeftValue(self, value)
local newValue = max(min(value, self._rightValue), self._minValue)
if newValue == self._leftValue then
return
end
self._leftValue = newValue
self:Draw()
end
function Slider._UpdateRightValue(self, value)
local newValue = min(max(value, self._leftValue), self._maxValue)
if newValue == self._rightValue then
return
end
self._rightValue = newValue
self:Draw()
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.InputOnEscapePressed(input)
input:ClearFocus()
end
function private.LeftInputOnEnterPressed(self)
local input = self:_GetBaseFrame().inputLeft
self:_UpdateLeftValue(input:GetNumber())
end
function private.RightInputOnEnterPressed(self)
local input = self:_GetBaseFrame().inputRight
self:_UpdateRightValue(input:GetNumber())
end
function private.FrameOnMouseDown(self)
local frame = self:_GetBaseFrame()
frame.inputLeft:ClearFocus()
frame.inputRight:ClearFocus()
local value = self:_GetCursorPositionValue()
local leftDiff = abs(value - self._leftValue)
local rightDiff = abs(value - self._rightValue)
if value < self._leftValue then
-- clicked to the left of the left thumb, so drag that
self._dragging = "left"
elseif value > self._rightValue then
-- clicked to the right of the right thumb, so drag that
self._dragging = "right"
elseif self._leftValue == self._rightValue then
-- just ignore this click since they clicked on both thumbs
elseif leftDiff < rightDiff then
-- clicked closer to the left thumb, so drag that
self._dragging = "left"
else
-- clicked closer to the right thumb (or right in the middle), so drag that
self._dragging = "right"
end
end
function private.FrameOnMouseUp(self)
self._dragging = nil
end
function private.FrameOnUpdate(self)
if not self._dragging then
return
end
if self._dragging == "left" then
self:_UpdateLeftValue(self:_GetCursorPositionValue())
elseif self._dragging == "right" then
self:_UpdateRightValue(self:_GetCursorPositionValue())
else
error("Unexpected dragging: "..tostring(self._dragging))
end
end

View File

@@ -0,0 +1,128 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- SniperScrollingTable UI Element Class.
-- A special shopping scrolling table used for sniper which has an extra icon column on the left. It is a subclass of
-- the @{AuctionScrollingTable} class.
-- @classmod SniperScrollingTable
local _, TSM = ...
local SniperScrollingTable = TSM.Include("LibTSMClass").DefineClass("SniperScrollingTable", TSM.UI.AuctionScrollingTable)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(SniperScrollingTable)
TSM.UI.SniperScrollingTable = SniperScrollingTable
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function SniperScrollingTable.__init(self)
self.__super:__init()
self._highestBrowseId = 0
self._onRowRemovedHandler = nil
end
function SniperScrollingTable.Acquire(self)
self.__super:Acquire()
self:GetScrollingTableInfo()
:NewColumn("icon", true)
:SetTitleIcon("iconPack.14x14/Attention")
:SetIconSize(14)
:SetIconHoverEnabled(true)
:SetIconClickHandler(private.RemoveIconClickHandler)
:SetIconFunction(private.RemoveIconFunction)
:SetJustifyH("CENTER")
:SetFont("BODY_BODY3")
:Commit()
:RemoveColumn("timeLeft")
:Commit()
if TSM.IsWowClassic() then
self._sortCol = "icon"
self._sortAscending = true
end
self._highestBrowseId = 0
end
function SniperScrollingTable.Release(self)
self._onRowRemovedHandler = nil
self.__super:Release()
end
--- Registers a script handler.
-- @tparam SniperScrollingTable self The sniper scrolling table object
-- @tparam string script The script to register for (supported scripts: `OnRowRemoved`)
-- @tparam function handler The script handler which will be called with the sniper scrolling table object followed by
-- any arguments to the script
-- @treturn SniperScrollingTable The sniper scrolling table object
function SniperScrollingTable.SetScript(self, script, handler)
if script == "OnRowRemoved" then
self._onRowRemovedHandler = handler
else
self.__super:SetScript(script, handler)
end
return self
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function SniperScrollingTable._UpdateData(self, queryChanged)
self.__super:_UpdateData(queryChanged)
self._highestBrowseId = 0
for _, row in ipairs(self._data) do
if row:IsSubRow() then
local _, _, browseId = row:GetListingInfo()
self._highestBrowseId = max(self._highestBrowseId, browseId or 0)
end
end
end
function SniperScrollingTable._GetSortValue(self, row, id, isAscending)
if id == "icon" then
if not row:IsSubRow() then
return 0
end
local _, _, browseId = row:GetListingInfo()
return -browseId
else
return self.__super:_GetSortValue(row, id, isAscending)
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.RemoveIconClickHandler(self, subRow)
if not subRow:IsSubRow() then
local baseItemString = subRow:GetBaseItemString()
subRow = self._firstSubRowByItem[baseItemString] or subRow
end
if self._onRowRemovedHandler then
self:_onRowRemovedHandler(subRow)
end
end
function private.RemoveIconFunction(self, row, isMouseOver)
if isMouseOver then
return "iconPack.14x14/Close/Default"
end
local isRecent = true
if row:IsSubRow() then
local _, _, browseId = row:GetListingInfo()
isRecent = self._highestBrowseId == browseId
end
return isRecent and "iconPack.14x14/Attention" or "iconPack.14x14/Close/Default"
end

106
Core/UI/Elements/Spacer.lua Normal file
View File

@@ -0,0 +1,106 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Spacer UI Element Class.
-- A spacer is a light-weight element which doesn't have any content but can be used to assist with layouts. It is a
-- subclass of the @{Element} class.
-- @classmod Spacer
local _, TSM = ...
local Spacer = TSM.Include("LibTSMClass").DefineClass("Spacer", TSM.UI.Element)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(Spacer)
TSM.UI.Spacer = Spacer
-- ============================================================================
-- Fake Frame Methods
-- ============================================================================
local FAKE_FRAME_MT = {
__index = {
SetParent = function(self, parent)
self._parent = parent
end,
GetParent = function(self)
return self._parent
end,
SetScale = function(self, scale)
self._scale = scale
end,
GetScale = function(self)
return self._scale
end,
SetWidth = function(self, width)
self._width = width
end,
GetWidth = function(self)
return self._width
end,
SetHeight = function(self, height)
self._height = height
end,
GetHeight = function(self)
return self._height
end,
Show = function(self)
self._visible = true
end,
Hide = function(self)
self._visible = false
end,
IsVisible = function(self)
return self._visible
end,
ClearAllPoints = function(self)
-- do nothing
end,
SetPoint = function(self, ...)
-- do nothing
end,
},
}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function Spacer.__init(self)
self.__super:__init(self)
local fakeFrame = {
_parent = nil,
_scale = 1,
_width = 0,
_height = 0,
_visible = false,
}
self._fakeFrame = setmetatable(fakeFrame, FAKE_FRAME_MT)
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function Spacer._GetBaseFrame(self)
return self._fakeFrame
end

View File

@@ -0,0 +1,113 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- TabGroup UI Element Class.
-- A tab group uses text and a horizontal line to denote the tabs, with coloring indicating the one which is selected.
-- It is a subclass of the @{ViewContainer} class.
-- @classmod TabGroup
local _, TSM = ...
local TabGroup = TSM.Include("LibTSMClass").DefineClass("TabGroup", TSM.UI.ViewContainer)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(TabGroup)
TSM.UI.TabGroup = TabGroup
local private = {}
local BUTTON_HEIGHT = 24
local BUTTON_PADDING_BOTTOM = 4
local LINE_THICKNESS = 2
local LINE_THICKNESS_SELECTED = 2
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function TabGroup.__init(self)
self.__super:__init()
self._buttons = {}
end
function TabGroup.Acquire(self)
self.__super.__super:AddChildNoLayout(UIElements.New("Frame", "buttons")
:SetLayout("HORIZONTAL")
:AddAnchor("TOPLEFT")
:AddAnchor("TOPRIGHT")
)
self.__super:Acquire()
end
function TabGroup.Release(self)
wipe(self._buttons)
self.__super:Release()
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function TabGroup._GetContentPadding(self, side)
if side == "TOP" then
return BUTTON_HEIGHT + BUTTON_PADDING_BOTTOM + LINE_THICKNESS
end
return self.__super:_GetContentPadding(side)
end
function TabGroup.Draw(self)
self.__super.__super.__super:Draw()
local selectedPath = self:GetPath()
local buttons = self:GetElement("buttons")
buttons:SetHeight(BUTTON_HEIGHT + BUTTON_PADDING_BOTTOM + LINE_THICKNESS)
buttons:ReleaseAllChildren()
for i, buttonPath in ipairs(self._pathsList) do
local isSelected = buttonPath == selectedPath
buttons:AddChild(UIElements.New("Frame", self._id.."_Tab"..i)
:SetLayout("VERTICAL")
:AddChild(UIElements.New("Button", "button")
:SetMargin(0, 0, 0, BUTTON_PADDING_BOTTOM)
:SetFont("BODY_BODY1_BOLD")
:SetJustifyH("CENTER")
:SetTextColor(isSelected and "INDICATOR" or "TEXT_ALT")
:SetContext(self)
:SetText(buttonPath)
:SetScript("OnEnter", not isSelected and private.OnButtonEnter)
:SetScript("OnLeave", not isSelected and private.OnButtonLeave)
:SetScript("OnClick", private.OnButtonClicked)
)
:AddChild(UIElements.New("Texture", "line")
:SetHeight(isSelected and LINE_THICKNESS_SELECTED or LINE_THICKNESS)
:SetTexture(isSelected and "INDICATOR" or "TEXT_ALT")
)
)
end
self.__super:Draw()
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.OnButtonEnter(button)
button:SetTextColor("TEXT")
:Draw()
end
function private.OnButtonLeave(button)
button:SetTextColor("TEXT_ALT")
:Draw()
end
function private.OnButtonClicked(button)
local self = button:GetContext()
local path = button:GetText()
self:SetPath(path, self:GetPath() ~= path)
end

217
Core/UI/Elements/Text.lua Normal file
View File

@@ -0,0 +1,217 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Text UI Element Class.
-- A text element simply holds a text string. It is a subclass of the @{Element} class.
-- @classmod Text
local _, TSM = ...
local Text = TSM.Include("LibTSMClass").DefineClass("Text", TSM.UI.Element)
local Color = TSM.Include("Util.Color")
local Theme = TSM.Include("Util.Theme")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(Text)
TSM.UI.Text = Text
local STRING_RIGHT_PADDING = 4
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function Text.__init(self, frame)
frame = frame or UIElements.CreateFrame(self, "Frame")
self.__super:__init(frame)
frame.text = UIElements.CreateFontString(self, frame)
self._textStr = ""
self._autoWidth = false
self._textColor = "TEXT"
self._font = nil
self._justifyH = "LEFT"
self._justifyV = "MIDDLE"
end
function Text.Release(self)
self._textStr = ""
self._autoWidth = false
self._textColor = "TEXT"
self._font = nil
self._justifyH = "LEFT"
self._justifyV = "MIDDLE"
self:_GetBaseFrame().text:SetSpacing(0)
self.__super:Release()
end
--- Sets the width of the text.
-- @tparam Text self The text object
-- @tparam ?number|string width The width of the text, "AUTO" to set the width based on the length
-- of the text, or nil to have an undefined width
-- @treturn Text The text object
function Text.SetWidth(self, width)
if width == "AUTO" then
self._autoWidth = true
else
self._autoWidth = false
self.__super:SetWidth(width)
end
return self
end
--- Sets the font.
-- @tparam Text self The text object
-- @tparam string font The font key
-- @treturn Text The text object
function Text.SetFont(self, font)
assert(Theme.GetFont(font))
self._font = font
return self
end
--- Sets the color of the text.
-- @tparam Text self The text object
-- @tparam Color|string color The text color as a Color object or a theme color key
-- @treturn Text The text object
function Text.SetTextColor(self, color)
assert((type(color) == "string" and Theme.GetColor(color)) or Color.IsInstance(color))
self._textColor = color
return self
end
--- Sets the horizontal justification of the text.
-- @tparam Text self The text object
-- @tparam string justifyH The horizontal justification (either "LEFT", "CENTER" or "RIGHT")
-- @treturn Text The text object
function Text.SetJustifyH(self, justifyH)
assert(justifyH == "LEFT" or justifyH == "CENTER" or justifyH == "RIGHT")
self._justifyH = justifyH
return self
end
--- Sets the vertical justification of the text.
-- @tparam Text self The text object
-- @tparam string justifyV The vertical justification (either "TOP", "MIDDLE" or "BOTTOM")
-- @treturn Text The text object
function Text.SetJustifyV(self, justifyV)
assert(justifyV == "TOP" or justifyV == "MIDDLE" or justifyV == "BOTTOM")
self._justifyV = justifyV
return self
end
--- Set the text.
-- @tparam Text self The text object
-- @tparam ?string|number text The text
-- @treturn Text The text object
function Text.SetText(self, text)
if type(text) == "number" then
text = tostring(text)
end
assert(type(text) == "string")
self._textStr = text
return self
end
--- Set formatted text.
-- @tparam Text self The text object
-- @tparam vararg ... The format string and parameters
-- @treturn Text The text object
function Text.SetFormattedText(self, ...)
self:SetText(format(...))
return self
end
--- Gets the text string.
-- @tparam Text self The text object
-- @treturn string The text string
function Text.GetText(self)
return self._textStr
end
--- Get the rendered text string width.
-- @tparam Text self The text object
-- @treturn number The rendered text string width
function Text.GetStringWidth(self)
local text = self:_GetBaseFrame().text
self:_ApplyFont()
text:SetText(self._textStr)
return text:GetStringWidth()
end
--- Get the rendered text string height.
-- @tparam Text self The text object
-- @treturn number The rendered text string height
function Text.GetStringHeight(self)
local text = self:_GetBaseFrame().text
self:_ApplyFont()
text:SetText(self._textStr)
return text:GetStringHeight()
end
function Text.Draw(self)
self.__super:Draw()
local text = self:_GetBaseFrame().text
text:ClearAllPoints()
text:SetAllPoints()
-- set the font
self:_ApplyFont()
-- set the justification
text:SetJustifyH(self._justifyH)
text:SetJustifyV(self._justifyV)
-- set the text color
text:SetTextColor(self:_GetTextColor():GetFractionalRGBA())
-- set the text
text:SetText(self._textStr)
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function Text._GetTextColor(self)
if type(self._textColor) == "string" then
return Theme.GetColor(self._textColor)
else
assert(Color.IsInstance(self._textColor))
return self._textColor
end
end
function Text._GetMinimumDimension(self, dimension)
if dimension == "WIDTH" and self._autoWidth then
return 0, self._width == nil
else
return self.__super:_GetMinimumDimension(dimension)
end
end
function Text._GetPreferredDimension(self, dimension)
if dimension == "WIDTH" and self._autoWidth then
return self:GetStringWidth() + STRING_RIGHT_PADDING
else
return self.__super:_GetPreferredDimension(dimension)
end
end
function Text._ApplyFont(self)
local text = self:_GetBaseFrame().text
local font = Theme.GetFont(self._font)
text:SetFont(font:GetWowFont())
-- There's a Blizzard bug where spacing incorrectly gets applied to embedded textures, so just set it to 0 in that case
-- TODO: come up with a better fix if we need multi-line text with embedded textures
if strfind(self._textStr, "\124T") then
text:SetSpacing(0)
else
text:SetSpacing(font:GetSpacing())
end
end

View File

@@ -0,0 +1,106 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Texture UI Element Class.
-- This is a simple, light-weight element which is used to display a texture. It is a subclass of the @{Element} class.
-- @classmod Texture
local _, TSM = ...
local Texture = TSM.Include("LibTSMClass").DefineClass("Texture", TSM.UI.Element)
local Color = TSM.Include("Util.Color")
local Theme = TSM.Include("Util.Theme")
local ItemInfo = TSM.Include("Service.ItemInfo")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(Texture)
TSM.UI.Texture = Texture
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function Texture.__init(self)
local texture = UIParent:CreateTexture()
-- hook SetParent/GetParent since textures can't have a nil parent
texture._oldSetParent = texture.SetParent
texture.SetParent = private.SetParent
texture.GetParent = private.GetParent
self.__super:__init(texture)
self._texture = nil
end
function Texture.Release(self)
self._texture = nil
self.__super:Release()
end
--- Sets the texture.
-- @tparam Texture self The texture object
-- @tparam ?string|number texture Either a texture pack string, itemString, WoW file id, or theme color key
-- @treturn Texture The texture object
function Texture.SetTexture(self, texture)
self._texture = texture
return self
end
--- Sets the texture and size based on a texture pack string.
-- @tparam Texture self The texture object
-- @tparam string texturePack A texture pack string
-- @treturn Texture The texture object
function Texture.SetTextureAndSize(self, texturePack)
self:SetTexture(texturePack)
self:SetSize(TSM.UI.TexturePacks.GetSize(texturePack))
return self
end
function Texture.Draw(self)
self.__super:Draw()
local texture = self:_GetBaseFrame()
texture:SetTexture(nil)
texture:SetTexCoord(0, 1, 0, 1)
texture:SetVertexColor(1, 1, 1, 1)
if type(self._texture) == "string" and TSM.UI.TexturePacks.IsValid(self._texture) then
-- this is a texture pack
TSM.UI.TexturePacks.SetTexture(texture, self._texture)
elseif type(self._texture) == "string" and strmatch(self._texture, "^[ip]:%d+") then
-- this is an itemString
texture:SetTexture(ItemInfo.GetTexture(self._texture))
elseif type(self._texture) == "string" then
-- this is a theme color key
texture:SetColorTexture(Theme.GetColor(self._texture):GetFractionalRGBA())
elseif type(self._texture) == "number" then
-- this is a wow file id
texture:SetTexture(self._texture)
elseif Color.IsInstance(self._texture) then
texture:SetColorTexture(self._texture:GetFractionalRGBA())
else
error("Invalid texture: "..tostring(self._texture))
end
end
-- ============================================================================
-- Private Helper Functions
-- ============================================================================
function private.SetParent(self, parent)
self._parent = parent
if parent then
self:Show()
else
self:Hide()
end
self:_oldSetParent(parent or UIParent)
end
function private.GetParent(self)
return self._parent
end

180
Core/UI/Elements/Toggle.lua Normal file
View File

@@ -0,0 +1,180 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- Toggle UI Element Class.
-- A toggle element allows the user to select between a fixed set of options. It is a subclass of the @{Container} class.
-- @classmod Toggle
local _, TSM = ...
local Toggle = TSM.Include("LibTSMClass").DefineClass("Toggle", TSM.UI.Container)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(Toggle)
TSM.UI.Toggle = Toggle
local private = {}
local BUTTON_PADDING = 16
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function Toggle.__init(self)
local frame = UIElements.CreateFrame(self, "Frame")
self.__super:__init(frame)
self._optionsList = {}
self._buttons = {}
self._onValueChangedHandler = nil
self._selectedOption = nil
self._booleanKey = nil
self._font = "BODY_BODY3"
end
function Toggle.Release(self)
wipe(self._optionsList)
wipe(self._buttons)
self._onValueChangedHandler = nil
self._selectedOption = nil
self._booleanKey = nil
self._font = "BODY_BODY3"
self.__super:Release()
end
--- Add an option.
-- @tparam Toggle self The toggle object
-- @tparam string option The text that goes with the option
-- @tparam boolean setSelected Whether or not to set this as the selected option
-- @treturn Toggle The toggle object
function Toggle.AddOption(self, option, setSelected)
tinsert(self._optionsList, option)
if setSelected then
self:SetOption(option)
end
return self
end
--- Sets the currently selected option.
-- @tparam Toggle self The toggle object
-- @tparam string option The selected option
-- @tparam boolean redraw Whether or not to redraw the toggle
-- @treturn Toggle The toggle object
function Toggle.SetOption(self, option, redraw)
if option ~= self._selectedOption then
self._selectedOption = option
if self._onValueChangedHandler then
self:_onValueChangedHandler(option)
end
end
if redraw then
self:Draw()
end
return self
end
--- Clears the currently selected option.
-- @tparam Toggle self The toggle object
-- @tparam boolean redraw Whether or not to redraw the toggle
-- @treturn Toggle The toggle object
function Toggle.ClearOption(self, redraw)
self._selectedOption = nil
if redraw then
self:Draw()
end
return self
end
--- Sets whether or not the toggle is disabled.
-- @tparam Toggle self The toggle object
-- @tparam boolean disabled Whether or not the toggle is disabled
-- @treturn Toggle The toggle object
function Toggle.SetDisabled(self, disabled)
self._disabled = disabled
return self
end
--- Registers a script handler.
-- @tparam Toggle self The toggle object
-- @tparam string script The script to register for (supported scripts: `OnValueChanged`)
-- @tparam function handler The script handler which will be called with the toggle object followed by any arguments to
-- the script
-- @treturn Toggle The toggle object
function Toggle.SetScript(self, script, handler)
if script == "OnValueChanged" then
self._onValueChangedHandler = handler
else
error("Unknown Toggle script: "..tostring(script))
end
return self
end
function Toggle.SetFont(self, font)
self._font = font
return self
end
--- Get the selected option.
-- @tparam Toggle self The toggle object
-- @treturn string The selected option
function Toggle.GetValue(self)
return self._selectedOption
end
function Toggle.Draw(self)
self.__super.__super:Draw()
-- add new buttons if necessary
while #self._buttons < #self._optionsList do
local num = #self._buttons + 1
local button = UIElements.New("Checkbox", self._id.."_Button"..num)
:SetFont(self._font)
:SetScript("OnValueChanged", private.ButtonOnClick)
self:AddChildNoLayout(button)
tinsert(self._buttons, button)
end
local selectedPath = self._selectedOption
local height = self:_GetDimension("HEIGHT")
local buttonWidth = (self:_GetDimension("WIDTH") / #self._buttons) + BUTTON_PADDING
local offsetX = 0
for i, button in ipairs(self._buttons) do
local buttonPath = self._optionsList[i]
if i <= #self._optionsList then
button:SetFont(self._font)
button:SetWidth("AUTO")
button:SetTheme("RADIO")
button:SetCheckboxPosition("LEFT")
button:SetText(buttonPath)
button:SetSize(buttonWidth, height)
button:SetDisabled(self._disabled)
button:WipeAnchors()
button:AddAnchor("TOPLEFT", offsetX, 0)
offsetX = offsetX + buttonWidth
else
button:Hide()
end
if buttonPath == selectedPath then
button:SetChecked(true, true)
else
button:SetChecked(false, true)
end
end
self.__super:Draw()
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.ButtonOnClick(button)
local self = button:GetParentElement()
self:SetOption(button:GetText(), true)
end

View File

@@ -0,0 +1,197 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- ToggleOnOff UI Element Class.
-- This is a simple on/off toggle which uses different textures for the different states. It is a subclass of the
-- @{Container} class.
-- @classmod ToggleOnOff
local _, TSM = ...
local ToggleOnOff = TSM.Include("LibTSMClass").DefineClass("ToggleOnOff", TSM.UI.Container)
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(ToggleOnOff)
TSM.UI.ToggleOnOff = ToggleOnOff
local private = {}
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function ToggleOnOff.__init(self)
local frame = UIElements.CreateFrame(self, "Frame")
self.__super:__init(frame)
self._value = false
self._disabled = false
self._settingTable = nil
self._settingKey = nil
self._onValueChangedHandler = nil
end
function ToggleOnOff.Acquire(self)
local frame = self:_GetBaseFrame()
self:AddChildNoLayout(UIElements.New("Frame", "toggle")
:SetLayout("HORIZONTAL")
:AddAnchor("TOPLEFT", frame)
:AddAnchor("BOTTOMRIGHT", frame)
:SetContext(self)
:AddChild(UIElements.New("Checkbox", "yes")
:SetWidth("AUTO")
:SetTheme("RADIO")
:SetFont("BODY_BODY2")
:SetText(YES)
:SetCheckboxPosition("LEFT")
:SetScript("OnValueChanged", private.OnYesClickHandler)
)
:AddChild(UIElements.New("Checkbox", "no")
:SetWidth("AUTO")
:SetTheme("RADIO")
:SetFont("BODY_BODY2")
:SetMargin(8, 0, 0, 0)
:SetText(NO)
:SetCheckboxPosition("LEFT")
:SetScript("OnValueChanged", private.OnNoClickHandler)
)
:AddChild(UIElements.New("Spacer", "spacer"))
)
self.__super:Acquire()
end
function ToggleOnOff.Release(self)
self._value = false
self._disabled = false
self._settingTable = nil
self._settingKey = nil
self._onValueChangedHandler = nil
--self:_GetBaseFrame():Enable()
self.__super:Release()
end
--- Sets the setting info.
-- This method is used to have the value of the toggle automatically correspond with the value of a field in a table.
-- This is useful for toggles which are tied directly to settings.
-- @tparam ToggleOnOff self The toggles object
-- @tparam table tbl The table which the field to set belongs to
-- @tparam string key The key into the table to be set based on the toggle's state
-- @treturn ToggleOnOff The toggles object
function ToggleOnOff.SetSettingInfo(self, tbl, key)
self._settingTable = tbl
self._settingKey = key
self._value = tbl[key]
return self
end
--- Sets whether or not the toggle is disabled.
-- @tparam ToggleOnOff self The toggles object
-- @tparam boolean disabled Whether or not the toggle is disabled
-- @tparam boolean redraw Whether or not to redraw the toggle
-- @treturn ToggleOnOff The toggles object
function ToggleOnOff.SetDisabled(self, disabled, redraw)
self._disabled = disabled
if disabled then
self:GetElement("toggle.yes"):SetDisabled(true)
self:GetElement("toggle.no"):SetDisabled(true)
else
self:GetElement("toggle.yes"):SetDisabled(false)
self:GetElement("toggle.no"):SetDisabled(false)
end
if redraw then
self:Draw()
end
return self
end
--- Set the value of the toggle.
-- @tparam ToggleOnOff self The toggles object
-- @tparam boolean value Whether the value is on (true) or off (false)
-- @tparam boolean redraw Whether or not to redraw the toggle
-- @treturn ToggleOnOff The toggles object
function ToggleOnOff.SetValue(self, value, redraw)
if value ~= self._value then
self._value = value
if self._settingTable then
self._settingTable[self._settingKey] = value
end
if self._onValueChangedHandler then
self:_onValueChangedHandler(value)
end
end
if redraw then
self:Draw()
end
return self
end
--- Registers a script handler.
-- @tparam ToggleOnOff self The toggles object
-- @tparam string script The script to register for (supported scripts: `OnValueChanged`)
-- @tparam function handler The script handler which will be called with the toggles object followed by any
-- arguments to the script
-- @treturn ToggleOnOff The toggles object
function ToggleOnOff.SetScript(self, script, handler)
if script == "OnValueChanged" then
self._onValueChangedHandler = handler
else
error("Unknown ToggleOnOff script: "..tostring(script))
end
return self
end
--- Get the value of the toggle.
-- @tparam ToggleOnOff self The toggles object
-- @treturn boolean The value of the toggle
function ToggleOnOff.GetValue(self)
return self._value
end
function ToggleOnOff.Draw(self)
if self._value then
self:GetElement("toggle.yes"):SetChecked(true, true)
self:GetElement("toggle.no"):SetChecked(false, true)
else
self:GetElement("toggle.yes"):SetChecked(false, true)
self:GetElement("toggle.no"):SetChecked(true, true)
end
if self._disabled then
self:GetElement("toggle.yes"):SetDisabled(true)
self:GetElement("toggle.no"):SetDisabled(true)
else
self:GetElement("toggle.yes"):SetDisabled(false)
self:GetElement("toggle.no"):SetDisabled(false)
end
self.__super:Draw()
end
-- ============================================================================
-- Local Script Handlers
-- ============================================================================
function private.OnYesClickHandler(button)
if not button:IsChecked() then
button:SetChecked(true, true)
return
end
local self = button:GetParentElement():GetContext()
self:SetValue(true, true)
end
function private.OnNoClickHandler(button)
if not button:IsChecked() then
button:SetChecked(true, true)
return
end
local self = button:GetParentElement():GetContext()
self:SetValue(false, true)
end

View File

@@ -0,0 +1,238 @@
-- ------------------------------------------------------------------------------ --
-- TradeSkillMaster --
-- https://tradeskillmaster.com --
-- All Rights Reserved - Detailed license information included with addon. --
-- ------------------------------------------------------------------------------ --
--- ViewContainer UI Element Class.
-- A view container allows the content to be changed depending on the selected view (called the path). It is a subclass of the @{Container} class.
-- @classmod ViewContainer
local _, TSM = ...
local ViewContainer = TSM.Include("LibTSMClass").DefineClass("ViewContainer", TSM.UI.Container)
local Table = TSM.Include("Util.Table")
local UIElements = TSM.Include("UI.UIElements")
UIElements.Register(ViewContainer)
TSM.UI.ViewContainer = ViewContainer
-- ============================================================================
-- Public Class Methods
-- ============================================================================
function ViewContainer.__init(self)
local frame = UIElements.CreateFrame(self, "Frame")
self.__super:__init(frame)
self._pathsList = {}
self._contextTable = nil
self._defaultContextTable = nil
end
function ViewContainer.Acquire(self)
self._path = nil
self._navCallback = nil
self.__super:Acquire()
end
function ViewContainer.Release(self)
wipe(self._pathsList)
self.__super:Release()
self._contextTable = nil
self._defaultContextTable = nil
end
function ViewContainer.SetLayout(self, layout)
error("ViewContainer doesn't support this method")
end
function ViewContainer.AddChild(self, child)
error("ViewContainer doesn't support this method")
end
function ViewContainer.AddChildNoLayout(self, child)
error("ViewContainer doesn't support this method")
end
--- Set the navigation callback.
-- @tparam ViewContainer self The view container object
-- @tparam function callback The function called when the selected path changes to get the new content
-- @treturn ViewContainer The view container object
function ViewContainer.SetNavCallback(self, callback)
self._navCallback = callback
return self
end
--- Add a path (view).
-- @tparam ViewContainer self The view container object
-- @tparam string path The path
-- @tparam[opt=false] boolean setSelected Set this as the selected path (view)
-- @treturn ViewContainer The view container object
function ViewContainer.AddPath(self, path, setSelected)
tinsert(self._pathsList, path)
if self._contextTable then
assert(setSelected == nil, "Cannot set selected path when using a context table")
local newPathIndex = Table.KeyByValue(self._pathsList, path)
if self._contextTable.pathIndex == newPathIndex then
self:SetPath(path)
end
elseif setSelected then
self:SetPath(path)
end
return self
end
--- Renames a path (view).
-- @tparam ViewContainer self The view container object
-- @tparam string path The new path
-- @tparam number index The index of the path to change
-- @treturn ViewContainer The view container object
function ViewContainer.RenamePath(self, path, index)
local changePath = self._pathsList[index] == self._path
self._pathsList[index] = path
if changePath then
self:SetPath(path)
end
return self
end
--- Set the selected path (view).
-- @tparam ViewContainer self The view container object
-- @tparam string path The selected path
-- @tparam boolean redraw Whether or not to redraw the view container
-- @treturn ViewContainer The view container object
function ViewContainer.SetPath(self, path, redraw)
if path ~= self._path then
local child = self:_GetChild()
if child then
assert(#self._layoutChildren == 1)
self:RemoveChild(child)
child:Release()
end
self.__super:AddChild(self:_navCallback(path))
self._path = path
-- Save the path index of the new selected path to the context table
if self._contextTable then
self._contextTable.pathIndex = Table.KeyByValue(self._pathsList, path)
end
end
if redraw then
self:Draw()
end
return self
end
--- Reload the current view.
-- @tparam ViewContainer self The view container object
function ViewContainer.ReloadContent(self)
local path = self._path
self._path = nil
self:SetPath(path, true)
end
--- Get the current path (view).
-- @tparam ViewContainer self The view container object
-- @treturn string The current path
function ViewContainer.GetPath(self)
return self._path
end
--- Get a list of the paths for the view container.
-- @tparam ViewContainer self The view container object
-- @treturn table The path list
function ViewContainer.GetPathList(self)
return self._pathsList
end
function ViewContainer.Draw(self)
self.__super.__super:Draw()
local child = self:_GetChild()
local childFrame = child:_GetBaseFrame()
-- set the child to be full-size
childFrame:ClearAllPoints()
local xOffset, yOffset = child:_GetMarginAnchorOffsets("BOTTOMLEFT")
local paddingXOffset, paddingYOffset = self:_GetPaddingAnchorOffsets("BOTTOMLEFT")
xOffset = xOffset + paddingXOffset - self:_GetContentPadding("LEFT")
yOffset = yOffset + paddingYOffset - self:_GetContentPadding("BOTTOM")
childFrame:SetPoint("BOTTOMLEFT", xOffset, yOffset)
xOffset, yOffset = child:_GetMarginAnchorOffsets("TOPRIGHT")
paddingXOffset, paddingYOffset = self:_GetPaddingAnchorOffsets("TOPRIGHT")
xOffset = xOffset + paddingXOffset - self:_GetContentPadding("RIGHT")
yOffset = yOffset + paddingYOffset - self:_GetContentPadding("TOP")
childFrame:SetPoint("TOPRIGHT", xOffset, yOffset)
child:Draw()
-- draw the no-layout children
for _, noLayoutChild in ipairs(self._noLayoutChildren) do
noLayoutChild:Draw()
end
end
--- Sets the context table.
-- This table can be used to save which tab is active, refrenced by the path index
-- @tparam ViewContainer self The view container object
-- @tparam table tbl The context table
-- @tparam table defaultTbl Default values
-- @treturn ViewContainer The view container object
function ViewContainer.SetContextTable(self, tbl, defaultTbl)
assert(defaultTbl.pathIndex ~= nil)
tbl.pathIndex = tbl.pathIndex or defaultTbl.pathIndex
self._contextTable = tbl
self._defaultContextTable = defaultTbl
return self
end
--- Sets the context table from a settings object.
-- @tparam ViewContainer self The view container object
-- @tparam Settings settings The settings object
-- @tparam string key The setting key
-- @treturn ViewContainer The view container object
function ViewContainer.SetSettingsContext(self, settings, key)
return self:SetContextTable(settings[key], settings:GetDefaultReadOnly(key))
end
-- ============================================================================
-- Private Class Methods
-- ============================================================================
function ViewContainer._GetMinimumDimension(self, dimension)
if dimension == "WIDTH" then
local width = self._width
if width then
return width, false
else
return self:_GetChild():_GetMinimumDimension(dimension)
end
elseif dimension == "HEIGHT" then
local height = self._height
if height then
return height, false
else
return self:_GetChild():_GetMinimumDimension(dimension)
end
else
error("Invalid dimension: "..tostring(dimension))
end
end
function ViewContainer._GetContentPadding(self, side)
if side == "TOP" then
return 0
elseif side == "BOTTOM" then
return 0
elseif side == "LEFT" then
return 0
elseif side == "RIGHT" then
return 0
else
error("Invalid side: "..tostring(side))
end
end
function ViewContainer._GetChild(self)
return self._layoutChildren[1]
end