view libs/ReBound-1.0/ReBound-1.0.lua @ 17:639282f3a0e0

More cleanup of main.lua, ReBound-1.0.lua
author Flick <flickerstreak@gmail.com>
date Fri, 23 Mar 2007 19:28:30 +0000
parents 2735edcf9ab7
children
line wrap: on
line source
--[[
Name: ReBound-1.0
Revision: $Rev: 3 $
Author: Flick
Website: 
Documentation: 
SVN: 
Description: A library to assist with click-binding
License: MIT
Dependencies: AceLibrary, AceEvent-2.0, AceLocale-2.2, AceOO-2.0, AceConsole-2.0
]]


local version_major, version_minor = "ReBound-1.0", "$Rev: 3 $"

if not AceLibrary then error(version_major .. " requires AceLibrary.") end
if not AceLibrary:IsNewVersion(version_major, version_minor) then return end
for _, lib in pairs({ "AceEvent-2.0", "AceLocale-2.2", "AceOO-2.0", "AceConsole-2.0" }) do
  if not AceLibrary:HasInstance(lib) then error(version_major .. " requires "..lib) end
end

local L = AceLibrary("AceLocale-2.2"):new("ReBound")

-- localization
L:RegisterTranslations( "enUS", function()
  return {
    ["none"] = true,
    ["Click"] = true,
    ["Right-click"] = true,
    ["Shift-click"] = true,
    ["Shift-right-click"] = true,
    ["Right Click"] = true,
    ["to select for binding"] = true,
    ["to select for alternate (right-click) binding"] = true,
    ["to clear binding"] = true,
    ["to clear alternate (right-click) binding"] = true,
    ["Press a key to assign binding"] = true,
    ["is now unbound"] = true,
  }
end )



local kbValidate = AceLibrary("AceConsole-2.0").keybindingValidateFunc

local colorGreen = "|cff00ff00"
local colorOrange = "|cffffcc00"
local colorOff = "|r"

local mouseButtonConvert = {
  LeftButton = "BUTTON1",
  RightButton = "BUTTON2",
  MiddleButton = "BUTTON3",
  Button4 = "BUTTON4",
  Button5 = "BUTTON5"
}

-- TODO: localize
local keybindAbbreviations = {
  [KEY_BACKSPACE]      = "BkSp",
  [KEY_BUTTON3]        = "M-3",
  [KEY_BUTTON4]        = "M-4",
  [KEY_BUTTON5]        = "M-5",
  [KEY_DOWN]           = "Down",
  [KEY_ESCAPE]         = "Esc",
  [KEY_INSERT]         = "Ins",
  [KEY_LEFT]           = "Left",
  [KEY_MOUSEWHEELDOWN] = "M-Down",
  [KEY_MOUSEWHEELUP]   = "M-Up",
  [KEY_NUMLOCK]        = "NumLk",
  [KEY_NUMPAD0]        = "Np0",
  [KEY_NUMPAD1]        = "Np1",
  [KEY_NUMPAD2]        = "Np2",
  [KEY_NUMPAD3]        = "Np3",
  [KEY_NUMPAD4]        = "Np4",
  [KEY_NUMPAD5]        = "Np5",
  [KEY_NUMPAD6]        = "Np6",
  [KEY_NUMPAD7]        = "Np7",
  [KEY_NUMPAD8]        = "Np8",
  [KEY_NUMPAD9]        = "Np9",
  [KEY_NUMPADDECIMAL]  = "Np.",
  [KEY_NUMPADDIVIDE]   = "Np/",
  [KEY_NUMPADMINUS]    = "Np-",
  [KEY_NUMPADMULTIPLY] = "Np*",
  [KEY_NUMPADPLUS]     = "Np+",
  [KEY_PAGEDOWN]       = "PgDn",
  [KEY_PAGEUP]         = "PgUp",
  [KEY_PRINTSCREEN]    = "PrScr",
  [KEY_RIGHT]          = "Right",
  [KEY_SCROLLLOCK]     = "ScrLk",
  [KEY_SPACE]          = "Sp",
  [KEY_UP]             = "Up",
}


local ReBound = AceLibrary("AceOO-2.0").Class("AceEvent-2.0")

--[[
  ReBound publishes the following events:

  -- temporary bindings (prior to SaveBindings() being called)
  REBOUND_BIND_TEMP   (id, key, targetFrameName, mouseButton)
  REBOUND_UNBIND_TEMP (id, key)

  -- permanent bindings (fired all at once when SaveBindings() is called)
  REBOUND_BIND        (id, key, targetFrameName, mouseButton)
  REBOUND_UNBIND      (id, key)

  These events are published in response to click actions ONLY. This means
  that if a key is unbound from a click-binding frame, and bound to some 
  other action, then REBOUND_UNBIND(id,key) will be fired.
]]


