Repository navigation
Expand file tree
/
Copy pathOptions.lua
More file actions
3105 lines (2890 loc) · 132 KB
/
Copy pathOptions.lua
File metadata and controls
3105 lines (2890 loc) · 132 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
-- Options.lua
-- Tessera's options window and everything around it.
--
-- Main parts, in file order:
-- Selection which block(s) the "Blocks" tab edits: all blocks, all launchers or one block;
-- smart placement of newly shown blocks (PlaceNearOptions) and SetField,
-- which writes a setting to every selected block and remembers it as a default.
-- Widgets small widget factory (header, note, checkbox, slider, dropdown, action
-- dropdown, color swatch, button). Every widget has a Refresh() method and a
-- layoutHeight, and is registered so RefreshOptions() can update them all.
-- Column layout NewLayout(): stacks widgets top-down in two columns.
-- Tab builders BuildBlockPage (Display / Text / Frame sub-tabs), BuildGeneralPage,
-- BuildLaunchersPage, BuildProfilesPage, BuildHelpPage.
-- Window SkinWindow() themes a Tessera window (FlareUI or Blizzard look);
-- BuildPanel() creates the main window and its tabs; Open/Toggle functions.
-- Blizzard page a small page under Options > AddOns that opens the real options window.
-- Placement banner shown while blocks are unlocked; hides the options window meanwhile.
-- Welcome popup shown once on first launch.
-- Debug report /tessera debug: prints diagnostics and shows them in a copyable window.
--
-- Note: some debug messages below are intentionally left in French (they are string literals).
local ADDON, ns = ...
local L = ns.L
-- Options window tabs:
-- Blocks selection (all blocks, all launchers or a single block) and its settings
-- Look style, how groups look, ready-made layouts, screen edges
-- General moving, combat, chat, minimap, accessibility
-- Launchers add and edit launcher blocks and DataBroker blocks
-- Broker share Tessera's blocks with DataBroker addons, DataBroker mode
-- Profiles profiles and reset
-- Help help topics (list on the left, selected topic on the right)
-- About author, translators, license, links, and the changelog (same layout as Help)
-- In DataBroker mode (Broker.lua) only the Broker, Help and About tabs are shown.
local panel
local widgets = {}
local PAGE_BLOCKS, PAGE_LOOK, PAGE_GENERAL, PAGE_LAUNCHERS, PAGE_BROKER, PAGE_PROFILES, PAGE_HELP, PAGE_ABOUT =
1, 2, 3, 4, 5, 6, 7, 8
local HIDDEN_IN_BROKER_MODE = { [PAGE_BLOCKS] = true, [PAGE_LOOK] = true, [PAGE_GENERAL] = true,
[PAGE_LAUNCHERS] = true, [PAGE_PROFILES] = true }
local pages, tabs, currentPage = {}, {}, PAGE_BLOCKS
-- Layout: 720 px window, two 300 px columns, 40 px margins (scaled down on small
-- screens, see FitPanelToScreen)
local PANEL_W, PANEL_H = 720, 735
local COL_W = 300
local COL_X = { 40, 380 }
local FULL_W = COL_X[2] + COL_W - COL_X[1]
local SPACING = 9 -- vertical space between two settings
local ALL = "*all" -- selection value "All blocks"
local LAUNCHERS = "*launchers" -- selection value "All launchers"
local selected = ALL
---------------------------------------------------------------------------
-- Selection
---------------------------------------------------------------------------
-- Keys of all launcher blocks, in display order
local function LauncherKeys()
local keys = {}
for _, key in ipairs(ns:GetBlockKeys()) do
if ns.IsLauncherKey(key) then table.insert(keys, key) end
end
return keys
end
-- True when the selection is a group (all blocks / all launchers) rather than one block
local function IsGroup() return selected == ALL or selected == LAUNCHERS end
-- Blocks affected by a setting change
local function Targets()
if selected == ALL then
-- spacers keep their own look: "All blocks" does not change them
-- (except their layer and visibility, see SPACER_SHARED_FIELDS)
local keys = {}
for _, key in ipairs(ns:GetBlockKeys()) do
if not ns.IsSpacerKey(key) then table.insert(keys, key) end
end
return keys
end
if selected == LAUNCHERS then return LauncherKeys() end
return { selected }
end
-- Block whose values are displayed (the first one of the selection)
local function Cfg()
local key = Targets()[1]
return key and ns.db.blocks[key]
end
-- Falls back to "All blocks" if the selected block or group no longer exists
local function ValidateSelection()
if selected == ALL then return end
if selected == LAUNCHERS then
if #LauncherKeys() == 0 then selected = ALL end
return
end
if not ns.db.blocks[selected] then selected = ALL end
end
-- State used by PlaceNearOptions: a counter for the fallback position, and the areas
-- of blocks placed during the last second (cleared by a timer)
local placedCount = 0
local recentPlaced, recentTimer = {}, nil
-- Does a block's saved position overlap another visible block?
function ns:OverlapsOtherBlock(key)
local cfg, f = ns.db.blocks[key], ns.frames[key]
if not cfg or not cfg.point or not f then return false end
local ratio = f:GetEffectiveScale() / UIParent:GetEffectiveScale()
local l, b, r, t = ns:PointToRect(cfg.point, f:GetWidth() > 1 and f:GetWidth() or cfg.width or 60,
cfg.height or 24, ratio)
for k, o in pairs(ns.frames) do
if k ~= key and o:IsShown() then
local ol, ob, or_, ot = ns:GetRectUI(o)
if ol and l < or_ - 2 and r > ol + 2 and b < ot - 2 and t > ob + 2 then return true end
end
end
return false
end
-- Places a block that appears (never placed, or its old spot is taken) in a free
-- spot next to the options window: columns alternate right / left of the window,
-- nearest first, each scanned top to bottom. All maths in UIParent units.
function ns:PlaceNearOptions(key)
local cfg = ns.db.blocks[key]
if not cfg then return end
cfg.dock, cfg.anchor = nil, nil
-- real block width (text included): shown off-screen for a moment to measure it
cfg.point = { "TOPLEFT", "BOTTOMLEFT", -2000, -2000 }
ns:ApplyStyle(key)
ns:UpdateBlock(key)
local f = ns.frames[key]
if not f then return end
local ratio = f:GetEffectiveScale() / UIParent:GetEffectiveScale()
local needed = 0
if f.text then
local tw = f.text.GetUnboundedStringWidth and f.text:GetUnboundedStringWidth() or f.text:GetStringWidth()
needed = math.ceil(tw or 0) + 8 + 2 * (f.inset or 0) + (cfg.showIcon and ((f.iconSize or 0) + 4) or 0)
end
local width = math.max(cfg.width or 60, needed, 100) * ratio
local height = (cfg.height or 24) * ratio
local step = height + 22 -- room for the block name above it
local screenW = UIParent:GetWidth()
-- areas already taken: visible blocks, blocks placed in the same click (the game
-- has not recomputed their position yet), the options window
local taken = {}
for k, o in pairs(ns.frames) do
if k ~= key and o:IsShown() and not recentPlaced[k] then
local l, b, r, t = ns:GetRectUI(o)
if l then table.insert(taken, { l, b, r, t + 18 }) end
end
end
for k, z in pairs(recentPlaced) do
if k ~= key then table.insert(taken, z) end
end
local pl, pb, pr, pt
if panel and panel:IsShown() then
pl, pb, pr, pt = ns:GetRectUI(panel)
if pl then table.insert(taken, { pl, pb, pr, pt }) end
end
-- Blizzard's action bars, minimap, chat...: a block placed on them could be hidden
for _, z in ipairs(ns:GetBlizzardRects()) do table.insert(taken, z) end
local function Free(x, y)
local l, b, r, t = x - 4, y - height - 4, x + width + 4, y + 18
for _, z in ipairs(taken) do
if l < z[3] and r > z[1] and b < z[4] and t > z[2] then return false end
end
return true
end
-- candidate columns: right 1, left 1, right 2, left 2... (around the screen
-- center when the options window is closed)
local right0, left0, top
if pl then
right0, left0, top = pr + 20, pl - 20 - width, pt - 20
else
right0 = screenW / 2 + 10
left0 = screenW / 2 - 10 - width
top = UIParent:GetHeight() / 2 + 160
end
-- highest free slot of a column, above the bottom of the screen (action bars area)
local BOTTOM = 140
local function FirstFree(cx)
if not cx then return nil end
for row = 0, math.floor((top - height - BOTTOM) / step) do
local cy = top - row * step
if Free(cx, cy) then return cy end
end
end
-- columns go in pairs (right n, left n): the side with the higher free slot wins, so
-- both sides of the window fill up evenly instead of one long column on the right
local x, y
for i = 0, 12 do
local rx, lx = right0 + i * (width + 16), left0 - i * (width + 16)
local ry = (rx + width <= screenW) and FirstFree(rx) or nil
local ly = (lx >= 0) and FirstFree(lx) or nil
if ry and (not ly or ry >= ly) then
x, y = rx, ry
elseif ly then
x, y = lx, ly
end
if x then break end
end
if not x then -- screen full: fallback position
x, y = math.max(0, math.min(right0, screenW - width)), top - (placedCount % 10) * step
end
placedCount = placedCount + 1
recentPlaced[key] = { x, y - height, x + width, y + 18 }
if not recentTimer then
recentTimer = true
C_Timer.After(1, function()
recentTimer = nil
wipe(recentPlaced)
end)
end
cfg.point = ns:MakeRelativePoint(x, y - height, x + width, y, ratio)
ns:ApplyStyle(key)
end
function ns:RefreshOptions()
if not panel then return end
ValidateSelection()
for _, w in ipairs(widgets) do
if w.isShown then w:SetShown(w.isShown() and true or false) end
if w.Refresh then w:Refresh() end
end
if ns.RelayoutBlockOptions then ns.RelayoutBlockOptions() end
end
-- Per-block settings, never stored as defaults for other blocks
local PER_BLOCK_FIELDS = { enabled = true, width = true, height = true }
-- Writes a setting to every selected block. Newly shown blocks get a style and a
-- free position; group changes are also saved as defaults for future blocks.
-- Settings that "All blocks" also gives to spacers: a spacer keeps its own look
-- (colors, frame), but it must stay on the same layer as the blocks around it, and
-- show, hide and fade with them
local SPACER_SHARED_FIELDS = {
strata = true, alpha = true, fadeOut = true, fadeAlpha = true, fadeShowCombat = true,
hideInCombat = true, hideInInstance = true, hideInVehicle = true, hideWhenResting = true,
}
local function SetField(name, value)
-- style template picked before the change: a block that is already visible
local refKey = name == "enabled" and value and ns:GetReferenceBlock()
if name == "enabled" then ns:PushUndo() end -- showing / hiding blocks can be undone
for _, key in ipairs(Targets()) do
local cfg = ns.db.blocks[key]
local appearing = name == "enabled" and value and not cfg.enabled
cfg[name] = ns.DeepCopy(value)
if appearing and not cfg.point and not cfg.dock and not cfg.anchor then
if refKey and refKey ~= key then ns:AdoptReferenceStyle(key, refKey) end
ns:PlaceNearOptions(key)
elseif appearing and not cfg.dock and not cfg.anchor and ns:OverlapsOtherBlock(key) then
ns:PlaceNearOptions(key) -- its old spot is taken: find a free one
end
ns:ApplyStyle(key)
end
if selected == ALL and SPACER_SHARED_FIELDS[name] then
for _, key in ipairs(ns:GetBlockKeys()) do
if ns.IsSpacerKey(key) then
ns.db.blocks[key][name] = ns.DeepCopy(value)
ns:ApplyStyle(key)
end
end
end
-- showing a single block: unlock so the user can place it
if name == "enabled" and value and not IsGroup() and ns.db.locked then ns:SetLocked(false) end
-- remembered for blocks created or added later (except visibility and sizes)
if not PER_BLOCK_FIELDS[name] then
if selected == ALL then ns.db.groupDefaults[name] = ns.DeepCopy(value) end
if selected == ALL or selected == LAUNCHERS then
ns.db.launcherDefaults[name] = ns.DeepCopy(value)
end
end
end
---------------------------------------------------------------------------
-- Widgets
---------------------------------------------------------------------------
-- Adds a widget to the refresh list; isShown (optional) decides its visibility
local function Register(w, isShown)
w.isShown = isShown
table.insert(widgets, w)
return w
end
-- Help tooltip shown when hovering an option
local function AttachTip(frame, title, tip)
if not frame or not tip then return end
frame:HookScript("OnEnter", function(self)
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetText(title, 1, 1, 1)
GameTooltip:AddLine(tip, nil, nil, nil, true)
GameTooltip:Show()
end)
frame:HookScript("OnLeave", function() GameTooltip:Hide() end)
end
-- Small title label at the top-left of a widget holder
local function AddTitle(holder, text)
local title = holder:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
title:SetPoint("TOPLEFT")
title:SetText(text)
return title
end
-- Section header: gold text with a thin line underneath
local function CreateHeader(parent, text)
local holder = CreateFrame("Frame", nil, parent)
holder:SetSize(COL_W, 30) -- room above the text: sections stand apart
local fs = holder:CreateFontString(nil, "OVERLAY", "GameFontNormal")
fs:SetPoint("BOTTOMLEFT", 0, 5)
fs:SetText(text)
local line = holder:CreateTexture(nil, "ARTWORK")
line:SetColorTexture(1, 0.82, 0, 0.4)
line:SetHeight(1)
line:SetPoint("BOTTOMLEFT")
line:SetPoint("BOTTOMRIGHT")
holder.Refresh = function() end
holder.layoutHeight = 34
return Register(holder)
end
-- Multi-line help text (default width: one column)
local function CreateNote(parent, text, width)
local holder = CreateFrame("Frame", nil, parent)
local fs = holder:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
fs:SetPoint("TOPLEFT")
fs:SetWidth(width or COL_W)
fs:SetJustifyH("LEFT")
fs:SetText(text)
holder.fs = fs
holder:SetSize(width or COL_W, math.max(14, fs:GetStringHeight()))
holder.Refresh = function() end
holder.layoutHeight = holder:GetHeight() + 8
return Register(holder)
end
-- Checkbox bound to a getter/setter pair
local function CreateCheck(parent, label, getter, setter, isShown, tip)
local cb = CreateFrame("CheckButton", nil, parent, "UICheckButtonTemplate")
cb:SetSize(24, 24)
local fs = cb.text or cb.Text
if not fs then
fs = cb:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
fs:SetPoint("LEFT", cb, "RIGHT", 2, 0)
end
fs:SetText(label)
cb:SetScript("OnClick", function(self) setter(self:GetChecked() and true or false) end)
AttachTip(cb, label, tip)
cb.Refresh = function(self) self:SetChecked(getter() and true or false) end
cb.layoutHeight = 26
return Register(cb, isShown)
end
-- Slider with stepper arrows; values are rounded to integers
local function CreateSlider(parent, label, minV, maxV, step, getter, setter, isShown, tip)
local holder = CreateFrame("Frame", nil, parent)
holder:SetSize(COL_W, 40)
local title = AddTitle(holder, label)
local slider = CreateFrame("Frame", nil, holder, "MinimalSliderWithSteppersTemplate")
slider:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -4)
slider:SetWidth(COL_W - 14)
slider:Init(getter() or minV, minV, maxV, (maxV - minV) / step, {
[MinimalSliderWithSteppersMixin.Label.Right] = function(v)
return tostring(math.floor(v + 0.5))
end,
})
slider:RegisterCallback(MinimalSliderWithSteppersMixin.Event.OnValueChanged, function(_, value)
if holder.updating then return end
setter(math.floor(value + 0.5))
end, holder)
if tip then
holder:EnableMouse(true)
AttachTip(holder, label, tip)
AttachTip(slider.Slider, label, tip)
end
holder.Refresh = function(self)
self.updating = true
slider:SetValue(getter() or minV)
self.updating = false
end
holder.layoutHeight = 46
return Register(holder, isShown)
end
-- Single-choice dropdown; options = list of {key, name} or a function returning it
local function CreateDropdown(parent, label, options, getter, setter, isShown, width, tip)
local holder = CreateFrame("Frame", nil, parent)
holder:SetSize(width or COL_W, 44)
local title = AddTitle(holder, label)
local dd = CreateFrame("DropdownButton", nil, holder, "WowStyle1DropdownTemplate")
dd:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -4)
dd:SetWidth((width or COL_W) - 12)
dd:SetupMenu(function(_, root)
-- an error here would stop the menu from opening: print it instead of swallowing it
local ok, err = pcall(function()
if root.SetScrollMode then root:SetScrollMode(400) end -- long lists
local list = type(options) == "function" and options() or options
for _, opt in ipairs(list) do
if opt.isTitle then
root:CreateTitle(opt.name)
else
root:CreateRadio(opt.name,
function(value) return getter() == value end,
function(value) setter(value) end,
opt.key)
end
end
end)
if not ok then ns.Print("|cffff5555[debug]|r menu « " .. label .. " » : " .. tostring(err)) end
end)
if tip then
holder:EnableMouse(true)
AttachTip(holder, label, tip)
AttachTip(dd, label, tip)
end
holder.Refresh = function() dd:GenerateMenu() end
holder.layoutHeight = 50
return Register(holder, isShown)
end
-- Action dropdown (each entry is a button)
local function CreateActionDropdown(parent, label, buildEntries, emptyText, tip)
local holder = CreateFrame("Frame", nil, parent)
holder:SetSize(COL_W, 44)
local title = AddTitle(holder, label)
local dd = CreateFrame("DropdownButton", nil, holder, "WowStyle1DropdownTemplate")
dd:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -4)
dd:SetWidth(COL_W - 12)
if dd.SetDefaultText then dd:SetDefaultText(L.CHOOSE) end
dd:SetupMenu(function(_, root)
if root.SetScrollMode then root:SetScrollMode(400) end
local entries = buildEntries()
if #entries == 0 then
root:CreateTitle(emptyText)
return
end
for _, e in ipairs(entries) do
root:CreateButton(e.name, e.func)
end
end)
if tip then
holder:EnableMouse(true)
AttachTip(holder, label, tip)
AttachTip(dd, label, tip)
end
holder.Refresh = function() dd:GenerateMenu() end
holder.layoutHeight = 50
return Register(holder)
end
-- Color swatch opening Blizzard's color picker (Cancel restores the previous color)
local function CreateColor(parent, label, hasAlpha, getter, setter, isShown, tip)
local b = CreateFrame("Button", nil, parent)
b:SetSize(COL_W, 22)
local edge = b:CreateTexture(nil, "BACKGROUND")
edge:SetSize(20, 20)
edge:SetPoint("LEFT")
edge:SetColorTexture(1, 1, 1, 1)
local swatch = b:CreateTexture(nil, "ARTWORK")
swatch:SetSize(16, 16)
swatch:SetPoint("CENTER", edge)
local fs = b:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
fs:SetPoint("LEFT", edge, "RIGHT", 6, 0)
fs:SetText(label)
b.Refresh = function()
local c = getter()
if c then swatch:SetColorTexture(c.r, c.g, c.b, c.a) end
end
local function Set(r, g, bl, a)
setter(r, g, bl, a)
b:Refresh()
end
b:SetScript("OnClick", function()
local c = getter()
if not c then return end
local prev = { r = c.r, g = c.g, b = c.b, a = c.a }
local function OnChange()
local r, g, bl = ColorPickerFrame:GetColorRGB()
local a = hasAlpha and ColorPickerFrame:GetColorAlpha() or 1
Set(r, g, bl, a)
end
ColorPickerFrame:SetupColorPickerAndShow({
r = c.r, g = c.g, b = c.b,
opacity = c.a,
hasOpacity = hasAlpha,
swatchFunc = OnChange,
opacityFunc = OnChange,
cancelFunc = function() Set(prev.r, prev.g, prev.b, prev.a) end,
})
end)
AttachTip(b, label, tip)
b.layoutHeight = 28
return Register(b, isShown)
end
-- Plain push button
-- Text box (free text, or numbers only); the value is saved on Enter or when
-- the box loses focus, Escape restores the saved value
local function CreateEditBox(parent, label, getter, setter, isShown, tip, numeric)
local holder = CreateFrame("Frame", nil, parent)
holder:SetSize(COL_W, 44)
local title = AddTitle(holder, label)
local box = CreateFrame("EditBox", nil, holder, "InputBoxTemplate")
box:SetSize(COL_W - 20, 20)
box:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 6, -4)
box:SetAutoFocus(false)
if numeric then box:SetNumeric(true) end
local cancelling = false
local function Commit(self)
if cancelling then return end
setter(self:GetText())
end
box:SetScript("OnEnterPressed", function(self)
Commit(self)
self:ClearFocus()
end)
box:SetScript("OnEditFocusLost", Commit)
box:SetScript("OnEscapePressed", function(self)
cancelling = true -- losing focus must not save what was typed
self:ClearFocus()
cancelling = false
holder:Refresh()
end)
holder.Refresh = function()
if not box:HasFocus() then
local v = getter()
box:SetText(v ~= nil and tostring(v) or "")
end
end
if tip then
holder:EnableMouse(true)
AttachTip(holder, label, tip)
AttachTip(box, label, tip)
end
holder.box = box
holder.layoutHeight = 46
return Register(holder, isShown)
end
-- A button text longer than the button (translations can be long): smaller font,
-- then cut with "..." (the tooltip keeps the full text)
local function FitButtonText(b)
local fs = b.GetFontString and b:GetFontString()
local w = b:GetWidth()
if not (fs and w and w > 0) then return end
if fs:GetStringWidth() > w - 14 then
b:SetNormalFontObject("GameFontNormalSmall")
b:SetHighlightFontObject("GameFontHighlightSmall")
b:SetDisabledFontObject("GameFontDisableSmall")
fs = b:GetFontString() or fs
fs:SetWidth(w - 10)
if fs.SetWordWrap then fs:SetWordWrap(false) end
end
end
ns.FitButtonText = FitButtonText
local function CreateButton(parent, text, onClick, isShown, width, tip)
local b = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
b:SetSize(width or COL_W, 24)
b:SetText(text)
FitButtonText(b)
b:SetScript("OnClick", onClick)
AttachTip(b, text, tip)
b.Refresh = function() end
b.layoutHeight = 30
return Register(b, isShown)
end
-- getter/setter pair for a block field of the current selection
local function Field(name)
return function()
local cfg = Cfg()
return cfg and cfg[name]
end,
function(v) SetField(name, v) end
end
-- Same as Field, for color tables {r, g, b, a}
local function ColorField(name)
return function()
local cfg = Cfg()
return cfg and cfg[name]
end,
function(r, g, b, a) SetField(name, { r = r, g = g, b = b, a = a }) end
end
---------------------------------------------------------------------------
-- Column layout
---------------------------------------------------------------------------
-- Returns a layout helper for a page: Place(w, col) stacks a widget in column 1 or 2,
-- Gap(col, h) adds (or, if negative, removes) vertical space, Bottom(col) gives the current y.
local function NewLayout(page, top)
local y = { top or -6, top or -6 }
local layout = {}
function layout.Place(w, col)
w:SetPoint("TOPLEFT", page, "TOPLEFT", COL_X[col], y[col])
y[col] = y[col] - w.layoutHeight - SPACING
-- lowest point reached by any page (checked by tools/mock_run.lua: must fit the window)
if not layout.untracked then ns.optionsLowestY = math.min(ns.optionsLowestY or 0, (page.layoutBase or 0) + y[col] + SPACING) end
return w
end
function layout.Gap(col, h) y[col] = y[col] - h end
function layout.Bottom(col) return y[col] end
return layout
end
---------------------------------------------------------------------------
-- "Blocks" tab
---------------------------------------------------------------------------
-- Entries of the selection dropdown: groups first, then data blocks, then launchers
-- Sort key for names: lower case without accents, so "Écran" sorts with the E's
local SortKey = ns.SortKey
local function SelectionOptions()
local list = { { key = ALL, name = L.ALL_BLOCKS } }
if #LauncherKeys() > 0 then
table.insert(list, { key = LAUNCHERS, name = L.ALL_LAUNCHERS })
end
table.insert(list, { isTitle = true, name = L.DATA_BLOCKS })
-- data blocks in alphabetical order of their (translated) name, whatever the
-- order they were registered in; then launchers and spacers, as GetBlockKeys sorts them
local keys, data = {}, {}
for _, key in ipairs(ns:GetBlockKeys()) do
if ns.IsForwardingKey(key) or ns.IsSpacerKey(key) then
table.insert(keys, key)
else
table.insert(data, key)
end
end
table.sort(data, function(a, b) return SortKey(ns:GetBlockName(a)) < SortKey(ns:GetBlockName(b)) end)
for i = #data, 1, -1 do table.insert(keys, 1, data[i]) end
local launchersTitle, brokersTitle, spacersTitle = false, false, false
for _, key in ipairs(keys) do
if ns.IsLauncherKey(key) and not launchersTitle then
launchersTitle = true
table.insert(list, { isTitle = true, name = L.LAUNCHERS })
end
if ns.IsBrokerKey(key) and not brokersTitle then
brokersTitle = true
table.insert(list, { isTitle = true, name = L.BROKERS })
end
if ns.IsSpacerKey(key) and not spacersTitle then
spacersTitle = true
table.insert(list, { isTitle = true, name = L.SPACERS })
end
-- shown blocks are easy to spot: a green dot; the others are greyed
local cfg = ns.db.blocks[key]
local name = ns:GetBlockName(key)
if cfg and cfg.enabled then
name = "|TInterface\\COMMON\\Indicator-Green:14:14|t " .. name
else
name = "|cff8c8c8c" .. name .. "|r"
end
table.insert(list, { key = key, name = name })
end
return list
end
-- Module key of the selection ("spacer" for every spacer), nil for a group, a
-- launcher or a DataBroker block (they have no module options)
local function SelectedModuleKey()
if IsGroup() then return nil end
if ns.IsSpacerKey(selected) then return "spacer" end
if ns.IsForwardingKey(selected) then return nil end
return selected
end
-- Functions that pack the module option stacks (see BuildModuleOptions)
local relayouts = {}
function ns.RelayoutBlockOptions()
for _, f in ipairs(relayouts) do f() end
end
-- Builds every module's own options (Modules.lua "options", hide when empty,
-- thresholds) in column col of a page, all stacked from the same top: only the
-- selected block's are visible. Used by the Behaviour sub-tab and the DataBroker
-- tab. condition (optional) must also be true for them to show. Returns the set of
-- module keys that have options.
-- filter(opt) picks the options of this page: by default those of the Behaviour tab
-- (options marked tab = "text", how the value is shown, are in the Text tab)
local function BuildModuleOptions(sub, P, col, keys, condition, filter)
filter = filter or function(opt) return opt.tab ~= "text" end
local function ModuleOf(key)
return key == "spacer" and ns.spacerModule or ns.modules[key]
end
local stackTop = P.Bottom(col)
local stacks = {}
local hasOptions = {}
for _, key in ipairs(keys) do
local mod = ModuleOf(key)
local list = {}
for _, opt in ipairs(mod.options or {}) do
if filter(opt) then table.insert(list, opt) end
end
if mod.isEmpty and filter({}) then
table.insert(list, { kind = "check", field = "hideEmpty", label = "HIDE_EMPTY", tip = "TIP_HIDE_EMPTY",
after = function() ns:UpdateAllVisibility() end })
end
if mod.thresholds and filter({}) then
local t = mod.thresholds
local tip = t.low and "TIP_THRESHOLD_LOW" or "TIP_THRESHOLD_HIGH"
table.insert(list, { kind = "slider", field = "warnAt", label = "THRESHOLD_WARN", tip = tip,
min = t.min, max = t.max, step = t.step, default = t.warn })
table.insert(list, { kind = "slider", field = "badAt", label = "THRESHOLD_BAD", tip = tip,
min = t.min, max = t.max, step = t.step, default = t.bad })
end
if #list > 0 then hasOptions[key] = true end
local firstWidget = #widgets + 1
P.untracked = true -- stacked widgets: measured when packed (relayout below)
for _, opt in ipairs(list) do
local shown = function()
if SelectedModuleKey() ~= key then return false end
if condition and not condition() then return false end
local cfg = Cfg()
return not opt.shown or (cfg and opt.shown(cfg)) or false
end
local get, set = Field(opt.field)
local label, tip = L[opt.label] or opt.label, L[opt.tip]
local setter = function(v)
set(v)
if opt.after then opt.after() end
if ns.UpdatePublishing then ns:UpdatePublishing() end -- shared value follows
ns:RefreshOptions() -- an option can show or hide others (spacer mode)
end
if opt.kind == "check" then
P.Place(CreateCheck(sub, label, get, setter, shown, tip), col)
elseif opt.kind == "dropdown" then
local values = {}
for _, v in ipairs(opt.values) do table.insert(values, { key = v.key, name = L[v.label] or v.label }) end
P.Place(CreateDropdown(sub, label, values, get, setter, shown, nil, tip), col)
elseif opt.kind == "slider" then
P.Place(CreateSlider(sub, label, opt.min, opt.max, opt.step or 1,
function() return get() or opt.default end, setter, shown, tip), col)
elseif opt.kind == "number" or opt.kind == "text" then
P.Place(CreateEditBox(sub, label, get, function(v)
if opt.kind == "number" then
v = tonumber(v)
if not v then return end
if opt.min then v = math.max(opt.min, v) end
end
setter(v)
end, shown, tip, opt.kind == "number"), col)
elseif opt.kind == "color" then
local getC, setC = ColorField(opt.field)
P.Place(CreateColor(sub, label, true, getC, function(...)
setC(...)
ns:ApplyStyle(selected)
end, shown, tip), col)
end
end
-- remember this module's widgets: hidden ones must not leave a gap
local stack = {}
for w = firstWidget, #widgets do table.insert(stack, widgets[w]) end
table.insert(stacks, stack)
P.Gap(col, P.Bottom(col) - stackTop) -- back to the top for the next module
P.untracked = nil
end
-- packs the visible widgets of each stack from the top (called after every refresh)
table.insert(relayouts, function()
for _, stack in ipairs(stacks) do
local y = stackTop
for _, w in ipairs(stack) do
if w:IsShown() then
w:ClearAllPoints()
w:SetPoint("TOPLEFT", sub, "TOPLEFT", COL_X[col], y)
y = y - w.layoutHeight - SPACING
ns.optionsLowestY = math.min(ns.optionsLowestY or 0, (sub.layoutBase or 0) + y + SPACING)
end
end
end
end)
return hasOptions
end
local blockSub = 1 -- open sub-tab: 1 Display, 2 Text, 3 Frame
-- Builds the "Blocks" tab: selection dropdown, three sub-tabs and single-block actions
local function BuildBlockPage(page)
local single = function() return not IsGroup() end
local isLauncher = function() return ns.IsLauncherKey(selected) end
-- shortcuts: a widget bound to a block field, with its help tooltip
local function Check(parent, label, field, tip)
local get, set = Field(field)
return CreateCheck(parent, label, get, set, nil, tip)
end
local function Slider(parent, label, minV, maxV, field, tip, isShown)
local get, set = Field(field)
return CreateSlider(parent, label, minV, maxV, 1, get, set, isShown, tip)
end
local function Dropdown(parent, label, options, field, tip)
local get, set = Field(field)
return CreateDropdown(parent, label, options, get, set, nil, nil, tip)
end
local function Color(parent, label, hasAlpha, field, tip)
local get, set = ColorField(field)
return CreateColor(parent, label, hasAlpha, get, set, nil, tip)
end
-- Block selection, full width
local top = NewLayout(page)
local ShowSub -- defined below (sub-tabs); a spacer's settings are in the Behaviour tab
local REPLACE_W = 140
local selection = CreateDropdown(page, L.SELECTION, SelectionOptions,
function() return selected end,
function(v)
selected = v
if ns.IsSpacerKey(v) and ShowSub then ShowSub(4) end
ns:RefreshOptions()
end, nil, FULL_W - REPLACE_W, L.TIP_SELECTION)
top.Place(selection, 1)
-- "Replace with...": another block takes the selected block's place
local replace = CreateButton(page, L.REPLACE_BLOCK, function(self)
if not (MenuUtil and MenuUtil.CreateContextMenu) then return end
ns:OpenReplaceMenu(selected, self)
end, function() return single() and ns.db.blocks[selected] ~= nil end, REPLACE_W - 8, L.TIP_REPLACE_BLOCK)
replace:SetPoint("BOTTOMLEFT", selection, "BOTTOMRIGHT", 0, 1)
top.Place(CreateNote(page, L.GROUP_HINT, FULL_W), 1)
-- Sub-tabs
local subY = top.Bottom(1) - 4
local subs, buttons = {}, {}
ShowSub = function(index)
blockSub = index
for i, sub in ipairs(subs) do
sub:SetShown(i == index)
buttons[i]:SetEnabled(i ~= index)
end
end
local subNames = { L.SUB_DISPLAY, L.SUB_TEXT, L.SUB_FRAME, L.SUB_BEHAVIOUR }
local subW = (FULL_W - 6 * (#subNames - 1)) / #subNames
for i, text in ipairs(subNames) do
local b = CreateFrame("Button", nil, page, "UIPanelButtonTemplate")
b:SetSize(subW, 22)
b:SetPoint("TOPLEFT", page, "TOPLEFT", COL_X[1] + (i - 1) * (subW + 6), subY)
b:SetText(text)
b:SetScript("OnClick", function() ShowSub(i) end)
buttons[i] = b
local sub = CreateFrame("Frame", nil, page)
sub:SetPoint("TOPLEFT", page, "TOPLEFT", 0, subY - 36)
sub.layoutBase = subY - 36
sub:SetPoint("BOTTOMRIGHT", page, "BOTTOMRIGHT", 0, 56)
subs[i] = sub
end
local line = page:CreateTexture(nil, "ARTWORK")
line:SetColorTexture(1, 0.82, 0, 0.25)
line:SetHeight(1)
line:SetPoint("TOPLEFT", page, "TOPLEFT", COL_X[1], subY - 28)
line:SetPoint("TOPRIGHT", page, "TOPLEFT", COL_X[1] + FULL_W, subY - 28)
-- 1. Display: what is shown, and the size
do
local sub = subs[1]
local P = NewLayout(sub)
P.Place(CreateHeader(sub, L.SECTION_DISPLAY), 1)
local spacerNote = P.Place(CreateNote(sub, L.SPACER_SETTINGS_HINT), 1)
spacerNote.isShown = function() return ns.IsSpacerKey(selected) end
P.Place(Check(sub, L.ENABLED, "enabled", L.TIP_ENABLED), 1)
P.Place(Check(sub, L.SHOW_LABEL, "showLabel", L.TIP_SHOW_LABEL), 1)
P.Place(Check(sub, L.SHOW_ICON, "showIcon", L.TIP_SHOW_ICON), 1)
P.Place(Check(sub, L.SHOW_TOOLTIP, "tooltip", L.TIP_SHOW_TOOLTIP), 1)
P.Place(Check(sub, L.COLORIZE, "colorize", L.TIP_COLORIZE), 1)
-- progress bar: only for blocks that have a progress (or a group selection)
local getP, setP = Field("progressBar")
P.Place(CreateCheck(sub, L.PROGRESS_BAR, getP, setP, function()
if IsGroup() then return true end
local mod = ns:GetModule(selected)
return mod and mod.progress ~= nil or false
end, L.TIP_PROGRESS_BAR), 1)
P.Gap(1, 6)
local getA, setA = Field("alpha")
P.Place(CreateSlider(sub, L.OPACITY, 10, 100, 5,
function() return math.floor((getA() or 1) * 100 + 0.5) end,
function(v) setA(v / 100) end, nil, L.TIP_OPACITY), 1)
P.Place(Dropdown(sub, L.STRATA, {
{ key = "BACKGROUND", name = L.STRATA_BACKGROUND },
{ key = "LOW", name = L.STRATA_LOW },
{ key = "MEDIUM", name = L.STRATA_MEDIUM },
{ key = "HIGH", name = L.STRATA_HIGH },
}, "strata", L.TIP_STRATA), 1)
P.Place(CreateHeader(sub, L.SECTION_SIZE), 2)
P.Place(Check(sub, L.AUTO_WIDTH, "autoWidth", L.AUTO_WIDTH_TIP), 2)
local sizeNote = P.Place(CreateNote(sub, L.SIZE_GROUP_HINT), 2)
sizeNote.isShown = IsGroup
-- a group selection shows the note in place of the sliders (same slot)
P.Gap(2, -(sizeNote.layoutHeight + SPACING))
P.Place(Slider(sub, L.WIDTH, 16, 600, "width", L.TIP_WIDTH, single), 2)
P.Place(Slider(sub, L.HEIGHT, 10, 100, "height", L.TIP_HEIGHT, single), 2)
-- scale of the whole merged group the block belongs to (stored on its root)
P.Place(CreateSlider(sub, L.GROUP_SCALE, 50, 300, 5,
function()
local root = not IsGroup() and ns.db.blocks[ns:GetRoot(selected)]
return math.floor(((root and root.scale) or 1) * 100 + 0.5)
end,
function(v) if not IsGroup() then ns:SetGroupScale(selected, v / 100) end end,
single, L.TIP_GROUP_SCALE), 2)
end
-- 2. Text
do
local sub = subs[2]
local P = NewLayout(sub)
P.Place(CreateHeader(sub, L.SECTION_TEXT), 1)
P.Place(Dropdown(sub, L.FONT, function() return ns:GetMediaList("font") end, "font", L.TIP_FONT), 1)
P.Place(Slider(sub, L.FONT_SIZE, 6, 32, "fontSize", L.TIP_FONT_SIZE), 1)
P.Place(Dropdown(sub, L.OUTLINE, {
{ key = "NONE", name = L.OUTLINE_NONE },
{ key = "OUTLINE", name = L.OUTLINE_THIN },
{ key = "THICKOUTLINE", name = L.OUTLINE_THICK },
}, "outline", L.TIP_OUTLINE), 1)
P.Place(CreateHeader(sub, L.SECTION_LAYOUT_TEXT), 2)
P.Place(Dropdown(sub, L.JUSTIFY, {
{ key = "LEFT", name = L.LEFT },
{ key = "CENTER", name = L.CENTER },
{ key = "RIGHT", name = L.RIGHT },
}, "justify", L.TIP_JUSTIFY), 2)
P.Place(Color(sub, L.TEXT_COLOR, false, "textColor", L.TIP_TEXT_COLOR), 2)
P.Place(Check(sub, L.CLASS_COLOR_TEXT, "classColorText", L.TIP_CLASS_COLOR_TEXT), 2)
-- how the selected block shows its value (gold format, clock, decimals...)
P.Gap(1, 6)
local header = P.Place(CreateHeader(sub, L.SECTION_VALUE), 1)
local textOptions = BuildModuleOptions(sub, P, 1, ns.order, nil, function(opt) return opt.tab == "text" end)
header.isShown = function()
local key = SelectedModuleKey()
return key ~= nil and textOptions[key] == true
end
end
-- 3. Frame: background on the left, border on the right
do
local sub = subs[3]
local P = NewLayout(sub)
P.Place(CreateHeader(sub, L.BACKGROUND), 1)
P.Place(Dropdown(sub, L.BACKGROUND, function() return ns:GetMediaList("background") end, "bg", L.TIP_BACKGROUND), 1)
P.Place(Color(sub, L.BG_COLOR, true, "bgColor", L.TIP_BG_COLOR), 1)
P.Place(Check(sub, L.CLASS_COLOR_BG, "classColorBg", L.TIP_CLASS_COLOR_BG), 1)
-- the icon: same size, frame and colors for every block (e.g. with "All blocks")
P.Gap(1, 8)
P.Place(CreateHeader(sub, L.SECTION_ICON), 1)
P.Place(Slider(sub, L.ICON_SIZE, 0, 40, "iconSize", L.TIP_ICON_SIZE), 1)
P.Place(Dropdown(sub, L.ICON_SHAPE, {
{ key = "none", name = L.ICON_SHAPE_NONE },
{ key = "square", name = L.ICON_SHAPE_SQUARE },
{ key = "round", name = L.ICON_SHAPE_ROUND },
}, "iconShape", L.TIP_ICON_SHAPE), 1)
P.Place(Color(sub, L.ICON_BORDER_COLOR, true, "iconBorderColor", L.TIP_ICON_BORDER_COLOR), 1)
P.Place(Check(sub, L.CLASS_COLOR_ICON, "classColorIcon", L.TIP_CLASS_COLOR_ICON), 1)
P.Place(Check(sub, L.ICON_DESATURATE, "iconDesaturate", L.TIP_ICON_DESATURATE), 1)
P.Place(CreateHeader(sub, L.BORDER), 2)
-- picking a border also restores its original thickness
P.Place(CreateDropdown(sub, L.BORDER, function() return ns:GetMediaList("border") end,
function()
local cfg = Cfg()
return cfg and cfg.border
end,
function(v)
SetField("border", v)
local media = ns:FindMedia("border", v)
local size = media and media.size
if size then SetField("borderSize", size) end
ns:RefreshOptions()
end, nil, nil, L.TIP_BORDER), 2)
P.Place(Slider(sub, L.BORDER_SIZE, 1, 32, "borderSize", L.TIP_BORDER_SIZE), 2)
-- Corner join: belongs to the border (not the block), with a reset to the original value
local function CurrentBorder()
local cfg = Cfg()
return cfg and cfg.border
end
local function HasCorners()
local b = CurrentBorder()
return b and b ~= "none" and b ~= "solid"