-
Notifications
You must be signed in to change notification settings - Fork 153
Expand file tree
/
Copy pathEllesmereUI_Conditions.lua
More file actions
376 lines (355 loc) · 16.9 KB
/
Copy pathEllesmereUI_Conditions.lua
File metadata and controls
376 lines (355 loc) · 16.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
-------------------------------------------------------------------------------
-- EllesmereUI_Conditions.lua
-- Conditional Overrides engine: condition definitions, the active-group
-- resolver, event wiring, and the keybind toggle. The value/unlock override
-- integration lives in EllesmereUI_SpecOverrides.lua (it shares that file's
-- fkey helpers); this file only decides WHICH conditional group is active
-- and notifies the override system when that changes.
--
-- Resolution ladder (user-approved):
-- 1. keybind -- any group whose keybind toggle is ON (creation order
-- breaks ties). Explicit user action outranks ambient state.
-- 2. darkmode -- the MAIN Dark Mode master reads on (UF+RF; the Class
-- Resource Bar master is a separate toggle and excluded).
-- Location-independent, so it applies inside AND outside
-- instances; below keybind (explicit action still wins).
-- 3. instance -- dungeon / raid / arena / battleground. Naturally mutually
-- exclusive (the client reports one instance type). First
-- group in creation order that checks the current type.
-- 4. solo -- not in any group. Lowest priority.
-- Exactly one conditional group is active at a time, or none.
--
-- Condition flips NEVER happen in combat: flips are recomputed fresh at
-- PLAYER_REGEN_ENABLED (re-resolve, never replay -- the zone can change
-- twice during a fight).
-------------------------------------------------------------------------------
local L = function(s) return EllesmereUI.L and EllesmereUI.L(s) or s end
-------------------------------------------------------------------------------
-- Condition definitions (ordered display list for the picker UI).
-- comingSoon entries render disabled in the picker and never match.
-------------------------------------------------------------------------------
-- The Dark Mode condition tracks the MAIN "Dark Mode" master checkbox in
-- Fonts & Colors (Unit Frames + Raid Frames), NOT the separate "Dark Mode
-- (Class Resource Bar)" master: users commonly dark one without the other,
-- and the checkbox users read as "Dark Mode" is the main one. This filter
-- mirrors that checkbox's own read (_dmNotRB in EUI__General_Options.lua).
local function _dmMainFilter(p) return p.id ~= "resourceBars" end
local function DarkModeMasterOn()
return (EllesmereUI.IsDarkModeAllOn and EllesmereUI.IsDarkModeAllOn(_dmMainFilter)) or false
end
EllesmereUI.CONDITIONS = {
{ id = "keybind", label = "Keybind (out of combat)" },
-- requires: pickable in the condition picker only while it returns true
-- (newly checking is refused and the row renders dimmed with the hint;
-- UNchecking is always allowed so an existing group can never be
-- trapped). Same predicate the resolver reads, so the picker never
-- offers a condition that cannot currently hold.
{ id = "darkmode", label = "Dark Mode",
requires = DarkModeMasterOn,
requiresHint = "Enable Dark Mode (Global Settings, Fonts & Colors) to use this condition" },
{ id = "dungeon", label = "Dungeon" },
{ id = "raid", label = "Raid" },
{ id = "arena", label = "Arena" },
{ id = "battleground", label = "Battleground" },
{ id = "solo", label = "Solo (not in a group)" },
{ id = "druid_form", label = "Druid Form (out of combat)", comingSoon = true },
}
local INSTANCE_COND = {
party = "dungeon",
raid = "raid",
arena = "arena",
pvp = "battleground",
}
-------------------------------------------------------------------------------
-- Group store: profile.condOverrideGroups (array, creation order = priority
-- order within a tier). Mirrors profile.specOverrideGroups:
-- { id, name, icon = { kind, key }, conds = { [condID] = true },
-- key = "ALT-X" | nil, keyOn = true|nil }
-- key/keyOn only meaningful when conds.keybind; keyOn persists (profile).
-------------------------------------------------------------------------------
local function GetProfileRoot()
if not (EllesmereUIDB and EllesmereUI.GetProfilesDB) then return nil end
local pdb = EllesmereUI.GetProfilesDB()
local name = pdb.activeProfile or "Default"
return pdb.profiles and pdb.profiles[name]
end
function EllesmereUI.Conditions_GetGroups(create)
local prof = GetProfileRoot()
if not prof then return nil end
if not prof.condOverrideGroups then
if not create then return nil end
prof.condOverrideGroups = {}
end
return prof.condOverrideGroups
end
function EllesmereUI.Conditions_GroupById(gid)
local groups = EllesmereUI.Conditions_GetGroups()
if not groups then return nil end
for _, g in ipairs(groups) do
if g.id == gid then return g end
end
return nil
end
function EllesmereUI.Conditions_NewGroupId()
-- PERSISTED counter, never max+1: deleted groups' per-gid value maps can
-- survive on other groups' shared entries, and a reused id silently
-- inherited the dead group's overrides. The counter seeds from max+1
-- once (legacy stores) and only ever grows.
local prof = GetProfileRoot()
local groups = EllesmereUI.Conditions_GetGroups(true)
local maxId = 0
for _, g in ipairs(groups or {}) do
if type(g.id) == "number" and g.id > maxId then maxId = g.id end
end
if not prof then return maxId + 1 end
local nextId = prof.condOverrideNextId or 0
if nextId <= maxId then nextId = maxId end
nextId = nextId + 1
prof.condOverrideNextId = nextId
return nextId
end
-------------------------------------------------------------------------------
-- Resolver
-------------------------------------------------------------------------------
--- The currently-ACTIVE conditional group per the ladder, or nil.
function EllesmereUI.Conditions_ActiveGroup()
local groups = EllesmereUI.Conditions_GetGroups()
if not groups or #groups == 0 then return nil end
-- Tier 1: keybind toggles (explicit user action).
for _, g in ipairs(groups) do
if g.conds and g.conds.keybind and g.keyOn then return g end
end
-- Tier 2: the MAIN Dark Mode master (UF+RF; the Class Resource Bar
-- master is deliberately excluded -- see DarkModeMasterOn above).
-- Location-independent, so it is checked BEFORE the instance early-outs.
if DarkModeMasterOn() then
for _, g in ipairs(groups) do
if g.conds and g.conds.darkmode then return g end
end
end
-- Tier 3: instance type (client reports exactly one).
local _, instanceType = IsInInstance()
local cond = instanceType and INSTANCE_COND[instanceType]
if cond then
for _, g in ipairs(groups) do
if g.conds and g.conds[cond] then return g end
end
return nil -- in an instance: solo never applies
end
-- Tier 4: solo.
if not IsInGroup() then
for _, g in ipairs(groups) do
if g.conds and g.conds.solo then return g end
end
end
return nil
end
function EllesmereUI.Conditions_ActiveGid()
local g = EllesmereUI.Conditions_ActiveGroup()
return g and g.id or nil
end
-------------------------------------------------------------------------------
-- Flip machinery: flag-and-recompute, never replay. The override system's
-- transition handler (SpecOverrides_CondTransition) owns the actual
-- harvest/apply work and returns false when it cannot run yet (mid spec
-- transition, editing session open) -- the next event or the spec
-- pipeline's own Conditions_Recheck tail retries with fresh state.
-------------------------------------------------------------------------------
local _appliedGid = nil -- gid of the conditional group currently applied
local _flipPending = false
local _establish = false -- post profile-apply: apply-only, no harvests
-- Persisted mirror of the applied pointer (profile root). The applied
-- conditional's values are baked into the profile's saved module data, so
-- the pointer must survive with that data: after a /reload the runtime
-- pointer reset to nil while live still held the overlay, and every bank
-- that then resolved the applied gid as nil re-banked the overlay into the
-- conditional DEFAULT maps -- permanent baseline loss on every /reload or
-- login with a conditional active.
local function PersistAppliedGid(gid)
local prof = EllesmereUI.GetActiveProfileData and EllesmereUI.GetActiveProfileData()
if prof then prof.condAppliedGid = gid end
end
function EllesmereUI.Conditions_AppliedGid()
if _appliedGid ~= nil then return _appliedGid end
-- Stale windows (fresh login, profile-apply MarkStale before the
-- establish Recheck lands): fall back to the persisted pointer,
-- validated against the active profile's groups (a hand-edited or
-- imported store may point at a deleted group).
local prof = EllesmereUI.GetActiveProfileData and EllesmereUI.GetActiveProfileData()
local gid = prof and prof.condAppliedGid
if gid and EllesmereUI.Conditions_GroupById and EllesmereUI.Conditions_GroupById(gid) then
return gid
end
return nil
end
--- Profile apply swapped every store wholesale: the applied pointer refers
--- to the OLD profile's groups. Reset without a transition (the incoming
--- stores already hold their own persisted state) and let the follow-up
--- Recheck ESTABLISH against the new profile: apply-only (live is the
--- incoming raw data -- harvesting it would corrupt the new store) and
--- forced even when no conditional is active (the incoming unlock stores
--- may hold layer-valued data with a reset active pointer).
function EllesmereUI.Conditions_MarkStale()
_appliedGid = nil
_flipPending = false
_establish = true
end
--- True between a profile apply and its (possibly combat-deferred)
--- establish Recheck. In that window live holds a MIXED state -- baseline
--- links restored over the incoming profile's layer-valued module data --
--- and layout harvests must refuse to bank it (a second in-combat boundary
--- otherwise baked the un-established state into baselineLayout).
function EllesmereUI.Conditions_EstablishPending()
return _establish
end
function EllesmereUI.Conditions_Recheck()
if InCombatLockdown() then
_flipPending = true
return
end
local g = EllesmereUI.Conditions_ActiveGroup()
local gid = g and g.id or nil
-- Effective applied state: the runtime pointer, or -- in the stale
-- windows (fresh login, MarkStale-to-establish) -- the persisted pointer
-- recording which conditional's values are baked into live data. Using
-- the raw runtime pointer here treated a post-/reload overlay as "no
-- conditional applied": the matching-condition login early-outed without
-- adopting it, and the non-matching login handed the transition a nil
-- oldGid, whose harvest banked the overlay into the DEFAULT maps.
local applied = EllesmereUI.Conditions_AppliedGid()
if gid == applied and not _establish then
-- Adopt a persisted pointer into the runtime on the no-op path so
-- later compares and MarkStale operate on live state.
_appliedGid = applied
return
end
-- An open unlock session never survives a layout-owner change (same rule
-- as spec changes): discard-close first, then transition.
if EllesmereUI._unlockModeActive and EllesmereUI.ForceCloseUnlockDiscard then
EllesmereUI.ForceCloseUnlockDiscard()
end
local handler = EllesmereUI.SpecOverrides_CondTransition
if not handler then return end
-- Consume the establish request BEFORE the handler runs: anything inside
-- it (a nested RefreshAllAddons tail calls MarkStale) may raise a FRESH
-- request, which must survive this call instead of being clobbered on
-- success -- the next signal then converges values and pointer.
local est = _establish
_establish = false
-- The options panel's Default Editing Mode is not a reason to defer, only
-- one to step out of: the Dark Mode condition's inputs are all options
-- widgets, so its flips are ALWAYS raised with that view up, and a
-- deferred flip left the outgoing conditional applied (and reporting
-- itself applied) until an unrelated event or a /reload -- silently
-- filing the user's baseline edits into the override's maps in between.
-- Establish transitions never harvest, so they run in place; only real
-- flips need the roundtrip.
local resumeView = false
if not est and EllesmereUI.SpecOverrides_SuspendDefaultView then
resumeView = EllesmereUI.SpecOverrides_SuspendDefaultView()
end
if resumeView then
-- Leaving the view restores the spec's values and runs the touched
-- modules' refreshers, and an unmapped module falls back to a full
-- RefreshAllAddons -- whose tail re-enters this resolver and can
-- converge the flip on its own. Re-read both ends so the handler
-- never harvests live against an outgoing group it no longer holds.
g = EllesmereUI.Conditions_ActiveGroup()
gid = g and g.id or nil
applied = EllesmereUI.Conditions_AppliedGid()
if gid == applied then
_appliedGid = applied
if EllesmereUI.SpecOverrides_ResumeDefaultView then
EllesmereUI.SpecOverrides_ResumeDefaultView()
end
return
end
end
if handler(applied, gid, est) then
_appliedGid = gid
PersistAppliedGid(gid)
else
-- Busy (spec transition / edit session / re-entrant refresh): keep
-- the request and retry on the next signal.
_establish = est or _establish
_flipPending = true
end
-- Strictly AFTER the pointer advances: re-entering the view harvests
-- through HarvestCurrent, which banks against the APPLIED gid.
if resumeView and EllesmereUI.SpecOverrides_ResumeDefaultView then
EllesmereUI.SpecOverrides_ResumeDefaultView()
end
end
-------------------------------------------------------------------------------
-- Keybind toggle: pooled hidden buttons + SetOverrideBindingClick, rebuilt
-- out of combat (regen-deferred), mirroring the Action Bars visibility
-- toggle. One key per group; presses in combat are ignored (the toggle is
-- "out of combat" by definition).
-------------------------------------------------------------------------------
local _keyBtnPool = {}
local _bindCombatFrame
function EllesmereUI.Conditions_ToggleKey(gid)
if InCombatLockdown() then return end
local g = EllesmereUI.Conditions_GroupById(gid)
if not g or not (g.conds and g.conds.keybind) then return end
g.keyOn = not g.keyOn or nil
EllesmereUI.Conditions_Recheck()
end
function EllesmereUI.Conditions_RebuildKeyBindings()
if InCombatLockdown() then
if not _bindCombatFrame then
_bindCombatFrame = CreateFrame("Frame")
_bindCombatFrame:SetScript("OnEvent", function(self)
self:UnregisterEvent("PLAYER_REGEN_ENABLED")
EllesmereUI.Conditions_RebuildKeyBindings()
end)
end
_bindCombatFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
return
end
for _, btn in ipairs(_keyBtnPool) do
ClearOverrideBindings(btn)
end
local groups = EllesmereUI.Conditions_GetGroups()
if not groups then return end
local i = 0
for _, g in ipairs(groups) do
if g.conds and g.conds.keybind and g.key and g.key ~= "" then
i = i + 1
local btn = _keyBtnPool[i]
if not btn then
btn = CreateFrame("Button", "EUICondKeyBtn" .. i, UIParent)
btn:Hide()
_keyBtnPool[i] = btn
end
local gid = g.id
btn:SetScript("OnClick", function() EllesmereUI.Conditions_ToggleKey(gid) end)
SetOverrideBindingClick(btn, true, g.key, btn:GetName())
end
end
end
-------------------------------------------------------------------------------
-- Events. GROUP_ROSTER_UPDATE drives solo; PEW/zone drive instance flips;
-- regen drains combat-deferred rechecks. Registration order matters for the
-- regen drain: this file loads after EllesmereUI_Profiles.lua, so the spec
-- pipeline's regen branch runs first and our recompute sees settled state.
-------------------------------------------------------------------------------
local evFrame = CreateFrame("Frame")
evFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
evFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA")
evFrame:RegisterEvent("GROUP_ROSTER_UPDATE")
evFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
evFrame:SetScript("OnEvent", function(_, event)
if event == "PLAYER_REGEN_ENABLED" then
if _flipPending then
_flipPending = false
EllesmereUI.Conditions_Recheck()
end
return
end
if event == "PLAYER_ENTERING_WORLD" then
-- Bindings are per-profile data; re-assert after every load screen.
EllesmereUI.Conditions_RebuildKeyBindings()
end
EllesmereUI.Conditions_Recheck()
end)