diff modules/Action.lua @ 109:410d036c43b2

- reorganize modularity file structure (part 1)
author Flick <flickerstreak@gmail.com>
date Thu, 08 Jan 2009 00:57:27 +0000
parents modules/ReAction_Action/ReAction_Action.lua@b2fb8f7dc780
children 77bb68eb402b
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/Action.lua	Thu Jan 08 00:57:27 2009 +0000
@@ -0,0 +1,1157 @@
+--[[
+  ReAction Action button module.
+
+  The button module implements standard action button functionality by wrapping Blizzard's 
+  ActionBarButtonTemplate frame and associated functions.
+
+  It also provides action remapping support for multiple pages and possessed targets
+  (Mind Control, Eyes of the Beast, Karazhan Chess event, various quests, etc).
+--]]
+
+-- local imports
+local ReAction = ReAction
+local L = ReAction.L
+local _G = _G
+local CreateFrame = CreateFrame
+local format = string.format
+local wipe = wipe
+
+ReAction:UpdateRevision("$Revision$")
+
+-- libraries
+local KB = LibStub("LibKeyBound-1.0")
+local LBF -- initialized later
+
+-- module declaration
+local moduleID = "Action"
+local module = ReAction:NewModule( moduleID )
+
+-- Class declarations
+local Button = { }
+local Handle = { }
+local PropHandler = { }
+
+-- Event handlers
+function module:OnInitialize()
+  self.db = ReAction.db:RegisterNamespace( moduleID,
+    { 
+      profile = {
+        bars = { },
+      }
+    }
+  )
+  self.handles = setmetatable({ }, weak)
+
+  ReAction:RegisterBarOptionGenerator(self, "GetBarOptions")
+
+  ReAction.RegisterCallback(self, "OnCreateBar")
+  ReAction.RegisterCallback(self, "OnRefreshBar")
+  ReAction.RegisterCallback(self, "OnDestroyBar")
+  ReAction.RegisterCallback(self, "OnEraseBar")
+  ReAction.RegisterCallback(self, "OnRenameBar")
+  ReAction.RegisterCallback(self, "OnConfigModeChanged")
+
+  LBF = LibStub("LibButtonFacade",true)
+
+  KB.RegisterCallback(self, "LIBKEYBOUND_ENABLED")
+  KB.RegisterCallback(self, "LIBKEYBOUND_DISABLED")
+  KB.RegisterCallback(self, "LIBKEYBOUND_MODE_COLOR_CHANGED","LIBKEYBOUND_ENABLED")
+end
+
+function module:OnEnable()
+  ReAction:RegisterBarType(L["Action Bar"], 
+    { 
+      type = moduleID,
+      defaultButtonSize = 36,
+      defaultBarRows = 1,
+      defaultBarCols = 12,
+      defaultBarSpacing = 3
+    }, true)
+  ReAction:GetModule("State"):RegisterStateProperty("page", nil, PropHandler.GetOptions(), PropHandler)
+end
+
+function module:OnDisable()
+  ReAction:UnregisterBarType(L["Action Bar"])
+  ReAction:GetModule("State"):UnregisterStateProperty("page")
+end
+
+function module:OnCreateBar(event, bar, name)
+  if bar.config.type == moduleID then
+    local profile = self.db.profile
+    if profile.bars[name] == nil then
+      profile.bars[name] = {
+        buttons = { }
+      }
+    end
+    if self.handles[bar] == nil then
+      self.handles[bar] = Handle:New(bar, profile.bars[name])
+    end
+  end
+end
+
+function module:OnRefreshBar(event, bar, name)
+  if self.handles[bar] then
+    self.handles[bar]:Refresh()
+  end
+end
+
+function module:OnDestroyBar(event, bar, name)
+  if self.handles[bar] then
+    self.handles[bar]:Destroy()
+    self.handles[bar] = nil
+  end
+end
+
+function module:OnEraseBar(event, bar, name)
+  self.db.profile.bars[name] = nil
+end
+
+function module:OnRenameBar(event, bar, oldname, newname)
+  b = self.db.profile.bars
+  b[newname], b[oldname] = b[oldname], nil
+end
+
+function module:OnConfigModeChanged(event, mode)
+  for _, h in pairs(self.handles) do
+    h:SetConfigMode(mode)
+  end
+end
+
+function module:LIBKEYBOUND_ENABLED(evt)
+  for _, h in pairs(self.handles) do
+    h:ShowGrid(true)
+    h:SetKeybindMode(true)
+  end
+end
+
+function module:LIBKEYBOUND_DISABLED(evt)
+  for _, h in pairs(self.handles) do
+    h:ShowGrid(false)
+    h:SetKeybindMode(false)
+  end
+end
+
+
+---- Interface ----
+function module:GetBarOptions(bar)
+  local h = self.handles[bar]
+  if h then
+    return h:GetOptions()
+  end
+end
+
+
+---- Bar Handle ----
+
+do
+  local options = {
+    hideEmpty = {
+      name = L["Hide Empty Buttons"],
+      order = 1,
+      type = "toggle",
+      width = "double",
+      get  = "GetHideEmpty",
+      set  = "SetHideEmpty",
+    },
+    lockButtons = {
+      name = L["Lock Buttons"],
+      desc = L["Prevents picking up/dragging actions.|nNOTE: This setting is overridden by the global setting in Blizzard's Action Buttons tab"],
+      order = 2,
+      type = "toggle",
+      disabled = "LockButtonsDisabled",
+      get = "GetLockButtons",
+      set = "SetLockButtons",
+    },
+    lockOnlyCombat = {
+      name = L["Only in Combat"],
+      desc = L["Only lock the buttons when in combat"],
+      order = 3,
+      type = "toggle",
+      disabled = "LockButtonsCombatDisabled",
+      get = "GetLockButtonsCombat",
+      set = "SetLockButtonsCombat",
+    },
+    pages = {
+      name  = L["# Pages"],
+      desc  = L["Use the Dynamic State tab to specify page transitions"],
+      order = 4,
+      type  = "range",
+      min   = 1,
+      max   = 10,
+      step  = 1,
+      get   = "GetNumPages",
+      set   = "SetNumPages",
+    },
+    mindcontrol = {
+      name = L["Mind Control Support"],
+      desc = L["When possessing a target (e.g. via Mind Control), map the first 12 buttons of this bar to the possessed target's actions."],
+      order = 5,
+      type = "toggle",
+      width = "double",
+      set = "SetMindControl",
+      get = "GetMindControl",
+    },
+    actions = {
+      name   = L["Edit Action IDs"],
+      order  = 6,
+      type   = "group",
+      inline = true,
+      args   = {
+        method = {
+          name   = L["Assign"],
+          order  = 1,
+          type   = "select",
+          width  = "double",
+          values = { [0] = L["Choose Method..."],
+                     [1] = L["Individually"],
+                     [2] = L["All at Once"], },
+          get    = "GetActionEditMethod",
+          set    = "SetActionEditMethod",
+        },
+        rowSelect = {
+          name   = L["Row"],
+          desc   = L["Rows are numbered top to bottom"],
+          order  = 2,
+          type   = "select",
+          width  = "half",
+          hidden = "IsButtonSelectHidden",
+          values = "GetRowList",
+          get    = "GetSelectedRow",
+          set    = "SetSelectedRow",
+        },
+        colSelect = {
+          name   = L["Col"],
+          desc   = L["Columns are numbered left to right"],
+          order  = 3,
+          type   = "select",
+          width  = "half",
+          hidden = "IsButtonSelectHidden",
+          values = "GetColumnList",
+          get    = "GetSelectedColumn",
+          set    = "SetSelectedColumn",
+        },
+        pageSelect = {
+          name   = L["Page"],
+          order  = 4,
+          type   = "select",
+          width  = "half",
+          hidden = "IsPageSelectHidden",
+          values = "GetPageList",
+          get    = "GetSelectedPage",
+          set    = "SetSelectedPage",
+        },
+        single = {
+          name   = L["Action ID"],
+          usage  = L["Specify ID 1-120"],
+          order  = 5,
+          type   = "input",
+          width  = "half",
+          hidden = "IsButtonSelectHidden",
+          get    = "GetActionID",
+          set    = "SetActionID",
+          validate = "ValidateActionID",
+        },
+        multi = {
+          name   = L["ID List"],
+          usage  = L["Specify a comma-separated list of IDs for each button in the bar (in order). Separate multiple pages with semicolons (;)"],
+          order  = 6,
+          type   = "input",
+          multiline = true,
+          width  = "double",
+          hidden = "IsMultiIDHidden",
+          get    = "GetMultiID",
+          set    = "SetMultiID",
+          validate = "ValidateMultiID",
+        },
+      },
+    },
+  }
+
+  local weak  = { __mode="k" }
+  local meta = { __index = Handle }
+
+  function Handle:New( bar, config )
+    local self = setmetatable(
+      {
+        bar = bar,
+        config = config,
+        btns = { }
+      }, 
+      meta)
+    
+    if self.config.buttons == nil then
+      self.config.buttons = { }
+    end
+    self:Refresh()
+    self:SetKeybindMode(ReAction:GetKeybindMode())
+    return self
+  end
+
+  function Handle:Refresh()
+    local r, c = self.bar:GetButtonGrid()
+    local n = r*c
+    local btnCfg = self.config.buttons
+    if n ~= #self.btns then
+      for i = 1, n do
+        if btnCfg[i] == nil then
+          btnCfg[i] = {}
+        end
+        if self.btns[i] == nil then
+          local b = Button:New(self, i, btnCfg[i], self.config)
+          self.btns[i] = b
+          self.bar:AddButton(i,b)
+        end
+      end
+      for i = n+1, #self.btns do
+        if self.btns[i] then
+          self.bar:RemoveButton(self.btns[i])
+          self.btns[i]:Destroy()
+          self.btns[i] = nil
+          btnCfg[i] = nil
+        end
+      end
+    end
+    local f = self.bar:GetFrame()
+    for _, b in ipairs(self.btns) do
+      b:Refresh()
+    end
+    f:SetAttribute("mindcontrol",self.config.mindcontrol)
+    f:Execute(
+      [[
+      doMindControl = self:GetAttribute("mindcontrol")
+      control:ChildUpdate()
+      ]])
+
+    f:SetAttribute("_onstate-mindcontrol",
+      -- function _onstate-mindcontrol(self, stateid, newstate)
+      [[
+        control:ChildUpdate()
+      ]])
+    RegisterStateDriver(f, "mindcontrol", "[bonusbar:5] mc; none")
+    self:UpdateButtonLock()
+  end
+
+  function Handle:Destroy()
+    for _,b in pairs(self.btns) do
+      if b then
+        b:Destroy()
+      end
+    end
+  end
+
+  function Handle:SetConfigMode(mode)
+    for _, b in pairs(self.btns) do
+      b:ShowGrid(mode)
+      b:ShowActionIDLabel(mode)
+    end
+  end
+
+  function Handle:ShowGrid(show)
+    for _, b in pairs(self.btns) do
+      b:ShowGrid(show)
+    end
+  end
+
+  function Handle:UpdateButtonLock()
+    local f = self.bar:GetFrame()
+    f:SetAttribute("lockbuttons",self.config.lockButtons)
+    f:SetAttribute("lockbuttonscombat",self.config.lockButtonsCombat)
+    f:Execute(
+      [[
+        lockButtons = self:GetAttribute("lockbuttons")
+        lockButtonsCombat = self:GetAttribute("lockbuttonscombat")
+      ]])
+  end
+
+  function Handle:SetKeybindMode(mode)
+    for _, b in pairs(self.btns) do
+      if mode then
+        -- set the border for all buttons to the keybind-enable color
+      	b.border:SetVertexColor(KB:GetColorKeyBoundMode())
+        b.border:Show()
+      elseif IsEquippedAction(b:GetActionID()) then
+        b.border:SetVertexColor(0, 1.0, 0, 0.35) -- from ActionButton.lua
+      else
+        b.border:Hide()
+      end
+    end
+  end
+
+  function Handle:GetLastButton()
+    return self.btns[#self.btns]
+  end
+
+    -- options handlers
+  function Handle:GetOptions()
+    return {
+      type = "group",
+      name = L["Action Buttons"],
+      handler = self,
+      args = options
+    }
+  end
+
+  function Handle:SetHideEmpty(info, value)
+    if value ~= self.config.hideEmpty then
+      self.config.hideEmpty = value
+      self:ShowGrid(not value)
+    end
+  end
+
+  function Handle:GetHideEmpty()
+    return self.config.hideEmpty
+  end
+
+  function Handle:GetLockButtons()
+    return LOCK_ACTIONBAR == "1" or self.config.lockButtons
+  end
+
+  function Handle:SetLockButtons(info, value)
+    self.config.lockButtons = value
+    self:UpdateButtonLock()
+  end
+
+  function Handle:LockButtonsDisabled()
+    return LOCK_ACTIONBAR == "1"
+  end
+
+  function Handle:GetLockButtonsCombat()
+    return self.config.lockButtonsCombat
+  end
+
+  function Handle:SetLockButtonsCombat(info, value)
+    self.config.lockButtonsCombat = value
+    self:UpdateButtonLock()
+  end
+
+  function Handle:LockButtonsCombatDisabled()
+    return LOCK_ACTIONBAR == "1" or not self.config.lockButtons
+  end
+
+  function Handle:GetNumPages()
+    return self.config.nPages
+  end
+
+  function Handle:SetNumPages(info, value)
+    self.config.nPages = value
+    self:Refresh()
+  end
+
+  function Handle:GetMindControl()
+    return self.config.mindcontrol
+  end
+
+  function Handle:SetMindControl(info, value)
+    self.config.mindcontrol = value
+    self:Refresh()
+  end
+
+  function Handle:GetActionEditMethod()
+    return self.editMethod or 0
+  end
+
+  function Handle:SetActionEditMethod(info, value)
+    self.editMethod = value
+  end
+
+  function Handle:IsButtonSelectHidden()
+    return self.editMethod ~= 1
+  end
+
+  function Handle:GetRowList()
+    local r,c = self.bar:GetButtonGrid()
+    if self.rowList == nil or #self.rowList ~= r then
+      local list = { }
+      for i = 1, r do
+        table.insert(list,i)
+      end
+      self.rowList = list
+    end
+    return self.rowList
+  end
+
+  function Handle:GetSelectedRow()
+    local r, c = self.bar:GetButtonGrid()
+    local row = self.selectedRow or 1
+    if row > r then
+      row = 1
+    end
+    self.selectedRow = row
+    return row
+  end
+
+  function Handle:SetSelectedRow(info, value)
+    self.selectedRow = value
+  end
+
+  function Handle:GetColumnList()
+    local r,c = self.bar:GetButtonGrid()
+    if self.columnList == nil or #self.columnList ~= c then
+      local list = { }
+      for i = 1, c do
+        table.insert(list,i)
+      end
+      self.columnList = list
+    end
+    return self.columnList
+  end
+
+  function Handle:GetSelectedColumn()
+    local r, c = self.bar:GetButtonGrid()
+    local col = self.selectedColumn or 1
+    if col > c then
+      col = 1
+    end
+    self.selectedColumn = col
+    return col
+  end
+
+  function Handle:SetSelectedColumn(info, value)
+    self.selectedColumn = value
+  end
+
+  function Handle:IsPageSelectHidden()
+    return self.editMethod ~= 1 or (self.config.nPages or 1) < 2
+  end
+
+  function Handle:GetPageList()
+    local n = self.config.nPages or 1
+    if self.pageList == nil or #self.pageList ~= n then
+      local p = { }
+      for i = 1, n do
+        table.insert(p,i)
+      end
+      self.pageList = p
+    end
+    return self.pageList
+  end
+
+  function Handle:GetSelectedPage()
+    local p = self.selectedPage or 1
+    if p > (self.config.nPages or 1) then
+      p = 1
+    end
+    self.selectedPage = p
+    return p
+  end
+
+  function Handle:SetSelectedPage(info, value)
+    self.selectedPage = value
+  end
+
+  function Handle:GetActionID()
+    local row = self.selectedRow or 1
+    local col = self.selectedColumn or 1
+    local r, c = self.bar:GetButtonGrid()
+    local n = (row-1) * c + col
+    local btn = self.btns[n]
+    if btn then
+      return tostring(btn:GetActionID(self.selectedPage or 1))
+    end
+  end
+
+  function Handle:SetActionID(info, value)
+    local row = self.selectedRow or 1
+    local col = self.selectedColumn or 1
+    local r, c = self.bar:GetButtonGrid()
+    local n = (row-1) * c + col
+    local btn = self.btns[n]
+    if btn then
+      btn:SetActionID(tonumber(value), self.selectedPage or 1)
+    end
+  end
+
+  function Handle:ValidateActionID(info, value)
+    value = tonumber(value)
+    if value == nil or value < 1 or value > 120 then
+      return L["Specify ID 1-120"]
+    end
+    return true
+  end
+
+  function Handle:IsMultiIDHidden()
+    return self.editMethod ~= 2
+  end
+
+  function Handle:GetMultiID()
+    local p = { }
+    for i = 1, self.config.nPages or 1 do
+      local b = { }
+      for _, btn in ipairs(self.btns) do
+        table.insert(b, btn:GetActionID(i))
+      end
+      table.insert(p, table.concat(b,","))
+    end
+    return table.concat(p,";\n")
+  end
+
+
+  local function ParseMultiID(nBtns, nPages, s)
+    if s:match("[^%d%s,;]") then
+      ReAction:Print("items other than digits, spaces, commas, and semicolons in string",s)
+      return nil
+    end
+    local p = { }
+    for list in s:gmatch("[^;]+") do
+      local pattern = ("^%s?$"):format(("%s*(%d+)%s*,"):rep(nBtns))
+      local ids = { list:match(pattern) }
+      if #ids ~= nBtns then
+        ReAction:Print("found",#ids,"buttons instead of",nBtns)
+        return nil
+      end
+      table.insert(p,ids)
+    end
+    if #p ~= nPages then
+      ReAction:Print("found",#p,"pages instead of",nPages)
+      return nil
+    end
+    return p
+  end
+
+  function Handle:SetMultiID(info, value)
+    local p = ParseMultiID(#self.btns, self.config.nPages or 1, value)
+    for page, b in ipairs(p) do
+      for button, id in ipairs(b) do
+        self.btns[button]:SetActionID(id, page)
+      end
+    end
+  end
+
+  function Handle:ValidateMultiID(info, value)
+    local bad = L["Invalid action ID list string"]
+    if value == nil or ParseMultiID(#self.btns, self.config.nPages or 1, value) == nil then
+      return bad
+    end
+    return true
+  end
+end
+
+
+------ State property options ------
+do
+  local pageOptions = {
+    page = {
+      name     = L["Show Page #"],
+      order    = 11,
+      type     = "select",
+      width    = "half",
+      disabled = "IsPageDisabled",
+      hidden   = "IsPageHidden",
+      values   = "GetPageValues",
+      set      = "SetProp",
+      get      = "GetPage",
+    },
+  }
+
+  local function GetBarConfig(bar)
+    return module.db.profile.bars[bar:GetName()]
+  end
+
+  function PropHandler.GetOptions()
+    return pageOptions
+  end
+
+  function PropHandler:IsPageDisabled()
+    local c = GetBarConfig(self.bar)
+    local n = c and c.nPages or 1
+    return not (n > 1)
+  end
+
+  function PropHandler:IsPageHidden()
+    return not GetBarConfig(self.bar)
+  end
+
+  function PropHandler:GetPageValues()
+    if not self._pagevalues then
+      self._pagevalues = { }
+    end
+    local c = GetBarConfig(self.bar)
+    if c then
+      local n = c.nPages
+        -- cache the results
+      if self._npages ~= n then
+        self._npages = n
+        wipe(self._pagevalues)
+        for i = 1, n do
+          self._pagevalues["page"..i] = i
+        end
+      end
+    end
+    return self._pagevalues
+  end
+
+  function PropHandler:GetPage(info)
+    return self:GetProp(info) or 1
+  end
+
+end
+
+------ ActionID allocation ------
+-- this needs to be high performance when requesting new IDs,
+-- or certain controls will become sluggish. However, the new-request
+-- infrastructure can be built lazily the first time that a new request
+-- comes in (which will only happen at user config time: at static startup
+-- config time all actionIDs should already have been assigned and stored
+-- in the config file)
+
+local IDAlloc
+do
+  local n = 120
+  
+  IDAlloc = setmetatable({ wrap = 1, freecount = n }, {__index = function() return 0 end})
+
+  function IDAlloc:Acquire(id, hint)
+    id = tonumber(id)
+    hint = tonumber(hint)
+    if id and (id < 1 or id > n) then
+      id = nil
+    end
+    if hint and (hint < 1 or hint > n) then
+      hint = nil
+    end
+    if id == nil then
+      -- get a free ID
+      if hint and self[hint] == 0 then
+        -- use the hint if it's free
+        id = hint
+      elseif self.freecount > 0 then
+        -- if neither the id nor the hint are defined or free, but
+        -- the list is known to have free IDs, then start searching
+        -- at the hint for a free one
+        for i = hint or 1, n do
+          if self[i] == 0 then
+            id = i
+            break
+          end
+        end
+        -- self.wrap the search
+        if id == nil and hint and hint > 1 then
+          for i = 1, hint - 1 do
+            if self[i] == 0 then
+              id = i
+              break
+            end
+          end
+        end
+      end
+      if id == nil then
+        -- if there are no free IDs, start wrapping at 1
+        id = self.wrap
+        self.wrap = id + 1
+        if self.wrap > n then
+          self.wrap = 1
+        end
+      end
+    end
+    if self[id] == 0 then
+      self.freecount = self.freecount - 1
+    end
+    self[id] = self[id] + 1
+    return id
+  end
+
+  function IDAlloc:Release(id)
+    id = tonumber(id)
+    if id and (id >= 1 or id <= n) then
+      self[id] = self[id] - 1
+      if self[id] == 0 then
+        self.freecount = self.freecount + 1
+        self.wrap = 1
+      end
+    end
+  end
+end
+
+------ Button class ------
+
+do
+  local frameRecycler = { }
+  local trash = CreateFrame("Frame")
+  local OnUpdate, KBAttach, GetActionName, GetHotkey, SetKey, FreeKey, ClearBindings, GetBindings
+  do
+    local ATTACK_BUTTON_FLASH_TIME = ATTACK_BUTTON_FLASH_TIME
+    local IsActionInRange = IsActionInRange
+
+    local buttonLookup = setmetatable({},{__mode="kv"})
+
+    function OnUpdate(frame, elapsed)
+      -- note: This function taints frame.flashtime and frame.rangeTimer. Both of these
+      --       are only read by ActionButton_OnUpdate (which this function replaces). In
+      --       all other places they're just written, so it doesn't taint any secure code.
+      if frame.flashing == 1 then
+        frame.flashtime = frame.flashtime - elapsed
+        if frame.flashtime <= 0 then
+          local overtime = -frame.flashtime
+          if overtime >= ATTACK_BUTTON_FLASH_TIME then
+            overtime = 0
+          end
+          frame.flashtime = ATTACK_BUTTON_FLASH_TIME - overtime
+
+          local flashTexture = frame.flash
+          if flashTexture:IsShown() then
+            flashTexture:Hide()
+          else
+            flashTexture:Show()
+          end
+        end
+      end
+      
+      if frame.rangeTimer then
+        frame.rangeTimer = frame.rangeTimer - elapsed;
+
+        if frame.rangeTimer <= 0 then
+          if IsActionInRange(frame.action) == 0 then
+            frame.icon:SetVertexColor(1.0,0.1,0.1)
+          else
+            ActionButton_UpdateUsable(frame)
+          end
+          frame.rangeTimer = 0.1
+        end
+      end
+    end
+
+    -- Use KeyBound-1.0 for binding, but use Override bindings instead of
+    -- regular bindings to support multiple profile use. This is a little
+    -- weird with the KeyBound dialog box (which has per-char selector as well
+    -- as an OK/Cancel box) but it's the least amount of effort to implement.
+    function GetActionName(f)
+      local b = buttonLookup[f]
+      if b then
+        return format("%s:%s", b.bar:GetName(), b.idx)
+      end
+    end
+
+    function GetHotkey(f)
+      local b = buttonLookup[f]
+      if b then
+        return KB:ToShortKey(b:GetConfig().hotkey)
+      end
+    end
+
+    function SetKey(f, key)
+      local b = buttonLookup[f]
+      if b then
+        local c = b:GetConfig()
+        if c.hotkey then
+          SetOverrideBinding(f, false, c.hotkey, nil)
+        end
+        if key then
+          SetOverrideBindingClick(f, false, key, f:GetName(), nil)
+        end
+        c.hotkey = key
+        b:DisplayHotkey(GetHotkey(f))
+      end
+    end
+
+    function FreeKey(f, key)
+      local b = buttonLookup[f]
+      if b then
+        local c = b:GetConfig()
+        if c.hotkey == key then
+          local action = f:GetActionName()
+          SetOverrideBinding(f, false, c.hotkey, nil)
+          c.hotkey = nil
+          b:DisplayHotkey(nil)
+          return action
+        end
+      end
+      return ReAction:FreeOverrideHotkey(key)
+    end
+
+    function ClearBindings(f)
+      SetKey(f, nil)
+    end
+
+    function GetBindings(f)
+      local b = buttonLookup[f]
+      if b then
+        return b:GetConfig().hotkey
+      end
+    end
+
+    function KBAttach( button )
+      local f = button:GetFrame()
+      f.GetActionName = GetActionName
+      f.GetHotkey     = GetHotkey
+      f.SetKey        = SetKey
+      f.FreeKey       = FreeKey
+      f.ClearBindings = ClearBindings
+      f.GetBindings   = GetBindings
+      buttonLookup[f] = button
+      f:SetKey(button:GetConfig().hotkey)
+      ReAction:RegisterKeybindFrame(f)
+      if ReAction:GetKeybindMode() then
+      	button.border:SetVertexColor(KB:GetColorKeyBoundMode())
+        button.border:Show()
+      end
+    end
+  end
+
+  local meta = {__index = Button}
+
+  function Button:New( handle, idx, config, barConfig )
+    local bar = handle.bar
+
+    -- create new self
+    self = setmetatable( 
+      { 
+        bar = bar,
+        idx = idx,
+        config = config,
+        barConfig = barConfig,
+      }, meta )
+
+    local name = config.name or ("ReAction_%s_%s_%d"):format(bar:GetName(),moduleID,idx)
+    self.name = name
+    config.name = name
+    local lastButton = handle:GetLastButton()
+    config.actionID = IDAlloc:Acquire(config.actionID, lastButton and lastButton.config.actionID) -- gets a free one if none configured
+    self.nPages = 1
+    
+    -- have to recycle frames with the same name: CreateFrame() doesn't overwrite
+    -- existing globals. Can't set to nil in the global because it's then tainted.
+    local parent = bar:GetFrame()
+    local f = frameRecycler[name]
+    if f then
+      f:SetParent(parent)
+    else
+      f = CreateFrame("CheckButton", name, parent, "ActionBarButtonTemplate")
+      -- ditch the old hotkey text because it's tied in ActionButton_Update() to the
+      -- standard binding. We use override bindings.
+      local hotkey = _G[name.."HotKey"]
+      hotkey:SetParent(trash)
+      hotkey = f:CreateFontString(nil, "ARTWORK", "NumberFontNormalSmallGray")
+      hotkey:SetWidth(36)
+      hotkey:SetHeight(18)
+      hotkey:SetJustifyH("RIGHT")
+      hotkey:SetJustifyV("TOP")
+      hotkey:SetPoint("TOPLEFT",f,"TOPLEFT",-2,-2)
+      f.hotkey = hotkey
+      f.icon = _G[name.."Icon"]
+      f.flash = _G[name.."Flash"]
+      f:SetScript("OnUpdate",OnUpdate)
+    end
+
+    self.hotkey = f.hotkey
+    self.border = _G[name.."Border"]
+
+    f:SetAttribute("action", config.actionID)
+    -- install mind control actions for all buttons just for simplicity
+    if self.idx <= 12 then
+      f:SetAttribute("*action-mc", 120 + self.idx)
+    end
+    
+    -- set a _childupdate handler, called within the header's context
+    f:SetAttribute("_childupdate", 
+      -- function _childupdate(self, snippetid, message)
+      [[
+        local action = "action"
+        if doMindControl and GetBonusBarOffset() == 5 then
+          action = "*action-mc"
+        elseif page and state and page[state] then
+          action = "*action-"..page[state]
+        end
+        local value = self:GetAttribute(action)
+        self:SetAttribute("action",value)
+      ]])
+
+    -- install drag wrappers to lock buttons 
+    bar:GetFrame():WrapScript(f, "OnDragStart",
+      -- OnDragStart(self, button, kind, value, ...)
+      [[
+        if lockButtons and (PlayerInCombat() or not lockButtonsCombat) and not IsModifiedClick("PICKUPACTION") then
+          return "clear"
+        end
+      ]])
+
+    self.frame = f
+
+
+    -- initialize the hide state
+    f:SetAttribute("showgrid",0)
+    self:ShowGrid(not barConfig.hideEmpty)
+    if ReAction:GetConfigMode() then
+      self:ShowGrid(true)
+    end
+
+    -- show the ID label if applicable
+    self:ShowActionIDLabel(ReAction:GetConfigMode())
+
+    -- attach the keybinder
+    KBAttach(self)
+
+    -- attach to skinner
+    bar:SkinButton(self,
+      {
+        HotKey = self.hotkey,
+      }
+    )
+
+    self:Refresh()
+    return self
+  end
+
+  function Button:Destroy()
+    local f = self.frame
+    f:UnregisterAllEvents()
+    f:Hide()
+    f:SetParent(UIParent)
+    f:ClearAllPoints()
+    if self.name then
+      frameRecycler[self.name] = f
+    end
+    if self.config.actionID then
+      IDAlloc:Release(self.config.actionID)
+    end
+    if self.config.pageactions then
+      for _, id in ipairs(self.config.pageactions) do
+        IDAlloc:Release(id)
+      end
+    end
+    self.frame = nil
+    self.config = nil
+    self.bar = nil
+  end
+
+  function Button:Refresh()
+    local f = self.frame
+    self.bar:PlaceButton(self, 36, 36)
+    self:RefreshPages()
+  end
+
+  function Button:GetFrame()
+    return self.frame
+  end
+
+  function Button:GetName()
+    return self.name
+  end
+
+  function Button:GetConfig()
+    return self.config
+  end
+
+  function Button:GetActionID(page)
+    if page == nil then
+      -- get the effective ID
+      return self.frame.action -- kept up-to-date by Blizzard's ActionButton_CalculateAction()
+    else
+      if page == 1 then
+        return self.config.actionID
+      else
+        return self.config.pageactions and self.config.pageactions[page] or self.config.actionID
+      end
+    end
+  end
+
+  function Button:SetActionID( id, page )
+    id = tonumber(id)
+    page = tonumber(page)
+    if id == nil or id < 1 or id > 120 then
+      error("Button:SetActionID - invalid action ID")
+    end
+    if page and page ~= 1 then
+      if not self.config.pageactions then
+        self.config.pageactions = { }
+      end
+      if self.config.pageactions[page] then
+        IDAlloc:Release(self.config.pageactions[page])
+      end
+      self.config.pageactions[page] = id
+      IDAlloc:Acquire(self.config.pageactions[page])
+      self.frame:SetAttribute(("*action-page%d"):format(page),id)
+    else
+      IDAlloc:Release(self.config.actionID)
+      self.config.actionID = id
+      IDAlloc:Acquire(self.config.actionID)
+      self.frame:SetAttribute("action",id)
+      if self.config.pageactions then
+        self.config.pageactions[1] = id
+        self.frame:SetAttribute("*action-page1",id)
+      end
+    end
+  end
+
+  function Button:RefreshPages( force )
+    local nPages = self.barConfig.nPages
+    if nPages and (nPages ~= self.nPages or force) then
+      local f = self:GetFrame()
+      local c = self.config.pageactions
+      if nPages > 1 and not c then
+        c = { }
+        self.config.pageactions = c
+      end
+      for i = 1, nPages do
+        if i > 1 then
+          c[i] = IDAlloc:Acquire(c[i], self.config.actionID + (i-1)*self.bar:GetNumButtons())
+        else
+          c[i] = self.config.actionID  -- page 1 is the same as the base actionID
+        end
+        f:SetAttribute(("*action-page%d"):format(i),c[i])
+      end
+      for i = nPages+1, #c do
+        IDAlloc:Release(c[i])
+        c[i] = nil
+        f:SetAttribute(("*action-page%d"):format(i),nil)
+      end
+      self.nPages = nPages
+    end
+  end
+
+  function Button:ShowGrid( show )
+    if not InCombatLockdown() then
+      local f = self.frame
+      local count = f:GetAttribute("showgrid")
+      if show then
+        count = count + 1
+      else
+        count = count - 1
+      end
+      if count < 0 then
+        count = 0
+      end
+      f:SetAttribute("showgrid",count)
+
+      if count >= 1 and not f:GetAttribute("statehidden") then
+        if LBF then
+          LBF:SetNormalVertexColor(self.frame, 1.0, 1.0, 1.0, 0.5)
+        else
+          self.frame:GetNormalTexture():SetVertexColor(1.0, 1.0, 1.0, 0.5);
+        end
+        f:Show()
+      elseif count < 1 and not HasAction(self:GetActionID()) then
+        f:Hide()
+      end
+    end
+  end
+
+  function Button:ShowActionIDLabel( show )
+    local f = self:GetFrame()
+    if show then
+      local id = self:GetActionID()
+      if not f.actionIDLabel then
+        local label = f:CreateFontString(nil,"OVERLAY","GameFontNormalLarge")
+        label:SetAllPoints()
+        label:SetJustifyH("CENTER")
+        label:SetShadowColor(0,0,0,1)
+        label:SetShadowOffset(2,-2)
+        f.actionIDLabel = label -- store the label with the frame for recycling
+
+        f:HookScript("OnAttributeChanged", 
+          function(frame, attr, value)
+            if label:IsVisible() and attr:match("action") then
+              label:SetText(tostring(frame.action))
+            end
+          end)
+      end
+      f.actionIDLabel:SetText(tostring(id))
+      f.actionIDLabel:Show()
+    elseif f.actionIDLabel then
+      f.actionIDLabel:Hide()
+    end
+  end
+
+  function Button:DisplayHotkey( key )
+    self.hotkey:SetText(key or "")
+  end
+end