From f5b34ab2fdcb97a0862283370b8d95acebdb3b30 Mon Sep 17 00:00:00 2001 From: Daniel <14241290+dnlxh@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:19:33 +0200 Subject: [PATCH] Add item level overlay to bag items Adds a small item level label to each equippable item in the player's bags. The label is coloured by item quality and updates automatically when bags change or items are upgraded. Can be toggled in the QoL settings tab under the new BAGS section, with a cogwheel for font size and label position (Top Left / Top Right / Bottom Left / Bottom Right). --- EUI_QoL.lua | 154 ++++++++++++++++++++++++++++++++++++++- EUI__General_Options.lua | 134 +++++++++++++++++++++++++++++++++- 2 files changed, 283 insertions(+), 5 deletions(-) diff --git a/EUI_QoL.lua b/EUI_QoL.lua index e6fe3e5a..1b15b5a3 100644 --- a/EUI_QoL.lua +++ b/EUI_QoL.lua @@ -728,6 +728,8 @@ qolFrame:SetScript("OnEvent", function(self) local function GetApplicantScore(applicantID) if not C_LFGList or not C_LFGList.GetApplicantMemberInfo then return nil end local _, _, _, _, _, _, _, _, _, _, _, dungeonScore = C_LFGList.GetApplicantMemberInfo(applicantID, 1) + if dungeonScore == nil then return nil end + if issecretvalue and issecretvalue(dungeonScore) then return nil end if type(dungeonScore) ~= "number" then return nil end return dungeonScore end @@ -754,9 +756,9 @@ qolFrame:SetScript("OnEvent", function(self) table.sort(applicants, function(a, b) local sa = scores[a] local sb = scores[b] - if sa ~= nil and sb ~= nil and sa ~= sb then return sa > sb end - if sa ~= nil and sb == nil then return true end - if sa == nil and sb ~= nil then return false end + if sa and sb and sa ~= sb then return sa > sb end + if sa and not sb then return true end + if not sa and sb then return false end return (originalOrder[a] or 0) < (originalOrder[b] or 0) end) end) @@ -960,6 +962,152 @@ qolFrame:SetScript("OnEvent", function(self) return false end + --------------------------------------------------------------------------- + -- Bag Item Level Labels + -- Draws a small item-level number on every equippable item in the bag. + -- Setting: EllesmereUIDB.bagIlvlEnabled (default: true) + --------------------------------------------------------------------------- + do + -- Items worth showing a level on: anything that occupies a gear slot. + -- Bags, tabards, ammo pouches and cosmetics are intentionally excluded. + local function IsGearSlot(equipSlot, classID, subclassID) + if not equipSlot or equipSlot == "" then return false end + if equipSlot == "INVTYPE_NON_EQUIP_IGNORE" then return false end + if equipSlot == "INVTYPE_TABARD" then return false end + if equipSlot == "INVTYPE_BAG" then return false end + if equipSlot == "INVTYPE_QUIVER" then return false end + -- classID 4 = Armor, subclassID 5 = Cosmetic + if classID == 4 and subclassID == 5 then return false end + return true + end + + -- Pixel nudge per corner so the number sits just inside the icon edge. + local CORNER_OFFSET = { + TOPLEFT = { 1, -1 }, + TOPRIGHT = { -1, -1 }, + BOTTOMLEFT = { 1, 1 }, + BOTTOMRIGHT = { -1, 1 }, + } + + local function GetCorner() + return (EllesmereUIDB and EllesmereUIDB.bagIlvlAnchor) or "BOTTOMLEFT" + end + + local function GetSize() + return (EllesmereUIDB and EllesmereUIDB.bagIlvlFontSize) or 11 + end + + local function GetFace() + return (EllesmereUI and EllesmereUI.GetFont and EllesmereUI.GetFont()) + or STANDARD_TEXT_FONT + end + + local function IsActive() + return not (EllesmereUIDB and EllesmereUIDB.bagIlvlEnabled == false) + end + + -- Retrieve or create the FontString attached to a button. + local function GetOrCreateTag(btn) + if btn._euiIlvlTag then return btn._euiIlvlTag end + -- Use OVERLAY so the text renders above the item icon texture + -- but stays below Blizzard's own search-dimming overlay. + local tag = btn:CreateFontString(nil, "OVERLAY") + tag:SetFont(GetFace(), GetSize(), "THINOUTLINE") + tag:SetShadowOffset(1, -1) + tag:SetShadowColor(0, 0, 0, 0.9) + btn._euiIlvlTag = tag + return tag + end + + local function PaintButton(btn, bag, slot) + if not btn then return end + local tag = GetOrCreateTag(btn) + + if not IsActive() then tag:Hide(); return end + + local link = C_Container.GetContainerItemLink(bag, slot) + if not link then tag:Hide(); return end + + local _, _, quality, _, _, classID, subclassID, _, equipSlot = + C_Item.GetItemInfo(link) + + if not IsGearSlot(equipSlot, classID, subclassID) then + tag:Hide(); return + end + + -- GetCurrentItemLevel via ItemLocation is the only reliable way + -- to get the actual equipped/upgraded level rather than base level. + local loc = ItemLocation:CreateFromBagAndSlot(bag, slot) + local lvl = loc and C_Item.GetCurrentItemLevel(loc) + if not lvl or lvl <= 0 then tag:Hide(); return end + + -- Refresh font in case the user changed size in settings + tag:SetFont(GetFace(), GetSize(), "THINOUTLINE") + + local corner = GetCorner() + local off = CORNER_OFFSET[corner] or CORNER_OFFSET.BOTTOMLEFT + tag:ClearAllPoints() + tag:SetPoint(corner, btn, corner, off[1], off[2]) + + -- Colour by quality; grey fallback for unknown quality + if quality and quality >= 0 then + local r, g, b = C_Item.GetItemQualityColor(quality) + tag:SetTextColor(r, g, b, 1) + else + tag:SetTextColor(0.8, 0.8, 0.8, 1) + end + + tag:SetText(lvl) + tag:Show() + end + + local function ScanOpenBags() + if ContainerFrameCombinedBags and ContainerFrameCombinedBags:IsShown() then + for _, btn in ContainerFrameCombinedBags:EnumerateValidItems() do + PaintButton(btn, btn:GetBagID(), btn:GetID()) + end + end + for _, frame in ipairs((ContainerFrameContainer and + ContainerFrameContainer.ContainerFrames) or {}) do + if frame:IsShown() then + for _, btn in frame:EnumerateValidItems() do + PaintButton(btn, btn:GetBagID(), btn:GetID()) + end + end + end + end + + -- Event-driven refresh + local watchFrame = CreateFrame("Frame") + watchFrame:RegisterEvent("BAG_UPDATE_DELAYED") + watchFrame:RegisterEvent("ITEM_UPGRADE_MASTER_SET_ITEM") + watchFrame:RegisterEvent("PLAYER_ENTERING_WORLD") + watchFrame:RegisterEvent("BAG_OPEN") + watchFrame:SetScript("OnEvent", function(_, event) + if event == "PLAYER_ENTERING_WORLD" then + C_Timer.After(2, ScanOpenBags) + else + ScanOpenBags() + end + end) + + -- Refresh when any individual bag frame becomes visible + for _, frame in ipairs((ContainerFrameContainer and + ContainerFrameContainer.ContainerFrames) or {}) do + frame:HookScript("OnShow", function() + C_Timer.After(0.1, ScanOpenBags) + end) + end + if ContainerFrameCombinedBags then + ContainerFrameCombinedBags:HookScript("OnShow", function() + C_Timer.After(0.1, ScanOpenBags) + end) + end + + -- Expose a refresh handle for the options panel + EllesmereUI._refreshBagIlvl = ScanOpenBags + end + local resetAnnounceFrame = CreateFrame("Frame") resetAnnounceFrame:RegisterEvent("CHAT_MSG_SYSTEM") resetAnnounceFrame:SetScript("OnEvent", function(self, event, msg) diff --git a/EUI__General_Options.lua b/EUI__General_Options.lua index e3eb3dd1..ceb71046 100644 --- a/EUI__General_Options.lua +++ b/EUI__General_Options.lua @@ -1616,7 +1616,7 @@ initFrame:SetScript("OnEvent", function(self) local function CreateFPSCounter() if fpsFrame then return end local FONT = EllesmereUI.GetFontPath("extras") - local FONT_SIZE = 12 + local FONT_SIZE = (EllesmereUIDB and EllesmereUIDB.fpsTextSize) or 12 local LABEL_SIZE = FONT_SIZE - 2 local SHADOW_X, SHADOW_Y = 1, -1 fpsFrame = CreateFrame("Frame", "EUI_FPSCounter", UIParent) @@ -1742,6 +1742,16 @@ initFrame:SetScript("OnEvent", function(self) local shouldShow = EllesmereUIDB and EllesmereUIDB.showFPS if shouldShow then CreateFPSCounter() + -- Re-apply text size from DB + local sz = (EllesmereUIDB and EllesmereUIDB.fpsTextSize) or 12 + local lblSz = sz - 2 + local fp = EllesmereUI.GetFontPath("extras") + local outF = EllesmereUI.GetFontOutlineFlag() + if fpsFrame._text then fpsFrame._text:SetFont(fp, sz, outF) end + if fpsFrame._textWorld then fpsFrame._textWorld:SetFont(fp, sz, outF) end + if fpsFrame._textLocal then fpsFrame._textLocal:SetFont(fp, sz, outF) end + if fpsFrame._lblWorld then fpsFrame._lblWorld:SetFont(fp, lblSz, outF) end + if fpsFrame._lblLocal then fpsFrame._lblLocal:SetFont(fp, lblSz, outF) end -- Apply saved position and scale local pos = EllesmereUIDB and EllesmereUIDB.fpsPos if pos and pos.point then @@ -1918,7 +1928,8 @@ initFrame:SetScript("OnEvent", function(self) -- Font -- pull from the global "extras" font key local fontPath = EllesmereUI.GetFontPath("extras") - fs:SetFont(fontPath, 18, EllesmereUI.GetFontOutlineFlag()) + local durSz = (EllesmereUIDB and EllesmereUIDB.durWarnTextSize) or 30 + fs:SetFont(fontPath, durSz, EllesmereUI.GetFontOutlineFlag()) -- Color local c = EllesmereUIDB and EllesmereUIDB.durWarnColor @@ -1962,6 +1973,7 @@ initFrame:SetScript("OnEvent", function(self) CreateDurabilityWarning() durWarnOverlay._applySettings() end + EllesmereUI._durWarnApplySettings = EllesmereUI._applyDurWarn -- Preview: show durability warning at its configured position EllesmereUI._durWarnPreview = function() @@ -2465,6 +2477,16 @@ initFrame:SetScript("OnEvent", function(self) local _, fpsCogShow = EllesmereUI.BuildCogPopup({ title = "FPS Counter Settings", rows = { + { type="slider", label="Text Size", + min=8, max=24, step=1, + get=function() + return (EllesmereUIDB and EllesmereUIDB.fpsTextSize) or 12 + end, + set=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.fpsTextSize = v + if EllesmereUI._applyFPSCounter then EllesmereUI._applyFPSCounter() end + end }, { type="toggle", label="Show Local MS", get=function() if not EllesmereUIDB or EllesmereUIDB.fpsShowLocalMS == nil then return true end @@ -2729,6 +2751,16 @@ initFrame:SetScript("OnEvent", function(self) local _, durCogShow = EllesmereUI.BuildCogPopup({ title = "Durability Settings", rows = { + { type="slider", label="Text Size", + min=10, max=50, step=1, + get=function() + return (EllesmereUIDB and EllesmereUIDB.durWarnTextSize) or 30 + end, + set=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.durWarnTextSize = v + if EllesmereUI._durWarnApplySettings then EllesmereUI._durWarnApplySettings() end + end }, { type="slider", label="Y-Offset", min=-600, max=600, step=1, get=function() @@ -3342,6 +3374,104 @@ initFrame:SetScript("OnEvent", function(self) end } ); y = y - h + --------------------------------------------------------------------------- + -- BAGS + --------------------------------------------------------------------------- + _, h = W:SectionHeader(parent, "BAGS", y); y = y - h + + local ilvlRow + ilvlRow, h = W:DualRow(parent, y, + { type="toggle", text="Show Item Level in Bags", + tooltip="Displays the current item level as a small label on each equippable item in your bags. The label colour matches the item quality.", + getValue=function() + return not (EllesmereUIDB and EllesmereUIDB.bagIlvlEnabled == false) + end, + setValue=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.bagIlvlEnabled = v and true or false + if EllesmereUI._refreshBagIlvl then EllesmereUI._refreshBagIlvl() end + EllesmereUI:RefreshPage() + end }, + { type="label", text="" } + ); y = y - h + + -- Cog on the item-level toggle (position & font-size sub-settings) + do + local leftRgn = ilvlRow._leftRegion + local function ilvlOff() + return EllesmereUIDB and EllesmereUIDB.bagIlvlEnabled == false + end + + -- Pre-declare the values table as upvalue so the CogPopup closure + -- captures a fully-initialised table (inline literals can arrive nil + -- if the popup is created lazily before the table is resolved). + local posValues = { + TOPLEFT = "Top Left", + TOPRIGHT = "Top Right", + BOTTOMLEFT = "Bottom Left", + BOTTOMRIGHT= "Bottom Right", + } + local posOrder = { "TOPLEFT", "TOPRIGHT", "BOTTOMLEFT", "BOTTOMRIGHT" } + + local _, ilvlCogShow = EllesmereUI.BuildCogPopup({ + title = "Bag Item Level Settings", + rows = { + { type="dropdown", + label="Position", + values=posValues, + order=posOrder, + get=function() + return (EllesmereUIDB and EllesmereUIDB.bagIlvlAnchor) or "BOTTOMLEFT" + end, + set=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.bagIlvlAnchor = v + if EllesmereUI._refreshBagIlvl then EllesmereUI._refreshBagIlvl() end + end }, + { type="slider", + label="Font Size", + min=6, max=20, step=1, + get=function() + return (EllesmereUIDB and EllesmereUIDB.bagIlvlFontSize) or 11 + end, + set=function(v) + if not EllesmereUIDB then EllesmereUIDB = {} end + EllesmereUIDB.bagIlvlFontSize = v + if EllesmereUI._refreshBagIlvl then EllesmereUI._refreshBagIlvl() end + end }, + }, + }) + + local ilvlCogBtn = CreateFrame("Button", nil, leftRgn) + ilvlCogBtn:SetSize(26, 26) + ilvlCogBtn:SetPoint("RIGHT", leftRgn._lastInline or leftRgn._control, "LEFT", -9, 0) + leftRgn._lastInline = ilvlCogBtn + ilvlCogBtn:SetFrameLevel(leftRgn:GetFrameLevel() + 5) + ilvlCogBtn:SetAlpha(ilvlOff() and 0.15 or 0.4) + local ilvlCogTex = ilvlCogBtn:CreateTexture(nil, "OVERLAY") + ilvlCogTex:SetAllPoints() + ilvlCogTex:SetTexture(EllesmereUI.COGS_ICON) + ilvlCogBtn:SetScript("OnEnter", function(self) self:SetAlpha(0.7) end) + ilvlCogBtn:SetScript("OnLeave", function(self) self:SetAlpha(ilvlOff() and 0.15 or 0.4) end) + ilvlCogBtn:SetScript("OnClick", function(self) ilvlCogShow(self) end) + + local ilvlCogBlock = CreateFrame("Frame", nil, ilvlCogBtn) + ilvlCogBlock:SetAllPoints() + ilvlCogBlock:SetFrameLevel(ilvlCogBtn:GetFrameLevel() + 10) + ilvlCogBlock:EnableMouse(true) + ilvlCogBlock:SetScript("OnEnter", function() + EllesmereUI.ShowWidgetTooltip(ilvlCogBtn, EllesmereUI.DisabledTooltip("Show Item Level in Bags")) + end) + ilvlCogBlock:SetScript("OnLeave", function() EllesmereUI.HideWidgetTooltip() end) + + EllesmereUI.RegisterWidgetRefresh(function() + local off = ilvlOff() + ilvlCogBtn:SetAlpha(off and 0.15 or 0.4) + if off then ilvlCogBlock:Show() else ilvlCogBlock:Hide() end + end) + if ilvlOff() then ilvlCogBlock:Show() else ilvlCogBlock:Hide() end + end + _, h = W:Spacer(parent, y, 20); y = y - h return math.abs(y) end