Files
moon-well-ui/data/addon/ForeverUI/DropDown.lua
T
Adrestie 5842eb7a2b ForeverUI : code en anglais, commentaires resserres, code mort retire
Traduction
- Identifiants en anglais dans tout le module : addon, ecrans d'accueil,
  outils Python, banc d'essai et patcheur C#.
- Fichiers au nom francais renommes (Gabarits.lua -> Templates.lua,
  Textes_*.lua -> Texts_*.lua, deployer.py -> deploy.py,
  tools/patcheur -> tools/patcher, etc.).
- Commandes /fui et options de deploy.py en anglais (--check, --server,
  --remove-dbc).
- Cles des variables sauvegardees et de data/dbc/statistics.json
  traduites, sans reprise des anciennes cles.
- API de la micro-barre : ForeverUI.AddMicroButton(def) avec les champs
  name, atlasSet, after, tooltip, onClick, ready, et
  ForeverUI.UpdateMicro(name, isOpen).
- Commentaires reecrits en anglais, courts, sans dates ni historique ;
  les fonctions au nom peu parlant et leurs parametres sont expliques.

Code mort
- Exports, fonctions et variables jamais lus retires.
- Retires a la demande des modules : ForeverUI.PvPJauge, les exports
  FondEntete / TuilerEntete / Polices de TradeSkill, l'art legacy-up-c60,
  honorfill(half)blue et inv_misc_book_09, leurs entrees d'atlas et les
  parties d'outils correspondantes.

Verifications : liaisons des noms comparees avant et apres, banc vert,
chemins de textures inchanges, generateurs reproduisant les fichiers,
patcheur identique octet pour octet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:15:52 +02:00

598 lines
20 KiB
Lua