--[[
  Calls to new() which share ids will return an existing object with that id, similar
  to AceLocale-2.2.
]]
ReBound.instances = { }
local super_new = ReBound.new
function ReBound:new( id )
  local instances = self.instances
  instances[id] = instances[id] or super_new(self,id)
  return instances[id]
end


--[[
  Class object constructor

  arguments:
    id : the ID that will be provided in events. This can be absolutely 
         anything, but a string is recommended.
]]
local super_init = ReBound.super.prototype.init
function ReBound.prototype:init( id )
  super_init(self)

  self.id = id
  self.frames = { }
  self.pending = { }
  self.bindings = { }
end




--[[
  Arguments:
  key: A string representation of a key, suitable for passing to SetBinding.
  target: The frame with an OnClick handler to which the click-binding should be attached
  [button]: The mouse button to emulate. Default is "LeftButton".
  [silent]: boolean - whether to suppress messages about keys being unbound.

  Returns:
  nothing.

  Notes:
  This does not save the bindings.
]]
function ReBound.prototype:SetBinding( key, target, button )
  if not key then error("ReBound:SetBinding() requires a key argument.") end
  if not target then error("ReBound:SetBinding() requires a binding target argument") end
  if key and not kbValidate(key) then error("ReBound:SetBinding(): invalid key code "..tostring(key)) end

  button = button or "LeftButton"

  -- prevent setting a binding that's already set
  local current = { self:GetBinding(target,button) }
  for _, b in pairs(current) do
    if b == key then
      return
    end
  end

  -- clear the old binding for the key. This isn't strictly necessary, but it allows us to collect
  -- notification of the unbinding in one place (ClearBinding).
  self:ClearBinding( key, nil, nil, silent )

  -- clear the old binding for the target and button (silently)
  self:ClearBinding( nil, target, button, true )

  -- set the new binding
  SetBindingClick(key, target:GetName(), button)

  -- store the temporary binding as "pending" for later notification
  table.insert(self.pending, key)

  -- notify listeners, e.g. for displaying the setting
  self:TriggerEvent("REBOUND_BIND_TEMP", self.id, key, target:GetName(), button)
end


--[[
  Arguments:
  [key]: A string representation of a key, suitable for passing to SetBinding. This can be nil if target is specified.
  [target]: The frame with a click keybinding to search for a key. 
  [button]: The mouse button to emulate. Default is "LeftButton". Only used with [target].
  [silent]: if true, omits printout.

  Returns:
  nothing.

  Notes:
  If key is provided, then the binding for that key is cleared. If key is not provided and target is provided, then
  all the bindings attached to the click-binding for that target are cleared.

  This does NOT save the bindings. Call SaveBindings() to commit the bindings to disk.
]]
function ReBound.prototype:ClearBinding( key, target, button, silent ) 
  if not target and not key then error("ReBound:ClearBinding() requires a key or click-binding target argument") end
  button = button or "LeftButton"

  local keys = key and { key } or { self:GetBinding(target,button) }
  for _, k in ipairs(keys) do
    -- Print a notification message
    if k and not silent then
      local action = GetBindingAction(k)
      if action then
        local name = GetBindingText(action,"BINDING_NAME_")
        local keyTxt = GetBindingText(k,"KEY_")
        -- make click-bindings look prettier
        local f, b = name:match("CLICK (.+)\:(.+)")
        if f then
          name = f
          if b ~= "LeftButton" then
            if b == "RightButton" then b = L["Right Click"] end
            name = f .."-"..b
          end
        end
        if name and #name > 0 then
          UIErrorsFrame:AddMessage(name.." ("..colorGreen..keyTxt..colorOff..") "..L["is now unbound"].."!")
        end
      end
    end
    SetBinding(k,nil)
    table.insert(self.pending,k)
    self:TriggerEvent("REBOUND_UNBIND_TEMP", self.id, k)
  end
end


--[[
  Gets the keys currently click-bound to a frame.

  Arguments:
  target: target frame to query
  [button]: mouse button to emulate ("LeftButton", "RightButton")

  Returns:
  key1, key2, key3, etc, as strings.
]]
function ReBound.prototype:GetBinding( target, button )
  if not target then error("ReBound:GetBinding() requires a target frame argument") end
  button = button or "LeftButton"
  return GetBindingKey("CLICK "..target:GetName()..":"..button)
