Repository navigation
Expand file tree
/
Copy pathPersonalPlayerNotesUtils.lua
More file actions
405 lines (362 loc) · 16.3 KB
/
Copy pathPersonalPlayerNotesUtils.lua
File metadata and controls
405 lines (362 loc) · 16.3 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
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
local personalPlayerNotes = ...
local L = LibStub("AceLocale-3.0"):GetLocale(personalPlayerNotes, true)
local AceConfigDialog = LibStub("AceConfigDialog-3.0")
-- Current SavedVariables schema version. Bump this and extend
-- MigrateSavedVariablesSchema() whenever the profile table shape changes
-- in a way that requires converting existing user data.
PersonalPlayerNotes.SCHEMA_VERSION = 2
--#region Client feature detection
--[[
Client capabilities are feature-detected instead of branched on
WOW_PROJECT_ID/flavor, since Classic clients have progressively picked up
modern Retail APIs and this avoids hardcoding assumptions per client.
]]
function PersonalPlayerNotes:HasModernMenuAPI()
return type(Menu) == "table" and type(Menu.ModifyMenu) == "function"
end
function PersonalPlayerNotes:HasModernTooltipAPI()
return type(TooltipDataProcessor) == "table"
and type(TooltipDataProcessor.AddTooltipPostCall) == "function"
and type(Enum) == "table"
and Enum.TooltipDataType ~= nil
end
function PersonalPlayerNotes:HasModernSettingsAPI()
return type(Settings) == "table" and type(Settings.OpenToCategory) == "function"
end
--[[
Some Classic clients don't expose the C_AddOns/C_UI namespaces yet, still
relying on the older global LoadAddOn/DisableAddOn/ReloadUI functions.
Confirmed via live testing on Wrath Classic.
]]
function PersonalPlayerNotes:HasModernAddOnAPI()
return type(C_AddOns) == "table"
and type(C_AddOns.LoadAddOn) == "function"
and type(C_AddOns.DisableAddOn) == "function"
end
function PersonalPlayerNotes:HasModernUIReloadAPI()
return type(C_UI) == "table" and type(C_UI.Reload) == "function"
end
--[[
Patch 12.0.0 (Midnight) "secret values" (e.g. world cursor tooltip units)
throw a hard Lua error if passed to an API like UnitIsPlayer(), so they
must be detected and skipped rather than caught after the fact.
issecretvalue() may not exist on older clients, hence the feature check.
https://warcraft.wiki.gg/wiki/Secret_Values
]]
function PersonalPlayerNotes:IsSecretUnit(unit)
return type(issecretvalue) == "function" and issecretvalue(unit) == true
end
--[[
Opens the Blizzard options window to this addon's category, falling back
to opening the Ace3 config dialog directly on clients without the modern
Settings API. Settings.OpenToCategory() needs the numeric category ID
from AceConfigDialog:AddToBlizOptions() (self.blizOptionsCategoryID),
not the addon name string.
]]
function PersonalPlayerNotes:OpenBlizzardOptions()
if self:HasModernSettingsAPI() and self.blizOptionsCategoryID then
Settings.OpenToCategory(self.blizOptionsCategoryID)
else
AceConfigDialog:Open("PersonalPlayerNotesSettings Info")
end
end
--#endregion
--#region SavedVariables migration
--[[
Parses a legacy Shitlist "Name-Realm" key into separate name/realm values.
Kept as a pure string function (no SavedVariables/WoW API access) so it
can be unit tested outside of WoW, see Tests/test_utils.lua.
]]
function PersonalPlayerNotes:ParseLegacyPlayerKey(key)
if type(key) ~= "string" then
return nil, nil
end
return key:match("([^-]+)-([^-]+)")
end
--[[
Runs once per login after the AceDB profile is loaded. AceDB-3.0 already
fills in missing fields from PersonalPlayerNotes.defaults, so this only
needs to handle actual structural changes between schema versions.
]]
function PersonalPlayerNotes:MigrateSavedVariablesSchema()
local profile = self.db.profile
local fromVersion = profile.schemaVersion or 0
if fromVersion < 1 then
-- Initial schema version, nothing to convert yet.
end
if fromVersion < 2 then
-- alert.sound used to be a 1-4 index into alert.sounds; sounds are
-- now selected by filename from PersonalPlayerNotes.SoundManifest
-- instead, so reset everyone to the new default and drop the array.
profile.alert.sound = "default.mp3"
profile.alert.sounds = nil
-- customSoundFile was superseded by customSounds before shipping;
-- just make sure both new fields exist.
profile.alert.customSoundFile = nil
profile.alert.customSounds = profile.alert.customSounds or {}
profile.alert.newCustomSound = profile.alert.newCustomSound or ""
end
profile.schemaVersion = PersonalPlayerNotes.SCHEMA_VERSION
end
--#endregion
--#region Addon metadata
--[[
Each of the following reads one .toc metadata field via
C_AddOns.GetAddOnMetadata(), falling back to L["PPN_NA"] if unset.
GetVersion() wraps the result in tostring() since GetAddOnMetadata() can
hand back a bare number for numeric-looking version strings.
]]
function PersonalPlayerNotes:GetVersion()
return tostring(C_AddOns.GetAddOnMetadata(personalPlayerNotes, "Version")) or L["PPN_NA"]
end
function PersonalPlayerNotes:GetTitle()
return C_AddOns.GetAddOnMetadata(personalPlayerNotes, "Title") or L["PPN_NA"]
end
function PersonalPlayerNotes:GetAuthor()
return C_AddOns.GetAddOnMetadata(personalPlayerNotes, "Author") or L["PPN_NA"]
end
function PersonalPlayerNotes:GetNotes()
return C_AddOns.GetAddOnMetadata(personalPlayerNotes, "Notes") or L["PPN_NA"]
end
function PersonalPlayerNotes:GetLocalizations()
return C_AddOns.GetAddOnMetadata(personalPlayerNotes, "X-Localizations") or L["PPN_NA"]
end
function PersonalPlayerNotes:GetCategory()
return C_AddOns.GetAddOnMetadata(personalPlayerNotes, "X-Category") or L["PPN_NA"]
end
function PersonalPlayerNotes:GetWebsite()
return C_AddOns.GetAddOnMetadata(personalPlayerNotes, "X-Website") or L["PPN_NA"]
end
function PersonalPlayerNotes:GetLicense()
return C_AddOns.GetAddOnMetadata(personalPlayerNotes, "X-License") or L["PPN_NA"]
end
--#endregion
-- AceConfigDialog-3.0 uses 170px per `width = 1` unit for a forced-row group
-- (see DialogWidthForUnits() below). WIDTH_OVERHEAD accounts for the Frame's
-- own border, ScrollFrame scrollbar gutter, and named-inline-group border
-- that eat into that content width; WIDTH_SAFETY_MARGIN is an empirical,
-- in-game-tested correction on top of that (see DIALOG_WIDTH_ADJUSTMENT
-- below for the one window that needs it added back instead).
local WIDTH_MULTIPLIER = 170
local WIDTH_OVERHEAD = 74
local WIDTH_SAFETY_MARGIN = -9
--[[
Converts a forced-row width (in width_multiplier units, see above) into
the Frame width needed to show it without wrapping.
]]
function PersonalPlayerNotes:DialogWidthForUnits(units)
return math.ceil(units * WIDTH_MULTIPLIER + WIDTH_OVERHEAD + WIDTH_SAFETY_MARGIN)
end
-- Width (in width_multiplier units, see above) of the widest forced row
-- each option window (keyed by appName) deliberately puts on one line, so
-- each window gets just enough width for its own content instead of one
-- blanket width sized for the widest window overall.
PersonalPlayerNotes.DIALOG_WIDTH_UNITS = {
["PersonalPlayerNotesSettings Options"] = 3,
["PersonalPlayerNotesSettings Reasons"] = 2.5,
["PersonalPlayerNotesSettings Listed_Players"] = 2.5,
}
-- Fallback for any appName not listed above.
PersonalPlayerNotes.DEFAULT_DIALOG_WIDTH_UNITS = 3
-- Per-appName pixel adjustment on top of DialogWidthForUnits() above, for
-- windows whose nested sub-rows need more width than the bare formula
-- gives (in-game tested).
PersonalPlayerNotes.DIALOG_WIDTH_ADJUSTMENT = {
["PersonalPlayerNotesSettings Options"] = 75,
}
-- Generic first-paint width for AceGUIDefaults(), before EnforceDialogWidth()
-- (which knows the appName) narrows it to the window's real width.
PersonalPlayerNotes.MIN_DIALOG_WIDTH =
PersonalPlayerNotes:DialogWidthForUnits(PersonalPlayerNotes.DEFAULT_DIALOG_WIDTH_UNITS)
-- Shared min/max height (pixels) for every option window - see
-- ResizeDialogToContent() below. A window auto-wraps to fit its content
-- below MAX_DIALOG_HEIGHT, and scrolls instead of growing past it.
PersonalPlayerNotes.MIN_DIALOG_HEIGHT = 200
PersonalPlayerNotes.MAX_DIALOG_HEIGHT = 800
-- Extra headroom on top of the exact-fit height ResizeDialogToContent()
-- computes - the exact-fit math still left the frame a few pixels too
-- short in practice, clipping the last row of content.
local HEIGHT_SAFETY_MARGIN = 12
--[[
Creates a bare AceGUI-3.0 Frame widget preconfigured the way this addon's
dropdown/minimap-icon "Add"/"Edit" popups expect it: releases itself on
close, fills its container, and hides the (unused) status bar text/frame.
Callers are expected to still call :SetTitle() before showing it.
]]
function PersonalPlayerNotes:AceGUIDefaults()
local aceGUI = LibStub("AceGUI-3.0"):Create("Frame")
-- Hide BEFORE registering OnClose: :Hide() fires OnClose synchronously,
-- and an already-registered release callback would release this
-- brand-new widget before the caller gets to configure/show it.
aceGUI:Hide()
aceGUI:SetCallback("OnClose", function(widget)
aceGUI:Release()
end)
aceGUI:SetLayout("Fill")
-- Generic first-paint size; EnforceDialogWidth()/ResizeDialogToContent()
-- narrow this down to the window's real size right after.
local minWidth = PersonalPlayerNotes.MIN_DIALOG_WIDTH
local minHeight = PersonalPlayerNotes.MIN_DIALOG_HEIGHT
aceGUI:SetWidth(minWidth)
if aceGUI.frame.SetResizeBounds then -- WoW 10.0+
aceGUI.frame:SetResizeBounds(minWidth, minHeight)
else
aceGUI.frame:SetMinResize(minWidth, minHeight)
end
aceGUI:SetStatusText(nil)
aceGUI.statustext:Hide()
aceGUI.statustext:GetParent():Hide()
return aceGUI
end
--[[
Overwrites AceConfigDialog's own per-appName status table width (and the
frame's resize floor) to match this window's own required width (see
DIALOG_WIDTH_UNITS/DIALOG_WIDTH_ADJUSTMENT above), then reapplies it.
Needed because AceConfigDialog:Open() always reapplies its own
remembered status.width via ApplyStatus() (defaulting to 700),
overwriting whatever width AceGUIDefaults() set - without this, a
window would only ever show its correct width after the user manually
resized it. Call this AFTER every AceConfigDialog:Open(appName, frame).
]]
function PersonalPlayerNotes:EnforceDialogWidth(appName, frame)
local status = AceConfigDialog:GetStatusTable(appName)
if not status then
return
end
local units = PersonalPlayerNotes.DIALOG_WIDTH_UNITS[appName] or PersonalPlayerNotes.DEFAULT_DIALOG_WIDTH_UNITS
local adjustment = PersonalPlayerNotes.DIALOG_WIDTH_ADJUSTMENT[appName] or 0
local width = PersonalPlayerNotes:DialogWidthForUnits(units) + adjustment
if frame.frame then
if frame.frame.SetResizeBounds then -- WoW 10.0+
frame.frame:SetResizeBounds(width, PersonalPlayerNotes.MIN_DIALOG_HEIGHT)
else
frame.frame:SetMinResize(width, PersonalPlayerNotes.MIN_DIALOG_HEIGHT)
end
end
if status.width ~= width then
status.width = width
frame:ApplyStatus()
end
end
--[[
Resizes frame to exactly fit its content (clamped between
MIN_DIALOG_HEIGHT and MAX_DIALOG_HEIGHT), and persists that height into
AceConfigDialog's own per-appName status table so it sticks. Call this
AFTER every AceConfigDialog:Open(appName, frame)/EnforceDialogWidth().
Growing to MAX_DIALOG_HEIGHT before measuring (instead of measuring at
the frame's current height) forces the ScrollFrame to re-check whether
it still needs a scrollbar - it only does that during a layout pass, so
without this a window opened once at a too-short height would keep
showing a scrollbar forever even after correctly resizing.
Writing the result into AceConfigDialog:GetStatusTable(appName).height
(rather than just SetHeight()) is required because AceConfigDialog's
ActivateControl reruns Open() on this same frame on almost every widget
interaction (dropdown selects, button clicks, ...), which reapplies
status.height and would otherwise snap the window back to its old size.
]]
function PersonalPlayerNotes:ResizeDialogToContent(appName, frame)
local scrollChild = frame.children and frame.children[1]
local content = scrollChild and scrollChild.content
if not content then
return
end
frame:SetHeight(PersonalPlayerNotes.MAX_DIALOG_HEIGHT)
scrollChild:FixScroll()
-- 57px is the Frame widget's own title bar/chrome overhead subtracted
-- from its height to get its content area (see OnHeightSet in
-- AceGUIContainer-Frame.lua). The "Fill" layout stretches the
-- ScrollFrame to exactly that content area with 0 extra padding (see
-- the "Fill" layout registration in AceGUI-3.0.lua), so
-- content height + 57 (+ HEIGHT_SAFETY_MARGIN, see above) is the Frame
-- height needed to show it all without scrolling.
local desiredHeight = (content:GetHeight() or 0) + 57 + HEIGHT_SAFETY_MARGIN
if desiredHeight < PersonalPlayerNotes.MIN_DIALOG_HEIGHT then
desiredHeight = PersonalPlayerNotes.MIN_DIALOG_HEIGHT
elseif desiredHeight > PersonalPlayerNotes.MAX_DIALOG_HEIGHT then
desiredHeight = PersonalPlayerNotes.MAX_DIALOG_HEIGHT
end
frame:SetHeight(desiredHeight)
scrollChild:FixScroll()
local status = AceConfigDialog:GetStatusTable(appName)
if status then
status.height = desiredHeight
end
end
--[[
Opens an AceConfig options table (appName) inside a frame built by
AceGUIDefaults(), instead of letting AceConfigDialog:Open() create its
own bare Frame widget (which shows an unstyled status bar strip along
the bottom that none of this addon's other windows have).
self.customDialogFrames caches one frame per appName so repeat opens
reuse/refeed the same frame instead of creating a new untracked one
each time (AceConfigDialog:Open() only de-dupes frames it created
itself). The cache entry is cleared when the frame closes.
]]
function PersonalPlayerNotes:OpenDialog(appName)
self.customDialogFrames = self.customDialogFrames or {}
local frame = self.customDialogFrames[appName]
if not frame then
frame = self:AceGUIDefaults()
frame:SetCallback("OnClose", function(widget)
if PersonalPlayerNotes.customDialogFrames[appName] == widget then
PersonalPlayerNotes.customDialogFrames[appName] = nil
end
widget:Release()
end)
self.customDialogFrames[appName] = frame
end
AceConfigDialog:Open(appName, frame)
self:EnforceDialogWidth(appName, frame)
self:ResizeDialogToContent(appName, frame)
frame:Show()
end
--[[
Closes every dialog opened via OpenDialog() above. AceConfigDialog:CloseAll()
only knows about frames it created itself (self.OpenFrames), so it can't
see or close these custom AceGUIDefaults()-backed containers - callers
that used to rely on :CloseAll() to reset to a clean slate before opening
a different window need to call this too.
]]
function PersonalPlayerNotes:CloseAllDialogs()
if not self.customDialogFrames then
return
end
for _, frame in pairs(self.customDialogFrames) do
frame:Hide()
end
end
--[[
Re-feeds a dialog previously opened via OpenDialog(), if it's currently
open, so option widgets whose displayed value changed elsewhere (e.g. the
Icon label after OpenIconPicker()) refresh immediately. AceConfigRegistry's
own NotifyChange() auto-refresh only sees frames it tracks itself, not
our custom containers, so callers must call this alongside it.
]]
function PersonalPlayerNotes:RefreshDialog(appName)
local frame = self.customDialogFrames and self.customDialogFrames[appName]
if frame then
AceConfigDialog:Open(appName, frame)
self:EnforceDialogWidth(appName, frame)
self:ResizeDialogToContent(appName, frame)
end
end
--[[
Prints to the default chat frame, prefixed with L["PPN_DEBUG"] instead of
L["PPN_PRINT"] when debug mode is on (self.db.profile.debug). Safe to
call before self.db exists (e.g. very early during load).
]]
function PersonalPlayerNotes:Print(...)
if self.db and self.db.profile.debug then
return print(L["PPN_DEBUG"], ...)
end
return print(L["PPN_PRINT"], ...)
end
--[[
Forwards to Print() only while debug mode is enabled; a no-op otherwise.
Used for verbose logging that shouldn't show up for regular users.
]]
function PersonalPlayerNotes:PrintDebug(...)
if self.db and self.db.profile.debug then
self:Print(...)
end
end