Files
benilla-octo/crates/benilla-app/assets/ui/UIDropDownMenu.xml
T
SamandClaude Fable 5 2c1ffca2ca dialogs come to the front, and the red line names what it needs
- dialogs carry the reference's toplevel bit, so the stack-split window opens above Bagnon instead of behind it
- the failure line fills its blank — "Requires Blacksmith Hammer", not "Requires" — and Opening casts stop printing a label the spell itself marks "no text"
- the pet bar's Attack/Follow/Stay tooltips name their keybinding, and typed text reaches the focused frame through the client's own delivery walk
- AtlasLoot parses again: our Lua keeps 5.0's compatibility semicolon

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 19:38:18 +07:00

1144 lines
59 KiB
XML

<!--
The dropdown kit — our own implementation of the 1.12 dropdown widget set. The reference
(FrameXML UIDropDownMenuTemplates.xml + UIDropDownMenu.lua) is the SPECIFICATION it answers to,
not the source of this text: the global names, the `info` table's field names, every geometry
constant and every observable behaviour below are the reference's and are load-bearing (addons
call, replace and read them by name); the code that expresses them is ours, and is written the
way we would write it — shared helpers, field lists and lookup tables where the reference
repeats itself. Decision 0203 phase 2's closing sub-slice, widened for decision 0434 phase 5:
the unit popups are the first MENU-mode + submenu customer; the world map's continent/zone
pickers and the trainer/tradeskill filters stay on the level-1 capsule path.
IN: UIDropDownMenuTemplate (the capsule: LabelFrame art trio, $parentText, the scroll-down
toggle button), UIDropDownMenuButtonTemplate (row: highlight, check, right-side icon, expand
arrow, the disabled-row InvisibleButton), UIDropDownListTemplate (Backdrop + MenuBackdrop
swap, 32 rows) instanced as DropDownList1/2, and the reference API: Initialize (displayMode +
level), AddButton(info, level) with isTitle/disabled/notClickable/notCheckable/hasArrow/
icon+texcoords/textR-G-B/justifyH/keepShownOnClick, Refresh, SetWidth/SetText/GetText,
SetSelectedID/Name/Value + getters, ClearAll, ToggleDropDownMenu(level, value, frame,
anchorName ["cursor" included], x, y) with the off-screen flip, HideDropDownMenu,
CloseDropDownMenus(level), Disable/EnableButton, SetButtonText, SetAnchor, JustifyText,
GetCurrentDropDown, the 2-second hover-away auto-close counting through the level chain.
THE COLOR SWATCH ROW came in with the colour picker (ColorPickerFrame.xml): the full
`hasColorSwatch` / `r` / `g` / `b` / `swatchFunc` / `hasOpacity` / `opacity` / `opacityFunc` /
`cancelFunc` field set, the `$parentColorSwatch` button on the row template, and
`UIDropDownMenuButton_OpenColorPicker` (spec: UIDropDownMenu.lua l.801-815). It is the second
half of one feature, not a separate one: `hasColorSwatch` draws the square, clicking the square
is the ONLY way the reference's colour picker ever opens, and `hasColorSwatch` is **145 files /
82 corpus addons**.
OUT (each waits for its first customer): menu level 3 (UIDROPDOWNMENU_MAXLEVELS is 2 — the
unit popups' deepest law; the XML gains DropDownList3 the day a customer needs it),
info.textHeight's per-row SetFont, the newbie
tooltips on rows (the ref gates them behind a default-off cvar), message() on the row/level
overflow guards (no dev-error dialog surface; the guard returns silently). The uiScale
correction came IN with its first scaled customer (0992, the options window's dropdown
rows): Toggle wears the owner's effective scale on the list.
Deviations, deliberate:
- THE WIDTH SETTLE. The ref sizes each row at AddButton time from normalText:GetWidth() and
applies the list max in OnShow — but our text measures land at extract, AFTER the script
tick that set the text (ui_script/extract's measure round-trip), so an AddButton-time
read is stale. The ref's width FORMULA (text + 60, arrow +10, icon +10, notCheckable -30,
list = max + 15) runs in the list's OnUpdate instead and applies on the first update
whose measures are real — one frame after open, imperceptible. A capsule that declared a
width (UIDropDownMenu_SetWidth → dropWidth/noResize: the world map pickers) keeps the
declared width applied synchronously at Toggle, the proven pre-0434 path (rows dropWidth,
list dropWidth + 32).
- THE FLIP SETTLES TOO: the off-screen check needs the list's resolved rect, which also
lands a frame late — it runs from the same OnUpdate chain, after the width applies.
- The default (no anchorName) seat anchors the list to the dropdown FRAME at (8, 7) —
pixel-identical to the reference's "$parentLeft BOTTOMLEFT (8, 22)" through the Left
texture's own (0, 17) seat.
- MENU displayMode hides the capsule art trio when present but skips the ref's toggle-button
texture blanking + re-anchor (every MENU customer here is a born-hidden 10x10 frame whose
capsule never draws).
- (Was: "the disabled-row InvisibleButton spans to the row's RIGHT — the ref stops it at the
color swatch, which is OUT". The swatch is IN now, so the ref's own anchor is restored:
`RIGHT relativeTo="$parentColorSwatch" relativePoint="LEFT"`.)
- The list OnShow resets showTimer AND isCounting (the ref writes `showTime`, a field
nothing reads — we implement the intent, not the typo).
-->
<Ui xmlns="http://www.blizzard.com/wow/ui/">
<Script><![CDATA[
UIDROPDOWNMENU_MAXBUTTONS = 32;
UIDROPDOWNMENU_MAXLEVELS = 2;
UIDROPDOWNMENU_BUTTON_HEIGHT = 16;
UIDROPDOWNMENU_BORDER_HEIGHT = 15;
UIDROPDOWNMENU_OPEN_MENU = nil;
UIDROPDOWNMENU_INIT_MENU = nil;
UIDROPDOWNMENU_MENU_LEVEL = 1;
UIDROPDOWNMENU_MENU_VALUE = nil;
UIDROPDOWNMENU_SHOW_TIME = 2;
-- Which menus are open, keyed by level. UnitPopup writes it and its OnUpdate enable/disable
-- pass walks it; each list's own OnHide clears its entry.
OPEN_DROPDOWNMENUS = {};
-- ── The kit's vocabulary ──────────────────────────────────────────────────────────────
-- Four things this file says over and over, said once here.
-- Every named child in the kit is `<owner's name><suffix>`: a row's Check/NormalText/Icon/
-- ExpandArrow/ColorSwatch/InvisibleButton, a capsule's Left/Middle/Right/Text/Button, the
-- swatch's NormalTexture. (The reference spells this two different ways for the same
-- object — off the list name plus the row index, and off the row itself.)
local function menuChild(frame, suffix)
return getglobal(frame:GetName()..suffix);
end
-- A row addressed the way the by-address API addresses it: level + index.
local function listRow(level, index)
return getglobal("DropDownList"..level.."Button"..index);
end
-- The `info` fields a row simply carries. AddButton copies them onto the row so the row's
-- own handlers — its click, the width settle, the colour square — can read them back later.
-- These names are the published API: the list is the documentation.
local ROW_FIELDS = {
"func", "owner", "arg1", "arg2", "keepShownOnClick", "tooltipTitle", "tooltipText",
"hasArrow", "notCheckable", "icon",
"hasColorSwatch", "hasOpacity", "opacity", "opacityFunc", "swatchFunc", "cancelFunc",
};
-- A capsule remembers its selection under exactly one of these; adopting one drops the
-- other two, which is why they can never disagree.
local SELECTION_KEYS = { "selectedName", "selectedID", "selectedValue" };
-- The capsule's three-piece LabelFrame art.
local CAPSULE_ART = { "Left", "Middle", "Right" };
-- Reset the levels this dropdown is about to rebuild, remember + run its init function,
-- then apply displayMode. A DIFFERENT dropdown taking the menu over resets the whole stack
-- from level 1; the same one keeps whatever sits above the level it is rebuilding.
function UIDropDownMenu_Initialize(frame, initFunction, displayMode, level)
frame = frame or this;
local name = frame:GetName();
local takeover = (name ~= UIDROPDOWNMENU_OPEN_MENU);
if ( takeover ) then
UIDROPDOWNMENU_MENU_LEVEL = 1;
end
UIDROPDOWNMENU_INIT_MENU = name;
for i = 1, UIDROPDOWNMENU_MAXLEVELS, 1 do
if ( takeover or i >= UIDROPDOWNMENU_MENU_LEVEL ) then
local list = getglobal("DropDownList"..i);
list.numButtons = 0;
for j = 1, UIDROPDOWNMENU_MAXBUTTONS, 1 do
listRow(i, j):Hide();
end
end
end
frame:SetHeight(UIDROPDOWNMENU_BUTTON_HEIGHT * 2);
if ( initFunction ) then
frame.initialize = initFunction;
initFunction(level);
end
if ( displayMode == "MENU" ) then
-- Drop the capsule art, when the frame has any (see the header deviation).
if ( getglobal(name.."Left") ) then
for _, part in ipairs(CAPSULE_ART) do
getglobal(name..part):Hide();
end
end
frame.displayMode = "MENU";
end
end
-- The reference's width law, run from the list's OnUpdate (the header's WIDTH SETTLE): rows
-- measure text + 60, +10 for an arrow OR a colour swatch, +10 for an icon, -30 when
-- notCheckable; the list takes max + 15. Waits until every text row has a real (nonzero)
-- measure. (The ref's `if ( info.hasArrow or info.hasColorSwatch )` is ONE +10, not two —
-- the two ride the same right-hand slot, and a row never has both.)
function UIDropDownMenu_SettleWidth(listFrame)
local listFrameName = listFrame:GetName();
if ( listFrame.fixedWidth ) then
listFrame.pendingWidth = nil;
return;
end
local maxWidth = 0;
local want, seen = 0, 0;
for i=1, listFrame.numButtons, 1 do
local button = getglobal(listFrameName.."Button"..i);
local text = button:GetText();
if ( text and text ~= "" ) then
want = want + 1;
local width = menuChild(button, "NormalText"):GetWidth();
if ( width > 0 ) then
seen = seen + 1;
width = width + 60;
if ( button.hasArrow or button.hasColorSwatch ) then
width = width + 10;
end
if ( button.notCheckable ) then
width = width - 30;
end
if ( button.icon ) then
width = width + 10;
end
if ( width > maxWidth ) then
maxWidth = width;
end
end
end
end
if ( want > 0 and seen < want ) then
return; -- measures not landed yet; next update
end
if ( maxWidth == 0 ) then
maxWidth = 130;
end
for i=1, UIDROPDOWNMENU_MAXBUTTONS, 1 do
getglobal(listFrameName.."Button"..i):SetWidth(maxWidth);
end
listFrame:SetWidth(maxWidth + 15);
listFrame.pendingWidth = nil;
end
-- Which of the list's corners meets which corner of its seat once the list is known to hang
-- off the bottom and/or the right of the screen. Level 1 re-seats against the anchor Toggle
-- stored and needs no nudge; a submenu flips around the row that opened it, and the nudge
-- is what keeps the flipped list glued to that row.
local FLIP = {
bottomRight = { seat = { "BOTTOMRIGHT", "BOTTOMLEFT" },
row = { "BOTTOMRIGHT", "BOTTOMLEFT", -11, -14 } },
bottom = { seat = { "BOTTOMLEFT", "TOPLEFT" },
row = { "BOTTOMLEFT", "BOTTOMRIGHT", 0, -14 } },
right = { seat = { "TOPRIGHT", "TOPLEFT" },
row = { "TOPRIGHT", "TOPLEFT", -11, 14 } },
};
-- The off-screen flip, from the same OnUpdate chain as the width (it needs the resolved
-- rect — the header's second settle).
function UIDropDownMenu_SettleFlip(listFrame)
local x, y = listFrame:GetCenter();
if ( not x or not y ) then
return; -- not resolved yet; next update
end
listFrame.pendingFlip = nil;
local rightEdge = listFrame:GetRight();
local offBottom = (y - listFrame:GetHeight()/2) < 0;
local offRight = (rightEdge ~= nil) and (rightEdge > GetScreenWidth());
local flip;
if ( offBottom and offRight ) then
flip = FLIP.bottomRight;
elseif ( offBottom ) then
flip = FLIP.bottom;
elseif ( offRight ) then
flip = FLIP.right;
else
return; -- it fits; the seat Toggle gave it stands
end
listFrame:ClearAllPoints();
if ( listFrame:GetID() == 1 ) then
local corner = flip.seat;
if ( listFrame.cursorMode ) then
-- Cursor menus keep their screen-space seat; only the corner changes.
listFrame:SetPoint(corner[1], "UIParent", "BOTTOMLEFT", listFrame.seatX, listFrame.seatY);
else
listFrame:SetPoint(corner[1], listFrame.seatTo, corner[2], listFrame.seatX, listFrame.seatY);
end
else
local corner = flip.row;
listFrame:SetPoint(corner[1], listFrame.anchorFrame, corner[2], corner[3], corner[4]);
end
end
-- The list's per-frame work: settle the width, then the flip (both wait on measures that
-- land a frame after the open — the header), then run the hover-away countdown.
function UIDropDownMenu_OnUpdate(elapsed)
if ( this.pendingWidth ) then
UIDropDownMenu_SettleWidth(this);
elseif ( this.pendingFlip ) then
UIDropDownMenu_SettleFlip(this);
end
-- The countdown runs only on a visible list that a StartCounting has armed (never on
-- open); reaching zero is what closes a menu the mouse walked away from.
if ( not this:IsVisible() or not this.isCounting or not this.showTimer ) then
return;
end
if ( this.showTimer < 0 ) then
this.showTimer = nil;
this.isCounting = nil;
this:Hide();
else
this.showTimer = this.showTimer - elapsed;
end
end
-- A submenu's hover state drives its ROOT list's timer, so the whole open stack lives or
-- dies together: both ends of the countdown climb `parent` to the list that owns the stack.
local function timerRoot(frame)
while ( frame.parent ) do
frame = frame.parent;
end
return frame;
end
function UIDropDownMenu_StartCounting(frame)
local root = timerRoot(frame);
root.showTimer = UIDROPDOWNMENU_SHOW_TIME;
root.isCounting = 1;
end
function UIDropDownMenu_StopCounting(frame)
local root = timerRoot(frame);
root.isCounting = nil;
end
-- The kit's one selection test, answering in the reference's own `[nil, 1]` currency. Name
-- beats id beats value, and only the first key the capsule actually carries is consulted —
-- note that GetSelectedID answers for a value-selected capsule too (it scans the open level
-- for the row), so the value arm below is reached only when that scan found nothing.
local function rowChecked(frame, button)
local name = UIDropDownMenu_GetSelectedName(frame);
if ( name ) then
return button:GetText() == name and 1 or nil;
end
local id = UIDropDownMenu_GetSelectedID(frame);
if ( id ) then
return button:GetID() == id and 1 or nil;
end
local value = UIDropDownMenu_GetSelectedValue(frame);
if ( value ) then
return button.value == value and 1 or nil;
end
end
-- Paint one row's checked state. The tick and the locked highlight always move together,
-- and the row remembers what it was told (its click handler reads it back).
local function paintChecked(button, checked)
if ( checked ) then
button:LockHighlight();
menuChild(button, "Check"):Show();
else
button:UnlockHighlight();
menuChild(button, "Check"):Hide();
end
button.checked = checked;
end
-- Build one menu row from an `info` table — `.text`/`.value`/`.func`/`.checked`/`.isTitle`/
-- `.disabled`/`.notClickable`/`.notCheckable`/`.hasArrow`/`.icon` + `.tCoord*`/`.textR-G-B`/
-- `.justifyH`/`.keepShownOnClick`/`.owner`/`.arg1`/`.arg2`/the tooltip pair/the colour-swatch
-- set. That table is the published API and its field names are the contract. Width rides the
-- settle (header), not an AddButton-time measure.
function UIDropDownMenu_AddButton(info, level)
level = level or 1;
local listFrame = getglobal("DropDownList"..level);
local listFrameName = listFrame:GetName();
local index = listFrame.numButtons + 1;
if ( index > UIDROPDOWNMENU_MAXBUTTONS or level > UIDROPDOWNMENU_MAXLEVELS ) then
return; -- the ref raises a dev-error dialog here (OUT, header)
end
listFrame.numButtons = index;
local button = listRow(level, index);
local normalText = menuChild(button, "NormalText");
local icon = menuChild(button, "Icon");
-- Captures OnEnter/OnLeave over a disabled row so the menu doesn't time out.
local invisibleButton = menuChild(button, "InvisibleButton");
invisibleButton:Hide();
button:Enable();
-- The label, and its colour: a row carrying its own textR/G/B wears it in both the
-- normal and the highlight state, so the label doesn't change colour under the mouse.
if ( info.text ) then
button:SetText(info.text);
local r, g, b = info.textR, info.textG, info.textB;
if ( not r ) then
r, g, b = HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b;
end
button:SetTextColor(r, g, b);
button:SetHighlightTextColor(r, g, b);
else
button:SetText("");
end
-- The right-hand icon rides on the label: a row with no text has no icon either.
if ( info.text and info.icon ) then
icon:SetTexture(info.icon);
if ( info.tCoordLeft ) then
icon:SetTexCoord(info.tCoordLeft, info.tCoordRight, info.tCoordTop, info.tCoordBottom);
end
icon:Show();
else
icon:Hide();
end
-- Everything the row simply carries over, then the value, which falls back to the label
-- so a menu built from plain text rows still has values to dispatch on.
for _, field in ipairs(ROW_FIELDS) do
button[field] = info[field];
end
button.value = info.value or info.text;
local expandArrow = menuChild(button, "ExpandArrow");
if ( info.hasArrow ) then
expandArrow:Show();
else
expandArrow:Hide();
end
-- The row's left inset, and its label's seat inside it. A notCheckable row (titles,
-- plain actions) pulls the label to the row's left edge and sits 10px in; a checkable
-- row leaves the 27px check column and sits 12px in — 6px tighter under MENU mode,
-- whose frames have no capsule for the rows to line up beneath.
local xPos = 5;
local yPos = -((button:GetID() - 1) * UIDROPDOWNMENU_BUTTON_HEIGHT) - UIDROPDOWNMENU_BORDER_HEIGHT;
normalText:ClearAllPoints();
if ( not info.notCheckable ) then
xPos = xPos + 12;
normalText:SetPoint("LEFT", button:GetName(), "LEFT", 27, 0);
elseif ( info.justifyH == "CENTER" ) then
xPos = xPos + 10;
normalText:SetPoint("CENTER", button:GetName(), "CENTER", -7, 0);
else
xPos = xPos + 10;
normalText:SetPoint("LEFT", button:GetName(), "LEFT", 0, 0);
end
-- Whose menu this is: the open capsule, or — nothing open yet — the one initializing.
local frame = UIDROPDOWNMENU_OPEN_MENU and getglobal(UIDROPDOWNMENU_OPEN_MENU);
if ( frame and frame.displayMode == "MENU" and not info.notCheckable ) then
xPos = xPos - 6;
end
frame = frame or getglobal(UIDROPDOWNMENU_INIT_MENU);
button:SetPoint("TOPLEFT", listFrameName, "TOPLEFT", xPos, yPos);
-- A row the owning capsule points at comes up checked. `info.checked` is only ever set
-- here, never cleared: a caller that declared the row checked keeps it.
if ( frame and rowChecked(frame, button) ) then
info.checked = 1;
end
paintChecked(button, info.checked);
-- The colour square: its NormalTexture IS the swatch art, tinted to the row's colour,
-- and the row keeps r/g/b so the square's own OnClick can seed the picker from them.
local colorSwatch = menuChild(button, "ColorSwatch");
if ( info.hasColorSwatch ) then
button.r, button.g, button.b = info.r, info.g, info.b;
menuChild(colorSwatch, "NormalTexture"):SetVertexColor(button.r, button.g, button.b);
colorSwatch:Show();
else
colorSwatch:Hide();
end
-- Three ways a row lands disabled, each choosing the colour its label wears while it is:
-- a title is gold, a notClickable row is white, a plainly disabled one keeps the gray
-- default. A title wins, which is the reference's own order (it applies isTitle second).
local disabledColor = GRAY_FONT_COLOR;
if ( info.notClickable ) then
info.disabled = 1;
disabledColor = HIGHLIGHT_FONT_COLOR;
end
if ( info.isTitle ) then
info.disabled = 1;
disabledColor = NORMAL_FONT_COLOR;
end
button:SetDisabledTextColor(disabledColor.r, disabledColor.g, disabledColor.b);
if ( info.disabled ) then
button:Disable();
invisibleButton:Show();
end
listFrame:SetHeight((index * UIDROPDOWNMENU_BUTTON_HEIGHT) + (UIDROPDOWNMENU_BORDER_HEIGHT * 2));
button:Show();
end
-- Re-derive every row's check + lock state on one level, and push the selected row's label
-- into the capsule's own text. Width stays the settle's job (header).
function UIDropDownMenu_Refresh(frame, useValue, dropdownLevel)
frame = frame or this;
dropdownLevel = dropdownLevel or UIDROPDOWNMENU_MENU_LEVEL;
for i = 1, UIDROPDOWNMENU_MAXBUTTONS, 1 do
local button = listRow(dropdownLevel, i);
local checked = rowChecked(frame, button);
if ( checked ) then
if ( useValue ) then
UIDropDownMenu_SetText(button.value, frame);
else
UIDropDownMenu_SetText(button:GetText(), frame);
end
end
paintChecked(button, checked);
end
end
-- The three setters are one operation: adopt one selection key, drop the other two, repaint.
local function selectBy(frame, key, selection, useValue)
for _, field in ipairs(SELECTION_KEYS) do
frame[field] = nil;
end
frame[key] = selection;
UIDropDownMenu_Refresh(frame, useValue);
end
function UIDropDownMenu_SetSelectedName(frame, name, useValue)
selectBy(frame, "selectedName", name, useValue);
end
function UIDropDownMenu_SetSelectedValue(frame, value, useValue)
selectBy(frame, "selectedValue", value, useValue);
end
function UIDropDownMenu_SetSelectedID(frame, id, useValue)
selectBy(frame, "selectedID", id, useValue);
end
function UIDropDownMenu_GetSelectedName(frame)
return frame.selectedName;
end
function UIDropDownMenu_GetSelectedValue(frame)
return frame.selectedValue;
end
-- With no explicit id, answer with the index of the row the selected name — failing that,
-- the selected value — points at on the open level.
function UIDropDownMenu_GetSelectedID(frame)
if ( frame.selectedID ) then
return frame.selectedID;
end
local name = UIDropDownMenu_GetSelectedName(frame);
local value = UIDropDownMenu_GetSelectedValue(frame);
if ( not name and not value ) then
return nil;
end
for i = 1, UIDROPDOWNMENU_MAXBUTTONS, 1 do
local button = listRow(UIDROPDOWNMENU_MENU_LEVEL, i);
if ( name ) then
if ( button:GetText() == name ) then
return i;
end
elseif ( button.value == value ) then
return i;
end
end
end
-- A row's click: run its func with `this` still on the row, then either flip the check in
-- place (keepShownOnClick) or shut the list the row belongs to. A row with no func does
-- nothing at all — not even the sound.
function UIDropDownMenuButton_OnClick()
if ( not this.func ) then
return;
end
this.func(this.arg1, this.arg2);
if ( not this.keepShownOnClick ) then
this:GetParent():Hide();
elseif ( this.checked ) then
this.checked = nil;
menuChild(this, "Check"):Hide();
else
this.checked = 1;
menuChild(this, "Check"):Show();
end
PlaySound("UChatScrollButton");
end
function HideDropDownMenu(level)
getglobal("DropDownList"..(level or 1)):Hide();
end
-- Open (or re-open) one level of the shared menu. The capsule that is already showing this
-- level closes it; anything else rebuilds through the stored initialize, seats the list (the
-- capsule seat, a named anchor, or the cursor), dresses it for its display mode and shows
-- it. Width and the off-screen flip settle from OnUpdate (header).
function ToggleDropDownMenu(level, value, dropDownFrame, anchorName, xOffset, yOffset)
level = level or 1;
UIDROPDOWNMENU_MENU_LEVEL = level;
UIDROPDOWNMENU_MENU_VALUE = value;
local listFrameName = "DropDownList"..level;
local listFrame = getglobal(listFrameName);
-- Called straight off a capsule's scroll button, `this` is that button and the capsule
-- it serves is its parent.
local owner = dropDownFrame or this:GetParent();
if ( listFrame:IsVisible() and UIDROPDOWNMENU_OPEN_MENU == owner:GetName() ) then
listFrame:Hide();
return;
end
listFrame:Hide();
-- The uiScale correction, arriving with its first scaled customer (0992: the options
-- window rides SetScale, and a list at scale 1 would draw its rows ~28% larger than the
-- capsule they serve): the shared list wears the OWNER's effective scale for this open.
-- Every pre-0992 customer sits at effective scale 1 — no-op.
local listScale = owner:GetEffectiveScale();
listFrame:SetScale(listScale);
if ( level == 1 ) then
dropDownFrame = owner;
UIDROPDOWNMENU_OPEN_MENU = dropDownFrame:GetName();
listFrame:ClearAllPoints();
listFrame.cursorMode = nil;
local point, relativeTo, relativePoint;
if ( not anchorName ) then
-- The capsule seat (header deviation), overridable via SetAnchor.
point = dropDownFrame.point or "TOPLEFT";
relativeTo = dropDownFrame.relativeTo or dropDownFrame:GetName();
relativePoint = dropDownFrame.relativePoint or "BOTTOMLEFT";
xOffset = dropDownFrame.xOffset or 8;
yOffset = dropDownFrame.yOffset or 7;
elseif ( anchorName == "cursor" ) then
-- Cursor coords are scale-1 units, and a scaled list's anchor offsets are in
-- ITS space, so divide (the reference's own cursor/uiScale dance).
local cursorX, cursorY = GetCursorPosition();
point = "TOPLEFT";
relativeTo = "UIParent";
relativePoint = "BOTTOMLEFT";
xOffset = cursorX / listScale + (xOffset or 0);
yOffset = cursorY / listScale + (yOffset or 0);
listFrame.cursorMode = 1;
else
point = "TOPLEFT";
relativeTo = anchorName;
relativePoint = "BOTTOMLEFT";
if ( not xOffset or not yOffset ) then
xOffset = 8;
yOffset = 22;
end
end
listFrame:SetPoint(point, relativeTo, relativePoint, xOffset, yOffset);
-- What the flip re-seats against (SettleFlip).
listFrame.seatTo = relativeTo;
listFrame.seatX = xOffset;
listFrame.seatY = yOffset;
else
dropDownFrame = dropDownFrame or getglobal(UIDROPDOWNMENU_OPEN_MENU);
listFrame:ClearAllPoints();
-- Anchor to the row itself, whether `this` is the row (whose parent is the bare
-- list — name length 13) or the row's ExpandArrow child.
local parentName = this:GetParent():GetName();
local anchorFrame = parentName;
if ( string.sub(parentName, 1, 12) == "DropDownList" and string.len(parentName) == 13 ) then
anchorFrame = this:GetName();
end
listFrame:SetPoint("TOPLEFT", anchorFrame, "TOPRIGHT", 0, 0);
listFrame.anchorFrame = anchorFrame;
end
-- MENU mode swaps the dialog box for the tooltip-style backdrop; exactly one is up.
local wanted, unwanted = "Backdrop", "MenuBackdrop";
if ( dropDownFrame and dropDownFrame.displayMode == "MENU" ) then
wanted, unwanted = "MenuBackdrop", "Backdrop";
end
getglobal(listFrameName..wanted):Show();
getglobal(listFrameName..unwanted):Hide();
UIDropDownMenu_Initialize(dropDownFrame, dropDownFrame.initialize, nil, level);
if ( listFrame.numButtons == 0 ) then
return;
end
-- A capsule that declared a width applies it synchronously (the proven pre-0434 path);
-- auto-sized menus ride the settle.
listFrame.fixedWidth = nil;
if ( dropDownFrame.noResize and dropDownFrame.dropWidth ) then
listFrame.fixedWidth = dropDownFrame.dropWidth;
for i = 1, UIDROPDOWNMENU_MAXBUTTONS, 1 do
listRow(level, i):SetWidth(listFrame.fixedWidth);
end
listFrame:SetWidth(listFrame.fixedWidth + 32);
end
listFrame:Show();
end
function CloseDropDownMenus(level)
for i = (level or 1), UIDROPDOWNMENU_MAXLEVELS, 1 do
getglobal("DropDownList"..i):Hide();
end
end
-- CloseMenus() — in the ref a C function (the ESC ladder's rung between the game-menu
-- checks and SpellStopCasting, UIParent.lua l.1488): close any open menu, truthy iff one
-- was open so the ladder eats the press. Every menu we have is a DropDownList, so the
-- Lua equivalent lives here with them (decision 0454).
function CloseMenus()
local closed
for i=1, UIDROPDOWNMENU_MAXLEVELS, 1 do
local list = getglobal("DropDownList"..i)
if list and list:IsVisible() then
closed = 1
end
end
if closed then
CloseDropDownMenus()
end
return closed
end
-- The capsule's declared width: the Middle art stretches to it, the frame itself carries the
-- two 25px end caps, and the label gets 25px less than the art. `dropWidth` is ours — the
-- fixed-width path in Toggle reuses the number (see the header).
function UIDropDownMenu_SetWidth(width, frame)
frame = frame or this;
menuChild(frame, "Middle"):SetWidth(width);
frame:SetWidth(width + 25 + 25);
menuChild(frame, "Text"):SetWidth(width - 25);
frame.dropWidth = width;
frame.noResize = 1;
end
function UIDropDownMenu_SetButtonWidth(width, frame)
frame = frame or this;
if ( width == "TEXT" ) then
width = menuChild(frame, "Text"):GetWidth();
end
menuChild(frame, "Button"):SetWidth(width);
frame.noResize = 1;
end
function UIDropDownMenu_SetText(text, frame)
frame = frame or this;
menuChild(frame, "Text"):SetText(text);
end
function UIDropDownMenu_GetText(frame)
frame = frame or this;
return menuChild(frame, "Text"):GetText();
end
-- Forget the selection and strip the marks off the open level's rows. Deliberately NOT a
-- Refresh: the reference found refreshing from here a bottleneck, and the rows are about to
-- be rebuilt anyway.
function UIDropDownMenu_ClearAll(frame)
for _, field in ipairs(SELECTION_KEYS) do
frame[field] = nil;
end
UIDropDownMenu_SetText("", frame);
for i = 1, UIDROPDOWNMENU_MAXBUTTONS, 1 do
local button = listRow(UIDROPDOWNMENU_MENU_LEVEL, i);
button:UnlockHighlight();
menuChild(button, "Check"):Hide();
end
end
-- Where the capsule's own label sits: against the left cap, against the right one (clear of
-- the scroll button), or centred on the Middle art. {point, art piece, its point, x, y}.
local JUSTIFY = {
LEFT = { "LEFT", "Left", "LEFT", 27, 2 },
RIGHT = { "RIGHT", "Right", "RIGHT", -43, 2 },
CENTER = { "CENTER", "Middle", "CENTER", -5, 2 },
};
function UIDropDownMenu_JustifyText(justification, frame)
frame = frame or this;
local text = menuChild(frame, "Text");
text:ClearAllPoints();
local seat = JUSTIFY[justification];
if ( seat ) then
text:SetPoint(seat[1], frame:GetName()..seat[2], seat[3], seat[4], seat[5]);
end
end
function UIDropDownMenu_SetAnchor(xOffset, yOffset, dropdown, point, relativeTo, relativePoint)
dropdown = dropdown or this;
dropdown.point, dropdown.relativeTo, dropdown.relativePoint = point, relativeTo, relativePoint;
dropdown.xOffset, dropdown.yOffset = xOffset, yOffset;
end
-- The dropdown a menu handler is acting for: the open capsule, or `this` when nothing is
-- open (a row's own func asking who it belongs to).
function UIDropDownMenu_GetCurrentDropDown()
if ( not UIDROPDOWNMENU_OPEN_MENU ) then
return this;
end
return getglobal(UIDROPDOWNMENU_OPEN_MENU);
end
function UIDropDownMenuButton_GetChecked()
return menuChild(this, "Check"):IsVisible();
end
function UIDropDownMenuButton_GetName()
return menuChild(this, "NormalText"):GetText();
end
-- The picker fields a swatch row seeds under the same name on both sides. `swatchFunc` is
-- the exception, set beside them: it lands on the picker as `func`, which is why it is the
-- LIVE-preview callback and not an on-accept one.
local PICKER_FIELDS = { "hasOpacity", "opacity", "opacityFunc", "cancelFunc" };
-- THE entry point to the colour picker (spec: UIDropDownMenu.lua l.801-815): every colour
-- option in every FrameXML dropdown and every Ace2/Dewdrop menu row lands here. Note what it
-- does that the picker itself does not — `previousValues` is captured HERE, and it is the
-- table `cancelFunc` is handed, so Cancel restoring the old colour is this function's doing.
function UIDropDownMenuButton_OpenColorPicker(button)
CloseMenus();
button = button or this;
UIDROPDOWNMENU_MENU_VALUE = button.value;
ColorPickerFrame.func = button.swatchFunc;
for _, field in ipairs(PICKER_FIELDS) do
ColorPickerFrame[field] = button[field];
end
ColorPickerFrame:SetColorRGB(button.r, button.g, button.b);
ColorPickerFrame.previousValues = {r = button.r, g = button.g, b = button.b, opacity = button.opacity};
ShowUIPanel(ColorPickerFrame);
end
-- The by-address trio: an addon that knows a row's level and index, rather than holding the
-- row itself, reaches it through these.
function UIDropDownMenu_DisableButton(level, id)
listRow(level, id):Disable();
end
function UIDropDownMenu_EnableButton(level, id)
listRow(level, id):Enable();
end
function UIDropDownMenu_SetButtonText(level, id, text, r, g, b)
local button = listRow(level, id);
button:SetText(text);
if ( r ) then
button:SetTextColor(r, g, b);
button:SetHighlightTextColor(r, g, b);
end
end
-- ── What the row template's handlers do ───────────────────────────────────────────────
-- Everything that sits on a menu row — the row itself, its colour square, its expand arrow,
-- the invisible cover over a disabled row — means the same two things to the menu when the
-- mouse arrives and when it leaves, and both are about the row's LIST rather than the row:
-- the timeout that closes a menu the mouse walked away from lives on the list, and so does
-- the level number that decides which lists count as deeper than this one. Five handlers
-- want exactly that, so it is written here once instead of five times in markup.
function UIDropDownMenuRow_HoverIn(list)
UIDropDownMenu_StopCounting(list);
CloseDropDownMenus(list:GetID() + 1);
end
function UIDropDownMenuRow_HoverOut(list)
UIDropDownMenu_StartCounting(list);
end
-- A `hasArrow` row opens its submenu one level below the list it sits in, carrying the row's
-- value as that submenu's context. Hovering the row, hovering the arrow and clicking the
-- arrow are three ways to ask for the same thing.
function UIDropDownMenuRow_OpenSubmenu(row)
ToggleDropDownMenu(row:GetParent():GetID() + 1, row.value);
end
-- The colour square's hover feedback. The square itself IS the row's colour, so the only
-- thing free to react is the 14x14 backing behind it: gold under the mouse, white off it.
function UIDropDownMenuSwatch_Highlight(swatch, color)
getglobal(swatch:GetName().."SwatchBg"):SetVertexColor(color.r, color.g, color.b);
end
]]></Script>
<!-- One menu row (ref UIDropDownMenuButtonTemplate: highlight, check, right-side icon, the
expand arrow, the disabled-row InvisibleButton; text re-anchored per AddButton). -->
<Button name="UIDropDownMenuButtonTemplate" virtual="true" hidden="true">
<Size><AbsDimension x="128" y="16"/></Size>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentHighlight" file="Interface\QuestFrame\UI-QuestTitleHighlight" alphaMode="ADD" setAllPoints="true" hidden="true"/>
</Layer>
<Layer level="ARTWORK">
<Texture name="$parentCheck" file="Interface\Buttons\UI-CheckBox-Check">
<Size><AbsDimension x="24" y="24"/></Size>
<Anchors><Anchor point="LEFT"/></Anchors>
</Texture>
<Texture name="$parentIcon" hidden="true">
<Size><AbsDimension x="15" y="15"/></Size>
<Anchors><Anchor point="RIGHT"><Offset><AbsDimension x="-5" y="0"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<!-- ref UIDropDownMenuTemplates.xml l.38-83. The colour square: a 16x16 button whose
NormalTexture is the ChatFrameColorSwatch art, tinted per row, over a 14x14
`$parentSwatchBg` that the hover recolours (gold in, white out) — that background
is the row's only hover feedback, since the square itself is the row's colour.
Clicking it is the ONLY thing in the whole reference UI that opens the colour
picker. -->
<Button name="$parentColorSwatch" hidden="true">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="RIGHT"><Offset><AbsDimension x="-6" y="0"/></Offset></Anchor></Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentSwatchBg">
<Size><AbsDimension x="14" y="14"/></Size>
<Anchors><Anchor point="CENTER"/></Anchors>
<Color r="1.0" g="1.0" b="1.0"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<!-- The open closes the menu first — that is `OpenColorPicker`'s own first
line, so it is not repeated here. -->
<OnClick>
UIDropDownMenuButton_OpenColorPicker(this:GetParent());
</OnClick>
<OnEnter>
UIDropDownMenuRow_HoverIn(this:GetParent():GetParent());
UIDropDownMenuSwatch_Highlight(this, NORMAL_FONT_COLOR);
</OnEnter>
<OnLeave>
UIDropDownMenuRow_HoverOut(this:GetParent():GetParent());
UIDropDownMenuSwatch_Highlight(this, HIGHLIGHT_FONT_COLOR);
</OnLeave>
</Scripts>
<NormalTexture name="$parentNormalTexture" file="Interface\ChatFrame\ChatFrameColorSwatch"/>
</Button>
<Button name="$parentExpandArrow" hidden="true">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="RIGHT"><Offset><AbsDimension x="-6" y="0"/></Offset></Anchor></Anchors>
<Scripts>
<OnClick>
UIDropDownMenuRow_OpenSubmenu(this:GetParent());
</OnClick>
<!-- Hovering the arrow opens the submenu, so this is the one hover on the row
that must NOT close the deeper levels — only the timeout stops. -->
<OnEnter>
UIDropDownMenuRow_OpenSubmenu(this:GetParent());
UIDropDownMenu_StopCounting(this:GetParent():GetParent());
</OnEnter>
<OnLeave>
UIDropDownMenuRow_HoverOut(this:GetParent():GetParent());
</OnLeave>
</Scripts>
<NormalTexture file="Interface\ChatFrame\ChatFrameExpandArrow"/>
</Button>
<!-- Disabled rows don't get mouse events; this transparent cover keeps the hover
alive (timeout + deeper-level close). It stops at the colour swatch's LEFT edge
(ref l.111-113) so a disabled row's swatch is still clickable. -->
<Button name="$parentInvisibleButton" hidden="true">
<Anchors>
<Anchor point="TOPLEFT"/>
<Anchor point="BOTTOMLEFT"/>
<Anchor point="RIGHT" relativeTo="$parentColorSwatch" relativePoint="LEFT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor>
</Anchors>
<Scripts>
<OnEnter>
UIDropDownMenuRow_HoverIn(this:GetParent():GetParent());
</OnEnter>
<OnLeave>
UIDropDownMenuRow_HoverOut(this:GetParent():GetParent());
</OnLeave>
</Scripts>
</Button>
</Frames>
<Scripts>
<OnClick>
UIDropDownMenuButton_OnClick();
</OnClick>
<!-- A row that has a submenu opens it on hover; a row that has not closes whatever is
deeper. Either way the list's timeout stops and the row lights up. -->
<OnEnter>
if ( this.hasArrow ) then
UIDropDownMenuRow_OpenSubmenu(this);
UIDropDownMenu_StopCounting(this:GetParent());
else
UIDropDownMenuRow_HoverIn(this:GetParent());
end
getglobal(this:GetName().."Highlight"):Show();
</OnEnter>
<OnLeave>
getglobal(this:GetName().."Highlight"):Hide();
UIDropDownMenuRow_HoverOut(this:GetParent());
</OnLeave>
</Scripts>
<ButtonText name="$parentNormalText">
<Anchors><Anchor point="LEFT"><Offset><AbsDimension x="27" y="0"/></Offset></Anchor></Anchors>
</ButtonText>
<NormalFont inherits="GameFontHighlightSmall" justifyH="LEFT"/>
<HighlightFont inherits="GameFontHighlightSmall" justifyH="LEFT"/>
<DisabledFont inherits="GameFontDisableSmall" justifyH="LEFT"/>
</Button>
<!-- The list (ref UIDropDownListTemplate): the two swappable backdrops + 32 rows.
FULLSCREEN_DIALOG, not the ref's DIALOG: the list must sort above ANY opener, and the
world map is FULLSCREEN strata — DIALOG would render the list underneath it (the strata
escalation later reference versions added to ToggleDropDownMenu, applied statically
here since our customer set is known). -->
<Button name="UIDropDownListTemplate" frameStrata="FULLSCREEN_DIALOG" virtual="true" hidden="true">
<Size><AbsDimension x="180" y="32"/></Size>
<Frames>
<Frame name="$parentBackdrop" setAllPoints="true">
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background" edgeFile="Interface\DialogFrame\UI-DialogBox-Border" tile="true">
<BackgroundInsets><AbsInset left="11" right="12" top="12" bottom="11"/></BackgroundInsets>
<TileSize><AbsValue val="32"/></TileSize>
<EdgeSize><AbsValue val="32"/></EdgeSize>
</Backdrop>
</Frame>
<Frame name="$parentMenuBackdrop" setAllPoints="true" hidden="true">
<Backdrop bgFile="Interface\Tooltips\UI-Tooltip-Background" edgeFile="Interface\Tooltips\UI-Tooltip-Border" tile="true">
<BackgroundInsets><AbsInset left="5" right="5" top="5" bottom="5"/></BackgroundInsets>
<TileSize><AbsValue val="16"/></TileSize>
<EdgeSize><AbsValue val="16"/></EdgeSize>
</Backdrop>
<Scripts>
<OnLoad>
this:SetBackdropBorderColor(TOOLTIP_DEFAULT_COLOR.r, TOOLTIP_DEFAULT_COLOR.g, TOOLTIP_DEFAULT_COLOR.b);
this:SetBackdropColor(TOOLTIP_DEFAULT_BACKGROUND_COLOR.r, TOOLTIP_DEFAULT_BACKGROUND_COLOR.g, TOOLTIP_DEFAULT_BACKGROUND_COLOR.b);
</OnLoad>
</Scripts>
</Frame>
<Button name="$parentButton1" inherits="UIDropDownMenuButtonTemplate" id="1"/>
<Button name="$parentButton2" inherits="UIDropDownMenuButtonTemplate" id="2"/>
<Button name="$parentButton3" inherits="UIDropDownMenuButtonTemplate" id="3"/>
<Button name="$parentButton4" inherits="UIDropDownMenuButtonTemplate" id="4"/>
<Button name="$parentButton5" inherits="UIDropDownMenuButtonTemplate" id="5"/>
<Button name="$parentButton6" inherits="UIDropDownMenuButtonTemplate" id="6"/>
<Button name="$parentButton7" inherits="UIDropDownMenuButtonTemplate" id="7"/>
<Button name="$parentButton8" inherits="UIDropDownMenuButtonTemplate" id="8"/>
<Button name="$parentButton9" inherits="UIDropDownMenuButtonTemplate" id="9"/>
<Button name="$parentButton10" inherits="UIDropDownMenuButtonTemplate" id="10"/>
<Button name="$parentButton11" inherits="UIDropDownMenuButtonTemplate" id="11"/>
<Button name="$parentButton12" inherits="UIDropDownMenuButtonTemplate" id="12"/>
<Button name="$parentButton13" inherits="UIDropDownMenuButtonTemplate" id="13"/>
<Button name="$parentButton14" inherits="UIDropDownMenuButtonTemplate" id="14"/>
<Button name="$parentButton15" inherits="UIDropDownMenuButtonTemplate" id="15"/>
<Button name="$parentButton16" inherits="UIDropDownMenuButtonTemplate" id="16"/>
<Button name="$parentButton17" inherits="UIDropDownMenuButtonTemplate" id="17"/>
<Button name="$parentButton18" inherits="UIDropDownMenuButtonTemplate" id="18"/>
<Button name="$parentButton19" inherits="UIDropDownMenuButtonTemplate" id="19"/>
<Button name="$parentButton20" inherits="UIDropDownMenuButtonTemplate" id="20"/>
<Button name="$parentButton21" inherits="UIDropDownMenuButtonTemplate" id="21"/>
<Button name="$parentButton22" inherits="UIDropDownMenuButtonTemplate" id="22"/>
<Button name="$parentButton23" inherits="UIDropDownMenuButtonTemplate" id="23"/>
<Button name="$parentButton24" inherits="UIDropDownMenuButtonTemplate" id="24"/>
<Button name="$parentButton25" inherits="UIDropDownMenuButtonTemplate" id="25"/>
<Button name="$parentButton26" inherits="UIDropDownMenuButtonTemplate" id="26"/>
<Button name="$parentButton27" inherits="UIDropDownMenuButtonTemplate" id="27"/>
<Button name="$parentButton28" inherits="UIDropDownMenuButtonTemplate" id="28"/>
<Button name="$parentButton29" inherits="UIDropDownMenuButtonTemplate" id="29"/>
<Button name="$parentButton30" inherits="UIDropDownMenuButtonTemplate" id="30"/>
<Button name="$parentButton31" inherits="UIDropDownMenuButtonTemplate" id="31"/>
<Button name="$parentButton32" inherits="UIDropDownMenuButtonTemplate" id="32"/>
</Frames>
<Scripts>
<OnClick>
this:Hide();
</OnClick>
<OnEnter>
UIDropDownMenu_StopCounting(this);
</OnEnter>
<OnLeave>
UIDropDownMenu_StartCounting(this);
</OnLeave>
<OnUpdate>
UIDropDownMenu_OnUpdate(arg1);
</OnUpdate>
<OnShow>
if ( this:GetID() > 1 ) then
this.parent = getglobal("DropDownList"..(this:GetID() - 1));
end
this.showTimer = nil;
this.isCounting = nil;
if ( not this.fixedWidth ) then
this.pendingWidth = 1;
end
this.pendingFlip = 1;
</OnShow>
</Scripts>
</Button>
<!-- The capsule (spec: ref UIDropDownMenuTemplate). Its art, geometry and texcoords are the
reference's own numbers — that is what makes it the same widget rather than a lookalike. -->
<Frame name="UIDropDownMenuTemplate" virtual="true">
<Size><AbsDimension x="40" y="32"/></Size>
<Layers>
<Layer level="ARTWORK">
<Texture name="$parentLeft" file="Interface\Glues\CharacterCreate\CharacterCreate-LabelFrame">
<Size><AbsDimension x="25" y="64"/></Size>
<Anchors><Anchor point="TOPLEFT"><Offset><AbsDimension x="0" y="17"/></Offset></Anchor></Anchors>
<TexCoords left="0" right="0.1953125" top="0" bottom="1"/>
</Texture>
<Texture name="$parentMiddle" file="Interface\Glues\CharacterCreate\CharacterCreate-LabelFrame">
<Size><AbsDimension x="115" y="64"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentLeft" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0.1953125" right="0.8046875" top="0" bottom="1"/>
</Texture>
<Texture name="$parentRight" file="Interface\Glues\CharacterCreate\CharacterCreate-LabelFrame">
<Size><AbsDimension x="25" y="64"/></Size>
<Anchors><Anchor point="LEFT" relativeTo="$parentMiddle" relativePoint="RIGHT"/></Anchors>
<TexCoords left="0.8046875" right="1" top="0" bottom="1"/>
</Texture>
<FontString name="$parentText" inherits="GameFontHighlightSmall" justifyH="RIGHT">
<Size><AbsDimension x="0" y="10"/></Size>
<Anchors><Anchor point="RIGHT" relativeTo="$parentRight"><Offset><AbsDimension x="-43" y="2"/></Offset></Anchor></Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<Button name="$parentButton">
<Size><AbsDimension x="24" y="24"/></Size>
<Anchors>
<Anchor point="TOPRIGHT" relativeTo="$parentRight">
<Offset><AbsDimension x="-16" y="-18"/></Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick>
ToggleDropDownMenu();
PlaySound("igMainMenuOptionCheckBoxOn");
</OnClick>
</Scripts>
<NormalTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollDown-Up"/>
<PushedTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollDown-Down"/>
<DisabledTexture file="Interface\ChatFrame\UI-ChatIcon-ScrollDown-Disabled"/>
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" alphaMode="ADD"/>
</Button>
</Frames>
<Scripts>
<OnHide>
CloseDropDownMenus();
</OnHide>
</Scripts>
</Frame>
<!-- The two shared lists (the ref authors DropDownList1..3; level 3 waits for a customer).
The per-instance <Scripts> answer the three jobs the reference hangs off these two elements
(spec: ref UIDropDownMenu.xml l.9-18, l.25-28). They were missing here entirely until they
were measured; each earns its place separately:
· `CloseDropDownMenus(level+1)` is the SUBMENU CATCH-ALL, and it is why the ref hangs it
off OnHide rather than off the toggle. `ToggleDropDownMenu`'s close arm is a bare
`listFrame:Hide()` (l.15-19 of the function) — so with no OnHide, opening a menu with a
`hasArrow` row, hovering the submenu open and then toggling level 1 shut LEFT
DropDownList2 on screen with its parent gone. OnHide catches that path and every other
one that hides level 1 (a parent Hide, the ESC ladder, an addon calling
`DropDownList1:Hide()` itself) — which is the point of putting it on the event.
· `OPEN_DROPDOWNMENUS[level] = nil` closes a real leak: UnitPopup.xml WRITES the level
(l.191, l.239) and its every-frame driver READS it (l.481), and nothing here ever
cleared it, so an entry survived its menu forever. No player-visible symptom is
reachable today and that is stated rather than implied — `UnitPopup_OnUpdate` returns
early unless DropDownList1 is visible AND a UnitPopupFrame owns the open menu, and
`UIDropDownMenu_AddButton` calls `button:Enable()` unconditionally, so a stale level's
Disable is undone the next time that list is built. Both of those are guards belonging
to other code; the divergence from the ref is here, so it is fixed here.
· UIDROPDOWNMENU_DEFAULT_TEXT_HEIGHT is DERIVED, never a literal. The ref forward-declares
it nil (UIDropDownMenu.lua l.16) and this OnLoad is the only thing that ever assigns it,
reading the height off Button1's own NormalText. `UIDropDownMenu_AddButton` then passes
it straight into `SetFont` (l.187) for every row without an explicit `info.textHeight`.
Dewdrop-2.0 does the same at its own l.489 — 64 corpus addon folders embed a copy, but
it is ONE library file replicated, and only that one of its two sites is unguarded (the
other is `info.textHeight or UIDROPDOWNMENU_DEFAULT_TEXT_HEIGHT or 10`). Writing a
literal here would have been a silent fidelity divergence, not a fix. -->
<!-- `toplevel` is authored on the ref's INSTANCES, not its template (decision 1318). -->
<Button name="DropDownList1" inherits="UIDropDownListTemplate" id="1" toplevel="true" parent="UIParent">
<Scripts>
<OnLoad>
local fontName, fontHeight, fontFlags = getglobal("DropDownList1Button1NormalText"):GetFont();
UIDROPDOWNMENU_DEFAULT_TEXT_HEIGHT = fontHeight;
</OnLoad>
<OnHide>
CloseDropDownMenus(2);
OPEN_DROPDOWNMENUS[1] = nil;
</OnHide>
</Scripts>
</Button>
<Button name="DropDownList2" inherits="UIDropDownListTemplate" id="2" toplevel="true" parent="UIParent">
<Scripts>
<OnHide>
CloseDropDownMenus(3);
OPEN_DROPDOWNMENUS[2] = nil;
</OnHide>
</Scripts>
</Button>
</Ui>