diff --git a/Localization/Translations/Options/Tracker.lua b/Localization/Translations/Options/Tracker.lua index 2e4ee6f..ad39abf 100644 --- a/Localization/Translations/Options/Tracker.lua +++ b/Localization/Translations/Options/Tracker.lua @@ -102,6 +102,24 @@ local trackerOptionsLocales = { ["frFR"] = "Affiche le niveau des quêtes avec le titre des quêtes.", }, --------------------------------------------------------- + ["Show Objective Marker Button"] = { + ["enUS"] = true, + }, + ["When this is checked, quests that can be reached by the floating objective marker get a button in the Questie Tracker that points the marker at them."] = { + ["enUS"] = true, + }, + --------------------------------------------------------- + ["Objective Marker Button Size"] = { + ["enUS"] = true, + }, + ["The size of the objective marker button shown next to each quest in the Questie Tracker."] = { + ["enUS"] = true, + }, + --------------------------------------------------------- + ["Point the objective marker at this quest"] = { + ["enUS"] = true, + }, + --------------------------------------------------------- ["Auto Minimize Completed Quests"] = { ["ptBR"] = "Minimizar missões concluídas", ["ruRU"] = "Свернуть выполненные", diff --git a/Modules/Options/QuestieOptionsDefaults.lua b/Modules/Options/QuestieOptionsDefaults.lua index d7d06ae..6491404 100644 --- a/Modules/Options/QuestieOptionsDefaults.lua +++ b/Modules/Options/QuestieOptionsDefaults.lua @@ -113,6 +113,8 @@ function QuestieOptionsDefaults:Load() autoTrackQuests = true, trackerShowCompleteQuests = true, trackerShowQuestLevel = true, + trackerShowSuperTrackButton = true, + trackerSuperTrackButtonSize = 25, collapseCompletedQuests = false, hideCompletedQuestObjectives = false, hideBlizzardCompletionText = false, diff --git a/Modules/Options/TrackerTab/QuestieOptionsTracker.lua b/Modules/Options/TrackerTab/QuestieOptionsTracker.lua index 4c2dd36..3febc66 100644 --- a/Modules/Options/TrackerTab/QuestieOptionsTracker.lua +++ b/Modules/Options/TrackerTab/QuestieOptionsTracker.lua @@ -13,6 +13,8 @@ local TrackerBaseFrame = QuestieLoader:ImportModule("TrackerBaseFrame") local TrackerLinePool = QuestieLoader:ImportModule("TrackerLinePool") ---@type TrackerQuestTimers local TrackerQuestTimers = QuestieLoader:ImportModule("TrackerQuestTimers") +---@type TrackerUtils +local TrackerUtils = QuestieLoader:ImportModule("TrackerUtils") ---@type QuestieArrow local QuestieArrow = QuestieLoader:ImportModule("QuestieArrow") @@ -175,6 +177,37 @@ function QuestieOptions.tabs.tracker:Initialize() QuestieTracker:Update() end }, + showSuperTrackButton = { + type = "toggle", + order = 5, + width = 1.5, + name = function() return l10n('Show Objective Marker Button') end, + desc = function() return l10n('When this is checked, quests that can be reached by the floating objective marker get a button in the Questie Tracker that points the marker at them.') end, + hidden = function() return not TrackerUtils:IsSuperTrackAvailable() end, + disabled = function() return not Questie.db.profile.trackerEnabled end, + get = function() return Questie.db.profile.trackerShowSuperTrackButton end, + set = function(_, value) + Questie.db.profile.trackerShowSuperTrackButton = value + QuestieTracker:Update() + end + }, + superTrackButtonSize = { + type = "range", + order = 6, + width = 1.5, + name = function() return l10n('Objective Marker Button Size') end, + desc = function() return l10n('The size of the objective marker button shown next to each quest in the Questie Tracker.') end, + hidden = function() return not TrackerUtils:IsSuperTrackAvailable() end, + disabled = function() return (not Questie.db.profile.trackerEnabled) or (not Questie.db.profile.trackerShowSuperTrackButton) end, + min = 8, + max = 32, + step = 1, + get = function() return Questie.db.profile.trackerSuperTrackButtonSize end, + set = function(_, value) + Questie.db.profile.trackerSuperTrackButtonSize = value + QuestieTracker:Update() + end + }, showQuestTimer = { type = "toggle", order = 3, diff --git a/Modules/Tracker/QuestieTracker.lua b/Modules/Tracker/QuestieTracker.lua index 318feb1..16885e9 100644 --- a/Modules/Tracker/QuestieTracker.lua +++ b/Modules/Tracker/QuestieTracker.lua @@ -335,6 +335,10 @@ function QuestieTracker.Initialize() Questie.db.profile.trackerSetpoint = "TOPLEFT" end + -- Tracks what the client considers supertracked. Permanent by design: hooksecurefunc cannot be + -- undone, and the value has to stay correct even while the Questie tracker is disabled. + TrackerUtils:InitSuperTrackHook() + if (not Questie.db.profile.trackerEnabled) then -- The Tracker is disabled, no need to continue return @@ -1093,6 +1097,10 @@ function QuestieTracker:Update() -- Adds the AI_VoiceOver Play Buttons line.playButton:SetPlayButton(questId) + -- Adds the button that points the floating objective marker at this quest. + -- Must run after SetPlayButton, since it anchors around the play button. + line.superTrackButton:SetSuperTrackButton(questId) + local usableQIB = false local sourceItemId = QuestieDB.QueryQuestSingle(quest.Id, "sourceItemId") local isLiveSourceItem = false diff --git a/Modules/Tracker/TrackerLinePool.lua b/Modules/Tracker/TrackerLinePool.lua index 15ad8cf..080dec8 100644 --- a/Modules/Tracker/TrackerLinePool.lua +++ b/Modules/Tracker/TrackerLinePool.lua @@ -35,6 +35,33 @@ local l10n = QuestieLoader:ImportModule("l10n") local C_Timer = QuestieCompat.C_Timer local C_QuestLog = QuestieCompat.C_QuestLog local GetQuestLogIndexByID = QuestieCompat.GetQuestLogIndexByID +-- Copies one texture region of a quest pin onto our button. The texture file travels with the +-- coordinates because the client swaps files between states, and the region is only shown when the +-- pin itself shows it -- the pin uses "number" for in-progress quests and "turnin" for completed +-- ones, never both. +local function MirrorPinRegion(destination, source, size, pinSize) + if (not source) or (not source:IsShown()) or (not source:GetTexture()) then + destination:Hide() + return + end + + -- Regions are not all the size of the pin -- the "?" is drawn larger than a digit -- so scale + -- them by how the pin itself was scaled instead of stretching each one to the button. + local scale = (pinSize and pinSize > 0) and (size / pinSize) or 1 + local width = (source:GetWidth() or 0) * scale + local height = (source:GetHeight() or 0) * scale + if width <= 0 or height <= 0 then + width = size + height = size + end + + destination:SetWidth(width) + destination:SetHeight(height) + destination:SetTexture(source:GetTexture()) + destination:SetTexCoord(source:GetTexCoord()) + destination:Show() +end + local function GetNumLines(label) if label.GetNumLines then return label:GetNumLines() @@ -404,6 +431,122 @@ function TrackerLinePool.Initialize(questFrame) line.playButton = playButton + -- create supertrack buttons for the Ascension floating objective marker + local superTrackButton = CreateFrame("Button", "linePool.superTrackButton" .. i, line) + superTrackButton:SetWidth(25) + superTrackButton:SetHeight(25) + superTrackButton:SetHitRectInsets(1, 1, 1, 1) + superTrackButton:SetHighlightTexture("Interface\\BUTTONS\\UI-Panel-MinimizeButton-Highlight") + + -- Same texture and atlas sub-rect the client's own quest POI pins use, so the tracker button + -- reads as native rather than as an addon icon. + superTrackButton:SetNormalTexture("Interface\\WorldMap\\UI-QuestPoi-NumberIcons") + superTrackButton:GetNormalTexture():SetTexCoord(0.5, 0.625, 0.875, 1) + + -- The client marks the selected pin with this glow rather than by swapping the icon, so the + -- tracker button highlights the same way the map pin does. + superTrackButton.glow = superTrackButton:CreateTexture(nil, "BACKGROUND") + superTrackButton.glow:SetTexture("Interface\\WorldMap\\UI-QuestPoi-IconGlow") + superTrackButton.glow:SetBlendMode("ADD") + superTrackButton.glow:SetPoint("CENTER", superTrackButton, "CENTER", 0, 0) + superTrackButton.glow:Hide() + + -- The digit printed inside the pin is another cell of the same atlas, drawn over the icon. + superTrackButton.number = superTrackButton:CreateTexture(nil, "OVERLAY") + superTrackButton.number:SetPoint("CENTER", superTrackButton, "CENTER", 0, 0) + superTrackButton.number:Hide() + + -- Completed quests draw a "?" from this separate region instead of a digit. + superTrackButton.turnin = superTrackButton:CreateTexture(nil, "OVERLAY") + superTrackButton.turnin:SetPoint("CENTER", superTrackButton, "CENTER", 0, 0) + superTrackButton.turnin:Hide() + + -- The quest id is remembered even while the button is hidden. POI frames only exist once the + -- world map has built them, so a quest that looks unreachable while the tracker is drawing + -- can become reachable later -- without the id we would have nothing left to re-check. + superTrackButton.SetSuperTrackButton = function(self, questId) + self.questId = questId + self:RefreshSuperTrackButton() + end + + superTrackButton.RefreshSuperTrackButton = function(self) + -- No map pin means the client has nothing to point the marker at (quest in another zone, + -- or a quest without map coordinates), so there is nothing to offer. + local pin = self.questId and TrackerUtils:GetSuperTrackPin(self.questId) + if (not pin) or (not Questie.db.profile.trackerShowSuperTrackButton) then + self:Hide() + return + end + + local buttonSize = Questie.db.profile.trackerSuperTrackButtonSize or 25 + self:SetWidth(buttonSize) + self:SetHeight(buttonSize) + self.glow:SetWidth(buttonSize * 1.4) + self.glow:SetHeight(buttonSize * 1.4) + + -- Mirror the pin rather than picking atlas cells ourselves, so whatever the client + -- decides to draw -- digit, "?", selected variant -- shows up here unchanged. The + -- texture file has to be copied along with the coordinates: completed quests swap in a + -- different file for these slots, and coordinates from one file applied to another + -- sample nonsense. + local pinTexture = pin.GetNormalTexture and pin:GetNormalTexture() + if pinTexture then + local ownTexture = self:GetNormalTexture() + ownTexture:SetTexture(pinTexture:GetTexture()) + ownTexture:SetTexCoord(pinTexture:GetTexCoord()) + end + + local pinSize = pin:GetWidth() + MirrorPinRegion(self.number, pin.number, buttonSize, pinSize) + MirrorPinRegion(self.turnin, pin.turnin, buttonSize, pinSize) + + -- Sit to the left of whatever already occupies the gutter: the AI_VoiceOver play button + -- when that addon is loaded, otherwise the quest collapse button. + self:ClearAllPoints() + if playButton:IsShown() then + self:SetPoint("RIGHT", playButton, "LEFT", -2, 0) + elseif line.expandQuest then + self:SetPoint("RIGHT", line.expandQuest, "LEFT", -2, 0) + else + self:SetPoint("RIGHT", line.label, "LEFT", -4, 0) + end + + -- The icon itself already shows the selected variant, copied from the pin above; the + -- glow is the one part the pin draws as a separate texture. + if TrackerUtils:GetSuperTrackedQuestId() == self.questId then + self.glow:Show() + else + self.glow:Hide() + end + + -- Has to sit above the tracker backdrop, which is what swallows a frame left at level 0. + self:SetFrameLevel(line:GetFrameLevel() + 10) + self:Show() + end + + superTrackButton:EnableMouse(true) + superTrackButton:RegisterForClicks("LeftButtonUp") + + superTrackButton:SetScript("OnClick", function(self) + if self.questId then + TrackerUtils:SetSuperTrackedQuest(self.questId) + end + end) + + superTrackButton:SetScript("OnEnter", function(self) + GameTooltip:SetOwner(self, "ANCHOR_RIGHT") + GameTooltip:SetText(l10n("Point the objective marker at this quest"), 1, 1, 1) + GameTooltip:Show() + end) + + superTrackButton:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + + superTrackButton:Hide() + + line.superTrackButton = superTrackButton + -- create expanding buttons for quests with objectives local expandQuest = CreateFrame("Button", "linePool.expandQuest" .. i, line) expandQuest.texture = expandQuest:CreateTexture(nil, "OVERLAY", nil, 0) @@ -731,11 +874,29 @@ function TrackerLinePool.ResetLinesForChange() line.playButton:SetAlpha(0) line.playButton:Hide() end + if line.superTrackButton then + line.superTrackButton.questId = nil + line.superTrackButton:Hide() + end end lineIndex = 0 end +-- Re-evaluates the supertrack buttons without rebuilding the tracker. Driven by the +-- SetSuperTrackedQuestID hook, which also fires on map open/close -- that is what makes buttons +-- appear once the world map has built its POI frames, since the tracker itself does not redraw then. +function TrackerLinePool.UpdateSuperTrackButtons() + -- Rebuild the map's POI frames first so a zone change is picked up even with the map closed. + TrackerUtils:PrimeSuperTrackFrames() + + for _, line in pairs(linePool) do + if line.superTrackButton and line.superTrackButton.questId then + line.superTrackButton:RefreshSuperTrackButton() + end + end +end + function TrackerLinePool.ResetButtonsForChange() if TrackerBaseFrame.isSizing == true or TrackerBaseFrame.isMoving == true then Questie:Debug(Questie.DEBUG_SPAM, "[TrackerLinePool:ResetButtonsForChange]") @@ -1098,6 +1259,12 @@ end TrackerLinePool.SetMode = function(self, mode) if mode ~= self.mode then self.mode = mode + -- Lines are recycled between zone headers, quest titles and objectives. Only quest title + -- lines own a supertrack button, so drop it whenever a line takes on another role. + if mode ~= "quest" and self.superTrackButton then + self.superTrackButton.questId = nil + self.superTrackButton:Hide() + end if mode == "zone" then local trackerFontSizeZone = Questie.db.profile.trackerFontSizeZone self.label:SetFont((LSM30 and LSM30.Fetch and LSM30:Fetch("font", Questie.db.profile.trackerFontZone)) or Questie.db.profile.trackerFontZone, trackerFontSizeZone, Questie.db.profile.trackerFontOutline) diff --git a/Modules/Tracker/TrackerUtils.lua b/Modules/Tracker/TrackerUtils.lua index c4dd9c0..866f9cd 100644 --- a/Modules/Tracker/TrackerUtils.lua +++ b/Modules/Tracker/TrackerUtils.lua @@ -1265,6 +1265,158 @@ function TrackerUtils:GetSortedQuestIds() return sortedQuestIds, questDetails end +-- Ascension's 3.3.5 client backports the retail floating objective marker ("SuperTracker"). +-- Supertracking is a slave of the quest log selection: the world map and the Blizzard watch frame +-- both funnel through SelectQuestLogEntry -> SuperTrackerUtil.SetToBestSuperTrackingType. Calling +-- C_SuperTrack.SetSuperTrackedQuestID directly only moves the marker until the next map interaction +-- stomps it, so we click the same POI frames the client itself clicks. +local superTrackedQuestId +local superTrackHooked + +function TrackerUtils:IsSuperTrackAvailable() + return (C_SuperTrack ~= nil) and ((WorldMapFrame_SelectQuestFrame ~= nil) or (WatchFrameQuestPOI_OnClick ~= nil)) +end + +-- Every path that changes the supertracked quest ends up in SetSuperTrackedQuestID -- our own +-- button, world map pins, the map quest list, the Blizzard tracker, and the automatic re-pick that +-- happens when the map switches zone. Hooking it is the only way to know what is supertracked, +-- since this client dropped the GetSuperTrackedQuestID getter. Caching what we last set would go +-- stale the moment the player changed it by any other means. +function TrackerUtils:InitSuperTrackHook() + if superTrackHooked or (not C_SuperTrack) then + return + end + + superTrackHooked = true + + hooksecurefunc(C_SuperTrack, "SetSuperTrackedQuestID", function(questId) + superTrackedQuestId = questId + TrackerLinePool.UpdateSuperTrackButtons() + end) + + if C_SuperTrack.ClearSuperTracker then + hooksecurefunc(C_SuperTrack, "ClearSuperTracker", function() + superTrackedQuestId = nil + TrackerLinePool.UpdateSuperTrackButtons() + end) + end +end + +function TrackerUtils:GetSuperTrackedQuestId() + return superTrackedQuestId +end + +-- The world map builds its quest POI frames lazily, so right after login -- or after a zone change +-- with the map still closed -- there is nothing to match a quest against and every button would +-- hide itself. The client can build them without the map being shown, and doing so does not disturb +-- which quest is currently supertracked. Skipped while the map is open so we never fight the player. +function TrackerUtils:PrimeSuperTrackFrames() + if WorldMapFrame and WorldMapFrame:IsShown() then + return + end + + if WorldMapFrame_UpdateQuests then + WorldMapFrame_UpdateQuests() + end +end + +---@return table|nil frame, function|nil clickHandler +local function GetSuperTrackFrame(questId) + if (not questId) or questId == 0 then + return nil + end + + -- Blizzard watch frame POI buttons carry the quest id directly. Questie empties the Blizzard + -- watch list (see QuestieTracker:AQW_Insert) so only a handful of quests ever have one. Both + -- scans below stop at the first gap: these frames are created in order, so a missing index + -- means there are no further ones and this runs once per quest line per redraw. + if WatchFrameQuestPOI_OnClick then + for i = 1, 30 do + local firstInRow = _G["poiWatchFrameLines" .. i .. "_1"] + if not firstInRow then + break + end + for j = 1, 5 do + local poiButton = (j == 1) and firstInRow or _G["poiWatchFrameLines" .. i .. "_" .. j] + if not poiButton then + break + end + if poiButton.questId == questId then + return poiButton, WatchFrameQuestPOI_OnClick + end + end + end + end + + -- World map quest frames cover every quest with a POI on the currently viewed map, which is the + -- wider net of the two. Pass the WorldMapQuestFrame itself and never its poiIcon -- + -- WorldMapFrame_SelectQuestFrame indexes questFrame.poiIcon and errors on the POI frame. + if WorldMapFrame_SelectQuestFrame then + if not _G["WorldMapQuestFrame1"] then + TrackerUtils:PrimeSuperTrackFrames() + end + + for i = 1, 25 do + local questFrame = _G["WorldMapQuestFrame" .. i] + if not questFrame then + break + end + if questFrame.questId == questId then + return questFrame, WorldMapFrame_SelectQuestFrame + end + end + end + + return nil +end + +-- A quest can only be supertracked while it has a POI frame, so quests in another zone or without +-- map coordinates simply have no button. +function TrackerUtils:CanSuperTrackQuest(questId) + return GetSuperTrackFrame(questId) ~= nil +end + +-- The map pin the client draws for this quest. Copying its texture coordinates is what keeps the +-- tracker button identical to the pin: the number, the "?" shown for completed quests and the +-- black-on-yellow selected variant all follow along without us mapping atlas cells by hand. The +-- number is also the pin's position among quests that actually have a POI, which is not the same as +-- the quest frame index -- another reason to read it from the client instead of deriving it. +---@return table|nil +function TrackerUtils:GetSuperTrackPin(questId) + if (not questId) or questId == 0 or (not WorldMapFrame_SelectQuestFrame) then + return nil + end + + if not _G["WorldMapQuestFrame1"] then + TrackerUtils:PrimeSuperTrackFrames() + end + + for i = 1, 25 do + local questFrame = _G["WorldMapQuestFrame" .. i] + if not questFrame then + break + end + if questFrame.questId == questId then + -- ownPOI is the pin drawn in the map's quest list, poiIcon the one on the map itself. + -- The list version is the one we mirror: it keeps the circular background on completed + -- quests, where the map version draws a bare "?". + return questFrame.ownPOI or questFrame.poiIcon + end + end + + return nil +end + +function TrackerUtils:SetSuperTrackedQuest(questId) + local frame, clickHandler = GetSuperTrackFrame(questId) + if not frame then + return false + end + + clickHandler(frame) + return true +end + function TrackerUtils:IsVoiceOverLoaded() -- Require not just that the VoiceOver addons are loaded, but that the runtime -- structure we index actually exists. Some VoiceOver builds (e.g. on Elune) expose