changeset 68:fcb5dad031f9

State transitions now working. Keybind toggle states, stance switching, etc. Hide bar is the only working state property, but the config is there for page #, re-anchoring, re-scaling, and keybind override.
author Flick <flickerstreak@gmail.com>
date Tue, 03 Jun 2008 22:57:34 +0000
parents 84721edaa749
children a785d6708388
files Bar.lua locale/enUS.lua modules/ReAction_State/ReAction_State.lua
diffstat 3 files changed, 504 insertions(+), 109 deletions(-) [+]
line wrap: on
line diff
--- a/Bar.lua	Wed May 28 17:59:44 2008 +0000
+++ b/Bar.lua	Tue Jun 03 22:57:34 2008 +0000
@@ -21,13 +21,14 @@
 
 local function Constructor( self, name, config )
   self.name, self.config = name, config
+  self.buttons = setmetatable({},{__mode="k"})
 
   if type(config) ~= "table" then
     error("ReAction.Bar: config table required")
   end
 
   local parent = config.parent and (ReAction:GetBar(config.parent) or _G[config.parent]) or UIParent
-  local f = CreateFrame("Frame",nil,parent,"SecureStateDriverTemplate")
+  local f = CreateFrame("Frame",nil,parent,"SecureStateHeaderTemplate")
   f:SetFrameStrata("MEDIUM")
   config.width = config.width or 480
   config.height = config.height or 40
@@ -37,9 +38,9 @@
   ReAction.RegisterCallback(self, "OnConfigModeChanged")
 
   self.frame = f
-  self:RefreshLayout()
   self:ApplyAnchor()
   f:Show()
+  self:RefreshLayout()
 end
 
 function Bar:Destroy()
@@ -165,13 +166,74 @@
   f:ClearAllPoints()
   f:SetPoint("TOPLEFT",x/scale,-y/scale)
   f:SetScale(scale)
+  self.buttons[f] = true
 end
 
+function Bar:GetNumPages()
+  return self.config.nPages or 1
+end
 
+function Bar:SetHideStates(s)
+  for f in pairs(self.buttons) do
+    if f:GetParent() == self.frame then
+      f:SetAttribute("hidestates",s)
+    end
+  end
+  SecureStateHeader_Refresh(self.frame)
+end
 
+function Bar:SetStateKeybind(keybind, state, defaultstate)
+  -- use a tiny offscreen button to get around making the bar itself a clickable button
+  local f = self.statebuttonframe
+  local off = ("%s_off"):format(state)
+  if keybind then
+    if not f then
+      f = CreateFrame("Button",self:GetName().."_statebutton",UIParent,"SecureActionButtonTemplate")
+      f:SetPoint("BOTTOMRIGHT",UIParent,"TOPLEFT")
+      f:SetWidth(1)
+      f:SetHeight(1)
+      f:SetAttribute("attribute-name", "state")
+      f:SetAttribute("attribute-frame",self.frame)
+      f:SetAttribute("stateheader",self.frame)
+      f:Show()
+      self.statebuttonframe = f
+    end
+    -- map two virtual buttons to toggle between the state and the default
+    f:SetAttribute(("statebutton-%s"):format(state),("%s:%s;%s"):format(state,off,state))
+    f:SetAttribute(("type-%s"):format(state),"attribute")
+    f:SetAttribute(("type-%s"):format(off),"attribute")
+    f:SetAttribute(("attribute-value-%s"):format(state), state)
+    f:SetAttribute(("attribute-value-%s"):format(off), defaultstate)
+    SetBindingClick(keybind, f:GetName(), state)
+  elseif f then
+    f:SetAttribute(("type-%s"):format(state),ATTRIBUTE_NOOP)
+    f:SetAttribute(("type-%s"):format(off),ATTRIBUTE_NOOP)
+  end
+end
 
