diff modules/ReAction_Action/ReAction_Action.lua @ 90:7cabc8ac6c16

Updates for wow 3.0 - TOC update - updated changed APIs/frame names - rewrote state code per new SecureHandlers API - cleaned up Bar, ActionButton code - removed AceLibrary/Dewdrop, menu from bar right-click - fixed various small bugs Updated WowAce external locations Updated README.html
author Flick <flickerstreak@gmail.com>
date Wed, 15 Oct 2008 16:29:41 +0000
parents fc83b3f5b322
children c2504a8b996c
line wrap: on
line diff
--- a/modules/ReAction_Action/ReAction_Action.lua	Mon Oct 13 23:32:33 2008 +0000
+++ b/modules/ReAction_Action/ReAction_Action.lua	Wed Oct 15 16:29:41 2008 +0000
@@ -5,9 +5,7 @@
   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). This is done
-  by interacting with the built-in State module to map these features to states via the 
-  "statebutton" attribute.
+  (Mind Control, Eyes of the Beast, Karazhan Chess event, various quests, etc).
 --]]
 
 -- local imports
@@ -26,41 +24,27 @@
 local moduleID = "Action"
 local module = ReAction:NewModule( moduleID )
 
--- Button class declaration
+-- Class declarations
 local Button = { }
-
--- private utility --
-local function RefreshLite(bar)
-  local btns = module.buttons[bar]
-  if btns then
-    for _, b in ipairs(btns) do
-      b:Refresh()
-    end
-  end
-end
-
-local function GetBarConfig(bar)
-  return module.db.profile.bars[bar:GetName()]
-end
-
+local Handle = { }
+local PropHandler = { }
 
 -- Event handlers
 function module:OnInitialize()
   self.db = ReAction.db:RegisterNamespace( moduleID,
     { 
       profile = {
-        buttons = { },
         bars = { },
       }
     }
   )
-  self.buttons = { }
+  self.handles = setmetatable({ }, weak)
 
   ReAction:RegisterBarOptionGenerator(self, "GetBarOptions")
 
-  ReAction.RegisterCallback(self, "OnCreateBar", "OnRefreshBar")
+  ReAction.RegisterCallback(self, "OnCreateBar")
+  ReAction.RegisterCallback(self, "OnRefreshBar")
   ReAction.RegisterCallback(self, "OnDestroyBar")
-  ReAction.RegisterCallback(self, "OnRefreshBar")
   ReAction.RegisterCallback(self, "OnEraseBar")
   ReAction.RegisterCallback(self, "OnRenameBar")
   ReAction.RegisterCallback(self, "OnConfigModeChanged")
@@ -74,118 +58,86 @@
   ReAction:RegisterBarType(L["Action Bar"], 
     { 
       type = moduleID,
-      defaultButtonSize = 36,
+      defaultButtonSize = 6,
       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 bar.config.type == moduleID then
-    if self.buttons[bar] == nil then
-      self.buttons[bar] = { }
-    end
-    local btns = self.buttons[bar]
-    local profile = self.db.profile
-    if profile.buttons[name] == nil then
-      profile.buttons[name] = {}
-    end
-    if profile.bars[name] == nil then
-      profile.bars[name] = {}
-    end
-    local btnCfg = profile.buttons[name]
-    local barCfg = profile.bars[name]
-
-    local r, c = bar:GetButtonGrid()
-    local n = r*c
-    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
-      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
-    RefreshLite(bar)
+  if self.handles[bar] then
+    self.handles[bar]:Refresh()
   end
 end
 
 function module:OnDestroyBar(event, bar, name)
-  if self.buttons[bar] then
-    local btns = self.buttons[bar]
-    for _,b in pairs(btns) do
-      if b then
-        b:Destroy()
-      end
-    end
-    self.buttons[bar] = nil
+  if self.handles[bar] then
+    self.handles[bar]:Destroy()
+    self.handles[bar] = nil
   end
 end
 
 function module:OnEraseBar(event, bar, name)
-  self.db.profile.buttons[name] = nil
   self.db.profile.bars[name] = nil
 end
 
 function module:OnRenameBar(event, bar, oldname, newname)
-  local b = self.db.profile.buttons
-  b[newname], b[oldname] = b[oldname], nil
-
   b = self.db.profile.bars
   b[newname], b[oldname] = b[oldname], nil
 end
 
 function module:OnConfigModeChanged(event, mode)
-  for _, bar in pairs(self.buttons) do
-    for _, b in pairs(bar) do
-      b:ShowGrid(mode)
-      b:ShowActionIDLabel(mode)
-    end
+  for _, h in pairs(self.handles) do
+    h:SetConfigMode(mode)
   end
 end
 
 function module:LIBKEYBOUND_ENABLED(evt)
-  -- set the border for all buttons to the keybind-enable color
-  local r,g,b,a = KB:GetColorKeyBoundMode()
-  for _, bar in pairs(self.buttons) do
-    for _, b in pairs(bar) do
-    	b.border:SetVertexColor(r,g,b,a)
-      b.border:Show()
-    end
+  for _, h in pairs(self.handles) do
+    h:SetKeybindMode(true)
   end
 end
 
 function module:LIBKEYBOUND_DISABLED(evt)
-  for _, bar in pairs(self.buttons) do
-    for _, b in pairs(bar) do
-      b.border:Hide()
-    end
+  for _, h in pairs(self.handles) do
+    h:SetKeybindMode(false)
   end
 end
 
 
----- Options ----
+---- Interface ----
+function module:GetBarOptions(bar)
+  local h = self.handles[bar]
+  if h then
+    return h:GetOptions()
+  end
+end
+
+
+---- Bar Handle ----
+
 do
-  local Handler = { }
-
   local options = {
     hideEmpty = {
       name = L["Hide Empty Buttons"],
@@ -206,9 +158,18 @@
       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 = 3,
+      type = "toggle",
+      width = "double",
+      set = "SetMindControl",
+      get = "GetMindControl",
+    },
     actions = {
       name   = L["Edit Action IDs"],
-      order  = 13,
+      order  = 4,
       type   = "group",
       inline = true,
       args   = {
@@ -277,66 +238,159 @@
           get    = "GetMultiID",
           set    = "SetMultiID",
           validate = "ValidateMultiID",
-        }
-      }
+        },
+      },
     },
   }
 
