view classes/ReAction.lua @ 10:f3a7bfebc283

Version 0.33
author Flick <flickerstreak@gmail.com>
date Tue, 20 Mar 2007 21:37:38 +0000
parents c05fd3e18b4f
children 90bf38d48efd
line wrap: on
line source
--
-- ReAction is a base class for action button management. It provides a
-- framework for button setup, placement, recycling, keybinding, etc.
-- It does not define any layout or actual action functionality,
-- which is deferred to derived classes. 
--
-- ReAction implements the ReBar.IButton interface. It is designed to be used with ReBar
-- for grouping and laying out buttons.
--
-- Each instance of a ReAction-derived object is associated with a single action
-- button frame, which may or may not have a one-to-one mapping with actionID.
--
-- ReAction makes use of a configuration structure, which can (and should) be
-- extended by implementations. A single config structure is shared amongst all
-- ReAction class instances within a single ReBar group, so the structure should
-- contain sub-tables for any property that needs to be defined on a per-button
-- basis. Each button is passed a 'barIdx' parameter to be used as an index into
-- such tables.
--
-- The base config structure is as follows:
--
-- config = {
--   type = "ReAction", -- static string (used by ReBar)
--   subtype = "string", -- ReAction implementation identifier (index into ReAction.buttonTypes)
--   ids = { {paged list}, {paged list}, ... } -- indexed by self.barIdx
-- }
--


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


-- private constants
-- TODO: localize these key names with GetBindingText(KEY_)
local keybindAbbreviations = {
  ["Mouse Button "] = "M-",
  ["Spacebar"] = "Sp",
  ["Num Pad "] = "Num-",
  ["Page Up"]  = "PgUp",
  ["Page Down"] = "PgDn",
  [" Arrow"] = "",
}


------------------------
-- Interface Declarations
------------------------

-- The ActionType interface defines what the button does when it is clicked. At a
-- minimum it must do the equivalent of SetAttribute("type", ...) and handle events that
-- cause the action to change.
-- ReAction implementations must provide this interface (implicitly or explicitly).
local IActionType = AceOO.Interface {
 SetID         = "function", -- SetID(id, [page]) optional argument indicates page #: omitting indicates default. self.config.idx[barIdx] must be updated.
 GetID         = "function", -- id = GetID([page]) optional argument indicates page #: omitting indicates current page
 IsActionEmpty = "function", -- bool = IsActionEmpty()
 SetupAction   = "function", -- one-time setup
 UpdateAction  = "function", -- general action properties should be refreshed
 PickupAction  = "function", -- pick up the action on the button and put on the cursor
 PlaceAction   = "function", -- place the action on the cursor
 UpdateTooltip = "function", -- update the tooltip with the action's info
}

-- The Display interface defines the "look and feel" of the action button. It should define the
-- actual widgets and widget layout and provide methods to update the display.
-- ReAction implementations must provide this interface (implicitly or explicitly).
-- Note that ReAction implementations may also require additional display interfaces to be supported.
--
-- Also note: the class 'new' method must take *only* the primary button ID as an argument.
local IDisplay = AceOO.Interface {
  SetupDisplay       = "function", -- SetupDisplay(buttonName), one-time setup
  UpdateDisplay      = "function", -- UpdateDisplay(), general display state should be refreshed
  TempShow           = "function", -- TempShow(visible), calls to this can be nested so keep track.
  GetActionFrame     = "function", -- f = GetActionFrame(), return a frame derived from SecureActionButtonTemplate (note: this is inherited unimplemented from ReBar.IButton)
  GetBaseButtonSize  = "function", -- sz = GetBaseButtonSize(), return size in pixels of the nominal button (square)
  DisplayID          = "function", -- DisplayID(show), true/false to show/hide the action ID (or equivalent)
  DisplayHotkey      = "function", -- DisplayHotkey(keyText, button), set the hotkey display text. 2nd argument specifies which button the hotkey is for, default is "LeftButton".
}


----------------------------
-- ReAction class definition
----------------------------

ReAction = AceOO.Class("AceEvent-2.0", ReBar.IButton, IActionType, IDisplay)
ReAction.virtual = true
ReAction.IActionType = IActionType
ReAction.IDisplay = IDisplay


-----------------------
-- Static class members
-----------------------

ReAction.recycler = CreateFrame("Frame",nil,UIParent)
ReAction.recycler:SetAllPoints(UIParent)
ReAction.recycler:Hide()

ReAction.buttonTypes = { }



-----------------------
-- Static class methods
-----------------------

function ReAction:AddButtonType( name, class, maxIDs )
  self.buttonTypes[name] = { subtype = class, maxIDs = maxIDs }
end

function ReAction:GetButtonType( name )
  if name then
    local t = self.buttonTypes[name]
    if t then
      return t.subtype, t.maxIDs
    end
  end
end

function ReAction:GetButtonTypeList()
  local list = { }
  for name, _ in pairs(self.buttonTypes) do
    table.insert(list,name)
  end
  return list
end

function ReAction:GetAvailableID( subtype, hint )
  local class, maxIDs = self:GetButtonType(subtype)

  -- store the list of action buttons in use in the button type class factory
  if class._idTbl == nil then class._idTbl = { } end
  local t = class._idTbl
  local id = nil
  
  -- find lowest ID not in use
  for i = 1, maxIDs do
    if t[i] == nil or t[i].inUse == false then
      id = i
      break
    end
  end

  if id == nil then return nil end  -- all action ids are in use

  -- if a hint is given, see if that one is free instead, as long as it's < maxIDs
  if hint and hint > 0 and hint <= maxIDs and (t[hint] == nil or t[hint].inUse == false) then
    id = hint
  end

  if t[id] == nil then 
    t[id] = { }
  end
  t[id].inUse = true

  return id, t[id]
