Files
benilla-octo/crates/benilla-app/assets/ui/KeyBindingsPage.xml
T
SamandClaude Opus 5.5 64cca13af1 comments: the era settings screens say why they replace 1.12's
The options window, the ESC menu and the key bindings page follow the 1.15 era client because
1.12's settings screens are much worse to use.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 05:26:00 +07:00

480 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), because 1.12's settings screens are much worse to use. It is
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>