-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathUI.lua
More file actions
4976 lines (4672 loc) · 186 KB
/
Copy pathUI.lua
File metadata and controls
4976 lines (4672 loc) · 186 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
--[[
MidnightHelper - UI.lua
Builds the main shell: movable/resizable frame, Blizzard dialog-style chrome
(gold border + stone tile, same family as Changelog), title bar, search row,
sidebar tabs, and module host panels. Core.lua toggles visibility via ns:ToggleMainWindow.
]]
local addonName, ns = ...
--------------------------------------------------------------------------------
-- Layout constants (tweak in one place)
--------------------------------------------------------------------------------
-- Wider default so Professions tab can show two treasure columns side-by-side.
-- Default matches a typical user-tuned layout (795×600 at UI scale 1.25 fits Delves + Guide).
local DEFAULT_WIDTH = 795
local DEFAULT_HEIGHT = 600
-- Suggested minimum for first-time installs only; never overrides mainWindowUserSized.
local MIN_DELVES_WINDOW_H = 800
local SIDEBAR_WIDTH = 132
local TITLE_BAR_HEIGHT = 32
-- Global search bar (full width under title) — not in the narrow sidebar: keeps the field wide on all tabs.
local SEARCH_BAR_HEIGHT = 30
local RESIZE_GRIP_SIZE = 16
local MIN_WIDTH, MIN_HEIGHT = 620, 600
-- Baseline caps; EnsureMainUI raises them on large displays (see UpdateMaxWindowBounds).
local MAX_WIDTH, MAX_HEIGHT = 1000, 920
-- Shared content-layout metrics for module panels (HomeDashboard, MidnightCodex,
-- and future tabs). Single source so insets stay consistent across tabs; modules
-- read these with local fallbacks so load order can never break them.
ns.UI_METRICS = {
sidePad = 14,
topPad = 12,
sectionGap = 8,
scrollGutter = 30,
}
local function ClampMainWidth(w)
w = tonumber(w) or DEFAULT_WIDTH
return math.max(MIN_WIDTH, math.min(MAX_WIDTH, w))
end
local function ClampMainHeight(h)
h = tonumber(h) or DEFAULT_HEIGHT
return math.max(MIN_HEIGHT, math.min(MAX_HEIGHT, h))
end
--- Raise the size caps on large displays (1440p+): the hard 1000x920 cap left
--- dead space on big screens. UIParent dimensions are in the same (scaled)
--- units as the main frame, so the comparison is scale-safe. Never lowers the
--- baseline caps, so small screens keep the original behavior.
local function UpdateMaxWindowBounds()
local sw = UIParent and UIParent.GetWidth and UIParent:GetWidth() or 0
local sh = UIParent and UIParent.GetHeight and UIParent:GetHeight() or 0
if sw > 0 then
MAX_WIDTH = math.max(1000, math.min(1400, math.floor(sw * 0.85)))
end
if sh > 0 then
MAX_HEIGHT = math.max(920, math.min(1200, math.floor(sh * 0.9)))
end
end
--- Prefer SavedVariables size; fall back to layout defaults.
local function GetSavedMainWidth()
local ui = ns.db and ns.db.ui
local w = ui and tonumber(ui.mainWidth)
if w and w >= MIN_WIDTH then
return ClampMainWidth(w)
end
return DEFAULT_WIDTH
end
local function GetSavedMainHeight()
local ui = ns.db and ns.db.ui
local h = ui and tonumber(ui.mainHeight)
if h and h >= MIN_HEIGHT then
return ClampMainHeight(h)
end
return DEFAULT_HEIGHT
end
--- One-time bump for pre-v3 saves that are too short for Delves; never shrink a user-resized window.
local function ResolveMainHeightForOpen()
local ui = ns.db and ns.db.ui
if not ui then
return DEFAULT_HEIGHT
end
local h = GetSavedMainHeight()
local ver = tonumber(ui.layoutVersion) or 0
if ui.mainWindowUserSized and h >= MIN_HEIGHT then
if ver < 3 then
ui.layoutVersion = 3
end
return h
end
if ver < 3 and h < MIN_DELVES_WINDOW_H then
h = DEFAULT_HEIGHT
ui.mainHeight = h
ui.layoutVersion = 3
elseif ver < 3 then
ui.layoutVersion = 3
end
return h
end
function ns:ApplySavedMainWindowSize()
if not self.mainUI then
return
end
local w = GetSavedMainWidth()
local h = ResolveMainHeightForOpen()
-- Programmatic resize: must not mark the window as user-sized (see OnSizeChanged guard).
ns._mhProgrammaticResize = true
self.mainUI:SetSize(w, h)
ns._mhProgrammaticResize = false
end
--- Restore the saved window position (F4.6). No saved point = leave the default CENTER.
function ns:ApplySavedMainWindowPosition()
local main = self.mainUI
if not main then
return
end
local ui = ns.db and ns.db.ui
local p = ui and ui.mainPoint
if type(p) ~= "table" or type(p.point) ~= "string" then
return
end
main:ClearAllPoints()
main:SetPoint(p.point, UIParent, p.relativePoint or p.point, tonumber(p.x) or 0, tonumber(p.y) or 0)
end
local ABOUT_BTN_WIDTH = 110
local ABOUT_BTN_HEIGHT = 22
local ABOUT_BTN_BOTTOM_INSET = 10
-- Chrome: Blizzard dialog stone + gold frame (aligns with Changelog), warm tab tints.
local MH_TEX = {
dialogBg = "Interface\\DialogFrame\\UI-DialogBox-Background",
dialogBgDark = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
goldEdge = "Interface\\DialogFrame\\UI-DialogBox-Gold-Border",
}
--- Children stay inside this inset so they don't paint over the dialog gold corner ornaments.
local MH_MAIN_EDGE = { L = 12, R = 12, T = 12, B = 11 }
local MH_CHROME = {
-- Fallbacks if a region still uses flat color.
mainBg = { 0.09, 0.09, 0.11, 0.96 },
outerEdge = { 0, 0, 0, 0.42 },
-- Title strip: warm bronze over dialog art.
titleStrip = { 0.2, 0.15, 0.1, 0.88 },
sidebar = { 0.075, 0.076, 0.082, 0.96 },
contentWell = { 0.048, 0.05, 0.062, 0.9 },
-- Gold accent (sidebar | content, subnav rules).
separator = { 0.78, 0.62, 0.32, 0.75 },
separatorShadow = { 0, 0, 0, 0.4 },
tabTexActive = { 0.98, 0.94, 0.82 },
tabTexInactive = { 0.78, 0.72, 0.62 },
}
-- Gedeeld kleur-palet (Tier-1 visuele-rust-pass, 16 jun). Eén accent-goud, één
-- rustige link-tint, één dim-grijs voor subtitels, en status-kleuren die ALLEEN
-- voor status zijn (niet voor decoratie). Panelen lezen hieruit i.p.v. elk hun
-- eigen tint te declareren → minder concurrerende kleuren = rustiger beeld.
-- RGB-tabellen voor SetTextColor; *_HEX voor |cff-markup (AARRGGBB, AA=ff).
-- The single source for panel status colours. Until now this table existed but was
-- read by nobody, so sixteen modules each declared their own COLOR_* and quietly
-- drifted apart: two greens, two ambers, two greys, and one link blue living under
-- three different names. The values below are the ones the panels already agreed on,
-- so adopting them changes nothing except the two outliers (DungeonGuide's duller
-- green/amber, WorldContent's duller heading gold), which is the point.
--
-- Status only. Category palettes (keybind roles, profession academy) encode meaning
-- of their own and are deliberately left alone.
ns.UI_COLORS = {
header = { 0.91, 0.76, 0.42 }, -- section headings / titles
gold = { 0.91, 0.76, 0.42 }, -- alias: chrome accents
GOLD_HEX = "ffe8c36a",
dim = { 0.75, 0.78, 0.82 }, -- secondary / explanatory text
DIM_HEX = "ffbfc7d1",
body = { 0.86, 0.87, 0.90 }, -- running text
BODY_HEX = "ffdcdde6",
footer = { 0.45, 0.47, 0.50 }, -- footnotes
FOOTER_HEX = "ff73787f",
good = { 0.45, 0.95, 0.5 }, -- done / ready
GOOD_HEX = "ff73f280",
warn = { 1, 0.84, 0.18 }, -- needs doing now
WARN_HEX = "ffffd62e",
soft = { 0.9, 0.82, 0.45 }, -- in hand, not urgent
SOFT_HEX = "ffe6d173",
prog = { 0.45, 0.85, 0.95 }, -- picked up / in progress
PROG_HEX = "ff73d9f2",
link = { 0.55, 0.78, 1 }, -- every clickable jump, one tint
LINK_HEX = "ff8cc7ff",
bad = { 0.90, 0.42, 0.42 },
BAD_HEX = "ffe66b6b",
}
local function MHUnpack4(t)
return t[1], t[2], t[3], t[4]
end
local function MHTintButtonTextures(btn, r, g, b)
if not btn or not btn.GetRegions then
return
end
for _, region in ipairs({ btn:GetRegions() }) do
if region.GetObjectType and region:IsObjectType("Texture") and region.SetVertexColor then
region:SetVertexColor(r, g, b)
end
end
end
--------------------------------------------------------------------------------
-- 4.0 palette C "Twilight lantern" (Rob's pick, 12 Sep 2026, out of three researched
-- proposals; contrast measured there: body 15.2:1, muted 8.4:1 on the window colour).
-- One table for the shell. The Classic look ignores it and keeps the 3.x Blizzard
-- textures and tints, restored live when the setting is switched.
--------------------------------------------------------------------------------
local LOOK_PALETTE = {
window = { 0.106, 0.086, 0.200, 0.95 }, -- #1B1633; >= ~90% opaque so the world does not eat contrast
sidebar = { 0.075, 0.059, 0.153, 0.97 }, -- #130F27
active = { 0.239, 0.180, 0.471, 1 }, -- #3D2E78
hover = { 0.165, 0.129, 0.314, 1 }, -- #2A2150
chip = { 0.239, 0.180, 0.471, 0.80 }, -- a button at rest: the active fill, a touch see-through
header = { 0.957, 0.871, 0.604 }, -- #F4DE9A
body = { 0.945, 0.933, 0.980 }, -- #F1EEFA
muted = { 0.722, 0.682, 0.859 }, -- #B8AEDB
accent = { 0.788, 0.659, 1.0, 1 }, -- #C9A8FF
row = { 0.137, 0.110, 0.259, 0.90 }, -- #231C42, a list row at rest: between window and hover
}
ns.LOOK_PALETTE = LOOK_PALETTE
local function MHLookOn()
return not (ns.IsClassicLookEnabled and ns:IsClassicLookEnabled())
end
local mhLookFonts
local function MHLookFont(kind)
if not mhLookFonts then
local function make(name, base, c)
local f = CreateFont(name)
f:CopyFontObject(base)
f:SetTextColor(c[1], c[2], c[3])
return f
end
mhLookFonts = {
normal = make("MidnightHelperLookFont", GameFontNormal, LOOK_PALETTE.body),
active = make("MidnightHelperLookFontActive", GameFontNormal, LOOK_PALETTE.header),
hover = make("MidnightHelperLookFontHover", GameFontHighlight, { 1, 1, 1 }),
}
end
return mhLookFonts[kind]
end
--- Blizzard's red panel-button art cannot be tinted indigo (SetVertexColor only multiplies),
--- so the new look fades the template's own textures out and draws a flat fill and a 2 px
--- accent bar of its own. Nothing of the template is removed: Classic fades it back in.
local function MHLookButtonParts(btn)
if btn._mhLookParts then
return btn._mhLookParts
end
local parts = { tex = {} }
for _, region in ipairs({ btn:GetRegions() }) do
if region.IsObjectType and region:IsObjectType("Texture") then
parts.tex[#parts.tex + 1] = region
end
end
local fill = btn:CreateTexture(nil, "BACKGROUND", nil, -8)
fill:SetAllPoints()
fill:Hide()
local bar = btn:CreateTexture(nil, "ARTWORK")
bar:SetPoint("TOPLEFT", btn, "TOPLEFT", 0, 0)
bar:SetPoint("BOTTOMLEFT", btn, "BOTTOMLEFT", 0, 0)
bar:SetWidth(2)
bar:Hide()
parts.fill, parts.bar = fill, bar
btn._mhLookParts = parts
btn:HookScript("OnEnter", function(self)
if self._mhLookOn and not self._mhLookActive then
parts.fill:SetColorTexture(MHUnpack4(LOOK_PALETTE.hover))
parts.fill:Show()
end
end)
btn:HookScript("OnLeave", function(self)
if self._mhLookOn and not self._mhLookActive then
if self._mhLookRest then
parts.fill:SetColorTexture(MHUnpack4(self._mhLookRest))
else
parts.fill:Hide()
end
end
end)
return parts
end
--- style "tab": a flat sidebar row, filled with a bar when active. style "chip": always a
--- filled button (title and search bar).
local function MHLookSkinButton(btn, active, style)
local parts = MHLookButtonParts(btn)
for _, t in ipairs(parts.tex) do
t:SetAlpha(0)
end
btn._mhLookOn = true
btn._mhLookActive = active and true or false
btn._mhLookRest = nil
parts.bar:Hide()
if style == "row" then
-- A list row (the Rares page, 15 Sep 2026): a quiet flat fill, lighter on hover. The row
-- keeps its own scalable font; its text carries its colours as codes.
btn._mhLookRest = LOOK_PALETTE.row
btn._mhLookKeepFont = true
parts.fill:SetColorTexture(MHUnpack4(LOOK_PALETTE.row))
parts.fill:Show()
return
end
if style == "chip" then
btn._mhLookRest = LOOK_PALETTE.chip
parts.fill:SetColorTexture(MHUnpack4(LOOK_PALETTE.chip))
parts.fill:Show()
btn:SetNormalFontObject(MHLookFont("active"))
elseif active then
parts.fill:SetColorTexture(MHUnpack4(LOOK_PALETTE.active))
parts.fill:Show()
parts.bar:SetColorTexture(MHUnpack4(LOOK_PALETTE.accent))
parts.bar:Show()
btn:SetNormalFontObject(MHLookFont("active"))
else
parts.fill:Hide()
btn:SetNormalFontObject(MHLookFont("normal"))
end
btn:SetHighlightFontObject(MHLookFont("hover"))
end
local function MHLookUnskinButton(btn)
local parts = btn._mhLookParts
if not parts or not btn._mhLookOn then
return
end
for _, t in ipairs(parts.tex) do
t:SetAlpha(1)
end
parts.fill:Hide()
parts.bar:Hide()
btn._mhLookOn, btn._mhLookActive, btn._mhLookRest = false, false, nil
if btn._mhLookKeepFont then
btn._mhLookKeepFont = nil
return
end
btn:SetNormalFontObject(GameFontNormal)
btn:SetHighlightFontObject(GameFontHighlight)
end
--- For modules that draw their own buttons in the look (the Rares page, 15 Sep 2026).
ns.MHLookSkinButton = MHLookSkinButton
ns.MHLookUnskinButton = MHLookUnskinButton
ns.MHLookOn = MHLookOn
local function MHRefreshSidebarTabChrome(activeId)
if not ns.tabButtons then
return
end
local lookOn = MHLookOn()
for id, btn in pairs(ns.tabButtons) do
if lookOn then
btn:SetAlpha(1)
MHLookSkinButton(btn, id == activeId, "tab")
else
MHLookUnskinButton(btn)
if id == activeId then
btn:SetAlpha(1)
MHTintButtonTextures(btn, MH_CHROME.tabTexActive[1], MH_CHROME.tabTexActive[2], MH_CHROME.tabTexActive[3])
else
btn:SetAlpha(0.9)
MHTintButtonTextures(btn, MH_CHROME.tabTexInactive[1], MH_CHROME.tabTexInactive[2], MH_CHROME.tabTexInactive[3])
end
end
end
end
local function MHGetLayoutMetrics()
local compact = ns.db and ns.db.settings and ns.db.settings.compactMode == true
return {
compact = compact,
sidebarWidth = compact and 124 or SIDEBAR_WIDTH,
sidebarTabHeight = compact and 24 or 28,
sidebarTabStep = compact and 30 or 34,
addonSubTabHeight = compact and 22 or 24,
addonSubNavHeight = compact and 34 or 40,
addonSubContentTopGap = compact and -4 or -6,
aboutBtnHeight = compact and 20 or ABOUT_BTN_HEIGHT,
aboutBtnWidth = compact and 104 or ABOUT_BTN_WIDTH,
searchBarHeight = compact and 26 or SEARCH_BAR_HEIGHT,
searchResetBtnWidth = compact and 94 or 108,
searchGoBtnWidth = compact and 64 or 72,
infoWindowWidth = compact and 236 or 268,
infoWindowHeight = compact and 180 or 210,
}
end
local function FitTitleBarButton(btn, minW, maxW)
if not btn or not btn.GetFontString then
return
end
local fs = btn:GetFontString()
if not fs or not fs.GetStringWidth then
return
end
minW = minW or 56
maxW = maxW or 120
local w = math.ceil((fs:GetStringWidth() or 0) + 14)
btn:SetWidth(math.min(maxW, math.max(minW, w)))
end
local function FitSidebarTabButton(btn, sidebarWidth)
if not btn or not btn.GetFontString then
return
end
local fs = btn:GetFontString()
if not fs or not fs.GetStringWidth or not fs.SetFont then
return
end
-- Capture the pristine font size ONCE. Without this, every relayout read the
-- already-shrunk size and shrank again, so long labels (deDE/frFR) kept getting
-- smaller each time the sidebar re-laid out (F3.8).
if not btn._mhBaseFont then
local font, size, flags = fs:GetFont()
if not (font and size) then
return -- font not ready yet
end
btn._mhBaseFont = { font = font, size = size, flags = flags }
end
local base = btn._mhBaseFont
local maxW = (sidebarWidth or MHGetLayoutMetrics().sidebarWidth) - 16
-- Always start from the base size, then shrink to fit (non-cumulative).
fs:SetFont(base.font, base.size, base.flags)
local size = base.size
local textW = fs:GetStringWidth() or 0
while textW + 24 > maxW and size > 9 do
size = size - 1
fs:SetFont(base.font, size, base.flags)
textW = fs:GetStringWidth() or textW
end
btn:SetWidth(maxW)
end
-- Top-level beta-gated tabs. macros/academy (→ Toolbox sub-tabs) and
-- reference (→ Codex category) are no longer top-level; their beta keys still
-- gate the merged locations via ns.IsBetaTabEnabled, so the Settings
-- checkboxes keep working.
-- Beta-badges uitgezet (Rob, 3 jul 2026): geen zichtbare "Beta"-markering meer op de
-- tabs. De gating-logica (ns.IsBetaTabEnabled / Settings-checkboxes) blijft intact —
-- dit haalt alleen de badge + beta-tooltip weg. Weer aanzetten? Vul tab-id's aan.
local MH_BETA_TAB_IDS = {}
-- Sidebar is grouped into labelled sections (header + tabs). Tab ids whose
-- buttons do not exist yet (home, ritual) are skipped during layout, which
-- reserves their slot for later phases without breaking the current build.
-- 1.9.0 Phase 2 (4-kamer-model, stap 1 = hergroeperen): de zijbalk volgt nu de
-- blueprint-kamers. "Me" is gesplitst in This week / My characters; Codex bundelt
-- alle kennis (gidsen); Tools en Settings staan apart. Titels hergebruiken
-- bestaande locale-keys (TAB_CODEX/TAB_SETTINGS), dus geen nieuwe sleutels nodig.
-- Stap 2 maakt hier klikbare icoon-kamerknoppen van. Home blijft de default/
-- fallback-tab in SelectTab (sidebar-volgorde staat daar los van).
local SIDEBAR_SECTIONS = {
{ key = "me_now", room = "me", titleKey = "SIDEBAR_SECTION_WEEK", ids = { "home", "starthere", "delves", "rares", "world", "events" } },
-- The old single "Character" list was an eight-noun wall with no scannable bucket:
-- someone hunting a collectible mount had nowhere obvious to look. Four named
-- groups; no tab moved out of reach (Rob, 10 jul).
{ key = "me_collections", room = "me", titleKey = "SIDEBAR_SECTION_COLLECTIONS", ids = { "mounts", "tradingpost", "achievements" } },
{ key = "me_gear", room = "me", titleKey = "SIDEBAR_SECTION_GEAR", ids = { "enchants", "tier" } },
{ key = "me_resources", room = "me", titleKey = "SIDEBAR_SECTION_RESOURCES", ids = { "currency", "omnium" } },
{ key = "me_alts", room = "me", titleKey = "SIDEBAR_SECTION_ALTS", ids = { "account", "delvelog" } },
{ key = "codex", room = "codex", titleKey = "TAB_CODEX", ids = { "codex", "dungeons", "raids", "guide", "smcguide" } },
{ key = "tools", room = "tools", titleKey = "SIDEBAR_SECTION_TOOLS", ids = { "toolslaunch", "toolbox", "addons" } },
{ key = "settings", room = "settings", titleKey = "TAB_SETTINGS", ids = { "settings" } },
}
-- 1.9.0 Phase 2 stap 2: klikbare icoon-kamerknoppen bovenaan de zijbalk. Elke
-- kamer toont alleen z'n eigen secties; de actieve kamer wordt AFGELEID van de
-- huidige tab (geen aparte staat om te syncen). Klikken op een kamer opent de
-- eerste zichtbare tab erin. Labels: SIDEBAR_ROOM_ME/_CODEX nieuw; Tools/Settings
-- hergebruiken bestaande keys.
local SIDEBAR_ROOMS = {
{ id = "me", labelKey = "SIDEBAR_ROOM_ME", icon = "Interface\\Icons\\Achievement_Character_Human_Male", defaultTab = "home" },
{ id = "codex", labelKey = "SIDEBAR_ROOM_CODEX", icon = "Interface\\Icons\\INV_Misc_Book_09", defaultTab = "codex" },
{ id = "tools", labelKey = "SIDEBAR_ROOM_TOOLS", icon = "Interface\\Icons\\Trade_Engineering", defaultTab = "toolslaunch" },
{ id = "settings", labelKey = "TAB_SETTINGS", icon = "Interface\\Icons\\INV_Misc_Gear_01", defaultTab = "settings" },
}
local SIDEBAR_ROOM_BY_ID = {}
for _, r in ipairs(SIDEBAR_ROOMS) do
SIDEBAR_ROOM_BY_ID[r.id] = r
end
local function MHRoomForTab(tabId)
if tabId == "screens" or tabId == "allsettings" then
-- The 4.0 Screens page and the All settings page (Modules/SettingsPage.lua) sit in the
-- Settings room.
return "settings"
end
-- 4.0 room card grids (Modules/RoomLauncher.lua) are panels named "room_<room>".
local launcherRoom = type(tabId) == "string" and tabId:match("^room_(%a+)$")
if launcherRoom and SIDEBAR_ROOM_BY_ID[launcherRoom] then
return launcherRoom
end
for _, section in ipairs(SIDEBAR_SECTIONS) do
for _, id in ipairs(section.ids) do
if id == tabId then
return section.room
end
end
end
return nil
end
local function MHActiveRoom()
return MHRoomForTab(ns.uiSelectedTab) or "me"
end
-- Phase 3 cross-links: interactieve tabs met een Codex-tegenhanger. De
-- titelbalk-knop "Read in Codex" springt naar die Codex-categorie.
local TAB_TO_CODEX = {
delves = "delves",
dungeons = "dungeons",
world = "world",
currency = "currencies",
omnium = "weekly",
}
local SIDEBAR_SECTION_GAP = 10
local SIDEBAR_HEADER_HEIGHT = 16
-- (Simpele modus / Tier 3 uitgefaseerd in Phase 2 — de kamer-rail verving 'm; alle
-- resten opgeruimd in F3.8.)
--- Can this screen exist here: on this client, with the beta switches, under the Guide's own
--- level rule? Not the player's show/hide choice (see SidebarTabVisible), so anything deciding
--- whether a tab may be OPEN asks this one.
local function SidebarTabAvailable(tabId)
if tabId == "omnium" and not (ns.IsOmniumFolioAvailable and ns.IsOmniumFolioAvailable()) then
return false -- 12.0.7-content: alleen op clients >= 120007
end
if tabId == "guide" then
return ns.IsBetaTabEnabled and ns.IsBetaTabEnabled("guide")
end
if MH_BETA_TAB_IDS[tabId] then
return ns.IsBetaTabEnabled and ns.IsBetaTabEnabled(tabId)
end
return true
end
-- The Toolbox's sub-tabs are screens of their own for show/hide, as on the 4.0 Tools cards.
local TOOLBOX_SCREENS = {
{ id = "consumables", labelKey = "TAB_CONSUMABLES" },
{ id = "macros", labelKey = "TAB_MACROS" },
{ id = "academy", labelKey = "TAB_ACADEMY" },
{ id = "professionsHub", labelKey = "TAB_PROFESSIONS" },
}
--- Listed in the sidebar, the room cards and the favourites menu: available AND not hidden by
--- the player (4.0 per-screen show/hide, Core.lua). The Toolbox tab stays while any of its
--- sub-tabs is shown.
local function SidebarTabVisible(tabId)
if not SidebarTabAvailable(tabId) then
return false
end
if tabId == "toolbox" then
for _, s in ipairs(TOOLBOX_SCREENS) do
if not (ns.IsScreenHidden and ns.IsScreenHidden(s.id)) then
return true
end
end
return false
end
return not (ns.IsScreenHidden and ns.IsScreenHidden(tabId))
end
local function MHAttachTabBetaBadge(btn, tabId)
if not btn or not MH_BETA_TAB_IDS[tabId] then
return
end
if not btn._mhBetaBadge then
local badge = btn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
badge:SetPoint("TOPRIGHT", btn, "TOPRIGHT", -3, -1)
badge:SetTextColor(1, 0.72, 0.15)
btn._mhBetaBadge = badge
btn:HookScript("OnEnter", function(self)
GameTooltip:SetOwner(self, "ANCHOR_CURSOR")
GameTooltip:AddLine(ns:L("TAB_BETA_TOOLTIP_TITLE"), 1, 0.82, 0)
GameTooltip:AddLine(ns:L("TAB_BETA_TOOLTIP_BODY"), 0.92, 0.92, 0.92, true)
GameTooltip:Show()
end)
btn:HookScript("OnLeave", function()
GameTooltip:Hide()
end)
end
btn._mhBetaBadge:SetText(ns:L("TAB_BETA_BADGE"))
end
local function MHGetInfoBodyKeyForTab(tabId)
if tabId == "codex" then
-- The embedded Reference category keeps its own help text.
if ns.GetActiveCodexCategory and ns.GetActiveCodexCategory() == "reference" then
return "INFO_DRAWER_BODY_REFERENCE"
end
return "INFO_DRAWER_BODY_CODEX"
elseif tabId == "home" then
return "INFO_DRAWER_BODY_HOME"
elseif tabId == "starthere" then
return "INFO_DRAWER_BODY_STARTHERE"
elseif tabId == "dungeons" then
return "INFO_DRAWER_BODY_DUNGEONS"
elseif tabId == "events" then
return "INFO_DRAWER_BODY_EVENTS"
elseif tabId == "omnium" then
return "INFO_DRAWER_BODY_OMNIUM"
elseif tabId == "toolslaunch" then
return "INFO_DRAWER_BODY_TOOLSLAUNCH"
elseif tabId == "delves" then
return "INFO_DRAWER_BODY_DELVES"
elseif tabId == "account" then
return "INFO_DRAWER_BODY_ACCOUNT"
elseif tabId == "rares" then
return "INFO_DRAWER_BODY_RARES"
elseif tabId == "achievements" then
return "INFO_DRAWER_BODY_ACHIEVEMENTS"
elseif tabId == "world" then
return "INFO_DRAWER_BODY_WORLD"
elseif tabId == "delvelog" then
return "INFO_DRAWER_BODY_DELVELOG"
elseif tabId == "enchants" then
return "INFO_DRAWER_BODY_ENCHANTS"
elseif tabId == "tier" then
return "INFO_DRAWER_BODY_TIER"
elseif tabId == "smcguide" then
return "INFO_DRAWER_BODY_SMC"
elseif tabId == "currency" then
return "INFO_DRAWER_BODY_CURRENCY"
elseif tabId == "professions" then
return "INFO_DRAWER_BODY_PROFESSIONS"
elseif tabId == "guide" then
return "INFO_DRAWER_BODY_GUIDE"
elseif tabId == "toolbox" then
-- Per-sub-tab help text inside the merged Toolbox tab.
local sid = ns.uiSelectedToolboxSubTab or "consumables"
if sid == "macros" then
return "INFO_DRAWER_BODY_MACROS"
elseif sid == "academy" then
return "INFO_DRAWER_BODY_ACADEMY"
elseif sid == "professionsHub" then
local inner = ns.uiSelectedProfHubInner or "overview"
if inner == "treasures" then
return "INFO_DRAWER_BODY_PROFESSIONS"
elseif inner == "course" then
return "INFO_DRAWER_BODY_PROFACADEMY"
end
return "INFO_DRAWER_BODY_PROFHUB"
end
return "INFO_DRAWER_BODY_CONSUMABLES"
elseif tabId == "mounts" then
-- No drawer text of their own (they fell back to Home's); each panel's subtitle
-- says what the screen is and exists in all seven packs.
return "MOUNTS_PANEL_SUBTITLE"
elseif tabId == "tradingpost" then
return "TRADINGPOST_SUBTITLE"
elseif tabId == "raids" then
return "RAIDS_PANEL_SUBTITLE"
elseif tabId == "addons" then
return "INFO_DRAWER_BODY_ADDONS"
elseif tabId == "settings" then
return "INFO_DRAWER_BODY_SETTINGS"
elseif tabId == "screens" then
return "SCREENS_PANEL_INTRO"
elseif tabId == "allsettings" then
return "SET_ALL_INTRO"
end
return "INFO_DRAWER_BODY_HOME"
end
--------------------------------------------------------------------------------
-- 4.0.0 look (Spec 37 concept A): a strip above the content column with the screen's
-- own icon and one line on what the screen is for. Built once for every tab, so no panel
-- needed an edit. The name is left out on purpose: nearly every panel draws its own
-- title, and the title bar already reads "Room > Tab".
-- The Classic setting (ns:IsClassicLookEnabled) hides it and puts the content back on
-- the level bar exactly as 3.x had it -- the way back Rob asked about, and the exit for
-- anyone who does not want the AI-made icons.
--------------------------------------------------------------------------------
-- Screen id -> icon stem (Media/Icons/<stem>_64.png) and tagline key. The Toolbox shows
-- its active sub-tab. Keys are literal so the linter can check that each one exists.
local LOOK_SCREENS = {
starthere = { stem = "starthere", tagline = "TAB_TAGLINE_STARTHERE" },
dungeons = { stem = "dungeons", tagline = "TAB_TAGLINE_DUNGEONS" },
codex = { stem = "codex", tagline = "TAB_TAGLINE_CODEX" },
home = { stem = "home", tagline = "TAB_TAGLINE_HOME" },
delves = { stem = "delves", tagline = "TAB_TAGLINE_DELVES" },
account = { stem = "account", tagline = "TAB_TAGLINE_ACCOUNT" },
rares = { stem = "rares", tagline = "TAB_TAGLINE_RARES" },
achievements = { stem = "achievements", tagline = "TAB_TAGLINE_ACHIEVEMENTS" },
mounts = { stem = "mounts", tagline = "TAB_TAGLINE_MOUNTS" },
tradingpost = { stem = "tradingpost", tagline = "TAB_TAGLINE_TRADINGPOST" },
raids = { stem = "raids", tagline = "TAB_TAGLINE_RAIDS" },
world = { stem = "world", tagline = "TAB_TAGLINE_WORLD" },
events = { stem = "events", tagline = "TAB_TAGLINE_EVENTS" },
delvelog = { stem = "delvelog", tagline = "TAB_TAGLINE_DELVELOG" },
enchants = { stem = "enchants", tagline = "TAB_TAGLINE_ENCHANTS" },
tier = { stem = "tier", tagline = "TAB_TAGLINE_TIER" },
omnium = { stem = "omnium", tagline = "TAB_TAGLINE_OMNIUM" },
smcguide = { stem = "smcguide", tagline = "TAB_TAGLINE_SMCGUIDE" },
currency = { stem = "currency", tagline = "TAB_TAGLINE_CURRENCY" },
guide = { stem = "guide", tagline = "TAB_TAGLINE_GUIDE" },
toolslaunch = { stem = "toolslaunch", tagline = "TAB_TAGLINE_TOOLSLAUNCH" },
addons = { stem = "addons", tagline = "TAB_TAGLINE_ADDONS" },
settings = { stem = "settings", tagline = "TAB_TAGLINE_SETTINGS" },
screens = { stem = "settings", tagline = "TAB_TAGLINE_SCREENS" },
allsettings = { stem = "settings", tagline = "TAB_TAGLINE_ALLSETTINGS" },
consumables ={ stem = "consumables", tagline = "TAB_TAGLINE_CONSUMABLES" },
macros = { stem = "macros", tagline = "TAB_TAGLINE_MACROS" },
academy = { stem = "academy", tagline = "TAB_TAGLINE_ACADEMY" },
professionsHub = { stem = "professions", tagline = "TAB_TAGLINE_PROFESSIONSHUB" },
-- The three room card grids (Modules/RoomLauncher.lua).
room_me = { stem = "home", tagline = "TAB_TAGLINE_ROOM_ME" },
room_codex = { stem = "codex", tagline = "TAB_TAGLINE_ROOM_CODEX" },
room_tools = { stem = "toolslaunch", tagline = "TAB_TAGLINE_ROOM_TOOLS" },
}
local LOOK_ICON_PATH = "Interface\\AddOns\\MidnightHelper\\Media\\Icons\\%s_64.png"
local LOOK_HEADER_H = 64
local LOOK_HEADER_H_COMPACT = 54
--- Anchors the 4.0 header (when shown) and the content column below the level bar.
--- The one place that decides where content starts: compact mode, the Classic setting
--- and the first build all go through here.
local function MHAnchorContentColumn(refs, m)
local top = refs.levelBar or refs.favRow or refs.searchBar
local header = refs.lookHeader
local useHeader = header ~= nil and not (ns.IsClassicLookEnabled and ns:IsClassicLookEnabled())
if header then
local h = m.compact and LOOK_HEADER_H_COMPACT or LOOK_HEADER_H
header:ClearAllPoints()
header:SetPoint("TOPLEFT", top, "BOTTOMLEFT", m.sidebarWidth, 0)
header:SetPoint("TOPRIGHT", top, "BOTTOMRIGHT", 0, 0)
header:SetHeight(h)
if header._mhIcon then
header._mhIcon:SetSize(h - 10, h - 10)
end
header:SetShown(useHeader)
end
if refs.content then
refs.content:ClearAllPoints()
if useHeader then
refs.content:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 0, 0)
else
refs.content:SetPoint("TOPLEFT", top, "BOTTOMLEFT", m.sidebarWidth, 0)
end
refs.content:SetPoint("BOTTOMRIGHT", refs.main, "BOTTOMRIGHT", -MH_MAIN_EDGE.R, MH_MAIN_EDGE.B)
end
end
-- The shell surfaces: flat palette colours in the new look, and the exact 3.x Blizzard
-- dialog textures + tints (or flat colour) in Classic.
local LOOK_SURFACES = {
{ ref = "titleTex", flat = "sidebar", texture = MH_TEX.dialogBgDark, vertex = { 0.55, 0.45, 0.35, 0.9 } },
{ ref = "searchBg", flat = "sidebar", texture = MH_TEX.dialogBgDark, vertex = { 0.22, 0.2, 0.24, 0.92 } },
{ ref = "favBg", flat = "sidebar", color = { 0.16, 0.15, 0.18, 0.85 } },
{ ref = "sidebarBg", flat = "sidebar", texture = MH_TEX.dialogBgDark, vertex = { 0.34, 0.32, 0.38, 0.94 } },
{ ref = "contentBg", flat = "window", texture = MH_TEX.dialogBg, vertex = { 0.42, 0.44, 0.52, 0.72 } },
}
local function MHLookSurface(tex, s, lookOn)
if not tex then
return
end
if lookOn then
tex:SetColorTexture(MHUnpack4(LOOK_PALETTE[s.flat]))
tex:SetVertexColor(1, 1, 1, 1)
elseif s.texture then
tex:SetTexture(s.texture)
if tex.SetHorizTile then
tex:SetHorizTile(true)
end
if tex.SetVertTile then
tex:SetVertTile(true)
end
tex:SetVertexColor(MHUnpack4(s.vertex))
else
tex:SetColorTexture(MHUnpack4(s.color))
tex:SetVertexColor(1, 1, 1, 1)
end
end
--- Applies the palette (or restores 3.x) to the shell, once per change of the Classic
--- setting; the sidebar rows and room buttons follow on the relayout it forces.
local function MHApplyLookChrome(refs)
local lookOn = MHLookOn()
-- The header switch must follow every change, also one made in Settings, so it is set before
-- the early return below.
if ns._mhRefreshLookToggle then
ns._mhRefreshLookToggle()
end
if refs._mhLookApplied == lookOn then
return
end
refs._mhLookApplied = lookOn
for _, s in ipairs(LOOK_SURFACES) do
MHLookSurface(refs[s.ref], s, lookOn)
end
if refs.titleText then
local c = lookOn and LOOK_PALETTE.header or { 1, 0.82, 0 }
refs.titleText:SetTextColor(c[1], c[2], c[3])
end
for _, btn in ipairs({ refs.infoToggleBtn or false, refs.aboutBtn or false, refs.lookToggleBtn or false, ns._mhCodexLinkBtn or false,
refs.searchResetBtn or false, refs.searchGoBtn or false }) do
if btn then
if lookOn then
MHLookSkinButton(btn, false, "chip")
else
MHLookUnskinButton(btn)
end
end
end
MHRefreshSidebarTabChrome(ns.uiSelectedTab)
-- The Silvermoon tab's pins follow the setting in place: 4.0 cards, or the 3.x buttons.
if ns.MH_RelayoutSMCPins then
ns.MH_RelayoutSMCPins()
end
-- The Rares page redraws its rows, rail and buttons in the look that is on now.
if ns.RefreshRaresPanel then
ns.RefreshRaresPanel()
end
if ns._mhRelayoutSidebarTabs and not ns._mhSidebarRelaying then
ns._mhRelayoutSidebarTabs()
end
-- Als laatste: het open scherm naar de weergave brengen die in deze look bestaat. Dit kan
-- SelectTab aanroepen en dus hier weer binnenkomen; de `_mhLookApplied`-poort hierboven staat
-- dan al op de nieuwe waarde, dus die tweede keer keert meteen terug.
if ns._mhRerouteForLook then
ns._mhRerouteForLook(lookOn)
end
end
--- Re-anchors (so a Classic value loaded after the window was built still wins) and
--- fills the header for the open screen. Called on every tab and Toolbox sub-tab switch
--- and on a language change.
function ns:RefreshLookHeader()
local refs = self._mhLayoutRefs
local header = refs and refs.lookHeader
if not header then
return
end
MHAnchorContentColumn(refs, MHGetLayoutMetrics())
MHApplyLookChrome(refs)
if not header:IsShown() then
return
end
local sid = self.uiSelectedTab or "home"
if sid == "toolbox" then
sid = self.uiSelectedToolboxSubTab or "consumables"
end
local screen = LOOK_SCREENS[sid] or LOOK_SCREENS.home
header._mhIcon:SetTexture(LOOK_ICON_PATH:format(screen.stem))
header._mhText:SetText(self:L(screen.tagline))
end
--- Apply ns:L() to the main shell (tabs, search row, SMC headers, side helpers). See Locales/*.lua.
function ns:RefreshLocaleUI()
if self.ApplyBindingLabels then
self:ApplyBindingLabels()
end
local r = self._mhLocaleRefs
if not r then
return
end
if r.title and r.title.SetText then
r.title:SetText(self:L("MAIN_TITLE"))
end
if r.titleVersion and r.titleVersion.SetText then
local ver = ns.GetAddonVersion and ns.GetAddonVersion() or "?"
r.titleVersion:SetText(self:L("MAIN_TITLE_VERSION_FMT"):format(ver))
end
if r.searchHint and r.searchHint.SetText then
r.searchHint:SetText(self:L("SEARCH_LABEL"))
end
if ns.mhSearchPlaceholder then
ns.mhSearchPlaceholder:SetText(self:L("SEARCH_PLACEHOLDER"))
end
if r.searchResetBtn and r.searchResetBtn.SetText then
r.searchResetBtn:SetText(self:L("SEARCH_MY_CHARACTER"))
end
if r.searchGoBtn and r.searchGoBtn.SetText then
r.searchGoBtn:SetText(self:L("SEARCH_GO"))
end
if r.aboutBtn and r.aboutBtn.SetText then
r.aboutBtn:SetText(self:L("ABOUT_BUTTON"))
end
if ns._mhRefreshLookToggle then
ns._mhRefreshLookToggle()
end
if r.tabKeys and self.tabButtons then
for id, btn in pairs(self.tabButtons) do
local key = r.tabKeys[id]
if key and btn and btn.SetText then
btn:SetText(self:EscapeButtonAmpersand(self:L(key)))
FitSidebarTabButton(btn)
end
end
end
if r.smcHeader and r.smcHeader.SetText then
r.smcHeader:SetText(self:L("SMC_GUIDE_TITLE"))
end
if r.smcSubtitle and r.smcSubtitle.SetText then
r.smcSubtitle:SetText(self:L("SMC_GUIDE_SUBTITLE"))
end
local sg = self.panels and self.panels.smcguide
local clRef = sg and sg._mhChecklistLocaleRefs
if type(clRef) == "table" then
if clRef.header and clRef.header.SetText then
clRef.header:SetText(self:L("SMC_CHECKLIST_TITLE"))
end
if clRef.hint and clRef.hint.SetText then
clRef.hint:SetText(self:L("SMC_CHECKLIST_HINT"))
end
local rows = clRef.rows
if type(rows) == "table" then
for i = 1, #rows do
local row = rows[i]
if row and row.titleFs and row.entry and row.entry.labelKey then
row.titleFs:SetText(self:L(row.entry.labelKey))
end
end
end
end
if ns.SMC_RefreshDynamicChecklist then
ns.SMC_RefreshDynamicChecklist()
end
if ns.MH_RefreshRoleAcademyPanel and self.panels and self.panels.academy then
ns.MH_RefreshRoleAcademyPanel(self.panels.academy)
end
if r.guideSubTabButtons then
local bg = r.guideSubTabButtons.guide
local bl = r.guideSubTabButtons.layout
if bg and bg.SetText then
bg:SetText(self:L("TAB_GUIDE_SUB_GUIDE"))
end
if bl and bl.SetText then
bl:SetText(self:L("TAB_GUIDE_SUB_LAYOUT"))
end
end
-- Toolbox sub-tab buttons + sub-panel headers (their ids are no longer in
-- TAB_DEFS, so the generic tab relabel loop above does not cover them).
if ns.toolboxSubTabButtons and ns._mhToolboxSubTabDefs then
for _, def in ipairs(ns._mhToolboxSubTabDefs) do
local btn = ns.toolboxSubTabButtons[def.id]
if btn and btn.SetText then
btn:SetText(self:L(def.labelKey))
end
local panel = self.panels and self.panels[def.id]
if panel and panel._header and panel._header.SetText then
panel._header:SetText(self:L(def.labelKey))
end
end
if ns.RelayoutToolboxSubNav then
ns.RelayoutToolboxSubNav()
end
end
if self.uiSelectedTab == "guide" and self._mhGuideRefresh then
self:_mhGuideRefresh()
end
local macroPanel = self.panels and self.panels.macros
if macroPanel and macroPanel._mhRefreshMacros then
macroPanel._mhRefreshMacros()
end
local consPanel = self.panels and self.panels.consumables
if consPanel and consPanel._mhRefreshConsumables then
consPanel._mhRefreshConsumables()
end
local layoutPanel = self._mhGuideLayoutPanel
if layoutPanel and layoutPanel._mhProtoBuilt and ns.KeyboardLayoutPrototype_Refresh then
ns.KeyboardLayoutPrototype_Refresh(layoutPanel)
end
if self.RefreshLookHeader then
self:RefreshLookHeader()
end
if self._mhRefreshSidePanel then
self:_mhRefreshSidePanel(self.uiSelectedTab or "delves")
end
end
function ns:ApplyCompactMode()
local refs = self._mhLayoutRefs
if not refs or not refs.main then
return
end
local m = MHGetLayoutMetrics()