35fc513c41
Sacs : règle GetContainerScale de Camelot (de 1 à 0,75 par pas de 0,01, jusqu'à ce que la colonne de gauche reste dans l'écran, ou à droite de la banque ouverte), avec l'empilement de ForeverUI ; refait quand la taille de l'écran ou l'échelle de l'interface change. Mode d'édition : un élément déplacé est accroché au point de l'écran du tiers où il est posé, et garde sa place par rapport à ce bord. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1417 lines
44 KiB
Lua
1417 lines
44 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.
|
|
-- /fui bags prints computed and actual sizes; /fui bags <key> <value> tries a value.
|
|
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.
|
|
local function collect()
|
|
local cells = {}
|
|
for _, bag in ipairs(Sort.bags or BAGS) do
|
|
for slot = 1, (GetContainerNumSlots(bag) or 0) do
|
|
table.insert(cells, {
|
|
bag = bag,
|
|
slot = slot,
|
|
object = readCell(bag, slot),
|
|
})
|
|
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()
|
|
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.
|
|
frame:SetScale(R.scale)
|
|
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
|
|
|
|
-- Read the height back at once, for /fui bags: a wrong read-back means the frame's anchors
|
|
-- set its height; a right one that later differs means something resizes it after us.
|
|
frame.foreverRequested = m.height
|
|
frame.foreverReadBack = frame:GetHeight()
|
|
|
|
-- 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)
|
|
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")
|
|
listener:SetScript("OnEvent", function()
|
|
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()
|
|
Search.All()
|
|
end
|
|
|
|
-- /fui bags: for each open bag, the computed size and the frame's actual size. A mismatch
|
|
-- means something resizes the frame after us.
|
|
ForeverUI.BagsDebug = function()
|
|
local openCount = 0
|
|
for _, frame in ipairs(frames) do
|
|
if frame:IsShown() and (frame.size or 0) > 0 then
|
|
openCount = openCount + 1
|
|
local name = frame:GetName()
|
|
local m = measure(frame)
|
|
local actualW, actualH = frame:GetWidth(), frame:GetHeight()
|
|
local sizeMatch = (math.abs(actualH - m.height) < 0.5)
|
|
and (math.abs(actualW - m.width) < 0.5)
|
|
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format(
|
|
"|cff66ccffForeverUI|r " .. L.BAGS_DEBUG_FRAME,
|
|
name, frame:GetID(), frame.size or 0, m.rowLines))
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format(
|
|
" " .. L.BAGS_DEBUG_SIZE,
|
|
m.width, m.height, m.grid, m.overhead, m.extra, actualW, actualH,
|
|
sizeMatch and ("|cff44ff44" .. L.BAGS_DEBUG_MATCH .. "|r") or ("|cffff4444" .. L.BAGS_DEBUG_MISMATCH .. "|r")))
|
|
|
|
if not sizeMatch then
|
|
-- The read-back tells which of the two cases it is.
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format(
|
|
" " .. L.BAGS_DEBUG_WITNESS,
|
|
tostring(frame.foreverRequested), tostring(frame.foreverReadBack),
|
|
tostring(frame.foreverUndoCount or 0),
|
|
(frame.foreverReadBack and frame.foreverRequested
|
|
and math.abs(frame.foreverReadBack - frame.foreverRequested) < 0.5)
|
|
and ("|cffff4444" .. L.BAGS_DEBUG_RESIZED_AFTER .. "|r")
|
|
or ("|cffff4444" .. L.BAGS_DEBUG_ANCHORS_FORCE .. "|r")))
|
|
|
|
local rows = string.format(" " .. L.BAGS_DEBUG_ANCHORS, frame:GetNumPoints())
|
|
for index = 1, frame:GetNumPoints() do
|
|
local point, target, targetPoint, x, y = frame:GetPoint(index)
|
|
rows = rows .. string.format(L.BAGS_DEBUG_ANCHOR,
|
|
tostring(point), tostring(targetPoint),
|
|
target and (target.GetName and target:GetName() or "?") or L.BAGS_DEBUG_SCREEN,
|
|
x or 0, y or 0)
|
|
end
|
|
DEFAULT_CHAT_FRAME:AddMessage(rows)
|
|
end
|
|
end
|
|
end
|
|
|
|
if openCount == 0 then
|
|
DEFAULT_CHAT_FRAME:AddMessage(
|
|
"|cff66ccffForeverUI|r " .. L.BAGS_DEBUG_NO_BAG_OPEN)
|
|
end
|
|
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format(
|
|
" " .. L.BAGS_DEBUG_SKINNED,
|
|
#frames, R.slot, tostring(field:IsShown())))
|
|
|
|
-- Watched currencies: what the client gives and what is shown. The client strip must be hidden.
|
|
local tracked = readTokens()
|
|
local names = {}
|
|
for _, token in ipairs(tracked) do
|
|
names[#names + 1] = string.format("%s=%s", token.name, tostring(token.count))
|
|
end
|
|
local segment = _G["ForeverUIBagTokens"]
|
|
local old = _G["BackpackTokenFrame"]
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format(
|
|
" " .. L.BAGS_DEBUG_TOKENS, #tracked, MAX_WATCHED_TOKENS or 3,
|
|
(#names > 0) and table.concat(names, ", ") or L.BAGS_DEBUG_NONE))
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format(
|
|
" " .. L.BAGS_DEBUG_SEGMENT,
|
|
tostring(segment and segment:IsShown()),
|
|
tostring(old and old:IsShown())))
|
|
|
|
-- All settings, sorted by name, so the list stays complete when settings change.
|
|
local keys = {}
|
|
for key in pairs(R) do
|
|
table.insert(keys, key)
|
|
end
|
|
table.sort(keys)
|
|
local row = ""
|
|
for _, key in ipairs(keys) do
|
|
row = row .. string.format("%s=%s ", key, tostring(R[key]))
|
|
if string.len(row) > 80 then
|
|
DEFAULT_CHAT_FRAME:AddMessage(" " .. row)
|
|
row = ""
|
|
end
|
|
end
|
|
if row ~= "" then
|
|
DEFAULT_CHAT_FRAME:AddMessage(" " .. row)
|
|
end
|
|
end
|
|
|
|
-- Changes a setting in game, to try it before fixing it in R.
|
|
-- key: name in R; value: new number
|
|
function ForeverUI.BagsSet(key, value)
|
|
if R[key] == nil then
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffForeverUI|r " .. L.BAGS_UNKNOWN_SETTING .. tostring(key))
|
|
return false
|
|
end
|
|
local count = tonumber(value)
|
|
if not count then
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffForeverUI|r " .. L.BAGS_NUMBER_EXPECTED)
|
|
return false
|
|
end
|
|
R[key] = count
|
|
ForeverUI.BagsApply()
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format(
|
|
"|cff66ccffForeverUI|r " .. L.BAGS_SET, key, tostring(count)))
|
|
return true
|
|
end
|