Repository navigation
Expand file tree
/
Copy pathCore.lua
More file actions
1236 lines (1122 loc) · 44.7 KB
/
Copy pathCore.lua
File metadata and controls
1236 lines (1122 loc) · 44.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
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
-- Dan's Raid Frames: Quick-access "/drf" window for all the Raid & Party Frame
-- options buried inside Blizzard's Edit Mode, with live 60 FPS resizing and zero 12.0 taint.
--
-- Party Frames and Raid Frames each have their own profile (ns.db.party / ns.db.raid).
--
-- IMPORTANT (WoW 12.0 / Forever secret-value rules):
-- 1. NEVER call Blizzard Lua methods on PartyFrame, CompactPartyFrame, CompactUnitFrame,
-- or EditModeManagerFrame. Doing so writes addon-tainted values into PartyFrame.settingMap /
-- frame.optionTable / frame.unit, causing secret-value errors in combat.
-- 2. To persist settings: we only read/write a fresh table from C_EditMode.GetLayouts()
-- and pass it to C_EditMode.SaveLayouts(layoutInfo) (a pure C++ call).
-- 3. To update frames LIVE on screen at 60 FPS: we only call pure C++ widget methods
-- (GetAttribute, SetSize, ClearAllPoints, SetPoint, SetShown) on the frames.
local ADDON_NAME, ns = ...
local PROFILE_DEFAULTS = {
useHorizontalGroups = false,
displayBorder = false,
sortBy = 0, -- 0 = Role, 1 = Group, 2 = Alphabetical
alignment = 0, -- party only: 0 = Left/Top, 1 = Center, 2 = Right/Bottom within the 5 slots
auraOrganizationType = 0, -- 0 = Legacy, 1 = Buffs Top, 2 = Buffs Bottom
frameWidth = 72,
frameHeight = 36,
opacity = 100,
bigDefensiveSize = 100,
buffSize = 100,
iconSize = 100,
}
local DEFAULTS = {
selectedMode = "party", -- "party" or "raid"
showRaidFramesInGroups = true,
}
local GLOBAL_KEYS = {
selectedMode = true,
showRaidFramesInGroups = true,
partyPos = true,
raidPos = true,
}
local LAYOUT_NAME = "DansRaidFrames"
local PREFIX = "|cff33ff99Dan's Raid Frames|r: "
-- Blizzard's Edit Mode slider limits for raid-style party/raid frames: { min, max, step }.
local RANGES = {
width = { 72, 144, 2 },
height = { 36, 72, 2 },
opacity = { 50, 100, 1 },
bigDefensiveSize = { 50, 200, 10 },
buffSize = { 50, 200, 10 },
iconSize = { 50, 200, 10 },
}
local ROLE_WEIGHT = {
TANK = 1,
HEALER = 2,
DAMAGER = 3,
NONE = 4,
}
-- Global names of Blizzard's compact unit frames, built once instead of
-- concatenating strings on every layout pass.
local PARTY_SLOTS = 5
local PARTY_MEMBER_NAMES = {}
for i = 1, PARTY_SLOTS do
PARTY_MEMBER_NAMES[i] = "CompactPartyFrameMember" .. i
end
local RAID_GROUP_NAMES, RAID_GROUP_MEMBER_NAMES = {}, {}
for g = 1, 8 do
RAID_GROUP_NAMES[g] = "CompactRaidGroup" .. g
RAID_GROUP_MEMBER_NAMES[g] = {}
for m = 1, 5 do
RAID_GROUP_MEMBER_NAMES[g][m] = "CompactRaidGroup" .. g .. "Member" .. m
end
end
local RAID_FRAME_NAMES = {}
for i = 1, 40 do
RAID_FRAME_NAMES[i] = "CompactRaidFrame" .. i
end
local GetCVarValue = (C_CVar and C_CVar.GetCVar) or GetCVar
local SetCVarValue = (C_CVar and C_CVar.SetCVar) or SetCVar
function ns.Print(msg)
DEFAULT_CHAT_FRAME:AddMessage(PREFIX .. tostring(msg))
end
local function OnOff(value)
return value and "|cff00ff00ON|r" or "|cffff0000OFF|r"
end
local function GetSettingDisplayInfo(settingID)
if not settingID or not EditModeSettingDisplayInfoManager
or not EditModeSettingDisplayInfoManager.systemSettingDisplayInfo
or not Enum or not Enum.EditModeSystem then
return nil
end
local list = EditModeSettingDisplayInfoManager.systemSettingDisplayInfo[Enum.EditModeSystem.UnitFrame]
if not list then
return nil
end
for _, setup in ipairs(list) do
if setup.setting == settingID then
return setup
end
end
return nil
end
local function GetDynamicRange(settingID, fallback)
local setup = GetSettingDisplayInfo(settingID)
if setup and type(setup.minValue) == "number" and type(setup.maxValue) == "number" then
return { setup.minValue, setup.maxValue, setup.stepSize or fallback[3] or 1 }
end
return { unpack(fallback) }
end
function ns.GetSizeRanges()
local S = Enum and Enum.EditModeUnitFrameSetting or {}
return {
width = GetDynamicRange(S.FrameWidth, RANGES.width),
height = GetDynamicRange(S.FrameHeight, RANGES.height),
opacity = GetDynamicRange(S.Opacity, RANGES.opacity),
bigDefensiveSize = GetDynamicRange(S.BigDefensiveIconSize, RANGES.bigDefensiveSize),
buffSize = GetDynamicRange(S.BuffIconSize, RANGES.buffSize),
iconSize = GetDynamicRange(S.DebuffIconSize, RANGES.iconSize),
}
end
local function Snap(value, range)
local min, max, step = range[1], range[2], range[3]
value = math.max(min, math.min(max, value))
return min + math.floor((value - min) / step + 0.5) * step
end
function ns.OpacityToAlpha(opacity)
return math.max(0.5, math.min(1.0, opacity / 100))
end
---------------------------------------------------------------------------
-- Pure C++ Live Visual Updater (Party & Raid Frames, zero taint)
---------------------------------------------------------------------------
local function PassThroughUnitSubframes(unitFrame)
if not unitFrame then
return
end
local function DisableClicksOnList(list)
if type(list) ~= "table" then
return
end
for i = 1, #list do
local sub = list[i]
if sub and sub.SetMouseClickEnabled then
sub:SetMouseClickEnabled(false)
end
end
end
DisableClicksOnList(unitFrame.buffFrames)
DisableClicksOnList(unitFrame.debuffFrames)
DisableClicksOnList(unitFrame.dispelDebuffFrames)
if unitFrame.CenterDefensiveBuff and unitFrame.CenterDefensiveBuff.SetMouseClickEnabled then
unitFrame.CenterDefensiveBuff:SetMouseClickEnabled(false)
end
end
local function DisableMouseOnDecoration(frame)
if not frame then
return
end
if frame.EnableMouse then
frame:EnableMouse(false)
end
if frame.SetMouseClickEnabled then
frame:SetMouseClickEnabled(false)
end
end
local function UpdateStandardPartyFramesShown(showStandard)
if not PartyFrame then
return
end
-- Keep PartyFrame shown because CompactPartyFrame is parented to it
PartyFrame:SetShown(true)
if PartyFrame.Background then
PartyFrame.Background:SetShown(false)
end
for i = 1, 4 do
local mf = PartyFrame["MemberFrame" .. i]
if mf then
if showStandard then
local unit = "party" .. i
mf:SetShown(UnitExists(unit) and true or false)
if mf.EnableMouse then mf:EnableMouse(true) end
else
mf:SetShown(false)
if mf.EnableMouse then mf:EnableMouse(false) end
end
end
end
end
local RAID_GROUP_GAP = 6
-- Rounds a size in a frame's UI units to a whole number of screen pixels. At UI scales that don't
-- map units to whole pixels (e.g. 0.64 at 1440p: 1 unit = 1.2 px), a 72-unit frame is 86.4 px, and
-- identical frames get drawn 86 or 87 px wide depending on where their edges fall. With whole-pixel
-- sizes every edge shares the same sub-pixel offset, so all frames round the same way.
local function SnapToPixels(size, frame)
local _, physH = GetPhysicalScreenSize()
if not frame or not physH or physH <= 0 then
return size
end
local pixelsPerUnit = frame:GetEffectiveScale() * physH / 768
if pixelsPerUnit <= 0 then
return size
end
return math.max(1, math.floor(size * pixelsPerUnit + 0.5)) / pixelsPerUnit
end
-- Frame-group sizes, shared by the layout code below and by the unlock-mode outline
-- (ns.GetFullGroupSize) so the outline always matches what a full group would look like.
local function GetRaidGroupTitleHeight(groupFrame, groupName)
local title = groupFrame and (groupFrame.title or _G[groupName .. "Title"])
return (title and title.GetHeight and title:GetHeight()) or 14
end
-- One raid group: 5 members plus its title and border padding.
local function GetRaidGroupSize(w, h, horiz, border, titleH)
return (horiz and (w * 5) or w) + (border and 14 or 0),
(horiz and h or (h * 5)) + titleH + (border and 12 or 2)
end
-- The party container always has room for PARTY_SLOTS members.
local function GetPartyContainerSize(w, h, horiz, border, titleH)
local padW = border and 14 or 0
local padH = border and 12 or 0
if horiz then
return w * PARTY_SLOTS + padW, h + titleH + 4 + padH
end
return w + padW, h * PARTY_SLOTS + titleH + 4 + padH
end
local function ApplyLiveToRaidFrames(w, h, horiz, border, alpha)
local container = _G["CompactRaidFrameContainer"]
w, h = SnapToPixels(w, container), SnapToPixels(h, container)
local gap = SnapToPixels(RAID_GROUP_GAP, container)
-- 1. Grouped raid frames (CompactRaidGroup1..8 and their Member1..5)
local shownGroups = {}
for g, groupName in ipairs(RAID_GROUP_NAMES) do
local groupFrame = _G[groupName]
if groupFrame then
DisableMouseOnDecoration(groupFrame)
local titleH = GetRaidGroupTitleHeight(groupFrame, groupName)
local padX = border and 7 or 0
local padY = border and 5 or 0
local prevMember = nil
for _, memberName in ipairs(RAID_GROUP_MEMBER_NAMES[g]) do
local mf = _G[memberName]
if mf then
mf:SetSize(w, h)
PassThroughUnitSubframes(mf)
mf:ClearAllPoints()
if not prevMember then
mf:SetPoint("TOPLEFT", groupFrame, "TOPLEFT", padX, -(titleH + padY))
elseif horiz then
mf:SetPoint("LEFT", prevMember, "RIGHT", 0, 0)
else
mf:SetPoint("TOP", prevMember, "BOTTOM", 0, 0)
end
prevMember = mf
end
end
groupFrame:SetSize(GetRaidGroupSize(w, h, horiz, border, titleH))
local bf = groupFrame.borderFrame or _G[groupName .. "BorderFrame"]
if bf then
DisableMouseOnDecoration(bf)
if border then
bf:ClearAllPoints()
bf:SetPoint("TOPLEFT", groupFrame, "TOPLEFT", 0, -titleH + 2)
bf:SetPoint("BOTTOMRIGHT", groupFrame, "BOTTOMRIGHT", 0, 0)
bf:SetShown(true)
else
bf:SetShown(false)
end
end
if groupFrame:IsShown() then
shownGroups[#shownGroups + 1] = groupFrame
end
end
end
-- Anchor visible raid groups with RAID_GROUP_GAP padding so groups never overlap
if container and #shownGroups > 0 then
for idx, groupFrame in ipairs(shownGroups) do
groupFrame:ClearAllPoints()
if idx == 1 then
groupFrame:SetPoint("TOPLEFT", container, "TOPLEFT", 0, 0)
else
local prevGroup = shownGroups[idx - 1]
if horiz then
groupFrame:SetPoint("TOPLEFT", prevGroup, "BOTTOMLEFT", 0, -gap)
else
groupFrame:SetPoint("TOPLEFT", prevGroup, "TOPRIGHT", gap, 0)
end
end
end
end
-- 2. Single-container / combined raid frames (CompactRaidFrame1..40)
local shownCombined = {}
for _, name in ipairs(RAID_FRAME_NAMES) do
local rf = _G[name]
if rf then
rf:SetSize(w, h)
PassThroughUnitSubframes(rf)
if #shownGroups == 0 and rf:IsShown() then
shownCombined[#shownCombined + 1] = rf
end
end
end
if container and #shownGroups == 0 and #shownCombined > 0 then
local startX = border and 7 or 0
local startY = border and -5 or 0
for idx, rf in ipairs(shownCombined) do
rf:ClearAllPoints()
if idx == 1 then
rf:SetPoint("TOPLEFT", container, "TOPLEFT", startX, startY)
elseif (idx - 1) % 5 == 0 then
local firstInPrevGroup = shownCombined[idx - 5]
if horiz then
rf:SetPoint("TOPLEFT", firstInPrevGroup, "BOTTOMLEFT", 0, -gap)
else
rf:SetPoint("TOPLEFT", firstInPrevGroup, "TOPRIGHT", gap, 0)
end
else
local prev = shownCombined[idx - 1]
if horiz then
rf:SetPoint("LEFT", prev, "RIGHT", 0, 0)
else
rf:SetPoint("TOP", prev, "BOTTOM", 0, 0)
end
end
end
end
if container then
DisableMouseOnDecoration(container)
if container.SetAlpha then
container:SetAlpha(alpha)
end
if container.borderFrame then
DisableMouseOnDecoration(container.borderFrame)
container.borderFrame:SetShown(border)
end
if ns.db and ns.db.raidPos and ns.db.raidPos.x and ns.db.raidPos.y and not ns.UsesEditModePositions() then
container:ClearAllPoints()
container:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", ns.db.raidPos.x, ns.db.raidPos.y)
end
end
end
function ns.GetActiveMode()
if ns.db and ns.db.selectedMode == "raid" then
return "raid"
end
return "party"
end
function ns.GetProfile(mode)
if not ns.db then
return PROFILE_DEFAULTS
end
if mode ~= "party" and mode ~= "raid" then
mode = ns.GetActiveMode()
end
return ns.db[mode] or PROFILE_DEFAULTS
end
function ns.SetActiveMode(mode)
if not ns.db then
return
end
mode = (mode == "raid") and "raid" or "party"
if ns.db.selectedMode == mode then
return
end
ns.db.selectedMode = mode
if ns.RefreshOptions then
ns.RefreshOptions()
end
end
-- Returns width, height, horizontal, border, alpha for a profile, snapped to the
-- same slider ranges the options panel uses.
local function ResolveLiveProfile(prof, ranges)
local opacity = Snap(prof.opacity or ranges.opacity[2], ranges.opacity)
return Snap(prof.frameWidth or ranges.width[1], ranges.width),
Snap(prof.frameHeight or ranges.height[1], ranges.height),
prof.useHorizontalGroups and true or false,
prof.displayBorder and true or false,
ns.OpacityToAlpha(opacity)
end
local function ApplyLiveToAllFrames()
if InCombatLockdown() or not ns.db then
return
end
local enabled = ns.db.showRaidFramesInGroups and true or false
-- While the party frames are unlocked, they stay visible when solo so they can be positioned.
local partyUnlocked = ns.GetUnlockedMode and ns.GetUnlockedMode() == "party"
local inRaid = IsInRaid()
local inParty = IsInGroup() and not inRaid
local ranges = ns.GetSizeRanges()
-- Apply Raid profile size, orientation, border & opacity to Raid Frames
ApplyLiveToRaidFrames(ResolveLiveProfile(ns.GetProfile("raid"), ranges))
-- 1. Visibility of CompactPartyFrame vs standard portrait PartyFrame
UpdateStandardPartyFramesShown(inParty and not enabled)
local partyProf = ns.GetProfile("party")
local w, h, horiz, border, alpha = ResolveLiveProfile(partyProf, ranges)
local sortBy = partyProf.sortBy or 0
local cpf = _G["CompactPartyFrame"]
if not cpf then
return
end
w, h = SnapToPixels(w, cpf), SnapToPixels(h, cpf)
DisableMouseOnDecoration(cpf)
if ns.db.partyPos and ns.db.partyPos.x and ns.db.partyPos.y and not ns.UsesEditModePositions() then
cpf:ClearAllPoints()
cpf:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", ns.db.partyPos.x, ns.db.partyPos.y)
end
if cpf.SetAlpha then
cpf:SetAlpha(alpha)
end
if inRaid then
cpf:SetShown(false)
elseif inParty then
cpf:SetShown(enabled)
else
cpf:SetShown(partyUnlocked and enabled)
end
-- 2. Collect and sort CompactPartyFrameMember1..5 using C++ GetAttribute("unit")
local entries = {}
for i, memberName in ipairs(PARTY_MEMBER_NAMES) do
local member = _G[memberName]
if member then
PassThroughUnitSubframes(member)
local unit = member.GetAttribute and member:GetAttribute("unit") or nil
local exists = (unit and UnitExists(unit)) and true or false
local role = (exists and UnitGroupRolesAssigned and UnitGroupRolesAssigned(unit)) or "NONE"
local name = (exists and UnitName(unit)) or ""
entries[#entries + 1] = {
frame = member,
index = i,
exists = exists,
roleWeight = ROLE_WEIGHT[role] or 4,
name = name,
}
end
end
table.sort(entries, function(a, b)
if a.exists ~= b.exists then
return a.exists
end
if sortBy == 0 then -- Role
if a.roleWeight ~= b.roleWeight then
return a.roleWeight < b.roleWeight
end
elseif sortBy == 2 then -- Alphabetical
if a.name ~= b.name then
return a.name < b.name
end
end
return a.index < b.index
end)
-- 3. Resize and anchor the sorted member frames using pure C++ widget methods.
local numShown = 0
for pos, item in ipairs(entries) do
if item.exists or item.frame:IsShown() then
numShown = pos
end
end
if numShown == 0 then
numShown = 1
end
local titleH = (cpf.title and cpf.title:IsShown() and cpf.title:GetHeight()) or 0
local firstX = border and 7 or 0
local firstY = -(titleH + (border and 5 or 0))
-- The container always has room for a full group of 5, so it keeps the same size (and stays
-- where Edit Mode centered it) whoever is in the group; alignment places the members inside.
local emptySlots = PARTY_SLOTS - numShown
local alignment = partyProf.alignment or 0
local shift = (alignment == 1 and emptySlots / 2) or (alignment == 2 and emptySlots) or 0
if horiz then
firstX = firstX + shift * w
else
firstY = firstY - shift * h
end
for pos, item in ipairs(entries) do
local member = item.frame
member:SetSize(w, h)
member:ClearAllPoints()
if pos == 1 then
member:SetPoint("TOPLEFT", cpf, "TOPLEFT", firstX, firstY)
else
local prev = entries[pos - 1].frame
if horiz then
member:SetPoint("LEFT", prev, "RIGHT", 0, 0)
else
member:SetPoint("TOP", prev, "BOTTOM", 0, 0)
end
end
end
-- 4. Update CompactPartyFrame container size & border frame
cpf:SetSize(GetPartyContainerSize(w, h, horiz, border, titleH))
local borderFrame = cpf.borderFrame or _G["CompactPartyFrameBorderFrame"]
if borderFrame then
DisableMouseOnDecoration(borderFrame)
if border and entries[1] and entries[numShown] then
borderFrame:ClearAllPoints()
borderFrame:SetPoint("TOPLEFT", entries[1].frame, "TOPLEFT", -7, 5)
borderFrame:SetPoint("BOTTOMRIGHT", entries[numShown].frame, "BOTTOMRIGHT", 7, -7)
borderFrame:SetShown(true)
else
borderFrame:SetShown(false)
end
end
end
local RAID_GROUPS = 8
-- Raid frames are either split into group frames or "combined" into one flow of CompactRaidFrameN.
-- Outside a raid nothing is shown, so assume groups (Blizzard's default).
local function RaidUsesCombinedFrames()
for _, groupName in ipairs(RAID_GROUP_NAMES) do
local groupFrame = _G[groupName]
if groupFrame and groupFrame:IsShown() then
return false
end
end
for _, name in ipairs(RAID_FRAME_NAMES) do
local rf = _G[name]
if rf and rf:IsShown() then
return true
end
end
return false
end
-- Width and height of a full group (5 party members, or 8 raid groups of 5) laid out the way
-- ApplyLiveVisuals would, measured from the container's top-left corner, in the frames' own units.
-- Unlock mode outlines this so you see the whole footprint, not just who's in the group right now.
function ns.GetFullGroupSize(mode)
local w, h, horiz, border = ResolveLiveProfile(ns.GetProfile(mode), ns.GetSizeRanges())
-- Same whole-pixel rounding as the layout code, so the outline matches the drawn frames.
local container = (mode == "raid") and _G["CompactRaidFrameContainer"] or _G["CompactPartyFrame"]
w, h = SnapToPixels(w, container), SnapToPixels(h, container)
local gaps = SnapToPixels(RAID_GROUP_GAP, container) * (RAID_GROUPS - 1)
if mode ~= "raid" then
local cpf = container
local titleH = (cpf and cpf.title and cpf.title:IsShown() and cpf.title:GetHeight()) or 0
return GetPartyContainerSize(w, h, horiz, border, titleH)
end
if RaidUsesCombinedFrames() then
-- Rows (horizontal) or columns (vertical) of 5, RAID_GROUP_GAP between each block of 5.
local startX, startY = border and 7 or 0, border and 5 or 0
if horiz then
return startX + w * 5, startY + h * RAID_GROUPS + gaps
end
return startX + w * RAID_GROUPS + gaps, startY + h * 5
end
local groupName = RAID_GROUP_NAMES[1]
local groupW, groupH = GetRaidGroupSize(w, h, horiz, border, GetRaidGroupTitleHeight(_G[groupName], groupName))
if horiz then
return groupW, groupH * RAID_GROUPS + gaps
end
return groupW * RAID_GROUPS + gaps, groupH
end
function ns.ApplyLiveVisuals()
ApplyLiveToAllFrames()
-- Keep the unlock-mode boxes (Movers.lua) on top of the frames after every layout change.
if ns.UpdateMovers then
ns.UpdateMovers()
end
end
---------------------------------------------------------------------------
-- Edit Mode C++ Persistence (Saves to BOTH Party and Raid systems)
---------------------------------------------------------------------------
local function IsEditModeAvailable()
return C_EditMode and C_EditMode.GetLayouts and C_EditMode.SaveLayouts
and EditModePresetLayoutManager and EditModePresetLayoutManager.GetCopyOfPresetLayouts
and Enum and Enum.EditModeUnitFrameSetting
and Enum.EditModeUnitFrameSetting.UseRaidStylePartyFrames ~= nil
end
local function IsEditModeReady()
return IsEditModeAvailable() and EditModeManagerFrame and EditModeManagerFrame.accountSettings ~= nil
end
-- In clients with Edit Mode, the Edit Mode layout owns where the frames sit. We never pin them to a
-- position of our own there, otherwise Blizzard's Edit Mode could no longer drag them.
-- Clients without Edit Mode keep the position in ns.db.partyPos / ns.db.raidPos instead.
function ns.UsesEditModePositions()
return IsEditModeAvailable() and true or false
end
-- Positions from unlock mode waiting to be written into the Edit Mode layout by SaveToEditMode.
local pendingAnchors = {}
local function DeepCopy(src, seen)
if type(src) ~= "table" then
return src
end
seen = seen or {}
if seen[src] then
return seen[src]
end
local dst = {}
seen[src] = dst
for k, v in pairs(src) do
if type(k) ~= "function" and type(k) ~= "userdata" and type(v) ~= "function" and type(v) ~= "userdata" then
dst[DeepCopy(k, seen)] = DeepCopy(v, seen)
end
end
return dst
end
-- Converts between human-readable display values and raw Edit Mode stored integers
-- without calling any Blizzard Lua methods on EditModeSettingDisplayInfoMixin.
local function IsConvertibleSetting(setup)
if not setup or type(setup.minValue) ~= "number" or type(setup.maxValue) ~= "number" then
return false
end
if EditModeSettingDisplayInfoMixin and setup.ConvertValue == EditModeSettingDisplayInfoMixin.ConvertValueDefault then
return false
end
return true
end
local function ToRawValue(settingID, displayValue)
if settingID == nil or displayValue == nil then
return displayValue
end
local setup = GetSettingDisplayInfo(settingID)
if setup and type(setup.minValue) == "number" and type(setup.maxValue) == "number" then
local min, max = setup.minValue, setup.maxValue
local clamped = math.max(min, math.min(max, displayValue))
if IsConvertibleSetting(setup) then
return math.floor(clamped - min + 0.5)
end
return math.floor(clamped + 0.5)
end
-- No display info available: width, height and opacity are stored as offsets from their minimum.
local S = Enum.EditModeUnitFrameSetting
local range = (settingID == S.FrameWidth and RANGES.width)
or (settingID == S.FrameHeight and RANGES.height)
or (settingID == S.Opacity and RANGES.opacity)
if range then
return math.max(range[1], math.min(range[2], displayValue)) - range[1]
end
return displayValue
end
-- Builds the same combined list EditModeManagerFrameMixin:UpdateLayoutInfo uses (and passes to
-- C_EditMode.SaveLayouts): Blizzard's presets first, then every saved layout. activeLayout is an
-- index into that list, so saved layouts are deep-copied but never dropped or reordered: any shift
-- would point activeLayout at another layout and switch the player off theirs (e.g. to "Modern").
local function LoadLayouts()
local layoutInfo = C_EditMode.GetLayouts()
if not layoutInfo or type(layoutInfo.layouts) ~= "table" then
return nil
end
-- Read-only on Blizzard's side: returns a fresh CopyTable of the preset layouts.
local all = EditModePresetLayoutManager:GetCopyOfPresetLayouts()
if type(all) ~= "table" then
return nil
end
for i = 1, #layoutInfo.layouts do
all[#all + 1] = DeepCopy(layoutInfo.layouts[i])
end
layoutInfo.layouts = all
if not all[layoutInfo.activeLayout] then
return nil
end
return layoutInfo
end
local function FindUnitFrameSystem(layout, systemIndex)
local systemID = Enum.EditModeSystem.UnitFrame
for _, system in ipairs(layout.systems or {}) do
if system.system == systemID and system.systemIndex == systemIndex then
return system
end
end
end
local function GetRawSystemSetting(system, settingID)
if settingID == nil then
return nil
end
for _, setting in ipairs(system.settings or {}) do
if setting.setting == settingID then
return setting.value
end
end
end
local function SetRawSystemSetting(system, settingID, rawValue)
if settingID == nil or rawValue == nil then
return
end
for _, setting in ipairs(system.settings) do
if setting.setting == settingID then
setting.value = rawValue
return
end
end
table.insert(system.settings, { setting = settingID, value = rawValue })
end
local function FindLayoutIndex(layoutInfo, name)
for index, layout in ipairs(layoutInfo.layouts) do
if layout.layoutName == name and layout.layoutType ~= Enum.EditModeLayoutType.Preset then
return index
end
end
end
local function CreateOwnLayout(layoutInfo)
local presetType = Enum.EditModeLayoutType.Preset
local accountType = Enum.EditModeLayoutType.Account
local numPresets, lastAccountIndex, numAccount = 0, nil, 0
for index, layout in ipairs(layoutInfo.layouts) do
if layout.layoutType == presetType then
numPresets = numPresets + 1
elseif layout.layoutType == accountType then
lastAccountIndex = index
numAccount = numAccount + 1
end
end
local maxLayouts = (Constants and Constants.EditModeConsts and Constants.EditModeConsts.EditModeMaxLayoutsPerType) or 5
if numAccount >= maxLayouts then
return nil
end
-- Always clone from Modern Preset (index 1) when creating our custom layout from a preset
local baseLayout = layoutInfo.layouts[1] or layoutInfo.layouts[layoutInfo.activeLayout]
local newLayout = DeepCopy(baseLayout)
newLayout.layoutType = accountType
newLayout.layoutName = LAYOUT_NAME
local newIndex = (lastAccountIndex or numPresets) + 1
table.insert(layoutInfo.layouts, newIndex, newLayout)
layoutInfo.activeLayout = newIndex
return newIndex
end
-- Saves separate Party (ns.db.party) and Raid (ns.db.raid) settings into the active Edit Mode layout.
function ns.SaveToEditMode()
if not ns.db then
return false
end
if not IsEditModeReady() then
if GetCVarValue and GetCVarValue("useCompactPartyFrames") ~= nil then
SetCVarValue("useCompactPartyFrames", ns.db.showRaidFramesInGroups and "1" or "0")
return true
end
return false
end
local layoutInfo = LoadLayouts()
local active = layoutInfo and layoutInfo.layouts[layoutInfo.activeLayout]
if not active then
return false, "Could not find active Edit Mode layout."
end
local switchedLayout = false
if active.layoutType == Enum.EditModeLayoutType.Preset then
local existing = FindLayoutIndex(layoutInfo, LAYOUT_NAME)
if existing then
layoutInfo.activeLayout = existing
elseif not CreateOwnLayout(layoutInfo) then
return false, "Active layout is a preset and max custom layouts reached."
end
switchedLayout = true
active = layoutInfo.layouts[layoutInfo.activeLayout]
end
local S = Enum.EditModeUnitFrameSetting
local D = PROFILE_DEFAULTS
local function ApplyProfileToSystem(sys, prof)
SetRawSystemSetting(sys, S.DisplayBorder, prof.displayBorder and 1 or 0)
SetRawSystemSetting(sys, S.SortPlayersBy, prof.sortBy or D.sortBy)
SetRawSystemSetting(sys, S.AuraOrganizationType, prof.auraOrganizationType or D.auraOrganizationType)
SetRawSystemSetting(sys, S.FrameWidth, ToRawValue(S.FrameWidth, prof.frameWidth or D.frameWidth))
SetRawSystemSetting(sys, S.FrameHeight, ToRawValue(S.FrameHeight, prof.frameHeight or D.frameHeight))
SetRawSystemSetting(sys, S.Opacity, ToRawValue(S.Opacity, prof.opacity or D.opacity))
SetRawSystemSetting(sys, S.BigDefensiveIconSize, ToRawValue(S.BigDefensiveIconSize, prof.bigDefensiveSize or D.bigDefensiveSize))
SetRawSystemSetting(sys, S.BuffIconSize, ToRawValue(S.BuffIconSize, prof.buffSize or D.buffSize))
SetRawSystemSetting(sys, S.DebuffIconSize, ToRawValue(S.DebuffIconSize, prof.iconSize or D.iconSize))
end
local uiTop = UIParent and UIParent.GetTop and UIParent:GetTop() or nil
local function ApplyCustomAnchor(sys, pos)
if not sys or not pos or not pos.x or not pos.y or not uiTop then
return
end
sys.isInDefaultPosition = false
sys.anchorInfo = sys.anchorInfo or {}
sys.anchorInfo.point = "TOPLEFT"
sys.anchorInfo.relativeTo = "UIParent"
sys.anchorInfo.relativePoint = "TOPLEFT"
sys.anchorInfo.offsetX = pos.x
sys.anchorInfo.offsetY = pos.y - uiTop
-- A leftover second anchor point would pull the frame away from the one written above.
sys.anchorInfo2 = nil
end
-- 1. Party Frames system (uses ns.db.party)
local partyProf = ns.GetProfile("party")
local partySys = FindUnitFrameSystem(active, Enum.EditModeUnitFrameSystemIndices.Party)
if partySys then
SetRawSystemSetting(partySys, S.UseRaidStylePartyFrames, ns.db.showRaidFramesInGroups and 1 or 0)
SetRawSystemSetting(partySys, S.ShowPartyFrameBackground, 0) -- Always off for raid-style frames
SetRawSystemSetting(partySys, S.UseHorizontalGroups, partyProf.useHorizontalGroups and 1 or 0)
ApplyProfileToSystem(partySys, partyProf)
ApplyCustomAnchor(partySys, pendingAnchors.partyPos or ns.db.partyPos)
end
-- 2. Raid Frames system (uses ns.db.raid)
if Enum.EditModeUnitFrameSystemIndices.Raid then
local raidProf = ns.GetProfile("raid")
local raidSys = FindUnitFrameSystem(active, Enum.EditModeUnitFrameSystemIndices.Raid)
if raidSys then
ApplyProfileToSystem(raidSys, raidProf)
ApplyCustomAnchor(raidSys, pendingAnchors.raidPos or ns.db.raidPos)
if S.RaidGroupDisplayType then
-- RaidGroupDisplayType: 0=SeparateVert, 1=SeparateHoriz, 2=CombineVert, 3=CombineHoriz
local horiz = raidProf.useHorizontalGroups and 1 or 0
local curType = GetRawSystemSetting(raidSys, S.RaidGroupDisplayType) or 0
local isCombined = (curType == 2 or curType == 3)
local newType = isCombined and (horiz == 1 and 3 or 2) or (horiz == 1 and 1 or 0)
SetRawSystemSetting(raidSys, S.RaidGroupDisplayType, newType)
end
end
end
C_EditMode.SaveLayouts(layoutInfo)
-- Same index the player had selected, unless we just moved off a preset onto our own layout.
if C_EditMode.SetActiveLayout then
C_EditMode.SetActiveLayout(layoutInfo.activeLayout)
end
if switchedLayout then
ns.Print(("Switched from preset to custom Edit Mode layout '%s'."):format(active.layoutName))
end
return true
end
---------------------------------------------------------------------------
-- Public API for Options UI & Slash Commands
---------------------------------------------------------------------------
function ns.GetSize(mode)
local prof = ns.GetProfile(mode)
return prof and prof.frameWidth or RANGES.width[1],
prof and prof.frameHeight or RANGES.height[1]
end
-- Called on every slider tick while dragging: resizes the real frames on screen at 60 FPS
-- and updates the 1:1 preview box without calling C_EditMode.SaveLayouts on every pixel.
function ns.StageSize(width, height)
local prof = ns.GetProfile()
local ranges = ns.GetSizeRanges()
if width then
prof.frameWidth = Snap(width, ranges.width)
end
if height then
prof.frameHeight = Snap(height, ranges.height)
end
ns.ApplyLiveVisuals()
if ns.UpdatePreviewBoxes then
ns.UpdatePreviewBoxes()
end
end
function ns.StageOption(key, value)
if not ns.db then
return
end
local ranges = ns.GetSizeRanges()
if ranges[key] and type(value) == "number" then
value = Snap(value, ranges[key])
end
if GLOBAL_KEYS[key] then
ns.db[key] = value
else
local prof = ns.GetProfile()
prof[key] = value
end
ns.ApplyLiveVisuals()
if ns.UpdatePreviewBoxes then
ns.UpdatePreviewBoxes()
end
end
function ns.CommitChanges(quiet)
ns.ApplyLiveVisuals()
local ok, err = ns.SaveToEditMode()
if not ok and err then
ns.Print(err)
elseif not quiet then
local mode = ns.GetActiveMode()
local prof = ns.GetProfile(mode)
local label = (mode == "raid") and "Raid Frames" or "Party Frames"
ns.Print(("Applied to %s (%d x %d, %s)"):format(
label,
prof.frameWidth or PROFILE_DEFAULTS.frameWidth,
prof.frameHeight or PROFILE_DEFAULTS.frameHeight,
prof.useHorizontalGroups and "Horizontal" or "Vertical"))
end
if ns.RefreshOptions then
ns.RefreshOptions()
end
return ok
end
function ns.SetSize(width, height, quiet)
ns.StageSize(width, height)
ns.CommitChanges(quiet)
end
function ns.SetEnabled(value)
ns.db.showRaidFramesInGroups = value and true or false
ns.CommitChanges(true)
ns.Print("Show raid frames in groups: " .. OnOff(ns.db.showRaidFramesInGroups))
end
function ns.SetOption(key, value)
if GLOBAL_KEYS[key] then
ns.db[key] = value
else
local prof = ns.GetProfile()
prof[key] = value
end
ns.CommitChanges(true)
end
-- Stores where unlock mode dropped a frame group (x, y = TOPLEFT in UIParent BOTTOMLEFT space, in the
-- moved frame's own units). With Edit Mode it goes straight into the Edit Mode layout.
function ns.SaveFramePosition(mode, x, y)
local posKey = (mode == "raid") and "raidPos" or "partyPos"
if ns.UsesEditModePositions() then
pendingAnchors[posKey] = { x = x, y = y }
ns.CommitChanges(true)
pendingAnchors[posKey] = nil
elseif ns.db then
ns.db[posKey] = { x = x, y = y }
end
end
-- Forgets the dragged position for a mode so Blizzard's default anchor applies again.
-- Returns false with Edit Mode, where the position lives in the Edit Mode layout instead.
local function ClearSavedPosition(mode)
if ns.UsesEditModePositions() then
return false
end
if ns.db then
ns.db[(mode == "raid") and "raidPos" or "partyPos"] = nil