end

function ReAction:Acquire(config, barIdx, pages, buttonsPerPage)
  local btnType = self:GetButtonType(config.subtype)
  if not btnType then
    error("ReAction: Unknown button type specified.")
  end
  pages = pages or 1
  
  local ids = { }
  local primary = nil

  for i = 1, pages do
    local hint = config.ids[barIdx] and config.ids[barIdx][i]
    if hint == nil and i > 1 and ids[i-1] then
      hint = ids[i-1] + (buttonsPerPage or 0)
    end
    local id, p = self:GetAvailableID(config.subtype, hint)
    if id == nil then
      break
    end
    primary = primary or p
    if id then
      ids[i] = id
    end
  end

  if primary then
    if not primary.button then
      primary.button = btnType:new(ids[1])
    end
    if primary.button then
      config.ids[barIdx] = ids
      primary.button:Configure(config,barIdx)
    end
  end

  return primary and primary.button
end

function ReAction:Release( b )
  if b then
    for i = 1, #b.config.ids[b.barIdx] do
      local id = b:GetID(i)
      if id then
        b.class._idTbl[id].inUse = false
      end
    end
    b.config = nil
    b:Recycle()
  end
end

function ReAction:ShowAllIds()
  for _, t in pairs(self.buttonTypes) do
    if t.subtype._idTbl then
      for _, tbl in pairs(t.subtype._idTbl) do
        if tbl.button then tbl.button:DisplayID(true) end
      end
    end
  end
  self.showIDs_ = true
end

function ReAction:HideAllIds()
  for _, t in pairs(self.buttonTypes) do
    if t.subtype._idTbl then
      for _, tbl in pairs(t.subtype._idTbl) do
        if tbl.button then tbl.button:DisplayID(false) end
      end
    end
  end
  self.showIDs_ = false
end


----------------------
-- Instance methods
----------------------

-- constructor

function ReAction.prototype:init()
  ReAction.super.prototype.init(self)
end


-- ReBar.IButton interface

function ReAction.prototype:BarUnlocked()
  self:TempShow(true)
  self:DisplayID(true)
end

function ReAction.prototype:BarLocked()
  self:TempShow(false)
  self:DisplayID(false)
end

function ReAction.prototype:PlaceButton(parent, point, x, y, sz)
  local b = self:GetActionFrame()
  local baseSize = self:GetBaseButtonSize()
  local scale = baseSize and baseSize ~= 0 and ((sz or 1) / baseSize) or 1
  if scale == 0 then scale = 1 end
  b:ClearAllPoints()
  b:SetParent(parent)
  b:SetScale(scale)
  b:SetPoint(point,x/scale,y/scale)
end

function ReAction.prototype:SetPages( n )
  n = tonumber(n)
  local ids = self.config.ids[self.barIdx]
  if n and n >= 1 then
    -- note that as long as n >= 1 then id[1] will never be modified, which is what we want
    -- because then the button frame ID would be out of sync with the static button name
    while #ids < n do
      local id = ReAction:GetAvailableID(self.config.subtype, ids[#ids] + #self.config.ids)
      if id == nil then
        break
      end
      self:SetID( id, #ids + 1 )
      table.insert(ids, id)
    end
    while #ids > n do
      local id = table.remove(ids)
      self:SetID( nil, #ids + 1 )
      self.class._idTbl[id].inUse = false
    end
  end
end

-- Event handlers

function ReAction.prototype:UPDATE_BINDINGS()
  self:DisplayHotkey(self:GetKeyBindingText("LeftButton", true),"LeftButton")
  self:DisplayHotkey(self:GetKeyBindingText("RightButton",true),"RightButton")
end


-- Internal functions

function ReAction.prototype:Recycle()
  self:UnregisterAllEvents()
  
  -- tuck the frame away
  local b = self:GetActionFrame()
  b:SetParent(ReAction.recycler)
  b:ClearAllPoints()
  b:SetPoint("TOPLEFT",0,0)
  b:Hide()
end

function ReAction.prototype:Configure( config, barIdx )
  self.config   = config
  self.barIdx   = barIdx

  local ids = config.ids[barIdx]
  self:SetID(ids[1]) -- default id
  for i = 1, #ids do
    self:SetID(ids[i], i) -- paged ids
  end
  self:UpdateAction()
  self:UpdateDisplay()
  self:DisplayHotkey(self:GetKeyBindingText(nil, true))

	self:RegisterEvent("UPDATE_BINDINGS")
end

function ReAction.prototype:SetKeyBinding( k, mouseBtn )
  if k == nil or kbValidate(k) then
    local current = self:GetKeyBinding(mouseBtn)
    if current then
      SetBinding(current,nil)
    end
    if k then
      SetBindingClick(k, self:GetActionFrame():GetName(), mouseBtn or "LeftButton")
    end
  end
end

function ReAction.prototype:GetKeyBinding( mouseBtn )
  return GetBindingKey("CLICK "..self:GetActionFrame():GetName()..":"..(mouseBtn or "LeftButton"))
end

function ReAction.prototype:GetKeyBindingText( mouseBtn, abbrev )
  local key = self:GetKeyBinding(mouseBtn)
  local txt = key and GetBindingText(key, "KEY_", abbrev and 1) or ""

  if txt and abbrev then
    -- further abbreviate some key names
    for pat, rep in pairs(keybindAbbreviations) do
      txt = string.gsub(txt,pat,rep)
    end
  end
  return txt
end

function ReAction.prototype:SetTooltip()
  GameTooltip_SetDefaultAnchor(GameTooltip, self:GetActionFrame())
  self:UpdateTooltip()
end

function ReAction.prototype:ClearTooltip()
  tooltipTime = nil
  GameTooltip:Hide()
end