changeset 64:2000f4f4c6af

Redesigned state interface. The only thing missing now is the actual state properties.
author Flick <flickerstreak@gmail.com>
date Wed, 28 May 2008 00:20:04 +0000
parents 768be7eb22a0
children 5ea65ec7d162
files locale/enUS.lua modules/ReAction_State/ReAction_State.lua
diffstat 2 files changed, 513 insertions(+), 543 deletions(-) [+]
line wrap: on
line diff
--- a/locale/enUS.lua	Thu May 22 22:02:08 2008 +0000
+++ b/locale/enUS.lua	Wed May 28 00:20:04 2008 +0000
@@ -101,78 +101,58 @@
 "Use the mouse to arrange and resize the bars on screen. Tooltips on bars indicate additional functionality.",
 
 -- modules/ReAction_State
-"Dynamic State",
-"Hide Bar",
-"Hide the bar while in this state",
-"Select State (%s):",
-"Key Binding",
-"Delete this State",
-"Select Rule Type",
-"Delete this Rule",
-"Show Rule String",
-"Toggles display of the raw rule string",
-"Rule String",
-"Warrior Stance",
+"State named '%s' already exists",
 "Battle Stance",
 "Defensive Stance",
 "Berserker Stance",
-"Druid Form",
-"Normal",
-"Bear",
-"Cat",
+"Caster Form",
+"Bear Form",
+"Cat Form",
 "Tree/Moonkin",
-"Caster",
 "Stealth",
+"No Stealth",
 "Shadowform",
-"Pet",
+"No Shadowform",
+"With Pet",
 "Without Pet",
-"With Pet",
-"Target",
-"No Target",
 "Hostile Target",
 "Friendly Target",
-"Default",
-"Focus",
-"No Focus",
+"No Target",
 "Hostile Focus",
 "Friendly Focus",
-"In Group",
+"No Focus",
+"Raid",
+"Party",
 "Solo",
-"Party",
-"Raid",
-"Key Press",
-"On Key Press",
-"Combat",
 "In Combat",
 "Out of Combat",
-"Custom",
-"Rule",
-"Syntax like macro conditions: see preset rules for examples",
-"States",
+"Warning: one or more incompatible rules were turned off",
+"Properties",
+"Delete this State",
+"Name",
+"Evaluation Order",
+"State transitions are evaluated in the order listed:\nMove a state up or down to change the order",
+"Up",
+"Down",
+"Rules",
+"Select this state",
+"by default",
+"when ANY of these",
+"when ALL of these",
+"via custom rule",
+"Clear All",
+"Custom Rule",
+"Syntax like macro rules: see preset rules for examples",
+"Invalid custom rule '%s': each clause must appear within [brackets]",
+"Dynamic State",
+"States are evaluated in the order they are listed",
 "New State...",
+"State Name",
 "Set a name for the new state",
-"State Name",
+"State names must be alphanumeric without spaces",
 "Create State",
-"Options",
-"Transition Rules",
-"New Rule...",
-"Rule Name",
-"Set a name for the new transition rule",
-"Create Rule",
-"Presets",
-"Presets are canned sets of states and transitions. You can create your own presets to add to ReAction's built in defaults.",
-"Select Preset",
-"Load",
-"Delete",
-"Save As...",
 "State named '%s' already exists",
-"Rule named '%s' already exists",
-"State names must be alphanumeric without spaces",
-"Rule names must be alphanumeric without spaces",
-"Invalid custom rule '%s': Rule cannot end with ';'",
-"Invalid custom rule '%s': Expressions must be separated by ';'",
-"Invalid custom rule '%s': Each expression must have a state",
-"Invalid custom rule '%s': '%s' is not a state",
+
 
 }) do
   L[string] = true
--- a/modules/ReAction_State/ReAction_State.lua	Thu May 22 22:02:08 2008 +0000
+++ b/modules/ReAction_State/ReAction_State.lua	Wed May 28 00:20:04 2008 +0000
@@ -20,49 +20,43 @@
     {
       profile = { 
         bars = { },
-        presets = { }
       }
     }
   )
-  self.states = { }
-  self.options = setmetatable({},{__mode="k"})
+
+  ReAction:RegisterBarOptionGenerator(self, "GetBarOptions")
+
+  ReAction.RegisterCallback(self, "OnCreateBar","OnRefreshBar")
+  ReAction.RegisterCallback(self, "OnRefreshBar")
+  ReAction.RegisterCallback(self, "OnEraseBar")
+  ReAction.RegisterCallback(self, "OnRenameBar")
+  ReAction.RegisterCallback(self, "OnConfigModeChanged")
 end
 
