Mercurial > wow > reaction
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