--[[ Copyright (c) 2010, Hendrik "nevcairiel" Leppkes <h.leppkes@gmail.com> All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the developer nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ]] local MAJOR_VERSION = "LibActionButton-1.0" local MINOR_VERSION = 20 if not LibStub then error(MAJOR_VERSION .. " requires LibStub.") end local lib, oldversion = LibStub:NewLibrary(MAJOR_VERSION, MINOR_VERSION) if not lib then return end local KeyBound = LibStub("LibKeyBound-1.0", true) local CBH = LibStub("CallbackHandler-1.0") lib.eventFrame = lib.eventFrame or CreateFrame("Frame") lib.eventFrame:UnregisterAllEvents() lib.buttonRegistry = lib.buttonRegistry or {} lib.activeButtons = lib.activeButtons or {} lib.callbacks = lib.callbacks or CBH:New(lib) local Generic = CreateFrame("CheckButton") local Generic_MT = {__index = Generic} local Action = setmetatable({}, {__index = Generic}) local Action_MT = {__index = Action} local PetAction = setmetatable({}, {__index = Generic}) local PetAction_MT = {__index = PetAction} local Spell = setmetatable({}, {__index = Generic}) local Spell_MT = {__index = Spell} local Item = setmetatable({}, {__index = Generic}) local Item_MT = {__index = Item} local Macro = setmetatable({}, {__index = Generic}) local Macro_MT = {__index = Macro} local Custom = setmetatable({}, {__index = Generic}) local Custom_MT = {__index = Custom} local type_meta_map = { empty = Generic_MT, action = Action_MT, --pet = PetAction_MT, spell = Spell_MT, item = Item_MT, macro = Macro_MT, custom = Custom_MT } local ButtonRegistry, ActiveButtons = lib.buttonRegistry, lib.activeButtons local Update, UpdateButtonState, UpdateUsable, UpdateCount, UpdateCooldown, UpdateTooltip local StartFlash, StopFlash, UpdateFlash, UpdateHotkeys, UpdateRangeTimer, UpdateOverlayGlow local UpdateFlyout, ShowGrid, HideGrid, UpdateGrid, SetupSecureSnippets, WrapOnClick local InitializeEventHandler, OnEvent, ForAllButtons, OnUpdate local SPELL_POWER_HOLY_POWER = SPELL_POWER_HOLY_POWER; local HAND_OF_LIGHT = GetSpellInfo(90174); local PLAYERCLASS = select(2, UnitClass('player')) local HOLY_POWER_SPELLS = { [85256] = GetSpellInfo(85256), --Templar's Verdict [53385] = GetSpellInfo(53385), --Divine Storm [53600] = GetSpellInfo(53600), --Shield of the Righteous }; local DefaultConfig = { outOfRangeColoring = "button", tooltip = "enable", showGrid = false, colors = { range = { 0.8, 0.1, 0.1 }, mana = { 0.5, 0.5, 1.0 }, hp = { 0.5, 0.5, 1.0 } }, hideElements = { macro = false, hotkey = false, equipped = false, }, keyBoundTarget = false, clickOnDown = false, } --- Create a new action button. -- @param id Internal id of the button (not used by LibActionButton-1.0, only for tracking inside the calling addon) -- @param name Name of the button frame to be created (not used by LibActionButton-1.0 aside from naming the frame) -- @param header Header that drives these action buttons (if any) function lib:CreateButton(id, name, header, config) if type(name) ~= "string" then error("Usage: CreateButton(id, name. header): Buttons must have a valid name!", 2) end if not header then error("Usage: CreateButton(id, name, header): Buttons without a secure header are not yet supported!", 2) end if not KeyBound then KeyBound = LibStub("LibKeyBound-1.0", true) end local button = setmetatable(CreateFrame("CheckButton", name, header, "SecureActionButtonTemplate, ActionButtonTemplate"), Generic_MT) button:RegisterForDrag("LeftButton", "RightButton") button:RegisterForClicks("AnyUp") -- Frame Scripts button:SetScript("OnEnter", Generic.OnEnter) button:SetScript("OnLeave", Generic.OnLeave) button:SetScript("PreClick", Generic.PreClick) button:SetScript("PostClick", Generic.PostClick) button.id = id button.header = header -- Mapping of state -> action button.state_types = {} button.state_actions = {} -- Store the LAB Version that created this button for debugging button.__LAB_Version = MINOR_VERSION -- just in case we're not run by a header, default to state 0 button:SetAttribute("state", 0) SetupSecureSnippets(button) WrapOnClick(button) -- Store all sub frames on the button object for easier access button.icon = _G[name .. "Icon"] button.flash = _G[name .. "Flash"] button.FlyoutBorder = _G[name .. "FlyoutBorder"] button.FlyoutBorderShadow = _G[name .. "FlyoutBorderShadow"] button.FlyoutArrow = _G[name .. "FlyoutArrow"] button.hotkey = _G[name .. "HotKey"] button.count = _G[name .. "Count"] button.actionName = _G[name .. "Name"] button.border = _G[name .. "Border"] button.cooldown = _G[name .. "Cooldown"] button.normalTexture = _G[name .. "NormalTexture"] -- Store the button in the registry, needed for event and OnUpdate handling if not next(ButtonRegistry) then InitializeEventHandler() end ButtonRegistry[button] = true button:UpdateConfig(config) -- run an initial update button:UpdateAction() UpdateHotkeys(button) -- somewhat of a hack for the Flyout buttons to not error. button.action = 0 lib.callbacks:Fire("OnButtonCreated", button) return button end function SetupSecureSnippets(button) button:SetAttribute("_custom", Custom.RunCustom) -- secure UpdateState(self, state) -- update the type and action of the button based on the state button:SetAttribute("UpdateState", [[ local state = ... self:SetAttribute("state", state) local type, action = (self:GetAttribute(format("labtype-%s", state)) or "empty"), self:GetAttribute(format("labaction-%s", state)) self:SetAttribute("type", type) if type ~= "empty" and type ~= "custom" then local action_field = (type == "pet") and "action" or type self:SetAttribute(action_field, action) self:SetAttribute("action_field", action_field) end local onStateChanged = self:GetAttribute("OnStateChanged") if onStateChanged then self:Run(onStateChanged, state, type, action) end ]]) -- this function is invoked by the header when the state changes button:SetAttribute("_childupdate-state", [[ self:RunAttribute("UpdateState", message) self:CallMethod("UpdateAction") ]]) -- secure PickupButton(self, kind, value, ...) -- utility function to place a object on the cursor button:SetAttribute("PickupButton", [[ local kind, value = ... if kind == "empty" then return "clear" elseif kind == "action" or kind == "pet" then local actionType = (kind == "pet") and "petaction" or kind return actionType, value elseif kind == "spell" or kind == "item" or kind == "macro" then return "clear", kind, value else print("LibActionButton-1.0: Unknown type: " .. tostring(kind)) return false end ]]) button:SetAttribute("OnDragStart", [[ if (self:GetAttribute("buttonlock") and not IsModifiedClick("PICKUPACTION")) or self:GetAttribute("LABdisableDragNDrop") then return false end local state = self:GetAttribute("state") local type = self:GetAttribute("type") -- if the button is empty, we can't drag anything off it if type == "empty" or type == "custom" then return false end -- Get the value for the action attribute local action_field = self:GetAttribute("action_field") local action = self:GetAttribute(action_field) -- non-action fields need to change their type to empty if type ~= "action" and type ~= "pet" then self:SetAttribute(format("labtype-%s", state), "empty") self:SetAttribute(format("labaction-%s", state), nil) -- update internal state self:RunAttribute("UpdateState", state) -- send a notification to the insecure code self:CallMethod("ButtonContentsChanged", state, "empty", nil) end -- return the button contents for pickup return self:RunAttribute("PickupButton", type, action) ]]) button:SetAttribute("OnReceiveDrag", [[ if self:GetAttribute("LABdisableDragNDrop") then return false end local kind, value, subtype, extra = ... if not kind or not value then return false end local state = self:GetAttribute("state") local buttonType, buttonAction = self:GetAttribute("type"), nil if buttonType == "custom" then return false end -- action buttons can do their magic themself -- for all other buttons, we'll need to update the content now if buttonType ~= "action" and buttonType ~= "pet" then -- with "spell" types, the 4th value contains the actual spell id if kind == "spell" then if extra then value = extra else print("no spell id?", ...) end elseif kind == "item" and value then value = format("item:%d", value) end -- Get the action that was on the button before if buttonType ~= "empty" then buttonAction = self:GetAttribute(self:GetAttribute("action_field")) end -- TODO: validate what kind of action is being fed in here -- We can only use a handful of the possible things on the cursor -- return false for all those we can't put on buttons self:SetAttribute(format("labtype-%d", state), kind) self:SetAttribute(format("labaction-%d", state), value) -- update internal state self:RunAttribute("UpdateState", state) -- send a notification to the insecure code self:CallMethod("ButtonContentsChanged", state, kind, value) else -- get the action for (pet-)action buttons buttonAction = self:GetAttribute("action") end return self:RunAttribute("PickupButton", buttonType, buttonAction) ]]) button:SetScript("OnDragStart", nil) -- Wrapped OnDragStart(self, button, kind, value, ...) button.header:WrapScript(button, "OnDragStart", [[ return self:RunAttribute("OnDragStart") ]]) -- Wrap twice, because the post-script is not run when the pre-script causes a pickup (doh) -- we also need some phony message, or it won't work =/ button.header:WrapScript(button, "OnDragStart", [[ return "message", "update" ]], [[ self:RunAttribute("UpdateState", self:GetAttribute("state")) ]]) button:SetScript("OnReceiveDrag", nil) -- Wrapped OnReceiveDrag(self, button, kind, value, ...) button.header:WrapScript(button, "OnReceiveDrag", [[ return self:RunAttribute("OnReceiveDrag", kind, value, ...) ]]) -- Wrap twice, because the post-script is not run when the pre-script causes a pickup (doh) -- we also need some phony message, or it won't work =/ button.header:WrapScript(button, "OnReceiveDrag", [[ return "message", "update" ]], [[ self:RunAttribute("UpdateState", self:GetAttribute("state")) ]]) end function WrapOnClick(button) -- Wrap OnClick, to catch changes to actions that are applied with a click on the button. button.header:WrapScript(button, "OnClick", [[ if self:GetAttribute("type") == "action" then local type, action = GetActionInfo(self:GetAttribute("action")) return nil, format("%s|%s", tostring(type), tostring(action)) end ]], [[ local type, action = GetActionInfo(self:GetAttribute("action")) if message ~= format("%s|%s", tostring(type), tostring(action)) then self:RunAttribute("UpdateState", self:GetAttribute("state")) end ]]) end ----------------------------------------------------------- --- utility function lib:GetAllButtons() local buttons = {} for button in next, ButtonRegistry do buttons[button] = true end return buttons end function Generic:ClearSetPoint(...) self:ClearAllPoints() self:SetPoint(...) end ----------------------------------------------------------- --- state management function Generic:ClearStates() for state in pairs(self.state_types) do self:SetAttribute(format("labtype-%s", state), nil) self:SetAttribute(format("labaction-%s", state), nil) end wipe(self.state_types) wipe(self.state_actions) end function Generic:SetState(state, kind, action) if not state then state = self:GetAttribute("state") end state = tostring(state) -- we allow a nil kind for setting a empty state if not kind then kind = "empty" end if not type_meta_map[kind] then error("SetStateAction: unknown action type: " .. tostring(kind), 2) end if kind ~= "empty" and action == nil then error("SetStateAction: an action is required for non-empty states", 2) end if kind ~= "custom" and action ~= nil and type(action) ~= "number" and type(action) ~= "string" or (kind == "custom" and type(action) ~= "table") then error("SetStateAction: invalid action data type, only strings and numbers allowed", 2) end if kind == "item" then if tonumber(action) then action = format("item:%s", action) else local itemString = string.match(itemLink, "^|c%x+|H(item[%d:]+)|h%[") if itemString then action = itemString end end end self.state_types[state] = kind self.state_actions[state] = action self:UpdateState(state) end function Generic:UpdateState(state) if not state then state = self:GetAttribute("state") end state = tostring(state) self:SetAttribute(format("labtype-%s", state), self.state_types[state]) self:SetAttribute(format("labaction-%s", state), self.state_actions[state]) if state ~= tostring(self:GetAttribute("state")) then return end if self.header then self.header:SetFrameRef("updateButton", self) self.header:Execute([[ local frame = self:GetFrameRef("updateButton") control:RunFor(frame, frame:GetAttribute("UpdateState"), frame:GetAttribute("state")) ]]) else -- TODO end self:UpdateAction() end function Generic:GetAction(state) if not state then state = self:GetAttribute("state") end state = tostring(state) return self.state_types[state] or "empty", self.state_actions[state] end function Generic:UpdateAllStates() for state in pairs(self.state_types) do self:UpdateState(state) end end function Generic:ButtonContentsChanged(state, kind, value) state = tostring(state) self.state_types[state] = kind or "emtpy" self.state_actions[state] = value lib.callbacks:Fire("OnButtonContentsChanged", self, state, self.state_types[state], self.state_actions[state]) self:UpdateAction(self) end function Generic:DisableDragNDrop(flag) if InCombatLockdown() then error("LibActionButton-1.0: You can only toggle DragNDrop out of combat!", 2) end if flag then self:SetAttribute("LABdisableDragNDrop", true) else self:SetAttribute("LABdisableDragNDrop", nil) end end function Generic:AddToButtonFacade(group) if type(group) ~= "table" or type(group.AddButton) ~= "function" then error("LibActionButton-1.0:AddToButtonFacade: You need to supply a proper group to use!", 2) end group:AddButton(self) self.LBFSkinned = true end function Generic:AddToMasque(group) if type(group) ~= "table" or type(group.AddButton) ~= "function" then error("LibActionButton-1.0:AddToMasque: You need to supply a proper group to use!", 2) end group:AddButton(self) self.MasqueSkinned = true end ----------------------------------------------------------- --- frame scripts -- copied (and adjusted) from SecureHandlers.lua local function PickupAny(kind, target, detail, ...) if kind == "clear" then ClearCursor() kind, target, detail = target, detail, ... end if kind == 'action' then PickupAction(target) elseif kind == 'item' then PickupItem(target) elseif kind == 'macro' then PickupMacro(target) elseif kind == 'petaction' then PickupPetAction(target) elseif kind == 'spell' then PickupSpell(target) elseif kind == 'companion' then PickupCompanion(target, detail) elseif kind == 'equipmentset' then PickupEquipmentSet(target) end end function Generic:OnUpdate(elapsed) if not GetCVarBool('lockActionBars') then return; end self.lastupdate = (self.lastupdate or 0) + elapsed; if (self.lastupdate < .2) then return end self.lastupdate = 0 local isDragKeyDown if GetModifiedClick("PICKUPACTION") == 'ALT' then isDragKeyDown = IsAltKeyDown() elseif GetModifiedClick("PICKUPACTION") == 'CTRL' then isDragKeyDown = IsControlKeyDown() elseif GetModifiedClick("PICKUPACTION") == 'SHIFT' then isDragKeyDown = IsShiftKeyDown() end if isDragKeyDown and (self.clickState == 'AnyDown' or self.clickState == nil) then self.clickState = 'AnyUp' self:RegisterForClicks(self.clickState) elseif self.clickState == 'AnyUp' and not isDragKeyDown then self.clickState = 'AnyDown' self:RegisterForClicks(self.clickState) end end function Generic:OnEnter() if self.config.tooltip ~= "disabled" and (self.config.tooltip ~= "nocombat" or not InCombatLockdown()) then UpdateTooltip(self) end if KeyBound then KeyBound:Set(self) end if self.config.clickOnDown then self:SetScript('OnUpdate', Generic.OnUpdate) end end function Generic:OnLeave() GameTooltip:Hide() self:SetScript('OnUpdate', nil) end -- Insecure drag handler to allow clicking on the button with an action on the cursor -- to place it on the button. Like action buttons work. function Generic:PreClick() if self._state_type == "action" or self._state_type == "pet" or InCombatLockdown() or self:GetAttribute("LABdisableDragNDrop") then return end -- check if there is actually something on the cursor local kind, value, subtype = GetCursorInfo() if not (kind and value) then return end self._old_type = self._state_type if self._state_type and self._state_type ~= "empty" then self._old_type = self._state_type self:SetAttribute("type", "empty") --self:SetState(nil, "empty", nil) end self._receiving_drag = true end local function formatHelper(input) if type(input) == "string" then return format("%q", input) else return tostring(input) end end function Generic:PostClick() UpdateButtonState(self) if self._receiving_drag and not InCombatLockdown() then if self._old_type then self:SetAttribute("type", self._old_type) self._old_type = nil end local oldType, oldAction = self._state_type, self._state_action local a, b, c = GetCursorInfo() self.header:SetFrameRef("updateButton", self) self.header:Execute(format([[ local frame = self:GetFrameRef("updateButton") control:RunFor(frame, frame:GetAttribute("OnReceiveDrag"), %s, %s, %s) control:RunFor(frame, frame:GetAttribute("UpdateState"), %s) ]], formatHelper(a), formatHelper(b), formatHelper(c), formatHelper(self:GetAttribute("state")))) PickupAny("clear", oldType, oldAction) end self._receiving_drag = nil end ----------------------------------------------------------- --- configuration local function merge(target, source, default) for k,v in pairs(default) do if type(v) ~= "table" then if source and source[k] ~= nil then target[k] = source[k] else target[k] = v end else if type(target[k]) ~= "table" then target[k] = {} else wipe(target[k]) end merge(target[k], type(source) == "table" and source[k], v) end end return target end function Generic:UpdateConfig(config) if config and type(config) ~= "table" then error("LibActionButton-1.0: UpdateConfig requires a valid configuration!", 2) end local oldconfig = self.config if not self.config then self.config = {} end -- merge the two configs merge(self.config, config, DefaultConfig) if self.config.outOfRangeColoring == "button" or (oldconfig and oldconfig.outOfRangeColoring == "button") then UpdateUsable(self) end if self.config.outOfRangeColoring == "hotkey" then self.outOfRange = nil elseif oldconfig and oldconfig.outOfRangeColoring == "hotkey" then self.hotkey:SetVertexColor(0.6, 0.6, 0.6) end if self.config.hideElements.macro then self.actionName:Show() else self.actionName:Hide() end UpdateHotkeys(self) UpdateGrid(self) Update(self) self:RegisterForClicks(self.config.clickOnDown and "AnyDown" or "AnyUp") end ----------------------------------------------------------- --- event handler function ForAllButtons(method, onlyWithAction, ...) assert(type(method) == "function") for button in next, (onlyWithAction and ActiveButtons or ButtonRegistry) do method(button, ...) end end function InitializeEventHandler() lib.eventFrame:SetScript("OnEvent", OnEvent) lib.eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD") lib.eventFrame:RegisterEvent("ACTIONBAR_SHOWGRID") lib.eventFrame:RegisterEvent("ACTIONBAR_HIDEGRID") --lib.eventFrame:RegisterEvent("ACTIONBAR_PAGE_CHANGED") --lib.eventFrame:RegisterEvent("UPDATE_BONUS_ACTIONBAR") --lib.eventFrame:RegisterEvent("UPDATE_OVERRIDE_ACTIONBAR") lib.eventFrame:RegisterEvent("ACTIONBAR_SLOT_CHANGED") lib.eventFrame:RegisterEvent("UPDATE_BINDINGS") lib.eventFrame:RegisterEvent("UPDATE_SHAPESHIFT_FORM") lib.eventFrame:RegisterEvent("ACTIONBAR_UPDATE_STATE") lib.eventFrame:RegisterEvent("ACTIONBAR_UPDATE_USABLE") lib.eventFrame:RegisterEvent("ACTIONBAR_UPDATE_COOLDOWN") lib.eventFrame:RegisterEvent("PLAYER_TARGET_CHANGED") lib.eventFrame:RegisterEvent("TRADE_SKILL_SHOW") lib.eventFrame:RegisterEvent("TRADE_SKILL_CLOSE") lib.eventFrame:RegisterEvent("ARCHAEOLOGY_CLOSED") lib.eventFrame:RegisterEvent("PLAYER_ENTER_COMBAT") lib.eventFrame:RegisterEvent("PLAYER_LEAVE_COMBAT") lib.eventFrame:RegisterEvent("START_AUTOREPEAT_SPELL") lib.eventFrame:RegisterEvent("STOP_AUTOREPEAT_SPELL") lib.eventFrame:RegisterUnitEvent("UNIT_ENTERED_VEHICLE", "player") lib.eventFrame:RegisterUnitEvent("UNIT_EXITED_VEHICLE", "player") lib.eventFrame:RegisterEvent("COMPANION_UPDATE") lib.eventFrame:RegisterEvent("UNIT_INVENTORY_CHANGED") lib.eventFrame:RegisterEvent("LEARNED_SPELL_IN_TAB") lib.eventFrame:RegisterEvent("PET_STABLE_UPDATE") lib.eventFrame:RegisterEvent("PET_STABLE_SHOW") lib.eventFrame:RegisterEvent("SPELL_ACTIVATION_OVERLAY_GLOW_SHOW") lib.eventFrame:RegisterEvent("SPELL_ACTIVATION_OVERLAY_GLOW_HIDE") -- With those two, do we still need the ACTIONBAR equivalents of them? lib.eventFrame:RegisterEvent("SPELL_UPDATE_CHARGES") lib.eventFrame:RegisterEvent("SPELL_UPDATE_COOLDOWN") lib.eventFrame:RegisterEvent("SPELL_UPDATE_USABLE") lib.eventFrame:RegisterEvent("PLAYER_EQUIPMENT_CHANGED") lib.eventFrame:Show() lib.eventFrame:SetScript("OnUpdate", OnUpdate) end function OnEvent(frame, event, arg1, ...) if event == "UNIT_INVENTORY_CHANGED" or event == "LEARNED_SPELL_IN_TAB" then local tooltipOwner = GameTooltip:GetOwner() if ButtonRegistry[tooltipOwner] then tooltipOwner:SetTooltip() end elseif event == "ACTIONBAR_SLOT_CHANGED" then --print("Update from: "..event) for button in next, ButtonRegistry do if button._state_type == "action" and (arg1 == 0 or arg1 == tonumber(button._state_action)) then Update(button) end end elseif event == "PLAYER_ENTERING_WORLD" or event == "UPDATE_SHAPESHIFT_FORM" then --print("Update from: "..event) ForAllButtons(Update) elseif event == "ACTIONBAR_PAGE_CHANGED" or event == "UPDATE_BONUS_ACTIONBAR" or event == "UPDATE_OVERRIDE_ACTIONBAR" then --print("Update from: "..event) ForAllButtons(Update) elseif event == "ACTIONBAR_SHOWGRID" then ShowGrid() elseif event == "ACTIONBAR_HIDEGRID" then HideGrid() elseif event == "UPDATE_BINDINGS" then ForAllButtons(UpdateHotkeys) elseif event == "PLAYER_TARGET_CHANGED" then UpdateRangeTimer() elseif event == "ACTIONBAR_UPDATE_STATE" or event == "UNIT_ENTERED_VEHICLE" or event == "UNIT_EXITED_VEHICLE" or (event == "COMPANION_UPDATE" and arg1 == "MOUNT") then ForAllButtons(UpdateButtonState, true) elseif event == "ACTIONBAR_UPDATE_USABLE" or event == "SPELL_UPDATE_USABLE" then ForAllButtons(UpdateUsable, true) elseif event == "ACTIONBAR_UPDATE_COOLDOWN" or event == "SPELL_UPDATE_COOLDOWN" then ForAllButtons(UpdateCooldown, true) elseif event == "TRADE_SKILL_SHOW" or event == "TRADE_SKILL_CLOSE" or event == "ARCHAEOLOGY_CLOSED" then ForAllButtons(UpdateButtonState, true) elseif event == "PLAYER_ENTER_COMBAT" then for button in next, ActiveButtons do if button:IsAttack() then StartFlash(button) end end elseif event == "PLAYER_LEAVE_COMBAT" then for button in next, ActiveButtons do if button:IsAttack() then StopFlash(button) end end elseif event == "START_AUTOREPEAT_SPELL" then for button in next, ActiveButtons do if button:IsAutoRepeat() then StartFlash(button) end end elseif event == "STOP_AUTOREPEAT_SPELL" then for button in next, ActiveButtons do if button.flashing == 1 and not button:IsAttack() then StopFlash(button) end end elseif event == "PET_STABLE_UPDATE" or event == "PET_STABLE_SHOW" then ForAllButtons(Update) elseif event == "SPELL_ACTIVATION_OVERLAY_GLOW_SHOW" then for button in next, ActiveButtons do local spellId = button:GetSpellId() if spellId and spellId == arg1 then ActionButton_ShowOverlayGlow(button) end end elseif event == "SPELL_ACTIVATION_OVERLAY_GLOW_HIDE" then for button in next, ActiveButtons do local spellId = button:GetSpellId() if spellId and spellId == arg1 then ActionButton_HideOverlayGlow(button) end end elseif event == "PLAYER_EQUIPMENT_CHANGED" then for button in next, ActiveButtons do if button._state_type == "item" then Update(button) end end elseif event == "SPELL_UPDATE_CHARGES" then ForAllButtons(Update) end end local flashTime = 0 local rangeTimer = -1 function OnUpdate(_, elapsed) flashTime = flashTime - elapsed rangeTimer = rangeTimer - elapsed -- Run the loop only when there is something to update if rangeTimer <= 0 or flashTime <= 0 then for button in next, ActiveButtons do -- Flashing if button.flashing == 1 and flashTime <= 0 then if button.flash:IsShown() then button.flash:Hide() else button.flash:Show() end end -- Range if rangeTimer <= 0 then local inRange = button:IsInRange() local oldRange = button.outOfRange button.outOfRange = (inRange == false) if oldRange ~= button.outOfRange then if button.config.outOfRangeColoring == "button" then UpdateUsable(button) elseif button.config.outOfRangeColoring == "hotkey" then local hotkey = button.hotkey if hotkey:GetText() == RANGE_INDICATOR then if inRange then hotkey:Show() else hotkey:Hide() end end if button.outOfRange then hotkey:SetVertexColor(unpack(button.config.colors.range)) else hotkey:SetVertexColor(0.6, 0.6, 0.6) end end end end end -- Update values if flashTime <= 0 then flashTime = flashTime + ATTACK_BUTTON_FLASH_TIME end if rangeTimer <= 0 then rangeTimer = TOOLTIP_UPDATE_TIME end end end local gridCounter = 0 function ShowGrid() gridCounter = gridCounter + 1 if gridCounter >= 1 then for button in next, ButtonRegistry do if button:IsShown() then button:SetAlpha(1.0) end end end end function HideGrid() if gridCounter > 0 then gridCounter = gridCounter - 1 end if gridCounter == 0 then for button in next, ButtonRegistry do if button:IsShown() and not button:HasAction() and not button.config.showGrid then button:SetAlpha(0.0) end end end end function UpdateGrid(self) if self.config.showGrid then self:SetAlpha(1.0) elseif gridCounter == 0 and self:IsShown() and not self:HasAction() then self:SetAlpha(0.0) end end ----------------------------------------------------------- --- KeyBound integration function Generic:GetHotkey() local name = "CLICK "..self:GetName()..":LeftButton" local key = GetBindingKey(self.config.keyBoundTarget or name) if not key and self.config.keyBoundTarget then key = GetBindingKey(name) end if key then return KeyBound and KeyBound:ToShortKey(key) or key end end local function getKeys(binding, keys) keys = keys or "" for i = 1, select("#", GetBindingKey(binding)) do local hotKey = select(i, GetBindingKey(binding)) if keys ~= "" then keys = keys .. ", " end keys = keys .. GetBindingText(hotKey, "KEY_") end return keys end function Generic:GetBindings() local keys, binding if self.config.keyBoundTarget then keys = getKeys(self.config.keyBoundTarget) end keys = getKeys("CLICK "..self:GetName()..":LeftButton") return keys end function Generic:SetKey(key) if self.config.keyBoundTarget then SetBinding(key, self.config.keyBoundTarget) else SetBindingClick(key, self:GetName(), "LeftButton") end end local function clearBindings(binding) while GetBindingKey(binding) do SetBinding(GetBindingKey(binding), nil) end end function Generic:ClearBindings() if self.config.keyBoundTarget then clearBindings(self.config.keyBoundTarget) end clearBindings("CLICK "..self:GetName()..":LeftButton") end ----------------------------------------------------------- --- button management function Generic:UpdateAction(force) local type, action = self:GetAction() if force or type ~= self._state_type or action ~= self._state_action then -- type changed, update the metatable if force or self._state_type ~= type then local meta = type_meta_map[type] or type_meta_map.empty setmetatable(self, meta) self._state_type = type end self._state_action = action Update(self) end end function Update(self) local test = self:GetName() if self:HasAction() then ActiveButtons[self] = true self:SetAlpha(1.0) UpdateButtonState(self) UpdateUsable(self) UpdateCooldown(self) UpdateFlash(self) self.count:SetAlpha(1) else ActiveButtons[self] = nil if gridCounter == 0 and not self.config.showGrid then self:SetAlpha(0.0) end self.count:SetAlpha(0) self.cooldown:Hide() end -- Add a green border if button is an equipped item if self:IsEquipped() and not self.config.hideElements.equipped then self.border:SetVertexColor(0, 1.0, 0, 0.35) self.border:Show() else self.border:Hide() end -- Update Action Text if not self:IsConsumableOrStackable() then self.actionName:SetText(self:GetActionText()) else self.actionName:SetText("") end -- Update icon and hotkey local texture = self:GetTexture() if texture then self.icon:SetTexture(texture) self.icon:Show() self.rangeTimer = - 1 self:SetNormalTexture("Interface\\Buttons\\UI-Quickslot2") if not self.LBFSkinned and not self.MasqueSkinned then self.normalTexture:SetTexCoord(0, 0, 0, 0) end else self.icon:Hide() self.cooldown:Hide() self.rangeTimer = nil self:SetNormalTexture("Interface\\Buttons\\UI-Quickslot") if self.hotkey:GetText() == RANGE_INDICATOR then self.hotkey:Hide() else self.hotkey:SetVertexColor(0.6, 0.6, 0.6) end if not self.LBFSkinned and not self.MasqueSkinned then self.normalTexture:SetTexCoord(-0.15, 1.15, -0.15, 1.17) end end self:UpdateLocal() UpdateCount(self) UpdateFlyout(self) UpdateOverlayGlow(self) if GameTooltip:GetOwner() == self then UpdateTooltip(self) end -- this could've been a spec change, need to call OnStateChanged for action buttons, if present if not InCombatLockdown() and self._state_type == "action" then local onStateChanged = self:GetAttribute("OnStateChanged") if onStateChanged then self.header:SetFrameRef("updateButton", self) self.header:Execute(([[ local frame = self:GetFrameRef("updateButton") control:RunFor(frame, frame:GetAttribute("OnStateChanged"), %s, %s, %s) ]]):format(formatHelper(self:GetAttribute("state")), formatHelper(self._state_type), formatHelper(self._state_action))) end end lib.callbacks:Fire("OnButtonUpdate", self) end function Generic:UpdateLocal() -- dummy function the other button types can override for special updating end function UpdateButtonState(self) if self:IsCurrentlyActive() or self:IsAutoRepeat() then self:SetChecked(true) else self:SetChecked(false) end lib.callbacks:Fire("OnButtonState", self) end local function IsHolyPowerAbility(actionId) if not actionId or type(actionId) ~= 'number' then return false; end local actionType, id = GetActionInfo(actionId); if actionType == 'macro' then local macroSpell = GetMacroSpell(id); if macroSpell then for spellId, spellName in pairs(HOLY_POWER_SPELLS) do if macroSpell == spellName then return true; end end end else return HOLY_POWER_SPELLS[id]; end return false; end function UpdateUsable(self) -- TODO: make the colors configurable -- TODO: allow disabling of the whole recoloring if self.config.outOfRangeColoring == "button" and self.outOfRange then self.icon:SetVertexColor(unpack(self.config.colors.range)) else local isUsable, notEnoughMana = self:IsUsable() local action = self._state_action if PLAYERCLASS == 'PALADIN' and IsHolyPowerAbility(action) and not(UnitPower('player', SPELL_POWER_HOLY_POWER) >= 3 or UnitBuff('player', HAND_OF_LIGHT)) then self.icon:SetVertexColor(unpack(self.config.colors.hp)) elseif isUsable then self.icon:SetVertexColor(1.0, 1.0, 1.0) --self.normalTexture:SetVertexColor(1.0, 1.0, 1.0) elseif notEnoughMana then self.icon:SetVertexColor(unpack(self.config.colors.mana)) --self.normalTexture:SetVertexColor(0.5, 0.5, 1.0) else self.icon:SetVertexColor(0.4, 0.4, 0.4) --self.normalTexture:SetVertexColor(1.0, 1.0, 1.0) end end lib.callbacks:Fire("OnButtonUsable", self) end function UpdateCount(self) local actionCount, charges, maxCharges = 0, 0, 0 local isItemAction = false if self._state_action and type(self._state_action) == "number" then charges, maxCharges = GetActionCharges(self._state_action) actionCount = self:GetCount() isItemAction = IsItemAction(self._state_action) end self.cooldown:SetParent(self) if self:IsConsumableOrStackable() or (not isItemAction and actionCount > 0) then if actionCount > (self.maxDisplayCount or 9999) then self.count:SetText("*") else self.count:SetText(actionCount) end elseif maxCharges > 1 then self.count:SetText(charges) else self.count:SetText("") end end function UpdateCooldown(self) local start, duration, enable, charges, maxCharges = self:GetCooldown() CooldownFrame_SetTimer(self.cooldown, start, duration, enable, charges, maxCharges) end function StartFlash(self) self.flashing = 1 flashTime = 0 UpdateButtonState(self) end function StopFlash(self) self.flashing = 0 self.flash:Hide() UpdateButtonState(self) end function UpdateFlash(self) if (self:IsAttack() and self:IsCurrentlyActive()) or self:IsAutoRepeat() then StartFlash(self) else StopFlash(self) end end function UpdateTooltip(self) if (GetCVar("UberTooltips") == "1") then GameTooltip_SetDefaultAnchor(GameTooltip, self); else GameTooltip:SetOwner(self, "ANCHOR_RIGHT"); end if self:SetTooltip() then self.UpdateTooltip = UpdateTooltip else self.UpdateTooltip = nil end end function UpdateHotkeys(self) local key = self:GetHotkey() if not key or key == "" or not self.config.hideElements.hotkey then self.hotkey:SetText(RANGE_INDICATOR) self.hotkey:SetPoint("TOPRIGHT", 0, -3); self.hotkey:Hide() else self.hotkey:SetText(key) self.hotkey:SetPoint("TOPRIGHT", 0, -3); self.hotkey:Show() end if self.postKeybind then self.postKeybind(nil, self) end end function UpdateOverlayGlow(self) local spellId = self:GetSpellId() if spellId and IsSpellOverlayed(spellId) then ActionButton_ShowOverlayGlow(self) else ActionButton_HideOverlayGlow(self) end end -- Hook UpdateFlyout so we can use the blizzy templates hooksecurefunc("ActionButton_UpdateFlyout", function(self, ...) if ButtonRegistry[self] then UpdateFlyout(self) end end) function UpdateFlyout(self) -- disabled FlyoutBorder/BorderShadow, those are not handled by LBF and look terrible self.FlyoutBorder:Hide() self.FlyoutBorderShadow:Hide() if self._state_type == "action" then -- based on ActionButton_UpdateFlyout in ActionButton.lua local actionType = GetActionInfo(self._state_action) if actionType == "flyout" then -- Update border and determine arrow position local arrowDistance if (SpellFlyout and SpellFlyout:IsShown() and SpellFlyout:GetParent() == self) or GetMouseFocus() == self then arrowDistance = 5 else arrowDistance = 2 end -- Update arrow self.FlyoutArrow:Show() self.FlyoutArrow:ClearAllPoints() local direction = self:GetAttribute("flyoutDirection"); if direction == "LEFT" then self.FlyoutArrow:SetPoint("LEFT", self, "LEFT", -arrowDistance, 0) SetClampedTextureRotation(self.FlyoutArrow, 270) elseif direction == "RIGHT" then self.FlyoutArrow:SetPoint("RIGHT", self, "RIGHT", arrowDistance, 0) SetClampedTextureRotation(self.FlyoutArrow, 90) elseif direction == "DOWN" then self.FlyoutArrow:SetPoint("BOTTOM", self, "BOTTOM", 0, -arrowDistance) SetClampedTextureRotation(self.FlyoutArrow, 180) else self.FlyoutArrow:SetPoint("TOP", self, "TOP", 0, arrowDistance) SetClampedTextureRotation(self.FlyoutArrow, 0) end -- return here, otherwise flyout is hidden return end if self.FlyoutUpdateFunc then self.FlyoutUpdateFunc(nil, self) end end self.FlyoutArrow:Hide() end function UpdateRangeTimer() rangeTimer = -1 end local function GetSpellIdByName(spellName) if not spellName then return end local spellLink = GetSpellLink(spellName) if spellLink then return tonumber(spellLink:match("spell:(%d+)")) end return nil end ----------------------------------------------------------- --- WoW API mapping --- Generic Button Generic.HasAction = function(self) return nil end Generic.GetActionText = function(self) return "" end Generic.GetTexture = function(self) return nil end Generic.GetCount = function(self) return 0 end Generic.GetCooldown = function(self) return 0, 0, 0 end Generic.IsAttack = function(self) return nil end Generic.IsEquipped = function(self) return nil end Generic.IsCurrentlyActive = function(self) return nil end Generic.IsAutoRepeat = function(self) return nil end Generic.IsUsable = function(self) return nil end Generic.IsConsumableOrStackable = function(self) return nil end Generic.IsInRange = function(self) return nil end Generic.SetTooltip = function(self) return nil end Generic.GetSpellId = function(self) return nil end ----------------------------------------------------------- --- Action Button Action.HasAction = function(self) return HasAction(self._state_action) end Action.GetActionText = function(self) return GetActionText(self._state_action) end Action.GetTexture = function(self) return GetActionTexture(self._state_action) end Action.GetCount = function(self) return GetActionCount(self._state_action) end Action.GetCooldown = function(self) return GetActionCooldown(self._state_action) end Action.IsAttack = function(self) return IsAttackAction(self._state_action) end Action.IsEquipped = function(self) return IsEquippedAction(self._state_action) end Action.IsCurrentlyActive = function(self) return IsCurrentAction(self._state_action) end Action.IsAutoRepeat = function(self) return IsAutoRepeatAction(self._state_action) end Action.IsUsable = function(self) return IsUsableAction(self._state_action) end Action.IsConsumableOrStackable = function(self) return IsConsumableAction(self._state_action) or IsStackableAction(self._state_action) end Action.IsInRange = function(self) return IsActionInRange(self._state_action, self:GetAttribute("unit")) end Action.SetTooltip = function(self) return GameTooltip:SetAction(self._state_action) end Action.GetSpellId = function(self) local actionType, id, subType = GetActionInfo(self._state_action) if actionType == "spell" then return id elseif actionType == "macro" then local _, _, spellId = GetMacroSpell(id) return spellId end end ----------------------------------------------------------- --- Spell Button Spell.HasAction = function(self) return true end Spell.GetActionText = function(self) return "" end Spell.GetTexture = function(self) return GetSpellTexture(self._state_action) end Spell.GetCount = function(self) return GetSpellCount(self._state_action) end Spell.GetCooldown = function(self) return GetSpellCooldown(self._state_action) end Spell.IsAttack = function(self) return IsAttackSpell(FindSpellBookSlotBySpellID(self._state_action), "spell") end -- needs spell book id as of 4.0.1.13066 Spell.IsEquipped = function(self) return nil end Spell.IsCurrentlyActive = function(self) return IsCurrentSpell(self._state_action) end Spell.IsAutoRepeat = function(self) return IsAutoRepeatSpell(FindSpellBookSlotBySpellID(self._state_action), "spell") end -- needs spell book id as of 4.0.1.13066 Spell.IsUsable = function(self) return IsUsableSpell(self._state_action) end Spell.IsConsumableOrStackable = function(self) return IsConsumableSpell(self._state_action) end Spell.IsInRange = function(self) return IsSpellInRange(FindSpellBookSlotBySpellID(self._state_action), "spell", self:GetAttribute("unit")) end -- needs spell book id as of 4.0.1.13066 Spell.SetTooltip = function(self) return GameTooltip:SetSpellByID(self._state_action) end Spell.GetSpellId = function(self) return self._state_action end ----------------------------------------------------------- --- Item Button local function getItemId(input) return input:match("^item:(%d+)") end Item.HasAction = function(self) return true end Item.GetActionText = function(self) return "" end Item.GetTexture = function(self) return GetItemIcon(self._state_action) end Item.GetCount = function(self) return GetItemCount(self._state_action, nil, true) end Item.GetCooldown = function(self) return GetItemCooldown(getItemId(self._state_action)) end Item.IsAttack = function(self) return nil end Item.IsEquipped = function(self) return IsEquippedItem(self._state_action) end Item.IsCurrentlyActive = function(self) return IsCurrentItem(self._state_action) end Item.IsAutoRepeat = function(self) return nil end Item.IsUsable = function(self) return IsUsableItem(self._state_action) end Item.IsConsumableOrStackable = function(self) return IsConsumableItem(self._state_action) end Item.IsInRange = function(self) return IsItemInRange(self._state_action, self:GetAttribute("unit")) end Item.SetTooltip = function(self) return GameTooltip:SetHyperlink(self._state_action) end Item.GetSpellId = function(self) return nil end ----------------------------------------------------------- --- Macro Button -- TODO: map results of GetMacroSpell/GetMacroItem to proper results Macro.HasAction = function(self) return true end Macro.GetActionText = function(self) return (GetMacroInfo(self._state_action)) end Macro.GetTexture = function(self) return (select(2, GetMacroInfo(self._state_action))) end Macro.GetCount = function(self) return 0 end Macro.GetCooldown = function(self) return 0, 0, 0 end Macro.IsAttack = function(self) return nil end Macro.IsEquipped = function(self) return nil end Macro.IsCurrentlyActive = function(self) return nil end Macro.IsAutoRepeat = function(self) return nil end Macro.IsUsable = function(self) return nil end Macro.IsConsumableOrStackable = function(self) return nil end Macro.IsInRange = function(self) return nil end Macro.SetTooltip = function(self) return nil end Macro.GetSpellId = function(self) return nil end ----------------------------------------------------------- --- Custom Button Custom.HasAction = function(self) return true end Custom.GetActionText = function(self) return "" end Custom.GetTexture = function(self) return self._state_action.texture end Custom.GetCount = function(self) return 0 end Custom.GetCooldown = function(self) return 0, 0, 0 end Custom.IsAttack = function(self) return nil end Custom.IsEquipped = function(self) return nil end Custom.IsCurrentlyActive = function(self) return nil end Custom.IsAutoRepeat = function(self) return nil end Custom.IsUsable = function(self) return true end Custom.IsConsumableOrStackable = function(self) return nil end Custom.IsInRange = function(self) return nil end Custom.SetTooltip = function(self) return GameTooltip:SetText(self._state_action.tooltip) end Custom.GetSpellId = function(self) return nil end Custom.RunCustom = function(self, unit, button) return self._state_action.func(self, unit, button) end ----------------------------------------------------------- --- Update old Buttons if oldversion and next(lib.buttonRegistry) then InitializeEventHandler() for button in next, lib.buttonRegistry do -- this refreshes the metatable on the button Generic.UpdateAction(button, true) SetupSecureSnippets(button) if oldversion < 12 then WrapOnClick(button) end end end