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(); } +