ca2b78e217
/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>
908 lines
29 KiB
Lua
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
|