mirror of
https://github.com/brues-code/pfUI.git
synced 2026-10-27 22:30:48 +00:00
* Add equipment manager auto-switching and "Unused" items panel Equipment manager (modules/equipmentmanager.lua): - Add automatic set switching: ordered triggers (mounted, swimming, eating/drinking, shapeshift form/stance, zone, instance type, time of day, resting, stealthed, targeting a boss) equip a set; topmost matching trigger wins, with a 0.5s debounce - Never swap gear in combat, while dead or casting; optionally restore the previous gear via a hidden "pfUI Auto-Restore" set - Add an "Unused" side panel listing equippable items in bags, bank and paperdoll that no set uses, with an ignore list (tools and quest disguises pre-ignored), bank snapshot and bag highlighting - Persist triggers, ignore list and bank snapshot in new per-character SavedVariable pfUI_eqmgr (all three .toc files) - Add Character > Equipment Manager options (autoswitch, announce, minimum item quality, include bank) in gui.lua and config.lua - Hide ignored-slot overlays when the manager frame is closed Add frFR translations for the new equipment manager strings. * equipmentmanager: lean on ClassicAPI for unused scan and autoswitch - Unused panel: list items via GetInventoryItemsForSlot instead of hand-packing locations and walking the bags. - Bank snapshot: record set membership while the bank is open. GetItemLocations reports bank items as missing once it closes, so set items in the bank were listed as unused. - Autoswitch: check CanUseEquipmentSets and the UseEquipmentSet result before marking a trigger applied, and retry when the swap is blocked. - Drive the engine from events (aura, form, zone, resting, target, regen) instead of a 4 Hz ticker; poll only swimming, time and eating. - Detect eating/drinking by the localized names of the Food (433) and Drink (430) auras instead of hardcoded locale strings and icon paths. - Drop presence guards on APIs that always exist; use strsplittable and strtrim for trigger values. * equipmentmanager: wake the dining trigger on player health/mana events Drop dining from the poll: listen to UNIT_HEALTH/UNIT_MANA for the player while a dining trigger is enabled, so taking the set off once full is event-driven. Register UNIT_AURA for the player only. * equipmentmanager: split trigger values with strsplit strsplittable needs ClassicAPI 1.17.11; strsplit has been there since 1.7.3, so this keeps the minimum where it is. * equipmentmanager: let the trigger hint wrap --------- Co-authored-by: Duciel <336374+Duciel@users.noreply.github.com> Co-authored-by: Brues <5278969+brues-code@users.noreply.github.com>
2183 lines
84 KiB
Lua
2183 lines
84 KiB
Lua
-- Equipment Manager module
|
|
-- Backport of the 4.3.4 GearManagerDialog UI on top of ClassicAPI's C_EquipmentSet.* API.
|
|
|
|
pfUI:RegisterModule("equipmentmanager", function()
|
|
if not C_EquipmentSet or not C_EquipmentSet.CanUseEquipmentSets() then return end
|
|
|
|
pfUI.equipmentmanager = {}
|
|
|
|
-- Per-character storage for the features ClassicAPI doesn't persist
|
|
-- itself: automatic-switch triggers (they reference this character's
|
|
-- setIDs), the "not in any set" ignore list and a snapshot of the bank
|
|
-- so bank items can be listed while the bank is closed.
|
|
_G.pfUI_eqmgr = _G.pfUI_eqmgr or {}
|
|
local db = _G.pfUI_eqmgr
|
|
db.triggers = db.triggers or {}
|
|
db.nextID = db.nextID or 1
|
|
db.bank = db.bank or {}
|
|
if not db.ignore then
|
|
-- Tools and quest disguises that are equippable but never belong in a
|
|
-- set (same list Outfitter ships). Users can un-ignore them.
|
|
db.ignore = {
|
|
[2901] = true, -- Mining Pick
|
|
[5956] = true, -- Blacksmith Hammer
|
|
[6219] = true, -- Arclight Spanner
|
|
[7005] = true, -- Skinning Knife
|
|
[7297] = true, -- Morbent's Bane
|
|
[10696] = true, -- Enchanted Azsharite Felbane Sword
|
|
[10697] = true, -- Enchanted Azsharite Felbane Dagger
|
|
[10698] = true, -- Enchanted Azsharite Felbane Staff
|
|
[20406] = true, -- Twilight Cultist Mantle
|
|
[20407] = true, -- Twilight Cultist Robe
|
|
[20408] = true, -- Twilight Cultist Cowl
|
|
}
|
|
end
|
|
|
|
-- Hidden set used by the trigger engine to remember the gear worn before
|
|
-- a trigger kicked in ("restore previous gear"). It's GUID-based like any
|
|
-- other set, so restoring is a single atomic UseEquipmentSet. Filtered
|
|
-- out of every list this module shows and deleted once it's restored.
|
|
local RESTORE_SET_NAME = "pfUI Auto-Restore"
|
|
|
|
local function IsRestoreSet(setID)
|
|
return setID and C_EquipmentSet.GetEquipmentSetInfo(setID) == RESTORE_SET_NAME
|
|
end
|
|
|
|
-- All set IDs except the hidden restore snapshot.
|
|
local function GetVisibleSetIDs()
|
|
local result = {}
|
|
for _, id in ipairs(C_EquipmentSet.GetEquipmentSetIDs() or {}) do
|
|
if not IsRestoreSet(id) then table.insert(result, id) end
|
|
end
|
|
return result
|
|
end
|
|
|
|
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
|
|
local UpdatePopouts -- forward decl; assigned once popouts + flyout exist
|
|
|
|
-- 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, 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:SetSize(220, 368)
|
|
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
|
|
UpdatePopouts()
|
|
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(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:SetSize(28, 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(PAPERDOLL_EQUIPMENTMANAGER)
|
|
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:SetSize(86, 22); btnEquip:SetText(EQUIPSET_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:SetSize(86, 22); btnSave:SetText(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(CONFIRM_SAVE_EQUIPMENT_SET, 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:SetSize(140, 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 MouseIsOverHitRect(this) then return end
|
|
if this.anchorBtn and MouseIsOverHitRect(this.anchorBtn) then return end
|
|
this:Hide()
|
|
end)
|
|
tinsert(UISpecialFrames, "pfEqMgrRowMenu") -- Escape closes it
|
|
|
|
rowMenu.changeBtn = CreateFrame("Button", nil, rowMenu, "UIPanelButtonTemplate")
|
|
rowMenu.changeBtn:SetSize(130, 20);
|
|
rowMenu.changeBtn:SetPoint("TOPLEFT", rowMenu, "TOPLEFT", 5, -3)
|
|
rowMenu.changeBtn:SetText(EQUIPMENT_SET_EDIT)
|
|
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:SetSize(130, 20);
|
|
rowMenu.deleteBtn:SetPoint("TOP", rowMenu.changeBtn, "BOTTOM", 0, -2)
|
|
rowMenu.deleteBtn:SetText(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(CONFIRM_DELETE_EQUIPMENT_SET, 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)
|
|
-- Height fits N rows + (N-1) inter-row gaps + 3px top/bottom padding.
|
|
listFrame:SetSize(180, 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:SetSize(170, 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:SetSize(30, 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:SetSize(16, 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)
|
|
|
|
-- Drag a set onto an action bar. PickupEquipmentSet is newer than
|
|
-- the rest of C_EquipmentSet, so dragging stays inert on ClassicAPI
|
|
-- builds that don't ship it yet.
|
|
if C_EquipmentSet.PickupEquipmentSet then
|
|
row:RegisterForDrag("LeftButton")
|
|
row:SetScript("OnDragStart", function()
|
|
if row.setID then C_EquipmentSet.PickupEquipmentSet(row.setID) end
|
|
end)
|
|
end
|
|
|
|
-- Show the gear and tooltip while the cursor is over the row or its
|
|
-- gear child. The gear sits inside the row's rectangle, so a single
|
|
-- MouseIsOverHitRect(row) check covers both. OnLeave on the row and the
|
|
-- gear catches every exit path, so no OnUpdate poll is needed.
|
|
local function HideRowHover()
|
|
if MouseIsOverHitRect(row) then return end
|
|
GameTooltip:Hide()
|
|
row.gear:Hide()
|
|
end
|
|
row:SetScript("OnEnter", function()
|
|
if not row.setID then return end
|
|
local name = C_EquipmentSet.GetEquipmentSetInfo(row.setID)
|
|
if name then
|
|
GameTooltip:SetOwner(row, "ANCHOR_RIGHT")
|
|
GameTooltip:SetEquipmentSet(name)
|
|
GameTooltip:Show()
|
|
end
|
|
row.gear:Show()
|
|
end)
|
|
row:SetScript("OnLeave", HideRowHover)
|
|
row.gear:SetScript("OnLeave", HideRowHover)
|
|
|
|
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:SetSize(170, SET_ROW_HEIGHT);
|
|
|
|
newSetRow.icon = newSetRow:CreateTexture(nil, "ARTWORK")
|
|
newSetRow.icon:SetSize(24, 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(PAPERDOLL_NEWEQUIPMENTSET)
|
|
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:SetSize(width or 70, 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:SetSize(472, 484)
|
|
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)
|
|
|
|
-- Equipment seed leads the grid with gear-relevant item icons. The widget
|
|
-- owns the name field, preview, grid, filter, and search; this module keeps
|
|
-- the OK/Cancel buttons and the create/save/rename flow below.
|
|
local iconPicker = CreateIconPicker("pfEqMgrIcon", namePopup,
|
|
IconDataProviderExtraType.Equipment, GEARSETS_POPUP_TEXT)
|
|
|
|
local btnPopupOK = MakeButton("pfEqMgrPopupOK", OKAY, namePopup, namePopup, 14, -340, 80)
|
|
|
|
local btnPopupCancel = MakeButton("pfEqMgrPopupCancel", CANCEL, namePopup, namePopup, 0, 0, 80)
|
|
btnPopupCancel:ClearAllPoints()
|
|
btnPopupCancel:SetPoint("BOTTOMRIGHT", namePopup, "BOTTOMRIGHT", -14, 12)
|
|
btnPopupCancel:SetScript("OnClick", function() namePopup:Hide() end)
|
|
|
|
-- Okay | Cancel, both anchored to the bottom-right corner.
|
|
btnPopupOK:ClearAllPoints()
|
|
btnPopupOK:SetPoint("BOTTOMRIGHT", btnPopupCancel, "BOTTOMLEFT", -6, 0)
|
|
|
|
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(iconPicker.GetIcon(), "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 "")
|
|
if prefillIcon then
|
|
-- Stored icons come back either as a full path or a bare basename
|
|
-- (see the row rendering in Refresh). Only prepend the prefix when
|
|
-- there's no path separator, so a full path isn't double-prefixed.
|
|
iconPicker.SetIcon(string.find(prefillIcon, "\\") and prefillIcon
|
|
or ("INTERFACE\\ICONS\\" .. prefillIcon))
|
|
else
|
|
iconPicker.SetIcon(nil)
|
|
end
|
|
namePopup.search:SetText("")
|
|
-- Rename only changes the name, so hide the whole icon-picking area.
|
|
iconPicker.SetIconAreaShown(action ~= "rename")
|
|
namePopup:Show()
|
|
if action ~= "rename" then iconPicker.Refresh() end
|
|
namePopup.editbox:SetFocus()
|
|
end
|
|
|
|
-- ============================================================
|
|
-- Equipment flyout (per-slot popup)
|
|
-- ============================================================
|
|
|
|
local flyout = CreateFrame("Frame", "pfEqMgrFlyout", UIParent)
|
|
-- The name/icon popup is sidecar-only, so it closes with the sidecar.
|
|
-- Popout arrows and the flyout are reconciled by UpdatePopouts: by
|
|
-- default they follow the sidecar, but with the "Always Show Equipment
|
|
-- Slot Flyouts" option they stay while the paperdoll is open.
|
|
frame:SetScript("OnHide", function()
|
|
namePopup:Hide()
|
|
UpdatePopouts()
|
|
-- The "ignored slot" overlays belong to the selected set, which is
|
|
-- only meaningful while the manager is open.
|
|
for _, overlay in pairs(slotOverlays) do overlay:Hide() end
|
|
end)
|
|
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(ERR_EQUIPMENT_MANAGER_BAGS_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, 1, 1, 1)
|
|
elseif this.specialAction == "ignore" then
|
|
GameTooltip:SetText(EQUIPMENT_MANAGER_IGNORE_SLOT, 1, 1, 1)
|
|
elseif this.specialAction == "unignore" then
|
|
GameTooltip:SetText(EQUIPMENT_MANAGER_UNIGNORE_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", GameTooltip_Hide)
|
|
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
|
|
local quality
|
|
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, _, q = GetContainerItemInfo(loc.bag, loc.slot)
|
|
quality = q
|
|
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("")
|
|
quality = GetInventoryItemQuality("player", loc.slot)
|
|
end
|
|
end
|
|
-- Quality border, matching the character slots
|
|
if quality and quality > 0 then
|
|
b.backdrop:SetBackdropBorderColor(GetItemQualityColor(quality))
|
|
else
|
|
b.backdrop:SetBackdropBorderColor(pfUI.cache.er, pfUI.cache.eg, pfUI.cache.eb, pfUI.cache.ea)
|
|
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()
|
|
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 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 INVSLOTS_EQUIPABLE_IN_COMBAT[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 INVSLOTS_EQUIPABLE_IN_COMBAT[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
|
|
|
|
-- Popout arrows follow the EM sidecar by default. With the
|
|
-- "Always Show Equipment Slot Flyouts" option they follow the paperdoll
|
|
-- instead, so gear can be swapped without opening the equipment manager.
|
|
local function PopoutsActive()
|
|
if C.character.inventory.equipflyout == "1" then
|
|
return PaperDollFrame:IsShown()
|
|
end
|
|
return frame:IsShown()
|
|
end
|
|
|
|
function UpdatePopouts()
|
|
local show = PopoutsActive()
|
|
for _, b in ipairs(popoutButtons) do
|
|
if show then b:Show() else b:Hide() end
|
|
end
|
|
if not show then flyout:Hide() end
|
|
end
|
|
|
|
PaperDollFrame:HookScript("OnShow", UpdatePopouts)
|
|
PaperDollFrame:HookScript("OnHide", UpdatePopouts)
|
|
|
|
-- ============================================================
|
|
-- Refresh
|
|
-- ============================================================
|
|
|
|
function pfUI.equipmentmanager.Refresh()
|
|
local ids = GetVisibleSetIDs()
|
|
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 frame:IsShown() and 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
|
|
|
|
pfUI.equipmentmanager.RefreshExtras()
|
|
end
|
|
|
|
-- ============================================================
|
|
-- Shared helpers for the extra panels (unused items / triggers)
|
|
-- ============================================================
|
|
|
|
local function CreateCheckbox(parent, label)
|
|
local cb = CreateFrame("CheckButton", nil, parent, "UICheckButtonTemplate")
|
|
cb:SetSize(20, 20)
|
|
SkinCheckbox(cb, 14)
|
|
if label then
|
|
cb.label = cb:CreateFontString(nil, "OVERLAY", "GameFontWhite")
|
|
cb.label:SetPoint("LEFT", cb, "RIGHT", 4, 0)
|
|
cb.label:SetText(label)
|
|
end
|
|
return cb
|
|
end
|
|
|
|
local function CreateSmallButton(parent, dir, size)
|
|
local b = CreateFrame("Button", nil, parent)
|
|
b:SetSize(size or 14, size or 14)
|
|
SkinArrowButton(b, dir, size or 14)
|
|
return b
|
|
end
|
|
|
|
-- Dropdown entries carry a `.value`; select one by value instead of index.
|
|
local function SelectDropdownValue(dd, value)
|
|
local menu = dd:GetMenu()
|
|
for i, entry in ipairs(menu or {}) do
|
|
if entry.value == value then
|
|
dd:SetSelection(i)
|
|
return
|
|
end
|
|
end
|
|
dd.id = nil
|
|
dd.text:SetText("")
|
|
end
|
|
|
|
local function Print(msg)
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff33ffccpf|cffffffffUI|r: " .. msg)
|
|
end
|
|
|
|
-- Side panels open to the right of the equipment manager. Only one is
|
|
-- shown at a time; they're children of the sidecar so they close with it.
|
|
local subPanels = {}
|
|
local function CreateSubPanel(name, width, height, titleText)
|
|
local p = CreateFrame("Frame", name, frame)
|
|
p:SetSize(width, height)
|
|
p:SetFrameStrata("HIGH")
|
|
p:SetPoint("TOPLEFT", frame, "TOPRIGHT", 2*border + 2, 0)
|
|
p:EnableMouse(true)
|
|
CreateBackdrop(p, nil, nil, .9)
|
|
CreateBackdropShadow(p)
|
|
p:Hide()
|
|
|
|
p.title = p:CreateFontString(nil, "OVERLAY", "GameFontNormal")
|
|
p.title:SetPoint("TOP", p, "TOP", 0, -10)
|
|
p.title:SetText(titleText)
|
|
|
|
p.close = CreateFrame("Button", nil, p, "UIPanelCloseButton")
|
|
p.close:SetPoint("TOPRIGHT", p, "TOPRIGHT", -4, -4)
|
|
p.close:SetScript("OnClick", function() p:Hide() end)
|
|
SkinCloseButton(p.close, p.backdrop or p, -6, -6)
|
|
|
|
table.insert(subPanels, p)
|
|
return p
|
|
end
|
|
|
|
local function ToggleSubPanel(p)
|
|
if p:IsShown() then
|
|
p:Hide()
|
|
return
|
|
end
|
|
for _, other in ipairs(subPanels) do other:Hide() end
|
|
p:Show()
|
|
end
|
|
|
|
-- ============================================================
|
|
-- Items not in any set
|
|
-- ============================================================
|
|
|
|
local WHERE_ORDER = { equipped = 1, bags = 2, bank = 3 }
|
|
local BANK_BAGS = { -1, 5, 6, 7, 8, 9, 10 } -- main bank, bank bags
|
|
|
|
local bankOpen = false
|
|
|
|
-- Returns the list of equippable items that no equipment set references,
|
|
-- plus how many of those are on the ignore list. Ignored items are only
|
|
-- included in the list when includeIgnored is set.
|
|
local function ScanUnusedItems(includeIgnored)
|
|
local minQuality = tonumber(C.equipmentmanager.unusedquality)
|
|
local result, numIgnored = {}, 0
|
|
|
|
local function Consider(entry)
|
|
entry.icon = C_Item.GetItemIconByID(entry.itemID)
|
|
entry.quality = C_Item.GetItemQualityByID(entry.itemID) or 1
|
|
if entry.quality < minQuality then return end
|
|
if db.ignore[entry.itemID] then
|
|
numIgnored = numIgnored + 1
|
|
if not includeIgnored then return end
|
|
entry.ignored = true
|
|
end
|
|
table.insert(result, entry)
|
|
end
|
|
|
|
-- Packed location of every item some set uses. Paperdoll and bag items
|
|
-- always resolve, so these match GetInventoryItemsForSlot's keys.
|
|
local inSet = {}
|
|
for _, setID in ipairs(GetVisibleSetIDs()) do
|
|
for _, location in pairs(C_EquipmentSet.GetItemLocations(setID)) do
|
|
inSet[location] = true
|
|
end
|
|
end
|
|
|
|
-- Everything on the paperdoll or in the bags that fits a gear slot,
|
|
-- keyed by packed location (rings and trinkets fit two slots).
|
|
local items = {}
|
|
for invSlot = 1, 19 do GetInventoryItemsForSlot(invSlot, items) end
|
|
|
|
for location, link in pairs(items) do
|
|
if not inSet[location] then
|
|
local loc = EquipmentManager_GetLocationData(location)
|
|
Consider({
|
|
where = loc.isBags and "bags" or "equipped",
|
|
bag = loc.bag, slot = loc.slot,
|
|
itemID = (C_Item.GetItemInfoInstant(link)), link = link,
|
|
})
|
|
end
|
|
end
|
|
|
|
if C.equipmentmanager.unusedbank == "1" then
|
|
for _, item in ipairs(db.bank) do
|
|
if not item.inSet then
|
|
Consider({ where = "bank", bag = item.bag, slot = item.slot, itemID = item.id, link = item.link })
|
|
end
|
|
end
|
|
end
|
|
|
|
table.sort(result, function(a, b)
|
|
if a.where ~= b.where then return WHERE_ORDER[a.where] < WHERE_ORDER[b.where] end
|
|
if a.quality ~= b.quality then return a.quality > b.quality end
|
|
if a.itemID ~= b.itemID then return a.itemID < b.itemID end
|
|
if a.bag ~= b.bag then return (a.bag or 0) < (b.bag or 0) end
|
|
return a.slot < b.slot
|
|
end)
|
|
|
|
return result, numIgnored
|
|
end
|
|
|
|
-- Snapshot the bank while it's open so its items stay listed afterwards.
|
|
-- Set membership is recorded now as well: ClassicAPI only resolves set
|
|
-- items in the bank while the bank window is open.
|
|
local function ScanBank()
|
|
local snapshot = {}
|
|
for _, bag in ipairs(BANK_BAGS) do
|
|
for slot = 1, GetContainerNumSlots(bag) do
|
|
local itemID = C_Container.GetContainerItemID(bag, slot)
|
|
if itemID then
|
|
table.insert(snapshot, {
|
|
id = itemID, link = GetContainerItemLink(bag, slot), bag = bag, slot = slot,
|
|
inSet = C_Container.GetContainerItemEquipmentSetInfo(bag, slot),
|
|
})
|
|
end
|
|
end
|
|
end
|
|
db.bank = snapshot
|
|
end
|
|
|
|
local UNUSED_COLS, UNUSED_ROWS = 7, 8
|
|
local UNUSED_SIZE, UNUSED_GAP = 30, 4
|
|
local UNUSED_STRIDE = UNUSED_SIZE + UNUSED_GAP
|
|
|
|
local unusedPanel = CreateSubPanel("pfEqMgrUnusedFrame",
|
|
UNUSED_COLS * UNUSED_STRIDE + 24, frame:GetHeight(), T["Items Not In Any Set"])
|
|
unusedPanel.offset = 0 -- first visible row
|
|
|
|
unusedPanel.info = unusedPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
|
|
unusedPanel.info:SetPoint("TOPLEFT", unusedPanel, "TOPLEFT", 14, -34)
|
|
unusedPanel.info:SetJustifyH("LEFT")
|
|
|
|
unusedPanel.showIgnored = CreateCheckbox(unusedPanel)
|
|
unusedPanel.showIgnored:SetPoint("TOPRIGHT", unusedPanel, "TOPRIGHT", -12, -29)
|
|
unusedPanel.showIgnored.label = unusedPanel.showIgnored:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
|
|
unusedPanel.showIgnored.label:SetPoint("RIGHT", unusedPanel.showIgnored, "LEFT", -4, 0)
|
|
unusedPanel.showIgnored.label:SetText(T["Show Ignored"])
|
|
|
|
local unusedGrid = CreateFrame("Frame", nil, unusedPanel)
|
|
unusedGrid:SetPoint("TOPLEFT", unusedPanel, "TOPLEFT", 12 + UNUSED_GAP/2, -54)
|
|
unusedGrid:SetSize(UNUSED_COLS * UNUSED_STRIDE, UNUSED_ROWS * UNUSED_STRIDE)
|
|
unusedGrid:EnableMouseWheel(true)
|
|
|
|
unusedPanel.empty = unusedPanel:CreateFontString(nil, "OVERLAY", "GameFontDisable")
|
|
unusedPanel.empty:SetPoint("CENTER", unusedGrid, "CENTER", 0, 0)
|
|
unusedPanel.empty:SetWidth(UNUSED_COLS * UNUSED_STRIDE - 10)
|
|
unusedPanel.empty:SetText(T["Every equippable item is part of a set."])
|
|
|
|
unusedPanel.hint = unusedPanel:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
|
|
unusedPanel.hint:SetPoint("BOTTOMLEFT", unusedPanel, "BOTTOMLEFT", 14, 12)
|
|
unusedPanel.hint:SetPoint("BOTTOMRIGHT", unusedPanel, "BOTTOMRIGHT", -14, 12)
|
|
unusedPanel.hint:SetJustifyH("LEFT")
|
|
unusedPanel.hint:SetText(T["Shift-Click: Link Ctrl-Click: Preview\nRight-Click: Ignore / Unignore"])
|
|
|
|
local RefreshUnusedPanel -- forward decl
|
|
local unusedButtons = {}
|
|
|
|
-- Bag highlight: while the panel is open, items of the list glow in
|
|
-- pfUI's bag and bank frames and every other item is dimmed.
|
|
local bagHighlight = { active = false, locs = {}, hooked = false }
|
|
|
|
local function ApplyBagHighlight(bag, slot)
|
|
local entry = pfUI.bags and pfUI.bags[bag] and pfUI.bags[bag].slots[slot]
|
|
if not entry or not entry.frame then return end
|
|
local f = entry.frame
|
|
local o = f.pfEqMgrUnused
|
|
|
|
if not bagHighlight.active or not f.hasItem then
|
|
if o then o:Hide() end
|
|
return
|
|
end
|
|
|
|
if not o then
|
|
o = CreateFrame("Frame", nil, f)
|
|
o:SetAllPoints(f)
|
|
o:SetFrameLevel(f:GetFrameLevel() + 3)
|
|
|
|
o.dim = o:CreateTexture(nil, "OVERLAY")
|
|
o.dim:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1)
|
|
o.dim:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1)
|
|
o.dim:SetTexture(0, 0, 0, .7)
|
|
|
|
o.glow = o:CreateTexture(nil, "OVERLAY")
|
|
o.glow:SetTexture("Interface\\Buttons\\UI-ActionButton-Border")
|
|
o.glow:SetBlendMode("ADD")
|
|
o.glow:SetVertexColor(.2, 1, .2, 1)
|
|
o.glow:SetPoint("CENTER", f, "CENTER", 0, 0)
|
|
|
|
f.pfEqMgrUnused = o
|
|
end
|
|
|
|
if bagHighlight.locs[bag] and bagHighlight.locs[bag][slot] then
|
|
local w = f:GetWidth()
|
|
if w and w > 0 then o.glow:SetSize(w * 1.8, w * 1.8) end
|
|
o.dim:Hide()
|
|
o.glow:Show()
|
|
else
|
|
o.glow:Hide()
|
|
o.dim:Show()
|
|
end
|
|
o:Show()
|
|
end
|
|
|
|
local function UpdateBagHighlights()
|
|
-- Only pfUI's own bag frames are decorated (the bags module may be off).
|
|
if not pfUI.bag or not pfUI.bags then return end
|
|
if not bagHighlight.hooked and pfUI.bag.UpdateSlot then
|
|
-- Re-apply whenever pfUI redraws a slot (item moved, bag reopened...).
|
|
hooksecurefunc(pfUI.bag, "UpdateSlot", function(_, bag, slot)
|
|
if bagHighlight.active then ApplyBagHighlight(bag, slot) end
|
|
end)
|
|
bagHighlight.hooked = true
|
|
end
|
|
for bag, data in pairs(pfUI.bags) do
|
|
if data and data.slots then
|
|
for slot in pairs(data.slots) do ApplyBagHighlight(bag, slot) end
|
|
end
|
|
end
|
|
end
|
|
|
|
local WHERE_TAG = { equipped = "|cff33ff33E|r", bags = "", bank = "|cff66aaffB|r" }
|
|
local WHERE_TEXT = { equipped = T["Equipped"], bags = T["In Bags"], bank = T["In Bank"] }
|
|
|
|
local function CreateUnusedButton(i)
|
|
local b = CreateFrame("Button", nil, unusedGrid)
|
|
b:SetSize(UNUSED_SIZE, UNUSED_SIZE)
|
|
local col = math.mod(i - 1, UNUSED_COLS)
|
|
local row = math.floor((i - 1) / UNUSED_COLS)
|
|
b:SetPoint("TOPLEFT", unusedGrid, "TOPLEFT", col * UNUSED_STRIDE + UNUSED_GAP/2, -row * UNUSED_STRIDE - UNUSED_GAP/2)
|
|
CreateBackdrop(b)
|
|
b:RegisterForClicks("LeftButtonUp", "RightButtonUp")
|
|
|
|
b.icon = b:CreateTexture(nil, "ARTWORK")
|
|
b.icon:SetAllPoints(b)
|
|
b.icon:SetTexCoord(.08, .92, .08, .92)
|
|
|
|
b.tag = b:CreateFontString(nil, "OVERLAY", "NumberFontNormalSmall")
|
|
b.tag:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", -1, 1)
|
|
|
|
b:SetScript("OnEnter", function()
|
|
local e = this.entry
|
|
if not e then return end
|
|
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
|
if e.where == "equipped" then
|
|
GameTooltip:SetInventoryItem("player", e.slot)
|
|
elseif e.where == "bags" then
|
|
GameTooltip:SetBagItem(e.bag, e.slot)
|
|
else
|
|
GameTooltip:SetHyperlink(e.link)
|
|
end
|
|
GameTooltip:AddLine(" ")
|
|
GameTooltip:AddLine(T["Not part of any equipment set"] .. " - " .. WHERE_TEXT[e.where], 1, .82, 0)
|
|
if e.ignored then
|
|
GameTooltip:AddLine(T["Ignored (Right-Click to unignore)"], .6, .6, .6)
|
|
end
|
|
GameTooltip:Show()
|
|
end)
|
|
b:SetScript("OnLeave", GameTooltip_Hide)
|
|
|
|
b:SetScript("OnClick", function()
|
|
local e = this.entry
|
|
if not e then return end
|
|
if arg1 == "RightButton" then
|
|
db.ignore[e.itemID] = (not db.ignore[e.itemID]) and true or nil
|
|
GameTooltip:Hide()
|
|
pfUI.equipmentmanager.Refresh()
|
|
elseif IsShiftKeyDown() then
|
|
if ChatFrameEditBox:IsVisible() then ChatFrameEditBox:Insert(e.link) end
|
|
elseif IsControlKeyDown() then
|
|
DressUpItemLink(e.link)
|
|
end
|
|
end)
|
|
|
|
return b
|
|
end
|
|
|
|
unusedGrid:SetScript("OnMouseWheel", function()
|
|
unusedPanel.offset = unusedPanel.offset - (arg1 or 0)
|
|
RefreshUnusedPanel()
|
|
end)
|
|
|
|
function RefreshUnusedPanel()
|
|
local list, numIgnored = ScanUnusedItems(unusedPanel.showIgnored:GetChecked() and true or nil)
|
|
local total = table.getn(list)
|
|
local numShownIgnored = unusedPanel.showIgnored:GetChecked() and numIgnored or 0
|
|
unusedPanel.info:SetText(string.format(T["%d items, %d ignored"], total - numShownIgnored, numIgnored))
|
|
|
|
local totalRows = math.ceil(total / UNUSED_COLS)
|
|
local maxOffset = math.max(0, totalRows - UNUSED_ROWS)
|
|
if unusedPanel.offset > maxOffset then unusedPanel.offset = maxOffset end
|
|
if unusedPanel.offset < 0 then unusedPanel.offset = 0 end
|
|
local first = unusedPanel.offset * UNUSED_COLS
|
|
|
|
for i = 1, UNUSED_COLS * UNUSED_ROWS do
|
|
local e = list[first + i]
|
|
local b = unusedButtons[i]
|
|
if e then
|
|
if not b then
|
|
b = CreateUnusedButton(i)
|
|
unusedButtons[i] = b
|
|
end
|
|
b.entry = e
|
|
b.icon:SetTexture(e.icon or "Interface\\Icons\\INV_Misc_QuestionMark")
|
|
b.icon:SetDesaturated(e.ignored and true or false)
|
|
b.icon:SetAlpha(e.ignored and .5 or 1)
|
|
b.tag:SetText(WHERE_TAG[e.where] or "")
|
|
local r, g, bl = GetItemQualityColor(e.quality or 1)
|
|
if b.backdrop then b.backdrop:SetBackdropBorderColor(r, g, bl, 1) end
|
|
b:Show()
|
|
elseif b then
|
|
b.entry = nil
|
|
b:Hide()
|
|
end
|
|
end
|
|
|
|
if total == 0 then unusedPanel.empty:Show() else unusedPanel.empty:Hide() end
|
|
|
|
bagHighlight.active = true
|
|
bagHighlight.locs = {}
|
|
for _, e in ipairs(list) do
|
|
if e.bag then
|
|
bagHighlight.locs[e.bag] = bagHighlight.locs[e.bag] or {}
|
|
bagHighlight.locs[e.bag][e.slot] = true
|
|
end
|
|
end
|
|
UpdateBagHighlights()
|
|
end
|
|
|
|
unusedPanel.showIgnored:SetScript("OnClick", function() RefreshUnusedPanel() end)
|
|
unusedPanel:SetScript("OnShow", function() RefreshUnusedPanel() end)
|
|
unusedPanel:SetScript("OnHide", function()
|
|
bagHighlight.active = false
|
|
UpdateBagHighlights()
|
|
end)
|
|
|
|
local bankEvents = CreateFrame("Frame")
|
|
bankEvents:RegisterEvent("BANKFRAME_OPENED")
|
|
bankEvents:RegisterEvent("BANKFRAME_CLOSED")
|
|
bankEvents:RegisterEvent("PLAYERBANKSLOTS_CHANGED")
|
|
bankEvents:RegisterEvent("BAG_UPDATE_DELAYED")
|
|
bankEvents:RegisterEvent("EQUIPMENT_SETS_CHANGED")
|
|
bankEvents:SetScript("OnEvent", function()
|
|
if event == "BANKFRAME_OPENED" then
|
|
bankOpen = true
|
|
elseif event == "BANKFRAME_CLOSED" then
|
|
bankOpen = false
|
|
return
|
|
end
|
|
if not bankOpen then return end
|
|
ScanBank()
|
|
if frame:IsShown() then pfUI.equipmentmanager.Refresh() end
|
|
end)
|
|
|
|
-- ============================================================
|
|
-- Automatic set switching (triggers)
|
|
-- ============================================================
|
|
|
|
local _, playerClass = UnitClass("player")
|
|
|
|
-- Shapeshift form IDs as GetShapeshiftFormID reports them on this client
|
|
-- (SpellShapeshiftForm.dbc, incl. Turtle's custom forms). A value can
|
|
-- list several IDs so e.g. "Bear Form" covers Dire Bear Form too.
|
|
local FORM_OPTIONS = {
|
|
DRUID = {
|
|
{ value = "0", text = T["No Form (Caster)"] },
|
|
{ value = "5,8", text = T["Bear Form"] },
|
|
{ value = "1", text = T["Cat Form"] },
|
|
{ value = "3,11", text = T["Travel Form"] },
|
|
{ value = "4", text = T["Aquatic Form"] },
|
|
{ value = "31", text = T["Moonkin Form"] },
|
|
{ value = "2,9", text = T["Tree of Life Form"] },
|
|
},
|
|
WARRIOR = {
|
|
{ value = "17", text = T["Battle Stance"] },
|
|
{ value = "18", text = T["Defensive Stance"] },
|
|
{ value = "19", text = T["Berserker Stance"] },
|
|
},
|
|
ROGUE = { { value = "30", text = T["Stealth"] } },
|
|
SHAMAN = { { value = "16", text = T["Ghost Wolf"] } },
|
|
PRIEST = { { value = "28", text = T["Shadowform"] } },
|
|
}
|
|
local myForms = FORM_OPTIONS[playerClass]
|
|
if myForms then table.insert(myForms, { value = "any", text = T["Any Form"] }) end
|
|
|
|
local INSTANCE_OPTIONS = {
|
|
{ value = "party", text = T["Dungeon"] },
|
|
{ value = "raid", text = T["Raid"] },
|
|
{ value = "pvp", text = T["Battleground"] },
|
|
{ value = "none", text = T["Open World"] },
|
|
}
|
|
|
|
-- value: nil (no parameter), "list" (dropdown from .options) or "text".
|
|
local TRIGGER_TYPES = {
|
|
{ key = "mounted", text = T["Mounted"] },
|
|
{ key = "swimming", text = T["Swimming"] },
|
|
{ key = "dining", text = T["Eating / Drinking"] },
|
|
{ key = "resting", text = T["Resting (Inn / City)"] },
|
|
{ key = "stealth", text = T["Stealthed"] },
|
|
{ key = "form", text = T["Shapeshift Form / Stance"], value = "list", options = myForms },
|
|
{ key = "instance", text = T["Instance Type"], value = "list", options = INSTANCE_OPTIONS },
|
|
{ key = "zone", text = T["Zone"], value = "text" },
|
|
{ key = "boss", text = T["Targeting A Boss"] },
|
|
{ key = "time", text = T["Time Of Day"], value = "text" },
|
|
}
|
|
local TRIGGER_TYPE = {}
|
|
for _, info in ipairs(TRIGGER_TYPES) do TRIGGER_TYPE[info.key] = info end
|
|
|
|
local function DefaultTriggerValue(key)
|
|
if key == "form" then return myForms and myForms[1].value end
|
|
if key == "instance" then return "raid" end
|
|
if key == "zone" then return GetRealZoneText() end
|
|
if key == "time" then return "20:00-23:00" end
|
|
end
|
|
|
|
local function DescribeTrigger(t)
|
|
local info = TRIGGER_TYPE[t.type]
|
|
if not info then return "?" end
|
|
if info.value == "list" then
|
|
for _, o in ipairs(info.options or {}) do
|
|
if o.value == t.value then return info.text .. ": " .. o.text end
|
|
end
|
|
return info.text
|
|
elseif info.value == "text" then
|
|
return info.text .. ": " .. (t.value or "")
|
|
end
|
|
return info.text
|
|
end
|
|
|
|
-- Condition checks ---------------------------------------------
|
|
|
|
-- Every food and drink buff shares the name of the base Food (433) or
|
|
-- Drink (430) aura, so their localized names identify all of them.
|
|
local DINING_AURAS = { C_Spell.GetSpellName(433), C_Spell.GetSpellName(430) }
|
|
|
|
-- Like Outfitter: the dining set stays on only while there's still
|
|
-- health or mana to regenerate, so it comes off as soon as you're full.
|
|
local function IsDining()
|
|
local healthFull = UnitHealth("player") >= UnitHealthMax("player")
|
|
local manaFull = UnitPowerType("player") ~= 0 or UnitMana("player") >= UnitManaMax("player")
|
|
if healthFull and manaFull then return false end
|
|
for _, name in ipairs(DINING_AURAS) do
|
|
if C_UnitAuras.GetAuraDataBySpellName("player", name, "HELPFUL") then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function InForm(value)
|
|
local formID = GetShapeshiftFormID() or 0
|
|
if value == "any" then return formID ~= 0 end
|
|
for _, id in ipairs({ strsplit(",", value) }) do
|
|
if tonumber(id) == formID then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Zone trigger values are "zone; subzone; ..." as typed or added with "Here".
|
|
local function ZoneEntries(value)
|
|
local entries = {}
|
|
for _, entry in ipairs({ strsplit(";", value or "") }) do
|
|
entry = strtrim(entry):lower()
|
|
if entry ~= "" then table.insert(entries, entry) end
|
|
end
|
|
return entries
|
|
end
|
|
|
|
local function InZone(value)
|
|
local zone, subzone, minimap = GetRealZoneText():lower(), GetSubZoneText():lower(), GetMinimapZoneText():lower()
|
|
for _, entry in ipairs(ZoneEntries(value)) do
|
|
if entry == zone or entry == subzone or entry == minimap then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function ParseTimeWindow(value)
|
|
local _, _, h1, m1, h2, m2 = (value or ""):find("^%s*(%d%d?):(%d%d)%s*%-%s*(%d%d?):(%d%d)%s*$")
|
|
if not h1 then return nil end
|
|
return tonumber(h1) * 60 + tonumber(m1), tonumber(h2) * 60 + tonumber(m2)
|
|
end
|
|
|
|
local function InTimeWindow(value)
|
|
local from, to = ParseTimeWindow(value)
|
|
if not from then return false end
|
|
local now = tonumber(date("%H")) * 60 + tonumber(date("%M"))
|
|
if from <= to then return now >= from and now < to end
|
|
return now >= from or now < to -- window wraps past midnight
|
|
end
|
|
|
|
local function TargetIsBoss()
|
|
if not UnitExists("target") or UnitIsDead("target") then return false end
|
|
if not UnitCanAttack("player", "target") or UnitIsPlayer("target") then return false end
|
|
return UnitClassification("target") == "worldboss" or UnitLevel("target") == -1
|
|
end
|
|
|
|
local function ConditionActive(t)
|
|
local key = t.type
|
|
if key == "mounted" then return IsMounted() and true or false
|
|
elseif key == "swimming" then return IsSwimming() and true or false
|
|
elseif key == "dining" then return IsDining()
|
|
elseif key == "resting" then return IsResting() and true or false
|
|
elseif key == "stealth" then return IsStealthed() and true or false
|
|
elseif key == "form" then return InForm(t.value)
|
|
elseif key == "instance" then
|
|
local _, instanceType = GetInstanceInfo()
|
|
return instanceType == t.value
|
|
elseif key == "zone" then return InZone(t.value)
|
|
elseif key == "boss" then return TargetIsBoss()
|
|
elseif key == "time" then return InTimeWindow(t.value)
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Engine ---------------------------------------------------------
|
|
|
|
-- forceApply: the active trigger was edited, equip its (new) set again.
|
|
local engine = { ready = false, timer = nil, poll = nil, forceApply = nil }
|
|
|
|
local function FindTrigger(id)
|
|
if not id then return nil end
|
|
for i, t in ipairs(db.triggers) do
|
|
if t.id == id then return t, i end
|
|
end
|
|
end
|
|
|
|
local function SetExists(setID)
|
|
return setID and C_EquipmentSet.GetEquipmentSetInfo(setID) and not IsRestoreSet(setID)
|
|
end
|
|
|
|
local function TriggerUsable(t)
|
|
return t.enabled and SetExists(t.setID) and TRIGGER_TYPE[t.type] and true or false
|
|
end
|
|
|
|
-- Highest-priority (topmost) enabled trigger whose condition holds.
|
|
local function GetActiveTrigger()
|
|
for _, t in ipairs(db.triggers) do
|
|
if TriggerUsable(t) and ConditionActive(t) then return t end
|
|
end
|
|
end
|
|
|
|
-- Whether the applied trigger differs from the one that should be, and
|
|
-- the one that should be.
|
|
local function NeedsSwitch()
|
|
local best = GetActiveTrigger()
|
|
return engine.forceApply or (best and best.id or 0) ~= (db.applied or 0), best
|
|
end
|
|
|
|
-- Gear can't change in combat or while dead, swapping mid-cast would
|
|
-- interrupt it, and only one set swap runs at a time: the switch waits.
|
|
local function SwapBlocked(setID)
|
|
if UnitAffectingCombat("player") or UnitIsDeadOrGhost("player") then return true end
|
|
if C_Spell.UnitCastingInfo("player") or C_Spell.UnitChannelInfo("player") then return true end
|
|
if CursorHasItem() or not C_EquipmentSet.CanUseEquipmentSets() then return true end
|
|
return C_EquipmentSet.EquipmentSetContainsLockedItems(setID)
|
|
end
|
|
|
|
-- A swap that's blocked or refused is retried by scheduling again.
|
|
local function TryEquip(setID)
|
|
return not SwapBlocked(setID) and C_EquipmentSet.UseEquipmentSet(setID)
|
|
end
|
|
|
|
-- Remember the gear worn before the first trigger fires. Only slots that
|
|
-- some trigger set actually changes are recorded, so restoring doesn't
|
|
-- undo unrelated gear changes made while the trigger was active.
|
|
local function TakeRestoreSnapshot()
|
|
local touched = {}
|
|
for _, t in ipairs(db.triggers) do
|
|
if TriggerUsable(t) then
|
|
local ignored = {}
|
|
for _, s in ipairs(C_EquipmentSet.GetIgnoredSlots(t.setID) or {}) do ignored[s] = true end
|
|
for s = 1, 19 do
|
|
if not ignored[s] then touched[s] = true end
|
|
end
|
|
end
|
|
end
|
|
|
|
C_EquipmentSet.ClearIgnoredSlotsForSave()
|
|
for s = 1, 19 do
|
|
if not touched[s] then C_EquipmentSet.IgnoreSlotForSave(s) end
|
|
end
|
|
local id = C_EquipmentSet.GetEquipmentSetID(RESTORE_SET_NAME)
|
|
if id then
|
|
C_EquipmentSet.SaveEquipmentSet(id)
|
|
else
|
|
C_EquipmentSet.CreateEquipmentSet(RESTORE_SET_NAME, "Spell_Nature_TimeStop")
|
|
end
|
|
C_EquipmentSet.ClearIgnoredSlotsForSave()
|
|
end
|
|
|
|
local function Announce(msg)
|
|
if C.equipmentmanager.announce == "1" then Print(msg) end
|
|
end
|
|
|
|
local function SetName(setID)
|
|
return (C_EquipmentSet.GetEquipmentSetInfo(setID)) or "?"
|
|
end
|
|
|
|
local RefreshTriggerPanel -- forward decl
|
|
local Schedule -- forward decl
|
|
|
|
local function Evaluate()
|
|
if not engine.ready or C.equipmentmanager.autoswitch ~= "1" then return end
|
|
|
|
local needed, best = NeedsSwitch()
|
|
if not needed then return end
|
|
local appliedID = db.applied or 0
|
|
|
|
if best then
|
|
if SwapBlocked(best.setID) then return Schedule() end
|
|
-- Only the first trigger in a chain records what to go back to.
|
|
if appliedID == 0 then TakeRestoreSnapshot() end
|
|
if not C_EquipmentSet.UseEquipmentSet(best.setID) then return Schedule() end
|
|
db.applied = best.id
|
|
Announce(string.format(T["Equipping %s (%s)"], SetName(best.setID), DescribeTrigger(best)))
|
|
else
|
|
local prev = FindTrigger(appliedID)
|
|
local leave = prev and prev.leave or "restore"
|
|
local restoreID = C_EquipmentSet.GetEquipmentSetID(RESTORE_SET_NAME)
|
|
if leave == "restore" and restoreID then
|
|
if not TryEquip(restoreID) then return Schedule() end
|
|
Announce(T["Restoring previous gear"])
|
|
elseif type(leave) == "number" and SetExists(leave) then
|
|
if not TryEquip(leave) then return Schedule() end
|
|
Announce(string.format(T["Equipping %s"], SetName(leave)))
|
|
end
|
|
-- The swap copied the item GUIDs when it started; the set can go.
|
|
if restoreID then C_EquipmentSet.DeleteEquipmentSet(restoreID) end
|
|
db.applied = nil
|
|
end
|
|
|
|
engine.forceApply = nil
|
|
RefreshTriggerPanel()
|
|
end
|
|
|
|
-- Switch half a second after something changed, so bobbing at the water
|
|
-- surface or a quick target swap doesn't thrash gear. Also retries a
|
|
-- swap that's blocked.
|
|
function Schedule()
|
|
if engine.timer then return end
|
|
engine.timer = C_Timer.NewTimer(0.5, function()
|
|
engine.timer = nil
|
|
Evaluate()
|
|
end)
|
|
end
|
|
|
|
-- Swimming and the time of day fire no event, so those are polled while
|
|
-- a trigger needs them.
|
|
local POLLED = { swimming = true, time = true }
|
|
|
|
local engineEvents = CreateFrame("Frame")
|
|
|
|
-- Call after any change to the triggers or the autoswitch setting.
|
|
local function UpdateEngine()
|
|
local poll, dining = false, false
|
|
if C.equipmentmanager.autoswitch == "1" then
|
|
for _, t in ipairs(db.triggers) do
|
|
if t.enabled and POLLED[t.type] then poll = true end
|
|
if t.enabled and t.type == "dining" then dining = true end
|
|
end
|
|
end
|
|
|
|
-- The dining set comes off once health and mana are full; these fire
|
|
-- constantly in combat, so only listen while a dining trigger exists.
|
|
if dining then
|
|
engineEvents:RegisterUnitEvent("UNIT_HEALTH", "player")
|
|
engineEvents:RegisterUnitEvent("UNIT_MANA", "player")
|
|
else
|
|
engineEvents:UnregisterEvent("UNIT_HEALTH")
|
|
engineEvents:UnregisterEvent("UNIT_MANA")
|
|
end
|
|
|
|
if poll and not engine.poll then
|
|
engine.poll = C_Timer.NewTicker(0.5, function()
|
|
if NeedsSwitch() then Schedule() end
|
|
end)
|
|
elseif not poll and engine.poll then
|
|
engine.poll:Cancel()
|
|
engine.poll = nil
|
|
end
|
|
Schedule()
|
|
end
|
|
|
|
engineEvents:RegisterEvent("PLAYER_ENTERING_WORLD")
|
|
engineEvents:RegisterEvent("PLAYER_LEAVING_WORLD")
|
|
engineEvents:RegisterUnitEvent("UNIT_AURA", "player") -- mounted, stealth, dining
|
|
engineEvents:RegisterEvent("UPDATE_SHAPESHIFT_FORM")
|
|
engineEvents:RegisterEvent("ZONE_CHANGED")
|
|
engineEvents:RegisterEvent("ZONE_CHANGED_INDOORS")
|
|
engineEvents:RegisterEvent("ZONE_CHANGED_NEW_AREA")
|
|
engineEvents:RegisterEvent("PLAYER_UPDATE_RESTING")
|
|
engineEvents:RegisterEvent("PLAYER_TARGET_CHANGED")
|
|
engineEvents:RegisterEvent("PLAYER_REGEN_ENABLED") -- a boss target died mid-fight
|
|
engineEvents:SetScript("OnEvent", function()
|
|
if event == "PLAYER_LEAVING_WORLD" then
|
|
engine.ready = false
|
|
elseif event == "PLAYER_ENTERING_WORLD" then
|
|
-- Zone/instance info is unreliable right after a loading screen.
|
|
engine.ready = false
|
|
C_Timer.After(3, function()
|
|
engine.ready = true
|
|
Schedule()
|
|
end)
|
|
else
|
|
Schedule()
|
|
end
|
|
end)
|
|
|
|
-- ============================================================
|
|
-- Trigger editor panel
|
|
-- ============================================================
|
|
|
|
local TRIGGER_ROWS, TRIGGER_ROW_H = 5, 24
|
|
local TRIGGER_STRIDE = TRIGGER_ROW_H + 2
|
|
local triggerPanel = CreateSubPanel("pfEqMgrTriggerFrame", 310, 404, T["Automatic Set Switching"])
|
|
local selectedTriggerID = nil
|
|
triggerPanel.offset = 0
|
|
|
|
local cbAuto = CreateCheckbox(triggerPanel, T["Enable Automatic Set Switching"])
|
|
cbAuto:SetPoint("TOPLEFT", triggerPanel, "TOPLEFT", 12, -30)
|
|
cbAuto:SetScript("OnClick", function()
|
|
C.equipmentmanager.autoswitch = this:GetChecked() and "1" or "0"
|
|
UpdateEngine()
|
|
end)
|
|
|
|
local triggerList = CreateFrame("Frame", nil, triggerPanel)
|
|
triggerList:SetPoint("TOPLEFT", triggerPanel, "TOPLEFT", 14, -56)
|
|
triggerList:SetSize(282, TRIGGER_ROWS * TRIGGER_STRIDE + 4)
|
|
CreateBackdrop(triggerList, nil, nil, .75)
|
|
triggerList:EnableMouseWheel(true)
|
|
triggerList:SetScript("OnMouseWheel", function()
|
|
triggerPanel.offset = triggerPanel.offset - (arg1 or 0)
|
|
RefreshTriggerPanel()
|
|
end)
|
|
|
|
triggerList.empty = triggerList:CreateFontString(nil, "OVERLAY", "GameFontDisable")
|
|
triggerList.empty:SetPoint("CENTER", triggerList, "CENTER", 0, 0)
|
|
triggerList.empty:SetWidth(260)
|
|
triggerList.empty:SetText(T["No triggers yet. Click \"New Trigger\" to add one."])
|
|
|
|
local function MoveTrigger(index, delta)
|
|
local target = index + delta
|
|
if target < 1 or target > table.getn(db.triggers) then return end
|
|
db.triggers[index], db.triggers[target] = db.triggers[target], db.triggers[index]
|
|
UpdateEngine()
|
|
RefreshTriggerPanel()
|
|
end
|
|
|
|
local triggerRows = {}
|
|
for i = 1, TRIGGER_ROWS do
|
|
local row = CreateFrame("Button", nil, triggerList)
|
|
row:SetSize(272, TRIGGER_ROW_H)
|
|
row:SetPoint("TOPLEFT", triggerList, "TOPLEFT", 5, -3 - (i - 1) * TRIGGER_STRIDE)
|
|
|
|
row.highlight = row:CreateTexture(nil, "BACKGROUND")
|
|
row.highlight:SetAllPoints(row)
|
|
row.highlight:SetTexture(.3, .3, .3, .4)
|
|
row.highlight:Hide()
|
|
|
|
row.check = CreateCheckbox(row)
|
|
row.check:SetPoint("LEFT", row, "LEFT", 0, 0)
|
|
row.check:SetScript("OnClick", function()
|
|
if not row.trigger then return end
|
|
row.trigger.enabled = this:GetChecked() and true or false
|
|
UpdateEngine()
|
|
RefreshTriggerPanel()
|
|
end)
|
|
|
|
row.delete = CreateSmallButton(row, "close", 14)
|
|
row.delete:SetPoint("RIGHT", row, "RIGHT", -2, 0)
|
|
row.down = CreateSmallButton(row, "down", 14)
|
|
row.down:SetPoint("RIGHT", row.delete, "LEFT", -4, 0)
|
|
row.up = CreateSmallButton(row, "up", 14)
|
|
row.up:SetPoint("RIGHT", row.down, "LEFT", -4, 0)
|
|
|
|
row.up:SetScript("OnClick", function() if row.index then MoveTrigger(row.index, -1) end end)
|
|
row.down:SetScript("OnClick", function() if row.index then MoveTrigger(row.index, 1) end end)
|
|
row.delete:SetScript("OnClick", function()
|
|
if not row.index then return end
|
|
local t = table.remove(db.triggers, row.index)
|
|
if t and t.id == selectedTriggerID then selectedTriggerID = nil end
|
|
UpdateEngine()
|
|
RefreshTriggerPanel()
|
|
end)
|
|
|
|
row.text = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
|
|
row.text:SetPoint("LEFT", row.check, "RIGHT", 4, 0)
|
|
row.text:SetPoint("RIGHT", row.up, "LEFT", -4, 0)
|
|
row.text:SetJustifyH("LEFT")
|
|
|
|
row:SetScript("OnClick", function()
|
|
if not row.trigger then return end
|
|
selectedTriggerID = row.trigger.id
|
|
RefreshTriggerPanel()
|
|
end)
|
|
|
|
row:SetScript("OnEnter", function()
|
|
if not row.trigger then return end
|
|
GameTooltip:SetOwner(row, "ANCHOR_RIGHT")
|
|
GameTooltip:SetText(DescribeTrigger(row.trigger), 1, 1, 1)
|
|
local t = row.trigger
|
|
local equip = SetExists(t.setID) and SetName(t.setID) or ("|cffff5555" .. T["No set selected"] .. "|r")
|
|
GameTooltip:AddDoubleLine(T["Equip"], equip, .7, .7, .7, 1, 1, 1)
|
|
local leave = T["Restore Previous Gear"]
|
|
if t.leave == "none" then leave = T["Do Nothing"]
|
|
elseif type(t.leave) == "number" then leave = SetExists(t.leave) and SetName(t.leave) or "?" end
|
|
GameTooltip:AddDoubleLine(T["When It Ends"], leave, .7, .7, .7, 1, 1, 1)
|
|
if db.applied == t.id then
|
|
GameTooltip:AddLine(T["Currently active"], .2, 1, .2)
|
|
elseif TriggerUsable(t) and ConditionActive(t) then
|
|
GameTooltip:AddLine(T["Condition met, but a trigger higher in the list wins"], 1, .82, 0)
|
|
end
|
|
GameTooltip:Show()
|
|
end)
|
|
row:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
|
|
|
triggerRows[i] = row
|
|
end
|
|
|
|
local btnNewTrigger = CreateFrame("Button", nil, triggerPanel, "UIPanelButtonTemplate")
|
|
btnNewTrigger:SetSize(110, 22)
|
|
btnNewTrigger:SetPoint("TOPLEFT", triggerList, "BOTTOMLEFT", 0, -6)
|
|
btnNewTrigger:SetText(T["New Trigger"])
|
|
SkinButton(btnNewTrigger)
|
|
btnNewTrigger:SetScript("OnClick", function()
|
|
local ids = GetVisibleSetIDs()
|
|
local t = {
|
|
id = db.nextID,
|
|
enabled = true,
|
|
type = "mounted",
|
|
setID = ids[1],
|
|
leave = "restore",
|
|
}
|
|
db.nextID = db.nextID + 1
|
|
table.insert(db.triggers, t)
|
|
selectedTriggerID = t.id
|
|
-- Scroll so the new trigger is visible.
|
|
triggerPanel.offset = math.max(0, table.getn(db.triggers) - TRIGGER_ROWS)
|
|
UpdateEngine()
|
|
RefreshTriggerPanel()
|
|
end)
|
|
|
|
-- Editor for the selected trigger
|
|
local editor = CreateFrame("Frame", nil, triggerPanel)
|
|
editor:SetPoint("TOPLEFT", btnNewTrigger, "BOTTOMLEFT", 0, -12)
|
|
editor:SetSize(282, 108)
|
|
|
|
editor.header = editor:CreateFontString(nil, "OVERLAY", "GameFontNormal")
|
|
editor.header:SetPoint("TOPLEFT", editor, "TOPLEFT", 0, 0)
|
|
editor.header:SetText(T["Selected Trigger"])
|
|
|
|
local EDITOR_ROW_Y = { -18, -44, -70, -96 }
|
|
local function EditorLabel(text, row)
|
|
local fs = editor:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
|
|
fs:SetPoint("TOPLEFT", editor, "TOPLEFT", 0, EDITOR_ROW_Y[row] - 4)
|
|
fs:SetText(text)
|
|
return fs
|
|
end
|
|
|
|
local function EditorDropdown(row)
|
|
local dd = CreateDropDownButton(nil, editor)
|
|
dd:SetWidth(190)
|
|
dd:SetPoint("TOPRIGHT", editor, "TOPRIGHT", 0, EDITOR_ROW_Y[row])
|
|
return dd
|
|
end
|
|
|
|
local function SelectedTrigger()
|
|
return FindTrigger(selectedTriggerID)
|
|
end
|
|
|
|
local function OnTriggerEdited()
|
|
-- Re-evaluate from scratch: the edit may change which trigger wins,
|
|
-- and an edited active trigger may now point at another set.
|
|
if selectedTriggerID and selectedTriggerID == db.applied then engine.forceApply = true end
|
|
UpdateEngine()
|
|
RefreshTriggerPanel()
|
|
end
|
|
|
|
EditorLabel(T["When"], 1)
|
|
local ddType = EditorDropdown(1)
|
|
ddType:SetMenu(function()
|
|
local menu = {}
|
|
for _, info in ipairs(TRIGGER_TYPES) do
|
|
if info.value ~= "list" or info.options then
|
|
local key = info.key
|
|
table.insert(menu, { text = info.text, value = key, func = function()
|
|
local t = SelectedTrigger()
|
|
if not t or t.type == key then return end
|
|
t.type = key
|
|
t.value = DefaultTriggerValue(key)
|
|
OnTriggerEdited()
|
|
end })
|
|
end
|
|
end
|
|
return menu
|
|
end)
|
|
|
|
local valueLabel = EditorLabel(T["Value"], 2)
|
|
local ddValue = EditorDropdown(2)
|
|
ddValue:SetMenu(function()
|
|
local menu = {}
|
|
local t = SelectedTrigger()
|
|
local info = t and TRIGGER_TYPE[t.type]
|
|
for _, o in ipairs(info and info.options or {}) do
|
|
local value = o.value
|
|
table.insert(menu, { text = o.text, value = value, func = function()
|
|
local t = SelectedTrigger()
|
|
if not t then return end
|
|
t.value = value
|
|
OnTriggerEdited()
|
|
end })
|
|
end
|
|
return menu
|
|
end)
|
|
|
|
local editValue = CreateTextBox(nil, editor)
|
|
editValue:SetHeight(20)
|
|
editValue:SetTextInsets(4, 4, 2, 2)
|
|
editValue:SetScript("OnTextChanged", function()
|
|
if this.updating then return end
|
|
local t = SelectedTrigger()
|
|
if not t then return end
|
|
t.value = this:GetText()
|
|
Schedule()
|
|
end)
|
|
editValue:SetScript("OnEnterPressed", function()
|
|
this:ClearFocus()
|
|
RefreshTriggerPanel()
|
|
end)
|
|
editValue:SetScript("OnEditFocusGained", function() this.hasFocus = true end)
|
|
editValue:SetScript("OnEditFocusLost", function()
|
|
this.hasFocus = nil
|
|
RefreshTriggerPanel()
|
|
end)
|
|
editValue:SetScript("OnEnter", function()
|
|
local t = SelectedTrigger()
|
|
if not t then return end
|
|
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
|
if t.type == "zone" then
|
|
GameTooltip:SetText(T["Zone or subzone name"], 1, 1, 1)
|
|
GameTooltip:AddLine(T["Separate several zones with ;"], .8, .8, .8, 1)
|
|
else
|
|
GameTooltip:SetText(T["Local time window, e.g. 20:00-23:00"], 1, 1, 1)
|
|
GameTooltip:AddLine(T["Windows may wrap past midnight (22:00-02:00)."], .8, .8, .8, 1)
|
|
end
|
|
GameTooltip:Show()
|
|
end)
|
|
editValue:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
|
|
|
local btnCurrentZone = CreateFrame("Button", nil, editor, "UIPanelButtonTemplate")
|
|
btnCurrentZone:SetSize(56, 20)
|
|
btnCurrentZone:SetPoint("TOPRIGHT", editor, "TOPRIGHT", 0, EDITOR_ROW_Y[2])
|
|
btnCurrentZone:SetText(T["Here"])
|
|
SkinButton(btnCurrentZone)
|
|
btnCurrentZone:SetScript("OnEnter", function()
|
|
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
|
GameTooltip:SetText(T["Add the current zone"], 1, 1, 1)
|
|
GameTooltip:AddLine(GetRealZoneText() .. ((GetSubZoneText() or "") ~= "" and (" / " .. GetSubZoneText()) or ""), .8, .8, .8)
|
|
GameTooltip:Show()
|
|
end)
|
|
btnCurrentZone:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
|
btnCurrentZone:SetScript("OnClick", function()
|
|
local t = SelectedTrigger()
|
|
if not t then return end
|
|
-- Shift adds the subzone instead of the zone.
|
|
local here = IsShiftKeyDown() and GetSubZoneText() or GetRealZoneText()
|
|
if here == "" then here = GetRealZoneText() end
|
|
local entries = ZoneEntries(t.value)
|
|
if table.getn(entries) == 0 then
|
|
t.value = here
|
|
else
|
|
for _, entry in ipairs(entries) do
|
|
if entry == here:lower() then return end
|
|
end
|
|
t.value = t.value .. "; " .. here
|
|
end
|
|
OnTriggerEdited()
|
|
end)
|
|
|
|
editValue:SetPoint("TOPLEFT", editor, "TOPRIGHT", -190, EDITOR_ROW_Y[2])
|
|
editValue:SetPoint("BOTTOMRIGHT", editor, "TOPRIGHT", 0, EDITOR_ROW_Y[2] - 20)
|
|
|
|
EditorLabel(T["Equip"], 3)
|
|
local ddSet = EditorDropdown(3)
|
|
ddSet:SetMenu(function()
|
|
local menu = {}
|
|
for _, id in ipairs(GetVisibleSetIDs()) do
|
|
local setID = id
|
|
table.insert(menu, { text = SetName(setID), value = setID, func = function()
|
|
local t = SelectedTrigger()
|
|
if not t then return end
|
|
t.setID = setID
|
|
OnTriggerEdited()
|
|
end })
|
|
end
|
|
return menu
|
|
end)
|
|
|
|
EditorLabel(T["When It Ends"], 4)
|
|
local ddLeave = EditorDropdown(4)
|
|
ddLeave:SetMenu(function()
|
|
local function Choose(value)
|
|
return function()
|
|
local t = SelectedTrigger()
|
|
if not t then return end
|
|
t.leave = value
|
|
OnTriggerEdited()
|
|
end
|
|
end
|
|
local menu = {
|
|
{ text = T["Restore Previous Gear"], value = "restore", func = Choose("restore") },
|
|
{ text = T["Do Nothing"], value = "none", func = Choose("none") },
|
|
}
|
|
for _, id in ipairs(GetVisibleSetIDs()) do
|
|
table.insert(menu, { text = T["Equip"] .. " " .. SetName(id), value = id, func = Choose(id) })
|
|
end
|
|
return menu
|
|
end)
|
|
|
|
editor.noSets = editor:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
|
|
editor.noSets:SetPoint("TOPRIGHT", editor, "TOPRIGHT", 0, EDITOR_ROW_Y[3] - 4)
|
|
editor.noSets:SetText(T["Create an equipment set first."])
|
|
|
|
local triggerHint = triggerPanel:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
|
|
triggerHint:SetPoint("BOTTOMLEFT", triggerPanel, "BOTTOMLEFT", 14, 12)
|
|
triggerHint:SetPoint("BOTTOMRIGHT", triggerPanel, "BOTTOMRIGHT", -14, 12)
|
|
triggerHint:SetHeight(40) -- room to wrap to three lines below the editor
|
|
triggerHint:SetJustifyH("LEFT")
|
|
triggerHint:SetJustifyV("BOTTOM")
|
|
triggerHint:SetText(T["Triggers higher in the list take priority. Gear is never swapped in combat, while dead or while casting: the switch waits until it can happen."])
|
|
|
|
editor.placeholder = triggerPanel:CreateFontString(nil, "OVERLAY", "GameFontDisable")
|
|
editor.placeholder:SetPoint("TOPLEFT", editor, "TOPLEFT", 0, -30)
|
|
editor.placeholder:SetPoint("TOPRIGHT", editor, "TOPRIGHT", 0, -30)
|
|
editor.placeholder:SetText(T["Select a trigger to edit it."])
|
|
|
|
local function RefreshTriggerRows()
|
|
cbAuto:SetChecked(C.equipmentmanager.autoswitch == "1")
|
|
|
|
local num = table.getn(db.triggers)
|
|
local maxOffset = math.max(0, num - TRIGGER_ROWS)
|
|
if triggerPanel.offset > maxOffset then triggerPanel.offset = maxOffset end
|
|
if triggerPanel.offset < 0 then triggerPanel.offset = 0 end
|
|
|
|
for i, row in ipairs(triggerRows) do
|
|
local index = i + triggerPanel.offset
|
|
local t = db.triggers[index]
|
|
if t then
|
|
row.trigger = t
|
|
row.index = index
|
|
row.check:SetChecked(t.enabled)
|
|
|
|
local color = "|cffffffff"
|
|
if not t.enabled then
|
|
color = "|cff808080"
|
|
elseif not SetExists(t.setID) then
|
|
color = "|cffff5555"
|
|
elseif db.applied == t.id then
|
|
color = "|cff33ff33"
|
|
elseif ConditionActive(t) then
|
|
color = "|cffffd100"
|
|
end
|
|
local target = SetExists(t.setID) and SetName(t.setID) or T["No set selected"]
|
|
row.text:SetText(color .. DescribeTrigger(t) .. "|r |cffaaaaaa>|r " .. color .. target .. "|r")
|
|
|
|
if t.id == selectedTriggerID then row.highlight:Show() else row.highlight:Hide() end
|
|
if index > 1 then row.up:Enable() else row.up:Disable() end
|
|
if index < num then row.down:Enable() else row.down:Disable() end
|
|
row:Show()
|
|
else
|
|
row.trigger = nil
|
|
row.index = nil
|
|
row:Hide()
|
|
end
|
|
end
|
|
if num == 0 then triggerList.empty:Show() else triggerList.empty:Hide() end
|
|
end
|
|
|
|
function RefreshTriggerPanel()
|
|
if not triggerPanel:IsShown() then return end
|
|
RefreshTriggerRows()
|
|
|
|
local num = table.getn(db.triggers)
|
|
local t = SelectedTrigger()
|
|
if not t then
|
|
editor:Hide()
|
|
editor.placeholder:SetShown(num > 0)
|
|
return
|
|
end
|
|
editor:Show()
|
|
editor.placeholder:Hide()
|
|
|
|
SelectDropdownValue(ddType, t.type)
|
|
|
|
local info = TRIGGER_TYPE[t.type]
|
|
local kind = info and info.value
|
|
valueLabel:SetShown(kind ~= nil)
|
|
ddValue:SetShown(kind == "list")
|
|
editValue:SetShown(kind == "text")
|
|
btnCurrentZone:SetShown(t.type == "zone")
|
|
if kind == "list" then
|
|
SelectDropdownValue(ddValue, t.value)
|
|
elseif kind == "text" then
|
|
editValue:ClearAllPoints()
|
|
editValue:SetPoint("TOPLEFT", editor, "TOPRIGHT", -190, EDITOR_ROW_Y[2])
|
|
if t.type == "zone" then
|
|
editValue:SetPoint("BOTTOMRIGHT", btnCurrentZone, "BOTTOMLEFT", -4, 0)
|
|
else
|
|
editValue:SetPoint("BOTTOMRIGHT", editor, "TOPRIGHT", 0, EDITOR_ROW_Y[2] - 20)
|
|
end
|
|
if not editValue.hasFocus then
|
|
editValue.updating = true
|
|
editValue:SetText(t.value or "")
|
|
editValue.updating = nil
|
|
end
|
|
local valid = t.type ~= "time" or ParseTimeWindow(t.value)
|
|
editValue:SetTextColor(1, valid and 1 or .3, valid and 1 or .3)
|
|
end
|
|
|
|
local hasSets = table.getn(GetVisibleSetIDs()) > 0
|
|
ddSet:SetShown(hasSets)
|
|
editor.noSets:SetShown(not hasSets)
|
|
SelectDropdownValue(ddSet, t.setID)
|
|
SelectDropdownValue(ddLeave, t.leave or "restore")
|
|
end
|
|
|
|
triggerPanel:SetScript("OnShow", function() RefreshTriggerPanel() end)
|
|
-- Row colors show which conditions currently hold; keep them live.
|
|
triggerPanel:SetScript("OnUpdate", function()
|
|
this.elapsed = (this.elapsed or 0) + (arg1 or 0)
|
|
if this.elapsed < 1 then return end
|
|
this.elapsed = 0
|
|
RefreshTriggerRows()
|
|
end)
|
|
triggerPanel:SetScript("OnHide", function()
|
|
for _, dd in ipairs({ ddType, ddValue, ddSet, ddLeave }) do dd:HideMenu() end
|
|
editValue:ClearFocus()
|
|
end)
|
|
|
|
-- ============================================================
|
|
-- Sidecar buttons that open the panels
|
|
-- ============================================================
|
|
|
|
local btnUnused = CreateFrame("Button", "pfEqMgrUnused", frame, "UIPanelButtonTemplate")
|
|
btnUnused:SetSize(86, 22)
|
|
btnUnused:SetPoint("TOPLEFT", listFrame, "BOTTOMLEFT", 0, -8)
|
|
btnUnused:SetText(T["Unused"])
|
|
SkinButton(btnUnused)
|
|
btnUnused:SetScript("OnClick", function() ToggleSubPanel(unusedPanel) end)
|
|
btnUnused:SetScript("OnEnter", function()
|
|
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
|
GameTooltip:SetText(T["Items Not In Any Set"], 1, 1, 1)
|
|
GameTooltip:AddLine(T["Equippable items in your bags, bank and paperdoll that no equipment set uses."], .8, .8, .8, 1)
|
|
GameTooltip:Show()
|
|
end)
|
|
btnUnused:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
|
|
|
local btnTriggers = CreateFrame("Button", "pfEqMgrTriggers", frame, "UIPanelButtonTemplate")
|
|
btnTriggers:SetSize(86, 22)
|
|
btnTriggers:SetPoint("LEFT", btnUnused, "RIGHT", 6, 0)
|
|
btnTriggers:SetText(T["Triggers"])
|
|
SkinButton(btnTriggers)
|
|
btnTriggers:SetScript("OnClick", function() ToggleSubPanel(triggerPanel) end)
|
|
btnTriggers:SetScript("OnEnter", function()
|
|
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
|
GameTooltip:SetText(T["Automatic Set Switching"], 1, 1, 1)
|
|
GameTooltip:AddLine(T["Equip sets automatically when mounted, swimming, eating, in a zone, at a given time and more."], .8, .8, .8, 1)
|
|
GameTooltip:Show()
|
|
end)
|
|
btnTriggers:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
|
|
|
-- Called at the end of every Refresh().
|
|
function pfUI.equipmentmanager.RefreshExtras()
|
|
local list = ScanUnusedItems()
|
|
btnUnused:SetText(T["Unused"] .. " (" .. table.getn(list) .. ")")
|
|
if unusedPanel:IsShown() then RefreshUnusedPanel() end
|
|
if triggerPanel:IsShown() then RefreshTriggerPanel() end
|
|
end
|
|
|
|
-- pfUI GUI hook (U["equipmentmanager"]).
|
|
function pfUI.equipmentmanager.UpdateConfig()
|
|
UpdateEngine()
|
|
if frame:IsShown() then pfUI.equipmentmanager.Refresh() end
|
|
end
|
|
|
|
UpdateEngine()
|
|
|
|
-- ============================================================
|
|
-- 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")
|
|
events:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
|
|
events:SetScript("OnEvent", function()
|
|
if not frame:IsShown() then return end
|
|
pfUI.equipmentmanager.Refresh()
|
|
end)
|
|
|
|
end)
|