changeset 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 491a6ffe7260
children c2504a8b996c
files Bar.lua Overlay.lua README.html ReAction.lua ReAction.toc State.lua lib/embeds.xml locale/enUS.lua modules/ReAction_Action/ReAction_Action.lua modules/ReAction_ConfigUI/ReAction_ConfigUI.lua modules/ReAction_PetAction/ReAction_PetAction.lua
diffstat 11 files changed, 1125 insertions(+), 1011 deletions(-) [+]
line wrap: on
line diff
--- a/Bar.lua	Mon Oct 13 23:32:33 2008 +0000
+++ b/Bar.lua	Wed Oct 15 16:29:41 2008 +0000
@@ -5,64 +5,48 @@
 local floor = math.floor
 local fmod = math.fmod
 local format = string.format
-local SecureStateHeader_Refresh = SecureStateHeader_Refresh
 
 ReAction:UpdateRevision("$Revision$")
 
+local Bar   = { }
+local proto = { __index = Bar }
+local weak  = { __mode = "k" }
 
------- BAR CLASS ------
-local Bar = { _classID = {} }
 ReAction.Bar = Bar -- export to ReAction
 
 function Bar:New( name, config )
-  -- create new self
-  self = setmetatable( { }, {__index = Bar} )
   if type(config) ~= "table" then
     error("ReAction.Bar: config table required")
   end
-  config.width = config.width or 480
-  config.height = config.height or 40
 
-  self.name, self.config = name, config
-  self.buttons = setmetatable({},{__mode="k"})
-  self.statedrivers = { }
-  self.keybinds = { }
+  -- create new self
+  self = setmetatable( 
+    { 
+      config  = config,
+      name    = name,
+      buttons = setmetatable( { }, weak ),
+      width   = config.width or 480,
+      height  = config.height or 40
+    }, 
+    proto )
+  
+  -- The frame type is 'Button' in order to have an OnClick handler. However, the frame itself is
+  -- not mouse-clickable by the user.
+  local parent = config.parent and (ReAction:GetBar(config.parent) or _G[config.parent]) or UIParent
+  local f = CreateFrame("Button", name and format("ReAction-%s",name), parent,
+                        "SecureHandlerStateTemplate, SecureHandlerClickTemplate")
+  f:SetFrameStrata("MEDIUM")
+  f:SetWidth(self.width)
+  f:SetWidth(self.height)
+  f:Show()
+  f:EnableMouse(false)
+  f:SetClampedToScreen(true)
 