+function Bar:SetStatePageMap(state, map)  -- map is a { ["statename"] = pagenumber } table
+  local f = self.frame
+  local tmp = { }
+  for s, p in pairs(map) do
+    table.insert(tmp, ("%s:%d"):format(s,p))
+  end
+  local spec = table.concat(tmp,";")
+  f:SetAttribute("statebutton",spec)
+end
 
-
+function Bar:SetStateKeybindOverrideMap(states) -- 'states' is an array of state-names that should have keybind overrides enabled
+  local f = self.frame
+  for i = 1, #states do
+    local s = states[i]
+    states[i] = ("%s:%s"):format(s,s)
+  end
+  table.insert(states,"_default")
+  f:SetAttribute("statebindings",table.concat(states,";"))
+  for b in pairs(self.buttons) do
+    -- TODO: signal child frames that they should 
+    -- maintain multiple bindings
+  end
+end
 
 --
 -- Bar config overlay
--- a/locale/enUS.lua	Wed May 28 17:59:44 2008 +0000
+++ b/locale/enUS.lua	Tue Jun 03 22:57:34 2008 +0000
@@ -127,23 +127,45 @@
 "In Combat",
 "Out of Combat",
 "Warning: one or more incompatible rules were turned off",
-"Properties",
+"Info",
 "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",
+"Properties",
+"Set the properties for the bar when in this state",
+"Hide Bar",
+"Show Page #",
+"Override Keybinds",
+"Set this state to maintain its own set of keybinds which override the defaults when active",
+"Position",
+"Set New Position",
+"Point",
+"Relative Point",
+"X Offset",
+"Y Offset",
+"Scale",
+"Set New Scale",
+"Selection Rule",
 "Select this state",
 "by default",
 "when ANY of these",
 "when ALL of these",
 "via custom rule",
+"via keybinding",
 "Clear All",
+"Conditions",
 "Custom Rule",
 "Syntax like macro rules: see preset rules for examples",
 "Invalid custom rule '%s': each clause must appear within [brackets]",
+"Keybinding",
+"Invoking a state keybind overrides all other transition rules. Toggle the keybind again to remove the override and return to the specified toggle-off state.",
+"State Hotkey",
+"Define an override toggle keybind",
+"Toggle Off State",
+"Select a state to return to when the keybind override is toggled off",
 "Dynamic State",
 "States are evaluated in the order they are listed",
 "New State...",
--- a/modules/ReAction_State/ReAction_State.lua	Wed May 28 17:59:44 2008 +0000
+++ b/modules/ReAction_State/ReAction_State.lua	Tue Jun 03 22:57:34 2008 +0000
@@ -34,7 +34,8 @@
 end
 
 -- PRIVATE --
-local InitRules, ApplyStates
+
+local InitRules, ApplyStates, SetProperty, GetProperty
 do
   -- As far as I can tell the macro clauses are NOT locale-specific.
   local ruleformats = { 
@@ -89,18 +90,26 @@
     ruleformats.treeOrMoonkin = ("form:%d"):format(treekin)
   end
 
+  -- return a new array of keys of table 't', sorted by comparing 
+  -- sub-fields (obtained via tfetch) of the table values
+  local function fieldsort( t, ... )
+    local r = { }
+    for k in pairs(t) do
+      table.insert(r,k)
+    end
+    local dotdotdot = { ... }
+    table.sort(r, function(lhs, rhs)
+       local olhs = tfetch(t[lhs], unpack(dotdotdot)) or 0
+       local orhs = tfetch(t[rhs], unpack(dotdotdot)) or 0
+       return olhs < orhs
+      end)
+    return r
+  end
+
   local 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)
+    local sorted = fieldsort(states, "rule", "order")
     for idx, name in ipairs(sorted) do
       local state = states[name]
       local semi = #s > 0 and "; " or ""
@@ -137,36 +146,130 @@
     if default then
       s = ("%s%s%s"):format(s, #s > 0 and "; " or "", default)
     end
-    return s
+    return s, default
   end
 
   local drivers = setmetatable({},{__mode="k"})
+  local propertyFuncs = { }
 
   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.."'")
