437 lines
14 KiB
Lua
437 lines
14 KiB
Lua
|
-- ------------------------------------------------------------------------------ --
|
||
|
-- 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
|