-  local parent = config.parent and (ReAction:GetBar(config.parent) or _G[config.parent]) or UIParent
-  local f = CreateFrame("Button",name and format("ReAction-%s",name),parent,"SecureStateHeaderTemplate, SecureActionButtonTemplate")
-  f:SetFrameStrata("MEDIUM")
-  f:SetWidth(config.width)
-  f:SetWidth(config.height)
-  f:Show()
-
-  -- The bar itself is also a Button derived from SecureActionButtonTemplate, so it has an OnClick handler
-  -- which we can use as a virtual button for keybinds, which will send attribute-value changes to itself.
-  -- However, we don't ever want the user to be able to click it directly.
-  f:EnableMouse(false)
-  f:SetAttribute("type","attribute")
-
-  -- Buttons are contained in an anonymous intermediate sub-frame. This arrangement is to specifically
-  -- address the issue of the interaction with hidestates and auto-hiding empty action buttons (the two
-  -- don't play nicely together). It also has the fringe benefit of making show/hide faster because a
-  -- single frame is shown/hidden instead of potentially dozens. Unfortunately it does add an extra layer
-  -- of indirection to all state changes, as a secondary (trivial) statemap must be invoked. This
-  -- complicates frame setup slightly.
-  local bf = CreateFrame("Frame", nil, f, "SecureStateHeaderTemplate")
-  bf:SetAllPoints()
-  bf:Show()
-  bf:SetAttribute("useparent*",true)          -- this facilitates SecureButton_GetModifiedAttribute()
-  bf:SetAttribute("statemap-parent","$input") -- However, we also need SetAttribute(state-parent) propagation too
-  f:SetAttribute("addchild",bf)
-
-  -- Both frames are read-only. Override the default accessors for this object.
+  -- Override the default frame accessor to provide strict read-only access
   function self:GetFrame()
     return f
   end
 
-  function self:GetButtonFrame()
-    return bf
-  end
-
   self:ApplyAnchor()
   ReAction.RegisterCallback(self, "OnConfigModeChanged")
 
@@ -72,16 +56,10 @@
 function Bar:Destroy()
   local f = self:GetFrame()
   f:UnregisterAllEvents()
+  ReAction.UnregisterAllCallbacks(self)
   f:Hide()
   f:SetParent(UIParent)
   f:ClearAllPoints()
-  ReAction.UnregisterAllCallbacks(self)
-  for driver in pairs(self.statedrivers) do
-    UnregisterStateDriver(f, driver)
-  end
-  self.labelString = nil
-  self.controlFrame = nil
-  self.config = nil
 end
 
 function Bar:OnConfigModeChanged(event, mode)
@@ -89,22 +67,26 @@
 end
 
 function Bar:ApplyAnchor()
-  local f, config = self:GetFrame(), self.config
-  f:SetWidth(config.width)
-  f:SetHeight(config.height)
-  local point  = config.point
+  local f = self:GetFrame()
+  local c = self.config
+  local p = c.point
+
+  f:SetWidth(c.width)
+  f:SetHeight(c.height)
   f:ClearAllPoints()
-  if point then
-    local anchor = f:GetParent()
-    if config.anchor then
-      local bar = ReAction:GetBar(config.anchor)
+  
+  if p then
+    local a = f:GetParent()
+    if c.anchor then
+      local bar = ReAction:GetBar(c.anchor)
       if bar then
-        anchor = bar:GetFrame()
+        a = bar:GetFrame()
       else
-        anchor = _G[config.anchor]
+        a = _G[c.anchor]
       end
     end
-    f:SetPoint(point, anchor or f:GetParent(), config.relpoint, config.x or 0, config.y or 0)
+    local fr = a or f:GetParent()
+    f:SetPoint(p, a or f:GetParent(), c.relpoint, c.x or 0, c.y or 0)
   else
     f:SetPoint("CENTER")
   end
@@ -122,7 +104,11 @@
 
 function Bar:GetAnchor()
   local c = self.config
-  return (c.point or "CENTER"), (c.anchor or self:GetFrame():GetParent():GetName()), (c.relpoint or c.point or "CENTER"), (c.x or 0), (c.y or 0)
+  return (c.point or "CENTER"), 
+         (c.anchor or self:GetFrame():GetParent():GetName()), 
+         (c.relpoint or c.point or "CENTER"), 
+         (c.x or 0), 
+         (c.y or 0)
 end
 
 function Bar:GetSize()
@@ -131,9 +117,9 @@
 end
 
 function Bar:SetSize(w,h)
+  local f = self:GetFrame()
   self.config.width = w
   self.config.height = h
-  local f = self:GetFrame()
   f:SetWidth(w)
   f:SetHeight(h)
 end
@@ -182,121 +168,54 @@
 
 function Bar:GetFrame()
   -- this method is included for documentation purposes. It is overridden
-  -- in the New method for each object.
+  -- for each object in the :New() method.
   error("Invalid Bar object: used without initialization")
 end
 
-function Bar:GetButtonFrame()
-  -- this method is included for documentation purposes. It is overridden
-  -- in the New method for each object.
-  error("Invalid Bar object: used without initialization")
+-- only ReAction:RenameBar() should call this function. Calling from any other
+-- context will desync the bar list in the ReAction class.
+function Bar:SetName(name)
+  self.name = name
+  self:SetLabel(self.name) -- Bar:SetLabel() defined in Overlay.lua
 end
 
--- only ReAction:RenameBar() should call this function
-function Bar:SetName(name)
-  self.name = name
-  -- controlLabelString is defined in Overlay.lua
-  if self.controlLabelString then
-    self.controlLabelString:SetText(self.name)
+function Bar:AddButton(idx, button)
+  local f = self:GetFrame()
+
+  -- store in a weak reverse-index array
+  self.buttons[button] = idx
+
+  -- Store a properly wrapped reference to the child frame as an attribute 
+  -- (accessible via "frameref-btn#")
+  f:SetFrameRef(format("btn%d",idx), button:GetFrame())
+end
+
+function Bar:RemoveButton(button)
+  local idx = self.buttons[button]
+  if idx then
+    self:GetFrame():SetAttribute(format("frameref-btn%d",idx),nil)
+    self.buttons[button] = nil
   end
 end
 
-function Bar:AddButton(idx, button)
-  -- store in a reverse-index array
-  self.buttons[button] = idx
-  self:GetButtonFrame():SetAttribute("addchild",button:GetFrame())
-  SecureStateHeader_Refresh(self:GetFrame())
-end
-
-function Bar:RemoveButton(button)
-  self.buttons[button] = nil
-end
-
-function Bar:IterateButtons() -- iterator returns button, idx
+-- iterator returns button, idx and does NOT iterate in index order
+function Bar:IterateButtons()
   return pairs(self.buttons)
 end
 
 function Bar:PlaceButton(button, baseW, baseH)
   local idx = self.buttons[button]
-  if not idx then return end
-  local r, c, s = self:GetButtonGrid()
-  local bh, bw = self:GetButtonSize()
-  local row, col = floor((idx-1)/c), fmod((idx-1),c) -- zero-based
-  local x, y = col*bw + (col+0.5)*s, row*bh + (row+0.5)*s
-  local scale = bw/baseW
-  local f = button:GetFrame()
+  if idx then 
+    local r, c, s = self:GetButtonGrid()
+    local bh, bw = self:GetButtonSize()
+    local row, col = floor((idx-1)/c), fmod((idx-1),c) -- zero-based
+    local x, y = col*bw + (col+0.5)*s, -(row*bh + (row+0.5)*s)
+    local scale = bw/baseW
+    local b = button:GetFrame()
 
-  f:ClearAllPoints()
-  f:SetPoint("TOPLEFT",x/scale,-y/scale)
-  f:SetScale(scale)
-end
-
--- Creates (or updates) a named binding which binds a key press to a call to SetAttribute()
--- pass a nil key to unbind
-function Bar:SetAttributeBinding( name, key, attribute, value )
-  if not name then
-    error("usage - Bar:SetAttributeBinding(name [, key, attribute, value]")
-  end
-  local f = self:GetFrame()
-
-  -- clear the old binding, if any
-  if self.keybinds[name] then
-    SetOverrideBinding(f, false, self.keybinds[name], nil)
-  end
-  if key then
-    f:SetAttribute(format("attribute-name-%s",name), attribute)
-    f:SetAttribute(format("attribute-value-%s",name), value)
-    SetOverrideBindingClick(f, false, key, f:GetName(), name) -- binding name is the virtual mouse button
-  end
-  self.keybinds[name] = key
-end
-
--- Sets up a state driver 'name' for the bar, using the provided 'rule'
--- Also sets attributes 'statemap-<name>-<key>'=<value> for each entry in the passed map
--- if 'rule' is nil or an empty string, the driver is unregistered.
-function Bar:SetStateDriver( name, rule, map )
-  local f = self:GetFrame()
-  if rule and #rule > 0 then
-    if map then
-      for key, value in pairs(map) do
-        f:SetAttribute( format("statemap-%s-%s",name,key), value )
-      end
-    end
-    RegisterStateDriver(f, name, rule)
-    self.statedrivers[name] = true
-  elseif self.statedrivers[name] then
-    UnregisterStateDriver(f, name)
-    self.statedrivers[name] = nil
+    b:ClearAllPoints()
+    b:SetPoint("TOPLEFT",x/scale,y/scale)
+    b:SetScale(scale)
   end
 end
 
--- Set an attribute on the frame (or each button if 'doButtons' = true)
--- Either or both 'map' and 'default' can be passed:
---   - If 'map' is omitted, then 'default' is set to the attribute.
---   - If 'map' is provided, then it is interpreted as an unordered
---     table of the form { ["statename"] = ["value"] }, and will be
---     converted into a SecureStateHeaderTemplate style state-parsed
---     string, e.g. "<state1>:<value1>;<state2>:<value2>". If 'default'
---     is also provided, then its value will be converted to a string
---     and appended.
-function Bar:SetStateAttribute( attribute, map, default, doButtons )
-  local value = default
-  if map then
-    local tmp = { }
-    for state, value in pairs(map) do
-      table.insert(tmp, format("%s:%s",tostring(state),tostring(value)))
-    end
-    if default then
-      table.insert(tmp, tostring(default))
-    end
-    value = table.concat(tmp,";")
-  end
-  if doButtons then
-    for b in self:IterateButtons() do
-      b:GetFrame():SetAttribute(attribute,value)
-    end
-  else
-    self:GetFrame():SetAttribute(attribute, value)
-  end
-  SecureStateHeader_Refresh(self:GetFrame())
-end
--- a/Overlay.lua	Mon Oct 13 23:32:33 2008 +0000
+++ b/Overlay.lua	Wed Oct 15 16:29:41 2008 +0000
@@ -1,96 +1,36 @@
-local ReAction = ReAction
-local L = ReAction.L
-local CreateFrame = CreateFrame
+local ReAction         = ReAction
+local L                = ReAction.L
+local CreateFrame      = CreateFrame
 local InCombatLockdown = InCombatLockdown
-local floor = math.floor
-local min = math.min
-local format = string.format
-local GameTooltip = GameTooltip
+local floor            = math.floor
+local min              = math.min
+local format           = string.format
+local GameTooltip      = GameTooltip
+local Bar              = ReAction.Bar
+local GetSize          = Bar.GetSize
+local GetButtonSize    = Bar.GetButtonSize
+local GetButtonGrid    = Bar.GetButtonGrid
+local SetSize          = Bar.SetSize
+local SetAnchor        = Bar.SetAnchor
+local SetButtonSize    = Bar.SetButtonSize
+local SetButtonGrid    = Bar.SetButtonGrid
+local ApplyAnchor      = Bar.ApplyAnchor
 
 ReAction:UpdateRevision("$Revision$")
 
--- Looking for a lightweight AceConfig3-struct-compatible 
--- replacement for Dewdrop (e.g. forthcoming AceConfigDropdown-3.0?).
--- Considering Blizzard's EasyMenu/UIDropDownMenu, but that's
--- a bit tricky to convert from AceConfig3-struct
-local Dewdrop = AceLibrary("Dewdrop-2.0")
-
-local function OpenMenu (frame, opts)
-  Dewdrop:Open(frame, "children", opts, "cursorX", true, "cursorY", true)
-end
-
-local function CloseMenu(frame)
-  if Dewdrop:GetOpenedParent() == frame then
-    Dewdrop:Close()
-  end
-end
-
-local function ShowMenu(bar)
-  if not bar.menuOpts then
-    bar.menuOpts = {
-      type = "group",
-      args = {
-        openConfig = {
-          type = "execute",
-          name = L["Settings..."],
-          desc = L["Open the editor for this bar"],
-          func = function() CloseMenu(bar.controlFrame); ReAction:ShowEditor(bar) end,
-          disabled = InCombatLockdown,
-          order = 1
-        },
-        delete = {
-          type = "execute",
-          name = L["Delete Bar"],
-          desc = L["Remove the bar from the current profile"],
-          confirm = L["Are you sure you want to remove this bar?"],
-          func = function() ReAction:EraseBar(bar) end,
-          order = 2
-        },
-      }
-    }
-  end
-  OpenMenu(bar.controlFrame, bar.menuOpts)
-end
-
-
 --
 -- Bar config overlay
 --
--- localize some of these for small OnUpdate performance boost
-local Bar           = ReAction.Bar
-local GetSize       = Bar.GetSize
-local GetButtonSize = Bar.GetButtonSize
-local GetButtonGrid = Bar.GetButtonGrid
-local SetSize       = Bar.SetSize
-local SetButtonSize = Bar.SetButtonSize
-local SetButtonGrid = Bar.SetButtonGrid
-local ApplyAnchor   = Bar.ApplyAnchor
+
+local function StoreSize(bar)
+  local f = bar:GetFrame()
+  SetSize( bar, f:GetWidth(), f:GetHeight() )
+end
 
 local function StoreExtents(bar)
   local f = bar:GetFrame()
-  local point, relativeTo, relativePoint, x, y = f:GetPoint(1)
-  relativeTo = relativeTo or f:GetParent()
-  local anchorTo
-  for name, b in ReAction:IterateBars() do
-    if b and b:GetFrame() == relativeTo then
-      anchorTo = name
-      break
-    end
-  end
-  anchorTo = anchorTo or relativeTo:GetName()
-  local c = bar.config
-  c.anchor = point
-  c.anchorTo = anchorTo
-  c.relativePoint = relativePoint
-  c.x = x
-  c.y = y
-  c.width, c.height = f:GetWidth(), f:GetHeight()
-end
-
-local function StoreSize(bar)
-  local f = bar:GetFrame()
-  local c = bar.config
-  c.width, c.height = f:GetWidth(), f:GetHeight()
+  SetAnchor( bar, f:GetPoint(1) )
+  SetSize( bar, f:GetWidth(), f:GetHeight() )
 end
 
 local function RecomputeButtonSize(bar)
@@ -449,7 +389,7 @@
   label:SetTextColor(1,1,1,1)
   label:SetText(bar:GetName())
   label:Show()
-  bar.controlLabelString = label  -- so that bar:SetName() can update it
+  bar.controlLabelString = label  -- so that bar:SetLabel() can update it
 
   local function StopResize()
     f:StopMovingOrSizing()
@@ -457,7 +397,7 @@
     f:SetScript("OnUpdate",nil)
     StoreSize(bar)
     ClampToButtons(bar)
-    ApplyAnchor(bar)
+    --ApplyAnchor(bar)
     ReAction:RefreshOptions()
   end
 
@@ -648,15 +588,17 @@
 
   control:SetScript("OnClick",
     function()
-      ShowMenu(bar)
+      ReAction:ShowEditor(bar)
     end
   )
 
+  control:Raise()
+
   return control
 end
 
 
--- export the ShowControls method to the Bar prototype
+-- export methods to the Bar prototype
 
 function Bar:ShowControls(show)
   if show then
@@ -666,9 +608,12 @@
     self.controlFrame:Show()
     self.controlFrame:Raise()
   elseif self.controlFrame then
-    CloseMenu(self.controlFrame)
     self.controlFrame:Hide()
   end
 end
 
-
+function Bar:SetLabel(name)
+  if self.controlLabelString then
+    self.controlLabelString:SetText(self.name)
+  end
+end
--- a/README.html	Mon Oct 13 23:32:33 2008 +0000
+++ b/README.html	Wed Oct 15 16:29:41 2008 +0000
@@ -10,7 +10,6 @@
 <a href="#welcome">Welcome to ReAction</a><br/>
 <a href="#credits">Credits</a><br/>
 <a href="#features">Features</a><br/>
-<a href="#nonfeatures">Non-Features</a><br/>
 <a href="#usage">Using ReAction</a><br/>
 <a href="#license">License</a><br/>
 </div>
@@ -19,7 +18,7 @@
 
 <a name="welcome"><h1>Welcome to ReAction</h1></a>
 <p>ReAction is a replacement for the various action bars provided by the default Blizzard UI. With ReAction, you can group your action buttons into bars however you want, and have as many bars as you want. Those bars can then have a number of dynamic behaviors assigned to them. ReAction provides an intuitive mouse-driven interface for arranging and resizing bars, and a dialog interface for the majority of the configuration options.</p>
-<p>There are many action bar addons out there. I suggest you try several and choose the one that works best for you. Other popular action bar addons include:</p>
+<p>ReAction is <b>not</b> an install-and-forget addon that magically makes your action bars look spiffy and work better. It's a tool, designed to help you build a UI to your liking. It's not exactly geared toward novice AddOn users, but it's pretty straightforward to use, so don't be scared off if this is your first AddOn. However, there are many action bar addons out there. I suggest you try several and choose the one that works best for you. Other popular action bar addons include:</p>
 <ul>
 <li>Bartender</li>
 <li>Flexbar</li>
@@ -28,6 +27,7 @@
 <li>InfiniBar</li>
 </ul>
 <p>Note that action bar addons are by nature generally incompatible with each other. Only enable one at a time! Most can currently be found on <a href="http://files.wowace.com">WowAce</a>, <a href="http://www.wowinterface.com">WoWInterface</a>, and/or <a href="http://wow.curse.com/downloads/">Curse Gaming</a>.</p>
+<p>ReAction is compatible with WoW 3.0.</p>
 
 <a name="credits"><h1>Credits</h1></a>
 <p>The WoW AddOn authoring community is rich and diverse. Virtually every successful addon author learns by reading, dissecting, and even pilfering (:-P) others' code. ReAction owes its existence to the following (in no particular order):</p>
@@ -44,7 +44,6 @@
 <li>jjsheets (InfiniBar and ButtonFacade)</li>
 <li>&lt;The Kids Are In Bed&gt; of US-Staghelm, my everynight crew and alpha testers</li>
 </ul>
-
 <p>ReAction embeds components of the Ace3 framework.</p>
 
 <a name="features"><h1>Features of ReAction</h1></a>
@@ -52,66 +51,104 @@
 <li>Unlimited number of bars</li>
 <li>Unlimited number of buttons per bar</li>
 <li>Arrange and resize bars, buttons, and grid layout with the mouse</li>
-<li>Keybinding assignments (left and right click) for each button</li>
-<li>Anchor bars to one another and to other UI elements</li>
-<li>Button support for actions, pet actions, micro menu, bags, stances, forms, stealth, shadowform, totems, auras, and aspects, as well as specialized buttons for macros, spells, and items</li>
+<li>Anchor bars to one another</li>
+<li>Support for actions and pet actions</li>
 <li>Paged actionbar and possess-target (e.g. Mind Control) support</li>
-<li>Dynamic bar effects, such as page transitions, show/hide, expand/collapse, etc, as allowed by the Blizzard secure API</li>
+<li>Dynamic bar effects, such as page transitions, show/hide, reposition, etc</li>
+<li>Modular extensible architecture</li>
+</ul>
+<p>The design goal of ReAction is to be user-friendly and feature-efficient. Useful features are incorporated: "fluff" is culled out. Many behaviors have been inspired by other addons. It is a work in progress.</p>
+
+<a name="plannedfeatures"><h2>Planned Features of Reaction</h2></a>
+<p>The following are planned upcoming features:</p>
+<ul>
+<li>Button support for micro menu and bag bars</li>
+<li>Button support with intelligent populating for forms, stealth, shadowform, totems, auras, presences, and aspects</li>
+<li>Non action-slot buttons which avoid the 120-action limitation, as an option</li>
+<li>Additional dynamic behaviors, including mouseover pop-up bars, stateful clicks, etc</li>
+<li>Manual page-switching controls for paged bars</li>
 <li>CyCircled and ButtonFacade compatibility (install CyCircled and/or ButtonFacade separately)</li>
-<li>Modular extensible architecture</li>
 <li>Optional FuBar configuration plugin</li>
+<li>More features for bar positioning and anchoring</li>
 </ul>
 
-<p>The design goal of ReAction is to be user-friendly and feature-efficient. Useful features are incorporated: "fluff" is culled out. Many behaviors have been inspired by other addons.</p>
-
-<a name="nonfeatures"><h1>NON-Features of ReAction</h1></a>
+<a name="nonfeatures"><h2>NON-Features of ReAction</h2></a>
 <ul>
 <li>There is no method of inserting custom user script hooks to button events. If you want to do that sort of thing you'll have to hack the code or write your own module.</li>
 <li>ReAction does not provide slash commands for most of its interface.</li>
-<li>ReAction does not integrate with Titan Panel.</li>
+<li>I'm not going to write a Titan panel plugin.</li>
 <li>There are no auto-layout options other than a grid layout.</li>
-<li>ReAction button keybindings cannot be configured via the standard Blizzard keybinding UI.</li>
+<li>If you hide the Blizzard main action bar, ReAction doesn't provide any replacement for the experience bar or "lag meter".</li>
+<li>No support for bar borders, backgrounds, or other textures.</li>
+<li>ReAction button keybindings are not configured via the standard Blizzard keybinding UI, and probably never will be.</li>
+<li>No plans to allow you to move around the default Blizzard buttons. ReAction makes and manages its own buttons, it doesn't swipe them from the default UI.</li>
 </ul>
-<p>None of these limitations are fundamental (except perhaps the keybindings). With the extensible architecture, an enterprising mod author could certainly write an addon module that extends ReAction's capabilities.</p>
+<p>Some of these (lack of an XP bar, for instance) are better served by other unrelated addons, as they're not action-button related. Others could be implemented by someone else as extensions.</p>
 
 <a name="usage"><h1>Using ReAction</h1></a>
 <h2>Installation</h2>
 <p>ReAction is distributed as a ZIP file: like all addons, just extract the archive contents into <b>World of Warcraft/Interface/Addons</b>. This will create a new folder called 'ReAction' in the Addons folder. Restart WoW, check that the AddOn is enabled in the character-select screen, and away you go.</p>
 <p>A common mistake is to extract the contents into a new folder named ReAction (thereby creating a doubly nested Addons/ReAction/ReAction), or to rename the ReAction folder. Make sure that the path to this README file is <b>World of Warcraft/Interface/Addons/ReAction/README.html</b>.</p>
 
+<h3>Modules</h3>
+<p>ReAction ships with several modules pre-installed. Each module is in fact a completely self-contained AddOn, and can be removed from the ReAction/modules/ folder and dragged up to the main AddOn level, if you like. This really isn't necessary, as the modules themselves don't consume many resources unless you're actually using their features, in which case they need to be installed anyway. But if you're into optimizing your personal AddOn footprint, you can promote the modules you aren't using. They're even marked Load-On-Demand, so if you decide to use their features later, they'll load automatically.</p>
+
 <h2>First Run</h2>
-<p>After ReAction is installed and enabled, and you've logged in to your WoW character, you probably won't notice anything different. If you have FuBar installed, the ReAction FuBar plugin will be visible on your FuBar. If not, you'll see a small ReAction button on your minimap.</p>
+<p>After ReAction is installed and enabled, and you've logged in to your WoW character, you probably won't notice anything different. ReAction assumes nothing about how you want to use it: by default, it does nothing at all. It doesn't make any new bars, and doesn't do anything to the standard Blizzard bars.</p>
 
-<p>ReAction assumes nothing about how you want to use it: by default, it does nothing at all. It doesn't make any new bars, and doesn't do anything to the standard Blizzard bars.</p>
+<p>To start, type /rxn (or /reaction) in the chat window. This will bring up the general ReAction dialogue window. You can also get there by hitting ESC to bring up the game menu, choose Interface Options, select the Addons tab, then select ReAction from the list.</p>
 
-<p>From here, you can either click the FuBar/minimap button, or type /rxn (or /reaction) in the chat window. This will bring up the general ReAction dialogue window. You can also get there by hitting ESC to bring up the game menu, choose Interface Options, select the Addons tab, then select ReAction from the list.</p>
+<p>The general configuration window only has a few options in it. The most interesting one is 'Hide Blizzard Action Bars'. Check that option, and *poof* your main action bar (as well as the extra bars and pet bar) disappears. Un-check it, and *whoosh* they're back. ReAction exists alongside the main action bar: you can use it with or without Blizzard's default UI. Note, however, that the default Blizzard UI makes use of all 120 action slots (depending on character class): therefore if you leave the Blizzard UI up, you'll almost certainly have to custom map the action slots of ReAction's buttons. For your first test-drive, I recommend hiding the Blizzard UI to start with a blank slate.</p>
 
-<p><b>Note:</b> By default, ReAction stores all settings per-character. Each character, when logged in for the first time using ReAction, will start from a blank slate. Use the Profile options to copy settings from one character to another. You can also use shared profiles, but note that ReAction also stores the keybindings with the profile, so toons using shared profiles will have shared keybinds (but different actions) on their buttons.</p>
-
-<p>To get started, click the button to launch the Bar Editor.</p>
+<p>To get started making some bars, click the button to launch the Bar Editor.</p>
 
 <h2>Laying Out Your Bars</h2>
+<p><b>Note:</b> By default, ReAction stores all settings per-character. Each character, when logged in for the first time using ReAction, will start from a blank slate. Use the Profile options to copy settings from one character to another. You can also use shared profiles, but note that ReAction also stores the keybindings with the profile, so toons using shared profiles will have shared keybinds (but different actions) on their buttons, <i>even if you select 'per-character bindings' in the Blizzard UI</i>.</p>
+
+<p>To create a bar, you'll need to open the ReAction Bar Editor dialogue, which is a separate window from the general configuration dialogue. To get there, either click the Edit Bars... button at the top of the main dialogue, or type /rxn edit. When the Bar Editor is open, you're in "config mode". You can drag the bars around the screen and resize them to your heart's content. This mode is automatically exited during combat. You can also toggle "config mode" <i>without</i> bringing up the Bar Editor dialog by typing /rxn config.</p>
+
+<p>Once in the Bar Editor, you can create bars, which are simply collections of buttons. Each bar has a number of buttons arranged in a grid, and must have a name. Set up these basic properties and create your bar.</p>
+
+<p>Now that you've got a bar, you can drag it around on screen to put it wherever you want. You can also change its grid layout, as follows:</p>
+<ul>
+<li>Drag on a corner to change the button size.</li>
+<li>Right-click drag on a corner to change the button spacing.</li>
+<li>Drag an edge to add/remove buttons to that edge.</li>
+</ul>
+
+<p>You can also right-click on a bar to configure its properties via the Bar Editor. Although a bar will work perfectly fine out of the box, the Bar Editor allows you to custom tune its behaviors and appearance.
+
+<h2>Key Bindings</h2>
+<p>To bind keys to your buttons, either type /rxn keybind or click the Key Bindings button in the main configuration dialogue. This will open a small window and outline all your ReAction buttons in green. To bind a key, hover your mouse over a button and press the key, or Escape to unbind the key from the button. Click the 'Save' button to exit keybinding mode. Note: keybinding changes are saved instantly and cannot be undone - the 'Revert' button doesn't actually do anything (yet).</p>
 
 <a name="config"><h2>Configuring Your Bars</h2></a>
+<h3>General Configuration</h3>
+<p>On the bar editor, each bar has several tabs to configure it. General configuration (e.g. position), action-type specific configuration (# pages, etc), and dynamic behavior. The general config options are pretty self-explanatory.</p>
 
-<h2>Key Bindings</h2>
+<h3>Action Buttons</h3>
+<p>Action buttons can be hidden when there's no action in that slot. They can also be re-mapped to the target's actions if you're currently Mind Controlling something. You can also configure the bar to have multiple 'pages', like the Blizzard UI's main bar. This won't actually have any effect unless you also specify dynamic page transitions, discussed in the next section. (manual page-switching controls coming later)</p>
 
-<h2>Dynamic Behavior</h2>
+<p>You can also specify the action-slot ID for each button, either individually or all at once via a string. WoW only gives you 120 action slots. Each button consumes one action slot <i>per page</i>, so if you have a 6-page 20-button bar, they're all used up. By default, ReAction allocates these action slots automatically so you don't have to worry about it. Just keep in mind that if you have more than 120 total slots in use, you'll get some repeats, which means that you'll have some buttons that mirror each other all the time. If you want to re-arrange the action slot ordering for whatever reason (for example, to keep the UIs on multiple computers in sync), you can do it in this panel. Or, if you want to have some intentional button mirrors (which would allow you to set up multiple keybindings for the same action, or show the same action in multiple pages or locations, without consuming multiple action slots), you can set it up that way too.</p>
+
+<h3>Pet Action Buttons</h3>
+<p>You can create a pet bar, which automatically shows itself when your pet is out. You can even create multiple pet bars, if you want, but there's not much motivation to do that. Pet buttons do not support multiple pages, and behave pretty much exactly like Blizzard's pet bar.</p>
+
+<h3>Dynamic Behavior</h3>
 <p>Dynamic bar behavior is a complex topic. At its most basic, you can have a bar switch between pages if you change stances/forms/stealth, just like the Blizzard UI does. For the more advanced user, multiple states can be created and transitions defined based on the available events as dictated by Blizzard's secure button API.</p>
 
 <p>To set up dynamic behavior for your bars, open the ReAction Bar Editor (either by shift-clicking the FuBar/minimap button, or from the Blizzard Interface/AddOns dialogue, or by typing /rxn edit in chat) and select the bar to work on. On the right panel, you'll see several tabs, one of which is labeled 'Dynamic State'. Choose that and you'll get a sub-panel with a state menu and more tabs.</p>
 
-<p><b>Example: </b> The simplest example is a bar that switches between three pages depending on a Warrior's stance. To set this up, create an action bar, give it three pages (see <a href="#config">Configuring Your Bars</a>), and then create three states for it. You can name those three states whatever you want, but for simplicity's sake, let's name them Battle, Defensive, and Berserker. Choose each state and select a page number (1,2, or 3) for it to display in the Properties tab. Then select the Rules tab, choose "when ALL of these" (or "when ANY of these"), and check the corresponding Battle/Defensive/Bersker checkbox for that state. NOTE: you can only do this on an actual Warrior character - other classes won't see the stance checkboxes, as they don't apply. That's it, you're done: the bar will now auto-switch between three pages when you change stances. Try it, and watch the actions on that bar change.</p>
+<p><b>Example: </b> The simplest example is a bar that switches between three pages depending on a Warrior's stance. To set this up, create an action bar, give it three pages (see <a href="#config">Configuring Your Bars</a>), and then create three states for it. You can name those three states whatever you want, but for simplicity's sake, let's name them Battle, Defensive, and Berserker. Choose each state and select a page number (1,2, or 3) for it to display in the Properties tab. Then select the Rules tab, choose "when ALL of these" (or "when ANY of these"), and check the corresponding Battle/Defensive/Bersker checkbox for that state. NOTE: you can only do this on an actual Warrior character - other classes won't see the stance checkboxes, as they don't apply, but you can do something similar for many classes). That's it, you're done: the bar will now auto-switch between three pages when you change stances. Try it, and watch the actions on that bar change (assuming, of course, that you've dropped abilities into those slots).</p>
 
 <p>Create states for your bar, and explore the various options of state properties and selection rules. State selection is evaluated in the order that the states are listed in the menu (top to bottom), so you may need to re-arrange them to get just the behavior that you want.</p>
 
-<p><b>How it works:</b> Every time the game registers an event of certain types (modifier key, stance/form/stealth change, target/focus change, party member change), it processes the list of states looking for a match, according to the selection rule. The first match becomes the new state immediately, and the state's properties are applied to the bar. The game also polls certain other conditions (anything that a macro can detect, see below) 5 times per second. So some state transitions will always be a tiny bit sluggish due to the architecture of the game.</p>
+<p><b>How it works:</b> Every time the game registers an event of certain types (modifier key, stance/form/stealth change, target/focus change, party member change), it processes the list of states looking for a match, according to the selection rule. The first match becomes the new state immediately, and the state's properties are applied to the bar. The game also polls certain other conditions (anything that a macro can detect, see below) 5 times per second. So some oddball state transitions will always be a tiny bit sluggish due to the architecture of the game.</p>
 
 <p>Remember: the more complicated you make your state-selectors, the more likely it is that you'll create a situation you didn't necessarily expect!</p>
 
 <p><b>Default State:</b> If you select one of your rules to be a default state in the Rules tab, then if none of the other states are true, it will be the state. If you don't have a default state, then if no rule is true, the bar will just stay in whatever condition it's already in. If you specify multiple states as default states, the highest-ranking one will be used.</p>
 
-<p><b>Custom Selection Rules:</b> Only the most common selection rules are included in the 'any' and 'all' checkbox listing. However, you can do almost anything that the game will allow you to via custom rules. Custom selection rules work exactly like a macro options clauses (in fact, they get passed to the same function, SecureOptionsCmdParse()). The following are all valid examples of what you can put in the Custom textbox:</p>
+<p><b>Custom Selection Rules:</b> Only the most common selection rules are included in the 'any' and 'all' checkbox listing. However, you can do almost anything that the game will allow you to via custom rules. Custom selection rules work exactly like a macro options clauses (in fact, they get passed to the same API function, SecureOptionsCmdParse()). The following are all valid examples of what you can put in the Custom textbox:</p>
 <ul style="font-family: Courier New, mono; font-size: 80%">
 <li> [harm] </li>
 <li> [mod:alt,help][notarget] </li>
@@ -122,11 +159,9 @@
 
 <p><b>Keybound States: </b> If you select 'via Keybinding' from the Selection Rule drop-down, then you can define a hotkey which will toggle the state on/off. When toggled on, the bar will remain in that state and <b>ignore</b> all other state transition events until you toggle it off. All keybound states default to 'off' when the UI reloads. It is possible to define multiple keybound states for a single bar. In that case, the keybound states will override each other.</p>
 
-<h2>Modules</h2>
-<p>ReAction ships with several modules pre-installed. Each module is in fact a completely self-contained AddOn, and can be removed from the ReAction/modules/ folder and dragged up to the main AddOn level, if you like. This really isn't necessary, as the modules themselves don't consume many resources unless you're actually using their features, in which case they need to be installed anyway. But if you're into optimizing your personal AddOn footprint, you can promote the modules you aren't using. They're even marked Load-On-Demand, so if you decide to use their features later, they'll load automatically.</p>
+<a name="license"><h1>License</h1></a>
+<p>ReAction is licenced under a variant of the MIT software licence, and is essentially freeware. See the <a href="LICENSE.txt">LICENSE.txt</a> file in this folder.</p>
+<p>ReAction embeds components of the Ace frameworks, which are covered under their own licenses.</p>
 
-<p>One module, the <b>FuBar_ReActionFu</b> plugin module, is instead installed by default as a peer-level folder to the main ReAction folder in the AddOns folder. If you don't like this sort of clutter in your AddOns folder, you can move it to the ReAction/modules folder. By default it's packaged separately because it's the one component that's fairly heavyweight relative to its utility, especially if you don't have FuBar installed.</p>
-
-<a name="license"><h1>License</h1></a>
 </body>
 </html>
--- a/ReAction.lua	Mon Oct 13 23:32:33 2008 +0000
+++ b/ReAction.lua	Wed Oct 15 16:29:41 2008 +0000
@@ -7,10 +7,10 @@
     - options
     - bar-type constructors
     
-  and publishes events when those collections change. It also implements a single property, 'config mode',
+  and publishes events when those collections change. It also implements a couple properties
   and has a couple convenience methods which drill down to particular modules.
   
-  Most of the "real work" of the addon happens in Bar.lua and the various modules.
+  Most of the "real work" of the addon happens in Bar.lua, Overlay.lua, State.lua, and the various modules.
 
   Events (with handler arguments):
   --------------------------------
@@ -226,7 +226,7 @@
         defaultBar = { }
       }
     }
