diff --git a/img/Character-Plus.blp b/img/Character-Plus.blp new file mode 100644 index 00000000..e072c6d8 Binary files /dev/null and b/img/Character-Plus.blp differ diff --git a/img/Gear_64Grey.blp b/img/Gear_64Grey.blp new file mode 100644 index 00000000..2ec67779 Binary files /dev/null and b/img/Gear_64Grey.blp differ diff --git a/img/UI-GearManager-Button-Pushed.blp b/img/UI-GearManager-Button-Pushed.blp new file mode 100644 index 00000000..2d56016e Binary files /dev/null and b/img/UI-GearManager-Button-Pushed.blp differ diff --git a/img/UI-GearManager-Button.blp b/img/UI-GearManager-Button.blp new file mode 100644 index 00000000..3e006863 Binary files /dev/null and b/img/UI-GearManager-Button.blp differ diff --git a/img/UI-GearManager-FlyoutButton.blp b/img/UI-GearManager-FlyoutButton.blp new file mode 100644 index 00000000..47e8ea8b Binary files /dev/null and b/img/UI-GearManager-FlyoutButton.blp differ diff --git a/img/UI-GearManager-ItemIntoBag.blp b/img/UI-GearManager-ItemIntoBag.blp new file mode 100644 index 00000000..0fa6f28c Binary files /dev/null and b/img/UI-GearManager-ItemIntoBag.blp differ diff --git a/img/UI-GearManager-LeaveItem-Opaque.blp b/img/UI-GearManager-LeaveItem-Opaque.blp new file mode 100644 index 00000000..09f1ec65 Binary files /dev/null and b/img/UI-GearManager-LeaveItem-Opaque.blp differ diff --git a/img/UI-GearManager-LeaveItem-Transparent.blp b/img/UI-GearManager-LeaveItem-Transparent.blp new file mode 100644 index 00000000..f8d96b7e Binary files /dev/null and b/img/UI-GearManager-LeaveItem-Transparent.blp differ diff --git a/img/UI-GearManager-Undo.blp b/img/UI-GearManager-Undo.blp new file mode 100644 index 00000000..c99497f7 Binary files /dev/null and b/img/UI-GearManager-Undo.blp differ diff --git a/init/modules.xml b/init/modules.xml index 1d6acbb5..a4caf30a 100644 --- a/init/modules.xml +++ b/init/modules.xml @@ -77,4 +77,5 @@ + \ No newline at end of file diff --git a/modules/equipmentmanager.lua b/modules/equipmentmanager.lua new file mode 100644 index 00000000..ad0e7d32 --- /dev/null +++ b/modules/equipmentmanager.lua @@ -0,0 +1,1060 @@ +-- Equipment Manager module +-- Backport of the 4.3.4 GearManagerDialog UI on top of ClassicAPI's +-- C_EquipmentSet.* API. Adds a 6th tab to CharacterFrame. + +pfUI:RegisterModule("equipmentmanager", function() + if not C_EquipmentSet or not C_EquipmentSet.CanUseEquipmentSets() then return end + + pfUI.equipmentmanager = pfUI.equipmentmanager or {} + + local SET_ROW_HEIGHT = 36 + + local rawborder, border = GetBorderSize() + local selectedSetID = nil + local pendingAction = nil -- "new" | "save" | "rename" — what the popups apply to + local slotOverlays = {} -- [invSlotID] = ignored-overlay texture on the character slot + local popoutButtons = {} -- popout arrow buttons; toggled with the EM frame + + -- Pending ignored-slot toggles per set: a slotID here means the + -- effective state is flipped from what's persisted. Committed on + -- Save, cleared on delete. + local pendingIgnoredToggles = {} + + local function GetEffectiveIgnored(setID) + local result = {} + if not setID then return result end + local persistent = C_EquipmentSet.GetIgnoredSlots(setID) or {} + for _, s in ipairs(persistent) do result[s] = true end + local pending = pendingIgnoredToggles[setID] + if pending then + for slotID in pairs(pending) do + if result[slotID] then result[slotID] = nil else result[slotID] = true end + end + end + return result + end + + local function ToggleIgnoredForSet(setID, slotID) + if not setID then return end + pendingIgnoredToggles[setID] = pendingIgnoredToggles[setID] or {} + if pendingIgnoredToggles[setID][slotID] then + pendingIgnoredToggles[setID][slotID] = nil + else + pendingIgnoredToggles[setID][slotID] = true + end + end + + local function SetPopoutReversed(popout, reversed) + if not popout then return end + local nc = reversed and popout.coordReversed or popout.coordNormal + local hc = reversed and popout.coordReversedHi or popout.coordNormalHi + popout:GetNormalTexture():SetTexCoord(unpack(nc)) + popout:GetHighlightTexture():SetTexCoord(unpack(hc)) + end + + local function EquipSet(setID) + if not setID then return end + if C_EquipmentSet.EquipmentSetContainsLockedItems(setID) then + UIErrorsFrame:AddMessage(ERR_CLIENT_LOCKED_OUT or "Locked items in set", 1, .1, .1, 1) + return + end + ClearCursor() + C_EquipmentSet.UseEquipmentSet(setID) + end + + -- ============================================================ + -- Main content frame: opens as a sidecar to the right of CharacterFrame + -- (matches ReputationDetailFrame's positioning pattern). + -- ============================================================ + + local frame = CreateFrame("Frame", "pfEquipmentManagerFrame", CharacterFrame) + frame:SetWidth(220) + frame:SetHeight(350) + frame:SetFrameStrata("HIGH") + frame:SetScript("OnShow", function() + this:ClearAllPoints() + if CharacterFrame.backdrop then + frame:SetPoint("TOPLEFT", CharacterFrame.backdrop, "TOPRIGHT", 2*border, -2) + else + frame:SetPoint("TOPLEFT", CharacterFrame, "TOPRIGHT", 0, 0) + end + end) + CreateBackdrop(frame, nil, nil, .9) + CreateBackdropShadow(frame) + frame:Hide() + + frame.title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal") + frame.title:SetPoint("TOP", frame, "TOP", 0, -10) + frame.title:SetText(T["Equipment Manager"] or "Equipment Manager") + + local closeBtn = CreateFrame("Button", nil, frame, "UIPanelCloseButton") + closeBtn:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -4, -4) + closeBtn:SetScript("OnClick", function() frame:Hide() end) + if SkinCloseButton then SkinCloseButton(closeBtn, frame.backdrop or frame, -6, -6) end + + -- ============================================================ + -- Toggle button on character pane (above the Hands slot) + -- ============================================================ + + local toggleBtn = CreateFrame("Button", "pfEqMgrToggleButton", PaperDollFrame) + toggleBtn:SetWidth(28) + toggleBtn:SetHeight(28) + toggleBtn:SetPoint("BOTTOM", CharacterHandsSlot, "TOP", 0, 4) + toggleBtn:SetNormalTexture(pfUI.path.."\\img\\UI-GearManager-Button") + toggleBtn:SetPushedTexture(pfUI.path.."\\img\\UI-GearManager-Button-Pushed") + toggleBtn:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square", "ADD") + toggleBtn:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetText(T["Equipment Manager"] or "Equipment Manager") + GameTooltip:Show() + end) + toggleBtn:SetScript("OnLeave", function() GameTooltip:Hide() end) + toggleBtn:SetScript("OnClick", function() + if frame:IsShown() then + frame:Hide() + else + frame:Show() + pfUI.equipmentmanager.Refresh() + end + end) + + local OpenNamePopup -- forward declared; assigned below + + -- ============================================================ + -- Equip + Save buttons (top of list area) + -- ============================================================ + + local btnEquip = CreateFrame("Button", "pfEqMgrEquip", frame, "UIPanelButtonTemplate") + btnEquip:SetWidth(86); btnEquip:SetHeight(22); btnEquip:SetText(T["Equip"] or "Equip") + btnEquip:SetPoint("TOPLEFT", frame, "TOPLEFT", 16, -30) + SkinButton(btnEquip) + btnEquip:SetScript("OnClick", function() EquipSet(selectedSetID) end) + + local btnSave = CreateFrame("Button", "pfEqMgrSave", frame, "UIPanelButtonTemplate") + btnSave:SetWidth(86); btnSave:SetHeight(22); btnSave:SetText(T["Save"] or "Save") + btnSave:SetPoint("LEFT", btnEquip, "RIGHT", 6, 0) + SkinButton(btnSave) + btnSave:SetScript("OnClick", function() + if not selectedSetID then return end + local name, icon = C_EquipmentSet.GetEquipmentSetInfo(selectedSetID) + if not name then return end + local targetID = selectedSetID + StaticPopupDialogs["PFUI_EQMGR_SAVE_CONFIRM"] = { + text = string.format(T["Would you like to save the equipment set '%s'?"] or "Would you like to save the equipment set '%s'?", name), + button1 = YES, button2 = NO, + OnAccept = function() + C_EquipmentSet.ClearIgnoredSlotsForSave() + local effective = GetEffectiveIgnored(targetID) + for slotID in pairs(effective) do + C_EquipmentSet.IgnoreSlotForSave(slotID) + end + C_EquipmentSet.SaveEquipmentSet(targetID, icon) + C_EquipmentSet.ClearIgnoredSlotsForSave() + pendingIgnoredToggles[targetID] = nil + pfUI.equipmentmanager.Refresh() + end, + timeout = 0, whileDead = 1, hideOnEscape = 1, exclusive = 1, + } + StaticPopup_Show("PFUI_EQMGR_SAVE_CONFIRM") + end) + + -- ============================================================ + -- Per-row context menu (single instance, repositioned per click). + -- Triggered by the gear icon on each set row. + -- ============================================================ + + local rowMenu = CreateFrame("Frame", "pfEqMgrRowMenu", UIParent) + rowMenu:SetFrameStrata("DIALOG") + rowMenu:SetWidth(140); rowMenu:SetHeight(50) + rowMenu:Hide() + CreateBackdrop(rowMenu, nil, nil, .95) + CreateBackdropShadow(rowMenu) + rowMenu:EnableMouse(true) + + -- Close on click outside via GLOBAL_MOUSE_DOWN (fires on any click + -- without consuming it). The anchorBtn check defers the gear-click + -- toggle to the gear's own OnClick so opening doesn't trigger close. + rowMenu:RegisterEvent("GLOBAL_MOUSE_DOWN") + rowMenu:SetScript("OnEvent", function() + if not this:IsShown() then return end + if MouseIsOver(this) then return end + if this.anchorBtn and MouseIsOver(this.anchorBtn) then return end + this:Hide() + end) + tinsert(UISpecialFrames, "pfEqMgrRowMenu") -- Escape closes it + + rowMenu.changeBtn = CreateFrame("Button", nil, rowMenu, "UIPanelButtonTemplate") + rowMenu.changeBtn:SetWidth(130); rowMenu.changeBtn:SetHeight(20) + rowMenu.changeBtn:SetPoint("TOPLEFT", rowMenu, "TOPLEFT", 5, -3) + rowMenu.changeBtn:SetText(T["Change Name/Icon"] or "Change Name/Icon") + SkinButton(rowMenu.changeBtn) + rowMenu.changeBtn:SetScript("OnClick", function() + rowMenu:Hide() + if not rowMenu.targetSetID then return end + local name, icon = C_EquipmentSet.GetEquipmentSetInfo(rowMenu.targetSetID) + selectedSetID = rowMenu.targetSetID + OpenNamePopup("save", name, icon) + end) + + rowMenu.deleteBtn = CreateFrame("Button", nil, rowMenu, "UIPanelButtonTemplate") + rowMenu.deleteBtn:SetWidth(130); rowMenu.deleteBtn:SetHeight(20) + rowMenu.deleteBtn:SetPoint("TOP", rowMenu.changeBtn, "BOTTOM", 0, -2) + rowMenu.deleteBtn:SetText(T["Delete"] or "Delete") + SkinButton(rowMenu.deleteBtn) + rowMenu.deleteBtn:SetScript("OnClick", function() + rowMenu:Hide() + if not rowMenu.targetSetID then return end + local targetID = rowMenu.targetSetID + local name = C_EquipmentSet.GetEquipmentSetInfo(targetID) + StaticPopupDialogs["PFUI_EQMGR_DELETE"] = { + text = string.format(T["Delete equipment set '%s'?"] or "Delete equipment set '%s'?", name or "?"), + button1 = YES, button2 = NO, + OnAccept = function() + pendingIgnoredToggles[targetID] = nil + C_EquipmentSet.DeleteEquipmentSet(targetID) + if selectedSetID == targetID then selectedSetID = nil end + pfUI.equipmentmanager.Refresh() + end, + timeout = 0, whileDead = 1, hideOnEscape = 1, exclusive = 1, + } + StaticPopup_Show("PFUI_EQMGR_DELETE") + end) + + -- ============================================================ + -- Set list (left column) + -- ============================================================ + + local LIST_VISIBLE_ROWS = 7 + local LIST_ROW_STRIDE = SET_ROW_HEIGHT + 2 + local listFrame = CreateFrame("Frame", nil, frame) + listFrame:SetPoint("TOPLEFT", frame, "TOPLEFT", 16, -58) + listFrame:SetWidth(180) + -- Height fits N rows + (N-1) inter-row gaps + 3px top/bottom padding. + listFrame:SetHeight(LIST_VISIBLE_ROWS * LIST_ROW_STRIDE + 4) + CreateBackdrop(listFrame, nil, nil, .75) + + -- Mouse-wheel scroll: list of [sets..., newSetRow] is virtualized + -- through LIST_VISIBLE_ROWS slots. Refresh() consumes listFrame.offset + -- and the wheel handler clamps + bumps it. + listFrame.offset = 0 + listFrame:EnableMouseWheel(true) + listFrame:SetScript("OnMouseWheel", function() + listFrame.offset = listFrame.offset - (arg1 or 0) + pfUI.equipmentmanager.Refresh() + end) + + local setRows = {} + local function CreateSetRow() + local row = CreateFrame("Button", nil, listFrame) + row:SetWidth(170) + row:SetHeight(SET_ROW_HEIGHT) + -- Position is set per-Refresh based on the row's visible slot; + -- start anchored to avoid uninitialized geometry before first Refresh. + row:SetPoint("TOPLEFT", listFrame, "TOPLEFT", 5, -3) + + row.icon = row:CreateTexture(nil, "ARTWORK") + row.icon:SetWidth(30) + row.icon:SetHeight(30) + row.icon:SetPoint("LEFT", row, "LEFT", 2, 0) + row.icon:SetTexCoord(.08, .92, .08, .92) + + row.text = row:CreateFontString(nil, "OVERLAY", "GameFontNormal") + row.text:SetPoint("LEFT", row.icon, "RIGHT", 6, 0) + row.text:SetPoint("RIGHT", row, "RIGHT", -22, 0) + row.text:SetJustifyH("LEFT") + + row.highlight = row:CreateTexture(nil, "BACKGROUND") + row.highlight:SetAllPoints(row) + row.highlight:SetTexture(.3, .3, .3, .4) + row.highlight:Hide() + + row.gear = CreateFrame("Button", nil, row) + row.gear:SetWidth(16); row.gear:SetHeight(16) + row.gear:SetPoint("RIGHT", row, "RIGHT", -4, 0) + row.gear.tex = row.gear:CreateTexture(nil, "ARTWORK") + row.gear.tex:SetAllPoints(row.gear) + row.gear.tex:SetTexture(pfUI.path.."\\img\\Gear_64Grey") + row.gear:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square", "ADD") + row.gear:Hide() + row.gear:SetScript("OnClick", function() + if not row.setID then return end + -- Toggle: clicking the gear that owns the open menu closes it. + -- GLOBAL_MOUSE_DOWN's anchorBtn check excludes the gear, so the + -- close has to happen here. + if rowMenu:IsShown() and rowMenu.targetSetID == row.setID then + rowMenu:Hide() + return + end + rowMenu.targetSetID = row.setID + rowMenu.anchorBtn = row.gear + rowMenu:ClearAllPoints() + rowMenu:SetPoint("TOPRIGHT", row.gear, "BOTTOMRIGHT", 0, 0) + rowMenu:Show() + end) + + row:SetScript("OnClick", function() + selectedSetID = row.setID + -- Detect double-click manually; vanilla Button has no native event. + local now = GetTime() + if row.lastClick and (now - row.lastClick) < 0.4 then + row.lastClick = nil + EquipSet(row.setID) + else + row.lastClick = now + end + pfUI.equipmentmanager.Refresh() + end) + + -- Hover handling: OnLeave fires when the cursor moves onto a child + -- (the gear button becomes the topmost mouse target). Use an + -- OnUpdate poll so the gear stays shown while the cursor is over + -- the row OR the gear itself. + row:SetScript("OnEnter", function() + if not this.setID then return end + local name = C_EquipmentSet.GetEquipmentSetInfo(this.setID) + if name then + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetEquipmentSet(name) + GameTooltip:Show() + end + row.gear:Show() + this:SetScript("OnUpdate", function() + if not MouseIsOver(this) and not MouseIsOver(row.gear) then + this:SetScript("OnUpdate", nil) + GameTooltip:Hide() + row.gear:Hide() + end + end) + end) + + return row + end + + -- Rows are allocated lazily in Refresh — ClassicAPI no longer caps + -- the number of equipment sets, so we grow the pool to numSets on + -- demand. The set list is wheel-scrolled, so only LIST_VISIBLE_ROWS + -- are ever positioned in-frame at a time, but each row is bound to a + -- specific set index (row[i] always shows ids[i]). + + local newSetRow = CreateFrame("Button", nil, listFrame) + newSetRow:SetWidth(170); newSetRow:SetHeight(SET_ROW_HEIGHT) + + newSetRow.icon = newSetRow:CreateTexture(nil, "ARTWORK") + newSetRow.icon:SetWidth(24); newSetRow.icon:SetHeight(24) + newSetRow.icon:SetPoint("LEFT", newSetRow, "LEFT", 5, 0) + newSetRow.icon:SetTexture(pfUI.path.."\\img\\Character-Plus") + + newSetRow.text = newSetRow:CreateFontString(nil, "OVERLAY", "GameFontNormal") + newSetRow.text:SetPoint("LEFT", newSetRow.icon, "RIGHT", 8, 0) + newSetRow.text:SetPoint("RIGHT", newSetRow, "RIGHT", -4, 0) + newSetRow.text:SetJustifyH("LEFT") + newSetRow.text:SetText(T["New Set"] or "New Set") + newSetRow.text:SetTextColor(0.2, 1, 0.2) + + newSetRow.highlight = newSetRow:CreateTexture(nil, "BACKGROUND") + newSetRow.highlight:SetAllPoints(newSetRow) + newSetRow.highlight:SetTexture(.3, .3, .3, .4) + newSetRow.highlight:Hide() + newSetRow:SetScript("OnEnter", function() newSetRow.highlight:Show() end) + newSetRow:SetScript("OnLeave", function() newSetRow.highlight:Hide() end) + newSetRow:SetScript("OnClick", function() OpenNamePopup("new") end) + + local function MakeButton(name, label, parent, anchor, ax, ay, width) + local b = CreateFrame("Button", name, parent, "UIPanelButtonTemplate") + b:SetWidth(width or 70) + b:SetHeight(22) + b:SetText(label) + b:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", ax, ay) + SkinButton(b) + return b + end + + -- ============================================================ + -- Name/icon entry popup + -- ============================================================ + + local namePopup = CreateFrame("Frame", "pfEqMgrNamePopup", UIParent) + namePopup:SetFrameStrata("DIALOG") + namePopup:SetWidth(472) + namePopup:SetHeight(498) + namePopup:SetPoint("CENTER", UIParent, "CENTER") + namePopup:Hide() + CreateBackdrop(namePopup, nil, nil, .9) + CreateBackdropShadow(namePopup) + namePopup:EnableMouse(true) + namePopup:SetMovable(true) + namePopup:RegisterForDrag("LeftButton") + namePopup:SetScript("OnDragStart", function() this:StartMoving() end) + namePopup:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) + + namePopup.title = namePopup:CreateFontString(nil, "OVERLAY", "GameFontNormal") + namePopup.title:SetPoint("TOP", namePopup, "TOP", 0, -10) + namePopup.title:SetText(T["Save Set"] or "Save Set") + + namePopup.nameLabel = namePopup:CreateFontString(nil, "OVERLAY", "GameFontNormal") + namePopup.nameLabel:SetPoint("TOPLEFT", namePopup, "TOPLEFT", 14, -34) + namePopup.nameLabel:SetText(T["Enter Set Name (Max 16 Characters):"] or "Enter Set Name (Max 16 Characters):") + + namePopup.editbox = CreateFrame("EditBox", "pfEqMgrNameEdit", namePopup, "InputBoxTemplate") + namePopup.editbox:SetWidth(280) + namePopup.editbox:SetHeight(20) + namePopup.editbox:SetPoint("TOPLEFT", namePopup, "TOPLEFT", 14, -52) + namePopup.editbox:SetAutoFocus(false) + namePopup.editbox:SetMaxLetters(16) + CreateBackdrop(namePopup.editbox) + + namePopup.selectedLabel = namePopup:CreateFontString(nil, "OVERLAY", "GameFontNormal") + namePopup.selectedLabel:SetPoint("TOPRIGHT", namePopup, "TOPRIGHT", -14, -28) + namePopup.selectedLabel:SetText(T["Currently Selected"] or "Currently Selected") + namePopup.selectedLabel:SetTextColor(1, 0.82, 0) + + namePopup.selectedPreview = CreateFrame("Frame", nil, namePopup) + namePopup.selectedPreview:SetWidth(42) + namePopup.selectedPreview:SetHeight(42) + namePopup.selectedPreview:SetPoint("TOPRIGHT", namePopup, "TOPRIGHT", -14, -44) + CreateBackdrop(namePopup.selectedPreview) + namePopup.selectedPreview.tex = namePopup.selectedPreview:CreateTexture(nil, "ARTWORK") + namePopup.selectedPreview.tex:SetAllPoints(namePopup.selectedPreview) + namePopup.selectedPreview.tex:SetTexCoord(.08, .92, .08, .92) + + namePopup.iconLabel = namePopup:CreateFontString(nil, "OVERLAY", "GameFontNormal") + namePopup.iconLabel:SetPoint("TOPLEFT", namePopup, "TOPLEFT", 14, -100) + namePopup.iconLabel:SetText(T["Choose an Icon:"] or "Choose an Icon:") + + -- Icon picker: 10×8 grid of buttons + scroll + local ICON_GRID_COLS = 10 + local ICON_GRID_ROWS = 8 + local ICON_BTN_SIZE = 36 + local ICON_BTN_PAD = 6 + + local iconScroll = CreateFrame("ScrollFrame", "pfEqMgrIconScroll", namePopup, "FauxScrollFrameTemplate") + iconScroll:SetPoint("TOPLEFT", namePopup, "TOPLEFT", 14, -130) + iconScroll:SetWidth(ICON_GRID_COLS * (ICON_BTN_SIZE + ICON_BTN_PAD) - ICON_BTN_PAD) + iconScroll:SetHeight(ICON_GRID_ROWS * (ICON_BTN_SIZE + ICON_BTN_PAD) - ICON_BTN_PAD) + + -- IconDataProviderMixin owns the icon DB, dedup, and lazy load. + -- Init on first picker open; release on hide so the cache GCs. + local provider = nil + -- Selection is tracked by PATH (not index) so it survives filter + -- changes: a spell icon you picked still saves correctly even after + -- you switch the filter to "Items" and it's no longer in the visible list. + local QUESTION_MARK = "INTERFACE\\ICONS\\INV_MISC_QUESTIONMARK" + local selectedIconPath = QUESTION_MARK + + local function EnsureProvider() + if not provider then + provider = CreateAndInitFromMixin(IconDataProviderMixin, + IconDataProviderExtraType.Equipment) + end + end + + -- Anchor scrollbar to iconScroll's right edge so its position tracks + -- the icon grid rather than the popup. -16/+16 vertical insets are the + -- standard up/down arrow spacing for UIPanelScrollBarTemplate. + local scrollbar = _G["pfEqMgrIconScrollScrollBar"] + if scrollbar then + scrollbar:ClearAllPoints() + scrollbar:SetPoint("TOPLEFT", iconScroll, "TOPRIGHT", 8, -16) + scrollbar:SetPoint("BOTTOMLEFT", iconScroll, "BOTTOMRIGHT", 8, 16) + SkinScrollbar(scrollbar) + end + + -- Filter dropdown: "All Icons" / "Spells" / "Items" (top right of icon area). + local filterDropdown = CreateFrame("Frame", "pfEqMgrIconFilter", namePopup, "UIDropDownMenuTemplate") + filterDropdown:SetPoint("TOPRIGHT", namePopup, "TOPRIGHT", 0, -94) + local currentFilter = "all" + local function ApplyFilter(value) + currentFilter = value + UIDropDownMenu_SetSelectedValue(filterDropdown, value) + if provider then + if value == "spells" then provider:SetIconTypes({ IconDataProviderIconType.Spell }) + elseif value == "items" then provider:SetIconTypes({ IconDataProviderIconType.Item }) + else provider:SetIconTypes(nil) end + -- Don't reset selection — selectedIconPath persists. If the + -- selected icon isn't in the new filter, no grid entry will be + -- highlighted but Save will still write the chosen icon. + pfUI.equipmentmanager.RefreshIconGrid() + end + end + UIDropDownMenu_Initialize(filterDropdown, function() + local info + info = {}; info.text = T["All Icons"] or "All Icons"; info.value = "all" + info.func = function() ApplyFilter("all") end + info.checked = currentFilter == "all" + UIDropDownMenu_AddButton(info) + info = {}; info.text = T["Spells"] or "Spells"; info.value = "spells" + info.func = function() ApplyFilter("spells") end + info.checked = currentFilter == "spells" + UIDropDownMenu_AddButton(info) + info = {}; info.text = T["Items"] or "Items"; info.value = "items" + info.func = function() ApplyFilter("items") end + info.checked = currentFilter == "items" + UIDropDownMenu_AddButton(info) + end) + UIDropDownMenu_SetWidth(120, filterDropdown) + UIDropDownMenu_SetSelectedValue(filterDropdown, "all") + SkinDropDown(filterDropdown) + + local iconButtons = {} + for r = 1, ICON_GRID_ROWS do + for c = 1, ICON_GRID_COLS do + local i = (r - 1) * ICON_GRID_COLS + c + local btn = CreateFrame("Button", nil, namePopup) + btn:SetWidth(ICON_BTN_SIZE) + btn:SetHeight(ICON_BTN_SIZE) + btn:SetPoint("TOPLEFT", iconScroll, "TOPLEFT", (c-1) * (ICON_BTN_SIZE + ICON_BTN_PAD), -(r-1) * (ICON_BTN_SIZE + ICON_BTN_PAD)) + CreateBackdrop(btn) + btn.texture = btn:CreateTexture(nil, "ARTWORK") + btn.texture:SetAllPoints(btn) + btn.texture:SetTexCoord(.08, .92, .08, .92) + btn.gridIndex = i + btn:SetScript("OnClick", function() + if this.iconIndex and provider then + local path = provider:GetIconByIndex(this.iconIndex) + if path then selectedIconPath = path end + pfUI.equipmentmanager.RefreshIconGrid() + end + end) + btn:SetScript("OnEnter", function() + if not this.iconIndex or not provider then return end + local path = provider:GetIconByIndex(this.iconIndex) + if type(path) == "string" then + local name = string.gsub(path, "^.-INTERFACE\\\\ICONS\\\\", "") + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetText(name) + GameTooltip:Show() + end + end) + btn:SetScript("OnLeave", GameTooltip_Hide) + iconButtons[i] = btn + end + end + + function pfUI.equipmentmanager.RefreshIconGrid() + EnsureProvider() + local numIcons = provider:GetNumIcons() + local numRows = math.ceil(numIcons / ICON_GRID_COLS) + FauxScrollFrame_Update(iconScroll, numRows, ICON_GRID_ROWS, ICON_BTN_SIZE + ICON_BTN_PAD) + local offset = FauxScrollFrame_GetOffset(iconScroll) + for i = 1, ICON_GRID_ROWS * ICON_GRID_COLS do + local listIdx = i + offset * ICON_GRID_COLS + local btn = iconButtons[i] + if listIdx <= numIcons then + btn:Show() + btn.iconIndex = listIdx + local path = provider:GetIconByIndex(listIdx) + btn.texture:SetTexture(path) + if path == selectedIconPath then + btn.backdrop:SetBackdropBorderColor(1, 0.82, 0, 1) + else + btn.backdrop:SetBackdropBorderColor(pfUI.cache.er, pfUI.cache.eg, pfUI.cache.eb, pfUI.cache.ea) + end + else + btn:Hide() + btn.iconIndex = nil + end + end + -- Sync the "Currently Selected" preview from the path directly so it + -- still shows the chosen icon when filtered out of the grid. + namePopup.selectedPreview.tex:SetTexture(selectedIconPath) + end + + iconScroll:SetScript("OnVerticalScroll", function() + FauxScrollFrame_OnVerticalScroll(ICON_BTN_SIZE + ICON_BTN_PAD, function() pfUI.equipmentmanager.RefreshIconGrid() end) + end) + + namePopup:SetScript("OnHide", function() + if provider then provider:Release(); provider = nil end + end) + + local btnPopupOK = MakeButton("pfEqMgrPopupOK", T["OK"] or "OK", namePopup, namePopup, 14, -340, 80) + btnPopupOK:ClearAllPoints() + btnPopupOK:SetPoint("BOTTOMLEFT", namePopup, "BOTTOMLEFT", 14, 12) + + local btnPopupCancel = MakeButton("pfEqMgrPopupCancel", T["Cancel"] or "Cancel", namePopup, namePopup, 0, 0, 80) + btnPopupCancel:ClearAllPoints() + btnPopupCancel:SetPoint("BOTTOMRIGHT", namePopup, "BOTTOMRIGHT", -14, 12) + btnPopupCancel:SetScript("OnClick", function() namePopup:Hide() end) + + btnPopupOK:SetScript("OnClick", function() + local name = namePopup.editbox:GetText() + if not name or name == "" then return end + -- Strip the prefix to match ClassicAPI's persisted short-form basenames. + local iconForSave = string.gsub(selectedIconPath, "INTERFACE\\ICONS\\", "") + if pendingAction == "new" then + C_EquipmentSet.CreateEquipmentSet(name, iconForSave) + C_EquipmentSet.ClearIgnoredSlotsForSave() + selectedSetID = C_EquipmentSet.GetEquipmentSetID(name) + elseif pendingAction == "save" and selectedSetID then + -- Commit pending ignored toggles: load the effective list into + -- ClassicAPI's session state, then SaveEquipmentSet captures it. + C_EquipmentSet.ClearIgnoredSlotsForSave() + local effective = GetEffectiveIgnored(selectedSetID) + for slotID in pairs(effective) do + C_EquipmentSet.IgnoreSlotForSave(slotID) + end + C_EquipmentSet.SaveEquipmentSet(selectedSetID, iconForSave) + C_EquipmentSet.ClearIgnoredSlotsForSave() + pendingIgnoredToggles[selectedSetID] = nil + elseif pendingAction == "rename" and selectedSetID then + C_EquipmentSet.ModifyEquipmentSet(selectedSetID, name) + end + namePopup:Hide() + pfUI.equipmentmanager.Refresh() + end) + + -- Assignment (not `local function`) so this fills in the forward + -- declaration at the top of the module — closures created earlier + -- (row gear menu, "+ New Set" row, etc.) capture the same upvalue. + function OpenNamePopup(action, prefillName, prefillIcon) + pendingAction = action + namePopup.editbox:SetText(prefillName or "") + EnsureProvider() + if prefillIcon then + local short = string.gsub(prefillIcon, "INTERFACE\\ICONS\\", "") + selectedIconPath = "INTERFACE\\ICONS\\" .. strupper(short) + else + selectedIconPath = QUESTION_MARK + end + if action == "rename" then + namePopup.title:SetText(T["Rename Set"] or "Rename Set") + iconScroll:Hide() + for _, b in ipairs(iconButtons) do b:Hide() end + namePopup.iconLabel:Hide() + namePopup.selectedLabel:Hide() + namePopup.selectedPreview:Hide() + filterDropdown:Hide() + else + namePopup.title:SetText(action == "new" and (T["Name Set"] or "Name Set") or (T["Save Set"] or "Save Set")) + iconScroll:Show() + namePopup.iconLabel:Show() + namePopup.selectedLabel:Show() + namePopup.selectedPreview:Show() + filterDropdown:Show() + end + namePopup:Show() + if action ~= "rename" then pfUI.equipmentmanager.RefreshIconGrid() end + namePopup.editbox:SetFocus() + end + + -- ============================================================ + -- Equipment flyout (per-slot popup) + -- ============================================================ + + local flyout = CreateFrame("Frame", "pfEqMgrFlyout", UIParent) + flyout:SetFrameStrata("DIALOG") + flyout:Hide() + CreateBackdrop(flyout, nil, nil, .9) + flyout.buttons = {} + + local FLYOUT_BTN_SIZE = 36 + local FLYOUT_COLS = 5 + -- Sentinels for the flyout's virtual entries (negative values never + -- collide with real packed locations from GetInventoryItemsForSlot). + local PLACEINBAGS_LOCATION = -1 + local IGNORESLOT_LOCATION = -2 -- "ignore this slot" — shown when not ignored + local UNIGNORESLOT_LOCATION = -3 -- "un-ignore this slot" — shown when ignored + + -- Find first empty bag slot and drop the cursor item into it. + local function UnequipToBags(invSlot) + if not GetInventoryItemID("player", invSlot) then return end + ClearCursor() + PickupInventoryItem(invSlot) + if not CursorHasItem() then return end + for bag = 0, 4 do + local nslots = GetContainerNumSlots(bag) or 0 + for slot = 1, nslots do + if not C_Container.GetContainerItemID(bag, slot) then + PickupContainerItem(bag, slot) + return + end + end + end + ClearCursor() + UIErrorsFrame:AddMessage(EQUIPMENT_MANAGER_BAGS_FULL or "Your bags are full.", 1, .1, .1, 1) + end + + local function MakeFlyoutButton(i) + local b = CreateFrame("Button", nil, flyout) + b:SetWidth(FLYOUT_BTN_SIZE) + b:SetHeight(FLYOUT_BTN_SIZE) + CreateBackdrop(b) + b.texture = b:CreateTexture(nil, "ARTWORK") + b.texture:SetAllPoints(b) + b.texture:SetTexCoord(.08, .92, .08, .92) + b.count = b:CreateFontString(nil, "OVERLAY", "NumberFontNormal") + b.count:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", -2, 2) + b:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + if this.specialAction == "placeInBags" then + GameTooltip:SetText(EQUIPMENT_MANAGER_PLACE_IN_BAGS or "Place in Bags", 1, 1, 1) + elseif this.specialAction == "ignore" then + GameTooltip:SetText(EQUIPMENT_MANAGER_IGNORE_SLOT or "Ignore this slot", 1, 1, 1) + elseif this.specialAction == "unignore" then + GameTooltip:SetText(EQUIPMENT_MANAGER_UNIGNORE_SLOT or "Stop ignoring this slot", 1, 1, 1) + elseif this.bag then + GameTooltip:SetBagItem(this.bag, this.slot) + elseif this.invSlot then + GameTooltip:SetInventoryItem("player", this.invSlot) + end + GameTooltip:Show() + end) + b:SetScript("OnLeave", function() GameTooltip:Hide() end) + b:SetScript("OnClick", function() + if this.specialAction == "placeInBags" then + UnequipToBags(flyout.targetInvSlot) + flyout:Hide() + return + elseif this.specialAction == "ignore" or this.specialAction == "unignore" then + ToggleIgnoredForSet(selectedSetID, flyout.targetInvSlot) + flyout:Hide() + pfUI.equipmentmanager.Refresh() + return + end + ClearCursor() + if this.bag then + PickupContainerItem(this.bag, this.slot) + elseif this.invSlot then + PickupInventoryItem(this.invSlot) + end + if CursorHasItem() then + PickupInventoryItem(flyout.targetInvSlot) + end + flyout:Hide() + end) + return b + end + + function pfUI.equipmentmanager.ShowFlyout(slotBtn) + -- Accept either the EM grid btn (has .slotID) or a vanilla + -- CharacterXxxxSlot button (use GetID()). + local invSlot = slotBtn.slotID or slotBtn:GetID() + + -- ClassicAPI's GetInventoryItemsForSlot does the eligibility filter + -- (invType → slot compatibility, 2H/finger/trinket rules) for us; + -- the result is `{[packedLocation] = itemLink}`. + local items = {} + GetInventoryItemsForSlot(invSlot, items) + -- Skip the item currently equipped in this slot (it would be a no-op swap). + items[invSlot + ITEM_INVENTORY_LOCATION_PLAYER] = nil + + -- Special buttons go FIRST (ignore/un-ignore, then place-in-bags), + -- with eligible items appended after. Layout matches modern WoW. + local ordered = {} + if selectedSetID then + local effective = GetEffectiveIgnored(selectedSetID) + table.insert(ordered, effective[invSlot] and UNIGNORESLOT_LOCATION or IGNORESLOT_LOCATION) + end + if GetInventoryItemID("player", invSlot) then + table.insert(ordered, PLACEINBAGS_LOCATION) + end + -- Eligible items, sorted by packed location for stable display. + local itemLocations = {} + for location in pairs(items) do + table.insert(itemLocations, location) + end + table.sort(itemLocations) + for _, location in ipairs(itemLocations) do + table.insert(ordered, location) + end + + flyout.targetInvSlot = invSlot + local num = table.getn(ordered) + while table.getn(flyout.buttons) < num do + flyout.buttons[table.getn(flyout.buttons) + 1] = MakeFlyoutButton(table.getn(flyout.buttons) + 1) + end + for i, b in ipairs(flyout.buttons) do + if i <= num then + local location = ordered[i] + b.specialAction = nil; b.bag = nil; b.slot = nil; b.invSlot = nil + if location == PLACEINBAGS_LOCATION then + b.specialAction = "placeInBags" + b.texture:SetTexture(pfUI.path.."\\img\\UI-GearManager-ItemIntoBag") + b.count:SetText("") + elseif location == IGNORESLOT_LOCATION then + b.specialAction = "ignore" + b.texture:SetTexture(pfUI.path.."\\img\\UI-GearManager-LeaveItem-Opaque") + b.count:SetText("") + elseif location == UNIGNORESLOT_LOCATION then + b.specialAction = "unignore" + b.texture:SetTexture(pfUI.path.."\\img\\UI-GearManager-Undo") + b.count:SetText("") + else + local loc = EquipmentManager_GetLocationData(location) + if loc.isBags then + b.bag = loc.bag; b.slot = loc.slot + local tex, count = GetContainerItemInfo(loc.bag, loc.slot) + b.texture:SetTexture(tex) + if count and count > 1 then b.count:SetText(count) else b.count:SetText("") end + else -- isPlayer (equipped in some other slot) + b.invSlot = loc.slot + b.texture:SetTexture(GetInventoryItemTexture("player", loc.slot)) + b.count:SetText("") + end + end + local col = math.mod(i - 1, FLYOUT_COLS) + local row = math.floor((i - 1) / FLYOUT_COLS) + b:ClearAllPoints() + b:SetPoint("TOPLEFT", flyout, "TOPLEFT", 4 + col * (FLYOUT_BTN_SIZE + 2), -4 - row * (FLYOUT_BTN_SIZE + 2)) + b:Show() + else + b:Hide() + end + end + + if num == 0 then + flyout:Hide() + UIErrorsFrame:AddMessage(T["No matching items in bags"] or "No matching items in bags", 1, 1, 0, 1) + return + end + + local cols = math.min(num, FLYOUT_COLS) + local rows = math.ceil(num / FLYOUT_COLS) + flyout:SetWidth(cols * (FLYOUT_BTN_SIZE + 2) + 6) + flyout:SetHeight(rows * (FLYOUT_BTN_SIZE + 2) + 6) + flyout:ClearAllPoints() + -- Anchor to the popout button so the flyout sits past it (avoids + -- overlapping the chevron). Falls back to the slot itself for the + -- EM grid btns which don't have a separate popout. + local anchorBtn = slotBtn.popout or slotBtn + if invSlot == 16 or invSlot == 17 or invSlot == 18 then + flyout:SetPoint("BOTTOMLEFT", anchorBtn, "TOPLEFT", 0, 4) + else + local centerX = (anchorBtn:GetLeft() or 0) + anchorBtn:GetWidth()/2 + if centerX > GetScreenWidth()/2 then + flyout:SetPoint("TOPRIGHT", anchorBtn, "TOPLEFT", -4, 0) + else + flyout:SetPoint("TOPLEFT", anchorBtn, "TOPRIGHT", 4, 0) + end + end + -- Reverse the chevron on the active popout, restore the previously + -- active one (if any). flyout.currentPopout drives OnHide cleanup. + if flyout.currentPopout and flyout.currentPopout ~= slotBtn.popout then + SetPopoutReversed(flyout.currentPopout, false) + end + flyout.currentPopout = slotBtn.popout + SetPopoutReversed(slotBtn.popout, true) + flyout:Show() + end + + flyout:SetScript("OnHide", function() + this.targetInvSlot = nil + if this.currentPopout then + SetPopoutReversed(this.currentPopout, false) + this.currentPopout = nil + end + end) + + -- ============================================================ + -- Paperdoll slot setup: ignored overlay + popout button per slot. + -- ============================================================ + + local CHAR_SLOT_NAMES = { + "HeadSlot", "NeckSlot", "ShoulderSlot", "BackSlot", "ChestSlot", + "ShirtSlot", "TabardSlot", "WristSlot", "HandsSlot", "WaistSlot", + "LegsSlot", "FeetSlot", "Finger0Slot", "Finger1Slot", + "Trinket0Slot", "Trinket1Slot", "MainHandSlot", "SecondaryHandSlot", + "RangedSlot", + } + -- Slot ID classification for popout positioning. Weapons sit at the + -- bottom of the paperdoll, so their popout goes on top (chevron up). + -- All other slots get the popout on their right side, chevron right. + local UP_ARROW_SLOTS = { [16]=1, [17]=1, [18]=1 } + local POPOUT_TEX = pfUI.path.."\\img\\UI-GearManager-FlyoutButton" + + for _, slotName in ipairs(CHAR_SLOT_NAMES) do + local slot = _G["Character"..slotName] + if slot then + -- "Ignored" overlay rendered on top of the paperdoll slot icon. + -- pfUI loads modules BEFORE skins (pfUI.lua:407-418), so at this + -- point slot.backdrop doesn't exist yet — the character skin + -- creates it later and reparents the item icon to it. A plain + -- texture on `slot` would render BENEATH the backdrop's icon. + -- Wrap the texture in a frame with a higher frame level so it + -- renders above the backdrop regardless of skin-init order. + local overlayFrame = CreateFrame("Frame", nil, slot) + overlayFrame:SetAllPoints(slot) + overlayFrame:SetFrameLevel(slot:GetFrameLevel() + 10) + local overlay = overlayFrame:CreateTexture(nil, "OVERLAY") + overlay:SetAllPoints(overlayFrame) + overlay:SetTexture(pfUI.path.."\\img\\UI-GearManager-LeaveItem-Transparent") + overlayFrame:Hide() + slotOverlays[slot:GetID()] = overlayFrame + + -- Popout arrow button — clicking opens the equipment flyout for + -- this slot. Replaces the Alt+click trigger with a discoverable + -- visual affordance matching modern WoW's paperdoll. + local invSlot = slot:GetID() + local popout = CreateFrame("Button", nil, slot) + popout:SetFrameLevel(slot:GetFrameLevel() + 1) + -- Match Blizzard's EquipmentFlyoutPopoutButtonTemplate: 16x32, + -- anchored LEFT to slot's RIGHT (chevron points away from slot + -- toward the flyout's opening side). + popout:SetWidth(16); popout:SetHeight(32) + if UP_ARROW_SLOTS[invSlot] then + -- Weapon row: popout on TOP, vertical orientation. + popout:SetWidth(32); popout:SetHeight(16) + popout:SetPoint("BOTTOM", slot, "TOP", 0, 0) + else + -- All horizontal slots get the popout on the RIGHT edge, chevron + -- points right. Right-column slot popouts extend slightly into + -- the EM sidecar gutter — acceptable, matches modern UX. + popout:SetPoint("LEFT", slot, "RIGHT", 0, 0) + end + popout:SetNormalTexture(POPOUT_TEX) + popout:SetHighlightTexture(POPOUT_TEX, "ADD") + -- Stash both texCoord variants (normal=closed, reversed=open) so + -- SetPopoutReversed can swap between them. Reversed = swap all + -- y-values 0↔0.5 / 0.5↔1, matching modern WoW. + if UP_ARROW_SLOTS[invSlot] then + -- Closed: chevron points UP (toward where the flyout will open). + popout.coordNormal = { 0.15625, 0.84375, 0, 0.5 } + popout.coordReversed = { 0.15625, 0.84375, 0.5, 0 } + popout.coordNormalHi = { 0.15625, 0.84375, 0.5, 1 } + popout.coordReversedHi = { 0.15625, 0.84375, 1, 0.5 } + else + popout.coordNormal = { 0.15625, 0.5, 0.84375, 0.5, 0.15625, 0, 0.84375, 0 } + popout.coordReversed = { 0.15625, 0, 0.84375, 0, 0.15625, 0.5, 0.84375, 0.5 } + popout.coordNormalHi = { 0.15625, 1, 0.84375, 1, 0.15625, 0.5, 0.84375, 0.5 } + popout.coordReversedHi = { 0.15625, 0.5, 0.84375, 0.5, 0.15625, 1, 0.84375, 1 } + end + SetPopoutReversed(popout, false) + slot.popout = popout + popout:SetScript("OnClick", function() + -- Toggle: if the flyout is showing for this same slot, hide it; + -- otherwise (re)open it for the clicked slot. + if flyout:IsShown() and flyout.targetInvSlot == invSlot then + flyout:Hide() + else + pfUI.equipmentmanager.ShowFlyout(slot) + end + end) + popout:Hide() + table.insert(popoutButtons, popout) + end + end + + -- Tie popout visibility to the EM sidecar. They appear when the + -- sidecar opens, hide when it closes, and the flyout closes too. + HookScript(frame, "OnShow", function() + for _, b in ipairs(popoutButtons) do b:Show() end + end) + HookScript(frame, "OnHide", function() + for _, b in ipairs(popoutButtons) do b:Hide() end + if flyout then flyout:Hide() end + end) + + -- ============================================================ + -- Refresh + -- ============================================================ + + function pfUI.equipmentmanager.Refresh() + local ids = C_EquipmentSet.GetEquipmentSetIDs() or {} + local numSets = table.getn(ids) + + -- Auto-select the first set if none is selected (or the previously + -- selected one was deleted). Falls through to "show equipped" only + -- when zero sets exist. + if numSets > 0 then + local stillExists = false + if selectedSetID then + for _, id in ipairs(ids) do + if id == selectedSetID then stillExists = true; break end + end + end + if not stillExists then selectedSetID = ids[1] end + else + selectedSetID = nil + end + + -- Virtual list = sets ++ newSetRow. Clamp the current scroll + -- offset against the visible window before laying out rows so + -- deleting sets snaps the list back into view. + local totalItems = numSets + 1 + local maxOffset = math.max(0, totalItems - LIST_VISIBLE_ROWS) + if listFrame.offset > maxOffset then listFrame.offset = maxOffset end + if listFrame.offset < 0 then listFrame.offset = 0 end + local off = listFrame.offset + + -- Set rows: lazy-grow the pool to numSets, then position visible + -- ones into slots and hide the rest. Each row stays bound to a + -- specific set index (row[i] always shows ids[i]). + for i = 1, numSets do + if not setRows[i] then setRows[i] = CreateSetRow() end + local row = setRows[i] + local slot = i - off + if slot >= 1 and slot <= LIST_VISIBLE_ROWS then + local setID = ids[i] + local name, icon, _, isEquipped, _, _, _, numMissing = C_EquipmentSet.GetEquipmentSetInfo(setID) + row.setID = setID + -- Color precedence: missing items (red) > equipped (green) > normal (white). + local color = (numMissing and numMissing > 0) and "|cffff5555" + or isEquipped and "|cff33ff33" or "|cffffffff" + row.text:SetText(color .. (name or "?") .. "|r") + if icon then + local full = string.find(icon, "\\") and icon or ("Interface\\Icons\\" .. icon) + row.icon:SetTexture(full) + else + row.icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark") + end + if setID == selectedSetID then row.highlight:Show() else row.highlight:Hide() end + row:ClearAllPoints() + row:SetPoint("TOPLEFT", listFrame, "TOPLEFT", 5, -3 - (slot - 1) * LIST_ROW_STRIDE) + row:Show() + else + row:Hide() + row.setID = nil + end + end + -- Hide pool entries past the current set count (left over from a + -- previous Refresh with more sets — e.g. just deleted one). + for i = numSets + 1, table.getn(setRows) do + setRows[i]:Hide() + setRows[i].setID = nil + end + + -- "+ New Set" row occupies virtual index numSets+1. + local newSlot = (numSets + 1) - off + if newSlot >= 1 and newSlot <= LIST_VISIBLE_ROWS then + newSetRow:ClearAllPoints() + newSetRow:SetPoint("TOPLEFT", listFrame, "TOPLEFT", 5, -3 - (newSlot - 1) * LIST_ROW_STRIDE) + newSetRow:Show() + else + newSetRow:Hide() + end + + -- Update the ignored-overlay textures attached to each paperdoll + -- slot. Uses effective state (persistent + pending toggles) so + -- uncommitted user changes show immediately. + local setIgnored = selectedSetID and GetEffectiveIgnored(selectedSetID) or {} + for slotID, overlay in pairs(slotOverlays) do + if selectedSetID and setIgnored[slotID] then overlay:Show() else overlay:Hide() end + end + + local hasSelection = selectedSetID and true or false + if hasSelection then btnEquip:Enable(); btnSave:Enable() + else btnEquip:Disable(); btnSave:Disable() end + end + + -- ============================================================ + -- Events + -- ============================================================ + + local events = CreateFrame("Frame") + events:RegisterEvent("EQUIPMENT_SETS_CHANGED") + events:RegisterEvent("EQUIPMENT_SWAP_FINISHED") + events:RegisterEvent("EQUIPMENT_SWAP_PENDING") + events:RegisterEvent("BAG_UPDATE_DELAYED") -- ClassicAPI debounced; ~4x fewer refreshes than BAG_UPDATE + events:RegisterEvent("UNIT_INVENTORY_CHANGED") + events:SetScript("OnEvent", function() + if not frame:IsShown() then return end + if event == "UNIT_INVENTORY_CHANGED" and arg1 ~= "player" then return end + pfUI.equipmentmanager.Refresh() + end) + + -- Hide the sidecar + flyout when the character pane closes. + HookScript(CharacterFrame, "OnHide", function() + frame:Hide() + flyout:Hide() + end) +end) diff --git a/pfUI.lua b/pfUI.lua index 0e9425b3..0dbcc078 100644 --- a/pfUI.lua +++ b/pfUI.lua @@ -26,7 +26,7 @@ pfUI.bootup = true -- ClassicAPI dependency check. -- pfUI relies pervasively on the modern C_* / SuperWoW / nameplate / focus -- API surface that ClassicAPI polyfills, so presence is required. -local PFUI_CLASSIC_API_MIN = 10202 -- v1.2.2 (X*10000 + Y*100 + Z) +local PFUI_CLASSIC_API_MIN = 10306 -- v1.3.4 (X*10000 + Y*100 + Z) if not CLASSIC_API_VERSION or CLASSIC_API_VERSION < PFUI_CLASSIC_API_MIN then pfUI.disabled = true