Repository navigation
Expand file tree
/
Copy pathStack.lua
More file actions
261 lines (239 loc) · 8.89 KB
/
Copy pathStack.lua
File metadata and controls
261 lines (239 loc) · 8.89 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
local _, BS = ...
-- The addon owns layout, Blizzard owns state: we re-parent Cooldown Manager item frames
-- into our bars and never branch on a combat value. Blizzard keeps showing/hiding them.
local BASE, STEP = 10, 10 -- bar frame level = BASE + layer * STEP
BS.TEXTURES = { flat = "Interface\\Buttons\\WHITE8x8", blizzard = "Interface\\TargetingFrame\\UI-StatusBar" }
BS.ALL_VIEWERS = { "EssentialCooldownViewer", "UtilityCooldownViewer", "BuffIconCooldownViewer", "BuffBarCooldownViewer" }
local stack
local containers = {} -- bar id -> container frame
local borrowed = {} -- Blizzard item frame -> the viewer it came from
-- Which viewers a widget may borrow from, in preference order.
local function sources(visibility, display)
local buffs = display == "bar" and "BuffBarCooldownViewer" or "BuffIconCooldownViewer"
if visibility == "aura" then return { buffs } end
-- proc not tracked as an aura: fall back to the cooldown frame with Blizzard's proc glow
if visibility == "proc" then return { buffs, "EssentialCooldownViewer", "UtilityCooldownViewer" } end
return { "EssentialCooldownViewer", "UtilityCooldownViewer" }
end
function BS.FrameSpell(item)
local id = BS.try(item.GetSpellID, item)
if type(id) == "number" then return id end
local info = item.cooldownInfo or BS.try(item.GetCooldownInfo, item)
if type(info) ~= "table" and C_CooldownViewer then
local cd = item.cooldownID or BS.try(item.GetCooldownID, item)
info = cd and BS.try(C_CooldownViewer.GetCooldownViewerCooldownInfo, cd)
end
id = type(info) == "table" and info.spellID
if not BS.secret(id) and type(id) == "number" then return id end
end
-- Active item frames of a viewer, plus the ones we already took from it
-- (re-parented frames no longer show up as its children).
local function items(viewer)
local list, seen = {}, {}
local function add(it)
if it and not seen[it] then seen[it] = true; list[#list + 1] = it end
end
local ok, frames = pcall(viewer.GetItemFrames, viewer)
if ok and type(frames) == "table" then
for _, it in ipairs(frames) do add(it) end
elseif viewer.itemFramePool then
pcall(function() for it in viewer.itemFramePool:EnumerateActive() do add(it) end end)
else
for _, it in ipairs({ viewer:GetChildren() }) do add(it) end
end
for it, origin in pairs(borrowed) do
if origin == viewer then add(it) end
end
return list
end
-- Ranks share a name (Forever lists each rank as its own row), so match by ID or name.
function BS.Matches(item, sp)
local id = BS.FrameSpell(item)
return id ~= nil and (id == sp.baseSpell or BS.SpellName(id) == sp.name)
end
local function find(sp, visibility, used)
for _, name in ipairs(sources(visibility, sp.display)) do
local v = _G[name]
if v then
for _, it in ipairs(items(v)) do
if not used[it] and BS.Matches(it, sp) then return it end
end
end
end
end
local function container(id)
if containers[id] then return containers[id] end
local c = CreateFrame("Frame", nil, stack)
c.preview = CreateFrame("Frame", nil, c)
c.preview:SetAllPoints()
local bg = c.preview:CreateTexture(nil, "BACKGROUND")
bg:SetAllPoints()
bg:SetColorTexture(0, 0, 0, 0.35)
c.label = c.preview:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
c.label:SetPoint("LEFT", 4, 0)
containers[id] = c
return c
end
local function place(it, c, b, j)
if not borrowed[it] then borrowed[it] = it:GetParent() end
local sp, horiz = b.spells[j], b.orientation ~= "vertical"
local w = horiz and b.size.w / #b.spells or b.size.w
local h = horiz and b.size.h or b.size.h / #b.spells
local x, y = horiz and (j - 1) * w or 0, horiz and 0 or -(j - 1) * h
if sp.display ~= "bar" or b.visibility == "always" then w = math.min(w, h); h = w end
it:SetParent(c)
it:SetFrameStrata(c:GetFrameStrata())
it:SetFrameLevel(c:GetFrameLevel() + 1)
it:ClearAllPoints()
it:SetPoint("TOPLEFT", c, "TOPLEFT", x, y)
it:SetSize(w, h)
local bar = it.Bar
if bar and bar.SetStatusBarColor then
-- ponytail: re-styled on each layout pass only; if Blizzard recolors bars between passes, hook the item's refresh
pcall(bar.SetStatusBarTexture, bar, BS.TEXTURES[b.style.texture])
pcall(bar.SetStatusBarColor, bar, BS.Hex(b.style.color))
end
end
function BS.Hex(s)
local r, g, b = tostring(s):match("^#?(%x%x)(%x%x)(%x%x)$")
if not r then return 1, 1, 1 end
return tonumber(r, 16) / 255, tonumber(g, 16) / 255, tonumber(b, 16) / 255
end
local hooked = {}
local function hookViewers()
for _, name in ipairs(BS.ALL_VIEWERS) do
local v = _G[name]
if v and not hooked[v] then
hooked[v] = true
-- Blizzard re-lays out its viewers (settings change, auras appearing); take our frames back right after.
for _, method in ipairs({ "RefreshLayout", "Layout" }) do
if type(v[method]) == "function" then hooksecurefunc(v, method, function() BS.Reapply() end) end
end
end
end
end
function BS.Layout()
if not stack then return end
hookViewers()
local p, used, live = BS.profile, {}, {}
local s = p.stack
stack:ClearAllPoints()
stack:SetPoint(s.point, UIParent, s.point, s.x, s.y)
stack:SetScale(s.scale)
stack:SetAlpha(s.alpha)
local n, sw, sh = #p.bars, 40, 40
for i, b in ipairs(p.bars) do
b.layer = n - i + 1 -- top of the list = top layer
local c = container(b.id)
live[c] = true
c:ClearAllPoints()
c:SetPoint("CENTER", stack, "CENTER", b.offset.x, b.offset.y)
c:SetSize(b.size.w, b.size.h)
c:SetFrameLevel(BASE + b.layer * STEP)
c.preview:SetFrameLevel(c:GetFrameLevel() + 5)
c.preview:SetShown(BS.preview == true)
c.label:SetText(b.layer .. " " .. b.name)
c:Show()
sw = math.max(sw, math.abs(b.offset.x) * 2 + b.size.w)
sh = math.max(sh, math.abs(b.offset.y) * 2 + b.size.h)
for j, sp in ipairs(b.spells) do
local it = find(sp, b.visibility, used)
if it then
used[it] = true
place(it, c, b, j)
end
end
end
stack:SetSize(sw, sh)
for _, c in pairs(containers) do
if not live[c] then c:Hide() end
end
-- ponytail: returned frames sit unanchored until Blizzard's next layout pass; we never call its layout ourselves (taint)
for it, origin in pairs(borrowed) do
if not used[it] then
borrowed[it] = nil
it:SetParent(origin)
end
end
BS.UpdateVisibility()
end
local busy
function BS.Reapply()
if busy or not BS.profile then return end
busy = true
local ok, err = pcall(BS.Layout)
busy = false
if not ok then geterrorhandler()(err) end
end
function BS.UpdateVisibility()
if not stack then return end
local s = BS.profile.stack
stack:SetShown(BS.preview or BS.editing or (BS.inCombat and s.showInCombat) or (not BS.inCombat and s.showOutOfCombat))
end
local function savePosition()
stack:StopMovingOrSizing()
local s, sc = BS.profile.stack, stack:GetScale()
local x, y = stack:GetCenter()
local ux, uy = UIParent:GetCenter()
s.point = "CENTER"
s.x, s.y = math.floor(x - ux / sc + 0.5), math.floor(y - uy / sc + 0.5)
BS.Changed()
end
function BS.SetEditing(on)
BS.editing = on
stack.edit:SetShown(on)
BS.UpdateVisibility()
end
function BS.Init()
stack = CreateFrame("Frame", "BarStackFrame", UIParent)
stack:SetFrameStrata("MEDIUM")
stack:SetMovable(true)
stack:SetClampedToScreen(true)
-- Edit Mode overlay: outline + label, drag to move. Nothing else is editable there.
local edit = CreateFrame("Frame", nil, stack)
edit:SetAllPoints()
edit:SetFrameLevel(8000)
edit:EnableMouse(true)
edit:RegisterForDrag("LeftButton")
edit:SetScript("OnDragStart", function() stack:StartMoving() end)
edit:SetScript("OnDragStop", savePosition)
local t = edit:CreateTexture(nil, "BACKGROUND")
t:SetAllPoints()
t:SetColorTexture(0.25, 0.55, 1, 0.35)
local label = edit:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
label:SetPoint("CENTER")
label:SetText("Bar Stack")
edit:Hide()
stack.edit = edit
if EventRegistry then
EventRegistry:RegisterCallback("EditMode.Enter", function() BS.SetEditing(true) end, stack)
EventRegistry:RegisterCallback("EditMode.Exit", function() BS.SetEditing(false) end, stack)
end
end
function BS.ViewerStats(v)
if not v then return 0, 0 end
local list, mapped = items(v), 0
for _, it in ipairs(list) do
if BS.FrameSpell(it) then mapped = mapped + 1 end
end
return #list, mapped
end
-- Spells currently in the Cooldown Manager, ranks grouped into one entry by name.
function BS.AvailableSpells()
local seen, out = {}, {}
for _, name in ipairs(BS.ALL_VIEWERS) do
local v = _G[name]
if v then
for _, it in ipairs(items(v)) do
local id = BS.FrameSpell(it)
local spell = id and BS.SpellName(id)
if spell and not seen[spell] then
seen[spell] = true
out[#out + 1] = { baseSpell = id, name = spell, icon = C_Spell and BS.try(C_Spell.GetSpellTexture, id) }
end
end
end
end
table.sort(out, function(a, b) return a.name < b.name end)
return out
end