-    -- default profile is character-specific
+    -- initial profile is character-specific
   )
   self.db.RegisterCallback(self,"OnProfileChanged")
   self.db.RegisterCallback(self,"OnProfileReset","OnProfileChanged")
@@ -347,6 +347,7 @@
   return bars[name]
 end
 
+-- returns pairs of name, bar
 function ReAction:IterateBars()
   return pairs(bars)
 end
@@ -459,7 +460,9 @@
       success, r = pcall(func, bar)
     end
     if success then
-      opts[module:GetName()] = { [module:GetName()] = r }
+      if r then
+        opts[module:GetName()] = { [module:GetName()] = r }
+      end
     else
       geterrorhandler()(r)
     end
--- a/ReAction.toc	Mon Oct 13 23:32:33 2008 +0000
+++ b/ReAction.toc	Wed Oct 15 16:29:41 2008 +0000
@@ -1,4 +1,4 @@
-## Interface: 20400
+## Interface: 30000
 ## Title: ReAction
 ## Notes: Action button layout and configuration
 ## DefaultState: enabled
--- a/State.lua	Mon Oct 13 23:32:33 2008 +0000
+++ b/State.lua	Wed Oct 15 16:29:41 2008 +0000
@@ -7,8 +7,9 @@
 local ReAction = ReAction
 local L = ReAction.L
 local _G = _G