+      local string, default = BuildRuleString(states)
       if string and #string > 0 then
+        drivers[bar] = true
+        -- register a map for each "statemap-reaction-XXX" to set 'state' to 'XXX'
+        -- UNLESS we're in a keybound state AND there's a default state, in which case
+        -- all keybound states go back to themselves.
+        local keybindprefix
+        if default then
+          local tmp = { }
+          for state, config in pairs(states) do
+            if tfetch(config, "rule", "type") == "keybind" then
+              bar:SetStateKeybind(tfetch(config,"rule","keybind"), state, tfetch(config,"rule","keybindreturn") or default or 0)
+              table.insert(tmp, ("%s:%s"):format(state,state))
+            end
+          end
+          if #tmp > 0 then
+            table.insert(tmp,"") -- to get a final ';'
+          end
+          keybindprefix = table.concat(tmp,";")
+        end
+        for state in pairs(states) do
+          frame:SetAttribute(("statemap-reaction-%s"):format(state), ("%s%s"):format(keybindprefix or "",state))
+        end
         -- 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
+        SecureStateHeader_Refresh(frame)
       elseif drivers[bar] then
         UnregisterStateDriver(frame, "reaction")
         drivers[bar] = nil
       end
+      for k, f in pairs(propertyFuncs) do
+        f(bar, states)
+      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 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
+
+  -- state property functions
+  function propertyFuncs.hide( bar, states )
+    local tmp = { }
+    for state, config in pairs(states) do
+      if config.hide then
+        table.insert(tmp, state)
+      end
+    end
+    local s = table.concat(tmp,",")
+    bar:SetHideStates(s)
+  end
+
+  function propertyFuncs.page( bar, states )
+    local map = { }
+    for state, config in pairs(states) do
+      map[state] = config.page
+    end
+    bar:SetStatePageMap(state, map)
+  end
+
+  function propertyFuncs.keybindstate( bar, states )
+    local map = { }
+    for state, config in pairs(states) do
+      if config.keybindstate then
+        table.insert(map,state)
+      end
+    end
+    bar:SetStateKeybindOverrideMap(map)
+  end
+
+  function propertyFuncs.enableanchor( bar, states )
+
+  end
+
+  function propertyFuncs.anchorPoint( bar, states )
+
+  end
+
+  function propertyFuncs.anchorRelPoint( bar, states )
+
+  end
+
+  function propertyFuncs.anchorX( bar, states )
+
+  end
+
+  function propertyFuncs.anchorY( bar, states )
+
+  end
+
+  function propertyFuncs.enablescale( bar, states )
+
+  end
+
+  function propertyFuncs.scale( bar, states )
+
+  end
+
 end
 
 
