Repository navigation
Expand file tree
/
Copy pathQuestCompareWindow.lua
More file actions
1970 lines (1920 loc) · 80.7 KB
/
Copy pathQuestCompareWindow.lua
File metadata and controls
1970 lines (1920 loc) · 80.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
local L = _G.QuestTogether.Translate
local QuestTogether = _G.QuestTogether
local VISIBLE_ROWS, ROW_HEIGHT = 11, 42
local MEMBER_ROW_HEIGHT, OBJECTIVE_ROW_HEIGHT = 24, 26
local DETAIL_BOTTOM_PADDING = 10
local function CompareRowHeight(rows, index)
local row, previous, following = rows[index], rows[index - 1], rows[index + 1]
local firstDetail = row.kind and (not previous or not previous.kind or previous.questId ~= row.questId)
local lastDetail = row.kind and (not following or not following.kind or following.questId ~= row.questId)
local height = row.kind == "member" and (row.hint and row.hint ~= "" and 34 or MEMBER_ROW_HEIGHT)
or row.kind == "objective" and OBJECTIVE_ROW_HEIGHT or ROW_HEIGHT
return height + (firstDetail and 6 or 0) + (lastDetail and DETAIL_BOTTOM_PADDING or 0)
end
local QUEST_WIDTH, MEMBER_WIDTH, ACTION_WIDTH = 350, 130, 160
local COLUMN_WIDTH = MEMBER_WIDTH - 4
local function ColumnLeft(frame, index)
return frame.columns[index].left
end
local function ColumnWidth(frame, index)
return frame.columns[index].width
end
local function ContentWidth(frame)
return math.max(frame.viewport:GetWidth(), QUEST_WIDTH + (frame.memberColumnsWidth or 0) + ACTION_WIDTH)
end
local MIN_WIDTH = 780
local function MinimumWidth(memberCount, columns)
local width = 0
for index = 1, math.min(memberCount, 5) do
width = width + (columns and columns[index] and columns[index].width + 4 or MEMBER_WIDTH)
end
return math.max(MIN_WIDTH, QUEST_WIDTH + width + ACTION_WIDTH + 62)
end
local COLORS = {
Have = { 0.24, 0.20, 0.14 },
Completed = { 0.12, 0.38, 0.18 },
Absent = { 0.38, 0.32, 0.24 },
Ready = { 0.12, 0.38, 0.18 },
Missing = { 0.55, 0.25, 0.06 },
Loading = { 0.38, 0.32, 0.24 },
Unknown = { 0.38, 0.32, 0.24 },
}
local DARK_COLORS = {
Have = { 0.86, 0.86, 0.84 },
Completed = { 0.40, 0.90, 0.48 },
Absent = { 0.68, 0.70, 0.73 },
Ready = { 0.40, 0.90, 0.48 },
Missing = { 1, 0.68, 0.30 },
Loading = { 0.68, 0.70, 0.73 },
Unknown = { 0.68, 0.70, 0.73 },
}
-- Compact cells must not depend on optional font glyphs. Native artwork keeps
-- turn-in readiness distinct from completed history, even in a narrow column.
local COMPACT_STATUS = {
Have = ".",
Ready = "|TInterface\\GossipFrame\\ActiveQuestIcon:16:16|t",
Completed = "|TInterface\\Buttons\\UI-CheckBox-Check:16:16|t",
Missing = "-",
Absent = "?",
Loading = "...",
Unknown = "?",
}
local function StatusLabel(status)
return L(status == "Absent" and "Not in log" or status)
end
local function MemberColor(addon, classFile, brightness)
return addon:GetWindowClassColor(classFile, brightness)
end
local Style = QuestTogether.QuestProgressStyle
local ShowPanel, ColorPanel, LayoutPanel, LayoutDetailsPanel = Style.ShowPanel, Style.ColorPanel, Style.LayoutPanel, Style.LayoutDetailsPanel
local function Label(parent, x, y, width, text, font)
local label = parent:CreateFontString(nil, "OVERLAY", font or "GameFontHighlightSmall")
label:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
label:SetWidth(width)
label:SetJustifyH("LEFT")
label:SetText(text or "")
return label
end
local function Button(addon, parent, x, y, width, text, callback)
local button = addon:CreateOwnedWindowFrame("Button", nil, parent, "UIPanelButtonTemplate")
button:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
button:SetSize(width, 24)
button:SetText(text)
button:SetScript("OnClick", callback)
return button
end
local function NativeTexture(parent, template, layer, width, height)
-- Match libchev's debug window with texture-only native artwork. These
-- regions are ours; no Blizzard frame scripts or shared frame state.
local texture = parent:CreateTexture(nil, layer, template)
texture:ClearAllPoints()
if width and height then
texture:SetSize(width, height)
end
return texture
end
local function Window(addon, width, height, title, parchment)
if not parchment then return addon:CreateScrollDialog(width, height, title) end
local frame = addon:CreateOwnedWindowFrame("Frame", nil, addon:GetOwnedUIParent())
frame:Hide()
frame:SetSize(width, height)
frame:SetPoint("CENTER")
frame:SetFrameStrata("DIALOG")
frame:SetToplevel(true)
frame:SetFlattensRenderLayers(true)
frame:SetClampedToScreen(true)
frame:SetMovable(true)
frame:EnableMouse(true)
frame:RegisterForDrag("LeftButton")
frame:SetScript("OnDragStart", function(self)
if addon:IsWorkBlocked("foreign_frame_mutation") then
return
end
addon:StartWindowDrag(self)
end)
frame:SetScript("OnDragStop", function(self)
addon:StopWindowDrag(self)
end)
frame.parchment = NativeTexture(frame, nil, "BACKGROUND")
frame.parchment:SetPoint("TOPLEFT", -16, 12)
frame.parchment:SetPoint("BOTTOMRIGHT", 16, -12)
frame.parchment:SetTexture(addon:GetScrollWindowTheme().texture)
frame.title = Label(
frame,
parchment and 50 or 16,
-5,
width - (parchment and 100 or 70),
title,
parchment and "GameFontNormalLarge" or "GameFontNormal"
)
if parchment then
frame.title:SetTextColor(0.22, 0.13, 0.06)
local logo = NativeTexture(frame, nil, "ARTWORK", 26, 26)
logo:SetPoint("TOPLEFT", 18, 0)
frame.title:ClearAllPoints()
frame.title:SetPoint("LEFT", logo, "RIGHT", 10, 0)
frame.title:SetHeight(30)
frame.title:SetJustifyV("MIDDLE")
frame.title:SetMaxLines(1)
logo:SetTexture(
addon.NAMEPLATE_PLAYER_ICON_TEXTURE or "Interface\\AddOns\\QuestTogether\\Media\\QuestTogetherIcon"
)
end
return frame
end
local function Checkbox(addon, parent, x, y, text, callback)
local check = addon:CreateOwnedWindowFrame("CheckButton", nil, parent, "MinimalCheckboxTemplate")
check:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
check:SetSize(26, 26)
check.label = Label(check, 30, -7, (parent.contentWidth or (parent:GetWidth() - 48)) - 26, text)
if parent.themeLabels then addon:AddScrollDialogLabel(parent, check.label) end
check:SetScript("OnClick", callback)
return check
end
-- These factories are addon-owned seams for live-safe UI fixtures.
function QuestTogether:CreatePartyQuestUIFrame(...)
return self:CreateOwnedWindowFrame(...)
end
function QuestTogether:GetPartyQuestUIParent()
return self:GetOwnedUIParent()
end
-- QT owns these buttons and textures; use Blizzard's ordinary mini quest POI
-- artwork without registering remote quests with Blizzard's POI button mixins.
local function FocusAtlas(texture, atlas, fallback)
-- Like Blizzard's POIButton_SetAtlas: recycled textures must discard the
-- previous atlas crop before switching between progress dots and turn-in art.
texture:SetTexCoord(0, 1, 0, 1)
local applied = false
if texture.SetAtlas then
local ok, result = pcall(texture.SetAtlas, texture, atlas)
applied = ok and result ~= false
if applied and texture.GetAtlas then
local readOK, current = pcall(texture.GetAtlas, texture)
applied = readOK and current == atlas
end
end
if not applied then
texture:SetTexture(fallback)
texture:SetTexCoord(0, 1, 0, 1)
end
end
local function CreateFocusButton(addon, row, column)
local button = addon:CreateOwnedWindowFrame("Button", nil, row)
button:SetSize(26, 26)
if button.SetMotionScriptsWhileDisabled then button:SetMotionScriptsWhileDisabled(true) end
button:SetPoint("TOPLEFT", QUEST_WIDTH + (column - 1) * MEMBER_WIDTH + COLUMN_WIDTH - 35, -8)
button.normal = button:CreateTexture(nil, "BACKGROUND")
button.pushed = button:CreateTexture(nil, "BACKGROUND")
-- Match Blizzard's POI Display frame: the symbol must render above native
-- normal/pushed button textures regardless of their draw-layer ordering.
button.display = addon:CreateOwnedWindowFrame("Frame", nil, button)
button.display:SetAllPoints()
button.display:SetFrameLevel(button:GetFrameLevel() + 1)
button.display:EnableMouse(false)
button.icon = button.display:CreateTexture(nil, "OVERLAY")
button.highlight = button:CreateTexture(nil, "HIGHLIGHT")
for _, texture in ipairs({ button.normal, button.pushed, button.highlight }) do texture:SetAllPoints() end
button.icon:SetSize(16, 16)
button.icon:SetPoint("CENTER")
button:SetNormalTexture(button.normal)
button:SetPushedTexture(button.pushed)
button:SetHighlightTexture(button.highlight)
FocusAtlas(button.highlight, "UI-QuestPoi-InnerGlow", "Interface\\Buttons\\UI-Common-MouseHilight")
local function Current()
return row.data and not row.data.kind and row.session == addon.partyQuestCompareSession
end
local function Leave() addon:HideSettingsTooltip(button) end
button:SetScript("OnLeave", Leave)
button:SetScript("OnHide", Leave)
button:SetScript("OnEnter", function()
if not Current() then return end
local hint = button.localPlayer and L("Click to focus this quest.")
or (button.selected and L("Click to follow this player's quest focus.") or L("This player is not focusing this quest."))
if not button.localPlayer and not button.knownFocus then hint = addon:GetPartyFocusLabel(button.memberName) end
if button.muted and button.clickable then
hint = hint .. "\n\n" .. string.format(L("Changing focus will stop following %s."), button.following)
end
addon:ShowSettingsTooltip(button, button.memberName, StatusLabel(button.questStatus or "Unknown") .. "\n" .. hint)
end)
button:SetScript("OnClick", function()
Leave()
if Current() and button.clickable and not addon:IsWorkBlocked("foreign_frame_mutation") then
addon:RequestPartyQuestFocus(button.memberName, row.data.questId)
end
end)
return button
end
local function PaintFocusButton(button, selected, complete, muted)
button.selected = selected
local suffix = selected and "-SuperTracked" or ""
FocusAtlas(button.normal, "UI-QuestPoi-QuestNumber" .. suffix, "Interface\\Buttons\\UI-Quickslot2")
FocusAtlas(button.pushed, "UI-QuestPoi-QuestNumber-Pressed" .. suffix, "Interface\\Buttons\\UI-Quickslot-Depress")
FocusAtlas(button.icon, complete and "UI-QuestIcon-TurnIn-Normal"
or (selected and "Quest-In-Progress-Icon-Brown" or "Quest-In-Progress-Icon-yellow"),
"Interface\\GossipFrame\\ActiveQuestIcon")
for _, texture in ipairs({ button.normal, button.pushed, button.icon }) do
if texture.SetDesaturated then texture:SetDesaturated(muted) end
local shade = muted and 0.55 or 1
texture:SetVertexColor(shade, shade, shade, 1)
end
end
local function DrawCompareRows(self, frame, session, rows, width, actionX)
local dark = self:GetOption("lightMode") ~= true
local colors = dark and DARK_COLORS or COLORS
local followTarget = self.GetPartyQuestFollowTarget and self:GetPartyQuestFollowTarget()
local focusIDs = {}
for i, member in ipairs(session.members) do
focusIDs[i] = self.GetPartyQuestFocusID and self:GetPartyQuestFocusID(member.name)
end
for i, row in ipairs(frame.rows) do
local slot = frame.visibleRows[i]
local data = slot and rows[slot.index]
if row.data ~= data then self:HideSettingsTooltip(row) end
row.data = data
row.session = session
if not data then
row:Hide()
row.clip:Hide()
else
row:Show()
row.clip:Show()
row:SetWidth(width)
if not data.kind and frame.keyboardSession == session and session.keyboardQuestId == data.questId then
row.keyboardHighlight:Show()
else
row.keyboardHighlight:Hide()
end
row:SetHeight(CompareRowHeight(rows, slot.index))
local expanded = not data.kind and session.expandedQuestIds and session.expandedQuestIds[data.questId]
local previous, following = rows[slot.index - 1], rows[slot.index + 1]
local first = not previous or not previous.kind or previous.questId ~= data.questId
local last = not following or not following.kind or following.questId ~= data.questId
local top, bottom = LayoutDetailsPanel(row, width, data.kind ~= nil, first, last, expanded, dark)
row.expander:SetTextColor(dark and 0.92 or 0.4, dark and 0.78 or 0.25, dark and 0.45 or 0.08)
row.expander:SetText(data.kind and "" or (expanded and "-" or "+"))
row.title:SetText(data.title)
ShowPanel(row.hover, false)
ColorPanel(row.hover, dark and 1 or 0.35, dark and 1 or 0.24, dark and 1 or 0.10, 0.08)
row.title:ClearAllPoints()
local indent = data.kind == "objective" and 64 or data.kind == "member" and 48 or 28
local own = session.byName[session.playerName]
local journalVisible = not data.kind and own and own.entries[data.questId] ~= nil
local titleTop = data.kind == "member" and (data.hint and data.hint ~= "" and 9 or 4)
or data.kind == "objective" and 3 or 5
row.title:SetPoint("TOPLEFT", indent, -titleTop - top)
row.title:SetWidth(data.kind and (actionX - indent - 16) or (QUEST_WIDTH - (journalVisible and 66 or 36)))
if journalVisible then
row.journal:SetEnabled(not self:IsWorkBlocked("foreign_frame_mutation"))
row.journal:Show()
else
row.journal:Hide()
end
row.partySummary:SetText(data.partySummary or "")
row.partySummary:SetTextColor(unpack(colors[data.partySummaryState or "Unknown"]))
if data.kind then
row.accent:Hide()
local r, g, b = MemberColor(self, data.classFile)
ColorPanel(row.background, r, g, b, 0.20)
else
if expanded then
row.accent:SetColorTexture(0.46, 0.30, 0.08, 1)
row.accent:Show()
else
row.accent:Hide()
end
ColorPanel(
row.background,
dark and 1 or 0.4,
dark and 0.85 or 0.28,
dark and 0.55 or 0.12,
expanded and 0.16 or (slot.index % 2 == 0 and 0.055 or 0.015)
)
end
Style.LayoutProgressBar(row, data, dark, 64, 22 + top, actionX - 80)
if data.kind == "member" then
row.title:SetTextColor(MemberColor(self, data.classFile, 0.32))
elseif dark then
row.title:SetTextColor(0.93, 0.92, 0.88)
elseif data.kind == "objective" then
row.title:SetTextColor(0.23, 0.17, 0.10)
else
row.title:SetTextColor(0.18, 0.12, 0.06)
end
for j, status in ipairs(data.cells) do
if not row.cells[j] then
row.cells[j] =
Label(row, QUEST_WIDTH + (j - 1) * MEMBER_WIDTH, -12, MEMBER_WIDTH - 40, "", "GameFontHighlight")
row.cellShades[j] = row:CreateTexture(nil, "BORDER")
row.cellShades[j]:SetPoint("TOPLEFT", ColumnLeft(frame, j), 0)
row.cellShades[j]:SetSize(COLUMN_WIDTH, ROW_HEIGHT - 1)
end
local column = frame.columns[j]
row.cellTooltips = row.cellTooltips or {}
if not row.cellTooltips[j] then
local control = self:CreateOwnedWindowFrame("Button", nil, row)
control:SetScript("OnEnter", function()
if row.session == self.partyQuestCompareSession and row.data
and not self:IsWorkBlocked("foreign_frame_mutation")
and QuestTogether.LibChev.CanMutateOwnedRegion(control) and control:IsVisible() then
self:ShowSettingsTooltip(control, control.memberName, StatusLabel(control.questStatus))
end
end)
local function Leave() self:HideSettingsTooltip(control) end
control:SetScript("OnLeave", Leave)
control:SetScript("OnHide", Leave)
control:SetScript("OnClick", function()
if self:IsWorkBlocked("foreign_frame_mutation") or not QuestTogether.LibChev.CanMutateOwnedRegion(control)
or not control:IsVisible() then return end
local click = row:GetScript("OnMouseUp")
if click then click(row, "LeftButton") end
end)
row.cellTooltips[j] = control
end
local statusControl = row.cellTooltips[j]
statusControl.memberName, statusControl.questStatus = session.members[j].name, status
statusControl:ClearAllPoints()
statusControl:SetPoint("TOPLEFT", column.left, 0)
statusControl:SetSize(column.width - ((status == "Have" or status == "Ready") and 32 or 0), ROW_HEIGHT - 1)
if column.collapsed then statusControl:Show() else statusControl:Hide() end
row.cellShades[j]:ClearAllPoints()
row.cellShades[j]:SetPoint("TOPLEFT", column.left, 0)
row.cellShades[j]:SetSize(column.width, ROW_HEIGHT - 1)
row.cells[j]:ClearAllPoints()
row.cells[j]:SetPoint("TOPLEFT", column.left + 4, -12)
row.cells[j]:SetWidth(column.width - ((status == "Have" or status == "Ready") and 36 or 8))
local r, g, b = MemberColor(self, session.members[j].classFile)
row.cellShades[j]:SetColorTexture(r, g, b, 0.20)
row.cellShades[j]:Show()
row.cells[j]:SetText(column.collapsed and (COMPACT_STATUS[status] or "?") or StatusLabel(status))
row.cells[j]:SetTextColor(unpack(colors[status] or colors.Unknown))
row.cells[j]:Show()
if not row.focusCells[j] then row.focusCells[j] = CreateFocusButton(self, row, j) end
local button, member = row.focusCells[j], session.members[j]
button:ClearAllPoints()
button:SetPoint("TOPLEFT", column.left + column.width - 31, -8)
button.questStatus = status
button.memberName, button.localPlayer = member.name, member.isLocal
button.knownFocus = focusIDs[j] ~= nil and focusIDs[j] >= 0
local selected = focusIDs[j] == data.questId
button.clickable = member.isLocal == true or selected
button:SetEnabled(button.clickable)
button.following = followTarget
if followTarget then
button.muted = not (selected and followTarget == member.name)
else
button.muted = not (member.isLocal or selected)
end
PaintFocusButton(button, selected, status == "Ready", button.muted)
if status == "Have" or status == "Ready" then button:Show() else button:Hide() end
end
for j = #data.cells + 1, #row.cells do
row.cells[j]:Hide()
row.cellShades[j]:Hide()
if row.focusCells[j] then row.focusCells[j]:Hide() end
if row.cellTooltips and row.cellTooltips[j] then row.cellTooltips[j]:Hide() end
end
row.action:ClearAllPoints()
row.action:SetPoint("TOPLEFT", actionX, -9)
row.action:SetHeight(24)
row.hint:SetMaxLines(2)
row.hint:SetJustifyH(data.kind and "LEFT" or "CENTER")
row.hint:ClearAllPoints()
row.hint:SetHeight(data.kind and (row:GetHeight() - top - bottom - 4) or (ROW_HEIGHT - 8))
local status, waiting = self:GetPartyQuestShareStatus(data.questId)
if data.kind then
status, waiting = nil, false
end
row.hint:SetTextColor(unpack(data.complete and colors.Ready or colors.Have))
if data.action and not waiting then
local cooldown = data.action == "request"
and self.GetPartyQuestShareRequestCooldown
and self:GetPartyQuestShareRequestCooldown(data.owner)
or 0
local coolingDown = cooldown > 0
row.action:SetText(
coolingDown and L("Please wait") or (data.action == "share" and L("Share") or L("Request Share"))
)
row.action:SetEnabled(not coolingDown and not self:IsWorkBlocked("quest_share"))
row.action:Show()
if status and status ~= "" then
-- Keep terminal feedback visible below a retry action, without
-- increasing row height or changing the scroll pool's geometry.
row.action:ClearAllPoints()
row.action:SetPoint("TOPLEFT", actionX, -1)
row.action:SetSize(ACTION_WIDTH - 12, 22)
row.hint:SetPoint("TOPLEFT", actionX, -24)
row.hint:SetSize(ACTION_WIDTH - 12, 18)
row.hint:SetMaxLines(1)
row.hint:SetText(L(status))
row.hint:Show()
else
row.action:SetWidth(ACTION_WIDTH - 12)
row.hint:Hide()
end
else
row.action:Hide()
if data.kind == "member" then
-- Center two-line notices beside the name instead of crowding
-- the panel's top edge with a fixed top-aligned text box.
row.hint:SetPoint("LEFT", row.title, "RIGHT", 16, 0)
row.hint:SetHeight(28)
else
row.hint:SetPoint("TOPLEFT", actionX, (data.kind and -2 or -4) - top)
end
row.hint:SetWidth(ACTION_WIDTH - (data.kind and 28 or 12))
row.hint:SetText(L(status or data.hint or ""))
row.hint:Show()
end
end
end
end
local function DetailHeights(rows)
local heights = {}
for index, row in ipairs(rows) do
if row.kind then
heights[row.questId] = (heights[row.questId] or 0) + CompareRowHeight(rows, index)
end
end
return heights
end
local function CompareHeight(frame)
local height = 0
for index in ipairs(frame.displayRows) do
height = height + CompareRowHeight(frame.displayRows, index)
end
for id, full in pairs(DetailHeights(frame.displayRows)) do
local transition = frame.expansions and frame.expansions[id]
if transition then
height = height - full + math.min(full, transition.reveal)
end
end
return height
end
function QuestTogether:CapturePartyQuestScrollAnchor()
local frame, session = rawget(self, "partyQuestCompareWindow"), rawget(self, "partyQuestCompareSession")
if not frame or not session or frame.displaySession ~= session then return end
local pixels, y, anchor = session.scrollPixels or 0, 0
for i, row in ipairs(frame.displayRows or {}) do
if not row.kind and y <= pixels then anchor = { questId = row.questId, offset = pixels - y } end
y = y + CompareRowHeight(frame.displayRows, i)
end
return anchor
end
local function RestoreScrollAnchor(addon, session, rows)
local anchor = session.restoreAnchor
if not anchor then return end
local y = 0
for i, row in ipairs(rows) do
if not row.kind and row.questId == anchor.questId then session.scrollPixels = y + anchor.offset; break end
y = y + CompareRowHeight(rows, i)
end
for _, member in ipairs(session.members) do
if member.state == "loading" or addon:HasPendingPartyQuestProgress(member) then return end
end
session.restoreAnchor = nil
end
local function ScrollCompare(self, frame, session, pixels, force)
if
self.partyQuestCompareSession ~= session
or frame.displaySession ~= session
or self:IsWorkBlocked("foreign_frame_mutation")
then
return
end
local rows = frame.displayRows
local heights, seen = DetailHeights(rows), {}
local maximum = math.max(0, CompareHeight(frame) - frame.rowsViewport:GetHeight())
pixels = math.max(0, math.min(pixels, maximum))
local visible, keys, y = {}, {}, 0
frame.navigationRows = {}
for index, data in ipairs(rows) do
local fullHeight = CompareRowHeight(rows, index)
if not data.kind then
frame.navigationRows[#frame.navigationRows + 1] = { questId = data.questId, top = y, height = fullHeight, data = data }
end
local height = fullHeight
if data.kind then
local id = data.questId
local transition = frame.expansions and frame.expansions[id]
local reveal = transition and transition.reveal or heights[id]
height = math.max(0, math.min(fullHeight, reveal - (seen[id] or 0)))
seen[id] = (seen[id] or 0) + fullHeight
end
if height > 0 and y + height > pixels and y < pixels + frame.rowsViewport:GetHeight() then
visible[#visible + 1] = { index = index, top = y, height = height }
keys[#keys + 1] = index
end
y = y + height
end
if frame.keyboardSession == session then
local selected = self:GetPartyQuestKeyboardTarget(frame.navigationRows, session.keyboardQuestId)
local id = selected and selected.questId
if session.keyboardQuestId ~= id then session.keyboardQuestId, force = id, true end
end
local origin = visible[1] and visible[1].top or 0
session.scrollPixels, session.offset = pixels, visible[1] and visible[1].index - 1 or 0
frame.visibleRows = visible
if self.SetPartyQuestCompletionInterest then
local interested, included = {}, {}
for _, slot in ipairs(visible) do
local id = rows[slot.index].questId
if not included[id] then interested[#interested + 1], included[id] = id, true end
end
self:SetPartyQuestCompletionInterest(interested)
end
for i, row in ipairs(frame.rows) do
local slot = visible[i]
if slot then
row.clip:ClearAllPoints()
row.clip:SetPoint("TOPLEFT", frame.rowsContent, "TOPLEFT", 0, -(slot.top - origin))
row.clip:SetSize(frame.content:GetWidth(), math.max(0.01, slot.height))
end
end
local key = table.concat(keys, ",")
if force or frame.drawnKey ~= key then
DrawCompareRows(self, frame, session, rows, frame.content:GetWidth(), frame.content:GetWidth() - ACTION_WIDTH)
frame.drawnKey = key
end
frame.rowsViewport:SetVerticalScroll(pixels - origin)
frame.scrolling = true
self:UpdateOwnedScrollBar(frame.vertical, pixels, maximum, frame.rowsViewport:GetHeight(), ROW_HEIGHT)
frame.scrolling = false
end
local function StopExpansion(frame)
frame.expansions, frame.animateExpansion, frame.expansionGeneration = nil, nil, nil
if frame.expansionAnimator and QuestTogether.LibChev.CanMutateOwnedRegion(frame.expansionAnimator) then
frame.expansionAnimator:SetScript("OnUpdate", nil)
end
end
local function AnimateExpansions(self, frame, session)
local transitions, generation = frame.expansions, {}
frame.expansionGeneration = generation
frame.expansionAnimator:SetScript("OnUpdate", function(_, elapsed)
if frame.expansions ~= transitions or frame.expansionGeneration ~= generation then
return
end
if self.partyQuestCompareSession ~= session or frame.displaySession ~= session then
StopExpansion(frame)
return
end
if self:IsWorkBlocked("foreign_frame_mutation") then
return
end
local remove, finished = {}, false
for id, transition in pairs(transitions) do
transition.elapsed = math.min(0.18, transition.elapsed + math.min(elapsed, 0.1))
local t = transition.elapsed / 0.18
transition.reveal = transition.from + (transition.target - transition.from) * t * t * (3 - 2 * t)
if t >= 1 then
if transition.closingRows then
remove[id] = true
end
transitions[id], finished = nil, true
end
end
if next(remove) then
local rows = {}
for _, row in ipairs(frame.displayRows) do
if not (row.kind and remove[row.questId]) then
rows[#rows + 1] = row
end
end
frame.displayRows = rows
end
if not next(transitions) then
StopExpansion(frame)
end
ScrollCompare(self, frame, session, session.scrollPixels or 0, finished)
end)
end
local function PrepareExpansions(self, frame, session, rows)
if self:GetOption("reduceMotion") then
StopExpansion(frame); frame.displayRows = rows; return
end
local intent = frame.animateExpansion
frame.animateExpansion = nil
local transitions = frame.expansions or {}
local full, oldFull = DetailHeights(rows), DetailHeights(frame.displayRows or {})
local roots, oldDetails = {}, {}
for _, row in ipairs(rows) do
if not row.kind then
roots[row.questId] = true
end
end
for _, row in ipairs(frame.displayRows or {}) do
if row.kind then
oldDetails[row.questId] = oldDetails[row.questId] or {}
local list = oldDetails[row.questId]
list[#list + 1] = row
end
end
if intent and intent.session == session and roots[intent.questId] then
local id = intent.questId
local opening = session.expandedQuestIds and session.expandedQuestIds[id]
local old = transitions[id]
local from = old and old.reveal or (opening and 0 or oldFull[id] or 0)
transitions[id] = {
from = from,
reveal = from,
target = opening and (full[id] or 0) or 0,
elapsed = 0,
closingRows = not opening and oldDetails[id] or nil,
}
end
for id, transition in pairs(transitions) do
if not roots[id] then
transitions[id] = nil
elseif not transition.closingRows and transition.target ~= (full[id] or 0) then
transition.from, transition.elapsed, transition.target = transition.reveal, 0, full[id] or 0
end
end
local display = {}
for _, row in ipairs(rows) do
display[#display + 1] = row
local transition = transitions[row.questId]
if not row.kind and transition and transition.closingRows then
for _, detail in ipairs(transition.closingRows) do
display[#display + 1] = detail
end
end
end
frame.displayRows = display
if next(transitions) then
frame.expansions = transitions
AnimateExpansions(self, frame, session)
else
StopExpansion(frame)
end
end
local function StopCompareScroll(frame)
frame.wheelTarget, frame.wheelSession = nil, nil
if QuestTogether.LibChev.CanMutateOwnedRegion(frame) then frame:SetScript("OnUpdate", nil) end
end
function QuestTogether:SetPartyQuestKeyboardNavigation(frame, active)
local session = self.partyQuestCompareSession
if active and (not session or frame.displaySession ~= session
or self:IsWorkBlocked("foreign_frame_mutation")
or not QuestTogether.LibChev.CanMutateOwnedRegion(frame) or not frame:IsVisible()
or not self.API.IsPartyQuestKeyboardInputBlocked or self.API.IsPartyQuestKeyboardInputBlocked(frame)) then
return false
end
local wasActive = frame.keyboardSession ~= nil or frame.keyboardFocusPending ~= nil
frame.keyboardFocusPending = nil
frame.keyboardSession = active and session or nil
if wasActive and not active and frame.keyboardInput then self:HideOwnedUI(frame.keyboardInput) end
if not self:IsWorkBlocked("foreign_frame_mutation") and QuestTogether.LibChev.CanMutateOwnedRegion(frame) then
if active and frame.keyboardInput then
self:CancelOwnedUICleanup(frame.keyboardInput)
frame.keyboardInput:SetPropagateKeyboardInput(true)
frame.keyboardInput:Show()
end
if session and frame.displaySession == session then
if active then
local selected = self:GetPartyQuestKeyboardTarget(frame.navigationRows, session.keyboardQuestId)
session.keyboardQuestId = selected and selected.questId
end
ScrollCompare(self, frame, session, session.scrollPixels or 0, true)
end
end
return true
end
function QuestTogether:ShowPartyQuestKeyboardMenu(frame, selected)
local session, questID = self.partyQuestCompareSession, selected and selected.questId
if not session or frame.displaySession ~= session or not questID or self:IsWorkBlocked("foreign_frame_mutation")
or not QuestTogether.LibChev.CanMutateOwnedRegion(frame) or not frame:IsVisible() then return false end
self:SetPartyQuestKeyboardNavigation(frame, false)
local function Current()
return self.partyQuestCompareSession == session and frame.displaySession == session
and QuestTogether.LibChev.CanMutateOwnedRegion(frame) and frame:IsVisible()
and not self:IsWorkBlocked("foreign_frame_mutation")
end
local anchor = frame.rowsViewport
for _, row in ipairs(frame.rows) do
if row.session == session and row.data and not row.data.kind and row.data.questId == questID and row:IsVisible() then
anchor = row
break
end
end
return self:CreatePartyQuestFilterMenu(anchor, function(_, root)
if not Current() then return end
root:CreateTitle(selected.data.title)
root:CreateButton(L("Expand or collapse quest"), function()
if Current() then self:TogglePartyQuestObjectives(questID) end
end)
local own = session.byName and session.byName[session.playerName]
if own and own.entries and own.entries[questID] then
root:CreateButton(L("Open in Quest Log"), function()
if Current() then self:OpenPartyQuestJournal(questID) end
end)
end
if selected.data.action == "share" then
root:CreateButton(L("Share"), function()
if Current() then self:SharePartyDiffQuest(questID) end
end)
elseif selected.data.action == "request" then
local owner = selected.data.owner
root:CreateButton(L("Request Share"), function()
if Current() then self:RequestPartyQuestShare(questID, owner) end
end)
end
end)
end
function QuestTogether:RequestPartyQuestKeyboardFocus(frame)
if not self.partyQuestCompareSession or self:IsWorkBlocked("foreign_frame_mutation")
or not QuestTogether.LibChev.CanMutateOwnedRegion(frame) or not frame:IsVisible() then return end
self:SetPartyQuestKeyboardNavigation(frame, false)
frame.keyboardFocusPending = self.partyQuestCompareSession
self:CancelOwnedUICleanup(frame.keyboardInput)
frame.keyboardInput:SetPropagateKeyboardInput(true)
frame.keyboardInput:Show()
end
function QuestTogether:ConfigurePartyQuestKeyboardNavigation(frame)
-- Only this child captures navigation keys. Hiding an ordinary owned frame
-- retires input during combat without changing a restricted input attribute.
local input = self:CreateOwnedWindowFrame("Frame", nil, frame)
frame.keyboardInput = input
input:SetAllPoints(frame)
input:SetFrameLevel(frame:GetFrameLevel() + 20)
input:EnableKeyboard(true)
input:SetPropagateKeyboardInput(true)
input:Hide()
-- Defer opening focus until the first render and the slash-command/menu
-- click that opened us have finished. Outside clicks and hide cancel it.
frame:HookScript("OnShow", function() self:RequestPartyQuestKeyboardFocus(frame) end)
frame.search:HookScript("OnEditFocusGained", function() self:SetPartyQuestKeyboardNavigation(frame, false) end)
frame:RegisterEvent("GLOBAL_MOUSE_DOWN")
frame:RegisterEvent("PLAYER_REGEN_DISABLED")
frame:SetScript("OnEvent", function(_, event)
if (frame.keyboardSession or frame.keyboardFocusPending) and (event == "PLAYER_REGEN_DISABLED"
or (event == "GLOBAL_MOUSE_DOWN"
and (not self.API.IsMouseOverPartyQuestWindow or not self.API.IsMouseOverPartyQuestWindow(frame)))) then
self:SetPartyQuestKeyboardNavigation(frame, false)
end
end)
local inherited = frame:GetScript("OnKeyDown")
frame:SetScript("OnKeyDown", function(_, key)
if not frame.keyboardSession and not self:IsWorkBlocked("foreign_frame_mutation")
and QuestTogether.LibChev.CanMutateOwnedRegion(frame)
and not (self.API.IsPartyQuestKeyboardInputBlocked and self.API.IsPartyQuestKeyboardInputBlocked(frame)) then
if inherited then inherited(frame, key) end
end
end)
input:SetScript("OnKeyDown", function(_, key)
local session = self.partyQuestCompareSession
if self:IsWorkBlocked("foreign_frame_mutation") or not QuestTogether.LibChev.CanMutateOwnedRegion(frame) then
self:SetPartyQuestKeyboardNavigation(frame, false)
return
end
input:SetPropagateKeyboardInput(true)
if self.API.IsPartyQuestKeyboardInputBlocked and self.API.IsPartyQuestKeyboardInputBlocked(frame) then
self:SetPartyQuestKeyboardNavigation(frame, false)
return
end
if not frame.keyboardSession then
return
end
if not session or frame.keyboardSession ~= session or frame.displaySession ~= session or not frame:IsVisible() then
self:SetPartyQuestKeyboardNavigation(frame, false)
return
end
if key == "ESCAPE" then
input:SetPropagateKeyboardInput(false)
self:DismissManagedWindow(frame, "escape")
return
elseif key == "TAB" then
input:SetPropagateKeyboardInput(false)
frame.search:SetFocus()
return
end
local moves = key == "UP" or key == "DOWN" or key == "HOME" or key == "END"
or key == "PAGEUP" or key == "PAGEDOWN"
local selected = self:GetPartyQuestKeyboardTarget(frame.navigationRows, session.keyboardQuestId,
moves and key or nil, frame.rowsViewport:GetHeight())
if not selected then return end
if moves then
session.keyboardQuestId = selected.questId
StopCompareScroll(frame)
local pixels = session.scrollPixels or 0
if selected.top < pixels then pixels = selected.top
elseif selected.top + selected.height > pixels + frame.rowsViewport:GetHeight() then
pixels = selected.top + selected.height - frame.rowsViewport:GetHeight()
end
ScrollCompare(self, frame, session, pixels, true)
elseif key == "LEFT" or key == "RIGHT" or key == "ENTER" or key == "NUMPADENTER" then
local expanded = session.expandedQuestIds and session.expandedQuestIds[selected.questId]
if (key == "LEFT" and expanded) or (key == "RIGHT" and not expanded)
or key == "ENTER" or key == "NUMPADENTER" then
frame.animateExpansion = { session = session, questId = selected.questId }
self:TogglePartyQuestObjectives(selected.questId)
end
elseif key == "SPACE" or key == "MENU" or key == "APPS" then
self:ShowPartyQuestKeyboardMenu(frame, selected)
else
-- Movement, chat, and other bindings remain untouched.
self:SetPartyQuestKeyboardNavigation(frame, false)
return
end
input:SetPropagateKeyboardInput(false)
end)
input:SetScript("OnKeyUp", function()
if self:IsWorkBlocked("foreign_frame_mutation") then
self:SetPartyQuestKeyboardNavigation(frame, false)
elseif QuestTogether.LibChev.CanMutateOwnedRegion(input) then
input:SetPropagateKeyboardInput(true)
end
end)
input:SetScript("OnUpdate", function()
if self:IsWorkBlocked("foreign_frame_mutation") then
self:SetPartyQuestKeyboardNavigation(frame, false)
elseif frame.keyboardFocusPending then
local expected = frame.keyboardFocusPending
if self.partyQuestCompareSession ~= expected or not frame:IsVisible() then
self:SetPartyQuestKeyboardNavigation(frame, false)
elseif frame.displaySession == expected then
self:SetPartyQuestKeyboardNavigation(frame, false)
self:SetPartyQuestKeyboardNavigation(frame, true)
end
elseif frame.keyboardSession and self.API.IsPartyQuestKeyboardInputBlocked(frame) then
self:SetPartyQuestKeyboardNavigation(frame, false)
end
end)
end
local function WheelCompare(self, frame, delta)
local session = self.partyQuestCompareSession
if not session or frame.displaySession ~= session or self:IsWorkBlocked("foreign_frame_mutation") then
return
end
session.restoreAnchor = nil
local maximum = math.max(0, CompareHeight(frame) - frame.rowsViewport:GetHeight())
frame.wheelTarget =
math.max(0, math.min(maximum, (frame.wheelTarget or session.scrollPixels or 0) - delta * ROW_HEIGHT * 1.5))
if self:GetOption("reduceMotion") then
local target = frame.wheelTarget
StopCompareScroll(frame); ScrollCompare(self, frame, session, target); return
end
frame.wheelSession = session
frame:SetScript("OnUpdate", function(_, elapsed)
if self.partyQuestCompareSession ~= session or frame.displaySession ~= session then
StopCompareScroll(frame)
return
end
if self:IsWorkBlocked("foreign_frame_mutation") then
return
end
if frame.wheelTarget == nil then
return
end
local current = session.scrollPixels or 0
local target = math.min(frame.wheelTarget, math.max(0, CompareHeight(frame) - frame.rowsViewport:GetHeight()))
frame.wheelTarget = target
local nextValue = current + (target - current) * (1 - math.exp(-18 * math.min(elapsed, 0.1)))
local done = math.abs(target - nextValue) < 0.5
ScrollCompare(self, frame, session, done and target or nextValue)
if done then
StopCompareScroll(frame)
end
end)
end
function QuestTogether:CreatePartyQuestCompareWindow()
if self.partyQuestCompareWindow then
return self.partyQuestCompareWindow
end
local parent = self:GetOwnedUIParent()
if not self:CanAccessForeignFrame(parent) then
return nil
end
local width = math.max(MIN_WIDTH, math.min(1280, parent:GetWidth() * 0.94))
local frame = Window(self, width, 720, L("Party Quest Log"), true)
-- Use the ordinary panel layer; HIGH also covers client quest-log panels.
-- Top-level focus can reorder this window within MEDIUM.
frame:SetFrameStrata("MEDIUM")
local scale = math.min(1, parent:GetHeight() * 0.94 / 690, parent:GetWidth() * 0.94 / width)
frame:SetScale(scale)
self.partyQuestCompareWindow = frame
frame.expansionAnimator = self:CreateOwnedWindowFrame("Frame", nil, frame)
frame.summary = Label(frame, 20, -47, width - 40, "", "GameFontHighlight")
frame.searchLabel = Label(frame, 20, -80, 65, L("Search:"), "GameFontHighlight")
frame.search = self:CreateOwnedWindowFrame("EditBox", nil, frame, "SearchBoxTemplate")
frame.search:SetPoint("TOPLEFT", 88, -74)
frame.search:SetSize(math.max(100, math.min(300, width - 500)), 26)
frame.search:SetAutoFocus(false)
frame.search:SetMaxLetters(96)
frame.search:HookScript("OnTextChanged", function(edit)
if not frame.rendering then
self:SetPartyQuestCompareFilter("search", edit:GetText())
end
end)
local function FocusList(edit)
if self:IsWorkBlocked("foreign_frame_mutation") or not QuestTogether.LibChev.CanMutateOwnedRegion(edit) then return end
edit:ClearFocus()
self:SetPartyQuestKeyboardNavigation(frame, true)
end
frame.search:HookScript("OnEscapePressed", FocusList)
frame.search:HookScript("OnEnterPressed", FocusList)
frame.search:HookScript("OnTabPressed", FocusList)
frame.filter = Button(self, frame, width - 386, -74, 124, L("Filters"), function(button)
self:SetPartyQuestKeyboardNavigation(frame, false)
self:ShowPartyQuestCompareFilters(button)
end)
frame.reset = Button(self, frame, width - 250, -74, 100, L("Reset"), function()
self:ResetPartyQuestCompareFilters()
end)
frame.activeFilters = Label(frame, 20, -111, width - 62, "", "GameFontHighlight")
frame.activeFilters:SetMaxLines(1)
frame.refresh = Button(self, frame, width - 140, -72, 112, L("Refresh"), function()
self:RefreshPartyRoster()
self:RefreshPartyQuestCompare()
end)
local close = self:CreateOwnedWindowFrame("Button", nil, frame, "UIPanelCloseButton")
frame.close = close
close:SetPoint("TOPRIGHT", 2, 1)
close:SetScript("OnClick", function() self:DismissManagedWindow(frame) end)
self:ConfigureWindowController(frame, {
dismiss = function() self:CancelPartyQuestCompare() end,
suspend = function()
self:SetPartyQuestKeyboardNavigation(frame, false)
StopCompareScroll(frame); StopExpansion(frame)
self:StopOwnedScrollBar(frame.vertical); self:StopOwnedScrollBar(frame.horizontal)
end,
theme = function()
if self.partyQuestCompareSession then self:RenderPartyQuestCompare() end
end,
})
frame.viewport = self:CreateOwnedWindowFrame("ScrollFrame", nil, frame)
frame.viewport:SetPoint("TOPLEFT", 20, -140)
frame.viewport:SetSize(width - 62, VISIBLE_ROWS * ROW_HEIGHT + 54)
frame.content = self:CreateOwnedWindowFrame("Frame", nil, frame.viewport)
frame.content:SetSize(QUEST_WIDTH + ACTION_WIDTH, VISIBLE_ROWS * ROW_HEIGHT + 54)
frame.viewport:SetScrollChild(frame.content)
frame.questHeader = Label(frame.content, 4, 0, QUEST_WIDTH - 8, L("QUEST"), "GameFontNormal")
frame.rowsViewport = self:CreateOwnedWindowFrame("ScrollFrame", nil, frame.content)
frame.rowsViewport:SetPoint("TOPLEFT", 0, -54)