-- Dropdown lists with camelot's art. 3.3.5 opens every menu (dropdowns and right-click menus)
-- in the global DropDownList1 (DropDownList2 for a submenu), so skinning it skins them all.
-- Style 1 follows camelot's MenuStyle1Mixin (Blizzard_Menu, mainline family, c60 art):
-- common-dropdown-bg background, 20-high rows, GameFontHighlight, and the
-- common-dropdown-ticksquare box with the yellow checkmark over it. 3.3.5 has no radio
-- buttons (only info.checked), so the box and checkmark serve everywhere.
-- UIDROPDOWNMENU_BORDER_HEIGHT stays 15 for both top and bottom (camelot uses 8 and 15):
-- changing it would move every row of every menu.
ForeverUI = ForeverUI or {}
-- Background in nine slices. Camelot stretches one texture, but common-dropdown-bg-c60
-- (68 x 68) has a shadow around the panel (9 left and right, 6 top, 12 bottom) and 6 px cut
-- corners: stretched, the shadow grows and the gold edge moves inward. Corners keep their
-- size (18 = thickest shadow 12 + cut 6). The margins are the image's shadow, so the gold
-- edge falls exactly on the frame edge.
local BACKGROUND_ATLAS = "common-dropdown-bg-c60"
local BACKGROUND_CORNER = 18
local BACKGROUND_MARGINS = { 9, 6, 9, 12 } -- left, top, right, bottom
local BACKGROUND_ALPHA = 0.925
local ROW_HEIGHT = 20
local CHECKBOX_ATLAS = "common-dropdown-ticksquare"
local CHECKBOX = 12
local CHECKMARK_ATLAS = "common-dropdown-icon-checkmark-yellow"
local CHECKMARK_W, CHECKMARK_H = 15, 14
local CHECKMARK_X, CHECKMARK_Y = 2, 1
local BACKDROPS = { "Backdrop", "MenuBackdrop" }
-- Style 2, for settings: a dropdown of the options windows has foreverStyle = 2
-- (Settings.lua), and its list matches the login screen options (ForeverUIGlueDropDown.lua).
-- Camelot's MenuStyle2Mixin (mainline/menutemplates.lua): nine-slice common-dropdown-c-bg
-- from (-17, 12) to (17, -22), margins 3 / 6 / 3 / 7, 20-high rows, radial tick for a
-- single choice, width = content but at least the button, list below the button. Rows use
-- size 10 (GameFont*SmallLeft) instead of 12. Other menus keep style 1.
local STYLE2 = {
background = "common-dropdown-c-bg",
backgroundA = { -17, 12 },
backgroundB = { 17, -22 },
margins = { 3, 6, 3, 7 }, -- left, top, right, bottom
circle = "common-dropdown-tickradial",
point = "common-dropdown-icon-radialtick-yellow",
span = 16 + 20, -- circle (-3 .. 15) + 1, plus the padding
noRadio = 20,
}
local function style2(frame)
return frame ~= nil and type(frame) == "table" and frame.foreverStyle == 2
end
-- The menu the list serves now: the one being filled, else the open one
local function currentMenu()
return UIDROPDOWNMENU_INIT_MENU or UIDROPDOWNMENU_OPEN_MENU
end
-- Gap the client keeps between the list and its rows: list at maxWidth + 25, rows at
-- maxWidth. Kept so the right margin does not move when the list widens.
local LIST_MARGIN = 25
-- Width. The client sizes the list on its longest text (maxWidth + 25); camelot uses the
-- opening button as a floor (DropdownButtonMixin:RegisterMenu, SetMinimumWidth). Here the
-- list gets exactly the button width, even when an entry is longer (its text is squeezed).
-- It is set on show: ToggleDropDownMenu sets UIDROPDOWNMENU_OPEN_MENU before showing the
-- list and clamps it to the screen after, so the width must be final by then.
-- Only the first level of a dropdown does this. Context menus (displayMode "MENU", whose
-- opener is an invisible 40-wide frame) and submenus take the width of their content,
-- measured again per row (measureButton) in the displayed font: the client measures in
-- the smaller GameFontHighlightSmallLeft.
local function applyWidth(list, wantedValue)
if math.abs(list:GetWidth() - wantedValue) < 0.5 then
return
end
list:SetWidth(wantedValue)
for index = 1, (list.numButtons or 0) do
local button = _G[list:GetName() .. "Button" .. index]
if button then
button:SetWidth(wantedValue - LIST_MARGIN)
end
end
end
-- Style 2: content inside its margins, at least the button on the first level
local function style2Width(list, opener, level)
local m = STYLE2.margins
local wantedValue = (list.foreverContent2 or 0) + m[1] + m[3]
if level == 1 and opener.foreverButton and opener.displayMode ~= "MENU" then
wantedValue = math.max(wantedValue, opener.foreverButton:GetWidth() or 0)
end
if not wantedValue or wantedValue <= 0 then
return
end
list:SetWidth(wantedValue)
for index = 1, (list.numButtons or 0) do
local button = _G[list:GetName() .. "Button" .. index]
if button then
button:SetWidth(wantedValue - m[1] - m[3])
end
end
end
local function fitWidth(list)
local opener = UIDROPDOWNMENU_OPEN_MENU
local level = list.foreverLevel or list:GetID()
if list.foreverStyle2 and style2(opener) then
style2Width(list, opener, level)
return
end
if level ~= 1 or not opener or not opener.GetWidth or opener.displayMode == "MENU" then
local content = list.foreverContent
if content and content > 0 then
-- A ForeverUI menu can ask for a floor: the width of the button that opens it, as
-- camelot's SetMinimumWidth
local minimum = level == 1 and opener and opener.foreverMinimum or 0
applyWidth(list, math.max(content + LIST_MARGIN, minimum))
end
return
end
local wantedValue = opener:GetWidth()
if not wantedValue or wantedValue <= 0 then
return
end
-- A ForeverUI menu can ask for camelot's rule as is: the button as a floor, the list
-- growing with its content (SetMinimumWidth)
if opener.foreverFloor and list.foreverContent then
wantedValue = math.max(wantedValue, list.foreverContent + LIST_MARGIN)
end
applyWidth(list, wantedValue)
end
-- Row width by the UIDropDownMenu_AddButton formula: text + 40, + 10 for an arrow or a
-- color swatch, - 30 without a checkbox, + 10 for an icon, + the requested padding.
local function measureButton(list, button, info)
if list.numButtons == 1 then
list.foreverContent = 0
end
local text = _G[button:GetName() .. "NormalText"]
if not (text and info and info.text) then
return
end
local width = text:GetStringWidth() + 40
if info.hasArrow or info.hasColorSwatch then width = width + 10 end
if info.notCheckable then width = width - 30 end
if info.icon then width = width + 10 end
if info.padding then width = width + info.padding end
if width > (list.foreverContent or 0) then
list.foreverContent = width
end
end
-- The list drops its two old backdrops for camelot's background. The backdrop is removed
-- instead of hiding the frame: ToggleDropDownMenu shows one frame or the other on each
-- opening, by displayMode. A frame without backdrop draws nothing.
local function skinList(list)
if list.foreverBackground then
return
end
local name = list:GetName()
for _, suffix in ipairs(BACKDROPS) do
local frame = name and _G[name .. suffix]
if frame and frame.SetBackdrop then
frame:SetBackdrop(nil)
end
end
local slices = ForeverUI.CreateNineSlice(list, BACKGROUND_ATLAS, BACKGROUND_CORNER,
BACKGROUND_MARGINS, "BACKGROUND")
if not slices then
return
end
for _, slice in ipairs(slices) do
slice:SetAlpha(BACKGROUND_ALPHA)
end
list.foreverBackground = slices[1]
list.foreverSlices = slices
list:HookScript("OnShow", fitWidth)
end
-- Checkbox in BORDER, checkmark in ARTWORK: 3.3.5 has no draw sublevels, so only the
-- layer puts the checkmark over its box.
local function skinButton(button)
if button.foreverCell then
return
end
local checkbox = button:CreateTexture(nil, "BORDER")
if not ForeverUI.SetAtlas(checkbox, CHECKBOX_ATLAS, true) then
checkbox:Hide()
end
checkbox:SetWidth(CHECKBOX)
checkbox:SetHeight(CHECKBOX)
checkbox:SetPoint("LEFT", button, "LEFT", 0, 0)
checkbox:Hide()
button.foreverCell = checkbox
local checkMark = _G[button:GetName() .. "Check"]
if checkMark then
ForeverUI.SetAtlas(checkMark, CHECKMARK_ATLAS, true)
checkMark:SetWidth(CHECKMARK_W)
checkMark:SetHeight(CHECKMARK_H)
checkMark:ClearAllPoints()
checkMark:SetPoint("CENTER", checkbox, "CENTER", CHECKMARK_X, CHECKMARK_Y)
end
end
-- Restores the style 1 checkmark after a style 2 list changed it
local function checkMarkStyle1(button)
if not button.foreverStyle2 then
return
end
button.foreverStyle2 = nil
if button.foreverCircle then
button.foreverCircle:Hide()
end
local checkMark = _G[button:GetName() .. "Check"]
if checkMark and button.foreverCell then
ForeverUI.SetAtlas(checkMark, CHECKMARK_ATLAS, true)
checkMark:SetWidth(CHECKMARK_W)
checkMark:SetHeight(CHECKMARK_H)
checkMark:ClearAllPoints()
checkMark:SetPoint("CENTER", button.foreverCell, "CENTER", CHECKMARK_X, CHECKMARK_Y)
end
end
-- A style 2 row: the circle under the client's checkmark (which becomes the yellow dot),
-- text on its right, the style 1 box hidden
local function style2Row(list, button, info)
local Tpl = ForeverUI.Templates
local m = STYLE2.margins
local i = button:GetID()
if not button.foreverCircle then
button.foreverCircle = button:CreateTexture(nil, "BORDER")
Tpl.Place(button.foreverCircle, STYLE2.circle, true)
button.foreverCircle:SetPoint("LEFT", button, "LEFT", -3, 0)
end
button.foreverStyle2 = true
if button.foreverCell then
button.foreverCell:Hide()
end
local checkMark = _G[button:GetName() .. "Check"]
if checkMark then
Tpl.Place(checkMark, STYLE2.point, true)
checkMark:ClearAllPoints()
checkMark:SetPoint("TOPLEFT", button.foreverCircle, "TOPLEFT")
end
button:SetHeight(ROW_HEIGHT)
button:ClearAllPoints()
button:SetPoint("TOPLEFT", list, "TOPLEFT", m[1], -(m[2] + (i - 1) * ROW_HEIGHT))
button:SetNormalFontObject(GameFontHighlightSmallLeft)
button:SetHighlightFontObject(GameFontHighlightSmallLeft)
if button.SetDisabledFontObject then
if info and info.isTitle then
button:SetDisabledFontObject(GameFontNormalSmallLeft)
elseif info and info.notClickable then
button:SetDisabledFontObject(GameFontHighlightSmallLeft)
else
button:SetDisabledFontObject(GameFontDisableSmallLeft)
end
end
local raw = button:GetText()
local clean = Tpl.Utf8Text(raw)
if clean ~= raw then
button:SetText(clean)
end
local text = _G[button:GetName() .. "NormalText"]
local checkable = not (info and info.notCheckable)
if checkable then
button.foreverCircle:Show()
else
button.foreverCircle:Hide()
end
if text then
text:ClearAllPoints()
if checkable then
text:SetPoint("LEFT", button.foreverCircle, "RIGHT", 1, 0)
elseif info and info.justifyH == "CENTER" then
text:SetPoint("CENTER", button, "CENTER", 0, 0)
else
text:SetPoint("LEFT", button, "LEFT", 0, 0)
end
-- Row extent (MeasureFrameExtents + padding of 20)
local e = (checkable and STYLE2.span or STYLE2.noRadio) + (text:GetStringWidth() or 0)
if i == 1 or e > (list.foreverContent2 or 0) then
list.foreverContent2 = e
end
end
list:SetHeight(m[2] + (list.numButtons or i) * ROW_HEIGHT + m[4])
end
-- The two backgrounds of a list: style 1 (slices) or style 2, by the menu it serves
local function applyListBackgrounds(list, isStyle2)
list.foreverStyle2 = isStyle2 and true or nil
if isStyle2 and not list.foreverBackground2 then
local Tpl = ForeverUI.Templates
local f = Tpl.StretchedAtlas(list, STYLE2.background, "BACKGROUND")
f.rect:SetPoint("TOPLEFT", list, "TOPLEFT", STYLE2.backgroundA[1], STYLE2.backgroundA[2])
f.rect:SetPoint("BOTTOMRIGHT", list, "BOTTOMRIGHT", STYLE2.backgroundB[1], STYLE2.backgroundB[2])
list.foreverBackground2 = f
end
if list.foreverBackground2 then
list.foreverBackground2:SetShown(isStyle2)
end
for _, slice in ipairs(list.foreverSlices or {}) do
if isStyle2 then slice:Hide() else slice:Show() end
end
end
-- Redone for every row added: UIDropDownMenu_AddButton resets the font to
-- GameFontHighlightSmallLeft each time, and sets whether the row has a checkbox
-- (info.notCheckable).
local function adjustButton(button)
checkMarkStyle1(button)
button:SetHeight(ROW_HEIGHT)
if GameFontHighlightLeft then
button:SetNormalFontObject(GameFontHighlightLeft)
button:SetHighlightFontObject(GameFontHighlightLeft)
end
if button.foreverCell then
if button.notCheckable then
button.foreverCell:Hide()
else
button.foreverCell:Show()
end
end
end
-- Row spacing follows row height. The client reads UIDROPDOWNMENU_BUTTON_HEIGHT for each
-- row, the list height and the dropdown height (UIDropDownMenu_InitializeHelper), but
-- writing that global from the addon would taint every client menu, including the
-- protected actions of a player's right-click menu. So the client lays rows at 16 and
-- we move them to 20 right after it.
local BORDER = UIDROPDOWNMENU_BORDER_HEIGHT or 15
local function relayoutRow(list, button)
local point, relativeTo, relativePoint, x = button:GetPoint(1)
if point then
button:ClearAllPoints()
button:SetPoint(point, relativeTo, relativePoint, x,
-((button:GetID() - 1) * ROW_HEIGHT) - BORDER)
end
list:SetHeight(((list.numButtons or 1) * ROW_HEIGHT) + (BORDER * 2))
end
if hooksecurefunc and type(UIDropDownMenu_InitializeHelper) == "function" then
hooksecurefunc("UIDropDownMenu_InitializeHelper", function(frame)
if frame and frame.SetHeight then
frame:SetHeight(ROW_HEIGHT * 2)
end
end)
end
if hooksecurefunc and type(UIDropDownMenu_AddButton) == "function" then
hooksecurefunc("UIDropDownMenu_AddButton", function(info, level)
level = level or 1
local list = _G["DropDownList" .. level]
if not list then
return
end
list.foreverLevel = level
skinList(list)
local isStyle2 = style2(currentMenu())
applyListBackgrounds(list, isStyle2)
local button = _G[list:GetName() .. "Button" .. (list.numButtons or 1)]
if button then
skinButton(button)
if isStyle2 then
style2Row(list, button, info)
else
relayoutRow(list, button)
adjustButton(button)
measureButton(list, button, info)
end
end
end)
end
-- Style 2: after ToggleDropDownMenu, put the list under camelot's button (even when the
-- client anchors it elsewhere) at its width, and repaint the button (arrow, open state)
-- with foreverPaint, set by Settings.lua
if hooksecurefunc and type(ToggleDropDownMenu) == "function" then
hooksecurefunc("ToggleDropDownMenu", function(level)
level = level or 1
local list = _G["DropDownList" .. level]
local opener = UIDROPDOWNMENU_OPEN_MENU
if not (list and style2(opener)) then
return
end
if list:IsShown() then
fitWidth(list)
if level == 1 and opener.foreverButton and opener.displayMode ~= "MENU" then
list:ClearAllPoints()
list:SetPoint("TOPLEFT", opener.foreverButton, "BOTTOMLEFT", 0, 0)
end
end
if opener.foreverPaint then
opener.foreverPaint()
end
end)
end
-- The button arrow goes back to rest when the list closes
for level = 1, (UIDROPDOWNMENU_MAXLEVELS or 2) do
local list = _G["DropDownList" .. level]
if list and list.HookScript then
list:HookScript("OnHide", function()
local opener = UIDROPDOWNMENU_OPEN_MENU
if style2(opener) and opener.foreverPaint then
opener.foreverPaint()
end
end)
end
end
-- UIDropDownMenu_Refresh resizes the list on its text (maxWidth + 25), erasing the wanted
-- width: set it again after it.
if hooksecurefunc and type(UIDropDownMenu_Refresh) == "function" then
hooksecurefunc("UIDropDownMenu_Refresh", function(frame, value, level)
local list = _G["DropDownList" .. (level or UIDROPDOWNMENU_MENU_LEVEL or 1)]
if list and list:IsShown() then
fitWidth(list)
end
end)
end
ForeverUI.DropDown = {
Skin = skinList,
SkinButton = skinButton,
Refresh = adjustButton,
Fit = fitWidth,
}
-- ------------------------------------------------------------ Unit menus
--
-- Our unit frames open the right-click menu through our own function (the "menu" of
-- their secure action), so the client builds the whole menu as addon code and blocks the
-- rows that call a protected function: SET_FOCUS, CLEAR_FOCUS, TARGET and PET_DISMISS
-- (UnitPopup.lua:1201-1384). Out of combat, a secure ForeverUI button covers each of these
-- rows and runs the action as a macro on the player's click. In combat no addon may place
-- or show a secure button, so these rows are grayed (UnitPopup_OnUpdate enables them
-- every frame, so they are disabled again). Only menus opened by our frames (M.open) are
-- touched: the client's own menus already work.
local M = {}
ForeverUI.UnitMenu = M
M.overlays = {}
M.grayedRows = {}
local HIGHLIGHT = "Interface" .. string.char(92) .. "QuestFrame" .. string.char(92) .. "UI-QuestTitleHighlight"
-- Full name, as UnitPopup_OnClick builds it (UnitPopup.lua:1169-1174)
local function fullName(menu)
local name, server = menu.name, menu.server
if name and server and (not menu.unit or not UnitIsSameServer("player", menu.unit)) then
return name .. "-" .. server
end
return name
end
-- Secure action of each protected row
local PROTECTED = {
SET_FOCUS = function(o, menu)
if not menu.unit then return false end
o:SetAttribute("type", "focus")
o:SetAttribute("unit", menu.unit)
end,
CLEAR_FOCUS = function(o)
o:SetAttribute("type", "macro")
o:SetAttribute("macrotext", "/clearfocus")
end,
TARGET = function(o, menu)
local name = fullName(menu)
if not name then return false end
o:SetAttribute("type", "macro")
o:SetAttribute("macrotext", "/targetexact " .. name)
end,
PET_DISMISS = function(o)
o:SetAttribute("type", "macro")
o:SetAttribute("macrotext", "/script PetDismiss()")
end,
}
M.PROTECTED = PROTECTED
-- Secure overlay button k, created on first use
local function overlay(k)
local o = M.overlays[k]
if o then return o end
o = CreateFrame("Button", "ForeverUIUnitMenuSecure" .. k, UIParent, "SecureActionButtonTemplate")
o:RegisterForClicks("LeftButtonUp")
o:SetHighlightTexture(HIGHLIGHT)
local h = o:GetHighlightTexture()
if h then h:SetBlendMode("ADD") end
-- The list closes by itself when the mouse leaves its rows: keep it open over the
-- overlay as over a row
o:SetScript("OnEnter", function() UIDropDownMenu_StopCounting(DropDownList1) end)
o:SetScript("OnLeave", function() UIDropDownMenu_StartCounting(DropDownList1) end)
o:SetScript("PostClick", function() CloseDropDownMenus() end)
o:Hide()
M.overlays[k] = o
return o
end
-- Out of combat only: a secure button cannot be touched under lockdown
function M.hide()
if InCombatLockdown() then return end
for _, o in ipairs(M.overlays) do
o:Hide()
o:ClearAllPoints()
end
end
local function grayOut(row)
row:Disable()
table.insert(M.grayedRows, row)
end
-- After UnitPopup_ShowMenu, on the first list: cover or gray the protected rows.
-- menu: the unit menu frame given to UnitPopup_ShowMenu
function M.place(menu)
M.hide()
table.wipe(M.grayedRows)
local list = DropDownList1
local combat = InCombatLockdown()
local k = 0
for i = 1, (list.numButtons or 0) do
local row = _G["DropDownList1Button" .. i]
local configure = row and PROTECTED[row.value]
if configure then
if combat then
grayOut(row)
else
local o = overlay(k + 1)
if configure(o, menu) ~= false then
k = k + 1
o:ClearAllPoints()
o:SetAllPoints(row)
-- DropDownList1 is toplevel and rises to the top of its strata when shown:
-- the TOOLTIP strata stays above it (as in SocialRaid.lua)
o:SetFrameStrata("TOOLTIP")
o:Show()
end
end
end
end
end
-- Opens a menu from one of our frames. fn: the client function that opens it;
-- ...: its arguments
function M.open(fn, ...)
M.ownCall = true
local ok, err = pcall(fn, ...)
M.ownCall = false
if not ok then error(err, 0) end
end
if hooksecurefunc and type(UnitPopup_ShowMenu) == "function" then
hooksecurefunc("UnitPopup_ShowMenu", function(menu)
if not M.ownCall or not menu or (UIDROPDOWNMENU_MENU_LEVEL or 1) ~= 1 then return end
M.place(menu)
end)
end
if hooksecurefunc and type(UnitPopup_OnUpdate) == "function" then
hooksecurefunc("UnitPopup_OnUpdate", function()
if #M.grayedRows == 0 or not DropDownList1:IsShown() then return end
for _, row in ipairs(M.grayedRows) do
if row:IsEnabled() == 1 then row:Disable() end
end
end)
end
if DropDownList1 then
DropDownList1:HookScript("OnHide", function()
M.hide()
table.wipe(M.grayedRows)
end)
end
-- On entering combat, before the lockdown: overlays go away and their rows are grayed
local watcher = CreateFrame("Frame")
watcher:RegisterEvent("PLAYER_REGEN_DISABLED")
watcher:SetScript("OnEvent", function()
local placedCount = false
for _, o in ipairs(M.overlays) do
if o:IsShown() then placedCount = true end
o:Hide()
o:ClearAllPoints()
end
if placedCount and DropDownList1:IsShown() then
for i = 1, (DropDownList1.numButtons or 0) do
local row = _G["DropDownList1Button" .. i]
if row and PROTECTED[row.value] then grayOut(row) end
end
end
end)
M.watcher = watcher