--- module event handlers
+
+-- module event handlers --
+
 function module:OnInitialize()
   self.db = ReAction.db:RegisterNamespace( moduleID, 
     {
@@ -205,7 +308,7 @@
 function module:OnRefreshBar(event, bar, name)
   local c = self.db.profile.bars[name]
   if c then
-    self:UpdateStates(bar)
+    ApplyStates(bar)
   end
 end
 
@@ -223,28 +326,6 @@
 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
-    self:UpdateStates(bar)
-  end
-end
 
 -- Options --
 
@@ -278,6 +359,19 @@
   local ruleMap    = { }
   local optionMap  = setmetatable({},{__mode="k"})
 
+  local pointTable = {
+    NONE        = " ",
+    CENTER      = L["Center"], 
+    LEFT        = L["Left"],
+    RIGHT       = L["Right"],
+    TOP         = L["Top"],
+    BOTTOM      = L["Bottom"],
+    TOPLEFT     = L["Top Left"],
+    TOPRIGHT    = L["Top Right"],
+    BOTTOMLEFT  = L["Bottom Left"],
+    BOTTOMRIGHT = L["Bottom Right"],
+  }
+
   -- unpack rules table into ruleSelect and ruleMap
   for _, c in ipairs(rules) do
     local rule, hidden, fields = unpack(c)
@@ -299,14 +393,26 @@
 
     local states = tbuild(module.db.profile.bars, bar:GetName(), "states")
 
-    local function put( key, value, ... )
+    local function update()
+      ApplyStates(bar)
+    end
+
+    local function setrule( key, value, ... )
       tbuild(states, opts.name, "rule", ...)[key] = value
     end
 
-    local function fetch( ... )
+    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 
@@ -317,9 +423,9 @@
         local rule, hidden, fields = unpack(c)
         local found = false
         for key in ipairs(fields) do
-          if fetch("values",key) then
+          if getrule("values",key) then
             if (found or setkey) and key ~= setkey then
-              put(key,false,"values")
+              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
@@ -359,12 +465,13 @@
       a.order, b.order = b.order, a.order
     end
 
-    local function update()
-      module:UpdateStates(bar)
+    local function anchordisable()
+      return not GetProperty(bar, opts.name, "enableanchor")
     end
 
+    tbuild(states, name)
 
-    opts.order = fetch("order")
+    opts.order = getrule("order")
     if opts.order == nil then
       -- add after the highest
       opts.order = 100
@@ -374,28 +481,31 @@
           opts.order = x + 1
         end
       end
-      put("order",opts.order)
+      setrule("order",opts.order)
     end
 
     opts.args = {
-      properties = {
+      ordering = {
+        name = L["Info"],
+        order = 1,
         type = "group",
-        name = L["Properties"],
-        order = 1,
         args = {
           delete = {
+            name = L["Delete this State"],
+            order = -1,
             type = "execute",
-            name = L["Delete this State"],
             func = function(info) 
-                module:DeleteState(bar,opts.name)
+                if states[opts.name] then
+                  states[opts.name] = nil
+                  ApplyStates(bar)
+                end
                 optionMap[bar].args[opts.name] = nil
               end,
-            order = -1
           },
           rename = {
-            type = "input",
             name = L["Name"],
             order = 1,
+            type = "input",
             get  = function() return opts.name end,
             set  = function(info, value) 
                      -- check for existing state name
@@ -411,17 +521,17 @@
             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,
+            type = "group",
+            inline = true,
             args = {
               up = {
+                name  = L["Up"],
+                order = 1,
                 type  = "execute",
-                name  = L["Up"],
                 width = "half",
-                order = 1,
                 func  = function()
                           local before, after = getNeighbors()
                           if before then
@@ -431,105 +541,254 @@
                         end,
               },
               down = {
+                name  = L["Down"],
+                order = 2,
                 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
+          }
         }
       },
+      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
+                       end,
+            hidden   = function()
+                         return bar:GetNumPages() < 2
+                       end,
+            values   = function()
+                         local pages = { none = " " }
+                         for i = 1, bar:GetNumPages() do
+                           pages[i] = i
+                         end
+                         return pages
+                       end,
+            set      = function(info, value)
+                         if value == "none" then
+                           setprop(info, nil)
+                         else
+                           setprop(info, value)
+                         end
+                       end,
+            get      = function(info)
+                         return getprop(info) or "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["Selection Rule"],
+        order  = 3,
         type   = "group",
-        name   = L["Rules"],
-        order  = 2,
         args   = {
           mode = {
+            name   = L["Select this state"],
+            order  = 2,
             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"] 
+              custom = L["via custom rule"],
+              keybind = L["via keybinding"],
             },
             set    = function( info, value )
-                       put("type", value)
+                       setrule("type", value)
                        fixall()
                        update()
                      end,
             get    = function( info )
-                       return fetch("type")
+                       return getrule("type")
                      end,
-            order  = 2
           },
           clear = {
+            name     = L["Clear All"],
+            order    = 3,
             type     = "execute",
-            name     = L["Clear All"],
+            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 = fetch("type")
+                         local type = getrule("type")
                          if type == "custom" then
-                           put("custom","")
+                           setrule("custom","")
                          elseif type == "any" or type == "all" then
-                           put("values", {})
+                           setrule("values", {})
                          end
                          update()
                        end,
-            order    = 3
           },
           inputs = {
+            name     = L["Conditions"],
+            order    = 4,
             type     = "multiselect",
-            name     = L["Rules"],
             hidden   = function()
-                         return fetch("type") == "custom"
+                         local t = getrule("type")
+                         return t ~= "any" and t ~= "all"
                        end,
             disabled = function()
-                         return fetch("type") == "default"
+                         local t = getrule("type")
+                         return t ~= "any" and t ~= "all"
                        end,
             values   = ruleSelect,
             set      = function(info, key, value )
-                         put(ruleMap[key], value or nil, "values")
+                         setrule(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
+                         return getrule("values", ruleMap[key]) or false
                        end,
-            order    = 4
           },
           custom = {
+            name = L["Custom Rule"],
+            order = 5,
             type = "input",
             multiline = true,
             hidden = function()
-                       return fetch("type") ~= "custom"
+                       return getrule("type") ~= "custom"
                      end,
             disabled = function()
-                         return fetch("type") == "default"
+                         return getrule("type") ~= "custom"
                        end,
-            name = L["Custom Rule"],
             desc = L["Syntax like macro rules: see preset rules for examples"],
             set  = function(info, value) 
-                     put("custom",value)
+                     setrule("custom",value)
                      update()
                    end,
             get  = function(info)
-                     return fetch("custom") or ""
+                     return getrule("custom") or ""
                    end,
             validate = function (info, rule)
                 local s = rule:gsub("%s","") -- remove all spaces
@@ -546,10 +805,55 @@
                 until c == nil
                 return true
               end,
-            order = 5,
-          }
-        }
-      }
+          },
+          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 overrides all other transition rules. Toggle the keybind again to remove the override and return to the specified toggle-off state."],
+                order = 1,
+                type = "description",
+              },
+              keybind = {
+                name = L["State Hotkey"],
+                desc = L["Define an override toggle keybind"],
+                order = 2,
+                type = "keybinding",
+                set  = function(info, value)
+                         setrule("keybind",value)
+                         update()
+                       end,
+                get  = function() return getrule("keybind") end,
+              },
+              default = {
+                name = L["Toggle Off State"],
+                desc = L["Select a state to return to when the keybind override is toggled off"],
+                order = 3,
+                type = "select",
+                values = function()
+                           local t = { }
+                           for k in pairs(states) do
+                             if k ~= opts.name then
+                               t[k] = k
+                             end
+                           end
+                           return t
+                         end,
+                set = function(info, value)
+                        setrule("keybindreturn",value)
+                        update()
+                      end,
+                get = function() return getrule("keybindreturn") end,
+              },
+            },
+          },
+        },
+      },
     }
     return opts
   end
@@ -564,39 +868,44 @@
       disabled = InCombatLockdown,
       args = {
         __desc__ = {
+          name = L["States are evaluated in the order they are listed"],
+          order = 1,
           type = "description",
-          name = L["States are evaluated in the order they are listed"],
-          order = 1
         },
         __new__ = {
-          type = "group",
           name = L["New State..."],
           order = 2,
+          type = "group",
           args = {
             name = {
-              type = "input",
               name = L["State Name"],
               desc = L["Set a name for the new state"],
+              order = 1,
+              type = "input",
               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 = {
+              name = L["Create State"],
+              order = 2,
               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 = ""
+                  if states[name] then
+                    ReAction:UserError(L["State named '%s' already exists"]:format(name))
+                  else
+                    -- TODO: select default state options and pass as final argument
+                    states[name] = { }
+                    optionMap[bar].args[name] = CreateStateOptions(bar,name)
+                    private.newstatename = ""
+                  end
                 end,
               disabled = function()
                   local name = private.newstatename or ""
                   return #name == 0 or name:find("%W")
                 end,
-              order = 2,
             }
           }
         }