-
-
--- ReAction module interface
-function module:ApplyToBar(bar)
-  self:RefreshBar(bar)
-end
-
-function module:RefreshBar(bar)
-  local c = self.db.profile.bars[bar:GetName()]
+function module:OnRefreshBar(event, bar, name)
+  local c = self.db.profile.bars[name]
   if c then
-    --self:BuildStates(bar)
-    --self:BuildRules(bar)
+    self:UpdateStates(bar)
   end
 end
 
-function module:RemoveFromBar(bar)
+function module:OnEraseBar(event, bar, name)
+  self.db.profile.bars[name] = nil
 end
 
-function module:EraseBarConfig(barName)
-  self.db.profile.bars[barName] = nil
-end
-
-function module:RenameBarConfig(oldname, newname)
+function module:OnRenameBar(event, bar, oldname, newname)
   local b = self.db.profile.bars
   bars[newname], bars[oldname] = bars[oldname], nil
 end
 
-function module:ApplyConfigMode(mode,bars)
-  -- swap out hidestates
+function module:OnConfigModeChanged(event, mode)
+  -- TODO: unregister all state drivers (temporarily) and hidestates
 end
 
 
 
 
--- Private --
+-- Utility --
 
 -- traverse a table tree by key list and fetch the result or first nil
 local function tfetch(t, ...)
@@ -82,9 +76,158 @@
   return t
 end
 
+-- PRIVATE --
+local BuildRuleString, ApplyStates
+do
+  local forms = { }
+  for i = 1, GetNumShapeshiftForms() do
+    local icon, name = GetShapeshiftFormInfo(i)
+    forms[name] = i;
+  end
+    -- TODO: need to find out if form name is localized, it probably is
+  local dStance = forms["Defensive Stance"] or 2
+  local zStance = forms["Berserker Stance"] or 3
+  local bForm   = forms["Dire Bear Form"] or forms["Bear Form"] or 1
+  local cForm   = forms["Cat Form"] or 3
+  local tForm   = forms["Tree of Life"] or forms["Moonkin Form"] or 5
+  local aForm   = forms["Aquatic Form"] or 2
+  local trForm  = forms["Travel Form"] or 4
+  local fForm   = forms["Flight Form"] or forms["Swift Flight Form"] or 6
 
