You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

973 lines
31 KiB

-- Neuron is a World of Warcraft® user interface addon.
-- Copyright (c) 2017-2021 Britt W. Yazel
-- Copyright (c) 2006-2014 Connor H. Chenoweth
-- This code is licensed under the MIT license (see LICENSE for details)
local _, addonTable = ...
local Neuron = addonTable.Neuron
local Spec = addonTable.utilities.Spec
---@class ActionButton : Button @define class ActionButton inherits from class Button
local ActionButton = setmetatable({}, {__index = Neuron.Button}) --this is the metatable for our button object
Neuron.ActionButton = ActionButton
---------------------------------------------------------
-------------------declare globals-----------------------
---------------------------------------------------------
local COMMAND_LIST = {
[SLASH_CAST1] = true,
[SLASH_CAST2] = true,
[SLASH_CAST3] = true,
[SLASH_CAST4] = true,
[SLASH_CASTRANDOM1] = true,
[SLASH_CASTRANDOM2] = true,
[SLASH_CASTSEQUENCE1] = true,
[SLASH_CASTSEQUENCE2] = true,
[SLASH_EQUIP1] = true,
[SLASH_EQUIP2] = true,
[SLASH_EQUIP3] = true,
[SLASH_EQUIP4] = true,
[SLASH_EQUIP_TO_SLOT1] = true,
[SLASH_EQUIP_TO_SLOT2] = true,
[SLASH_USE1] = true,
[SLASH_USE2] = true,
[SLASH_USERANDOM1] = true,
[SLASH_USERANDOM2] = true,
["/cast"] = true,
["/castrandom"] = true,
["/castsequence"] = true,
["/spell"] = true,
["/equip"] = true,
["/eq"] = true,
["/equipslot"] = true,
["/use"] = true,
["/userandom"] = true,
["/summonpet"] = true,
["/click"] = true,
["#showtooltip"] = true,
}
---Constructor: Create a new Neuron Button object (this is the base object for all Neuron button types)
---@param bar Bar @Bar Object this button will be a child of
---@param buttonID number @Button ID that this button will be assigned
---@param defaults table @Default options table to be loaded onto the given button
---@return ActionButton @ A newly created ActionButton object
function ActionButton.new(bar, buttonID, defaults)
--call the parent object constructor with the provided information specific to this button type
local newButton = Neuron.Button.new(bar, buttonID, ActionButton, "ActionBar", "ActionButton", "NeuronActionButtonTemplate")
if defaults then
newButton:SetDefaults(defaults)
end
return newButton
end
function ActionButton:InitializeButton()
self:ClearButton(true)
SecureHandler_OnLoad(self)
if self.class ~= "flyout" then
self:SetupEvents()
end
self:SetAttribute("type", "macro")
self:SetAttribute("*macrotext*", self.SanitizedMacro(self:GetMacroText()))
self:SetAttribute("hotkeypri", self.keys.hotKeyPri)
self:SetAttribute("hotkeys", self.keys.hotKeys)
self:SetScript("PostClick", function(_, mousebutton) self:PostClick(mousebutton) end)
self:SetScript("OnReceiveDrag", function(_, preclick) self:OnReceiveDrag(preclick) end)
self:SetScript("OnDragStart", function(_, mousebutton) self:OnDragStart(mousebutton) end)
--this is to allow for the correct releasing of the button when dragging icons off of the bar
--we need to hook to the WorldFrame OnReceiveDrag and OnMouseDown so that we can "let go" of the spell when we drag it off the bar
if not Neuron:IsHooked(WorldFrame, "OnReceiveDrag") then
Neuron:HookScript(WorldFrame, "OnReceiveDrag", function() ActionButton:WorldFrame_OnReceiveDrag() end)
end
if not Neuron:IsHooked(WorldFrame, "OnMouseDown") then
Neuron:HookScript(WorldFrame, "OnMouseDown", function() ActionButton:WorldFrame_OnReceiveDrag() end)
end
self:SetScript("OnAttributeChanged", function(_, name, value) self:OnAttributeChanged(name, value) end)
self:SetScript("OnEnter", function() self:UpdateTooltip() end)
self:SetScript("OnLeave", function() GameTooltip:Hide() end)
if Neuron.isWoWRetail then
self:SetAttribute("overrideID_Offset", 204)
self:SetAttribute("vehicleID_Offset", 180)
self:SetAttribute("dragonridingID_Offset", 120)
else
self:SetAttribute("overrideID_Offset", 156)
self:SetAttribute("vehicleID_Offset", 132)
end
--This is so that hotkeypri works properly with priority/locked buttons
self:WrapScript(self, "OnShow", [[
for i=1,select('#',(":"):split(self:GetAttribute("hotkeys"))) do
self:SetBindingClick(self:GetAttribute("hotkeypri"), select(i,(":"):split(self:GetAttribute("hotkeys"))), self:GetName())
end
]])
self:SetFrameRef("uiparent", UIParent)
self:WrapScript(self, "OnHide", [[
UIParent = self:GetFrameRef("uiparent")
if (not self:GetParent():GetAttribute("concealed")) and (not UIParent:IsShown() == false) then
for key in gmatch(self:GetAttribute("hotkeys"), "[^:]+") do
self:ClearBinding(key)
end
end
]])
self:SetAttribute("_childupdate",
[[
if message then
local msg = (":"):split(message)
if msg:find("vehicle") then
if not self:GetAttribute(msg.."-actionID") then
self:SetAttribute("type", "action")
self:SetAttribute("*action*", self:GetAttribute("barPos")+self:GetAttribute("vehicleID_Offset"))
end
self:SetAttribute("HasActionID", true)
self:Show()
elseif msg:find("dragonriding") then
if not self:GetAttribute(msg.."-actionID") then
self:SetAttribute("type", "action")
self:SetAttribute("*action*", self:GetAttribute("barPos")+self:GetAttribute("dragonridingID_Offset"))
end
self:SetAttribute("HasActionID", true)
self:Show()
elseif msg:find("possess") then
if not self:GetAttribute(msg.."-actionID") then
self:SetAttribute("type", "action")
self:SetAttribute("*action*", self:GetAttribute("barPos")+self:GetAttribute("vehicleID_Offset"))
end
self:SetAttribute("HasActionID", true)
self:Show()
elseif msg:find("override") then
if not self:GetAttribute(msg.."-actionID") then
self:SetAttribute("type", "action")
self:SetAttribute("*action*", self:GetAttribute("barPos")+self:GetAttribute("overrideID_Offset"))
self:SetAttribute("HasActionID", true)
end
self:SetAttribute("HasActionID", true)
self:Show()
else
if not self:GetAttribute(msg.."-actionID") then
self:SetAttribute("type", "macro")
self:SetAttribute("*macrotext*", self:GetAttribute(msg.."-macro_Text"))
--if there is a macro present, or if showGrid is enabled, show the button. If not, hide it. This works in combat.
if (self:GetAttribute("*macrotext*") and #self:GetAttribute("*macrotext*") > 0) or self:GetAttribute("showGrid") then
self:Show()
else
self:Hide()
end
self:SetAttribute("HasActionID", false)
else
self:SetAttribute("HasActionID", true)
end
end
self:SetAttribute("activestate", msg)
end
]])
--this is our rangecheck timer for each button. Every 0.5 seconds it queries if the button is usable
--this doubles our CPU usage, but it really helps usability quite a bit
--this is a direct replacement to the old "onUpdate" code that did this job
if self:TimeLeft(self.rangeTimer) == 0 then
self.rangeTimer = self:ScheduleRepeatingTimer("UpdateUsable", 0.5)
end
self:UpdateAll()
self:UpdateFlyout(true)
self:InitializeButtonSettings()
end
function ActionButton:InitializeButtonSettings()
self:SetFrameStrata(Neuron.STRATAS[self.bar:GetStrata()-1])
self:SetScale(self.bar:GetBarScale())
if self.bar:GetShowBindText() then
self.Hotkey:Show()
self.Hotkey:SetTextColor(self.bar:GetBindColor()[1],self.bar:GetBindColor()[2],self.bar:GetBindColor()[3])
else
self.Hotkey:Hide()
end
if self.bar:GetShowButtonText() then
self.Name:Show()
self.Name:SetTextColor(self.bar:GetMacroColor()[1],self.bar:GetMacroColor()[2],self.bar:GetMacroColor()[3])
else
self.Name:Hide()
end
if self.bar:GetShowCountText() then
self.Count:Show()
self.Count:SetTextColor(self.bar:GetCountColor()[1],self.bar:GetCountColor()[2],self.bar:GetCountColor()[3])
else
self.Count:Hide()
end
--[[
if self.bar:GetClickMode() == "UpClick" then
self:RegisterForClicks("AnyUp")
elseif self.bar:GetClickMode() == "DownClick" then
self:RegisterForClicks("AnyDown")
end
]]
self:RegisterForClicks("AnyUp")
self:RegisterForDrag("LeftButton", "RightButton")
self:SetSkinned()
end
function ActionButton:SetupEvents()
self:RegisterEvent("PLAYER_ENTERING_WORLD")
self:RegisterEvent("UPDATE_MACROS")
self:RegisterEvent("ACTIONBAR_SLOT_CHANGED", "UpdateAll")
self:RegisterEvent("ACTIONBAR_UPDATE_COOLDOWN", "UpdateCooldown")
self:RegisterEvent("SPELL_UPDATE_CHARGES", "UpdateCount")
self:RegisterEvent("PLAYER_EQUIPMENT_CHANGED", "UpdateAll")
self:RegisterEvent("SPELLS_CHANGED", "UpdateAll")
self:RegisterEvent("MODIFIER_STATE_CHANGED", "UpdateAll")
self:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED")
self:RegisterEvent("UNIT_SPELLCAST_FAILED", "UNIT_SPELLCAST_INTERRUPTED")
self:RegisterEvent("BAG_UPDATE_COOLDOWN", "UpdateStatus")
self:RegisterEvent("BAG_UPDATE", "UpdateStatus")
self:RegisterEvent("PLAYER_STARTED_MOVING", "UpdateUsable")
self:RegisterEvent("PLAYER_STOPPED_MOVING", "UpdateUsable")
self:RegisterEvent("ACTIONBAR_UPDATE_USABLE", "UpdateUsable")
self:RegisterEvent("ACTIONBAR_UPDATE_STATE", "UpdateAll")
self:RegisterEvent("TRADE_SKILL_SHOW", "UpdateAll")
self:RegisterEvent("TRADE_SKILL_CLOSE", "UpdateAll")
self:RegisterEvent("PLAYER_TARGET_CHANGED", "UpdateAll")
self:RegisterEvent("UNIT_PET", "UpdateAll")
if Neuron.isWoWRetail then
self:RegisterEvent("EQUIPMENT_SETS_CHANGED")
self:RegisterEvent("UNIT_ENTERED_VEHICLE", "UpdateAll")
self:RegisterEvent("UNIT_ENTERING_VEHICLE", "UpdateAll")
self:RegisterEvent("UNIT_EXITED_VEHICLE", "UpdateAll")
self:RegisterEvent("PLAYER_FOCUS_CHANGED", "UpdateAll")
self:RegisterEvent("COMPANION_UPDATE", "UpdateAll")
self:RegisterEvent("SPELL_ACTIVATION_OVERLAY_GLOW_SHOW", "UpdateGlow")
self:RegisterEvent("SPELL_ACTIVATION_OVERLAY_GLOW_HIDE", "UpdateGlow")
self:RegisterEvent("UPDATE_VEHICLE_ACTIONBAR", "UpdateAll")
self:RegisterEvent("UPDATE_POSSESS_BAR", "UpdateAll")
self:RegisterEvent("UPDATE_OVERRIDE_ACTIONBAR", "UpdateAll")
self:RegisterEvent("UPDATE_BONUS_ACTIONBAR", "UpdateAll")
end
end
function ActionButton:OnAttributeChanged(name, value)
if value and self.data then
if name == "activestate" then
--Part 2 of Druid Prowl overwrite fix (part 1 below)
-----------------------------------------------------
--breaks out of the loop due to flag set below
if Neuron.class == "DRUID" and self.ignoreNextOverrideStance == true and value == "homestate" then
self.ignoreNextOverrideStance = nil
self.bar:SetState("stealth") --have to add this in otherwise the button icons change but still retain the homestate ability actions
return
else
self.ignoreNextOverrideStance = nil
end
-----------------------------------------------------
-----------------------------------------------------
if self:GetAttribute("HasActionID") then
self.actionID = self:GetAttribute("*action*")
else
--clear any actionID that has been set
self.actionID = nil
--this is a safety check in case the state we're switching into doesn't have table set up for it yet
-- i.e. stealth1 or stance2
if not self.statedata[value] then
self.statedata[value] = {}
end
--Part 1 of Druid Prowl overwrite fix
---------------------------------------------------
--druids have an issue where once stance will get immediately overwritten by another. I.E. stealth immediately getting overwritten by homestate if they go immediately into prowl from caster form
--this conditional sets a flag to ignore the next most stance flag, as that one is most likely in error and should be ignored
if Neuron.class == "DRUID" and value == "stealth1" then
self.ignoreNextOverrideStance = true
end
------------------------------------------------------
------------------------------------------------------
--swap out our data with the data stored for the particular state
self.data = self.statedata[value]
self:ClearButton()
end
--This will remove any old button state data from the saved variable's memory
for id,data in pairs(self.statedata) do
if (self.bar.data[id:match("%a+")] or id == "") and self.bar.data["custom"] then
elseif not self.bar.data[id:match("%a+")] then
self.statedata[id]= nil
end
end
self:UpdateAll()
end
if name == "update" then
self:UpdateAll()
end
end
end
function ActionButton:OnLeave(...)
GameTooltip:Hide()
if self.flyout and self.flyout.arrow then
self.flyout.arrow:SetPoint(self.flyout.arrowPoint, self.flyout.arrowX, self.flyout.arrowY)
end
end
------------------------------------------------------------
--------------General Button Methods------------------------
------------------------------------------------------------
function ActionButton:GetDragAction()
return "macro"
end
function ActionButton:ClearButton(clearAttributes)
self.spell = nil
self.spellID = nil
self.item = nil
self.unit = nil
if not InCombatLockdown() and clearAttributes then
self:SetAttribute("unit", nil)
self:SetAttribute("type", nil)
self:SetAttribute("type1", nil)
self:SetAttribute("type2", nil)
self:SetAttribute("*action*", nil)
self:SetAttribute("*macrotext*", nil)
self:SetAttribute("*action1", nil)
self:SetAttribute("*macrotext2", nil)
end
end
function ActionButton:UpdateGlow()
if self.bar:GetSpellGlow() and self.spellID then
--druid fix for thrash glow not showing for feral druids.
--Thrash Guardian: 77758
--Thrash Feral: 106832
--But the joint thrash is 106830 (this is the one that results true when the ability is procced)
--Swipe(Bear): 213771
--Swipe(Cat): 106785
--Swipe(NoForm): 213764
if self.spell and self.spell:lower() == "thrash()" and IsSpellOverlayed(106830) then --this is a hack for feral druids (Legion patch 7.3.0. Bug reported)
self:StartGlow()
elseif self.spell and self.spell:lower() == "swipe()" and IsSpellOverlayed(106785) then --this is a hack for feral druids (Legion patch 7.3.0. Bug reported)
self:StartGlow()
elseif IsSpellOverlayed(self.spellID) then --this is the default "true" condition
self:StartGlow()
else --this is the default "false" condition
self:StopGlow()
end
else --this stops the glow on buttons that have no spellID's, i.e. when switching states and a procced ability is overlapping an empty button
self:StopGlow()
end
end
function ActionButton:StartGlow()
if self.bar:GetSpellGlow() == "default" then
ActionButton_ShowOverlayGlow(self)
else
self.Shine:Show()
AutoCastShine_AutoCastStart(self.Shine);
end
end
function ActionButton:StopGlow()
if self.bar:GetSpellGlow() == "default" then
ActionButton_HideOverlayGlow(self)
else
self.Shine:Hide()
AutoCastShine_AutoCastStop(self.Shine);
end
end
------------------------------------------------------------------------------
---------------------Event Functions------------------------------------------
------------------------------------------------------------------------------
function ActionButton:PLAYER_ENTERING_WORLD()
self:UpdateAll()
self:ApplyBindings()
-- if self.flyout then --this is a hack to get around CallPet not working on initial login. (weirdly it worked on /reload, but not login)
-- self:ScheduleTimer(function() self:InitializeButton() end, 1)
-- end
end
function ActionButton:UNIT_SPELLCAST_INTERRUPTED(unit)
if (unit == "player" or unit == "pet") and self.spell then
self:UpdateCooldown()
end
end
function ActionButton:UPDATE_MACROS()
if not InCombatLockdown() and self:GetMacroBlizzMacro() then
self:PlaceBlizzMacro(self:GetMacroBlizzMacro())
end
end
function ActionButton:EQUIPMENT_SETS_CHANGED()
if not InCombatLockdown() and self:GetMacroEquipmentSet() then
self:PlaceBlizzEquipSet(self:GetMacroEquipmentSet())
end
end
-----------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------
function ActionButton.SanitizedMacro(macro)
if type(macro) == "string" and #macro > 0 then
--adds an empty line above and below the macro
macro = "\n"..macro.."\n"
--I'm not sure what this line does, but it adds spaces before control characters.
--Maybe it's adding spaces before newlines?
return macro:gsub("(%c+)", " %1")
else
return nil
end
end
function ActionButton:UpdateButtonSpec()
local spec = Spec.active(self.bar:GetMultiSpec())
self:LoadDataFromDatabase(spec, self.bar.handler:GetAttribute("activestate") or "homestate")
self:InitializeButtonSettings()
self:UpdateFlyout()
self:UpdateAll()
end
--this function is used to "fake" a state change in the button editor so you can see what each state will look like
function ActionButton:FakeStateChange(state)
if state then
local msg = (":"):split(state)
if msg:find("vehicle") then
if not self:GetAttribute(msg.."-actionID") then
self:SetAttribute("type", "action")
self:SetAttribute("*action*", self:GetAttribute("barPos")+self:GetAttribute("vehicleID_Offset"))
end
self:SetAttribute("HasActionID", true)
self:Show()
elseif msg:find("possess") then
if not self:GetAttribute(msg.."-actionID") then
self:SetAttribute("type", "action")
self:SetAttribute("*action*", self:GetAttribute("barPos")+self:GetAttribute("vehicleID_Offset"))
end
self:SetAttribute("HasActionID", true)
self:Show()
elseif msg:find("override") then
if not self:GetAttribute(msg.."-actionID") then
self:SetAttribute("type", "action")
self:SetAttribute("*action*", self:GetAttribute("barPos")+self:GetAttribute("overrideID_Offset"))
self:SetAttribute("HasActionID", true)
end
self:SetAttribute("HasActionID", true)
self:Show()
else
if not self:GetAttribute(msg.."-actionID") then
self:SetAttribute("type", "macro")
self:SetAttribute("*self*", self:GetAttribute(msg.."-macro_Text"))
--if there is a macro present, or if showGrid is enabled, show the button. If not, hide it. This works in combat.
if self:GetAttribute("*macrotext*") and #self:GetAttribute("*macrotext*") > 0 or self:GetAttribute("showGrid") then
self:Show()
else
self:Hide()
end
self:SetAttribute("HasActionID", false)
else
self:SetAttribute("HasActionID", true)
end
end
self:SetAttribute("activestate", msg)
end
end
--this will generate a spell macro
--spell: name of spell to use
--subname: subname of spell to use (optional)
--return: macro text
function ActionButton:AutoWriteMacro(spell)
local DB = Neuron.db.profile
local spellName
local spellID
local altName
local altSpellID
--if there is an alt name associated with a given ability, and the alt name is known (i.e. the base spell) use the alt name instead
--This is important because a macro written with the base name "/cast Roll()" will work for talented abilities, but "/cast Chi Torpedo" won't work for base abilities
if Neuron.spellCache[spell:lower()] then
spellName = Neuron.spellCache[spell:lower()].spellName
spellID = Neuron.spellCache[spell:lower()].spellID
altName = Neuron.spellCache[spell:lower()].altName
altSpellID = Neuron.spellCache[spell:lower()].altSpellID
if altSpellID and IsSpellKnown(altSpellID) then
spell = altName
else
spell = spellName
end
else
_,_,_,_,_,_,spellID = GetSpellInfo(spell)
end
local modifier, modKey = " ", nil
local bar = Neuron.currentBar or self.bar
if bar.data.mouseOverCast and DB.mouseOverMod ~= "NONE" then
modKey = DB.mouseOverMod
modifier = modifier.."[@mouseover,mod:"..modKey.."]"
elseif bar.data.mouseOverCast and DB.mouseOverMod == "NONE" then
modifier = modifier.."[@mouseover,exists]"
end
if bar.data.selfCast and GetModifiedClick("SELFCAST") ~= "NONE" then
modKey = GetModifiedClick("SELFCAST")
modifier = modifier.."[@player,mod:"..modKey.."]"
end
if bar.data.focusCast and GetModifiedClick("FOCUSCAST") ~= "NONE" then
modKey = GetModifiedClick("FOCUSCAST")
modifier = modifier.."[@focus,exists,mod:"..modKey.."]"
end
if bar.data.rightClickTarget then
modKey = ""
modifier = modifier.."[@player"..modKey..",btn:2]"
end
if modifier ~= " " then --(modKey then
modifier = modifier.."[] "
end
return "#autowrite\n/cast"..modifier..spell.."()"
end
function ActionButton:GetPosition(relFrame)
local point
if not relFrame then
relFrame = self:GetParent()
end
local s = self:GetScale()
local w, h = relFrame:GetWidth()/s, relFrame:GetHeight()/s
local x, y = self:GetCenter()
local vert = (y>h/1.5) and "TOP" or (y>h/3) and "CENTER" or "BOTTOM"
local horz = (x>w/1.5) and "RIGHT" or (x>w/3) and "CENTER" or "LEFT"
if vert == "CENTER" then
point = horz
elseif horz == "CENTER" then
point = vert
else
point = vert..horz
end
if vert:find("CENTER") then y = y - h/2 end
if horz:find("CENTER") then x = x - w/2 end
if point:find("RIGHT") then x = x - w end
if point:find("TOP") then y = y - h end
return point, x, y
end
--This will update the modifier value in a macro when a bar is set with a target conditional
--@spell: this is hte macro text to be updated
--return: updated macro text
--[[function ActionButton:AutoUpdateMacro(macro)
local DB = Neuron.db.profile
if GetModifiedClick("SELFCAST") ~= "NONE" then
macro = macro:gsub("%[@player,mod:%u+%]", "[@player,mod:"..GetModifiedClick("SELFCAST").."]")
else
macro = macro:gsub("%[@player,mod:%u+%]", "")
end
if GetModifiedClick("FOCUSCAST") ~= "NONE" then
macro = macro:gsub("%[@focus,mod:%u+%]", "[@focus,exists,mod:"..GetModifiedClick("FOCUSCAST").."]")
else
macro = macro:gsub("%[@focus,mod:%u+%]", "")
end
if DB.mouseOverMod ~= "NONE" then
macro = macro:gsub("%[@mouseover,mod:%u+%]", "[@mouseover,mod:"..DB.mouseOverMod .."]")
macro = macro:gsub("%[@mouseover,exists]", "[@mouseover,mod:"..DB.mouseOverMod .."]")
else
macro = macro:gsub("%[@mouseover,mod:%u+%]", "[@mouseover,exists]")
end
return macro
end
-- This will iterate through a set of buttons. For any buttons that have the #autowrite flag in its macro, that
-- macro will then be updated to via AutoWriteMacro to include selected target macro option, or via AutoUpdateMacro
-- to update a current target macro's toggle modifier.
-- @param global(boolean): if true will go though all buttons, else it will just update the button set for the current bar
function ActionButton:UpdateMacroCastTargets(global_update)
local button_list = {}
if global_update then
for _,bar in ipairs(Neuron.bars) do
for _, object in ipairs(bar.buttons) do
table.insert(button_list, object)
end
end
else
local bar = Neuron.currentBar
for i, object in ipairs(bar.buttons) do
table.insert(button_list, object)
end
end
for index, button in pairs(button_list) do
local cur_button = button.DB
local macro_update = false
for i = 1,2 do
for state, info in pairs(cur_button[i]) do
if info:GetMacroText() and info:GetMacroText():find("#autowrite\n/cast") then
local spell = ""
spell = info:GetMacroText():gsub("%[.*%]", "")
spell = spell:match("#autowrite\n/cast%s*(.+)%((.*)%)")
if spell then
if global_update then
info:SetMacroText(button:AutoUpdateMacro(info.macro_Text))
else
info:SetMacroText(button:AutoWriteMacro(spell))
end
end
macro_update = true
end
end
end
if macro_update then
button:UpdateFlyout()
button:InitializeButton()
end
end
end]]
-----------------------------------------------------
--------------------- Overrides ---------------------
-----------------------------------------------------
--overwrite function in parent class Button
function ActionButton:UpdateAll()
--pass to parent UpdateAll function
Neuron.Button.UpdateAll(self)
if Neuron.isWoWRetail then
self:UpdateGlow()
end
end
---@return {}|{spell: string, spellID: number, unit:string}|{item:string, unit:string}
function ActionButton.ExtractMacroData(macro)
if not macro then
return {}
end
-- the results. probably either spell or item will be nil
---@type string|nil
local spell
---@type number|nil
local spellID
---@type string|nil
local item
---@type string
local unit
--extract the parsed contents of a macro and assign them for further processing
local command, abilityOrItem, target
for cmd, content in gmatch(macro, "(%c%p%a+)(%C+)") do
--"cmd" is "/cast" or "/use" or "#autowrite" or "#showtooltip" etc
--"content" is everything else, like "Chi Torpedo()"
if cmd then
cmd = cmd:gsub("^%c+", "") --remove unneeded characters
end
if content then
content = content:gsub("^%s+", "") --remove unneeded characters
end
--we only want the first in the list if there is a list, so if the first thing is a "#showtooltip" <ability> then we want to capture the ability
--if the first line is #showtootltip and it is blank after, we want to ignore this particular loop and jump to the next
if not abilityOrItem or #abilityOrItem < 1 then
abilityOrItem, target = SecureCmdOptionParse(content)
command = cmd
end
end
unit = target or "target"
if COMMAND_LIST[command] then
if abilityOrItem and #abilityOrItem > 0 and command:find("/castsequence") then --this always will set the button info the next ability or item in the sequence
_, item, spell = QueryCastSequence(abilityOrItem) --it will only ever return as either item or spell, never both
elseif abilityOrItem and #abilityOrItem > 0 then
if Neuron.itemCache[abilityOrItem:lower()] then --if our abilityOrItem is actually an item in our cache, amend it as such
item = abilityOrItem
elseif GetItemInfo(abilityOrItem) then
item = abilityOrItem
elseif tonumber(abilityOrItem) and GetInventoryItemLink("player", abilityOrItem) then --in case abilityOrItem is a number and corresponds to a valid inventory item
item = GetInventoryItemLink("player", abilityOrItem)
elseif Neuron.spellCache[abilityOrItem:lower()] then
spell = abilityOrItem
spellID = Neuron.spellCache[abilityOrItem:lower()].spellID
elseif GetSpellInfo(abilityOrItem) then
spell = abilityOrItem
_,_,_,_,_,_,spellID = GetSpellInfo(abilityOrItem)
end
end
end
return {spell = spell, spellID = spellID, unit = unit, item = item}
end
--overwrite function in parent class Button
function ActionButton:UpdateData()
--clear any lingering values before we re-parse and reassign
self:ClearButton()
--if we have no macro content then bail immediately
--if we have an actionID on this button bail immediately
if self.actionID then
--don't set any values as they'll get in the way later
return
end
local cleanMacro = self.SanitizedMacro(self:GetMacroText())
local spellItemUnit = self.ExtractMacroData(cleanMacro)
self.spell = spellItemUnit.spell
self.spellID = spellItemUnit.spellID
self.item = spellItemUnit.item
self.unit = spellItemUnit.unit
end
--overwrite function in parent class Button
function ActionButton:UpdateVisibility(show)
if self:HasAction() or Neuron.dragging or show or self.bar:GetShowGrid() or Neuron.buttonEditMode or Neuron.barEditMode or Neuron.bindingMode then
self.isShown = true
else
self.isShown = false
end
if not InCombatLockdown() then
self:SetAttribute("showGrid", self.bar:GetShowGrid()) --this is important because in our state switching code, we can't query self.showGrid directly
if self.isShown then
self:Show()
else
self:Hide()
end
end
Neuron.Button.UpdateVisibility(self)
end
-----------------------------------------------------------------------------------------
-------------------------------------- Set Icon -----------------------------------------
-----------------------------------------------------------------------------------------
function ActionButton:UpdateIcon()
if Neuron.pendingReload then
return
end
local spec = Spec.active(self.bar:GetMultiSpec())
local state = self.bar.handler:GetAttribute("activestate") or "homestate"
-- if we have any issues with flyouts or other edge cases, then
-- then we can build our data from our ActionButton instead of using
-- the database values. but we need to keep GetAppearance stateless
-- so that we can use it in other contexts: like the settings dialog
---@type GenericSpecData
local data = (
self.DB
and CopyTable(self.DB[spec][state], true)
or {macro_Text = self:GetMacroText(), macro_Icon = self:GetMacroIcon()}
)
data.actionID = self.actionID -- this is for vehicle, possession, etc
self:ApplyAppearance(self:GetAppearance(data))
--make sure our button gets the correct Normal texture if we're not using a Masque skin
self:UpdateNormalTexture()
end
---@alias Border {[1]:number,[2]:number,[3]:number,[4]:number}|nil
---@alias TextureRef number|string
--- @param data GenericSpecData
--- @return TextureRef, Border an icon texture, and an rgb tuple, both nilable
function ActionButton:GetAppearance(data)
local spellItem = self.ExtractMacroData(self.SanitizedMacro(data.macro_Text))
local spell, item = spellItem.spell, spellItem.item
local texture, border
if data.actionID then
return self.GetActionAppearance(data.actionID)
elseif spell then
texture, border = self.GetSpellAppearance(spell)
elseif item then
texture, border = self.GetItemAppearance(item)
-- macro must go after spells and items, for blizz macro #showtooltip to work
elseif data.macro_Icon then
texture, border = data.macro_Icon, nil
else
texture, border = nil, nil
end
return texture, border
end
function ActionButton:ApplyAppearance(texture, border)
if texture then
self.Icon:SetTexture(texture)
self.Icon:Show()
else
self.Name:SetText("")
self.Icon:SetTexture("")
self.Icon:Hide()
end
if border then
self.Border:SetVertexColor(unpack(border))
self.Border:Show()
else
self.Border:Hide()
end
end
--- @return number|string, nil @an icon texture, and an rgb tuple, both nilable
function ActionButton.GetSpellAppearance(spell)
--Hide the border in case this button used to have an equipped item in it
--otherwise it will continue to have a green border until a reload takes place
local border = nil
---@type number|string|nil
local texture = GetSpellTexture(spell)
if not texture then
if Neuron.spellCache[spell:lower()] then
texture = Neuron.spellCache[spell:lower()].icon
end
end
if not texture then
texture = "INTERFACE\\ICONS\\INV_MISC_QUESTIONMARK"
end
return texture, border
end
--- @return number|string, Border @an icon texture, and an rgb tuple, both nilable
function ActionButton.GetItemAppearance(item)
local border = nil
---@type number|string|nil
local texture = GetItemIcon(item)
if not texture then
if Neuron.itemCache[item:lower()] then
texture = GetItemIcon("item:"..Neuron.itemCache[item:lower()]..":0:0:0:0:0:0:0"--[[@as number]])
end
end
if not texture then
texture = "INTERFACE\\ICONS\\INV_MISC_QUESTIONMARK"
end
if IsEquippedItem(item) then --makes the border green when item is equipped and dragged to a button
border = {0, 1.0, 0, 0.2}
end
return texture, border
end
--- @return texture, border an icon texture, and an rgb tuple, both nilable
function ActionButton.GetActionAppearance(actionID)
local texture, border
if HasAction(actionID) then
texture = GetActionTexture(actionID) or ""
end
return texture, border
end