--[[------------------------------------------------------------------- -- Clique - Copyright 2006-2010 - James N. Whitehead II -- -- This is an updated version of the original 'Clique' addon -- designed to work better with multi-button mice, and those players -- who want to be able to bind keyboard combinations to enable -- hover-casting on unit frames. It's a bit of a paradigm shift from -- the original addon, but should make a much simpler and more -- powerful addon. -- -- * Any keyboard combination can be set as a binding. -- * Any mouse combination can be set as a binding. -- * The only types that are allowed are spells and macros. -- -- The concept of 'click-sets' has been simplified and extended -- so that the user can specify their own binding-sets, allowing -- for different bindings for different sets of frames. By default -- the following binding-sets are available: -- -- * default - These bindings are active on all frames, unless -- overridden by another binding in a more specific binding-set. -- * ooc - These bindings will ONLY be active when the player is -- out of combat. -- * enemy - These bindings are ONLY active when the unit you are -- clicking on is an enemy, i.e. a unit that you can attack. -- * friendly - These bindings are ONLY active when the unit you are -- clicking on is a friendly unit, i.e. one that you can assist -- * hovercast - These bindings will be available whenever you are over -- a unit frame, or a unit in the 3D world. -- * global - These bindings will be always available. They -- do not specify a target for the action, so if the action requires -- a target, you must specify it after performing the binding. -- -- The binding-sets layer on each other, with the 'default' binding-set -- being at the bottom, and any other binding-set being layered on top. -- Clique will detect any conflicts that you have other than with -- default bindings, and will warn you of the situation. -------------------------------------------------------------------]]-- local addonName, addon = ... local L = addon.L function addon:Initialize() -- Are we running on release rather than classic? self.compatRelease = WOW_PROJECT_ID == WOW_PROJECT_MAINLINE -- Create an AceDB, but it needs to be cleared first self.db = LibStub("AceDB-3.0"):New("CliqueDB3", self.defaults) self.db.RegisterCallback(self, "OnNewProfile", "OnNewProfile") self.db.RegisterCallback(self, "OnProfileChanged", "OnProfileChanged") self.settings = self.db.char self.bindings = self.db.profile.bindings self.ccframes = {} self.hccframes = {} -- Registration for group headers (in-combat safe) self.header = CreateFrame("Frame", addonName .. "HeaderFrame", UIParent, "SecureHandlerBaseTemplate,SecureHandlerAttributeTemplate") ClickCastHeader = addon.header -- This snippet will clear any dangling bindings that might have occurred -- as a result of frames being shown/hidden. local oacScript = [[ if name == "hasunit" and value == "false" and danglingButton then -- Check if we should clear the bindings if not danglingButton:IsUnderMouse() or not danglingButton:IsVisible() then if {{debug}} then print("Clique: clearing bindings, unit lost") end self:RunFor(danglingButton, self:GetAttribute("setup_onleave")) danglingButton = nil else if {{debug}} then print("Clique: ignoring unit loss, frame still here") end end end ]] oacScript = oacScript:gsub("{{debug}}", self.settings.debugUnitIssue and "true" or "false") self.header:SetAttribute("_onattributechanged", oacScript) RegisterAttributeDriver(self.header, "hasunit", "[@mouseover, exists] true; false") -- Create a secure action button that's sole purpose is to cancel a -- pending spellcast (the targeting hand) self.stopbutton = CreateFrame("Button", addonName .. "StopButton", nil, "SecureActionButtonTemplate") self.stopbutton.name = self.stopbutton:GetName() self.stopbutton:SetAttribute("type", "stop") -- Create a secure action button that can be used for 'hovercast' and 'global' self.globutton = CreateFrame("Button", addonName .. "SABButton", UIParent, "SecureActionButtonTemplate, SecureHandlerBaseTemplate") -- Create a table within the addon header to store the frames -- that are registered for click-casting self.header:Execute([[ ccframes = table.new() ]]) -- Create a table within the addon header to store the frame bakcklist self.header:Execute([[ blacklist = table.new() ]]) -- This snippet is executed from the SecureHandlerEnterLeaveTemplate -- _onenter and _onleave attributes. The 'self' attribute will contain -- the unit frame itself. self.header:SetAttribute("clickcast_onenter", [===[ local header = self:GetParent():GetFrameRef("clickcast_header") header:RunFor(self, header:GetAttribute("setup_onenter")) ]===]) -- This snippet is executed from the SecureHandlerEnterLeaveTemplate -- _onenter and _onleave attributes. The 'self' attribute will contain -- the unit frame itself. self.header:SetAttribute("clickcast_onleave", [===[ local header = self:GetParent():GetFrameRef("clickcast_header") header:RunFor(self, header:GetAttribute("setup_onleave")) ]===]) local setup, remove = self:GetClickAttributes() self.header:SetAttribute("setup_clicks", setup) self.header:SetAttribute("remove_clicks", remove) -- This snippet is executed from within the initialConfigFunction secure -- snippet. The unit frame button is passed in the 'clickcast_button' -- attribute, which can only be accomplished in a restricted environment. self.header:SetAttribute("clickcast_register", [===[ local button = self:GetAttribute("clickcast_button") -- Export this frame so we can display it in the insecure environment self:SetAttribute("export_register", button) button:SetAttribute("clickcast_onenter", self:GetAttribute("clickcast_onenter")) button:SetAttribute("clickcast_onleave", self:GetAttribute("clickcast_onleave")) ccframes[button] = true self:RunFor(button, self:GetAttribute("setup_clicks")) ]===]) -- This snippet is executed from the Clique:UnregisterFrame() function, or -- possibly from some other restricted environment. The unit frame is passed -- in the 'clickcast_button' attribute, which can only be accomplished -- in a restricted environment. self.header:SetAttribute("clickcast_unregister", [===[ local button = self:GetAttribute("clickcast_button") -- Export this frame so it can be removed from the blacklist editor self:SetAttribute("export_unregister", button) -- Remove any click and binding attributes that have already been set self:RunFor(button, self:GetAttribute("clickcast_onleave")) self:RunFor(button, self:GetAttribute("remove_clicks")) button:SetAttribute("clickcast_onenter", nil) button:SetAttribute("clickcast_onleave", nil) ccframes[button] = nil ]===]) -- We need to track frame registrations so we can display secure frames in -- the frame blacklist editor. This is done via the 'export_register' and -- 'export_unregister' attributes. self.header:HookScript("OnAttributeChanged", function(frame, name, value) if name == "export_register" and type(value) ~= nil then -- Convert the userdata object to the global object so we have access -- to all of the correct methods, such as 'RegisterForClicks'' local name = value.GetName and value:GetName() if name then local button = _G[name] self.hccframes[name] = button self:UpdateRegisteredClicks(button) end elseif name == "export_unregister" and type(value) ~= nil then local name = value.GetName and value:GetName() if name then self.hccframes[name] = nil end end end) local set, clr = self:GetBindingAttributes() self.header:SetAttribute("setup_onenter", set) self.header:SetAttribute("setup_onleave", clr) -- Get the override binding attributes for the global click frame self.globutton.setup, self.globutton.remove = self:GetClickAttributes(true) self.globutton.setbinds, self.globutton.clearbinds = self:GetBindingAttributes(true) -- Compatability with old Clique 1.x registrations local oldClickCastFrames = ClickCastFrames ClickCastFrames = setmetatable({}, {__newindex = function(t, k, v) if v == nil or v == false then self:UnregisterFrame(k) else self:RegisterFrame(k, v) end end}) -- Iterate over the frames that were set before we arrived if oldClickCastFrames then for frame, options in pairs(oldClickCastFrames) do self:RegisterFrame(frame, options) end end self:EnableBlizzardFrames() -- Register for combat events to ensure we can swap between the two states self:RegisterEvent("PLAYER_REGEN_DISABLED", "EnteringCombat") self:RegisterEvent("PLAYER_REGEN_ENABLED", "LeavingCombat") self:RegisterEvent("PLAYER_ENTERING_WORLD", "PlayerEnteringWorld") -- Register for Clique-based messages for settings updates, etc. self:RegisterMessage("BINDINGS_CHANGED") self:RegisterMessage("BLACKLIST_CHANGED") -- Handle combat watching so we can change ooc based on party combat status addon:UpdateCombatWatch() -- Handle talent specs for release if self.compatRelease then self:RegisterEvent("ACTIVE_TALENT_GROUP_CHANGED", "TalentGroupChanged") self:TalentGroupChanged() end self:FireMessage("BLACKLIST_CHANGED") self:FireMessage("BINDINGS_CHANGED") end -- These tables are a queue for frame registration/unregistration addon.regqueue = {} addon.unregqueue = {} addon.regclickqueue = {} -- These function may be called during combat. When that is the case, the -- request must be queued until combat ends, and then we can attempt to -- register those frames. This is mainly due to integration with the -- Blizzard raid frames, which we cannot 'register' while in combat. function addon:RegisterFrame(button) if InCombatLockdown() then table.insert(self.regqueue, button) return end self.ccframes[button] = true self:UpdateRegisteredClicks(button) -- Wrap the OnEnter/OnLeave scripts in order to handle keybindings addon.header:WrapScript(button, "OnEnter", addon.header:GetAttribute("setup_onenter")) addon.header:WrapScript(button, "OnLeave", addon.header:GetAttribute("setup_onleave")) -- Set the attributes on the frame self.header:SetFrameRef("cliquesetup_button", button) self.header:Execute(self.header:GetAttribute("setup_clicks"), button) end function addon:UnregisterFrame(button) if InCombatLockdown() then table.insert(self.unregqueue, button) return end -- Clear any click/bind attributes self.header:SetFrameRef("cliquesetup_button", button) self.header:Execute([[ local button = self:GetFrameRef("cliquesetup_button") self:RunFor(button, self:GetAttribute("setup_onleave")) self:RunAttribute("remove_clicks") ]]) self.ccframes[button] = nil -- Unwrap the OnEnter/OnLeave scripts, if they were set addon.header:UnwrapScript(button, "OnEnter") addon.header:UnwrapScript(button, "OnLeave") end function addon:Enable() -- Make the options window a pushable panel window UIPanelWindows["CliqueConfig"] = { area = "left", pushable = 1, whileDead = 1, } -- Set the tooltip for the spellbook tab CliqueSpellTab.tooltip = L["Clique binding configuration"] end -- A new profile is being created in the db, called 'profile' function addon:OnNewProfile(event, db, profile) table.insert(db.profile.bindings, { key = "BUTTON1", type = "target", unit = "mouseover", sets = { default = true }, }) table.insert(db.profile.bindings, { key = "BUTTON2", type = "menu", sets = { default = true }, }) self.bindings = db.profile.bindings end function addon:OnProfileChanged(event, db, newProfile) self.bindings = db.profile.bindings self:FireMessage("BINDINGS_CHANGED") end local function ATTR(indent, prefix, attr, suffix, value) local fmt = [[%sbutton:SetAttribute("%s%s%s%s%s", %q)]] return fmt:format(indent, prefix, #prefix > 0 and "-" or "", attr, tonumber(suffix) and "" or "-", suffix, value) end local function REMATTR(prefix, attr, suffix, value) local fmt = [[button:SetAttribute("%s%s%s%s%s", nil)]] return fmt:format(prefix, #prefix > 0 and "-" or "", attr, tonumber(suffix) and "" or "-", suffix) end -- A sort function that determines in what order bindings should be applied. -- This function should be treated with care, it can drastically change behavior local function ApplicationOrder(a, b) local acnt, bcnt = 0, 0 for k,v in pairs(a.sets) do acnt = acnt + 1 end for k,v in pairs(b.sets) do bcnt = bcnt + 1 end -- Force out-of-combat clicks to take the HIGHEST priority if a.sets.ooc and not b.sets.ooc then return true elseif b.sets.ooc and not a.sets.ooc then return false elseif a.sets.ooc and b.sets.ooc then return acnt < bcnt end -- Try to give any 'default' clicks LOWEST priority if a.sets.default and not b.sets.default then return true elseif a.sets.default and b.sets.default then return acnt < bcnt end end local function shouldApply(global, entry) -- If this is the global button and this is a 'global' binding if global and (entry.sets.hovercast or entry.sets.global) then return true elseif not global then -- Check to see if there's a non-global binding to be set for k, v in pairs(entry.sets) do if k ~= "global" and k ~= "hovercast" then return true end end return false end end local function correctSpec(entry) if not addon.compatRelease then return true end -- Check to ensure we're on the right spec for this binding local currentSpec = GetSpecialization() if currentSpec and entry.sets["spec" .. tostring(currentSpec)] then return true end -- Need to check the other spec sets to ensure this shouldn't be -- deactivated for i = 1, GetNumSpecializations() do if entry.sets["spec" .. tostring(i)] then return false end end return true end -- This function takes a single argument indicating if the attributes being -- computed are for the special 'global' button used by Clique. It then -- computes the set of attributes necessary for the player's bindings to be -- active on all the appropriate frames. The logic here is quite delicate but -- also rather well commented. function addon:GetClickAttributes(global) -- In these scripts, 'self' should always be the header local bits = { "local inCombat = control:GetAttribute('inCombat')", "local setupbutton = self:GetFrameRef('cliquesetup_button')", "local button = setupbutton or self", } local rembits = { "local inCombat = control:GetAttribute('inCombat')", "local setupbutton = self:GetFrameRef('cliquesetup_button')", "local button = setupbutton or self", } -- Check to see if the frame being setup is blacklisted. Do not perform -- this check on the global frame. if not global then bits[#bits + 1] = "local name = button:GetName()" bits[#bits + 1] = "if blacklist[name] then return end" rembits[#rembits + 1] = "local name = button:GetName()" rembits[#rembits + 1] = "if blacklist[name] then return end" end -- Sort the bindings so they are applied in order. This sort ensures that -- any 'ooc' bindings are applied first. table.sort(self.bindings, ApplicationOrder) -- Build a small table of ooc keys that are 'taken' so we can check for -- masking conflicts with the friend/enemy sets. local oocKeys = {} for idx, entry in ipairs(self.bindings) do if shouldApply(global, entry) and entry.sets.ooc and entry.key then oocKeys[entry.key] = true end end for idx, entry in ipairs(self.bindings) do -- Global (i.e. 'hovercast' and 'global') bindings are only applied -- on the global frame, and not on any others. Additionally, any -- non-global bindings are only applied on non-global frames. handle -- this logic here. if shouldApply(global, entry) and correctSpec(entry) and entry.key then -- Check to see if this is a 'friend' or an 'enemy' binding, and -- check if it would mask an 'ooc' binding with the same key. If -- so, we need to add code that prevents this from happening, by -- stopping the friend/enemy binding from being applied when the -- player is out of combat. local indent = "" local oocmask = oocKeys[entry.key] -- This code needs to set/clear a binding depending on combat -- state. We do both in this function to ensure that we don't have -- to run remove_clicks every single time the combat status -- changes. local startbits if oocmask and not entry.sets.ooc then -- This means that the binding will mask the 'ooc' binding -- with the same key, so we must ensure this is only set when -- we are in combat. bits[#bits + 1] = "if inCombat then -- non-ooc that is masking" indent = indent .. " " elseif entry.sets.ooc then -- This is a standard 'ooc' binding, so we want to ensure its -- only applied when out of combat, and cleared otherwise. bits[#bits + 1] = "if not inCombat then -- ooc binding" indent = indent .. " " startbits = #rembits + 1 end local prefix, suffix = addon:GetBindingPrefixSuffix(entry, global) -- Set up help/harm bindings. The button value will be either a number, -- in the case of mouse buttons, otherwise it will be a string of -- characters. Harmbuttons work alongside modifiers, so we need to include -- then in the remapping. if entry.sets.friend then if global then -- A modified binding that uses friend/enemy must have the unmodified -- 'unit' attribute set, in order to do the friend/enemy lookup. Add -- that here. -- -- NOTE: This will not work with useOwnerUnit and usesuffix frames -- such as pet frames that use the owner's parent. This is a problem -- with the way the 'mouseover' unit resolves in these cases. bits[#bits + 1] = ATTR(indent, prefix, "unit", suffix, "mouseover") rembits[#rembits + 1] = REMATTR(prefix, "unit", suffix) end local newbutton = "friend" .. suffix bits[#bits + 1] = ATTR(indent, prefix, "helpbutton", suffix, newbutton) rembits[#rembits + 1] = REMATTR(prefix, "helpbutton", suffix) suffix = newbutton elseif entry.sets.enemy then if global then -- A modified binding that uses friend/enemy must have the unmodified -- 'unit' attribute set, in order to do the friend/enemy lookup. Add -- that here. -- -- NOTE: This will not work with useOwnerUnit and usesuffix frames -- such as pet frames that use the owner's parent. This is a problem -- with the way the 'mouseover' unit resolves in these cases. bits[#bits + 1] = ATTR(indent, prefix, "unit", suffix, "mouseover") rembits[#rembits + 1] = REMATTR(prefix, "unit", suffix) end local newbutton = "enemy" .. suffix bits[#bits + 1] = ATTR(indent, prefix, "harmbutton", suffix, newbutton) rembits[#rembits + 1] = REMATTR(prefix, "harmbutton", suffix) suffix = newbutton end -- When we're setting up the 'global' button, and the binding is in the -- 'hovercast' binding set, we need to specify the unit on which to take -- the action. In this case, that's just mouseover. if global and entry.sets.hovercast then bits[#bits + 1] = ATTR(indent, prefix, "unit", suffix, "mouseover") rembits[#rembits + 1] = REMATTR(prefix, "unit", suffix) end -- Build any needed SetAttribute() calls if entry.type == "target" then bits[#bits + 1] = ATTR(indent, prefix, "type", suffix, entry.type) rembits[#rembits + 1] = REMATTR(prefix, "type", suffix) elseif entry.type == "menu" then set_text = ATTR(indent, prefix, "type", suffix, "togglemenu") bits[#bits + 1] = string.gsub(set_text, '"togglemenu"', 'button:GetAttribute("*type2") == "menu" and "menu" or "togglemenu"') rembits[#rembits + 1] = REMATTR(prefix, "type", suffix) elseif entry.type == "spell" and self.settings.stopcastingfix then -- Implement the 'stop casting' fix local macrotext local spellText = addon:SpellTextWithSubName(entry) if entry.sets.global then -- Do not include @mouseover macrotext = string.format("/click %s\n/cast %s", self.stopbutton.name, spellText) else macrotext = string.format("/click %s\n/cast [@mouseover] %s", self.stopbutton.name, entry.spell) end bits[#bits + 1] = ATTR(indent, prefix, "type", suffix, "macro") bits[#bits + 1] = ATTR(indent, prefix, "macrotext", suffix, macrotext) rembits[#rembits + 1] = REMATTR(prefix, "type", suffix) rembits[#rembits + 1] = REMATTR(prefix, "macrotext", suffix) elseif entry.type == "spell" then local spellText = addon:SpellTextWithSubName(entry) bits[#bits + 1] = ATTR(indent, prefix, "type", suffix, entry.type) bits[#bits + 1] = ATTR(indent, prefix, "spell", suffix, spellText) rembits[#rembits + 1] = REMATTR(prefix, "type", suffix) rembits[#rembits + 1] = REMATTR(prefix, "spell", suffix) elseif entry.type == "macro" and self.settings.stopcastingfix then local macrotext = string.format("/click %s\n%s", self.stopbutton.name, entry.macrotext) bits[#bits + 1] = ATTR(indent, prefix, "type", suffix, entry.type) bits[#bits + 1] = ATTR(indent, prefix, "macrotext", suffix, macrotext) rembits[#rembits + 1] = REMATTR(prefix, "type", suffix) rembits[#rembits + 1] = REMATTR(prefix, "macrotext", suffix) elseif entry.type == "macro" then bits[#bits + 1] = ATTR(indent, prefix, "type", suffix, entry.type) bits[#bits + 1] = ATTR(indent, prefix, "macrotext", suffix, entry.macrotext) rembits[#rembits + 1] = REMATTR(prefix, "type", suffix) rembits[#rembits + 1] = REMATTR(prefix, "macrotext", suffix) else error(string.format("Invalid action type: '%s'", entry.type)) end -- Finish the conditional statements started above if oocmask and not entry.sets.ooc then -- This means that the binding will mask the 'ooc' binding -- with the same key, so we must ensure this is only set when -- we are in combat. bits[#bits + 1] = "end" indent = indent:sub(1, -3) elseif entry.sets.ooc then -- This is a standard 'ooc' binding, so we want to ensure its -- only applied when out of combat, and cleared otherwise. local endbits = #rembits bits[#bits + 1] = "else -- clear ooc binding" for i = startbits, endbits, 1 do bits[#bits + 1] = indent .. rembits[i] end bits[#bits + 1] = "end" indent = indent:sub(1, -3) end end end return table.concat(bits, "\n"), table.concat(rembits, "\n") end local B_SET = [[self:SetBindingClick(true, %q, self, %q);]] local B_CLR = [[self:ClearBinding(%q);]] -- This function takes a single argument, indicating whether the attributes -- should be built for the special global button or not, and returns an -- attribute that can set the appropriate attributes, and one that can clear function addon:GetBindingAttributes(global) local set, clr -- If this is not the global button, include some logic that solves issues -- when the frame disappears or the frame loses focus without the OnLeave -- event firing. -- -- TODO: In the future, this should be done via OnHide or other ways as well if global then set = {} clr = {} else set = { "local button = self", "local name = button:GetName()", "if danglingButton then control:RunFor(danglingButton, control:GetAttribute('setup_onleave')) end", "if blacklist[name] then return end", "danglingButton = button", } clr = { "local button = self", "local name = button:GetName()", "if blacklist[name] then return end", "danglingButton = nil", } end -- This function is greatly simplified in that regardless of whether or -- not bindings mask one another, they still need to be set as binding -- clicks on the frame. Simply make a list of the keys that need to be -- bound, and bind them. local unique = {} for idx, entry in ipairs(self.bindings) do if entry.key then if shouldApply(global, entry) and correctSpec(entry) then if global then -- Allow for the re-binding of clicks and keys, except for -- unmodified left/right-click if entry.key ~= "BUTTON1" and entry.key ~= "BUTTON2" then local prefix, suffix = addon:GetBindingPrefixSuffix(entry, global) local key = self:ConvertSpecialKeys(entry) local attr = B_SET:format(key, suffix) if not unique[attr] then set[#set + 1] = attr clr[#clr + 1] = B_CLR:format(key) unique[attr] = true end end else local buttonNum = entry.key:match("BUTTON(%d+)$") if not buttonNum then -- Only apply key-based binding clicks, let the raw -- attributes handle the others local prefix, suffix = addon:GetBindingPrefixSuffix(entry, global) local key = self:ConvertSpecialKeys(entry) local attr = B_SET:format(key, suffix) if not unique[attr] then set[#set + 1] = attr clr[#clr + 1] = B_CLR:format(key) unique[attr] = true end end end end end end return table.concat(set, "\n"), table.concat(clr, "\n") end -- This function adds a binding to the player's current profile. The -- following options can be included in the click-cast entry: -- -- entry = { -- -- The full prefix and suffix of the key being bound -- key = "ALT-CTRL-SHIFT-BUTTON1", -- -- The icon to be used for displaying this entry -- icon = "Interface\\Icons\\Spell_Nature_HealingTouch", -- -- -- Any restricted sets that this click should be applied to -- sets = {"ooc", "harm", "help", "frames_blizzard"}, -- -- -- The type of the click-binding -- type = "spell", -- type = "macro", -- type = "target", -- type = "menu", -- -- -- Any arguments for given click type -- spell = "Healing Touch", -- macrotext = "/run Nature's Swiftness\n/cast [target=mouseover] Healing Touch", -- unit = "mouseover", -- } function addon:AddBinding(entry) if InCombatLockdown() then return false end -- TODO: Check to see if the new binding conflicts with an existing binding -- TODO: Validate the entry to ensure it has the correct arguments, etc. if not entry.sets then entry.sets = {default = true} end table.insert(self.bindings, entry) self:FireMessage("BINDINGS_CHANGED") return true end local function bindingeq(a, b) assert(type(a) == "table", "Error during deletion comparison") assert(type(b) == "table", "Error during deletion comparison") if a.type ~= b.type then return false elseif a.type == "target" then return a.key == b.key elseif a.type == "menu" then return a.key == b.key elseif a.type == "spell" then return a.spell == b.spell and a.key == b.key and a.spellSubName == b.spellSubName elseif a.type == "macro" then return a.macrotext == b.macrotext and a.key == b.key end return false end function addon:DeleteBinding(entry) if InCombatLockdown() then return false end -- Look for an entry that matches the given binding and remove it for idx, bind in ipairs(self.bindings) do if bindingeq(entry, bind) then -- Found the entry that matches, so remove it table.remove(self.bindings, idx) break end end self:FireMessage("BINDINGS_CHANGED") end function addon:ClearAttributes() self.header:Execute([[ for button, enabled in pairs(ccframes) do self:RunFor(button, self:GetAttribute("remove_clicks")) end ]]) for button, enabled in pairs(self.ccframes) do -- Perform the setup of click bindings self.header:SetFrameRef("cliquesetup_button", button) self.header:Execute(self.header:GetAttribute("remove_clicks"), button) end -- Clear global attributes local globutton = self.globutton globutton:Execute(globutton.remove) globutton:Execute(globutton.clearbinds) end -- Recompute all attributes, so they can later be applied. function addon:UpdateAttributes() local setup, remove = self:GetClickAttributes() self.header:SetAttribute("setup_clicks", setup) self.header:SetAttribute("remove_clicks", remove) local set, clr = self:GetBindingAttributes() self.header:SetAttribute("setup_onenter", set) self.header:SetAttribute("setup_onleave", clr) local globutton = self.globutton globutton.setup, globutton.remove = self:GetClickAttributes(true) globutton.setbinds, globutton.clearbinds = self:GetBindingAttributes(true) end function addon:ApplyAttributes() -- Handle all of the securely registered frames self.header:Execute([[ for button, enabled in pairs(ccframes) do self:RunFor(button, self:GetAttribute("setup_clicks")) end ]]) -- Now any compat frames that used the old method for button, enabled in pairs(self.ccframes) do -- Unwrap any existing enter/leave scripts self.header:UnwrapScript(button, "OnEnter") self.header:UnwrapScript(button, "OnLeave") self.header:WrapScript(button, "OnEnter", addon.header:GetAttribute("setup_onenter")) self.header:WrapScript(button, "OnLeave", addon.header:GetAttribute("setup_onleave")) -- Perform the setup of click bindings self.header:SetFrameRef("cliquesetup_button", button) self.header:Execute(self.header:GetAttribute("setup_clicks"), button) end -- Update the global button attributes self.globutton:Execute(self.globutton.setup) self.globutton:Execute(self.globutton.setbinds) end function addon:TalentGroupChanged() local currentProfile = self.db:GetCurrentProfile() local newProfile local currentSpec = GetSpecialization() if self.settings.specswap and currentSpec then local settingsKey = string.format("spec%d_profileKey", currentSpec) if self.settings[settingsKey] then newProfile = self.settings[settingsKey] end if newProfile ~= currentProfile and type(newProfile) == "string" then self.db:SetProfile(newProfile) end end self:FireMessage("BINDINGS_CHANGED") end function addon:PlayerEnteringWorld() self:FireMessage("BINDINGS_CHANGED") end function addon:UpdateCombatWatch() if self.settings.fastooc then if not self.registeredUnitFlags then self:RegisterEvent("UNIT_FLAGS", "CheckPartyCombat") self.registeredUnitFlags = true end else self:UnregisterEvent("UNIT_FLAGS") self.registeredUnitFlags = false end end function addon:UpdateBlacklist() local bits = { "blacklist = table.wipe(blacklist)", } for frame, value in pairs(self.settings.blacklist) do if not not value then bits[#bits + 1] = string.format("blacklist[%q] = true", frame) end end addon.header:Execute(table.concat(bits, ";\n")) addon:UpdateRegisteredClicks() end function addon:EnteringCombat() -- If there are no 'ooc' bindings, then no need to re-apply if not self.has_ooc then return end -- Check to see if we're already in combat, so we don't re-apply if not self.header:GetAttribute("inCombat") then -- Apply attributes, indicating we need the 'combat' set self.header:SetAttribute("inCombat", true) self.globutton:SetAttribute("inCombat", true) addon:ApplyAttributes() end end function addon:LeavingCombat() -- Process any frames in the registration queue for idx, button in ipairs(self.regqueue) do self:RegisterFrame(button) end if next(self.regqueue) then table.wipe(self.regqueue) end -- Process any frames in the unregistration queue for idx, button in ipairs(self.unregqueue) do self:UnregisterFrame(button) end if next(self.regqueue) then table.wipe(self.regqueue) end -- Process any frames in the clickregister queue for idx, button in ipairs(self.regclickqueue) do self:UpdateRegisteredClicks(button) end if next(self.regclickqueue) then table.wipe(self.regclickqueue) end -- Only apply attributes if we have an 'ooc' binding set if self.has_ooc then if self.partyincombat then self.partyincombat = false end -- Clear previously set attributes self:ClearAttributes() -- Apply attributes, indicating we want the 'ooc' set self.header:SetAttribute("inCombat", false) self.globutton:SetAttribute("inCombat", false) self:ApplyAttributes() end end function addon:CheckPartyCombat(event, unit) if InCombatLockdown() or not unit then return end if not self.has_ooc then -- No change required if no ooc bindings return end if self.settings.fastooc then if UnitInParty(unit) or UnitInRaid(unit) then if UnitAffectingCombat(unit) == 1 then -- Trigger pre-combat switch for fastooc self.partyincombat = true self.combattrigger = UnitGUID(unit) self.header:SetAttribute("inCombat", true) self.globutton:SetAttribute("inCombat", true) addon:ApplyAttributes() elseif self.partyincombat then -- The unit is out of combat, so try to clear our flag if self.combattrigger == UnitGUID(unit) then self.partyincombat = false self.header:SetAttribute("inCombat", false) self.globutton:SetAttribute("inCombat", false) addon:ApplyAttributes() end end end end end -- This function returns whether or not a frame is blacklisted in the current -- users settings function addon:IsFrameBlacklisted(frame) local name = frame if type(frame) == "table" then name = frame.GetName and frame:GetName() end return self.settings.blacklist[name] end -- Update both registered clicks, and ensure that mousewheel events are enabled -- on the frame. function addon:UpdateRegisteredClicks(button) if InCombatLockdown() then table.insert(self.regclickqueue, button) return end local direction = self.settings.downclick and "AnyDown" or "AnyUp" -- Short version that only updates clicks for one frame if button and not self:IsFrameBlacklisted(button) then button:RegisterForClicks(direction) button:EnableMouseWheel(true) return end for button in pairs(self.ccframes) do if not self:IsFrameBlacklisted(button) then button:RegisterForClicks(direction) button:EnableMouseWheel(true) end end for name, button in pairs(self.hccframes) do if not self:IsFrameBlacklisted(button) then button:RegisterForClicks(direction) button:EnableMouseWheel(true) end end end -- Handler function for message indicating that a change as occurred -- with the configured bindings. This is the only place that the -- bindings should be re-computed. If this handler is called during -- combat than execution should be deferred until the user exits -- combat. function addon:BINDINGS_CHANGED() if InCombatLockdown() then self:Defer("BINDINGS_CHANGED") return end -- Clear any existing attributes self:ClearAttributes() -- Very simple optimisation. If the player has no 'ooc' bindings -- set, then attributes can be applied once and then only updated -- when the bindings list is changed. local has_ooc = false for idx, entry in ipairs(self.bindings) do if entry.sets.ooc then has_ooc = true break end end self.has_ooc = has_ooc -- Update all click/binding attributes self:UpdateAttributes() -- Update the bindings list, if open CliqueConfig:UpdateList() -- Update the actual attributes on all frames self:ApplyAttributes() end function addon:BLACKLIST_CHANGED() if InCombatLockdown() then self:Defer("BLACKLIST_CHANGED") return end -- Clear attributes on all frames self:ClearAttributes() -- Actually update the blacklist accordingly local bits = { "blacklist = table.wipe(blacklist)", } for frame, value in pairs(self.settings.blacklist) do if not not value then bits[#bits + 1] = string.format("blacklist[%q] = true", frame) end end addon.header:Execute(table.concat(bits, ";\n")) -- Update the registered clicks, to catch any unblacklisted frames self:UpdateRegisteredClicks() -- Update the options panel self:UpdateOptionsPanel() -- Update the actual attributes on all frames self:ApplyAttributes() end local contains = function(arr, value) for idx, key in ipairs(arr) do if key == value then return true end end return false end SLASH_CLIQUE1 = "/clique" SlashCmdList["CLIQUE"] = function(msg, editbox) local profile = (msg or ""):match("^profile (.+)$") if profile then if InCombatLockdown() then addon:Printf("Cannot change profiles while in combat lockdown") else local availableProfiles = addon.db:GetProfiles({}) if contains(availableProfiles, profile) then addon:Printf("Switching to profile '%s'", profile) addon.db:SetProfile(profile) else addon:Printf("Cannot find profile '%s'", profile) end end else if SpellBookFrame:IsVisible() then CliqueConfig:ShowWithSpellBook() else ShowUIPanel(CliqueConfig) end end end