end


--[[
  Gets the localized, abbreviated key cap text for the primary binding on a target. 
  Abbreviations are more aggressive than the standard Blizzard abbreviation
  (which just shortens modifier keys)

  Arguments:
  target: target frame to query
  [abbrev]: boolean flag to abbreviate the result
  [button]: mouse button to emulate ("LeftButton", "RightButton")

  Returns:
  abbreviated, localized key label
]]
function ReBound.prototype:GetBindingText( target, abbrev, button )
  local key = self:GetBinding(target,button)
  local txt = key and GetBindingText(key, "KEY_", abbrev and 1) or ""

  if txt and abbrev then
    -- further abbreviate some key names
    txt = string.gsub(txt, "[^%-]+$", keybindAbbreviations)
    -- the above does not handle "num pad -"
    txt = string.gsub(txt, KEY_NUMPADMINUS, keybindAbbreviations)
  end
  return txt
end


--[[
  Publishes permanent binding notification events.
]]
local function PublishBindings(self)
  for _, key in ipairs(self.pending) do
    local action = GetBindingAction(key)
    local frame, button
    if action then
      frame, button = action:match("CLICK (.+)\:(.+)")
    end
    if frame == nil then
      self:TriggerEvent("REBOUND_UNBIND", self.id, key)
    else
      self:TriggerEvent("REBOUND_BIND", self.id, key, frame, button)
    end
  end
  self.pending = { }
end


--[[
  Saves the bindings using the current scheme. Also publishes events indicating that the
  bindings have been saved/cleared permanently.
]]
function ReBound.prototype:SaveBindings()
  SaveBindings(GetCurrentBindingSet()) -- will trigger an UPDATE_BINDINGS event.
  PublishBindings(self)
end


--[[
  Reverts the bindings to the ones previously saved. Also publishes events indicating that the 
  bindings have been reverted.
]]
function ReBound.prototype:RevertBindings()
  LoadBindings(GetCurrentBindingSet()) -- should trigger an UPDATE_BINDINGS event.
  PublishBindings(self)
end


--[[
  Clears all bindings associated with registered frames. This is useful, for example, when switching profiles
  and the keybinding data is stored in the profile.
]]
function ReBound.prototype:ClearRegisteredBindings()
  for f, _ in pairs(self.frames) do
    self:ClearBinding(nil,f,"LeftButton",true)
    self:ClearBinding(nil,f,"RightButton",true)
  end
end


--[[
  Registers a target frame by creating a click-binding frame and putting that frame in the list of 
  registered frames, which can then be all shown/hidden as one unit.

  Arguments:
  target = the frame whose OnClick handler should be the target of keybinding

  Returns:
  A clickbinder frame.
]]
function ReBound.prototype:Register( target )
  local f = self:CreateClickBindingFrame(target)
  self.frames[target] = f
  return f
end


--[[ 
  Unregisters a target frame by removing it from the internal list. Does nothing to the clickbinding frame.

  Arguments:
  target = the frame whose OnClick handler should no longer be managed. do NOT pass the clickbinding frame.

  Returns:
  nothing.
]]
function ReBound.prototype:Unregister( target )
  self.frames[target] = nil
end


--[[
  Unregisters all registered frames.
]]
function ReBound.prototype:UnregisterAll()
  self.frames = { }
end



--[[
  Shows all the registered click binding frames.
]]
function ReBound.prototype:ShowRegisteredFrames()
  if InCombatLockdown() then
    -- can't set bindings while in combat, so don't bother showing them
    UIErrorsFrame:AddMessage(ERR_NOT_IN_COMBAT)
  else
    for _, f in pairs(self.frames) do
      f:Show()
    end
  end
end


--[[
  Hides all the registered click binding frames.
]]
function ReBound.prototype:HideRegisteredFrames()
  -- because these frames aren't protected, there's no restriction 
  -- on hiding them while in combat.
  for _, f in pairs(self.frames) do
    f:Hide()
  end
end