+local format = string.format
 local InCombatLockdown = InCombatLockdown
-local format = string.format
+local RegisterStateDriver = RegisterStateDriver
 
 ReAction:UpdateRevision("$Revision$")
 
@@ -53,10 +54,205 @@
 end
 
 
-local InitRules, ApplyStates, SetProperty, GetProperty, RegisterProperty
+local InitRules, ApplyStates, CleanupStates, SetProperty, GetProperty, RegisterProperty, ShowAll
 
 -- PRIVATE --
 do
+
+  -- the field names must match the field names of the options table, below
+  -- the field values are secure snippets
+  local properties = { 
+    hide = 
+      [[
+        local h = hide and hide[state] and not showAll
+        if h ~= hidden then
+          if h then
+            self:Hide()
+          else
+            self:Show()
+          end
+          hidden = h
+        end
+      ]],
+
+    --keybindState  TODO: broken
+
+      -- the anchoring is handled in a special handler
+    anchorEnable = true,
+    --anchorFrame = true,  TODO: broken
+    anchorPoint = true,
+    anchorRelPoint = true,
+    anchorX = true,
+    anchorY = true,
+    enableScale = true,
+    scale = true,
+  }
+
+
+  --
+  -- Secure Handler Snippets
+  --
+  local SetHandlerData, SetStateDriver, SetStateKeybind, RefreshState
+  do
+    local stateHandler_propInit = 
+    [[
+      propfuncs = table.new()
+      local proplist = self:GetAttribute("prop-func-list")
+      for s in string.gmatch(proplist, "(%w+)") do
+        table.insert(propfuncs, s)
+      end
+    ]]
+
+    local onStateHandler = 
+    -- function _onstate-reaction( self, stateid, newstate )
+    [[
+      print("received state",newstate,"on bar",self:GetName())
+      set_state = newstate or set_state
+
+      local oldState = state
+      state = state_override or set_state or state
+
+      for i = 1, #propfuncs do
+        print("running state func",propfuncs[i])
+        control:RunAttribute("func-"..propfuncs[i])
+      end
+      
+      if anchorEnable and anchorEnable[state] ~= anchorstate then
+        anchorstate = anchorEnable[state]
+        control:RunAttribute("func-doanchor")
+      end
+
+      control:ChildUpdate()
+    ]]
+
+    local anchorHandler = 
+    -- function func-doanchor( self )
+    [[
+      -- TODO
+      if anchorstate then
+        -- TODO: get anchor data from state tables
+      else
+        -- TODO: get anchor data from defaults
+      end
+    ]]
+
+    local onClickHandler = 
+    -- function OnClick( self, button, down )
+    [[
+      if state_override == button then
+        state_override = nil -- toggle
+      else
+        state_override = button
+      end
+    ]] .. onStateHandler
+
+    local weak         = { __mode = "k" }
+    local statedrivers = setmetatable( { }, weak )
+    local keybinds     = setmetatable( { }, weak )
+
+    -- Construct a lua assignment as a code string and execute it within the header
+    -- frame's sandbox. 'value' must be a string, boolean, number, or nil. If called
+    -- with four arguments, then it treats 'varname' as an existing global table and
+    -- sets a key-value pair. For a slight efficiency boost, pass the values in as
+    -- attributes and fetch them as attributes from the snippet code, to leverage snippet
+    -- caching.
+    function SetHandlerData( bar, varname, value, key )
+      local f = bar:GetFrame()
+      f:SetAttribute("data-varname",varname)
+      f:SetAttribute("data-value",  value)
+      f:SetAttribute("data-key",    key)
+      f:Execute(
+        [[
+          local name  = self:GetAttribute("data-varname")
+          local value = self:GetAttribute("data-value")
+          local key   = self:GetAttribute("data-key")
+          if name then
+            if key then
+              if not _G[name] then
+                _G[name] = table.new()
+              end
+              _G[name][key] = value
+            else
+              _G[name] = value
+            end
+          end
+        ]])
+    end
+
+    function SetDefaultAnchor( bar )
+      local point, frame, relPoint, x, y = bar:GetAnchor()
+      SetHandlerData(bar, "defaultAnchor", point, "point")
+      SetHandlerData(bar, "defaultAnchor", relPoint, "relPoint")
+      SetHandlerData(bar, "defaultAnchor", x, "x")
+      SetHandlerData(bar, "defaultAnchor", y, "y")
+
+      if frame then
+        local f = bar:GetFrame()
+        f:SetFrameRef("defaultAnchor", f)
+        f:Execute(
+          [[
+            defaultAnchor.frame = self:GetAttribute("frameref-defaultAnchor")
+          ]])
+      end
+    end
+
+    function RefreshState( bar )
+      SetDefaultAnchor(bar)
+      bar:GetFrame():Execute([[control:RunAttribute("reaction-refresh")]])
+    end
+
+    function SetStateDriver( bar, rule )
+      local f = bar:GetFrame()
+
+      local props = { }
+      for p, h in pairs(properties) do
+        if type(h) == "string" then
+          table.insert(props,p)
+          f:SetAttribute("func-"..p, h)
+        end
+      end
+      f:SetAttribute("prop-func-list", table.concat(props," "))
+      f:Execute(stateHandler_propInit)
+      f:SetAttribute("reaction-refresh", onStateHandler)
+      f:SetAttribute("func-doanchor", anchorHandler)
+      
+      if rule and #rule > 0 then
+        f:SetAttribute( "_onstate-reaction", onStateHandler )
+        RegisterStateDriver(f, "reaction", rule)
+        statedrivers[bar] = rule
+      elseif statedrivers[bar] then
+        UnregisterStateDriver(f, "reaction")
+        f:SetAttribute( "_onstate-reaction", nil )
+        statedrivers[bar] = nil
+      end
+    end
+
+    function SetStateKeybind( bar, key, state )
+      local f = bar:GetFrame()
+
+      local kb = keybinds[bar]
+      if kb == nil then
+        if key == nil then
+          -- nothing to do
+          return
+        end
+        kb = { }
+        keybinds[bar] = kb
+      end
+
+      -- clear the old binding, if any
+      if kb[state] then
+        SetOverrideBinding(f, false, kb[state], nil)
+      end
+      kb[state] = key
+
+      if key then
+        f:SetAttribute("_onclick", onClickHandler)
+        SetOverrideBindingClick(f, false, key, state, nil) -- state name is the virtual mouse button
+      end
+    end
+  end
+
   -- As far as I can tell the macro clauses are NOT locale-specific.
   local ruleformats = { 
     stealth       = "stealth",
@@ -113,146 +309,10 @@
     ruleformats.moonkin   = format("form:%d",moonkin)
   end
 
-
-  -- state property functions
-  local ofskeys = {
-    anchorPoint = "point",
-    anchorRelPoint = "relpoint",
-    anchorX = "x",
-    anchorY = "y" 
-  }
-
-  local barofsidx = {
-    anchorPoint = 1,
-    anchorRelPoint = 3,
-    anchorX = 4,
-    anchorY = 5
-  }
-
-  local function UpdatePartialAnchor(bar, states, ckey)
-    local map = { }
-    local bc = bar.config
-    for state, c in pairs(states) do
-      if c.enableAnchor then
-        map[state] = c[ckey]
-      end
-    end
-    local ofskey = ofskeys[ckey]
-    local default = select(barofsidx[ckey], bar:GetAnchor())
-    bar:SetStateAttribute(format("headofs%s",ofskeys[ckey]), map, default)
-  end
-  
-  -- the table key name for each function maps to the name of the config element
-  local propertyFuncs = { 
-    hide = function( bar, states )
-      local hs = { }
-      for state, config in pairs(states) do
-        if config.hide then
-          table.insert(hs, state)
-        end
-      end
-      bar:GetButtonFrame():SetAttribute("hidestates", table.concat(hs,","))
-    end,
-
-    keybindstate = function( bar, states )
-      local map = { }
-      for state, config in pairs(states) do
-        local kbset = config.keybindstate and state
-        map[state] = kbset
-        for button in bar:IterateButtons() do
-          -- TODO: inform children they should maintain multiple binding sets
-          -- ?? button:UpdateBindingSet(kbset)
-        end
-      end
-      bar:SetStateAttribute("statebindings", map, true) -- apply to button frame, bindings only work for direct children
-    end,
-
-    enableAnchor = function( bar, states )
-      for ckey in pairs(ofskeys) do
-        UpdatePartialAnchor(bar, states, ckey)
-      end
-    end,
-
-    enableScale = function( bar, states )
-      local map = { }
-      for state, c in pairs(states) do
-        if c.enableScale then
-          map[state] = c.scale
-        end
-      end
-      bar:SetStateAttribute("headscale", map, 1.0)
-    end,
-  }
-
-  -- generate some table entries
-  propertyFuncs.scale = propertyFuncs.enableScale
-  for ckey in pairs(ofskeys) do
-    propertyFuncs[ckey] = function( bar, states )
-      UpdatePartialAnchor(bar, states, ckey)
-    end
-  end
-
-
-  function GetProperty( bar, state, propname )
-    return tfetch(module.db.profile.bars, bar:GetName(), "states", state, propname)
-  end
-
-  function SetProperty( bar, state, propname, value )
-    local states = tbuild(module.db.profile.bars, bar:GetName(), "states")
-    tbuild(states, state)[propname] = value
-    local f = propertyFuncs[propname]
-    if f then
-      f(bar, states)
-    end
-  end
-
-  function RegisterProperty( propname, f )
-    propertyFuncs[propname] = f
-    for bar in ReAction:IterateBars() do
-      local states = tfetch(module.db.profile.bars, bar:GetName(), "states")
-      if states then
-        f(bar,states)
-      end
-    end
-  end
-
-
-
-  --
-  -- Build a state-transition spec string and statemap to be passed to
-  -- Bar:SetStateDriver().
-  --
-  -- The statemap building is complex: keybound states override all 
-  -- other transitions, so must remain in their current state, but must
-  -- also remember other transitions that happen while they're stuck there
-  -- so that when the binding is toggled off it can return to the proper state
-  --
-  local function BuildStateMap(states)
+  local function BuildRule(states)
     local rules = { }
-    local statemap = { }
-    local keybinds = { }
     local default
 
-    -- first grab all the keybind override states
-    -- and construct an override template
-    local override
-    do
-      local overrides = { }
-      for name, state in pairs(states) do
-        local type = tfetch(state, "rule", "type")
-        if type == "keybind" then
-          -- use the state-stack to remember the current transition
-          -- use $s as a marker for a later call to gsub()
-          table.insert(overrides, format("%s:$s set() %s", name, name))
-        end
-      end
-      if #overrides > 0 then
-        table.insert(overrides, "") -- for a trailing ';'
-      end
-      override = table.concat(overrides, ";") or ""
-    end
-
-    -- now iterate the rules in order
     for idx, state in ipairs(fieldsort(states, "rule", "order")) do
       local c = states[state].rule
       local type = c.type
@@ -284,43 +344,83 @@
           end
         end
       end
-      
-      -- use a different virtual button for the actual keybind transition,
-      -- to implement a toggle. You have to clear it regardless of the type
-      -- (which is usually a no-op) to unbind state transitions when switching
-      -- transition types.
-      local bindbutton = format("%s_binding",state)
-      if type == "keybind" then
-        keybinds[bindbutton] = c.keybind or false
-        statemap[bindbutton] = format("%s:pop();*:set(%s)", state, state)
-      else
-        keybinds[bindbutton] = false
-      end
-
-      -- construct the statemap. gsub() the state name into the override template.
-      statemap[state] = format("%s%s", override:gsub("%$s",state), state)
     end
     -- make sure that the default, if any, is last
     if default then
       table.insert(rules, default)
     end
-    return table.concat(rules,";"), statemap, keybinds
+    return table.concat(rules,";")
+  end
+
+  local function BuildKeybinds( bar, states )
+    for name, state in pairs(states) do
+      local type = tfetch(state, "rule", "type")
+      if type == "keybind" then
+        local key = tfetch(state, "rule", "keybind")
+        SetStateKeybind(bar, key, name)
+      else
+        SetStateKeybind(bar, nil, name) -- this clears an existing keybind
+      end
+    end
+  end
+
+  function GetProperty( bar, state, propname )
+    return tfetch(module.db.profile.bars, bar:GetName(), "states", state, propname)
+  end
+
+  function SetProperty( bar, state, propname, value )
+    local s = tbuild(module.db.profile.bars, bar:GetName(), "states", state)
+    s[propname] = value
+    SetHandlerData(bar, propname, value, state)
+    RefreshState(bar)
+  end
+
+  function RegisterProperty( propname, snippet )
+    properties[propname] = snippet or true
+    print("registered property",propname)
+    for _, bar in ReAction:IterateBars() do
+      local states = tfetch(module.db.profile.bars, bar:GetName(), "states")
+      if states then
+        for name, s in pairs(states) do
+          SetHandlerData(bar, propname, s[propname], name)
+        end
+        SetStateDriver(bar, BuildRule(states))
+        RefreshState(bar)
+      end
+    end
+  end
+
+  function UnregisterProperty( propname )
+    properties[propname] = nil
+    for _, bar in ReAction:IterateBars() do
+      SetHandlerData(bar, propname, nil)
+      SetStateDriver(bar, BuildRule(states))
+      RefreshState(bar)
+    end
   end
 
   function ApplyStates( bar )
     local states = tfetch(module.db.profile.bars, bar:GetName(), "states")
     if states then
-      local rule, statemap, keybinds = BuildStateMap(states)
-      bar:SetStateDriver("reaction", rule, statemap)
-      for state, key in pairs(keybinds) do
-        bar:SetAttributeBinding(state, key, "state-reaction", state)
+      for propname in pairs(properties) do
+        for name, s in pairs(states) do
+          SetHandlerData(bar, propname, s[propname], name)
+        end
       end
-      for k, f in pairs(propertyFuncs) do
-        f(bar, states)
-      end
+      BuildKeybinds(bar, states)
+      SetStateDriver(bar, BuildRule(states))
+      RefreshState(bar)
     end
   end
 
+  function CleanupStates( bar )
+    SetStateDriver(bar, nil)
+  end
+
+  function ShowAll( bar, show )
+    SetHandlerData(bar, "showAll", show)
+    RefreshState(bar)
+  end
 end
 
 
@@ -342,6 +442,7 @@
   ReAction:RegisterBarOptionGenerator(self, "GetBarOptions")
 
   ReAction.RegisterCallback(self, "OnCreateBar","OnRefreshBar")
+  ReAction.RegisterCallback(self, "OnDestroyBar")
   ReAction.RegisterCallback(self, "OnRefreshBar")
   ReAction.RegisterCallback(self, "OnEraseBar")
   ReAction.RegisterCallback(self, "OnRenameBar")
@@ -351,7 +452,7 @@
 function module:PLAYER_AURAS_CHANGED()
   self:UnregisterEvent("PLAYER_AURAS_CHANGED")
   -- on login the number of stances is 0 until this event fires during the init sequence.
-  -- however if you reload just the UI the number of stances is correct immediately
+  -- however if you just reload the UI the number of stances is correct immediately
   -- and this event won't fire until you gain/lose buffs/debuffs, at which point you might
   -- be in combat.
   if not InCombatLockdown() then
@@ -369,6 +470,10 @@
   end
 end
 
+function module:OnDestroyBar(event, bar, name)
+  CleanupStates(bar)
+end
+
 function module:OnEraseBar(event, bar, name)
   self.db.profile.bars[name] = nil
 end
@@ -379,7 +484,11 @@
 end
 
 function module:OnConfigModeChanged(event, mode)
-  -- nothing to do (yet)
+  for name, bar in ReAction:IterateBars() do
+    if self.db.profile.bars[name] then
+      ShowAll(bar, mode)
+    end
+  end
 end
 
 
@@ -505,30 +614,40 @@
           set  = "SetProp",
           get  = "GetProp",
         },
