From f647223e949a8c9365e211d0adceece2efbcffa4 Mon Sep 17 00:00:00 2001 From: Jaime Bernardo Date: Thu, 23 Sep 2021 14:23:22 +0100 Subject: [PATCH] [Shortcut Guide] Activate with Windows key press (#13342) * [Shortcut Guide] Activate with Windows key press * fix spellchecker * pr comments: fix search and add lock * Add activation method combo box * fix spellchecker issue for customized * Standardize centralized hotkeys file names * Add warning when using the long win key method * Address PR feedback on text * More PR feedback --- .../ShortcutGuideModuleInterface/dllmain.cpp | 42 ++++- .../interface/powertoy_module_interface.h | 8 + ...zedHotkeys.cpp => centralized_hotkeys.cpp} | 2 +- ...ralizedHotkeys.h => centralized_hotkeys.h} | 0 src/runner/centralized_kb_hook.cpp | 147 +++++++++++++++++- src/runner/centralized_kb_hook.h | 2 + src/runner/main.cpp | 2 +- src/runner/powertoy_module.cpp | 17 +- src/runner/runner.vcxproj | 4 +- src/runner/runner.vcxproj.filters | 4 +- src/runner/tray_icon.cpp | 4 +- .../ShortcutGuideProperties.cs | 4 + .../ViewModels/ShortcutGuideViewModel.cs | 38 +++++ .../Strings/en-us/Resources.resw | 18 +++ .../Views/ShortcutGuidePage.xaml | 36 ++++- 15 files changed, 308 insertions(+), 20 deletions(-) rename src/runner/{CentralizedHotkeys.cpp => centralized_hotkeys.cpp} (99%) rename src/runner/{CentralizedHotkeys.h => centralized_hotkeys.h} (100%) diff --git a/src/modules/ShortcutGuide/ShortcutGuideModuleInterface/dllmain.cpp b/src/modules/ShortcutGuide/ShortcutGuideModuleInterface/dllmain.cpp index 36d88f156..6fb26c5d3 100644 --- a/src/modules/ShortcutGuide/ShortcutGuideModuleInterface/dllmain.cpp +++ b/src/modules/ShortcutGuide/ShortcutGuideModuleInterface/dllmain.cpp @@ -64,7 +64,7 @@ public: PowerToysSettings::PowerToyValues values = PowerToysSettings::PowerToyValues::from_json_string(config, get_key()); - ParseHotkey(values); + ParseSettings(values); } catch (std::exception ex) { @@ -119,6 +119,10 @@ public: virtual std::optional GetHotkeyEx() override { Logger::trace("GetHotkeyEx()"); + if (m_shouldReactToPressedWinKey) + { + return std::nullopt; + } return m_hotkey; } @@ -154,6 +158,16 @@ public: } } + virtual bool keep_track_of_pressed_win_key() override + { + return m_shouldReactToPressedWinKey; + } + + virtual UINT milliseconds_win_key_must_be_pressed() override + { + return m_millisecondsWinKeyShouldBePressed; + } + private: std::wstring app_name; //contains the non localized key of the powertoy @@ -163,6 +177,12 @@ private: // Hotkey to invoke the module HotkeyEx m_hotkey; + + // If the module should be activated through the legacy pressing windows key behavior. + const UINT DEFAULT_MILLISECONDS_WIN_KEY_SHOULD_BE_PRESSED = 900; + bool m_shouldReactToPressedWinKey = false; + UINT m_millisecondsWinKeyShouldBePressed = DEFAULT_MILLISECONDS_WIN_KEY_SHOULD_BE_PRESSED; + HANDLE exitEvent; bool StartProcess(std::wstring args = L"") @@ -239,7 +259,7 @@ private: PowerToysSettings::PowerToyValues settings = PowerToysSettings::PowerToyValues::load_from_settings_file(app_key); - ParseHotkey(settings); + ParseSettings(settings); } catch (std::exception ex) { @@ -251,13 +271,17 @@ private: } } - void ParseHotkey(PowerToysSettings::PowerToyValues& settings) + void ParseSettings(PowerToysSettings::PowerToyValues& settings) { + m_shouldReactToPressedWinKey = false; + m_millisecondsWinKeyShouldBePressed = DEFAULT_MILLISECONDS_WIN_KEY_SHOULD_BE_PRESSED; + auto settingsObject = settings.get_raw_json(); if (settingsObject.GetView().Size()) { try { + // Parse HotKey auto jsonHotkeyObject = settingsObject.GetNamedObject(L"properties").GetNamedObject(L"open_shortcutguide"); auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonHotkeyObject); m_hotkey = HotkeyEx(); @@ -287,6 +311,18 @@ private: { Logger::warn("Failed to initialize Shortcut Guide start shortcut"); } + try + { + // Parse Legacy windows key press behavior settings + auto jsonUseLegacyWinKeyBehaviorObject = settingsObject.GetNamedObject(L"properties").GetNamedObject(L"use_legacy_press_win_key_behavior"); + m_shouldReactToPressedWinKey = (bool)jsonUseLegacyWinKeyBehaviorObject.GetNamedBoolean(L"value"); + auto jsonPressTimeObject = settingsObject.GetNamedObject(L"properties").GetNamedObject(L"press_time"); + m_millisecondsWinKeyShouldBePressed = (UINT)jsonPressTimeObject.GetNamedNumber(L"value"); + } + catch (...) + { + Logger::warn("Failed to get legacy win key behavior settings"); + } } else { diff --git a/src/modules/interface/powertoy_module_interface.h b/src/modules/interface/powertoy_module_interface.h index b57f291cd..e9e51d118 100644 --- a/src/modules/interface/powertoy_module_interface.h +++ b/src/modules/interface/powertoy_module_interface.h @@ -98,6 +98,14 @@ public: */ virtual bool on_hotkey(size_t hotkeyId) { return false; } + /* These are for enabling the legacy behavior of showing the shortcut guide after pressing the win key. + * keep_track_of_pressed_win_key returns true if the module wants to keep track of the win key being pressed. + * milliseconds_win_key_must_be_pressed returns the number of milliseconds the win key should be pressed before triggering the module. + * Don't use these for new modules. + */ + virtual bool keep_track_of_pressed_win_key() { return false; } + virtual UINT milliseconds_win_key_must_be_pressed() { return 0; } + virtual void send_settings_telemetry() { } diff --git a/src/runner/CentralizedHotkeys.cpp b/src/runner/centralized_hotkeys.cpp similarity index 99% rename from src/runner/CentralizedHotkeys.cpp rename to src/runner/centralized_hotkeys.cpp index 3bfdccd90..75630d9bb 100644 --- a/src/runner/CentralizedHotkeys.cpp +++ b/src/runner/centralized_hotkeys.cpp @@ -1,5 +1,5 @@ #include "pch.h" -#include "CentralizedHotkeys.h" +#include "centralized_hotkeys.h" #include #include diff --git a/src/runner/CentralizedHotkeys.h b/src/runner/centralized_hotkeys.h similarity index 100% rename from src/runner/CentralizedHotkeys.h rename to src/runner/centralized_hotkeys.h diff --git a/src/runner/centralized_kb_hook.cpp b/src/runner/centralized_kb_hook.cpp index eec4b7c54..7ebcd214d 100644 --- a/src/runner/centralized_kb_hook.cpp +++ b/src/runner/centralized_kb_hook.cpp @@ -3,6 +3,7 @@ #include #include #include +#include namespace CentralizedKeyboardHook { @@ -22,6 +23,30 @@ namespace CentralizedKeyboardHook std::mutex mutex; HHOOK hHook{}; + // To store information about handling pressed keys. + struct PressedKeyDescriptor + { + DWORD virtualKey; // Virtual Key code of the key we're keeping track of. + std::wstring moduleName; + std::function action; + UINT_PTR idTimer; // Timer ID for calling SET_TIMER with. + UINT millisecondsToPress; // How much time the key must be pressed. + bool operator<(const PressedKeyDescriptor& other) const + { + // We'll use the virtual key as the real key, since looking for a hit with the key is done in the more time sensitive path (low level keyboard hook). + return virtualKey < other.virtualKey; + }; + }; + std::multiset pressedKeyDescriptors; + std::mutex pressedKeyMutex; + + // keep track of last pressed key, to detect repeated keys and if there are more keys pressed. + const DWORD VK_DISABLED = CommonSharedConstants::VK_DISABLED; + DWORD vkCodePressed = VK_DISABLED; + + // Save the runner window handle for registering timers. + HWND runnerWindow; + struct DestroyOnExit { ~DestroyOnExit() @@ -30,15 +55,88 @@ namespace CentralizedKeyboardHook } } destroyOnExitObj; + // Handle the pressed key proc + void PressedKeyTimerProc( + HWND hwnd, + UINT message, + UINT_PTR idTimer, + DWORD dwTime) + { + std::multiset copy; + { + // Make a copy, to look for the action to call. + std::unique_lock lock{ pressedKeyMutex }; + copy = pressedKeyDescriptors; + } + for (const auto& it : copy) + { + if (it.idTimer == idTimer) + { + it.action(); + } + } + + KillTimer(hwnd, idTimer); + } + LRESULT CALLBACK KeyboardHookProc(_In_ int nCode, _In_ WPARAM wParam, _In_ LPARAM lParam) { - if (nCode < 0 || ((wParam != WM_KEYDOWN) && (wParam != WM_SYSKEYDOWN))) + if (nCode < 0) { return CallNextHookEx(hHook, nCode, wParam, lParam); } const auto& keyPressInfo = *reinterpret_cast(lParam); + // Check if the keys are pressed. + if (!pressedKeyDescriptors.empty()) + { + bool wasKeyPressed = vkCodePressed != VK_DISABLED; + // Hold the lock for the shortest possible duration + if ((wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN)) + { + if (!wasKeyPressed) + { + // If no key was pressed before, let's start a timer to take into account this new key. + std::unique_lock lock{ pressedKeyMutex }; + PressedKeyDescriptor dummy{ .virtualKey = keyPressInfo.vkCode }; + auto [it, last] = pressedKeyDescriptors.equal_range(dummy); + for (; it != last; ++it) + { + SetTimer(runnerWindow, it->idTimer, it->millisecondsToPress, PressedKeyTimerProc); + } + } + else if (vkCodePressed != keyPressInfo.vkCode) + { + // If a different key was pressed, let's clear the timers we have started for the previous key. + std::unique_lock lock{ pressedKeyMutex }; + PressedKeyDescriptor dummy{ .virtualKey = vkCodePressed }; + auto [it, last] = pressedKeyDescriptors.equal_range(dummy); + for (; it != last; ++it) + { + KillTimer(runnerWindow, it->idTimer); + } + } + vkCodePressed = keyPressInfo.vkCode; + } + if (wParam == WM_KEYUP || wParam == WM_SYSKEYUP) + { + std::unique_lock lock{ pressedKeyMutex }; + PressedKeyDescriptor dummy{ .virtualKey = keyPressInfo.vkCode }; + auto [it, last] = pressedKeyDescriptors.equal_range(dummy); + for (; it != last; ++it) + { + KillTimer(runnerWindow, it->idTimer); + } + vkCodePressed = 0x100; + } + } + + if ((wParam != WM_KEYDOWN) && (wParam != WM_SYSKEYDOWN)) + { + return CallNextHookEx(hHook, nCode, wParam, lParam); + } + Hotkey hotkey{ .win = (GetAsyncKeyState(VK_LWIN) & 0x8000) || (GetAsyncKeyState(VK_RWIN) & 0x8000), .ctrl = static_cast(GetAsyncKeyState(VK_CONTROL) & 0x8000), @@ -85,20 +183,48 @@ namespace CentralizedKeyboardHook hotkeyDescriptors.insert({ .hotkey = hotkey, .moduleName = moduleName, .action = std::move(action) }); } + void AddPressedKeyAction(const std::wstring& moduleName, const DWORD vk, const UINT milliseconds, std::function&& action) noexcept + { + // Calculate a unique TimerID. + auto hash = std::hash{}(moduleName); // Hash the module as the upper part of the timer ID. + const UINT upperId = hash & 0xFFFF; + const UINT lowerId = vk & 0xFFFF; // The key to press can be the lower ID. + const UINT timerId = upperId << 16 | lowerId; + std::unique_lock lock{ pressedKeyMutex }; + pressedKeyDescriptors.insert({ .virtualKey = vk, .moduleName = moduleName, .action = std::move(action), .idTimer = timerId, .millisecondsToPress = milliseconds }); + } + void ClearModuleHotkeys(const std::wstring& moduleName) noexcept { Logger::trace(L"UnRegister hotkey action for {}", moduleName); - std::unique_lock lock{ mutex }; - auto it = hotkeyDescriptors.begin(); - while (it != hotkeyDescriptors.end()) { - if (it->moduleName == moduleName) + std::unique_lock lock{ mutex }; + auto it = hotkeyDescriptors.begin(); + while (it != hotkeyDescriptors.end()) { - it = hotkeyDescriptors.erase(it); + if (it->moduleName == moduleName) + { + it = hotkeyDescriptors.erase(it); + } + else + { + ++it; + } } - else + } + { + std::unique_lock lock{ pressedKeyMutex }; + auto it = pressedKeyDescriptors.begin(); + while (it != pressedKeyDescriptors.end()) { - ++it; + if (it->moduleName == moduleName) + { + it = pressedKeyDescriptors.erase(it); + } + else + { + ++it; + } } } } @@ -131,4 +257,9 @@ namespace CentralizedKeyboardHook hHook = NULL; } } + + void RegisterWindow(HWND hwnd) noexcept + { + runnerWindow = hwnd; + } } diff --git a/src/runner/centralized_kb_hook.h b/src/runner/centralized_kb_hook.h index 89ad70835..1ae36ebac 100644 --- a/src/runner/centralized_kb_hook.h +++ b/src/runner/centralized_kb_hook.h @@ -9,5 +9,7 @@ namespace CentralizedKeyboardHook void Start() noexcept; void Stop() noexcept; void SetHotkeyAction(const std::wstring& moduleName, const Hotkey& hotkey, std::function&& action) noexcept; + void AddPressedKeyAction(const std::wstring& moduleName, const DWORD vk, const UINT milliseconds, std::function&& action) noexcept; void ClearModuleHotkeys(const std::wstring& moduleName) noexcept; + void RegisterWindow(HWND hwnd) noexcept; }; diff --git a/src/runner/main.cpp b/src/runner/main.cpp index 564b48975..070028c12 100644 --- a/src/runner/main.cpp +++ b/src/runner/main.cpp @@ -32,7 +32,7 @@ #include #include #include "centralized_kb_hook.h" -#include "CentralizedHotkeys.h" +#include "centralized_hotkeys.h" #if _DEBUG && _WIN64 #include "unhandled_exception_handler.h" diff --git a/src/runner/powertoy_module.cpp b/src/runner/powertoy_module.cpp index 9ecd4a9bd..7adb523a0 100644 --- a/src/runner/powertoy_module.cpp +++ b/src/runner/powertoy_module.cpp @@ -1,7 +1,7 @@ #include "pch.h" #include "powertoy_module.h" #include "centralized_kb_hook.h" -#include "CentralizedHotkeys.h" +#include "centralized_hotkeys.h" #include #include @@ -84,4 +84,19 @@ void PowertoyModule::UpdateHotkeyEx() CentralizedHotkeys::AddHotkeyAction({ hotkey.modifiersMask, hotkey.vkCode }, { pt_module->get_key(), action }); } + + // HACK: + // Just for enabling the shortcut guide legacy behavior of pressing the Windows Key. + // This is not the sort of behavior we'd like to have generalized on other modules. + // But this was a way to bring back the long windows key behavior that the community wanted back while maintaining the separate process. + if (pt_module->keep_track_of_pressed_win_key()) + { + auto modulePtr = pt_module.get(); + auto action = [modulePtr] { + modulePtr->OnHotkeyEx(); + return false; + }; + CentralizedKeyboardHook::AddPressedKeyAction(pt_module->get_key(), VK_LWIN, pt_module->milliseconds_win_key_must_be_pressed(), action); + CentralizedKeyboardHook::AddPressedKeyAction(pt_module->get_key(), VK_RWIN, pt_module->milliseconds_win_key_must_be_pressed(), action); + } } diff --git a/src/runner/runner.vcxproj b/src/runner/runner.vcxproj index 27e40b9b8..cfd9f9e07 100644 --- a/src/runner/runner.vcxproj +++ b/src/runner/runner.vcxproj @@ -47,7 +47,7 @@ - + Create @@ -66,7 +66,7 @@ - + diff --git a/src/runner/runner.vcxproj.filters b/src/runner/runner.vcxproj.filters index 66f157402..0c2660856 100644 --- a/src/runner/runner.vcxproj.filters +++ b/src/runner/runner.vcxproj.filters @@ -39,7 +39,7 @@ Utils - + Utils @@ -84,7 +84,7 @@ Utils - + Utils diff --git a/src/runner/tray_icon.cpp b/src/runner/tray_icon.cpp index 7e94fb6a1..1c059eff7 100644 --- a/src/runner/tray_icon.cpp +++ b/src/runner/tray_icon.cpp @@ -2,7 +2,8 @@ #include "Generated files/resource.h" #include "settings_window.h" #include "tray_icon.h" -#include "CentralizedHotkeys.h" +#include "centralized_hotkeys.h" +#include "centralized_kb_hook.h" #include #include @@ -249,6 +250,7 @@ void start_tray_icon() nullptr); WINRT_VERIFY(hwnd); CentralizedHotkeys::RegisterWindow(hwnd); + CentralizedKeyboardHook::RegisterWindow(hwnd); memset(&tray_icon_data, 0, sizeof(tray_icon_data)); tray_icon_data.cbSize = sizeof(tray_icon_data); tray_icon_data.hIcon = icon; diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ShortcutGuideProperties.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ShortcutGuideProperties.cs index f70a92cb1..83db634d2 100644 --- a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ShortcutGuideProperties.cs +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ShortcutGuideProperties.cs @@ -11,6 +11,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library public ShortcutGuideProperties() { OverlayOpacity = new IntProperty(90); + UseLegacyPressWinKeyBehavior = new BoolProperty(false); PressTime = new IntProperty(900); Theme = new StringProperty("system"); DisabledApps = new StringProperty(); @@ -23,6 +24,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonPropertyName("overlay_opacity")] public IntProperty OverlayOpacity { get; set; } + [JsonPropertyName("use_legacy_press_win_key_behavior")] + public BoolProperty UseLegacyPressWinKeyBehavior { get; set; } + [JsonPropertyName("press_time")] public IntProperty PressTime { get; set; } diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ViewModels/ShortcutGuideViewModel.cs b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ViewModels/ShortcutGuideViewModel.cs index 50720a3e3..d16004c87 100644 --- a/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ViewModels/ShortcutGuideViewModel.cs +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI.Library/ViewModels/ShortcutGuideViewModel.cs @@ -52,6 +52,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels SendConfigMSG = ipcMSGCallBackFunc; _isEnabled = GeneralSettingsConfig.Enabled.ShortcutGuide; + _useLegacyPressWinKeyBehavior = Settings.Properties.UseLegacyPressWinKeyBehavior.Value; _pressTime = Settings.Properties.PressTime.Value; _opacity = Settings.Properties.OverlayOpacity.Value; _disabledApps = Settings.Properties.DisabledApps.Value; @@ -66,6 +67,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels private bool _isEnabled; private int _themeIndex; + private bool _useLegacyPressWinKeyBehavior; private int _pressTime; private int _opacity; @@ -151,6 +153,42 @@ namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels } } + public bool UseLegacyPressWinKeyBehavior + { + get + { + return _useLegacyPressWinKeyBehavior; + } + + set + { + if (_useLegacyPressWinKeyBehavior != value) + { + _useLegacyPressWinKeyBehavior = value; + Settings.Properties.UseLegacyPressWinKeyBehavior.Value = value; + NotifyPropertyChanged(); + } + } + } + + public int PressTime + { + get + { + return _pressTime; + } + + set + { + if (_pressTime != value) + { + _pressTime = value; + Settings.Properties.PressTime.Value = value; + NotifyPropertyChanged(); + } + } + } + public string DisabledApps { get diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Strings/en-us/Resources.resw index 7bedd15da..dec061d33 100644 --- a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Strings/en-us/Resources.resw +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Strings/en-us/Resources.resw @@ -615,6 +615,24 @@ Press duration before showing (ms) pressing a key in milliseconds + + How long to press the Windows key to activate the module + + + Activation method + + + Use a shortcut or press the Windows key for some time to activate + + + Customized shortcut + + + Hold down Windows key + + + In some edge cases Shortcut Guide might not function correctly when using this activation method + Appearance & behavior diff --git a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/ShortcutGuidePage.xaml b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/ShortcutGuidePage.xaml index aa9e4a4eb..dc6d986d4 100644 --- a/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/ShortcutGuidePage.xaml +++ b/src/settings-ui/Microsoft.PowerToys.Settings.UI/Views/ShortcutGuidePage.xaml @@ -12,6 +12,9 @@ + + + - + + + + + + + + + + + + + + + + + + +