ui: modifier chord support in the keybind registry

This commit is contained in:
mchughalex 2026-07-20 00:08:07 +01:00
parent 8642f6fe12
commit 66c42a571c
2 changed files with 66 additions and 9 deletions

View file

@ -57,7 +57,9 @@ std::string VirtualKeyToString(VirtualKey vk);
* binding is visible in the settings overlay and persisted to config.
*
* @param name CVAR name for this bind (e.g. "bind_console").
* @param default_key Default key name (e.g. "Backtick", "F3").
* @param default_key Default key spec: a key name (e.g. "Backtick", "F3")
* or a modifier chord "Mod+...+Key" (e.g. "Ctrl+Shift+B";
* modifiers are Ctrl/Control, Shift, Alt, Super/Win).
* @param description Human-readable description for the settings UI.
* @param callback Function to invoke when the bound key is pressed.
*/
@ -73,9 +75,11 @@ void UnregisterBind(std::string_view name);
/**
* Process a key-down event against all registered binds.
*
* Looks up each bind's current key from its CVAR, parses it, and compares
* against the event's virtual key. If a match is found, the bind's callback
* is invoked and the event is marked as handled.
* Looks up each bind's current key spec from its CVAR, parses it, and
* compares against the event's virtual key. Chord specs ("Ctrl+Shift+B")
* additionally require the event's modifier state to match exactly; plain
* single-key specs fire regardless of held modifiers. If a match is found,
* the bind's callback is invoked and the event is marked as handled.
*
* @param e The key event to process.
* @return True if a bind matched and the event was consumed.

View file

@ -163,6 +163,49 @@ std::string VirtualKeyToString(VirtualKey vk) {
return {};
}
/* ---- Chord parsing ---- */
/* A key spec is either a single key name ("Home", "F3") or a chord of
modifier names joined to a final key with '+' ("Ctrl+Shift+B",
"Alt+NumpadPlus"). The last token is always the main key; every earlier
token must be a modifier name (Ctrl/Control, Shift, Alt, Super/Win). */
struct KeyChord {
VirtualKey key = VirtualKey::kNone;
bool shift = false;
bool ctrl = false;
bool alt = false;
bool super_key = false;
bool has_modifiers() const { return shift || ctrl || alt || super_key; }
};
static KeyChord ParseKeyChord(std::string_view spec) {
KeyChord chord;
size_t pos = 0;
while (true) {
const size_t plus = spec.find('+', pos);
if (plus == std::string_view::npos) {
chord.key = ParseVirtualKey(spec.substr(pos));
return chord;
}
const std::string_view token = spec.substr(pos, plus - pos);
if (token == "Ctrl" || token == "Control") {
chord.ctrl = true;
} else if (token == "Shift") {
chord.shift = true;
} else if (token == "Alt") {
chord.alt = true;
} else if (token == "Super" || token == "Win") {
chord.super_key = true;
} else {
/* Unknown modifier name: treat the whole spec as invalid rather than
silently binding the trailing key. */
chord.key = VirtualKey::kNone;
return chord;
}
pos = plus + 1;
}
}
/* ---- Bind registry ---- */
struct BindEntry {
@ -219,12 +262,22 @@ bool ProcessKeyEvent(KeyEvent& e) {
for (auto& entry : g_binds) {
if (!entry.callback)
continue;
VirtualKey vk = ParseVirtualKey(entry.current_key);
if (vk != VirtualKey::kNone && e.virtual_key() == vk) {
entry.callback();
e.set_handled(true);
return true;
const KeyChord chord = ParseKeyChord(entry.current_key);
if (chord.key == VirtualKey::kNone || e.virtual_key() != chord.key)
continue;
/* Chord binds require the exact modifier set. Plain binds keep firing
regardless of held modifiers, so a bind like "End" still works while
Shift/Ctrl are held for another purpose. */
if (chord.has_modifiers() &&
(e.is_shift_pressed() != chord.shift ||
e.is_ctrl_pressed() != chord.ctrl ||
e.is_alt_pressed() != chord.alt ||
e.is_super_pressed() != chord.super_key)) {
continue;
}
entry.callback();
e.set_handled(true);
return true;
}
return false;
}