-        keybindstate = {
+        --[[ BROKEN
+        keybindState = {
           name  = L["Override Keybinds"],
           desc  = L["Set this state to maintain its own set of keybinds which override the defaults when active"],
           order = 91,
           type  = "toggle",
           set   = "SetProp",
           get   = "GetProp",
-        },
+        }, ]]
         position = {
           name  = L["Position"],
           order = 92,
           type  = "group",
           inline = true,
           args = {
-            enableAnchor = {
+            anchorEnable = {
               name  = L["Set New Position"],
               order = 1,
               type  = "toggle",
               set   = "SetProp",
               get   = "GetProp",
             },
+            --[[ TODO: broken
+            anchorFrame = {
+              name   = L["Anchor Frame"],
+              order  = 2,
+              type   = "select",
+              values = "GetAnchorFrames",
+              set    = ???
+              get    = ???
+            },   ]]
             anchorPoint = {
               name  = L["Point"],
-              order = 2,
+              order = 3,
               type  = "select",
               values = pointTable,
               set   = "SetAnchorPointProp",
@@ -538,7 +657,7 @@
             },
             anchorRelPoint = {
               name  = L["Relative Point"],
-              order = 3,
+              order = 4,
               type  = "select",
               values = pointTable,
               set   = "SetAnchorPointProp",
@@ -548,7 +667,7 @@
             },
             anchorX = {
               name  = L["X Offset"],
-              order = 4,
+              order = 5,
               type  = "range",
               min   = -100,
               max   = 100,
@@ -560,7 +679,7 @@
             },
             anchorY = {
               name  = L["Y Offset"],
-              order = 5,
+              order = 6,
               type  = "range",
               min   = -100,
               max   = 100,
@@ -680,10 +799,35 @@
     },
   }
 
-  local StateHandler = { }
+  local handlers = { }
+  local meta = {
+    __index = function(self, key)
+      for _, h in pairs(handlers) do
+        if h[key] then
+          return h[key]
+        end
+      end
+    end,
+  }
+  local StateHandler = setmetatable({ }, meta)
+  local proto        = { __index = StateHandler }
+
+  function RegisterPropertyOptions( field, options, handler )
+    stateOptions.properties.plugins[field] = options
+    handlers[field] = handler
+  end
+
+  function UnregisterPropertyOptions( field )
+    stateOptions.properties.plugins[field] = nil
+    handlers[field] = nil
+  end
 
   function StateHandler:New( bar, opts )
-    local self = setmetatable({ bar = bar }, { __index = StateHandler })
+    local self = setmetatable(
+      { 
+        bar = bar 
+      }, 
+      proto )
 
     function self:GetName()
       return opts.name
@@ -700,10 +844,9 @@
     -- get reference to states table: even if the bar
     -- name changes the states table ref won't
     self.states = tbuild(module.db.profile.bars, bar:GetName(), "states")
+    self.state  = tbuild(self.states, opts.name)
 
-    tbuild(self.states, opts.name)
-
-    opts.order = self:GetRule("order")
+    opts.order = self:GetRuleField("order")
     if opts.order == nil then
       -- add after the highest
       opts.order = 100
@@ -713,7 +856,7 @@
           opts.order = x + 1
         end
       end
-      self:SetRule("order",opts.order)
+      self:SetRuleField("order",opts.order)
     end
 
     return self
@@ -721,12 +864,12 @@
 
   -- helper methods
 
-  function StateHandler:SetRule( key, value, ... )
-    tbuild(self.states, self:GetName(), "rule", ...)[key] = value
+  function StateHandler:SetRuleField( key, value, ... )
+    tbuild(self.state, "rule", ...)[key] = value
   end
 
-  function StateHandler:GetRule( ... )
-    return tfetch(self.states, self:GetName(), "rule", ...)
+  function StateHandler:GetRuleField( ... )
+    return tfetch(self.state, "rule", ...)
   end
 
   function StateHandler:FixAll( setkey )
@@ -735,7 +878,7 @@
     -- be chosen arbitrarily. Otherwise, if selecting a new checkbox from the field-set,
     -- it will be retained.
     local notified = false
-    if self:GetRule("type") == "all" then
+    if self:GetRuleField("type") == "all" then
       for _, c in ipairs(rules) do
         local rule, hidden, fields = unpack(c)
         local once = false
@@ -748,9 +891,9 @@
         end
         for idx, field in ipairs(fields) do
           local key = next(field)
-          if self:GetRule("values",key) then
+          if self:GetRuleField("values",key) then
             if once and key ~= setkey then
-              self:SetRule(key,false,"values")
+              self:SetRuleField(key,false,"values")
               if not setkey and not notified then
                 ReAction:UserError(L["Warning: one or more incompatible rules were turned off"])
                 notified = true
@@ -858,37 +1001,37 @@
   end
 
   function StateHandler:SetType(info, value)
-    self:SetRule("type", value)
+    self:SetRuleField("type", value)
     self:FixAll()
     ApplyStates(self.bar)
   end
 
   function StateHandler:GetType()
-    return self:GetRule("type")
+    return self:GetRuleField("type")
   end
 
   function StateHandler:GetClearAllDisabled()
-    local t = self:GetRule("type")
+    local t = self:GetRuleField("type")
     return not( t == "any" or t == "all" or t == "custom")
   end
 
   function StateHandler:ClearAllConditions()
-    local t = self:GetRule("type")
+    local t = self:GetRuleField("type")
     if t == "custom" then
-      self:SetRule("custom","")
+      self:SetRuleField("custom","")
     elseif t == "any" or t == "all" then
-      self:SetRule("values", {})
+      self:SetRuleField("values", {})
     end
     ApplyStates(self.bar)
   end
 
   function StateHandler:GetConditionsDisabled()
-    local t = self:GetRule("type")
+    local t = self:GetRuleField("type")
     return not( t == "any" or t == "all")
   end
 
   function StateHandler:SetCondition(info, key, value)
-    self:SetRule(ruleMap[key], value or nil, "values")
+    self:SetRuleField(ruleMap[key], value or nil, "values")
     if value then
       self:FixAll(ruleMap[key])
     end
@@ -896,20 +1039,20 @@
   end
 
   function StateHandler:GetCondition(info, key)
-    return self:GetRule("values", ruleMap[key]) or false
+    return self:GetRuleField("values", ruleMap[key]) or false
   end
 
   function StateHandler:GetCustomDisabled()
-    return self:GetRule("type") ~= "custom"
+    return self:GetRuleField("type") ~= "custom"
   end
 
   function StateHandler:SetCustomRule(info, value)
-    self:SetRule("custom",value)
+    self:SetRuleField("custom",value)
     ApplyStates(self.bar)
   end
 
   function StateHandler:GetCustomRule()
-    return self:GetRule("custom") or ""
+    return self:GetRuleField("custom") or ""
   end
 
   function StateHandler:ValidateCustomRule(info, value)
@@ -929,18 +1072,18 @@
   end
 
   function StateHandler:GetKeybindDisabled()
-    return self:GetRule("type") ~= "keybind"
+    return self:GetRuleField("type") ~= "keybind"
   end
 
   function StateHandler:GetKeybind()
-    return self:GetRule("keybind")
+    return self:GetRuleField("keybind")
   end
 
   function StateHandler:SetKeybind(info, value)
     if value and #value == 0 then
       value = nil
     end
-    self:SetRule("keybind",value)
+    self:SetRuleField("keybind",value)
     ApplyStates(self.bar)
   end
 
@@ -957,17 +1100,6 @@
     return opts
   end
 
-
-  function RegisterPropertyOptions( field, options, handler )
-    stateOptions.properties.plugins[field] = options
-    if handler then
-      for k,v in pairs(handler) do
-        StateHandler[k] = v
-      end
-    end
-  end
-
-
   function module:GetBarOptions(bar)
     local private = { }
     local states = tbuild(module.db.profile.bars, bar:GetName(), "states")
@@ -1033,23 +1165,23 @@
 
 -- Module API --
 
--- Pass in a property field-name, an implementation function, a static options table, and an 
+-- Pass in a property field-name, an implementation secure snippet, a static options table, and an 
 -- optional options handler method-table
 --
--- propertyImplFunc prototype:
---   propertyImplFunc( bar, stateTable )
---     where stateTable is a { ["statename"] = { state config } } table.
---
 -- The options table is static, i.e. not bar-specific and should only reference handler method
 -- strings (either existing ones or those added via optHandler). The existing options are ordered
 -- 90-99. Order #1 is reserved for the heading.
 --
--- The contents of optHandler, if provided, will be added to the existing StateHandler metatable.
+-- The contents of optHandler, if provided, will be added to the existing StateHandler options metatable.
 -- See above, for existing API. In particular see the properties set up in the New method: self.bar,
 -- self.states, and self:GetName(), and the generic property handlers self:GetProp() and self:SetProp().
 --
-function module:RegisterStateProperty( field, propertyImplFunc, options, optHandler )
-  RegisterProperty(field, propertyImplFunc)
+function module:RegisterStateProperty( field, snippetHandler, options, optHandler )
+  RegisterProperty(field, snippetHandler)
   RegisterPropertyOptions(field, options, optHandler)
 end
 
+function module:UnregisterStateProperty( field )
+  UnregisterProperty(field)
+  UnregisterPropertyOptions(field)
+end
--- a/lib/embeds.xml	Mon Oct 13 23:32:33 2008 +0000
+++ b/lib/embeds.xml	Wed Oct 15 16:29:41 2008 +0000
@@ -12,8 +12,4 @@
   <Include file="AceEvent-3.0\AceEvent-3.0.xml"/>
   <Include file="LibKeyBound-1.0\lib.xml"/>
 
-  <!-- for now -->
-  <Script file="AceLibrary\AceLibrary.lua"/>
-  <Script file="Dewdrop-2.0\Dewdrop-2.0.lua"/>
-
 </Ui>
--- a/locale/enUS.lua	Mon Oct 13 23:32:33 2008 +0000
+++ b/locale/enUS.lua	Wed Oct 15 16:29:41 2008 +0000
@@ -140,7 +140,7 @@
 "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.",
+"When possessing a target (e.g. via Mind Control), map the first 12 buttons of this bar to the possessed target's actions.",
 "Show Page #",
 "Action Buttons",
 
--- 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
--- a/modules/ReAction_ConfigUI/ReAction_ConfigUI.lua	Mon Oct 13 23:32:33 2008 +0000
+++ b/modules/ReAction_ConfigUI/ReAction_ConfigUI.lua	Wed Oct 15 16:29:41 2008 +0000
@@ -51,7 +51,7 @@
 end
 
 function module:OpenConfig()
-  InterfaceOptionsFrame_OpenToFrame(configName)
+  InterfaceOptionsFrame_OpenToCategory(configName)
 end
 
 function module:InitializeOptions()
--- a/modules/ReAction_PetAction/ReAction_PetAction.lua	Mon Oct 13 23:32:33 2008 +0000
+++ b/modules/ReAction_PetAction/ReAction_PetAction.lua	Wed Oct 15 16:29:41 2008 +0000
@@ -61,7 +61,9 @@
   if bar.config.type == moduleID then
     -- auto show/hide when pet exists
     bar:GetFrame():SetAttribute("unit","pet")
-    RegisterUnitWatch(bar:GetFrame())
+    if not ReAction:GetConfigMode() then
+      RegisterUnitWatch(bar:GetFrame())
+    end
     self:OnRefreshBar(event, bar, name)
   end
 end
@@ -192,11 +194,16 @@
 })
 
 local frameRecycler = {}
