Repository navigation
Expand file tree
/
Copy pathBuild.lua
More file actions
1817 lines (1608 loc) · 74.5 KB
/
Copy pathBuild.lua
File metadata and controls
1817 lines (1608 loc) · 74.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
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
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
local ADDON, ns = ...
-- Buttons and bar frames: creation, appearance, layout.
--
-- === Division of labour with the engine ===
--
-- A button is a SecureActionButtonTemplate and gets its action slot as the
-- "action" attribute. The engine then does the pressing: casting, cooldown,
-- usability and range are its business. Ours is to create the button, tell it
-- the slot and draw what is visible on it.
--
-- This is not a convenience. Casting from insecure code is impossible; an
-- addon that "presses the button itself" is asking the engine to do it by a
-- longer route.
--
-- === Forbidden in combat ===
--
-- Creating a button, assigning its slot, binding a key to it, showing or
-- hiding a bar. All of it is protected and all of it happens out of combat.
-- In combat the buttons run on what the engine was told in advance: which
-- page to show in a stance, when to hide the bar.
local Build = {}
ns.Build = Build
-- Bar frames we created: key -> frame.
local frames = {}
-- Buttons: bar key -> array of buttons.
local buttons = {}
Build.frames = frames
Build.buttons = buttons
--- Whether combat is in progress.
-- @return boolean
local function inCombat()
if type(InCombatLockdown) ~= "function" then return false end
local ok, locked = pcall(InCombatLockdown)
return ok and locked and true or false
end
Build.InCombat = inCombat
--============================================================================
-- Bar frame
--============================================================================
--- Create the bar frame, or return the existing one.
--
-- SecureHandlerStateTemplate is required for two things at once: page paging
-- and the visibility condition. The engine evaluates both, so both work in
-- combat.
--
-- For moving purposes the frame is ordinary: it is ours, so Core/Mover.lua
-- drags it like any other. That was the point of the whole design.
--
-- @param key string bar key
-- @return table|nil
function Build.Frame(key)
if type(key) ~= "string" or key == "" then return nil end
if frames[key] then return frames[key] end
if type(CreateFrame) ~= "function" then return nil end
if inCombat() then return nil end
local name = "RackBar_" .. key
local ok, frame = pcall(CreateFrame, "Frame", name, _G.UIParent,
"SecureHandlerStateTemplate")
if not ok or type(frame) ~= "table" then return nil end
pcall(frame.SetSize, frame, 1, 1)
pcall(frame.SetPoint, frame, "CENTER", _G.UIParent, "CENTER", 0, 0)
frames[key] = frame
buttons[key] = buttons[key] or {}
return frame
end
--============================================================================
-- Button
--============================================================================
-- Layers of the button's artwork and what sits on them.
--
-- Field names here are ours: we created the button and nobody else reads it.
-- The game-owned aliases are set at the end of dressButton.
--- A piece the game's template already made, if it made one.
--
-- Two ways of asking, because the client has had two. Modern builds hang the
-- piece on the button by a key - button.icon, button.cooldown - and older
-- ones name it after the button: RackButton1Icon. A build that answers
-- neither gets nothing back, and the caller makes its own.
--
-- Using the template's piece rather than our own is the whole point. A piece
-- the game created is driven by the game, and Show and Hide on it work in
-- combat. One we create is a child of a protected frame, where both are
-- blocked - silently, taking the rest of the repaint down with them.
--
-- @param button table the button
-- @param key string the modern field name
-- @param suffix string what the old name adds to the button's name
-- @return table|nil
local function piece(button, key, suffix)
if type(button) ~= "table" then return nil end
if type(button[key]) == "table" then return button[key] end
if type(button.GetName) == "function" then
local ok, name = pcall(button.GetName, button)
if ok and type(name) == "string" and name ~= "" then
local said = _G[name .. suffix]
if type(said) == "table" then return said end
end
end
return nil
end
local function dressButton(button)
-- Whether the game's template actually handed its pieces over.
--
-- Bartender4 gets its icon, cooldown, flash and labels from
-- ActionButtonTemplate. If that template is absent here, or names its
-- pieces differently, we build our own - and ours are addon-made
-- children of a protected button, which behave differently in combat.
-- Which of the two we are on decides what else has to be defended
-- against, so it is recorded rather than assumed.
button.rackTemplated = piece(button, "cooldown", "Cooldown") ~= nil
local icon = piece(button, "icon", "Icon")
or button:CreateTexture(nil, "BACKGROUND")
-- Above the frame fill, below everything drawn over it: cooldown, equipped
-- border, activation glow. The sublevel is given as a number rather than
-- left to creation order - order is what once hid the icon under the fill.
pcall(icon.SetDrawLayer, icon, "BACKGROUND", 0)
icon:SetPoint("TOPLEFT", button, "TOPLEFT", 1, -1)
icon:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", -1, 1)
-- The crop removes the icon file's own border, which on a button with a
-- frame of its own reads as a second frame.
icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
button.rackIcon = icon
-- Cooldown. A CooldownFrameTemplate of its own: the engine drives it, so
-- the seconds are not ours to count.
-- Named, and anchored TO THE BUTTON.
--
-- === Both of those were wrong, and the second is why nothing was drawn ===
--
-- A Cooldown is a FRAME. Frame:SetAllPoints expects a frame, and it was
-- being handed the icon TEXTURE. Neither the game's own buttons nor
-- LibActionButton-1.0 ever anchor a cooldown to a texture - they anchor
-- it to the button, with SetAllPoints() or with two SetPoints. Anchored
-- to a texture the frame has no size it can draw into, so the numbers
-- arrived, the frame took them, and the sweep had nowhere to appear.
--
-- The player saw it as: long cooldowns and the global cooldown alike
-- simply never showed.
--
-- The name matters for a different reason: a template's own pieces are
-- reached by name on older builds, and we support more than one build.
local cooldownName
if type(button.GetName) == "function" then
local okName, said = pcall(button.GetName, button)
if okName and type(said) == "string" and said ~= "" then
cooldownName = said .. "Cooldown"
end
end
local cooldown = piece(button, "cooldown", "Cooldown")
if cooldown == nil then
-- Ours, so ours to place. Anchored to the BUTTON and never to the
-- icon texture: Frame:SetAllPoints wants a frame, and anchored to a
-- texture the frame has no size to draw into.
cooldown = CreateFrame("Cooldown", cooldownName, button,
"CooldownFrameTemplate")
cooldown:ClearAllPoints()
cooldown:SetAllPoints(button)
end
-- A cooldown that came from the template keeps the template's own
-- anchors. LibActionButton-1.0 re-anchors one only when it is hiding the
-- button's border, and otherwise leaves it exactly as the game set it -
-- which is how the game's own buttons draw it.
-- The level is LEFT ALONE.
--
-- LibActionButton-1.0 sets a level only on its charge cooldown, never on
-- this one, and that is right: two frames on the same level draw in no
-- defined order, so a cooldown lowered to the button's own level can end
-- up under the button's artwork instead of over the icon.
--
-- Whatever the template gave it is what the game's own buttons use.
-- Dressed, not left bare.
--
-- A Cooldown frame straight out of the template carries no swipe colour
-- and no edge texture of its own, and a sweep drawn in no colour is a
-- sweep nobody sees. The game's own buttons are dressed before use and so
-- is every button LibActionButton-1.0 makes; these are its values.
--
-- Regression this fixes: the global cooldown was invisible. The numbers
-- reached the frame and the frame drew them in nothing.
pcall(cooldown.SetSwipeColor, cooldown, 0, 0, 0)
pcall(cooldown.SetEdgeTexture, cooldown,
"Interface\\Cooldown\\edge")
pcall(cooldown.SetDrawSwipe, cooldown, true)
-- The edge is the bright line that runs round the sweep, and the bling is
-- the flash when it ends. Both are the game's defaults on its own
-- buttons; the flash is off because a hundred and twenty of them going
-- off together is a light show, not information.
pcall(cooldown.SetDrawEdge, cooldown, false)
pcall(cooldown.SetDrawBling, cooldown, false)
button.rackCooldown = cooldown
-- Labels. The font object is named at creation: a FontString with no font
-- assigned drops on SetText, and drops the first time text arrives before
-- a font does.
local hotkey = piece(button, "HotKey", "HotKey")
or button:CreateFontString(nil, "OVERLAY",
"NumberFontNormalSmallGray")
hotkey:SetPoint("TOPRIGHT", button, "TOPRIGHT", -2, -2)
button.rackHotkey = hotkey
local macro = piece(button, "Name", "Name")
or button:CreateFontString(nil, "OVERLAY",
"GameFontHighlightSmallOutline")
macro:SetPoint("BOTTOM", button, "BOTTOM", 0, 2)
button.rackMacro = macro
local count = piece(button, "Count", "Count")
or button:CreateFontString(nil, "OVERLAY", "NumberFontNormal")
count:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", -2, 2)
button.rackCount = count
-- Charge cooldown: one charge coming back while the others are usable.
--
-- A separate frame rather than the same one: the ordinary cooldown is not
-- running at this point - the spell is castable - and reusing its sweep
-- would show "unavailable" for something available.
--
-- Drawn as a STRIP along the bottom, not as a sweep over the icon: a dark
-- sweep over a castable spell reads as a cooldown, a strip reads as
-- "coming back". The game draws it the same way.
-- Anchored to the BUTTON, for the reason written at the cooldown above:
-- a frame anchored to a texture has no size to draw into.
local charge = CreateFrame("Cooldown",
cooldownName and (cooldownName .. "Charge") or nil,
button, "CooldownFrameTemplate")
charge:SetPoint("BOTTOMLEFT", button, "BOTTOMLEFT", 0, 0)
charge:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 0, 0)
charge:SetHeight(4)
pcall(charge.SetDrawEdge, charge, false)
pcall(charge.SetDrawSwipe, charge, true)
pcall(charge.SetHideCountdownNumbers, charge, true)
-- Shown and transparent, not hidden: this frame is ours and sits under a
-- protected button, so it is revealed with alpha (ns.Paint.Reveal). A
-- hidden frame has nothing for alpha to reveal.
charge:Show()
charge:SetAlpha(0)
button.rackCharge = charge
-- The flash: auto-attack and auto-repeat blink while they are running.
--
-- It is how the game says "this is what you are doing right now", and
-- without it a melee player cannot tell auto-attack is on except by
-- watching the character. The game's own texture and its own blend, so
-- that it reads the same as on every other bar on screen.
--
-- Hidden at birth and shown by the ticker (ns.Paint.Flashes): a texture
-- that blinks is a texture somebody has to toggle, and the addon already
-- has one frame doing that kind of work.
local flash = piece(button, "Flash", "Flash")
or button:CreateTexture(nil, "OVERLAY")
flash:SetTexture("Interface\\Buttons\\UI-QuickslotRed")
flash:SetBlendMode("ADD")
flash:SetAllPoints(button)
flash:Hide()
button.rackFlash = flash
-- The name the game uses for this piece, for the same reason the icon and
-- the cooldown carry theirs: a neighbour looking for it finds it.
button.Flash = flash
-- Activation glow: the game says this spell is the one to cast now.
--
-- === Why not the game's artwork ===
--
-- The native glow is an animated gold flare built from a texture set and
-- driven by a 250-line library. In a dark flat theme it is the only gold
-- object on screen and argues with everything else; the textures are also
-- named differently across builds.
--
-- === Why a pulse rather than a static border ===
--
-- The glow has to be distinguishable from the equipped border and from
-- mouse hover, and the palette has no third colour to spare for it. So it
-- differs by MOTION, not colour: the glowing button is the only one on the
-- bar that breathes. Motion is caught at the edge of vision, which is how
-- a rotation hint is read.
do
local glow = CreateFrame("Frame", nil, button)
glow:SetPoint("TOPLEFT", icon, "TOPLEFT", -2, 2)
glow:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", 2, -2)
local okLevel, level = pcall(button.GetFrameLevel, button)
if okLevel and type(level) == "number" then
pcall(glow.SetFrameLevel, glow, level + 3)
end
local r, g, b = ns.Skin.Color("accent")
local sides = {
{ "TOPLEFT", "TOPRIGHT", nil, 2 },
{ "BOTTOMLEFT", "BOTTOMRIGHT", nil, 2 },
{ "TOPLEFT", "BOTTOMLEFT", 2, nil },
{ "TOPRIGHT", "BOTTOMRIGHT", 2, nil },
}
for i = 1, #sides do
local side = sides[i]
local line = glow:CreateTexture(nil, "OVERLAY")
line:SetPoint(side[1])
line:SetPoint(side[2])
if side[3] then line:SetWidth(side[3]) end
if side[4] then line:SetHeight(side[4]) end
if r then line:SetColorTexture(r, g, b, 1) end
end
glow:Show()
glow:SetAlpha(0)
button.rackGlow = glow
-- The pulse uses an engine animation group rather than a per-frame count
-- of our own: counting on OnUpdate for 120 buttons would cost a frame of
-- combat and look identical.
--
-- A missing animation group is a normal answer, not an error: the glow
-- then shines steadily instead of breathing, and it is still a glow.
if type(glow.CreateAnimationGroup) == "function" then
local okGroup, pulse = pcall(glow.CreateAnimationGroup, glow)
if okGroup and type(pulse) == "table" then
pcall(pulse.SetLooping, pulse, "BOUNCE")
local okFade, fade = pcall(pulse.CreateAnimation, pulse, "Alpha")
if okFade and type(fade) == "table" then
pcall(fade.SetFromAlpha, fade, 1)
pcall(fade.SetToAlpha, fade, 0.35)
pcall(fade.SetDuration, fade, 0.6)
button.rackGlowPulse = pulse
end
end
end
end
-- Pet autocast: a strip under the icon.
--
-- A pet ability with autocast on is cast by the pet without an order. The
-- game says so with spinning sparkles around the button - artwork a dark
-- flat theme has no source for, and named differently across builds.
--
-- It must be distinguishable from the three things the button already
-- says: equipped (border), cast this now (pulse), newly learned (corner
-- dot). The palette has no fourth colour, so this one differs by shape
-- again: a strip along the bottom edge of the icon means "runs itself".
do
local auto = button:CreateTexture(nil, "OVERLAY")
auto:SetHeight(2)
auto:SetPoint("BOTTOMLEFT", icon, "BOTTOMLEFT", 0, 0)
auto:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", 0, 0)
local r, g, b = ns.Skin.Color("accent")
if r then pcall(auto.SetColorTexture, auto, r, g, b, 1) end
auto:Hide()
button.rackAuto = auto
end
-- New-action mark: a dot in the corner of the icon.
--
-- The game drops newly learned spells onto the bars itself and marks
-- them, so the player notices what appeared. The mark is cleared by the
-- first use or by mouse hover (ns.Paint.Touch).
--
-- === Why a dot, and not a border or a warm colour ===
--
-- A border already says "equipped" and the pulse says "cast this now"; a
-- third border is more than the button can carry. A warm colour is
-- forbidden by the palette rule: this theme has exactly one warm role, and
-- it belongs to a reading that demands action, not to a mark that can
-- light up on ten buttons at once (Core/Skin.lua).
--
-- So the mark differs by SHAPE rather than colour, which is enough: a new
-- spell is looked for once.
do
local fresh = button:CreateTexture(nil, "OVERLAY")
fresh:SetSize(5, 5)
fresh:SetPoint("TOPLEFT", icon, "TOPLEFT", 1, -1)
local r, g, b = ns.Skin.Color("accent")
if r then pcall(fresh.SetColorTexture, fresh, r, g, b, 1) end
fresh:Hide()
button.rackFresh = fresh
end
-- Equipped-item border. Wanted where a weapon or shield lies on the bar:
-- the player sees what is worn without opening a bag.
--
-- Palette accent rather than the game's green gem: there is no green in
-- this theme at all, and a single green object would outshine everything
-- else on screen.
--
-- Built from four strips for the same reason the button frame is: not
-- every frame has SetBackdrop on this client, while textures always work
-- (Core/Skin.lua).
do
local worn = CreateFrame("Frame", nil, button)
worn:SetAllPoints(icon)
-- The border goes ABOVE the icon and the cooldown: otherwise the
-- cooldown sweep paints over it and worn gear looks unworn on cooldown.
local okLevel, level = pcall(button.GetFrameLevel, button)
if okLevel and type(level) == "number" then
pcall(worn.SetFrameLevel, worn, level + 2)
end
local r, g, b = ns.Skin.Color("accent")
local sides = {
{ "TOPLEFT", "TOPRIGHT", nil, 1 },
{ "BOTTOMLEFT", "BOTTOMRIGHT", nil, 1 },
{ "TOPLEFT", "BOTTOMLEFT", 1, nil },
{ "TOPRIGHT", "BOTTOMRIGHT", 1, nil },
}
for i = 1, #sides do
local side = sides[i]
local line = worn:CreateTexture(nil, "OVERLAY")
line:SetPoint(side[1])
line:SetPoint(side[2])
if side[3] then line:SetWidth(side[3]) end
if side[4] then line:SetHeight(side[4]) end
if r then line:SetColorTexture(r, g, b, 1) end
end
worn:Show()
worn:SetAlpha(0)
button.rackEquipped = worn
end
-- Frame, hover response and pressed state.
--
-- The regions are created HERE; Skinning.lua decides how they look. The
-- split is not cosmetic: the player picks the button look - our flat one,
-- the game's art or a Masque skin - and changes it at will. A texture,
-- once created, cannot be given back to the engine, so creating one per
-- look would accumulate three textures per button, 300 per bar.
--
-- Hence one texture per state, re-dressed by Skinning.
ns.Skin.Backdrop(button, "panel", "line")
-- State textures: pushed, highlight, checked.
--
-- === The engine must create them, not us ===
--
-- Creating them ourselves and handing them over ("here is a texture, be
-- the pushed one") does not work: the button accepts the texture but does
-- not drive it, so the texture stays visible ALWAYS.
--
-- The pushed texture is a dark inset square drawn over the icon, which is
-- why every icon on the bar looked black from the start.
--
-- Passing a FILE PATH instead makes the engine create the texture, show
-- and hide it itself. The object can then be fetched back with
-- Get*Texture() and recoloured, which is what Skinning.lua needs.
--
-- The white pixel here is a canvas, not a picture: the palette gives it
-- its colour, the flat look paints it, and the game look replaces it with
-- the game's own art.
local FLAT = ns.Media.FLAT
if type(button.SetCheckedTexture) == "function" then
pcall(button.SetCheckedTexture, button, FLAT)
if type(button.GetCheckedTexture) == "function" then
local ok, said = pcall(button.GetCheckedTexture, button)
if ok then button.rackChecked = said end
end
end
if type(button.SetPushedTexture) == "function" then
pcall(button.SetPushedTexture, button, FLAT)
if type(button.GetPushedTexture) == "function" then
local ok, said = pcall(button.GetPushedTexture, button)
if ok then button.rackPushed = said end
end
end
if type(button.SetHighlightTexture) == "function" then
pcall(button.SetHighlightTexture, button, FLAT)
if type(button.GetHighlightTexture) == "function" then
local ok, said = pcall(button.GetHighlightTexture, button)
if ok then button.rackHover = said end
end
end
-- Game-owned ALIASES for the button's parts.
--
-- === Why a second name for things already named ===
--
-- Neighbouring addons find the parts of someone else's button by the
-- game's field names: the icon in "icon", the cooldown in "cooldown", the
-- key label in "HotKey". That is how the game's own button is built, and
-- skinning addons, Masque and darkening addons all read those names.
--
-- Our prefixed names stay internal - they make it obvious the field is
-- ours - but the button has to look like a button from outside.
--
-- Regression this fixed: a darkening addon looks for the icon in order NOT
-- to touch it (the icon is content, it identifies the spell). Not finding
-- the usual field, it treated the icon as chrome and darkened it with the
-- frame. Buttons lit up in colour and went black a moment later.
--
-- One line per part, and it settles a dozen such conversations in advance.
button.icon = icon
button.cooldown = button.rackCooldown
button.HotKey = button.rackHotkey
button.Name = button.rackMacro
button.Count = button.rackCount
ns.Skinning.Dress(button.rackBar, button)
end
--- Create a bar button, or return the existing one.
--
-- The button's name comes from ns.Bind.ButtonName: it is needed here and when
-- binding a key, and two spellings drifting apart would break the key
-- silently.
--
-- @param key string bar key
-- @param index number button number
-- @return table|nil
function Build.Button(key, index)
local parent = Build.Frame(key)
if not parent then return nil end
if inCombat() then return nil end
local list = buttons[key]
if list[index] then return list[index] end
local name = ns.Bind.ButtonName(key, index)
if not name then return nil end
-- The game's own action button template, plus the secure one.
--
-- === Why the game's template and not a bare button ===
--
-- A bare SecureActionButtonTemplate gives a button with nothing on it,
-- and every piece then has to be created by us: icon, cooldown, flash,
-- labels. Those pieces are CHILDREN OF A PROTECTED FRAME, and in combat
-- Show and Hide on such a frame are blocked - silently, aborting the
-- whole call stack. The repaint then died part way through, and
-- everything painted after the first hidden frame stopped updating in a
-- fight: no cooldown, no range colour, nothing. Out of combat it all
-- worked, which is what the symptom looked like.
--
-- ActionButtonTemplate is how the game makes its own buttons and how
-- LibActionButton-1.0 makes Bartender4's. Its pieces come from the
-- client rather than from us, and they behave in combat because the game
-- drives them.
--
-- The bare template stays as a fallback: this addon declares every live
-- client and must not fail to build a button on one that lacks it.
local ok, button = pcall(CreateFrame, "CheckButton", name, parent,
"ActionButtonTemplate, SecureActionButtonTemplate")
if not ok or type(button) ~= "table" then
ok, button = pcall(CreateFrame, "CheckButton", name, parent,
"SecureActionButtonTemplate")
end
if not ok or type(button) ~= "table" then return nil end
-- The template's own handlers are taken off.
--
-- === This is the bug, and it is their comment that names it ===
--
-- LibActionButton-1.0 writes, at this exact point:
--
-- button:SetScript("OnAttributeChanged", nil)
-- -- inherited templates bring in a handler here which we
-- -- don't want, so get rid of it
--
-- ActionButtonTemplate carries the game's own action button code with
-- it. On the game's buttons that code knows the slot it is looking at;
-- on ours it does not, so it runs, finds nothing, and HIDES the pieces -
-- the cooldown among them.
--
-- OnAttributeChanged is the worst of them, and it is what made this look
-- like a combat bug. It fires on every attribute change, and the secure
-- state driver changes attributes constantly in a fight - paging,
-- stances, vehicles. Out of combat the attributes sit still and the
-- handler rarely runs, so everything looked right; in a fight it ran
-- over and over and hid the cooldown as fast as the repaint set it.
--
-- The pieces are kept, the behaviour is dropped. Ours is set afterwards.
for _, script in ipairs({ "OnAttributeChanged", "OnEvent", "OnUpdate",
"OnShow", "OnHide", "OnLoad" }) do
pcall(button.SetScript, button, script, nil)
end
if type(button.UnregisterAllEvents) == "function" then
pcall(button.UnregisterAllEvents, button)
end
-- Which bar this button belongs to. Needed where the button repaints
-- itself - after a drag: the settings are fetched by key, and finding the
-- button by walking every bar backwards would be a second way to answer a
-- question it can already answer.
button.rackBar = key
-- Its own index, held for the same reason as the bar key: the key-binding
-- mode asks the button what it does. The field is the button's PLACE on
-- the bar, which the slot offset does not change.
button.rackIndex = index
-- The slot is also an attribute, but that attribute is a DIFFERENT number.
--
-- * button.rackIndex - the button's PLACE on the bar. The key-binding mode
-- says "third button of the main bar" by it, and Slots.Shift does not
-- change it: the player presses the third from the left regardless of
-- what lies on it.
-- * attribute "rackSlot" - which slot OF THE PAGE the button shows. The
-- offset rotates this one (Build.Assign) and the paging snippet reads
-- it: the secure handler lives in a separate environment and sees only
-- attributes. Without it the snippet cannot compute the slot and the bar
-- stays on page one forever.
--
-- The unrotated slot is set here; how large the offset is belongs to the
-- settings, which this function knows nothing about.
pcall(button.SetAttribute, button, "rackSlot", index)
-- What the button does. For an action bar it is "use this slot"; for the
-- pet bar "use this pet action", and the numbering differs: slot ten of a
-- bar and pet action ten are different things with the same number.
if key == "pet" then
button.rackPetIndex = index
pcall(button.SetAttribute, button, "type", "pet")
pcall(button.SetAttribute, button, "action", index)
elseif key == "stance" then
-- A stance button gets no number: it has a spell rather than a slot,
-- and the repaint names it where it learns the name (Stance.Assign).
button.rackStanceIndex = index
else
pcall(button.SetAttribute, button, "type", "action")
end
-- Snippets the button runs ITSELF when the bar says so.
--
-- The alternative would be the bar walking its children - and a list of
-- children cannot be obtained in the secure handler environment (see the
-- paging snippet below). The bar calls ChildUpdate and each button does
-- its own part.
--
-- The value arrives as "message": that is the engine's own name for it.
pcall(button.SetAttribute, button, "_childupdate-page", [[
local slot = self:GetAttribute("rackSlot")
if not slot then return end
local page = tonumber(message) or 1
self:SetAttribute("actionpage", page)
self:SetAttribute("action", (page - 1) * 12 + slot)
local header = self:GetParent()
local pick = header and header:GetAttribute("rackPickTarget")
if pick then header:RunFor(self, pick) end
]])
pcall(button.SetAttribute, button, "_childupdate-pick", [[
local header = self:GetParent()
local pick = header and header:GetAttribute("rackPickTarget")
if pick then header:RunFor(self, pick) end
]])
-- Click registration follows the game's own ActionButtonUseKeyDown CVar.
-- Build.Clicks picks one of AnyUp/AnyDown; we do not add a second setting
-- about the same thing.
Build.Clicks(button)
if type(button.RegisterForDrag) == "function" then
pcall(button.RegisterForDrag, button, "LeftButton", "RightButton")
end
Build.Draggable(button)
-- Flyout opening. Action bars only: a pet slot or a stance list has no
-- source for a flyout.
if key ~= "pet" and key ~= "stance" then
Build.Hatch(button, parent)
end
-- Right-click autocast toggling - pet bar only.
if key == "pet" then ns.Pet.Autocast(button) end
-- Tooltip. The mode is asked of the bar at the moment the mouse arrives
-- rather than remembered at creation: the player changes it in the
-- settings, and a remembered value would hold until the next login.
if ns.Tip then
ns.Tip.Hook(button, function()
return ns.Bars.TipMode(button.rackBar)
end)
end
dressButton(button)
list[index] = button
return button
end
--- Create a flyout button.
--
-- === How it differs from a bar button ===
--
-- Not at all in appearance and entirely in substance. It is dressed by the
-- same dressButton on purpose: buttons dropping out of a flyout have to look
-- like bar buttons, or the flyout reads as a foreign window.
--
-- In substance it carries a SPELL, not a slot: no index, no page, no paging.
-- So it gets no "rackSlot" attribute, no snippets and no drag handlers - the
-- game owns the flyout's contents and there is nothing to rearrange.
--
-- These buttons are shown and placed by the secure snippet in Flyout.lua; our
-- part is to create and dress them.
--
-- @param index number position in the pool
-- @param parent table the flyout handler
-- @return table|nil
function Build.Spare(index, parent)
if inCombat() then return nil end
if type(CreateFrame) ~= "function" then return nil end
if type(parent) ~= "table" then return nil end
local name = "RackFlyoutButton" .. tostring(index)
-- The same template as a bar button, for the same reason (Build.Button).
local ok, button = pcall(CreateFrame, "CheckButton", name, parent,
"ActionButtonTemplate, SecureActionButtonTemplate")
if not ok or type(button) ~= "table" then
ok, button = pcall(CreateFrame, "CheckButton", name, parent,
"SecureActionButtonTemplate")
end
if not ok or type(button) ~= "table" then return nil end
-- The template's own handlers are taken off.
--
-- === This is the bug, and it is their comment that names it ===
--
-- LibActionButton-1.0 writes, at this exact point:
--
-- button:SetScript("OnAttributeChanged", nil)
-- -- inherited templates bring in a handler here which we
-- -- don't want, so get rid of it
--
-- ActionButtonTemplate carries the game's own action button code with
-- it. On the game's buttons that code knows the slot it is looking at;
-- on ours it does not, so it runs, finds nothing, and HIDES the pieces -
-- the cooldown among them.
--
-- OnAttributeChanged is the worst of them, and it is what made this look
-- like a combat bug. It fires on every attribute change, and the secure
-- state driver changes attributes constantly in a fight - paging,
-- stances, vehicles. Out of combat the attributes sit still and the
-- handler rarely runs, so everything looked right; in a fight it ran
-- over and over and hid the cooldown as fast as the repaint set it.
--
-- The pieces are kept, the behaviour is dropped. Ours is set afterwards.
for _, script in ipairs({ "OnAttributeChanged", "OnEvent", "OnUpdate",
"OnShow", "OnHide", "OnLoad" }) do
pcall(button.SetScript, button, script, nil)
end
if type(button.UnregisterAllEvents) == "function" then
pcall(button.UnregisterAllEvents, button)
end
pcall(button.SetAttribute, button, "type", "spell")
pcall(button.Hide, button)
Build.Clicks(button)
-- The method the snippet repaints the button through. The snippet sees
-- none of our tables or functions - it can only call a METHOD by name,
-- and this is that name (CallMethod("RackPaint")).
button.RackPaint = function(self, spell)
return ns.Flyout.Paint(self, spell)
end
if ns.Tip then
ns.Tip.Hook(button, function() return ns.Bars.TipMode("main") end)
end
dressButton(button)
return button
end
--- Give the engine the opening of flyouts on this button.
--
-- === Why a click wrapper and not an ordinary handler ===
--
-- A flyout is opened in combat, where insecure code may not show a secure
-- button. A wrapper is itself a snippet: the engine runs it, before the click,
-- and no such restriction applies.
--
-- Returning false from the pre-body means "there was no click": otherwise the
-- button would both open the flyout and use what lies in the slot - and what
-- lies there is the flyout itself, which has nothing to use.
--
-- The snippet asks what is in the slot by itself: GetActionInfo exists in the
-- restricted environment. So no attribute carries the flyout id, and there is
-- nothing for such an attribute to drift from.
--
-- @param button table
-- @param header table the bar frame, which is also the handler
-- @return boolean
--- Whether the game's bars are locked right now.
--
-- Locked is not a ban but a MODIFIER REQUIREMENT: with bars locked a spell
-- is picked up with a modifier held, and which one is the player's setting.
-- The caller decides what to do about it - the Lua path asks
-- IsModifiedClick itself, the drag snippet is handed this as an attribute
-- because the restricted environment cannot read a CVar.
--
-- A build with no such setting answers "not locked": refusing to let a
-- player move their spells because a question could not be asked is the
-- worse failure.
--
-- @return boolean
function Build.Locked()
if type(_G.GetCVarBool) ~= "function" then return false end
local ok, locked = pcall(_G.GetCVarBool, "lockActionBars")
if not ok then return false end
return locked and true or false
end
--- Let the button be dragged IN COMBAT, through the restricted environment.
--
-- === Why the Lua handler cannot do it ===
--
-- PickupAction and PlaceAction are protected: called from addon code in
-- combat they are blocked. So the Lua OnDragStart has to refuse in a fight,
-- and a player cannot rearrange their bars while one is on.
--
-- The restricted environment may do it. A wrapped OnDragStart runs there,
-- and what it RETURNS is the pickup: "action", slot. The engine performs
-- the move itself, so nothing protected is called by us at all.
--
-- LibActionButton-1.0 works this way - it sets its Lua OnDragStart to nil
-- and wraps the script on the header instead - and that is why Bartender4
-- can move spells mid-fight.
--
-- The lock is honoured in the snippet because it must be: with bars locked
-- the game wants a modifier held, and IsModifiedClick is one of the few
-- calls the restricted environment is given.
--
-- @param button any the button
-- @param header any the bar's secure header
-- @return boolean whether the secure drag was installed
function Build.Haul(button, header)
if type(button) ~= "table" or type(header) ~= "table" then return false end
if type(header.WrapScript) ~= "function" then return false end
local ok = pcall(header.WrapScript, header, button, "OnDragStart", [[
if self:GetAttribute("type") ~= "action" then return end
if self:GetAttribute("rackLocked")
and not IsModifiedClick("PICKUPACTION") then
return false
end
return "action", self:GetAttribute("action")
]])
if not ok then return false end
pcall(header.WrapScript, header, button, "OnReceiveDrag", [[
if self:GetAttribute("type") ~= "action" then return end
return "action", self:GetAttribute("action")
]])
-- The Lua handlers go, or both run and the spell is picked up twice.
pcall(button.SetScript, button, "OnDragStart", nil)
pcall(button.SetScript, button, "OnReceiveDrag", nil)
button.rackHauled = true
return true
end
function Build.Hatch(button, header)
if type(button) ~= "table" or type(header) ~= "table" then return false end
if type(header.WrapScript) ~= "function" then return false end
if type(header.SetFrameRef) ~= "function" then return false end
local host = ns.Flyout.Handler()
if host == nil then return false end
pcall(header.SetFrameRef, header, "rackFlyout", host)
return pcall(header.WrapScript, header, button, "OnClick", [[
if self:GetAttribute("type") ~= "action" then return end
local host = owner:GetFrameRef("rackFlyout")
if not host then return end
local kind, id = GetActionInfo(self:GetAttribute("action"))
if kind == "flyout" then
host:SetAttribute("rackParent", self)
host:RunAttribute("RackOpen", id)
return false
end
-- Any other button closes an open flyout. Otherwise it would hang on
-- screen until the player moved the mouse away.
host:Hide()
]]) and true or false
end
--- Let the button pick up and accept spells with the mouse.
--
-- === Why this is separate and shaped this way ===
--
-- A bar you cannot drop a spell onto is not a bar. The player expects our
-- buttons to behave like the game's: pick up from one, drop on another, shake
-- the spare off onto the ground.
--
-- The work is done by PickupAction and PlaceAction, both protected. Out of
-- combat an addon may call them; in combat it may not, and that is not our
-- limitation - the game forbids rearranging bars mid-fight, and its own
-- buttons cannot be dragged there either.
--
-- The in-combat refusal is therefore silent. Saying "not in combat" here
-- would misattribute the rule and repeat what the player already sees on the
-- game's own buttons.
--
-- === The bar lock ===
--
-- The game can lock bars against accidental dragging (the lockActionBars
-- CVar). A locked button is not picked up - neither the game's nor ours. The
-- game is asked rather than a second setting of our own being kept: two
-- settings about one thing drift at the first edit of either.
--
-- @param button table
--- Whether the button fires on key down or on key up.
--
-- === Why not both at once ===
--
-- Both were registered, on the theory that the engine would sort it out. It
-- did: the button fired TWICE, on down and on up.
--
-- Worse, it broke dragging. Taking a spell off a button means pressing the
-- mouse on it and moving - and the spell was cast at the moment of the press,
-- before any movement. The player saw "the click fires first, and only then
-- do I drag".
--
-- The game has one setting for this (ActionButtonUseKeyDown) covering all its
-- buttons, and that is what is asked. A second one of ours would drift from
-- it at the first edit.
--
-- @param button any the button
-- @return boolean whether it was set
function Build.Clicks(button)
if type(button) ~= "table" then return false end
if type(button.RegisterForClicks) ~= "function" then return false end
-- BOTH edges, exactly as LibActionButton-1.0 registers them:
--
-- button:RegisterForClicks("AnyDown", "AnyUp")
--
-- Choosing one from the ActionButtonUseKeyDown setting is what we did
-- before, and it leaves the button deaf whenever the guess is wrong:
-- the setting is missing on some builds, and a key arriving on the
-- other edge then matches nothing at all. The player presses and
-- nothing happens.
--
-- Registering both is safe because the secure template decides for
-- itself which edge casts on - that is its business, not ours - and it
-- is what every working bar addon does.
local ok = pcall(button.RegisterForClicks, button, "AnyDown", "AnyUp")
if ok then return true end
-- A build that takes only one edge at a time still gets the one its
-- own setting asks for.
local down = false
if type(_G.GetCVarBool) == "function" then
local okSaid, said = pcall(_G.GetCVarBool, "ActionButtonUseKeyDown")
if okSaid then down = said and true or false end
end