-  function module:GetBarOptions(bar)
+  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()
+    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
+    for _, b in ipairs(self.btns) do
+      b:Refresh()
+    end
+    local f = self.bar:GetFrame()
+    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")
+  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:SetKeybindMode(mode)
+    for _, b in pairs(self.btns) do
+      if mode then
+        -- set the border for all buttons to the keybind-enable color
+        local r,g,b,a = KB:GetColorKeyBoundMode()
+      	b.border:SetVertexColor(r,g,b,a)
+        b.border:Show()
+      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 = Handler:New(bar),
-      hidden = "Hidden",
+      handler = self,
       args = options
     }
   end
 
-  -- options handler private
-  function Handler:New(bar)
-    return setmetatable( { bar = bar }, { __index = Handler } )
-  end
-
-  function Handler:Hidden()
-    return self.bar.config.type ~= moduleID
-  end
-
-  function Handler:SetHideEmpty(info, value)
-    local c = GetBarConfig(self.bar)
-    if value ~= c.hideEmpty then
+  function Handle:SetHideEmpty(info, value)
+    if value ~= self.config.hideEmpty then
       for b in self.bar:IterateButtons() do
         b:ShowGrid(not value)
       end
-      c.hideEmpty = value
+      self.config.hideEmpty = value
     end
   end
 
-  function Handler:GetHideEmpty()
-    return GetBarConfig(self.bar).hideEmpty
+  function Handle:GetHideEmpty()
+    return self.config.hideEmpty
   end
 
-  function Handler:GetNumPages()
-    return GetBarConfig(self.bar).nPages
+  function Handle:GetNumPages()
+    return self.config.nPages
   end
 
-  function Handler:SetNumPages(info, value)
-    GetBarConfig(self.bar).nPages = value
-    RefreshLite(self.bar)
+  function Handle:SetNumPages(info, value)
+    self.config.nPages = value
+    self:Refresh()
   end
 
-  function Handler:GetActionEditMethod()
+  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 Handler:SetActionEditMethod(info, value)
+  function Handle:SetActionEditMethod(info, value)
     self.editMethod = value
   end
 
-  function Handler:IsButtonSelectHidden()
+  function Handle:IsButtonSelectHidden()
     return self.editMethod ~= 1
   end
 
-  function Handler:GetRowList()
+  function Handle:GetRowList()
     local r,c = self.bar:GetButtonGrid()
     if self.rowList == nil or #self.rowList ~= r then
       local list = { }
@@ -348,7 +402,7 @@
     return self.rowList
   end
 
-  function Handler:GetSelectedRow()
+  function Handle:GetSelectedRow()
     local r, c = self.bar:GetButtonGrid()
     local row = self.selectedRow or 1
     if row > r then
@@ -358,11 +412,11 @@
     return row
   end
 
-  function Handler:SetSelectedRow(info, value)
+  function Handle:SetSelectedRow(info, value)
     self.selectedRow = value
   end
 
