Files
Questie-X/Modules/FramePool/QuestieFrame.lua
T
Xurkon dc155b6583 fix(map): completed quest pins stuck on world map until reload (#9)
Draw-queue race: ProcessQueue processes the world-map and minimap queues
independently. When a frame was unloaded while still queued, its
minimap-queue entry could trigger the deferred Unload first (removing
both icons and clearing _needsUnload), then a later map-queue entry
re-added the world-map icon -- orphaned, since Unload had nil'd the
questIdFrames/_G reference, so it lingered until /reload while the
minimap cleared. Unload now drops the frame's pending draw-queue entries
(new QuestieMap:DequeueFrameDrawCalls) so an unloaded frame can never be
re-added to a map.
2026-06-11 07:13:01 -05:00

567 lines
22 KiB
Lua

---@type QuestieFramePool
local QuestieFramePool = QuestieLoader:ImportModule("QuestieFramePool")
---@type QuestieMap
local QuestieMap = QuestieLoader:ImportModule("QuestieMap")
---@type QuestieDBMIntegration
local QuestieDBMIntegration = QuestieLoader:ImportModule("QuestieDBMIntegration")
---@type QuestieDB
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
---@type DailyQuests
local DailyQuests = QuestieLoader:ImportModule("DailyQuests")
---@type QuestieLink
local QuestieLink = QuestieLoader:ImportModule("QuestieLink")
---@type QuestieQuest
local QuestieQuest = QuestieLoader:ImportModule("QuestieQuest")
--- COMPATIBILITY ---
local C_Map = QuestieCompat.C_Map
local WorldMapFrame = QuestieCompat.WorldMapFrame
local HBDPins = QuestieCompat.HBDPins or LibStub("HereBeDragonsQuestie-Pins-2.0")
QuestieFramePool.Qframe = {}
local _Qframe = {}
---@return IconFrame
function QuestieFramePool.Qframe:New(frameId, OnEnter)
---@class IconFrame : Button
local newFrame = CreateFrame("Button", "QuestieFrame" .. frameId)
newFrame.frameId = frameId;
-- Add the frames to the ignore list of the Minimap Button Bag (MBB) addon
-- This is quite ugly but the only thing we can do currently from our side
-- Check #1504
if MBB_Ignore then
tinsert(MBB_Ignore, newFrame:GetName())
end
newFrame.isSkinned = true -- prevents ElvUI_Enhanced_MinimapButtonGrabber from hidding our pins
if frameId > 5000 then
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieFramePool] Over 5000 frames... maybe there is a leak?", frameId)
end
-- Glow is a Texture (not a Frame) so it can stack correctly inside ScrollFrames
local glow = newFrame:CreateTexture(nil, "BACKGROUND")
glow:SetTexture(Questie.icons["glow"])
glow:SetBlendMode("BLEND")
glow:SetDrawLayer("BACKGROUND", 0)
glow:SetWidth(18)
glow:SetHeight(18)
glow:SetPoint("CENTER", newFrame, "CENTER", 0, 0)
glow:Hide()
newFrame.glow = glow
newFrame:SetFrameStrata("FULLSCREEN");
newFrame:SetWidth(16) -- Set these to whatever height/width is needed
newFrame:SetHeight(16) -- for your Texture
newFrame:SetPoint("CENTER", -8, -8)
newFrame:EnableMouse(true)
local newTexture = newFrame:CreateTexture(nil, "OVERLAY", nil, 0)
--t:SetTexture("Interface\\Icons\\INV_Misc_Eye_02.blp")
--t:SetTexture("Interface\\Addons\\!Questie\\Icons\\available.blp")
newTexture:SetWidth(16)
newTexture:SetHeight(16)
newTexture:SetAllPoints(newFrame)
-- Make sure the icon itself always draws above the glow
newTexture:SetDrawLayer("OVERLAY", 1)
if not QuestieCompat.Is335 then
newTexture:SetTexelSnappingBias(0)
newTexture:SetSnapToPixelGrid(false)
end
local glowt = newFrame.glow
---@class IconTexture : Texture
newFrame.texture = newTexture;
newFrame.texture.OLDSetVertexColor = newFrame.texture.SetVertexColor;
function newFrame.texture:SetVertexColor(r, g, b, a)
self:OLDSetVertexColor(r, g, b, a);
--We save the colors to the texture object, this way we don't need to use GetVertexColor
self.r = r or 1;
self.g = g or 1;
self.b = b or 1;
self.a = a or 1;
end
--We save the colors to the texture object, this way we don't need to use GetVertexColor
newFrame.texture:SetVertexColor(1, 1, 1, 1);
newFrame.glowTexture = glowt
newFrame.glowTexture.OLDSetVertexColor = newFrame.glowTexture.SetVertexColor;
function newFrame.glowTexture:SetVertexColor(r, g, b, a)
self:OLDSetVertexColor(r, g, b, a);
--We save the colors to the texture object, this way we don't need to use GetVertexColor
self.r = r or 1;
self.g = g or 1;
self.b = b or 1;
self.a = a or 1;
end
--We save the colors to the texture object, this way we don't need to use GetVertexColor
newFrame.glowTexture:SetVertexColor(1, 1, 1, 1);
newFrame.glowTexture:SetTexture(Questie.icons["glow"])
newFrame.glow:Hide()
newFrame:SetScript("OnEnter", OnEnter); --Script Toolip
newFrame:SetScript("OnLeave", _Qframe.OnLeave) --Script Exit Tooltip
newFrame:RegisterForClicks("RightButtonUp", "LeftButtonUp")
newFrame:SetScript("OnClick", _Qframe.OnClick);
newFrame.GlowUpdate = _Qframe.GlowUpdate
newFrame.BaseOnUpdate = _Qframe.BaseOnUpdate
newFrame.BaseOnShow = _Qframe.BaseOnShow
newFrame.BaseOnHide = _Qframe.BaseOnHide
newFrame.UpdateTexture = _Qframe.UpdateTexture
newFrame.Unload = _Qframe.Unload
-- functions for fake hide/unhide
newFrame.FadeOut = _Qframe.FadeOut
newFrame.FadeIn = _Qframe.FadeIn
newFrame.FakeHide = _Qframe.FakeHide
newFrame.FakeShow = _Qframe.FakeShow
newFrame.OnShow = _Qframe.OnShow
newFrame.OnHide = _Qframe.OnHide
newFrame.ShouldBeHidden = _Qframe.ShouldBeHidden
newFrame.data = nil
newFrame:Hide()
return newFrame
end
function _Qframe:OnLeave()
if WorldMapTooltip then
WorldMapTooltip:Hide()
WorldMapTooltip._rebuild = nil
end
if GameTooltip then
GameTooltip:Hide()
GameTooltip._Rebuild = nil
end
--Reset highlighting if it exists.
if self.data.lineFrames then
for _, lineFrame in pairs(self.data.lineFrames) do
local line = lineFrame.line
line:SetColorTexture(line.dR, line.dG, line.dB, line.dA)
end
end
if self.data.touchedPins then
for i = #self.data.touchedPins, 1, -1 do
local entry = self.data.touchedPins[i]
local icon = entry.icon;
icon.texture:SetVertexColor(unpack(entry.color));
end
self.data.touchedPins = nil;
end
GameTooltip.ShownAsMapIcon = false
if QuestieCompat.Is335 then QuestieCompat.SetupTooltip(self, true) end
end
function _Qframe:OnClick(button)
if self and self.UiMapID and WorldMapFrame and WorldMapFrame:IsShown() and not IsModifierKeyDown() and not self.miniMapIcon then
if button == "RightButton" then
local currentMapParent = WorldMapFrame:GetMapID()
if currentMapParent then
local mapInfo = C_Map.GetMapInfo(currentMapParent)
currentMapParent = mapInfo.parentMapID
if currentMapParent and currentMapParent > 0 then
WorldMapFrame:SetMapID(currentMapParent)
end
end
else
if self.UiMapID ~= WorldMapFrame:GetMapID() then
WorldMapFrame:SetMapID(self.UiMapID);
end
end
else
-- This will work in either the WorldMapFrame or the MiniMapFrame as long as there is an icon
if self and self.UiMapID and button == "LeftButton" then
if (not ChatEdit_GetActiveWindow()) then
if self.data.Type == "available" and IsShiftKeyDown() then
StaticPopupDialogs["QUESTIE_CONFIRMHIDE"]:SetQuest(self.data.Id)
StaticPopup_Show("QUESTIE_CONFIRMHIDE")
elseif self.data.Type == "manual" and IsShiftKeyDown() and not self.data.ManualTooltipData.disableShiftToRemove then
QuestieMap:UnloadManualFrames(self.data.id)
end
else
ChatEdit_InsertLink(QuestieLink:GetQuestLinkString(self.data.QuestData.level, self.data.QuestData.name, self.data.Id))
end
end
end
-- TomTom integration
if self and self.UiMapID and IsControlKeyDown() and TomTom and TomTom.AddWaypoint then
local m = self.UiMapID
local x = self.x / 100
local y = self.y / 100
local title = self.data.Name
local add = true
-- Remove old waypoint if set
if Questie.db.char._tom_waypoint and TomTom.RemoveWaypoint then
local waypoint = Questie.db.char._tom_waypoint
TomTom:RemoveWaypoint(waypoint)
add = QuestieCompat.Is335 or (waypoint[1] ~= m or waypoint[2] ~= x or waypoint[3] ~= y or waypoint.title ~= title or waypoint.from ~= "Questie")
end
-- Add waypoint
if QuestieCompat.Is335 then
Questie.db.char._tom_waypoint = QuestieCompat.TomTom_AddWaypoint(title, m, self.x, self.y)
else
Questie.db.char._tom_waypoint = add and TomTom:AddWaypoint(m, x, y, { title = title, crazy = true, from = "Questie" })
end
end
-- Make sure we don't break the map ping feature - this allows us to ping our own icons.
if self.miniMapIcon and button == "RightButton" and not IsModifierKeyDown() then
local _, _, _, x, y = self:GetPoint()
Minimap:PingLocation(x, y)
end
end
function _Qframe:GlowUpdate()
if self.glow and self.glow.IsShown and self.glow:IsShown() then
--Due to this always being 1:1 we can assume that if one isn't correct, the other isn't either
--We can also assume that both change at the same time so we only check one.
if (self.glow:GetWidth() ~= self:GetWidth() * 1.13) then ---self.glow:GetHeight() ~= self:GetHeight() * 1.13
self.glow:SetWidth(self:GetWidth() * 1.13)
self.glow:SetHeight(self:GetHeight() * 1.13)
self.glow:SetPoint("CENTER", self, 0, 0)
end
if self.data and self.data.ObjectiveData and self.data.ObjectiveData.Color and self.glowTexture then
--Due to us now saving the alpha inside of the texture we don't need to check the main texture anymore.
--The question is is it faster to get and compare or just set straight up?
if (self.glowTexture.r ~= self.data.ObjectiveData.Color[1] or self.glowTexture.g ~= self.data.ObjectiveData.Color[2] or self.glowTexture.b ~= self.data.ObjectiveData.Color[3] or self.texture.a ~= self.glowTexture.a) then
self.glowTexture:SetVertexColor(self.data.ObjectiveData.Color[1], self.data.ObjectiveData.Color[2], self.data.ObjectiveData.Color[3], self.texture.a or 1)
end
end
end
end
function _Qframe:BaseOnShow()
local data = self.data
if data and data.Type and data.Type == "complete" then
self:SetFrameLevel(self:GetFrameLevel() + 1)
end
if ((self.miniMapIcon and Questie.db.profile.alwaysGlowMinimap) or ((not self.miniMapIcon) and Questie.db.profile.alwaysGlowMap)) and
data and data.ObjectiveData and
data.ObjectiveData.Color and
(data.Type and (data.Type ~= "available" and data.Type ~= "complete")
) then
self.glow:SetWidth(self:GetWidth() * 1.13)
self.glow:SetHeight(self:GetHeight() * 1.13)
self.glow:SetPoint("CENTER", self, 0, 0)
local _, _, _, alpha = self.texture:GetVertexColor()
self.glowTexture:SetVertexColor(data.ObjectiveData.Color[1], data.ObjectiveData.Color[2], data.ObjectiveData.Color[3], alpha or 1)
self.glow:Show()
end
end
function _Qframe:BaseOnHide()
self.glow:Hide()
end
function _Qframe:UpdateTexture(texture)
--Different settings depending on noteType
local globalScale
local objectiveColor
local alpha
if (self.miniMapIcon) then
globalScale = Questie.db.profile.globalMiniMapScale;
objectiveColor = Questie.db.profile.questMinimapObjectiveColors;
alpha = 0;
else
globalScale = Questie.db.profile.globalScale;
objectiveColor = Questie.db.profile.questObjectiveColors;
alpha = 1;
end
self.texture:SetTexture(texture)
--self.data.Icon = texture;
local colors = { 1, 1, 1 }
if self.data.IconColor ~= nil and objectiveColor then
colors = self.data.IconColor
end
self.texture:SetVertexColor(colors[1], colors[2], colors[3], alpha);
if self.data.IconScale then
local scale = 16 * ((self.data:GetIconScale() or 1) * (globalScale or 0.7));
self:SetWidth(scale)
self:SetHeight(scale)
else
self:SetWidth(16)
self:SetHeight(16)
end
end
function _Qframe:Unload()
if not self._loaded then
self._needsUnload = true
-- Drop any pending draw-queue entries so this frame can't be re-added to a map
-- after it was unloaded while still queued (see DequeueFrameDrawCalls). Without
-- this a completed quest's world-map pin could linger until /reload. (#9)
QuestieMap:DequeueFrameDrawCalls(self)
return -- icon is still in the draw queue
end
-- Even when already loaded, the frame may have been re-queued for a redraw; clear those
-- pending draw calls so they don't re-add the icon after this unload. (#9)
QuestieMap:DequeueFrameDrawCalls(self)
self._needsUnload = nil
self._loaded = nil
--Questie:Debug(Questie.DEBUG_SPAM, "[_Qframe:Unload]")
self:SetScript("OnUpdate", nil)
self:SetScript("OnShow", nil)
self:SetScript("OnHide", nil)
self:SetFrameStrata("FULLSCREEN");
self:SetFrameLevel(0);
-- Reset questIdFrames so they won't be toggled again
local frameName = self:GetName()
if frameName and self.data.Id and QuestieMap.questIdFrames[self.data.Id] and QuestieMap.questIdFrames[self.data.Id][frameName] then
QuestieMap.questIdFrames[self.data.Id][frameName] = nil
end
--We are reseting the frames, making sure that no data is wrong.
if self ~= nil and self.hidden and self._show ~= nil and self._hide ~= nil then -- restore state to normal (toggle questie)
self.hidden = false
self.Show = self._show;
self.Hide = self._hide;
self._show = nil
self._hide = nil
end
self.shouldBeShowing = nil
self.faded = nil
HBDPins:RemoveMinimapIcon(Questie, self)
HBDPins:RemoveWorldMapIcon(Questie, self)
QuestieDBMIntegration:UnregisterHudQuestIcon(tostring(self))
if (self.texture) then
self.texture:SetVertexColor(1, 1, 1, 1)
end
self.miniMapIcon = nil;
self:SetScript("OnUpdate", nil)
if self.fadeLogicTimer then
self.fadeLogicTimer:Cancel();
end
if self.glowLogicTimer then
self.glowLogicTimer:Cancel();
end
--Unload potential waypoint frames that are used for pathing.
if self.data and self.data.lineFrames then
for _, lineFrame in pairs(self.data.lineFrames) do
lineFrame:Unload();
end
end
if self.OnHide then self:OnHide() end -- the event might trigger after OnHide=nil even if its set after self:Hide()
self:Hide()
self.glow:Hide()
self.data = nil -- Just to be safe
self.x = nil
self.y = nil
self.AreaID = nil
self.UiMapID = nil
self.lastGlowFade = nil
self.worldX = nil
self.worldY = nil
QuestieFramePool:RecycleFrame(self)
end
function _Qframe:FadeOut()
if not self.faded then
self.faded = true
if self.texture then
local r, g, b = self.texture:GetVertexColor()
self.texture:SetVertexColor(r, g, b, Questie.db.profile.iconFadeLevel)
end
if self.glowTexture then
local r, g, b = self.glowTexture:GetVertexColor()
self.glowTexture:SetVertexColor(r, g, b, Questie.db.profile.iconFadeLevel)
end
if self.data.lineFrames then
for _, lineFrame in pairs(self.data.lineFrames) do
local line = lineFrame.line
if line then
line:SetColorTexture(line.dR, line.dG, line.dB, Questie.db.global.iconFadeLevel)
end
end
end
end
end
function _Qframe:FadeIn()
if self.faded then
self.faded = nil
if self.texture then
local r, g, b = self.texture:GetVertexColor()
self.texture:SetVertexColor(r, g, b, 1)
end
if self.glowTexture then
local r, g, b = self.glowTexture:GetVertexColor()
self.glowTexture:SetVertexColor(r, g, b, 1)
end
if self.data.lineFrames then
for _, lineFrame in pairs(self.data.lineFrames) do
local line = lineFrame.line
if line then
line:SetColorTexture(line.dR, line.dG, line.dB, line.dA)
end
end
end
end
end
--- This is needed because HBD will show the icons again after switching zones and stuff like that
function _Qframe:FakeHide()
if not self.hidden then
if _G.QuestieDebugPins and self.miniMapIcon then
local parent = self:GetParent()
local point1, rel1, point2, xOff, yOff = self:GetPoint()
print(string.format(
"[QD] FakeHide name=%s shown=%s hidden=%s should=%s parent=%s point=(%s,%s,%s,%s,%s)",
tostring(self:GetName()),
tostring(self:IsShown()),
tostring(self.hidden),
tostring(self.shouldBeShowing),
tostring(parent and parent.GetName and parent:GetName() or parent),
tostring(point1),
tostring(rel1 and rel1.GetName and rel1:GetName() or rel1),
tostring(point2),
tostring(xOff),
tostring(yOff)
))
end
self.shouldBeShowing = self:IsShown();
self._show = self.Show;
self.Show = function()
self.shouldBeShowing = true;
end
self:Hide();
if self.data.lineFrames then
for _, line in pairs(self.data.lineFrames) do
line:Hide()
end
end
self._hide = self.Hide;
self.Hide = function()
self.shouldBeShowing = false;
end
self.hidden = true
end
end
--- This is needed because HBD will show the icons again after switching zones and stuff like that
function _Qframe:FakeShow()
if self.hidden then
if _G.QuestieDebugPins and self.miniMapIcon then
local parent = self:GetParent()
local point1, rel1, point2, xOff, yOff = self:GetPoint()
print(string.format(
"[QD] FakeShow name=%s shown=%s hidden=%s should=%s parent=%s point=(%s,%s,%s,%s,%s)",
tostring(self:GetName()),
tostring(self:IsShown()),
tostring(self.hidden),
tostring(self.shouldBeShowing),
tostring(parent and parent.GetName and parent:GetName() or parent),
tostring(point1),
tostring(rel1 and rel1.GetName and rel1:GetName() or rel1),
tostring(point2),
tostring(xOff),
tostring(yOff)
))
end
self.hidden = false
self.Show = self._show;
self.Hide = self._hide;
self._show = nil
self._hide = nil
if self.shouldBeShowing then
self:Show();
if self.data.lineFrames then
for _, line in pairs(self.data.lineFrames) do
line:Show()
end
end
end
end
end
---Checks wheather the frame/icon should be hidden or not. Only for quest icons/frames.
---@return boolean @True if the frame/icon should be hidden and :FakeHide() should be called, false otherwise
function _Qframe:ShouldBeHidden()
local profile = Questie.db.profile
local data = self.data
local iconType = data.Type -- v6.5.1 values: available, complete, manual, monster, object, item, event. This function is not called with manual.
local questId = data.Id
--investigate quest and cache results to minimize DB lookups
local repeatable = QuestieDB.IsRepeatable(questId)
local event = QuestieDB.IsActiveEventQuest(questId)
local dungeon = QuestieDB.IsDungeonQuest(questId)
local raid = QuestieDB.IsRaidQuest(questId)
local pvp = QuestieDB.IsPvPQuest(questId)
local normal = not (repeatable or event or dungeon or raid or pvp)
if (not profile.enabled) -- all quest icons disabled
or ((not profile.enableMapIcons) and (not self.miniMapIcon))
or ((not profile.enableMiniMapIcons) and (self.miniMapIcon))
or ((self.miniMapIcon) and profile.hideMiniMapQuestIcons and (iconType == "available" or iconType == "complete"))
or ((not profile.enableTurnins) and iconType == "complete")
or ((not profile.enableObjectives) and (iconType == "monster" or iconType == "object" or iconType == "event" or iconType == "item"))
or (profile.hideUnexploredMapIcons and not QuestieMap.utils:IsExplored(self.UiMapID, self.x, self.y)) -- Hides unexplored map icons
or (profile.hideUntrackedQuestsMapIcons and not QuestieQuest:ShouldShowQuestNotes(questId)) -- Hides untracked map icons
or (data.ObjectiveData and data.ObjectiveData.HideIcons)
or (data.QuestData and data.QuestData.HideIcons and iconType ~= "complete")
-- Hide only available quest icons of following quests. I.e. show objectives and complete icons always (when they are in questlog).
-- i.e. (iconType == "available") == (iconType ~= "monster" and iconType ~= "object" and iconType ~= "event" and iconType ~= "item" and iconType ~= "complete"):
or (iconType == "available"
and ((not DailyQuests:IsActiveDailyQuest(questId)) -- hide not-today-dailies
or ((not profile.enableAvailable) and normal)
or ((not profile.showRepeatableQuests) and repeatable)
or (profile.hideRepeatableBelowMaxLevel and (repeatable or QuestieDB.IsBoardQuest(questId)) and (UnitLevel("player") or 0) < 60) -- Hide Callboard/repeatable quests below 60 (#10)
or ((not profile.showEventQuests) and event)
or ((not profile.showDungeonQuests) and dungeon)
or ((not profile.showRaidQuests) and raid)
or ((not profile.showPvPQuests) and pvp)
-- this quest group isn't loaded at all while disabled:
-- or ((not questieCharDB.showAQWarEffortQuests) and QuestieQuestBlacklist.AQWarEffortQuests[questId])
)
)
then
return true
end
return false
end