Repository navigation
Expand file tree
/
Copy pathLabels.lua
More file actions
453 lines (412 loc) · 13.5 KB
/
Copy pathLabels.lua
File metadata and controls
453 lines (412 loc) · 13.5 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
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
local _, addon = ...
local L = addon.L
local slots, bars = {}, {}
local values = {}
local unverified = {}
local bindingSet, loadedSet, scopeError
local scopeReady = false
local scopeToken = 0
local loadPending = false
local loadSerial = 0
addon.MIN_KEYBIND_FONT_SIZE = 10
addon.MAX_KEYBIND_FONT_SIZE = 14
local function LabelKey(bar, index)
return bar.id .. ":" .. index
end
local function IsSpace(codepoint)
return codepoint == 32 or codepoint == 160 or codepoint == 5760
or (codepoint >= 8192 and codepoint <= 8202)
or codepoint == 8239 or codepoint == 8287 or codepoint == 12288
end
function addon.NormalizeLabel(text)
if type(text) ~= "string" then
return nil, L.INVALID_LABEL
end
local index, first, last = 1
while index <= #text do
local start = index
local byte = text:byte(index)
local length, minimum
if byte < 128 then
length, minimum = 1, 0
elseif byte >= 194 and byte <= 223 then
length, minimum = 2, 128
elseif byte >= 224 and byte <= 239 then
length, minimum = 3, 2048
elseif byte >= 240 and byte <= 244 then
length, minimum = 4, 65536
else
return nil, L.INVALID_UTF8
end
local codepoint = length == 1 and byte or byte % (2 ^ (7 - length))
for offset = 1, length - 1 do
local continuation = text:byte(index + offset)
if not continuation or continuation < 128 or continuation > 191 then
return nil, L.INVALID_UTF8
end
codepoint = codepoint * 64 + continuation - 128
end
if codepoint < minimum or codepoint > 1114111 or (codepoint >= 55296 and codepoint <= 57343) then
return nil, L.INVALID_UTF8
end
if codepoint < 32 or (codepoint >= 127 and codepoint <= 159)
or codepoint == 8232 or codepoint == 8233 then
return nil, L.INVALID_LABEL
end
index = index + length
if not IsSpace(codepoint) then
first = first or start
last = index - 1
end
end
return first and text:sub(first, last) or ""
end
function addon.LiteralLabel(text)
return (text:gsub("|", "||"))
end
local function Notify()
if addon.RefreshActionLabels then
addon.RefreshActionLabels()
end
if addon.RefreshSettings then
addon.RefreshSettings()
end
end
local function IsKnownScope(scope)
return scope == Enum.BindingSet.Account or scope == Enum.BindingSet.Character
end
local function IsScopeReady()
return scopeReady and not loadPending and bindingSet == GetCurrentBindingSet()
end
function addon.GetScopeName()
if bindingSet == Enum.BindingSet.Account then
return L.ACCOUNT_SCOPE
elseif bindingSet == Enum.BindingSet.Character then
return L.CHARACTER_SCOPE
end
return L.UNKNOWN_BINDING_SCOPE:format(tostring(bindingSet))
end
function addon.GetScopeNotice()
if scopeError then
return scopeError
elseif not IsScopeReady() then
return L.SCOPE_LOADING
end
return bindingSet == Enum.BindingSet.Account and L.ACCOUNT_NOTICE or L.CHARACTER_NOTICE
end
function addon.GetScopeToken()
return scopeToken
end
function addon.CanEdit(expectedToken)
if expectedToken ~= nil and expectedToken ~= scopeToken then
return false, L.SCOPE_CHANGED
elseif InCombatLockdown() then
return false, L.COMBAT_READ_ONLY
elseif scopeError then
return false, scopeError
elseif not IsScopeReady() then
return false, L.SCOPE_LOADING
end
return true
end
function addon.IsEnabled()
return IsScopeReady() and addon.db ~= nil and addon.db.enabled
end
function addon.ShouldHideMacroNames()
return IsScopeReady() and addon.db ~= nil and addon.db.hideMacroNames == true
end
function addon.IsValidKeybindFontSize(size)
return type(size) == "number" and size >= addon.MIN_KEYBIND_FONT_SIZE
and size <= addon.MAX_KEYBIND_FONT_SIZE and size % 1 == 0
end
function addon.GetKeybindFontSize()
if IsScopeReady() and addon.db then
return addon.db.keybindFontSize
end
end
local function InvalidateInteractions()
scopeToken = scopeToken + 1
if addon.CancelScopeInteractions then
addon.CancelScopeInteractions()
end
end
local function ValidSnapshot(snapshot, slot)
local function ValidKey(key)
return key == false or (type(key) == "string" and key ~= "" and not key:find("%c"))
end
return type(snapshot) == "table"
and snapshot.command == slot.bar.bindingPrefix .. slot.index
and type(snapshot.context) == "number"
and snapshot.context >= 0 and snapshot.context % 1 == 0
and ValidKey(snapshot.keyboard) and ValidKey(snapshot.gamepad)
and (snapshot.device == "keyboard" or snapshot.device == "gamepad")
end
local function BindingChanged(before, after)
if before.keyboard == false and before.gamepad == false then
return false
end
return before.context ~= after.context or before[before.device] ~= after[before.device]
or (before.device ~= after.device and before[after.device] ~= after[after.device])
end
local function ReconcileProfile(preserveUncertain)
local cleared, skipped, unavailable = {}, 0, 0
values = {}
unverified = {}
for id, text in pairs(addon.db.overrides) do
local slot = slots[id]
local normalized = slot and addon.NormalizeLabel(text)
local before = addon.db.bindingSnapshots[id]
if not normalized or normalized == "" or not ValidSnapshot(before, slot) then
skipped = skipped + 1
else
local after = addon.GetBindingSnapshot(slot.bar, slot.index)
if not after or (preserveUncertain and BindingChanged(before, after)) then
unavailable = unavailable + 1
unverified[id] = normalized
elseif BindingChanged(before, after) then
addon.db.overrides[id] = nil
addon.db.bindingSnapshots[id] = nil
cleared[#cleared + 1] = slot
else
values[id] = normalized
addon.db.bindingSnapshots[id] = after
end
end
end
local scopeName = addon.GetScopeName()
if skipped > 0 then
addon.Print(L.SKIPPED_LABELS:format(scopeName, skipped))
end
if unavailable > 0 then
addon.Print(L.SKIPPED_BINDINGS:format(scopeName, unavailable))
end
if #cleared == 1 then
local slot = cleared[1]
addon.Print(L.CLEARED_LABEL:format(GetBindingName(slot.bar.bindingPrefix .. slot.index), scopeName))
elseif #cleared > 1 then
addon.Print(L.CLEARED_LABELS:format(#cleared, scopeName))
end
end
local function ActivateScope(scope, preserveUncertain)
if scope ~= bindingSet or not scopeReady then
InvalidateInteractions()
end
bindingSet = scope
loadedSet = nil
scopeReady = false
values = {}
unverified = {}
local db, reason = addon.GetProfile(scope)
addon.db = db
scopeError = reason
if not db then
Notify()
return false, reason
end
ReconcileProfile(preserveUncertain)
scopeReady = true
Notify()
return true
end
local function ActivateAndReport(scope, preserveUncertain)
local success, reason = ActivateScope(scope, preserveUncertain)
if not success then
addon.Print(reason)
end
end
local function SuspendScope()
if scopeReady then
InvalidateInteractions()
end
scopeReady = false
Notify()
end
local function OnBindingsSaved()
local current = GetCurrentBindingSet()
if loadPending then
loadPending = false
ActivateAndReport(current, true)
elseif loadedSet and loadedSet ~= current then
SuspendScope()
addon.Print(L.SCOPE_LOADING)
else
ActivateAndReport(current)
end
end
local function OnBindingsLoaded(scope)
loadPending = false
if scope == Enum.BindingSet.Default then
-- Defaults are an uncommitted edit in the current scope, not another profile.
loadedSet = nil
Notify()
return
elseif scope == Enum.BindingSet.Current then
scope = GetCurrentBindingSet()
end
if not IsKnownScope(scope) then
ActivateAndReport(scope)
elseif scope == GetCurrentBindingSet() then
ActivateAndReport(scope)
else
loadedSet = scope
SuspendScope()
end
end
function addon.GetLabel(bar, index)
if IsScopeReady() then
return values[LabelKey(bar, index)]
end
end
function addon.SetLabel(bar, index, text, expectedToken)
local allowed, reason = addon.CanEdit(expectedToken)
if not allowed then
return false, reason
end
local id = LabelKey(bar, index)
if not slots[id] then
return false, L.INVALID_BUTTON
end
local normalized, validationError = addon.NormalizeLabel(text)
if not normalized then
return false, validationError
end
local snapshot
if normalized ~= "" then
snapshot, reason = addon.GetBindingSnapshot(bar, index)
if not snapshot then
return false, reason
end
end
values[id] = normalized ~= "" and normalized or nil
unverified[id] = nil
addon.db.overrides[id] = values[id]
addon.db.bindingSnapshots[id] = snapshot
Notify()
return true
end
function addon.SetEnabled(enabled, expectedToken)
local allowed, reason = addon.CanEdit(expectedToken)
if not allowed then
return false, reason
end
if type(enabled) ~= "boolean" then
return false, L.INVALID_DATABASE:format("enabled must be a boolean")
end
addon.db.enabled = enabled
Notify()
return true
end
function addon.SetHideMacroNames(hidden, expectedToken)
local allowed, reason = addon.CanEdit(expectedToken)
if not allowed then
return false, reason
end
if type(hidden) ~= "boolean" then
return false, L.INVALID_DATABASE:format("hideMacroNames must be a boolean")
end
addon.db.hideMacroNames = hidden
Notify()
return true
end
function addon.SetKeybindFontSize(size, expectedToken)
local allowed, reason = addon.CanEdit(expectedToken)
if not allowed then
return false, reason
end
if size ~= nil and not addon.IsValidKeybindFontSize(size) then
return false, L.INVALID_FONT_SIZE:format(addon.MIN_KEYBIND_FONT_SIZE, addon.MAX_KEYBIND_FONT_SIZE)
end
addon.db.keybindFontSize = size
Notify()
return true
end
function addon.ResetLabels(barID, expectedToken)
local allowed, reason = addon.CanEdit(expectedToken)
if not allowed then
return false, reason
end
if barID and not bars[barID] then
return false, L.INVALID_BAR
end
for _, entries in ipairs({ addon.db.overrides, addon.db.bindingSnapshots, values, unverified }) do
for id in pairs(entries) do
if not barID or (type(id) == "string" and id:sub(1, #barID + 1) == barID .. ":") then
entries[id] = nil
end
end
end
Notify()
return true
end
local function OnInputDeviceChanged()
if not IsScopeReady() then
return
end
for id in pairs(values) do
local slot = slots[id]
local before = addon.db.bindingSnapshots[id]
local after = addon.GetBindingSnapshot(slot.bar, slot.index)
if after and before.context == after.context
and before.keyboard == after.keyboard and before.gamepad == after.gamepad then
before.device = after.device
end
end
end
local function RetryBindingReads()
if not IsScopeReady() then
return
end
local restored = false
for id, label in pairs(unverified) do
local slot = slots[id]
local before = addon.db.bindingSnapshots[id]
local after = addon.GetBindingSnapshot(slot.bar, slot.index)
if after and not BindingChanged(before, after) then
values[id] = label
unverified[id] = nil
addon.db.bindingSnapshots[id] = after
restored = true
end
end
if restored then
Notify()
end
end
function addon.InitializeLabels(preserveUncertain)
for _, bar in ipairs(addon.Bars) do
bars[bar.id] = true
for index = 1, bar.buttonCount do
slots[LabelKey(bar, index)] = { bar = bar, index = index }
end
end
local initialized, reason = ActivateScope(GetCurrentBindingSet(), preserveUncertain)
if not initialized then
return false, reason
end
hooksecurefunc("SaveBindings", OnBindingsSaved)
hooksecurefunc("LoadBindings", OnBindingsLoaded)
local events = CreateFrame("Frame")
events:RegisterEvent("BINDINGS_LOADED")
events:RegisterEvent("GAME_PAD_ACTIVE_CHANGED")
events:RegisterEvent("ADDON_LOADED")
events:RegisterEvent("PLAYER_ENTERING_WORLD")
events:SetScript("OnEvent", function(_, event)
if event == "GAME_PAD_ACTIVE_CHANGED" then
OnInputDeviceChanged()
elseif event == "BINDINGS_LOADED" then
loadPending = true
loadSerial = loadSerial + 1
local serial = loadSerial
-- The synchronous event precedes the LoadBindings posthook that identifies its target.
C_Timer.After(0, function()
if loadPending and loadSerial == serial then
loadPending = false
ActivateAndReport(GetCurrentBindingSet(), true)
end
end)
else
RetryBindingReads()
end
end)
return true
end