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

908 lines
29 KiB
Lua

-- Equipment manager page of the character sheet's right pane (camelot EquipmentManagerPane).
-- The client's GearManagerDialog is moved into the pane and its set buttons are reskinned as
-- cards; the client keeps the list, the selection, the tooltips and the clicks.
-- Sizes and anchors: camelot PaperDollFrame.xml and GearSetButtonTemplate.
ForeverUI = ForeverUI or {}
local L = ForeverUI.L
-- Card layout differs from camelot's GearSetButtonTemplate (card 169 x 44, background 152 x 49
-- at x = 42): list at x = 9, card 40 high, background 174 x 45 (the art keeps its 5 px overhang).
-- Width 174 makes the gap from the pane separator to the icon (8 px) equal the gap from the card
-- to the pane's right edge: 233 - 8 - LIST_X - 42 = 174. The button covers the whole card.
local CARD_BACKGROUND_W, CARD_BACKGROUND_H = 174, 45
local CARD_BACKGROUND_X = 42
local CARD_W, CARD_H = CARD_BACKGROUND_X + CARD_BACKGROUND_W, 40
local CARD_ICON, CARD_ICON_X = 36, 4
local CARD_TEXT_X = 55
local CARD_TEXT_W, CARD_TEXT_H = 98, 38
-- camelot anchors the checkmark at RIGHT -23 of a button that ends 25 px before the card edge.
-- Here the button covers the whole card, so the checkmark sits 12 px from the card's right edge.
local CHECKMARK_X = -12
local LIST_X, LIST_Y = 9, -8
local LIST_Y2 = 105
local BUTTON_W, BUTTON_H = 99, 28
local BUTTON_Y, BUTTON_GAP = 20, 50
local NEW_BUTTON_W, NEW_BUTTON_H = 180, 34
local NEW_BUTTON_Y, NEW_BUTTON_ICON_X = 50, 13
-- common-insideframe (107 x 107) has a pattern in each corner: nine-sliced, not stretched.
-- Corner 20: the corner pattern ends at 19 px.
-- Margins from camelot's TOPLEFT (1, 1) / BOTTOMRIGHT (-4, 2), shifted 3 px to the right.
local BORDER_CORNER = 20
local BORDER_MARGINS = { -4, 1, -1, -2 }
local ATLAS_BACKGROUND = "ui-character-info-outfitcard"
local ATLAS_HOVER = "ui-character-info-outfitcard-hover"
local ATLAS_SELECTED = "ui-character-info-outfitcard-selected"
local ATLAS_CHECKMARK = "ui-character-info-icon-tick"
local ATLAS_CIRCLE = "ui-character-info-outfiticon-frame"
local ATLAS_PLUS = "ui-character-info-icon-add"
local ATLAS_BORDER = "common-insideframe"
local ATLAS_LINE = "ui-character-info-scrollline"
-- camelot GearSetButtonTemplate hover buttons: Delete 14 x 14 at BOTTOMRIGHT (-21, 2), Edit
-- 16 x 16 left of it (-1); alpha 0.5 idle, 1 on hover; the texture shifts (1, -1) when pressed.
-- camelot's gear assigns a specialization, which 3.3.5 lacks: here it reopens the save popup on
-- the set to rename it or change its icon. SETTINGS replaces the missing EQUIPMENT_SET_SETTINGS.
local DELETE_SIZE = 14
local DELETE_X, DELETE_Y = -21, 2
local EDIT_SIZE = 16
local EDIT_X = -1
local DELETE_ICON = "Interface\\Buttons\\UI-GroupLoot-Pass-Up"
local EDIT_ICON = "Interface\\WorldMap\\GEAR_64GREY"
local IDLE, HOVER = 0.5, 1.0
local panel, offset = nil, 0
-- Editing a set. 3.3.5 has no ModifyEquipmentSet: SaveEquipmentSet(name, icon) saves the worn
-- gear. So an edit equips the old set, saves it under the new name and icon, deletes the old one
-- and gives the new name the old one's rank. The swap is asynchronous (EQUIPMENT_SWAP_FINISHED).
-- 3.3.5 lists sets in creation order; the display order is kept per character (characterSets).
local editSession
local intendedClose -- set while the addon hides the popup itself
local EDIT_TIMEOUT = 10 -- seconds before an edit is abandoned
-- Sets belong to a character and ForeverUIDB to the account: the display order and the last set
-- worn are kept per character (one character's order was pruned to another's sets). The first
-- use takes over the account-wide values of earlier versions.
local function characterSets()
ForeverUIDB = ForeverUIDB or {}
ForeverUIDB.sets = ForeverUIDB.sets or {}
local key = tostring(UnitName("player")) .. "-" .. tostring(GetRealmName())
local sets = ForeverUIDB.sets[key]
if not sets then
sets = { order = {}, equipped = ForeverUIDB.equippedSet }
for rank, name in ipairs(ForeverUIDB.setOrder or {}) do
sets.order[rank] = name
end
ForeverUIDB.sets[key] = sets
end
return sets
end
local function savedOrder()
return characterSets().order
end
-- Client set indices in the saved order; sets not yet known go last.
local function orderedSets()
local total = (GetNumEquipmentSets and GetNumEquipmentSets()) or 0
local byName, inside = {}, {}
for index = 1, total do
local name = GetEquipmentSetInfo(index)
if name then
byName[name] = index
end
end
-- Each name is kept once: duplicates take several ranks and push the card past the last rank.
-- The saved order is pruned only when the client lists at least one set: between a save and
-- its event the client lists none, and pruning would empty it.
local order = savedOrder()
local list, kept = {}, {}
for _, name in ipairs(order) do
if byName[name] and not inside[name] then
list[#list + 1] = byName[name]
inside[name] = true
kept[#kept + 1] = name
end
end
for index = 1, total do
local name = GetEquipmentSetInfo(index)
if name and not inside[name] then
list[#list + 1] = index
inside[name] = true
kept[#kept + 1] = name
end
end
if total > 0 then
for rank = #order, 1, -1 do
order[rank] = nil
end
for rank, name in ipairs(kept) do
order[rank] = name
end
end
return list
end
ForeverUI.EquipmentSetsOrder = orderedSets
-- A set is worn when each piece is in its own slot (the unpacked slot must match the key).
-- 3.3.5 has no active set and two sets can hold the same pieces, so the checkmark goes to the
-- last set equipped (UseEquipmentSet hook), only while it is still worn.
local function piecesInPlace(name)
if not name or not GetEquipmentSetLocations or not EquipmentManager_UnpackLocation then
return false
end
local locations = GetEquipmentSetLocations(name)
if not locations then
return false
end
local found = false
for slotId, location in pairs(locations) do
if type(location) == "number" and location > 1 then
local player, _, bags, slot = EquipmentManager_UnpackLocation(location)
if not player or bags or slot ~= slotId then
return false
end
found = true
end
end
return found
end
-- Name of the last set equipped.
local function activeSet()
return characterSets().equipped
end
local function isSetWorn(name)
return name ~= nil and name == activeSet() and piecesInPlace(name)
end
-- Remember the last set equipped, across sessions.
if hooksecurefunc and type(UseEquipmentSet) == "function" then
hooksecurefunc("UseEquipmentSet", function(name)
characterSets().equipped = name
if ForeverUI.EquipmentSetsLayout then
ForeverUI.EquipmentSetsLayout()
end
end)
end
-- Height of the card list, and how many cards fit in it.
local function listHeight()
if not panel then
return 0
end
return panel:GetHeight() - (-LIST_Y) - LIST_Y2
end
local function visibleCards()
local location = math.floor(listHeight() / CARD_H)
if location < 1 then
location = 1
end
return location
end
-- Reskins a client set button as a card, once.
local function skinCard(button)
if button.foreverCard then
return
end
button:SetWidth(CARD_W)
button:SetHeight(CARD_H)
-- Hide the 3.3.5 empty-slot background; the icon is the button's NormalTexture and stays.
local icon = button:GetNormalTexture()
local regions = { button:GetRegions() }
for _, region in ipairs(regions) do
if region ~= icon and region.GetObjectType and region:GetObjectType() == "Texture" then
local path = region.GetTexture and region:GetTexture()
if type(path) == "string" and string.find(string.lower(path), "emptyslot") then
region:SetAlpha(0)
end
end
end
if button:GetHighlightTexture() then
button:GetHighlightTexture():SetAlpha(0)
end
if button.GetCheckedTexture and button:GetCheckedTexture() then
button:GetCheckedTexture():SetAlpha(0)
end
local function map(atlas, layer)
local t = button:CreateTexture(nil, layer)
ForeverUI.SetAtlas(t, atlas, true)
t:SetWidth(CARD_BACKGROUND_W)
t:SetHeight(CARD_BACKGROUND_H)
t:SetPoint("TOPLEFT", button, "TOPLEFT", CARD_BACKGROUND_X, 0)
return t
end
-- Selected (ARTWORK) draws above hover (BORDER), whatever the creation order.
button.foreverBackground = map(ATLAS_BACKGROUND, "BACKGROUND")
button.foreverHover = map(ATLAS_HOVER, "BORDER")
button.foreverHover:Hide()
button.foreverSelected = map(ATLAS_SELECTED, "ARTWORK")
button.foreverSelected:Hide()
if icon then
icon:ClearAllPoints()
icon:SetWidth(CARD_ICON)
icon:SetHeight(CARD_ICON)
icon:SetPoint("LEFT", button, "LEFT", CARD_ICON_X, 0)
icon:SetDrawLayer("ARTWORK")
end
local circle = button:CreateTexture(nil, "OVERLAY")
ForeverUI.SetAtlas(circle, ATLAS_CIRCLE)
circle:SetPoint("CENTER", icon or button, "CENTER", 0, 0)
local checkMark = button:CreateTexture(nil, "OVERLAY")
ForeverUI.SetAtlas(checkMark, ATLAS_CHECKMARK)
checkMark:SetPoint("RIGHT", button, "RIGHT", CHECKMARK_X, 0)
checkMark:Hide()
button.foreverCheck = checkMark
local text = _G[button:GetName() .. "Name"]
if text then
text:ClearAllPoints()
text:SetFontObject(GameFontNormalLeft or GameFontNormal)
text:SetWidth(CARD_TEXT_W)
text:SetHeight(CARD_TEXT_H)
text:SetJustifyH("LEFT")
text:SetPoint("LEFT", button, "LEFT", CARD_TEXT_X, 0)
end
-- Delete and edit buttons, shown on the hovered card.
-- name: frame name suffix; side: size; icon: texture path; tooltip: tooltip text
local function smallButton(name, side, icon, tooltip, onClick)
local b = CreateFrame("Button", button:GetName() .. name, button)
b:SetWidth(side)
b:SetHeight(side)
b:SetFrameLevel(button:GetFrameLevel() + 2)
local t = b:CreateTexture(nil, "ARTWORK")
t:SetTexture(icon)
t:SetAllPoints(b)
t:SetAlpha(IDLE)
b.texture = t
b:SetScript("OnEnter", function(self)
self.texture:SetAlpha(HOVER)
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetText(tooltip)
GameTooltip:Show()
end)
b:SetScript("OnLeave", function(self)
self.texture:SetAlpha(IDLE)
GameTooltip:Hide()
end)
b:SetScript("OnMouseDown", function(self)
self.texture:ClearAllPoints()
self.texture:SetPoint("TOPLEFT", self, "TOPLEFT", 1, -1)
self.texture:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", 1, -1)
end)
b:SetScript("OnMouseUp", function(self)
self.texture:ClearAllPoints()
self.texture:SetAllPoints(self)
end)
b:SetScript("OnClick", onClick)
b:Hide()
return b
end
button.foreverDelete = smallButton("ForeverUIDelete", DELETE_SIZE,
DELETE_ICON, DELETE, function(self)
local map = self:GetParent()
if not map.name or map.name == "" then
return
end
local window = StaticPopup_Show("CONFIRM_DELETE_EQUIPMENT_SET", map.name)
if window then
window.data = map.name
elseif UIErrorsFrame then
UIErrorsFrame:AddMessage(ERR_CLIENT_LOCKED_OUT, 1.0, 0.1, 0.1, 1.0)
end
end)
button.foreverDelete:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT",
DELETE_X, DELETE_Y)
button.foreverEdit = smallButton("ForeverUIEdit", EDIT_SIZE,
EDIT_ICON, SETTINGS, function(self)
local map = self:GetParent()
if not map.name or map.name == "" then
return
end
if ForeverUI.EquipmentSetEdit then
ForeverUI.EquipmentSetEdit(map.name)
end
end)
button.foreverEdit:SetPoint("RIGHT", button.foreverDelete, "LEFT",
EDIT_X, 0)
button.foreverCard = true
end
-- The set list is polled: sets live on the server, and no return, event or frame delay tells
-- when the client list is ready. Every 0.2 s while the pane is shown, the count, names and icons
-- are compared with the last layout, and the cards are laid out again when they differ.
local WATCH_INTERVAL = 0.2
local lastState, sinceCheck = nil, 0
local function listState()
local total = (GetNumEquipmentSets and GetNumEquipmentSets()) or 0
local parts = { tostring(total) }
for index = 1, total do
local name, icon = GetEquipmentSetInfo(index)
parts[#parts + 1] = tostring(name) .. "=" .. tostring(icon)
end
return table.concat(parts, "|")
end
local function watchList(elapsed)
sinceCheck = sinceCheck + (elapsed or 0)
if sinceCheck < WATCH_INTERVAL then
return
end
sinceCheck = 0
local state = listState()
if state ~= lastState then
lastState = state
if GearManagerDialog_Update then
GearManagerDialog_Update()
end
-- layoutCards is defined below: call it through the published function.
ForeverUI.EquipmentSetsLayout()
end
end
-- Makes the next poll lay out the list again.
function ForeverUI.EquipmentSetsForget()
lastState = nil
sinceCheck = WATCH_INTERVAL
end
-- Hover is polled each frame, as in camelot's PaperDollEquipmentManagerPane_OnUpdate: the
-- delete and edit buttons are children of the card, so entering them fires the card's OnLeave.
local function trackHover(self, elapsed)
watchList(elapsed)
local dialog = _G["GearManagerDialog"]
if not dialog or not dialog.buttons then
return
end
for _, button in ipairs(dialog.buttons) do
if button.foreverCard and button:IsShown() then
local hovered = button:IsMouseOver() and button.name and button.name ~= ""
if hovered then
button.foreverHover:Show()
button.foreverDelete:Show()
button.foreverEdit:Show()
else
button.foreverHover:Hide()
button.foreverDelete:Hide()
button.foreverEdit:Hide()
end
end
end
end
ForeverUI.EquipmentSetsHover = trackHover
-- Lays out the existing sets in a column, from the scroll offset.
local function layoutCards()
local dialog = _G["GearManagerDialog"]
if not dialog or not dialog.buttons or not panel then
return
end
local total = (GetNumEquipmentSets and GetNumEquipmentSets()) or 0
local location = visibleCards()
if offset > total - location then
offset = math.max(0, total - location)
end
-- Button i holds client set i; the saved order decides where it goes.
local order = orderedSets()
local buttonRank = {}
for position, index in ipairs(order) do
buttonRank[index] = position
end
local previous
local byRank = {}
for index, button in ipairs(dialog.buttons) do
skinCard(button)
if buttonRank[index] then
byRank[buttonRank[index]] = button
end
end
for position = 1, #dialog.buttons do
local button = byRank[position]
-- The client index is order[position], not the position itself.
local index = order[position]
local rank = position - offset
if button and index and index <= total and rank >= 1 and rank <= location then
button:ClearAllPoints()
if previous then
button:SetPoint("TOPLEFT", previous, "BOTTOMLEFT", 0, 0)
else
button:SetPoint("TOPLEFT", panel, "TOPLEFT", LIST_X, LIST_Y)
end
previous = button
button:Show()
if button.foreverSelected then
if button:GetChecked() then
button.foreverSelected:Show()
else
button.foreverSelected:Hide()
end
end
if button.foreverCheck then
if isSetWorn(button.name) then
button.foreverCheck:Show()
else
button.foreverCheck:Hide()
end
end
elseif button then
button:Hide()
end
end
-- Buttons without a set stay hidden.
for index, button in ipairs(dialog.buttons) do
if not buttonRank[index] then
button:Hide()
end
end
end
ForeverUI.EquipmentSetsLayout = layoutCards
local function build(pane)
panel = CreateFrame("Frame", "ForeverUIEquipmentPane", pane)
-- Hidden until ForeverUI.Panes shows it as the equipment page: GearManagerDialog_Update also
-- runs on EQUIPMENT_SWAP_FINISHED (a set worn from a bar, /equipset) and can build the panel
-- before that page was ever opened, over the stats page
panel:Hide()
panel:SetPoint("TOPLEFT", pane.stone, "BOTTOMLEFT", 0, 0)
panel:SetPoint("BOTTOMRIGHT", pane, "BOTTOMRIGHT", 0, 0)
panel:SetFrameLevel(pane:GetFrameLevel() + 3)
panel.border = ForeverUI.CreateNineSlice(panel, ATLAS_BORDER,
BORDER_CORNER, BORDER_MARGINS, "BORDER")
local line = panel:CreateTexture(nil, "BORDER")
ForeverUI.SetAtlas(line, ATLAS_LINE)
line:SetPoint("TOP", panel, "BOTTOM", 0, LIST_Y2)
-- Mouse wheel scrolling: camelot's scroll bar is not reproduced.
panel:SetScript("OnUpdate", trackHover)
panel:EnableMouseWheel(true)
panel:SetScript("OnMouseWheel", function(self, direction)
local total = (GetNumEquipmentSets and GetNumEquipmentSets()) or 0
offset = math.max(0, math.min(offset - direction, total - visibleCards()))
layoutCards()
end)
return panel
end
-- Equip and Save stay the client's. New Set clears the selection then runs Save (3.3.5 has no
-- New Set button). The client's Delete is hidden: each card has its own.
local function placeButtons()
local equipButton = _G["GearManagerDialogEquipSet"]
local saveButton = _G["GearManagerDialogSaveSet"]
local clear = _G["GearManagerDialogDeleteSet"]
if equipButton then
equipButton:SetWidth(BUTTON_W)
equipButton:SetHeight(BUTTON_H)
equipButton:ClearAllPoints()
equipButton:SetPoint("BOTTOM", panel, "BOTTOM", -BUTTON_GAP, BUTTON_Y)
end
if saveButton then
saveButton:SetWidth(BUTTON_W)
saveButton:SetHeight(BUTTON_H)
saveButton:ClearAllPoints()
saveButton:SetPoint("BOTTOM", panel, "BOTTOM", BUTTON_GAP, BUTTON_Y)
end
if clear then
clear:SetWidth(BUTTON_W)
clear:SetHeight(BUTTON_H)
clear:ClearAllPoints()
clear:SetPoint("BOTTOM", saveButton or panel, "TOP", 0, 0)
clear:Hide()
end
if not panel.new then
local new = CreateFrame("Button", "ForeverUIEquipmentNewSet", panel,
"UIPanelButtonTemplate")
new:SetWidth(NEW_BUTTON_W)
new:SetHeight(NEW_BUTTON_H)
new:SetPoint("BOTTOM", panel, "BOTTOM", 0, NEW_BUTTON_Y)
-- No client string matches camelot's PAPERDOLL_NEWEQUIPMENTSET.
new:SetText(L.EQUIPMENTMANAGER_NEW_SET)
-- Same look as the stat selectors: tertiary button, pressed while held.
ForeverUI.SkinTertiaryButton(new, true)
local plus = new:CreateTexture(nil, "OVERLAY")
ForeverUI.SetAtlas(plus, ATLAS_PLUS)
plus:SetPoint("LEFT", new, "LEFT", NEW_BUTTON_ICON_X, 0)
new:SetScript("OnClick", function()
local dialog = _G["GearManagerDialog"]
if dialog then
dialog.selectedSetName = nil
dialog.selectedSet = nil
end
if GearManagerDialogSaveSet_OnClick then
GearManagerDialogSaveSet_OnClick(_G["GearManagerDialogSaveSet"])
end
end)
panel.new = new
end
-- New Set must stay above the client dialog, which covers the pane and takes the mouse.
-- The dialog raises itself on each OnShow and this runs after it, so the level is recomputed
-- from the dialog's current level every time.
local dialog = _G["GearManagerDialog"]
if panel.new and dialog then
panel.new:SetFrameLevel(dialog:GetFrameLevel() + 5)
end
end
-- Moves the client dialog into the pane: its window art is hidden and it covers the pane,
-- holding only its cards and buttons.
local function hostDialog()
local dialog = _G["GearManagerDialog"]
if not dialog or dialog.foreverHosted then
return
end
dialog:SetParent(panel)
dialog:ClearAllPoints()
dialog:SetAllPoints(panel)
dialog:SetFrameLevel(panel:GetFrameLevel() + 1)
-- GearManagerDialog is toplevel and its OnShow calls Raise(), which would put it above
-- everything in the pane on each opening.
if dialog.SetToplevel then
dialog:SetToplevel(false)
end
local regions = { dialog:GetRegions() }
for _, region in ipairs(regions) do
if region.GetObjectType and region:GetObjectType() == "Texture" then
region:SetAlpha(0)
end
end
if dialog.title then
dialog.title:Hide()
end
dialog.foreverHosted = true
end
-- Hides the red close button, every time: the client shows it again when it reopens.
-- UIPanelDialogTemplate names it $parentClose (GearManagerDialogClose), not $parentCloseButton.
local function hideCloseButton()
for _, name in ipairs({ "GearManagerDialogClose", "GearManagerDialogCloseButton" }) do
local button = _G[name]
if button then
button:Hide()
end
end
end
local function applySkin()
local panes = ForeverUI.CharacterPanes
local pane = panes and panes.right
if not pane or not pane.stone or InCombatLockdown() then
return
end
if not panel then
build(pane)
end
-- The pane is a page of the right host: ForeverUI.Panes shows and hides it with the client
-- dialog, now its child, so it does not survive a side tab change.
hostDialog()
hideCloseButton()
placeButtons()
layoutCards()
end
-- GearManagerDialog is one of the client's UIChildWindows: CloseChildWindows hides it whenever a
-- panel opens beside the sheet, and the page loses its Equip and Save buttons. Shown again while
-- the page is on screen; out of combat only, its OnShow writes an attribute of the protected
-- CharacterFrame and calls UpdateUIPanelPositions.
hooksecurefunc("CloseChildWindows", function()
local dialog = _G["GearManagerDialog"]
if dialog and dialog.foreverHosted and panel and panel:IsVisible() and not dialog:IsShown()
and not InCombatLockdown() then
dialog:Show()
end
end)
ForeverUI.EquipmentPane = {
Apply = applySkin,
-- The page records its root once: the panel (our own frame) is built even in combat, where
-- applySkin waits, or a page first opened in combat would stay empty for the session
Frame = function()
local panes = ForeverUI.CharacterPanes
if not panel and panes and panes.right and panes.right.stone then
build(panes.right)
end
return panel
end,
}
if hooksecurefunc then
for _, name in ipairs({ "GearManagerDialog_Update", "GearManagerDialog_OnShow" }) do
if type(_G[name]) == "function" then
hooksecurefunc(name, function() applySkin() end)
end
end
end
-- ========== Edit a set
local pendingEdit = CreateFrame("Frame", "ForeverUIEquipmentEdit")
pendingEdit:Hide()
-- Gives new the rank of old in the saved order (appended if old is not there).
local function replaceInOrder(old, new)
local order = savedOrder()
local location
for rank, known in ipairs(order) do
if known == old then
order[rank] = new
location = rank
break
end
end
if not location then
location = #order + 1
order[location] = new
end
-- The new name may already be elsewhere in the order: keep only this copy.
for rank = #order, 1, -1 do
if rank ~= location and order[rank] == new then
table.remove(order, rank)
if rank < location then
location = location - 1
end
end
end
end
-- Saves the worn gear under the new name and icon, then deletes the old set.
local function finishEdit()
local e = editSession
editSession = nil
pendingEdit:Hide()
if not e then
return
end
-- SaveEquipmentSet refuses an empty name.
if not e.name or e.name == "" or not SaveEquipmentSet then
return
end
-- GearManagerDialogPopup_Update sets selectedIcon only for icons on the visible page: when the
-- set's icon is on another page it stays nil and SaveEquipmentSet fails, so the edit stops
-- before anything is deleted.
if type(e.icon) ~= "number" then
if UIErrorsFrame then
UIErrorsFrame:AddMessage(ERR_CLIENT_LOCKED_OUT, 1.0, 0.1, 0.1, 1.0)
end
return
end
local renamed = e.name ~= e.old
-- At MAX_EQUIPMENT_SETS_PER_PLAYER a rename has no free slot to save before deleting, so the
-- old set is deleted first. Its gear is worn at this point, so nothing is lost.
local cap = MAX_EQUIPMENT_SETS_PER_PLAYER or 10
local before = (GetNumEquipmentSets and GetNumEquipmentSets()) or 0
if renamed and before >= cap and DeleteEquipmentSet then
DeleteEquipmentSet(e.old)
replaceInOrder(e.old, e.name)
renamed = false
end
SaveEquipmentSet(e.name, e.icon)
-- Delete the old set only if the new one exists, so a failed save loses nothing.
if renamed and DeleteEquipmentSet then
if GetEquipmentSetInfoByName and GetEquipmentSetInfoByName(e.name) then
DeleteEquipmentSet(e.old)
replaceInOrder(e.old, e.name)
elseif UIErrorsFrame then
UIErrorsFrame:AddMessage(ERR_CLIENT_LOCKED_OUT, 1.0, 0.1, 0.1, 1.0)
end
end
local sets = characterSets()
if sets.equipped == e.old then
sets.equipped = e.name
end
local dialog = _G["GearManagerDialog"]
if dialog then
dialog.selectedSetName = e.name
end
if ForeverUI.EquipmentSetsRefresh then
ForeverUI.EquipmentSetsRefresh()
end
end
-- SaveEquipmentSet and DeleteEquipmentSet return before the client list changes; the list is
-- refreshed on EQUIPMENT_SETS_CHANGED, registered here because the client listens to it only
-- while its dialog is shown.
pendingEdit:RegisterEvent("EQUIPMENT_SETS_CHANGED")
pendingEdit:RegisterEvent("EQUIPMENT_SWAP_FINISHED")
pendingEdit:SetScript("OnEvent", function(self, event, completed, name)
if event == "EQUIPMENT_SETS_CHANGED" then
-- On the next frame: the client may not have rebuilt its list yet.
if ForeverUI.EquipmentSetsRefresh then
ForeverUI.EquipmentSetsRefresh()
end
return
end
if editSession and completed and name == editSession.old then
finishEdit()
end
end)
-- Abandon an edit whose gear swap does not finish in time (combat, locked item).
pendingEdit:SetScript("OnUpdate", function(self, elapsed)
if not editSession then
self:Hide()
return
end
editSession.rest = (editSession.rest or EDIT_TIMEOUT) - (elapsed or 0)
if editSession.rest <= 0 then
editSession = nil
self:Hide()
if UIErrorsFrame then
UIErrorsFrame:AddMessage(ERR_CLIENT_LOCKED_OUT, 1.0, 0.1, 0.1, 1.0)
end
end
end)
-- Starts an edit: equips the old set; finishEdit runs on EQUIPMENT_SWAP_FINISHED.
-- name: new set name; icon: icon index from the popup
local function startEdit(name, icon)
if not editSession or not name or name == "" then
editSession = nil
return
end
editSession.name = name
editSession.icon = icon
-- Already worn: nothing to equip.
if piecesInPlace(editSession.old) then
finishEdit()
return
end
if UseEquipmentSet then
UseEquipmentSet(editSession.old)
end
editSession.rest = EDIT_TIMEOUT
pendingEdit:Show()
end
-- The popup's Okay is replaced, not hooked: a hook would run after the client has already saved
-- the worn gear under that name. Outside an edit the client's handler runs unchanged.
local function reclaimOkay()
local okay = _G["GearManagerDialogPopupOkay"]
if not okay or okay.foreverOkay then
return
end
okay:SetScript("OnClick", function(self, button, pressed)
local popup = _G["GearManagerDialogPopup"]
if editSession and popup and popup.name and popup.name ~= "" then
-- Read the name before hiding: GearManagerDialogPopup_OnHide clears popup.name and abandons
-- the edit, hence the intendedClose flag.
local name = popup.name
-- No icon chosen, no index: the client's GetEquipmentSetIconInfo(nil) raises an error.
local iconIndex
if popup.selectedIcon then
iconIndex = select(2, GetEquipmentSetIconInfo(popup.selectedIcon))
end
local inProgress = editSession
intendedClose = true
popup:Hide()
intendedClose = nil
editSession = inProgress
startEdit(name, iconIndex)
return
end
if GearManagerDialogPopupOkay_OnClick then
GearManagerDialogPopupOkay_OnClick(self, button, pressed)
end
end)
okay.foreverOkay = true
end
-- Opens the save popup on an existing set (called by the gear button).
function ForeverUI.EquipmentSetEdit(name)
if not name or name == "" then
return
end
local dialog = _G["GearManagerDialog"]
if dialog then
dialog.selectedSetName = name
if GearManagerDialog_Update then
GearManagerDialog_Update()
end
end
editSession = { old = name }
reclaimOkay()
if GearManagerDialogSaveSet_OnClick then
GearManagerDialogSaveSet_OnClick(_G["GearManagerDialogSaveSet"])
end
-- Set the name here: the client fills the field only in RecalculateGearManagerDialogPopup,
-- which only OnShow calls.
local field = _G["GearManagerDialogPopupEditBox"]
if field then
field:SetText(name)
end
end
-- Popup closed another way (Cancel, Escape, the sheet closing): the edit is abandoned. The XML
-- binds OnHide to GearManagerDialogPopup_OnHide itself (function=), so a hook on the global
-- never runs (see Taxi.lua): the frame's script is hooked. Otherwise the stale session made
-- the next ordinary save equip the old set, save it under the typed name and delete it.
local editPopup = _G["GearManagerDialogPopup"]
if editPopup and editPopup.HookScript then
editPopup:HookScript("OnHide", function()
if editSession and not intendedClose and not pendingEdit:IsShown() then
editSession = nil
end
end)
end
-- Lays the list out again on the next frame: right after a save or delete,
-- GetNumEquipmentSets and GetEquipmentSetInfo still return the old state.
-- Requested by events and by the end of an edit; it hides itself after one run.
local listRecheck = CreateFrame("Frame", "ForeverUIEquipmentRecheck")
listRecheck:Hide()
listRecheck:SetScript("OnUpdate", function(self)
self:Hide()
if GearManagerDialog_Update then
GearManagerDialog_Update()
end
ForeverUI.EquipmentSetsLayout()
end)
function ForeverUI.EquipmentSetsRefresh()
-- Forget the known state so the next poll lays out the list, whenever the client is done.
ForeverUI.EquipmentSetsForget()
listRecheck:Show()
end