Repository navigation
Expand file tree
/
Copy pathOptions.lua
More file actions
2226 lines (2149 loc) · 134 KB
/
Copy pathOptions.lua
File metadata and controls
2226 lines (2149 loc) · 134 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
-- Webby Frames: the settings window (/webby): a window of its own, with the pages on the left (General,
-- All frames, one per frame, the modules' own) and the page's options on the right in collapsible
-- sections. Being an ordinary frame, it can stay open while you play, target things or use Edit Mode.
-- Esc > Options > AddOns > Webby Frames only describes the addon and has a button that opens this
-- window. Loads after WebbyFrames.lua (see the .toc), which shares what it needs through ns; the main
-- file builds it at login (Opt.build).
local _, ns = ...
local Opt = ns.Opt
Opt.sections = {} -- (every collapsible section on every page: opened as you left them, Opt.state)
local UNITS, UNIT_LABEL, frames, Party, Extras = ns.UNITS, ns.UNIT_LABEL, ns.frames, ns.Party, ns.Extras
local getSetting, setSetting, defaultFor, frameOn, plain = ns.getSetting, ns.setSetting, ns.defaultFor, ns.frameOn, ns.plain
local DEFAULT_SETTINGS, ALWAYS_SHOWN, OWN_CLASS, COLOR_MODES = ns.DEFAULT_SETTINGS, ns.ALWAYS_SHOWN, ns.OWN_CLASS, ns.COLOR_MODES
local hasCastAndThreat, textureChoices, fontChoices, Profiles = ns.hasCastAndThreat, ns.textureChoices, ns.fontChoices, ns.Profiles
local defaultTip, moveTipToDefault, addonVersion = ns.defaultTip, ns.moveTipToDefault, ns.addonVersion
-- (the actions: assigned late in the main file, so read when used)
local function resetFrame(...) return ns.resetFrame(...) end
local function copySettings(...) return ns.copySettings(...) end
local function openSettings(...) return ns.openSettings(...) end
local function openEditMode(...) return ns.openEditMode(...) end
function Opt.tip(frame, title, tip)
if not tip then return end
frame:SetScript("OnEnter", function(self)
defaultTip(self)
GameTooltip:SetText(title or "", 1, 1, 1)
GameTooltip:AddLine(tip, 1, 0.82, 0, true)
GameTooltip:Show()
end)
frame:SetScript("OnLeave", function() GameTooltip:Hide() end)
end
---------------------------------------------------------------------------
-- The look: flat dark panels, one accent colour, thin lines. Everything is drawn from plain colour
-- textures (no art files), so the whole window recolours from THEME. Change THEME.accent to retint it.
---------------------------------------------------------------------------
local WHITE = "Interface\\Buttons\\WHITE8X8"
local THEME = {
accent = { 1, 0.72, 0.18 }, -- amber: lit toggles, slider fills, the page you're on
bg = { 0.035, 0.04, 0.05 }, -- the window
panel = { 0.06, 0.068, 0.085 }, -- the page list and the page
raised = { 0.1, 0.11, 0.135 }, -- fields, buttons, section cards
line = { 1, 1, 1, 0.08 }, -- hairlines
text = { 0.93, 0.94, 0.96 },
dim = { 0.56, 0.59, 0.65 },
}
Opt.THEME, Opt.WHITE = THEME, WHITE
Opt.LOGO = "Interface\\AddOns\\WebbyFrames\\Media\\logo" -- (the web emblem; also the minimap button's and the AddOns list's icon)
local A = THEME.accent
-- Font objects by size (the game's own font, so every language still has its letters).
local fontObjects = {}
function Opt.fontObject(size)
local name = "WebbyFramesOptFont" .. size
if not fontObjects[size] then
local f = CreateFont(name)
local path = GameFontNormal and select(1, GameFontNormal:GetFont()) or "Fonts\\FRIZQT__.TTF"
f:SetFont(path, size, "")
f:SetShadowOffset(1, -1)
f:SetShadowColor(0, 0, 0, 0.85)
f:SetTextColor(THEME.text[1], THEME.text[2], THEME.text[3])
fontObjects[size] = f
end
return name
end
function Opt.text(parent, size, color, layer)
local fs = parent:CreateFontString(nil, layer or "OVERLAY", Opt.fontObject(size or 12))
local c = color or THEME.text
fs:SetTextColor(c[1], c[2], c[3], c[4] or 1)
return fs
end
function Opt.rect(parent, layer, c, a, sub)
local t = parent:CreateTexture(nil, layer or "BACKGROUND", nil, sub)
t:SetColorTexture(c[1], c[2], c[3], a or c[4] or 1)
return t
end
-- A colour fading across a texture (left to right: "HORIZONTAL"; bottom to top: "VERTICAL").
function Opt.gradient(tex, orient, c1, a1, c2, a2)
tex:SetTexture(WHITE)
local ok = CreateColor and pcall(tex.SetGradient, tex, orient, CreateColor(c1[1], c1[2], c1[3], a1), CreateColor(c2[1], c2[2], c2[3], a2))
if not ok then pcall(tex.SetGradientAlpha, tex, orient, c1[1], c1[2], c1[3], a1, c2[1], c2[2], c2[3], a2) end
end
-- A one-pixel outline on a draw layer ("HIGHLIGHT": only while the mouse is over a button).
function Opt.outline(frame, layer, c, a)
local lines = {}
for i, p in ipairs({ { "TOPLEFT", "TOPRIGHT" }, { "BOTTOMLEFT", "BOTTOMRIGHT" }, { "TOPLEFT", "BOTTOMLEFT" }, { "TOPRIGHT", "BOTTOMRIGHT" } }) do
local t = Opt.rect(frame, layer or "BORDER", c, a)
t:SetPoint(p[1]); t:SetPoint(p[2])
if i <= 2 then t:SetHeight(1) else t:SetWidth(1) end
lines[i] = t
end
return lines
end
-- A flat panel with a hairline edge. (edge: how bright the edge is, 0-1; the old grey value)
function Opt.dark(frame, edge, shade)
local c = shade or THEME.raised
frame:SetBackdrop({ bgFile = WHITE, edgeFile = WHITE, edgeSize = 1 })
frame:SetBackdropColor(c[1], c[2], c[3], 0.96)
frame:SetBackdropBorderColor(1, 1, 1, 0.05 + (edge or 0.4) * 0.2)
end
-- A clickable field (dropdowns, key buttons): a panel that lights its edge and brightens on hover.
function Opt.styleField(b)
Opt.dark(b, 0.4)
local hl = Opt.rect(b, "HIGHLIGHT", { 1, 1, 1 }, 0.04)
hl:SetAllPoints()
Opt.outline(b, "HIGHLIGHT", A, 0.8)
end
-- A small down-pointing chevron, tinted (rotate it with SetRotation to point elsewhere).
function Opt.chevron(parent, size)
local t = parent:CreateTexture(nil, "ARTWORK")
t:SetSize(size or 14, size or 14)
t:SetTexture("Interface\\Buttons\\Arrow-Down-Up")
pcall(t.SetDesaturated, t, true)
t:SetVertexColor(THEME.dim[1], THEME.dim[2], THEME.dim[3])
return t
end
-- A flat button: a raised panel, its text in capitals, an accent edge and lift on hover (and while
-- LockHighlight'ed, for a tab that's picked). b:SetText / b:GetFontString work as usual.
function Opt.newButton(parent, w, h, text)
local b = CreateFrame("Button", nil, parent, "BackdropTemplate")
b:SetSize(w or 160, h or 26)
Opt.dark(b, 0.5)
local fs = Opt.text(b, 11)
fs:SetPoint("LEFT", 8, 0)
fs:SetPoint("RIGHT", -8, 0)
pcall(fs.SetWordWrap, fs, false)
b:SetFontString(fs)
local hl = Opt.rect(b, "HIGHLIGHT", A, 0.12)
hl:SetAllPoints()
Opt.outline(b, "HIGHLIGHT", A, 0.9)
local bar = Opt.rect(b, "HIGHLIGHT", A, 1)
bar:SetPoint("BOTTOMLEFT"); bar:SetPoint("BOTTOMRIGHT"); bar:SetHeight(2)
b:SetScript("OnMouseDown", function(self) if self:IsEnabled() then fs:SetPoint("LEFT", 9, -1) end end)
b:SetScript("OnMouseUp", function() fs:SetPoint("LEFT", 8, 0) end)
b:SetMotionScriptsWhileDisabled(true)
if text then b:SetText(text:upper()) end
return b
end
-- A flat X (the window's close button, a row's remove button).
function Opt.newX(parent, size)
local b = CreateFrame("Button", nil, parent)
b:SetSize(size or 26, size or 26)
local hl = Opt.rect(b, "HIGHLIGHT", { 0.9, 0.25, 0.2 }, 0.35)
hl:SetAllPoints()
for _, deg in ipairs({ 45, -45 }) do
local t = Opt.rect(b, "ARTWORK", THEME.text, 0.85)
t:SetSize((size or 26) * 0.55, 2)
t:SetPoint("CENTER")
t:SetRotation(math.rad(deg))
end
return b
end
-- A toggle switch (a CheckButton, so GetChecked / SetChecked / OnClick work as for a check box): a dark
-- track with the knob on the left, or an accent track with the knob on the right when on.
function Opt.newCheck(parent)
local cb = CreateFrame("CheckButton", nil, parent)
cb:SetSize(36, 18)
local track = Opt.rect(cb, "BACKGROUND", THEME.raised, 1)
track:SetAllPoints()
Opt.outline(cb, "BORDER", { 1, 1, 1 }, 0.12)
local lit = Opt.rect(cb, "ARTWORK", A, 0.9)
lit:SetAllPoints()
cb:SetCheckedTexture(lit)
local hl = Opt.rect(cb, "HIGHLIGHT", { 1, 1, 1 }, 0.08)
hl:SetAllPoints()
-- (the knob on a frame of its own above the switch, so the game's drawing of the checked fill can
-- never cover it)
local top = CreateFrame("Frame", nil, cb)
top:SetAllPoints()
top:SetFrameLevel(cb:GetFrameLevel() + 2)
local shadow = Opt.rect(top, "ARTWORK", { 0, 0, 0 }, 0.45)
shadow:SetSize(14, 14)
local knob = Opt.rect(top, "OVERLAY", THEME.text, 1)
knob:SetSize(12, 12)
local function place()
local on = cb:GetChecked() and true or false
knob:ClearAllPoints()
knob:SetPoint(on and "RIGHT" or "LEFT", on and -3 or 3, 0)
shadow:ClearAllPoints()
shadow:SetPoint("CENTER", knob, "CENTER", 0, -1)
-- on: a light knob at the right of the amber track; off: a grey one at the left of the dark track
if on then knob:SetColorTexture(1, 1, 1, 1) else knob:SetColorTexture(THEME.dim[1], THEME.dim[2], THEME.dim[3], 1) end
end
local setChecked = cb.SetChecked
function cb:SetChecked(v) setChecked(self, v); place() end
cb:SetScript("PostClick", place) -- (the builders set OnClick; this runs after it)
place()
return cb
end
-- The open dropdown's list (one, shared by every dropdown). Long lists scroll with the mouse wheel;
-- a click anywhere else closes it.
function Opt.closeMenu() if Opt.menu then Opt.menu:Hide() end end
function Opt.openMenu(owner, choices, current, pick)
local m = Opt.menu
if not m then
m = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
Opt.menu = m
m:SetFrameStrata("FULLSCREEN_DIALOG")
m:SetClampedToScreen(true)
m:EnableMouse(true)
m:EnableMouseWheel(true)
m:SetBackdrop({ bgFile = WHITE, edgeFile = WHITE, edgeSize = 1 })
m:SetBackdropColor(THEME.bg[1], THEME.bg[2], THEME.bg[3], 0.98)
m:SetBackdropBorderColor(A[1], A[2], A[3], 0.55)
m.buttons = {}
for i = 1, Opt.MENU_ROWS do
local b = CreateFrame("Button", nil, m)
b:SetHeight(20)
b:SetPoint("TOPLEFT", 6, -6 - (i - 1) * 20)
b:SetPoint("RIGHT", m, "RIGHT", -6, 0)
local hl = Opt.rect(b, "HIGHLIGHT", A, 0.16)
hl:SetAllPoints()
-- (the choice that's set: an accent bar at its left)
b.check = Opt.rect(b, "ARTWORK", A, 1)
b.check:SetSize(3, 14)
b.check:SetPoint("LEFT", 6, 0)
b.icon = b:CreateTexture(nil, "ARTWORK")
b.icon:SetSize(16, 16)
b.icon:SetPoint("LEFT", 22, 0)
b.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
b.text = Opt.text(b, 12)
b.text:SetPoint("RIGHT", -4, 0)
b.text:SetJustifyH("LEFT")
pcall(b.text.SetWordWrap, b.text, false)
b:SetScript("OnClick", function(self)
m:Hide()
if self.value ~= nil and m.pick then m.pick(self.value) end
end)
m.buttons[i] = b
end
m.more = Opt.text(m, 10, THEME.dim)
m.more:SetPoint("BOTTOMRIGHT", -8, 4)
-- a search box on top of long lists: typing keeps the choices whose name has it in
m.search = CreateFrame("EditBox", nil, m, "BackdropTemplate")
m.search:SetHeight(20)
m.search:SetPoint("TOPLEFT", 6, -6)
m.search:SetPoint("RIGHT", m, "RIGHT", -6, 0)
m.search:SetAutoFocus(false)
m.search:SetFontObject(Opt.fontObject(12))
m.search:SetTextInsets(6, 6, 0, 0)
Opt.dark(m.search, 0.5)
m.search.hint = Opt.text(m.search, 12, THEME.dim)
m.search.hint:SetPoint("LEFT", 6, 0)
m.search.hint:SetText("Type to search")
m.search:SetScript("OnTextChanged", function(self)
self.hint:SetShown(self:GetText() == "")
m.offset = 0
m:Filter()
m:Fill()
end)
m.search:SetScript("OnEscapePressed", function(self) self:ClearFocus(); m:Hide() end)
m.search:SetScript("OnEnterPressed", function(self) self:ClearFocus() end)
-- the choices shown: all of them, or those matching the search (headings drop out then)
function m:Filter()
local q = (self.searching and self.search:GetText() or ""):lower()
if q == "" then self.choices = self.all; return end
local list = {}
for _, c in ipairs(self.all) do
if not c.header and tostring(c[2] or c[1]):lower():find(q, 1, true) then list[#list + 1] = c end
end
self.choices = list
end
function m:Fill()
local n = #self.choices
local top = self.searching and 26 or 0
for i, b in ipairs(self.buttons) do
b:SetPoint("TOPLEFT", 6, -6 - top - (i - 1) * 20)
local c = self.choices[self.offset + i]
b:SetShown(c ~= nil)
if c then
-- (a heading: gold, not clickable; a choice may have an icon)
b.value = (not c.header) and c[1] or nil
b:EnableMouse(not c.header)
b.text:SetText(c[2] or (not c.header and tostring(c[1])) or "")
local tc = c.header and A or THEME.text
b.text:SetTextColor(tc[1], tc[2], tc[3])
b.check:SetShown(not c.header and c[1] == self.current)
b.icon:SetShown(c.icon ~= nil)
if c.icon then b.icon:SetTexture(c.icon) end
b.text:ClearAllPoints()
b.text:SetPoint("LEFT", c.header and 4 or (c.icon and 42 or 22), 0)
b.text:SetPoint("RIGHT", -4, 0)
end
end
local more = n > Opt.MENU_ROWS
self.more:SetText(more and string.format("%d-%d of %d, scroll for more", self.offset + 1, math.min(n, self.offset + Opt.MENU_ROWS), n) or "")
self:SetHeight(math.min(math.max(n, 1), Opt.MENU_ROWS) * 20 + 12 + top + (more and 14 or 0))
end
m:SetScript("OnMouseWheel", function(self, delta)
self.offset = math.max(0, math.min(#self.choices - Opt.MENU_ROWS, self.offset - delta * 3))
self:Fill()
end)
pcall(m.RegisterEvent, m, "GLOBAL_MOUSE_DOWN")
m:SetScript("OnEvent", function(self)
if self:IsShown() and not self:IsMouseOver() and not (self.owner and self.owner:IsMouseOver()) then self:Hide() end
end)
end
m.owner, m.all, m.current, m.pick = owner, choices or {}, current, pick
m.choices = m.all
m.offset = 0
-- (the search box, for lists longer than the menu)
m.searching = #m.all > Opt.MENU_ROWS
m.search:SetShown(m.searching)
m.search:SetText("")
for i, c in ipairs(m.choices) do
if c[1] == current then m.offset = math.max(0, math.min(#m.choices - Opt.MENU_ROWS, i - math.floor(Opt.MENU_ROWS / 2))) end
end
m:ClearAllPoints()
m:SetPoint("TOPLEFT", owner, "BOTTOMLEFT", 0, -2)
m:SetWidth(math.max(owner:GetWidth(), 240))
m:Fill()
m:Show()
m:Raise()
end
-- Greys a row out (and stops its controls taking clicks) while what it depends on is off.
function Opt.setRowEnabled(row, on)
if row.enabled == on then return end
row.enabled = on
for _, c in ipairs(row.controls or {}) do
if c.SetEnabled then pcall(c.SetEnabled, c, on)
elseif on and c.Enable then pcall(c.Enable, c)
elseif c.Disable then pcall(c.Disable, c) end
if c.EnableMouse then pcall(c.EnableMouse, c, on) end
end
for _, t in ipairs(row.texts or {}) do
local c = on and THEME.text or THEME.dim
t:SetTextColor(c[1], c[2], c[3], on and 1 or 0.6)
end
-- (and its controls faded)
for _, c in ipairs(row.controls or {}) do
if c.SetAlpha then c:SetAlpha(on and 1 or 0.4) end
end
end
function Opt.newPage(id, name)
local page = { id = id, name = name, rows = {} }
Opt.pages[id] = page
Opt.order[#Opt.order + 1] = id
return page
end
-- Redraws the shown page: which rows show (closed sections and B.showIf hide them), where they sit,
-- which are greyed out (B.under) and what each control shows. Cheap enough to run on every change.
local layoutErrorShown = false
-- (a page's rows are made the first time it's shown, or searched)
local function makeRows(page)
if page.content then return end
page.content = CreateFrame("Frame", nil, Opt.scroll)
page.content:SetSize(Opt.CONTENT_W, 10)
page.content:Hide()
for _, row in ipairs(page.rows) do
local f = CreateFrame("Frame", nil, page.content)
f:SetHeight(row.height)
row.frame = f
if row.section or row.card then
-- (the rows of a section sit on its card; a page's own rows can ask for one too: row.card)
local card = Opt.rect(f, "BACKGROUND", THEME.raised, 0.35)
card:SetPoint("TOPLEFT", 6, 0)
card:SetPoint("BOTTOMRIGHT", -6, 0)
end
local ok, err = pcall(row.make, f, row)
if not ok and not layoutErrorShown then
layoutErrorShown = true
print("|cffff9900Webby Frames:|r an options row couldn't be drawn: " .. tostring(err))
end
end
end
-- Whether a row's own conditions (B.showIf, B.under, and its section's) let it show.
local function rowShowable(row)
for _, r in ipairs({ row, row.section and row.section.header }) do
for _, pred in ipairs(r.preds or {}) do
local ok, v = pcall(pred)
if not (ok and v) then return false end
end
end
return true
end
-- Puts a row at y in content (the search results borrow rows from their pages), greys it out or
-- not, and redraws its controls. Returns the height it takes.
-- (off: greyed out whatever its own conditions say: a search result that isn't in use now)
-- (which: for a pair of switches, the one half a search matched; nil: both, as on the page)
local function placeRow(row, content, y, off, which)
if row.frame:GetParent() ~= content then row.frame:SetParent(content) end
row.frame:Show()
if row.only and row.onlyNow ~= (which or 0) then
row.onlyNow = which or 0
row.only(which)
end
row.frame:ClearAllPoints()
row.frame:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -y)
row.frame:SetPoint("RIGHT", content, "RIGHT")
row.y = y
local on = not off
if on and row.enabledIf then
local ok, v = pcall(row.enabledIf)
on = ok and v and true or false
end
if on and row.parentRow and row.parentRow.enabled == false then on = false end
Opt.setRowEnabled(row, on)
if row.refresh then
row.updating = true
pcall(row.refresh)
row.updating = false
end
return row.height
end
-- The search results, in place of a page (like the game's own settings search): every setting
-- whose name has what was typed in, from every page, live, under its page's and section's name.
local function layoutSearch(q)
local content = Opt.searchContent
if not content then
content = CreateFrame("Frame", nil, Opt.scroll)
content:SetSize(Opt.CONTENT_W, 10)
content.heads = {}
content.none = Opt.text(content, 13, THEME.dim)
content.none:SetPoint("TOPLEFT", Opt.LABEL_X, -16)
content.none:SetText("No settings match.")
Opt.searchContent = content
end
for _, p in pairs(Opt.pages) do
if p.content then p.content:Hide() end
end
content:Show()
if Opt.scroll:GetScrollChild() ~= content then
Opt.scroll:SetScrollChild(content)
Opt.scroll:SetVerticalScroll(0)
end
for _, h in ipairs(content.heads) do h:Hide() end
content.notes = content.notes or {}
for _, n in ipairs(content.notes) do n:Hide() end
for _, row in ipairs(Opt.borrowed or {}) do row.frame:Hide() end
Opt.borrowed = {}
local placed = {}
local y, nHeads, nNotes, lastHead = 6, 0, 0, nil
local function heading(text)
if text == lastHead then return end
lastHead = text
nHeads = nHeads + 1
local fs = content.heads[nHeads]
if not fs then
fs = Opt.text(content, 12, A)
fs:SetJustifyH("LEFT")
content.heads[nHeads] = fs
end
fs:ClearAllPoints()
fs:SetPoint("TOPLEFT", content, "TOPLEFT", Opt.LABEL_X, -(y + 12))
fs:SetText(text)
fs:Show()
y = y + 32
end
-- (a small line saying why the setting under it is greyed out)
local function note(text, indent)
nNotes = nNotes + 1
local fs = content.notes[nNotes]
if not fs then
fs = Opt.text(content, 11, THEME.dim)
fs:SetJustifyH("LEFT")
content.notes[nNotes] = fs
end
fs:ClearAllPoints()
fs:SetPoint("TOPLEFT", content, "TOPLEFT", Opt.LABEL_X + 20 + 18 * (indent or 0), -(y + 4))
fs:SetText(text)
fs:Show()
y = y + 18
end
-- (what to look for: what was typed, and the words that mean the same)
local terms = Opt.searchTerms(q)
local function matches(text)
if not text then return false end
for _, term in ipairs(terms) do if text:find(term, 1, true) then return true end end
return false
end
-- (a pair of switches where only one half's name matches: just that half)
local function half(row)
local h = row.halves
if not h then return nil end
local m1, m2 = matches(h[1]:lower()), matches(h[2]:lower())
if m1 and not m2 then return 1 elseif m2 and not m1 then return 2 end
end
local function place(row, off, which)
if placed[row] then return end
placed[row] = true
y = y + placeRow(row, content, y, off, which)
Opt.borrowed[#Opt.borrowed + 1] = row
end
for _, id in ipairs(Opt.order) do
local page = Opt.pages[id]
local pageOff = page and UNIT_LABEL[id] and not frameOn(id)
for _, row in ipairs(page and page.rows or {}) do
if matches(row.find) and not placed[row] then
makeRows(page)
if row.frame then
-- a heading each time the page or section changes
heading(page.name:upper() .. (row.section and (" |cff8f96a6/ " .. row.section.title:upper() .. "|r") or ""))
local why, parent, usable = Opt.whyOff(page, row, pageOff)
if not why then
place(row, false, half(row))
else
-- not in use now: the switch it needs, to turn on right here; a note saying why; then
-- the setting itself. It's greyed out only while that switch above it is off: a
-- setting for the other party style or a hidden frame can still be set, ready.
if parent and not placed[parent] and parent.frame then place(parent) end
note(why, row.indent)
place(row, not usable, half(row))
end
end
end
end
end
content.none:SetShown(#Opt.borrowed == 0)
return content, y
end
-- Words that mean the same: typing any of them (or the start of one, 3 letters on) searches for all of
-- them, so "threat" and "aggro" find the same settings, and "color" finds "colour".
Opt.SYNONYMS = {
{ "threat", "aggro" }, { "health", "hp" }, { "power", "mana", "rage", "energy" },
{ "dispel", "cleanse", "decurse" }, { "buff", "hot" }, { "debuff", "dot" }, { "absorb", "shield" },
{ "cast bar", "castbar", "casting" }, { "colour", "color" }, { "opacity", "alpha", "transparent", "see-through" },
{ "spacing", "gap", "padding" }, { "numbers", "values" }, { "pet", "minion" },
}
function Opt.searchTerms(q)
-- (only when what's typed is one of the words, or from 3 letters the start of one: "health colour"
-- stays as typed rather than turning into "anything with hp or color")
local terms, seen = { q }, { [q] = true }
for _, set in ipairs(Opt.SYNONYMS) do
local hit = false
for _, w in ipairs(set) do
if w == q or (#q >= 3 and w:find(q, 1, true) == 1) then hit = true; break end
end
if hit then
for _, w in ipairs(set) do if not seen[w] then seen[w] = true; terms[#terms + 1] = w end end
end
end
return terms
end
-- Words people search for that aren't in a setting's name (by setting key), so "kick" finds Cast bar and
-- "oom" Healer mana. Only ever whole settings: section headings aren't searched (too broad).
-- (the shield and heal glows, on every frame page and the raid tiles alike)
Opt.GLOW_CHOICES = { { "off", "Off" }, { "pulse", "Soft pulse" }, { "shimmer", "Shimmer" }, { "snow", "Snowglobe" }, { "blizzard", "Blizzard" } }
Opt.GLOW_TIP = "Soft pulse: a soft glow breathing in and out.\nShimmer: a band of light sweeping across about once a second.\nSnowglobe: glowing flakes floating inside; when it changes they swirl up and settle again.\nBlizzard: a snowstorm, lots of small flakes driven down and across by the wind."
Opt.BORDER_TIP = "Blizzard's spell alert glow round it, in its colour. Goes with any glow, or on its own."
Opt.SEARCH_WORDS = {
enabled = "enable disable turn off addon", combatNumbers = "floating combat text fct damage numbers",
combatStyle = "straight fountain static animation scroll combat numbers",
combatTarget = "target combat numbers other players damage",
minimapButton = "minimap icon", scale = "size bigger smaller", font = "text typeface",
frameTint = "border color gold class", portraitStyle = "class icon face picture",
barTexture = "hp health statusbar", powerTexture = "mana rage energy focus power statusbar",
hpAlpha = "hp transparent see-through alpha", mpAlpha = "mana rage energy transparent see-through alpha",
textMode = "hp mana values text", textFormat = "percent percentage values text",
colorMode = "hp color class reaction", rpHealthColor = "hp color class", petColor = "hp color",
npcColor = "hp color mob enemy", healthFade = "hp red low health gradient color",
healPrediction = "heal prediction hots", healTexture = "heal prediction incoming statusbar", healGlow = "heal glow pulse shimmer snowglobe blizzard snowstorm sparkle", absorbGlow = "shield absorb glow pulse shimmer snowglobe blizzard snowstorm sparkle",
healBorder = "heal glow edge outline proc", absorbBorder = "shield absorb glow edge outline proc", healAlpha = "heal incoming transparent", absorbs = "shield absorb", absorbTexture = "shield absorb statusbar stripes",
absorbAlpha = "shield absorb transparent", damageTrail = "hit lost red hp trail",
damageTrailTexture = "recent damage trail statusbar", damageTrailColor = "recent damage trail color colour",
damageTrailDelay = "recent damage trail wait linger", damageTrailAlpha = "recent damage trail transparent opacity alpha",
damagePredict = "spell damage preview", costPredict = "mana cost", costShade = "mana cost", manaTick = "mp5 regen spark five second rule 5sr",
manaTickSize = "mana tick mp5 spark", manaTickSparkle = "mana tick mp5 star", manaTickStar = "mana tick mp5 star shape", costTexture = "mana cost statusbar", costTrailTexture = "mana cost spent statusbar",
costGlow = "mana cost glow pulse shimmer snowglobe blizzard", costGlowAlpha = "mana cost glow strength",
threatOn = "aggro tank", threatShow = "aggro tank", threatStyle = "aggro tank percent", threatColors = "aggro tank color",
dispelGlow = "cleanse decurse magic curse poison disease debuff", dispelWho = "cleanse decurse debuff",
rpDispelStyle = "cleanse decurse debuff", rpDispelIcon = "cleanse decurse magic curse poison disease debuff",
auraBuffs = "hots auras", auraDebuffs = "dots auras", rpBuffs = "hots auras", rpDebuffs = "dots auras", auraMine = "dots my debuffs", auraMineBig = "dots my debuffs",
auraTimer = "cooldown duration timer countdown swipe", auraPos = "auras buffs debuffs",
castShow = "castbar casting spell interrupt kick", castText = "castbar spell", castIcon = "castbar spell",
castTexture = "castbar statusbar", castSpark = "castbar", castFade = "castbar fade flash", castFadeTime = "castbar fade",
partyGap = "gap padding", rpGap = "gap padding", rangeFade = "range distance alpha", rangeAlpha = "range distance alpha fade",
partyTooltip = "mouseover hover", partyStyle = "raid frames compact grid tiles portrait", partySort = "order role sort",
partyLayout = "horizontal vertical", rpPets = "pet minion", partyPets = "pet minion", rpPetPos = "pets", rpPetHeight = "pets",
missingBuffs = "buff reminder fortitude intellect mark blessing", healerMana = "healer oom",
druidMana = "bear cat form", druidBars = "bear cat form mana", comboPoints = "rogue cp",
petHappy = "hunter loyalty feed", weaponEnchants = "poison oil sharpening stone imbue enchant",
buffBars = "timer bars", rpShieldStyle = "absorb statusbar stripes", rpShieldAlpha = "absorb shield transparent", rpShieldEdge = "absorb",
rpDefensives = "cooldowns wall divine ice block barkskin", rpSelf = "me player", rpSolo = "alone",
clickTooltip = "click casting bindings", clickAllFrames = "click casting bindings",
clickCasting = "mouseover clique bindings spells",
}
-- Why a search result isn't in use now (nil: it is); the row to show above it that would bring it in (a
-- switch it sits under, or the frame's own Show), if there is one; and whether it can still be changed
-- as it is (true, unless it sits under a switch that's off).
Opt.predNotes, Opt.predParts = {}, {}
local function firstFailing(row)
for _, r in ipairs({ row, row.section and row.section.header }) do
for _, pred in ipairs(r and r.preds or {}) do
local ok, v = pcall(pred)
if not (ok and v) then return pred end
end
end
end
-- (a condition made of others says why through the first of them that fails, else its own note)
local function noteFor(pred)
for _, part in ipairs(Opt.predParts[pred] or {}) do
local ok, v = pcall(part)
if not (ok and v) then
local n = noteFor(part)
if n then return n end
end
end
return Opt.predNotes[pred]
end
function Opt.whyOff(page, row, pageOff)
if pageOff then
for _, r in ipairs(page.rows) do
if r.keys and r.keys.frameShown then return "The " .. page.name:lower() .. " frame is turned off.", r, true end
end
return "The " .. page.name:lower() .. " frame is turned off.", nil, true
end
local bad = firstFailing(row)
if not bad then return nil end
local parent = row.parentRow
if parent and bad == row.enabledIf then
-- (the switch it needs isn't in use either: say why that is)
local parentWhy = Opt.whyOff(page, parent, false)
if parentWhy then return parentWhy, parent, false end
local label = parent.search or "the setting above"
if parent.kind == "switch" then return "Needs " .. label .. " turned on.", parent, false end
return "Depends on " .. label .. ".", parent, false
end
return noteFor(bad) or "Not used with your current choices.", nil, true
end
function Opt.layout()
local w = Opt.window
if not (w and w:IsShown()) then return end
local q = Opt.query or ""
local content, y
if #q >= 2 then
content, y = layoutSearch(q)
else
local page = Opt.pages[Opt.current]
if not page then return end
makeRows(page)
if Opt.searchContent then Opt.searchContent:Hide() end
for _, p in pairs(Opt.pages) do
if p.content then p.content:SetShown(p == page) end
end
content = page.content
if Opt.scroll:GetScrollChild() ~= content then
Opt.scroll:SetScrollChild(content)
Opt.scroll:SetVerticalScroll(0)
end
y = 6
for _, row in ipairs(page.rows) do
local shown = (not row.section or row.section.expanded) and rowShowable(row)
if shown then
y = y + placeRow(row, content, y)
else
if row.frame:GetParent() ~= content then row.frame:SetParent(content) end
row.frame:Hide()
end
end
end
content:SetHeight(y + 10)
-- (the plain scroll frame doesn't keep its position inside the new height by itself)
local max = math.max(0, y + 10 - Opt.scroll:GetHeight())
if Opt.scroll:GetVerticalScroll() > max then Opt.scroll:SetVerticalScroll(max) end
-- the scroll bar only when there's something to scroll (the mouse wheel does nothing either way then)
if Opt.scrollBar then Opt.scrollBar:SetShown(max > 0) end
local searching = #q >= 2
for id, b in pairs(Opt.navButtons or {}) do b.selected:SetShown(not searching and id == Opt.current) end
Opt.layoutNav()
end
-- The page list: General, All frames, and under All frames the frame pages, folded away until its +
-- is clicked. Frames turned off on the General page aren't listed.
function Opt.layoutNav()
if not Opt.navButtons then return end
-- the page on show was a frame just turned off: go to All frames instead
local cur = Opt.current
if cur and UNIT_LABEL[cur] and not frameOn(cur) then
Opt.current = "all"
Opt.changed()
end
local y = 8
for _, id in ipairs(Opt.order) do
local b = Opt.navButtons[id]
local isFrame = UNIT_LABEL[id] ~= nil -- (General, All frames and Click casting are top-level)
local shown = not isFrame or (Opt.framesOpen and frameOn(id))
if b then
b:SetShown(shown and true or false)
if shown then
b:ClearAllPoints()
b:SetPoint("TOPLEFT", 1, -y)
b:SetPoint("RIGHT", b:GetParent(), "RIGHT", -1, 0)
y = y + b:GetHeight() + 2
end
end
end
local all = Opt.navButtons.all
if all and all.toggle then all.toggle.fs:SetText(Opt.framesOpen and "-" or "+") end
end
-- After a change: redraw once, on the next frame (several settings can change in one click).
-- While a mouse button is down (a slider drag), wait until it is released so the drag isn't reset.
function Opt.changed()
if Opt.pendingLayout then return end
Opt.pendingLayout = true
local function run()
local down = IsMouseButtonDown and (IsMouseButtonDown("LeftButton") or IsMouseButtonDown("RightButton"))
if down then C_Timer.After(0.05, run); return end
Opt.pendingLayout = false
Opt.layout()
end
C_Timer.After(0, run)
end
function Opt.select(id)
if not Opt.pages[id] then id = "general" end
-- a frame turned off has no page to show
if UNIT_LABEL[id] and not frameOn(id) then id = "all" end
-- (picking a page ends a search)
if Opt.searchBox and Opt.searchBox:GetText() ~= "" then Opt.searchBox:SetText(""); Opt.searchBox:ClearFocus() end
-- (the page being left keeps its scroll position; the one picked is remembered, and goes back to its own)
if id ~= Opt.current then Opt.saveScroll() end
local changed = id ~= Opt.current
Opt.current = id
Opt.state().page = id
Opt.closeMenu()
Opt.layout()
if changed then Opt.restoreScroll(id) end
end
-- The settings search, at the top of the window on every page: typing swaps the page for the
-- matching settings from every page (see layoutSearch); clearing it brings the page back.
function Opt.setQuery(text)
local q = (text or ""):lower():gsub("^%s+", ""):gsub("%s+$", "")
if q == Opt.query then return end
Opt.query = q
Opt.closeMenu()
Opt.scroll:SetVerticalScroll(0)
Opt.layout()
end
function Opt.makeSearch(w, rightOf)
local box = CreateFrame("EditBox", nil, w, "BackdropTemplate")
box:SetSize(250, 28)
-- (in the title bar, centred on it, left of the close button)
box:SetPoint("RIGHT", rightOf, "LEFT", -10, 0)
box:SetAutoFocus(false)
box:SetFontObject(Opt.fontObject(12))
box:SetTextInsets(28, 24, 0, 0)
Opt.dark(box, 0.3, THEME.panel)
-- (an accent underline while you're typing in it)
local focus = Opt.rect(box, "ARTWORK", A, 1)
focus:SetPoint("BOTTOMLEFT"); focus:SetPoint("BOTTOMRIGHT"); focus:SetHeight(2)
focus:Hide()
local glass = box:CreateTexture(nil, "OVERLAY")
glass:SetSize(14, 14)
glass:SetPoint("LEFT", 9, 0)
glass:SetTexture("Interface\\Common\\UI-Searchbox-Icon")
glass:SetVertexColor(THEME.dim[1], THEME.dim[2], THEME.dim[3])
local hint = Opt.text(box, 12, THEME.dim)
hint:SetPoint("LEFT", 28, 0)
hint:SetText("Search settings")
local clear = Opt.newX(box, 18)
clear:SetPoint("RIGHT", -5, 0)
clear:SetScript("OnClick", function() box:SetText(""); box:ClearFocus() end)
clear:Hide()
box:SetScript("OnTextChanged", function(self)
local empty = self:GetText() == ""
hint:SetShown(empty)
clear:SetShown(not empty)
Opt.setQuery(self:GetText())
end)
box:SetScript("OnEditFocusGained", function()
focus:Show()
glass:SetVertexColor(A[1], A[2], A[3])
end)
box:SetScript("OnEditFocusLost", function()
focus:Hide()
glass:SetVertexColor(THEME.dim[1], THEME.dim[2], THEME.dim[3])
end)
box:SetScript("OnEnterPressed", function(self) self:ClearFocus() end)
box:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
Opt.searchBox = box
return box
end
function Opt.buildWindow()
if Opt.window then return Opt.window end
local w = CreateFrame("Frame", "WebbyFramesOptions", UIParent, "BackdropTemplate")
Opt.window = w
w:Hide()
w:SetSize(820, 600)
local pos = WebbyFramesDB and WebbyFramesDB.optionsPos
if type(pos) == "table" and pos[1] then w:SetPoint(pos[1], UIParent, pos[2] or pos[1], pos[3] or 0, pos[4] or 0)
else w:SetPoint("CENTER") end
w:SetFrameStrata("HIGH")
w:SetToplevel(true)
w:SetClampedToScreen(true)
w:SetMovable(true)
w:EnableMouse(true)
w:RegisterForDrag("LeftButton")
w:SetScript("OnDragStart", w.StartMoving)
w:SetScript("OnDragStop", function(self)
self:StopMovingOrSizing()
local p, _, rp, x, y = self:GetPoint()
if WebbyFramesDB then WebbyFramesDB.optionsPos = { p, rp, x, y } end
end)
w:SetBackdrop({ edgeFile = WHITE, edgeSize = 1 })
w:SetBackdropBorderColor(1, 1, 1, 0.1)
tinsert(UISpecialFrames, "WebbyFramesOptions") -- (Esc closes it)
-- a soft shadow round it
for i, s in ipairs({ { 4, 0.35 }, { 10, 0.18 }, { 18, 0.08 } }) do
local sh = Opt.rect(w, "BACKGROUND", { 0, 0, 0 }, s[2], -8 + i)
sh:SetPoint("TOPLEFT", -s[1], s[1])
sh:SetPoint("BOTTOMRIGHT", s[1], -s[1])
end
-- (the window's own fill, over the shadow)
local fill = Opt.rect(w, "BACKGROUND", THEME.bg, 0.97, -4)
fill:SetPoint("TOPLEFT", 1, -1)
fill:SetPoint("BOTTOMRIGHT", -1, 1)
-- the title bar: a faint glow of the accent from the left, and an accent line under it
local BAR_H = 56
local glow = w:CreateTexture(nil, "BACKGROUND", nil, 1)
glow:SetPoint("TOPLEFT", 1, -1)
glow:SetPoint("BOTTOMRIGHT", w, "TOPRIGHT", -1, -BAR_H)
Opt.gradient(glow, "HORIZONTAL", A, 0.16, A, 0)
local shine = w:CreateTexture(nil, "BACKGROUND", nil, 2)
shine:SetPoint("TOPLEFT", 1, -1)
shine:SetPoint("BOTTOMRIGHT", w, "TOPRIGHT", -1, -BAR_H)
Opt.gradient(shine, "VERTICAL", { 1, 1, 1 }, 0, { 1, 1, 1 }, 0.04)
local rule = w:CreateTexture(nil, "BORDER")
rule:SetPoint("TOPLEFT", 1, -BAR_H)
rule:SetPoint("TOPRIGHT", -1, -BAR_H)
rule:SetHeight(1)
Opt.gradient(rule, "HORIZONTAL", A, 0.9, A, 0.05)
-- the logo: the web emblem, and the name stacked beside it (WEBBY large in the accent, F R A M E S
-- small and spaced out under it)
local logo = w:CreateTexture(nil, "ARTWORK")
logo:SetSize(46, 46)
logo:SetPoint("LEFT", w, "TOPLEFT", 12, -BAR_H / 2)
logo:SetTexture(Opt.LOGO)
local title = Opt.text(w, 19, A)
title:SetPoint("TOPLEFT", logo, "RIGHT", 8, 16) -- (the two lines together centred on the emblem)
title:SetText("WEBBY")
-- FRAMES: one letter each with the same gap between every pair, so the word starts and ends
-- exactly under WEBBY (even gaps, not even steps: the letters aren't all as wide)
local letters, fsList, used = { "F", "R", "A", "M", "E", "S" }, {}, 0
for i, ch in ipairs(letters) do
local fs = Opt.text(w, 11)
fs:SetText(ch)
fsList[i] = fs
used = used + fs:GetStringWidth()
end
local gap = math.max(1, (title:GetStringWidth() - used) / (#fsList - 1))
local x = 1
for _, fs in ipairs(fsList) do
fs:SetPoint("TOPLEFT", title, "BOTTOMLEFT", math.floor(x + 0.5), -4)
x = x + fs:GetStringWidth() + gap
end
local ver = Opt.text(w, 10, THEME.dim)
ver:SetPoint("BOTTOMLEFT", title, "BOTTOMRIGHT", 10, 1)
ver:SetText("v" .. addonVersion())
local close = Opt.newX(w, 30)
close:SetPoint("RIGHT", w, "TOPRIGHT", -14, -BAR_H / 2)
close:SetScript("OnClick", function() w:Hide() end)
Opt.makeSearch(w, close)
-- Reload UI, between the name and the search (for the options that need a /reload)
local reload = Opt.newButton(w, 104, 28, "Reload UI")
reload:SetPoint("RIGHT", Opt.searchBox, "LEFT", -10, 0)
reload:SetScript("OnClick", function()
if C_UI and C_UI.Reload then C_UI.Reload() else ReloadUI() end
end)
Opt.tip(reload, "Reload UI", "Reloads the interface (the same as /reload), for the options that need it, like turning Webby Frames or its party frames on or off.")
-- the page list
local NAV_W, GAP = 172, 10
local nav = CreateFrame("Frame", nil, w, "BackdropTemplate")
nav:SetPoint("TOPLEFT", 14, -(BAR_H + 12))
nav:SetPoint("BOTTOMLEFT", 14, 14)
nav:SetWidth(NAV_W)
Opt.dark(nav, 0.15, THEME.panel)
Opt.navButtons = {}
for i, id in ipairs(Opt.order) do
local page = Opt.pages[id]
local top = not UNIT_LABEL[id] -- (General, All frames, Click casting...; the frames sit under All frames)
local b = CreateFrame("Button", nil, nav)
b:SetHeight(top and 30 or 24)
b:SetPoint("TOPLEFT", 1, -6 - (i - 1) * 30)
b:SetPoint("RIGHT", nav, "RIGHT", -1, 0)
local hl = Opt.rect(b, "HIGHLIGHT", { 1, 1, 1 }, 0.05)
hl:SetAllPoints()
local fs = Opt.text(b, top and 12 or 11, THEME.dim)
fs:SetPoint("LEFT", top and 16 or 28, 0)
fs:SetText(top and page.name:upper() or page.name)
-- (the page on show: an accent bar at its left, the accent fading out behind it, its name lit)
-- (b.selected is an empty frame: shown / hidden by Opt.layout, it lights these up)
local wash = b:CreateTexture(nil, "BACKGROUND")
wash:SetAllPoints()
Opt.gradient(wash, "HORIZONTAL", A, 0.22, A, 0)
local bar = Opt.rect(b, "ARTWORK", A, 1)
bar:SetPoint("TOPLEFT"); bar:SetPoint("BOTTOMLEFT"); bar:SetWidth(3)
local sel = CreateFrame("Frame", nil, b)
local function lit(on)
wash:SetShown(on); bar:SetShown(on)
local c = on and THEME.text or THEME.dim
fs:SetTextColor(c[1], c[2], c[3])
end
sel:SetScript("OnShow", function() lit(true) end)
sel:SetScript("OnHide", function() lit(false) end)
lit(false)
sel:Hide()
b.selected = sel
b:SetScript("OnClick", function()
pcall(PlaySound, SOUNDKIT and SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON or 856)
Opt.select(id)
end)
if id == "all" then
-- the + / - that shows or folds away the frame pages under it
local t = CreateFrame("Button", nil, b)
t:SetSize(22, 22)
t:SetPoint("RIGHT", -6, 0)
local thl = Opt.rect(t, "HIGHLIGHT", { 1, 1, 1 }, 0.08)
thl:SetAllPoints()
t.fs = Opt.text(t, 16, THEME.dim)
t.fs:SetPoint("CENTER", 0, 1)
t:SetScript("OnClick", function()
Opt.framesOpen = not Opt.framesOpen
Opt.state().framesOpen = Opt.framesOpen or nil
Opt.layoutNav()
end)
b.toggle = t
end
Opt.navButtons[id] = b
end
Opt.framesOpen = Opt.state().framesOpen and true or false
Opt.layoutNav()
-- the page itself, scrolling
local area = CreateFrame("Frame", nil, w, "BackdropTemplate")
area:SetPoint("TOPLEFT", nav, "TOPRIGHT", GAP, 0)
area:SetPoint("BOTTOMRIGHT", -14, 14)
Opt.dark(area, 0.15, THEME.panel)
local ok, scroll = pcall(CreateFrame, "ScrollFrame", "WebbyFramesOptionsScroll", area, "UIPanelScrollFrameTemplate")
if not ok then
scroll = CreateFrame("ScrollFrame", nil, area)
scroll:EnableMouseWheel(true)
scroll:SetScript("OnMouseWheel", function(self, delta)
local child = self:GetScrollChild()
local max = math.max(0, (child and child:GetHeight() or 0) - self:GetHeight())
self:SetVerticalScroll(math.max(0, math.min(max, self:GetVerticalScroll() - delta * 40)))
end)
end
scroll:SetPoint("TOPLEFT", 4, -6)
scroll:SetPoint("BOTTOMRIGHT", -16, 6)
-- (the template's scroll bar, slimmed down: no arrow buttons, a thin track and a thin thumb)
pcall(function()
local bar = scroll.ScrollBar or _G["WebbyFramesOptionsScrollScrollBar"]
if not bar then return end
Opt.scrollBar = bar -- (shown only while the page is longer than the window: Opt.layout)
scroll.scrollBarHideable = true -- (and the template's own range check agrees: it hides it at no range too)
for _, r in ipairs({ bar:GetRegions() }) do if r.SetAlpha then r:SetAlpha(0) end end
for _, k in ipairs({ "ScrollUpButton", "ScrollDownButton" }) do
local btn = bar[k] or (bar:GetName() and _G[bar:GetName() .. k])
if btn then btn:SetAlpha(0); btn:EnableMouse(false); btn:SetSize(1, 1) end
end
bar:ClearAllPoints()
bar:SetPoint("TOPRIGHT", area, "TOPRIGHT", -5, -8)
bar:SetPoint("BOTTOMRIGHT", area, "BOTTOMRIGHT", -5, 8)
bar:SetWidth(6)
local track = Opt.rect(bar, "BACKGROUND", { 1, 1, 1 }, 0.05)
track:SetAllPoints()
local thumb = bar:GetThumbTexture()
if thumb then
thumb:SetAlpha(1)
thumb:SetColorTexture(A[1], A[2], A[3], 0.6)
thumb:SetSize(6, 48)
end
end)
Opt.scroll = scroll
Opt.CONTENT_W = 820 - 14 - NAV_W - GAP - 14 - 4 - 16
-- (opening: a quick fade in)
local fade = w:CreateAnimationGroup()
local a = fade:CreateAnimation("Alpha")
a:SetFromAlpha(0)
a:SetToAlpha(1)
a:SetDuration(0.14)
pcall(a.SetSmoothing, a, "OUT")