changeset 87:3499ac7c3a9b

Implemented paged actions and mind control actions, and config menus to suit. There's still some sort of bug in the actionID-selection routine, it doesn't always auto-select the IDs that I think it's going to if you resize a bar several times (especially in the presence of multiple pages).
author Flick <flickerstreak@gmail.com>
date Sat, 28 Jun 2008 00:54:21 +0000
parents f32e2375e39b
children fc83b3f5b322
files locale/enUS.lua modules/ReAction_Action/ReAction_Action.lua
diffstat 2 files changed, 594 insertions(+), 102 deletions(-) [+]
line wrap: on
line diff
--- a/locale/enUS.lua	Fri Jun 27 18:40:40 2008 +0000
+++ b/locale/enUS.lua	Sat Jun 28 00:54:21 2008 +0000
@@ -77,7 +77,6 @@
 "Properties",
 "Set the properties for the bar when in this state",
 "Hide Bar",
-"Show Page #",
 "(none)",
 "Override Keybinds",
 "Set this state to maintain its own set of keybinds which override the defaults when active",
@@ -123,7 +122,26 @@
 "Action Bar",
 "Action Bars",
 "Hide Empty Buttons",
-"Hide buttons when empty. This option is not supported for multi-state bars",
+"# Pages",
+"Use the Dynamic State tab to specify page transitions",
+"Edit Action IDs",
+"Assign",
+"Choose Method...",
+"Individually",
+"All at Once",
+"Row",
+"Rows are numbered top to bottom",
+"Col",
+"Columns are numbered left to right",
+"Page",
+"Action ID",
+"Specify ID 1-120",
+"ID List",
+"Specify a comma-separated list of IDs for each button in the bar (in order). Separate multiple pages with semicolons (;)",
+"Invalid action ID list string",
+"Mind Control Support",
+"When possessing a target (e.g. via Mind Control), map the first 12 buttons of this bar to the possessed target's actions. Select the 'Mind Control' option for the rule type to enable.",
+"Show Page #",
 "Action Buttons",
 
 -- modules/ReAction_PetAction
--- a/modules/ReAction_Action/ReAction_Action.lua	Fri Jun 27 18:40:40 2008 +0000
+++ b/modules/ReAction_Action/ReAction_Action.lua	Sat Jun 28 00:54:21 2008 +0000
@@ -2,11 +2,12 @@
   ReAction Action button module.
 
   The button module implements standard action button functionality by wrapping Blizzard's 
-  ActionButton frame and associated functions.
+  ActionBarButtonTemplate frame and associated functions.
 
-  It also provides support for multiple pages (interacting with the State module) as well
-  as optional action remapping for possessed targets (mind control).
-
+  It also provides action remapping support for multiple pages and possessed targets
+  (Mind Control, Eyes of the Beast, Karazhan Chess event, various quests, etc). This is done
+  by interacting with the built-in State module to map these features to states via the 
+  "statebutton" attribute.
 --]]
 
 -- local imports
@@ -15,7 +16,7 @@
 local _G = _G
 local CreateFrame = CreateFrame
 
-ReAction:UpdateRevision("$Revision: 103 $")
+ReAction:UpdateRevision("$Revision$")
 
 -- module declaration
 local moduleID = "Action"
@@ -24,7 +25,7 @@
 -- Button class declaration
 local Button = { }
 
--- private --
+-- private utility --
 local function RefreshLite(bar)
   local btns = module.buttons[bar]
   if btns then
@@ -34,6 +35,11 @@
   end
 end
 