+local meta = { __index = Button }
 
 function Button:New( bar, idx, config )
   -- create new self
-  self = setmetatable( { }, { __index = Button } )
-  self.bar, self.idx, self.config = bar, idx, config
+  self = setmetatable( 
+    { 
+      bar = bar,
+      idx = idx,
+      config = config,
+    }, meta )
 
   local name = config.name or ("ReAction_%s_%s_%d"):format(bar:GetName(),moduleID,idx)
   config.name = name
@@ -205,10 +212,9 @@
   
   -- have to recycle frames with the same name:
   -- otherwise you either get references to old textures because named CreateFrame()
-  -- doesn't overwrite existing globals (below)
-  -- or, if you set them to nil in the global table, you get taint because of the
-  -- crappy PetActionBar code.
-  local parent = bar:GetButtonFrame()
+  -- doesn't overwrite existing globals. Can't set them to nil in the global table, 
+  -- as it causes taint.
+  local parent = bar:GetFrame()
   local f = frameRecycler[name]
   if f then
     f:SetParent(parent)
@@ -222,7 +228,7 @@
   self.frame = f
   self.icon = _G[("%sIcon"):format(name)]
   self.acTex = _G[("%sAutoCastable"):format(name)]
-  self.acModel = _G[("%sAutoCast"):format(name)]
+  self.acModel = _G[("%sShine"):format(name)]
   self.cooldown = _G[("%sCooldown"):format(name)]
   self.hotkey = _G[("%sHotKey"):format(name)]
 
@@ -249,7 +255,7 @@
       end
     end)
 
-  self.binder = ReAction:AttachBinder(self)
+  --self.binder = ReAction:AttachBinder(self)
 
   self:Refresh()
   return self