-- click binding frame implementation functions
local function ShowTooltip1( self )
  local target = self:GetParent()

  GameTooltip:ClearLines()
  GameTooltip:SetOwner(self,"ANCHOR_TOPRIGHT")
  -- line 1: button name and current binding
  GameTooltip:AddDoubleLine(target:GetName(), colorGreen.."("..(self.ReBound:GetBinding(target,"LeftButton") or L["none"])..")"..colorOff)
  -- line 2: current right-click binding (if any)
  local binding2 = self.ReBound:GetBinding(target,"RightButton")
  if binding2 then
    GameTooltip:AddDoubleLine(L["Right-click"]..":", colorGreen.."("..binding2..")"..colorOff)
  end
  -- line 3: instructions
  GameTooltip:AddLine(colorGreen..L["Click"]..colorOff.." "..L["to select for binding"])
  GameTooltip:AddLine(colorGreen..L["Shift-click"]..colorOff.." "..L["to clear binding"])
  GameTooltip:AddLine("")
  GameTooltip:AddLine(colorOrange..L["Right-click"]..colorOff.." "..L["to select for alternate (right-click) binding"])
  GameTooltip:AddLine(colorOrange..L["Shift-right-click"]..colorOff.." "..L["to clear alternate (right-click) binding"])
  GameTooltip:Show()
end

local function ShowTooltip2( self )
  if GameTooltip:IsOwned(self) then
    local target = self:GetParent()
    GameTooltip:ClearLines()
    GameTooltip:SetOwner(self)
    local clickSuffix = self.selectedButton == "RightButton" and (" ("..L["Right-click"]..")") or ""
    -- line 1: button name and binding to be set
    GameTooltip:AddDoubleLine(target:GetName()..clickSuffix, colorGreen.."("..(self.ReBound:GetBinding(target,self.selectedButton) or L["none"])..")"..colorOff)
    -- line 2: instructions
    GameTooltip:AddLine(colorGreen..L["Press a key to assign binding"]..colorOff)
    GameTooltip:Show()
  end
end

local function OnClick( self, button )
  if button == "LeftButton" or button == "RightButton" then
    if IsShiftKeyDown() then
      self.ReBound:ClearBinding( nil, self:GetParent(), button )
      self.selectedButton = nil
      self:EnableKeyboard(false)
      ShowTooltip1(self)
    else
      self.selectedButton = button
      self:EnableKeyboard(true)
      ShowTooltip2(self)
    end
  elseif self.selectedButton then
    self.ReBound:SetBinding( mouseButtonConvert[button], self:GetParent(), self.selectedButton )
    self.selectedButton = nil
    self:EnableKeyboard(false)
    ShowTooltip1(self)
  end
end

local function OnEnter( self )
  -- clear current binding button
  self.selectedButton = nil
  -- show tooltip 1
  ShowTooltip1(self)
end

local function OnLeave( self )
  -- disable keyboard input, if it was enabled
  self:EnableKeyboard(false)
  -- hide tooltip
  if GameTooltip:IsOwned(self) then
    GameTooltip:Hide()
  end
end

local function OnKeyDown( self, key )
  if key == nil or key == "UNKNOWN" or key == "SHIFT" or key == "CTRL" or key == "ALT" then 
    return
  end
  if IsShiftKeyDown()   then key = "SHIFT-"..key end
  if IsControlKeyDown() then key = "CTRL-"..key end
  if IsAltKeyDown()     then key = "ALT-"..key end

  if key ~= "ESCAPE" then
    self.ReBound:SetBinding( key, self:GetParent(), self.selectedButton )
  end

  self:EnableKeyboard(false)
  self.selectedButton = nil
  ShowTooltip1(self)
end

--[[
  Creates a click-binding frame attached to the target frame, which can be used for point-and-click keybind assignments. The
  frame is initially hidden by default. It is not registered with ReBound for automatic show/hide: use Register() for that.

  Arguments:
  target - the frame whose OnClick handler should be the target of keybinding

  Returns:
  A clickbinder frame.
]]
function ReBound.prototype:CreateClickBindingFrame( target )
  local f = CreateFrame("Button", nil, target)
  f.ReBound = self
  f:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square")
  f:SetToplevel(1)
  f:SetFrameStrata("DIALOG")
  f:RegisterForClicks("AnyUp")
  f:SetScript("OnClick",   OnClick)
  f:SetScript("OnEnter",   OnEnter)
  f:SetScript("OnLeave",   OnLeave)
  f:SetScript("OnKeyDown", OnKeyDown)
  f:SetAllPoints(target)
  f:Hide()
  return f
end



-- library setup

local function activate( self, oldLib, oldDeactivate )
  -- copy the list of active instances
  self.instances = { }
  if oldLib and oldLib.instances then
    for k,v in pairs(oldLib.instances) do
      self.instances[k] = v
    end
  end
  
  if oldDeactivate then
    oldDeactivate(oldLib)
  end
end

AceLibrary:Register(ReBound, version_major, version_minor, activate)
ReBound = nil