Files
moon-well-ui/data/addon/ForeverUI/ObjectiveTracker.lua
T
Adrestie 2fd1b024e4 ForeverUI : titres de quête de camelot 1.60.1.70450
- Suivi des objectifs : titre « [niveau(+)] Titre (Type) », comme
  QuestObjectiveTrackerMixin:UpdateSingle avec QuestUtilsOverrides :
  préfixe de niveau toujours posé, « + » pour une élite, puis le type pour
  quatre cas (Élite, Donjon, JcJ, Raid), tirés du questTag de
  GetQuestLogTitle comparé aux textes du client (ELITE,
  CALENDAR_TYPE_DUNGEON, CALENDAR_TYPE_PVP, RAID). Pas de couleur de
  difficulté : la CVar showQuestDifficultyColor n'existe pas en 3.3.5.
- Journal : même étiquette de type pour les quatre cas (l'élite seule
  avant) ; lignes de quête décalées de 4 (titleFrameLeftPadding).
- « Lien dans la discussion » retiré des menus du suivi et du journal
  (camelot ne le garde qu'en mode manette) ; Maj-clic lie toujours la
  quête.
SUIVI_DES_QUETES.md : décision 1 révisée.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 23:59:16 +02:00

1251 lines
44 KiB
Lua

-- Camelot's quest and achievement tracker on the WotLK WatchFrame (docs/SUIVI_DES_QUETES.md).
-- WatchFrame, its events and CVars, VISIBLE_WATCHES, WatchFrameItem<n> and the handler API stay;
-- only the three WotLK display handlers are replaced by camelot's two modules.
-- Missing from 3.3.5, so absent here: Focus tracking, progress bars, automatic quest popups.
ForeverUI = ForeverUI or {}
local T = {}
ForeverUI.ObjectiveTracker = T
local L = ForeverUI.L
local G = {
width = 260, headerH = 32, headerTextX = 7, headerTextL = 208,
buttonW = 18, buttonH = 19, buttonX = -1, filterX = -2,
modulesTop = 38, rowsBottom = 12,
moduleHeaderH = 26, moduleCountedHeader = 25, moduleTextL = 200, moduleButtonSide = 16, moduleButtonX = 1,
blockX = 20, firstBlockY = -10, blockY = -10, rowGap = 4,
itemSide = 26, itemFrame = 42, rightGap = 2,
checkMark = 16, checkMarkX = -10, checkMarkY = 2,
poiX = -17, poiY = -5,
rowGlowL = 180, headerGlowL = 240,
timerW = 192, timerH = 20, barW = 128, barH = 10, barX = -4,
maxCriteria = 5,
-- default place: camelot EditModePresetLayouts, ObjectiveTracker system,
-- TOPRIGHT of UIParent at (-110, -275)
defaultX = -110, defaultY = -275, minHeight = 140,
}
-- OBJECTIVE_TRACKER_COLOR, values from camelot's GlobalColor.db2
local COLOR = {
normal = { 0.8, 0.8, 0.8 }, normalHover = { 1, 1, 1 },
failed = { 0.8, 0.098, 0.098 }, failedHover = { 1, 0.125, 0.125 },
header = { 0.749, 0.612, 0 }, headerHover = { 1, 0.824, 0 },
finished = { 0.6, 0.6, 0.6 },
title = { 1, 0.824, 0 }, -- NORMAL_FONT_COLOR
bar = { 0.26, 0.42, 1 }, barBackground = { 0.04, 0.07, 0.18 },
}
COLOR.normal.inverse = COLOR.normalHover
COLOR.normalHover.inverse = COLOR.normal
COLOR.failed.inverse = COLOR.failedHover
COLOR.failedHover.inverse = COLOR.failed
-- 3.3.5 strings when it has them, camelot's otherwise (Textes_<locale>.lua)
local TEXT = {
all = L.OBJECTIVETRACKER_ALL_OBJECTIVES, -- TRACKER_ALL_OBJECTIVES
quests = QUESTS_LABEL, -- TRACKER_HEADER_QUESTS (3.3.5: QUESTS_LABEL)
achievements = ACHIEVEMENTS, -- TRACKER_HEADER_ACHIEVEMENTS (3.3.5: ACHIEVEMENTS)
ready = L.OBJECTIVETRACKER_READY, -- QUEST_WATCH_QUEST_READY
viewPage = OBJECTIVES_VIEW_IN_QUESTLOG, -- OBJECTIVES_VIEW_IN_QUESTLOG (in 3.3.5)
viewMap = L.OBJECTIVETRACKER_OPEN_MAP, -- OBJECTIVES_SHOW_QUEST_MAP
untrack = L.OBJECTIVETRACKER_UNTRACK, -- OBJECTIVES_STOP_TRACKING
abandonQuest = ABANDON_QUEST_ABBREV, -- ABANDON_QUEST_ABBREV (in 3.3.5)
viewAchievement = OBJECTIVES_VIEW_ACHIEVEMENT, -- OBJECTIVES_VIEW_ACHIEVEMENT (in 3.3.5)
minutes = "%.2d:%.2d", -- MINUTES_SECONDS
hours = "%.2d:%.2d:%.2d", -- HOURS_MINUTES_SECONDS
}
local FONT = "Fonts\\FRIZQT__.TTF"
local function color(fs, c)
fs:SetTextColor(c[1], c[2], c[3])
fs.color = c
end
-- ObjectiveTrackerLineFont (12) and ObjectiveTrackerHeaderFont (14), black shadow (1, -1)
local function font(fs, size)
fs:SetFont(FONT, size)
fs:SetShadowOffset(1, -1)
fs:SetShadowColor(0, 0, 0, 1)
fs:SetJustifyH("LEFT")
fs:SetJustifyV("TOP")
end
local function settings()
ForeverUIDB = ForeverUIDB or {}
ForeverUIDB.tracking = ForeverUIDB.tracking or {}
ForeverUIDB.tracking.collapsedModules = ForeverUIDB.tracking.collapsedModules or {}
return ForeverUIDB.tracking
end
-- camelot SecondsToClock
local function clock(seconds)
seconds = math.max(0, math.floor(seconds))
local h = math.floor(seconds / 3600)
local m = math.floor((seconds % 3600) / 60)
local s = seconds % 60
if h > 0 then
return format(TEXT.hours, h, m, s)
end
return format(TEXT.minutes, m, s)
end
-- ---------- Animations
-- One OnUpdate plays every running step: a delay, a duration, and a function that receives
-- the progress from 0 to 1. 3.3.5 animation groups lack fromAlpha / toAlpha and
-- setToFinalAlpha, so camelot's animations are replayed by hand with its timings.
-- A finish callback may start new steps (a chained step, a WatchFrame_Update): finish
-- callbacks run after the loop, since Lua leaves a traversal undefined when a key is added
-- during it, and the frame stops only once no step is left.
local A = { steps = {} }
A.frame = CreateFrame("Frame")
A.frame:Hide()
A.frame:SetScript("OnUpdate", function(self, elapsed)
local finished
for key, e in pairs(A.steps) do
e.t = e.t + elapsed
if e.t >= e.delay then
local p = (e.duration > 0) and math.min(1, (e.t - e.delay) / e.duration) or 1
e.fn(p)
if p >= 1 then
A.steps[key] = nil
if e.finish then
finished = finished or {}
table.insert(finished, e.finish)
end
end
end
end
if finished then
for _, finish in ipairs(finished) do finish() end
end
if not next(A.steps) then self:Hide() end
end)
-- key: { object, name }, a step with the same key replaces the running one;
-- fn(progress) runs each frame; finish: called once done
local function play(key, delay, duration, fn, finish)
key = tostring(key[1]) .. key[2]
A.steps[key] = { t = 0, delay = delay, duration = duration, fn = fn, finish = finish }
A.frame:Show()
end
T.play = play
T.animations = A
-- smoothing="OUT"
local function easeOut(p) return 1 - (1 - p) * (1 - p) end
-- ---------- Buttons
-- atlasButton: l, h: size; normal, pressed, hover: atlas names
local function atlasButton(parent, l, h, normal, pressed, hover)
local b = CreateFrame("Button", nil, parent)
b:SetWidth(l)
b:SetHeight(h)
local e = ForeverUI.AtlasEntry(normal)
b:SetNormalTexture(e and e[1] or "")
ForeverUI.SetAtlas(b:GetNormalTexture(), normal, true)
b:GetNormalTexture():SetAllPoints(b)
b:SetPushedTexture(e and e[1] or "")
ForeverUI.SetAtlas(b:GetPushedTexture(), pressed, true)
b:GetPushedTexture():SetAllPoints(b)
b:SetHighlightTexture(e and e[1] or "")
local s = b:GetHighlightTexture()
ForeverUI.SetAtlas(s, hover, true)
s:SetAllPoints(b)
s:SetBlendMode("ADD")
return b
end
local function buttonStates(b, normal, pressed)
ForeverUI.SetAtlas(b:GetNormalTexture(), normal, true)
ForeverUI.SetAtlas(b:GetPushedTexture(), pressed, true)
end
-- opens a dropdown through the world map's menu helper
local function menu(anchor, list)
if ForeverUI.WorldMap and ForeverUI.WorldMap.openMenu then
ForeverUI.WorldMap.openMenu(anchor, list)
end
end
-- ---------- Main header
-- ObjectiveTrackerContainerHeaderTemplate, on WatchFrame
local function createHeader()
local h = CreateFrame("Frame", "ForeverUIObjectiveTrackerHeader", WatchFrame)
h:SetWidth(G.width)
h:SetHeight(G.headerH)
h:SetPoint("TOPLEFT", WatchFrame, "TOPLEFT", 0, 0)
local background = h:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(background, "ui-questtracker-primary-objective-header")
background:SetPoint("CENTER", h, "CENTER", 0, 0)
h.background = background
local text = h:CreateFontString(nil, "ARTWORK")
font(text, 14)
color(text, COLOR.title)
text:SetWidth(G.headerTextL)
text:SetJustifyV("MIDDLE")
text:SetPoint("LEFT", h, "LEFT", G.headerTextX, 0)
text:SetText(TEXT.all)
h.text = text
local shrink = atlasButton(h, G.buttonW, G.buttonH, "ui-questtrackerbutton-collapse-all",
"ui-questtrackerbutton-collapse-all-pressed", "ui-questtrackerbutton-red-highlight")
shrink:SetPoint("RIGHT", h, "RIGHT", G.buttonX, 0)
-- WatchFrame_CollapseExpandButton_OnClick, with camelot's sound
shrink:SetScript("OnClick", function()
PlaySound("igMainMenuOptionCheckBoxOn")
if WatchFrame.collapsed then
WatchFrame.userCollapsed = nil
WatchFrame_Expand(WatchFrame)
else
WatchFrame.userCollapsed = true
WatchFrame_Collapse(WatchFrame)
end
end)
h.shrink = shrink
-- filter button (camelot has it but hides it): WotLK sorting and filters
local filter = atlasButton(h, G.buttonW, G.buttonH, "ui-questtrackerbutton-filter",
"ui-questtrackerbutton-filter-pressed", "ui-questtrackerbutton-red-highlight")
filter:SetPoint("RIGHT", shrink, "LEFT", G.filterX, 0)
filter:SetScript("OnClick", function(self)
PlaySound("igMainMenuOptionCheckBoxOn")
T.filterMenu(self)
end)
h.filter = filter
h:Hide()
return h
end
-- WatchFrameHeaderDropDown_Initialize, entry for entry
function T.filterMenu(anchor)
local sort = WATCHFRAME_SORT_TYPE
local filter = WATCHFRAME_FILTER_TYPE
local function sortBy(value)
return function() WatchFrame_SetSorting(nil, value) end
end
local function filterBy(value)
return function() WatchFrame_SetFilter(nil, value) end
end
local function active(bit_)
return bit.band(filter, bit_) == bit_
end
menu(anchor, {
{ text = TRACKER_SORT_LABEL, isTitle = true },
{ text = TRACKER_SORT_PROXIMITY, checked = sort == WATCHFRAME_SORT_PROXIMITY, func = sortBy(WATCHFRAME_SORT_PROXIMITY) },
{ text = TRACKER_SORT_DIFFICULTY_HIGH, checked = sort == WATCHFRAME_SORT_DIFFICULTY_HIGH, func = sortBy(WATCHFRAME_SORT_DIFFICULTY_HIGH) },
{ text = TRACKER_SORT_DIFFICULTY_LOW, checked = sort == WATCHFRAME_SORT_DIFFICULTY_LOW, func = sortBy(WATCHFRAME_SORT_DIFFICULTY_LOW) },
{ text = TRACKER_SORT_MANUAL, checked = sort == WATCHFRAME_SORT_MANUAL, func = sortBy(WATCHFRAME_SORT_MANUAL) },
{ text = TRACKER_FILTER_LABEL, isTitle = true },
{ text = TRACKER_FILTER_ACHIEVEMENTS, checked = active(WATCHFRAME_FILTER_ACHIEVEMENTS), keepShownOnClick = 1, func = filterBy(WATCHFRAME_FILTER_ACHIEVEMENTS) },
{ text = TRACKER_FILTER_COMPLETED_QUESTS, checked = active(WATCHFRAME_FILTER_COMPLETED_QUESTS), keepShownOnClick = 1, func = filterBy(WATCHFRAME_FILTER_COMPLETED_QUESTS) },
{ text = TRACKER_FILTER_REMOTE_ZONES, checked = active(WATCHFRAME_FILTER_REMOTE_ZONES), keepShownOnClick = 1, func = filterBy(WATCHFRAME_FILTER_REMOTE_ZONES) },
})
end
-- ---------- Modules
-- ObjectiveTrackerModuleTemplate: a header, then the blocks.
-- title: header text; key: saved collapse state key
local function createModule(name, title, key)
local m = CreateFrame("Frame", name, WatchFrameLines)
m:SetWidth(G.width)
m:SetHeight(10)
m.key = key
local h = CreateFrame("Frame", nil, m)
h:SetWidth(G.width)
h:SetHeight(G.moduleHeaderH)
h:SetPoint("TOPLEFT", m, "TOPLEFT", 0, 0)
local background = h:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(background, "ui-questtracker-secondary-objective-header")
background:SetPoint("CENTER", h, "CENTER", 0, 0)
local text = h:CreateFontString(nil, "ARTWORK")
font(text, 14)
color(text, COLOR.title)
text:SetWidth(G.moduleTextL)
text:SetJustifyV("MIDDLE")
text:SetPoint("LEFT", h, "LEFT", G.headerTextX, 0)
text:SetText(title)
local shine = h:CreateTexture(nil, "ARTWORK")
ForeverUI.SetAtlas(shine, "ui-questtracker-objfx-shine")
shine:SetWidth(shine:GetWidth() * 0.95)
shine:SetHeight(shine:GetHeight() * 0.95)
shine:SetPoint("CENTER", h, "CENTER", -150, 1)
shine:SetAlpha(0)
local glow = h:CreateTexture(nil, "OVERLAY")
ForeverUI.SetAtlas(glow, "ui-questtracker-objfx-barglow")
glow:SetPoint("CENTER", h, "CENTER", -120, 1)
glow:SetAlpha(0)
local button = atlasButton(h, G.moduleButtonSide, G.moduleButtonSide, "ui-questtrackerbutton-secondary-collapse",
"ui-questtrackerbutton-secondary-collapse-pressed", "ui-questtrackerbutton-yellow-highlight")
button:SetPoint("RIGHT", h, "RIGHT", G.moduleButtonX, 0)
button:SetScript("OnClick", function()
PlaySound("igMainMenuOptionCheckBoxOn")
local r = settings().collapsedModules
r[key] = not r[key] or nil
WatchFrame_Update()
end)
h.background, h.text, h.shine, h.glow, h.button = background, text, shine, glow, button
m.header = h
m.blocks, m.freeBlocks = {}, {}
m:Hide()
return m
end
-- ObjectiveTrackerModuleHeaderMixin:PlayAddAnimation
local function animateModuleHeader(m)
local h = m.header
local key = m
h.background:SetAlpha(0)
h.button:SetAlpha(0)
play({ key, "background" }, 0, 0.5, function(p) h.background:SetAlpha(p) end)
play({ key, "button" }, 0, 1, function(p) h.button:SetAlpha(p) end)
play({ key, "glow" }, 0, 0.2, function(p) h.glow:SetAlpha(p) end, function()
play({ key, "glow" }, 0, 0.6, function(p) h.glow:SetAlpha(1 - p) end)
end)
play({ key, "shine" }, 0.2, 1.2, function(p)
local q = math.min(1, p * 1.2 / 0.7)
h.shine:ClearAllPoints()
h.shine:SetPoint("CENTER", h, "CENTER", -150 + 200 * q, 1)
h.shine:SetAlpha(1 - p)
end, function() h.shine:SetAlpha(0) end)
end
-- ---------- Blocks
local function createRow(block)
local l = CreateFrame("Frame", nil, block)
local dash = l:CreateFontString(nil, "ARTWORK")
font(dash, 12)
dash:SetPoint("TOPLEFT", l, "TOPLEFT", 0, 1)
dash:SetText(QUEST_DASH)
local text = l:CreateFontString(nil, "ARTWORK")
font(text, 12)
text:SetPoint("TOP", l, "TOP", 0, 0)
text:SetPoint("LEFT", dash, "RIGHT", 0, 0)
local checkMark = l:CreateTexture(nil, "ARTWORK")
ForeverUI.SetAtlas(checkMark, "ui-questtracker-tracker-check", true)
checkMark:SetWidth(G.checkMark)
checkMark:SetHeight(G.checkMark)
checkMark:SetPoint("TOPLEFT", l, "TOPLEFT", G.checkMarkX, G.checkMarkY)
checkMark:Hide()
local flare = l:CreateTexture(nil, "OVERLAY")
ForeverUI.SetAtlas(flare, "ui-questtracker-tracker-check-glow", true)
flare:SetWidth(G.checkMark)
flare:SetHeight(G.checkMark)
flare:SetPoint("CENTER", checkMark, "CENTER", 0, 0)
flare:SetAlpha(0)
local glow = l:CreateTexture(nil, "OVERLAY")
ForeverUI.SetAtlas(glow, "ui-questtracker-objfx-barglow", true)
glow:SetWidth(G.rowGlowL)
glow:SetPoint("LEFT", text, "LEFT", -2, 0)
glow:SetPoint("TOP", l, "TOP", 0, 0)
glow:SetPoint("BOTTOM", l, "BOTTOM", 0, -4)
glow:SetAlpha(0)
l.dash, l.text, l.checkMark, l.flare, l.glow = dash, text, checkMark, flare, glow
return l
end
-- ObjectiveTrackerTimerBarTemplate
local function createTimer(block)
local m = CreateFrame("Frame", nil, block)
m:SetWidth(G.timerW)
m:SetHeight(G.timerH)
-- camelot GameFontHighlightMedium: FRIZQT 14, white, shadow
local text = m:CreateFontString(nil, "ARTWORK")
text:SetFont(FONT, 14)
text:SetShadowOffset(1, -1)
text:SetShadowColor(0, 0, 0, 1)
text:SetJustifyH("LEFT")
text:SetPoint("LEFT", m, "LEFT", 0, 0)
local bar = CreateFrame("StatusBar", nil, m)
bar:SetWidth(G.barW)
bar:SetHeight(G.barH)
bar:SetPoint("RIGHT", m, "RIGHT", G.barX, 0)
bar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
bar:SetStatusBarColor(COLOR.bar[1], COLOR.bar[2], COLOR.bar[3])
local background = bar:CreateTexture(nil, "BACKGROUND")
background:SetTexture(COLOR.barBackground[1], COLOR.barBackground[2], COLOR.barBackground[3])
background:SetAllPoints(bar)
local BAR_BORDER = "Interface\\PaperDollInfoFrame\\UI-Character-Skills-BarBorder"
local g = bar:CreateTexture(nil, "ARTWORK")
g:SetTexture(BAR_BORDER)
g:SetTexCoord(0.007843, 0.043137, 0.193548, 0.774193)
g:SetWidth(9) g:SetHeight(14)
g:SetPoint("LEFT", bar, "LEFT", -3, 0)
local d = bar:CreateTexture(nil, "ARTWORK")
d:SetTexture(BAR_BORDER)
d:SetTexCoord(0.043137, 0.007843, 0.193548, 0.774193)
d:SetWidth(9) d:SetHeight(14)
d:SetPoint("RIGHT", bar, "RIGHT", 3, 0)
local mid = bar:CreateTexture(nil, "ARTWORK")
mid:SetTexture(BAR_BORDER)
mid:SetTexCoord(0.113726, 0.1490196, 0.193548, 0.774193)
mid:SetPoint("TOPLEFT", g, "TOPRIGHT", 0, 0)
mid:SetPoint("BOTTOMRIGHT", d, "BOTTOMLEFT", 0, 0)
m.text, m.bar = text, bar
-- ObjectiveTrackerTimerBarMixin:OnUpdate and GetTextColor
m:SetScript("OnUpdate", function(self)
if not self.duration then return end
local rest = self.duration - (GetTime() - self.start)
self.bar:SetValue(math.max(0, rest))
if rest < -1 then
self.duration = nil
WatchFrame_Update()
return
end
rest = math.max(0, rest)
self.text:SetText(clock(rest))
local part = rest / self.duration
if part > 0.66 then
self.text:SetTextColor(1, 1, 1)
elseif part > 0.33 then
self.text:SetTextColor(1, 1, (part - 0.33) / 0.33)
else
self.text:SetTextColor(1, part / 0.33, 0)
end
end)
return m
end
local function createBlock(m)
local b = CreateFrame("Frame", nil, m)
local title = b:CreateFontString(nil, "ARTWORK")
font(title, 12)
title:SetPoint("TOPLEFT", b, "TOPLEFT", 0, 0)
b.title = title
local glow = b:CreateTexture(nil, "OVERLAY")
ForeverUI.SetAtlas(glow, "ui-questtracker-objfx-barglow", true)
glow:SetWidth(G.headerGlowL)
glow:SetPoint("TOPLEFT", title, "TOPLEFT", 0, 3)
glow:SetPoint("BOTTOMLEFT", title, "BOTTOMLEFT", 0, -4)
glow:SetAlpha(0)
b.glow = glow
-- HeaderButton: over the title, left and right click
local button = CreateFrame("Button", nil, b)
button:SetPoint("TOPLEFT", title, "TOPLEFT", 0, 0)
button:SetPoint("BOTTOMRIGHT", title, "BOTTOMRIGHT", 0, 0)
button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
button:SetScript("OnClick", function(_, mouse) m.onClick(b, mouse) end)
button:SetScript("OnEnter", function() T.toggleHighlight(b, true) end)
button:SetScript("OnLeave", function() T.toggleHighlight(b, false) end)
b.button = button
b.rows, b.freeRows = {}, {}
return b
end
-- ObjectiveTrackerBlockMixin:UpdateHighlight; yes: hovered
function T.toggleHighlight(b, yes)
b.hover = yes
color(b.title, yes and COLOR.headerHover or COLOR.header)
local dash = yes and COLOR.normalHover or COLOR.normal
for _, l in ipairs(b.shownRows or {}) do
local c = l.text.color
if c and c.inverse and ((yes and (c == COLOR.normal or c == COLOR.failed))
or (not yes and (c == COLOR.normalHover or c == COLOR.failedHover))) then
color(l.text, c.inverse)
end
l.dash:SetTextColor(dash[1], dash[2], dash[3])
end
end
-- A block starts from scratch on every pass (Reset), and so do its rows.
local function acquireBlock(m, id)
local b = m.blocks[id]
if not b then
b = table.remove(m.freeBlocks) or createBlock(m)
m.blocks[id] = b
end
b.id, b.inUse = id, true
b:SetWidth(G.width - G.blockX)
b.contentHeight = 0
b.last = nil
b.right = 0
b.shownRows = {}
for _, l in pairs(b.rows) do l.used = nil end
if b.timer then b.timer:Hide() b.timer.duration = nil end
b:ClearAllPoints()
b:SetAlpha(1)
b.title:SetAlpha(1)
return b
end
-- Widths are set by hand: the block is not placed yet when its texts are measured, and
-- 3.3.5 only measures a text that has a width (camelot relies on anchors).
-- camelot 70450 block title (QuestObjectiveTrackerMixin:UpdateSingle with QuestUtilsOverrides):
-- "[" level ("+" if elite) "] " title, then the quest type ("(Elite)", "(Dungeon)"...). The
-- difficulty color (showQuestDifficultyColor) is a modern CVar 3.3.5 lacks: not applied.
local function trackerTitle(title, level, questTag)
if not title then
return title
end
local tagText = ForeverUI.QuestTagText and ForeverUI.QuestTagText(questTag)
if tagText then
title = title .. " " .. tagText
end
if level and level > 0 then
title = "[" .. level .. ((questTag == ELITE) and "+" or "") .. "] " .. title
end
return title
end
local function setBlockTitle(b, text)
b.title:SetWidth(G.width - G.blockX + b.right)
b.title:SetHeight(0)
b.title:SetText(text)
color(b.title, b.hover and COLOR.headerHover or COLOR.header)
b.contentHeight = b.title:GetHeight()
end
-- ObjectiveTrackerBlockMixin:AddObjective. key: row id in the block; dash: show the dash;
-- c: text color (default normal)
local function row(b, key, text, dash, c)
local l = b.rows[key]
if not l then
l = table.remove(b.freeRows) or createRow(b)
b.rows[key] = l
end
l.used = true
l:ClearAllPoints()
l:SetPoint("TOPLEFT", b.last or b.title, "BOTTOMLEFT", 0, -G.rowGap)
local width = G.width - G.blockX + b.right
l:SetWidth(width)
l.dash:SetText(QUEST_DASH)
if dash then l.dash:Show() else l.dash:Hide() end
l.text:SetWidth(width - l.dash:GetStringWidth())
l.text:SetHeight(0)
l.text:SetText(text)
c = c or COLOR.normal
if b.hover and c.inverse then c = c.inverse end
color(l.text, c)
local tc = b.hover and COLOR.normalHover or COLOR.normal
l.dash:SetTextColor(tc[1], tc[2], tc[3])
local h = l.text:GetHeight()
l:SetHeight(h)
l:SetAlpha(1)
l:Show()
b.contentHeight = b.contentHeight + h + G.rowGap
b.last = l
table.insert(b.shownRows, l)
return l
end
-- ObjectiveTrackerBlockMixin:AddTimerBar; start: GetTime() when the timer began
local function timer(b, duration, start)
if not b.timer then b.timer = createTimer(b) end
local m = b.timer
m:ClearAllPoints()
m:SetPoint("TOPLEFT", b.last or b.title, "BOTTOMLEFT", 0, -G.rowGap)
m.bar:SetMinMaxValues(0, duration)
m.duration, m.start = duration, start
m:Show()
b.contentHeight = b.contentHeight + G.timerH + G.rowGap
b.last = m
end
-- rows and blocks not reused in this pass are released
local function releaseRows(b)
for key, l in pairs(b.rows) do
if not l.used then
l:Hide()
l.checkMark:Hide()
l.glow:SetAlpha(0)
l.flare:SetAlpha(0)
b.rows[key] = nil
table.insert(b.freeRows, l)
end
end
end
local function releaseBlocks(m)
for id, b in pairs(m.blocks) do
if not b.inUse then
b:Hide()
b.hover = nil
m.blocks[id] = nil
table.insert(m.freeBlocks, b)
end
end
end
-- check state of a row: Completed = check mark shown, no animation;
-- Completing (animate) = check mark + flare + glow (CheckAnim and GlowAnim)
local function checkOff(l, animate)
l.checkMark:Show()
if not animate then
l.checkMark:SetAlpha(1)
return
end
play({ l, "checkMark" }, 0, 0.3, function(p)
local s = (p < 0.5) and (1 + 0.2 * easeOut(p * 2)) or (1.2 - 0.2 * easeOut((p - 0.5) * 2))
l.checkMark:SetWidth(G.checkMark * s)
l.checkMark:SetHeight(G.checkMark * s)
l.checkMark:SetAlpha(math.min(1, p / 0.53))
l.flare:SetWidth(G.checkMark * s)
l.flare:SetHeight(G.checkMark * s)
l.flare:SetAlpha(p < 0.5 and p * 2 or (1 - p) * 2)
end, function()
l.checkMark:SetWidth(G.checkMark) l.checkMark:SetHeight(G.checkMark) l.checkMark:SetAlpha(1)
l.flare:SetAlpha(0)
end)
T.sweep(l.glow, G.rowGlowL, 0.1, 0.66, 0.33, 0.58)
end
-- a glow that stretches from the left (Scale x 0 -> 1, origin LEFT), then fades out
-- (Alpha 1 -> 0)
function T.sweep(t, width, delay, duration, alphaDelay, alphaDuration)
play({ t, "width" }, delay, duration, function(p)
t:SetWidth(math.max(1, width * easeOut(p)))
end)
play({ t, "alpha" }, alphaDelay, alphaDuration, function(p) t:SetAlpha(1 - p) end,
function() t:SetAlpha(0) t:SetWidth(width) end)
end
-- ObjectiveTrackerAnimBlockMixin:PlayAddAnimation: title glow, then the title and rows fade in
local function animateAdd(b)
b.glow:SetWidth(1)
b.glow:SetAlpha(1)
play({ b.glow, "width" }, 0.15, 0.31, function(p) b.glow:SetWidth(math.max(1, G.headerGlowL * p)) end)
play({ b.glow, "alpha" }, 0.33, 0.41, function(p) b.glow:SetAlpha(1 - p) end,
function() b.glow:SetAlpha(0) b.glow:SetWidth(G.headerGlowL) end)
b.title:SetAlpha(0)
play({ b.title, "alpha" }, 0, 0.03, function(p) b.title:SetAlpha(p) end)
for _, l in ipairs(b.shownRows) do
l:SetAlpha(0)
play({ l, "entry" }, 0, 0.5, function(p) l:SetAlpha(p) end)
end
end
-- ---------- Layout
-- ObjectiveTrackerModuleMixin: BeginLayout, AddBlock / CanFitBlock, EndLayout.
-- position: height available; finalize returns the module height, 0 when hidden.
local function begin(m, position)
m.position = position
m.contentHeight = G.moduleCountedHeader
m.last = nil
m.content = false
m.skipped = false
m.tried = false
for _, b in pairs(m.blocks) do b.inUse = nil end
end
-- returns true when the block fits
local function place(m, b)
m.tried = true
releaseRows(b)
b:SetHeight(math.max(1, b.contentHeight))
local gap = m.last and G.blockY or G.firstBlockY
if m.contentHeight + b.contentHeight - gap > m.position then
m.skipped = true
b.inUse = nil
return false
end
m.content = true
if settings().collapsedModules[m.key] then
b.inUse = nil
return true
end
b:ClearAllPoints()
if m.last then
b:SetPoint("TOP", m.last, "BOTTOM", 0, gap)
else
b:SetPoint("TOP", m.header, "BOTTOM", 0, gap)
end
b:SetPoint("LEFT", m, "LEFT", G.blockX, 0)
b:SetPoint("RIGHT", m, "RIGHT", 0, 0)
b:Show()
m.contentHeight = m.contentHeight + b.contentHeight - gap
m.last = b
return true
end
local function finalize(m, lineFrame, offset)
releaseBlocks(m)
local collapsed = settings().collapsedModules[m.key]
buttonStates(m.header.button,
collapsed and "ui-questtrackerbutton-secondary-expand" or "ui-questtrackerbutton-secondary-collapse",
collapsed and "ui-questtrackerbutton-secondary-expand-pressed" or "ui-questtrackerbutton-secondary-collapse-pressed")
if m.content then
m:ClearAllPoints()
m:SetPoint("TOPLEFT", lineFrame, "TOPLEFT", 0, offset)
m:SetHeight(m.contentHeight)
local wasShown = m:IsShown() and m.displayed
m:Show()
m.displayed = true
if not wasShown then animateModuleHeader(m) end
return m.contentHeight
end
m:Hide()
m.displayed = nil
return 0
end
-- space left in WatchFrameLines below this offset
local function remainingSpace(maxHeight, offset)
return (maxHeight or 0) - G.modulesTop - G.rowsBottom + (offset or 0)
end
-- ---------- Quests
-- durations: largest remaining time seen per quest. 3.3.5 only gives the remaining time
-- (GetQuestTimers), so that value is the timer bar's full duration.
local Q = { states = {}, seen = nil, durations = {} }
-- WatchFrame_DisplayTrackedQuests for the data, QuestObjectiveTracker for the display.
-- Zone quests: WotLK's LOCAL_MAP_QUESTS is kept in our own table. The client fills it only in
-- its quest handler, which is removed; writing it from the addon would taint it, and with it
-- the world map that reads it.
local locales = {}
T.locales = locales
T.numItems = 0
local function displayQuests(lineFrame, initialOffset, maxHeight, frameWidth)
local m = T.quests
begin(m, remainingSpace(maxHeight, initialOffset))
local money = GetMoney()
local numWatched = GetNumQuestWatches()
local nPOI = { numbered = 0, inside = 0, outside = 0 }
local objects = 0
local seen = {}
for w = 1, numWatched do
local index = GetQuestIndexForWatch(w)
if index then
local title, _, _, _, _, _, _, _, questID = GetQuestLogTitle(index)
seen[questID or title] = true
end
end
local questTimers = {}
local timers = { GetQuestTimers() }
for i, seconds in ipairs(timers) do
local index = GetQuestIndexForTimer(i)
if index then questTimers[index] = seconds end
end
local selection
if WorldMapFrame and WorldMapFrame:IsShown() then
selection = WORLDMAP_SETTINGS.selectedQuestId
else
table.wipe(locales)
locales["zone"] = GetCurrentMapZone()
for id in pairs(CURRENT_MAP_QUESTS) do
locales[id] = true
end
end
table.wipe(VISIBLE_WATCHES)
for w = 1, numWatched do
local index = GetQuestIndexForWatch(w)
if index then
local title, level, questTag, _, _, _, isComplete, _, questID = GetQuestLogTitle(index)
local required = GetQuestLogRequiredMoney(index)
local numObjectives = GetNumQuestLeaderBoards(index)
local failed = isComplete and isComplete < 0
if failed then
isComplete = false
elseif isComplete and isComplete > 0 then
isComplete = true
elseif numObjectives == 0 and money >= required then
isComplete = true
else
isComplete = false
end
-- WotLK filters
local keep = true
if isComplete and bit.band(WATCHFRAME_FILTER_TYPE, WATCHFRAME_FILTER_COMPLETED_QUESTS) ~= WATCHFRAME_FILTER_COMPLETED_QUESTS then
keep = false
elseif bit.band(WATCHFRAME_FILTER_TYPE, WATCHFRAME_FILTER_REMOTE_ZONES) ~= WATCHFRAME_FILTER_REMOTE_ZONES and not locales[questID] then
keep = false
end
if keep then
if required > 0 then WatchFrame.watchMoney = true end
local _, object, charges = GetQuestLogSpecialItemInfo(index)
local key = questID or title
local before = Q.states[key]
local state = { finished = {}, isComplete = isComplete }
local b = acquireBlock(m, key)
b.index, b.watch, b.questID, b.questTitle = index, w, questID, title
-- quest item, right of the block
local button
if object and not isComplete then
objects = objects + 1
button = T.object(objects, lineFrame, index, object, charges)
b.right = -(G.itemSide + G.rightGap)
end
setBlockTitle(b, trackerTitle(title, level, questTag))
local animate = {}
if isComplete then
-- QUEST_LOG_UPDATE: objectives already shown fade out (FadeOutAnim: 1 s then 0.1 s),
-- then the completion text
local fade = before and not before.isComplete and not (before.fade and before.fade <= GetTime())
if fade then
state.fade = before.fade or (GetTime() + 1.1)
for j = 1, numObjectives do
local text = GetQuestLogLeaderBoard(j, index)
if text then
local l = row(b, j, WatchFrame_ReverseQuestObjective(text), false, COLOR.finished)
state.finished[j] = true
table.insert(animate, { l, not (before.finished and before.finished[j]) })
play({ l, "fade" }, math.max(0, state.fade - 0.1 - GetTime()), 0.1,
function(p) l:SetAlpha(1 - p) end)
end
end
state.isComplete = false
T.restart(state.fade)
else
local completionText = GetQuestLogCompletionText(index)
if completionText then
row(b, "QuestComplete", completionText, false)
else
row(b, "QuestComplete", TEXT.ready, false, COLOR.finished)
end
end
elseif failed then
row(b, "Failed", FAILED, false, COLOR.failed)
else
for j = 1, numObjectives do
local text, _, finished = GetQuestLogLeaderBoard(j, index)
if text then
text = WatchFrame_ReverseQuestObjective(text)
if finished then
state.finished[j] = true
local l = row(b, j, text, false, COLOR.finished)
table.insert(animate, { l, before and not (before.finished and before.finished[j]) })
else
local l = row(b, j, text, true)
l.checkMark:Hide()
end
end
end
if required > money then
row(b, "Money", GetMoneyString(money) .. " / " .. GetMoneyString(required), true)
end
local rest = questTimers[index]
if rest then
local duration = math.max(Q.durations[key] or 0, rest)
Q.durations[key] = duration
timer(b, duration, GetTime() - (duration - rest))
end
end
Q.states[key] = state
if place(m, b) then
if not settings().collapsedModules[m.key] then
table.insert(VISIBLE_WATCHES, index)
for _, a in ipairs(animate) do checkOff(a[1], a[2]) end
if button then
button:ClearAllPoints()
button:SetPoint("TOPRIGHT", b, "TOPRIGHT", 0, 0)
button:Show()
end
-- WotLK POI button (QuestPOI_DisplayButton, 32 x 32), centered where camelot centers
-- its 20 x 20 one
if WatchFrame.showObjectives then
local poi
if CURRENT_MAP_QUESTS[questID] then
if isComplete then
nPOI.inside = nPOI.inside + 1
poi = QuestPOI_DisplayButton("WatchFrameLines", QUEST_POI_COMPLETE_IN, nPOI.inside, questID)
else
nPOI.numbered = nPOI.numbered + 1
poi = QuestPOI_DisplayButton("WatchFrameLines", QUEST_POI_NUMERIC, nPOI.numbered, questID)
end
elseif isComplete then
nPOI.outside = nPOI.outside + 1
poi = QuestPOI_DisplayButton("WatchFrameLines", QUEST_POI_COMPLETE_OUT, nPOI.outside, questID)
end
if poi then
poi:ClearAllPoints()
poi:SetPoint("CENTER", b.title, "TOPLEFT", G.poiX, G.poiY)
end
end
-- a quest absent from the previous pass is new (3.3.5 has no QUEST_WATCH_LIST_CHANGED)
if Q.seen and not Q.seen[key] then
animateAdd(b)
end
elseif button then
button:Hide()
end
else
if button then
button:Hide()
objects = objects - 1
end
break
end
end
end
end
for i = objects + 1, T.numItems do
local it = _G["WatchFrameItem" .. i]
if it then it:Hide() end
end
QuestPOI_HideButtons("WatchFrameLines", QUEST_POI_NUMERIC, nPOI.numbered + 1)
QuestPOI_HideButtons("WatchFrameLines", QUEST_POI_COMPLETE_IN, nPOI.inside + 1)
QuestPOI_HideButtons("WatchFrameLines", QUEST_POI_COMPLETE_OUT, nPOI.outside + 1)
if selection then
QuestPOI_SelectButtonByQuestId("WatchFrameLines", selection, true)
end
-- remember this pass; the first pass animates no quest (they were already there)
for key in pairs(Q.states) do
if not seen[key] then Q.states[key] = nil end
end
Q.seen = seen
local height = finalize(m, lineFrame, initialOffset)
return height, G.width, numWatched
end
T.displayQuests = displayQuests
-- one more pass when a fade ends
function T.restart(when)
play({ T, "rerun" }, math.max(0, when - GetTime()), 0, function() end, function()
WatchFrame_Update()
end)
end
-- WatchFrameItem<n>: the WotLK buttons, reskinned once. n: button number;
-- index: quest log index; icon, charges: the quest item
function T.object(n, lineFrame, index, icon, charges)
local b = _G["WatchFrameItem" .. n]
if not b then
b = CreateFrame("Button", "WatchFrameItem" .. n, lineFrame, "WatchFrameItemButtonTemplate")
end
-- button count: WotLK's WATCHFRAME_NUM_ITEMS is read only by its removed quest handler;
-- kept here so the addon never writes that global
if n > T.numItems then T.numItems = n end
if not b.foreverSkinApplied then
b.foreverSkinApplied = true
b:SetWidth(G.itemSide)
b:SetHeight(G.itemSide)
local e = ForeverUI.AtlasEntry("ui-questtrackerbutton-questitem-frame")
b:SetNormalTexture(e and e[1] or "")
local t = b:GetNormalTexture()
ForeverUI.SetAtlas(t, "ui-questtrackerbutton-questitem-frame", true)
t:ClearAllPoints()
t:SetWidth(G.itemFrame)
t:SetHeight(G.itemFrame)
t:SetPoint("CENTER", b, "CENTER", 0, 0)
b:SetPushedTexture(e and e[1] or "")
local p = b:GetPushedTexture()
ForeverUI.SetAtlas(p, "ui-questtrackerbutton-questitem-frame", true)
p:ClearAllPoints()
p:SetWidth(G.itemFrame)
p:SetHeight(G.itemFrame)
p:SetPoint("CENTER", b, "CENTER", 0, 0)
end
b:SetID(index)
SetItemButtonTexture(b, icon)
SetItemButtonCount(b, charges)
b.charges = charges
WatchFrameItem_UpdateCooldown(b)
b.rangeTimer = -1
return b
end
-- ---------- Achievements
local function showAchievements(lineFrame, initialOffset, maxHeight, frameWidth, ...)
local m = T.achievements
begin(m, remainingSpace(maxHeight, initialOffset))
local count = select("#", ...)
local arena = ArenaEnemyFrames and ArenaEnemyFrames:IsShown()
if bit.band(WATCHFRAME_FILTER_TYPE, WATCHFRAME_FILTER_ACHIEVEMENTS) == WATCHFRAME_FILTER_ACHIEVEMENTS then
for i = 1, count do
local id = select(i, ...)
local category = GetAchievementCategory(id)
local _, name, _, completed, _, _, _, description = GetAchievementInfo(id)
if not completed and not arena or category == WATCHFRAME_ACHIEVEMENT_ARENA_CATEGORY then
local b = acquireBlock(m, id)
b.achievement = id
setBlockTitle(b, name)
local numCriteria = GetAchievementNumCriteria(id)
if numCriteria > 0 then
local shownItems = 0
for j = 1, numCriteria do
local text, _, filled, _, _, _, flags, _, quantity, criteriaID = GetAchievementCriteriaInfo(id, j)
if filled or shownItems > G.maxCriteria then
-- skip: criterion done, or past the limit
elseif shownItems == G.maxCriteria and numCriteria > G.maxCriteria + 1 then
row(b, "Extra", "...", false)
shownItems = shownItems + 1
else
if bit.band(flags, ACHIEVEMENT_CRITERIA_PROGRESS_BAR) == ACHIEVEMENT_CRITERIA_PROGRESS_BAR then
text = quantity
end
row(b, j, text, true)
shownItems = shownItems + 1
local stopwatch = WATCHFRAME_TIMEDCRITERIA[criteriaID]
if stopwatch and GetTime() - stopwatch.startTime < stopwatch.duration then
timer(b, stopwatch.duration, stopwatch.startTime)
end
end
end
else
row(b, 1, description, true)
for _, stopwatch in pairs(WATCHFRAME_TIMEDCRITERIA) do
if stopwatch.achievementID == id and GetTime() - stopwatch.startTime <= stopwatch.duration then
timer(b, stopwatch.duration, stopwatch.startTime)
break
end
end
end
if not place(m, b) then
break
end
end
end
end
local height = finalize(m, lineFrame, initialOffset)
return height, G.width, count
end
local function achievementHandler(lineFrame, initialOffset, maxHeight, frameWidth)
return showAchievements(lineFrame, initialOffset, maxHeight, frameWidth, GetTrackedAchievements())
end
T.achievementHandler = achievementHandler
-- ---------- Clicks
-- QuestObjectiveTrackerMixin:OnBlockHeaderClick
local function questClick(b, mouse)
if IsModifiedClick("CHATLINK") and ChatEdit_GetActiveWindow and ChatEdit_GetActiveWindow() then
local link = GetQuestLink(b.index)
if link then ChatEdit_InsertLink(link) end
return
end
if mouse ~= "RightButton" then
CloseDropDownMenus()
if IsModifiedClick("QUESTWATCHTOGGLE") then
WatchFrame_StopTrackingQuest(nil, b.watch)
else
T.openPage(b.watch)
end
return
end
local w = b.watch
local index = b.index
local l = {
{ text = b.questTitle, isTitle = true },
{ text = TEXT.viewPage, func = function() T.openPage(w) end },
{ text = TEXT.viewMap, func = function() WatchFrame_OpenMapToQuest(nil, w) end },
{ text = TEXT.untrack, func = function() WatchFrame_StopTrackingQuest(nil, w) end },
}
if GetQuestLogPushable and (GetNumPartyMembers() > 0 or GetNumRaidMembers() > 1) then
local selection = GetQuestLogSelection()
SelectQuestLogEntry(index)
local shareable = GetQuestLogPushable()
SelectQuestLogEntry(selection)
if shareable then
table.insert(l, { text = SHARE_QUEST, func = function() WatchFrame_ShareQuest(nil, w) end })
end
end
-- camelot 70450 drops "Share in chat" outside the gamepad UI; Shift-click links the quest
table.insert(l, { text = TEXT.abandonQuest, func = function() WatchFrame_AbandonQuest(nil, w) end })
-- WotLK manual ordering
local n = #VISIBLE_WATCHES
local rank = WatchFrame_GetVisibleIndex(index)
if n > 1 and rank then
if rank > 1 then
table.insert(l, { text = TRACKER_SORT_MANUAL_UP, func = function() WatchFrame_MoveQuest(nil, index, -1) end })
table.insert(l, { text = TRACKER_SORT_MANUAL_TOP, func = function() WatchFrame_MoveQuest(nil, index, -100) end })
end
if rank < n then
table.insert(l, { text = TRACKER_SORT_MANUAL_DOWN, func = function() WatchFrame_MoveQuest(nil, index, 1) end })
table.insert(l, { text = TRACKER_SORT_MANUAL_BOTTOM, func = function() WatchFrame_MoveQuest(nil, index, 100) end })
end
end
menu("cursor", l)
end
-- AchievementObjectiveTrackerMixin:OnBlockHeaderClick
local function achievementClick(b, mouse)
local id = b.achievement
if IsModifiedClick("CHATLINK") and ChatEdit_GetActiveWindow and ChatEdit_GetActiveWindow() then
local link = GetAchievementLink(id)
if link then ChatEdit_InsertLink(link) end
return
end
if mouse ~= "RightButton" then
CloseDropDownMenus()
if IsModifiedClick("QUESTWATCHTOGGLE") then
WatchFrame_StopTrackingAchievement(nil, id)
else
WatchFrame_OpenAchievementFrame(nil, id)
end
return
end
local _, name = GetAchievementInfo(id)
menu("cursor", {
{ text = name, isTitle = true },
{ text = TEXT.viewAchievement, func = function() WatchFrame_OpenAchievementFrame(nil, id) end },
{ text = TEXT.untrack, func = function() WatchFrame_StopTrackingAchievement(nil, id) end },
})
end
-- QuestMapFrame_OpenToQuestDetails: map, side panel, quest page. WotLK first expands the
-- quest's header (WatchFrameLinkButtonTemplate_OnLeftClick).
function T.openPage(watch)
local index = GetQuestIndexForWatch(watch)
if not index then return end
ExpandQuestHeader(GetQuestSortIndex(index))
index = GetQuestIndexForWatch(watch)
if index and ForeverUI.QuestLog and ForeverUI.QuestLog.openPage then
ForeverUI.QuestLog.openPage(index)
end
end
-- ---------- Assembly
local function suppressWotLKHeader()
for _, f in ipairs({ WatchFrameHeader, WatchFrameCollapseExpandButton }) do
if f then
f:SetAlpha(0)
f:EnableMouse(false)
f:Hide()
end
end
end
-- After each WatchFrame_Update our header replaces WotLK's. WotLK decides whether to show it
-- only in WatchFrame_Update, so that decision is kept: its header is already hidden when
-- WatchFrame_Collapse / _Expand call us again. fromUpdate: called from WatchFrame_Update.
local function afterUpdate(fromUpdate)
local h = T.header
if not h then return end
if fromUpdate then
T.headerWanted = WatchFrameHeader:IsShown() and true or false
end
local showRegion = T.headerWanted
suppressWotLKHeader()
if showRegion then
h:Show()
local active = WatchFrameCollapseExpandButton:IsEnabled() == 1
if active then h.shrink:Enable() else h.shrink:Disable() end
else
h:Hide()
end
if WatchFrame.collapsed then
buttonStates(h.shrink, "ui-questtrackerbutton-expand-all", "ui-questtrackerbutton-expand-all-pressed")
else
buttonStates(h.shrink, "ui-questtrackerbutton-collapse-all", "ui-questtrackerbutton-collapse-all-pressed")
end
end
T.afterUpdate = afterUpdate
-- WatchFrame_SetWidth and WatchFrame_Collapse / _Expand: 260, collapsed or not.
-- WATCHFRAME_EXPANDEDWIDTH and WATCHFRAME_MAXLINEWIDTH keep the client's values: the width
-- is re-applied after it (hooks below), and our handlers ignore the width it passes.
local function width()
WatchFrame:SetWidth(G.width)
end
-- The tracker follows a CARRIER registered in edit mode. WotLK's
-- UIParent_ManageFramePositions re-anchors WatchFrame under MinimapCluster on every pass and
-- adds a BOTTOMRIGHT point, so WatchFrame is re-placed after it. Its height runs from the
-- carrier down to WotLK's bottom (CONTAINER_OFFSET_Y, above the action bars).
function T.place()
local carrier = T.carrier
if not carrier then return end
WatchFrame:ClearAllPoints()
WatchFrame:SetPoint("TOPLEFT", carrier, "TOPLEFT", 0, 0)
local top = carrier:GetTop()
if top then
WatchFrame:SetHeight(math.max(G.minHeight, top - (CONTAINER_OFFSET_Y or 0)))
end
end
local function buildCarrier()
local carrier = CreateFrame("Frame", "ForeverUIObjectiveTrackerHolder", UIParent)
carrier:SetWidth(G.width)
carrier:SetHeight(G.headerH)
T.carrier = carrier
local L = ForeverUI.Layout
if L and L.Register then
L.Register(carrier, "tracking", ForeverUI.L.OBJECTIVETRACKER_EDIT_LABEL, "TOPRIGHT", "TOPRIGHT", G.defaultX, G.defaultY)
-- moved, reset or applied at login: the tracker follows
local function after(id)
if id == "tracking" then T.place() end
end
hooksecurefunc(L, "Save", after)
hooksecurefunc(L, "Apply", after)
end
if UIParent_ManageFramePositions then
hooksecurefunc("UIParent_ManageFramePositions", T.place)
end
T.place()
end
local function build()
if T.header or not WatchFrame or not WatchFrameLines then
return
end
buildCarrier()
T.header = createHeader()
T.quests = createModule("ForeverUIQuestObjectiveTracker", TEXT.quests, "quests")
T.quests.onClick = questClick
T.achievements = createModule("ForeverUIAchievementObjectiveTracker", TEXT.achievements, "achievements")
T.achievements.onClick = achievementClick
WatchFrameLines:ClearAllPoints()
WatchFrameLines:SetPoint("TOPLEFT", WatchFrame, "TOPLEFT", 0, -G.modulesTop)
WatchFrameLines:SetPoint("BOTTOMRIGHT", WatchFrame, "BOTTOMRIGHT", 0, G.rowsBottom)
width()
-- camelot's two modules, in camelot's order, replace the three WotLK handlers;
-- addon handlers stay after them
WatchFrame_RemoveObjectiveHandler(WatchFrame_HandleDisplayQuestTimers)
WatchFrame_RemoveObjectiveHandler(WatchFrame_HandleDisplayTrackedAchievements)
WatchFrame_RemoveObjectiveHandler(WatchFrame_DisplayTrackedQuests)
table.insert(WATCHFRAME_OBJECTIVEHANDLERS, 1, displayQuests)
table.insert(WATCHFRAME_OBJECTIVEHANDLERS, 2, achievementHandler)
hooksecurefunc("WatchFrame_Update", function() afterUpdate(true) end)
hooksecurefunc("WatchFrame_SetWidth", function()
if not WatchFrame.collapsed then width() end
end)
-- Tracking a quest from the map: WotLK adds it to LOCAL_MAP_QUESTS when that table is the
-- shown zone's, and removes it otherwise (WorldMapFrame.lua:2162-2167). The table is ours,
-- so do the same, then redo the refresh the client already ran without it.
if WorldMapTrackQuest_Toggle then
hooksecurefunc("WorldMapTrackQuest_Toggle", function(checkMark)
local id = WORLDMAP_SETTINGS and WORLDMAP_SETTINGS.selectedQuestId
if not id then return end
if checkMark then
if locales["zone"] == GetCurrentMapZone() then locales[id] = true end
else
locales[id] = nil
end
WatchFrame_Update()
end)
end
hooksecurefunc("WatchFrame_Collapse", function(self)
self:SetWidth(G.width)
afterUpdate()
end)
hooksecurefunc("WatchFrame_Expand", function(self)
self:SetWidth(G.width)
afterUpdate()
end)
suppressWotLKHeader()
-- WatchFrame_Update measures the frame, so not before it is placed (it is not at addon
-- load; its login events refresh it)
if WatchFrame:GetTop() and WatchFrame:GetBottom() then
WatchFrame_Update()
end
end
T.build = build
build()
ForeverUI.ObjectiveTrackerDebug = function()
local prefix = "|cff66ccffForeverUI|r "
if not T.header then
DEFAULT_CHAT_FRAME:AddMessage(prefix .. L.OBJECTIVETRACKER_DEBUG_NOT_BUILT)
return
end
local n = 0
for _ in pairs(T.quests.blocks) do n = n + 1 end
local a = 0
for _ in pairs(T.achievements.blocks) do a = a + 1 end
DEFAULT_CHAT_FRAME:AddMessage(prefix .. string.format(
L.OBJECTIVETRACKER_DEBUG_STATE,
WatchFrame:GetWidth(), WatchFrame:GetHeight(), tostring(WatchFrame.collapsed), tostring(T.header:IsShown()),
n, tostring(T.quests:IsShown()), a, tostring(T.achievements:IsShown()), #WATCHFRAME_OBJECTIVEHANDLERS,
tostring(WATCHFRAME_SORT_TYPE), tostring(WATCHFRAME_FILTER_TYPE)))
end