flickerstreak@10: --[[ flickerstreak@10: Name: ReBound-1.0 flickerstreak@12: Revision: $Rev: 2 $ flickerstreak@10: Author: Flick flickerstreak@10: Website: flickerstreak@10: Documentation: flickerstreak@10: SVN: flickerstreak@12: Description: A library to assist with click-binding flickerstreak@10: License: MIT flickerstreak@12: Dependencies: AceLibrary, AceEvent-2.0, AceLocale-2.2, AceOO-2.0 flickerstreak@10: ]] flickerstreak@10: flickerstreak@10: flickerstreak@12: local version_major, version_minor = "ReBound-1.0", "$Rev: 2 $" flickerstreak@10: flickerstreak@10: if not AceLibrary then error(version_major .. " requires AceLibrary.") end flickerstreak@10: if not AceLibrary:IsNewVersion(version_major, version_minor) then return end flickerstreak@10: if not AceLibrary:HasInstance("AceEvent-2.0") then error(version_major .. " requires AceEvent-2.0.") end flickerstreak@10: if not AceLibrary:HasInstance("AceLocale-2.2") then error(version_major .. " requires AceLocale-2.2.") end flickerstreak@12: if not AceLibrary:HasInstance("AceOO-2.0") then error(version_major .. " requires AceOO-2.0.") end flickerstreak@10: flickerstreak@10: local L = AceLibrary("AceLocale-2.2"):new("ReBound") flickerstreak@10: flickerstreak@10: local colorGreen = "|cff00ff00" flickerstreak@10: local colorOff = "|r" flickerstreak@10: flickerstreak@10: local mouseButtonConvert = { flickerstreak@12: LeftButton = "BUTTON1", flickerstreak@12: RightButton = "BUTTON2", flickerstreak@10: MiddleButton = "BUTTON3", flickerstreak@10: Button4 = "BUTTON4", flickerstreak@10: Button5 = "BUTTON5" flickerstreak@10: } flickerstreak@10: flickerstreak@12: --local BINDING_PENDING = { } -- a constant flickerstreak@12: flickerstreak@10: -- localization flickerstreak@10: L:RegisterTranslations( "enUS", function() flickerstreak@10: return { flickerstreak@10: ["none"] = true, flickerstreak@10: ["Right-click"] = true, flickerstreak@12: ["Right Click"] = true, flickerstreak@10: ["Click to select for binding"] = true, flickerstreak@10: ["Shift-click to clear binding"] = true, flickerstreak@10: ["Press a key to assign binding"] = true, flickerstreak@10: ["is now unbound"] = true, flickerstreak@10: } flickerstreak@10: end ) flickerstreak@10: flickerstreak@10: flickerstreak@12: local ReBound = AceLibrary("AceOO-2.0").Class("AceEvent-2.0") flickerstreak@10: flickerstreak@12: --[[ flickerstreak@12: ReBound publishes the following events: flickerstreak@10: flickerstreak@12: -- temporary bindings (prior to SaveBindings() being called) flickerstreak@12: REBOUND_BIND_TEMP (id, key, targetFrameName, mouseButton) flickerstreak@12: REBOUND_UNBIND_TEMP (id, key) flickerstreak@12: flickerstreak@12: -- permanent bindings (fired all at once when SaveBindings() is called) flickerstreak@12: REBOUND_BIND (id, key, targetFrameName, mouseButton) flickerstreak@12: REBOUND_UNBIND (id, key) flickerstreak@12: flickerstreak@12: These events are published in response to click actions ONLY. This means flickerstreak@12: that if a key is unbound from a click-binding frame, and bound to some flickerstreak@12: other action, then REBOUND_UNBIND(id,key) will be fired. flickerstreak@12: ]] flickerstreak@12: flickerstreak@12: flickerstreak@12: --[[ flickerstreak@12: Calls to new() which share ids will return an existing object with that id, similar flickerstreak@12: to AceLocale-2.2. flickerstreak@12: ]] flickerstreak@12: local super_new = ReBound.new flickerstreak@12: function ReBound:new( id ) flickerstreak@12: self.instances = self.instances or { } flickerstreak@12: if self.instances[id] then flickerstreak@12: return self.instances[id] flickerstreak@12: else flickerstreak@12: local i = super_new(self,id) flickerstreak@12: self.instances[id] = i flickerstreak@12: return i flickerstreak@12: end flickerstreak@12: end flickerstreak@12: flickerstreak@12: flickerstreak@12: --[[ flickerstreak@12: Class object constructor flickerstreak@12: flickerstreak@12: arguments: flickerstreak@12: id : the ID that will be provided in events. This can be absolutely flickerstreak@12: anything, but a string is recommended. flickerstreak@12: ]] flickerstreak@12: local super_init = ReBound.prototype.init flickerstreak@12: function ReBound.prototype:init( id ) flickerstreak@12: super_init(self) flickerstreak@12: flickerstreak@12: self.id = id flickerstreak@12: self.frames = { } flickerstreak@12: self.pending = { } flickerstreak@12: self.bindings = { } flickerstreak@12: end flickerstreak@12: flickerstreak@12: flickerstreak@10: flickerstreak@10: flickerstreak@10: --[[ flickerstreak@10: Arguments: flickerstreak@10: key: A string representation of a key, suitable for passing to SetBinding. flickerstreak@12: target: The frame with an OnClick handler to which the click-binding should be attached flickerstreak@10: [button]: The mouse button to emulate. Default is "LeftButton". flickerstreak@10: flickerstreak@10: Returns: flickerstreak@10: nothing. flickerstreak@10: flickerstreak@10: Notes: flickerstreak@10: This does not save the bindings. flickerstreak@10: ]] flickerstreak@12: function ReBound.prototype:SetBinding( key, target, button ) flickerstreak@12: if not key then error("ReBound:SetBinding() requires a key argument.") end flickerstreak@12: if not target then error("ReBound:SetBinding() requires a binding target argument") end flickerstreak@10: button = button or "LeftButton" flickerstreak@10: flickerstreak@10: -- prevent setting a binding that's already set flickerstreak@10: local current = { self:GetBinding(target,button) } flickerstreak@10: for _, b in pairs(current) do flickerstreak@10: if b == key then flickerstreak@10: return flickerstreak@10: end flickerstreak@10: end flickerstreak@10: flickerstreak@10: -- clear the old binding for the key. This isn't strictly necessary, but it allows us to collect flickerstreak@10: -- notification of the unbinding in one place (ClearBinding). flickerstreak@10: self:ClearBinding( key ) flickerstreak@10: flickerstreak@10: -- clear the old binding for the target and button (silently) flickerstreak@10: self:ClearBinding( nil, target, button, true ) flickerstreak@10: flickerstreak@10: -- set the new binding flickerstreak@10: SetBindingClick(key, target:GetName(), button) flickerstreak@10: flickerstreak@12: -- store the temporary binding as "pending" for later notification flickerstreak@12: table.insert(self.pending, key) flickerstreak@12: flickerstreak@12: -- notify listeners, e.g. for displaying the setting flickerstreak@12: self:TriggerEvent("REBOUND_BIND_TEMP", self.id, key, target:GetName(), button) flickerstreak@10: end flickerstreak@10: flickerstreak@10: flickerstreak@10: --[[ flickerstreak@10: Arguments: flickerstreak@10: [key]: A string representation of a key, suitable for passing to SetBinding. This can be nil if target is specified. flickerstreak@10: [target]: The frame with a click keybinding to search for a key. flickerstreak@10: [button]: The mouse button to emulate. Default is "LeftButton". Only used with [target]. flickerstreak@10: [silent]: if true, omits printout. flickerstreak@10: flickerstreak@10: Returns: flickerstreak@10: nothing. flickerstreak@10: flickerstreak@10: Notes: flickerstreak@10: If key is provided, then the binding for that key is cleared. If key is not provided and target is provided, then flickerstreak@10: all the bindings attached to the click-binding for that target are cleared. flickerstreak@10: flickerstreak@10: This does NOT save the bindings. Call SaveBindings() to commit the bindings to disk. flickerstreak@10: ]] flickerstreak@12: function ReBound.prototype:ClearBinding( key, target, button, silent ) flickerstreak@12: if not target and not key then error("ReBound:ClearBinding() requires a key or click-binding target argument") end flickerstreak@10: button = button or "LeftButton" flickerstreak@10: flickerstreak@10: local keys = key and { key } or { self:GetBinding(target,button) } flickerstreak@10: for _, k in ipairs(keys) do flickerstreak@10: -- Print a notification message flickerstreak@10: if k and not silent then flickerstreak@10: local action = GetBindingAction(k) flickerstreak@10: if action then flickerstreak@10: local name = GetBindingText(action,"BINDING_NAME_") flickerstreak@10: local keyTxt = GetBindingText(k,"KEY_") flickerstreak@10: -- make click-bindings look prettier flickerstreak@10: local f, b = name:match("CLICK (.+)\:(.+)") flickerstreak@10: if f then flickerstreak@10: name = f flickerstreak@10: if b ~= "LeftButton" then flickerstreak@12: if b == "RightButton" then b = L["Right Click"] end flickerstreak@10: name = f .."-"..b flickerstreak@10: end flickerstreak@10: end flickerstreak@10: if name and #name > 0 then flickerstreak@10: UIErrorsFrame:AddMessage(name.." ("..colorGreen..keyTxt..colorOff..") "..L["is now unbound"].."!") flickerstreak@10: end flickerstreak@10: end flickerstreak@10: end flickerstreak@10: SetBinding(k,nil) flickerstreak@12: table.insert(self.pending,k) flickerstreak@12: self:TriggerEvent("REBOUND_UNBIND_TEMP", self.id, k) flickerstreak@10: end flickerstreak@10: end flickerstreak@10: flickerstreak@10: flickerstreak@10: --[[ flickerstreak@12: Gets the keys currently click-bound to a frame. flickerstreak@10: flickerstreak@10: Arguments: flickerstreak@10: target: target frame to query flickerstreak@10: [button]: mouse button to emulate ("LeftButton", "RightButton") flickerstreak@10: flickerstreak@10: Returns: flickerstreak@10: key1, key2, key3, etc, as strings. flickerstreak@10: ]] flickerstreak@12: function ReBound.prototype:GetBinding( target, button ) flickerstreak@12: if not target then error("ReBound:GetBinding() requires a target frame argument") end flickerstreak@10: button = button or "LeftButton" flickerstreak@10: return GetBindingKey("CLICK "..target:GetName()..":"..button) flickerstreak@10: end flickerstreak@10: flickerstreak@10: flickerstreak@10: --[[ flickerstreak@12: Publishes permanent binding notification events. flickerstreak@12: ]] flickerstreak@12: local function PublishBindings(self) flickerstreak@12: for _, key in ipairs(self.pending) do flickerstreak@12: local action = GetBindingAction(key) flickerstreak@12: local frame, button flickerstreak@12: if action then flickerstreak@12: frame, button = action:match("CLICK (.+)\:(.+)") flickerstreak@12: end flickerstreak@12: if frame == nil then flickerstreak@12: self:TriggerEvent("REBOUND_UNBIND", self.id, key) flickerstreak@12: else flickerstreak@12: self:TriggerEvent("REBOUND_BIND", self.id, key, frame, button) flickerstreak@12: end flickerstreak@12: end flickerstreak@12: self.pending = { } flickerstreak@12: end flickerstreak@12: flickerstreak@12: flickerstreak@12: --[[ flickerstreak@12: Saves the bindings using the current scheme. Also publishes events indicating that the flickerstreak@12: bindings have been saved/cleared permanently. flickerstreak@12: ]] flickerstreak@12: function ReBound.prototype:SaveBindings() flickerstreak@12: SaveBindings(GetCurrentBindingSet()) -- will trigger an UPDATE_BINDINGS event. flickerstreak@12: PublishBindings(self) flickerstreak@12: end flickerstreak@12: flickerstreak@12: flickerstreak@12: --[[ flickerstreak@12: Reverts the bindings to the ones previously saved. Also publishes events indicating that the flickerstreak@12: bindings have been reverted. flickerstreak@12: ]] flickerstreak@12: function ReBound.prototype:RevertBindings() flickerstreak@12: LoadBindings(GetCurrentBindingSet()) -- should trigger an UPDATE_BINDINGS event. flickerstreak@12: PublishBindings(self) flickerstreak@12: end flickerstreak@12: flickerstreak@12: flickerstreak@12: --[[ flickerstreak@12: Clears all bindings associated with registered frames. This is useful, for example, when switching profiles flickerstreak@12: and the keybinding data is stored in the profile. flickerstreak@12: ]] flickerstreak@12: function ReBound.prototype:ClearRegisteredBindings() flickerstreak@12: for f, _ in pairs(self.frames) do flickerstreak@12: self:ClearBinding(nil,f,"LeftButton",true) flickerstreak@12: self:ClearBinding(nil,f,"RightButton",true) flickerstreak@12: end flickerstreak@12: end flickerstreak@12: flickerstreak@12: flickerstreak@12: --[[ flickerstreak@10: Registers a target frame by creating a click-binding frame and putting that frame in the list of flickerstreak@10: registered frames, which can then be all shown/hidden as one unit. flickerstreak@10: flickerstreak@10: Arguments: flickerstreak@10: target = the frame whose OnClick handler should be the target of keybinding flickerstreak@10: flickerstreak@10: Returns: flickerstreak@10: A clickbinder frame. flickerstreak@10: ]] flickerstreak@12: function ReBound.prototype:Register( target ) flickerstreak@10: local f = self:CreateClickBindingFrame(target) flickerstreak@10: self.frames[target] = f flickerstreak@10: return f flickerstreak@10: end flickerstreak@10: flickerstreak@10: flickerstreak@10: --[[ flickerstreak@10: Unregisters a target frame by removing it from the internal list. Does nothing to the clickbinding frame. flickerstreak@10: flickerstreak@10: Arguments: flickerstreak@12: target = the frame whose OnClick handler should no longer be managed. do NOT pass the clickbinding frame. flickerstreak@10: flickerstreak@10: Returns: flickerstreak@10: nothing. flickerstreak@10: ]] flickerstreak@12: function ReBound.prototype:Unregister( target ) flickerstreak@10: self.frames[target] = nil flickerstreak@10: end flickerstreak@10: flickerstreak@10: flickerstreak@10: --[[ flickerstreak@12: Unregisters all registered frames. flickerstreak@12: ]] flickerstreak@12: function ReBound.prototype:UnregisterAll() flickerstreak@12: self.frames = { } flickerstreak@12: end flickerstreak@12: flickerstreak@12: flickerstreak@12: flickerstreak@12: --[[ flickerstreak@10: Shows all the registered click binding frames. flickerstreak@10: ]] flickerstreak@12: function ReBound.prototype:ShowFrames() flickerstreak@10: if InCombatLockdown() then flickerstreak@10: -- can't set bindings while in combat, so don't bother showing them flickerstreak@10: UIErrorsFrame:AddMessage(ERR_NOT_IN_COMBAT) flickerstreak@10: else flickerstreak@10: for _, f in pairs(self.frames) do flickerstreak@10: f:Show() flickerstreak@10: end flickerstreak@10: end flickerstreak@10: end flickerstreak@10: flickerstreak@10: flickerstreak@10: --[[ flickerstreak@10: Hides all the registered click binding frames. flickerstreak@10: ]] flickerstreak@12: function ReBound.prototype:HideFrames() flickerstreak@12: -- because these frames aren't protected, there's no restriction flickerstreak@12: -- on hiding them while in combat. flickerstreak@10: for _, f in pairs(self.frames) do flickerstreak@10: f:Hide() flickerstreak@10: end flickerstreak@10: end flickerstreak@10: flickerstreak@10: -- click binding frame implementation functions flickerstreak@10: local function ShowTooltip1( self ) flickerstreak@10: local target = self:GetParent() flickerstreak@10: flickerstreak@10: GameTooltip:ClearLines() flickerstreak@10: GameTooltip:SetOwner(self,"ANCHOR_TOPRIGHT") flickerstreak@10: -- line 1: button name and current binding flickerstreak@10: GameTooltip:AddDoubleLine(target:GetName(), colorGreen.."("..(self.ReBound:GetBinding(target,"LeftButton") or L["none"])..")"..colorOff) flickerstreak@10: -- line 2: current right-click binding (if any) flickerstreak@10: local binding2 = self.ReBound:GetBinding(target,"RightButton") flickerstreak@10: if binding2 then flickerstreak@10: GameTooltip:AddDoubleLine(L["Right-click"]..":", colorGreen.."("..binding2..")"..colorOff) flickerstreak@10: end flickerstreak@10: -- line 3: instructions flickerstreak@10: GameTooltip:AddLine(L["Click to select for binding"]) flickerstreak@10: GameTooltip:AddLine(L["Shift-click to clear binding"]) flickerstreak@10: GameTooltip:Show() flickerstreak@10: end flickerstreak@10: flickerstreak@10: local function ShowTooltip2( self ) flickerstreak@10: if GameTooltip:IsOwned(self) then flickerstreak@10: local target = self:GetParent() flickerstreak@10: GameTooltip:ClearLines() flickerstreak@10: GameTooltip:SetOwner(self) flickerstreak@10: local clickSuffix = self.selectedButton == "RightButton" and (" ("..L["Right-click"]..")") or "" flickerstreak@10: -- line 1: button name and binding to be set flickerstreak@10: GameTooltip:AddDoubleLine(target:GetName()..clickSuffix, colorGreen.."("..(self.ReBound:GetBinding(target,self.selectedButton) or L["none"])..")"..colorOff) flickerstreak@10: -- line 2: instructions flickerstreak@10: GameTooltip:AddLine(colorGreen..L["Press a key to assign binding"]..colorOff) flickerstreak@10: GameTooltip:Show() flickerstreak@10: end flickerstreak@10: end flickerstreak@10: flickerstreak@10: local function OnClick( self, button ) flickerstreak@10: if button == "LeftButton" or button == "RightButton" then flickerstreak@10: if IsShiftKeyDown() then flickerstreak@10: self.ReBound:ClearBinding( nil, self:GetParent(), button ) flickerstreak@10: self.selectedButton = nil flickerstreak@10: self:EnableKeyboard(false) flickerstreak@10: ShowTooltip1(self) flickerstreak@10: else flickerstreak@10: self.selectedButton = button flickerstreak@10: self:EnableKeyboard(true) flickerstreak@10: ShowTooltip2(self) flickerstreak@10: end flickerstreak@10: elseif self.selectedButton then flickerstreak@10: self.ReBound:SetBinding( mouseButtonConvert[button], self:GetParent(), self.selectedButton ) flickerstreak@10: self.selectedButton = nil flickerstreak@10: self:EnableKeyboard(false) flickerstreak@10: ShowTooltip1(self) flickerstreak@10: end flickerstreak@10: end flickerstreak@10: flickerstreak@10: local function OnEnter( self ) flickerstreak@10: -- clear current binding button flickerstreak@10: self.selectedButton = nil flickerstreak@10: -- show tooltip 1 flickerstreak@10: ShowTooltip1(self) flickerstreak@10: end flickerstreak@10: flickerstreak@10: local function OnLeave( self ) flickerstreak@10: -- disable keyboard input, if it was enabled flickerstreak@10: self:EnableKeyboard(false) flickerstreak@10: -- hide tooltip flickerstreak@10: if GameTooltip:IsOwned(self) then flickerstreak@10: GameTooltip:Hide() flickerstreak@10: end flickerstreak@10: end flickerstreak@10: flickerstreak@10: local function OnKeyDown( self, key ) flickerstreak@10: if key == nil or key == "UNKNOWN" or key == "SHIFT" or key == "CTRL" or key == "ALT" then flickerstreak@10: return flickerstreak@10: end flickerstreak@10: if IsShiftKeyDown() then key = "SHIFT-"..key end flickerstreak@10: if IsControlKeyDown() then key = "CTRL-"..key end flickerstreak@10: if IsAltKeyDown() then key = "ALT-"..key end flickerstreak@10: flickerstreak@10: if key ~= "ESCAPE" then flickerstreak@10: self.ReBound:SetBinding( key, self:GetParent(), self.selectedButton ) flickerstreak@10: end flickerstreak@10: flickerstreak@10: self:EnableKeyboard(false) flickerstreak@10: self.selectedButton = nil flickerstreak@10: ShowTooltip1(self) flickerstreak@10: end flickerstreak@10: flickerstreak@10: --[[ flickerstreak@10: Creates a click-binding frame attached to the target frame, which can be used for point-and-click keybind assignments. The flickerstreak@10: frame is initially hidden by default. It is not registered with ReBound for automatic show/hide: use Register() for that. flickerstreak@10: flickerstreak@10: Arguments: flickerstreak@10: target - the frame whose OnClick handler should be the target of keybinding flickerstreak@10: flickerstreak@10: Returns: flickerstreak@10: A clickbinder frame. flickerstreak@10: ]] flickerstreak@12: function ReBound.prototype:CreateClickBindingFrame( target ) flickerstreak@10: local f = CreateFrame("Button", nil, target) flickerstreak@10: f.ReBound = self flickerstreak@10: f:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square") flickerstreak@10: f:SetToplevel(1) flickerstreak@10: f:SetFrameStrata("DIALOG") flickerstreak@10: f:RegisterForClicks("AnyUp") flickerstreak@10: f:SetScript("OnClick", OnClick) flickerstreak@10: f:SetScript("OnEnter", OnEnter) flickerstreak@10: f:SetScript("OnLeave", OnLeave) flickerstreak@10: f:SetScript("OnKeyDown", OnKeyDown) flickerstreak@10: f:SetAllPoints(target) flickerstreak@10: f:Hide() flickerstreak@10: return f flickerstreak@10: end flickerstreak@10: flickerstreak@10: flickerstreak@10: flickerstreak@10: -- library setup flickerstreak@10: flickerstreak@10: local function activate( self, oldLib, oldDeactivate ) flickerstreak@12: -- copy the list of active instances flickerstreak@12: self.instances = { } flickerstreak@12: if oldLib and oldLib.instances then flickerstreak@12: for k,v in pairs(oldLib.instances) do flickerstreak@12: self.instances[k] = v flickerstreak@12: end flickerstreak@12: end flickerstreak@12: flickerstreak@10: if oldDeactivate then flickerstreak@10: oldDeactivate(oldLib) flickerstreak@10: end flickerstreak@10: end flickerstreak@10: flickerstreak@12: AceLibrary:Register(ReBound, version_major, version_minor, activate) flickerstreak@10: ReBound = nil