-- Quest log as the list pane of the world map. In camelot the log is not a window: it is -- QuestMapFrame, a 330-wide pane on the right of the map (blizzard_worldmap.lua, -- AttachQuestLog), and L opens the map with it. Quest markers and map jumps use WotLK's own -- (WorldMapQuestFrame, QuestPOI_DisplayButton, WorldMap_OpenToQuest). Clicking a quest -- replaces the list with its details page. ForeverUI = ForeverUI or {} local J = {} ForeverUI.QuestLog = J local L = ForeverUI.L -- Pane layout (camelot questmapframe.xml) local V = { width = 330, x = -3, top = -25, down = 3, listRight = -22, listBottom = 9, listTop = -29, headerW = 289, headerH = 22, headerX = 9, questW = 290, questTextX = 31, questTextY = -8, checkboxSide = 14, checkboxY = -8, objectiveW = 220, objectiveTextW = 205, searchW = 200, searchH = 20, searchX = 6, searchY = 7, counterW = 100, counterH = 20, counterX = 3, settingW = 15, settingH = 16, settingX = 19, settingY = 25, barX = 8, barTop = 2, barBottom = -4, step = 16, } -- Row gaps from QuestLogQuests_UpdateButtonSpacing, for the row types 3.3.5 has local GAP = { headerAfterNone = 8, headerAfterHeader = 6, headerAfterQuest = 4, questAfterHeader = 2, questAfterQuest = -3, } -- camelot colors: GlobalColor.db2, and blizzard_framexmlbase/constants.lua for difficulty local COLOR = { objective = { 0.8, 0.8, 0.8 }, -- QUEST_OBJECTIVE_FONT_COLOR objectiveHover = { 1, 1, 1 }, -- QUEST_OBJECTIVE_HIGHLIGHT_FONT_COLOR headerIdle = { 0.502, 0.502, 0.502 }, -- DISABLED_FONT_COLOR headerHover = { 1, 1, 1 }, -- HIGHLIGHT_FONT_COLOR difficulty = { trivial = { 0.50, 0.50, 0.50 }, standard = { 0.25, 0.75, 0.25 }, difficult = { 1.00, 0.82, 0.00 }, verydifficult = { 1.00, 0.50, 0.25 }, impossible = { 1.00, 0.10, 0.10 }, }, difficultyHover = { trivial = { 0.70, 0.70, 0.70 }, standard = { 0.43, 0.93, 0.43 }, difficult = { 1.00, 1.00, 0.10 }, verydifficult = { 1.00, 0.75, 0.44 }, impossible = { 1.00, 0.40, 0.40 }, }, -- Quest details page: the "Default" material of GetMaterialTextColors text = { 0.18, 0.122, 0.059 }, -- DEFAULT_MATERIAL_TEXT_COLOR title = { 0, 0, 0 }, -- DEFAULT_MATERIAL_TITLETEXT_COLOR titleShadow = { 0.49, 0.35, 0.05 }, -- QuestFont_Shadow_Huge objectiveDone = { 0.2, 0.2, 0.2 }, -- QUEST_OBJECTIVE_COMPLETED_FONT_COLOR reward = { 0.902, 0.788, 0.671 }, -- QuestMapRewardsFont commonGray = { 0.659, 0.659, 0.659 }, -- COMMON_GRAY_COLOR } -- Texts: 3.3.5's when it has them, camelot's otherwise (GlobalStrings.db2), in -- Texts_.lua local TEXT = { search = L.QUESTLOG_SEARCH, -- SEARCH_QUEST_LOG noResults = L.QUESTLOG_NO_RESULTS, -- QUEST_LOG_NO_RESULTS empty = L.QUESTLOG_EMPTY, -- QUEST_LOG_NO_QUESTS counter = L.QUESTLOG_COUNT, -- QUEST_LOG_COUNT_TEMPLATE showObjectives = L.QUESTLOG_SHOW_OBJECTIVES, -- QUEST_LOG_SHOW_OBJECTIVES ready = L.QUESTLOG_READY, -- QUEST_WATCH_QUEST_READY details = L.QUESTLOG_CLICK_DETAILS, -- CLICK_QUEST_DETAILS untrack = L.QUESTLOG_UNTRACK_QUEST, -- UNTRACK_QUEST shareInChat = L.QUESTLOG_SHARE_IN_CHAT, -- SHARE_IN_CHAT trackAll = L.QUESTLOG_TRACK_ALL, -- QUEST_LOG_TRACK_ALL untrackAll = L.QUESTLOG_UNTRACK_ALL, -- QUEST_LOG_UNTRACK_ALL -- Quest details page untrackShort = L.QUESTLOG_UNTRACK, -- UNTRACK_QUEST_ABBREV rewards = QUEST_REWARDS, -- REWARDS (3.3.5: QUEST_REWARDS) titleFailed = L.QUESTLOG_TITLE_FAILED, -- QUEST_TITLE_FORMAT_FAILED } local FONT = "Fonts\\FRIZQT__.TTF" local function color(t, c) t:SetTextColor(c[1], c[2], c[3]) end -- ---------------------------------------------------------------- state -- ForeverUIDB exists only once our saved variables are loaded. local function settings() ForeverUIDB = ForeverUIDB or {} ForeverUIDB.questLog = ForeverUIDB.questLog or {} local r = ForeverUIDB.questLog if r.pane == nil then r.pane = true end -- questLogOpen if r.objectives == nil then r.objectives = true end return r end J.settings = settings -- -------------------------------------------------------------- difficulty -- WotLK's ranking, camelot's colors. local function difficultyKey(level) local c = GetQuestDifficultyColor and GetQuestDifficultyColor(level) if c and QuestDifficultyColors then for key, value in pairs(QuestDifficultyColors) do if value == c and COLOR.difficulty[key] then return key end end end return "difficult" end -- ------------------------------------------------------------- data -- A log entry, with what its row shows. local function entry(index) local title, level, tag, group, headerFlag, collapsed, completed, daily, questID = GetQuestLogTitle(index) if not title then return nil end return { index = index, title = title, level = level, questTag = tag, isHeader = headerFlag and true or false, isCollapsed = collapsed and true or false, isComplete = completed, isDaily = daily, questID = questID, } end -- camelot 70450 QuestMapFrameOverrides.titleFrameLeftPadding local QUEST_LEFT_PADDING = 4 local function isElite(info) return info.questTag == ELITE end -- camelot QuestUtilsOverrides.GetQuestTagText (1.60.1.70450): the quest type as text, for four -- types only: "(Elite)", "(Dungeon)", "(PvP)", "(Raid)" (CALENDAR_TYPE_DUNGEON, CALENDAR_TYPE_PVP, -- RAID). 3.3.5 GetQuestLogTitle gives the type as a localized text (QuestInfo.dbc): compared with -- the client's own strings; "Raid (10)" / "Raid (25)" start with RAID. Group and Heroic: nothing. -- Shared with the objective tracker. local function questTagText(tag) if not tag or tag == "" then return nil end local label if tag == ELITE then label = ELITE elseif tag == CALENDAR_TYPE_DUNGEON or tag == LFG_TYPE_DUNGEON then label = CALENDAR_TYPE_DUNGEON elseif tag == PVP or tag == CALENDAR_TYPE_PVP then label = CALENDAR_TYPE_PVP elseif RAID and string.sub(tag, 1, string.len(RAID)) == RAID then label = RAID end return label and format(PARENS_TEMPLATE, label) or nil end ForeverUI.QuestTagText = questTagText -- QuestLogQuests_GetTitle with camelot's override: "[" level ("+" if elite) "] ", -- preceded by "[n] " if n party members have the quest. local function questTitle(info) local title = "[" .. tostring(info.level) .. (isElite(info) and "+" or "") .. "] " .. info.title local members = 0 for i = 1, (GetNumPartyMembers and GetNumPartyMembers() or 0) do if IsUnitOnQuest and IsUnitOnQuest(info.index, "party" .. i) then members = members + 1 end end if members > 0 then title = "[" .. members .. "] " .. title end return title end -- ------------------------------------------------------------- search -- QuestSearcher: plain text, on the title, objective text and objectives. A collapsed -- header hides its quests in 3.3.5, so everything is expanded during a search and each -- header gets its state back by name. local R = { text = nil, states = nil } local function expandAll() R.states = {} for i = GetNumQuestLogEntries(), 1, -1 do local info = entry(i) if info and info.isHeader then R.states[info.title] = info.isCollapsed if info.isCollapsed then ExpandQuestHeader(i) end end end end local function restoreStates() if not R.states then return end for i = GetNumQuestLogEntries(), 1, -1 do local info = entry(i) if info and info.isHeader and R.states[info.title] then CollapseQuestHeader(i) end end R.states = nil end -- Whether a quest matches the search text local function linkMatches(info) if not R.text then return true end local searching = R.text if string.find(string.lower(info.title), searching, 1, true) then return true end local selection = GetQuestLogSelection() SelectQuestLogEntry(info.index) local _, objectives = GetQuestLogQuestText() SelectQuestLogEntry(selection) if objectives and string.find(string.lower(objectives), searching, 1, true) then return true end for i = 1, GetNumQuestLeaderBoards(info.index) do local text = GetQuestLogLeaderBoard(i, info.index) if text and string.find(string.lower(text), searching, 1, true) then return true end end return false end -- Filters the list by text; an empty text ends the search function J.find(text) if text and text ~= "" then if not R.text then expandAll() end R.text = string.lower(text) else if R.text then restoreStates() end R.text = nil end J.update() end -- ------------------------------------------------------ markers, map local POI_PARENT = "ForeverUIQuestScrollContents" -- WotLK numbering for the shown map: WorldMapFrame_GetQuestFrame counts completed quests -- apart (QUEST_POI_COMPLETE_IN) and numbers the others (QUEST_POI_NUMERIC, index - completed). local function mapPois() local pois = {} local completedCount = 0 for i = 1, (WorldMapFrame and WorldMapFrame.numQuests or 0) do local f = _G["WorldMapQuestFrame" .. i] if f and f.questId and f.questId ~= 0 then if f.completed then completedCount = completedCount + 1 pois[f.questId] = { QUEST_POI_COMPLETE_IN, completedCount } else pois[f.questId] = { QUEST_POI_NUMERIC, i - completedCount } end end end return pois end local function selectedQuest() return WORLDMAP_SETTINGS and WORLDMAP_SETTINGS.selectedQuestId end -- Lights or clears a quest's area on the map, like the WotLK list on hover -- (WorldMapQuestFrame_OnEnter / _OnLeave). The selected quest keeps its area. local function zone(info, lightUp) if not (WorldMapBlobFrame and WorldMapBlobFrame.DrawQuestBlob and info.questID) then return end if info.isComplete and info.isComplete > 0 then return end if not lightUp and selectedQuest() == info.questID then return end WorldMapBlobFrame:DrawQuestBlob(info.questID, lightUp and true or false) end -- Map part of QuestMapFrame_ShowQuestDetails: WorldMap_OpenToQuest moves the map to the -- quest's zone and selects the quest. local function showOnMap(info) SelectQuestLogEntry(info.index) if info.questID and WorldMap_OpenToQuest then WorldMap_OpenToQuest(info.questID) end if J.onSelect then J.onSelect(info.index) end end J.showOnMap = showOnMap -- ------------------------------------------------------------- actions -- _QuestLog_ToggleQuestWatch from QuestLogFrame.lua (local to the client, so copied). -- The list and page are then redrawn: AddQuestWatch and RemoveQuestWatch fire no event -- the pane listens to. local function toggleTracking(index) if IsQuestWatched(index) then RemoveQuestWatch(index) WatchFrame_Update() else if GetNumQuestWatches() >= MAX_WATCHABLE_QUESTS then UIErrorsFrame:AddMessage(format(QUEST_WATCH_TOO_MANY, MAX_WATCHABLE_QUESTS), 1.0, 0.1, 0.1, 1.0) return end AddQuestWatch(index) WatchFrame_Update() end if J.isOpen() then J.update() J.updateDetails() end end J.toggleTracking = toggleTracking -- Abandon button of QuestLogFrame.xml local function abandonQuest(index) SelectQuestLogEntry(index) SetAbandonQuest() local objects = GetAbandonQuestItems() if objects then StaticPopup_Hide("ABANDON_QUEST") StaticPopup_Show("ABANDON_QUEST_WITH_ITEMS", GetAbandonQuestName(), objects) else StaticPopup_Hide("ABANDON_QUEST_WITH_ITEMS") StaticPopup_Show("ABANDON_QUEST", GetAbandonQuestName()) end end local function shareQuest(index) SelectQuestLogEntry(index) QuestLogPushQuest() PlaySound("igQuestLogOpen") end local function inGroup() return (GetNumPartyMembers and GetNumPartyMembers() > 0) or (GetNumRaidMembers and GetNumRaidMembers() > 1) end local function menu(anchor, list) if ForeverUI.WorldMap and ForeverUI.WorldMap.openMenu then ForeverUI.WorldMap.openMenu(anchor, list) end end -- QuestMapLogTitleButton_CreateContextMenu, without super tracking (missing in 3.3.5) local function questMenu(row) local index = row.index local l = {} table.insert(l, { text = IsQuestWatched(index) and TEXT.untrack or TRACK_QUEST, func = function() toggleTracking(index) end, }) local shareable = GetQuestLogPushable and GetQuestLogPushable(index) and inGroup() table.insert(l, { text = SHARE_QUEST, disabled = not shareable, func = function() shareQuest(index) end, }) -- camelot 70450 drops "Share in chat" outside the gamepad UI; kept in the log on the user's -- request (2026-10-18), removed from the tracker only table.insert(l, { text = TEXT.shareInChat, func = function() local link = GetQuestLink(index) if link and not (ChatEdit_InsertLink and ChatEdit_InsertLink(link)) then ChatFrame_OpenChat(link) end end, }) table.insert(l, { text = ABANDON_QUEST, func = function() abandonQuest(index) end, }) menu(row, l) end -- QuestMapLogHeaderButton_CreateContextMenu: track or untrack the quests of a header -- (SetHeaderQuestsTracked) local function trackHeader(headerName, follow) local inside = false for i = 1, GetNumQuestLogEntries() do local info = entry(i) if info and info.isHeader then inside = (info.title == headerName) elseif info and inside then local isTracked = IsQuestWatched(i) and true or false if follow and not isTracked then if GetNumQuestWatches() >= MAX_WATCHABLE_QUESTS then UIErrorsFrame:AddMessage(format(QUEST_WATCH_TOO_MANY, MAX_WATCHABLE_QUESTS), 1.0, 0.1, 0.1, 1.0) break end AddQuestWatch(i) elseif not follow and isTracked then RemoveQuestWatch(i) end end end WatchFrame_Update() J.update() end -- ------------------------------------------------------------- tooltip -- QuestMapLogTitleButton_OnEnter: title, failure, completion or objective text, then -- objectives (white, grey when done), required money, the click hint. local function tooltip(row) local info = row.info GameTooltip:ClearAllPoints() GameTooltip:SetPoint("TOPLEFT", row, "TOPRIGHT", 34, 0) GameTooltip:SetOwner(row, "ANCHOR_PRESERVE") GameTooltip:SetText(info.title) local width = 20 + math.max(231, GameTooltipTextLeft1:GetStringWidth()) -- Right edge of the map in UI units: WorldMapFrame has its own scale (WorldMap.lua) local right = WorldMapFrame:GetRight() * WorldMapFrame:GetEffectiveScale() / UIParent:GetEffectiveScale() if width > UIParent:GetRight() - right then GameTooltip:ClearAllPoints() GameTooltip:SetPoint("TOPRIGHT", row, "TOPLEFT", -5, 0) GameTooltip:SetOwner(row, "ANCHOR_PRESERVE") GameTooltip:SetText(info.title) end if info.isComplete and info.isComplete < 0 then GameTooltip:AddLine(FAILED, 1, 0.125, 0.125) end GameTooltip:AddLine(" ") if info.isComplete and info.isComplete > 0 then GameTooltip:AddLine(GetQuestLogCompletionText(info.index) or TEXT.ready, 1, 1, 1, true) GameTooltip:AddLine(" ") else local selection = GetQuestLogSelection() SelectQuestLogEntry(info.index) local _, objectives = GetQuestLogQuestText() SelectQuestLogEntry(selection) GameTooltip:AddLine(objectives, 1, 1, 1, true) GameTooltip:AddLine(" ") local addSeparator = false for i = 1, GetNumQuestLeaderBoards(info.index) do local text, _, finished = GetQuestLogLeaderBoard(i, info.index) if text then local g = finished and 0.502 or 1 GameTooltip:AddLine(QUEST_DASH .. text, g, g, g, true) addSeparator = true end end local required = GetQuestLogRequiredMoney(info.index) if required and required > 0 then local money = GetMoney() local g = 1 if required <= money then money = required g = 0.502 end GameTooltip:AddLine(QUEST_DASH .. GetMoneyString(money) .. " / " .. GetMoneyString(required), g, g, g) addSeparator = true end if addSeparator then GameTooltip:AddLine(" ") end end GameTooltip:AddLine(TEXT.details, 0.098, 1, 0.098) GameTooltip:Show() end -- ------------------------------------------------------ quest details page -- -- camelot DetailsFrame (questmapframe.xml, QuestLogQuestDetailsMixin) and QuestInfo -- (QUEST_TEMPLATE_MAP_DETAILS / _MAP_REWARDS). The QuestInfo frames are not reused: they -- are unique and shared with the NPC quest frame, which reparents them on every show. The -- page has its own, stacked like QuestInfo_Display (each TOPLEFT on the previous BOTTOMLEFT). -- Missing in 3.3.5, so absent here: giver portrait, path buttons, campaign seal, spell -- objectives, major faction reputation, currencies. 3.3.5 arena point rewards look like honor. local D = { width = 308, height = 502, x = -22, y = -1, backgroundMax = 440, edgeLeft = -3, edgeH = 4, edgeRight = 3, edgeBottom = 17, backW = 307, backH = 52, backButtonW = 90, backButtonX = 11, backButtonY = 4, buttonH = 22, abandonW = 105, shareW = 103, trackW = 105, buttonsX = -3, buttonsY = -2, separatorX = 6, textW = 298, textH = 430, textX = 5, textY = -43, barX = 13, barTop = 17, barBottom = -27, content = 289, -- contentWidth rewardW = 307, rewardY = 23, rewardLevel = 50, rewardMin = 124, rewardTileTop = 56, tile = 83, captionY = -22, -- QUEST_TEMPLATE_MAP_REWARDS and questinfo.lua rewardX = 12, rewardY2 = -52, rewardSpacing = 8, rewardSectionGap = 5, rewardRowGap = 2, rewardExtra = 62, rewardEmpty = 59, choiceW = 265, itemW = 134, itemH = 30, icon = 30, nameW = 92, nameH = 36, step = 16, } -- camelot text styles with their fonts (fontstyles.xml, fonts.xml): QuestTitleFont = -- MORPHEUS 18, shadow (1, -1); QuestFont = FRIZQT 13; QuestFontNormalSmall = FRIZQT 12; -- QuestMapRewardsFont = FRIZQT 10; QuestFont_Huge = MORPHEUS 18. local MORPHEUS = "Fonts\\MORPHEUS.ttf" local FONTS = { title = { MORPHEUS, 18 }, text = { FONT, 13 }, small = { FONT, 12 }, reward = { FONT, 10 }, caption = { MORPHEUS, 18 }, } local PANEL = "Interface\\Buttons\\UI-Panel-Button-" local ICONS = { money = "Interface\\Icons\\INV_Misc_Coin_01", xp = "interface\\ForeverUI\\icons\\xp_icon", title = "Interface\\Icons\\INV_Misc_Note_02", honor = "interface\\ForeverUI\\icons\\pvpcurrency-honor-", arena = "Interface\\PVPFrame\\PVP-ArenaPoints-Icon", outline = "Interface\\ForeverUI\\common\\whiteiconframe", } -- Applies a FONTS model to a font string; c: optional color local function font(fs, model, c) local p = FONTS[model] fs:SetFont(p[1], p[2]) if c then color(fs, c) end end -- camelot UIPanelButtonTemplate: three UI-Panel-Button-* pieces, where the 3.3.5 button -- stretches a single one. local function panelButton(parent, name, text, width) local b = CreateFrame("Button", name, parent) b:SetWidth(width) b:SetHeight(D.buttonH) local function piece(u1, u2) local t = b:CreateTexture(nil, "BACKGROUND") t:SetTexCoord(u1, u2, 0, 0.6875) return t end local g = piece(0, 0.09375) g:SetWidth(12) g:SetPoint("TOPLEFT", b, "TOPLEFT", 0, 0) g:SetPoint("BOTTOMLEFT", b, "BOTTOMLEFT", 0, 0) local d = piece(0.53125, 0.625) d:SetWidth(12) d:SetPoint("TOPRIGHT", b, "TOPRIGHT", 0, 0) d:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", 0, 0) local m = piece(0.09375, 0.53125) m:SetPoint("TOPLEFT", g, "TOPRIGHT", 0, 0) m:SetPoint("BOTTOMRIGHT", d, "BOTTOMLEFT", 0, 0) b.pieces = { g, m, d } local function state(suffix) for _, t in ipairs(b.pieces) do t:SetTexture(PANEL .. suffix) end end state("Up") local fs = b:CreateFontString(nil, "ARTWORK") fs:SetFontObject(GameFontNormal) fs:SetPoint("CENTER", b, "CENTER", 0, 0) b:SetFontString(fs) b:SetNormalFontObject(GameFontNormal) b:SetHighlightFontObject(GameFontHighlight) b:SetDisabledFontObject(GameFontDisable) b:SetText(text) b:SetHighlightTexture(PANEL .. "Highlight") local s = b:GetHighlightTexture() if s then s:SetTexCoord(0, 0.625, 0, 0.6875) s:SetBlendMode("ADD") end b.active = true b:SetScript("OnMouseDown", function(self) if self.active then state("Down") end end) b:SetScript("OnMouseUp", function(self) if self.active then state("Up") end end) -- UIPanelButton_OnEnable / _OnDisable function b:Activate(yes) self.active = yes and true or false if yes then self:Enable() state("Up") else self:Disable() state("Disabled") end end return b end -- SmallItemButtonTemplate (+ SmallQuestRewardItemButtonTemplate): icon 30, QuestItemBorder -- name frame at its size, name 92 x 36, count at the icon's bottom right, quality outline. local function createItem(parent, name) local b = CreateFrame("Button", name, parent) b:SetWidth(D.itemW) b:SetHeight(D.itemH) b:RegisterForClicks("LeftButtonUp", "RightButtonUp") local icon = b:CreateTexture(nil, "BACKGROUND") icon:SetWidth(D.icon) icon:SetHeight(D.icon) icon:SetPoint("TOPLEFT", b, "TOPLEFT", 0, 0) local frame = b:CreateTexture(nil, "BACKGROUND") ForeverUI.SetAtlas(frame, "questitemborder") frame:SetPoint("LEFT", icon, "RIGHT", 2, 0) local text = b:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") text:SetWidth(D.nameW) text:SetHeight(D.nameH) text:SetJustifyH("LEFT") text:SetPoint("LEFT", frame, "LEFT", 4, 0) local count = b:CreateFontString(nil, "ARTWORK", "NumberFontNormalSmall") count:SetJustifyH("RIGHT") count:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", 0, 1) local outline = b:CreateTexture(nil, "OVERLAY") outline:SetTexture(ICONS.outline) outline:SetAllPoints(icon) outline:Hide() b.Icon, b.NameFrame, b.Name, b.Count, b.IconBorder = icon, frame, text, count, outline return b end -- SetItemButtonCount: the count shows only above 1 local function setItemCount(b, n) if n and n > 1 then b.Count:SetText(n) b.Count:Show() else b.Count:Hide() end end -- Log index of a quest; the 3.3.5 log only has indexes. local function questIndex(questID) if not questID then return nil end for i = 1, GetNumQuestLogEntries() do local info = entry(i) if info and not info.isHeader and info.questID == questID then return i end end return nil end -- Reward buttons: tooltip and click of QuestInfoRewardItemMixin (item) and -- QuestInfoRewardSpellCodeMixin (spell). local function onRewardEnter(self) local d = J.details if d and d.index then SelectQuestLogEntry(d.index) end GameTooltip:SetOwner(self, "ANCHOR_RIGHT") if self.objectType == "item" then GameTooltip:SetQuestLogItem(self.type, self:GetID()) if GameTooltip_ShowCompareItem then GameTooltip_ShowCompareItem(GameTooltip) end elseif self.objectType == "spell" then GameTooltip:SetQuestLogRewardSpell() elseif self.tooltip then GameTooltip:SetText(self.tooltip, 1, 1, 1) else GameTooltip:Hide() return end GameTooltip:Show() end local function onRewardClick(self) if not IsModifiedClick() then return end local d = J.details if d and d.index then SelectQuestLogEntry(d.index) end if self.objectType == "item" then HandleModifiedItemClick(GetQuestLogItemLink(self.type, self:GetID())) elseif self.objectType == "spell" and IsModifiedClick("CHATLINK") then ChatEdit_InsertLink(GetQuestLogSpellLink()) end end local function hookReward(b) b:SetScript("OnEnter", onRewardEnter) b:SetScript("OnLeave", function() GameTooltip:Hide() end) b:SetScript("OnClick", onRewardClick) return b end -- Rewards background: header, bottom, and between them the vertical pattern in tiles of 83, -- since SetTexCoord cannot repeat an atlas piece. local function placeRewardsBackground(r, height) local e = ForeverUI.AtlasEntry("questlog-reward-tile-vertical") local rest = height - D.rewardTileTop local n = 0 while e and rest > 0 do n = n + 1 local t = r.tiles[n] if not t then t = r:CreateTexture(nil, "BACKGROUND") ForeverUI.SetAtlas(t, "questlog-reward-tile-vertical") r.tiles[n] = t end local h = math.min(D.tile, rest) t:SetHeight(h) t:SetTexCoord(e[2], e[3], e[4], e[4] + (e[5] - e[4]) * h / D.tile) t:ClearAllPoints() t:SetPoint("TOPLEFT", r.top, "BOTTOMLEFT", 0, -(n - 1) * D.tile) t:Show() rest = rest - h end for i = n + 1, #r.tiles do r.tiles[i]:Hide() end end -- Builds the quest details page inside the pane v local function createDetails(v) local d = CreateFrame("Frame", "ForeverUIQuestDetailsFrame", v) d:SetWidth(D.width) d:SetHeight(D.height) d:SetPoint("TOPRIGHT", v, "TOPRIGHT", D.x, D.y) d:EnableMouse(true) -- Background at the frame's width, capped at 440 (AdjustBackgroundTexture) local background = d:CreateTexture(nil, "BACKGROUND") ForeverUI.SetAtlas(background, "questdetailsbackgrounds") local e = ForeverUI.AtlasEntry("questdetailsbackgrounds") background:SetPoint("TOPLEFT", d, "TOPLEFT", 0, 0) background:SetPoint("TOPRIGHT", d, "TOPRIGHT", 0, 0) if e then local wantedValue = e[7] * D.width / e[6] if wantedValue > D.backgroundMax then background:SetTexCoord(e[2], e[3], e[4], e[4] + (e[5] - e[4]) * D.backgroundMax / wantedValue) background:SetHeight(D.backgroundMax) else background:SetHeight(wantedValue) end end d.background = background -- Border (QuestLogBorderFrameTemplate), level 100 local edge = CreateFrame("Frame", nil, d) edge:SetPoint("TOPLEFT", d, "TOPLEFT", D.edgeLeft, D.edgeH) edge:SetPoint("BOTTOMRIGHT", d, "BOTTOMRIGHT", D.edgeRight, D.edgeBottom) edge:SetFrameLevel(d:GetFrameLevel() + 100) if ForeverUI.CreateNineSlice then ForeverUI.CreateNineSlice(edge, "questlog-frame", 53, { 0, 0, 0, 0 }, "BORDER") end local filigree = edge:CreateTexture(nil, "ARTWORK") ForeverUI.SetAtlas(filigree, "questlog-frame-filigree") filigree:SetPoint("TOP", edge, "TOP", 0, 1) d.edge = edge -- Top banner and Back button local banner = CreateFrame("Frame", nil, d) banner:SetWidth(D.backW) banner:SetHeight(D.backH) banner:SetPoint("TOPLEFT", d, "TOPLEFT", 0, 0) local image = banner:CreateTexture(nil, "BORDER") ForeverUI.SetAtlas(image, "questlog-reward-top-frame") image:SetPoint("TOPLEFT", banner, "TOPLEFT", 0, 0) local backButton = panelButton(banner, "ForeverUIQuestDetailsBackButton", BACK, D.backButtonW) backButton:SetPoint("LEFT", banner, "LEFT", D.backButtonX, D.backButtonY) backButton:SetScript("OnClick", function() PlaySound("igMainMenuOptionCheckBoxOn") J.backFromDetails() end) d.backButton = backButton -- Scrolling text local scroll = CreateFrame("ScrollFrame", "ForeverUIQuestDetailsScrollFrame", d) scroll:SetWidth(D.textW) scroll:SetHeight(D.textH) scroll:SetPoint("TOPLEFT", d, "TOPLEFT", D.textX, D.textY) local content = CreateFrame("Frame", "ForeverUIQuestDetailsContents", scroll) content:SetWidth(D.textW) content:SetHeight(1) scroll:SetScrollChild(content) d.scroll, d.content = scroll, content local function text(model, c) local fs = content:CreateFontString(nil, "BACKGROUND") font(fs, model, c) fs:SetJustifyH("LEFT") fs:SetWidth(D.content) return fs end d.title = text("title", COLOR.title) d.title:SetShadowOffset(1, -1) d.title:SetShadowColor(COLOR.titleShadow[1], COLOR.titleShadow[2], COLOR.titleShadow[3], 1) d.objectivesText = text("text", COLOR.text) d.timer = text("small", COLOR.text) d.group = text("text", COLOR.text) d.descriptionHeader = text("title", COLOR.title) d.descriptionHeader:SetShadowOffset(1, -1) d.descriptionHeader:SetShadowColor(COLOR.titleShadow[1], COLOR.titleShadow[2], COLOR.titleShadow[3], 1) d.descriptionHeader:SetText(QUEST_DESCRIPTION) d.description = text("text", COLOR.text) d.objectives = {} d.newObjective = function() return text("small", COLOR.text) end -- Required money: label and a small static money frame local money = CreateFrame("Frame", nil, content) money:SetWidth(285) money:SetHeight(28) local moneyText = money:CreateFontString(nil, "BACKGROUND") font(moneyText, "small") moneyText:SetPoint("LEFT", money, "LEFT", 0, 0) moneyText:SetText(REQUIRED_MONEY) local purse = CreateFrame("Frame", "ForeverUIQuestDetailsRequiredMoney", money, "SmallMoneyFrameTemplate") purse:SetPoint("LEFT", moneyText, "RIGHT", 10, 0) if MoneyFrame_SetType then MoneyFrame_SetType(purse, "STATIC") end money.text, money.purse = moneyText, purse d.money = money -- Countdown (QuestInfoTimerFrame_OnUpdate) local clock = CreateFrame("Frame", nil, content) clock:SetScript("OnUpdate", function(self, elapsed) if self.rest then self.rest = math.max(self.rest - elapsed, 0) d.timer:SetText(TIME_REMAINING .. " " .. SecondsToTime(self.rest)) end end) d.clock = clock -- Space left for the rewards, at the bottom of the text local space = CreateFrame("Frame", nil, content) space:SetWidth(5) space:SetHeight(5) d.space = space local bar = ForeverUI.CreateScrollBar and ForeverUI.CreateScrollBar("ForeverUIQuestDetailsScrollBar", d, scroll) if bar then bar:ClearAllPoints() bar:SetPoint("TOPLEFT", scroll, "TOPRIGHT", D.barX, D.barTop) bar:SetPoint("BOTTOMLEFT", scroll, "BOTTOMRIGHT", D.barX, D.barBottom) bar:SetFrameLevel(edge:GetFrameLevel() + 1) bar.onScroll = function(step) J.scrollDetails(step * D.step) end d.bar = bar end scroll:EnableMouseWheel(true) scroll:SetScript("OnMouseWheel", function(_, direction) if d.bar and d.bar:IsShown() then d.bar:MoveTo(d.bar.offset - direction * 3) end end) -- Rewards: a still ScrollFrame that clips overflow (3.3.5 has no clipChildren; a ScrollFrame -- is the only frame that clips its children) local container = CreateFrame("ScrollFrame", "ForeverUIQuestDetailsRewardsContainer", d) container:SetWidth(D.rewardW) container:SetHeight(100) container:SetPoint("BOTTOMLEFT", d, "BOTTOMLEFT", 0, D.rewardY) container:SetFrameLevel(d:GetFrameLevel() + D.rewardLevel) local r = CreateFrame("Frame", "ForeverUIQuestDetailsRewardsFrame", container) r:SetWidth(D.rewardW) r:SetHeight(275) container:SetScrollChild(r) local down = r:CreateTexture(nil, "BORDER") ForeverUI.SetAtlas(down, "questlog-reward-bottom") down:SetPoint("BOTTOMLEFT", r, "BOTTOMLEFT", 0, 0) local top = r:CreateTexture(nil, "BORDER") ForeverUI.SetAtlas(top, "questlog-reward-header-top") top:SetPoint("TOPLEFT", r, "TOPLEFT", 0, 0) local caption = r:CreateFontString(nil, "ARTWORK") font(caption, "caption", COLOR.reward) caption:SetPoint("TOP", r, "TOP", 0, D.captionY) caption:SetText(TEXT.rewards) r.top, r.down, r.caption, r.tiles = top, down, caption, {} d.container, d.rewards = container, r -- MapQuestInfoRewardsFrame, at (12, -52) local list = CreateFrame("Frame", nil, r) list:SetWidth(285) list:SetHeight(10) list:SetPoint("TOPLEFT", r, "TOPLEFT", D.rewardX, D.rewardY2) local header = CreateFrame("Frame", nil, list) header:SetWidth(1) header:SetHeight(1) header:SetPoint("TOPLEFT", list, "TOPLEFT", 0, 0) local function rewardCaption(width) local fs = list:CreateFontString(nil, "BACKGROUND") font(fs, "reward", COLOR.reward) fs:SetJustifyH("LEFT") if width then fs:SetWidth(width) end return fs end list.header = header list.choose = rewardCaption(D.choiceW) list.receive = rewardCaption() list.playerTitle = rewardCaption() list.playerTitle:SetText(REWARD_TITLE) list.spell = rewardCaption() list.objects = {} list.xp = hookReward(createItem(list)) list.xp.Icon:SetTexture(ICONS.xp) list.xp.Name:SetFontObject(NumberFontNormal) list.money = hookReward(createItem(list)) list.money.Icon:SetTexture(ICONS.money) list.money.Name:SetFontObject(GameFontHighlight) list.title = hookReward(createItem(list)) list.title.Icon:SetTexture(ICONS.title) list.honor = hookReward(createItem(list)) list.arena = hookReward(createItem(list)) list.arena.Icon:SetTexture(ICONS.arena) list.spellButton = hookReward(createItem(list)) list.spellButton.objectType = "spell" d.list = list -- Abandon, Share, Track local abandon = panelButton(d, "ForeverUIQuestDetailsAbandonButton", ABANDON_QUEST_ABBREV, D.abandonW) abandon:SetPoint("BOTTOMLEFT", d, "BOTTOMLEFT", D.buttonsX, D.buttonsY) abandon:SetScript("OnClick", function() if d.index then abandonQuest(d.index) end end) local share = panelButton(d, "ForeverUIQuestDetailsShareButton", SHARE_QUEST_ABBREV, D.shareW) share:SetPoint("LEFT", abandon, "RIGHT", 0, 0) for i, side in ipairs({ { "RIGHT", "LEFT", D.separatorX }, { "LEFT", "RIGHT", -D.separatorX } }) do local s = share:CreateTexture(nil, "OVERLAY") ForeverUI.SetAtlas(s, "ui-frame-btndivmiddle") s:SetPoint(side[1], share, side[2], side[3], 0) share["separator" .. i] = s end share:SetScript("OnClick", function() if d.index then shareQuest(d.index) end end) local follow = panelButton(d, "ForeverUIQuestDetailsTrackButton", TRACK_QUEST_ABBREV, D.trackW) follow:SetPoint("LEFT", share, "RIGHT", 0, 0) follow:SetScript("OnClick", function() PlaySound("igMainMenuOptionCheckBoxOn") if d.index then toggleTracking(d.index) J.updateDetailsButtons() J.update() end end) d.abandon, d.share, d.follow = abandon, share, follow d:Hide() return d end -- QuestMapFrame_UpdateQuestDetailsButtons, with the 3.3.5 rules -- (QuestLogControlPanel_UpdateState) function J.updateDetailsButtons() local d = J.details if not d or not d.index then return end -- QuestLog_SetSelection: the selected quest is also the one to abandon, and -- GetAbandonQuestName names it if abandoning is allowed SelectQuestLogEntry(d.index) -- QuestLog_SetSelection hides the abandon popups before SetAbandonQuest: a refresh must not -- retarget an open one (tracker menu), whose Yes abandons the marked quest, whatever name -- it shows if not (StaticPopup_Visible("ABANDON_QUEST") or StaticPopup_Visible("ABANDON_QUEST_WITH_ITEMS")) then SetAbandonQuest() d.abandon:Activate(GetAbandonQuestName() and true or false) end if IsQuestWatched(d.index) then d.follow:SetText(TEXT.untrackShort) else d.follow:SetText(TRACK_QUEST_ABBREV) end d.share:Activate(GetQuestLogPushable() and inGroup() and true or false) end -- QuestInfo_ShowRewards for what 3.3.5 provides. Returns the list height, or nil if there is -- no reward. local function showRewards(d) local l = d.list for _, b in ipairs(l.objects) do b:Hide() end for _, f in ipairs({ l.choose, l.receive, l.playerTitle, l.spell, l.xp, l.money, l.title, l.honor, l.arena, l.spellButton }) do f:ClearAllPoints() f:Hide() end l.rows, l.headers = 0, 0 local numItems = GetNumQuestLogRewards() or 0 local numChoices = GetNumQuestLogChoices() or 0 local money = GetQuestLogRewardMoney() or 0 local xp = GetQuestLogRewardXP and GetQuestLogRewardXP() or 0 local honor = GetQuestLogRewardHonor() or 0 local arena = GetQuestLogRewardArenaPoints and GetQuestLogRewardArenaPoints() or 0 local playerTitle = GetQuestLogRewardTitle() local spellIcon, spellName, spellTradeSkill, spellLearned = GetQuestLogRewardSpell() if numItems + numChoices == 0 and money == 0 and xp == 0 and honor == 0 and arena == 0 and not playerTitle and not spellIcon then return nil end -- camelot layout: two elements per row; a section starts again on the left; a header takes -- a row of its own local total = l.header:GetHeight() local anchor = l.header local newSection, onePerRow, rightPlaced = true, false, false local function section(wide) newSection = true onePerRow = wide and true or false end local function add(f, h) if not newSection and not rightPlaced and not onePerRow then f:SetPoint("TOPLEFT", anchor, "TOPRIGHT", D.rewardSpacing, 0) rightPlaced = true else local gap = newSection and D.rewardSectionGap or D.rewardRowGap f:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -gap) total = total + (h or f:GetHeight()) + gap anchor = f rightPlaced = false newSection = false l.rows = l.rows + 1 end f:Show() end local function header(f) l.headers = l.headers + 1 section(true) add(f) end local n = 0 local function object(type, i, read) n = n + 1 local b = l.objects[n] if not b then b = hookReward(createItem(l, "ForeverUIQuestDetailsRewardItem" .. n)) l.objects[n] = b end local name, icon, count, quality, usable = read(i) b.type, b.objectType = type, "item" b:SetID(i) b.Name:SetText(name) b.Icon:SetTexture(icon) setItemCount(b, count) -- camelot BAG_ITEM_QUALITY_COLORS: common in COMMON_GRAY_COLOR, higher qualities in their -- color, poor without outline local c = quality and quality >= 2 and ITEM_QUALITY_COLORS and ITEM_QUALITY_COLORS[quality] if quality == 1 then b.IconBorder:SetVertexColor(COLOR.commonGray[1], COLOR.commonGray[2], COLOR.commonGray[3]) b.IconBorder:Show() elseif c then b.IconBorder:SetVertexColor(c.r, c.g, c.b) b.IconBorder:Show() else b.IconBorder:Hide() end if usable then b.Icon:SetVertexColor(1, 1, 1) b.NameFrame:SetVertexColor(1, 1, 1) else b.Icon:SetVertexColor(0.9, 0, 0) b.NameFrame:SetVertexColor(0.9, 0, 0) end add(b, D.itemH) end -- Choices if numChoices > 0 then l.choose:SetText(numChoices == 1 and REWARD_ITEMS_ONLY or REWARD_CHOICES) header(l.choose) section() for i = 1, numChoices do object("choice", i, GetQuestLogChoiceInfo) end end -- Spell: 3.3.5 gives only one if spellIcon then l.spell:SetText((spellTradeSkill and REWARD_TRADESKILL_SPELL) or (not spellLearned and REWARD_AURA) or REWARD_SPELL) header(l.spell) section() l.spellButton.Icon:SetTexture(spellIcon) l.spellButton.Name:SetText(spellName) setItemCount(l.spellButton, 0) add(l.spellButton, D.itemH) end -- Title if playerTitle then header(l.playerTitle) l.title.Name:SetText(playerTitle) setItemCount(l.title, 0) section() add(l.title, D.itemH) end -- Rewards received in any case if numItems > 0 or money > 0 or xp > 0 or honor > 0 or arena > 0 then l.receive:SetText((numChoices > 0 or spellIcon or playerTitle) and REWARD_ITEMS or REWARD_ITEMS_ONLY) header(l.receive) section() if xp > 0 then l.xp.Name:SetText(xp) setItemCount(l.xp, 0) add(l.xp, D.itemH) end if money > 0 then l.money.Name:SetText(GetMoneyString(money)) setItemCount(l.money, 0) add(l.money, D.itemH) end section() for i = 1, numItems do object("reward", i, GetQuestLogRewardInfo) end if honor > 0 then local faction = UnitFactionGroup("player") == "Horde" and "horde" or "alliance" l.honor.Icon:SetTexture(ICONS.honor .. faction) l.honor.Name:SetText(HONOR) setItemCount(l.honor, honor) l.honor.tooltip = HONOR_POINTS section() add(l.honor, D.itemH) end if arena > 0 then l.arena.Name:SetText(ARENA_POINTS) setItemCount(l.arena, arena) l.arena.tooltip = ARENA_POINTS section() add(l.arena, D.itemH) end end l:SetHeight(total) return total end -- AdjustRewardsFrameContainer: if all the text fits, or there is only one row, the rewards -- show in full; otherwise they grow as the text scrolls down, never below 124. function J.adjustRewards() local d = J.details if not d then return end local r = d.rewards local height = r:GetHeight() local scrollRange = d.scrollRange or 0 if scrollRange == 0 or (d.list.rows - d.list.headers) <= 1 then d.container:SetHeight(height) return end local h = height - (scrollRange - (d.scroll:GetVerticalScroll() or 0)) if h < D.rewardMin then h = D.rewardMin end d.container:SetHeight(h) end function J.scrollDetails(y) local d = J.details if not d then return end d.scroll:SetVerticalScroll(math.min(y, d.scrollRange or 0)) J.adjustRewards() end -- QuestInfo_Display(QUEST_TEMPLATE_MAP_DETAILS) then (..._MAP_REWARDS). Returns false if the -- quest left the log. 3.3.5 does not measure scroll content, so the height is summed -- element by element. start: true to scroll back to the top. local function showDetails(start) local d = J.details local index = questIndex(d.questID) if not index then return false end d.index = index SelectQuestLogEntry(index) local info = entry(index) local c = d.content local last, total = nil, 0 local function place(region, dx, dy, h, down) region:ClearAllPoints() if last then region:SetPoint("TOPLEFT", last, "BOTTOMLEFT", dx, dy) else region:SetPoint("TOPLEFT", c, "TOPLEFT", dx, dy) end region:Show() last = down or region total = total - dy + (h or region:GetHeight() or 0) end -- Title, with the failed mark local title = info.title if info.isComplete and info.isComplete < 0 then title = format(TEXT.titleFailed, title) end d.title:SetText(title) place(d.title, 5, -10) local description, objectives = GetQuestLogQuestText() d.objectivesText:SetText(objectives) place(d.objectivesText, 0, -5) -- Countdown local rest = GetQuestLogTimeLeft() d.clock.rest = rest if rest then d.timer:SetText(TIME_REMAINING .. " " .. SecondsToTime(rest)) place(d.timer, 0, -10) else d.timer:Hide() end -- Objectives: completed ones turn grey and say "(Complete)" for _, o in ipairs(d.objectives) do o:Hide() end local seen = 0 for i = 1, GetNumQuestLeaderBoards() do local text, kind, finished = GetQuestLogLeaderBoard(i) if kind ~= "spell" and kind ~= "log" then seen = seen + 1 local o = d.objectives[seen] if not o then o = d.newObjective() d.objectives[seen] = o end if not text or text == "" then text = kind end if finished then color(o, COLOR.objectiveDone) text = text .. " (" .. COMPLETE .. ")" else color(o, COLOR.text) end o:SetText(text) place(o, 0, seen == 1 and -10 or -2) end end -- Required money: black if short, grey otherwise local required = GetQuestLogRequiredMoney() or 0 if required > 0 then MoneyFrame_Update(d.money.purse:GetName(), required) if required > GetMoney() then color(d.money.text, { 0, 0, 0 }) if SetMoneyFrameColor then SetMoneyFrameColor(d.money.purse:GetName(), "red") end else color(d.money.text, { 0.2, 0.2, 0.2 }) if SetMoneyFrameColor then SetMoneyFrameColor(d.money.purse:GetName(), "white") end end place(d.money, 0, 0, 28) else d.money:Hide() end -- Suggested group local group = GetQuestLogGroupNum() or 0 if group > 0 then d.group:SetText(format(QUEST_SUGGESTED_GROUP_NUM, group)) place(d.group, 0, -10) else d.group:Hide() end place(d.descriptionHeader, 0, -20) d.description:SetText(description) place(d.description, 0, -5) -- Rewards (SetRewardsHeight): their height plus 62, or 59 when empty; the space at the -- bottom of the text leaves them the same room local list = showRewards(d) local height if list then d.list:Show() height = list + D.rewardExtra else d.list:Hide() height = D.rewardEmpty end d.rewards:SetHeight(height) placeRewardsBackground(d.rewards, height) if d.list.rows - d.list.headers > 0 then d.rewards.caption:Show() else d.rewards.caption:Hide() end d.space:SetHeight(height) place(d.space, 0, 0, height) c:SetHeight(math.max(total, 1)) d.scrollRange = math.max(0, total - D.textH) if d.bar then local rows = math.ceil(total / D.step) local visibleCount = math.floor(D.textH / D.step) local offset = start and 0 or math.min(d.bar.offset or 0, math.max(0, rows - visibleCount)) d.bar:Configure(rows, visibleCount, offset) d.scroll:SetVerticalScroll(math.min(offset * D.step, d.scrollRange)) else d.scroll:SetVerticalScroll(0) end J.adjustRewards() J.updateDetailsButtons() return true end function J.isDetailsOpen() return J.details and J.details:IsShown() and true or false end -- QuestMapFrame_ShowQuestDetails: the map moves to the quest (keeping the one we leave), -- the list hides, the page shows. function J.openDetails(info) local d = J.details if not d or not info or not info.questID then showOnMap(info) return end -- 3.3.5: GetCurrentMapAreaID() is SetMapByID's ID + 1, and 0 on the views Blizzard restores -- with SetMapZoom (WorldMapFrame_ToggleWindowSize) d.returnMap = GetCurrentMapAreaID and GetCurrentMapAreaID() - 1 d.returnContinent = GetCurrentMapContinent and GetCurrentMapContinent() d.returnFloor = GetCurrentMapDungeonLevel and GetCurrentMapDungeonLevel() d.map = nil d.questID = info.questID showOnMap(info) d.map = GetCurrentMapAreaID and GetCurrentMapAreaID() StaticPopup_Hide("ABANDON_QUEST") StaticPopup_Hide("ABANDON_QUEST_WITH_ITEMS") -- Shown before it is filled: texts are measured only when visible J.pane.list:Hide() if J.pane.bar then J.pane.bar:Hide() end d:Show() if not showDetails(true) then J.closeDetails() end end -- QuestMapFrame_CloseQuestDetails function J.closeDetails() local d = J.details if not d then return end d:Hide() d.questID, d.index, d.map, d.returnMap, d.returnFloor = nil, nil, nil, nil, nil d.returnContinent = nil d.clock.rest = nil J.pane.list:Show() StaticPopup_Hide("ABANDON_QUEST") StaticPopup_Hide("ABANDON_QUEST_WITH_ITEMS") J.update() end -- QuestMapFrame_ReturnFromQuestDetails: the map goes back where it was function J.backFromDetails() local d = J.details if not d then return end local map, floor = d.returnMap, d.returnFloor if map and map >= 0 and SetMapByID then SetMapByID(map) if floor and floor > 0 and SetDungeonMapLevel then SetDungeonMapLevel(floor) end elseif d.returnContinent and SetMapZoom then SetMapZoom(d.returnContinent) end J.closeDetails() end -- QuestLogMixin:Refresh and QuestMapFrame_UpdateAll: the page follows the log; a quest that -- leaves it returns to the list; a map that is no longer the quest's closes the page. function J.updateDetails() local d = J.details if not J.isDetailsOpen() then return end if d.map and GetCurrentMapAreaID and GetCurrentMapAreaID() ~= d.map then J.closeDetails() return end if not showDetails(false) then J.backFromDetails() end end -- ------------------------------------------------------------- pane -- Builds the quest log pane on the right of the map local function createPane(map) local v = CreateFrame("Frame", "ForeverUIQuestLogPanel", map) v:SetFrameStrata("HIGH") v:EnableMouse(true) v:SetWidth(V.width) v:SetPoint("TOPRIGHT", map, "TOPRIGHT", V.x, V.top) v:SetPoint("BOTTOMRIGHT", map, "BOTTOMRIGHT", V.x, V.down) -- QuestsFrame, then QuestScrollFrame local list = CreateFrame("ScrollFrame", "ForeverUIQuestScrollFrame", v) list:SetPoint("TOPLEFT", v, "TOPLEFT", 0, V.listTop) list:SetPoint("BOTTOMRIGHT", v, "BOTTOMRIGHT", V.listRight, V.listBottom) v.list = list local background = list:CreateTexture(nil, "BACKGROUND") ForeverUI.SetAtlas(background, "questlog-main-background-2x") background:SetPoint("TOPLEFT", list, "TOPLEFT", 0, 0) v.background = background local empty = list:CreateFontString(nil, "ARTWORK") empty:SetFont(FONT, 16) empty:SetWidth(250) empty:SetPoint("CENTER", list, "CENTER", 0, 50) empty:SetText(TEXT.empty) empty:Hide() v.empty = empty local none = list:CreateFontString(nil, "ARTWORK") none:SetFont(FONT, 14) none:SetWidth(250) none:SetPoint("TOP", list, "TOP", 0, -30) none:SetText(TEXT.noResults) none:Hide() v.none = none local content = CreateFrame("Frame", "ForeverUIQuestScrollContents", list) content:SetWidth(304) content:SetHeight(454) list:SetScrollChild(content) v.content = content -- Border: questlog-frame, filigree on top, shadow at the bottom local edge = CreateFrame("Frame", nil, list) edge:SetPoint("TOPLEFT", list, "TOPLEFT", -3, 7) edge:SetPoint("BOTTOMRIGHT", list, "BOTTOMRIGHT", 3, -6) v.edge = edge edge:SetFrameLevel(list:GetFrameLevel() + 20) -- camelot nine-slices questlog-frame (UiTextureAtlasElementSliceData.db2: 53 on each side -- of a 107 x 107 element); stretched in one piece its mouldings are distorted. if ForeverUI.CreateNineSlice then ForeverUI.CreateNineSlice(edge, "questlog-frame", 53, { 0, 0, 0, 0 }, "BORDER") end local filigree = edge:CreateTexture(nil, "ARTWORK") ForeverUI.SetAtlas(filigree, "questlog-frame-filigree") filigree:SetPoint("TOP", edge, "TOP", 0, 1) local shadow = edge:CreateTexture(nil, "BACKGROUND") ForeverUI.SetAtlas(shadow, "questlog-frame-gradient-bottom") shadow:SetPoint("BOTTOM", edge, "BOTTOM", 0, 4) v.shadow = shadow -- Scroll bar, in steps of 16 local bar = ForeverUI.CreateScrollBar and ForeverUI.CreateScrollBar("ForeverUIQuestScrollBar", v, list) if bar then bar:ClearAllPoints() bar:SetPoint("TOPLEFT", list, "TOPRIGHT", V.barX, V.barTop) bar:SetPoint("BOTTOMLEFT", list, "BOTTOMRIGHT", V.barX, V.barBottom) bar:SetFrameLevel(edge:GetFrameLevel() + 1) bar.onScroll = function(step) list:SetVerticalScroll(step * V.step) J.updateShadow() end v.bar = bar end list:EnableMouseWheel(true) list:SetScript("OnMouseWheel", function(_, direction) if v.bar and v.bar:IsShown() then v.bar:MoveTo(v.bar.offset - direction * 3) end end) -- Search box (SearchBoxTemplate) local r = CreateFrame("EditBox", "ForeverUIQuestSearchBox", list) r:SetWidth(V.searchW) r:SetHeight(V.searchH) r:SetPoint("BOTTOMLEFT", list, "TOPLEFT", V.searchX, V.searchY) r:SetAutoFocus(false) r:SetMaxLetters(60) r:SetFontObject("GameFontHighlightSmall") r:SetTextInsets(16, 20, 0, 0) local function border(parent, prefix) local g = parent:CreateTexture(nil, "BACKGROUND") ForeverUI.SetAtlas(g, prefix .. "-left", true) g:SetWidth(8) g:SetHeight(20) g:SetPoint("LEFT", parent, "LEFT", -5, 0) local d = parent:CreateTexture(nil, "BACKGROUND") ForeverUI.SetAtlas(d, prefix .. "-right", true) d:SetWidth(8) d:SetHeight(20) d:SetPoint("RIGHT", parent, "RIGHT", 0, 0) local m = parent:CreateTexture(nil, "BACKGROUND") ForeverUI.SetAtlas(m, prefix .. "-middle", true) m:SetPoint("TOPLEFT", g, "TOPRIGHT") m:SetPoint("BOTTOMRIGHT", d, "BOTTOMLEFT") return g, m, d end border(r, "common-search-border") local magnifier = r:CreateTexture(nil, "OVERLAY") ForeverUI.SetAtlas(magnifier, "common-search-magnifyingglass", true) magnifier:SetWidth(10) magnifier:SetHeight(10) magnifier:SetPoint("LEFT", r, "LEFT", 1, -1) local instruction = r:CreateFontString(nil, "ARTWORK", "GameFontDisableSmall") instruction:SetPoint("TOPLEFT", r, "TOPLEFT", 16, 0) instruction:SetPoint("BOTTOMRIGHT", r, "BOTTOMRIGHT", -20, 0) instruction:SetJustifyH("LEFT") instruction:SetTextColor(0.35, 0.35, 0.35) instruction:SetText(TEXT.search) local clear = CreateFrame("Button", nil, r) clear:SetWidth(17) clear:SetHeight(17) clear:SetPoint("RIGHT", r, "RIGHT", -3, 0) local closeButton = clear:CreateTexture(nil, "ARTWORK") ForeverUI.SetAtlas(closeButton, "common-search-clearbutton", true) closeButton:SetWidth(10) closeButton:SetHeight(10) closeButton:SetPoint("CENTER", clear, "CENTER", 0, 0) closeButton:SetAlpha(0.5) clear:SetScript("OnEnter", function() closeButton:SetAlpha(1) end) clear:SetScript("OnLeave", function() closeButton:SetAlpha(0.5) end) clear:SetScript("OnClick", function() r:SetText("") r:ClearFocus() end) clear:Hide() r:SetScript("OnEscapePressed", function(self) self:ClearFocus() end) r:SetScript("OnEnterPressed", function(self) self:ClearFocus() end) r:SetScript("OnEditFocusGained", function() instruction:Hide() end) r:SetScript("OnEditFocusLost", function(self) if self:GetText() == "" then instruction:Show() end end) r:SetScript("OnTextChanged", function(self) local t = self:GetText() if t == "" then clear:Hide() else clear:Show() instruction:Hide() end J.find(t) end) v.search = r -- Counter (InputBoxVisualTemplate) local counter = CreateFrame("Frame", "ForeverUIQuestLogCount", list) counter:SetWidth(V.counterW) counter:SetHeight(V.counterH) counter:SetPoint("TOPLEFT", r, "TOPRIGHT", V.counterX, 0) local _, _, right = border(counter, "common-search-border") local counterText = counter:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") counterText:SetPoint("TOPRIGHT", right, "TOPRIGHT", -5, -5) counter.text = counterText -- The daily quest counter (WotLK only) has no place in camelot: it lives in the counter's -- tooltip. counter:EnableMouse(true) counter:SetScript("OnEnter", function(self) GameTooltip:SetOwner(self, "ANCHOR_BOTTOMRIGHT") GameTooltip:SetText(format(QUEST_LOG_DAILY_COUNT_TEMPLATE, GetDailyQuestsCompleted(), GetMaxDailyQuests())) if QUEST_LOG_DAILY_TOOLTIP then GameTooltip:AddLine(format(QUEST_LOG_DAILY_TOOLTIP, GetMaxDailyQuests(), SecondsToTime(GetQuestResetTime(), nil, 1)), 1, 1, 1, true) end GameTooltip:Show() end) counter:SetScript("OnLeave", function() GameTooltip:Hide() end) v.counter = counter -- Settings: one check box, objectives local setting = CreateFrame("Button", "ForeverUIQuestSettingsButton", list) setting:SetWidth(V.settingW) setting:SetHeight(V.settingH) setting:SetPoint("TOPRIGHT", list, "TOPRIGHT", V.settingX, V.settingY) local icon = setting:CreateTexture(nil, "ARTWORK") ForeverUI.SetAtlas(icon, "questlog-icon-setting") icon:SetPoint("CENTER", setting, "CENTER", 0, 0) local hover = setting:CreateTexture(nil, "HIGHLIGHT") ForeverUI.SetAtlas(hover, "questlog-icon-setting") hover:SetPoint("CENTER", setting, "CENTER", 0, 0) hover:SetBlendMode("ADD") hover:SetAlpha(0.4) setting:SetScript("OnMouseDown", function() icon:SetPoint("CENTER", setting, "CENTER", 1, -1) end) setting:SetScript("OnMouseUp", function() icon:SetPoint("CENTER", setting, "CENTER", 0, 0) end) setting:SetScript("OnClick", function(self) menu(self, { { text = TEXT.showObjectives, checked = settings().objectives, keepShownOnClick = 1, func = function() settings().objectives = not settings().objectives J.update() end, } }) end) v.setting = setting v.headers, v.quests, v.objectives = {}, {}, {} J.details = createDetails(v) v:Hide() return v end -- --------------------------------------------------------------- rows -- Header background. The modern client nine-slices common-button-list-collapseExpand -- (18 left, 18 right, tiled center); stretched in one piece over 289 its rounded ends and -- center grain are distorted. So the two ends keep their width and the center (28 of 64 -- texels) is tiled, the last tile clipped: 3.3.5 cannot repeat part of a sheet. -- layer: draw layer; mode, alpha: optional blend mode and alpha local HEADER_SLICE = { side = 18, image = 64 } local function placeHeaderBackground(b, layer, mode, alpha) local e = ForeverUI.AtlasEntry("common-button-list-collapseexpand") if not e then return end local du = (e[3] - e[2]) / HEADER_SLICE.image local side = HEADER_SLICE.side local middle = HEADER_SLICE.image - 2 * side local function piece(u1, u2) local t = b:CreateTexture(nil, layer) t:SetTexture(e[1]) t:SetTexCoord(u1, u2, e[4], e[5]) if mode then t:SetBlendMode(mode) end if alpha then t:SetAlpha(alpha) end return t end local left = piece(e[2], e[2] + side * du) left:SetWidth(side) left:SetPoint("TOPLEFT", b, "TOPLEFT", 0, 0) left:SetPoint("BOTTOMLEFT", b, "BOTTOMLEFT", 0, 0) local right = piece(e[3] - side * du, e[3]) right:SetWidth(side) right:SetPoint("TOPRIGHT", b, "TOPRIGHT", 0, 0) right:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", 0, 0) local rest = V.headerW - 2 * side local x = 0 while rest > 0 do local l = math.min(middle, rest) local u1 = e[2] + side * du local t = piece(u1, u1 + l * du) t:SetWidth(l) t:SetPoint("TOPLEFT", left, "TOPRIGHT", x, 0) t:SetPoint("BOTTOMLEFT", left, "BOTTOMRIGHT", x, 0) x = x + l rest = rest - l end end local function createHeader(v, i) local b = CreateFrame("Button", "ForeverUIQuestLogHeader" .. i, v.content) b:SetWidth(V.headerW) b:SetHeight(V.headerH) b:RegisterForClicks("LeftButtonUp", "RightButtonUp") -- ListHeaderVisualTemplate NormalTexture and HighlightTexture: the background in -- BACKGROUND, and the same in ADD at 0.4 in the HIGHLIGHT layer, drawn only on hover placeHeaderBackground(b, "BACKGROUND") placeHeaderBackground(b, "HIGHLIGHT", "ADD", 0.4) local plus = CreateFrame("Button", nil, b) plus:SetWidth(20) plus:SetHeight(20) plus:SetPoint("RIGHT", b, "RIGHT", -6, 0) plus:EnableMouse(false) plus.Icon = plus:CreateTexture(nil, "ARTWORK") plus.Icon:SetPoint("CENTER", plus, "CENTER", 0, 0) b.plus = plus local text = b:CreateFontString(nil, "OVERLAY") text:SetFont(FONT, 15) text:SetShadowOffset(1, -1) text:SetShadowColor(0, 0, 0, 1) text:SetJustifyH("LEFT") text:SetPoint("LEFT", b, "LEFT", 8, 0) text:SetPoint("RIGHT", plus, "LEFT", -4, 0) b.text = text b:SetScript("OnEnter", function(self) color(self.text, COLOR.headerHover) end) b:SetScript("OnLeave", function(self) color(self.text, COLOR.headerIdle) end) b:SetScript("OnMouseDown", function(self) self.text:SetPoint("LEFT", self, "LEFT", 9, -1) self.plus.Icon:SetPoint("CENTER", self.plus, "CENTER", 1, -1) end) b:SetScript("OnMouseUp", function(self) self.text:SetPoint("LEFT", self, "LEFT", 8, 0) self.plus.Icon:SetPoint("CENTER", self.plus, "CENTER", 0, 0) end) b:SetScript("OnClick", function(self, button) PlaySound("igMainMenuOptionCheckBoxOn") if button == "RightButton" then local name = self.info.title menu(self, { { text = TEXT.trackAll, func = function() trackHeader(name, true) end }, { text = TEXT.untrackAll, func = function() trackHeader(name, false) end }, }) elseif self.info.isCollapsed then ExpandQuestHeader(self.info.index) else CollapseQuestHeader(self.info.index) end end) return b end local function createQuest(v, i) local b = CreateFrame("Button", "ForeverUIQuestLogTitle" .. i, v.content) b:SetWidth(V.questW) b:RegisterForClicks("LeftButtonUp", "RightButtonUp") -- Track check box, 14 x 14, hit insets -10 local checkbox = CreateFrame("Button", nil, b) checkbox:SetWidth(V.checkboxSide) checkbox:SetHeight(V.checkboxSide) checkbox:SetPoint("TOPRIGHT", b, "TOPRIGHT", 0, V.checkboxY) checkbox:SetHitRectInsets(-10, -10, -10, -10) local square = checkbox:CreateTexture(nil, "BACKGROUND") ForeverUI.SetAtlas(square, "questlog-icon-ticksquare") square:SetPoint("CENTER", checkbox, "CENTER", 0, 0) local checkMark = checkbox:CreateTexture(nil, "ARTWORK") ForeverUI.SetAtlas(checkMark, "questlog-icon-checkmark-yellow") checkMark:SetPoint("CENTER", checkbox, "CENTER", 1, 1) local checkboxHover = checkbox:CreateTexture(nil, "HIGHLIGHT") ForeverUI.SetAtlas(checkboxHover, "questlog-icon-ticksquare") checkboxHover:SetPoint("CENTER", checkbox, "CENTER", 0, 0) checkboxHover:SetBlendMode("ADD") checkboxHover:SetAlpha(0.4) checkbox:SetScript("OnClick", function() PlaySound(IsQuestWatched(b.index) and "igMainMenuOptionCheckBoxOff" or "igMainMenuOptionCheckBoxOn") toggleTracking(b.index) end) b.checkbox, b.checkMark = checkbox, checkMark -- Highlight, under the text (BORDER layer) local glow = b:CreateTexture(nil, "BORDER") ForeverUI.SetAtlas(glow, "questlog-quest-glow-yellow") glow:Hide() b.glow = glow local text = b:CreateFontString(nil, "ARTWORK", "GameFontNormalLeft") text:SetPoint("TOPLEFT", b, "TOPLEFT", V.questTextX, V.questTextY) text:SetJustifyH("LEFT") b.text = text local tag = b:CreateFontString(nil, "ARTWORK", "GameFontNormalLeft") tag:SetPoint("RIGHT", checkbox, "LEFT", -4, 0) tag:SetPoint("TOP", text, "TOP", 0, 0) tag:Hide() b.tag = tag glow:SetPoint("TOP", text, "TOP", 0, 4) glow:SetPoint("BOTTOM", text, "BOTTOM", 0, -4) b:SetScript("OnEnter", function(self) local c = COLOR.difficultyHover[self.difficulty] color(self.text, c) color(self.tag, c) for _, o in ipairs(self.objectiveRows) do color(o.text, COLOR.objectiveHover) color(o.dash, COLOR.objectiveHover) end self.glow:Show() zone(self.info, true) tooltip(self) end) b:SetScript("OnLeave", function(self) local c = COLOR.difficulty[self.difficulty] color(self.text, c) color(self.tag, c) for _, o in ipairs(self.objectiveRows) do color(o.text, COLOR.objective) color(o.dash, COLOR.objective) end self.glow:Hide() zone(self.info, false) GameTooltip:Hide() end) b:SetScript("OnMouseDown", function(self) self.text:SetPoint("TOPLEFT", self, "TOPLEFT", V.questTextX + 1, V.questTextY - 1) end) b:SetScript("OnMouseUp", function(self) self.text:SetPoint("TOPLEFT", self, "TOPLEFT", V.questTextX, V.questTextY) end) b:SetScript("OnClick", function(self, button) if IsModifiedClick("CHATLINK") and ChatEdit_InsertLink and ChatEdit_InsertLink(GetQuestLink(self.index)) then return end PlaySound("igMainMenuOptionCheckBoxOn") if IsShiftKeyDown() then toggleTracking(self.index) elseif button == "RightButton" then questMenu(self) else J.openDetails(self.info) end end) b.objectiveRows = {} return b end local function createObjective(v, i) local o = CreateFrame("Frame", nil, v.content) o:SetWidth(V.objectiveW) local dash = o:CreateFontString(nil, "ARTWORK", "GameFontHighlight") dash:SetPoint("TOPLEFT", o, "TOPLEFT", 0, 0) dash:SetHeight(16) dash:SetText(QUEST_DASH) local text = o:CreateFontString(nil, "ARTWORK", "GameFontHighlight") text:SetWidth(V.objectiveTextW) text:SetJustifyH("LEFT") text:SetJustifyV("TOP") text:SetPoint("TOPLEFT", dash, "TOPRIGHT", 0, 0) o.dash, o.text = dash, text return o end -- Reuses or creates the n-th frame of a pool; factory(v, n) creates it local function acquire(v, list, n, factory) local f = list[n] if not f then f = factory(v, n) list[n] = f end f:ClearAllPoints() f:Show() return f end -- QuestLogQuests_AddQuestButton for 3.3.5; count: running counters and map markers local function fillQuest(v, b, info, count) b.info, b.index = info, info.index b.difficulty = difficultyKey(info.level) local c = COLOR.difficulty[b.difficulty] b.text:SetText(questTitle(info)) color(b.text, c) color(b.tag, c) local tagText = questTagText(info.questTag) if tagText then b.tag:SetText(tagText) b.tag:Show() b.text:SetPoint("RIGHT", b.tag, "LEFT", -4, 0) else b.tag:Hide() b.text:SetPoint("RIGHT", b.checkbox, "LEFT", -4, 0) end if IsQuestWatched(info.index) then b.checkMark:Show() else b.checkMark:Hide() end b.glow:Hide() local height = 8 + b.text:GetHeight() for _, o in ipairs(b.objectiveRows) do o:Hide() end b.objectiveRows = {} local function objective(text, previous) count.o = count.o + 1 local o = acquire(v, v.objectives, count.o, createObjective) o.text:SetText(text) color(o.text, COLOR.objective) color(o.dash, COLOR.objective) local h = o.text:GetHeight() o:SetHeight(h) if previous then o:SetPoint("TOPLEFT", previous, "BOTTOMLEFT", 0, -2) h = h + 2 else o:SetPoint("TOPLEFT", b.text, "BOTTOMLEFT", 0, -3) h = h + 3 end table.insert(b.objectiveRows, o) height = height + h return o end if not settings().objectives then height = height + 4 elseif info.isComplete and info.isComplete > 0 then objective(GetQuestLogCompletionText(info.index) or TEXT.ready) else local previous for i = 1, GetNumQuestLeaderBoards(info.index) do local text, _, finished = GetQuestLogLeaderBoard(i, info.index) if text and not finished then previous = objective(text, previous) end end local required = GetQuestLogRequiredMoney(info.index) local money = GetMoney() if required and required > money then objective(GetMoneyString(money) .. " / " .. GetMoneyString(required), previous) end end -- Marker: the shown map's one, if any. camelot POIButton is 20 x 20 at (6, -4); the WotLK -- one is 32 x 32, so it is centered at the same place. local r = count.pois and info.questID and count.pois[info.questID] if r and QuestPOI_DisplayButton then -- The client counts the buttons it creates per type (parent.poiTable) and -- QuestPOI_HideAllButtons hides 1 to that count: numbers follow the map, and a quest of a -- collapsed header leaves a gap, so the lower numbers are created first (hidden). for n = 1, r[2] - 1 do if not _G["poi" .. POI_PARENT .. r[1] .. "_" .. n] then QuestPOI_DisplayButton(POI_PARENT, r[1], n, 0):Hide() end end local poi = QuestPOI_DisplayButton(POI_PARENT, r[1], r[2], info.questID) poi:ClearAllPoints() poi:SetPoint("CENTER", b, "TOPLEFT", 6 + 10, -4 - 10) poi:SetFrameLevel(b:GetFrameLevel() + 2) poi.info = info poi:SetScript("OnClick", function(self) PlaySound("igMainMenuOptionCheckBoxOn") showOnMap(self.info) end) if selectedQuest() == info.questID and QuestPOI_SelectButton then QuestPOI_SelectButton(poi) end end -- Room for the quest marker (+6 in the source) height = height + 6 b:SetHeight(height) end -- QuestLogQuests_Update: everything is rebuilt on each pass. function J.update() local v = J.pane if not v or not v:IsShown() then return end for _, f in ipairs(v.headers) do f:Hide() end for _, f in ipairs(v.quests) do f:Hide() end for _, f in ipairs(v.objectives) do f:Hide() end local count = { e = 0, q = 0, o = 0, pois = mapPois() } if QuestPOI_HideAllButtons then QuestPOI_HideAllButtons(POI_PARENT) end local top = 0 local previous = nil -- "header", "quest" or nil local currentHeader = nil local queued = nil -- a header waiting for its first quest local function place(f, type, left) local gap if type == "header" then gap = (previous == nil and GAP.headerAfterNone) or (previous == "header" and GAP.headerAfterHeader) or GAP.headerAfterQuest else gap = (previous == "header" and GAP.questAfterHeader) or GAP.questAfterQuest end top = top + gap f:SetPoint("TOPLEFT", v.content, "TOPLEFT", left, -top) top = top + f:GetHeight() previous = type end local function placeHeader(info) count.e = count.e + 1 local e = acquire(v, v.headers, count.e, createHeader) e.info = info e.text:SetText(info.title) color(e.text, COLOR.headerIdle) ForeverUI.SetAtlas(e.plus.Icon, info.isCollapsed and "common-button-list-plus" or "common-button-list-minus") place(e, "header", V.headerX) end local questCount = 0 for i = 1, GetNumQuestLogEntries() do local info = entry(i) if info and info.isHeader then currentHeader = info -- A collapsed header hides its quests: show it as is. Expanded, it waits for a quest to -- show (QuestLogQuests_ShouldShowHeaderButton) if info.isCollapsed and not R.text then placeHeader(info) queued = nil else queued = info end elseif info then questCount = questCount + 1 if linkMatches(info) then if queued then placeHeader(queued) queued = nil end count.q = count.q + 1 local b = acquire(v, v.quests, count.q, createQuest) fillQuest(v, b, info, count) -- camelot 70450 QuestMapFrameOverrides.titleFrameLeftPadding: quest rows 4 px in place(b, "quest", QUEST_LEFT_PADDING) end end end v.content:SetHeight(math.max(top, 1)) -- Background: empty or normal, clipped to the frame height local isEmpty = (count.q == 0 and count.e == 0) -- Double-density variant: the 1x one is upscaled and pixelated on high-resolution screens. local atlas = (isEmpty and not R.text) and "questlog-empty-quest-background-2x" or "questlog-main-background-2x" ForeverUI.SetAtlas(v.background, atlas) local e = ForeverUI.AtlasEntry(atlas) local hFrame = v.list:GetHeight() or 0 if e and hFrame > 0 and hFrame < e[7] then v.background:SetHeight(hFrame) v.background:SetTexCoord(e[2], e[3], e[4], e[4] + (e[5] - e[4]) * hFrame / e[7]) end if isEmpty and not R.text then v.empty:Show() else v.empty:Hide() end if isEmpty and R.text then v.none:Show() else v.none:Hide() end if isEmpty and not R.text then v.search:EnableMouse(false) else v.search:EnableMouse(true) end -- camelot counter, with the WotLK maximum local _, quests = GetNumQuestLogEntries() quests = quests or questCount local red = quests > MAX_QUESTLOG_QUESTS v.counter.text:SetText(format(TEXT.counter, red and RED_FONT_COLOR_CODE or "|cffffffff", quests, MAX_QUESTLOG_QUESTS)) -- Scroll bar if v.bar then local total = math.ceil(top / V.step) local visibleCount = math.floor(hFrame / V.step) local offset = math.min(v.bar.offset or 0, math.max(0, total - visibleCount)) v.bar:Configure(total, visibleCount, offset) v.list:SetVerticalScroll(offset * V.step) end -- Border and background follow the bar: with the bar hidden they extend over its 22 pixels -- to the pane edge (the border's right edge then overflows by 3, mirroring its left edge); -- with the bar shown they keep camelot's size. local position = (v.bar and v.bar:IsShown()) and 0 or -V.listRight v.edge:ClearAllPoints() v.edge:SetPoint("TOPLEFT", v.list, "TOPLEFT", -3, 7) v.edge:SetPoint("BOTTOMRIGHT", v.list, "BOTTOMRIGHT", 3 + position, -6) if e then v.background:SetWidth(e[6] + position) end J.updateShadow() -- A details page is open: the list stays hidden, and its bar too if J.isDetailsOpen() and v.bar then v.bar:Hide() end end -- The bottom shadow fades as the end nears function J.updateShadow() local v = J.pane if not v then return end local scrollRange = math.max(0, v.content:GetHeight() - (v.list:GetHeight() or 0)) local scroll = v.list:GetVerticalScroll() or 0 local h = v.shadow:GetHeight() or 1 local alpha = (scrollRange - scroll) / (h > 0 and h or 1) if alpha < 0 then alpha = 0 elseif alpha > 1 then alpha = 1 end v.shadow:SetAlpha(alpha) end -- ------------------------------------------------------ show, hide function J.isOpen() return J.pane and J.pane:IsShown() end -- Called by WorldMap.lua when it places the small window: the pane follows the saved state -- (questLogOpen). function J.place() local v = J.pane if not v then return end -- Child of WorldMapFrame, which has the map's scale: the inverse brings it back to UI units v:SetScale(ForeverUI.WorldMap and ForeverUI.WorldMap.inverse or 1) if settings().pane then v:Show() else v:Hide() end J.update() end function J.hide() if J.pane then J.pane:Hide() end end -- HandleUserActionToggleSidePanel function J.togglePane() settings().pane = not settings().pane if ForeverUI.WorldMap and ForeverUI.WorldMap.layoutWindowed then ForeverUI.WorldMap.layoutWindowed() end end -- L key: HandleUserActionToggleQuestLog. Map open with the pane: close it; otherwise open -- it as a small window with the pane. WotLK keeps its size mode between openings; from full -- screen, its own function switches back to the small window. function J.toggleQuestLog() if WorldMapFrame:IsShown() and J.isOpen() then HideUIPanel(WorldMapFrame) return end -- Showing QuestLogFrame may have closed the map first: in movable mode the map is a -- "center" panel that gives way (UIParent.lua:1355). Closed in this same frame with its -- pane means L meant to close it. if J.mapClosed and J.closedWithPane then J.mapClosed = nil return end settings().pane = true if WORLDMAP_SETTINGS.size ~= WORLDMAP_WINDOWED_SIZE then if WorldMapFrame:IsShown() then WorldMapFrame_ToggleWindowSize() return end WorldMap_ToggleSizeDown() end if WorldMapFrame:IsShown() then if ForeverUI.WorldMap and ForeverUI.WorldMap.layoutWindowed then ForeverUI.WorldMap.layoutWindowed() end else ShowUIPanel(WorldMapFrame) end end -- QuestMapFrame_OpenToQuestDetails, called by the quest tracker: map as a small window, -- pane open, then the quest page. function J.openPage(index) local info = entry(index) if not info or info.isHeader then return end settings().pane = true if WORLDMAP_SETTINGS.size ~= WORLDMAP_WINDOWED_SIZE then if WorldMapFrame:IsShown() then WorldMapFrame_ToggleWindowSize() else WorldMap_ToggleSizeDown() end end if WorldMapFrame:IsShown() then if ForeverUI.WorldMap and ForeverUI.WorldMap.layoutWindowed then ForeverUI.WorldMap.layoutWindowed() end else ShowUIPanel(WorldMapFrame) end J.openDetails(info) end -- -------------------------------------------------------------- setup local function build() if J.pane or not WorldMapFrame then return J.pane end J.pane = createPane(WorldMapFrame) return J.pane end J.build = build -- QuestLogFrame stays in place, invisible: its functions and OnShow work for addons, but -- showing it opens the pane. The hook runs after ShowUIPanel and hides it in the same -- frame, so it is never drawn. -- The hook's flag lasts until the next frame. Not GetTime(): time advances during a frame -- (opening the map stalls it almost a second), and the fallback below would then toggle the -- log again and close the map just opened. local flagClearer = CreateFrame("Frame") flagClearer:Hide() flagClearer:SetScript("OnUpdate", function(self) J.handled, J.mapClosed = nil, nil self:Hide() end) J.flagClearer = flagClearer if hooksecurefunc then hooksecurefunc("ShowUIPanel", function(frame) if frame and frame == QuestLogFrame and QuestLogFrame:IsShown() then J.handled = true flagClearer:Show() HideUIPanel(QuestLogFrame) J.toggleQuestLog() end end) -- Showing QuestLogFrame can fail silently: with the map maximized (a "full" panel), the -- panel manager refuses any other panel (FramePositionDelegate:ShowUIPanel, -- UIParent.lua:1341) and the hook above is not called. ToggleFrame(QuestLogFrame) is the -- only path of the micro button and L: if it did not reach the hook (no flag), take over. hooksecurefunc("ToggleFrame", function(frame) if frame and frame == QuestLogFrame then if J.handled then J.handled = nil elseif not QuestLogFrame:IsShown() then J.toggleQuestLog() end end end) end -- On entering the world camelot expands the current zone and collapses the others -- (QuestMapFrame_ResetFilters). 3.3.5 does not say which header is "on the map": it is the -- one named after the zone. local function collapseByZone() local zone = GetRealZoneText and GetRealZoneText() if not zone or zone == "" then return end for i = GetNumQuestLogEntries(), 1, -1 do local info = entry(i) if info and info.isHeader then if info.title == zone then if info.isCollapsed then ExpandQuestHeader(i) end elseif not info.isCollapsed then CollapseQuestHeader(i) end end end end J.collapseByZone = collapseByZone -- WotLK rebuilds its markers on each map or quest change: the list takes their numbers. if hooksecurefunc and WorldMapFrame_UpdateQuests then hooksecurefunc("WorldMapFrame_UpdateQuests", function() if J.isOpen() then J.update() J.updateDetails() end end) end if WorldMapFrame and WorldMapFrame.HookScript then WorldMapFrame:HookScript("OnHide", function() -- A flag cleared on the next frame (not GetTime: see above) J.mapClosed = true J.closedWithPane = J.isOpen() and true or false J.flagClearer:Show() end) end -- The pane is a child of the map and keeps its own shown flag when the map closes: the map is -- checked too (J.place and the WorldMapFrame_UpdateQuests hook rebuild it on opening), or -- every quest event rebuilds a hidden pane, and a page whose quest left the log moves the -- closed map (J.backFromDetails). Quest events come in bursts: one refresh on the next frame. local refresher = CreateFrame("Frame") refresher:Hide() refresher:SetScript("OnUpdate", function(self) self:Hide() if J.isOpen() and WorldMapFrame:IsShown() then J.update() J.updateDetails() end end) local listener = CreateFrame("Frame") listener:RegisterEvent("QUEST_LOG_UPDATE") listener:RegisterEvent("UNIT_QUEST_LOG_CHANGED") listener:RegisterEvent("PARTY_MEMBERS_CHANGED") listener:RegisterEvent("QUEST_WATCH_UPDATE") listener:RegisterEvent("PLAYER_ENTERING_WORLD") listener:SetScript("OnEvent", function(_, event) if event == "PLAYER_ENTERING_WORLD" then if not J.alreadyCollapsed then J.alreadyCollapsed = true collapseByZone() end end if J.isOpen() and WorldMapFrame:IsShown() then refresher:Show() end end) build() ForeverUI.QuestLogDebug = function() local v = J.pane local prefix = "|cff66ccffForeverUI|r " if not v then DEFAULT_CHAT_FRAME:AddMessage(prefix .. L.QUESTLOG_DEBUG_NOT_BUILT) return end local headers, quests = 0, 0 for _, f in ipairs(v.headers) do if f:IsShown() then headers = headers + 1 end end for _, f in ipairs(v.quests) do if f:IsShown() then quests = quests + 1 end end DEFAULT_CHAT_FRAME:AddMessage(prefix .. string.format( L.QUESTLOG_DEBUG_STATE, v:IsShown() and L.QUESTLOG_DEBUG_OPEN or L.QUESTLOG_DEBUG_CLOSED, v:GetWidth(), v:GetHeight(), headers, quests, tostring(R.text), tostring(settings().objectives))) end