Index: binaries/data/mods/public/gui/credits/texts/programming.json =================================================================== --- binaries/data/mods/public/gui/credits/texts/programming.json +++ binaries/data/mods/public/gui/credits/texts/programming.json @@ -232,6 +232,7 @@ { "nick": "sathyam", "name": "Sathyam Vellal" }, { "nick": "sbirmi", "name": "Sharad Birmiwal" }, { "nick": "sbte", "name": "Sven Baars" }, + { "nick": "Schweini", "name": "Laurenz Reinthaler"}, { "nick": "scroogie", "name": "André Gemünd" }, { "nick": "scythetwirler", "name": "Casey X." }, { "nick": "serveurix" }, Index: binaries/data/mods/public/gui/options/options.json =================================================================== --- binaries/data/mods/public/gui/options/options.json +++ binaries/data/mods/public/gui/options/options.json @@ -84,6 +84,12 @@ "tooltip": "Show only generic names for units." } ] + }, + { + "type": "boolean", + "label": "Mac Mouse", + "tooltip": "Enables the emulation of a right-click with Ctrl+Click on Mac. Hotkeys that used 'Ctrl' before will be changed to 'Cmd'.", + "config": "macmouse" } ] }, Index: source/ps/ConfigDB.h =================================================================== --- source/ps/ConfigDB.h +++ source/ps/ConfigDB.h @@ -196,6 +196,11 @@ */ [[nodiscard]] CConfigDBHook RegisterHookAndCall(const CStr& name, std::function hook); + /** + * Same as RegisterHookAndCall, but doesn't immediately call the hook. + */ + [[nodiscard]] CConfigDBHook RegisterHook(const CStr& name, std::function hook); + void UnregisterHook(CConfigDBHook&& hook); void UnregisterHook(std::unique_ptr hook); Index: source/ps/ConfigDB.cpp =================================================================== --- source/ps/ConfigDB.cpp +++ source/ps/ConfigDB.cpp @@ -514,6 +514,11 @@ CConfigDBHook CConfigDB::RegisterHookAndCall(const CStr& name, std::function hook) { hook(); + return RegisterHook(name, hook); +} + +CConfigDBHook CConfigDB::RegisterHook(const CStr& name, std::function hook) +{ std::lock_guard s(m_Mutex); return CConfigDBHook(*this, m_Hooks.emplace(name, hook)); } Index: source/ps/GameSetup/GameSetup.cpp =================================================================== --- source/ps/GameSetup/GameSetup.cpp +++ source/ps/GameSetup/GameSetup.cpp @@ -639,14 +639,6 @@ // SDL2 >= 2.0.9 defaults to 32 pixels (to support touch screens) but that can break our double-clicking. SDL_SetHint(SDL_HINT_MOUSE_DOUBLE_CLICK_RADIUS, "1"); #endif - -#if OS_MACOSX - // Some Mac mice only have one button, so they can't right-click - // but SDL2 can emulate that with Ctrl+Click - bool macMouse = false; - CFG_GET_VAL("macmouse", macMouse); - SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, macMouse ? "1" : "0"); -#endif } static void ShutdownSDL() Index: source/ps/Hotkey.cpp =================================================================== --- source/ps/Hotkey.cpp +++ source/ps/Hotkey.cpp @@ -28,13 +28,109 @@ #include "ps/Globals.h" #include "ps/KeyName.h" +static_assert(std::is_integral::type>::value, "SDL_Scancode is not an integral enum."); +static_assert(SDL_USEREVENT_ == SDL_USEREVENT, "SDL_USEREVENT_ is not the same type as the real SDL_USEREVENT"); +static_assert(UNUSED_HOTKEY_CODE == SDL_SCANCODE_UNKNOWN); + static bool unified[UNIFIED_LAST - UNIFIED_SHIFT]; std::unordered_map g_HotkeyMap; -namespace { +namespace +{ std::unordered_map g_HotkeyStatus; + // Look up each key binding in the config file and set the mappings for + // all key combinations that trigger it. + void LoadConfigBindings(CConfigDB& configDB) + { +#if OS_MACOSX + bool macMouse = false; + CFG_GET_VAL("macmouse", macMouse); +#endif + + ResetActiveHotkeys(); + g_HotkeyMap.clear(); + g_HotkeyStatus.clear(); + + for (const std::pair& configPair : configDB.GetValuesWithPrefix(CFG_COMMAND, "hotkey.")) + { + std::string hotkeyName = configPair.first.substr(7); // strip the "hotkey." prefix + + // "unused" is kept or the A23->24 migration, this can likely be removed in A25. + if (configPair.second.empty() || (configPair.second.size() == 1 && configPair.second.front() == "unused")) + { + // Unused hotkeys must still be registered in the map to appear in the hotkey editor. + SHotkeyMapping unusedCode; + unusedCode.name = hotkeyName; + unusedCode.primary = SKey{ UNUSED_HOTKEY_CODE }; + g_HotkeyMap[UNUSED_HOTKEY_CODE].push_back(unusedCode); + continue; + } + + for (const CStr& hotkey : configPair.second) + { + std::vector keyCombination; + +#if OS_MACOSX + // When 'macmouse' is on, replace hotkeys that are exactly 'ctrl' with 'cmd' instead. + if (macMouse && hotkey == "Ctrl") + keyCombination = { { UNIFIED_SUPER } }; + else +#endif + // Indent for convenience in the above #if + { + // Iterate through multiple-key bindings (e.g. Ctrl+I) + boost::char_separator sep("+"); + typedef boost::tokenizer > tokenizer; + tokenizer tok(hotkey, sep); + for (tokenizer::iterator it = tok.begin(); it != tok.end(); ++it) + { + // Attempt decode as key name + SDL_Scancode scancode = FindScancode(it->c_str()); + if (!scancode) + { + LOGWARNING("Hotkey mapping used invalid key '%s'", hotkey.c_str()); + continue; + } + + SKey key = { scancode }; + + keyCombination.push_back(key); + } + } + + std::vector::iterator itKey, itKey2; + for (itKey = keyCombination.begin(); itKey != keyCombination.end(); ++itKey) + { + SHotkeyMapping bindCode; + + bindCode.name = hotkeyName; + bindCode.primary = SKey{ itKey->code }; + + for (itKey2 = keyCombination.begin(); itKey2 != keyCombination.end(); ++itKey2) + if (itKey != itKey2) // Push any auxiliary keys + bindCode.requires.push_back(*itKey2); + + g_HotkeyMap[itKey->code].push_back(bindCode); + } + } + } + } + + void CheckMacMouse() + { +#if OS_MACOSX + bool macMouse = false; + CFG_GET_VAL("macmouse", macMouse); + SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, macMouse ? "1" : "0"); + // This clears existing hotkeys. + LoadConfigBindings(g_ConfigDB); +#endif + } + + std::unique_ptr m_MacMouseHook; + struct PressedHotkey { PressedHotkey(const SHotkeyMapping* m, bool t) : mapping(m), retriggered(t) {}; @@ -67,80 +163,19 @@ // List of active keys relevant for hotkeys. std::vector activeScancodes; -} - -static_assert(std::is_integral::type>::value, "SDL_Scancode is not an integral enum."); -static_assert(SDL_USEREVENT_ == SDL_USEREVENT, "SDL_USEREVENT_ is not the same type as the real SDL_USEREVENT"); -static_assert(UNUSED_HOTKEY_CODE == SDL_SCANCODE_UNKNOWN); - -// Look up each key binding in the config file and set the mappings for -// all key combinations that trigger it. -static void LoadConfigBindings(CConfigDB& configDB) -{ - for (const std::pair& configPair : configDB.GetValuesWithPrefix(CFG_COMMAND, "hotkey.")) - { - std::string hotkeyName = configPair.first.substr(7); // strip the "hotkey." prefix - - // "unused" is kept or the A23->24 migration, this can likely be removed in A25. - if (configPair.second.empty() || (configPair.second.size() == 1 && configPair.second.front() == "unused")) - { - // Unused hotkeys must still be registered in the map to appear in the hotkey editor. - SHotkeyMapping unusedCode; - unusedCode.name = hotkeyName; - unusedCode.primary = SKey{ UNUSED_HOTKEY_CODE }; - g_HotkeyMap[UNUSED_HOTKEY_CODE].push_back(unusedCode); - continue; - } - - for (const CStr& hotkey : configPair.second) - { - std::vector keyCombination; - - // Iterate through multiple-key bindings (e.g. Ctrl+I) - boost::char_separator sep("+"); - typedef boost::tokenizer > tokenizer; - tokenizer tok(hotkey, sep); - for (tokenizer::iterator it = tok.begin(); it != tok.end(); ++it) - { - // Attempt decode as key name - SDL_Scancode scancode = FindScancode(it->c_str()); - if (!scancode) - { - LOGWARNING("Hotkey mapping used invalid key '%s'", hotkey.c_str()); - continue; - } - - SKey key = { scancode }; - keyCombination.push_back(key); - } - - std::vector::iterator itKey, itKey2; - for (itKey = keyCombination.begin(); itKey != keyCombination.end(); ++itKey) - { - SHotkeyMapping bindCode; - - bindCode.name = hotkeyName; - bindCode.primary = SKey{ itKey->code }; - - for (itKey2 = keyCombination.begin(); itKey2 != keyCombination.end(); ++itKey2) - if (itKey != itKey2) // Push any auxiliary keys - bindCode.requires.push_back(*itKey2); - - g_HotkeyMap[itKey->code].push_back(bindCode); - } - } - } -} +} // anonymous namespace void LoadHotkeys(CConfigDB& configDB) { - pressedHotkeys.clear(); LoadConfigBindings(configDB); + m_MacMouseHook = std::make_unique(configDB.RegisterHook("macmouse", CheckMacMouse)); } void UnloadHotkeys() { - pressedHotkeys.clear(); + m_MacMouseHook.reset(); + + ResetActiveHotkeys(); g_HotkeyMap.clear(); g_HotkeyStatus.clear(); }