view State.lua @ 81:57f8151ea0f0

- Fixed some snafus with creating bars - Added support for opening the bar editor to a particular path - Creating bars/states now selects the new bar/state in the config editor - moved Bar:SetStateAttribute() back to working optionally on buttons rather than buttonFrame container
author Flick <flickerstreak@gmail.com>
date Wed, 25 Jun 2008 21:07:18 +0000
parents 42ec2938d65a
children 1ad208c25618
line wrap: on
line source
--[[
  ReAction bar state driver interface

--]]

-- local imports
local ReAction = ReAction
local L = ReAction.L
local _G = _G
local InCombatLockdown = InCombatLockdown
local format = string.format

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


local InitRules, ApplyStates, SetProperty, GetProperty, RegisterProperty

-- PRIVATE --
do
  -- 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 = GetShapeshiftFormInfo(i)
      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


  -- state property functions
  local ofskeys = {
    anchorPoint = "point",
    anchorRelPoint = "relpoint",
    anchorX = "x",
    anchorY = "y" 
  }

  local barofsidx = {
    anchorPoint = 1,
    anchorRelPoint = 3,
    anchorX = 4,
    anchorY = 5
  }

  local function UpdatePartialAnchor(bar, states, ckey)
    local map = { }
    local bc = bar.config
    for state, c in pairs(states) do
      if c.enableAnchor then
        map[state] = c[ckey]
      end
    end
    local ofskey = ofskeys[ckey]
    local default = select(barofsidx[ckey], bar:GetAnchor())
    bar:SetStateAttribute(format("headofs%s",ofskeys[ckey]), map, default)
  end
  
  -- the table key name for each function maps to the name of the config element
  local propertyFuncs = { 
    hide = function( bar, states )
      local hs = { }
      for state, config in pairs(states) do
        if config.hide then
          table.insert(hs, state)
        end
      end
      bar:GetButtonFrame():SetAttribute("hidestates", table.concat(hs,","))
    end,

    keybindstate = function( bar, states )
      local map = { }
      for state, config in pairs(states) do
        local kbset = config.keybindstate and state
        map[state] = kbset
        for button in bar:IterateButtons() do
          -- TODO: inform children they should maintain multiple binding sets
          -- ?? button:UpdateBindingSet(kbset)
        end
      end
      bar:SetStateAttribute("statebindings", map, true) -- apply to button frame, bindings only work for direct children
    end,

    enableAnchor = function( bar, states )
      for ckey in pairs(ofskeys) do
        UpdatePartialAnchor(bar, states, ckey)
      end
    end,

    enableScale = function( bar, states )
      local map = { }
      for state, c in pairs(states) do
        if c.enableScale then
          map[state] = c.scale
        end
      end
      bar:SetStateAttribute("headscale", map, 1.0)
    end,
  }

  -- generate some table entries
  propertyFuncs.scale = propertyFuncs.enableScale
  for ckey in pairs(ofskeys) do
    propertyFuncs[ckey] = function( bar, states )
      UpdatePartialAnchor(bar, states, ckey)
    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

  function RegisterProperty( propname, f )
    propertyFuncs[propname] = f
    for bar in ReAction:IterateBars() do
      local states = tfetch(module.db.profile.bars, bar:GetName(), "states")
      if states then
        f(bar,states)
      end
    end
  end



  --
  -- Build a state-transition spec string and statemap to be passed to
  -- Bar:SetStateDriver().
  --
  -- The statemap building is complex: keybound states override all 
  -- other transitions, so must remain in their current state, but must
  -- also remember other transitions that happen while they're stuck there
  -- so that when the binding is toggled off it can return to the proper state
  --
  local function BuildStateMap(states)
    local rules = { }
    local statemap = { }
    local keybinds = { }
    local default

    -- first grab all the keybind override states
    -- and construct an override template
    local override
    do
      local overrides = { }
      for name, state in pairs(states) do
        local type = tfetch(state, "rule", "type")
        if type == "keybind" then
          -- use the state-stack to remember the current transition
          -- use $s as a marker for a later call to gsub()
          table.insert(overrides, format("%s:$s set() %s", name, name))
        end
      end
      if #overrides > 0 then
        table.insert(overrides, "") -- for a trailing ';'
      end
      override = table.concat(overrides, ";") or ""
    end

    -- now iterate the rules in order
    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" then
        if c.values then
          local clauses = { }
          for key, value in pairs(c.values) do
            table.insert(clauses, format("[%s]", ruleformats[key]))
          end
          if #clauses > 0 then
            table.insert(rules, format("%s %s", table.concat(clauses), state))
          end
        end
      elseif 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
            table.insert(rules, format("%s %s", format("[%s]", table.concat(clauses, ",")), state))
          end
        end
      end
      
      -- use a different virtual button for the actual keybind transition,
      -- to implement a toggle. You have to clear it regardless of the type
      -- (which is usually a no-op) to unbind state transitions when switching
      -- transition types.
      local bindbutton = format("%s_binding",state)
      if type == "keybind" then
        keybinds[bindbutton] = c.keybind or false
        statemap[bindbutton] = format("%s:pop();*:set(%s)", state, state)
      else
        keybinds[bindbutton] = false
      end

      -- construct the statemap. gsub() the state name into the override template.
      statemap[state] = format("%s%s", override:gsub("%$s",state), state)
    end
    -- make sure that the default, if any, is last
    if default then
      table.insert(rules, default)
    end
    return table.concat(rules,";"), statemap, keybinds
  end

  function ApplyStates( bar )
    local states = tfetch(module.db.profile.bars, bar:GetName(), "states")
    if states then
      local rule, statemap, keybinds = BuildStateMap(states)
      bar:SetStateDriver("reaction", rule, statemap)
      for state, key in pairs(keybinds) do
        bar:SetAttributeBinding(state, key, "state-reaction", state)
      end
      for k, f in pairs(propertyFuncs) do
        f(bar, states)
      end
    end
  end

