changeset 79:a45255f5d0c2

- Converted State options to use static options table + metatable handler - Fixed bug with FixAll() - Added external property handler registration - Removed pages handler (to be migrated to ReAction_Action)
author Flick <flickerstreak@gmail.com>
date Tue, 24 Jun 2008 23:39:43 +0000
parents 502cdb5666e2
children 42ec2938d65a
files State.lua
diffstat 1 files changed, 553 insertions(+), 470 deletions(-) [+]
line wrap: on
line diff
--- a/State.lua	Tue Jun 24 00:10:33 2008 +0000
+++ b/State.lua	Tue Jun 24 23:39:43 2008 +0000
@@ -53,7 +53,7 @@
 end
 
 
-local InitRules, ApplyStates, SetProperty, GetProperty
+local InitRules, ApplyStates, SetProperty, GetProperty, RegisterProperty
 
 -- PRIVATE --
 do
@@ -142,7 +142,7 @@
     bar:SetStateAttribute(format("headofs%s",ofskeys[ckey]), map, default)
   end
   
-  -- the name of the function maps to the name of the config element
+  -- the table key name for each function maps to the name of the config element
   local propertyFuncs = { 
     hide = function( bar, states )
       local hs = { }
@@ -154,16 +154,6 @@
       bar:SetStateAttribute("hidestates", nil, table.concat(hs,","), true) -- pass to buttons
     end,
 
-    page = function( bar, states )
-      local map = { }
-      for state, config in pairs(states) do
-        if config.page then
-          map[state] = format("page%d",config.page)
-        end
-      end
-      bar:SetStateAttribute("statebutton", map)
-    end,
-
     keybindstate = function( bar, states )
       local map = { }
       for state, config in pairs(states) do
@@ -216,6 +206,16 @@
     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
+
 
 
   --
@@ -379,19 +379,19 @@
 end
 
 function module:OnConfigModeChanged(event, mode)
-  -- TODO: unregister all state drivers (temporarily) and hidestates
+  -- nothing to do (yet)
 end
 
 
 
 -- Options --
 
-local CreateBarOptions
+local CreateBarOptions, RegisterPropertyOptions
 do
+  local playerClass = select(2, UnitClass("player"))
   local function ClassCheck(...)
     for i = 1, select('#',...) do
-      local _, c = UnitClass("player")
-      if c == select(i,...) then
+      if playerClass == select(i,...) then
         return false
       end
     end
@@ -442,465 +442,532 @@
     end
   end
 
+  local stateOptions = {
+    ordering = {
+      name = L["Info"],
+      order = 1,
+      type = "group",
+      args = {
+        delete = {
+          name = L["Delete this State"],
+          order = -1,
+          type = "execute",
+          func = "DeleteState",
+        },
+        rename = {
+          name = L["Name"],
+          order = 1,
+          type = "input",
+          get  = "GetName",
+          set  = "SetStateName",
+          pattern = "^%w*$",
+          usage = L["State names must be alphanumeric without spaces"],
+        },
+        ordering = {
+          name = L["Evaluation Order"],
+          desc = L["State transitions are evaluated in the order listed:\nMove a state up or down to change the order"],
+          order = 2,
+          type = "group",
+          inline = true,
+          args = {
+            up = {
+              name  = L["Up"],
+              order = 1,
+              type  = "execute",
+              width = "half",
+              func  = "MoveStateUp",
+            },
+            down = {
+              name  = L["Down"],
+              order = 2,
+              type  = "execute",
+              width = "half",
+              func  = "MoveStateDown",
+            }
+          }
+        }
+      }
+    },
+    properties = {
+      name = L["Properties"],
+      order = 2,
+      type = "group",
+      args = { 
+        desc = {
+          name = L["Set the properties for the bar when in this state"],
+          order = 1,
+          type = "description"
+        },
+        hide = {
+          name = L["Hide Bar"],
+          order = 91,
+          type = "toggle",
+          set  = "SetProp",
+          get  = "GetProp",
+        },
+        keybindstate = {
+          name  = L["Override Keybinds"],
+          desc  = L["Set this state to maintain its own set of keybinds which override the defaults when active"],
+          order = 92,
+          type  = "toggle",
+          set   = "SetProp",
+          get   = "GetProp",
+        },
+        position = {
+          name  = L["Position"],
+          order = 93,
+          type  = "group",
+          inline = true,
+          args = {
+            enableAnchor = {
+              name  = L["Set New Position"],
+              order = 1,
+              type  = "toggle",
+              set   = "SetProp",
+              get   = "GetProp",
+            },
+            anchorPoint = {
+              name  = L["Point"],
+              order = 2,
+              type  = "select",
+              values = pointTable,
+              set   = "SetAnchorPointProp",
+              get   = "GetAnchorPointProp",
+              disabled = "GetAnchorDisabled",
+              hidden = "GetAnchorDisabled",
+            },
+            anchorRelPoint = {
+              name  = L["Relative Point"],
+              order = 3,
+              type  = "select",
+              values = pointTable,
+              set   = "SetAnchorPointProp",
+              get   = "GetAnchorPointProp",
+              disabled = "GetAnchorDisabled",
+              hidden = "GetAnchorDisabled",
+            },
+            anchorX = {
+              name  = L["X Offset"],
+              order = 4,
+              type  = "range",
+              min   = -100,
+              max   = 100,
+              step  = 1,
+              set   = "SetProp",
+              get   = "GetProp",
+              disabled = "GetAnchorDisabled",
+              hidden = "GetAnchorDisabled",
+            },
+            anchorY = {
+              name  = L["Y Offset"],
+              order = 5,
+              type  = "range",
+              min   = -100,
+              max   = 100,
+              step  = 1,
+              set   = "SetProp",
+              get   = "GetProp",
+              disabled = "GetAnchorDisabled",
+              hidden = "GetAnchorDisabled",
+            },
+          },
+        },
+        scale = {
+          name  = L["Scale"],
+          order = 94,
+          type  = "group",
+          inline = true,
+          args = {
+            enableScale = {
+              name  = L["Set New Scale"],
+              order = 1,
+              type  = "toggle",
+              set   = "SetProp",
+              get   = "GetProp",
+            },
+            scale = {
+              name  = L["Scale"],
+              order = 2,
+              type  = "range",
+              min   = 0.1,
+              max   = 2.5,
+              step  = 0.05,
+              isPercent = true,
+              set   = "SetProp",
+              get   = "GetProp",
+              disabled = "GetScaleDisabled",
+              hidden = "GetScaleDisabled",
+            },
+          },
+        },
+      },
+      plugins = { }
+    },
+    rules = {
+      name   = L["Rule"],
+      order  = 3,
+      type   = "group",
+      args   = {
+        mode = {
+          name   = L["Select this state"],
+          order  = 2,
+          type   = "select",
+          style  = "radio",
+          values = { 
+            default = L["by default"], 
+            any = L["when ANY of these"], 
+            all = L["when ALL of these"], 
+            custom = L["via custom rule"],
+            keybind = L["via keybinding"],
+          },
+          set    = "SetType",
+          get    = "GetType",
+        },
+        clear = {
+          name     = L["Clear All"],
+          order    = 3,
+          type     = "execute",
+          hidden   = "GetClearAllDisabled",
+          disabled = "GetClearAllDisabled",
+          func     = "ClearAllConditions",
+        },
+        inputs = {
+          name     = L["Conditions"],
+          order    = 4,
+          type     = "multiselect",
+          hidden   = "GetConditionsDisabled",
+          disabled = "GetConditionsDisabled",
+          values   = ruleSelect,
+          set      = "SetCondition",
+          get      = "GetCondition",
+        },
+        custom = {
+          name = L["Custom Rule"],
+          order = 5,
+          type = "input",
+          multiline = true,
+          hidden = "GetCustomDisabled",
+          disabled = "GetCustomDisabled",
+          desc = L["Syntax like macro rules: see preset rules for examples"],
+          set  = "SetCustomRule",
+          get  = "GetCustomRule",
+          validate = "ValidateCustomRule",
+        },
+        keybind = {
+          name = L["Keybinding"],
+          order = 6,
+          inline = true,
+          hidden = "GetKeybindDisabled",
+          disabled = "GetKeybindDisabled",
+          type = "group",
+          args = {
+            desc = {
+              name = L["Invoking a state keybind toggles an override of all other transition rules."],
+              order = 1,
+              type = "description",
+            },
+            keybind = {
+              name = L["State Hotkey"],
+              desc = L["Define an override toggle keybind"],
+              order = 2,
+              type = "keybinding",
+              set  = "SetKeybind",
+              get  = "GetKeybind",
+            },
+          },
+        },
+      },
+    },
+  }
+
+  local StateHandler = { }
+
+  function StateHandler:New( bar, opts )
+    local self = setmetatable({ bar = bar }, { __index = StateHandler })
+
+    function self:GetName()
+      return opts.name
+    end
+
+    function self:SetName(name)
+      opts.name = name
+    end
+
+    function self:GetOrder()
+      return opts.order
+    end
+
+    -- 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")
+
+    tbuild(self.states, opts.name)
+
+    opts.order = self:GetRule("order")
+    if opts.order == nil then
+      -- add after the highest
+      opts.order = 100
+      for _, state in pairs(self.states) do
+        local x = tonumber(tfetch(state, "rule", "order"))
+        if x and x >= opts.order then
+          opts.order = x + 1
+        end
+      end
+      self:SetRule("order",opts.order)
+    end
+
+    return self
+  end
+
+  -- helper methods
+
+  function StateHandler:SetRule( key, value, ... )
+    tbuild(self.states, self:GetName(), "rule", ...)[key] = value
+  end
+
+  function StateHandler:GetRule( ... )
+    return tfetch(self.states, self:GetName(), "rule", ...)
+  end
+
+  function StateHandler:FixAll( setkey )
+    -- if multiple selections in the same group are chosen when 'all' is selected,
+    -- keep only one of them. If changing the mode, the first in the fields list will 
+    -- 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
+      for _, c in ipairs(rules) do
+        local rule, hidden, fields = unpack(c)
+        local once = false
+        if setkey then
+          for idx, field in ipairs(fields) do
+            if next(field) == setkey then
+              once = true
+            end
+          end
+        end
+        for idx, field in ipairs(fields) do
+          local key = next(field)
+          if self:GetRule("values",key) then
+            if once and key ~= setkey then
+              self:SetRule(key,false,"values")
+              if not setkey and not notified then
+                ReAction:UserError(L["Warning: one or more incompatible rules were turned off"])
+                notified = true
+              end
+            end
+            once = true
+          end
+        end
+      end
+    end
+  end
+
+  function StateHandler:GetNeighbors()
+    local before, after
+    for k, v in pairs(self.states) do
+      local o = tonumber(tfetch(v, "rule", "order"))
+      if o and k ~= self:GetName() then
+        local obefore = tfetch(self.states,before,"rule","order")
+        local oafter  = tfetch(self.states,after,"rule","order")
+        if o < self:GetOrder() and (not obefore or obefore < o) then
+          before = k
+        end
+        if o > self:GetOrder() and (not oafter or oafter > o) then
+          after = k
+        end
+      end
+    end
+    return before, after
+  end
+
+  function StateHandler:SwapOrder( a, b )
+    -- do options table
+    local args = optionMap[self.bar].args
+    args[a].order, args[b].order = args[b].order, args[a].order
+    -- do profile
+    a = tbuild(self.states, a, "rule")
+    b = tbuild(self.states, b, "rule")
+    a.order, b.order = b.order, a.order
+  end
+
+  -- handler methods 
+
+  function StateHandler:GetProp( info )
+    -- gets property of the same name as the options arg
+    return GetProperty(self.bar, self:GetName(), info[#info])
+  end
+
+  function StateHandler:SetProp( info, value )
+    -- sets property of the same name as the options arg
+    SetProperty(self.bar, self:GetName(), info[#info], value)
+  end
+
+  function StateHandler:DeleteState()
+    if self.states[self:GetName()] then
+      self.states[self:GetName()] = nil
+      ApplyStates(self.bar)
+    end
+    optionMap[self.bar].args[self:GetName()] = nil
+  end
+
+  function StateHandler:SetStateName(info, value)
+    -- check for existing state name
+    if self.states[value] then
+      ReAction:UserError(format(L["State named '%s' already exists"],value))
+      return
+    end
+    local args = optionMap[self.bar].args
+    local name = self:GetName()
+    self.states[value], args[value], self.states[name], args[name] = self.states[name], args[name], nil, nil
+    self:SetName(value)
+    ApplyStates(self.bar)
+  end
+
+  function StateHandler:MoveStateUp()
+    local before, after = self:GetNeighbors()
+    if before then
+      self:SwapOrder(before, self:GetName())
+      ApplyStates(self.bar)
+    end
+  end
+
+  function StateHandler:MoveStateDown()
+    local before, after = self:GetNeighbors()
+    if after then
+      self:SwapOrder(self:GetName(), after)
+      ApplyStates(self.bar)
+    end
+  end
+
+  function StateHandler:GetAnchorDisabled()
+    return not GetProperty(self.bar, self:GetName(), "enableAnchor")
+  end
+
+  function StateHandler:SetAnchorPointProp(info, value)
+    self:SetProp(info, value ~= "NONE" and value or nil)
+  end
+
+  function StateHandler:GetAnchorPointProp(info)
+    return self:GetProp(info) or "NONE"
+  end
+
+  function StateHandler:GetScaleDisabled()
+    return not GetProperty(self.bar, self:GetName(), "enableScale")
+  end
+
+  function StateHandler:SetType(info, value)
+    self:SetRule("type", value)
+    self:FixAll()
+    ApplyStates(self.bar)
+  end
+
+  function StateHandler:GetType()
+    return self:GetRule("type")
+  end
+
+  function StateHandler:GetClearAllDisabled()
+    local t = self:GetRule("type")
+    return not( t == "any" or t == "all" or t == "custom")
+  end
+
+  function StateHandler:ClearAllConditions()
+    local t = self:GetRule("type")
+    if t == "custom" then
+      self:SetRule("custom","")
+    elseif t == "any" or t == "all" then
+      self:SetRule("values", {})
+    end
+    ApplyStates(self.bar)
+  end
+
+  function StateHandler:GetConditionsDisabled()
+    local t = self:GetRule("type")
+    return not( t == "any" or t == "all")
+  end
+
+  function StateHandler:SetCondition(info, key, value)
+    self:SetRule(ruleMap[key], value or nil, "values")
+    if value then
+      self:FixAll(ruleMap[key])
+    end
+    ApplyStates(self.bar)
+  end
+
+  function StateHandler:GetCondition(info, key)
+    return self:GetRule("values", ruleMap[key]) or false
+  end
+
+  function StateHandler:GetCustomDisabled()
+    return self:GetRule("type") ~= "custom"
+  end
+
+  function StateHandler:SetCustomRule(info, value)
+    self:SetRule("custom",value)
+    ApplyStates(self.bar)
+  end
+
+  function StateHandler:GetCustomRule()
+    return self:GetRule("custom") or ""
+  end
+
+  function StateHandler:ValidateCustomRule(info, value)
+    local s = value:gsub("%s","") -- remove all spaces
+    -- unfortunately %b and captures don't support the '+' notation, or this would be considerably simpler
+    repeat
+      if s == "" then
+        return true
+      end
+      local c, r = s:match("(%b[])(.*)")
+      if c == nil and s and #s > 0 then
+        return format(L["Invalid custom rule '%s': each clause must appear within [brackets]"],value or "")
+      end
+      s = r
+    until c == nil
+    return true
+  end
+
+  function StateHandler:GetKeybindDisabled()
+    return self:GetRule("type") ~= "keybind"
+  end
+
+  function StateHandler:GetKeybind()
+    return self:GetRule("keybind")
+  end
+
+  function StateHandler:SetKeybind(info, value)
+    if value and #value == 0 then
+      value = nil
+    end
+    self:SetRule("keybind",value)
+    ApplyStates(self.bar)
+  end
+
   local function CreateStateOptions(bar, name)
     local opts = { 
       type = "group",
       name = name,
       childGroups = "tab",
+      args = stateOptions
     }
 
-    local states = tbuild(module.db.profile.bars, bar:GetName(), "states")
+    opts.handler = StateHandler:New(bar,opts)
 
-    local function update()
-      ApplyStates(bar)
-    end
-
-    local function setrule( key, value, ... )
-      tbuild(states, opts.name, "rule", ...)[key] = value
-    end
-
-    local function getrule( ... )
-      return tfetch(states, opts.name, "rule", ...)
-    end
-
-    local function setprop(info, value)
-      SetProperty(bar, opts.name, info[#info], value)
-    end
-
-    local function getprop(info)
-      return GetProperty(bar, opts.name, info[#info])
-    end
-
-    local function fixall(setkey)
-      -- if multiple selections in the same group are chosen when 'all' is selected,
-      -- keep only one of them. If changing the mode, the first in the fields list will 
-      -- be chosen arbitrarily. Otherwise, if selecting a new checkbox from the field-set,
-      -- it will be retained.
-      local notified = false
-      for _, c in ipairs(rules) do
-        local rule, hidden, fields = unpack(c)
-        local found = false
-        for key in ipairs(fields) do
-          if getrule("values",key) then
-            if (found or setkey) and key ~= setkey then
-              setrule(key,false,"values")
-              if not setkey and not notified then
-                ReAction:UserError(L["Warning: one or more incompatible rules were turned off"])
-                notified = true
-              end
-            end
-            found = true
-          end
-        end
-      end
-    end
-
-    local function getNeighbors()
-      local before, after
-      for k, v in pairs(states) do
-        local o = tonumber(tfetch(v, "rule", "order"))
-        if o and k ~= opts.name then
-          local obefore = tfetch(states,before,"rule","order")
-          local oafter  = tfetch(states,after,"rule","order")
-          if o < opts.order and (not obefore or obefore < o) then
-            before = k
-          end
-          if o > opts.order and (not oafter or oafter > o) then
-            after = k
-          end
-        end
-      end
-      return before, after
-    end
-
-    local function swapOrder( a, b )
-      -- do options table
-      local args = optionMap[bar].args
-      args[a].order, args[b].order = args[b].order, args[a].order
-      -- do profile
-      a = tbuild(states, a, "rule")
-      b = tbuild(states, b, "rule")
-      a.order, b.order = b.order, a.order
-    end
-
-    local function anchordisable()
-      return not GetProperty(bar, opts.name, "enableAnchor")
-    end
-
-    tbuild(states, name)
-
-    opts.order = getrule("order")
-    if opts.order == nil then
-      -- add after the highest
-      opts.order = 100
-      for _, state in pairs(states) do
-        local x = tonumber(tfetch(state, "rule", "order"))
-        if x and x >= opts.order then
-          opts.order = x + 1
-        end
-      end
-      setrule("order",opts.order)
-    end
-
-    opts.args = {
-      ordering = {
-        name = L["Info"],
-        order = 1,
-        type = "group",
-        args = {
-          delete = {
-            name = L["Delete this State"],
-            order = -1,
-            type = "execute",
-            func = function(info) 
-                if states[opts.name] then
-                  states[opts.name] = nil
-                  ApplyStates(bar)
-                end
-                optionMap[bar].args[opts.name] = nil
-              end,
-          },
-          rename = {
-            name = L["Name"],
-            order = 1,
-            type = "input",
-            get  = function() return opts.name end,
-            set  = function(info, value) 
-                     -- check for existing state name
-                     if states[value] then
-                       format(L["State named '%s' already exists"],value)
-                     end
-                     local args = optionMap[bar].args
-                     states[value], args[value], states[opts.name], args[opts.name] = states[opts.name], args[opts.name], nil, nil
-                     opts.name = value
-                     update()
-                   end, 
-            pattern = "^%w*$",
-            usage = L["State names must be alphanumeric without spaces"],
-          },
-          ordering = {
-            name = L["Evaluation Order"],
-            desc = L["State transitions are evaluated in the order listed:\nMove a state up or down to change the order"],
-            order = 2,
-            type = "group",
-            inline = true,
-            args = {
-              up = {
-                name  = L["Up"],
-                order = 1,
-                type  = "execute",
-                width = "half",
-                func  = function()
-                          local before, after = getNeighbors()
-                          if before then
-                            swapOrder(before, opts.name)
-                            update()
-                          end
-                        end,
-              },
-              down = {
-                name  = L["Down"],
-                order = 2,
-                type  = "execute",
-                width = "half",
-                func  = function() 
-                          local before, after = getNeighbors()
-                          if after then
-                            swapOrder(opts.name, after)
-                            update()
-                          end
-                        end,
-              }
-            }
-          }
-        }
-      },
-      properties = {
-        name = L["Properties"],
-        order = 2,
-        type = "group",
-        args = { 
-          desc = {
-            name = L["Set the properties for the bar when in this state"],
-            order = 1,
-            type = "description"
-          },
-          hide = {
-            name = L["Hide Bar"],
-            order = 2,
-            type = "toggle",
-            set  = setprop,
-            get  = getprop,
-          },
-          page = {
-            name  = L["Show Page #"],
-            order = 3,
-            type  = "select",
-            disabled = function()
-                         --return bar:GetNumPages() < 2
-                         return true
-                       end,
-            hidden   = function()
-                         --return bar:GetNumPages() < 2
-                         return true
-                       end,
-            values   = function()
-                         -- use off-by-one ordering to put (none) first in the list
-                         local pages = { [1] = L["(none)"] }
-                         --for i = 1, bar:GetNumPages() do
-                         --  pages[i+1] = i
-                         --end
-                         return pages
-                       end,
-            set      = function(info, value)
-                         value = value - 1
-                         setprop(info, value > 0 and value or nil)
-                       end,
-            get      = function(info)
-                         return getprop(info) or L["(none)"]
-                       end,
-          },
-          keybindstate = {
-            name  = L["Override Keybinds"],
-            desc  = L["Set this state to maintain its own set of keybinds which override the defaults when active"],
-            order = 4,
-            type  = "toggle",
-            set   = setprop,
-            get   = getprop,
-          },
-          position = {
-            name  = L["Position"],
-            order = 5,
-            type  = "group",
-            inline = true,
-            args = {
-              enableAnchor = {
-                name  = L["Set New Position"],
-                order = 1,
-                type  = "toggle",
-                set   = setprop,
-                get   = getprop,
-              },
-              anchorPoint = {
-                name  = L["Point"],
-                order = 2,
-                type  = "select",
-                values = pointTable,
-                set   = function(info, value) setprop(info, value ~= "NONE" and value or nil) end,
-                get   = function(info) return getprop(info) or "NONE" end,
-                disabled = anchordisable,
-                hidden = anchordisable,
-              },
-              anchorRelPoint = {
-                name  = L["Relative Point"],
-                order = 3,
-                type  = "select",
-                values = pointTable,
-                set   = function(info, value) setprop(info, value ~= "NONE" and value or nil) end,
-                get   = function(info) return getprop(info) or "NONE" end,
-                disabled = anchordisable,
-                hidden = anchordisable,
-              },
-              anchorX = {
-                name  = L["X Offset"],
-                order = 4,
-                type  = "range",
-                min   = -100,
-                max   = 100,
-                step  = 1,
-                set   = setprop,
-                get   = getprop,
-                disabled = anchordisable,
-                hidden = anchordisable,
-              },
-              anchorY = {
-                name  = L["Y Offset"],
-                order = 5,
-                type  = "range",
-                min   = -100,
-                max   = 100,
-                step  = 1,
-                set   = setprop,
-                get   = getprop,
-                disabled = anchordisable,
-                hidden = anchordisable,
-              },
-            },
-          },
-          scale = {
-            name  = L["Scale"],
-            order = 6,
-            type  = "group",
-            inline = true,
-            args = {
-              enableScale = {
-                name  = L["Set New Scale"],
-                order = 1,
-                type  = "toggle",
-                set   = setprop,
-                get   = getprop,
-              },
-              scale = {
-                name  = L["Scale"],
-                order = 2,
-                type  = "range",
-                min   = 0.1,
-                max   = 2.5,
-                step  = 0.05,
-                isPercent = true,
-                set   = setprop,
-                get   = function(info) return getprop(info) or 1 end,
-                disabled = function() return not GetProperty(bar, opts.name, "enableScale") end,
-                hidden = function() return not GetProperty(bar, opts.name, "enableScale") end,
-              },
-            },
-          },
-        },
-      },
-      rules = {
-        name   = L["Rule"],
-        order  = 3,
-        type   = "group",
-        args   = {
-          mode = {
-            name   = L["Select this state"],
-            order  = 2,
-            type   = "select",
-            style  = "radio",
-            values = { 
-              default = L["by default"], 
-              any = L["when ANY of these"], 
-              all = L["when ALL of these"], 
-              custom = L["via custom rule"],
-              keybind = L["via keybinding"],
-            },
-            set    = function( info, value )
-                       setrule("type", value)
-                       fixall()
-                       update()
-                     end,
-            get    = function( info )
-                       return getrule("type")
-                     end,
-          },
-          clear = {
-            name     = L["Clear All"],
-            order    = 3,
-            type     = "execute",
-            hidden   = function()
-                         local t = getrule("type")
-                         return t ~= "any" and t ~= "all"
-                       end,
-            disabled = function()
-                         local t = getrule("type")
-                         return t ~= "any" and t ~= "all"
-                       end,
-            func     = function()
-                         local type = getrule("type")
-                         if type == "custom" then
-                           setrule("custom","")
-                         elseif type == "any" or type == "all" then
-                           setrule("values", {})
-                         end
-                         update()
-                       end,
-          },
-          inputs = {
-            name     = L["Conditions"],
-            order    = 4,
-            type     = "multiselect",
-            hidden   = function()
-                         local t = getrule("type")
-                         return t ~= "any" and t ~= "all"
-                       end,
-            disabled = function()
-                         local t = getrule("type")
-                         return t ~= "any" and t ~= "all"
-                       end,
-            values   = ruleSelect,
-            set      = function(info, key, value )
-                         setrule(ruleMap[key], value or nil, "values")
-                         if value then
-                           fixall(ruleMap[key])
-                         end
-                         update()
-                       end,
-            get      = function(info, key)
-                         return getrule("values", ruleMap[key]) or false
-                       end,
-          },
-          custom = {
-            name = L["Custom Rule"],
-            order = 5,
-            type = "input",
-            multiline = true,
-            hidden = function()
-                       return getrule("type") ~= "custom"
-                     end,
-            disabled = function()
-                         return getrule("type") ~= "custom"
-                       end,
-            desc = L["Syntax like macro rules: see preset rules for examples"],
-            set  = function(info, value) 
-                     setrule("custom",value)
-                     update()
-                   end,
-            get  = function(info)
-                     return getrule("custom") or ""
-                   end,
-            validate = function (info, rule)
-                local s = rule:gsub("%s","") -- remove all spaces
-                -- unfortunately %b and captures don't support the '+' notation, or this would be considerably simpler
-                repeat
-                  if s == "" then
-                    return true
-                  end
-                  local c, r = s:match("(%b[])(.*)")
-                  if c == nil and s and #s > 0 then
-                    return format(L["Invalid custom rule '%s': each clause must appear within [brackets]"],rule)
-                  end
-                  s = r
-                until c == nil
-                return true
-              end,
-          },
-          keybind = {
-            name = L["Keybinding"],
-            order = 6,
-            inline = true,
-            hidden = function() return getrule("type") ~= "keybind" end,
-            disabled = function() return getrule("type") ~= "keybind" end,
-            type = "group",
-            args = {
-              desc = {
-                name = L["Invoking a state keybind toggles an override of all other transition rules."],
-                order = 1,
-                type = "description",
-              },
-              keybind = {
-                name = L["State Hotkey"],
-                desc = L["Define an override toggle keybind"],
-                order = 2,
-                type = "keybinding",
-                set  = function(info, value)
-                         if value and #value == 0 then
-                           value = nil
-                         end
-                         setrule("keybind",value)
-                         update()
-                       end,
-                get  = function() return getrule("keybind") end,
-              },
-            },
-          },
-        },
-      },
-    }
     return opts
   end
 
 
-  CreateBarOptions = function(bar)
+  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")
     local options = {
@@ -954,17 +1021,33 @@
         }
       }
     }
-    local states = tfetch(module.db.profile.bars, bar:GetName(), "states")
-    if states then
-      for name, config in pairs(states) do
-        options.args[name] = CreateStateOptions(bar,name)
-      end
+    for name, config in pairs(states) do
+      options.args[name] = CreateStateOptions(bar,name)
     end
     optionMap[bar] = options
     return options
   end
 end
 
-function module:GetBarOptions(bar)
-  return CreateBarOptions(bar)
+-- Module API --
+
+-- Pass in a property field-name, an implementation function, 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
+-- 91-100. Order #1 is reserved for the heading.
+--
+-- The contents of optHandler, if provided, will be added to the existing StateHandler 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)
+  RegisterPropertyOptions(field, options, optHandler)
 end
+