changeset 62:f9cdb920470a

Added first cut on State module. Menu system only, it doesn't do anything. The menu system and data storage will change substantially when the implementation takes shape.
author Flick <flickerstreak@gmail.com>
date Tue, 13 May 2008 16:42:52 +0000
parents 2ee41dcd673f
children 768be7eb22a0
files locale/enUS.lua modules/ReAction_State/ReAction_State.lua modules/modules.xml
diffstat 3 files changed, 649 insertions(+), 59 deletions(-) [+]
line wrap: on
line diff
--- a/locale/enUS.lua	Tue May 13 16:42:03 2008 +0000
+++ b/locale/enUS.lua	Tue May 13 16:42:52 2008 +0000
@@ -101,7 +101,79 @@
 "Use the mouse to arrange and resize the bars on screen. Tooltips on bars indicate additional functionality.",
 
 -- modules/ReAction_State
-"Dynamic Behavior",
+"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",
+"Battle Stance",
+"Defensive Stance",
+"Berserker Stance",
+"Druid Form",
+"Normal",
+"Bear",
+"Cat",
+"Tree/Moonkin",
+"Caster",
+"Stealth",
+"Shadowform",
+"Pet",
+"Without Pet",
+"With Pet",
+"Target",
+"No Target",
+"Hostile Target",
+"Friendly Target",
+"Default",
+"Focus",
+"No Focus",
+"Hostile Focus",
+"Friendly Focus",
+"In Group",
+"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",
+"New State...",
+"Set a name for the new state",
+"State Name",
+"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
 end
--- a/modules/ReAction_State/ReAction_State.lua	Tue May 13 16:42:03 2008 +0000
+++ b/modules/ReAction_State/ReAction_State.lua	Tue May 13 16:42:52 2008 +0000
@@ -1,5 +1,5 @@
 --[[
-  ReAction bar state machine
+  ReAction bar state driver interface
 
 --]]
 
@@ -13,85 +13,599 @@
 local moduleID = "State"
 local module = ReAction:NewModule( moduleID )
 
--- module methods
+
+-- module event handlers
 function module:OnInitialize()
-  self.db = ReAction:RegisterNamespace( moduleID, 
+  self.db = ReAction.db:RegisterNamespace( moduleID, 
     {
-      profile = { }
+      profile = { 
+        bars = { },
+        presets = { }
+      }
     }
   )
+  self.states = { }
+  self.options = setmetatable({},{__mode="k"})
 end
 
-function module:OnEnable()
+
+
+-- ReAction module interface
+function module:ApplyToBar(bar)
+  self:RefreshBar(bar)
+end
+
+function module:RefreshBar(bar)
+  local c = self.db.profile.bars[bar:GetName()]
+  if c then
+    --self:BuildStates(bar)
+    --self:BuildRules(bar)
+  end
+end
+
+function module:RemoveFromBar(bar)
+end
+
+function module:EraseBarConfig(barName)
+  self.db.profile.bars[barName] = nil
+end
+
+function module:RenameBarConfig(oldname, newname)
+  local b = self.db.profile.bars
+  bars[newname], bars[oldname] = bars[oldname], nil
+end
+
+function module:ApplyConfigMode(mode,bars)
+  -- swap out hidestates
+end
+
+
+
+
+-- Private --
+
+-- traverse a table tree by key list and fetch the result or first nil
+local function tfetch(t, ...)
+  for i = 1, select('#', ...) do
+    t = t and t[select(i, ...)]
+  end
+  return t
+end
+
+-- traverse a table tree by key list and build tree as necessary
+local function tbuild(t, ...)
+  for i = 1, select('#', ...) do
+    local key = select(i, ...)
+    if not t[key] then t[key] = { } end
+    t = t[key]
+  end
+  return t
+end
+
+
+local rules
+local BuildRuleString
+do
+  local function ClassCheck(...)
+    for i = 1, select('#',...) do
+      local _, c = UnitClass("player")
+      if c == select(i,...) then
+        return false
+      end
+    end
+    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
+  }
+
+  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
+
+    -- 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
+      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
 
-function module:OnDisable()
 
+
+-- 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:GetGlobalOptions( configModule )
---  return {}
---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:GetGlobalBarOptions( configModule )
---
---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:GetModuleOptions( configModule )
---
---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:GetBarConfigOptions( bar, configModule )
-  if not bar.modConfigOpts[moduleID] then
-    local IsEnabled = function() 
-      return false
+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
 
-    bar.modConfigOpts[moduleID] = {
-      state = {
-        type = "group",
-        name = L["Dynamic Behavior"],
-        desc = L["Dynamic Behavior"],
-        args = {
-          enable = {
-            type = "toggle",
-            name = L["Enable dynamic behavior"],
-            desc = L["Toggles dynamic behavior for this bar"],
-            get  = function() return false end,
-            set  = function(x) end,
-            disabled = InCombatLockdown,
-            order = 1
-          },
+    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
 
-          default = {
-            type = "text",
-            name = L["Default State"],
-            desc = L["State when no conditions apply"],
-            get  = function() return false end,
-            set  = function(x) end,
-            disabled = IsEnabled,
-            order = 2
-          },
-
-          stealth = {
-            type = "text",
-            name = L["Behavior when Stealthed"],
-            desc = L["Change bar state when stealthed"],
-            get  = function() return false end,
-            set  = function(x) end,
-            disabled = IsEnabled,
-            validate = { },
-          },
-
+    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
         }
       }
     }
+
+    -- 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
+          }
+        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
+    }
+
+    -- 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
-  return bar.modConfigOpts[moduleID]
+
+  CreateBarOptions = function(bar)
+    local private = { }
+    local options = {
+      type = "group",
+      name = L["Dynamic State"],
+      childGroups = "tab",
+      disabled = InCombatLockdown,
+      args = {
+        states = {
+          type = "group",
+          name = L["States"],
+          childGroups = "tree",
+          order = 1,
+          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,
+                }
+              }
+            }
+          }
+        },
+        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)
+      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
+    return options
+  end
 end
 
-function module:GetBarMenuOptions( bar, configModule )
-
+function module:GetBarOptions(bar)
+  if not self.options[bar] then
+    self.options[bar] = CreateBarOptions(bar)
+  end
+  return self.options[bar]
 end
-
--- a/modules/modules.xml	Tue May 13 16:42:03 2008 +0000
+++ b/modules/modules.xml	Tue May 13 16:42:52 2008 +0000
@@ -6,13 +6,15 @@
 <Include file="ReAction_ConfigUI\ReAction_ConfigUI.xml"/>
 <Include file="ReAction_HideBlizzard\ReAction_HideBlizzard.xml"/>
 
-<!-- action button modules -->
+<!-- general utility modules -->
+<Include file="ReAction_State\ReAction_State.xml"/>
+
+<!-- button modules -->
 <Include file="ReAction_Action\ReAction_Action.xml"/>
 <Include file="ReAction_PetAction\ReAction_PetAction.xml"/>
 <Include file="ReAction_PossessBar\Reaction_PossessBar.xml"/>
 
 <!-- not yet implemented
-<Include file="ReAction_State\ReAction_State.xml"/>
 <Include file="ReAction_BagBar\ReAction_BagBar.xml"/>
 <Include file="ReAction_Label\ReAction_Label.xml"/>
 <Include file="ReAction_MicroMenu\ReAction_MicroMenu.xml"/>