-  function Handler:GetColumnList()
+  function Handle:GetColumnList()
     local r,c = self.bar:GetButtonGrid()
     if self.columnList == nil or #self.columnList ~= c then
       local list = { }
@@ -374,7 +428,7 @@
     return self.columnList
   end
 
-  function Handler:GetSelectedColumn()
+  function Handle:GetSelectedColumn()
     local r, c = self.bar:GetButtonGrid()
     local col = self.selectedColumn or 1
     if col > c then
@@ -384,16 +438,16 @@
     return col
   end
 
-  function Handler:SetSelectedColumn(info, value)
+  function Handle:SetSelectedColumn(info, value)
     self.selectedColumn = value
   end
 
-  function Handler:IsPageSelectHidden()
-    return self.editMethod ~= 1 or (GetBarConfig(self.bar).nPages or 1) < 2
+  function Handle:IsPageSelectHidden()
+    return self.editMethod ~= 1 or (self.config.nPages or 1) < 2
   end
 
-  function Handler:GetPageList()
-    local n = GetBarConfig(self.bar).nPages or 1
+  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
@@ -404,42 +458,42 @@
     return self.pageList
   end
 
-  function Handler:GetSelectedPage()
+  function Handle:GetSelectedPage()
     local p = self.selectedPage or 1
-    if p > (GetBarConfig(self.bar).nPages or 1) then
+    if p > (self.config.nPages or 1) then
       p = 1
     end
     self.selectedPage = p
     return p
   end
 
-  function Handler:SetSelectedPage(info, value)
+  function Handle:SetSelectedPage(info, value)
     self.selectedPage = value
   end
 
-  function Handler:GetActionID()
+  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 = module.buttons[self.bar][n]
+    local btn = self.btns[n]
     if btn then
       return tostring(btn:GetActionID(self.selectedPage or 1))
     end
   end
 
-  function Handler:SetActionID(info, value)
+  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 = module.buttons[self.bar][n]
+    local btn = self.btns[n]
     if btn then
       btn:SetActionID(tonumber(value), self.selectedPage or 1)
     end
   end
 
-  function Handler:ValidateActionID(info, value)
+  function Handle:ValidateActionID(info, value)
     value = tonumber(value)
     if value == nil or value < 1 or value > 120 then
       return L["Specify ID 1-120"]
@@ -447,15 +501,15 @@
     return true
   end
 
-  function Handler:IsMultiIDHidden()
+  function Handle:IsMultiIDHidden()
     return self.editMethod ~= 2
   end
 
-  function Handler:GetMultiID()
+  function Handle:GetMultiID()
     local p = { }
-    for i = 1, GetBarConfig(self.bar).nPages or 1 do
+    for i = 1, self.config.nPages or 1 do
       local b = { }
-      for _, btn in ipairs(module.buttons[self.bar]) do
+      for _, btn in ipairs(self.btns) do
         table.insert(b, btn:GetActionID(i))
       end
       table.insert(p, table.concat(b,","))
@@ -486,19 +540,18 @@
     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)
+  function Handle:SetMultiID(info, value)
+    local p = ParseMultiID(#btns, self.config.nPages or 1, value)
     for page, b in ipairs(p) do
       for button, id in ipairs(b) do
-        btns[button]:SetActionID(id, page)
+        self.btns[button]:SetActionID(id, page)
       end
     end
   end
 
-  function Handler:ValidateMultiID(info, value)
+  function Handle: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
+    if value == nil or ParseMultiID(#self.btns, self.config.nPages or 1, value) == nil then
       return bad
     end
     return true
@@ -509,20 +562,9 @@
 ------ 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,
+      order = 11,
       type  = "select",
       width = "half",
       disabled = "IsPageDisabled",
@@ -533,53 +575,25 @@
     },
   }
 
-  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)
+  local function GetBarConfig(bar)
+    return module.db.profile.bars[bar:GetName()]
   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
-    end
-    return true
+  function PropHandler.GetOptions()
+    return pageOptions
   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)
+  function PropHandler:IsPageDisabled()
+    local c = GetBarConfig(self.bar)
+    local n = c and c.nPages or 1
+    return not (n > 1)
   end
 
-  function PageOptsHandler:IsPageHidden()
+  function PropHandler:IsPageHidden()
     return not GetBarConfig(self.bar)
   end
 
-  function PageOptsHandler:GetPageValues()
+  function PropHandler:GetPageValues()
     local c = GetBarConfig(self.bar)
     if c then
       local n = c.nPages
@@ -588,18 +602,17 @@
         self._npages = n
         -- cache the results
         for i = 1, n do
