diff --git a/src/config/ConfigManager.cpp b/src/config/ConfigManager.cpp index b0c44408..ac5c101e 100644 --- a/src/config/ConfigManager.cpp +++ b/src/config/ConfigManager.cpp @@ -6,6 +6,7 @@ #include "helpers/VarList.hpp" #include "../protocols/LayerShell.hpp" +#include #include #include #include @@ -18,6 +19,7 @@ #include #include #include +#include extern "C" char** environ; @@ -1908,6 +1910,7 @@ std::optional CConfigManager::handleBind(const std::string& command bool nonConsuming = false; bool transparent = false; bool ignoreMods = false; + bool multiKey = false; const auto BINDARGS = command.substr(4); for (auto& arg : BINDARGS) { @@ -1925,6 +1928,8 @@ std::optional CConfigManager::handleBind(const std::string& command transparent = true; } else if (arg == 'i') { ignoreMods = true; + } else if (arg == 's') { + multiKey = true; } else { return "bind: invalid flag"; } @@ -1943,10 +1948,21 @@ std::optional CConfigManager::handleBind(const std::string& command else if ((ARGS.size() > 4 && !mouse) || (ARGS.size() > 3 && mouse)) return "bind: too many args"; + std::set KEYSYMS; + std::set MODS; + + if (multiKey) { + for (auto splitKey : CVarList(ARGS[1], 8, '&')) { + KEYSYMS.insert(xkb_keysym_from_name(splitKey.c_str(), XKB_KEYSYM_CASE_INSENSITIVE)); + } + for (auto splitMod : CVarList(ARGS[0], 8, '&')) { + MODS.insert(xkb_keysym_from_name(splitMod.c_str(), XKB_KEYSYM_CASE_INSENSITIVE)); + } + } const auto MOD = g_pKeybindManager->stringToModMask(ARGS[0]); const auto MODSTR = ARGS[0]; - const auto KEY = ARGS[1]; + const auto KEY = multiKey ? "" : ARGS[1]; auto HANDLER = ARGS[2]; @@ -1970,7 +1986,7 @@ std::optional CConfigManager::handleBind(const std::string& command return "Invalid mod, requested mod \"" + MODSTR + "\" is not a valid mod."; } - if (KEY != "") { + if ((KEY != "") || multiKey) { SParsedKey parsedKey = parseKey(KEY); if (parsedKey.catchAll && m_szCurrentSubmap == "") { @@ -1978,8 +1994,8 @@ std::optional CConfigManager::handleBind(const std::string& command return "Invalid catchall, catchall keybinds are only allowed in submaps."; } - g_pKeybindManager->addKeybind(SKeybind{parsedKey.key, parsedKey.keycode, parsedKey.catchAll, MOD, HANDLER, COMMAND, locked, m_szCurrentSubmap, release, repeat, mouse, - nonConsuming, transparent, ignoreMods}); + g_pKeybindManager->addKeybind(SKeybind{parsedKey.key, KEYSYMS, parsedKey.keycode, parsedKey.catchAll, MOD, MODS, HANDLER, COMMAND, locked, m_szCurrentSubmap, release, + repeat, mouse, nonConsuming, transparent, ignoreMods, multiKey}); } return {}; diff --git a/src/managers/KeybindManager.cpp b/src/managers/KeybindManager.cpp index ead83fa8..ea064031 100644 --- a/src/managers/KeybindManager.cpp +++ b/src/managers/KeybindManager.cpp @@ -8,7 +8,9 @@ #include "../devices/IKeyboard.hpp" #include "../managers/SeatManager.hpp" +#include #include +#include #include #include @@ -536,8 +538,48 @@ int repeatKeyHandler(void* data) { return 0; } +eMultiKeyCase CKeybindManager::mkKeysymSetMatches(const std::set keybindKeysyms, const std::set pressedKeysyms) { + // Returns whether two sets of keysyms are equal, partially equal, or not + // matching. (Partially matching means that pressed is a subset of bound) + + std::set boundKeysNotPressed; + std::set pressedKeysNotBound; + + std::set_difference(keybindKeysyms.begin(), keybindKeysyms.end(), pressedKeysyms.begin(), pressedKeysyms.end(), + std::inserter(boundKeysNotPressed, boundKeysNotPressed.begin())); + std::set_difference(pressedKeysyms.begin(), pressedKeysyms.end(), keybindKeysyms.begin(), keybindKeysyms.end(), + std::inserter(pressedKeysNotBound, pressedKeysNotBound.begin())); + + if (boundKeysNotPressed.empty() && pressedKeysNotBound.empty()) + return MK_FULL_MATCH; + + if (boundKeysNotPressed.size() && pressedKeysNotBound.empty()) + return MK_PARTIAL_MATCH; + + return MK_NO_MATCH; +} + +eMultiKeyCase CKeybindManager::mkBindMatches(const SKeybind keybind) { + if (mkKeysymSetMatches(keybind.sMkMods, m_sMkMods) != MK_FULL_MATCH) + return MK_NO_MATCH; + + return mkKeysymSetMatches(keybind.sMkKeys, m_sMkKeys); +} + bool CKeybindManager::handleKeybinds(const uint32_t modmask, const SPressedKeyWithMods& key, bool pressed) { - bool found = false; + bool found = false; + + if (pressed) { + if (keycodeToModifier(key.keycode)) + m_sMkMods.insert(key.keysym); + else + m_sMkKeys.insert(key.keysym); + } else { + if (keycodeToModifier(key.keycode)) + m_sMkMods.erase(key.keysym); + else + m_sMkKeys.erase(key.keysym); + } static auto PDISABLEINHIBIT = CConfigValue("binds:disable_keybind_grabbing"); @@ -559,7 +601,13 @@ bool CKeybindManager::handleKeybinds(const uint32_t modmask, const SPressedKeyWi if (!IGNORECONDITIONS && ((modmask != k.modmask && !k.ignoreMods) || k.submap != m_szCurrentSelectedSubmap || k.shadowed)) continue; - if (!key.keyName.empty()) { + if (k.multiKey) { + switch (mkBindMatches(k)) { + case MK_NO_MATCH: continue; + case MK_PARTIAL_MATCH: found = true; continue; + case MK_FULL_MATCH: found = true; + } + } else if (!key.keyName.empty()) { if (key.keyName != k.key) continue; } else if (k.keycode != 0) { @@ -672,25 +720,29 @@ void CKeybindManager::shadowKeybinds(const xkb_keysym_t& doesntHave, const uint3 if (k.handler == "global" || k.transparent) continue; // can't be shadowed - const auto KBKEY = xkb_keysym_from_name(k.key.c_str(), XKB_KEYSYM_CASE_INSENSITIVE); - const auto KBKEYUPPER = xkb_keysym_to_upper(KBKEY); + if (k.multiKey && (mkBindMatches(k) == MK_FULL_MATCH)) + shadow = true; + else { + const auto KBKEY = xkb_keysym_from_name(k.key.c_str(), XKB_KEYSYM_CASE_INSENSITIVE); + const auto KBKEYUPPER = xkb_keysym_to_upper(KBKEY); - for (auto& pk : m_dPressedKeys) { - if ((pk.keysym != 0 && (pk.keysym == KBKEY || pk.keysym == KBKEYUPPER))) { - shadow = true; + for (auto& pk : m_dPressedKeys) { + if ((pk.keysym != 0 && (pk.keysym == KBKEY || pk.keysym == KBKEYUPPER))) { + shadow = true; - if (pk.keysym == doesntHave && doesntHave != 0) { - shadow = false; - break; + if (pk.keysym == doesntHave && doesntHave != 0) { + shadow = false; + break; + } } - } - if (pk.keycode != 0 && pk.keycode == k.keycode) { - shadow = true; + if (pk.keycode != 0 && pk.keycode == k.keycode) { + shadow = true; - if (pk.keycode == doesntHaveCode && doesntHaveCode != 0) { - shadow = false; - break; + if (pk.keycode == doesntHaveCode && doesntHaveCode != 0) { + shadow = false; + break; + } } } } diff --git a/src/managers/KeybindManager.hpp b/src/managers/KeybindManager.hpp index da98a749..a2025bb0 100644 --- a/src/managers/KeybindManager.hpp +++ b/src/managers/KeybindManager.hpp @@ -2,6 +2,7 @@ #include "../defines.hpp" #include +#include #include "../Compositor.hpp" #include #include @@ -13,20 +14,23 @@ class CPluginSystem; class IKeyboard; struct SKeybind { - std::string key = ""; - uint32_t keycode = 0; - bool catchAll = false; - uint32_t modmask = 0; - std::string handler = ""; - std::string arg = ""; - bool locked = false; - std::string submap = ""; - bool release = false; - bool repeat = false; - bool mouse = false; - bool nonConsuming = false; - bool transparent = false; - bool ignoreMods = false; + std::string key = ""; + std::set sMkKeys = {}; + uint32_t keycode = 0; + bool catchAll = false; + uint32_t modmask = 0; + std::set sMkMods = {}; + std::string handler = ""; + std::string arg = ""; + bool locked = false; + std::string submap = ""; + bool release = false; + bool repeat = false; + bool mouse = false; + bool nonConsuming = false; + bool transparent = false; + bool ignoreMods = false; + bool multiKey = false; // DO NOT INITIALIZE bool shadowed = false; @@ -57,6 +61,12 @@ struct SParsedKey { bool catchAll = false; }; +enum eMultiKeyCase { + MK_NO_MATCH = 0, + MK_PARTIAL_MATCH, + MK_FULL_MATCH +}; + class CKeybindManager { public: CKeybindManager(); @@ -105,6 +115,11 @@ class CKeybindManager { bool handleKeybinds(const uint32_t, const SPressedKeyWithMods&, bool); + std::set m_sMkKeys = {}; + std::set m_sMkMods = {}; + eMultiKeyCase mkBindMatches(const SKeybind); + eMultiKeyCase mkKeysymSetMatches(const std::set, const std::set); + bool handleInternalKeybinds(xkb_keysym_t); bool handleVT(xkb_keysym_t);