Repository navigation
Expand file tree
/
Copy pathProfiles.lua
More file actions
837 lines (775 loc) · 33.7 KB
/
Copy pathProfiles.lua
File metadata and controls
837 lines (775 loc) · 33.7 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
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
-- Keystance profiles: a character's bars and bar keys saved under a name ("Ret", "Prot",
-- "Holy"), applied on request, with one-step undo and a way back to the "Before Keystance"
-- snapshot. Profiles belong to one character (spells differ by class). A profile can also
-- carry gear (Gear.lua): an ItemRack set (p.itemrack) or items of its own (p.gear), which go
-- on first; Undo puts the previous gear back.
--
-- A profile holds action slots 1-180 and the keys of bar buttons (commands that fire an
-- action slot); movement, windows and every other key are never part of it. Applying
-- changes only what differs; anything that can't be set is reported and its slot is left
-- as it was. Every change first records the whole setup, so Undo puts all of it back.
-- In combat, changes wait for combat to end.
local ADDON, ns = ...
if ns.disabled then return end -- another copy of Keystance is running (Core.lua)
local L = ns.L
local pairs, ipairs, type, time, next = pairs, ipairs, type, time, next
local GetCursorInfo = GetCursorInfo
local GetBindingAction, GetBindingKey, GetBindingText = GetBindingAction, GetBindingKey, GetBindingText
local MAX_NAME = 24
local function Print(msg) ns.Print(msg) end
local function Notify(msg) ns.Notify(msg) end
local function Copy(t)
if type(t) ~= "table" then return t end
local c = {}
for k, v in pairs(t) do c[k] = Copy(v) end
return c
end
local function Char() return ns.char end
-- A tidy profile name, or nil and why not.
function ns.CleanProfileName(name)
name = (name or ""):gsub("^%s+", ""):gsub("%s+$", "")
if name == "" then return nil, L["A profile needs a name."] end
if #name > MAX_NAME then return nil, L["Profile names can be up to %d letters."]:format(MAX_NAME) end
return name
end
-- The character's profiles as a sorted list of names.
function ns.ProfileNames()
local names = {}
for name in pairs(Char() and Char().profiles or {}) do names[#names + 1] = name end
table.sort(names, function(a, b) return a:lower() < b:lower() end)
return names
end
-- Finds a profile by name, ignoring case.
function ns.FindProfile(name)
name = (name or ""):lower()
for key in pairs(Char() and Char().profiles or {}) do
if key:lower() == name then return key end
end
end
-- The current bars and bar keys as a profile.
local function Capture()
local state = ns.CurrentState()
local binds, nBinds = {}, 0
for command, keys in pairs(state.binds) do
if ns.IsBarCommand(command) then
binds[command] = keys
nBinds = nBinds + #keys
end
end
local nSlots = 0
for _ in pairs(state.slots) do nSlots = nSlots + 1 end
local now = time()
return { slots = state.slots, binds = binds, nSlots = nSlots, nBinds = nBinds, created = now, updated = now }
end
-- Saves the current setup as a profile. Replaces one of that name only if `replace`.
-- Returns the name, or nil and why not.
function ns.SaveProfile(name, replace)
local c = Char()
if not c then return nil, L["Not logged in yet."] end
local clean, why = ns.CleanProfileName(name)
if not clean then return nil, why end
local existing = ns.FindProfile(clean)
if existing and not replace then return nil, L["There's already a profile called %s."]:format(existing) end
local p = Capture()
if existing then
local old = c.profiles[existing]
p.created, p.gear, p.itemrack, p.icon = old.created, old.gear, old.itemrack, old.icon -- Update keeps them
-- With Keystance's own gear, Update saves the gear worn now too: the slots the profile
-- has gear for (a weapons-only profile stays weapons-only; a slot worn empty now drops
-- out). If the game hasn't loaded the items yet, the old gear stays.
if ns.ProfileGear(old) == "items" then
local items, why = ns.CaptureGear(ns.GearSlotsOf(old.gear))
if items then
p.gear = next(items) and items or nil
else
Print(L["Gear not saved: %s"]:format(why))
end
end
c.profiles[existing] = nil
end
c.profiles[clean] = p
ns.NameBindings()
ns.ProfilesChanged()
return clean
end
function ns.RenameProfile(old, new)
local c = Char()
local key = ns.FindProfile(old)
if not key then return nil, L["No profile called %s."]:format(tostring(old)) end
local clean, why = ns.CleanProfileName(new)
if not clean then return nil, why end
local other = ns.FindProfile(clean)
if other and other ~= key then return nil, L["There's already a profile called %s."]:format(other) end
if clean ~= key then c.profiles[clean], c.profiles[key] = c.profiles[key], nil end
if c.active == key then c.active = clean end
-- A switch to it waiting for combat to end follows the new name.
if ns.pendingProfile == key then
ns.pendingProfile = nil
ns.ApplyProfile(clean)
end
for _, rule in ipairs(c.rules) do
if rule.profile == key then rule.profile = clean end -- rules follow the rename
end
ns.ProfileRenamed(key, clean) -- and its key
ns.ProfilesChanged()
return clean
end
function ns.DuplicateProfile(name, new)
local c = Char()
local key = ns.FindProfile(name)
if not key then return nil, L["No profile called %s."]:format(tostring(name)) end
local clean, why = ns.CleanProfileName(new)
if not clean then return nil, why end
if ns.FindProfile(clean) then return nil, L["There's already a profile called %s."]:format(ns.FindProfile(clean)) end
local p = Copy(c.profiles[key])
p.created, p.updated = time(), time()
c.profiles[clean] = p
ns.NameBindings()
ns.ProfilesChanged()
return clean
end
-- Sets a profile's gear: items of its own ({ [slot] = item string }, or nil for none), or
-- an ItemRack set by name (nil for none).
function ns.SetProfileGear(name, items)
local key = ns.FindProfile(name)
if not key then return nil, L["No profile called %s."]:format(tostring(name)) end
Char().profiles[key].gear = (items and next(items)) and items or nil
ns.ProfilesChanged()
return key
end
-- Sets a profile's icon (nil: automatic).
function ns.SetProfileIcon(name, icon)
local key = ns.FindProfile(name)
if not key then return nil end
Char().profiles[key].icon = icon
ns.ProfilesChanged()
if ns.RefreshSpellPanel then ns.RefreshSpellPanel() end
return key
end
-- Sets one slot of a profile's own gear (nil: leave that slot alone).
function ns.SetGearSlot(name, slot, item)
local key = ns.FindProfile(name)
if not key then return nil, L["No profile called %s."]:format(tostring(name)) end
local p = Char().profiles[key]
p.gear = p.gear or {}
p.gear[slot] = item
if not next(p.gear) then p.gear = nil end
ns.ProfilesChanged()
return key
end
function ns.SetProfileItemRack(name, set)
local key = ns.FindProfile(name)
if not key then return nil, L["No profile called %s."]:format(tostring(name)) end
Char().profiles[key].itemrack = set
ns.ProfilesChanged()
return key
end
function ns.DeleteProfile(name)
local c = Char()
local key = ns.FindProfile(name)
if not key then return nil, L["No profile called %s."]:format(tostring(name)) end
c.profiles[key] = nil
if c.active == key then c.active = nil end
ns.ProfileDeleted(key)
ns.ProfilesChanged()
return key
end
---------------------------------------------------------------------------
-- Applying, undo and restore
---------------------------------------------------------------------------
local function Report(what, slots, keys, failures)
Notify(L["%s (%d slots, %d keys changed)."]:format(what, slots, keys))
if #failures > 0 then
Print(L["%d couldn't be set and were left as they were:"]:format(#failures))
for i = 1, math.min(#failures, 6) do Print(" " .. failures[i]) end
if #failures > 6 then Print(L[" and %d more."]:format(#failures - 6)) end
end
end
-- What a change touched: the slots and keys that differ between `before` and now, so an
-- undo puts back those and nothing else (not changes the player made since).
local function Touched(before)
local after = ns.CurrentState()
local slots, keys = {}, {}
for slot = 1, ns.MANAGED_SLOTS do
if not ns.SameAction(before.slots[slot], after.slots[slot]) then slots[slot] = true end
end
local was, now = {}, {}
for command, list in pairs(before.binds) do
for _, key in ipairs(list) do was[key] = command end
end
for command, list in pairs(after.binds) do
for _, key in ipairs(list) do now[key] = command end
end
for key, command in pairs(was) do
if now[key] ~= command then keys[key] = true end
end
for key, command in pairs(now) do
if was[key] ~= command then keys[key] = true end
end
return slots, keys
end
-- The undo record for a change from `before`: what it touched, with how things were.
local function Record(before, label, gear, homes, profileBinds)
local slots, keys = Touched(before)
return { state = before, only = slots, keys = keys, gear = gear, homes = homes, profileBinds = profileBinds,
label = label, at = time() }
end
-- Applies a state now, recording the setup before it for Undo. `done` is said afterwards
-- ("Prot applied"); `undo` names the change for the Undo button ("applying Prot").
-- options.gearChanged says gear is going on too, options.gearBefore is the gear it
-- replaces and options.gearHomes where the items it puts into empty slots came from, kept
-- for Undo.
local function Change(state, options, done, undo, onDone)
local c = Char()
local before = ns.CurrentState()
local ok, slots, keys, failures = ns.ApplyState(state, options)
if not ok then
Print(L["Nothing changed: %s."]:format(slots))
return false
end
if slots + keys > 0 or options.gearChanged then
c.lastChange = Record(before, undo, options.gearBefore, options.gearHomes, options.profileBinds)
if ns.StatusStale then ns.StatusStale() end
end
if onDone then onDone() end
Report(done, slots, keys, failures)
ns.ProfilesChanged()
return true
end
ns.ApplyChange = Change
-- Records the setup from before a change made elsewhere (a drop), for Undo.
function ns.RecordChange(before, label)
local c = Char()
if c then c.lastChange = Record(before, label) end
if ns.StatusStale then ns.StatusStale() end -- the strip's Undo follows
end
-- What the Undo button would undo ("applying Prot"), or nil.
function ns.UndoLabel()
local c = Char()
return c and c.lastChange and c.lastChange.label
end
-- Applies a profile, now or after combat: its gear first, then bars and keys. keys = false
-- leaves keys alone ("bars only"); noGear leaves gear alone (a rule switching because the
-- player changed gear mustn't undo that change). While this character shares the account's
-- keybinds, changing keys would change them for every character, so it asks first (owner's
-- decision, PLAN.md) unless `asked`.
function ns.ApplyProfile(name, keys, asked, noGear)
local c = Char()
local key = ns.FindProfile(name)
if not key then return Print(L["No profile called %s. /kst profiles lists them."]:format(tostring(name))) end
if keys ~= false and not asked and ns.SharedKeybinds() then
local _, keyChanges = ns.CountChanges(c.profiles[key], "bars")
if keyChanges > 0 then return ns.AskSharedKeybinds(key, noGear) end
end
local now = ns.OutOfCombat("apply", function()
ns.pendingProfile = nil
local p = c.profiles[key]
if not p then return end -- deleted meanwhile
local gearBefore, gearChanged, gearHomes
if not noGear then gearBefore, gearChanged, gearHomes = ns.StartProfileGear(p) end
Change(p, { keys = keys ~= false, scope = "bars", gearBefore = gearBefore, gearChanged = gearChanged,
gearHomes = gearHomes },
L["%s applied"]:format(key), L["applying %s"]:format(key), function() c.active = key end)
end)
if not now then
-- Shown on the profile switcher until combat ends (another click changes or cancels it).
ns.pendingProfile = key
Notify(L["%s will apply when combat ends."]:format(key))
ns.ProfilesChanged()
end
end
-- Drops a profile switch waiting for combat to end.
function ns.CancelPendingProfile()
local key = ns.pendingProfile
if not key then return false end
ns.pendingProfile = nil
ns.CancelWaiting("apply")
Notify(L["Switching to %s cancelled."]:format(key))
ns.ProfilesChanged()
return true
end
-- Puts back the setup from before the last change (a second Undo redoes it).
function ns.Undo()
local c = Char()
if not (c and c.lastChange) then return Print(L["Nothing to undo."]) end
ns.pendingProfile = nil -- Undo takes the queue's place
local now = ns.OutOfCombat("apply", function()
local last = c.lastChange
if not last then return end
-- The gear from before goes back on, items taken off going back where they came from
-- (kept for a second Undo in turn).
local gearBefore, gearChanged, gearHomes
if last.gear and ns.GearChanges(last.gear) > 0 and not GetCursorInfo() then
gearBefore, gearHomes = ns.CaptureGear(ns.GearSlotsOf(last.gear), true), {}
local ok, why = ns.EquipGear(last.gear, ns.GearReport, last.homes, gearHomes)
gearChanged = ok
if not ok then Print(L["Gear not changed: %s."]:format(why)) end
end
-- Profiles' saved keys, if the change moved them (moving the hand), go back too.
local profileBinds
if last.profileBinds then
profileBinds = {}
for name, binds in pairs(last.profileBinds) do
local p = c.profiles[name]
if p then
profileBinds[name] = p.binds
p.binds = binds
end
end
end
-- Only what that change touched (records from before this was kept: everything).
Change(last.state, { scope = "all", only = last.only, onlyKeys = last.only and (last.keys or {}),
gearBefore = gearBefore, gearChanged = gearChanged, gearHomes = gearHomes, profileBinds = profileBinds },
L["Undid %s"]:format(last.label), L["the undo"], function() c.active = nil end)
end)
if not now then Notify(L["Undo will happen when combat ends."]) end
end
---------------------------------------------------------------------------
-- Moving the hand one key right or left (Ergonomics.lua's map): every keybind in the
-- left-hand block, in every Shift/Ctrl/Alt layer, and the keys the character's profiles
-- saved, so applying one afterwards keeps the new place. The original setup isn't touched.
-- One change: Undo puts it all back.
---------------------------------------------------------------------------
local LAYERS = { "", "SHIFT-", "CTRL-", "CTRL-SHIFT-", "ALT-", "ALT-SHIFT-", "ALT-CTRL-", "ALT-CTRL-SHIFT-" }
local function ShiftKey(key, to)
local prefix, base = ns.SplitKey(key)
local dest = to[base]
return dest and (prefix .. dest) or key
end
local function ShiftHand(layoutKey, dir)
local c = Char()
local to, why = ns.HandShiftMap(layoutKey, dir)
if not to then return Print(L["Keys not moved: %s."]:format(why)) end
ns.OwnKeybindsFirst()
local state = ns.CurrentState()
local was, touched = {}, {}
for command, keys in pairs(state.binds) do
for _, key in ipairs(keys) do was[key] = command end
end
local now = {}
for key, command in pairs(was) do now[key] = command end
for from, dest in pairs(to) do
for _, prefix in ipairs(LAYERS) do
now[prefix .. dest] = was[prefix .. from]
touched[prefix .. dest], touched[prefix .. from] = true, true
end
end
local binds = {}
for key, command in pairs(now) do
binds[command] = binds[command] or {}
table.insert(binds[command], key)
end
-- The profiles' keys, kept for Undo, then moved.
local profileBinds = {}
for name, p in pairs(c.profiles) do
if type(p.binds) == "table" then
profileBinds[name] = p.binds
local moved = {}
for command, keys in pairs(p.binds) do
moved[command] = {}
for i, key in ipairs(keys) do moved[command][i] = ShiftKey(key, to) end
end
p.binds = moved
end
end
local map = ns.ReachMap(layoutKey)
local dirName = dir > 0 and L["right"] or L["left"]
local ok = Change({ slots = state.slots, binds = binds },
{ scope = "all", only = {}, onlyKeys = touched, profileBinds = profileBinds },
L["Your keys moved one key %s"]:format(dirName), L["moving your keys %s"]:format(dirName))
if not ok then
for name, binds in pairs(profileBinds) do c.profiles[name].binds = binds end
return
end
map = ns.ReachMap(layoutKey) -- read again: the movement keys moved
Notify(L["Your movement keys are now %s."]:format(table.concat({ map.keys.forward, map.keys.left, map.keys.back,
map.keys.right }, " ")))
end
-- The movement keys, and the edge keys that wrap round, after moving that way: for the question.
local function Describe(layoutKey, dir)
local map = ns.ReachMap(layoutKey)
local to = ns.HandShiftMap(layoutKey, dir)
local k = map.keys
local moves = table.concat({ to[k.forward], to[k.left], to[k.back], to[k.right] }, " ")
local wraps, onto = {}, {}
for from, dest in pairs(to) do
-- A wrap: the key's binds land on the other side of the row.
local pos = ns.KeyPosition(layoutKey, from)
local there = ns.KeyPosition(layoutKey, dest)
if pos and there and (pos[1] - there[1]) * dir > 0 then
wraps[#wraps + 1], onto[#onto + 1] = from, dest
end
end
table.sort(wraps)
table.sort(onto)
return moves, table.concat(wraps, " "), table.concat(onto, " ")
end
-- Asks first (what moves where), then moves the hand: dir 1 right, -1 left.
function ns.AskShiftHand(dir)
local layoutKey = ns.LayoutKey(ns.db.settings.layout)
local to, why = ns.HandShiftMap(layoutKey, dir)
if not to then return Print(L["Keys not moved: %s."]:format(why)) end
local moves, wraps, onto = Describe(layoutKey, dir)
local text = L["Move every keybind on the left of your keyboard one key %s, in every Shift, Ctrl and Alt layer?\n\nYour movement keys go to %s. What's on %s goes round to %s. Your profiles' keys move too.\n\nUndo puts it all back."]
:format(dir > 0 and L["right"] or L["left"], moves, wraps, onto)
if ns.SharedKeybinds() then
text = text .. "\n\n" .. L["Your keybinds are shared by all your characters, so this character gets its own first: the others keep theirs."]
end
ns.Dialog("KEYSTANCE_SHIFT_HAND", {
text = "%s",
button1 = L["Move"],
button2 = CANCEL or L["Cancel"],
OnAccept = function(_, data)
local now = ns.OutOfCombat("apply", function() ShiftHand(data.layout, data.dir) end)
if not now then Notify(L["Your keys will move when combat ends."]) end
end,
})
StaticPopup_Show("KEYSTANCE_SHIFT_HAND", text, nil, { layout = layoutKey, dir = dir })
end
-- Puts back the bars and every key as they were before Keystance (undoable).
function ns.RestoreOriginal()
local c = Char()
if not (c and c.snapshot) then return Print(L["Your original setup hasn't been saved yet."]) end
ns.pendingProfile = nil -- Restore takes the queue's place
local now = ns.OutOfCombat("apply", function()
Change(c.snapshot, { scope = "all" }, L["Your original setup is back"], L["restoring your original setup"],
function() c.active = nil end)
end)
if not now then Notify(L["Your original setup will come back when combat ends."]) end
end
---------------------------------------------------------------------------
-- Account-wide keybinds
---------------------------------------------------------------------------
-- True while this character uses the keybinds shared by the whole account.
function ns.SharedKeybinds()
return GetCurrentBindingSet() == 1
end
-- Before a key is changed: the "Before Keystance" snapshot if it isn't taken yet, and this
-- character's own keybinds if it shares the account's (so other characters keep theirs).
-- Out of combat only (callers check).
function ns.OwnKeybindsFirst()
ns.TakeSnapshot()
if ns.SharedKeybinds() then SaveBindings(2) end
end
-- Gives this character its own keybinds: the current ones are copied, so nothing changes
-- on screen, and the account's set (what other characters use) is left untouched.
-- SaveBindings(2) does this (measured 2026-09-28: every bind kept, survives relog).
function ns.UseOwnKeybinds()
if not ns.SharedKeybinds() then return Print(L["This character already has its own keybinds."]) end
local now = ns.OutOfCombat("ownkeys", function()
ns.OwnKeybindsFirst()
Notify(L["This character now has its own keybinds. Your other characters keep theirs."])
ns.ProfilesChanged()
end)
if not now then Notify(L["This character gets its own keybinds when combat ends."]) end
end
---------------------------------------------------------------------------
-- Keybinds (Bindings.xml, under Keystance in the game's Key Bindings): Next profile,
-- Profile 1-6 and Open Keystance. Each Profile N keybind belongs to one profile
-- (c.keySlots[N] = name), so a profile keeps its key when others are added or renamed.
-- A key is set from the profile's row (or the Actions panel), or in Key Bindings.
---------------------------------------------------------------------------
local BINDABLE = 6
local function SlotCommand(n) return "KEYSTANCE_PROFILE" .. n end
ns.ProfileSlotCommand = SlotCommand
-- The character's keybind slots: { [N] = profile name }. Made at first use from the keys
-- already bound, which followed name order before (keeps what the player set up).
local function Slots()
local c = Char()
if not c then return {} end
if not c.keySlots then
c.keySlots = {}
local names = ns.ProfileNames()
for n = 1, BINDABLE do
if names[n] and GetBindingKey(SlotCommand(n)) then c.keySlots[n] = names[n] end
end
end
return c.keySlots
end
ns.ProfileKeySlots = Slots
-- The keybind slot a profile has, or nil.
function ns.ProfileSlot(name)
for n, owner in pairs(Slots()) do
if owner == name then return n end
end
end
-- The profile's slot, giving it a free one if it has none (nil when all six are taken).
function ns.AssignProfileSlot(name)
local have = ns.ProfileSlot(name)
if have then return have end
local slots = Slots()
for n = 1, BINDABLE do
if not slots[n] then
slots[n] = name
return n, true
end
end
end
-- Applies the profile after the one in use (in name order, wrapping round).
function ns.NextProfile()
local names = ns.ProfileNames()
if #names == 0 then return Print(L["No profiles yet. /kst save Name saves your current bars and keys."]) end
local c, at = Char(), 0
for i, name in ipairs(names) do
if name == c.active then at = i end
end
ns.ApplyProfile(names[at % #names + 1])
end
-- Names the numbered keybinds after their profiles ("Profile 1: Prot") in Key Bindings.
local function BindingLabel(n, slots)
return slots[n] and L["Profile %d: %s"]:format(n, slots[n]) or L["Profile %d"]:format(n)
end
local function NameBindings()
local slots = Char() and Slots() or {}
BINDING_NAME_KEYSTANCE_PROFILE1, BINDING_NAME_KEYSTANCE_PROFILE2, BINDING_NAME_KEYSTANCE_PROFILE3 =
BindingLabel(1, slots), BindingLabel(2, slots), BindingLabel(3, slots)
BINDING_NAME_KEYSTANCE_PROFILE4, BINDING_NAME_KEYSTANCE_PROFILE5, BINDING_NAME_KEYSTANCE_PROFILE6 =
BindingLabel(4, slots), BindingLabel(5, slots), BindingLabel(6, slots)
end
ns.NameBindings = NameBindings
BINDING_HEADER_KEYSTANCE = "Keystance"
BINDING_NAME_KEYSTANCE_NEXT = L["Next profile"]
BINDING_NAME_KEYSTANCE_TOGGLE = L["Open Keystance"]
NameBindings()
-- Keeps the slots in step with the profiles (a rename carries its slot; a deleted
-- profile's slot is freed).
function ns.ProfileRenamed(old, new)
local n = ns.ProfileSlot(old)
if n then Slots()[n] = new end
NameBindings()
end
function ns.ProfileDeleted(name)
local n = ns.ProfileSlot(name)
if n then Slots()[n] = nil end
NameBindings()
end
-- The key that switches to a profile, as the game shows it ("F2"), or nil.
function ns.ProfileKey(name)
local n = ns.ProfileSlot(name)
local key = n and GetBindingKey(SlotCommand(n))
return key and GetBindingText(key) or nil
end
-- Makes `key` ("SHIFT-F2") the one key that switches to the profile, undoably. A key doing
-- something else is taken (and said); a slot reused from a deleted profile loses its old
-- keys. Not in combat.
function ns.SetProfileKey(name, key)
local profile = ns.FindProfile(name)
if not profile then return false end
if ns.InCombat() then
Print(L["Not in combat: try again when combat ends."])
return false
end
local n, fresh = ns.AssignProfileSlot(profile)
if not n then
Print(L["Up to %d profiles can have keys. Take one off another profile first."]:format(BINDABLE))
return false
end
local command = SlotCommand(n)
local was = GetBindingAction(key)
if was == command then
Notify(L["%s is already on %s."]:format(profile, key))
return false
end
ns.OwnKeybindsFirst()
local before = ns.CurrentState()
local state = ns.CurrentState()
for cmd, keys in pairs(state.binds) do
for i = #keys, 1, -1 do
if keys[i] == key then table.remove(keys, i) end
end
if #keys == 0 then state.binds[cmd] = nil end
end
state.binds[command] = { key }
local ok, why = ns.ApplyState(state, { scope = "all" })
if not ok then
if fresh then Slots()[n] = nil end
Print(L["Nothing changed: %s."]:format(why))
return false
end
ns.RecordChange(before, L["putting %s on %s"]:format(profile, key))
NameBindings()
Notify(L["%s is now on %s."]:format(profile, key))
if was and was ~= "" then Print(L["(%s was %s.)"]:format(key, ns.CommandName(was))) end
ns.ProfilesChanged()
return true
end
-- Takes a profile's key off (undoably) and frees its slot. Not in combat.
function ns.ClearProfileKey(name)
local profile = ns.FindProfile(name)
local n = profile and ns.ProfileSlot(profile)
if not n then return false end
if ns.InCombat() then
Print(L["Not in combat: try again when combat ends."])
return false
end
local before = ns.CurrentState()
local state = ns.CurrentState()
if state.binds[SlotCommand(n)] then
ns.OwnKeybindsFirst()
state.binds[SlotCommand(n)] = nil
local ok, why = ns.ApplyState(state, { scope = "all" })
if not ok then
Print(L["Nothing changed: %s."]:format(why))
return false
end
ns.RecordChange(before, L["taking %s's key off"]:format(profile))
end
Slots()[n] = nil
NameBindings()
Notify(L["%s has no key now."]:format(profile))
ns.ProfilesChanged()
return true
end
ns.On("PLAYER_LOGIN", NameBindings)
-- What the keybinds run (Bindings.xml).
function Keystance_Binding(what)
if what == "next" then return ns.NextProfile() end
if what == "window" then return ns.ToggleWindow() end
local name = Slots()[what]
if not (name and ns.FindProfile(name)) then
return Print(L["No profile is on this key yet: set one from the Profiles tab."])
end
ns.ApplyProfile(name)
end
---------------------------------------------------------------------------
-- Slash commands
---------------------------------------------------------------------------
ns.AddCommand("save", function(arg)
local name, why = ns.SaveProfile(arg)
if not name and ns.FindProfile(arg) then
return Print(why .. " " .. L["Use the Profiles tab's Update button to replace it."])
end
Print(name and L["Saved your bars and keys as %s."]:format(name) or why)
end)
ns.AddCommand("apply", function(arg) ns.ApplyProfile(arg) end)
ns.AddCommand("undo", function() ns.Undo() end)
ns.AddCommand("restore", function() ns.ConfirmRestore() end)
ns.AddCommand("ownkeys", function() ns.UseOwnKeybinds() end)
ns.AddCommand("profiles", function()
local names = ns.ProfileNames()
if #names == 0 then return Print(L["No profiles yet. /kst save Name saves your current bars and keys."]) end
Print(L["Profiles: %s"]:format(table.concat(names, ", ")))
end)
---------------------------------------------------------------------------
-- Dialogs. Each is added to Blizzard's StaticPopupDialogs the first time it's needed, and
-- never by assigning the global itself (that taints it).
---------------------------------------------------------------------------
local Dialog = ns.Dialog
-- The text typed into a dialog's box (the field's name differs between client versions).
local function Typed(dialog)
local box = dialog.editBox or dialog.EditBox or (dialog.GetEditBox and dialog:GetEditBox())
return box and box:GetText() or ""
end
-- Asks what to do when a profile would change keybinds shared by every character. `noGear`
-- (a rule's switch) is carried through the answer.
function ns.AskSharedKeybinds(name, noGear)
Dialog("KEYSTANCE_SHARED_KEYS", {
text = L["Your keybinds are shared by all your characters, so %s would change them for everyone.\n\nKeystance recommends giving this character its own keybinds first: nothing changes on screen, and your other characters keep theirs."],
button1 = L["Own keybinds, then apply"],
button2 = CANCEL or "Cancel",
button3 = L["Bars only"],
OnAccept = function(_, data)
ns.UseOwnKeybinds()
ns.ApplyProfile(data.name, nil, true, data.noGear)
end,
OnAlt = function(_, data) ns.ApplyProfile(data.name, false, nil, data.noGear) end,
})
StaticPopup_Show("KEYSTANCE_SHARED_KEYS", name, nil, { name = name, noGear = noGear })
end
-- Asks before putting the original setup back.
function ns.ConfirmRestore()
local c = Char()
if not (c and c.snapshot) then return Print(L["Your original setup hasn't been saved yet."]) end
Dialog("KEYSTANCE_RESTORE", {
text = L["Put your bars and every keybind back as they were before Keystance (saved %s)?\n\nYou can undo this."],
button1 = L["Restore"],
button2 = CANCEL or "Cancel",
OnAccept = function() ns.RestoreOriginal() end,
})
StaticPopup_Show("KEYSTANCE_RESTORE", date("%d %b %Y", c.snapshot.at))
end
function ns.ConfirmDelete(name)
Dialog("KEYSTANCE_DELETE", {
text = L["Delete the profile %s? Your bars and keys stay as they are."],
button1 = DELETE or "Delete",
button2 = CANCEL or "Cancel",
OnAccept = function(_, data)
local ok, why = ns.DeleteProfile(data)
if ok then Notify(L["Deleted %s."]:format(ok)) else Print(why) end
end,
})
StaticPopup_Show("KEYSTANCE_DELETE", name, nil, name)
end
-- Asks for a name, then calls fn(name); `text` says what it's for.
local nameHandler
local function NameAccepted(dialog)
local fn = nameHandler
nameHandler = nil
if fn then fn(Typed(dialog)) end
end
function ns.AskName(text, fn, default)
Dialog("KEYSTANCE_NAME", {
text = "%s",
button1 = OKAY or "OK",
button2 = CANCEL or "Cancel",
hasEditBox = true,
maxLetters = MAX_NAME,
OnAccept = NameAccepted,
EditBoxOnEnterPressed = function(box)
local dialog = box:GetParent()
NameAccepted(dialog)
dialog:Hide()
end,
EditBoxOnEscapePressed = function(box) box:GetParent():Hide() end,
OnShow = function(dialog)
local box = dialog.editBox or dialog.EditBox or (dialog.GetEditBox and dialog:GetEditBox())
if box then
box:SetText(dialog.data or "")
box:SetFocus()
box:HighlightText()
end
end,
})
nameHandler = fn
StaticPopup_Show("KEYSTANCE_NAME", text, nil, default or "")
end
-- New profile from the current setup: asks for a name; asks before replacing one.
function ns.NewProfile()
ns.AskName(L["Name the profile (for example Ret, Prot or Holy):"], function(name)
local saved, why = ns.SaveProfile(name)
if saved then Notify(L["Saved your bars and keys as %s."]:format(saved)) else Print(why) end
end)
end
function ns.AskRename(name)
ns.AskName(L["New name for %s:"]:format(name), function(new)
local ok, why = ns.RenameProfile(name, new)
if ok then Notify(L["Renamed %s to %s."]:format(name, ok)) else Print(why) end
end, name)
end
function ns.AskDuplicate(name)
ns.AskName(L["Name for the copy of %s:"]:format(name), function(new)
local ok, why = ns.DuplicateProfile(name, new)
if ok then Notify(L["Copied %s as %s."]:format(name, ok)) else Print(why) end
end, name .. " 2")
end
-- What Update saves: the gear too when the profile has Keystance's own gear and that's the
-- gear source in use.
function ns.UpdateSaves(name)
local key = ns.FindProfile(name)
local p = key and Char().profiles[key]
return (p and ns.ProfileGear(p) == "items") and L["bars, keys and gear"] or L["bars and keys"]
end
-- Replaces a profile with the current setup, after asking.
function ns.ConfirmUpdate(name)
local what = ns.UpdateSaves(name)
Dialog("KEYSTANCE_UPDATE", {
text = L["Replace %s with your %s as they are now?"],
button1 = L["Replace"],
button2 = CANCEL or "Cancel",
OnAccept = function(_, data)
local ok, why = ns.SaveProfile(data, true)
if ok then Notify(L["Updated %s from your current setup."]:format(ok)) else Print(why) end
end,
})
StaticPopup_Show("KEYSTANCE_UPDATE", name, what, name)
end