-          self._pagevalues[i] = i
+          self._pagevalues["page"..i] = i
         end
       end
       return self._pagevalues
     end
   end
 
-  function PageOptsHandler:GetPage(info)
+  function PropHandler:GetPage(info)
     return self:GetProp(info) or 1
   end
-  
-  ReAction:GetModule("State"):RegisterStateProperty("page", pageImpl, pageOptions, PageOptsHandler)
+
 end
 
 ------ ActionID allocation ------
@@ -680,350 +693,387 @@
 
 ------ Button class ------
 
-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 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"})
+    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
+    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
-        frame.flashtime = ATTACK_BUTTON_FLASH_TIME - overtime
+      end
+      
+      if frame.rangeTimer then
+        frame.rangeTimer = frame.rangeTimer - elapsed;
 
-        local flashTexture = frame.flash
-        if flashTexture:IsShown() then
-          flashTexture:Hide()
-        else
-          flashTexture:Show()
+        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)
+    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
     
-    if frame.rangeTimer then
-      frame.rangeTimer = frame.rangeTimer - elapsed;
+    -- 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
 
-      if frame.rangeTimer <= 0 then
-        if IsActionInRange(frame.action) == 0 then
-          frame.icon:SetVertexColor(1.0,0.1,0.1)
+    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
+    
+    -- wrap the OnClick handler to use a pagemap from the header's context
+    parent:WrapScript(f, "OnClick", 
+      -- function OnClick(self, button, down)
+      [[
+        if doMindControl and GetBonusBarOffset() == 5 then
+          return "mc"
         else
-          ActionButton_UpdateUsable()
+          return state and page and page[state] or button
         end
-        frame.rangeTimer = 0.1
+      ]])
+
+    -- set a _childupdate handler, called within the header's context
+    -- SetAttribute() is a brute-force way to trigger ActionButton_UpdateAction(). Setting "*action1"
+    -- will, in the absence of a useful replacement for SecureButton_GetEffectiveButton(), force
+    -- ActionButton_CalculateAction() to use the new action-id for display purposes. It also 
+    -- sort of obviates the OnClick handler, but hopefully this is only temporary until
+    -- SecureButton_GetEffectiveButton() gets fixed.
+    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)
+        print("setting action",value,"page[state]=",page and page[state])
+        self:SetAttribute("*action1",value)
+      ]])
+
+    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)
+    end
+
+    -- show the ID label if applicable
+    self:ShowActionIDLabel(ReAction:GetConfigMode())
+
+    -- attach the keybinder
+    KBAttach(self)
+
+    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
 
-  -- 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)
+  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 GetHotkey(f)
-    local b = buttonLookup[f]
-    if b then
-      return KB:ToShortKey(b:GetConfig().hotkey)
+  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 SetKey(f, key)
-    local b = buttonLookup[f]
-    if b then
-      local c = b:GetConfig()
-      if c.hotkey then
-        SetOverrideBinding(f, false, c.hotkey, nil)
+  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 key then
-        SetOverrideBindingClick(f, false, key, f:GetName(), nil)
+      if count < 0 then
+        count = 0
       end
-      c.hotkey = key
-      b:DisplayHotkey(GetHotkey(f))
+      f:SetAttribute("showgrid",count)
+
+      if count >= 1 and not f:GetAttribute("statehidden") then
+        self.normalTexture:SetVertexColor(1.0, 1.0, 1.0, 0.5);
+        f:Show()
+      elseif count < 1 and not HasAction(self:GetActionID()) then
+        f:Hide()
+      end
     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
+  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
-    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
+      f.actionIDLabel:SetText(tostring(id))
+      f.actionIDLabel:Show()
+    elseif f.actionIDLabel then
+      f.actionIDLabel:Hide()
     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)
+  function Button:DisplayHotkey( key )
+    self.hotkey:SetText(key or "")
   end
 end
-
-
-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
-
-  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
-  
-  -- 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")
-    -- 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 action support for all buttons here just for simplicity
-  if self.idx <= 12 then
-    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)
-  end
-
-  -- show the ID label if applicable
-  self:ShowActionIDLabel(ReAction:GetConfigMode())
-
-  -- attach the keybinder
-  KBAttach(self)
-
-  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.pages 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)
-  if self.barConfig.mckeybinds then
-    f:SetAttribute("bindings-mc", self.barConfig.mckeybinds[self.idx])
-  end
-  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
-    -- new in 2.4.1: can't call ActionButton_ShowGrid/HideGrid because they won't update the attribute
-    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
-      self.normalTexture:SetVertexColor(1.0, 1.0, 1.0, 0.5);
-      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 == "state-parent" or 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