Mercurial > wow > reaction
view State.lua @ 76:c8c8610fd864
Bar menu now opens directly to bar config page (thanks for the AceConfigDialog update, nevcairiel!)
author | Flick <flickerstreak@gmail.com> |
---|---|
date | Thu, 19 Jun 2008 17:48:57 +0000 |
parents | 06cd74bdc7da |
children | da8ba8783924 |
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 -- 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 -- 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 name of the 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:SetStateAttribute("hidestates", nil, table.concat(hs,","), true) -- pass to buttons end, page = function( bar, states ) local map = { } for state, config in pairs(states) do if config.page then map[state] = format("page%d",config.page) end end bar:SetStateAttribute("statebutton", map) 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) 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 -- -- 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) -- 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"]}, {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 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 format(L["State named '%s' already exists"],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 return true end, hidden = function() --return bar:GetNumPages() < 2 return true end, values = function() -- use off-by-one ordering to put (none) first in the list local pages = { [1] = L["(none)"] } --for i = 1, bar:GetNumPages() do -- pages[i+1] = i --end return pages end, set = function(info, value) value = value - 1 setprop(info, value > 0 and value or nil) end, get = function(info) return getprop(info) or L["(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 format(L["Invalid custom rule '%s': each clause must appear within [brackets]"],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 states = tbuild(module.db.profile.bars, bar:GetName(), "states") 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(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) 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