diff --git a/src/modules/keyboardmanager/dll/KeyboardManager.vcxproj b/src/modules/keyboardmanager/dll/KeyboardManager.vcxproj index 86039b95e0..794810cd71 100644 --- a/src/modules/keyboardmanager/dll/KeyboardManager.vcxproj +++ b/src/modules/keyboardmanager/dll/KeyboardManager.vcxproj @@ -52,15 +52,15 @@ Create - - - {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd} - - - {6955446d-23f7-4023-9bb3-8657f904af99} - - - {8affa899-0b73-49ec-8c50-0fadda57b2fc} + + + {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd} + + + {6955446d-23f7-4023-9bb3-8657f904af99} + + + {8affa899-0b73-49ec-8c50-0fadda57b2fc} @@ -85,4 +85,4 @@ - \ No newline at end of file + diff --git a/src/modules/keyboardmanager/dll/dllmain.cpp b/src/modules/keyboardmanager/dll/dllmain.cpp index 11fe776281..499b4a9693 100644 --- a/src/modules/keyboardmanager/dll/dllmain.cpp +++ b/src/modules/keyboardmanager/dll/dllmain.cpp @@ -28,12 +28,24 @@ BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lp return TRUE; } +namespace +{ + const wchar_t JSON_KEY_PROPERTIES[] = L"properties"; + const wchar_t JSON_KEY_WIN[] = L"win"; + const wchar_t JSON_KEY_ALT[] = L"alt"; + const wchar_t JSON_KEY_CTRL[] = L"ctrl"; + const wchar_t JSON_KEY_SHIFT[] = L"shift"; + const wchar_t JSON_KEY_CODE[] = L"code"; + const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"ToggleShortcut"; +} + // Implement the PowerToy Module Interface and all the required methods. class KeyboardManager : public PowertoyModuleIface { private: // The PowerToy state. bool m_enabled = false; + bool m_active = false; // The PowerToy name that will be shown in the settings. const std::wstring app_name = GET_RESOURCE_STRING(IDS_KEYBOARDMANAGER); @@ -41,10 +53,146 @@ private: //contains the non localized key of the powertoy std::wstring app_key = KeyboardManagerConstants::ModuleName; + // Hotkey for toggling the module + Hotkey m_hotkey = { .key = 0 }; + + ULONGLONG m_lastHotkeyToggleTime = 0; + HANDLE m_hProcess = nullptr; HANDLE m_hTerminateEngineEvent = nullptr; + void refresh_process_state() + { + if (m_hProcess && WaitForSingleObject(m_hProcess, 0) != WAIT_TIMEOUT) + { + CloseHandle(m_hProcess); + m_hProcess = nullptr; + m_active = false; + } + } + + bool start_engine() + { + refresh_process_state(); + if (m_hProcess) + { + m_active = true; + return true; + } + + if (!m_hTerminateEngineEvent) + { + Logger::error(L"Cannot start keyboard manager engine because terminate event is not available"); + m_active = false; + return false; + } + + unsigned long powertoys_pid = GetCurrentProcessId(); + std::wstring executable_args = std::to_wstring(powertoys_pid); + + SHELLEXECUTEINFOW sei{ sizeof(sei) }; + sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI }; + sei.lpFile = L"KeyboardManagerEngine\\PowerToys.KeyboardManagerEngine.exe"; + sei.nShow = SW_SHOWNORMAL; + sei.lpParameters = executable_args.data(); + if (ShellExecuteExW(&sei) == false) + { + Logger::error(L"Failed to start keyboard manager engine"); + auto message = get_last_error_message(GetLastError()); + if (message.has_value()) + { + Logger::error(message.value()); + } + + m_active = false; + return false; + } + + m_hProcess = sei.hProcess; + if (m_hProcess) + { + SetPriorityClass(m_hProcess, REALTIME_PRIORITY_CLASS); + m_active = true; + return true; + } + + m_active = false; + return false; + } + + void stop_engine() + { + refresh_process_state(); + if (!m_hProcess) + { + m_active = false; + return; + } + + SetEvent(m_hTerminateEngineEvent); + auto waitResult = WaitForSingleObject(m_hProcess, 1500); + if (waitResult == WAIT_TIMEOUT) + { + TerminateProcess(m_hProcess, 0); + WaitForSingleObject(m_hProcess, 500); + } + + CloseHandle(m_hProcess); + m_hProcess = nullptr; + ResetEvent(m_hTerminateEngineEvent); + m_active = false; + } + + void parse_hotkey(PowerToysSettings::PowerToyValues& settings) + { + auto settingsObject = settings.get_raw_json(); + if (settingsObject.GetView().Size()) + { + try + { + auto jsonHotkeyObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES) + .GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT); + m_hotkey.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN); + m_hotkey.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT); + m_hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT); + m_hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL); + m_hotkey.key = static_cast(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE)); + } + catch (...) + { + Logger::error("Failed to initialize Keyboard Manager toggle shortcut"); + } + } + + if (!m_hotkey.key) + { + // Set default: Win+Shift+K + m_hotkey.win = true; + m_hotkey.shift = true; + m_hotkey.ctrl = false; + m_hotkey.alt = false; + m_hotkey.key = 'K'; + } + } + + // Load the settings file. + void init_settings() + { + try + { + // Load and parse the settings file for this PowerToy. + PowerToysSettings::PowerToyValues settings = + PowerToysSettings::PowerToyValues::load_from_settings_file(get_key()); + parse_hotkey(settings); + } + catch (std::exception&) + { + Logger::warn(L"An exception occurred while loading the settings file"); + // Error while loading from the settings file. Let default values stay as they are. + } + } + public: // Constructor KeyboardManager() @@ -65,8 +213,20 @@ public: Logger::error(message.value()); } } + + init_settings(); }; + ~KeyboardManager() + { + stop_engine(); + if (m_hTerminateEngineEvent) + { + CloseHandle(m_hTerminateEngineEvent); + m_hTerminateEngineEvent = nullptr; + } + } + // Destroy the powertoy and free memory virtual void destroy() override { @@ -117,6 +277,7 @@ public: // Parse the input JSON string. PowerToysSettings::PowerToyValues values = PowerToysSettings::PowerToyValues::from_json_string(config, get_key()); + parse_hotkey(values); // If you don't need to do any custom processing of the settings, proceed // to persists the values calling: @@ -134,33 +295,7 @@ public: m_enabled = true; // Log telemetry Trace::EnableKeyboardManager(true); - - unsigned long powertoys_pid = GetCurrentProcessId(); - std::wstring executable_args = L""; - executable_args.append(std::to_wstring(powertoys_pid)); - - SHELLEXECUTEINFOW sei{ sizeof(sei) }; - sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI }; - sei.lpFile = L"KeyboardManagerEngine\\PowerToys.KeyboardManagerEngine.exe"; - sei.nShow = SW_SHOWNORMAL; - sei.lpParameters = executable_args.data(); - if (ShellExecuteExW(&sei) == false) - { - Logger::error(L"Failed to start keyboard manager engine"); - auto message = get_last_error_message(GetLastError()); - if (message.has_value()) - { - Logger::error(message.value()); - } - } - else - { - m_hProcess = sei.hProcess; - if (m_hProcess) - { - SetPriorityClass(m_hProcess, REALTIME_PRIORITY_CLASS); - } - } + start_engine(); } // Disable the powertoy @@ -169,15 +304,7 @@ public: m_enabled = false; // Log telemetry Trace::EnableKeyboardManager(false); - - if (m_hProcess) - { - SetEvent(m_hTerminateEngineEvent); - WaitForSingleObject(m_hProcess, 1500); - - TerminateProcess(m_hProcess, 0); - m_hProcess = nullptr; - } + stop_engine(); } // Returns if the powertoys is enabled @@ -192,9 +319,54 @@ public: return false; } + // Return the invocation hotkey for toggling + virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override + { + if (m_hotkey.key) + { + if (hotkeys && buffer_size >= 1) + { + hotkeys[0] = m_hotkey; + } + return 1; + } + else + { + return 0; + } + } + + // Process the hotkey event + virtual bool on_hotkey(size_t /*hotkeyId*/) override + { + if (!m_enabled) + { + return false; + } + + constexpr ULONGLONG hotkeyToggleDebounceMs = 500; + const auto now = GetTickCount64(); + if (now - m_lastHotkeyToggleTime < hotkeyToggleDebounceMs) + { + return true; + } + m_lastHotkeyToggleTime = now; + + refresh_process_state(); + if (m_active) + { + stop_engine(); + } + else + { + start_engine(); + } + + return true; + } }; extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() { return new KeyboardManager(); -} \ No newline at end of file +} diff --git a/src/settings-ui/Settings.UI.Library/KeyboardManagerProperties.cs b/src/settings-ui/Settings.UI.Library/KeyboardManagerProperties.cs index 25a84dbb2c..1da3bfd72b 100644 --- a/src/settings-ui/Settings.UI.Library/KeyboardManagerProperties.cs +++ b/src/settings-ui/Settings.UI.Library/KeyboardManagerProperties.cs @@ -21,12 +21,17 @@ namespace Microsoft.PowerToys.Settings.UI.Library [CmdConfigureIgnoreAttribute] public GenericProperty> KeyboardConfigurations { get; set; } + public HotkeySettings DefaultToggleShortcut => new HotkeySettings(true, false, false, true, 0x4B); + public KeyboardManagerProperties() { + ToggleShortcut = DefaultToggleShortcut; KeyboardConfigurations = new GenericProperty>(new List { "default", }); ActiveConfiguration = new GenericProperty("default"); } + public HotkeySettings ToggleShortcut { get; set; } + public string ToJsonString() { return JsonSerializer.Serialize(this); diff --git a/src/settings-ui/Settings.UI.Library/KeyboardManagerSettings.cs b/src/settings-ui/Settings.UI.Library/KeyboardManagerSettings.cs index 5682d6d865..84ba811b90 100644 --- a/src/settings-ui/Settings.UI.Library/KeyboardManagerSettings.cs +++ b/src/settings-ui/Settings.UI.Library/KeyboardManagerSettings.cs @@ -2,8 +2,9 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections.Generic; using System.Text.Json.Serialization; - +using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; namespace Microsoft.PowerToys.Settings.UI.Library @@ -32,5 +33,18 @@ namespace Microsoft.PowerToys.Settings.UI.Library { return false; } + + public HotkeyAccessor[] GetAllHotkeyAccessors() + { + var hotkeyAccessors = new List + { + new HotkeyAccessor( + () => Properties.ToggleShortcut, + value => Properties.ToggleShortcut = value ?? Properties.DefaultToggleShortcut, + "Toggle_Shortcut"), + }; + + return hotkeyAccessors.ToArray(); + } } } diff --git a/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs b/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs index 36fd08ecd2..9ad0af9d68 100644 --- a/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs +++ b/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs @@ -10,7 +10,6 @@ using System.Text.Json.Serialization; using System.Threading.Tasks; using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; -using SettingsUILibrary = Settings.UI.Library; namespace Microsoft.PowerToys.Settings.UI.SerializationContext; @@ -24,6 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.SerializationContext; [JsonSerializable(typeof(FileLocksmithSettings))] [JsonSerializable(typeof(FindMyMouseSettings))] [JsonSerializable(typeof(IList))] +[JsonSerializable(typeof(KeyboardManagerSettings))] [JsonSerializable(typeof(LightSwitchSettings))] [JsonSerializable(typeof(MeasureToolSettings))] [JsonSerializable(typeof(MouseHighlighterSettings))] diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml index bfa2b2495c..b2fef9d908 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml @@ -70,6 +70,13 @@ + + + + Customize the shortcut to activate this module + + Toggle shortcut + + + Use a shortcut to toggle this module on or off (note that the Settings UI will not update) + Paste as plain text directly diff --git a/src/settings-ui/Settings.UI/ViewModels/KeyboardManagerViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/KeyboardManagerViewModel.cs index d7bf9862bc..0c142cf6c0 100644 --- a/src/settings-ui/Settings.UI/ViewModels/KeyboardManagerViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/KeyboardManagerViewModel.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using System.Windows.Input; @@ -18,18 +19,20 @@ using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands; +using Microsoft.PowerToys.Settings.UI.SerializationContext; using Microsoft.PowerToys.Settings.Utilities; using Microsoft.Win32; namespace Microsoft.PowerToys.Settings.UI.ViewModels { - public partial class KeyboardManagerViewModel : Observable + public partial class KeyboardManagerViewModel : PageViewModelBase { private GeneralSettings GeneralSettingsConfig { get; set; } private readonly SettingsUtils _settingsUtils; - private const string PowerToyName = KeyboardManagerSettings.ModuleName; + protected override string ModuleName => KeyboardManagerSettings.ModuleName; + private const string JsonFileType = ".json"; // Default editor path. Can be removed once the new WinUI3 editor is released. @@ -74,15 +77,15 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels _settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils)); - if (_settingsUtils.SettingsExists(PowerToyName)) + if (_settingsUtils.SettingsExists(ModuleName)) { try { - Settings = _settingsUtils.GetSettingsOrDefault(PowerToyName); + Settings = _settingsUtils.GetSettingsOrDefault(ModuleName); } catch (Exception e) { - Logger.LogError($"Exception encountered while reading {PowerToyName} settings.", e); + Logger.LogError($"Exception encountered while reading {ModuleName} settings.", e); #if DEBUG if (e is ArgumentException || e is ArgumentNullException || e is PathTooLongException) { @@ -100,7 +103,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels else { Settings = new KeyboardManagerSettings(); - _settingsUtils.SaveSettings(Settings.ToJsonString(), PowerToyName); + _settingsUtils.SaveSettings(Settings.ToJsonString(), ModuleName); } } @@ -174,6 +177,41 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } } + public override Dictionary GetAllHotkeySettings() + { + var hotkeysDict = new Dictionary + { + [ModuleName] = [ToggleShortcut], + }; + + return hotkeysDict; + } + + public HotkeySettings ToggleShortcut + { + get => Settings.Properties.ToggleShortcut; + set + { + if (Settings.Properties.ToggleShortcut != value) + { + Settings.Properties.ToggleShortcut = value ?? Settings.Properties.DefaultToggleShortcut; + OnPropertyChanged(nameof(ToggleShortcut)); + NotifySettingsChanged(); + } + } + } + + private void NotifySettingsChanged() + { + // Using InvariantCulture as this is an IPC message + SendConfigMSG( + string.Format( + CultureInfo.InvariantCulture, + "{{ \"powertoys\": {{ \"{0}\": {1} }} }}", + ModuleName, + JsonSerializer.Serialize(Settings, SourceGenerationContextContext.Default.KeyboardManagerSettings))); + } + public static List CombineShortcutLists(List globalShortcutList, List appSpecificShortcutList) { string allAppsDescription = "All Apps"; @@ -256,13 +294,13 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels { if (editor != null && editor.HasExited) { - Logger.LogInfo($"Previous instance of {PowerToyName} editor exited"); + Logger.LogInfo($"Previous instance of {ModuleName} editor exited"); editor = null; } if (editor != null) { - Logger.LogInfo($"The {PowerToyName} editor instance {editor.Id} exists. Bringing the process to the front"); + Logger.LogInfo($"The {ModuleName} editor instance {editor.Id} exists. Bringing the process to the front"); BringProcessToFront(editor); return; } @@ -298,14 +336,14 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels } string path = Path.Combine(Environment.CurrentDirectory, editorPath); - Logger.LogInfo($"Starting {PowerToyName} editor from {path}"); + Logger.LogInfo($"Starting {ModuleName} editor from {path}"); // InvariantCulture: type represents the KeyboardManagerEditorType enum value editor = Process.Start(path, $"{type.ToString(CultureInfo.InvariantCulture)} {Environment.ProcessId}"); } catch (Exception e) { - Logger.LogError($"Exception encountered when opening an {PowerToyName} editor", e); + Logger.LogError($"Exception encountered when opening an {ModuleName} editor", e); } } @@ -338,7 +376,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels { while (!readSuccessfully && !ts.IsCancellationRequested) { - profileExists = _settingsUtils.SettingsExists(PowerToyName, fileName); + profileExists = _settingsUtils.SettingsExists(ModuleName, fileName); if (!profileExists) { break; @@ -347,12 +385,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels { try { - _profile = _settingsUtils.GetSettingsOrDefault(PowerToyName, fileName); + _profile = _settingsUtils.GetSettingsOrDefault(ModuleName, fileName); readSuccessfully = true; } catch (Exception e) { - Logger.LogError($"Exception encountered when reading {PowerToyName} settings", e); + Logger.LogError($"Exception encountered when reading {ModuleName} settings", e); } } @@ -379,23 +417,23 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels if (!completedInTime) { - Logger.LogError($"Timeout encountered when loading {PowerToyName} profile"); + Logger.LogError($"Timeout encountered when loading {ModuleName} profile"); } } catch (Exception e) { // Failed to load the configuration. - Logger.LogError($"Exception encountered when loading {PowerToyName} profile", e); + Logger.LogError($"Exception encountered when loading {ModuleName} profile", e); success = false; } if (!profileExists) { - Logger.LogInfo($"Couldn't load {PowerToyName} profile because it doesn't exist"); + Logger.LogInfo($"Couldn't load {ModuleName} profile because it doesn't exist"); } else if (!success) { - Logger.LogError($"Couldn't load {PowerToyName} profile"); + Logger.LogError($"Couldn't load {ModuleName} profile"); } return success;