-local rules
-local BuildRuleString
+  -- TODO: do the macro conditional strings need to be localized?
+  --       they're not contained in GlobalStrings.lua, but the parsing is done
+  --       via the C function SecureCmdOptionParse(), so it's not clear
+  local ruleformats = { 
+    battle        = "stance:1",
+    defensive     = ("stance:%d"):format(dStance),
+    berserker     = ("stance:%d"):format(zStance),
+    caster        = ("form:0/%d/%d/%d"):format(aForm, trForm, fForm),
+    bear          = ("form:%d"):format(bForm),
+    cat           = ("form:%d"):format(cForm),
+    treeOrMoonkin = ("form:%d"):format(tForm),
+    stealth       = "stealth",
+    nostealth     = "nostealth",
+    shadowform    = "form:1",
+    noshadowform  = "noform",
+    pet           = "pet",
+    nopet         = "nopet",
+    harm          = "target=target,harm",
+    help          = "target=target,help",
+    notarget      = "target=target,noexists",
+    focusharm     = "target=focus,harm",
+    focushelp     = "target=focus,help",
+    nofocus       = "target=focus,noexists",
+    raid          = "group:raid",
+    party         = "group:party",
+    solo          = "nogroup",
+    combat        = "combat",
+    nocombat      = "nocombat",
+  }
+
+  function BuildRuleString(states)
+    local s = ""
+    local default
+    local sorted = { }
+    for name in pairs(states) do
+      table.insert(sorted,name)
+    end
+    table.sort(sorted, function(lhs, rhs)
+        local olhs = tfetch(states[lhs],"rule","order") or 0
+        local orhs = tfetch(states[rhs],"rule","order") or 0
+        return olhs < orhs
+      end)
+    for idx, name in ipairs(sorted) do
+      local state = states[name]
+      local semi = #s > 0 and "; " or ""
+      local mode = tfetch(state,"rule","type")
+      if mode == "default" then
+        default = name
+      elseif mode == "custom" then
+        if state.rule.custom then
+          -- strip out all spaces from the custom rule
+          s = ("%s%s%s %s"):format(s, semi, state.rule.custom:gsub("%s",""), name)
+        end
+      elseif mode == "any" then
+        if state.rule.values then
+          local clause = ""
+          for key, value in pairs(state.rule.values) do
+            clause = ("%s[%s]"):format(clause,ruleformats[key])
+          end
+          if #clause > 0 then
+            s = ("%s%s%s %s"):format(s, semi, clause, name)
+          end
+        end
+      elseif mode == "all" then
+        if state.rule.values then
+          local clause = ""
+          for key, value in pairs(state.rule.values) do
+            clause = ("%s%s%s"):format(clause,#clause > 0 and "," or "", ruleformats[key])
+          end
+          if #clause > 0 then
+            s = ("%s%s[%s] %s"):format(s, semi, clause, name)
+          end
+        end
+      end
+    end
+    if default then
+      s = ("%s%s%s"):format(s, #s > 0 and "; " or "", default)
+    end
+    return s
+  end
+
+  local drivers = setmetatable({},{__mode="k"})
+
+  function ApplyStates( bar )
+    local states = tfetch(module.db.profile.bars, bar:GetName(), "states")
+    if states then
+      local frame = bar:GetFrame()
+      local string = BuildRuleString(states)
+      ReAction:Print("'"..string.."'")
+      if string and #string > 0 then
+        -- register a handler to set the value of attribute "state-reaction" 
+        -- in response to events as per the rule string
+        RegisterStateDriver(frame, "reaction", string)
+        drivers[bar] = true
+        -- register a trivial map for each "statemap-reaction-XXX" to set 'state' to 'XXX'
+        for state in pairs(states) do
+          frame:SetAttribute(("statemap-reaction-%s"):format(state), state)
+        end
+      elseif drivers[bar] then
+        UnregisterStateDriver(frame, "reaction")
+        drivers[bar] = nil
+      end
+    end
+  end
+end
+
+
+
+-- API --
+
+function module:UpdateStates( bar )
+  ApplyStates(bar)
+end
+
+function module:CreateState( bar, name )
+  local states = tbuild(self.db.profile.bars, bar:GetName(), "states")
+  if states[name] then
+    ReAction:UserError(L["State named '%s' already exists"]:format(name))
+  else
+    states[name] = { }
+  end
+end
+
+function module:DeleteState( bar, name )
+  local states = tfetch(self.db.profile.bars, bar:GetName(), "states")
+  if states[name] then
+    states[name] = nil
+    ApplyStates(bar)
+  end
+end
+
+-- Options --
+
+local CreateBarOptions
 do
   local function ClassCheck(...)
     for i = 1, select('#',...) do
@@ -96,516 +239,359 @@
     return true
   end
 
-  -- The structure of this table is important: each row is designed to be unpack()ed
-  -- into variables as defined in the header comment row.
-  -- The 'fields' subtable (index 4) structure is also important: its subtables 
-  -- are ordered to match appearances of '%s' (string-substitutions) in the corresponding
-  -- rules[] format-string entry. Each subtable's single element's key is the storage key used
-  -- in the user db, and the name of the group-element table in the config tree, so it too
-  -- is important.
-  --
-  -- While this allows for very compact code and data storage, the scheme is admittedly obtuse,
-  -- so I document it here for my own sanity.
-  --
-  rules = {
-    --  rule         name                 hidden                          fields
-    { "stance",  L["Warrior Stance"], ClassCheck("WARRIOR"),          { {battle = L["Battle Stance"]}, {defensive = L["Defensive Stance"]}, {berserker = L["Berserker Stance"]} } },
-    { "form",    L["Druid Form"],     ClassCheck("DRUID"),            { {bear = L["Bear"]}, {cat = L["Cat"]}, {treeOrMoonkin = L["Tree/Moonkin"]}, {caster = L["Normal"]},  } },
-    { "stealth", L["Stealth"],        ClassCheck("ROGUE","DRUID"),    { {stealth = L["Stealth"]}, {normal = L["Normal"]} } },
-    { "shadow",  L["Shadowform"],     ClassCheck("PRIEST"),           { {shadowform = L["Shadowform"]}, {normal = L["Normal"]} } },
-    { "pet",     L["Pet"],            ClassCheck("HUNTER","WARLOCK"), { {pet = L["With Pet"]}, {nopet = L["Without Pet"]} } },
-    { "target",  L["Target"],         false,                          { {hostile = L["Hostile Target"]}, {friendly = L["Friendly Target"]}, {none = L["No Target"]} } },
-    { "focus",   L["Focus"],          false,                          { {hostile = L["Hostile Focus"]}, {friendly = L["Friendly Focus"]}, {none = L["No Focus"]} } },
-    { "group",   L["In Group"],       false,                          { {raid = L["Raid"]}, {party = L["Party"]}, {solo = L["Solo"]} } },
-    { "key",     L["Key Press"],      false,                          { {state = L["On Key Press"]}, {keybinding = false} } },  -- keybinding has no state-selector, it's implemented elsewhere
-    { "combat",  L["Combat"],         false,                          { {combat = L["In Combat"]}, {nocombat = L["Out of Combat"]} } },
-    { "custom",  L["Custom"],         false,                          { {rule = false} } }, -- custom has no state-selector, it's implemented elsewhere
+  -- pre-sorted by the order they should appear in
+  local rules = {
+    --  rule          hidden                          fields
+    { "stance",  ClassCheck("WARRIOR"),          { {battle = L["Battle Stance"]}, {defensive = L["Defensive Stance"]}, {berserker = L["Berserker Stance"]} } },
+    { "form",    ClassCheck("DRUID"),            { {caster = L["Caster Form"]}, {bear = L["Bear Form"]}, {cat = L["Cat Form"]}, {treeOrMoonkin = L["Tree/Moonkin"]}  } },
+    { "stealth", ClassCheck("ROGUE","DRUID"),    { {stealth = L["Stealth"]}, {nostealth = L["No Stealth"]} } },
+    { "shadow",  ClassCheck("PRIEST"),           { {shadowform = L["Shadowform"]}, {noshadowform = L["No Shadowform"]} } },
+    { "pet",     ClassCheck("HUNTER","WARLOCK"), { {pet = L["With Pet"]}, {nopet = L["Without Pet"]} } },
+    { "target",  false,                          { {harm = L["Hostile Target"]}, {help = L["Friendly Target"]}, {notarget = L["No Target"]} } },
+    { "focus",   false,                          { {focusharm = L["Hostile Focus"]}, {focushelp = L["Friendly Focus"]}, {nofocus = L["No Focus"]} } },
+    { "group",   false,                          { {raid = L["Raid"]}, {party = L["Party"]}, {solo = L["Solo"]} } },
+    { "combat",  false,                          { {combat = L["In Combat"]}, {nocombat = L["Out of Combat"]} } },
   }
 
-  do
-    local forms = { }
-    for i = 1, GetNumShapeshiftForms() do
-      local icon, name = GetShapeshiftFormInfo(i)
-      -- TODO: need to find out if name is localized, it probably is
-      forms[name] = i;
-    end
-    local dStance = forms["Defensive Stance"] or 2
-    local zStance = forms["Berserker Stance"] or 3
-    local bForm   = forms["Dire Bear Form"] or forms["Bear Form"] or 1
-    local cForm   = forms["Cat Form"] or 3
-    local tForm   = forms["Tree of Life"] or forms["Moonkin Form"] or 5
+  local ruleSelect = { }
+  local ruleMap    = { }
+  local optionMap  = setmetatable({},{__mode="k"})
 
-    -- TODO: do the macro conditional strings need to be localized?
-    local ruleformats = { 
-      stance  = ("[stance:1] %%s; [stance:%d] %%s; [stance:%d] %%s"):format(dStance,zStance),
-      form    = ("[form:%d] %%s; [form:%d] %%s; [form:%d] %%s; %%s"):format(bForm,cForm,tForm),
-      stealth = "[stealth] %s; %s",
-      shadow  = "[stance:1] %s; %s",
-      pet     = "[pet] %s; %s",
-      target  = "[harm] %s; [help] %s; %s",
-      focus   = "[target=focus,harm] %s; [target=focus,help] %s; %s",
-      group   = "[group:raid] %s; [group] %s; %s",
-      combat  = "[combat] %s; %s",
-      custom  = "%s",
-    }
-
-    local fieldmap = { }
-    for i = 1, #rules do
-      fieldmap[rules[i][1]] = rules[i][4]
-    end
-
-    local _scratch = { }
-    function BuildRuleString(bar, name)
-      local rule, data = module:GetRule(bar,name)
-      if not rule then return "" end
-      local fields = fieldmap[rule]
-      for i = 1, #fields do
-        _scratch[i] = data[next(fields[i])] -- TODO: insert default state here
+  -- unpack rules table into ruleSelect and ruleMap
+  for _, c in ipairs(rules) do
+    local rule, hidden, fields = unpack(c)
+    if not hidden then
+      for _, field in ipairs(fields) do
+        local key, label = next(field)
+        table.insert(ruleSelect, label)
+        table.insert(ruleMap, key)
       end
-      for i = #fields+1, #_scratch do
-        _scratch[i] = nil
-      end
-      local success, value = pcall(string.format, ruleformats[rule], unpack(_scratch))
-      return success and value or "<error>"
     end
   end
 
-
-end
-
-
-
--- API --
-
-function module:BuildStates( bar )
-  local c = tfetch(self.db.profile.bars, bar:GetName(), "states")
-  if c then
-    for name, s in pairs(c) do
-      -- TODO: new state here
-    end
-  end
-end
-
-function module:CreateState( bar, name )
-  local c = tbuild(self.db.profile.bars, bar:GetName(), "states")
-  if c[name] then
-    ReAction:UserError(L["State named '%s' already exists"]:format(name))
-  else
-    c[name] = { }
-    -- TODO: new state here
-  end
-end
-
-function module:DeleteState( bar, name )
-  local c = tfetch(self.db.profile.bars, bar:GetName(), "states")
-  if c[name] then
-    -- TODO: delete state
-    c[name] = nil
-  end
-end
-
-function module:BuildRules( bar )
-  for bar, c in pairs(self.db.profile.bars) do
-    if c.rules then
-      for name, t in pairs(c.rules) do
-        local rule, config = next(t)
-        self:SetRule(bar, name, rule, config)
-      end
-    end
-  end
-end
-
-function module:CreateRule( bar, name, rule, config )
-  local c = tbuild(self.db.profile.bars, bar:GetName(), "rules")
-  if c[name] then
-    ReAction:UserError(L["Rule named '%s' already exists"]:format(name))
-  else
-    tbuild(self.db.profile.bars, bar:GetName(), "rules", name)
-    if rule then
-      self:SetRule(bar,name,rule,config)
-    end
-  end
-end
-
-function module:DeleteRule( bar, name )
-  local c = tfetch(self.db.profile.bars, bar:GetName(), "rules")
-  if c[name] then
-    local f = bar:GetFrame()
-    -- TODO: delete rule
-    c[name] = nil
-  end
-end
-
-function module:UpdateRule( bar, name )
-  local rule, c = self:GetRule(bar,name)
-  -- TODO: remove all relevant outdated attributes
-  -- TODO: set new attributes
-end
-
-function module:GetRule(bar, name)
-  local c = tfetch(self.db.profile.bars, bar:GetName(), "rules", name)
-  if c then
-    return next(c) -- returns key, value (= rulename, configtable)
-  end
-end
-
-function module:SetRule(bar, name, rule, config)
-  tbuild(self.db.profile.bars, bar:GetName(), "rules")[name] = { [rule] = (config or {}) }
-  self:UpdateRule(bar,name)
-end
-
-
--- options --
-local CreateBarOptions
-do
-  local function GetRuleConfig(bar, name, rule, field)
-    return tfetch(module.db.profile.bars, bar:GetName(), "rules", name, rule, field)
-  end
-
-  local function SetRuleConfig( bar, name, rule, field, value )
-    tbuild(module.db.profile.bars, bar:GetName(), "rules", name, rule)[field] = value
-  end
-
   local function CreateStateOptions(bar, name)
-    return {
-      type = "group",
-      name = name,
-      args = {
-        -- show/hide would go here
-        -- page # would go here
-        -- anchoring would go here
-        __delete__ = {
-          type = "execute",
-          name = L["Delete this State"],
-          func = function(info) 
-              module:DeleteState(bar,name) 
-              module.options[bar].args.states.args[name] = nil
-            end,
-          order = -1
-        },
-      }
-    }
-  end
-
-
-  -- display rule string setting is shared between all rule opts and is transient
-  -- (mostly used for debugging)
-  local display = { show = false }
-
-  local function CreateRuleOptions(bar, name)
-    local function get(info)
-      local rule  = info[#info-1]
-      local field = info[#info]
-      return GetRuleConfig(bar,name,rule,field) or ""
-    end
-
-    local function set(info, value)
-      local rule  = info[#info-1]
-      local field = info[#info]
-      SetRuleConfig(bar,name,rule,field,value)
-      module:UpdateRule(bar,name)
-    end
-
     local opts = { 
       type = "group",
       name = name,
-      childGroups = "inline",
-      args = {
-        __select__ = {
-          type = "select",
-          name = L["Select Rule Type"],
-          get  = function(info) return module:GetRule(bar,name) or "" end,
-          set  = function(info,value) module:SetRule(bar,name,value) end, -- TODO: get default rule config and pass as final value
-          values = function()
-              local v = { }
-              for i = 1, #rules do
-                local rule, name, hidden = unpack(rules[i])
-                if not hidden then
-                  v[rule] = name
-                end
-              end
-              return v
-            end,
-          order = 1,
-        },
-        __delete__ = {
-          type = "execute",
-          name = L["Delete this Rule"],
-          func = function(info) 
-              module:DeleteRule(bar,name) 
-              module.options[bar].args.rules.args[name] = nil
-            end,
-          order = -3
-        },
-        --
-        -- rule selection groups will be inserted here
-        --
-        __show__ = {
-          type = "toggle",
-          name = L["Show Rule String"],
-          desc = L["Toggles display of the raw rule string"],
-          get  = function() return display.show end,
-          set  = function(info,value) display.show = value end,
-          order = -2
-        },
-        __rule__ = {
-          type = "input",
-          name = L["Rule String"],
-          disabled = true,
-          multiline = true,
-          width = "double",
-          hidden = function() return not display.show end,
-          get = function() return BuildRuleString(bar,name) end,
-          set = function() end,
-          order = -1
-        }
-      }
+      childGroups = "tab",
     }
 
-    -- unpack rules table
-    for i = 1, #rules do
-      local rule, label, _, fields = unpack(rules[i])
-      local hidden = function() 
-        return module:GetRule(bar,name) ~= rule 
-      end
-      opts.args[rule] = {
-        type = "group",
-        name = label,
-        hidden = hidden,
-        disabled = hidden,
-        inline = true,
-        args = { }
-      }
-      for j = 1, #fields do
-        local field, label = next(fields[j]) -- extract from table of single key-value pair
-        if field and label then
-          opts.args[rule].args[field] = {
-            type = "select",
-            name = L["Select State (%s):"]:format(label),
-            values = function ()
-                local states = tfetch(module.db.profile.bars,bar:GetName(),"states")
-                local v = { }
-                if states then
-                  for k in pairs(states) do
-                    v[k] = k
-                  end
-                end
-                return v
-              end,
-            set  = set,
-            get  = get,
-            order = 100 + j
-          }
+    local states = tbuild(module.db.profile.bars, bar:GetName(), "states")
+
+    local function put( key, value, ... )
+      tbuild(states, opts.name, "rule", ...)[key] = value
+    end
+
+    local function fetch( ... )
+      return tfetch(states, opts.name, "rule", ...)
+    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 fetch("values",key) then
+            if (found or setkey) and key ~= setkey then
+              put(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
 
-    -- set up special entry for keybinding
-    opts.args.key.args.binding = {
-      type = "keybinding",
-      name = L["Key Binding"],
-      get = get,
-      set = set,
-      order = -1
+    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 update()
+      module:UpdateStates(bar)
+    end
+
+
+    opts.order = fetch("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
+      put("order",opts.order)
+    end
+
+    opts.args = {
+      properties = {
+        type = "group",
+        name = L["Properties"],
+        order = 1,
+        args = {
+          delete = {
+            type = "execute",
+            name = L["Delete this State"],
+            func = function(info) 
+                module:DeleteState(bar,opts.name)
+                optionMap[bar].args[opts.name] = nil
+              end,
+            order = -1
+          },
+          rename = {
+            type = "input",
+            name = L["Name"],
+            order = 1,
+            get  = function() return opts.name end,
+            set  = function(info, value) 
+                     -- check for existing state name
+                     if states[value] then
+                       L["State named '%s' already exists"]:format(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 = {
+            type = "group",
+            inline = true,
+            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,
+            args = {
+              up = {
+                type  = "execute",
+                name  = L["Up"],
+                width = "half",
+                order = 1,
+                func  = function()
+                          local before, after = getNeighbors()
+                          if before then
+                            swapOrder(before, opts.name)
+                            update()
+                          end
+                        end,
+              },
+              down = {
+                type  = "execute",
+                name  = L["Down"],
+                width = "half",
+                order = 2,
+                func  = function() 
+                          local before, after = getNeighbors()
+                          if after then
+                            ReAction:Print(opts.name, after)
+                            swapOrder(opts.name, after)
+                            update()
+                          end
+                        end,
+              }
+            }
+          },
+          -- keybinding for show-this-state would go here
+          -- show/hide would go here
+          -- page # would go here
+          -- anchoring would go here
+        }
+      },
+      rules = {
+        type   = "group",
+        name   = L["Rules"],
+        order  = 2,
+        args   = {
+          mode = {
+            type   = "select",
+            style  = "radio",
+            name   = L["Select this state"],
+            values = { 
+              default = L["by default"], 
+              any = L["when ANY of these"], 
+              all = L["when ALL of these"], 
+              custom = L["via custom rule"] 
+            },
+            set    = function( info, value )
+                       put("type", value)
+                       fixall()
+                       update()
+                     end,
+            get    = function( info )
+                       return fetch("type")
+                     end,
+            order  = 2
+          },
+          clear = {
+            type     = "execute",
+            name     = L["Clear All"],
+            func     = function()
+                         local type = fetch("type")
+                         if type == "custom" then
+                           put("custom","")
+                         elseif type == "any" or type == "all" then
+                           put("values", {})
+                         end
+                         update()
+                       end,
+            order    = 3
+          },
+          inputs = {
+            type     = "multiselect",
+            name     = L["Rules"],
+            hidden   = function()
+                         return fetch("type") == "custom"
+                       end,
+            disabled = function()
+                         return fetch("type") == "default"
+                       end,
+            values   = ruleSelect,
+            set      = function(info, key, value )
+                         put(ruleMap[key], value or nil, "values")
+                         if value then
+                           fixall(ruleMap[key])
+                         end
+                         update()
+                       end,
+            get      = function(info, key)
+                         return fetch("values", ruleMap[key]) or false
+                       end,
+            order    = 4
+          },
+          custom = {
+            type = "input",
+            multiline = true,
+            hidden = function()
+                       return fetch("type") ~= "custom"
+                     end,
+            disabled = function()
+                         return fetch("type") == "default"
+                       end,
+            name = L["Custom Rule"],
+            desc = L["Syntax like macro rules: see preset rules for examples"],
+            set  = function(info, value) 
+                     put("custom",value)
+                     update()
+                   end,
+            get  = function(info)
+                     return fetch("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 L["Invalid custom rule '%s': each clause must appear within [brackets]"]:format(rule)
+                  end
+                  s = r
+                until c == nil
+                return true
+              end,
+            order = 5,
+          }
+        }
+      }
     }
-
-    -- set up special entry for custom
-    opts.args.custom.args.rule = {
-      type = "input",
-      name = L["Rule"],
-      desc = L["Syntax like macro conditions: see preset rules for examples"],
-      get  = get,
-      set  = set,
-      validate = function (info, rule)
-          local s = rule:gsub("%s","") -- remove all spaces
-          if s:match(";$") then -- can't end with semicolon
-            return L["Invalid custom rule '%s': Rule cannot end with ';'"]:format(rule)
-          end
-          if s == "" then
-            return true
-          end
-          -- unfortunately %b and captures don't support the '+' notation, or this would be considerably simpler
-          repeat
-            repeat
-              local c, r = s:match("(%b[])(.*)")
-              if r then s = r end
-            until c == nil
-            local state, s = s:match("(%w+)(.*)")
-            if not state then
-              return L["Invalid custom rule '%s': Each expression must have a state"]:format(rule)
-            end
-            if not tfetch(module.db.profile.bars,bar:GetName(),"states",state) then
-              return L["Invalid custom rule '%s': '%s' is not a state"]:format(rule,state)
-            end
-            if s:match("^[^;]") then
-              return L["Invalid custom rule '%s': Expressions must be separated by ';'"]:format(rule)
-            end
-          until #s == 0
-          return true
-        end,
-      multiline = true,
-      order = 1,
-    }
-
     return opts
   end
 
+
   CreateBarOptions = function(bar)
     local private = { }
     local options = {
       type = "group",
       name = L["Dynamic State"],
-      childGroups = "tab",
+      childGroups = "tree",
       disabled = InCombatLockdown,
       args = {
-        states = {
+        __desc__ = {
+          type = "description",
+          name = L["States are evaluated in the order they are listed"],
+          order = 1
+        },
+        __new__ = {
           type = "group",
-          name = L["States"],
-          childGroups = "tree",
-          order = 1,
+          name = L["New State..."],
+          order = 2,
           args = {
-            __new__ = {
-              type = "group",
-              name = L["New State..."],
-              order = 1,
-              args = {
-                name = {
-                  type = "input",
-                  name = L["State Name"],
-                  desc = L["Set a name for the new state"],
-                  get = function() return private.newstatename or "" end,
-                  set = function(info,value) private.newstatename = value end,
-                  pattern = "^%w*$",
-                  usage = L["State names must be alphanumeric without spaces"],
-                  order = 1
-                },
-                create = {
-                  type = "execute",
-                  name = L["Create State"],
-                  func = function ()
-                      local name = private.newstatename
-                      module:CreateState(bar,name) -- TODO: select default state options and pass as final argument
-                      module.options[bar].args.states.args[name] = CreateStateOptions(bar,name)
-                      private.newstatename = ""
-                    end,
-                  disabled = function()
-                      local name = private.newstatename or ""
-                      return #name == 0 or name:find("%W")
-                    end,
-                  order = 2,
-                }
-              }
+            name = {
+              type = "input",
+              name = L["State Name"],
+              desc = L["Set a name for the new state"],
+              get = function() return private.newstatename or "" end,
+              set = function(info,value) private.newstatename = value end,
+              pattern = "^%w*$",
+              usage = L["State names must be alphanumeric without spaces"],
+              order = 1
+            },
+            create = {
+              type = "execute",
+              name = L["Create State"],
+              func = function ()
+                  local name = private.newstatename
+                  module:CreateState(bar,name) -- TODO: select default state options and pass as final argument
+                  optionMap[bar].args[name] = CreateStateOptions(bar,name)
+                  private.newstatename = ""
+                end,
+              disabled = function()
+                  local name = private.newstatename or ""
+                  return #name == 0 or name:find("%W")
+                end,
+              order = 2,
             }
           }
-        },
-        rules = {
-          type = "group",
-          name = L["Transition Rules"],
-          childGroups = "tree",
-          order = 2,
-          args = {
-            __new__ = {
-              type = "group",
-              name = L["New Rule..."],
-              order = 1,
-              args = {
-                name = {
-                  type = "input",
-                  name = L["Rule Name"],
-                  desc = L["Set a name for the new transition rule"],
-                  get = function() return private.newtransname or "" end,
-                  set = function(info,value) private.newtransname = value end,
-                  pattern = "^%w*$",
-                  usage = L["Rule names must be alphanumeric without spaces"],
-                  order = 1
-                },
-                create = {
-                  type = "execute",
-                  name = L["Create Rule"],
-                  func = function ()
-                      local name = private.newtransname
-                      module:CreateRule(bar,name) -- TODO: select default rule and add as final argument
-                      module.options[bar].args.rules.args[name] = CreateRuleOptions(bar,name)
-                      private.newtransname = ""
-                    end,
-                  disabled = function ()
-                      local name = private.newtransname or ""
-                      return #name == 0 or name:find("%W")
-                    end,
-                  order = 2,
-                },
-              }
-            }
-          }
-        },
-        presets = {
-          type = "group",
-          name = L["Presets"],
-          order = 3,
-          args = {
-            desc = {
-              type = "description",
-              name = L["Presets are canned sets of states and transitions. You can create your own presets to add to ReAction's built in defaults."],
-              order = 1,
-            },
-            select = {
-              type = "select",
-              name = L["Select Preset"],
-              set  = function(info,value) end,
-              get  = function() return "" end,
-              values = function() return { } end,
-              order = 2,
-            },
-            load = {
-              type = "execute",
-              name = L["Load"],
-              func = function() end,
-              width = "half",
-              order = 3,
-            },
-            delete = {
-              type = "execute",
-              name = L["Delete"],
-              disabled = function() return false end,
-              func = function() end,
-              width = "half",
-              order = 4,
-            },
-            hdr = {
-              type = "header",
-              name = " ",
-              order = 5,
-            },
-            save = {
-              type = "input",
-              name = L["Save As..."],
-              get  = function() return "" end,
-              set  = function(info,name) end,
-              order = 6,
-            },
-          },
-        },
+        }
       }
     }
     local states = tfetch(module.db.profile.bars, bar:GetName(), "states")
     if states then
       for name, config in pairs(states) do
-        options.args.states.args[name] = CreateStateOptions(bar,name)
+        options.args[name] = CreateStateOptions(bar,name)
       end
     end
-    local rules = tfetch(module.db.profile.bars, bar:GetName(), "rules")
-    if rules then
-      for name, config in pairs(rules) do
-        options.args.rules.args[name] = CreateRuleOptions(bar,name)
-      end
-    end
+    optionMap[bar] = options
     return options
   end
 end
 
 function module:GetBarOptions(bar)
-  if not self.options[bar] then
-    self.options[bar] = CreateBarOptions(bar)
-  end
-  return self.options[bar]
+  return CreateBarOptions(bar)
 end