From 3d69785ca4e25c6510666a7fe9fd07e36e005bab Mon Sep 17 00:00:00 2001 From: Kai Tao <69313318+vanzue@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:25:11 +0800 Subject: [PATCH] =?UTF-8?q?Cmdpal=20Powertoys=20Extension:=20Support=20mou?= =?UTF-8?q?se=20without=20borders=20easy=20mouse=20=E2=80=A6=20(#45350)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description You don't have to go to powertoys settings to * toggle the mouse move from machine to another * you can trigger reconnect when connection lost from cmdpal * You can toggle whether kbm is turning on or not from cmdpal ## Summary of the Pull Request Add several missing cmd to powretoys cmdpal extension ## PR Checklist - [ ] Closes: #xxx - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed https://github.com/user-attachments/assets/9ea019f7-988b-4542-afc5-a80f0fc99ef8 For the kbm toggle, when it's off, kbm will not map anything, if it's on, kbm will take effect For the mouse without borders, add these two functionality as command image And verified for the two command, works as expected --- .github/actions/spell-check/expect.txt | 1 + .../PowerToysSetupVNext/KeyboardManager.wxs | 13 ++ .../generateAllFileComponents.ps1 | 4 + src/common/interop/Constants.cpp | 18 +++ src/common/interop/Constants.h | 5 + src/common/interop/Constants.idl | 5 + src/common/interop/shared_constants.h | 7 ++ .../MouseWithoutBorders/App/Class/Program.cs | 1 + .../App/Core/CommandEventHandler.cs | 114 ++++++++++++++++++ .../App/MouseWithoutBorders.csproj | 1 + .../KeyboardManagerListeningOff.svg | 18 +++ .../KeyboardManagerListeningOn.svg | 18 +++ .../ToggleKeyboardManagerListeningCommand.cs | 24 ++++ .../MWBReconnectCommand.cs | 35 ++++++ .../ToggleMWBEasyMouseCommand.cs | 35 ++++++ .../Helpers/KeyboardManagerStateService.cs | 80 ++++++++++++ .../Helpers/PowerToysResourcesHelper.cs | 10 ++ .../Microsoft.CmdPal.Ext.PowerToys.csproj | 18 +++ .../KeyboardManagerModuleCommandProvider.cs | 25 +++- ...ouseWithoutBordersModuleCommandProvider.cs | 16 +++ .../Pages/PowerToysExtensionPage.cs | 2 +- .../Pages/PowerToysListPage.cs | 9 +- .../PowerToysCommandsProvider.cs | 2 +- .../PowerToysExtensionCommandsProvider.cs | 10 +- .../Properties/Resources.Designer.cs | 36 ++++++ .../Properties/Resources.resx | 25 ++++ .../KeyboardManagerEngine/main.cpp | 3 +- src/modules/keyboardmanager/dll/dllmain.cpp | 63 ++++++++-- 28 files changed, 574 insertions(+), 24 deletions(-) create mode 100644 src/modules/MouseWithoutBorders/App/Core/CommandEventHandler.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Assets/KeyboardManager/KeyboardManagerListeningOff.svg create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Assets/KeyboardManager/KeyboardManagerListeningOn.svg create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/KeyboardManager/ToggleKeyboardManagerListeningCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseWithoutBorders/MWBReconnectCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseWithoutBorders/ToggleMWBEasyMouseCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/KeyboardManagerStateService.cs diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 736bfb17f4..0d4f2a7e3e 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -797,6 +797,7 @@ MAPPEDTOSAMEKEY MAPTOSAMESHORTCUT MAPVK MARKDOWNPREVIEWHANDLERCPP +MAXDWORD MAXSHORTCUTSIZE maxversiontested MBM diff --git a/installer/PowerToysSetupVNext/KeyboardManager.wxs b/installer/PowerToysSetupVNext/KeyboardManager.wxs index 9aa9fc9472..5585a76c9e 100644 --- a/installer/PowerToysSetupVNext/KeyboardManager.wxs +++ b/installer/PowerToysSetupVNext/KeyboardManager.wxs @@ -2,7 +2,19 @@ + + + + + + + + + + + + @@ -44,6 +56,7 @@ + diff --git a/installer/PowerToysSetupVNext/generateAllFileComponents.ps1 b/installer/PowerToysSetupVNext/generateAllFileComponents.ps1 index 6724d95170..001e021019 100644 --- a/installer/PowerToysSetupVNext/generateAllFileComponents.ps1 +++ b/installer/PowerToysSetupVNext/generateAllFileComponents.ps1 @@ -172,6 +172,10 @@ Generate-FileComponents -fileListName "HostsAssetsFiles" -wxsFilePath $PSScriptR Generate-FileList -fileDepsJson "" -fileListName ImageResizerAssetsFiles -wxsFilePath $PSScriptRoot\ImageResizer.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\ImageResizer" Generate-FileComponents -fileListName "ImageResizerAssetsFiles" -wxsFilePath $PSScriptRoot\ImageResizer.wxs +#KeyboardManager +Generate-FileList -fileDepsJson "" -fileListName KeyboardManagerAssetsFiles -wxsFilePath $PSScriptRoot\KeyboardManager.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\KeyboardManager" +Generate-FileComponents -fileListName "KeyboardManagerAssetsFiles" -wxsFilePath $PSScriptRoot\KeyboardManager.wxs + # Light Switch Service Generate-FileList -fileDepsJson "" -fileListName LightSwitchFiles -wxsFilePath $PSScriptRoot\LightSwitch.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\LightSwitchService" Generate-FileComponents -fileListName "LightSwitchFiles" -wxsFilePath $PSScriptRoot\LightSwitch.wxs diff --git a/src/common/interop/Constants.cpp b/src/common/interop/Constants.cpp index a03bd2b4c8..8a2c672a88 100644 --- a/src/common/interop/Constants.cpp +++ b/src/common/interop/Constants.cpp @@ -287,8 +287,26 @@ namespace winrt::PowerToys::Interop::implementation { return CommonSharedConstants::POWER_DISPLAY_TERMINATE_APP_MESSAGE; } + hstring Constants::MWBToggleEasyMouseEvent() + { + return CommonSharedConstants::MWB_TOGGLE_EASY_MOUSE_EVENT; + } + hstring Constants::MWBReconnectEvent() + { + return CommonSharedConstants::MWB_RECONNECT_EVENT; + } + hstring Constants::OpenNewKeyboardManagerEvent() { return CommonSharedConstants::OPEN_NEW_KEYBOARD_MANAGER_EVENT; } + hstring Constants::ToggleKeyboardManagerActiveEvent() + { + return CommonSharedConstants::TOGGLE_KEYBOARD_MANAGER_ACTIVE_EVENT; + } + hstring Constants::KeyboardManagerEngineInstanceMutex() + { + return CommonSharedConstants::KEYBOARD_MANAGER_ENGINE_INSTANCE_MUTEX; + } } + diff --git a/src/common/interop/Constants.h b/src/common/interop/Constants.h index e4587f561b..575c7673e2 100644 --- a/src/common/interop/Constants.h +++ b/src/common/interop/Constants.h @@ -75,7 +75,11 @@ namespace winrt::PowerToys::Interop::implementation static hstring PowerDisplayToggleMessage(); static hstring PowerDisplayApplyProfileMessage(); static hstring PowerDisplayTerminateAppMessage(); + static hstring MWBToggleEasyMouseEvent(); + static hstring MWBReconnectEvent(); static hstring OpenNewKeyboardManagerEvent(); + static hstring ToggleKeyboardManagerActiveEvent(); + static hstring KeyboardManagerEngineInstanceMutex(); }; } @@ -85,3 +89,4 @@ namespace winrt::PowerToys::Interop::factory_implementation { }; } + diff --git a/src/common/interop/Constants.idl b/src/common/interop/Constants.idl index 58b713b9ca..91883a050e 100644 --- a/src/common/interop/Constants.idl +++ b/src/common/interop/Constants.idl @@ -72,7 +72,12 @@ namespace PowerToys static String PowerDisplayToggleMessage(); static String PowerDisplayApplyProfileMessage(); static String PowerDisplayTerminateAppMessage(); + static String MWBToggleEasyMouseEvent(); + static String MWBReconnectEvent(); static String OpenNewKeyboardManagerEvent(); + static String ToggleKeyboardManagerActiveEvent(); + static String KeyboardManagerEngineInstanceMutex(); } } } + diff --git a/src/common/interop/shared_constants.h b/src/common/interop/shared_constants.h index 9788e2bf35..1166e1e305 100644 --- a/src/common/interop/shared_constants.h +++ b/src/common/interop/shared_constants.h @@ -172,11 +172,18 @@ namespace CommonSharedConstants // Path to events used by Keyboard Manager const wchar_t OPEN_NEW_KEYBOARD_MANAGER_EVENT[] = L"Local\\PowerToysOpenNewKeyboardManagerEvent-9c1d2e3f-4b5a-6c7d-8e9f-0a1b2c3d4e5f"; + const wchar_t TOGGLE_KEYBOARD_MANAGER_ACTIVE_EVENT[] = L"Local\\PowerToysToggleKeyboardManagerActiveEvent-7f3a1d5c-2e94-4ff4-8b6a-90fd2bc4d2a7"; + const wchar_t KEYBOARD_MANAGER_ENGINE_INSTANCE_MUTEX[] = L"Local\\PowerToys_KBMEngine_InstanceMutex"; // used from quick access window const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a"; const wchar_t CMDPAL_EXIT_EVENT[] = L"Local\\PowerToysCmdPal-ExitEvent-eb73f6be-3f22-4b36-aee3-62924ba40bfd"; + // Path to the events used by MouseWithoutBorders + const wchar_t MWB_TOGGLE_EASY_MOUSE_EVENT[] = L"Local\\PowerToysMWB-ToggleEasyMouseEvent-a9c8d7b6-e5f4-3c2a-1b0d-9e8f7a6b5c4d"; + const wchar_t MWB_RECONNECT_EVENT[] = L"Local\\PowerToysMWB-ReconnectEvent-b8d7c6a5-f4e3-2b1c-0a9d-8e7f6a5b4c3d"; + // Max DWORD for key code to disable keys. const DWORD VK_DISABLED = 0x100; } + diff --git a/src/modules/MouseWithoutBorders/App/Class/Program.cs b/src/modules/MouseWithoutBorders/App/Class/Program.cs index 23513e1515..144007e92f 100644 --- a/src/modules/MouseWithoutBorders/App/Class/Program.cs +++ b/src/modules/MouseWithoutBorders/App/Class/Program.cs @@ -229,6 +229,7 @@ namespace MouseWithoutBorders.Class if (!Common.RunOnLogonDesktop) { StartSettingSyncThread(); + CommandEventHandler.StartListening(); } Application.EnableVisualStyles(); diff --git a/src/modules/MouseWithoutBorders/App/Core/CommandEventHandler.cs b/src/modules/MouseWithoutBorders/App/Core/CommandEventHandler.cs new file mode 100644 index 0000000000..0429936637 --- /dev/null +++ b/src/modules/MouseWithoutBorders/App/Core/CommandEventHandler.cs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation +// 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; +using System.Threading; + +using MouseWithoutBorders.Class; +using PowerToys.Interop; + +namespace MouseWithoutBorders.Core +{ + /// + /// Handles command events from external sources (e.g., Command Palette). + /// Uses named events for inter-process communication, following the same pattern as other PowerToys modules. + /// + internal static class CommandEventHandler + { + private static CancellationTokenSource _cancellationTokenSource; + + /// + /// Starts listening for command events on background threads. + /// + public static void StartListening() + { + _cancellationTokenSource = new CancellationTokenSource(); + CancellationToken exitToken = _cancellationTokenSource.Token; + + // Start listener for Toggle Easy Mouse event + StartEventListener(Constants.MWBToggleEasyMouseEvent(), ToggleEasyMouse, exitToken); + + // Start listener for Reconnect event + StartEventListener(Constants.MWBReconnectEvent(), Reconnect, exitToken); + } + + /// + /// Stops listening for command events. + /// + public static void StopListening() + { + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = null; + } + + private static void StartEventListener(string eventName, Action callback, CancellationToken cancel) + { + new System.Threading.Thread(() => + { + try + { + using var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, eventName); + WaitHandle[] waitHandles = new WaitHandle[] { cancel.WaitHandle, eventHandle }; + + while (!cancel.IsCancellationRequested) + { + int result = WaitHandle.WaitAny(waitHandles); + if (result == 1) + { + // Execute callback on UI thread using Common.DoSomethingInUIThread + Common.DoSomethingInUIThread(callback); + } + else + { + // Cancellation requested + return; + } + } + } + catch (Exception ex) + { + Logger.Log($"Error in event listener for {eventName}: {ex.Message}"); + } + }) + { IsBackground = true, Name = $"MWB-{eventName}-Listener" }.Start(); + } + + /// + /// Toggles Easy Mouse between Enabled and Disabled states. + /// This is the same logic used by the hotkey handler. + /// + public static void ToggleEasyMouse() + { + if (Common.RunOnLogonDesktop || Common.RunOnScrSaverDesktop) + { + return; + } + + EasyMouseOption easyMouseOption = (EasyMouseOption)Setting.Values.EasyMouse; + + if (easyMouseOption is EasyMouseOption.Disable or EasyMouseOption.Enable) + { + Setting.Values.EasyMouse = (int)(easyMouseOption == EasyMouseOption.Disable ? EasyMouseOption.Enable : EasyMouseOption.Disable); + + Common.ShowToolTip($"Easy Mouse has been toggled to [{(EasyMouseOption)Setting.Values.EasyMouse}].", 3000); + + Logger.Log($"Easy Mouse toggled to {(EasyMouseOption)Setting.Values.EasyMouse} via command event."); + } + } + + /// + /// Initiates a reconnection attempt to all machines. + /// This is the same logic used by the hotkey handler. + /// + public static void Reconnect() + { + Common.ShowToolTip("Reconnecting...", 2000); + Common.LastReconnectByHotKeyTime = Common.GetTick(); + InitAndCleanup.PleaseReopenSocket = InitAndCleanup.REOPEN_WHEN_HOTKEY; + + Logger.Log("Reconnect initiated via command event."); + } + } +} diff --git a/src/modules/MouseWithoutBorders/App/MouseWithoutBorders.csproj b/src/modules/MouseWithoutBorders/App/MouseWithoutBorders.csproj index acc66deea6..675e927334 100644 --- a/src/modules/MouseWithoutBorders/App/MouseWithoutBorders.csproj +++ b/src/modules/MouseWithoutBorders/App/MouseWithoutBorders.csproj @@ -218,6 +218,7 @@ + diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Assets/KeyboardManager/KeyboardManagerListeningOff.svg b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Assets/KeyboardManager/KeyboardManagerListeningOff.svg new file mode 100644 index 0000000000..c87dc28133 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Assets/KeyboardManager/KeyboardManagerListeningOff.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Assets/KeyboardManager/KeyboardManagerListeningOn.svg b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Assets/KeyboardManager/KeyboardManagerListeningOn.svg new file mode 100644 index 0000000000..de3512d9af --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Assets/KeyboardManager/KeyboardManagerListeningOn.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/KeyboardManager/ToggleKeyboardManagerListeningCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/KeyboardManager/ToggleKeyboardManagerListeningCommand.cs new file mode 100644 index 0000000000..bbb2e45910 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/KeyboardManager/ToggleKeyboardManagerListeningCommand.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToysExtension.Helpers; +using PowerToysExtension.Properties; + +namespace PowerToysExtension.Commands; + +internal sealed partial class ToggleKeyboardManagerListeningCommand : InvokableCommand +{ + public ToggleKeyboardManagerListeningCommand() + { + Name = "Toggle Keyboard Manager active state"; + } + + public override CommandResult Invoke() + { + return KeyboardManagerStateService.TryToggleListening() + ? CommandResult.KeepOpen() + : CommandResult.ShowToast(Resources.ResourceManager.GetString("KeyboardManager_ToggleListening_Error", Resources.Culture) ?? "Keyboard Manager is unavailable. Try enabling it in PowerToys settings."); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseWithoutBorders/MWBReconnectCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseWithoutBorders/MWBReconnectCommand.cs new file mode 100644 index 0000000000..cd23373e4f --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseWithoutBorders/MWBReconnectCommand.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation +// 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; +using System.Threading; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToys.Interop; + +namespace PowerToysExtension.Commands; + +/// +/// Triggers a reconnection attempt in Mouse Without Borders via the shared event. +/// +internal sealed partial class MWBReconnectCommand : InvokableCommand +{ + public MWBReconnectCommand() + { + Name = "Mouse Without Borders: Reconnect"; + } + + public override CommandResult Invoke() + { + try + { + using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.MWBReconnectEvent()); + evt.Set(); + return CommandResult.Dismiss(); + } + catch (Exception ex) + { + return CommandResult.ShowToast($"Failed to reconnect Mouse Without Borders: {ex.Message}"); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseWithoutBorders/ToggleMWBEasyMouseCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseWithoutBorders/ToggleMWBEasyMouseCommand.cs new file mode 100644 index 0000000000..845d6c6569 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/MouseWithoutBorders/ToggleMWBEasyMouseCommand.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation +// 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; +using System.Threading; +using Microsoft.CommandPalette.Extensions.Toolkit; +using PowerToys.Interop; + +namespace PowerToysExtension.Commands; + +/// +/// Toggles Easy Mouse feature in Mouse Without Borders via the shared event. +/// +internal sealed partial class ToggleMWBEasyMouseCommand : InvokableCommand +{ + public ToggleMWBEasyMouseCommand() + { + Name = "Mouse Without Borders: Toggle Easy Mouse"; + } + + public override CommandResult Invoke() + { + try + { + using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.MWBToggleEasyMouseEvent()); + evt.Set(); + return CommandResult.Dismiss(); + } + catch (Exception ex) + { + return CommandResult.ShowToast($"Failed to toggle Easy Mouse: {ex.Message}"); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/KeyboardManagerStateService.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/KeyboardManagerStateService.cs new file mode 100644 index 0000000000..c2c139b60e --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/KeyboardManagerStateService.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation +// 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; +using System.Threading; +using PowerToys.Interop; + +namespace PowerToysExtension.Helpers; + +internal static class KeyboardManagerStateService +{ + private static readonly object Sync = new(); + private static readonly Timer PollingTimer; + private static bool _lastKnownListeningState = IsListening(); + + internal static event Action? StatusChanged; + + static KeyboardManagerStateService() + { + PollingTimer = new Timer( + static _ => PollStatus(), + null, + TimeSpan.FromMilliseconds(500), + TimeSpan.FromMilliseconds(500)); + } + + internal static bool IsListening() + { + try + { + if (Mutex.TryOpenExisting(Constants.KeyboardManagerEngineInstanceMutex(), out var mutex)) + { + mutex.Dispose(); + return true; + } + } + catch + { + // The engine mutex is best-effort state. Treat failures as not listening. + } + + return false; + } + + internal static bool TryToggleListening() + { + try + { + using var evt = EventWaitHandle.OpenExisting(Constants.ToggleKeyboardManagerActiveEvent()); + var signaled = evt.Set(); + PollStatus(); + return signaled; + } + catch + { + return false; + } + } + + private static void PollStatus() + { + var isListening = IsListening(); + var raiseChanged = false; + + lock (Sync) + { + if (isListening != _lastKnownListeningState) + { + _lastKnownListeningState = isListening; + raiseChanged = true; + } + } + + if (raiseChanged) + { + StatusChanged?.Invoke(); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/PowerToysResourcesHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/PowerToysResourcesHelper.cs index 91dd3f05b1..5427263228 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/PowerToysResourcesHelper.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Helpers/PowerToysResourcesHelper.cs @@ -9,11 +9,21 @@ namespace PowerToysExtension.Helpers; internal static class PowerToysResourcesHelper { + private const string AssetsRoot = "Assets\\"; private const string SettingsIconRoot = "WinUI3Apps\\Assets\\Settings\\Icons\\"; internal static IconInfo IconFromSettingsIcon(string fileName) => IconHelpers.FromRelativePath($"{SettingsIconRoot}{fileName}"); + internal static IconInfo KeyboardManagerListeningIcon(bool isListening) => IconHelpers.FromRelativePath( + isListening + ? $"{AssetsRoot}KeyboardManager\\KeyboardManagerListeningOn.svg" + : $"{AssetsRoot}KeyboardManager\\KeyboardManagerListeningOff.svg"); + +#if DEBUG + public static IconInfo ProviderIcon() => IconFromSettingsIcon("PowerToys.dark.png"); +#else public static IconInfo ProviderIcon() => IconFromSettingsIcon("PowerToys.png"); +#endif public static IconInfo ModuleIcon(this SettingsWindow module) { diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Microsoft.CmdPal.Ext.PowerToys.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Microsoft.CmdPal.Ext.PowerToys.csproj index 8479699538..97ac98c992 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Microsoft.CmdPal.Ext.PowerToys.csproj +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Microsoft.CmdPal.Ext.PowerToys.csproj @@ -30,6 +30,10 @@ PreserveNewest + + + PreserveNewest + @@ -77,6 +81,20 @@ + + + + + + + + PreserveNewest + + + PreserveNewest + + + true diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/KeyboardManagerModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/KeyboardManagerModuleCommandProvider.cs index eebf4289c7..e1502f8937 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/KeyboardManagerModuleCommandProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/KeyboardManagerModuleCommandProvider.cs @@ -18,8 +18,22 @@ internal sealed class KeyboardManagerModuleCommandProvider : ModuleCommandProvid { public override IEnumerable BuildCommands() { - var title = SettingsWindow.KBM.ModuleDisplayName(); - var icon = SettingsWindow.KBM.ModuleIcon(); + var module = SettingsWindow.KBM; + var title = module.ModuleDisplayName(); + var icon = module.ModuleIcon(); + + if (ModuleEnablementService.IsModuleEnabled(module)) + { + var isListening = KeyboardManagerStateService.IsListening(); + yield return new ListItem(new ToggleKeyboardManagerListeningCommand() { Id = "com.microsoft.powertoys.keyboardManager.toggleListening" }) + { + Title = GetResourceString("KeyboardManager_ToggleListening_Title", "Keyboard Manager: Toggle active state"), + Subtitle = isListening + ? GetResourceString("KeyboardManager_ToggleListening_On_Subtitle", "Keyboard Manager is active. Invoke to stop listening.") + : GetResourceString("KeyboardManager_ToggleListening_Off_Subtitle", "Keyboard Manager is paused. Invoke to start listening."), + Icon = PowerToysResourcesHelper.KeyboardManagerListeningIcon(isListening), + }; + } if (IsUseNewEditorEnabled()) { @@ -31,7 +45,7 @@ internal sealed class KeyboardManagerModuleCommandProvider : ModuleCommandProvid }; } - yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.KBM, title) { Id = "com.microsoft.powertoys.keyboardManager.openSettings" }) + yield return new ListItem(new OpenInSettingsCommand(module, title) { Id = "com.microsoft.powertoys.keyboardManager.openSettings" }) { Title = title, Subtitle = Resources.KeyboardManager_Settings_Subtitle, @@ -39,6 +53,11 @@ internal sealed class KeyboardManagerModuleCommandProvider : ModuleCommandProvid }; } + private static string GetResourceString(string resourceName, string fallback) + { + return Resources.ResourceManager.GetString(resourceName, Resources.Culture) ?? fallback; + } + private static bool IsUseNewEditorEnabled() { try diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/MouseWithoutBordersModuleCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/MouseWithoutBordersModuleCommandProvider.cs index 9198651351..d07f24028e 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/MouseWithoutBordersModuleCommandProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Modules/MouseWithoutBordersModuleCommandProvider.cs @@ -17,6 +17,8 @@ internal sealed class MouseWithoutBordersModuleCommandProvider : ModuleCommandPr { var title = SettingsWindow.MouseWithoutBorders.ModuleDisplayName(); var icon = SettingsWindow.MouseWithoutBorders.ModuleIcon(); + var easyMouseIcon = new IconInfo("\uE962"); + var reconnectIcon = new IconInfo("\uE72C"); yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.MouseWithoutBorders, title) { Id = "com.microsoft.powertoys.mouseWithoutBorders.openSettings" }) { @@ -24,5 +26,19 @@ internal sealed class MouseWithoutBordersModuleCommandProvider : ModuleCommandPr Subtitle = Resources.MouseWithoutBorders_Settings_Subtitle, Icon = icon, }; + + yield return new ListItem(new ToggleMWBEasyMouseCommand()) + { + Title = Resources.MouseWithoutBorders_ToggleEasyMouse_Title, + Subtitle = Resources.MouseWithoutBorders_ToggleEasyMouse_Subtitle, + Icon = easyMouseIcon, + }; + + yield return new ListItem(new MWBReconnectCommand()) + { + Title = Resources.MouseWithoutBorders_Reconnect_Title, + Subtitle = Resources.MouseWithoutBorders_Reconnect_Subtitle, + Icon = reconnectIcon, + }; } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/PowerToysExtensionPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/PowerToysExtensionPage.cs index 7082169629..2b3042a35d 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/PowerToysExtensionPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/PowerToysExtensionPage.cs @@ -14,7 +14,7 @@ internal sealed partial class PowerToysExtensionPage : ListPage { public PowerToysExtensionPage() { - Icon = Helpers.PowerToysResourcesHelper.IconFromSettingsIcon("PowerToys.png"); + Icon = Helpers.PowerToysResourcesHelper.ProviderIcon(); Title = Resources.PowerToys_DisplayName; Name = Resources.PowerToysExtension_CommandsName; } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/PowerToysListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/PowerToysListPage.cs index b91c8e9a5c..c1d0ea51b3 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/PowerToysListPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Pages/PowerToysListPage.cs @@ -15,20 +15,21 @@ internal sealed partial class PowerToysListPage : ListPage public PowerToysListPage() { - Icon = PowerToysResourcesHelper.IconFromSettingsIcon("PowerToys.png"); + Icon = PowerToysResourcesHelper.ProviderIcon(); Name = Title = Resources.PowerToys_DisplayName; Id = "com.microsoft.cmdpal.powertoys"; - SettingsChangeNotifier.SettingsChanged += OnSettingsChanged; + SettingsChangeNotifier.SettingsChanged += OnItemsChanged; + KeyboardManagerStateService.StatusChanged += OnItemsChanged; _empty = new CommandItem() { - Icon = PowerToysResourcesHelper.IconFromSettingsIcon("PowerToys.png"), + Icon = PowerToysResourcesHelper.ProviderIcon(), Title = Resources.PowerToys_NoMatchingModule, Subtitle = SearchText, }; EmptyContent = _empty; } - private void OnSettingsChanged() + private void OnItemsChanged() { RaiseItemsChanged(0); } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysCommandsProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysCommandsProvider.cs index f3d22c7e5a..ed38151987 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysCommandsProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysCommandsProvider.cs @@ -16,7 +16,7 @@ public sealed partial class PowerToysCommandsProvider : CommandProvider public PowerToysCommandsProvider() { DisplayName = Resources.PowerToys_DisplayName; - Icon = PowerToysResourcesHelper.IconFromSettingsIcon("PowerToys.png"); + Icon = PowerToysResourcesHelper.ProviderIcon(); } public override ICommandItem[] TopLevelCommands() => diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysExtensionCommandsProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysExtensionCommandsProvider.cs index f0dd1fc829..4acac7260b 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysExtensionCommandsProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/PowerToysExtensionCommandsProvider.cs @@ -17,7 +17,7 @@ public partial class PowerToysExtensionCommandsProvider : CommandProvider public PowerToysExtensionCommandsProvider() { DisplayName = Resources.PowerToys_DisplayName; - Icon = PowerToysResourcesHelper.IconFromSettingsIcon("PowerToys.png"); + Icon = PowerToysResourcesHelper.ProviderIcon(); _commands = [ new CommandItem(new Pages.PowerToysListPage()) { @@ -25,6 +25,9 @@ public partial class PowerToysExtensionCommandsProvider : CommandProvider Subtitle = Resources.PowerToys_Subtitle, }, ]; + + SettingsChangeNotifier.SettingsChanged += RaiseModuleItemsChanged; + KeyboardManagerStateService.StatusChanged += RaiseModuleItemsChanged; } public override ICommandItem[] TopLevelCommands() @@ -63,4 +66,9 @@ public partial class PowerToysExtensionCommandsProvider : CommandProvider return null; } + + private void RaiseModuleItemsChanged() + { + RaiseItemsChanged(); + } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Properties/Resources.Designer.cs index d821ce85b1..458ca5b7db 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Properties/Resources.Designer.cs @@ -1014,6 +1014,42 @@ namespace PowerToysExtension.Properties { } } + /// + /// Looks up a localized string similar to Toggle Easy Mouse. + /// + internal static string MouseWithoutBorders_ToggleEasyMouse_Title { + get { + return ResourceManager.GetString("MouseWithoutBorders_ToggleEasyMouse_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Toggle easy mouse switching between machines. + /// + internal static string MouseWithoutBorders_ToggleEasyMouse_Subtitle { + get { + return ResourceManager.GetString("MouseWithoutBorders_ToggleEasyMouse_Subtitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Reconnect. + /// + internal static string MouseWithoutBorders_Reconnect_Title { + get { + return ResourceManager.GetString("MouseWithoutBorders_Reconnect_Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Reconnect to other machines. + /// + internal static string MouseWithoutBorders_Reconnect_Subtitle { + get { + return ResourceManager.GetString("MouseWithoutBorders_Reconnect_Subtitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Open New+ settings. /// diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Properties/Resources.resx index 6dd758d219..77887afeb1 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Properties/Resources.resx +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Properties/Resources.resx @@ -414,6 +414,18 @@ Open the Keyboard Manager remap editor + + Keyboard Manager: Toggle active state + + + Keyboard Manager is active. Invoke to stop listening. + + + Keyboard Manager is paused. Invoke to start listening. + + + Keyboard Manager is unavailable. Try enabling it in PowerToys settings. + Light Switch: Toggle theme @@ -462,6 +474,18 @@ Open Mouse Without Borders settings + + Toggle Easy Mouse + + + Mouse Without Borders: Toggle Easy Mouse feature on/off + + + Reconnect + + + Mouse Without Borders: Reconnect to all machines + Open New+ settings @@ -640,3 +664,4 @@ N/A + diff --git a/src/modules/keyboardmanager/KeyboardManagerEngine/main.cpp b/src/modules/keyboardmanager/KeyboardManagerEngine/main.cpp index df48555df5..0d5cfc0955 100644 --- a/src/modules/keyboardmanager/KeyboardManagerEngine/main.cpp +++ b/src/modules/keyboardmanager/KeyboardManagerEngine/main.cpp @@ -12,7 +12,7 @@ #include #include -const std::wstring instanceMutexName = L"Local\\PowerToys_KBMEngine_InstanceMutex"; +const std::wstring instanceMutexName = CommonSharedConstants::KEYBOARD_MANAGER_ENGINE_INSTANCE_MUTEX; int WINAPI wWinMain(_In_ HINSTANCE /*hInstance*/, _In_opt_ HINSTANCE /*hPrevInstance*/, @@ -90,3 +90,4 @@ int WINAPI wWinMain(_In_ HINSTANCE /*hInstance*/, return 0; } + diff --git a/src/modules/keyboardmanager/dll/dllmain.cpp b/src/modules/keyboardmanager/dll/dllmain.cpp index 61d5e9b0f4..9756dbb103 100644 --- a/src/modules/keyboardmanager/dll/dllmain.cpp +++ b/src/modules/keyboardmanager/dll/dllmain.cpp @@ -73,6 +73,7 @@ private: HANDLE m_hTerminateEngineEvent = nullptr; HANDLE m_open_new_editor_event_handle{ nullptr }; + HANDLE m_toggle_active_event_handle{ nullptr }; std::thread m_toggle_thread; std::atomic m_toggle_thread_running{ false }; @@ -87,6 +88,19 @@ private: } } + void toggle_engine() + { + refresh_process_state(); + if (m_active) + { + stop_engine(); + } + else + { + start_engine(); + } + } + bool start_engine() { refresh_process_state(); @@ -273,6 +287,7 @@ public: } m_open_new_editor_event_handle = CreateDefaultEvent(CommonSharedConstants::OPEN_NEW_KEYBOARD_MANAGER_EVENT); + m_toggle_active_event_handle = CreateDefaultEvent(CommonSharedConstants::TOGGLE_KEYBOARD_MANAGER_ACTIVE_EVENT); init_settings(); }; @@ -291,6 +306,11 @@ public: CloseHandle(m_open_new_editor_event_handle); m_open_new_editor_event_handle = nullptr; } + if (m_toggle_active_event_handle) + { + CloseHandle(m_toggle_active_event_handle); + m_toggle_active_event_handle = nullptr; + } if (m_hEditorProcess) { CloseHandle(m_hEditorProcess); @@ -422,25 +442,45 @@ public: void StartOpenEditorListener() { - if (m_toggle_thread_running || !m_open_new_editor_event_handle) + if (m_toggle_thread_running || (!m_open_new_editor_event_handle && !m_toggle_active_event_handle)) { return; } m_toggle_thread_running = true; m_toggle_thread = std::thread([this]() { + HANDLE handles[2]{}; + DWORD handle_count = 0; + DWORD open_editor_index = MAXDWORD; + DWORD toggle_active_index = MAXDWORD; + + if (m_open_new_editor_event_handle) + { + open_editor_index = handle_count; + handles[handle_count++] = m_open_new_editor_event_handle; + } + + if (m_toggle_active_event_handle) + { + toggle_active_index = handle_count; + handles[handle_count++] = m_toggle_active_event_handle; + } + while (m_toggle_thread_running) { - const DWORD wait_result = WaitForSingleObject(m_open_new_editor_event_handle, 500); + const DWORD wait_result = WaitForMultipleObjects(handle_count, handles, FALSE, 500); if (!m_toggle_thread_running) { break; } - if (wait_result == WAIT_OBJECT_0) + if (open_editor_index != MAXDWORD && wait_result == (WAIT_OBJECT_0 + open_editor_index)) { launch_editor(); - ResetEvent(m_open_new_editor_event_handle); + } + else if (toggle_active_index != MAXDWORD && wait_result == (WAIT_OBJECT_0 + toggle_active_index)) + { + toggle_engine(); } } }); @@ -458,6 +498,10 @@ public: { SetEvent(m_open_new_editor_event_handle); } + if (m_toggle_active_event_handle) + { + SetEvent(m_toggle_active_event_handle); + } if (m_toggle_thread.joinable()) { m_toggle_thread.join(); @@ -551,15 +595,7 @@ public: if (hotkeyId == 0) { // Toggle engine on/off - refresh_process_state(); - if (m_active) - { - stop_engine(); - } - else - { - start_engine(); - } + toggle_engine(); } else if (hotkeyId == 1) { @@ -575,3 +611,4 @@ extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() { return new KeyboardManager(); } +