end



-- module event handlers --

function module:OnInitialize()
  self.db = ReAction.db:RegisterNamespace( moduleID, 
    {
      profile = { 
        bars = { },
      }
    }
  )

  InitRules()
  self:RegisterEvent("PLAYER_AURAS_CHANGED")

  ReAction:RegisterBarOptionGenerator(self, "GetBarOptions")

  ReAction.RegisterCallback(self, "OnCreateBar","OnRefreshBar")
  ReAction.RegisterCallback(self, "OnRefreshBar")
  ReAction.RegisterCallback(self, "OnEraseBar")
  ReAction.RegisterCallback(self, "OnRenameBar")
  ReAction.RegisterCallback(self, "OnConfigModeChanged")
end

function module:PLAYER_AURAS_CHANGED()
  self:UnregisterEvent("PLAYER_AURAS_CHANGED")
  -- on login the number of stances is 0 until this event fires during the init sequence.
  -- however if you reload just the UI the number of stances is correct immediately
  -- and this event won't fire until you gain/lose buffs/debuffs, at which point you might
  -- be in combat.
  if not InCombatLockdown() then
    InitRules()
    for name, bar in ReAction:IterateBars() do
      self:OnRefreshBar(nil,bar,name)
    end
  end
end

function module:OnRefreshBar(event, bar, name)
  local c = self.db.profile.bars[name]
  if c then
    ApplyStates(bar)
  end
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)
  -- nothing to do (yet)
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",
        },
        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 = {
            enableAnchor = {
              name  = L["Set New Position"],
              order = 1,
              type  = "toggle",
              set   = "SetProp",
              get   = "GetProp",
            },
            anchorPoint = {
              name  = L["Point"],
              order = 2,
              type  = "select",
              values = pointTable,
              set   = "SetAnchorPointProp",
              get   = "GetAnchorPointProp",
              disabled = "GetAnchorDisabled",
              hidden = "GetAnchorDisabled",
            },
            anchorRelPoint = {
              name  = L["Relative Point"],
              order = 3,
              type  = "select",
              values = pointTable,
              set   = "SetAnchorPointProp",
              get   = "GetAnchorPointProp",
              disabled = "GetAnchorDisabled",
              hidden = "GetAnchorDisabled",
            },
            anchorX = {
              name  = L["X Offset"],
              order = 4,
              type  = "range",
              min   = -100,
              max   = 100,
              step  = 1,
              set   = "SetProp",
              get   = "GetProp",
              disabled = "GetAnchorDisabled",
              hidden = "GetAnchorDisabled",
            },
            anchorY = {
              name  = L["Y Offset"],
              order = 5,
              type  = "range",
              min   = -100,
              max   = 100,
              step  = 1,
              set   = "SetProp",
              get   = "GetProp",
              disabled = "GetAnchorDisabled",
              hidden = "GetAnchorDisabled",
            },
          },
        },
        scale = {
          name  = L["Scale"],
          order = 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.1,
              max   = 2.5,
              step  = 0.05,
              isPercent = true,
              set   = "SetProp",
              get   = "GetProp",
              disabled = "GetScaleDisabled",
              hidden = "GetScaleDisabled",
            },
          },
        },
      },
      plugins = { }
    },
    rules = {
      name   = L["Rule"],
      order  = 3,
      type   = "group",
      args   = {
        mode = {
          name   = L["Select this state"],
          order  = 2,
          type   = "select",
          style  = "radio",
          values = { 
            default = L["by default"], 
            any = L["when ANY of these"], 
            all = L["when ALL of these"], 
            custom = L["via custom rule"],
            keybind = L["via keybinding"],
          },
          set    = "SetType",
          get    = "GetType",
        },
        clear = {
          name     = L["Clear All"],
          order    = 3,
          type     = "execute",
          hidden   = "GetClearAllDisabled",
          disabled = "GetClearAllDisabled",
          func     = "ClearAllConditions",
        },
        inputs = {
          name     = L["Conditions"],
          order    = 4,
          type     = "multiselect",
          hidden   = "GetConditionsDisabled",
          disabled = "GetConditionsDisabled",
          values   = ruleSelect,
          set      = "SetCondition",
          get      = "GetCondition",
        },
        custom = {
          name = L["Custom Rule"],
          order = 5,
          type = "input",
          multiline = true,
          hidden = "GetCustomDisabled",
          disabled = "GetCustomDisabled",
          desc = L["Syntax like macro rules: see preset rules for examples"],
          set  = "SetCustomRule",
          get  = "GetCustomRule",
          validate = "ValidateCustomRule",
        },
        keybind = {
          name = L["Keybinding"],
          order = 6,
          inline = true,
          hidden = "GetKeybindDisabled",
          disabled = "GetKeybindDisabled",
          type = "group",
          args = {
            desc = {
              name = L["Invoking a state keybind toggles an override of all other transition rules."],
              order = 1,
              type = "description",
            },
            keybind = {
              name = L["State Hotkey"],
              desc = L["Define an override toggle keybind"],
              order = 2,
              type = "keybinding",
              set  = "SetKeybind",
              get  = "GetKeybind",
            },
          },
        },
      },
    },
  }

  local StateHandler = { }

  function StateHandler:New( bar, opts )
    local self = setmetatable({ bar = bar }, { __index = StateHandler })

    function self:GetName()
      return opts.name
    end

    function self:SetName(name)
      opts.name = name
    end

    function self:GetOrder()
      return opts.order
    end

    -- get reference to states table: even if the bar
    -- name changes the states table ref won't
    self.states = tbuild(module.db.profile.bars, bar:GetName(), "states")

    tbuild(self.states, opts.name)

    opts.order = self:GetRule("order")
    if opts.order == nil then
      -- add after the highest
      opts.order = 100
      for _, state in pairs(self.states) do
        local x = tonumber(tfetch(state, "rule", "order"))
        if x and x >= opts.order then
          opts.order = x + 1
        end
      end
      self:SetRule("order",opts.order)
    end

    return self
  end

  -- helper methods

  function StateHandler:SetRule( key, value, ... )
    tbuild(self.states, self:GetName(), "rule", ...)[key] = value
  end

  function StateHandler:GetRule( ... )
    return tfetch(self.states, self:GetName(), "rule", ...)
  end

  function StateHandler:FixAll( setkey )
    -- if multiple selections in the same group are chosen when 'all' is selected,
    -- keep only one of them. If changing the mode, the first in the fields list will 
    -- be chosen arbitrarily. Otherwise, if selecting a new checkbox from the field-set,
    -- it will be retained.
    local notified = false
    if self:GetRule("type") == "all" then
      for _, c in ipairs(rules) do
        local rule, hidden, fields = unpack(c)
        local once = false
        if setkey then
          for idx, field in ipairs(fields) do
            if next(field) == setkey then
              once = true
            end
          end
        end
        for idx, field in ipairs(fields) do
          local key = next(field)
          if self:GetRule("values",key) then
            if once and key ~= setkey then
              self:SetRule(key,false,"values")
              if not setkey and not notified then
                ReAction:UserError(L["Warning: one or more incompatible rules were turned off"])
                notified = true
              end
            end
            once = true
          end
        end
      end
    end
  end

  function StateHandler:GetNeighbors()
    local before, after
    for k, v in pairs(self.states) do
      local o = tonumber(tfetch(v, "rule", "order"))
      if o and k ~= self:GetName() then
        local obefore = tfetch(self.states,before,"rule","order")
        local oafter  = tfetch(self.states,after,"rule","order")
        if o < self:GetOrder() and (not obefore or obefore < o) then
          before = k
        end
        if o > self:GetOrder() and (not oafter or oafter > o) then
          after = k
        end
      end
    end
    return before, after
  end

  function StateHandler:SwapOrder( a, b )
    -- do options table
    local args = optionMap[self.bar].args
    args[a].order, args[b].order = args[b].order, args[a].order
    -- do profile
    a = tbuild(self.states, a, "rule")
    b = tbuild(self.states, b, "rule")
    a.order, b.order = b.order, a.order
  end

  -- handler methods 

  function StateHandler:GetProp( info )
    -- gets property of the same name as the options arg
    return GetProperty(self.bar, self:GetName(), info[#info])
  end

  function StateHandler:SetProp( info, value )
    -- sets property of the same name as the options arg
    SetProperty(self.bar, self:GetName(), info[#info], value)
  end

  function StateHandler:DeleteState()
    if self.states[self:GetName()] then
      self.states[self:GetName()] = nil
      ApplyStates(self.bar)
    end
    optionMap[self.bar].args[self:GetName()] = nil
  end

  function StateHandler:SetStateName(info, value)
    -- check for existing state name
    if self.states[value] then
      ReAction:UserError(format(L["State named '%s' already exists"],value))
      return
    end
    local args = optionMap[self.bar].args
    local name = self:GetName()
    self.states[value], args[value], self.states[name], args[name] = self.states[name], args[name], nil, nil
    self:SetName(value)
    ApplyStates(self.bar)
  end

  function StateHandler:MoveStateUp()
    local before, after = self:GetNeighbors()
    if before then
      self:SwapOrder(before, self:GetName())
      ApplyStates(self.bar)
    end
  end

  function StateHandler:MoveStateDown()
    local before, after = self:GetNeighbors()
    if after then
      self:SwapOrder(self:GetName(), after)
      ApplyStates(self.bar)
    end
  end

  function StateHandler:GetAnchorDisabled()
    return not GetProperty(self.bar, self:GetName(), "enableAnchor")
  end

  function StateHandler:SetAnchorPointProp(info, value)
    self:SetProp(info, value ~= "NONE" and value or nil)
  end

  function StateHandler:GetAnchorPointProp(info)
    return self:GetProp(info) or "NONE"
  end

  function StateHandler:GetScaleDisabled()
    return not GetProperty(self.bar, self:GetName(), "enableScale")
  end

  function StateHandler:SetType(info, value)
    self:SetRule("type", value)
    self:FixAll()
    ApplyStates(self.bar)
  end

  function StateHandler:GetType()
    return self:GetRule("type")
  end

  function StateHandler:GetClearAllDisabled()
    local t = self:GetRule("type")
    return not( t == "any" or t == "all" or t == "custom")
  end

  function StateHandler:ClearAllConditions()
    local t = self:GetRule("type")
    if t == "custom" then
      self:SetRule("custom","")
    elseif t == "any" or t == "all" then
      self:SetRule("values", {})
    end
    ApplyStates(self.bar)
  end

  function StateHandler:GetConditionsDisabled()
    local t = self:GetRule("type")
    return not( t == "any" or t == "all")
  end

  function StateHandler:SetCondition(info, key, value)
    self:SetRule(ruleMap[key], value or nil, "values")
    if value then
      self:FixAll(ruleMap[key])
    end
    ApplyStates(self.bar)
  end

  function StateHandler:GetCondition(info, key)
    return self:GetRule("values", ruleMap[key]) or false
  end

  function StateHandler:GetCustomDisabled()
    return self:GetRule("type") ~= "custom"
  end

  function StateHandler:SetCustomRule(info, value)
    self:SetRule("custom",value)
    ApplyStates(self.bar)
  end

  function StateHandler:GetCustomRule()
    return self:GetRule("custom") or ""
  end

  function StateHandler:ValidateCustomRule(info, value)
    local s = value:gsub("%s","") -- remove all spaces
    -- unfortunately %b and captures don't support the '+' notation, or this would be considerably simpler
    repeat
      if s == "" then
        return true
      end
      local c, r = s:match("(%b[])(.*)")
      if c == nil and s and #s > 0 then
        return format(L["Invalid custom rule '%s': each clause must appear within [brackets]"],value or "")
      end
      s = r
    until c == nil
    return true
  end

  function StateHandler:GetKeybindDisabled()
    return self:GetRule("type") ~= "keybind"
  end

  function StateHandler:GetKeybind()
    return self:GetRule("keybind")
  end

  function StateHandler:SetKeybind(info, value)
    if value and #value == 0 then
      value = nil
    end
    self:SetRule("keybind",value)
    ApplyStates(self.bar)
  end

  local function CreateStateOptions(bar, name)
    local opts = { 
      type = "group",
      name = name,
      childGroups = "tab",
      args = stateOptions
    }

    opts.handler = StateHandler:New(bar,opts)

    return opts
  end


  function RegisterPropertyOptions( field, options, handler )
    stateOptions.properties.plugins[field] = options
    if handler then
      for k,v in pairs(handler) do
        StateHandler[k] = v
      end
    end
  end


  function module:GetBarOptions(bar)
    local private = { }
    local states = tbuild(module.db.profile.bars, bar:GetName(), "states")
    local options = {
      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 function, a static options table, and an 
-- optional options handler method-table
--
-- propertyImplFunc prototype:
--   propertyImplFunc( bar, stateTable )
--     where stateTable is a { ["statename"] = { state config } } table.
--
-- The options table is static, i.e. not bar-specific and should only reference handler method
-- strings (either existing ones or those added via optHandler). The existing options are ordered
-- 90-99. Order #1 is reserved for the heading.
--
-- The contents of optHandler, if provided, will be added to the existing StateHandler metatable.
-- See above, for existing API. In particular see the properties set up in the New method: self.bar,
-- self.states, and self:GetName(), and the generic property handlers self:GetProp() and self:SetProp().
--
function module:RegisterStateProperty( field, propertyImplFunc, options, optHandler )
  RegisterProperty(field, propertyImplFunc)
  RegisterPropertyOptions(field, options, optHandler)
end