+local function GetBarConfig(bar)
+  return module.db.profile.bars[bar:GetName()]
+end
+
+
 -- Event handlers
 function module:OnInitialize()
   self.db = ReAction.db:RegisterNamespace( moduleID,
@@ -90,22 +96,24 @@
 
     local r, c = bar:GetButtonGrid()
     local n = r*c
-    for i = 1, n do
-      if btnCfg[i] == nil then
-        btnCfg[i] = {}
+    if n ~= #btns then
+      for i = 1, n do
+        if btnCfg[i] == nil then
+          btnCfg[i] = {}
+        end
+        if btns[i] == nil then
+          local b = Button:New(bar, i, btnCfg[i], barCfg)
+          btns[i] = b
+          bar:AddButton(i,b)
+        end
       end
-      if btns[i] == nil then
-        local b = Button:New(bar, i, btnCfg[i], barCfg)
-        btns[i] = b
-        bar:AddButton(i,b)
-      end
-    end
-    for i = n+1, #btns do
-      if btns[i] then
-        bar:RemoveButton(btns[i])
-        btns[i] = btns[i]:Destroy()
-        if btnCfg[i] then
-          btnCfg[i] = nil
+      for i = n+1, #btns do
+        if btns[i] then
+          bar:RemoveButton(btns[i])
+          btns[i] = btns[i]:Destroy()
+          if btnCfg[i] then
+            btnCfg[i] = nil
+          end
         end
       end
     end
@@ -149,35 +157,116 @@
 
 
 ---- Options ----
-local Handler = { }
+do
+  local Handler = { }
 
-local options = {
-  hideEmpty = {
-    name = L["Hide Empty Buttons"],
-    desc = L["Hide buttons when empty. This option is not supported for multi-state bars"],
-    order = 1,
-    type = "toggle",
-    get  = "GetHideEmpty",
-    set  = "SetHideEmpty",
-  },
-}
+  local options = {
+    hideEmpty = {
+      name = L["Hide Empty Buttons"],
+      order = 1,
+      type = "toggle",
+      width = "double",
+      get  = "GetHideEmpty",
+      set  = "SetHideEmpty",
+    },
+    pages = {
+      name  = L["# Pages"],
+      desc  = L["Use the Dynamic State tab to specify page transitions"],
+      order = 2,
+      type  = "range",
+      min   = 1,
+      max   = 10,
+      step  = 1,
+      get   = "GetNumPages",
+      set   = "SetNumPages",
+    },
+    actions = {
+      name   = L["Edit Action IDs"],
+      order  = 13,
+      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",
+        }
+      }
+    },
+  }
 
-function module:GetBarOptions(bar)
-  return {
-    type = "group",
-    name = L["Action Buttons"],
-    handler = Handler:New(bar),
-    hidden = "Hidden",
-    args = options
-  }
-end
-
--- options handler private
-do
-  local function GetBarConfig( bar )
-    return module.db.profile.bars[bar:GetName()]
+  function module:GetBarOptions(bar)
+    return {
+      type = "group",
+      name = L["Action Buttons"],
+      handler = Handler:New(bar),
+      hidden = "Hidden",
+      args = options
+    }
   end
 
+  -- options handler private
   function Handler:New(bar)
     return setmetatable( { bar = bar }, { __index = Handler } )
   end
@@ -199,64 +288,407 @@
   function Handler:GetHideEmpty()
     return GetBarConfig(self.bar).hideEmpty
   end
