Files
benilla-octo/crates/benilla-app/assets/ui/KeyBindingsPage.xml
T
SamandClaude Opus 5.5 a32300fad9 comments: six interface files say what each frame is and what it mirrors
A frame's comment names the stock file or 1.12 default its shape follows;
a Lua comment stays where the code would otherwise be "fixed". Notes the
code contradicts are gone. With XML and Lua comments stripped every file
is identical, and each is well-formed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 19:28:45 +07:00

479 lines
23 KiB
XML

<Ui>
<!--
The key bindings page: the options window's Keybindings category, over the engine's
binding table and the 1.12 binding API. This file holds the templates and the script; the
rows are children of the Keybindings body in OptionsFrame.xml, which loads after it. Only
commands the engine dispatches are registered, so only they and their categories appear.
Deviation: the page is the era Settings panel's Keybindings page (Keybindings.lua,
KeybindingsOverrides.lua): one list of collapsible category sections, the Character
Specific Keybindings checkbox as its first row, and search results that open this page.
Capture and set follow 1.12's Blizzard_BindingUI where the two differ: a capsule click
arms, the next key or mouse 3/4/5 press binds at once, left and right clicks only select,
the wheel binds like any key, and ESC while armed binds Escape (1.12's OnKeyDown has no
ESCAPE filter; Unbind Key clears).
Deviation: every bind, unbind and reset saves at once, as the era panel commits, where
1.12 had Okay and Cancel; only Reset To Default and leaving the character set confirm.
-->
<!-- A key capsule, 1.12's KeyBindingFrameBindingButtonTemplate: UIPanelButtonTemplate2 art at
180x22 with the small font trio, registered for all five buttons so a middle, 4 or 5
press reaches the capture. Selected is its LockHighlight, the ADD highlight held on. -->
<Button name="KeyBindingButtonTemplate" virtual="true">
<Size><AbsDimension x="180" y="22"/></Size>
<ButtonText name="$parentText"/>
<NormalTexture file="Interface\Buttons\UI-Panel-Button-Up"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></NormalTexture>
<PushedTexture file="Interface\Buttons\UI-Panel-Button-Down"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></PushedTexture>
<HighlightTexture file="Interface\Buttons\UI-Panel-Button-Highlight" alphaMode="ADD"><TexCoords left="0" right="0.625" top="0" bottom="0.6875"/></HighlightTexture>
<NormalFont inherits="GameFontHighlightSmall"/>
<HighlightFont inherits="GameFontHighlightSmall"/>
<DisabledFont inherits="GameFontDisableSmall"/>
<Scripts>
<OnLoad>this:RegisterForClicks("LeftButtonUp", "RightButtonUp", "MiddleButtonUp", "Button4Up", "Button5Up")</OnLoad>
<OnClick>KeyBindingButton_OnClick(arg1)</OnClick>
</Scripts>
</Button>
<!-- A list row, painted as a binding (name, two capsules) or as a section header: one toggle
in 1.12's QuestLogTitleButton dress, the 16x16 plus/minus square and gold label, since
the era's expand arrows are not in 1.12. Both are 25 tall, the era's one row height
(SettingsKeybindingSectionInitializer:GetExtent). -->
<Frame name="KeyBindingRowTemplate" virtual="true">
<Size><AbsDimension x="560" y="25"/></Size>
<Layers>
<Layer level="BACKGROUND">
<FontString name="$parentDescription" inherits="GameFontNormalSmall" justifyH="LEFT">
<Size><AbsDimension x="170" y="24"/></Size>
<Anchors><Anchor point="LEFT"/></Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<Button name="$parentHeader" hidden="true">
<Anchors>
<Anchor point="TOPLEFT"/>
<Anchor point="BOTTOMRIGHT"/>
</Anchors>
<Layers>
<Layer level="ARTWORK">
<Texture name="$parentIcon">
<Size><AbsDimension x="16" y="16"/></Size>
<Anchors><Anchor point="LEFT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor></Anchors>
</Texture>
</Layer>
</Layers>
<ButtonText name="$parentText" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="24" y="1"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="0" y="1"/></Offset></Anchor>
</Anchors>
</ButtonText>
<NormalFont inherits="GameFontNormal"/>
<HighlightFont inherits="GameFontHighlight"/>
<Scripts>
<OnClick>KeyBindings_ToggleSection(this.sectionIndex)</OnClick>
</Scripts>
</Button>
<Button name="$parentKey1Button" inherits="KeyBindingButtonTemplate" id="1">
<Anchors><Anchor point="LEFT"><Offset><AbsDimension x="175" y="0"/></Offset></Anchor></Anchors>
</Button>
<Button name="$parentKey2Button" inherits="KeyBindingButtonTemplate" id="2">
<Anchors><Anchor point="LEFT" relativeTo="$parentKey1Button" relativePoint="RIGHT"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
</Button>
</Frames>
</Frame>
<Script><![CDATA[
-- ── Strings ── a 1.12 GlobalStrings global, or the fallback for a harness without them.
function KeyBindings_String(name, fallback)
local s = getglobal(name)
if s then return s end
return fallback
end
-- ── Key display ── 1.12's GetBindingText in full form, as the reference shows it here and
-- in tooltips (the action bar's abbreviated form is the stock GetBindingText): modifier
-- prefixes stay uppercase tokens, the base key reads its KEY_* global, else itself.
-- "CTRL--" is CTRL plus the minus key.
function KeyBindings_KeyText(key)
if not key then
return NORMAL_FONT_COLOR_CODE .. KeyBindings_String("NOT_BOUND", "Not Bound") .. FONT_COLOR_CODE_CLOSE
end
local prefix = ""
local rest = key
while true do
local p = string.sub(rest, 1, 4)
if p == "ALT-" then
prefix = prefix .. "ALT-"
rest = string.sub(rest, 5)
elseif string.sub(rest, 1, 5) == "CTRL-" then
prefix = prefix .. "CTRL-"
rest = string.sub(rest, 6)
elseif string.sub(rest, 1, 6) == "SHIFT-" then
prefix = prefix .. "SHIFT-"
rest = string.sub(rest, 7)
else
break
end
end
return prefix .. KeyBindings_String("KEY_" .. rest, rest)
end
-- ── Page state ── `sections` is the era section model, `selected`/`keyID` the 1.12
-- selection, and `searchPainted` maps OptionsFrame.xml's live search rows (row name to
-- binding index).
KeyBindingsPage = { sections = {}, items = {} }
KEYBINDINGS_ROW_BASE = "BenillaOptionsFrameContainerBodyKeybindingsRow"
KEYBINDINGS_DISPLAYED = 19
KEYBINDINGS_ROW_HEIGHT = 25
KEYBINDINGS_SEARCH_ROWS = 8
-- ── Section model ── the era's CreateKeybindingInitializers: one section per category
-- token in first-appearance order, which is 1.12's Bindings.xml order. Sections start
-- collapsed (the era's Classic overrides pass no `expanded`); a rebuild keeps each state.
function KeyBindings_BuildSections()
local page = KeyBindingsPage
local was = {}
for _, s in ipairs(page.sections) do
was[s.token] = s.expanded
end
page.sections = {}
local byToken = {}
for i = 1, GetNumBindings() do
local _, category = GetBinding(i)
local sec = byToken[category]
if not sec then
sec = { token = category, indices = {}, expanded = was[category] }
byToken[category] = sec
table.insert(page.sections, sec)
end
table.insert(sec.indices, i)
end
KeyBindings_Flatten()
end
-- The visible list the faux scroll runs over (the era's EvaluateVisibility): each header,
-- then its rows when expanded.
function KeyBindings_Flatten()
local page = KeyBindingsPage
page.items = {}
for n, sec in ipairs(page.sections) do
table.insert(page.items, { header = n })
if sec.expanded then
for _, i in ipairs(sec.indices) do
table.insert(page.items, { bind = i })
end
end
end
end
-- A header click (the era's OnExpandedChanged), voiced with the checkbox sound.
function KeyBindings_ToggleSection(n)
local sec = KeyBindingsPage.sections[n]
if not sec then return end
PlaySound("igMainMenuOptionCheckBoxOn")
sec.expanded = not sec.expanded
KeyBindings_Flatten()
KeyBindingsPage_Update()
end
-- Expands or collapses a section silently; the capture fixture opens sections with it.
function KeyBindings_ExpandSection(n, on)
local sec = KeyBindingsPage.sections[n]
if not sec then return end
sec.expanded = on and true or false
KeyBindings_Flatten()
end
-- ── Painters ── one row in one mode, shared by the list and the search rows, whose
-- capsules capture in place as the era's live results do.
function KeyBindings_PaintBindRow(name, index)
local command, _, key1, key2 = GetBinding(index)
local b1 = getglobal(name .. "Key1Button")
local b2 = getglobal(name .. "Key2Button")
b1.commandName = command
b2.commandName = command
b1:SetText(KeyBindings_KeyText(key1))
b2:SetText(KeyBindings_KeyText(key2))
if key1 then b1:SetAlpha(1) else b1:SetAlpha(0.8) end
if key2 then b2:SetAlpha(1) else b2:SetAlpha(0.8) end
local desc = getglobal(name .. "Description")
desc:SetText(KeyBindings_String("BINDING_NAME_" .. command, command))
desc:Show()
local page = KeyBindingsPage
if page.selected == command and page.keyID == 1 then
b1:LockHighlight()
else
b1:UnlockHighlight()
end
if page.selected == command and page.keyID == 2 then
b2:LockHighlight()
else
b2:UnlockHighlight()
end
b1:Show()
b2:Show()
getglobal(name .. "Header"):Hide()
end
function KeyBindings_PaintHeaderRow(name, n)
local sec = KeyBindingsPage.sections[n]
local header = getglobal(name .. "Header")
header.sectionIndex = n
header:SetText(KeyBindings_String(sec.token, sec.token))
local icon = getglobal(name .. "HeaderIcon")
if sec.expanded then
icon:SetTexture("Interface\\Buttons\\UI-MinusButton-Up")
else
icon:SetTexture("Interface\\Buttons\\UI-PlusButton-Up")
end
header:Show()
getglobal(name .. "Description"):Hide()
getglobal(name .. "Key1Button"):Hide()
getglobal(name .. "Key2Button"):Hide()
end
-- ── The list repaint (1.12 KeyBindingFrame_Update over the era section list). ──
function KeyBindingsPage_Update()
local page = KeyBindingsPage
local scroll = getglobal("BenillaOptionsFrameContainerBodyKeybindingsScrollFrame")
local offset = FauxScrollFrame_GetOffset(scroll)
for i = 1, KEYBINDINGS_DISPLAYED do
local name = KEYBINDINGS_ROW_BASE .. i
local row = getglobal(name)
local item = page.items[offset + i]
if item and item.header then
KeyBindings_PaintHeaderRow(name, item.header)
row:Show()
elseif item then
KeyBindings_PaintBindRow(name, item.bind)
row:Show()
else
row:Hide()
end
end
FauxScrollFrame_Update(scroll, table.getn(page.items),
KEYBINDINGS_DISPLAYED, KEYBINDINGS_ROW_HEIGHT)
-- The trough is ours and the stock kit never shows one, so it follows the bar
-- `FauxScrollFrame_Update` has just shown or hidden.
BenillaScrollTrough_Follow(scroll:GetName())
KeyBindings_UpdateUnbindKey()
end
-- The page entry (BenillaOptionsFrame_SelectCategory): rebuilds the sections, since a
-- rebind or a set switch may have changed the table, then repaints from the top.
function KeyBindingsPage_Refresh()
KeyBindings_BuildSections()
local box = getglobal("BenillaOptionsFrameContainerBodyKeybindingsCharacterRowCheck")
if GetCurrentBindingSet() == 2 then
box:SetChecked(1)
else
box:SetChecked(nil)
end
getglobal("BenillaOptionsFrameContainerBodyKeybindingsScrollFrameScrollBar"):SetValue(0)
KeyBindingsPage_Update()
end
function KeyBindings_Output(text)
getglobal("BenillaOptionsFrameContainerBodyKeybindingsOutput"):SetText(text)
end
-- ── Selection ── selecting a capsule arms the host's capture (BenillaBindCapture), which
-- swallows raw input and hands the canonical chord to KeyBindings_OnHostKey; deselecting
-- disarms.
function KeyBindings_SetSelected(command, keyID)
KeyBindingsPage.selected = command
KeyBindingsPage.keyID = keyID
BenillaBindCapture(command ~= nil)
end
function KeyBindings_UpdateUnbindKey()
local unbind = getglobal("BenillaOptionsFrameContainerUnbind")
if KeyBindingsPage.selected then
unbind:Enable()
else
unbind:Disable()
end
end
-- Repaints whichever view shows binding rows, the list or every search row, since a bind
-- from one search row can unbind a victim shown on another.
function KeyBindings_RepaintVisible()
if KeyBindingsPage.searchPainted then
for name, index in pairs(KeyBindingsPage.searchPainted) do
KeyBindings_PaintBindRow(name, index)
end
KeyBindings_UpdateUnbindKey()
else
KeyBindingsPage_Update()
end
end
function KeyBindingButton_OnClick(button)
-- As 1.12: while armed, only a left or right click reselects or deselects. A middle,
-- 4 or 5 press is a bind that the armed capture takes after this click, so it must
-- leave the selection alone.
local page = KeyBindingsPage
if page.selected then
if button ~= "LeftButton" and button ~= "RightButton" then
return
end
if page.selected == this.commandName and page.keyID == this:GetID() then
KeyBindings_SetSelected(nil)
KeyBindings_Output("")
else
KeyBindings_SetSelected(this.commandName, this:GetID())
KeyBindings_Output(string.format(
KeyBindings_String("BIND_KEY_TO_COMMAND", "Press Key to Bind to Command -> %s"),
KeyBindings_String("BINDING_NAME_" .. this.commandName, this.commandName)))
end
else
KeyBindings_SetSelected(this.commandName, this:GetID())
KeyBindings_Output(string.format(
KeyBindings_String("BIND_KEY_TO_COMMAND", "Press Key to Bind to Command -> %s"),
KeyBindings_String("BINDING_NAME_" .. this.commandName, this.commandName)))
end
KeyBindings_RepaintVisible()
end
-- The live commit: saves the set a bind, unbind or reset has just changed.
function KeyBindings_Commit()
SaveBindings(GetCurrentBindingSet())
end
-- The host's capture callback, `chord` canonical (ALT-CTRL-SHIFT-TOKEN): 1.12's OnKeyDown
-- armed branch, then the live commit. A stolen chord names its victim in red only when it
-- was the victim's last key; the command's keys rebind in slot order.
function KeyBindings_OnHostKey(chord)
local page = KeyBindingsPage
if not page.selected then
return
end
local selected = page.selected
local oldAction = GetBindingAction(chord)
if oldAction ~= "" and oldAction ~= selected then
local k1, k2 = GetBindingKey(oldAction)
if (not k1 or k1 == chord) and (not k2 or k2 == chord) then
KeyBindings_Output(string.format(
KeyBindings_String("KEY_UNBOUND_ERROR", "|cffff0000%s Function is Now Unbound!|r"),
KeyBindings_String("BINDING_NAME_" .. oldAction, oldAction)))
else
KeyBindings_Output(KeyBindings_String("KEY_BOUND", "Key Bound Successfully"))
end
else
KeyBindings_Output(KeyBindings_String("KEY_BOUND", "Key Bound Successfully"))
end
local key1, key2 = GetBindingKey(selected)
if key1 then SetBinding(key1) end
if key2 then SetBinding(key2) end
if page.keyID == 1 then
KeyBindings_SetOrRestore(chord, selected, key1)
if key2 then SetBinding(key2, selected) end
else
if key1 then SetBinding(key1, selected) end
KeyBindings_SetOrRestore(chord, selected, key2)
end
KeyBindings_Commit()
KeyBindings_SetSelected(nil)
KeyBindings_RepaintVisible()
end
-- 1.12's KeyBindingFrame_SetBinding: a refused bind restores the slot's old key. SetBinding
-- refuses only a string that is not a key, so this fires for a chord the capture could not
-- name, never for the wheel; the message stays 1.12's wheel text.
function KeyBindings_SetOrRestore(key, command, oldKey)
if not SetBinding(key, command) then
if oldKey then SetBinding(oldKey, command) end
KeyBindings_Output(KeyBindings_String(
"KEYBINDINGFRAME_MOUSEWHEEL_ERROR",
"Can't bind mousewheel to actions with up and down states"))
end
end
-- The Unbind Key button (OptionsFrame.xml): 1.12's body, which empties the selected slot
-- and slides the other into slot order, then the live commit.
function KeyBindings_Unbind()
local page = KeyBindingsPage
local key1, key2 = GetBindingKey(page.selected)
if key1 then SetBinding(key1) end
if key2 then SetBinding(key2) end
if key1 and page.keyID == 1 then
if key2 then SetBinding(key2, page.selected) end
else
if key1 then SetBinding(key1, page.selected) end
end
KeyBindings_Commit()
KeyBindings_SetSelected(nil)
KeyBindings_Output("")
KeyBindings_RepaintVisible()
end
-- ── Character-specific set ── 1.12's binding sets under the era's checkbox. Checking
-- saves set 2 at once, a copy of the current binds (LoadBindings(2) falls back to the
-- account set), and that saved set is what the checkbox reads. Unchecking deletes it, so
-- it confirms (the era confirms only on uncheck) and the box stays checked meanwhile.
-- The accept loads set 1 before saving it; saving first would write the character binds
-- into the account set.
function KeyBindings_CharacterToggle(box)
PlaySound("igMainMenuOptionCheckBoxOn")
if box:GetChecked() then
LoadBindings(2)
SaveBindings(2)
KeyBindings_SetSelected(nil)
KeyBindings_Output("")
KeyBindingsPage_Refresh()
else
box:SetChecked(1)
StaticPopup_Show("CONFIRM_DELETING_CHARACTER_SPECIFIC_BINDINGS")
end
end
StaticPopupDialogs["CONFIRM_DELETING_CHARACTER_SPECIFIC_BINDINGS"] = {
text = KeyBindings_String("CONFIRM_DELETING_CHARACTER_SPECIFIC_BINDINGS",
"Really switch to general key bindings? All key bindings specific to this character will be permanantly deleted."),
button1 = KeyBindings_String("OKAY", "Okay"),
button2 = KeyBindings_String("CANCEL", "Cancel"),
OnAccept = function()
LoadBindings(1)
SaveBindings(1)
KeyBindings_SetSelected(nil)
KeyBindings_Output("")
KeyBindingsPage_Refresh()
end,
}
-- Reset To Default, the page's Defaults button: it confirms, as the era's does, with our
-- own string since 1.12 has none, and resets the current set with a live commit.
StaticPopupDialogs["CONFIRM_RESET_KEYBINDINGS"] = {
text = "Are you sure you want to reset all keybindings to their defaults?",
button1 = KeyBindings_String("OKAY", "Okay"),
button2 = KeyBindings_String("CANCEL", "Cancel"),
OnAccept = function()
PlaySound("igMainMenuOptionCheckBoxOn")
LoadBindings(0)
KeyBindings_Commit()
KeyBindings_SetSelected(nil)
KeyBindings_Output("")
KeyBindingsPage_Refresh()
end,
}
-- ── Search ── a binding's tag is its display name uppercased (the era's
-- AddSearchTags(GetBindingName(action))); matches list under the Keybindings group head,
-- which opens this page (the era's `redirectCategory`, Blizzard_SettingsPanel.lua l.578).
-- OptionsFrame.xml paints them as live rows. Deviation: a static pool of
-- KEYBINDINGS_SEARCH_ROWS rows, where the era's results scroll.
function KeyBindings_SearchMatches(words)
local out = {}
for i = 1, GetNumBindings() do
local command = GetBinding(i)
local tag = string.upper(KeyBindings_String("BINDING_NAME_" .. command, command))
local s = OptionsSearch_Score(tag, words)
if s then
table.insert(out, { score = s, index = i })
end
end
return out
end
]]></Script>
</Ui>