Repository navigation
Expand file tree
/
Copy pathwindow.lua
More file actions
366 lines (333 loc) · 12.8 KB
/
Copy pathwindow.lua
File metadata and controls
366 lines (333 loc) · 12.8 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
local _, ns = ...
local ui = ns.ui
-- Sized so the topographic preview (its native map is ~870×320 for a full setup)
-- fills the right panel at a comfortable size without clipping (#124): the panel
-- is WIN_W - ~264 wide, and the preview's fill scale grows with both dimensions.
local WIN_W = 1328
local WIN_H = 621
local LIST_W = 240
local FILTER_W = 130
local PAD = 8
local ROW_H = 22
local BTN_H = 22
local window = nil
-- default position: centered on UIParent with this offset (matches the
-- `Center` anchor in CreateWindow); reused by /bars resetpos
local DEFAULT_X, DEFAULT_Y = -75, 150
local function savePosition(f)
local x = f._widget:GetLeft() - UIParent:GetLeft()
local y = f._widget:GetTop() - UIParent:GetTop()
f._widget:ClearAllPoints()
f:TopLeft(UIParent, ui.edge.TopLeft, x, y)
ns.db.windowPos = { x = x, y = y }
end
-- Keep a TOPLEFT offset within UIParent. The window is clamped-to-screen so a
-- drag can't strand it, but a saved position from a LARGER resolution/UI scale
-- can land off-screen after a shrink — pull it back into bounds on restore.
local function clampToScreen(x, y)
local pw, ph = UIParent:GetWidth(), UIParent:GetHeight()
x = math.max(0, math.min(x, math.max(0, pw - WIN_W)))
-- y is the TOPLEFT offset from UIParent's top, so it ranges [-(ph-WIN_H), 0]
y = math.min(0, math.max(y, -math.max(0, ph - WIN_H)))
return x, y
end
local function restorePosition(f)
local pos = ns.db.windowPos
if pos then
local x, y = clampToScreen(pos.x, pos.y)
f._widget:ClearAllPoints()
f:TopLeft(UIParent, ui.edge.TopLeft, x, y)
-- persist the clamped position so the recovery sticks
if x ~= pos.x or y ~= pos.y then ns.db.windowPos = { x = x, y = y } end
else
savePosition(f) -- freeze computed center into TOPLEFT anchor on first run
end
end
-- Recenter the window to its default spot and persist it (for /bars resetpos —
-- recovery when a resolution change orphaned the saved position off-screen).
local function resetPosition(f)
f._widget:ClearAllPoints()
f:Center(DEFAULT_X, DEFAULT_Y)
savePosition(f)
end
local function CreateWindow()
local f = ui.TitleFrame:new{
name = "ActionBarMasterWindow",
title = "Action Bar Master",
special = true,
level = 600,
position = { Center = {-75, 150}, Width = WIN_W, Height = WIN_H },
}
f.titlebar:SetScript("OnMouseUp", function()
f._widget:StopMovingOrSizing()
savePosition(f)
end)
restorePosition(f)
-- On-screen bar labels live only while the window is shown; hook the frame's
-- visibility so every close path (button, Escape, /bars toggle) drives them.
f._widget:HookScript("OnShow", function() ns.barOverlay.Show() end)
f._widget:HookScript("OnHide", function() ns.barOverlay.Hide() end)
-- ── Column headers ───────────────────────────────────────────────────────
local profilesHeader = ui.Label:new{
parent = f,
text = "Profiles",
position = {
TopLeft = { f.titlebar, ui.edge.BottomLeft, PAD, -PAD },
Width = LIST_W,
Height = ROW_H,
},
}
-- ── Left panel (profile list) ────────────────────────────────────────────
local listPanel = ui.CleanFrame:new{
parent = f,
position = {
TopLeft = { profilesHeader, ui.edge.BottomLeft, 0, -2 },
Width = LIST_W,
Bottom = { f, ui.edge.Bottom, 0, PAD + BTN_H + PAD + ROW_H + PAD },
},
}
local function OnSelect(p)
if not p then f._barsPreview.Update(nil); return end
local profile, err = ns.Decode(p.encoded or "")
if err then ns.Print("Decode error: " .. err) end
-- barLayout is stored on the entry (not serialized) — reattach it for the preview.
if profile then profile.barLayout = p.barLayout end
f._barsPreview.Update(profile)
end
local _, refreshList, getSelected, setSelected, setClassFilter = ns.BuildProfileList(listPanel, OnSelect)
f._refreshList = refreshList
f._getSelected = getSelected
-- Check All / Uncheck All for the selector chips. The label is DERIVED from the
-- chips (SetOnChanged below keeps it live), never from a private flag here — a
-- flag desyncs the moment the user ticks chips individually, costing a dead click.
local CHECKALL_W = 90
local checkAllBtn
checkAllBtn = ui.Button:new{
parent = f,
template = "UIPanelButtonTemplate",
glow = false,
onClick = function()
f._barSelect.SetAllChecked(not f._barSelect.AllChecked())
end,
position = {
TopLeft = { f.titlebar, ui.edge.BottomLeft, PAD + LIST_W + PAD, -PAD },
Width = CHECKALL_W,
Height = ROW_H,
},
}
checkAllBtn:Text("Uncheck All")
checkAllBtn:TextAlign("CENTER")
ns.BuildClassFilter(f, {
TopLeft = { f.titlebar, ui.edge.BottomLeft, PAD + LIST_W + PAD + CHECKALL_W + PAD, -PAD },
Width = FILTER_W,
Height = ROW_H,
}, function(key, specOnly) setClassFilter(key, specOnly) end)
-- ── Right panel (bar selector + topographic preview) ─────────────────────
local barsPanel = ui.Frame:new{
parent = f,
position = {
TopLeft = { profilesHeader, ui.edge.BottomLeft, LIST_W + PAD, -2 },
BottomRight = { f, ui.edge.BottomRight, -PAD, PAD + BTN_H + PAD + ROW_H + PAD },
},
}
-- Per-bar include selector (feeds the Save/Load barFilter) across the top.
f._barSelect = ns.BuildBarSelect(barsPanel)
-- Keep the button's label naming the next click's action however the chip state
-- moved — the button itself, or the user ticking chips one at a time.
f._barSelect.SetOnChanged(function(all)
checkAllBtn:Text(all and "Uncheck All" or "Check All")
end)
-- Topographic layout preview fills the rest, below the selector (#124: it is
-- now the primary bar view — the old fixed 15×12 grid is gone).
local previewPanel = ui.Frame:new{
parent = barsPanel,
position = {
TopLeft = { barsPanel, ui.edge.TopLeft, 0, -(f._barSelect.Height() + PAD) },
BottomRight = { barsPanel, ui.edge.BottomRight, 0, 0 },
},
}
f._barsPreview = ns.BuildBarsPreview(previewPanel)
-- Hovering a selector chip lights up the bar it controls in the preview.
f._barSelect.SetHighlighter(f._barsPreview.Highlight)
-- ── Dialogs ──────────────────────────────────────────────────────────────
local saveDialog = ns.BuildSaveDialog(f, function() refreshList() end)
local expDlg = ns.BuildExportDialog(f)
local impDlg = ns.BuildImportDialog(f, function(profile)
f._pendingProfile = profile
f._pendingBarFilter = nil
local what = profile.spec ~= "" and profile.spec or "shared bars"
StaticPopup_Show("ABM_CONFIRM_IMPORT", profile.char .. " / " .. what)
end)
-- ── Bottom buttons ───────────────────────────────────────────────────────
local W1, W2, BSEP, GSEP = 110, 60, 4, 20 -- wide btn, std btn, btn gap, group gap
local lx1 = PAD
local lx2 = lx1 + W1 + BSEP
local lx3 = lx2 + W2 + BSEP
local lx4 = lx3 + W2 + BSEP
local rx1 = lx4 + W2 + GSEP
local rx2 = rx1 + W2 + BSEP
local rx3 = rx2 + W2 + BSEP
local LX = { lx1, lx2, lx3, lx4 }
local RX = { rx1, rx2, rx3 }
local btnDefs = {
{
label = "Autosave Now", x = LX[1], w = 110,
fn = function() ns.AutoSave(); refreshList() end,
},
{
label = "Save", x = LX[2],
fn = function()
local checked = f._barSelect.GetChecked()
-- 15 abm bars (slots 1-180, 12 per bar); the pet toggle isn't counted here.
local total, inc = 15, 0
for abm = 1, total do
if checked[abm] ~= false then inc = inc + 1 end
end
saveDialog._note:Text(inc == total
and "Saving all bars (full profile)"
or ("Saving " .. inc .. " of " .. total .. " bars |cff888888(shared, all classes)|r"))
saveDialog:Show()
saveDialog._nameBox:Text("")
saveDialog._nameBox._widget:SetFocus()
end,
},
{
label = "Load", x = LX[3],
fn = function()
local p = getSelected()
if not p then ns.Print("Select a profile first."); return end
local profile, err = ns.Decode(p.encoded or "")
if not profile then ns.Print("Load failed: " .. (err or "?")); return end
f._pendingProfile = profile
f._pendingBarFilter = f._barSelect.GetChecked()
StaticPopup_Show("ABM_CONFIRM_IMPORT", p.name)
end,
},
{
label = "Delete", x = LX[4],
fn = function()
local p = getSelected()
if not p then ns.Print("Select a profile first."); return end
f._pendingDelete = p -- hold the entry, not its index (issue #65)
StaticPopup_Show("ABM_CONFIRM_DELETE", p.name)
end,
},
{
label = "Export", x = RX[1],
fn = function()
-- export the selected saved profile's own string (the only way to
-- share another character's profile); fall back to a live capture of
-- the current bars when nothing is selected
local p = getSelected()
local encoded = (p and p.encoded) or ns.Encode(ns.Capture())
expDlg._box:Text(encoded)
expDlg._box._widget:HighlightText(0, -1)
expDlg:Show()
end,
},
{
label = "Import", x = RX[2],
fn = function()
impDlg._box:Text("")
impDlg:Show()
impDlg._box._widget:SetFocus()
end,
},
{
label = "Dupes", x = RX[3],
fn = function() ns.OpenDupes() end,
},
}
for _, def in ipairs(btnDefs) do
local btn = ui.Button:new{
parent = f,
template = "UIPanelButtonTemplate",
glow = false,
onClick = def.fn,
position = {
Left = { f, ui.edge.Left, def.x, 0 },
Bottom = { f, ui.edge.Bottom, 0, PAD },
Width = def.w or 60,
Height = BTN_H,
},
}
btn:Text(def.label)
btn:TextAlign("CENTER")
end
StaticPopupDialogs["ABM_CONFIRM_DELETE"] = {
text = "Delete '%s'?", button1 = DELETE, button2 = CANCEL,
timeout = 0, whileDead = 1, hideOnEscape = 1,
OnAccept = function(self)
-- resolve the entry's CURRENT index at accept time: an autosave inserting
-- at index 1 during the confirm would otherwise shift a cached index onto
-- the wrong profile. If it's gone (e.g. replaced in place by autosave),
-- delete nothing rather than guess.
local target = f._pendingDelete
if target then
for idx, p in ipairs(ns.db.profiles) do
if p == target then
table.remove(ns.db.profiles, idx)
break
end
end
setSelected(nil)
refreshList()
f._barsPreview.Update(nil)
end
f._pendingDelete = nil
end,
OnCancel = function(self) f._pendingDelete = nil end,
}
StaticPopupDialogs["ABM_CONFIRM_IMPORT"] = {
text = "Restore bars from '%s'?", button1 = ACCEPT, button2 = CANCEL,
timeout = 0, whileDead = 1, hideOnEscape = 1,
OnAccept = function(self)
if f._pendingProfile then
ns.Restore(f._pendingProfile, f._pendingBarFilter)
f._pendingProfile = nil
f._pendingBarFilter = nil
end
end,
OnCancel = function(self)
f._pendingProfile = nil
f._pendingBarFilter = nil
end,
}
return f
end
function ns:Open()
if not window then window = CreateWindow() end
window._refreshList()
-- nothing selected: preview the current character's live bars so the view isn't
-- empty (the bar selector's chips are always present regardless of selection)
if not window._getSelected() then
local live = ns.Capture()
window._barsPreview.Update(live)
end
window:Show()
-- TitleFrame is created shown, so the OnShow hook misses the first open —
-- drive the overlay directly here (idempotent with the hook on later shows).
ns.barOverlay.Show()
end
function ns:HideMainWindow()
if window then window:Hide() end
end
-- Toggle convention: re-running the bare command on an open window closes it.
function ns:ToggleMainWindow()
if window and window._widget:IsShown() then
window:Hide()
else
self:Open()
end
end
ns:registerCommand("", nil, function(self) self:ToggleMainWindow() end, "Toggle Action Bar Master")
-- Recenter the window — recovery if a resolution/UI-scale change left it
-- off-screen. Clears the saved position when the window isn't built yet so the
-- next open starts centered.
ns:registerCommand("resetpos", nil, function()
ns.db.windowPos = nil
if window then
resetPosition(window)
window:Show()
end
end, "Reset the window to its default position")