diff --git a/include/rex/ui/keybinds.h b/include/rex/ui/keybinds.h index 3120f16..d092130 100644 --- a/include/rex/ui/keybinds.h +++ b/include/rex/ui/keybinds.h @@ -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. diff --git a/src/ui/keybinds.cpp b/src/ui/keybinds.cpp index 847cd8f..8a473e7 100644 --- a/src/ui/keybinds.cpp +++ b/src/ui/keybinds.cpp @@ -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; }