diff modules/State.lua @ 109:410d036c43b2

- reorganize modularity file structure (part 1)
author Flick <flickerstreak@gmail.com>
date Thu, 08 Jan 2009 00:57:27 +0000
parents
children 5c189f44e776
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/State.lua	Thu Jan 08 00:57:27 2009 +0000
@@ -0,0 +1,1338 @@
+--[[
+  ReAction bar state driver interface
+
+--]]
+
+-- local imports
+local ReAction = ReAction
+local L = ReAction.L
+local _G = _G
+local format = string.format
+local InCombatLockdown = InCombatLockdown
+local RegisterStateDriver = RegisterStateDriver
+
+ReAction:UpdateRevision("$Revision$")
+
+-- module declaration
+local moduleID = "State"
+local module = ReAction:NewModule( moduleID, "AceEvent-3.0" )
+
+-- Utility --
+
+-- 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
+
+-- 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 path = { ... }
+  table.sort(r, function(lhs, rhs)
+     local olhs = tfetch(t[lhs], unpack(path)) or 0
+     local orhs = tfetch(t[rhs], unpack(path)) or 0
+     return olhs < orhs
+    end)
+  return r
+end
+
+-- set a frame-ref, if the frame is valid, or set nil to the
+-- corresponding attribute
+local function SetFrameRef(frame, name, refFrame)
+  if refFrame then
+    local _, explicit = refFrame:IsProtected()
+    if not explicit then
+      refFrame = nil
+    end
+  end
+  if refFrame then
+    frame:SetFrameRef(name,refFrame)
+  else
+    frame:SetAttribute("frameref-"..name,nil)
+  end
+end
+
+
+local InitRules, ApplyStates, CleanupStates, SetProperty, GetProperty, RegisterProperty, ShowAll
+
+-- PRIVATE --
+do
+
+  -- the field names must match the field names of the options table, below
+  -- the field values are secure snippets or 'true' to skip the snippet for that property.
+  local properties = { 
+    hide = 
+      [[
+        local h = hide and hide[state] and not showAll
+        if h ~= hidden then
+          if h then
+            self:Hide()
+          else
+            self:Show()
+          end
+          hidden = h
+        end
+        if showAll then
+          control:CallMethod("UpdateHiddenLabel", hide and hide[state])
+        end
+      ]],
+
+    --keybindState  TODO: broken
+
+    anchorEnable = 
+      [[
+        local old_anchor = anchorstate
+        anchorstate = (anchorEnable and anchorEnable[state]) and state
+        if old_anchor ~= anchorstate or not set_state then
+          if anchorstate and anchorPoint then
+            if anchorPoint[state] then
+              self:ClearAllPoints()
+              local f = self:GetAttribute("frameref-anchor-"..anchorstate)
+              if f then
+                self:SetPoint(anchorPoint[state], f, anchorRelPoint[state], anchorX[state], anchorY[state])
+              end
+            end
+          elseif defaultAnchor and defaultAnchor.point then
+            self:ClearAllPoints()
+            self:SetPoint(defaultAnchor.point, defaultAnchor.frame, 
+                          defaultAnchor.relPoint, defaultAnchor.x, defaultAnchor.y)
+          end
+        end
+      ]],
+      -- anchorEnable handles all the other bits
+    anchorFrame = true,
+    anchorPoint = true,
+    anchorRelPoint = true,
+    anchorX = true,
+    anchorY = true,
+
+
+    enableScale = 
+      [[
+        local old_scale = scalestate
+        scalestate = (enableScale and enableScale[state]) and state
+        if old_scale ~= scalestate or not set_state then
+          if scalestate and scale then
+            if scale[state] then
+              self:SetScale(scale[state])
+            end
+          else
+            self:SetScale(1.0)
+          end
+        end
+      ]],
+      -- enableScale handles scale
+    scale = true,
+
+    enableAlpha = 
+      [[
+        local old_alpha = alphastate
+        alphastate = (enableAlpha and enableAlpha[state]) and state
+        if old_alpha ~= alphastate or not set_state then
+          control:CallMethod("UpdateAlpha", alphastate and alpha[state] or defaultAlpha)
+        end
+      ]],
+      -- enableAlpha handles alpha
+    alpha = true,
+  }
+
+  local weak         = { __mode = "k" }
+  local statedrivers = setmetatable( { }, weak )
+  local keybinds     = setmetatable( { }, weak )
+
+  --
+  -- Secure Handler Snippets
+  --
+  local SetHandlerData, SetStateDriver, SetStateKeybind, RefreshState
+  do
+    local stateHandler_propInit = 
+    [[
+      propfuncs = table.new()
+      local proplist = self:GetAttribute("prop-func-list")
+      for s in string.gmatch(proplist, "(%w+)") do
+        table.insert(propfuncs, s)
+      end
+    ]]
+
+    local onStateHandler = 
+    -- function _onstate-reaction( self, stateid, newstate )
+    [[
+      set_state = newstate
+
+      local oldState = state
+      state = state_override or set_state or state
+      for i = 1, #propfuncs do
+        control:RunAttribute("func-"..propfuncs[i])
+      end
+      
+      control:ChildUpdate()
+
+      if oldState ~= state then
+        control:CallMethod("StateRefresh", state)
+      end
+    ]]
+
+    local onClickHandler = 
+    -- function OnClick( self, button, down )
+    [[
+      if state_override == button then
+        state_override = nil -- toggle
+      else
+        state_override = button
+      end
+    ]] .. onStateHandler
+
+    local function UpdateAlpha( frame, alpha )
+      if alpha then
+        frame:SetAlpha(alpha)
+      end
+    end
+
+    -- Construct a lua assignment as a code string and execute it within the header
+    -- frame's sandbox. 'value' must be a string, boolean, number, or nil. If called
+    -- with four arguments, then it treats 'varname' as an existing global table and
+    -- sets a key-value pair. For a slight efficiency boost, pass the values in as
+    -- attributes and fetch them as attributes from the snippet code, to leverage snippet
+    -- caching.
+    function SetHandlerData( bar, varname, value, key )
+      local f = bar:GetFrame()
+      f:SetAttribute("data-varname",varname)
+      f:SetAttribute("data-value",  value)
+      f:SetAttribute("data-key",    key)
+      f:Execute(
+        [[
+          local name  = self:GetAttribute("data-varname")
+          local value = self:GetAttribute("data-value")
+          local key   = self:GetAttribute("data-key")
+          if name then
+            if key then
+              if not _G[name] then
+                _G[name] = table.new()
+              end
+              _G[name][key] = value
+            else
+              _G[name] = value
+            end
+          end
+        ]])
+    end
+
+    function SetDefaultAnchor( bar )
+      local point, frame, relPoint, x, y = bar:GetAnchor()
+      SetHandlerData(bar, "defaultAnchor", point, "point")
+      SetHandlerData(bar, "defaultAnchor", relPoint, "relPoint")
+      SetHandlerData(bar, "defaultAnchor", x, "x")
+      SetHandlerData(bar, "defaultAnchor", y, "y")
+      SetHandlerData(bar, "defaultAlpha",  bar:GetAlpha())
+
+      local f = bar:GetFrame()
+      f.UpdateAlpha = UpdateAlpha
+      SetFrameRef(f, "defaultAnchor", _G[frame or "UIParent"])
+      f:Execute(
+        [[
+          defaultAnchor.frame = self:GetAttribute("frameref-defaultAnchor")
+        ]])
+    end
+
+    function RefreshState( bar )
+      SetDefaultAnchor(bar)
+      bar:GetFrame():Execute(
+        [[
+          if self:GetAttribute("reaction-refresh") then
+            control:RunAttribute("reaction-refresh")
+          end
+        ]])
+    end
+
+    function SetStateDriver( bar, rule )
+      local f = bar:GetFrame()
+
+      if not f.UpdateHiddenLabel then
+        function f:UpdateHiddenLabel(hide)
+          bar:SetLabelSubtext( hide and L["Hidden"] )
+        end
+      end
+
+      function f:StateRefresh( state )
+        bar:RefreshControls()
+      end
+
+      local props = { }
+      for p, h in pairs(properties) do
+        if type(h) == "string" then
+          table.insert(props,p)
+          f:SetAttribute("func-"..p, h)
+        end
+      end
+      f:SetAttribute("prop-func-list", table.concat(props," "))
+      f:Execute(stateHandler_propInit)
+      f:SetAttribute("reaction-refresh", onStateHandler)
+      
+      if rule and #rule > 0 then
+        f:SetAttribute( "_onstate-reaction", onStateHandler )
+        RegisterStateDriver(f, "reaction", rule)
+        statedrivers[bar] = rule
+      elseif statedrivers[bar] then
+        UnregisterStateDriver(f, "reaction")
+        f:SetAttribute( "_onstate-reaction", nil )
+        statedrivers[bar] = nil
+      end
+    end
+
+    function SetStateKeybind( bar, key, state )
+      local f = bar:GetFrame()
+
+      local kb = keybinds[bar]
+      if kb == nil then
+        if key == nil then
+          -- nothing to do
+          return
+        end
+        kb = { }
+        keybinds[bar] = kb
+      end
+
+      -- clear the old binding, if any
+      if kb[state] then
+        SetOverrideBinding(f, false, kb[state], nil)
+      end
+      kb[state] = key
+
+      if key then
+        f:SetAttribute("_onclick", onClickHandler)
+        SetOverrideBindingClick(f, false, key, state, nil) -- state name is the virtual mouse button
+      end
+    end
+  end
+
+  -- As far as I can tell the macro clauses are NOT locale-specific.
+  local ruleformats = { 
+    stealth       = "stealth",
+    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",
+    possess       = "bonusbar:5",
+  }
+
+  -- Have to do these shenanigans instead of hardcoding the stances/forms because the 
+  -- ordering varies if the character is missing a form. For warriors this is rarely
+  -- a problem (c'mon, who actually skips the level 10 def stance quest?) but for druids
+  -- it can be. Some people never bother to do the aquatic form quest until well past
+  -- when they get cat form, and stance 5/6 can be flight, tree, or moonkin depending
+  -- on talents.
+  function InitRules()
+    local forms = { }
+      -- sort by icon since it's locale-independent
+    for i = 1, GetNumShapeshiftForms() do
+      local icon, name, active = GetShapeshiftFormInfo(i)
+      -- if it's the current form, the icon is wrong (Ability_Spell_WispSplode)
+      -- so capture it from the spell info directly
+      if active then
+        local _1, _2
+        _1, _2, icon = GetSpellInfo(name)
+      end
+      forms[icon] = i;
+    end
+      -- use 9 if not found since 9 is never a valid stance/form
+    local defensive = forms["Interface\\Icons\\Ability_Warrior_DefensiveStance"] or 9
+    local berserker = forms["Interface\\Icons\\Ability_Racial_Avatar"] or 9
+    local bear      = forms["Interface\\Icons\\Ability_Racial_BearForm"] or 9 -- bear and dire bear share the same icon
+    local aquatic   = forms["Interface\\Icons\\Ability_Druid_AquaticForm"] or 9
+    local cat       = forms["Interface\\Icons\\Ability_Druid_CatForm"] or 9
+    local travel    = forms["Interface\\Icons\\Ability_Druid_TravelForm"] or 9
+    local tree      = forms["Interface\\Icons\\Ability_Druid_TreeofLife"] or 9
+    local moonkin   = forms["Interface\\Icons\\Spell_Nature_ForceOfNature"] or 9
+    local flight    = forms["Interface\\Icons\\Ability_Druid_FlightForm"] or 9 -- flight and swift flight share the same icon
+
+    ruleformats.battle    = "stance:1"
+    ruleformats.defensive = format("stance:%d",defensive)
+    ruleformats.berserker = format("stance:%d",berserker)
+    ruleformats.caster    = format("form:0/%d/%d/%d",aquatic, travel, flight)
+    ruleformats.bear      = format("form:%d",bear)
+    ruleformats.cat       = format("form:%d",cat)
+    ruleformats.tree      = format("form:%d",tree)
+    ruleformats.moonkin   = format("form:%d",moonkin)
+  end
+
+  local function BuildRule(states)
+    local rules = { }
+    local default
+
+    for idx, state in ipairs(fieldsort(states, "rule", "order")) do
+      local c = states[state].rule
+      local type = c.type
+      if type == "default" then
+        default = default or state
+      elseif type == "custom" then
+        if c.custom then
+          -- strip out all spaces from the custom rule
+          table.insert(rules, format("%s %s", c.custom:gsub("%s",""), state))
+        end
+      elseif type == "any" or type == "all" then
+        if c.values then
+          local clauses = { }
+          for key, value in pairs(c.values) do
+            table.insert(clauses, ruleformats[key])
+          end
+          if #clauses > 0 then
+            local sep = (type == "any") and "][" or ","
+            table.insert(rules, format("[%s] %s", table.concat(clauses,sep), state))
+          end
+        end
+      end
+    end
+    -- make sure that the default, if any, is last
+    if default then
+      table.insert(rules, default)
+    end
+    return table.concat(rules,";")
+  end
+
+  local function BuildKeybinds( bar, states )
+    for name, state in pairs(states) do
+      local type = tfetch(state, "rule", "type")
+      if type == "keybind" then
+        local key = tfetch(state, "rule", "keybind")
+        SetStateKeybind(bar, key, name)
+      else
+        SetStateKeybind(bar, nil, name) -- this clears an existing keybind
+      end
+    end
+  end
+
+  function GetProperty( bar, state, propname )
+    return tfetch(module.db.profile.bars, bar:GetName(), "states", state, propname)
+  end
+
+  function SetProperty( bar, state, propname, value )
+    local s = tbuild(module.db.profile.bars, bar:GetName(), "states", state)
+    s[propname] = value
+    SetHandlerData(bar, propname, value, state)
+    RefreshState(bar)
+  end
+
+  function RegisterProperty( propname, snippet )
+    properties[propname] = snippet or true
+    for _, bar in ReAction:IterateBars() do
+      local states = tfetch(module.db.profile.bars, bar:GetName(), "states")
+      if states then
+        for name, s in pairs(states) do
+          SetHandlerData(bar, propname, s[propname], name)
+        end
+        SetStateDriver(bar, BuildRule(states))
+        RefreshState(bar)
+      end
+    end
+  end
+
+  function UnregisterProperty( propname )
+    properties[propname] = nil
+    for _, bar in ReAction:IterateBars() do
+      SetHandlerData(bar, propname, nil)
+      SetStateDriver(bar, BuildRule(states))
+      RefreshState(bar)
+    end
+  end
+
+  function ApplyStates( bar )
+    local states = tfetch(module.db.profile.bars, bar:GetName(), "states")
+    if states then
+      for propname in pairs(properties) do
+        for name, s in pairs(states) do
+          if propname == "anchorFrame" then
+            SetFrameRef(bar:GetFrame(), "anchor-"..name, _G[s.anchorFrame])
+          else
+            SetHandlerData(bar, propname, s[propname], name)
+          end
+        end
+      end
+      BuildKeybinds(bar, states)
+      SetHandlerData(bar, "showAll", ReAction:GetConfigMode())
+      SetStateDriver(bar, BuildRule(states))
+      RefreshState(bar)
+    end
+  end
+
+  function CleanupStates( bar )
+    SetStateDriver(bar, nil)
+  end
+
+  function ShowAll( bar, show )
+    if statedrivers[bar] then
+      SetHandlerData(bar, "showAll", show)
+      RefreshState(bar)
+    end
+  end
+end
+
+
+
+-- module event handlers --
+
+function module:OnInitialize()
+  self.db = ReAction.db:RegisterNamespace( moduleID, 
+    {
+      profile = { 
+        bars = { },
+      }
+    }
+  )
+
+  self:RegisterEvent("UPDATE_SHAPESHIFT_FORMS")
+
+  ReAction:RegisterBarOptionGenerator(self, "GetBarOptions")
+
+  ReAction.RegisterCallback(self, "OnCreateBar","OnRefreshBar")
+  ReAction.RegisterCallback(self, "OnDestroyBar")
+  ReAction.RegisterCallback(self, "OnRefreshBar")
+  ReAction.RegisterCallback(self, "OnEraseBar")
+  ReAction.RegisterCallback(self, "OnRenameBar")
+  ReAction.RegisterCallback(self, "OnConfigModeChanged")
+end
+
+function module:OnEnable()
+  self:UPDATE_SHAPESHIFT_FORMS() -- it doesn't fire on a /reloadui
+end
+
+function module:UPDATE_SHAPESHIFT_FORMS()
+  -- Re-parse the rules table according to the new form list.
+  -- This happens both at initial login (after PLAYER_ENTERING_WORLD)
+  -- as well as when gaining new abilities. 
+  InitRules()
+  for name, bar in ReAction:IterateBars() do
+    self:OnRefreshBar(nil,bar,name)
+  end
+end
+
+function module:OnRefreshBar(event, bar, name)
+  local c = self.db.profile.bars[name]
+  if c then
+    ApplyStates(bar)
+  end
+end
+
+function module:OnDestroyBar(event, bar, name)
+  CleanupStates(bar)
+end
+
+function module:OnEraseBar(event, bar, name)
+  self.db.profile.bars[name] = nil
+end
+
+function module:OnRenameBar(event, bar, oldname, newname)
+  local bars = self.db.profile.bars
+  bars[newname], bars[oldname] = bars[oldname], nil
+end
+
+function module:OnConfigModeChanged(event, mode)
+  for name, bar in ReAction:IterateBars() do
+    if self.db.profile.bars[name] then
+      ShowAll(bar, mode)
+    end
+  end
+end
+
+
+
+-- Options --
+
+local CreateBarOptions, RegisterPropertyOptions
+do
+  local playerClass = select(2, UnitClass("player"))
+  local function ClassCheck(...)
+    for i = 1, select('#',...) do
+      if playerClass == select(i,...) then
+        return false
+      end
+    end
+    return true
+  end
+
+  -- 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"]}, {tree = L["Tree of Life"]}, {moonkin = L["Moonkin Form"]} } },
+    { "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"]} } },
+    { "possess", false,                          { {possess = L["Mind Control"]} } },
+    { "group",   false,                          { {raid = L["Raid"]}, {party = L["Party"]}, {solo = L["Solo"]} } },
+    { "combat",  false,                          { {combat = L["In Combat"]}, {nocombat = L["Out of Combat"]} } },
+  }
+
+  local ruleSelect = { }
+  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)
+    if not hidden then
+      for _, field in ipairs(fields) do
+        local key, label = next(field)
+        table.insert(ruleSelect, label)
+        table.insert(ruleMap, key)
+      end
+    end
+  end
+
+  local stateOptions = {
+    ordering = {
+      name = L["Info"],
+      order = 1,
+      type = "group",
+      args = {
+        delete = {
+          name = L["Delete this State"],
+          order = -1,
+          type = "execute",
+          func = "DeleteState",
+        },
+        rename = {
+          name = L["Name"],
+          order = 1,
+          type = "input",
+          get  = "GetName",
+          set  = "SetStateName",
+          pattern = "^%w*$",
+          usage = L["State names must be alphanumeric without spaces"],
+        },
+        ordering = {
+          name = L["Evaluation Order"],
+          desc = L["State transitions are evaluated in the order listed:\nMove a state up or down to change the order"],
+          order = 2,
+          type = "group",
+          inline = true,
+          args = {
+            up = {
+              name  = L["Up"],
+              order = 1,
+              type  = "execute",
+              width = "half",
+              func  = "MoveStateUp",
+            },
+            down = {
+              name  = L["Down"],
+              order = 2,
+              type  = "execute",
+              width = "half",
+              func  = "MoveStateDown",
+            }
+          }
+        }
+      }
+    },
+    properties = {
+      name = L["Properties"],
+      order = 2,
+      type = "group",
+      args = { 
+        desc = {
+          name = L["Set the properties for the bar when in this state"],
+          order = 1,
+          type = "description"
+        },
+        hide = {
+          name = L["Hide Bar"],
+          order = 90,
+          type = "toggle",
+          set  = "SetProp",
+          get  = "GetProp",
+        },
+        --[[ BROKEN
+        keybindState = {
+          name  = L["Override Keybinds"],
+          desc  = L["Set this state to maintain its own set of keybinds which override the defaults when active"],
+          order = 91,
+          type  = "toggle",
+          set   = "SetProp",
+          get   = "GetProp",
+        }, ]]
+        position = {
+          name  = L["Position"],
+          order = 92,
+          type  = "group",
+          inline = true,
+          args = {
+            anchorEnable = {
+              name  = L["Reposition"],
+              order = 1,
+              type  = "toggle",
+              set   = "SetProp",
+              get   = "GetProp",
+            },
+            anchorFrame = {
+              name   = L["Anchor Frame"],
+              order  = 2,
+              type   = "select",
+              values = "GetAnchorFrames",
+              set    = "SetAnchorFrame",
+              get    = "GetAnchorFrame",
+              disabled = "GetAnchorDisabled",
+              hidden = "GetAnchorDisabled",
+            },
+            anchorPoint = {
+              name  = L["Point"],
+              order = 3,
+              type  = "select",
+              values = pointTable,
+              set   = "SetAnchorPointProp",
+              get   = "GetAnchorPointProp",
+              disabled = "GetAnchorDisabled",
+              hidden = "GetAnchorDisabled",
+            },
+            anchorRelPoint = {
+              name  = L["Relative Point"],
+              order = 4,
+              type  = "select",
+              values = pointTable,
+              set   = "SetAnchorPointProp",
+              get   = "GetAnchorPointProp",
+              disabled = "GetAnchorDisabled",
+              hidden = "GetAnchorDisabled",
+            },
+            anchorX = {
+              name  = L["X Offset"],
+              order = 5,
+              type  = "range",
+              min   = -100,
+              max   = 100,
+              step  = 1,
+              set   = "SetProp",
+              get   = "GetProp",
+              disabled = "GetAnchorDisabled",
+              hidden = "GetAnchorDisabled",
+            },
+            anchorY = {
+              name  = L["Y Offset"],
+              order = 6,
+              type  = "range",
+              min   = -100,
+              max   = 100,
+              step  = 1,
+              set   = "SetProp",
+              get   = "GetProp",
+              disabled = "GetAnchorDisabled",
+              hidden = "GetAnchorDisabled",
+            },
+          },
+        },
+        scale = {
+          name  = L["Scale"],
+          order = 93,
+          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.25,
+              max   = 2.5,
+              step  = 0.05,
+              isPercent = true,
+              set   = "SetProp",
+              get   = "GetScale",
+              disabled = "GetScaleDisabled",
+              hidden = "GetScaleDisabled",
+            },
+          },
+        },
+        alpha = {
+          name  = L["Transparency"],
+          order = 94,
+          type  = "group",
+          inline = true,
+          args = {
+            enableAlpha = {
+              name  = L["Set Transparency"],
+              order = 1,
+              type  = "toggle",
+              set   = "SetProp",
+              get   = "GetProp",
+            },
+            alpha = {
+              name  = L["Transparency"],
+              order = 2,
+              type  = "range",
+              min   = 0,
+              max   = 1,
+              step  = 0.01,
+              bigStep = 0.05,
+              isPercent = true,
+              set   = "SetProp",
+              get   = "GetAlpha",
+              disabled = "GetAlphaDisabled",
+              hidden = "GetAlphaDisabled",
+            },
+          },
+        },
+      },
+      plugins = { }
+    },
+    rules = {
+      name   = L["Rule"],
+      order  = 3,
+      type   = "group",
+      args   = {
+        mode = {
+          name   = L["Select this state"],
+          order  = 2,
+          type   = "select",
+          style  = "radio",
+          values = { 
+            default = L["by default"], 
+            any = L["when ANY of these"], 
+            all = L["when ALL of these"], 
+            custom = L["via custom rule"],
+            keybind = L["via keybinding"],
+          },
+          set    = "SetType",
+          get    = "GetType",
+        },
+        clear = {
+          name     = L["Clear All"],
+          order    = 3,
+          type     = "execute",
+          hidden   = "GetClearAllDisabled",
+          disabled = "GetClearAllDisabled",
+          func     = "ClearAllConditions",
+        },
+        inputs = {
+          name     = L["Conditions"],
+          order    = 4,
+          type     = "multiselect",
+          hidden   = "GetConditionsDisabled",
+          disabled = "GetConditionsDisabled",
+          values   = ruleSelect,
+          set      = "SetCondition",
+          get      = "GetCondition",
+        },
+        custom = {
+          name = L["Custom Rule"],
+          order = 5,
+          type = "input",
+          multiline = true,
+          hidden = "GetCustomDisabled",
+          disabled = "GetCustomDisabled",
+          desc = L["Syntax like macro rules: see preset rules for examples"],
+          set  = "SetCustomRule",
+          get  = "GetCustomRule",
+          validate = "ValidateCustomRule",
+        },
+        keybind = {
+          name = L["Keybinding"],
+          order = 6,
+          inline = true,
+          hidden = "GetKeybindDisabled",
+          disabled = "GetKeybindDisabled",
+          type = "group",
+          args = {
+            desc = {
+              name = L["Invoking a state keybind toggles an override of all other transition rules."],
+              order = 1,
+              type = "description",
+            },
+            keybind = {
+              name = L["State Hotkey"],
+              desc = L["Define an override toggle keybind"],
+              order = 2,
+              type = "keybinding",
+              set  = "SetKeybind",
+              get  = "GetKeybind",
+            },
+          },
+        },
+      },
+    },
+  }
+
+  local handlers = { }
+  local meta = {
+    __index = function(self, key)
+      for _, h in pairs(handlers) do
+        if h[key] then
+          return h[key]
+        end
+      end
+    end,
+  }
+  local StateHandler = setmetatable({ }, meta)
+  local proto        = { __index = StateHandler }
+
+  function RegisterPropertyOptions( field, options, handler )
+    stateOptions.properties.plugins[field] = options
+    handlers[field] = handler
+  end
+
+  function UnregisterPropertyOptions( field )
+    stateOptions.properties.plugins[field] = nil
+    handlers[field] = nil
+  end
+
+  function StateHandler:New( bar, opts )
+    local self = setmetatable(
+      { 
+        bar = bar 
+      }, 
+      proto )
+
+    function self:GetName()
+      return opts.name
+    end
+
+    function self:SetName(name)
+      opts.name = name
+    end
+
+    function self:GetOrder()
+      return opts.order
+    end
+
+    -- get reference to states table: even if the bar
+    -- name changes the states table ref won't
+    self.states = tbuild(module.db.profile.bars, bar:GetName(), "states")
+    self.state  = tbuild(self.states, opts.name)
+
+    opts.order = self:GetRuleField("order")
+    if opts.order == nil then
+      -- add after the highest
+      opts.order = 100
+      for _, state in pairs(self.states) do
+        local x = tonumber(tfetch(state, "rule", "order"))
+        if x and x >= opts.order then
+          opts.order = x + 1
+        end
+      end
+      self:SetRuleField("order",opts.order)
+    end
+
+    return self
+  end
+
+  -- helper methods
+
+  function StateHandler:SetRuleField( key, value, ... )
+    tbuild(self.state, "rule", ...)[key] = value
+  end
+
+  function StateHandler:GetRuleField( ... )
+    return tfetch(self.state, "rule", ...)
+  end
+
+  function StateHandler:FixAll( setkey )
+    -- if multiple selections in the same group are chosen when 'all' is selected,
+    -- keep only one of them. If changing the mode, the first in the fields list will 
+    -- be chosen arbitrarily. Otherwise, if selecting a new checkbox from the field-set,
+    -- it will be retained.
+    local notified = false
+    if self:GetRuleField("type") == "all" then
+      for _, c in ipairs(rules) do
+        local rule, hidden, fields = unpack(c)
+        local once = false
+        if setkey then
+          for idx, field in ipairs(fields) do
+            if next(field) == setkey then
+              once = true
+            end
+          end
+        end
+        for idx, field in ipairs(fields) do
+          local key = next(field)
+          if self:GetRuleField("values",key) then
+            if once and key ~= setkey then
+              self:SetRuleField(key,false,"values")
+              if not setkey and not notified then
+                ReAction:UserError(L["Warning: one or more incompatible rules were turned off"])
+                notified = true
+              end
+            end
+            once = true
+          end
+        end
+      end
+    end
+  end
+
+  function StateHandler:GetNeighbors()
+    local before, after
+    for k, v in pairs(self.states) do
+      local o = tonumber(tfetch(v, "rule", "order"))
+      if o and k ~= self:GetName() then
+        local obefore = tfetch(self.states,before,"rule","order")
+        local oafter  = tfetch(self.states,after,"rule","order")
+        if o < self:GetOrder() and (not obefore or obefore < o) then
+          before = k
+        end
+        if o > self:GetOrder() and (not oafter or oafter > o) then
+          after = k
+        end
+      end
+    end
+    return before, after
+  end
+
+  function StateHandler:SwapOrder( a, b )
+    -- do options table
+    local args = optionMap[self.bar].args
+    args[a].order, args[b].order = args[b].order, args[a].order
+    -- do profile
+    a = tbuild(self.states, a, "rule")
+    b = tbuild(self.states, b, "rule")
+    a.order, b.order = b.order, a.order
+  end
+
+  -- handler methods 
+
+  function StateHandler:GetProp( info )
+    -- gets property of the same name as the options arg
+    return GetProperty(self.bar, self:GetName(), info[#info])
+  end
+
+  function StateHandler:SetProp( info, value )
+    -- sets property of the same name as the options arg
+    SetProperty(self.bar, self:GetName(), info[#info], value)
+  end
+
+  function StateHandler:DeleteState()
+    if self.states[self:GetName()] then
+      self.states[self:GetName()] = nil
+      ApplyStates(self.bar)
+    end
+    optionMap[self.bar].args[self:GetName()] = nil
+  end
+
+  function StateHandler:SetStateName(info, value)
+    -- check for existing state name
+    if self.states[value] then
+      ReAction:UserError(format(L["State named '%s' already exists"],value))
+      return
+    end
+    local args = optionMap[self.bar].args
+    local name = self:GetName()
+    self.states[value], args[value], self.states[name], args[name] = self.states[name], args[name], nil, nil
+    self:SetName(value)
+    ApplyStates(self.bar)
+    ReAction:ShowEditor(self.bar, moduleID, value)
+    end
+
+  function StateHandler:MoveStateUp()
+    local before, after = self:GetNeighbors()
+    if before then
+      self:SwapOrder(before, self:GetName())
+      ApplyStates(self.bar)
+    end
+  end
+
+  function StateHandler:MoveStateDown()
+    local before, after = self:GetNeighbors()
+    if after then
+      self:SwapOrder(self:GetName(), after)
+      ApplyStates(self.bar)
+    end
+  end
+
+  function StateHandler:GetAnchorDisabled()
+    return not GetProperty(self.bar, self:GetName(), "anchorEnable")
+  end
+
+  function StateHandler:GetAnchorFrames(info)
+    self._anchorframes = self._anchorframes or { }
+    table.wipe(self._anchorframes)
+
+    table.insert(self._anchorframes, "UIParent")
+    for name, bar in ReAction:IterateBars() do
+      table.insert(self._anchorframes, bar:GetFrame():GetName())
+    end
+    return self._anchorframes
+  end
+
+  function StateHandler:GetAnchorFrame(info)
+    local value = self:GetProp(info)
+    for k,v in pairs(self._anchorframes) do
+      if v == value then
+        return k
+      end
+    end
+  end
+
+  function StateHandler:SetAnchorFrame(info, value)
+    local f = _G[self._anchorframes[value]]
+    if f then
+      SetFrameRef(self.bar:GetFrame(), "anchor-"..self:GetName(), f)
+      self:SetProp(info, f:GetName())
+    end
+  end
+
+  function StateHandler:SetAnchorPointProp(info, value)
+    self:SetProp(info, value ~= "NONE" and value or nil)
+  end
+
+  function StateHandler:GetAnchorPointProp(info)
+    return self:GetProp(info) or "NONE"
+  end
+
+  function StateHandler:GetScale(info)
+    return self:GetProp(info) or 1.0
+  end
+
+  function StateHandler:GetScaleDisabled()
+    return not GetProperty(self.bar, self:GetName(), "enableScale")
+  end
+
+  function StateHandler:GetAlpha(info)
+    return self:GetProp(info) or 1.0
+  end
+
+  function StateHandler:GetAlphaDisabled()
+    return not GetProperty(self.bar, self:GetName(), "enableAlpha")
+  end
+
+  function StateHandler:SetType(info, value)
+    self:SetRuleField("type", value)
+    self:FixAll()
+    ApplyStates(self.bar)
+  end
+
+  function StateHandler:GetType()
+    return self:GetRuleField("type")
+  end
+
+  function StateHandler:GetClearAllDisabled()
+    local t = self:GetRuleField("type")
+    return not( t == "any" or t == "all" or t == "custom")
+  end
+
+  function StateHandler:ClearAllConditions()
+    local t = self:GetRuleField("type")
+    if t == "custom" then
+      self:SetRuleField("custom","")
+    elseif t == "any" or t == "all" then
+      self:SetRuleField("values", {})
+    end
+    ApplyStates(self.bar)
+  end
+
+  function StateHandler:GetConditionsDisabled()
+    local t = self:GetRuleField("type")
+    return not( t == "any" or t == "all")
+  end
+
+  function StateHandler:SetCondition(info, key, value)
+    self:SetRuleField(ruleMap[key], value or nil, "values")
+    if value then
+      self:FixAll(ruleMap[key])
+    end
+    ApplyStates(self.bar)
+  end
+
+  function StateHandler:GetCondition(info, key)
+    return self:GetRuleField("values", ruleMap[key]) or false
+  end
+
+  function StateHandler:GetCustomDisabled()
+    return self:GetRuleField("type") ~= "custom"
+  end
+
+  function StateHandler:SetCustomRule(info, value)
+    self:SetRuleField("custom",value)
+    ApplyStates(self.bar)
+  end
+
+  function StateHandler:GetCustomRule()
+    return self:GetRuleField("custom") or ""
+  end
+
+  function StateHandler:ValidateCustomRule(info, value)
+    local s = value:gsub("%s","") -- remove all spaces
+    -- unfortunately %b and captures don't support the '+' notation, or this would be considerably simpler
+    repeat
+      if s == "" then
+        return true
+      end
+      local c, r = s:match("(%b[])(.*)")
+      if c == nil and s and #s > 0 then
+        return format(L["Invalid custom rule '%s': each clause must appear within [brackets]"],value or "")
+      end
+      s = r
+    until c == nil
+    return true
+  end
+
+  function StateHandler:GetKeybindDisabled()
+    return self:GetRuleField("type") ~= "keybind"
+  end
+
+  function StateHandler:GetKeybind()
+    return self:GetRuleField("keybind")
+  end
+
+  function StateHandler:SetKeybind(info, value)
+    if value and #value == 0 then
+      value = nil
+    end
+    self:SetRuleField("keybind",value)
+    ApplyStates(self.bar)
+  end
+
+  local function CreateStateOptions(bar, name)
+    local opts = { 
+      type = "group",
+      name = name,
+      childGroups = "tab",
+      args = stateOptions
+    }
+
+    opts.handler = StateHandler:New(bar,opts)
+
+    return opts
+  end
+
+  function module:GetBarOptions(bar)
+    local private = { }
+    local states = tbuild(module.db.profile.bars, bar:GetName(), "states")
+    local options = {
+      name = L["Dynamic State"],
+      type = "group",
+      order = -1,
+      childGroups = "tree",
+      disabled = InCombatLockdown,
+      args = {
+        __desc__ = {
+          name = L["States are evaluated in the order they are listed"],
+          order = 1,
+          type = "description",
+        },
+        __new__ = {
+          name = L["New State..."],
+          order = 2,
+          type = "group",
+          args = {
+            name = {
+              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"],
+            },
+            create = {
+              name = L["Create State"],
+              order = 2,
+              type = "execute",
+              func = function ()
+                  local name = private.newstatename
+                  if states[name] then
+                    ReAction:UserError(format(L["State named '%s' already exists"],name))
+                  else
+                    -- TODO: select default state options and pass as final argument
+                    states[name] = { }
+                    optionMap[bar].args[name] = CreateStateOptions(bar,name)
+                    ReAction:ShowEditor(bar, moduleID, name)
+                    private.newstatename = ""
+                  end
+                end,
+              disabled = function()
+                  local name = private.newstatename or ""
+                  return #name == 0 or name:find("%W")
+                end,
+            }
+          }
+        }
+      }
+    }
+    for name, config in pairs(states) do
+      options.args[name] = CreateStateOptions(bar,name)
+    end
+    optionMap[bar] = options
+    return options
+  end
+end
+
+-- Module API --
+
+-- Pass in a property field-name, an implementation secure snippet, a static options table, and an 
+-- optional options handler method-table
+--
+-- The options table is static, i.e. not bar-specific and should only reference handler method
+-- strings (either existing ones or those added via optHandler). The existing options are ordered
+-- 90-99. Order #1 is reserved for the heading.
+--
+-- The contents of optHandler, if provided, will be added to the existing StateHandler options metatable.
+-- See above, for existing API. In particular see the properties set up in the New method: self.bar,
+-- self.states, and self:GetName(), and the generic property handlers self:GetProp() and self:SetProp().
+--
+function module:RegisterStateProperty( field, snippetHandler, options, optHandler )
+  RegisterProperty(field, snippetHandler)
+  RegisterPropertyOptions(field, options, optHandler)
+end
+
+function module:UnregisterStateProperty( field )
+  UnregisterProperty(field)
+  UnregisterPropertyOptions(field)
+end
+
+
+-- Export methods to Bar class --
+
+function ReAction.Bar:GetState()
+  return GetManagedEnvironment(self:GetFrame()).state
+end
+
+ReAction.Bar.GetStateProperty = GetProperty
+ReAction.Bar.SetStateProperty = SetProperty