Files
Adrestie ca2b78e217 Remove every debug command
/fui keeps edit mode, reset and list. The thirty diagnostic and tuning
functions behind the other subcommands (bar, bags, character, tabs, close,
model, minimap, mouse, map, questlog, chat, chatlines, tabard...) go with
them, with Panes.Report, the bags read-back and the 205 texts they alone used.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 13:39:35 +02:00

1330 lines
41 KiB
Lua

-- ForeverUI: bag windows in the Camelot style (mainline ContainerFrameMixin geometry).
-- 3.3.5 has no C_Container.SetItemSearch: search matches name, type and subtype in Lua and
-- veils the rest (searchOverlay, black at 80 %). Nor C_Container.SortBags: items move one by
-- one, ordered by GetAuctionItemClasses, then quality, name and stack size.
-- Bag window settings, from ContainerFrameMixin (mainline/containerframe.lua).
-- Height = rows x slot + (rows - 1) x cellGap + GetPaddingHeight() + CalculateExtraHeight(),
-- so the window follows its contents; the width is the constant CONTAINER_WIDTH.
local R = {
-- Grid
slot = 37, -- ContainerFrameItemButtonTemplate
cellGap = 5, -- ITEM_SPACING_X and ITEM_SPACING_Y
columns = 4, -- GetColumns()
width = 178, -- CONTAINER_WIDTH
-- Height
-- camelot uses 9 for GetFirstButtonOffsetY(); with its metal border drawn here that leaves
-- only 1.5 below the last row (7.5 left, 6 right). 6 more evens out the four sides.
firstButtonY = 15, -- camelot 9
firstButtonX = -7, -- GetInitialItemAnchor()
header = 48, -- GetPaddingHeight(): "titlebar and attic"
searchStrip = 30, -- same, + 30 on the backpack
-- Purse, and the backpack grid anchored to it
purseHeight = 13, -- UpdateMoneyFrame()
purseSide = 8, -- UpdateCurrencyFrames()
purseBottom = 14, -- camelot 8, raised by the same 6
purseGridGap = 4, -- ContainerFrameBackpackMixin:GetInitialItemAnchor()
purseFrame = 17, -- camelot's border overhangs the purse
-- Watched currencies strip, below the purse
-- ContainerFrameTokenWatcherMixin:UpdateCurrencyFrames: the strip takes the bottom
-- (BOTTOMLEFT (8, 8), BOTTOMRIGHT (-8, 8)) and the purse sits on its TOP, (0, 3).
-- CalculateExtraHeight adds the strip height. Tokens chain leftwards from GetInitialTokenAnchor.
tokenHeight = 17, -- BackpackTokenFrameTemplate
tokenSide = 8, -- UpdateCurrencyFrames()
tokenGap = 3, -- gap between the strip and the purse above it
tokenWidth = 50, -- BackpackTokenTemplate
tokenPiece = 12, -- its height, and the icon's
tokenIconX = 4,
tokenIconY = 1,
tokenOriginX = -17, -- GetInitialTokenAnchor()
tokenOriginY = -1,
-- Attic
-- Top-anchored, so the box and the sort button move down by the 6 the window grew.
fieldWidth = 96, -- SetSearchBoxPoint()
fieldHeight = 18,
fieldX = 42,
fieldY = -43, -- camelot -37
sortWidth = 28,
sortHeight = 26,
sortX = -9, -- UpdateSearchBox()
sortY = -40, -- camelot -34
-- Frame decorations
titleLeft = 35, -- SetTitleOffsets(35): left edge of the title
titleRight = -24, -- SetTitleOffsets default
titleContainer = -1, -- TitleContainer: TOPLEFT / TOPRIGHT at -1
titleStripHeight = 20, -- its height
titleText = -5, -- TitleText: TOP (0, -5) in the container
closeSize = 24,
closeX = 1,
closeY = 0,
-- Portrait: 3.3.5 does not mask icons; as for its minimap buttons, the ring's opaque metal
-- hides the corners of a square icon. camelot's ring is clear up to 10.3 px from the centre,
-- fades up to 14.3 and is opaque up to 20.4, so the side must be >= 28.6 (edges cover the
-- fade) and <= 28.8 (corners stay under the metal): 28.
portrait = 28,
portraitX = 14, -- centre of the ring, measured on its art
portraitY = -17, -- and centre of the source portrait
-- Bag stacking (UpdateContainerFrameAnchors)
bagGap = 8, -- CONTAINER_SPACING
columnGap = -11, -- column step
rightEdge = 10, -- GetInitialContainerFrameOffsetX, bars excluded
bottomEdge = 85, -- CONTAINER_OFFSET_Y
topMargin = 8, -- margin below the top of the screen (not in the source)
minScale = 0.75, -- CONTAINER_SCALE: bags shrink down to it when they do not fit
bankMargin = 25, -- the left column stays this far right of an open bank
-- Overall
scale = 1, -- 1 = camelot size; 1.25 = a quarter larger
}
ForeverUI = ForeverUI or {}
local FRAME_COUNT = NUM_CONTAINER_FRAMES or 13
local BAGS = { 0, 1, 2, 3, 4 } -- backpack and the four equipped bags
-- Lua 5.1 reads backslashes as escapes: the separator is built from its char code.
local SEP = string.char(92)
local SQUARE_HOVER = "Interface" .. SEP .. "Buttons" .. SEP .. "ButtonHilight-Square"
local QUALITY_FRAME = "Interface" .. SEP .. "ForeverUI" .. SEP .. "common" .. SEP .. "whiteiconframe"
-- UpdateMiscellaneousFrames: the backpack shows INV_Misc_Bag_08, the keyring its own icon,
-- an equipped bag its item icon. Icons are 64 px with a 4 px dark border.
local ICON_BORDER = 4 / 64
local BACKPACK_PORTRAIT = "Interface" .. SEP .. "Icons" .. SEP .. "INV_Misc_Bag_08"
-- camelot's "Interface/Icons/ui-hud-actionbar-keyring" does not exist in 3.3.5; this is the
-- client's keyring icon.
local KEYRING_PORTRAIT = "Interface" .. SEP .. "ContainerFrame" .. SEP .. "KeyRing-Bag-Icon"
-- camelot frames every slot, full or empty, in dark gray (0.39, measured on a screenshot).
-- The quality color takes over from uncommon.
local FRAME_GRAY = 0.39
local TINTED_QUALITY = 2
local L = ForeverUI.L
local CLEANUP_TEXT = L.BAGS_CLEANUP
local CLEANUP_TOOLTIP = L.BAGS_CLEANUP_TOOLTIP
ForeverUI = ForeverUI or {}
-- ------------------------------------------------------------ frames
local frames = {}
local function skinButton(button)
if not button or button.foreverSkinned then
return
end
local name = button:GetName()
-- SetItemButtonTexture_Base: an empty slot shows emptyBackgroundAtlas on the icon itself,
-- never as a second texture. The 3.3.5 slot art is hidden.
local normalFont = button:GetNormalTexture()
if normalFont then
normalFont:SetAlpha(0)
end
local hover = button:GetHighlightTexture()
if hover then
hover:SetTexture(SQUARE_HOVER)
hover:SetBlendMode("ADD")
hover:ClearAllPoints()
hover:SetAllPoints(button)
end
local icon = _G[name .. "IconTexture"]
if icon then
icon:ClearAllPoints()
icon:SetAllPoints(button)
icon:SetTexCoord(0, 1, 0, 1)
icon:SetDrawLayer("BORDER")
end
-- Item outline: WhiteIconFrame tinted by quality, as in SetItemButtonQuality_Base.
local outline = button:CreateTexture(nil, "OVERLAY")
outline:SetTexture(QUALITY_FRAME)
outline:SetAllPoints(button)
outline:SetVertexColor(FRAME_GRAY, FRAME_GRAY, FRAME_GRAY)
button.foreverOutline = outline
-- Search veil: black at 80 % over the whole button, as on the source ItemButton.
local veil = button:CreateTexture(nil, "OVERLAY")
veil:SetTexture(0, 0, 0, 0.8)
veil:SetAllPoints(button)
veil:Hide()
button.foreverVeil = veil
button.foreverSkinned = true
end
local function skinFrame(frame)
if not frame or frame.foreverSkinned then
return
end
local name = frame:GetName()
-- 3.3.5 background art: four stacked pieces plus the one-slot version.
-- ContainerFrame_GenerateFrame re-shows and retextures them on each open but keeps their alpha.
for _, suffix in ipairs({ "BackgroundTop", "BackgroundMiddle1", "BackgroundMiddle2",
"BackgroundBottom", "Background1Slot" }) do
local texture = _G[name .. suffix]
if texture then
texture:SetAlpha(0)
end
end
ForeverUI.SetPanelArt(frame)
-- The client portrait is replaced by an icon cut round by the ring, not masked.
local old = _G[name .. "Portrait"]
if old then
old:SetAlpha(0)
end
-- BORDER layer: always above the panel background (BACKGROUND) and below the metal
-- (OVERLAY), whatever the creation order.
local portrait = frame:CreateTexture(nil, "BORDER")
portrait:SetWidth(R.portrait)
portrait:SetHeight(R.portrait)
portrait:SetPoint("CENTER", frame, "TOPLEFT", R.portraitX, R.portraitY)
frame.foreverPortrait = portrait
-- TitledPanelMixin:SetTitleOffsets(35) and TitleContainer: a 20 px frame from 35 to
-- width - 24, text at TOP (0, -5), so the title centres between portrait and close button.
-- A child frame, because the HeldBagLayout metal (OVERLAY) would cover a region of the bag
-- frame. The client title is hidden.
local oldTitle = _G[name .. "Name"]
if oldTitle then
oldTitle:SetAlpha(0)
end
local titleStrip = CreateFrame("Frame", nil, frame)
titleStrip:SetFrameLevel(frame:GetFrameLevel() + 2)
titleStrip:SetHeight(R.titleStripHeight)
titleStrip:SetPoint("TOPLEFT", frame, "TOPLEFT", R.titleLeft, R.titleContainer)
titleStrip:SetPoint("TOPRIGHT", frame, "TOPRIGHT", R.titleRight, R.titleContainer)
local title = titleStrip:CreateFontString(nil, "OVERLAY", "GameFontNormal")
title:SetPoint("TOP", titleStrip, "TOP", 0, R.titleText)
title:SetPoint("LEFT", titleStrip, "LEFT")
title:SetPoint("RIGHT", titleStrip, "RIGHT")
title:SetJustifyH("CENTER")
frame.foreverTitleStrip = titleStrip
frame.foreverTitle = title
-- Close button: 24 x 24, TOPRIGHT (1, 0), the red X of modern panels
-- (UIPanelCloseButtonNoScripts, atlas RedButton-Exit).
local close = _G[name .. "CloseButton"]
if close then
close:SetWidth(R.closeSize)
close:SetHeight(R.closeSize)
close:ClearAllPoints()
close:SetPoint("TOPRIGHT", frame, "TOPRIGHT", R.closeX, R.closeY)
for atlas, method in pairs({ ["redbutton-exit"] = "GetNormalTexture",
["redbutton-exit-pressed"] = "GetPushedTexture",
["redbutton-exit-disabled"] = "GetDisabledTexture",
["redbutton-highlight"] = "GetHighlightTexture" }) do
local texture = close[method] and close[method](close)
if texture then
ForeverUI.SetAtlas(texture, atlas, true)
texture:ClearAllPoints()
texture:SetAllPoints(close)
if atlas == "redbutton-highlight" then
texture:SetBlendMode("ADD")
end
end
end
end
for index = 1, MAX_CONTAINER_ITEMS do
skinButton(_G[name .. "Item" .. index])
end
frame.foreverSkinned = true
table.insert(frames, frame)
end
-- ---------------------------------------------------------- search
local Search = { text = "" }
ForeverUI.BagSearch = Search
-- Item name, type and subtype. An uncached item still has its name in the link.
local function itemInfoOf(link)
if not link then
return nil
end
local name, _, _, _, _, type_, subType = GetItemInfo(link)
if name then
return name, type_, subType
end
return string.match(link, "%[(.+)%]")
end
function Search.Matches(link)
if Search.text == "" then
return true
end
if not link then
return false
end
local name, type_, subType = itemInfoOf(link)
for _, field in ipairs({ name, type_, subType }) do
if field and string.find(string.lower(field), Search.text, 1, true) then
return true
end
end
return false
end
-- SetItemButtonTexture_Base: one texture holds either the item or the empty-slot background.
-- 3.3.5 hides the icon of an empty slot, so it is shown again with the atlas.
-- texture: item icon, or nil for an empty slot.
local function placeIcon(button, texture)
local icon = _G[button:GetName() .. "IconTexture"]
if not icon then
return
end
if texture then
icon:SetTexture(texture)
icon:SetTexCoord(0, 1, 0, 1)
else
ForeverUI.SetAtlas(icon, "bags-item-slot64", true)
end
icon:Show()
end
function Search.Apply(frame)
local name = frame:GetName()
local bag = frame:GetID()
for index = 1, frame.size or 0 do
local button = _G[name .. "Item" .. index]
if button and button.foreverVeil then
local slot = button:GetID()
local link = GetContainerItemLink(bag, slot)
placeIcon(button, GetContainerItemInfo(bag, slot))
if link and not Search.Matches(link) then
button.foreverVeil:Show()
else
button.foreverVeil:Hide()
end
-- The outline is always shown; it takes the quality color from uncommon up.
local outline = button.foreverOutline
if outline then
local quality = select(4, GetContainerItemInfo(bag, slot))
local color = link and quality and quality >= TINTED_QUALITY
and ITEM_QUALITY_COLORS and ITEM_QUALITY_COLORS[quality]
if color then
outline:SetVertexColor(color.r, color.g, color.b)
else
outline:SetVertexColor(FRAME_GRAY, FRAME_GRAY, FRAME_GRAY)
end
outline:Show()
end
end
end
end
-- Veils the keyring button when no keyring item matches the search
-- (camelot BaseBagSlotButtonMixin:UpdateBagMatchesSearch). No search, no veil.
function Search.Keyring()
local b = KeyRingButton
if not b then
return
end
if not b.foreverVeil then
local veil = b:CreateTexture(nil, "OVERLAY")
veil:SetTexture(0, 0, 0, 0.8)
veil:SetAllPoints(b)
veil:Hide()
b.foreverVeil = veil
end
local match = Search.text == ""
if not match then
local bag = KEYRING_CONTAINER or -2
for slot = 1, GetContainerNumSlots(bag) or 0 do
local link = GetContainerItemLink(bag, slot)
if link and Search.Matches(link) then
match = true
break
end
end
end
if match then b.foreverVeil:Hide() else b.foreverVeil:Show() end
end
function Search.All()
for _, frame in ipairs(frames) do
if frame:IsShown() then
Search.Apply(frame)
end
end
Search.Keyring()
end
function Search.Set(text)
Search.text = string.lower(text or "")
Search.All()
end
-- -------------------------------------------------------------- sort
-- Categories keep the game's order, as listed by GetAuctionItemClasses.
local Sort = { queue = {}, active = false }
ForeverUI.BagSort = Sort
local classRank
-- classRank: item class name -> its rank in GetAuctionItemClasses.
local function buildRanks()
if classRank then
return
end
classRank = {}
if GetAuctionItemClasses then
local classes = { GetAuctionItemClasses() }
for index, name in ipairs(classes) do
classRank[name] = index
end
end
end
-- Item in a bag slot with its sort keys; nil when the slot is empty.
local function readCell(bag, slot)
local link = GetContainerItemLink(bag, slot)
if not link then
return nil
end
local _, count, locked = GetContainerItemInfo(bag, slot)
local name, _, quality, _, _, type_, subType, maxStack = GetItemInfo(link)
return {
link = link,
count = count or 1,
locked = locked,
name = name or string.match(link, "%[(.+)%]") or link,
quality = quality or 0,
className = classRank[type_ or ""] or 99,
subType = subType or "",
maxStack = maxStack or 1,
}
end
-- Sort order: class, subtype, quality (best first), name, then larger stacks first.
local function before(a, b)
if a.className ~= b.className then
return a.className < b.className
end
if a.subType ~= b.subType then
return a.subType < b.subType
end
if a.quality ~= b.quality then
return a.quality > b.quality
end
if a.name ~= b.name then
return a.name < b.name
end
return a.count > b.count
end
-- Current bag state: one cell per slot, in order. General bags only (bagType 0, as
-- EquipmentManager_UpdateFreeBagSpace): a quiver, soul bag or profession bag refuses most
-- items (ERR_WRONG_BAG_TYPE), so it keeps its contents.
local function collect()
local cells = {}
for _, bag in ipairs(Sort.bags or BAGS) do
local _, bagType = GetContainerNumFreeSlots(bag)
if (bagType or 0) == 0 then
for slot = 1, (GetContainerNumSlots(bag) or 0) do
table.insert(cells, {
bag = bag,
slot = slot,
object = readCell(bag, slot),
})
end
end
end
return cells
end
local function locked(cell)
local _, _, isLocked = GetContainerItemInfo(cell.bag, cell.slot)
return isLocked
end
-- Makes one move per call; returns true while work remains. The server must confirm each
-- move before the next, otherwise the slot is still locked.
local function doStep()
-- The last move did not complete (refused by its target, or the player took an item
-- meanwhile): put the item back and stop, rather than wait on its locked slot
if CursorHasItem() then
ClearCursor()
return false
end
local cells = collect()
-- 1. merge partial stacks of the same item
for i = 1, #cells do
local a = cells[i].object
if a and a.count < a.maxStack then
for j = i + 1, #cells do
local b = cells[j].object
if b and b.link == a.link and b.count < b.maxStack then
if locked(cells[i]) or locked(cells[j]) then
return true
end
PickupContainerItem(cells[j].bag, cells[j].slot)
PickupContainerItem(cells[i].bag, cells[i].slot)
return true
end
end
end
end
-- 2. order: cell i must hold the i-th sorted item
local objects = {}
for _, cell in ipairs(cells) do
if cell.object then
table.insert(objects, cell.object)
end
end
table.sort(objects, before)
for i = 1, #cells do
local wanted = objects[i]
local present = cells[i].object
local sameItem = (wanted == nil and present == nil)
or (wanted and present and wanted.link == present.link and wanted.count == present.count)
if not sameItem then
if wanted == nil then
return false -- nothing left to place
end
-- find the cell holding the wanted item
for j = i + 1, #cells do
local candidate = cells[j].object
if candidate and candidate.link == wanted.link and candidate.count == wanted.count then
if locked(cells[i]) or locked(cells[j]) then
return true
end
PickupContainerItem(cells[j].bag, cells[j].slot)
PickupContainerItem(cells[i].bag, cells[i].slot)
return true
end
end
return false -- not found: stop rather than loop
end
end
return false -- everything in place
end
local MAX_STEPS = 400
local timeSinceStep = 0
local clock = CreateFrame("Frame", "ForeverUIBagSortTicker")
clock:Hide()
clock:SetScript("OnUpdate", function(self, elapsed)
timeSinceStep = timeSinceStep + elapsed
if timeSinceStep < 0.1 then
return
end
timeSinceStep = 0
Sort.steps = (Sort.steps or 0) + 1
if Sort.steps > MAX_STEPS or not doStep() then
Sort.active = false
self:Hide()
Search.All()
end
end)
-- bags: bag ids to sort (the bank, Bank.lua); defaults to the player's bags
function Sort.Start(bags)
if Sort.active then
return
end
Sort.bags = bags
if CursorHasItem() then
ClearCursor()
end
buildRanks()
Sort.active = true
Sort.steps = 0
timeSinceStep = 0
clock:Show()
end
-- ------------------------------------------- search box and sort button
local field = CreateFrame("EditBox", "ForeverUIBagSearchBox", UIParent, "InputBoxTemplate")
field:SetWidth(R.fieldWidth)
field:SetHeight(R.fieldHeight)
field:SetAutoFocus(false)
field:SetMaxLetters(15)
field:SetTextInsets(16, 20, 0, 0)
field:Hide()
local magnifier = field:CreateTexture(nil, "OVERLAY")
ForeverUI.SetAtlas(magnifier, "common-search-magnifyingglass", true)
magnifier:SetWidth(10)
magnifier:SetHeight(10)
magnifier:SetPoint("LEFT", field, "LEFT", 1, -1)
local placeholder = field:CreateFontString(nil, "ARTWORK", "GameFontDisableSmall")
placeholder:SetPoint("LEFT", field, "LEFT", 16, 0)
placeholder:SetText(SEARCH)
local clear = CreateFrame("Button", nil, field)
clear:SetWidth(17)
clear:SetHeight(17)
clear:SetPoint("RIGHT", field, "RIGHT", -3, 0)
clear:Hide()
local closeButton = clear:CreateTexture(nil, "ARTWORK")
ForeverUI.SetAtlas(closeButton, "common-search-clearbutton", true)
closeButton:SetWidth(10)
closeButton:SetHeight(10)
closeButton:SetPoint("TOPLEFT", clear, "TOPLEFT", 3, -3)
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()
field:SetText("")
field:ClearFocus()
end)
local function updateField()
local text = field:GetText() or ""
if text == "" and not field:HasFocus() then
placeholder:Show()
else
placeholder:Hide()
end
if text == "" then
clear:Hide()
else
clear:Show()
end
end
field:SetScript("OnTextChanged", function(self)
updateField()
Search.Set(self:GetText())
end)
field:SetScript("OnEditFocusGained", updateField)
field:SetScript("OnEditFocusLost", updateField)
field:SetScript("OnEscapePressed", function(self) self:ClearFocus() end)
field:SetScript("OnEnterPressed", function(self) self:ClearFocus() end)
-- BagSearch_OnChar: four identical characters in a row release the focus, so a held key
-- does not trap the player in the box.
field:SetScript("OnChar", function(self)
local text = self:GetText() or ""
if string.len(text) >= 4 then
local repeated = true
for i = 1, 3 do
if string.sub(text, -i, -i) ~= string.sub(text, -1 - i, -1 - i) then
repeated = false
break
end
end
if repeated then
self:ClearFocus()
end
end
end)
local sortButton = CreateFrame("Button", "ForeverUIBagSortButton", UIParent)
sortButton:SetWidth(R.sortWidth)
sortButton:SetHeight(R.sortHeight)
sortButton:Hide()
-- 3.3.5: SetNormalTexture and the like take only a file path; a texture object raises an
-- error. So the sheet path is set first, then the atlas is applied to the created texture.
-- place, getter: setter and getter names; width, height: optional centred size; add: ADD blend
local function applyState(button, place, getter, atlas, width, height, add)
local e = ForeverUI.AtlasEntry(atlas)
if not e then
return nil
end
button[place](button, e[1])
local texture = button[getter](button)
if not texture then
return nil
end
ForeverUI.SetAtlas(texture, atlas, true)
texture:ClearAllPoints()
if width then
texture:SetWidth(width)
texture:SetHeight(height)
texture:SetPoint("CENTER")
else
texture:SetAllPoints(button)
end
if add then
texture:SetBlendMode("ADD")
end
return texture
end
applyState(sortButton, "SetNormalTexture", "GetNormalTexture", "bags-button-autosort-up")
applyState(sortButton, "SetPushedTexture", "GetPushedTexture", "bags-button-autosort-down")
-- The highlight is a client file, not an atlas entry: 24 x 23, centred.
sortButton:SetHighlightTexture(SQUARE_HOVER)
local sortHighlight = sortButton:GetHighlightTexture()
if sortHighlight then
sortHighlight:SetBlendMode("ADD")
sortHighlight:ClearAllPoints()
sortHighlight:SetWidth(24)
sortHighlight:SetHeight(23)
sortHighlight:SetPoint("CENTER")
end
sortButton:SetScript("OnClick", function()
Sort.Start()
end)
sortButton:SetScript("OnEnter", function(self)
GameTooltip:SetOwner(self, "ANCHOR_LEFT")
GameTooltip:SetText(CLEANUP_TEXT, 1, 1, 1)
GameTooltip:AddLine(CLEANUP_TOOLTIP, nil, nil, nil, true)
GameTooltip:Show()
end)
sortButton:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
-- ------------------------------------------------------------ measures
-- ContainerFrameMixin:GetRows, CalculateWidth, CalculateHeight, GetPaddingHeight,
-- CalculateExtraHeight and their backpack overrides. The height derives from the row count,
-- never from a chain of anchors.
local function measure(frame)
local size = frame.size or 0
local m = { isBackpack = frame:GetID() == 0 }
m.rowLines = math.ceil(size / R.columns) -- GetRows()
m.grid = m.rowLines * R.slot + (m.rowLines - 1) * R.cellGap
-- GetPaddingHeight(): bottom of the first button plus title and attic; the backpack adds the
-- search box strip.
m.overhead = R.firstButtonY + R.header
if m.isBackpack then
m.overhead = m.overhead + R.searchStrip
end
-- CalculateExtraHeight(): the purse, on the backpack only, plus the watched currencies strip
-- when there is one.
m.tokens = m.isBackpack and ForeverUI.BagsWatchedCount() or 0
m.extra = m.isBackpack and R.purseHeight or 0
if m.tokens > 0 then
m.extra = m.extra + R.tokenHeight + R.tokenGap
end
m.height = m.grid + m.overhead + m.extra -- CalculateHeight()
m.width = R.width -- CalculateWidth()
return m
end
-- ContainerFrameMixin:UpdateSearchBox: the search box and sort button show only on the
-- backpack.
local function layoutTools()
local host
for _, frame in ipairs(frames) do
if frame:IsShown() and frame:GetID() == 0 then
host = frame
break
end
end
if not host then
field:Hide()
sortButton:Hide()
-- BagSearch_OnHide: no search box visible, so the search is cleared.
if field:GetText() ~= "" then
field:SetText("")
end
return
end
-- Both are anchored to the top, as in the source: the attic has a fixed height.
field:SetParent(host)
field:ClearAllPoints()
field:SetWidth(R.fieldWidth)
field:SetHeight(R.fieldHeight)
field:SetPoint("TOPLEFT", host, "TOPLEFT", R.fieldX, R.fieldY)
field:Show()
sortButton:SetParent(host)
sortButton:ClearAllPoints()
sortButton:SetWidth(R.sortWidth)
sortButton:SetHeight(R.sortHeight)
sortButton:SetPoint("TOPRIGHT", host, "TOPRIGHT", R.sortX, R.sortY)
sortButton:Show()
end
-- Purse border: two ends and a stretched middle, 17 high.
local function skinPurse(purse)
if not purse or purse.foreverBordered then
return
end
purse:SetHeight(R.purseHeight)
local left = purse:CreateTexture(nil, "BACKGROUND")
if not ForeverUI.SetAtlas(left, "common-coinbox-left", true) then
left:Hide()
return
end
left:SetWidth(R.purseFrame / 2)
left:SetHeight(R.purseFrame)
left:SetPoint("LEFT", purse, "LEFT", 0, 0)
local right = purse:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(right, "common-coinbox-right", true)
right:SetWidth(R.purseFrame / 2)
right:SetHeight(R.purseFrame)
right:SetPoint("RIGHT", purse, "RIGHT", 0, 0)
local middle = purse:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(middle, "_common-coinbox-center", true)
middle:SetPoint("TOPLEFT", left, "TOPRIGHT")
middle:SetPoint("BOTTOMRIGHT", right, "BOTTOMLEFT")
purse.foreverBordered = true
end
-- ------------------------------------------- watched currencies, below the purse
-- GetBackpackCurrencyInfo(i) gives name, count, typeSpecial, icon and item id, i up to
-- MAX_WATCHED_TOKENS (3). As in the currency tab, typeSpecial 1 (arena points) and 2 (honor,
-- per faction, cropped to 0.03125 .. 0.59375) have their own icons.
local ARENA_TOKEN = "Interface\\PVPFrame\\PVP-ArenaPoints-Icon"
local HONOR_TOKEN = "Interface\\TargetingFrame\\UI-PVP-%s"
local TOKEN_CORNER, TOKEN_SIDE = 0.03125, 0.59375
-- Token count font: GameFontHighlight, one size larger than camelot's GameFontHighlightSmall.
-- The count is anchored LEFT and RIGHT (not TOPLEFT) so it stays centred on the icon.
local TOKEN_FONT = "GameFontHighlight"
local function readTokens()
local list = {}
local maximum = MAX_WATCHED_TOKENS or 3
for rank = 1, maximum do
local name, count, special, icon = nil, nil, nil, nil
if GetBackpackCurrencyInfo then
name, count, special, icon = GetBackpackCurrencyInfo(rank)
end
if name then
list[#list + 1] = {
name = name, count = count or 0,
special = special, icon = icon,
}
end
end
return list
end
-- Number of watched currencies. Read from the data, not the screen: the window height is
-- computed before the strip is laid out.
function ForeverUI.BagsWatchedCount()
return #readTokens()
end
local function placeTokenIcon(texture, data)
if data.special == 1 then
texture:SetTexture(ARENA_TOKEN)
texture:SetTexCoord(0, 1, 0, 1)
elseif data.special == 2 then
local faction = UnitFactionGroup and UnitFactionGroup("player")
if faction then
texture:SetTexture(string.format(HONOR_TOKEN, faction))
texture:SetTexCoord(TOKEN_CORNER, TOKEN_SIDE, TOKEN_CORNER, TOKEN_SIDE)
else
texture:SetTexture("")
texture:SetTexCoord(0, 1, 0, 1)
end
else
texture:SetTexture(data.icon or "")
texture:SetTexCoord(0, 1, 0, 1)
end
end
-- Creates the backpack's currency strip once.
local function setupSegment(frame)
if frame.foreverSegment then
return frame.foreverSegment
end
local segment = CreateFrame("Frame", "ForeverUIBagTokens", frame)
segment:SetHeight(R.tokenHeight)
-- Border: two 8 x 17 ends and a stretched middle, cut like the purse.
local left = segment:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(left, "common-currencybox-left", true)
left:SetWidth(R.tokenSide)
left:SetHeight(R.tokenHeight)
left:SetPoint("LEFT", segment, "LEFT", 0, 0)
local right = segment:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(right, "common-currencybox-right", true)
right:SetWidth(R.tokenSide)
right:SetHeight(R.tokenHeight)
right:SetPoint("RIGHT", segment, "RIGHT", 0, 0)
local middle = segment:CreateTexture(nil, "BACKGROUND")
ForeverUI.SetAtlas(middle, "_common-currencybox-center", true)
middle:SetPoint("TOPLEFT", left, "TOPRIGHT")
middle:SetPoint("BOTTOMRIGHT", right, "BOTTOMLEFT")
segment.tokens = {}
for rank = 1, (MAX_WATCHED_TOKENS or 3) do
local token = CreateFrame("Button", "ForeverUIBagToken" .. rank, segment)
token:SetWidth(R.tokenWidth)
token:SetHeight(R.tokenPiece)
local icon = token:CreateTexture(nil, "ARTWORK")
icon:SetWidth(R.tokenPiece)
icon:SetHeight(R.tokenPiece)
icon:SetPoint("RIGHT", token, "RIGHT", R.tokenIconX, R.tokenIconY)
token.icon = icon
local count = token:CreateFontString(nil, "ARTWORK", TOKEN_FONT)
count:SetJustifyH("RIGHT")
count:SetPoint("LEFT", token, "LEFT", 0, 0)
count:SetPoint("RIGHT", icon, "LEFT", 0, 0)
token.count = count
-- Tokens chain leftwards from the right edge.
if rank == 1 then
token:SetPoint("RIGHT", segment, "RIGHT", R.tokenOriginX, R.tokenOriginY)
else
token:SetPoint("RIGHT", segment.tokens[rank - 1], "LEFT", 0, 0)
end
token:Hide()
segment.tokens[rank] = token
end
frame.foreverSegment = segment
return segment
end
-- Fills the strip from the data; returns the number of currencies shown.
local function updateSegment(frame)
local segment = setupSegment(frame)
local list = readTokens()
for rank, token in ipairs(segment.tokens) do
local data = list[rank]
if data then
placeTokenIcon(token.icon, data)
token.count:SetText(data.count)
token:Show()
else
token:Hide()
end
end
if #list > 0 then
segment:Show()
else
segment:Hide()
end
return #list
end
-- Title and portrait (ContainerFrameMixin:UpdateName, UpdateMiscellaneousFrames).
local function updateHeader(frame)
-- UpdateName: the name comes from the bag. The keyring has none (GetBagName(-2) returns
-- nothing), so the client's own title text is used.
local title = frame.foreverTitle
if title then
local bagName = GetBagName and GetBagName(frame:GetID())
if not bagName or bagName == "" then
local oldTitle = _G[frame:GetName() .. "Name"]
bagName = oldTitle and oldTitle:GetText() or ""
end
title:SetText(bagName)
end
-- UpdateMiscellaneousFrames: backpack, keyring, or the bag's own item icon.
local portrait = frame.foreverPortrait
if portrait then
local id = frame:GetID()
local texture
if id == 0 then
texture = BACKPACK_PORTRAIT
elseif id == (KEYRING_CONTAINER or -2) then
texture = KEYRING_PORTRAIT
elseif ContainerIDToInventoryID then
texture = GetInventoryItemTexture("player", ContainerIDToInventoryID(id))
end
portrait:SetTexture(texture)
-- Crop the 4 px dark border of the 64 px icon, as the client does for icons in a ring.
portrait:SetTexCoord(ICON_BORDER, 1 - ICON_BORDER,
ICON_BORDER, 1 - ICON_BORDER)
end
end
-- Sizes the window, places purse and currency strip, and re-anchors the whole grid
-- (ContainerFrameMixin:UpdateFrameSize, GetInitialItemAnchor, GetAnchorLayout).
local function layoutGrid(frame)
updateHeader(frame)
local size = frame.size or 0
if size <= 1 then
return -- the one-slot gift bag keeps its shape
end
local name = frame:GetName()
local m = measure(frame)
local purse = _G[name .. "MoneyFrame"]
-- UpdateFrameSize(): the window takes the computed size. The scale belongs to layoutBags
-- (containerScale shrinks bags that do not fit): setting it here, on every bag update,
-- would undo it without re-stacking.
frame:SetWidth(m.width)
frame:SetHeight(m.height)
-- UpdateFrameSize calls NineSliceUtil.UpdateCornerCropping right after SetSize; otherwise
-- the corners overlap on a short window.
if ForeverUI.UpdatePanelCorners then
ForeverUI.UpdatePanelCorners(frame)
end
-- UpdateCurrencyFrames(): the currency strip takes the bottom, the purse sits on it and the
-- grid hangs on the purse. Without watched currencies the purse takes the bottom.
if m.isBackpack and purse then
skinPurse(purse)
local placed = updateSegment(frame)
local segment = frame.foreverSegment
purse:ClearAllPoints()
if placed > 0 and segment then
segment:ClearAllPoints()
segment:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT",
R.tokenSide, R.purseBottom)
segment:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT",
-R.tokenSide, R.purseBottom)
purse:SetPoint("BOTTOMLEFT", segment, "TOPLEFT", 0, R.tokenGap)
purse:SetPoint("BOTTOMRIGHT", segment, "TOPRIGHT", 0, R.tokenGap)
else
purse:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT",
R.purseSide, R.purseBottom)
purse:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT",
-R.purseSide, R.purseBottom)
end
purse:Show()
end
-- GetAnchorLayout(): BottomRightToTopLeft, 4 columns, gap 5. The whole grid is re-anchored
-- because 3.3.5 uses its own gaps (4 px between rows).
for index = 1, size do
local button = _G[name .. "Item" .. index]
if button then
button:SetWidth(R.slot)
button:SetHeight(R.slot)
button:ClearAllPoints()
if index == 1 then
if m.isBackpack and purse then
-- ContainerFrameBackpackMixin:GetInitialItemAnchor()
button:SetPoint("BOTTOMRIGHT", purse, "TOPRIGHT",
0, R.purseGridGap)
else
-- ContainerFrameMixin:GetInitialItemAnchor()
button:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT",
R.firstButtonX, R.firstButtonY)
end
elseif math.fmod(index - 1, R.columns) == 0 then
button:SetPoint("BOTTOMRIGHT", _G[name .. "Item" .. (index - R.columns)],
"TOPRIGHT", 0, R.cellGap)
else
button:SetPoint("BOTTOMRIGHT", _G[name .. "Item" .. (index - 1)],
"BOTTOMLEFT", -R.cellGap, 0)
end
end
end
end
-- Width taken by the visible right action bars.
local function rightBarsWidth()
-- EditModeUtil:GetRightActionBarWidth() does not exist in 3.3.5; its right bars are
-- MultiBarRight and MultiBarLeft.
local width = 0
for _, barName in ipairs({ "MultiBarRight", "MultiBarLeft" }) do
local bar = _G[barName]
if bar and bar:IsShown() then
width = width + bar:GetWidth()
end
end
return width
end
local function openBags()
-- The client keeps the stacking order in ContainerFrame1.bags (GetBagsShown in the source).
local list = {}
if ContainerFrame1 and ContainerFrame1.bags then
for _, frameName in ipairs(ContainerFrame1.bags) do
local frame = _G[frameName]
if frame and frame:IsShown() then
table.insert(list, frame)
end
end
end
if #list == 0 then
for _, frame in ipairs(frames) do
if frame:IsShown() and (frame.size or 0) > 0 then
table.insert(list, frame)
end
end
end
return list
end
-- GetContainerScale: from 1 down to CONTAINER_SCALE by 0.01, the first scale at which no bag
-- is taller than a column and the left column stays right of the screen edge, or of an open
-- bank. Stacking as in layoutBags. bags: the open bag frames, bottom to top
local function containerScale(bags)
local leftLimit = 0
if BankFrame and BankFrame:IsShown() and BankFrame:GetRight() then
leftLimit = BankFrame:GetRight() * BankFrame:GetEffectiveScale() / UIParent:GetEffectiveScale()
- R.bankMargin
end
local s = 1
while s > R.minScale do
local k = s * R.scale
local available = (GetScreenHeight() - R.bottomEdge - R.topMargin) / k
local free, columns, fits = available, 1, true
for index, frame in ipairs(bags) do
local height = frame:GetHeight()
if height > available then
fits = false
break
end
if index == 1 then
free = free - height
elseif free < height + R.bagGap then
columns = columns + 1
free = available - height
else
free = free - height - R.bagGap
end
end
local width = (columns * R.width - (columns - 1) * R.columnGap) * k
local left = GetScreenWidth() - rightBarsWidth() - R.rightEdge - width
if fits and left >= leftLimit then
break
end
s = s - 0.01
end
return math.max(s, R.minScale) * R.scale
end
-- UpdateContainerFrameAnchors: bags stack bottom to top, CONTAINER_SPACING apart, and start
-- a new column to the left when the screen is full. Unlike the source, a stacked bag costs
-- its height plus the gap, and topMargin is kept below the screen top, so no bag goes past
-- the top edge.
local function layoutBags()
local bags = openBags()
local scale = containerScale(bags)
local screenHeight = GetScreenHeight() / scale
local offsetX = (rightBarsWidth() + R.rightEdge) / scale
local offsetY = R.bottomEdge / scale
local available = screenHeight - offsetY - R.topMargin / scale
local free = available
local previous, firstInColumn
for index, frame in ipairs(bags) do
frame:SetScale(scale)
frame:ClearAllPoints()
local height = frame:GetHeight()
if index == 1 then
frame:SetPoint("BOTTOMRIGHT", frame:GetParent(), "BOTTOMRIGHT",
-offsetX, offsetY)
firstInColumn = frame
free = free - height
elseif free < height + R.bagGap then
frame:SetPoint("BOTTOMRIGHT", firstInColumn, "BOTTOMLEFT",
R.columnGap, 0)
firstInColumn = frame
free = available - height
else
frame:SetPoint("BOTTOMRIGHT", previous, "TOPRIGHT", 0, R.bagGap)
free = free - height - R.bagGap
end
previous = frame
end
end
ForeverUI.BagsStack = layoutBags
-- 3.3.5 resizes its bag frames at times the hooks do not all cover. One pass on the next
-- frame re-measures and re-applies the size if it moved. Only hooks request it, never
-- itself, so it cannot loop against the client.
local recheck = CreateFrame("Frame", "ForeverUIBagsRecheck")
recheck:Hide()
recheck:SetScript("OnUpdate", function(self)
self:Hide()
local needsRedo = false
for _, frame in ipairs(frames) do
if frame:IsShown() and (frame.size or 0) > 1 then
local m = measure(frame)
if math.abs(frame:GetHeight() - m.height) > 0.5
or math.abs(frame:GetWidth() - m.width) > 0.5 then
frame.foreverUndoCount = (frame.foreverUndoCount or 0) + 1
layoutGrid(frame)
needsRedo = true
end
end
end
if needsRedo then
-- A height changed: the stacking depends on it.
layoutBags()
end
end)
local function requestRecheck()
recheck:Show()
end
ForeverUI.BagsLayout = layoutTools
-- ------------------------------------------------------------- hooks
local function skinAll()
for index = 1, FRAME_COUNT do
skinFrame(_G["ContainerFrame" .. index])
end
end
skinAll()
updateField()
if hooksecurefunc then
-- The client resets its own pieces each time a bag opens.
hooksecurefunc("ContainerFrame_GenerateFrame", function(frame)
-- The bank window shows the bank bags itself (Bank.lua): OpenAllBags (shift + B, bank
-- open) and ToggleBag would show them again, each in a bag window.
local id = frame:GetID()
if id > NUM_BAG_SLOTS and id <= NUM_BAG_SLOTS + NUM_BANKBAGSLOTS then
frame:Hide()
return
end
skinFrame(frame)
layoutGrid(frame)
layoutTools()
-- GenerateFrame runs updateContainerFrameAnchors (our stacking) before it shows the bag,
-- so the new bag was skipped; stack again now that it is shown.
layoutBags()
Search.All()
requestRecheck()
end)
-- ContainerFrame_Update runs on every bag update and resets the client's pieces; the grid
-- is re-applied after it, or the original size comes back.
hooksecurefunc("ContainerFrame_Update", function(frame)
layoutGrid(frame)
Search.Apply(frame)
requestRecheck()
end)
hooksecurefunc("ContainerFrame_OnHide", function()
layoutTools()
end)
-- The client resets scale and anchors on each re-layout: re-apply ours, size included.
hooksecurefunc("updateContainerFrameAnchors", function()
for _, frame in ipairs(frames) do
if frame:GetScale() ~= R.scale then
frame:SetScale(R.scale)
end
if frame:IsShown() then
layoutGrid(frame)
end
end
-- The client just stacked the bags with its own gaps; re-stack with camelot's.
layoutBags()
requestRecheck()
end)
end
-- A new screen size or UI scale changes the room left for the bags.
local screenWatcher = CreateFrame("Frame")
screenWatcher:RegisterEvent("DISPLAY_SIZE_CHANGED")
screenWatcher:RegisterEvent("UI_SCALE_CHANGED")
screenWatcher:SetScript("OnEvent", function() layoutBags() end)
-- ManageBackpackTokenFrame reparents BackpackTokenFrame into the backpack and resets its
-- height (BACKPACK_HEIGHT + 22), undoing ours: the client strip stays hidden and the
-- backpack is re-laid out after it.
local function silenceClientSegment()
local old = _G["BackpackTokenFrame"]
if old then
old:Hide()
if old.EnableMouse then
old:EnableMouse(false)
end
end
end
if hooksecurefunc and type(_G["ManageBackpackTokenFrame"]) == "function" then
hooksecurefunc("ManageBackpackTokenFrame", function()
silenceClientSegment()
for _, frame in ipairs(frames) do
if frame:IsShown() and frame:GetID() == 0 then
layoutGrid(frame)
end
end
end)
end
-- "Show on Backpack" always goes through SetCurrencyBackpack (currency tab boxes and the
-- client's modified click), so the backpack is re-laid out after it.
if hooksecurefunc and type(_G["SetCurrencyBackpack"]) == "function" then
hooksecurefunc("SetCurrencyBackpack", function()
for _, frame in ipairs(frames) do
if frame:IsShown() and frame:GetID() == 0 then
layoutGrid(frame)
end
end
end)
end
local listener = CreateFrame("Frame", "ForeverUIBagsWatcher")
listener:RegisterEvent("PLAYER_ENTERING_WORLD")
listener:RegisterEvent("BAG_UPDATE")
-- A watched currency amount changed.
listener:RegisterEvent("CURRENCY_DISPLAY_UPDATE")
-- BAG_UPDATE comes in bursts (loot, vendor, mail, two per sort move): one pass on the next
-- frame for the whole burst (a hidden frame still gets its events). The bag that changed is
-- already redone by the ContainerFrame_Update hook.
listener:Hide()
listener:SetScript("OnEvent", function(self) self:Show() end)
listener:SetScript("OnUpdate", function(self)
self:Hide()
skinAll()
silenceClientSegment()
for _, frame in ipairs(frames) do
if frame:IsShown() then
layoutGrid(frame)
end
end
layoutTools()
Search.All()
requestRecheck()
end)
-- Re-applies the whole layout after a setting changes.
function ForeverUI.BagsApply()
for _, frame in ipairs(frames) do
layoutGrid(frame)
end
layoutTools()
-- R.scale reaches the frames through the stacking
layoutBags()
Search.All()
end