Quantcast
--[[-------------------------------------------------------------------
--  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 w/conere 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
                local 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