Files
moon-well-ui/data/addon/ForeverUI/IconPicker.lua
T
Claude 39fbfc8c10 ForeverUI : barres, la barre de forme reste à l'écran en combat
Les boutons bonus sont ancrés à la glissière, ce qui la rend protégée :
en combat, ses SetPoint étaient bloqués (action bloquée par image). Après
un retrait hors combat elle reposait 45 px plus bas, et un druide prenant
une forme en combat gardait sa barre à moitié sous l'écran tout le combat.
Elle ne bouge plus en combat, attend au repos, y revient avant le
verrouillage, et suit une barre réaffichée pendant son propre retrait.
Durée : BONUSACTIONBAR_SLIDETIME du client (0,15 s).

Familier et postures : le porteur, protégé par les boutons qu'on y ancre,
était masqué avant le test de combat (familier mort ou expiré en combat :
action bloquée). Le test passe d'abord ; la sortie de combat le replace.

Choix d'icône d'un ensemble : une icône portée deux fois (deux anneaux,
bijoux ou armes identiques) ne compte qu'une fois, comme
RefreshEquipmentSetIconInfo, sinon la grille gagnait des cases vides au
delà de la liste du client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KxRZNHkq453dyUMcWsyUf1
2026-10-01 23:19:58 +00:00

506 lines
17 KiB
Lua

