view State.lua @ 72:aa88aed52124

Fixed bugs with state keybinds. Simplified state driver API
author Flick <flickerstreak@gmail.com>
date Thu, 05 Jun 2008 18:34:36 +0000
parents 3d2cef5dc459
children 06cd74bdc7da
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

-- 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

-- PRIVATE --

local InitRules, ApplyStates, SetProperty, GetProperty
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",
  }

  -- 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 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 treekin   = forms["Interface\\Icons\\Ability_Druid_TreeofLife"] or 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     = ("stance:%d"):format(defensive)
    ruleformats.berserker     = ("stance:%d"):format(berserker)
    ruleformats.caster        = ("form:0/%d/%d/%d"):format(aquatic, travel, flight)
    ruleformats.bear          = ("form:%d"):format(bear)
    ruleformats.cat           = ("form:%d"):format(cat)
    ruleformats.treeOrMoonkin = ("form:%d"):format(treekin)
  end

  -- return a new array of keys of table 't', sorted by comparing 
  -- sub-fields (obtained via tfetch) of the table values
  local function fieldsort( t, ... )
    local r = { }
    for k in pairs(t) do
      table.insert(r,k)
    end
    local dotdotdot = { ... }
    table.sort(r, function(lhs, rhs)
       local olhs = tfetch(t[lhs], unpack(dotdotdot)) or 0
       local orhs = tfetch(t[rhs], unpack(dotdotdot)) or 0
       return olhs < orhs
      end)
    return r
  end

  local function BuildRules(states)
    local rules = { }
    local keybinds = { }
    local default
    local fmt = "%s %s"
    for idx, state in ipairs(fieldsort(states, "rule", "order")) do
      local c = states[state].rule
      local type = tfetch(c,"type")
      if type == "default" then
        default = default or state
      elseif type == "keybind" then
        keybinds[state] = c.keybind or false
      elseif type == "custom" then
        if c.custom then
          -- strip out all spaces from the custom rule
          table.insert(rules, fmt:format(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, ("[%s]"):format(ruleformats[key]))
          end
          if #clauses > 0 then
            table.insert(rules, fmt:format(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, fmt:format(("[%s]"):format(table.concat(clauses, ",")), state))
          end
        end
      end
    end
    if default then
      table.insert(rules, default)
    end
    return rules, keybinds
  end

  local propertyFuncs = { }

  function ApplyStates( bar )
    local states = tfetch(module.db.profile.bars, bar:GetName(), "states")
    if states then
      local rules, keybinds = BuildRules(states)
      bar:SetStateDriver(table.concat(rules,";"), states, keybinds)
      for k, f in pairs(propertyFuncs) do
        f(bar, states)
      end
    end
  end

  function GetProperty( bar, state, propname )
    return tfetch(module.db.profile.bars, bar:GetName(), "states", state, propname)
  end

  function SetProperty( bar, state, propname, value )
    local states = tbuild(module.db.profile.bars, bar:GetName(), "states")
    tbuild(states, state)[propname] = value
    local f = propertyFuncs[propname]
    if f then
      f(bar, states)
    end
  end

  -- state property functions
  function propertyFuncs.hide( bar, states )
    local tmp = { }
    for state, config in pairs(states) do
      if config.hide then
        table.insert(tmp, state)
      end
    end
    local s = table.concat(tmp,",")
    bar:SetHideStates(s)
  end

  function propertyFuncs.page( bar, states )
    local map = { }
    for state, config in pairs(states) do
      map[state] = config.page
    end
    bar:SetStatePageMap(state, map)
  end

  function propertyFuncs.keybindstate( bar, states )
    local map = { }
    for state, config in pairs(states) do
      if config.keybindstate then
        table.insert(map,state)
      end
    end
    bar:SetStateKeybindOverrideMap(map)
  end

  local function updateAnchor(bar, states)
    local map = { }
    for state, c in pairs(states) do
      if c.enableAnchor then
        map[state] = { point = c.anchorPoint, relpoint = c.anchorRelPoint, x = c.anchorX, y = c.anchorY }
      end
    end
    bar:SetStateAnchorMap(map)
  end

  propertyFuncs.enableAnchor   = updateAnchor
  propertyFuncs.anchorPoint    = updateAnchor
  propertyFuncs.anchorRelPoint = updateAnchor
  propertyFuncs.anchorX        = updateAnchor
  propertyFuncs.anchorY        = updateAnchor

  local function updateScale( bar, states )
    local map = { }
    for state, c in pairs(states) do
      if c.enablescale then
        map[state] = c.scale
      end
    end
    bar:SetStateScaleMap(map)
  end

  propertyFuncs.enablescale = updateScale
  propertyFuncs.scale       = updateScale

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 b = self.db.profile.bars
  bars[newname], bars[oldname] = bars[oldname], nil
end

function module:OnConfigModeChanged(event, mode)
  -- TODO: unregister all state drivers (temporarily) and hidestates
end



-- Options --

local CreateBarOptions
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

  -- pre-sorted by the order they should appear in
  local rules = {
    --  rule          hidden                          fields
    { "stance",  ClassCheck("WARRIOR"),          { {battle = L["Battle Stance"]}, {defensive = L["Defensive Stance"]}, {berserker = L["Berserker Stance"]} } },
    { "form",    ClassCheck("DRUID"),            { {caster = L["Caster Form"]}, {bear = L["Bear Form"]}, {cat = L["Cat Form"]}, {treeOrMoonkin = L["Tree/Moonkin"]}  } },
    { "stealth", ClassCheck("ROGUE","DRUID"),    { {stealth = L["Stealth"]}, {nostealth = L["No Stealth"]} } },
    { "shadow",  ClassCheck("PRIEST"),           { {shadowform = L["Shadowform"]}, {noshadowform = L["No Shadowform"]} } },
    { "pet",     ClassCheck("HUNTER","WARLOCK"), { {pet = L["With Pet"]}, {nopet = L["Without Pet"]} } },
    { "target",  false,                          { {harm = L["Hostile Target"]}, {help = L["Friendly Target"]}, {notarget = L["No Target"]} } },
    { "focus",   false,                          { {focusharm = L["Hostile Focus"]}, {focushelp = L["Friendly Focus"]}, {nofocus = L["No Focus"]} } },
    { "group",   false,                          { {raid = L["Raid"]}, {party = L["Party"]}, {solo = L["Solo"]} } },
    { "combat",  false,                          { {combat = L["In Combat"]}, {nocombat = L["Out of Combat"]} } },
  }

  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 function CreateStateOptions(bar, name)
    local opts = { 
      type = "group",
      name = name,
      childGroups = "tab",
    }

    local states = tbuild(module.db.profile.bars, bar:GetName(), "states")

    local function update()
      ApplyStates(bar)
    end

    local function setrule( key, value, ... )
      tbuild(states, opts.name, "rule", ...)[key] = value
    end

    local function getrule( ... )
      return tfetch(states, opts.name, "rule", ...)
    end

    local function setprop(info, value)
      SetProperty(bar, opts.name, info[#info], value)
    end

    local function getprop(info)
      return GetProperty(bar, opts.name, info[#info])
    end

    local function fixall(setkey)
      -- if multiple selections in the same group are chosen when 'all' is selected,
      -- keep only one of them. If changing the mode, the first in the fields list will 
      -- be chosen arbitrarily. Otherwise, if selecting a new checkbox from the field-set,
      -- it will be retained.
      local notified = false
      for _, c in ipairs(rules) do
        local rule, hidden, fields = unpack(c)
        local found = false
        for key in ipairs(fields) do
          if getrule("values",key) then
            if (found or setkey) and key ~= setkey then
              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
            found = true
          end
        end
      end
    end

    local function getNeighbors()
      local before, after
      for k, v in pairs(states) do
        local o = tonumber(tfetch(v, "rule", "order"))
        if o and k ~= opts.name then
          local obefore = tfetch(states,before,"rule","order")
          local oafter  = tfetch(states,after,"rule","order")
          if o < opts.order and (not obefore or obefore < o) then
            before = k
          end
          if o > opts.order and (not oafter or oafter > o) then
            after = k
          end
        end
      end
      return before, after
    end

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

    local function anchordisable()
      return not GetProperty(bar, opts.name, "enableAnchor")
    end

    tbuild(states, name)

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

    opts.args = {
      ordering = {
        name = L["Info"],
        order = 1,
        type = "group",
        args = {
          delete = {
            name = L["Delete this State"],
            order = -1,
            type = "execute",
            func = function(info) 
                if states[opts.name] then
                  states[opts.name] = nil
                  ApplyStates(bar)
                end
                optionMap[bar].args[opts.name] = nil
              end,
          },
          rename = {
            name = L["Name"],
            order = 1,
            type = "input",
            get  = function() return opts.name end,
            set  = function(info, value) 
                     -- check for existing state name
                     if states[value] then
                       L["State named '%s' already exists"]:format(value)
                     end
                     local args = optionMap[bar].args
                     states[value], args[value], states[opts.name], args[opts.name] = states[opts.name], args[opts.name], nil, nil
                     opts.name = value
                     update()
                   end, 
            pattern = "^%w*$",
            usage = L["State names must be alphanumeric without spaces"],
          },
          ordering = {
            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  = function()
                          local before, after = getNeighbors()
                          if before then
                            swapOrder(before, opts.name)
                            update()
                          end
                        end,
              },
              down = {
                name  = L["Down"],
                order = 2,
                type  = "execute",
                width = "half",
                func  = function() 
                          local before, after = getNeighbors()
                          if after then
                            swapOrder(opts.name, after)
                            update()
                          end
                        end,
              }
            }
          }
        }
      },
      properties = {
        name = L["Properties"],
        order = 2,
        type = "group",
        args = { 
          desc = {
            name = L["Set the properties for the bar when in this state"],
            order = 1,
            type = "description"
          },
          hide = {
            name = L["Hide Bar"],
            order = 2,
            type = "toggle",
            set  = setprop,
            get  = getprop,
          },
          page = {
            name  = L["Show Page #"],
            order = 3,
            type  = "select",
            disabled = function()
                         return bar:GetNumPages() < 2
                       end,
            hidden   = function()
                         return bar:GetNumPages() < 2
                       end,
            values   = function()
                         local pages = { none = " " }
                         for i = 1, bar:GetNumPages() do
                           pages[i] = i
                         end
                         return pages
                       end,
            set      = function(info, value)
                         if value == "none" then
                           setprop(info, nil)
                         else
                           setprop(info, value)
                         end
                       end,
            get      = function(info)
                         return getprop(info) or "none"
                       end,
          },
          keybindstate = {
            name  = L["Override Keybinds"],
            desc  = L["Set this state to maintain its own set of keybinds which override the defaults when active"],
            order = 4,
            type  = "toggle",
            set   = setprop,
            get   = getprop,
          },
          position = {
            name  = L["Position"],
            order = 5,
            type  = "group",
            inline = true,
            args = {
              enableAnchor = {
                name  = L["Set New Position"],
                order = 1,
                type  = "toggle",
                set   = setprop,
                get   = getprop,
              },
              anchorPoint = {
                name  = L["Point"],
                order = 2,
                type  = "select",
                values = pointTable,
                set   = function(info, value) setprop(info, value ~= "NONE" and value or nil) end,
                get   = function(info) return getprop(info) or "NONE" end,
                disabled = anchordisable,
                hidden = anchordisable,
              },
              anchorRelPoint = {
                name  = L["Relative Point"],
                order = 3,
                type  = "select",
                values = pointTable,
                set   = function(info, value) setprop(info, value ~= "NONE" and value or nil) end,
                get   = function(info) return getprop(info) or "NONE" end,
                disabled = anchordisable,
                hidden = anchordisable,
              },
              anchorX = {
                name  = L["X Offset"],
                order = 4,
                type  = "range",
                min   = -100,
                max   = 100,
                step  = 1,
                set   = setprop,
                get   = getprop,
                disabled = anchordisable,
                hidden = anchordisable,
              },
              anchorY = {
                name  = L["Y Offset"],
                order = 5,
                type  = "range",
                min   = -100,
                max   = 100,
                step  = 1,
                set   = setprop,
                get   = getprop,
                disabled = anchordisable,
                hidden = anchordisable,
              },
            },
          },
          scale = {
            name  = L["Scale"],
            order = 6,
            type  = "group",
            inline = true,
            args = {
              enablescale = {
                name  = L["Set New Scale"],
                order = 1,
                type  = "toggle",
                set   = setprop,
                get   = getprop,
              },
              scale = {
                name  = L["Scale"],
                order = 2,
                type  = "range",
                min   = 0.1,
                max   = 2.5,
                step  = 0.05,
                isPercent = true,
                set   = setprop,
                get   = function(info) return getprop(info) or 1 end,
                disabled = function() return not GetProperty(bar, opts.name, "enablescale") end,
                hidden = function() return not GetProperty(bar, opts.name, "enablescale") end,
              },
            },
          },
        },
      },
      rules = {
        name   = L["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    = function( info, value )
                       setrule("type", value)
                       fixall()
                       update()
                     end,
            get    = function( info )
                       return getrule("type")
                     end,
          },
          clear = {
            name     = L["Clear All"],
            order    = 3,
            type     = "execute",
            hidden   = function()
                         local t = getrule("type")
                         return t ~= "any" and t ~= "all"
                       end,
            disabled = function()
                         local t = getrule("type")
                         return t ~= "any" and t ~= "all"
                       end,
            func     = function()
                         local type = getrule("type")
                         if type == "custom" then
                           setrule("custom","")
                         elseif type == "any" or type == "all" then
                           setrule("values", {})
                         end
                         update()
                       end,
          },
          inputs = {
            name     = L["Conditions"],
            order    = 4,
            type     = "multiselect",
            hidden   = function()
                         local t = getrule("type")
                         return t ~= "any" and t ~= "all"
                       end,
            disabled = function()
                         local t = getrule("type")
                         return t ~= "any" and t ~= "all"
                       end,
            values   = ruleSelect,
            set      = function(info, key, value )
                         setrule(ruleMap[key], value or nil, "values")
                         if value then
                           fixall(ruleMap[key])
                         end
                         update()
                       end,
            get      = function(info, key)
                         return getrule("values", ruleMap[key]) or false
                       end,
          },
          custom = {
            name = L["Custom Rule"],
            order = 5,
            type = "input",
            multiline = true,
            hidden = function()
                       return getrule("type") ~= "custom"
                     end,
            disabled = function()
                         return getrule("type") ~= "custom"
                       end,
            desc = L["Syntax like macro rules: see preset rules for examples"],
            set  = function(info, value) 
                     setrule("custom",value)
                     update()
                   end,
            get  = function(info)
                     return getrule("custom") or ""
                   end,
            validate = function (info, rule)
                local s = rule:gsub("%s","") -- remove all spaces
                -- unfortunately %b and captures don't support the '+' notation, or this would be considerably simpler
                repeat
                  if s == "" then
                    return true
                  end
                  local c, r = s:match("(%b[])(.*)")
                  if c == nil and s and #s > 0 then
                    return L["Invalid custom rule '%s': each clause must appear within [brackets]"]:format(rule)
                  end
                  s = r
                until c == nil
                return true
              end,
          },
          keybind = {
            name = L["Keybinding"],
            order = 6,
            inline = true,
            hidden = function() return getrule("type") ~= "keybind" end,
            disabled = function() return getrule("type") ~= "keybind" end,
            type = "group",
            args = {
              desc = {
                name = L["Invoking a state keybind 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  = function(info, value)
                         if value and #value == 0 then
                           value = nil
                         end
                         setrule("keybind",value)
                         update()
                       end,
                get  = function() return getrule("keybind") end,
              },
            },
          },
        },
      },
    }
    return opts
  end


  CreateBarOptions = function(bar)
    local private = { }
    local options = {
      type = "group",
      name = L["Dynamic State"],
      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(L["State named '%s' already exists"]:format(name))
                  else
                    -- TODO: select default state options and pass as final argument
                    states[name] = { }
                    optionMap[bar].args[name] = CreateStateOptions(bar,name)
                    private.newstatename = ""
                  end
                end,
              disabled = function()
                  local name = private.newstatename or ""
                  return #name == 0 or name:find("%W")
                end,
            }
          }
        }
      }
    }
    local states = tfetch(module.db.profile.bars, bar:GetName(), "states")
    if states then
      for name, config in pairs(states) do
        options.args[name] = CreateStateOptions(bar,name)
      end
    end
    optionMap[bar] = options
    return options
  end
end

function module:GetBarOptions(bar)
  return CreateBarOptions(bar)
end