+
+  function Handler:GetNumPages()
+    return GetBarConfig(self.bar).nPages
+  end
+
+  function Handler:SetNumPages(info, value)
+    GetBarConfig(self.bar).nPages = value
+    RefreshLite(self.bar)
+  end
+
+  function Handler:GetActionEditMethod()
+    return self.editMethod or 0
+  end
+
+  function Handler:SetActionEditMethod(info, value)
+    self.editMethod = value
+  end
+
+  function Handler:IsButtonSelectHidden()
+    return self.editMethod ~= 1
+  end
+
+  function Handler: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 Handler: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 Handler:SetSelectedRow(info, value)
+    self.selectedRow = value
+  end
+
+  function Handler: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 Handler: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 Handler:SetSelectedColumn(info, value)
+    self.selectedColumn = value
+  end
+
+  function Handler:IsPageSelectHidden()
+    return self.editMethod ~= 1 or (GetBarConfig(self.bar).nPages or 1) < 2
+  end
+
+  function Handler:GetPageList()
+    local n = GetBarConfig(self.bar).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 Handler:GetSelectedPage()
+    local p = self.selectedPage or 1
+    if p > (GetBarConfig(self.bar).nPages or 1) then
+      p = 1
+    end
+    self.selectedPage = p
+    return p
+  end
+
+  function Handler:SetSelectedPage(info, value)
+    self.selectedPage = value
+  end
+
+  function Handler: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 = module.buttons[self.bar][n]
+    if btn then
+      return tostring(btn:GetActionID(self.selectedPage or 1))
+    end
+  end
+
+  function Handler: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 = module.buttons[self.bar][n]
+    if btn then
+      btn:SetActionID(tonumber(value), self.selectedPage or 1)
+    end
+  end
+
+  function Handler: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 Handler:IsMultiIDHidden()
+    return self.editMethod ~= 2
+  end
+
+  function Handler:GetMultiID()
+    local p = { }
+    for i = 1, GetBarConfig(self.bar).nPages or 1 do
+      local b = { }
+      for _, btn in ipairs(module.buttons[self.bar]) 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 Handler:SetMultiID(info, value)
+    local btns = module.buttons[self.bar]
+    local p = ParseMultiID(#btns, GetBarConfig(self.bar).nPages or 1, value)
+    for page, b in ipairs(p) do
+      for button, id in ipairs(b) do
+        btns[button]:SetActionID(id, page)
+      end
+    end
+  end
+
+  function Handler:ValidateMultiID(info, value)
+    local bad = L["Invalid action ID list string"]
+    if value == nil or ParseMultiID(#module.buttons[self.bar], GetBarConfig(self.bar).nPages or 1, value) == nil then
+      return bad
+    end
+    return true
+  end
 end
 
 
------- Button class ------
+------ State property options ------
+do
+  local pageOptions = {
+    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. Select the 'Mind Control' option for the rule type to enable."],
+      order = 11,
+      type = "toggle",
+      disabled = "IsMCDisabled",
+      hidden = "IsPageHidden",
+      width = "double",
+      set = "SetProp",
+      get = "GetProp",
+    },
+    page = {
+      name  = L["Show Page #"],
+      order = 12,
+      type  = "select",
+      width = "half",
+      disabled = "IsPageDisabled",
+      hidden   = "IsPageHidden",
+      values   = "GetPageValues",
+      set      = "SetProp",
+      get      = "GetPage",
+    },
+  }
 
--- use-count of action IDs
-local nActionIDs = 120
-local ActionIDList = setmetatable( {}, {
-  __index = function(self, idx)
-    if idx == nil then
-      for i = 1, nActionIDs do
-        if rawget(self,i) == nil then
-          rawset(self,i,1)
-          return i
+  local function pageImpl( bar, states )
+    local map = { }
+    for state, c in pairs(states) do
+      if c.mindcontrol then
+        map[state] = "mc"
+      elseif c.page then
+        map[state] = ("page%d"):format(c.page)
+      end
+    end
+    bar:SetStateAttribute("statebutton", map, 1, true)
+  end
+
+  local PageOptsHandler = { } -- will inherit properties and methods via State:RegisterStateProperty
+
+  function PageOptsHandler:IsMCDisabled(info)
+    if self:IsPageHidden() then
+      return true
+    end
+    -- only allow this if the mind-control selector or custom/keybind is chosen
+    -- see State.lua for the structure of the 'rule' config element
+    local rule = self.states[self:GetName()].rule
+    if rule then
+      if rule.type == "custom" or rule.type == "keybind" then
+        return false
+      else
+        if rule.values and rule.values.possess then
+          return false
         end
       end
-      error("ran out of action IDs")
-    else
-      local c = rawget(self,idx) or 0
-      rawset(self,idx,c+1)
-      return idx
     end
-  end,
-  __newindex = function(self,idx,value)
-    if value == nil then
-      value = rawget(self,idx)
-      if value == 1 then
-        value = nil
-      elseif value then
-        value = value - 1
+    return true
+  end
+
+  function PageOptsHandler:IsPageDisabled()
+    -- disabled if not an action button
+    return not GetBarConfig(self.bar) or
+    -- OR mind-control remapping is enabled
+      self.states[self:GetName()].mindcontrol or
+    -- OR only one page is enabled
+      (GetBarConfig(self.bar).nPages and GetBarConfig(self.bar).nPages < 2)
+  end
+
+  function PageOptsHandler:IsPageHidden()
+    return not GetBarConfig(self.bar)
+  end
+
+  function PageOptsHandler:GetPageValues()
+    local c = GetBarConfig(self.bar)
+    if c then
+      local n = c.nPages
+      if self._npages ~= n then
+        self._pagevalues = { }
+        self._npages = n
+        -- cache the results
+        for i = 1, n do
+          self._pagevalues[i] = i
+        end
+      end
+      return self._pagevalues
+    end
+  end
+
+  function PageOptsHandler:GetPage(info)
+    return self:GetProp(info) or 1
+  end
+  
+  ReAction:GetModule("State"):RegisterStateProperty("page", pageImpl, pageOptions, PageOptsHandler)
+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
-    rawset(self,idx,value)
+    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
+
+local frameRecycler = { }
+
+------ Button class ------
 function Button:New( bar, idx, config, barConfig )
   -- create new self
   self = setmetatable( { }, {__index = Button} )
   self.bar, self.idx, self.config, self.barConfig = bar, idx, config, barConfig
 
-  config.name = config.name or ("ReAction_%s_%d"):format(bar:GetName(),idx)
-  self.name = config.name
-  config.actionID = ActionIDList[config.actionID] -- gets a free one if none configured
+  local name = config.name or ("ReAction_%s_%s_%d"):format(bar:GetName(),moduleID,idx)
+  self.name = name
+  config.name = name
+  local lastButton = module.buttons[bar][#module.buttons[bar]]
+  config.actionID = IDAlloc:Acquire(config.actionID, lastButton and lastButton.config.actionID) -- gets a free one if none configured
   self.nPages = 1
   
-  local f = CreateFrame("CheckButton", self.name, bar:GetButtonFrame(), "ActionBarButtonTemplate")
+  -- have to recycle frames with the same name: CreateFrame()
+  -- doesn't overwrite existing globals (below). Can't set to nil in the global
+  -- table because you end up getting taint
+  local parent = bar:GetButtonFrame()
+  local f = frameRecycler[name]
+  if f then
+    f:SetParent(parent)
+  else
+    f = CreateFrame("CheckButton", name, parent, "ActionBarButtonTemplate")
+  end
 
   f:SetAttribute("action", config.actionID)
   -- install mind control action support for all buttons here just for simplicity
   if self.idx <= 12 then
-    f:SetAttribute("action-mc", 120 + self.idx)
+    f:SetAttribute("*action-mc", 120 + self.idx)
   end
 
   self.frame = f
   self.normalTexture = getglobal(format("%sNormalTexture",f:GetName()))
 
   -- initialize the hide state
+  f:SetAttribute("showgrid",0)
   self:ShowGrid(not barConfig.hideEmpty)
   if ReAction:GetConfigMode() then
     self:ShowGrid(true)
@@ -276,14 +708,14 @@
   f:SetParent(UIParent)
   f:ClearAllPoints()
   if self.name then
-    _G[self.name] = nil
+    frameRecycler[self.name] = f
   end
   if self.config.actionID then
-    ActionIDList[self.config.actionID] = nil
+    IDAlloc:Release(self.config.actionID)
   end
   if self.config.pages then
-    for _, id in ipairs(self.config.pages) do
-      ActionIDList[id] = nil
+    for _, id in ipairs(self.config.pageactions) do
+      IDAlloc:Release(id)
     end
   end
   self.frame = nil
@@ -308,31 +740,70 @@
   return self.name
 end
 
-function Button:GetActionID()
-  return SecureButton_GetModifiedAttribute(self.frame, "action")
+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:RefreshPages()
-  local nPages = 1 --self.bar:GetNumPages()
-  if nPages ~= self.nPages then
+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.pages
+    local c = self.config.pageactions
     if nPages > 1 and not c then
       c = { }
-      self.config.pages = c
+      self.config.pageactions = c
     end
     for i = 1, nPages do
-      c[i] = ActionIDList[c[i]] -- gets a free one if none configured
-      f:SetAttribute(("action-page%d"):format(i))
+      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
-      ActionIDList[c[i]] = nil
+      IDAlloc:Release(c[i])
       c[i] = nil
-      f:SetAttribute(("action-page%d"):format(i))
+      f:SetAttribute(("*action-page%d"):format(i),nil)
     end
-
-    -- TODO:
-    -- apply next-page, prev-page, and direct-page keybinds (via bar:SetStateKeybind abstraction)
+    self.nPages = nPages
   end
 end
 
@@ -361,27 +832,27 @@
 end
 
 function Button:ShowActionIDLabel( show )
+  local f = self:GetFrame()
   if show then
     local id = self:GetActionID()
-    if not self.actionIDLabel and id and id ~= 0 then
-      local f = self:GetFrame()
+    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)
-      label:SetText(tostring(id))
-      self.actionIDLabel = label
+      f.actionIDLabel = label -- store the label with the frame for recycling
       f:HookScript("OnAttributeChanged", 
         function(frame, attr, value)
-          if attr == "state-parent" then
-            label:SetText(tostring(self:GetActionID()))
+          if label:IsVisible() and (attr == "state-parent" or attr:match("action")) then
+            label:SetText(tostring(frame.action))
           end
         end)
     end
-    self.actionIDLabel:Show()
-  elseif self.actionIDLabel then
-    self.actionIDLabel:Hide()
+    f.actionIDLabel:SetText(tostring(id))
+    f.actionIDLabel:Show()
+  elseif f.actionIDLabel then
+    f.actionIDLabel:Hide()
   end
 end