-- Icon picker of equipment sets (GearManagerDialogPopup) in the layout of camelot
-- IconSelectorPopupFrameTemplate (SharedUIPanelTemplates.xml) and ScrollBoxSelectorMixin.
-- The client fills a grid of 5 per row through the NUM_GEARSET_ICON* globals. Writing them
-- from the addon would taint every client function that reads them, so the client fills its
-- grid and we redo it after it in rows of 10 (populate, realign).
ForeverUI = ForeverUI or {}
local L = ForeverUI.L
-- Positions from camelot IconSelectorPopupFrameTemplate. The window takes the character
-- sheet's height instead of camelot's 495; from the grid (-97) to the button base, seven
-- rows fit.
local POPUP_W, POPUP_H = 525, 484
local HEADER_X, HEADER_Y = 24, -21
local FIELD_X, FIELD_Y = 29, -35
local CHOOSE_X, CHOOSE_Y = 24, -79
local ZONE_W, ZONE_H = 275, 45
local ZONE_X, ZONE_Y = -13, -13
local CHOICE_ICON = 36
local CHOICE_X, CHOICE_Y = -4.5, -3.5
-- camelot ScrollBoxSelectorMixin: stride 10, button 36, padding 5, spacing 10
local GRID_X, GRID_Y = 21, -97
local ICON = 36
local PER_ROW = 10
local ROWS = 7
local GAP = 10
local MARGIN = 5
local STEP = ICON + GAP
-- camelot SelectionFrameTemplate buttons: Cancel 78 x 22 at BOTTOMRIGHT (-11, 13), Okay at
-- its left (x = -2). They fall exactly in the two slots of the bottom right corner art.
local BOTTOM_BUTTON_W, BOTTOM_BUTTON_H = 78, 22
local BOTTOM_BUTTON_X, BOTTOM_BUTTON_Y = -11, 13
local BOTTOM_BUTTON_GAP = -2
local RIGHT_EDGE = 17 -- width of !macropopup-right
-- Skin sizes (background, frame, bar). Declared here because placeWindow uses them to
-- center the bar in the remaining space.
local BACKGROUND_ALPHA = 0.8
local BACKGROUND_MARGIN = 7
local BAR_W = 8
local ARROW_W, ARROW_H = 17, 11
local CURSOR_H = 36 -- minimal-scrollbar-thumb-bottom
local built = false
local SHOWN_COUNT = PER_ROW * ROWS
-- Creates the missing buttons on the client's template (the client makes only fifteen, at
-- load), then lays out the whole grid in rows of ten.
local function layoutGrid(popup)
for index = #popup.buttons + 1, SHOWN_COUNT do
local button = CreateFrame("CheckButton",
"GearManagerDialogPopupButton" .. index, popup,
"GearSetPopupButtonTemplate")
button:SetID(index)
table.insert(popup.buttons, button)
end
for index, button in ipairs(popup.buttons) do
button:SetWidth(ICON)
button:SetHeight(ICON)
button:ClearAllPoints()
if index == 1 then
button:SetPoint("TOPLEFT", popup, "TOPLEFT",
GRID_X + MARGIN, GRID_Y - MARGIN)
elseif math.fmod(index - 1, PER_ROW) == 0 then
button:SetPoint("TOPLEFT", popup.buttons[index - PER_ROW],
"BOTTOMLEFT", 0, -GAP)
else
button:SetPoint("TOPLEFT", popup.buttons[index - 1], "TOPRIGHT",
GAP, 0)
end
end
end
-- Current choice zone. camelot shows the selected icon and a click scrolls the list to it;
-- 3.3.5 does this jump with RecalculateGearManagerDialogPopup.
local function placeCurrentChoice(popup)
if popup.foreverChoice then
return
end
local zone = CreateFrame("Frame", "ForeverUIIconChoice", popup)
zone:SetWidth(ZONE_W)
zone:SetHeight(ZONE_H)
zone:SetPoint("TOPRIGHT", popup, "TOPRIGHT", ZONE_X, ZONE_Y)
local button = CreateFrame("Button", "ForeverUIIconChoiceButton", zone)
button:SetWidth(CHOICE_ICON)
button:SetHeight(CHOICE_ICON)
button:SetPoint("TOPRIGHT", zone, "TOPRIGHT", CHOICE_X, CHOICE_Y)
local icon = button:CreateTexture(nil, "ARTWORK")
icon:SetAllPoints(button)
button.icon = icon
local highlight = button:CreateTexture(nil, "HIGHLIGHT")
highlight:SetTexture("Interface\\Buttons\\ButtonHilight-Square")
highlight:SetBlendMode("ADD")
highlight:SetAllPoints(button)
-- From the text table: ICON_SELECTION_TITLE_CURRENT and its description are missing from
-- this client.
local title = zone:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
title:SetPoint("TOPRIGHT", button, "TOPLEFT", -6, -2)
title:SetText(L.ICONPICKER_CURRENTLY_SELECTED)
local help = zone:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
help:SetPoint("TOPRIGHT", title, "BOTTOMRIGHT", 0, -2)
help:SetText(L.ICONPICKER_CLICK_TO_VIEW)
button:SetScript("OnClick", function()
if RecalculateGearManagerDialogPopup then
RecalculateGearManagerDialogPopup()
end
end)
popup.foreverChoice = button
popup.foreverZone = zone
end
-- The icon shown in the zone follows the client's selection.
local function updateCurrentChoice()
local popup = _G["GearManagerDialogPopup"]
if not popup or not popup.foreverChoice then
return
end
local texture = popup.selectedTexture
if not texture and popup.selectedIcon and GetEquipmentSetIconInfo then
texture = GetEquipmentSetIconInfo(popup.selectedIcon)
end
popup.foreverChoice.icon:SetTexture(texture or "")
end
-- Sizes the window and places the client's name field, labels, scroll frame and buttons
local function placeWindow(popup)
local sheet = _G["CharacterFrame"]
popup:SetWidth(POPUP_W)
popup:SetHeight((sheet and sheet:GetHeight()) or POPUP_H)
local field = _G["GearManagerDialogPopupEditBox"]
if field then
field:ClearAllPoints()
field:SetPoint("TOPLEFT", popup, "TOPLEFT", FIELD_X, FIELD_Y)
end
-- The two client labels are unnamed regions, found by their text (GEARSETS_POPUP_TEXT and
-- MACRO_POPUP_CHOOSE_ICON).
local regions = { popup:GetRegions() }
for _, region in ipairs(regions) do
if region.GetObjectType and region:GetObjectType() == "FontString" then
local text = region:GetText()
region:ClearAllPoints()
if text == MACRO_POPUP_CHOOSE_ICON then
region:SetPoint("TOPLEFT", popup, "TOPLEFT", CHOOSE_X, CHOOSE_Y)
else
region:SetPoint("TOPLEFT", popup, "TOPLEFT", HEADER_X, HEADER_Y)
end
end
end
local scrolling = _G["GearManagerDialogPopupScrollFrame"]
if scrolling then
scrolling:SetWidth(PER_ROW * STEP - GAP + 2 * MARGIN)
scrolling:SetHeight(ROWS * STEP - GAP + 2 * MARGIN)
scrolling:ClearAllPoints()
scrolling:SetPoint("TOPLEFT", popup, "TOPLEFT", GRID_X, GRID_Y)
end
-- Same space on both sides of the bar: between the last icon column and the bar, and
-- between the bar and the window edge. It is computed, so it follows the grid and window.
local bar = _G["GearManagerDialogPopupScrollFrameScrollBar"]
if bar then
local iconsRight = GRID_X + MARGIN + PER_ROW * STEP - GAP
local free = (POPUP_W - RIGHT_EDGE) - iconsRight
local gap = (free - BAR_W) / 2
bar:ClearAllPoints()
bar:SetPoint("TOPRIGHT", popup, "TOPRIGHT",
-(RIGHT_EDGE + gap), GRID_Y - MARGIN)
bar:SetPoint("BOTTOMRIGHT", popup, "TOPRIGHT",
-(RIGHT_EDGE + gap), GRID_Y - MARGIN - (ROWS * STEP - GAP))
end
local okay = _G["GearManagerDialogPopupOkay"]
local cancel = _G["GearManagerDialogPopupCancel"]
if cancel then
cancel:SetWidth(BOTTOM_BUTTON_W)
cancel:SetHeight(BOTTOM_BUTTON_H)
cancel:ClearAllPoints()
cancel:SetPoint("BOTTOMRIGHT", popup, "BOTTOMRIGHT",
BOTTOM_BUTTON_X, BOTTOM_BUTTON_Y)
end
if okay and cancel then
okay:SetWidth(BOTTOM_BUTTON_W)
okay:SetHeight(BOTTOM_BUTTON_H)
okay:ClearAllPoints()
okay:SetPoint("RIGHT", cancel, "LEFT", BOTTOM_BUTTON_GAP, 0)
end
end
-- ------------------------------------------------------------ filling
-- Equipped items with an icon, like RefreshEquipmentSetIconInfo, which counts an icon worn
-- twice (two rings, trinkets or weapons alike) once: otherwise the grid gains blank cells
-- whose index lies past the client's list
local seenIcons = {}
local function countEquippedItems()
local n = 0
wipe(seenIcons)
for i = INVSLOT_FIRST_EQUIPPED, INVSLOT_LAST_EQUIPPED do
local texture = GetInventoryItemTexture("player", i)
if texture and not seenIcons[texture] then
seenIcons[texture] = true
n = n + 1
end
end
return n
end
-- The client's count (_TotalItems): items, macro icons, and the special icon when
-- RecalculateGearManagerDialogPopup has set one
local function tally()
local base = countEquippedItems() + GetNumMacroIcons()
if GetEquipmentSetIconInfo(base + 1) then
base = base + 1
end
return base
end
local inProgress = false
-- After GearManagerDialogPopup_Update: refill as a grid of ten. Each button's ID is set so
-- that GearSetPopupButton_OnClick (offset x 5 + ID) lands on the icon it shows. Scrolling
-- counts rows of ten with the client's GEARSET_ICON_ROW_HEIGHT (read only).
local function populate()
local popup = _G["GearManagerDialogPopup"]
local scrolling = _G["GearManagerDialogPopupScrollFrame"]
if inProgress or not (built and popup and scrolling and popup.buttons) then
return
end
inProgress = true
local offset = FauxScrollFrame_GetOffset(scrolling) or 0
local total = tally()
local clientPerRow = NUM_GEARSET_ICONS_PER_ROW or 5
for i, button in ipairs(popup.buttons) do
local index = offset * PER_ROW + i
button:SetID(offset * (PER_ROW - clientPerRow) + i)
if i <= SHOWN_COUNT and index <= total then
local texture = GetEquipmentSetIconInfo(index)
button.icon:SetTexture(texture)
button:Show()
if index == popup.selectedIcon then
button:SetChecked(1)
elseif texture and texture == popup.selectedTexture then
button:SetChecked(1)
popup:SetSelection(false, index)
else
button:SetChecked(nil)
end
else
button.icon:SetTexture("")
button:Hide()
end
end
FauxScrollFrame_Update(scrolling, math.ceil(total / PER_ROW), ROWS, GEARSET_ICON_ROW_HEIGHT)
inProgress = false
end
-- After RecalculateGearManagerDialogPopup: scroll the selected icon into view with the
-- client's rule (at least ROWS rows shown, no move if it is on the first page)
local function realign()
local popup = _G["GearManagerDialogPopup"]
local scrolling = _G["GearManagerDialogPopupScrollFrame"]
if not (built and popup and scrolling) then
return
end
local total = tally()
local match = popup.selectedIcon
if not match and popup.selectedTexture then
for index = 1, total do
if GetEquipmentSetIconInfo(index) == popup.selectedTexture then
match = index
break
end
end
end
if match then
local last = math.floor((total - 1) / PER_ROW)
local rowLine = math.floor((match - 1) / PER_ROW)
rowLine = rowLine + math.min(ROWS - 1, last - rowLine) - (ROWS - 1)
if match <= SHOWN_COUNT then
rowLine = 0
end
FauxScrollFrame_OnVerticalScroll(scrolling, rowLine * GEARSET_ICON_ROW_HEIGHT, GEARSET_ICON_ROW_HEIGHT, nil)
end
populate()
end
-- Builds the grid and choice zone once, then applies the skin; skipped in combat
local function applySkin()
local popup = _G["GearManagerDialogPopup"]
if not popup or not popup.buttons or InCombatLockdown() then
return
end
if not built then
layoutGrid(popup)
placeCurrentChoice(popup)
placeWindow(popup)
built = true
end
if ForeverUI.IconPickerSkin then
ForeverUI.IconPickerSkin()
end
updateCurrentChoice()
end
ForeverUI.IconPicker = { Apply = applySkin }
-- ============================================================ skin
--
-- camelot: black background at 80 %, inset 7; SelectionFrameTemplate frame, a nine-slice
-- of macropopup-* atlases (the bottom right corner is 174 wide: it holds the button base);
-- MinimalScrollBar, 8 wide, three-piece track and thumb, 17 x 11 arrows.
-- Replaces the 3.3.5 window art with the camelot background and frame
local function skinFrame(popup)
if popup.foreverFrame then
return
end
-- Hide the 3.3.5 window art.
local regions = { popup:GetRegions() }
for _, region in ipairs(regions) do
if region.GetObjectType and region:GetObjectType() == "Texture" then
region:SetAlpha(0)
end
end
local background = popup:CreateTexture(nil, "BACKGROUND")
background:SetTexture(0, 0, 0, BACKGROUND_ALPHA)
background:SetPoint("TOPLEFT", popup, "TOPLEFT", BACKGROUND_MARGIN, -BACKGROUND_MARGIN)
background:SetPoint("BOTTOMRIGHT", popup, "BOTTOMRIGHT", -BACKGROUND_MARGIN, BACKGROUND_MARGIN)
local function piece(atlas, point)
local t = popup:CreateTexture(nil, "BORDER")
if not ForeverUI.SetAtlas(t, atlas) then
t:Hide()
end
if point then
t:SetPoint(point, popup, point, 0, 0)
end
return t
end
local topLeft = piece("macropopup-topleft-c60", "TOPLEFT")
local topRight = piece("macropopup-topright-c60", "TOPRIGHT")
local bottomLeft = piece("macropopup-bottomleft-c60", "BOTTOMLEFT")
local bottomRight = piece("macropopup-bottomright-c60", "BOTTOMRIGHT")
local top = piece("_macropopup-top-c60")
top:SetPoint("TOPLEFT", topLeft, "TOPRIGHT")
top:SetPoint("TOPRIGHT", topRight, "TOPLEFT")
local down = piece("_macropopup-bottom-c60")
down:SetPoint("BOTTOMLEFT", bottomLeft, "BOTTOMRIGHT")
down:SetPoint("BOTTOMRIGHT", bottomRight, "BOTTOMLEFT")
-- Side strips keep their atlas width: anchoring them by opposite corners would stretch them
-- to the next corner, and the bottom right corner is 174 wide. Two points on the same side
-- are enough.
local left = piece("!macropopup-left-c60")
left:SetPoint("TOPLEFT", topLeft, "BOTTOMLEFT")
left:SetPoint("BOTTOMLEFT", bottomLeft, "TOPLEFT")
local right = piece("!macropopup-right-c60")
right:SetPoint("TOPRIGHT", topRight, "BOTTOMRIGHT")
right:SetPoint("BOTTOMRIGHT", bottomRight, "TOPRIGHT")
popup.foreverFrame = { topLeft, topRight, bottomLeft, bottomRight, top, down, left, right }
popup.foreverBackground = background
end
-- Scroll bar: the FauxScrollFrame bar keeps its behavior; only its textures change.
local function skinBar()
local bar = _G["GearManagerDialogPopupScrollFrameScrollBar"]
if not bar or bar.foreverBar then
return
end
-- The bar's old trim is not on the bar: the client's scroll frame holds two 30-wide
-- textures (the track outline), so hide them too.
local frame = _G["GearManagerDialogPopupScrollFrame"]
if frame then
for _, region in ipairs({ frame:GetRegions() }) do
if region.GetObjectType and region:GetObjectType() == "Texture" then
region:SetAlpha(0)
end
end
end
bar:SetWidth(BAR_W)
-- The thumb is a region of the bar: keep it out of this loop, or it stays invisible after
-- being retextured.
local cursor = _G["GearManagerDialogPopupScrollFrameScrollBarThumbTexture"]
for _, region in ipairs({ bar:GetRegions() }) do
if region ~= cursor and region.GetObjectType
and region:GetObjectType() == "Texture" then
region:SetAlpha(0)
end
end
local function slice(atlas, layer)
local t = bar:CreateTexture(nil, layer or "BACKGROUND")
if not ForeverUI.SetAtlas(t, atlas) then
t:Hide()
end
t:SetWidth(BAR_W)
return t
end
local trackTop = slice("minimal-scrollbar-track-top-c60")
trackTop:SetPoint("TOP", bar, "TOP", 0, 0)
local bottomLeft = slice("minimal-scrollbar-track-bottom-c60")
bottomLeft:SetPoint("BOTTOM", bar, "BOTTOM", 0, 0)
local trackMiddle = slice("!minimal-scrollbar-track-middle-c60")
trackMiddle:SetPoint("TOPLEFT", trackTop, "BOTTOMLEFT")
trackMiddle:SetPoint("BOTTOMRIGHT", bottomLeft, "TOPRIGHT")
-- The client has one thumb texture where camelot has three: use the stretchable middle
-- piece at the size of camelot's shortest thumb (8 x 36).
if cursor then
ForeverUI.SetAtlas(cursor, "minimal-scrollbar-thumb-middle-c60", true)
cursor:SetWidth(BAR_W)
cursor:SetHeight(CURSOR_H)
cursor:SetAlpha(1)
end
for name, atlas in pairs({
["GearManagerDialogPopupScrollFrameScrollBarScrollUpButton"] =
"minimal-scrollbar-arrow-top-c60",
["GearManagerDialogPopupScrollFrameScrollBarScrollDownButton"] =
"minimal-scrollbar-arrow-bottom-c60",
}) do
local button = _G[name]
if button then
button:SetWidth(ARROW_W)
button:SetHeight(ARROW_H)
for _, method in ipairs({ "GetNormalTexture", "GetPushedTexture",
"GetDisabledTexture", "GetHighlightTexture" }) do
local texture = button[method] and button[method](button)
if texture then
texture:SetAlpha(0)
end
end
local arrow = button:CreateTexture(nil, "ARTWORK")
ForeverUI.SetAtlas(arrow, atlas)
arrow:SetPoint("CENTER", button, "CENTER", 0, 0)
end
end
bar.foreverBar = true
end
-- Right of the character sheet: camelot anchors its TOPLEFT to the TOPRIGHT of the frame it
-- accompanies. The 3.3.5 anchor, the gear manager, is no longer a window here.
local function placeBeside(popup)
local sheet = _G["CharacterFrame"]
if not sheet then
return
end
popup:ClearAllPoints()
popup:SetPoint("TOPLEFT", sheet, "TOPRIGHT", 0, 0)
end
ForeverUI.IconPickerSkin = function()
local popup = _G["GearManagerDialogPopup"]
if not popup then
return
end
skinFrame(popup)
skinBar()
placeBeside(popup)
end
-- Run at load, and last: the client fills its grid on first open and needs its eighty
-- buttons then; IconPickerSkin must already be defined when this runs.
applySkin()
if hooksecurefunc then
for _, name in ipairs({ "GearManagerDialogPopup_OnShow", "GearManagerDialogPopup_Update" }) do
if type(_G[name]) == "function" then
hooksecurefunc(name, function() applySkin() end)
end
end
if type(GearManagerDialogPopup_Update) == "function" then
hooksecurefunc("GearManagerDialogPopup_Update", populate)
end
if type(RecalculateGearManagerDialogPopup) == "function" then
hooksecurefunc("RecalculateGearManagerDialogPopup", realign)
end
end