From f651d1a6118a306cc908c49cf550d1a300e55e4f Mon Sep 17 00:00:00 2001
From: Zach Teutsch <88554871+zateutsch@users.noreply.github.com>
Date: Wed, 4 Mar 2026 15:46:42 -0500
Subject: [PATCH] [Keyboard Manager] Updated WinUI3 KBM and toggles (#45649)
## Running the Project
**Option 1: Test via runner**
1. Check out branch `niels9001/kbm-ux-consolidation`
2. Build PowerToys project
3. Manually build `Modules/KeyboardManagerEditorUI` project separately
4. Run `runner` project
5. Ensure experimental features are enabled in general settings (should
be on by default)
6. Launch keyboard manager via settings app
**Option 2: Test via installer**
1. Install PowerToys via installer on azure pipeline
1. Launch keyboard manager
## Validation
For each page (Text, Remappings, Programs, URLs):
* Create shortcuts with variable options and ensure they run as expected
* Delete shortcuts and ensure they no longer execute
* Try to create invalid shortcuts to check for proper validation
* Ensure created shortcuts appear in Power Toys Settings Keyboard
manager page
* Try toggling shortcuts
* Try deleting shortcuts while toggled off
### UI
* Any feedback on UI design appreciated as well
Closes: #15870
Closes: #31902
Closes: #45302
Closes: #36227
Closes: #16093
Closes: #13409
Closes: #9919
Closes: #9482
Closes: #8798
Closes: #7054
Closes: #2733
Closes: #2027
Closes: #30167
---------
Co-authored-by: Hao Liu
Co-authored-by: chenmy77 <162882040+chenmy77@users.noreply.github.com>
Co-authored-by: Niels Laute
Co-authored-by: Jay <65828559+Jay-o-Way@users.noreply.github.com>
Co-authored-by: Jaylyn Barbee <51131738+Jaylyn-Barbee@users.noreply.github.com>
Co-authored-by: Dustin L. Howett
---
.github/actions/spell-check/expect.txt | 4 +
.github/copilot-instructions.md | 2 +-
.pipelines/ESRPSigning_core.json | 5 +
Directory.Build.props | 1 +
Directory.Packages.props | 2 +-
PowerToys.slnx | 50 +-
.../Controls/KeyVisual/KeyVisual.xaml | 22 +-
src/common/interop/Constants.cpp | 4 +
src/common/interop/Constants.h | 1 +
src/common/interop/Constants.idl | 1 +
src/common/interop/shared_constants.h | 3 +
.../Properties/Resources.Designer.cs | 2 +-
.../OpenNewKeyboardManagerEditorCommand.cs | 35 +
.../KeyboardManagerModuleCommandProvider.cs | 46 +
.../Properties/Resources.Designer.cs | 18 +
.../Properties/Resources.resx | 6 +
.../EditorHelpers.cpp | 2 +
.../ShortcutControl.cpp | 3 +-
.../SingleKeyRemapControl.cpp | 3 +-
.../KeyboardManagerEditorLibraryWrapper.cpp | 750 ++++++++++++-
.../KeyboardManagerEditorLibraryWrapper.h | 81 +-
.../KeyboardManagerEditorUI/App.xaml | 16 -
.../FluentIconsKeyboardManager.png | Bin 0 -> 3386 bytes
.../Assets/KeyboardManagerEditor/Keyboard.ico | Bin 0 -> 114683 bytes
.../Square150x150Logo.png | Bin 0 -> 11694 bytes
.../KeyboardManagerEditor/Square44x44Logo.png | Bin 0 -> 2562 bytes
.../KeyboardManagerEditor/StoreLogo.png | Bin 0 -> 2985 bytes
.../Controls/IconLabelControl.xaml | 48 +
.../Controls/IconLabelControl.xaml.cs | 88 ++
.../Controls/UnifiedMappingControl.xaml | 387 +++++++
.../Controls/UnifiedMappingControl.xaml.cs | 997 ++++++++++++++++++
.../Helpers/ActionType.cs | 15 +
.../Helpers/EditorConstants.cs | 18 +
.../Helpers/IToggleableShortcut.cs | 23 +
.../Helpers/KeyInputMode.cs | 12 +
.../Helpers/KeyboardHookHelper.cs | 251 +++++
.../Helpers/ProgramShortcut.cs | 38 +
.../Helpers/Remapping.cs | 51 +
.../Helpers/RemappingHelper.cs | 180 ++++
.../Helpers/TextMapping.cs | 27 +
.../Helpers/URLShortcut.cs | 27 +
.../Helpers/ValidationErrorType.cs | 28 +
.../Helpers/ValidationHelper.cs | 247 +++++
.../Interop/KeyMapping.cs | 21 +
.../Interop/KeyToTextMapping.cs | 19 +
.../Interop/KeyType.cs | 21 +
.../Interop/KeyboardManagerInterop.cs | 165 +++
.../Interop/KeyboardMappingService.cs | 296 ++++++
.../Interop/ShortcutKeyMapping.cs | 103 ++
.../Interop/ShortcutOperationType.cs | 20 +
.../KeyboardManagerEditorUI.csproj | 55 +-
.../KeyboardManagerEditorXAML/App.xaml | 22 +
.../App.xaml.cs | 40 +-
.../KeyboardManagerEditorXAML/MainWindow.xaml | 39 +
.../MainWindow.xaml.cs | 62 ++
.../KeyboardManagerEditorUI/MainWindow.xaml | 18 -
.../MainWindow.xaml.cs | 42 -
.../Package.appxmanifest | 16 +-
.../Pages/MainPage.xaml | 548 ++++++++++
.../Pages/MainPage.xaml.cs | 897 ++++++++++++++++
.../Settings/EditorSettings.cs | 20 +
.../Settings/SettingsManager.cs | 275 +++++
.../Settings/ShortcutSettings.cs | 24 +
.../Strings/en-US/Resources.resw | 295 ++++++
.../Styles/Button.xaml | 759 +++++++++++++
.../Styles/Colors.xaml | 21 +
.../KeyboardEventHandlers.cpp | 12 +-
.../SetKeyEventTests.cpp | 81 ++
.../keyboardmanager/common/Helpers.cpp | 153 ++-
src/modules/keyboardmanager/common/Helpers.h | 3 +
src/modules/keyboardmanager/common/Shortcut.h | 5 +-
src/modules/keyboardmanager/dll/dllmain.cpp | 229 +++-
src/modules/keyboardmanager/dll/trace.cpp | 11 +
src/modules/keyboardmanager/dll/trace.h | 3 +
.../Services/QuickAccessLauncher.cs | 8 +
.../QuickAccess/QuickAccessLauncher.cs | 7 +
.../QuickAccess/QuickAccessViewModel.cs | 40 +-
.../KeyboardManagerProperties.cs | 8 +
.../KeyboardManagerSettings.cs | 4 +
.../Events/ModuleLaunchedFromSettingsEvent.cs | 26 +
.../ViewModelTests/KeyboardManager.cs | 6 +-
.../Views/KeyboardManagerPage.xaml | 425 ++++----
.../Views/KeyboardManagerPage.xaml.cs | 5 +
.../Settings.UI/Strings/en-us/Resources.resw | 44 +-
.../ViewModels/KeyboardManagerViewModel.cs | 137 ++-
85 files changed, 8080 insertions(+), 399 deletions(-)
create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Commands/KeyboardManager/OpenNewKeyboardManagerEditorCommand.cs
delete mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/App.xaml
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Assets/KeyboardManagerEditor/FluentIconsKeyboardManager.png
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Assets/KeyboardManagerEditor/Keyboard.ico
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Assets/KeyboardManagerEditor/Square150x150Logo.png
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Assets/KeyboardManagerEditor/Square44x44Logo.png
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Assets/KeyboardManagerEditor/StoreLogo.png
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/IconLabelControl.xaml
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/IconLabelControl.xaml.cs
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/UnifiedMappingControl.xaml
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/UnifiedMappingControl.xaml.cs
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/ActionType.cs
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/EditorConstants.cs
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/IToggleableShortcut.cs
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/KeyInputMode.cs
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/KeyboardHookHelper.cs
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/ProgramShortcut.cs
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/Remapping.cs
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/RemappingHelper.cs
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/TextMapping.cs
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/URLShortcut.cs
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/ValidationErrorType.cs
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/ValidationHelper.cs
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyMapping.cs
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyToTextMapping.cs
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyType.cs
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyboardManagerInterop.cs
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyboardMappingService.cs
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/ShortcutKeyMapping.cs
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/ShortcutOperationType.cs
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/KeyboardManagerEditorXAML/App.xaml
rename src/modules/keyboardmanager/KeyboardManagerEditorUI/{ => KeyboardManagerEditorXAML}/App.xaml.cs (57%)
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/KeyboardManagerEditorXAML/MainWindow.xaml
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/KeyboardManagerEditorXAML/MainWindow.xaml.cs
delete mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/MainWindow.xaml
delete mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/MainWindow.xaml.cs
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/MainPage.xaml
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/MainPage.xaml.cs
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Settings/EditorSettings.cs
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Settings/SettingsManager.cs
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Settings/ShortcutSettings.cs
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Strings/en-US/Resources.resw
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Styles/Button.xaml
create mode 100644 src/modules/keyboardmanager/KeyboardManagerEditorUI/Styles/Colors.xaml
create mode 100644 src/settings-ui/Settings.UI.Library/Telemetry/Events/ModuleLaunchedFromSettingsEvent.cs
diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index c26d17f63c..5e714e88db 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -205,6 +205,7 @@ comdlg
comexp
cominterop
commandpalette
+commoncontrols
compmgmt
COMPOSITIONFULL
CONFIGW
@@ -677,6 +678,7 @@ jpnime
Jsons
jsonval
jxr
+kbmcontrols
keybd
KEYBDDATA
KEYBDINPUT
@@ -1530,6 +1532,7 @@ tlc
TPMLEFTALIGN
TPMRETURNCMD
TNP
+Toggleable
Toolhelp
toolwindow
TOPDOWNDIB
@@ -2203,6 +2206,7 @@ wft
wikimedia
wikipedia
windowedge
+WINDOWSAPPRUNTIME
windowsml
winexe
winforms
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index c87bd62b01..0a929b9067 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -33,4 +33,4 @@ These are auto-applied based on file location:
## Detailed Documentation
- [Architecture](../doc/devdocs/core/architecture.md)
-- [Coding Style](../doc/devdocs/development/style.md)
+- [Coding Style](../doc/devdocs/development/style.md)
\ No newline at end of file
diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json
index df3a610cf0..3833f59652 100644
--- a/.pipelines/ESRPSigning_core.json
+++ b/.pipelines/ESRPSigning_core.json
@@ -106,7 +106,12 @@
"PowerToys.SvgThumbnailProvider.dll",
"PowerToys.SvgThumbnailProvider.exe",
"PowerToys.SvgThumbnailProviderCpp.dll",
+ "PowerToys.KeyboardManager.dll",
+ "KeyboardManagerEditor\\PowerToys.KeyboardManagerEditor.exe",
+ "KeyboardManagerEditorUI\\PowerToys.KeyboardManagerEditorUI.exe",
+ "KeyboardManagerEngine\\PowerToys.KeyboardManagerEngine.exe",
+ "PowerToys.KeyboardManagerEditorLibraryWrapper.dll",
"WinUI3Apps\\PowerToys.HostsModuleInterface.dll",
"WinUI3Apps\\PowerToys.HostsUILib.dll",
"WinUI3Apps\\PowerToys.Hosts.dll",
diff --git a/Directory.Build.props b/Directory.Build.props
index a2e19ad31d..aa2d3fc600 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -20,6 +20,7 @@
direct
false
$(Platform)
+ false
true
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 6ce7261dfa..4c8d382c7b 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -26,7 +26,7 @@
-
+
diff --git a/PowerToys.slnx b/PowerToys.slnx
index 9b95ddac1d..44b02db3fc 100644
--- a/PowerToys.slnx
+++ b/PowerToys.slnx
@@ -497,6 +497,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -720,31 +745,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/common/Common.UI.Controls/Controls/KeyVisual/KeyVisual.xaml b/src/common/Common.UI.Controls/Controls/KeyVisual/KeyVisual.xaml
index 147c7d782a..6d3547dc33 100644
--- a/src/common/Common.UI.Controls/Controls/KeyVisual/KeyVisual.xaml
+++ b/src/common/Common.UI.Controls/Controls/KeyVisual/KeyVisual.xaml
@@ -1,11 +1,11 @@
+ xmlns:commoncontrols="using:Microsoft.PowerToys.Common.UI.Controls">
-
+
-
+
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/IconLabelControl.xaml.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/IconLabelControl.xaml.cs
new file mode 100644
index 0000000000..4e12beaa44
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/IconLabelControl.xaml.cs
@@ -0,0 +1,88 @@
+// 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 KeyboardManagerEditorUI.Helpers;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+
+namespace KeyboardManagerEditorUI.Controls
+{
+ [TemplatePart(Name = TypeIconPart, Type = typeof(FontIcon))]
+ [TemplatePart(Name = LabelTextPart, Type = typeof(TextBlock))]
+ public sealed partial class IconLabelControl : Control
+ {
+ private const string TypeIconPart = "TypeIcon";
+ private const string LabelTextPart = "LabelText";
+
+ private FontIcon? _typeIcon;
+ private TextBlock? _labelText;
+
+ public static readonly DependencyProperty ActionTypeProperty =
+ DependencyProperty.Register(
+ nameof(ActionType),
+ typeof(ActionType),
+ typeof(IconLabelControl),
+ new PropertyMetadata(ActionType.Text, OnActionTypeChanged));
+
+ public static readonly DependencyProperty LabelProperty =
+ DependencyProperty.Register(
+ nameof(Label),
+ typeof(string),
+ typeof(IconLabelControl),
+ new PropertyMetadata(string.Empty));
+
+ public ActionType ActionType
+ {
+ get => (ActionType)GetValue(ActionTypeProperty);
+ set => SetValue(ActionTypeProperty, value);
+ }
+
+ public string Label
+ {
+ get => (string)GetValue(LabelProperty);
+ set => SetValue(LabelProperty, value);
+ }
+
+ public IconLabelControl()
+ {
+ this.DefaultStyleKey = typeof(IconLabelControl);
+ }
+
+ protected override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ _typeIcon = GetTemplateChild(TypeIconPart) as FontIcon;
+ _labelText = GetTemplateChild(LabelTextPart) as TextBlock;
+
+ UpdateIcon();
+ }
+
+ private static void OnActionTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is IconLabelControl control)
+ {
+ control.UpdateIcon();
+ }
+ }
+
+ private void UpdateIcon()
+ {
+ if (_typeIcon == null)
+ {
+ return;
+ }
+
+ _typeIcon.Glyph = ActionType switch
+ {
+ ActionType.Program => "\uECAA",
+ ActionType.Text => "\uE8D2",
+ ActionType.Shortcut => "\uEDA7",
+ ActionType.MouseClick => "\uE962",
+ ActionType.Url => "\uE774",
+ _ => "\uE8A5",
+ };
+ }
+ }
+}
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/UnifiedMappingControl.xaml b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/UnifiedMappingControl.xaml
new file mode 100644
index 0000000000..d3c9ea780d
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/UnifiedMappingControl.xaml
@@ -0,0 +1,387 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/UnifiedMappingControl.xaml.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/UnifiedMappingControl.xaml.cs
new file mode 100644
index 0000000000..022d27de8d
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/UnifiedMappingControl.xaml.cs
@@ -0,0 +1,997 @@
+// 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.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using KeyboardManagerEditorUI.Helpers;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Windows.Storage;
+using Windows.Storage.Pickers;
+using Windows.System;
+using WinRT.Interop;
+using static KeyboardManagerEditorUI.Interop.ShortcutKeyMapping;
+
+#pragma warning disable SA1124 // Do not use regions
+
+namespace KeyboardManagerEditorUI.Controls
+{
+ ///
+ /// Unified control that consolidates all mapping input types:
+ /// - Key/Shortcut remapping (InputControl)
+ /// - Text output (TextPageInputControl)
+ /// - URL opening (UrlPageInputControl)
+ /// - App launching (AppPageInputControl)
+ ///
+ public sealed partial class UnifiedMappingControl : UserControl, IDisposable, IKeyboardHookTarget
+ {
+ #region Fields
+
+ private readonly ObservableCollection _triggerKeys = new();
+ private readonly ObservableCollection _actionKeys = new();
+
+ private bool _disposed;
+ private bool _internalUpdate;
+
+ private KeyInputMode _currentInputMode = KeyInputMode.OriginalKeys;
+
+ // Dirty tracking: marks fields that have had content then were cleared
+ private bool _textContentDirty;
+ private bool _urlPathDirty;
+ private bool _programPathDirty;
+
+ public bool AllowChords { get; set; } = true;
+
+ #endregion
+
+ #region Events
+
+ ///
+ /// Raised whenever the validation state of the control changes (inputs filled/cleared).
+ ///
+ public event EventHandler? ValidationStateChanged;
+
+ #endregion
+
+ #region Enums
+
+ ///
+ /// Defines the type of trigger for the mapping.
+ ///
+ public enum TriggerType
+ {
+ KeyOrShortcut,
+ Mouse,
+ }
+
+ ///
+ /// Defines the type of action to perform.
+ ///
+ public enum ActionType
+ {
+ KeyOrShortcut,
+ Text,
+ OpenUrl,
+ OpenApp,
+ MouseClick,
+ }
+
+ ///
+ /// Defines the mouse button options.
+ ///
+ public enum MouseButton
+ {
+ LeftMouse,
+ RightMouse,
+ ScrollUp,
+ ScrollDown,
+ }
+
+ #endregion
+
+ #region Properties
+
+ ///
+ /// Gets the current trigger type.
+ ///
+ public TriggerType CurrentTriggerType
+ {
+ get
+ {
+ if (TriggerTypeComboBox?.SelectedItem is ComboBoxItem item)
+ {
+ return item.Tag?.ToString() switch
+ {
+ "Mouse" => TriggerType.Mouse,
+ _ => TriggerType.KeyOrShortcut,
+ };
+ }
+
+ return TriggerType.KeyOrShortcut;
+ }
+ }
+
+ ///
+ /// Gets the current action type.
+ ///
+ public ActionType CurrentActionType
+ {
+ get
+ {
+ if (ActionTypeComboBox?.SelectedItem is ComboBoxItem item)
+ {
+ return item.Tag?.ToString() switch
+ {
+ "Text" => ActionType.Text,
+ "OpenUrl" => ActionType.OpenUrl,
+ "OpenApp" => ActionType.OpenApp,
+ "MouseClick" => ActionType.MouseClick,
+ _ => ActionType.KeyOrShortcut,
+ };
+ }
+
+ return ActionType.KeyOrShortcut;
+ }
+ }
+
+ #endregion
+
+ #region Constructor
+
+ public UnifiedMappingControl()
+ {
+ this.InitializeComponent();
+
+ TriggerKeys.ItemsSource = _triggerKeys;
+ ActionKeys.ItemsSource = _actionKeys;
+
+ _triggerKeys.CollectionChanged += (_, _) => RaiseValidationStateChanged();
+ _actionKeys.CollectionChanged += (_, _) => RaiseValidationStateChanged();
+
+ this.Unloaded += UnifiedMappingControl_Unloaded;
+ }
+
+ #endregion
+
+ #region Lifecycle Events
+
+ private void UserControl_Loaded(object sender, RoutedEventArgs e)
+ {
+ // Set up event handlers for app-specific checkbox
+ AppSpecificCheckBox.Checked += AppSpecificCheckBox_Changed;
+ AppSpecificCheckBox.Unchecked += AppSpecificCheckBox_Changed;
+
+ // Activate keyboard hook for the trigger input
+ if (TriggerKeyToggleBtn.IsChecked == true)
+ {
+ _currentInputMode = KeyInputMode.OriginalKeys;
+ KeyboardHookHelper.Instance.ActivateHook(this);
+ }
+ }
+
+ private void UnifiedMappingControl_Unloaded(object sender, RoutedEventArgs e)
+ {
+ Reset();
+ CleanupKeyboardHook();
+ }
+
+ #endregion
+
+ #region Trigger Type Handling
+
+ private void TriggerTypeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (TriggerTypeComboBox?.SelectedItem is ComboBoxItem item)
+ {
+ string? tag = item.Tag?.ToString();
+
+ // Cleanup keyboard hook when switching to mouse
+ if (tag == "Mouse")
+ {
+ CleanupKeyboardHook();
+ UncheckAllToggleButtons();
+ }
+ }
+ }
+
+ private void TriggerKeyToggleBtn_Checked(object sender, RoutedEventArgs e)
+ {
+ if (TriggerKeyToggleBtn.IsChecked == true)
+ {
+ _currentInputMode = KeyInputMode.OriginalKeys;
+
+ // Uncheck action toggle if checked
+ if (ActionKeyToggleBtn?.IsChecked == true)
+ {
+ ActionKeyToggleBtn.IsChecked = false;
+ }
+
+ KeyboardHookHelper.Instance.ActivateHook(this);
+ }
+ }
+
+ private void TriggerKeyToggleBtn_Unchecked(object sender, RoutedEventArgs e)
+ {
+ if (_currentInputMode == KeyInputMode.OriginalKeys)
+ {
+ CleanupKeyboardHook();
+ }
+ }
+
+ #endregion
+
+ #region Action Type Handling
+
+ private void ActionTypeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (ActionTypeComboBox?.SelectedItem is ComboBoxItem item)
+ {
+ string? tag = item.Tag?.ToString();
+
+ // Cleanup keyboard hook when switching away from key/shortcut
+ if (tag != "KeyOrShortcut")
+ {
+ if (_currentInputMode == KeyInputMode.RemappedKeys)
+ {
+ CleanupKeyboardHook();
+ }
+
+ if (ActionKeyToggleBtn?.IsChecked == true)
+ {
+ ActionKeyToggleBtn.IsChecked = false;
+ }
+ }
+ }
+
+ HideValidationMessage();
+ RaiseValidationStateChanged();
+ }
+
+ private void ActionKeyToggleBtn_Checked(object sender, RoutedEventArgs e)
+ {
+ if (ActionKeyToggleBtn.IsChecked == true)
+ {
+ _currentInputMode = KeyInputMode.RemappedKeys;
+
+ // Uncheck trigger toggle if checked
+ if (TriggerKeyToggleBtn?.IsChecked == true)
+ {
+ TriggerKeyToggleBtn.IsChecked = false;
+ }
+
+ KeyboardHookHelper.Instance.ActivateHook(this);
+ }
+ }
+
+ private void ActionKeyToggleBtn_Unchecked(object sender, RoutedEventArgs e)
+ {
+ if (_currentInputMode == KeyInputMode.RemappedKeys)
+ {
+ CleanupKeyboardHook();
+ }
+ }
+
+ #endregion
+
+ #region App-Specific Handling
+
+ private void AppSpecificCheckBox_Changed(object sender, RoutedEventArgs e)
+ {
+ if (_internalUpdate)
+ {
+ return;
+ }
+
+ CleanupKeyboardHook();
+ UncheckAllToggleButtons();
+
+ AppNameTextBox.Visibility = AppSpecificCheckBox.IsChecked == true
+ ? Visibility.Visible
+ : Visibility.Collapsed;
+ }
+
+ private void UpdateAppSpecificCheckBoxState()
+ {
+ // Only enable app-specific remapping for shortcuts (multiple keys).
+ bool isShortcut = _triggerKeys.Count > 1;
+ bool alreadyChecked = AppSpecificCheckBox.IsChecked == true;
+
+ try
+ {
+ _internalUpdate = true;
+
+ AppSpecificCheckBox.IsEnabled = isShortcut || alreadyChecked;
+ if (!isShortcut && !alreadyChecked)
+ {
+ AppSpecificCheckBox.IsChecked = false;
+ AppNameTextBox.Visibility = Visibility.Collapsed;
+ }
+ }
+ finally
+ {
+ _internalUpdate = false;
+ }
+ }
+
+ #endregion
+
+ #region TextBox Focus Handlers
+
+ private void AppNameTextBox_GotFocus(object sender, RoutedEventArgs e)
+ {
+ CleanupKeyboardHook();
+ UncheckAllToggleButtons();
+ }
+
+ private void TextContentBox_GotFocus(object sender, RoutedEventArgs e)
+ {
+ CleanupKeyboardHook();
+ UncheckAllToggleButtons();
+ }
+
+ private void TextContentBox_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ _textContentDirty = true;
+ RaiseValidationStateChanged();
+ }
+
+ private void UrlPathInput_GotFocus(object sender, RoutedEventArgs e)
+ {
+ CleanupKeyboardHook();
+ UncheckAllToggleButtons();
+ }
+
+ private void UrlPathInput_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ _urlPathDirty = true;
+ RaiseValidationStateChanged();
+ }
+
+ private void ProgramPathInput_GotFocus(object sender, RoutedEventArgs e)
+ {
+ CleanupKeyboardHook();
+ UncheckAllToggleButtons();
+ }
+
+ private void ProgramPathInput_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ _programPathDirty = true;
+ RaiseValidationStateChanged();
+ }
+
+ private void ProgramArgsInput_GotFocus(object sender, RoutedEventArgs e)
+ {
+ CleanupKeyboardHook();
+ UncheckAllToggleButtons();
+ }
+
+ private void StartInPathInput_GotFocus(object sender, RoutedEventArgs e)
+ {
+ CleanupKeyboardHook();
+ UncheckAllToggleButtons();
+ }
+
+ #endregion
+
+ #region File/Folder Pickers
+
+ private async void ProgramPathSelectButton_Click(object sender, RoutedEventArgs e)
+ {
+ var picker = new FileOpenPicker();
+
+ var hwnd = WindowNative.GetWindowHandle(App.MainWindow);
+ InitializeWithWindow.Initialize(picker, hwnd);
+
+ picker.FileTypeFilter.Add(".exe");
+
+ StorageFile file = await picker.PickSingleFileAsync();
+
+ if (file != null)
+ {
+ ProgramPathInput.Text = file.Path;
+ RaiseValidationStateChanged();
+ }
+ }
+
+ private async void StartInSelectButton_Click(object sender, RoutedEventArgs e)
+ {
+ var picker = new FolderPicker();
+
+ var hwnd = WindowNative.GetWindowHandle(App.MainWindow);
+ InitializeWithWindow.Initialize(picker, hwnd);
+
+ picker.FileTypeFilter.Add("*");
+
+ StorageFolder folder = await picker.PickSingleFolderAsync();
+
+ if (folder != null)
+ {
+ StartInPathInput.Text = folder.Path;
+ }
+ }
+
+ #endregion
+
+ #region IKeyboardHookTarget Implementation
+
+ public void OnKeyDown(VirtualKey key, List formattedKeys)
+ {
+ if (_currentInputMode == KeyInputMode.OriginalKeys)
+ {
+ _triggerKeys.Clear();
+ foreach (var keyName in formattedKeys)
+ {
+ _triggerKeys.Add(keyName);
+ }
+
+ UpdateAppSpecificCheckBoxState();
+ }
+ else if (_currentInputMode == KeyInputMode.RemappedKeys)
+ {
+ _actionKeys.Clear();
+ foreach (var keyName in formattedKeys)
+ {
+ _actionKeys.Add(keyName);
+ }
+ }
+ }
+
+ public void ClearKeys()
+ {
+ if (_currentInputMode == KeyInputMode.OriginalKeys)
+ {
+ _triggerKeys.Clear();
+ }
+ else
+ {
+ _actionKeys.Clear();
+ }
+ }
+
+ public void OnInputLimitReached()
+ {
+ ShowNotificationTip("Shortcuts can only have up to 4 modifier keys");
+ }
+
+ #endregion
+
+ #region Public API - Getters
+
+ ///
+ /// Gets the trigger keys.
+ ///
+ public List GetTriggerKeys() => _triggerKeys.ToList();
+
+ ///
+ /// Gets the action keys (for Key/Shortcut action type).
+ ///
+ public List GetActionKeys() => _actionKeys.ToList();
+
+ ///
+ /// Gets the selected mouse trigger.
+ ///
+ public MouseButton? GetMouseTrigger()
+ {
+ if (MouseTriggerComboBox?.SelectedItem is ComboBoxItem item)
+ {
+ return item.Tag?.ToString() switch
+ {
+ "LeftMouse" => MouseButton.LeftMouse,
+ "RightMouse" => MouseButton.RightMouse,
+ "ScrollUp" => MouseButton.ScrollUp,
+ "ScrollDown" => MouseButton.ScrollDown,
+ _ => null,
+ };
+ }
+
+ return null;
+ }
+
+ ///
+ /// Gets the text content (for Text action type).
+ ///
+ public string GetTextContent() => TextContentBox?.Text ?? string.Empty;
+
+ ///
+ /// Gets the URL (for OpenUrl action type).
+ ///
+ public string GetUrl() => UrlPathInput?.Text ?? string.Empty;
+
+ ///
+ /// Gets the program path (for OpenApp action type).
+ ///
+ public string GetProgramPath() => ProgramPathInput?.Text ?? string.Empty;
+
+ ///
+ /// Gets the program arguments (for OpenApp action type).
+ ///
+ public string GetProgramArgs() => ProgramArgsInput?.Text ?? string.Empty;
+
+ ///
+ /// Gets the start-in directory (for OpenApp action type).
+ ///
+ public string GetStartInDirectory() => StartInPathInput?.Text ?? string.Empty;
+
+ ///
+ /// Gets whether the mapping is app-specific.
+ ///
+ public bool GetIsAppSpecific()
+ {
+ return AppSpecificCheckBox?.IsChecked ?? false;
+ }
+
+ ///
+ /// Gets the app name for app-specific mappings.
+ ///
+ public string GetAppName()
+ {
+ return GetIsAppSpecific() ? (AppNameTextBox?.Text ?? string.Empty) : string.Empty;
+ }
+
+ ///
+ /// Gets the elevation level (for OpenApp action type).
+ ///
+ public ElevationLevel GetElevationLevel() => (ElevationLevel)(ElevationComboBox?.SelectedIndex ?? 0);
+
+ ///
+ /// Gets the window visibility (for OpenApp action type).
+ ///
+ public StartWindowType GetVisibility() => (StartWindowType)(VisibilityComboBox?.SelectedIndex ?? 0);
+
+ ///
+ /// Gets the if-running action (for OpenApp action type).
+ ///
+ public ProgramAlreadyRunningAction GetIfRunningAction() => (ProgramAlreadyRunningAction)(IfRunningComboBox?.SelectedIndex ?? 0);
+
+ #endregion
+
+ #region Public API - Validation
+
+ ///
+ /// Returns true when all required fields for the current action type are filled.
+ ///
+ public bool IsInputComplete()
+ {
+ // Trigger keys are always required
+ if (_triggerKeys.Count == 0)
+ {
+ return false;
+ }
+
+ return CurrentActionType switch
+ {
+ ActionType.KeyOrShortcut => _actionKeys.Count > 0,
+ ActionType.Text => !string.IsNullOrWhiteSpace(TextContentBox?.Text),
+ ActionType.OpenUrl => !string.IsNullOrWhiteSpace(UrlPathInput?.Text),
+ ActionType.OpenApp => !string.IsNullOrWhiteSpace(ProgramPathInput?.Text),
+ _ => false,
+ };
+ }
+
+ #endregion
+
+ #region Public API - Setters
+
+ ///
+ /// Sets the trigger keys.
+ ///
+ public void SetTriggerKeys(List keys)
+ {
+ _triggerKeys.Clear();
+ if (keys != null)
+ {
+ foreach (var key in keys)
+ {
+ _triggerKeys.Add(key);
+ }
+ }
+
+ UpdateAppSpecificCheckBoxState();
+ }
+
+ ///
+ /// Sets the action keys.
+ ///
+ public void SetActionKeys(List keys)
+ {
+ _actionKeys.Clear();
+ if (keys != null)
+ {
+ foreach (var key in keys)
+ {
+ _actionKeys.Add(key);
+ }
+ }
+ }
+
+ ///
+ /// Sets the action type.
+ ///
+ public void SetActionType(ActionType actionType)
+ {
+ int index = actionType switch
+ {
+ ActionType.Text => 1,
+ ActionType.OpenUrl => 2,
+ ActionType.OpenApp => 3,
+ ActionType.MouseClick => 4,
+ _ => 0,
+ };
+
+ if (ActionTypeComboBox != null)
+ {
+ ActionTypeComboBox.SelectedIndex = index;
+ }
+ }
+
+ ///
+ /// Sets the text content (for Text action type).
+ ///
+ public void SetTextContent(string text)
+ {
+ if (TextContentBox != null)
+ {
+ TextContentBox.Text = text;
+ }
+ }
+
+ ///
+ /// Sets the URL (for OpenUrl action type).
+ ///
+ public void SetUrl(string url)
+ {
+ if (UrlPathInput != null)
+ {
+ UrlPathInput.Text = url;
+ }
+ }
+
+ ///
+ /// Sets the program path (for OpenApp action type).
+ ///
+ public void SetProgramPath(string path)
+ {
+ if (ProgramPathInput != null)
+ {
+ ProgramPathInput.Text = path;
+ }
+ }
+
+ ///
+ /// Sets the program arguments (for OpenApp action type).
+ ///
+ public void SetProgramArgs(string args)
+ {
+ if (ProgramArgsInput != null)
+ {
+ ProgramArgsInput.Text = args;
+ }
+ }
+
+ ///
+ /// Sets the start-in directory (for OpenApp action type).
+ ///
+ public void SetStartInDirectory(string path)
+ {
+ if (StartInPathInput != null)
+ {
+ StartInPathInput.Text = path;
+ }
+ }
+
+ ///
+ /// Sets the elevation level (for OpenApp action type).
+ ///
+ public void SetElevationLevel(ElevationLevel elevationLevel)
+ {
+ if (ElevationComboBox != null)
+ {
+ ElevationComboBox.SelectedIndex = (int)elevationLevel;
+ }
+ }
+
+ ///
+ /// Sets the window visibility (for OpenApp action type).
+ ///
+ public void SetVisibility(StartWindowType visibility)
+ {
+ if (VisibilityComboBox != null)
+ {
+ VisibilityComboBox.SelectedIndex = (int)visibility;
+ }
+ }
+
+ ///
+ /// Sets the if-already-running action (for OpenApp action type).
+ ///
+ public void SetIfRunningAction(ProgramAlreadyRunningAction ifRunningAction)
+ {
+ if (IfRunningComboBox != null)
+ {
+ IfRunningComboBox.SelectedIndex = (int)ifRunningAction;
+ }
+ }
+
+ ///
+ /// Sets whether the mapping is app-specific.
+ ///
+ public void SetAppSpecific(bool isAppSpecific, string appName)
+ {
+ if (AppSpecificCheckBox != null)
+ {
+ AppSpecificCheckBox.IsChecked = isAppSpecific;
+ if (isAppSpecific && AppNameTextBox != null)
+ {
+ AppNameTextBox.Text = appName;
+ AppNameTextBox.Visibility = Visibility.Visible;
+ }
+ }
+ }
+
+ #endregion
+
+ #region Helper Methods
+
+ private void UncheckAllToggleButtons()
+ {
+ if (TriggerKeyToggleBtn?.IsChecked == true)
+ {
+ TriggerKeyToggleBtn.IsChecked = false;
+ }
+
+ if (ActionKeyToggleBtn?.IsChecked == true)
+ {
+ ActionKeyToggleBtn.IsChecked = false;
+ }
+ }
+
+ private void CleanupKeyboardHook()
+ {
+ KeyboardHookHelper.Instance.CleanupHook();
+ }
+
+ private void RaiseValidationStateChanged()
+ {
+ UpdateInlineValidation();
+ ValidationStateChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ ///
+ /// Shows or hides the inline validation InfoBar based on the current state.
+ /// Only shows errors for output fields that have been interacted with (had content then cleared).
+ ///
+ private void UpdateInlineValidation()
+ {
+ // Only validate the active action type's output field
+ switch (CurrentActionType)
+ {
+ case ActionType.Text:
+ if (TextContentBox != null && _textContentDirty && string.IsNullOrWhiteSpace(TextContentBox.Text))
+ {
+ ShowValidationErrorFromType(ValidationErrorType.EmptyTargetText);
+ return;
+ }
+
+ break;
+
+ case ActionType.OpenUrl:
+ if (UrlPathInput != null && _urlPathDirty && string.IsNullOrWhiteSpace(UrlPathInput.Text))
+ {
+ ShowValidationErrorFromType(ValidationErrorType.EmptyUrl);
+ return;
+ }
+
+ break;
+
+ case ActionType.OpenApp:
+ if (ProgramPathInput != null && _programPathDirty && string.IsNullOrWhiteSpace(ProgramPathInput.Text))
+ {
+ ShowValidationErrorFromType(ValidationErrorType.EmptyProgramPath);
+ return;
+ }
+
+ break;
+ }
+
+ HideValidationMessage();
+ }
+
+ ///
+ /// Resets all inputs to their default state.
+ ///
+ public void Reset()
+ {
+ _triggerKeys.Clear();
+ _actionKeys.Clear();
+
+ UncheckAllToggleButtons();
+
+ _currentInputMode = KeyInputMode.OriginalKeys;
+
+ // Reset dirty tracking
+ _textContentDirty = false;
+ _urlPathDirty = false;
+ _programPathDirty = false;
+
+ // Hide any validation messages
+ HideValidationMessage();
+
+ // Reset combo boxes
+ if (TriggerTypeComboBox != null)
+ {
+ TriggerTypeComboBox.SelectedIndex = 0;
+ }
+
+ if (ActionTypeComboBox != null)
+ {
+ ActionTypeComboBox.SelectedIndex = 0;
+ }
+
+ if (MouseTriggerComboBox != null)
+ {
+ MouseTriggerComboBox.SelectedIndex = -1;
+ }
+
+ // Reset text inputs
+ if (TextContentBox != null)
+ {
+ TextContentBox.Text = string.Empty;
+ }
+
+ if (UrlPathInput != null)
+ {
+ UrlPathInput.Text = string.Empty;
+ }
+
+ if (ProgramPathInput != null)
+ {
+ ProgramPathInput.Text = string.Empty;
+ }
+
+ if (ProgramArgsInput != null)
+ {
+ ProgramArgsInput.Text = string.Empty;
+ }
+
+ if (StartInPathInput != null)
+ {
+ StartInPathInput.Text = string.Empty;
+ }
+
+ if (AppNameTextBox != null)
+ {
+ AppNameTextBox.Text = string.Empty;
+ AppNameTextBox.Visibility = Visibility.Collapsed;
+ }
+
+ // Reset checkboxes
+ if (AppSpecificCheckBox != null)
+ {
+ AppSpecificCheckBox.IsChecked = false;
+ AppSpecificCheckBox.IsEnabled = false;
+ }
+
+ // Reset app combo boxes
+ if (ElevationComboBox != null)
+ {
+ ElevationComboBox.SelectedIndex = 0;
+ }
+
+ if (IfRunningComboBox != null)
+ {
+ IfRunningComboBox.SelectedIndex = 0;
+ }
+
+ if (VisibilityComboBox != null)
+ {
+ VisibilityComboBox.SelectedIndex = 0;
+ }
+
+ HideValidationMessage();
+ }
+
+ ///
+ /// Resets only the toggle buttons without clearing the key displays.
+ ///
+ public void ResetToggleButtons()
+ {
+ UncheckAllToggleButtons();
+ }
+
+ #endregion
+
+ #region Notifications
+
+ ///
+ /// Shows a warning notification in the InfoBar.
+ ///
+ public void ShowNotificationTip(string message)
+ {
+ ShowValidationMessage("Warning", message, InfoBarSeverity.Warning);
+ }
+
+ ///
+ /// Shows an error in the InfoBar with title and message.
+ ///
+ public void ShowValidationError(string title, string message)
+ {
+ ShowValidationMessage(title, message, InfoBarSeverity.Error);
+ }
+
+ ///
+ /// Shows a validation error based on the error type.
+ ///
+ public void ShowValidationErrorFromType(ValidationErrorType errorType)
+ {
+ if (ValidationHelper.ValidationMessages.TryGetValue(errorType, out var messageInfo))
+ {
+ ShowValidationError(messageInfo.Title, messageInfo.Message);
+ }
+ else
+ {
+ ShowValidationError("Validation Error", "An unknown validation error occurred.");
+ }
+ }
+
+ ///
+ /// Shows a message in the InfoBar with the specified severity.
+ ///
+ private void ShowValidationMessage(string title, string message, InfoBarSeverity severity)
+ {
+ if (ValidationInfoBar != null)
+ {
+ ValidationInfoBar.Title = title;
+ ValidationInfoBar.Message = message;
+ ValidationInfoBar.Severity = severity;
+ ValidationInfoBar.IsOpen = true;
+ }
+ }
+
+ ///
+ /// Hides the validation InfoBar.
+ ///
+ public void HideValidationMessage()
+ {
+ if (ValidationInfoBar != null)
+ {
+ ValidationInfoBar.IsOpen = false;
+ }
+ }
+
+ #endregion
+
+ #region IDisposable
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ CleanupKeyboardHook();
+ HideValidationMessage();
+ }
+
+ _disposed = true;
+ }
+ }
+
+ #endregion
+
+ private void AllowChordsCheckBox_Click(object sender, RoutedEventArgs e)
+ {
+ AllowChords = AllowChordsCheckBox.IsChecked == true;
+ }
+ }
+}
+
+#pragma warning restore SA1124 // Do not use regions
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/ActionType.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/ActionType.cs
new file mode 100644
index 0000000000..acaf60a4c3
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/ActionType.cs
@@ -0,0 +1,15 @@
+// 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.
+
+namespace KeyboardManagerEditorUI.Helpers
+{
+ public enum ActionType
+ {
+ Program,
+ Text,
+ Shortcut,
+ MouseClick,
+ Url,
+ }
+}
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/EditorConstants.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/EditorConstants.cs
new file mode 100644
index 0000000000..67fb1eb26a
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/EditorConstants.cs
@@ -0,0 +1,18 @@
+// 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.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace KeyboardManagerEditorUI.Helpers
+{
+ public static class EditorConstants
+ {
+ // Default notification timeout
+ public const int DefaultNotificationTimeout = 1500;
+ }
+}
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/IToggleableShortcut.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/IToggleableShortcut.cs
new file mode 100644
index 0000000000..a49719f8e7
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/IToggleableShortcut.cs
@@ -0,0 +1,23 @@
+// 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.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace KeyboardManagerEditorUI.Helpers
+{
+ internal interface IToggleableShortcut
+ {
+ public List Shortcut { get; set; }
+
+ bool IsActive { get; set; }
+
+ string Id { get; set; }
+
+ string AppName { get; set; }
+ }
+}
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/KeyInputMode.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/KeyInputMode.cs
new file mode 100644
index 0000000000..99dc14752a
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/KeyInputMode.cs
@@ -0,0 +1,12 @@
+// 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.
+
+namespace KeyboardManagerEditorUI.Helpers
+{
+ public enum KeyInputMode
+ {
+ OriginalKeys,
+ RemappedKeys,
+ }
+}
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/KeyboardHookHelper.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/KeyboardHookHelper.cs
new file mode 100644
index 0000000000..4becb4401a
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/KeyboardHookHelper.cs
@@ -0,0 +1,251 @@
+// 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.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using KeyboardManagerEditorUI.Interop;
+using Microsoft.PowerToys.Settings.UI.Library;
+using Windows.System;
+
+namespace KeyboardManagerEditorUI.Helpers
+{
+ public class KeyboardHookHelper : IDisposable
+ {
+ private static KeyboardHookHelper? _instance;
+
+ public static KeyboardHookHelper Instance => _instance ??= new KeyboardHookHelper();
+
+ private KeyboardMappingService _mappingService;
+
+ private HotkeySettingsControlHook? _keyboardHook;
+
+ // The active page using this keyboard hook
+ private IKeyboardHookTarget? _activeTarget;
+
+ private HashSet _currentlyPressedKeys = new();
+ private List _keyPressOrder = new();
+
+ private bool _disposed;
+
+ // Singleton to make sure only one instance of the hook is active
+ private KeyboardHookHelper()
+ {
+ _mappingService = new KeyboardMappingService();
+ }
+
+ public void ActivateHook(IKeyboardHookTarget target)
+ {
+ CleanupHook();
+
+ _activeTarget = target;
+
+ _currentlyPressedKeys.Clear();
+ _keyPressOrder.Clear();
+
+ _keyboardHook = new HotkeySettingsControlHook(
+ KeyDown,
+ KeyUp,
+ () => true,
+ (key, extraInfo) => true);
+ }
+
+ public void CleanupHook()
+ {
+ if (_keyboardHook != null)
+ {
+ _keyboardHook.Dispose();
+ _keyboardHook = null;
+ }
+
+ _currentlyPressedKeys.Clear();
+ _keyPressOrder.Clear();
+ _activeTarget = null;
+ }
+
+ private void KeyDown(int key)
+ {
+ if (_activeTarget == null)
+ {
+ return;
+ }
+
+ VirtualKey virtualKey = (VirtualKey)key;
+
+ if (_currentlyPressedKeys.Contains(virtualKey))
+ {
+ return;
+ }
+
+ // if no keys are pressed, clear the lists when a new key is pressed
+ if (_currentlyPressedKeys.Count == 0)
+ {
+ _activeTarget.ClearKeys();
+ _keyPressOrder.Clear();
+ }
+
+ // Count current modifiers
+ int modifierCount = _currentlyPressedKeys.Count(k => RemappingHelper.IsModifierKey(k));
+
+ // If adding this key would exceed the limits (4 modifiers + 1 action key), don't add it and show notification
+ if ((RemappingHelper.IsModifierKey(virtualKey) && modifierCount >= 4) ||
+ (!RemappingHelper.IsModifierKey(virtualKey) && _currentlyPressedKeys.Count >= 5))
+ {
+ _activeTarget.OnInputLimitReached();
+ return;
+ }
+
+ // Check if this is a different variant of a modifier key already pressed
+ if (RemappingHelper.IsModifierKey(virtualKey))
+ {
+ // Remove existing variant of this modifier key if a new one is pressed
+ // This is to ensure that only one variant of a modifier key is displayed at a time
+ RemoveExistingModifierVariant(virtualKey);
+ }
+
+ if (_currentlyPressedKeys.Add(virtualKey))
+ {
+ _keyPressOrder.Add(virtualKey);
+
+ // Notify the target page
+ _activeTarget.OnKeyDown(virtualKey, GetFormattedKeyList());
+ }
+ }
+
+ private void KeyUp(int key)
+ {
+ if (_activeTarget == null)
+ {
+ return;
+ }
+
+ VirtualKey virtualKey = (VirtualKey)key;
+
+ if (_currentlyPressedKeys.Remove(virtualKey))
+ {
+ _keyPressOrder.Remove(virtualKey);
+
+ _activeTarget.OnKeyUp(virtualKey, GetFormattedKeyList());
+ }
+ }
+
+ // Display the modifier keys and the action key in order, e.g. "Ctrl + Alt + A"
+ private List GetFormattedKeyList()
+ {
+ if (_activeTarget == null)
+ {
+ return new List();
+ }
+
+ List keyList = new List();
+ List modifierKeys = new List();
+ VirtualKey? actionKey = null;
+ VirtualKey? actionKeyChord = null;
+
+ foreach (var key in _keyPressOrder)
+ {
+ if (!_currentlyPressedKeys.Contains(key))
+ {
+ continue;
+ }
+
+ if (RemappingHelper.IsModifierKey(key))
+ {
+ if (!modifierKeys.Contains(key))
+ {
+ modifierKeys.Add(key);
+ }
+ }
+ else if (actionKey.HasValue && _activeTarget.AllowChords)
+ {
+ actionKeyChord = key;
+ }
+ else
+ {
+ actionKey = key;
+ }
+ }
+
+ foreach (var key in modifierKeys)
+ {
+ keyList.Add(_mappingService.GetKeyDisplayName((int)key));
+ }
+
+ if (actionKey.HasValue)
+ {
+ keyList.Add(_mappingService.GetKeyDisplayName((int)actionKey.Value));
+ }
+
+ if (actionKeyChord.HasValue && _activeTarget.AllowChords)
+ {
+ keyList.Add(_mappingService.GetKeyDisplayName((int)actionKeyChord.Value));
+ }
+
+ return keyList;
+ }
+
+ private void RemoveExistingModifierVariant(VirtualKey key)
+ {
+ KeyType keyType = (KeyType)KeyboardManagerInterop.GetKeyType((int)key);
+
+ // No need to remove if the key is an action key
+ if (keyType == KeyType.Action)
+ {
+ return;
+ }
+
+ foreach (var existingKey in _currentlyPressedKeys.ToList())
+ {
+ if (existingKey != key)
+ {
+ KeyType existingKeyType = (KeyType)KeyboardManagerInterop.GetKeyType((int)existingKey);
+
+ // Remove the existing key if it is a modifier key and has the same type as the new key
+ if (existingKeyType == keyType)
+ {
+ _currentlyPressedKeys.Remove(existingKey);
+ _keyPressOrder.Remove(existingKey);
+ }
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ CleanupHook();
+ _mappingService?.Dispose();
+ }
+
+ _disposed = true;
+ }
+ }
+ }
+
+ public interface IKeyboardHookTarget
+ {
+ bool AllowChords { get; }
+
+ void OnKeyDown(VirtualKey key, List formattedKeys);
+
+ void OnKeyUp(VirtualKey key, List formattedKeys)
+ {
+ }
+
+ void ClearKeys();
+
+ void OnInputLimitReached();
+ }
+}
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/ProgramShortcut.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/ProgramShortcut.cs
new file mode 100644
index 0000000000..cdf60dcf60
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/ProgramShortcut.cs
@@ -0,0 +1,38 @@
+// 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.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using static KeyboardManagerEditorUI.Interop.ShortcutKeyMapping;
+
+namespace KeyboardManagerEditorUI.Helpers
+{
+ public class ProgramShortcut : IToggleableShortcut
+ {
+ public List Shortcut { get; set; } = new List();
+
+ public string AppToRun { get; set; } = string.Empty;
+
+ public string Args { get; set; } = string.Empty;
+
+ public bool IsActive { get; set; } = true;
+
+ public string Id { get; set; } = string.Empty;
+
+ public bool IsAllApps { get; set; } = true;
+
+ public string AppName { get; set; } = string.Empty;
+
+ public string StartInDirectory { get; set; } = string.Empty;
+
+ public string Elevation { get; set; } = string.Empty;
+
+ public string IfRunningAction { get; set; } = string.Empty;
+
+ public string Visibility { get; set; } = string.Empty;
+ }
+}
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/Remapping.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/Remapping.cs
new file mode 100644
index 0000000000..918a66205d
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/Remapping.cs
@@ -0,0 +1,51 @@
+// 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.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace KeyboardManagerEditorUI.Helpers
+{
+ public partial class Remapping : INotifyPropertyChanged, IToggleableShortcut
+ {
+ public List Shortcut { get; set; } = new List();
+
+ public List RemappedKeys { get; set; } = new List();
+
+ public bool IsAllApps { get; set; } = true;
+
+ public string AppName { get; set; } = string.Empty;
+
+ private bool IsEnabledValue { get; set; } = true;
+
+ public string Id { get; set; } = string.Empty;
+
+ public bool IsActive { get; set; } = true;
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ public bool IsEnabled
+ {
+ get => IsEnabledValue;
+ set
+ {
+ if (IsEnabledValue != value)
+ {
+ IsEnabledValue = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ private void OnPropertyChanged([CallerMemberName] string propertyName = "")
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+}
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/RemappingHelper.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/RemappingHelper.cs
new file mode 100644
index 0000000000..48c32d853c
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/RemappingHelper.cs
@@ -0,0 +1,180 @@
+// 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.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using KeyboardManagerEditorUI.Interop;
+using KeyboardManagerEditorUI.Settings;
+using ManagedCommon;
+using Windows.System;
+
+namespace KeyboardManagerEditorUI.Helpers
+{
+ public static class RemappingHelper
+ {
+ public static bool SaveMapping(KeyboardMappingService mappingService, List originalKeys, List remappedKeys, bool isAppSpecific, string appName, bool saveToSettings = true)
+ {
+ if (mappingService == null)
+ {
+ Logger.LogError("Mapping service is null, cannot save mapping");
+ return false;
+ }
+
+ try
+ {
+ if (originalKeys == null || originalKeys.Count == 0 || remappedKeys == null || remappedKeys.Count == 0)
+ {
+ return false;
+ }
+
+ if (originalKeys.Count == 1)
+ {
+ int originalKey = mappingService.GetKeyCodeFromName(originalKeys[0]);
+
+ if (originalKey != 0)
+ {
+ string targetKeysString = string.Join(";", remappedKeys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
+ ShortcutKeyMapping shortcutKeyMapping = new ShortcutKeyMapping()
+ {
+ OperationType = ShortcutOperationType.RemapShortcut,
+ OriginalKeys = originalKey.ToString(CultureInfo.InvariantCulture),
+ TargetKeys = targetKeysString,
+ TargetApp = isAppSpecific ? appName : string.Empty,
+ };
+ if (remappedKeys.Count == 1)
+ {
+ int targetKey = mappingService.GetKeyCodeFromName(remappedKeys[0]);
+ if (targetKey != 0)
+ {
+ mappingService.AddSingleKeyMapping(originalKey, targetKey);
+ }
+ }
+ else
+ {
+ mappingService.AddSingleKeyMapping(originalKey, targetKeysString);
+ }
+
+ if (saveToSettings)
+ {
+ SettingsManager.AddShortcutKeyMappingToSettings(shortcutKeyMapping);
+ }
+ }
+ }
+ else
+ {
+ string originalKeysString = string.Join(";", originalKeys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
+ string targetKeysString = string.Join(";", remappedKeys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
+
+ ShortcutKeyMapping shortcutKeyMapping = new ShortcutKeyMapping()
+ {
+ OperationType = ShortcutOperationType.RemapShortcut,
+ OriginalKeys = originalKeysString,
+ TargetKeys = targetKeysString,
+ TargetApp = isAppSpecific ? appName : string.Empty,
+ };
+
+ if (isAppSpecific && !string.IsNullOrEmpty(appName))
+ {
+ mappingService.AddShortcutMapping(originalKeysString, targetKeysString, appName);
+ }
+ else
+ {
+ mappingService.AddShortcutMapping(originalKeysString, targetKeysString);
+ }
+
+ if (saveToSettings)
+ {
+ SettingsManager.AddShortcutKeyMappingToSettings(shortcutKeyMapping);
+ }
+ }
+
+ return mappingService.SaveSettings();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Error saving mapping: " + ex.Message);
+ return false;
+ }
+ }
+
+ public static bool DeleteRemapping(KeyboardMappingService mappingService, Remapping remapping, bool deleteFromSettings = true)
+ {
+ if (mappingService == null)
+ {
+ return false;
+ }
+
+ try
+ {
+ if (remapping.Shortcut.Count == 1)
+ {
+ // Single key mapping
+ int originalKey = mappingService.GetKeyCodeFromName(remapping.Shortcut[0]);
+ if (originalKey != 0)
+ {
+ if (mappingService.DeleteSingleKeyMapping(originalKey))
+ {
+ if (deleteFromSettings)
+ {
+ SettingsManager.RemoveShortcutKeyMappingFromSettings(remapping.Id);
+ }
+
+ return mappingService.SaveSettings();
+ }
+ }
+ }
+ else if (remapping.Shortcut.Count > 1)
+ {
+ // Shortcut mapping
+ string originalKeysString = string.Join(";", remapping.Shortcut.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
+
+ bool deleteResult;
+ if (!remapping.IsAllApps && !string.IsNullOrEmpty(remapping.AppName))
+ {
+ // App-specific shortcut key mapping
+ deleteResult = mappingService.DeleteShortcutMapping(originalKeysString, remapping.AppName);
+ }
+ else
+ {
+ // Global shortcut key mapping
+ deleteResult = mappingService.DeleteShortcutMapping(originalKeysString);
+ }
+
+ if (deleteResult && deleteFromSettings)
+ {
+ SettingsManager.RemoveShortcutKeyMappingFromSettings(remapping.Id);
+ }
+
+ return deleteResult ? mappingService.SaveSettings() : false;
+ }
+
+ return false;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError($"Error deleting remapping: {ex.Message}");
+ return false;
+ }
+ }
+
+ public static bool IsModifierKey(VirtualKey key)
+ {
+ return key == VirtualKey.Control
+ || key == VirtualKey.LeftControl
+ || key == VirtualKey.RightControl
+ || key == VirtualKey.Menu
+ || key == VirtualKey.LeftMenu
+ || key == VirtualKey.RightMenu
+ || key == VirtualKey.Shift
+ || key == VirtualKey.LeftShift
+ || key == VirtualKey.RightShift
+ || key == VirtualKey.LeftWindows
+ || key == VirtualKey.RightWindows;
+ }
+ }
+}
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/TextMapping.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/TextMapping.cs
new file mode 100644
index 0000000000..59a582ec69
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/TextMapping.cs
@@ -0,0 +1,27 @@
+// 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.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace KeyboardManagerEditorUI.Helpers
+{
+ public class TextMapping : IToggleableShortcut
+ {
+ public List Shortcut { get; set; } = new List();
+
+ public string Text { get; set; } = string.Empty;
+
+ public bool IsAllApps { get; set; } = true;
+
+ public string AppName { get; set; } = string.Empty;
+
+ public bool IsActive { get; set; } = true;
+
+ public string Id { get; set; } = string.Empty;
+ }
+}
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/URLShortcut.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/URLShortcut.cs
new file mode 100644
index 0000000000..b47e66f568
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/URLShortcut.cs
@@ -0,0 +1,27 @@
+// 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.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace KeyboardManagerEditorUI.Helpers
+{
+ public class URLShortcut : IToggleableShortcut
+ {
+ public List Shortcut { get; set; } = new List();
+
+ public string URL { get; set; } = string.Empty;
+
+ public bool IsActive { get; set; } = true;
+
+ public string Id { get; set; } = string.Empty;
+
+ public bool IsAllApps { get; set; } = true;
+
+ public string AppName { get; set; } = string.Empty;
+ }
+}
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/ValidationErrorType.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/ValidationErrorType.cs
new file mode 100644
index 0000000000..7177f497af
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/ValidationErrorType.cs
@@ -0,0 +1,28 @@
+// 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.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace KeyboardManagerEditorUI.Helpers
+{
+ public enum ValidationErrorType
+ {
+ NoError,
+ EmptyOriginalKeys,
+ EmptyRemappedKeys,
+ ModifierOnly,
+ EmptyAppName,
+ IllegalShortcut,
+ DuplicateMapping,
+ SelfMapping,
+ EmptyTargetText,
+ EmptyUrl,
+ EmptyProgramPath,
+ OneKeyMapping,
+ }
+}
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/ValidationHelper.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/ValidationHelper.cs
new file mode 100644
index 0000000000..526d6e01d2
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Helpers/ValidationHelper.cs
@@ -0,0 +1,247 @@
+// 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.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using KeyboardManagerEditorUI.Interop;
+using KeyboardManagerEditorUI.Settings;
+using ManagedCommon;
+
+namespace KeyboardManagerEditorUI.Helpers
+{
+ public static class ValidationHelper
+ {
+ public static readonly Dictionary ValidationMessages = new()
+ {
+ { ValidationErrorType.EmptyOriginalKeys, ("Missing Original Keys", "Please enter at least one original key to create a remapping.") },
+ { ValidationErrorType.EmptyRemappedKeys, ("Missing Target Keys", "Please enter at least one target key to create a remapping.") },
+ { ValidationErrorType.ModifierOnly, ("Invalid Shortcut", "Shortcuts must contain at least one action key in addition to modifier keys (Ctrl, Alt, Shift, Win).") },
+ { ValidationErrorType.EmptyAppName, ("Missing Application Name", "You've selected app-specific remapping but haven't specified an application name. Please enter the application name.") },
+ { ValidationErrorType.IllegalShortcut, ("Reserved System Shortcut", "Win+L and Ctrl+Alt+Delete are reserved system shortcuts and cannot be remapped.") },
+ { ValidationErrorType.DuplicateMapping, ("Duplicate Remapping", "This key or shortcut is already remapped.") },
+ { ValidationErrorType.SelfMapping, ("Invalid Remapping", "A key or shortcut cannot be remapped to itself. Please choose a different target.") },
+ { ValidationErrorType.EmptyTargetText, ("Missing Target Text", "Please enter the text to be inserted when the shortcut is pressed.") },
+ { ValidationErrorType.EmptyUrl, ("Missing URL", "Please enter the URL to open when the shortcut is pressed.") },
+ { ValidationErrorType.EmptyProgramPath, ("Missing Program Path", "Please enter the program path to launch when the shortcut is pressed.") },
+ { ValidationErrorType.OneKeyMapping, ("Invalid Remapping", "A single key cannot be remapped to a Program or URL shortcut. Please choose a combination of keys.") },
+ };
+
+ public static ValidationErrorType ValidateKeyMapping(
+ List originalKeys,
+ List remappedKeys,
+ bool isAppSpecific,
+ string appName,
+ KeyboardMappingService mappingService,
+ bool isEditMode = false,
+ Remapping? editingRemapping = null)
+ {
+ if (originalKeys == null || originalKeys.Count == 0)
+ {
+ return ValidationErrorType.EmptyOriginalKeys;
+ }
+
+ if (remappedKeys == null || remappedKeys.Count == 0)
+ {
+ return ValidationErrorType.EmptyRemappedKeys;
+ }
+
+ if ((originalKeys.Count > 1 && ContainsOnlyModifierKeys(originalKeys)) ||
+ (remappedKeys.Count > 1 && ContainsOnlyModifierKeys(remappedKeys)))
+ {
+ return ValidationErrorType.ModifierOnly;
+ }
+
+ if (isAppSpecific && string.IsNullOrWhiteSpace(appName))
+ {
+ return ValidationErrorType.EmptyAppName;
+ }
+
+ if (originalKeys.Count > 1 && IsIllegalShortcut(originalKeys, mappingService))
+ {
+ return ValidationErrorType.IllegalShortcut;
+ }
+
+ if (IsDuplicateMapping(originalKeys, isEditMode, mappingService, appName))
+ {
+ return ValidationErrorType.DuplicateMapping;
+ }
+
+ if (IsSelfMapping(originalKeys, remappedKeys, mappingService))
+ {
+ return ValidationErrorType.SelfMapping;
+ }
+
+ return ValidationErrorType.NoError;
+ }
+
+ public static ValidationErrorType ValidateTextMapping(
+ List keys,
+ string textContent,
+ bool isAppSpecific,
+ string appName,
+ KeyboardMappingService mappingService,
+ bool isEditMode = false)
+ {
+ if (keys == null || keys.Count == 0)
+ {
+ return ValidationErrorType.EmptyOriginalKeys;
+ }
+
+ if (string.IsNullOrWhiteSpace(textContent))
+ {
+ return ValidationErrorType.EmptyTargetText;
+ }
+
+ if (keys.Count > 1 && ContainsOnlyModifierKeys(keys))
+ {
+ return ValidationErrorType.ModifierOnly;
+ }
+
+ if (isAppSpecific && string.IsNullOrWhiteSpace(appName))
+ {
+ return ValidationErrorType.EmptyAppName;
+ }
+
+ if (keys.Count > 1 && IsIllegalShortcut(keys, mappingService))
+ {
+ return ValidationErrorType.IllegalShortcut;
+ }
+
+ if (IsDuplicateMapping(keys, isEditMode, mappingService, appName))
+ {
+ return ValidationErrorType.DuplicateMapping;
+ }
+
+ return ValidationErrorType.NoError;
+ }
+
+ public static ValidationErrorType ValidateUrlMapping(
+ List originalKeys,
+ string url,
+ bool isAppSpecific,
+ string appName,
+ KeyboardMappingService mappingService,
+ bool isEditMode = false,
+ Remapping? editingRemapping = null)
+ {
+ if (string.IsNullOrWhiteSpace(url))
+ {
+ return ValidationErrorType.EmptyUrl;
+ }
+
+ return ValidateProgramOrUrlMapping(originalKeys, isAppSpecific, appName, mappingService, isEditMode, editingRemapping);
+ }
+
+ public static ValidationErrorType ValidateAppMapping(
+ List originalKeys,
+ string programPath,
+ bool isAppSpecific,
+ string appName,
+ KeyboardMappingService mappingService,
+ bool isEditMode = false,
+ Remapping? editingRemapping = null)
+ {
+ if (string.IsNullOrWhiteSpace(programPath))
+ {
+ return ValidationErrorType.EmptyProgramPath;
+ }
+
+ return ValidateProgramOrUrlMapping(originalKeys, isAppSpecific, appName, mappingService, isEditMode, editingRemapping);
+ }
+
+ public static bool IsDuplicateMapping(List keys, bool isEditMode, KeyboardMappingService mappingService, string appName)
+ {
+ int upperLimit = isEditMode ? 1 : 0;
+ string shortcutKeysString = BuildKeyCodeString(keys, mappingService);
+ return SettingsManager.EditorSettings.ShortcutSettingsDictionary.Values
+ .Count(settings => KeyboardManagerInterop.AreShortcutsEqual(settings.Shortcut.OriginalKeys, shortcutKeysString) &&
+ (string.IsNullOrEmpty(settings.Shortcut.TargetApp) || string.IsNullOrEmpty(appName) || settings.Shortcut.TargetApp == appName)) > upperLimit;
+ }
+
+ public static bool IsSelfMapping(List originalKeys, List remappedKeys, KeyboardMappingService mappingService)
+ {
+ if (mappingService == null || originalKeys == null || remappedKeys == null ||
+ originalKeys.Count == 0 || remappedKeys.Count == 0)
+ {
+ return false;
+ }
+
+ string originalKeysString = BuildKeyCodeString(originalKeys, mappingService);
+ string remappedKeysString = BuildKeyCodeString(remappedKeys, mappingService);
+
+ return KeyboardManagerInterop.AreShortcutsEqual(originalKeysString, remappedKeysString);
+ }
+
+ public static bool ContainsOnlyModifierKeys(List keys)
+ {
+ if (keys == null || keys.Count == 0)
+ {
+ return false;
+ }
+
+ return keys.All(key =>
+ {
+ int keyCode = KeyboardManagerInterop.GetKeyCodeFromName(key);
+ var keyType = (KeyType)KeyboardManagerInterop.GetKeyType(keyCode);
+ return keyType != KeyType.Action;
+ });
+ }
+
+ public static bool IsKeyOrphaned(int originalKey, KeyboardMappingService mappingService)
+ {
+ // Check single key mappings
+ foreach (var mapping in mappingService.GetSingleKeyMappings())
+ {
+ if (!mapping.IsShortcut && int.TryParse(mapping.TargetKey, out int targetKey) && targetKey == originalKey)
+ {
+ return false;
+ }
+ }
+
+ // Check shortcut mappings
+ foreach (var mapping in mappingService.GetShortcutMappings())
+ {
+ string[] targetKeys = mapping.TargetKeys.Split(';');
+ if (targetKeys.Length == 1 && int.TryParse(targetKeys[0], out int shortcutTargetKey) && shortcutTargetKey == originalKey)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static ValidationErrorType ValidateProgramOrUrlMapping(
+ List originalKeys,
+ bool isAppSpecific,
+ string appName,
+ KeyboardMappingService mappingService,
+ bool isEditMode = false,
+ Remapping? editingRemapping = null)
+ {
+ if (originalKeys.Count < 2)
+ {
+ return ValidationErrorType.OneKeyMapping;
+ }
+
+ ValidationErrorType error = ValidateKeyMapping(originalKeys, originalKeys, isAppSpecific, appName, mappingService, isEditMode, editingRemapping);
+
+ return error == ValidationErrorType.SelfMapping ? ValidationErrorType.NoError : error;
+ }
+
+ private static bool IsIllegalShortcut(List keys, KeyboardMappingService mappingService)
+ {
+ string shortcutKeysString = BuildKeyCodeString(keys, mappingService);
+ Logger.LogInfo($"Checking if shortcut is illegal: {shortcutKeysString}");
+ return KeyboardManagerInterop.IsShortcutIllegal(shortcutKeysString);
+ }
+
+ private static string BuildKeyCodeString(List keys, KeyboardMappingService mappingService)
+ {
+ return string.Join(";", keys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
+ }
+ }
+}
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyMapping.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyMapping.cs
new file mode 100644
index 0000000000..5c65da9706
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyMapping.cs
@@ -0,0 +1,21 @@
+// 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.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace KeyboardManagerEditorUI.Interop
+{
+ public class KeyMapping
+ {
+ public int OriginalKey { get; set; }
+
+ public string TargetKey { get; set; } = string.Empty;
+
+ public bool IsShortcut { get; set; }
+ }
+}
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyToTextMapping.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyToTextMapping.cs
new file mode 100644
index 0000000000..b5a51d0707
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyToTextMapping.cs
@@ -0,0 +1,19 @@
+// 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.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace KeyboardManagerEditorUI.Interop
+{
+ public class KeyToTextMapping
+ {
+ public int OriginalKey { get; set; }
+
+ public string TargetText { get; set; } = string.Empty;
+ }
+}
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyType.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyType.cs
new file mode 100644
index 0000000000..c36270bb1e
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyType.cs
@@ -0,0 +1,21 @@
+// 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.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace KeyboardManagerEditorUI.Interop
+{
+ public enum KeyType
+ {
+ Win = 0,
+ Ctrl = 1,
+ Alt = 2,
+ Shift = 3,
+ Action = 4,
+ }
+}
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyboardManagerInterop.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyboardManagerInterop.cs
new file mode 100644
index 0000000000..ee66add153
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyboardManagerInterop.cs
@@ -0,0 +1,165 @@
+// 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.Runtime.InteropServices;
+using System.Text;
+
+namespace KeyboardManagerEditorUI.Interop
+{
+ public static class KeyboardManagerInterop
+ {
+ private const string DllName = "Powertoys.KeyboardManagerEditorLibraryWrapper.dll";
+ private const CallingConvention Convention = CallingConvention.Cdecl;
+
+ // Configuration Management
+ [DllImport(DllName, CallingConvention = Convention)]
+ internal static extern IntPtr CreateMappingConfiguration();
+
+ [DllImport(DllName, CallingConvention = Convention)]
+ internal static extern void DestroyMappingConfiguration(IntPtr config);
+
+ [DllImport(DllName, CallingConvention = Convention)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool LoadMappingSettings(IntPtr config);
+
+ [DllImport(DllName, CallingConvention = Convention)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool SaveMappingSettings(IntPtr config);
+
+ // Get Mapping Functions
+ [DllImport(DllName, CallingConvention = Convention)]
+ internal static extern int GetSingleKeyRemapCount(IntPtr config);
+
+ [DllImport(DllName, CallingConvention = Convention)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool GetSingleKeyRemap(IntPtr config, int index, ref SingleKeyMapping mapping);
+
+ [DllImport(DllName, CallingConvention = Convention)]
+ internal static extern int GetSingleKeyToTextRemapCount(IntPtr config);
+
+ [DllImport(DllName, CallingConvention = Convention)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool GetSingleKeyToTextRemap(IntPtr config, int index, ref KeyboardTextMapping mapping);
+
+ [DllImport(DllName, CallingConvention = Convention)]
+ internal static extern int GetShortcutRemapCount(IntPtr config);
+
+ [DllImport(DllName, CallingConvention = Convention)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool GetShortcutRemap(IntPtr config, int index, ref ShortcutMapping mapping);
+
+ [DllImport(DllName, CallingConvention = Convention)]
+ internal static extern int GetShortcutRemapCountByType(IntPtr config, int operationType);
+
+ [DllImport(DllName, CallingConvention = Convention)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool GetShortcutRemapByType(IntPtr config, int operationType, int index, ref ShortcutMapping mapping);
+
+ // Add Mapping Functions
+ [DllImport(DllName, CallingConvention = Convention)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool AddSingleKeyRemap(IntPtr config, int originalKey, int targetKey);
+
+ [DllImport(DllName, CallingConvention = Convention, CharSet = CharSet.Unicode)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool AddSingleKeyToTextRemap(IntPtr config, int originalKey, [MarshalAs(UnmanagedType.LPWStr)] string targetText);
+
+ [DllImport(DllName, CallingConvention = Convention, CharSet = CharSet.Unicode)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool AddSingleKeyToShortcutRemap(IntPtr config, int originalKey, [MarshalAs(UnmanagedType.LPWStr)] string targetKeys);
+
+ [DllImport(DllName, CallingConvention = Convention, CharSet = CharSet.Unicode)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool AddShortcutRemap(
+ IntPtr config,
+ [MarshalAs(UnmanagedType.LPWStr)] string originalKeys,
+ [MarshalAs(UnmanagedType.LPWStr)] string targetKeys,
+ [MarshalAs(UnmanagedType.LPWStr)] string targetApp,
+ int operationType = 0,
+ [MarshalAs(UnmanagedType.LPWStr)] string appPathOrUri = "",
+ [MarshalAs(UnmanagedType.LPWStr)] string? args = null,
+ [MarshalAs(UnmanagedType.LPWStr)] string? startDirectory = null,
+ int elevation = 0,
+ int ifRunningAction = 0,
+ int visibility = 0);
+
+ // Delete Mapping Functions
+ [DllImport(DllName, CallingConvention = Convention)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool DeleteSingleKeyRemap(IntPtr mappingConfiguration, int originalKey);
+
+ [DllImport(DllName, CallingConvention = Convention)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool DeleteSingleKeyToTextRemap(IntPtr config, int originalKey);
+
+ [DllImport(DllName, CallingConvention = Convention, CharSet = CharSet.Unicode)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool DeleteShortcutRemap(IntPtr mappingConfiguration, [MarshalAs(UnmanagedType.LPWStr)] string originalKeys, [MarshalAs(UnmanagedType.LPWStr)] string targetApp);
+
+ // Key Utility Functions
+ [DllImport(DllName, CallingConvention = Convention, CharSet = CharSet.Unicode)]
+ internal static extern int GetKeyCodeFromName([MarshalAs(UnmanagedType.LPWStr)] string keyName);
+
+ [DllImport(DllName, CallingConvention = Convention, CharSet = CharSet.Unicode)]
+ internal static extern void GetKeyDisplayName(int keyCode, [Out] StringBuilder keyName, int maxLength);
+
+ [DllImport(DllName, CallingConvention = Convention)]
+ internal static extern int GetKeyType(int keyCode);
+
+ // Validation Functions
+ [DllImport(DllName, CallingConvention = Convention, CharSet = CharSet.Unicode)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool IsShortcutIllegal([MarshalAs(UnmanagedType.LPWStr)] string shortcutKeys);
+
+ [DllImport(DllName, CallingConvention = Convention, CharSet = CharSet.Unicode)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static extern bool AreShortcutsEqual([MarshalAs(UnmanagedType.LPWStr)] string lShort, [MarshalAs(UnmanagedType.LPWStr)] string rShortcut);
+
+ // String Management Functions
+ [DllImport(DllName, CallingConvention = Convention)]
+ internal static extern void FreeString(IntPtr str);
+
+ public static string GetStringAndFree(IntPtr handle)
+ {
+ if (handle == IntPtr.Zero)
+ {
+ return string.Empty;
+ }
+
+ string? result = Marshal.PtrToStringUni(handle);
+ FreeString(handle);
+ return result ?? string.Empty;
+ }
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ public struct SingleKeyMapping
+ {
+ public int OriginalKey;
+ public IntPtr TargetKey;
+ [MarshalAs(UnmanagedType.Bool)]
+ public bool IsShortcut;
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ public struct KeyboardTextMapping
+ {
+ public int OriginalKey;
+ public IntPtr TargetText;
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ public struct ShortcutMapping
+ {
+ public IntPtr OriginalKeys;
+ public IntPtr TargetKeys;
+ public IntPtr TargetApp;
+ public int OperationType;
+ public IntPtr TargetText;
+ public IntPtr ProgramPath;
+ public IntPtr ProgramArgs;
+ public IntPtr UriToOpen;
+ }
+}
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyboardMappingService.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyboardMappingService.cs
new file mode 100644
index 0000000000..f4c2e6a696
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/KeyboardMappingService.cs
@@ -0,0 +1,296 @@
+// 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.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+using ManagedCommon;
+
+namespace KeyboardManagerEditorUI.Interop
+{
+ public class KeyboardMappingService : IDisposable
+ {
+ private IntPtr _configHandle;
+ private bool _disposed;
+
+ public KeyboardMappingService()
+ {
+ _configHandle = KeyboardManagerInterop.CreateMappingConfiguration();
+ if (_configHandle == IntPtr.Zero)
+ {
+ Logger.LogError("Failed to create mapping configuration");
+ throw new InvalidOperationException("Failed to create mapping configuration");
+ }
+
+ KeyboardManagerInterop.LoadMappingSettings(_configHandle);
+ }
+
+ public List GetSingleKeyMappings()
+ {
+ var result = new List();
+ int count = KeyboardManagerInterop.GetSingleKeyRemapCount(_configHandle);
+
+ for (int i = 0; i < count; i++)
+ {
+ var mapping = default(SingleKeyMapping);
+ if (KeyboardManagerInterop.GetSingleKeyRemap(_configHandle, i, ref mapping))
+ {
+ result.Add(new KeyMapping
+ {
+ OriginalKey = mapping.OriginalKey,
+ TargetKey = KeyboardManagerInterop.GetStringAndFree(mapping.TargetKey),
+ IsShortcut = mapping.IsShortcut,
+ });
+ }
+ }
+
+ return result;
+ }
+
+ public List GetShortcutMappings()
+ {
+ var result = new List();
+ int count = KeyboardManagerInterop.GetShortcutRemapCount(_configHandle);
+
+ for (int i = 0; i < count; i++)
+ {
+ var mapping = default(ShortcutMapping);
+ if (KeyboardManagerInterop.GetShortcutRemap(_configHandle, i, ref mapping))
+ {
+ result.Add(new ShortcutKeyMapping
+ {
+ OriginalKeys = KeyboardManagerInterop.GetStringAndFree(mapping.OriginalKeys),
+ TargetKeys = KeyboardManagerInterop.GetStringAndFree(mapping.TargetKeys),
+ TargetApp = KeyboardManagerInterop.GetStringAndFree(mapping.TargetApp),
+ OperationType = (ShortcutOperationType)mapping.OperationType,
+ TargetText = KeyboardManagerInterop.GetStringAndFree(mapping.TargetText),
+ ProgramPath = KeyboardManagerInterop.GetStringAndFree(mapping.ProgramPath),
+ ProgramArgs = KeyboardManagerInterop.GetStringAndFree(mapping.ProgramArgs),
+ UriToOpen = KeyboardManagerInterop.GetStringAndFree(mapping.UriToOpen),
+ });
+ }
+ }
+
+ return result;
+ }
+
+ public List GetShortcutMappingsByType(ShortcutOperationType operationType)
+ {
+ var result = new List();
+ int count = KeyboardManagerInterop.GetShortcutRemapCountByType(_configHandle, (int)operationType);
+
+ for (int i = 0; i < count; i++)
+ {
+ var mapping = default(ShortcutMapping);
+ if (KeyboardManagerInterop.GetShortcutRemapByType(_configHandle, (int)operationType, i, ref mapping))
+ {
+ result.Add(new ShortcutKeyMapping
+ {
+ OriginalKeys = KeyboardManagerInterop.GetStringAndFree(mapping.OriginalKeys),
+ TargetKeys = KeyboardManagerInterop.GetStringAndFree(mapping.TargetKeys),
+ TargetApp = KeyboardManagerInterop.GetStringAndFree(mapping.TargetApp),
+ OperationType = (ShortcutOperationType)mapping.OperationType,
+ TargetText = KeyboardManagerInterop.GetStringAndFree(mapping.TargetText),
+ ProgramPath = KeyboardManagerInterop.GetStringAndFree(mapping.ProgramPath),
+ ProgramArgs = KeyboardManagerInterop.GetStringAndFree(mapping.ProgramArgs),
+ UriToOpen = KeyboardManagerInterop.GetStringAndFree(mapping.UriToOpen),
+ });
+ }
+ }
+
+ return result;
+ }
+
+ public List GetKeyToTextMappings()
+ {
+ var result = new List();
+ int count = KeyboardManagerInterop.GetSingleKeyToTextRemapCount(_configHandle);
+
+ for (int i = 0; i < count; i++)
+ {
+ var mapping = default(KeyboardTextMapping);
+ if (KeyboardManagerInterop.GetSingleKeyToTextRemap(_configHandle, i, ref mapping))
+ {
+ result.Add(new KeyToTextMapping
+ {
+ OriginalKey = mapping.OriginalKey,
+ TargetText = KeyboardManagerInterop.GetStringAndFree(mapping.TargetText),
+ });
+ }
+ }
+
+ return result;
+ }
+
+ public string GetKeyDisplayName(int keyCode)
+ {
+ var keyName = new StringBuilder(64);
+ KeyboardManagerInterop.GetKeyDisplayName(keyCode, keyName, keyName.Capacity);
+ return keyName.ToString();
+ }
+
+ public int GetKeyCodeFromName(string keyName)
+ {
+ if (string.IsNullOrEmpty(keyName))
+ {
+ return 0;
+ }
+
+ int keyCode = KeyboardManagerInterop.GetKeyCodeFromName(keyName);
+ Logger.LogInfo($"Key code for key name {keyName}: {keyCode}");
+ return keyCode;
+ }
+
+ public bool AddSingleKeyMapping(int originalKey, int targetKey)
+ {
+ return KeyboardManagerInterop.AddSingleKeyRemap(_configHandle, originalKey, targetKey);
+ }
+
+ public bool AddSingleKeyMapping(int originalKey, string targetKeys)
+ {
+ if (string.IsNullOrEmpty(targetKeys))
+ {
+ return false;
+ }
+
+ if (!targetKeys.Contains(';') && int.TryParse(targetKeys, out int targetKey))
+ {
+ return KeyboardManagerInterop.AddSingleKeyRemap(_configHandle, originalKey, targetKey);
+ }
+ else
+ {
+ return KeyboardManagerInterop.AddSingleKeyToShortcutRemap(_configHandle, originalKey, targetKeys);
+ }
+ }
+
+ public bool AddSingleKeyToTextMapping(int originalKey, string targetText)
+ {
+ if (string.IsNullOrEmpty(targetText))
+ {
+ return false;
+ }
+
+ return KeyboardManagerInterop.AddSingleKeyToTextRemap(_configHandle, originalKey, targetText);
+ }
+
+ public bool AddShortcutMapping(string originalKeys, string targetKeys, string targetApp = "", ShortcutOperationType operationType = ShortcutOperationType.RemapShortcut)
+ {
+ if (string.IsNullOrEmpty(originalKeys) || string.IsNullOrEmpty(targetKeys))
+ {
+ return false;
+ }
+
+ return KeyboardManagerInterop.AddShortcutRemap(_configHandle, originalKeys, targetKeys, targetApp, (int)operationType);
+ }
+
+ public bool AddShortcutMapping(ShortcutKeyMapping shortcutKeyMapping)
+ {
+ if (string.IsNullOrEmpty(shortcutKeyMapping.OriginalKeys) || string.IsNullOrEmpty(shortcutKeyMapping.TargetKeys))
+ {
+ return false;
+ }
+
+ if (shortcutKeyMapping.OperationType == ShortcutOperationType.RunProgram && string.IsNullOrEmpty(shortcutKeyMapping.ProgramPath))
+ {
+ return false;
+ }
+
+ if (shortcutKeyMapping.OperationType == ShortcutOperationType.OpenUri && string.IsNullOrEmpty(shortcutKeyMapping.UriToOpen))
+ {
+ return false;
+ }
+
+ if (shortcutKeyMapping.OperationType == ShortcutOperationType.RunProgram)
+ {
+ return KeyboardManagerInterop.AddShortcutRemap(
+ _configHandle,
+ shortcutKeyMapping.OriginalKeys,
+ shortcutKeyMapping.TargetKeys,
+ shortcutKeyMapping.TargetApp,
+ (int)shortcutKeyMapping.OperationType,
+ shortcutKeyMapping.ProgramPath,
+ string.IsNullOrEmpty(shortcutKeyMapping.ProgramArgs) ? null : shortcutKeyMapping.ProgramArgs,
+ string.IsNullOrEmpty(shortcutKeyMapping.StartInDirectory) ? null : shortcutKeyMapping.StartInDirectory,
+ (int)shortcutKeyMapping.Elevation,
+ (int)shortcutKeyMapping.IfRunningAction,
+ (int)shortcutKeyMapping.Visibility);
+ }
+ else if (shortcutKeyMapping.OperationType == ShortcutOperationType.OpenUri)
+ {
+ return KeyboardManagerInterop.AddShortcutRemap(
+ _configHandle,
+ shortcutKeyMapping.OriginalKeys,
+ shortcutKeyMapping.TargetKeys,
+ shortcutKeyMapping.TargetApp,
+ (int)shortcutKeyMapping.OperationType,
+ shortcutKeyMapping.UriToOpen);
+ }
+
+ return KeyboardManagerInterop.AddShortcutRemap(
+ _configHandle,
+ shortcutKeyMapping.OriginalKeys,
+ shortcutKeyMapping.TargetKeys,
+ shortcutKeyMapping.TargetApp,
+ (int)shortcutKeyMapping.OperationType);
+ }
+
+ public bool SaveSettings()
+ {
+ return KeyboardManagerInterop.SaveMappingSettings(_configHandle);
+ }
+
+ public bool DeleteSingleKeyMapping(int originalKey)
+ {
+ return KeyboardManagerInterop.DeleteSingleKeyRemap(_configHandle, originalKey);
+ }
+
+ public bool DeleteSingleKeyToTextMapping(int originalKey)
+ {
+ if (originalKey == 0)
+ {
+ return false;
+ }
+
+ return KeyboardManagerInterop.DeleteSingleKeyToTextRemap(_configHandle, originalKey);
+ }
+
+ public bool DeleteShortcutMapping(string originalKeys, string targetApp = "")
+ {
+ if (string.IsNullOrEmpty(originalKeys))
+ {
+ return false;
+ }
+
+ return KeyboardManagerInterop.DeleteShortcutRemap(_configHandle, originalKeys, targetApp ?? string.Empty);
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (_configHandle != IntPtr.Zero)
+ {
+ KeyboardManagerInterop.DestroyMappingConfiguration(_configHandle);
+ _configHandle = IntPtr.Zero;
+ }
+
+ _disposed = true;
+ }
+ }
+
+ ~KeyboardMappingService()
+ {
+ Dispose(false);
+ }
+ }
+}
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/ShortcutKeyMapping.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/ShortcutKeyMapping.cs
new file mode 100644
index 0000000000..8d51d8fbb0
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/ShortcutKeyMapping.cs
@@ -0,0 +1,103 @@
+// 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.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace KeyboardManagerEditorUI.Interop
+{
+ public class ShortcutKeyMapping
+ {
+ public string OriginalKeys { get; set; } = string.Empty;
+
+ public string TargetKeys { get; set; } = string.Empty;
+
+ public string TargetApp { get; set; } = string.Empty;
+
+ public ShortcutOperationType OperationType { get; set; }
+
+ public string TargetText { get; set; } = string.Empty;
+
+ public string ProgramPath { get; set; } = string.Empty;
+
+ public string ProgramArgs { get; set; } = string.Empty;
+
+ public string StartInDirectory { get; set; } = string.Empty;
+
+ public ElevationLevel Elevation { get; set; } = ElevationLevel.NonElevated;
+
+ public ProgramAlreadyRunningAction IfRunningAction { get; set; } = ProgramAlreadyRunningAction.ShowWindow;
+
+ public StartWindowType Visibility { get; set; } = StartWindowType.Normal;
+
+ public string UriToOpen { get; set; } = string.Empty;
+
+ public enum ElevationLevel
+ {
+ NonElevated = 0,
+ Elevated = 1,
+ DifferentUser = 2,
+ }
+
+ public enum StartWindowType
+ {
+ Normal = 0,
+ Hidden = 1,
+ Minimized = 2,
+ Maximized = 3,
+ }
+
+ public enum ProgramAlreadyRunningAction
+ {
+ ShowWindow = 0,
+ StartAnother = 1,
+ DoNothing = 2,
+ Close = 3,
+ EndTask = 4,
+ CloseAndEndTask = 5,
+ }
+
+ public override bool Equals(object? obj)
+ {
+ if (obj is not ShortcutKeyMapping other)
+ {
+ return false;
+ }
+
+ return OriginalKeys == other.OriginalKeys &&
+ TargetKeys == other.TargetKeys &&
+ TargetApp == other.TargetApp &&
+ OperationType == other.OperationType &&
+ TargetText == other.TargetText &&
+ ProgramPath == other.ProgramPath &&
+ ProgramArgs == other.ProgramArgs &&
+ StartInDirectory == other.StartInDirectory &&
+ Elevation == other.Elevation &&
+ IfRunningAction == other.IfRunningAction &&
+ Visibility == other.Visibility &&
+ UriToOpen == other.UriToOpen;
+ }
+
+ public override int GetHashCode()
+ {
+ HashCode hash = default(HashCode);
+ hash.Add(OriginalKeys);
+ hash.Add(TargetKeys);
+ hash.Add(TargetApp);
+ hash.Add(OperationType);
+ hash.Add(TargetText);
+ hash.Add(ProgramPath);
+ hash.Add(ProgramArgs);
+ hash.Add(StartInDirectory);
+ hash.Add(Elevation);
+ hash.Add(IfRunningAction);
+ hash.Add(Visibility);
+ hash.Add(UriToOpen);
+ return hash.ToHashCode();
+ }
+ }
+}
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/ShortcutOperationType.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/ShortcutOperationType.cs
new file mode 100644
index 0000000000..4a7472d1b7
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Interop/ShortcutOperationType.cs
@@ -0,0 +1,20 @@
+// 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.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace KeyboardManagerEditorUI.Interop
+{
+ public enum ShortcutOperationType
+ {
+ RemapShortcut = 0,
+ RunProgram = 1,
+ OpenUri = 2,
+ RemapText = 3,
+ }
+}
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/KeyboardManagerEditorUI.csproj b/src/modules/keyboardmanager/KeyboardManagerEditorUI/KeyboardManagerEditorUI.csproj
index b71aa65515..5a09a82766 100644
--- a/src/modules/keyboardmanager/KeyboardManagerEditorUI/KeyboardManagerEditorUI.csproj
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/KeyboardManagerEditorUI.csproj
@@ -8,16 +8,36 @@
KeyboardManagerEditorUI
app.manifest
true
- true
+ true
enable
None
True
false
false
PowerToys.KeyboardManagerEditorUI
- $(RepoRoot)$(Platform)\$(Configuration)\$(MSBuildProjectName)
+ ..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps
+
+ PowerToys.KeyboardManagerEditorUI.pri
+
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -27,12 +47,16 @@
Tools extension to be activated for this project even if the Windows App SDK Nuget
package has not yet been restored.
-->
-
+
-
-
+
+
+
+
+
+
@@ -40,9 +64,26 @@
+
+
-
+
+ Always
+
+
+ Always
+
+
+
+
+ MSBuild:Compile
+
+
+
+
+ MSBuild:Compile
+
-
+
true
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/KeyboardManagerEditorXAML/App.xaml b/src/modules/keyboardmanager/KeyboardManagerEditorUI/KeyboardManagerEditorXAML/App.xaml
new file mode 100644
index 0000000000..796c0d86ca
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/KeyboardManagerEditorXAML/App.xaml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 960
+
+ M12.001 2C17.5238 2 22.001 6.47715 22.001 12C22.001 17.5228 17.5238 22 12.001 22C6.47813 22 2.00098 17.5228 2.00098 12C2.00098 6.47715 6.47813 2 12.001 2ZM12.7813 7.46897L12.6972 7.39635C12.4362 7.2027 12.078 7.20031 11.8146 7.38918L11.7206 7.46897L11.648 7.55308C11.4544 7.81407 11.452 8.17229 11.6409 8.43568L11.7206 8.52963L14.4403 11.2493H7.75027L7.6485 11.2561C7.31571 11.3013 7.05227 11.5647 7.00712 11.8975L7.00027 11.9993L7.00712 12.1011C7.05227 12.4339 7.31571 12.6973 7.6485 12.7424L7.75027 12.7493H14.4403L11.72 15.4697L11.6474 15.5538C11.4295 15.8474 11.4536 16.264 11.7198 16.5303C11.9861 16.7967 12.4027 16.8209 12.6964 16.6032L12.7805 16.5306L16.782 12.5306L16.8547 12.4464C17.0484 12.1854 17.0508 11.8272 16.8619 11.5638L16.7821 11.4698L12.7813 7.46897L12.6972 7.39635L12.7813 7.46897Z
+
+
+
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/App.xaml.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/KeyboardManagerEditorXAML/App.xaml.cs
similarity index 57%
rename from src/modules/keyboardmanager/KeyboardManagerEditorUI/App.xaml.cs
rename to src/modules/keyboardmanager/KeyboardManagerEditorUI/KeyboardManagerEditorXAML/App.xaml.cs
index 88b7f7996c..17930550bc 100644
--- a/src/modules/keyboardmanager/KeyboardManagerEditorUI/App.xaml.cs
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/KeyboardManagerEditorXAML/App.xaml.cs
@@ -7,7 +7,12 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
+using System.Threading.Tasks;
+using KeyboardManagerEditorUI.Helpers;
+using KeyboardManagerEditorUI.Settings;
using ManagedCommon;
+using Microsoft.UI;
+using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
@@ -29,14 +34,22 @@ namespace KeyboardManagerEditorUI
public partial class App : Application
{
///
+ /// Initializes a new instance of the class.
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
///
public App()
{
this.InitializeComponent();
- Logger.InitializeLogger("\\Keyboard Manager\\WinUI3Editor\\Logs");
- Logger.LogInfo("keyboard-manager WinUI3 editor logger is initialized");
+
+ Task.Run(() =>
+ {
+ Logger.InitializeLogger("\\Keyboard Manager\\WinUI3Editor\\Logs");
+ });
+
+ UnhandledException += App_UnhandledException;
+
+ SettingsManager.CorrelateServiceAndEditorMappings();
}
///
@@ -45,11 +58,28 @@ namespace KeyboardManagerEditorUI
/// Details about the launch request and process.
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
- window = new MainWindow();
- window.Activate();
+ MainWindow = new MainWindow();
+
+ MainWindow.DispatcherQueue.TryEnqueue(() =>
+ {
+ MainWindow.Activate();
+ MainWindow.DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
+ {
+ (MainWindow.Content as FrameworkElement)?.UpdateLayout();
+ });
+ });
+
Logger.LogInfo("keyboard-manager WinUI3 editor window is launched");
}
- private Window? window;
+ ///
+ /// Log the unhandled exception for the editor.
+ ///
+ private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
+ {
+ Logger.LogError("Unhandled exception", e.Exception);
+ }
+
+ internal static MainWindow MainWindow { get; private set; } = null!;
}
}
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/KeyboardManagerEditorXAML/MainWindow.xaml b/src/modules/keyboardmanager/KeyboardManagerEditorUI/KeyboardManagerEditorXAML/MainWindow.xaml
new file mode 100644
index 0000000000..1e4b99e8a3
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/KeyboardManagerEditorXAML/MainWindow.xaml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/KeyboardManagerEditorXAML/MainWindow.xaml.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/KeyboardManagerEditorXAML/MainWindow.xaml.cs
new file mode 100644
index 0000000000..9e9ccb6cf2
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/KeyboardManagerEditorXAML/MainWindow.xaml.cs
@@ -0,0 +1,62 @@
+// 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.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.WindowsRuntime;
+using KeyboardManagerEditorUI.Helpers;
+using Microsoft.UI;
+using Microsoft.UI.Windowing;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Controls.Primitives;
+using Microsoft.UI.Xaml.Data;
+using Microsoft.UI.Xaml.Input;
+using Microsoft.UI.Xaml.Media;
+using Microsoft.UI.Xaml.Navigation;
+using Windows.Foundation;
+using Windows.Foundation.Collections;
+using WinUIEx;
+
+namespace KeyboardManagerEditorUI
+{
+ public sealed partial class MainWindow : WindowEx
+ {
+ public MainWindow()
+ {
+ this.InitializeComponent();
+ SetTitleBar();
+ this.Activated += MainWindow_Activated;
+ this.Closed += MainWindow_Closed;
+ }
+
+ private void SetTitleBar()
+ {
+ ExtendsContentIntoTitleBar = true;
+ this.SetIcon(@"Assets\KeyboardManagerEditor\Keyboard.ico");
+ this.SetTitleBar(titleBar);
+ Title = "Keyboard Manager";
+ }
+
+ private void MainWindow_Activated(object sender, WindowActivatedEventArgs args)
+ {
+ if (args.WindowActivationState == WindowActivationState.Deactivated)
+ {
+ // Release the keyboard hook when the window is deactivated
+ KeyboardHookHelper.Instance.CleanupHook();
+ }
+ }
+
+ private void MainWindow_Closed(object sender, WindowEventArgs args)
+ {
+ KeyboardHookHelper.Instance.Dispose();
+ this.Activated -= MainWindow_Activated;
+ this.Closed -= MainWindow_Closed;
+ }
+ }
+}
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/MainWindow.xaml b/src/modules/keyboardmanager/KeyboardManagerEditorUI/MainWindow.xaml
deleted file mode 100644
index 43311b4618..0000000000
--- a/src/modules/keyboardmanager/KeyboardManagerEditorUI/MainWindow.xaml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/MainWindow.xaml.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/MainWindow.xaml.cs
deleted file mode 100644
index 2fa4779c19..0000000000
--- a/src/modules/keyboardmanager/KeyboardManagerEditorUI/MainWindow.xaml.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-// 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.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Runtime.InteropServices;
-using System.Runtime.InteropServices.WindowsRuntime;
-using Microsoft.UI.Xaml;
-using Microsoft.UI.Xaml.Controls;
-using Microsoft.UI.Xaml.Controls.Primitives;
-using Microsoft.UI.Xaml.Data;
-using Microsoft.UI.Xaml.Input;
-using Microsoft.UI.Xaml.Media;
-using Microsoft.UI.Xaml.Navigation;
-using Windows.Foundation;
-using Windows.Foundation.Collections;
-
-namespace KeyboardManagerEditorUI
-{
- ///
- /// An empty window that can be used on its own or navigated to within a Frame.
- ///
- public sealed partial class MainWindow : Window
- {
- [DllImport("KeyboardManagerEditorLibraryWrapper.dll", CallingConvention = CallingConvention.Cdecl)]
- private static extern bool CheckIfRemappingsAreValid();
-
- public MainWindow()
- {
- this.InitializeComponent();
- }
-
- private void MyButton_Click(object sender, RoutedEventArgs e)
- {
- // Call the C++ function to check if the current remappings are valid
- myButton.Content = CheckIfRemappingsAreValid() ? "Valid" : "Invalid";
- }
- }
-}
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Package.appxmanifest b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Package.appxmanifest
index a85dcbffdb..f131ae118c 100644
--- a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Package.appxmanifest
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Package.appxmanifest
@@ -15,9 +15,9 @@
- KeyboardManagerEditorUI
+ Keyboard Manager
haoliuu
- Assets\StoreLogo.png
+ Assets\KeyboardManagerEditor\StoreLogo.png
@@ -34,13 +34,13 @@
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
-
-
+ Square150x150Logo="Assets\KeyboardManagerEditor\Square150x150Logo.png"
+ Square44x44Logo="Assets\KeyboardManagerEditor\Square44x44Logo.png">
+
+
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/MainPage.xaml b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/MainPage.xaml
new file mode 100644
index 0000000000..7fbc97502e
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/MainPage.xaml
@@ -0,0 +1,548 @@
+
+
+
+
+ 800
+ 800
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/MainPage.xaml.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/MainPage.xaml.cs
new file mode 100644
index 0000000000..e6b345d1b5
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/MainPage.xaml.cs
@@ -0,0 +1,897 @@
+// 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.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Globalization;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using KeyboardManagerEditorUI.Controls;
+using KeyboardManagerEditorUI.Helpers;
+using KeyboardManagerEditorUI.Interop;
+using KeyboardManagerEditorUI.Settings;
+using ManagedCommon;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using static KeyboardManagerEditorUI.Interop.ShortcutKeyMapping;
+
+namespace KeyboardManagerEditorUI.Pages
+{
+ ///
+ /// A consolidated page that displays all mappings from Remappings, Text, Programs, and URLs pages.
+ ///
+#pragma warning disable SA1124 // Do not use regions
+ public sealed partial class MainPage : Page, IDisposable, INotifyPropertyChanged
+ {
+ private KeyboardMappingService? _mappingService;
+ private bool _disposed;
+ private bool _isEditMode;
+ private EditingItem? _editingItem;
+ private string _mappingState = "Empty";
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ public string MappingState
+ {
+ get => _mappingState;
+ private set
+ {
+ if (_mappingState != value)
+ {
+ _mappingState = value;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(MappingState)));
+ }
+ }
+ }
+
+ public ObservableCollection RemappingList { get; } = new();
+
+ public ObservableCollection TextMappings { get; } = new();
+
+ public ObservableCollection ProgramShortcuts { get; } = new();
+
+ public ObservableCollection UrlShortcuts { get; } = new();
+
+ [DllImport("PowerToys.KeyboardManagerEditorLibraryWrapper.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
+ private static extern void GetKeyDisplayName(int keyCode, [Out] StringBuilder keyName, int maxLength);
+
+ private sealed class EditingItem
+ {
+ public enum ItemType
+ {
+ Remapping,
+ TextMapping,
+ ProgramShortcut,
+ UrlShortcut,
+ }
+
+ public ItemType Type { get; set; }
+
+ public object Item { get; set; } = null!;
+
+ public List OriginalTriggerKeys { get; set; } = new();
+
+ public string? AppName { get; set; }
+
+ public bool IsAllApps { get; set; } = true;
+ }
+
+ public MainPage()
+ {
+ this.InitializeComponent();
+
+ try
+ {
+ _mappingService = new KeyboardMappingService();
+ LoadAllMappings();
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Failed to initialize KeyboardMappingService in MainPage page: " + ex.Message);
+ }
+
+ Unloaded += All_Unloaded;
+ }
+
+ private void All_Unloaded(object sender, RoutedEventArgs e) => Dispose();
+
+ #region Dialog Show Methods
+
+ private async void NewRemappingBtn_Click(object sender, RoutedEventArgs e)
+ {
+ _isEditMode = false;
+ _editingItem = null;
+ UnifiedMappingControl.Reset();
+ RemappingDialog.Title = "New remapping";
+ await ShowRemappingDialog();
+ }
+
+ private async void RemappingsList_ItemClick(object sender, ItemClickEventArgs e)
+ {
+ if (e.ClickedItem is not Remapping remapping)
+ {
+ return;
+ }
+
+ _isEditMode = true;
+ _editingItem = new EditingItem
+ {
+ Type = EditingItem.ItemType.Remapping,
+ Item = remapping,
+ OriginalTriggerKeys = remapping.Shortcut.ToList(),
+ AppName = remapping.AppName,
+ IsAllApps = remapping.IsAllApps,
+ };
+
+ UnifiedMappingControl.Reset();
+ UnifiedMappingControl.SetTriggerKeys(remapping.Shortcut.ToList());
+ UnifiedMappingControl.SetActionType(UnifiedMappingControl.ActionType.KeyOrShortcut);
+ UnifiedMappingControl.SetActionKeys(remapping.RemappedKeys.ToList());
+ UnifiedMappingControl.SetAppSpecific(!remapping.IsAllApps, remapping.AppName);
+ RemappingDialog.Title = "Edit remapping";
+ await ShowRemappingDialog();
+ }
+
+ private async void TextMappingsList_ItemClick(object sender, ItemClickEventArgs e)
+ {
+ if (e.ClickedItem is not TextMapping textMapping)
+ {
+ return;
+ }
+
+ _isEditMode = true;
+ _editingItem = new EditingItem
+ {
+ Type = EditingItem.ItemType.TextMapping,
+ Item = textMapping,
+ OriginalTriggerKeys = textMapping.Shortcut.ToList(),
+ AppName = textMapping.AppName,
+ IsAllApps = textMapping.IsAllApps,
+ };
+
+ UnifiedMappingControl.Reset();
+ UnifiedMappingControl.SetTriggerKeys(textMapping.Shortcut.ToList());
+ UnifiedMappingControl.SetActionType(UnifiedMappingControl.ActionType.Text);
+ UnifiedMappingControl.SetTextContent(textMapping.Text);
+ UnifiedMappingControl.SetAppSpecific(!textMapping.IsAllApps, textMapping.AppName);
+ RemappingDialog.Title = "Edit remapping";
+ await ShowRemappingDialog();
+ }
+
+ private async void ProgramShortcutsList_ItemClick(object sender, ItemClickEventArgs e)
+ {
+ if (e.ClickedItem is not ProgramShortcut programShortcut)
+ {
+ return;
+ }
+
+ _isEditMode = true;
+ _editingItem = new EditingItem
+ {
+ Type = EditingItem.ItemType.ProgramShortcut,
+ Item = programShortcut,
+ OriginalTriggerKeys = programShortcut.Shortcut.ToList(),
+ AppName = programShortcut.AppName,
+ IsAllApps = programShortcut.IsAllApps,
+ };
+
+ UnifiedMappingControl.Reset();
+ UnifiedMappingControl.SetTriggerKeys(programShortcut.Shortcut.ToList());
+ UnifiedMappingControl.SetActionType(UnifiedMappingControl.ActionType.OpenApp);
+ UnifiedMappingControl.SetProgramPath(programShortcut.AppToRun);
+ UnifiedMappingControl.SetProgramArgs(programShortcut.Args);
+
+ if (!string.IsNullOrEmpty(programShortcut.Id) &&
+ SettingsManager.EditorSettings.ShortcutSettingsDictionary.TryGetValue(programShortcut.Id, out var settings))
+ {
+ var mapping = settings.Shortcut;
+ UnifiedMappingControl.SetStartInDirectory(mapping.StartInDirectory);
+ UnifiedMappingControl.SetElevationLevel(mapping.Elevation);
+ UnifiedMappingControl.SetVisibility(mapping.Visibility);
+ UnifiedMappingControl.SetIfRunningAction(mapping.IfRunningAction);
+ }
+
+ UnifiedMappingControl.SetAppSpecific(!programShortcut.IsAllApps, programShortcut.AppName);
+ RemappingDialog.Title = "Edit remapping";
+ await ShowRemappingDialog();
+ }
+
+ private async void UrlShortcutsList_ItemClick(object sender, ItemClickEventArgs e)
+ {
+ if (e.ClickedItem is not URLShortcut urlShortcut)
+ {
+ return;
+ }
+
+ _isEditMode = true;
+ _editingItem = new EditingItem
+ {
+ Type = EditingItem.ItemType.UrlShortcut,
+ Item = urlShortcut,
+ OriginalTriggerKeys = urlShortcut.Shortcut.ToList(),
+ AppName = urlShortcut.AppName,
+ IsAllApps = urlShortcut.IsAllApps,
+ };
+
+ UnifiedMappingControl.Reset();
+ UnifiedMappingControl.SetTriggerKeys(urlShortcut.Shortcut.ToList());
+ UnifiedMappingControl.SetActionType(UnifiedMappingControl.ActionType.OpenUrl);
+ UnifiedMappingControl.SetUrl(urlShortcut.URL);
+ UnifiedMappingControl.SetAppSpecific(!urlShortcut.IsAllApps, urlShortcut.AppName);
+ RemappingDialog.Title = "Edit remapping";
+ await ShowRemappingDialog();
+ }
+
+ private async System.Threading.Tasks.Task ShowRemappingDialog()
+ {
+ RemappingDialog.PrimaryButtonClick += RemappingDialog_PrimaryButtonClick;
+ UnifiedMappingControl.ValidationStateChanged += UnifiedMappingControl_ValidationStateChanged;
+ RemappingDialog.IsPrimaryButtonEnabled = UnifiedMappingControl.IsInputComplete();
+
+ await RemappingDialog.ShowAsync();
+
+ RemappingDialog.PrimaryButtonClick -= RemappingDialog_PrimaryButtonClick;
+ UnifiedMappingControl.ValidationStateChanged -= UnifiedMappingControl_ValidationStateChanged;
+ _isEditMode = false;
+ _editingItem = null;
+ KeyboardHookHelper.Instance.CleanupHook();
+ }
+
+ private void UnifiedMappingControl_ValidationStateChanged(object? sender, EventArgs e)
+ {
+ if (!UnifiedMappingControl.IsInputComplete())
+ {
+ RemappingDialog.IsPrimaryButtonEnabled = false;
+ return;
+ }
+
+ if (_mappingService != null)
+ {
+ List triggerKeys = UnifiedMappingControl.GetTriggerKeys();
+ if (triggerKeys?.Count > 0)
+ {
+ ValidationErrorType error = ValidateMapping(UnifiedMappingControl.CurrentActionType, triggerKeys);
+ if (error != ValidationErrorType.NoError)
+ {
+ UnifiedMappingControl.ShowValidationErrorFromType(error);
+ RemappingDialog.IsPrimaryButtonEnabled = false;
+ return;
+ }
+ }
+ }
+
+ UnifiedMappingControl.HideValidationMessage();
+ RemappingDialog.IsPrimaryButtonEnabled = true;
+ }
+
+ #endregion
+
+ #region Save Logic
+
+ private void RemappingDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
+ {
+ UnifiedMappingControl.HideValidationMessage();
+
+ if (_mappingService == null)
+ {
+ Logger.LogError("Mapping service is null, cannot save mapping");
+ UnifiedMappingControl.ShowValidationError("Error", "Mapping service is not available.");
+ args.Cancel = true;
+ return;
+ }
+
+ try
+ {
+ List triggerKeys = UnifiedMappingControl.GetTriggerKeys();
+
+ if (triggerKeys == null || triggerKeys.Count == 0)
+ {
+ UnifiedMappingControl.ShowValidationError("Missing Original Keys", "Please enter at least one original key to create a remapping.");
+ args.Cancel = true;
+ return;
+ }
+
+ ValidationErrorType validationError = ValidateMapping(UnifiedMappingControl.CurrentActionType, triggerKeys);
+ if (validationError != ValidationErrorType.NoError)
+ {
+ UnifiedMappingControl.ShowValidationErrorFromType(validationError);
+ args.Cancel = true;
+ return;
+ }
+
+ if (_isEditMode && _editingItem != null)
+ {
+ DeleteExistingMapping();
+ }
+
+ bool saved = UnifiedMappingControl.CurrentActionType switch
+ {
+ UnifiedMappingControl.ActionType.KeyOrShortcut => SaveKeyOrShortcutMapping(triggerKeys),
+ UnifiedMappingControl.ActionType.Text => SaveTextMapping(triggerKeys),
+ UnifiedMappingControl.ActionType.OpenUrl => SaveUrlMapping(triggerKeys),
+ UnifiedMappingControl.ActionType.OpenApp => SaveProgramMapping(triggerKeys),
+ UnifiedMappingControl.ActionType.MouseClick => throw new NotImplementedException("Mouse click remapping is not yet supported."),
+ _ => false,
+ };
+
+ if (saved)
+ {
+ LoadAllMappings();
+ }
+ else
+ {
+ UnifiedMappingControl.ShowValidationError("Save Failed", "Failed to save the remapping. Please try again.");
+ args.Cancel = true;
+ }
+ }
+ catch (NotImplementedException ex)
+ {
+ UnifiedMappingControl.ShowValidationError("Not Implemented", ex.Message);
+ args.Cancel = true;
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Error saving mapping: " + ex.Message);
+ UnifiedMappingControl.ShowValidationError("Error", "An error occurred while saving: " + ex.Message);
+ args.Cancel = true;
+ }
+ }
+
+ private ValidationErrorType ValidateMapping(UnifiedMappingControl.ActionType actionType, List triggerKeys)
+ {
+ bool isAppSpecific = UnifiedMappingControl.GetIsAppSpecific();
+ string appName = UnifiedMappingControl.GetAppName();
+ Remapping? editingRemapping = _isEditMode && _editingItem?.Item is Remapping r ? r : null;
+
+ return actionType switch
+ {
+ UnifiedMappingControl.ActionType.KeyOrShortcut => ValidationHelper.ValidateKeyMapping(
+ triggerKeys, UnifiedMappingControl.GetActionKeys(), isAppSpecific, appName, _mappingService!, _isEditMode, editingRemapping),
+ UnifiedMappingControl.ActionType.Text => ValidationHelper.ValidateTextMapping(
+ triggerKeys, UnifiedMappingControl.GetTextContent(), isAppSpecific, appName, _mappingService!, _isEditMode),
+ UnifiedMappingControl.ActionType.OpenUrl => ValidationHelper.ValidateUrlMapping(
+ triggerKeys, UnifiedMappingControl.GetUrl(), isAppSpecific, appName, _mappingService!, _isEditMode),
+ UnifiedMappingControl.ActionType.OpenApp => ValidationHelper.ValidateAppMapping(
+ triggerKeys, UnifiedMappingControl.GetProgramPath(), isAppSpecific, appName, _mappingService!, _isEditMode),
+ _ => ValidationErrorType.NoError,
+ };
+ }
+
+ private void DeleteExistingMapping()
+ {
+ if (_editingItem == null || _mappingService == null)
+ {
+ return;
+ }
+
+ try
+ {
+ switch (_editingItem.Type)
+ {
+ case EditingItem.ItemType.Remapping when _editingItem.Item is Remapping remapping:
+ RemappingHelper.DeleteRemapping(_mappingService, remapping);
+ break;
+
+ default:
+ if (_editingItem.Item is IToggleableShortcut shortcut)
+ {
+ DeleteShortcutMapping(_editingItem.OriginalTriggerKeys, _editingItem.AppName ?? string.Empty);
+ if (!string.IsNullOrEmpty(shortcut.Id))
+ {
+ SettingsManager.RemoveShortcutKeyMappingFromSettings(shortcut.Id);
+ }
+ }
+
+ break;
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Error deleting existing mapping: " + ex.Message);
+ }
+ }
+
+ private void DeleteShortcutMapping(List originalKeys, string targetApp = "")
+ {
+ bool deleted = originalKeys.Count == 1
+ ? DeleteSingleKeyToTextMapping(originalKeys[0])
+ : DeleteMultiKeyMapping(originalKeys, targetApp);
+
+ if (deleted)
+ {
+ _mappingService!.SaveSettings();
+ }
+ }
+
+ private bool DeleteMultiKeyMapping(List originalKeys, string targetApp = "")
+ {
+ string originalKeysString = string.Join(";", originalKeys.Select(k => _mappingService!.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
+ return _mappingService!.DeleteShortcutMapping(originalKeysString, targetApp);
+ }
+
+ private bool SaveKeyOrShortcutMapping(List triggerKeys)
+ {
+ List actionKeys = UnifiedMappingControl.GetActionKeys();
+ if (actionKeys == null || actionKeys.Count == 0)
+ {
+ return false;
+ }
+
+ return RemappingHelper.SaveMapping(
+ _mappingService!,
+ triggerKeys,
+ actionKeys,
+ UnifiedMappingControl.GetIsAppSpecific(),
+ UnifiedMappingControl.GetAppName());
+ }
+
+ private bool SaveTextMapping(List triggerKeys)
+ {
+ string textContent = UnifiedMappingControl.GetTextContent();
+ bool isAppSpecific = UnifiedMappingControl.GetIsAppSpecific();
+ string appName = UnifiedMappingControl.GetAppName();
+
+ if (string.IsNullOrEmpty(textContent))
+ {
+ return false;
+ }
+
+ return triggerKeys.Count == 1
+ ? SaveSingleKeyToTextMapping(triggerKeys[0], textContent, isAppSpecific, appName)
+ : SaveShortcutToTextMapping(triggerKeys, textContent, isAppSpecific, appName);
+ }
+
+ private bool SaveSingleKeyToTextMapping(string keyName, string textContent, bool isAppSpecific, string appName)
+ {
+ int originalKey = _mappingService!.GetKeyCodeFromName(keyName);
+ if (originalKey == 0)
+ {
+ return false;
+ }
+
+ var shortcutKeyMapping = new ShortcutKeyMapping
+ {
+ OperationType = ShortcutOperationType.RemapText,
+ OriginalKeys = originalKey.ToString(CultureInfo.InvariantCulture),
+ TargetKeys = textContent,
+ TargetText = textContent,
+ TargetApp = isAppSpecific ? appName : string.Empty,
+ };
+
+ bool saved = _mappingService.AddSingleKeyToTextMapping(originalKey, textContent);
+ if (saved)
+ {
+ _mappingService.SaveSettings();
+ SettingsManager.AddShortcutKeyMappingToSettings(shortcutKeyMapping);
+ }
+
+ return saved;
+ }
+
+ private bool SaveShortcutToTextMapping(List triggerKeys, string textContent, bool isAppSpecific, string appName)
+ {
+ string originalKeysString = string.Join(";", triggerKeys.Select(k => _mappingService!.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
+
+ var shortcutKeyMapping = new ShortcutKeyMapping
+ {
+ OperationType = ShortcutOperationType.RemapText,
+ OriginalKeys = originalKeysString,
+ TargetKeys = textContent,
+ TargetText = textContent,
+ TargetApp = isAppSpecific ? appName : string.Empty,
+ };
+
+ bool saved = isAppSpecific && !string.IsNullOrEmpty(appName)
+ ? _mappingService!.AddShortcutMapping(originalKeysString, textContent, appName, ShortcutOperationType.RemapText)
+ : _mappingService!.AddShortcutMapping(originalKeysString, textContent, operationType: ShortcutOperationType.RemapText);
+
+ if (saved)
+ {
+ _mappingService.SaveSettings();
+ SettingsManager.AddShortcutKeyMappingToSettings(shortcutKeyMapping);
+ }
+
+ return saved;
+ }
+
+ private bool SaveUrlMapping(List triggerKeys)
+ {
+ string url = UnifiedMappingControl.GetUrl();
+ if (string.IsNullOrEmpty(url))
+ {
+ return false;
+ }
+
+ string originalKeysString = string.Join(";", triggerKeys.Select(k => _mappingService!.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
+
+ var shortcutKeyMapping = new ShortcutKeyMapping
+ {
+ OperationType = ShortcutOperationType.OpenUri,
+ OriginalKeys = originalKeysString,
+ TargetKeys = originalKeysString,
+ UriToOpen = url,
+ TargetApp = UnifiedMappingControl.GetIsAppSpecific() ? UnifiedMappingControl.GetAppName() : string.Empty,
+ };
+
+ bool saved = _mappingService!.AddShortcutMapping(shortcutKeyMapping);
+ if (saved)
+ {
+ _mappingService.SaveSettings();
+ SettingsManager.AddShortcutKeyMappingToSettings(shortcutKeyMapping);
+ }
+
+ return saved;
+ }
+
+ private bool SaveProgramMapping(List triggerKeys)
+ {
+ string programPath = UnifiedMappingControl.GetProgramPath();
+ if (string.IsNullOrEmpty(programPath))
+ {
+ return false;
+ }
+
+ string originalKeysString = string.Join(";", triggerKeys.Select(k => _mappingService!.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
+
+ var shortcutKeyMapping = new ShortcutKeyMapping
+ {
+ OperationType = ShortcutOperationType.RunProgram,
+ OriginalKeys = originalKeysString,
+ TargetKeys = originalKeysString,
+ ProgramPath = programPath,
+ ProgramArgs = UnifiedMappingControl.GetProgramArgs(),
+ StartInDirectory = UnifiedMappingControl.GetStartInDirectory(),
+ IfRunningAction = UnifiedMappingControl.GetIfRunningAction(),
+ Visibility = UnifiedMappingControl.GetVisibility(),
+ Elevation = UnifiedMappingControl.GetElevationLevel(),
+ TargetApp = UnifiedMappingControl.GetIsAppSpecific() ? UnifiedMappingControl.GetAppName() : string.Empty,
+ };
+
+ bool saved = _mappingService!.AddShortcutMapping(shortcutKeyMapping);
+ if (saved)
+ {
+ _mappingService.SaveSettings();
+ SettingsManager.AddShortcutKeyMappingToSettings(shortcutKeyMapping);
+ }
+
+ return saved;
+ }
+
+ #endregion
+
+ #region Delete Handlers
+
+ private async void DeleteMapping_Click(object sender, RoutedEventArgs e)
+ {
+ if (sender is not MenuFlyoutItem menuFlyoutItem || _mappingService == null)
+ {
+ return;
+ }
+
+ if (await DeleteConfirmationDialog.ShowAsync() != ContentDialogResult.Primary)
+ {
+ return;
+ }
+
+ try
+ {
+ switch (menuFlyoutItem.Tag)
+ {
+ case Remapping remapping:
+ HandleRemappingDelete(remapping);
+ UpdateHasAnyMappings();
+ break;
+
+ case IToggleableShortcut shortcut:
+ HandleShortcutDelete(shortcut);
+ LoadAllMappings();
+ break;
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Error deleting mapping: " + ex.Message);
+ }
+ }
+
+ private void HandleRemappingDelete(Remapping remapping)
+ {
+ if (!remapping.IsActive)
+ {
+ SettingsManager.RemoveShortcutKeyMappingFromSettings(remapping.Id);
+ LoadRemappings();
+ }
+ else if (RemappingHelper.DeleteRemapping(_mappingService!, remapping))
+ {
+ LoadRemappings();
+ }
+ else
+ {
+ Logger.LogWarning($"Failed to delete remapping: {string.Join("+", remapping.Shortcut)}");
+ }
+ }
+
+ private void HandleShortcutDelete(IToggleableShortcut shortcut)
+ {
+ bool deleted = shortcut.Shortcut.Count == 1
+ ? DeleteSingleKeyToTextMapping(shortcut.Shortcut[0]) // Remapping has its own handler, single key will always be text mapping
+ : DeleteMultiKeyShortcut(shortcut);
+
+ if (deleted)
+ {
+ _mappingService!.SaveSettings();
+ }
+
+ SettingsManager.RemoveShortcutKeyMappingFromSettings(shortcut.Id);
+ }
+
+ private bool DeleteMultiKeyShortcut(IToggleableShortcut shortcut)
+ {
+ string originalKeys = string.Join(";", shortcut.Shortcut.Select(k => _mappingService!.GetKeyCodeFromName(k)));
+ return _mappingService!.DeleteShortcutMapping(originalKeys, shortcut.AppName);
+ }
+
+ #endregion
+
+ #region Toggle Switch Handlers
+
+ private void ToggleSwitch_Toggled(object sender, RoutedEventArgs e)
+ {
+ if (sender is not ToggleSwitch toggleSwitch || toggleSwitch.DataContext is not IToggleableShortcut shortcut || _mappingService == null)
+ {
+ return;
+ }
+
+ try
+ {
+ if (toggleSwitch.IsOn)
+ {
+ EnableShortcut(shortcut);
+ }
+ else
+ {
+ DisableShortcut(shortcut);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Error toggling shortcut active state: " + ex.Message);
+ }
+ }
+
+ private void EnableShortcut(IToggleableShortcut shortcut)
+ {
+ if (shortcut is Remapping remapping)
+ {
+ RemappingHelper.SaveMapping(_mappingService!, remapping.Shortcut, remapping.RemappedKeys, !remapping.IsAllApps, remapping.AppName, false);
+ shortcut.IsActive = true;
+ SettingsManager.ToggleShortcutKeyMappingActiveState(shortcut.Id);
+ return;
+ }
+
+ ShortcutKeyMapping shortcutKeyMapping = SettingsManager.EditorSettings.ShortcutSettingsDictionary[shortcut.Id].Shortcut;
+ bool saved = shortcut.Shortcut.Count == 1
+ ? _mappingService!.AddSingleKeyToTextMapping(_mappingService.GetKeyCodeFromName(shortcut.Shortcut[0]), shortcutKeyMapping.TargetText)
+ : shortcutKeyMapping.OperationType == ShortcutOperationType.RemapText
+ ? _mappingService!.AddShortcutMapping(shortcutKeyMapping.OriginalKeys, shortcutKeyMapping.TargetText, operationType: ShortcutOperationType.RemapText)
+ : _mappingService!.AddShortcutMapping(shortcutKeyMapping);
+
+ if (saved)
+ {
+ shortcut.IsActive = true;
+ SettingsManager.ToggleShortcutKeyMappingActiveState(shortcut.Id);
+ _mappingService.SaveSettings();
+ }
+ }
+
+ private void DisableShortcut(IToggleableShortcut shortcut)
+ {
+ if (shortcut is Remapping remapping)
+ {
+ shortcut.IsActive = false;
+ RemappingHelper.DeleteRemapping(_mappingService!, remapping, false);
+ SettingsManager.ToggleShortcutKeyMappingActiveState(shortcut.Id);
+ return;
+ }
+
+ bool deleted = shortcut.Shortcut.Count == 1
+ ? DeleteSingleKeyToTextMapping(shortcut.Shortcut[0])
+ : DeleteMultiKeyMapping(shortcut.Shortcut, shortcut.AppName);
+
+ if (deleted)
+ {
+ shortcut.IsActive = false;
+ SettingsManager.ToggleShortcutKeyMappingActiveState(shortcut.Id);
+ _mappingService!.SaveSettings();
+ }
+ }
+
+ private bool DeleteSingleKeyToTextMapping(string keyName)
+ {
+ int originalKey = _mappingService!.GetKeyCodeFromName(keyName);
+ return originalKey != 0 && _mappingService.DeleteSingleKeyToTextMapping(originalKey);
+ }
+
+ #endregion
+
+ #region Load Methods
+
+ private void LoadAllMappings()
+ {
+ LoadRemappings();
+ LoadTextMappings();
+ LoadProgramShortcuts();
+ LoadUrlShortcuts();
+ UpdateHasAnyMappings();
+ }
+
+ private void UpdateHasAnyMappings()
+ {
+ bool hasAny = RemappingList.Count > 0 || TextMappings.Count > 0 || ProgramShortcuts.Count > 0 || UrlShortcuts.Count > 0;
+ MappingState = hasAny ? "HasMappings" : "Empty";
+ }
+
+ private void LoadRemappings()
+ {
+ SettingsManager.EditorSettings.ShortcutsByOperationType.TryGetValue(ShortcutOperationType.RemapShortcut, out var remapShortcutIds);
+
+ if (_mappingService == null || remapShortcutIds == null)
+ {
+ return;
+ }
+
+ RemappingList.Clear();
+
+ foreach (var id in remapShortcutIds)
+ {
+ ShortcutSettings shortcutSettings = SettingsManager.EditorSettings.ShortcutSettingsDictionary[id];
+ ShortcutKeyMapping mapping = shortcutSettings.Shortcut;
+ var originalKeyNames = ParseKeyCodes(mapping.OriginalKeys);
+ var remappedKeyNames = ParseKeyCodes(mapping.TargetKeys);
+
+ RemappingList.Add(new Remapping
+ {
+ Shortcut = originalKeyNames,
+ RemappedKeys = remappedKeyNames,
+ IsAllApps = string.IsNullOrEmpty(mapping.TargetApp),
+ AppName = mapping.TargetApp ?? string.Empty,
+ Id = shortcutSettings.Id,
+ IsActive = shortcutSettings.IsActive,
+ });
+ }
+ }
+
+ private void LoadTextMappings()
+ {
+ SettingsManager.EditorSettings.ShortcutsByOperationType.TryGetValue(ShortcutOperationType.RemapText, out var remapShortcutIds);
+
+ if (_mappingService == null || remapShortcutIds == null)
+ {
+ return;
+ }
+
+ TextMappings.Clear();
+
+ foreach (var id in remapShortcutIds)
+ {
+ ShortcutSettings shortcutSettings = SettingsManager.EditorSettings.ShortcutSettingsDictionary[id];
+ ShortcutKeyMapping mapping = shortcutSettings.Shortcut;
+ var originalKeyNames = ParseKeyCodes(mapping.OriginalKeys);
+
+ TextMappings.Add(new TextMapping
+ {
+ Shortcut = originalKeyNames,
+ Text = mapping.TargetText,
+ IsAllApps = string.IsNullOrEmpty(mapping.TargetApp),
+ AppName = mapping.TargetApp ?? string.Empty,
+ Id = shortcutSettings.Id,
+ IsActive = shortcutSettings.IsActive,
+ });
+ }
+ }
+
+ private void LoadProgramShortcuts()
+ {
+ SettingsManager.EditorSettings.ShortcutsByOperationType.TryGetValue(ShortcutOperationType.RunProgram, out var remapShortcutIds);
+
+ if (_mappingService == null || remapShortcutIds == null)
+ {
+ return;
+ }
+
+ ProgramShortcuts.Clear();
+
+ foreach (var id in remapShortcutIds)
+ {
+ ShortcutSettings shortcutSettings = SettingsManager.EditorSettings.ShortcutSettingsDictionary[id];
+ ShortcutKeyMapping mapping = shortcutSettings.Shortcut;
+ var originalKeyNames = ParseKeyCodes(mapping.OriginalKeys);
+
+ ProgramShortcuts.Add(new ProgramShortcut
+ {
+ Shortcut = originalKeyNames,
+ AppToRun = mapping.ProgramPath,
+ Args = mapping.ProgramArgs,
+ IsActive = shortcutSettings.IsActive,
+ Id = shortcutSettings.Id,
+ IsAllApps = string.IsNullOrEmpty(mapping.TargetApp),
+ AppName = mapping.TargetApp ?? string.Empty,
+ StartInDirectory = mapping.StartInDirectory,
+ Elevation = mapping.Elevation.ToString(),
+ IfRunningAction = mapping.IfRunningAction.ToString(),
+ Visibility = mapping.Visibility.ToString(),
+ });
+ }
+ }
+
+ private void LoadUrlShortcuts()
+ {
+ SettingsManager.EditorSettings.ShortcutsByOperationType.TryGetValue(ShortcutOperationType.OpenUri, out var remapShortcutIds);
+
+ if (_mappingService == null || remapShortcutIds == null)
+ {
+ return;
+ }
+
+ UrlShortcuts.Clear();
+
+ foreach (var id in remapShortcutIds)
+ {
+ ShortcutSettings shortcutSettings = SettingsManager.EditorSettings.ShortcutSettingsDictionary[id];
+ ShortcutKeyMapping mapping = shortcutSettings.Shortcut;
+ var originalKeyNames = ParseKeyCodes(mapping.OriginalKeys);
+
+ UrlShortcuts.Add(new URLShortcut
+ {
+ Shortcut = originalKeyNames,
+ URL = mapping.UriToOpen,
+ Id = shortcutSettings.Id,
+ IsActive = shortcutSettings.IsActive,
+ IsAllApps = string.IsNullOrEmpty(mapping.TargetApp),
+ AppName = mapping.TargetApp ?? string.Empty,
+ });
+ }
+ }
+
+ private List ParseKeyCodes(string keyCodesString)
+ {
+ return keyCodesString.Split(';')
+ .Where(keyCode => int.TryParse(keyCode, out int code))
+ .Select(keyCode => _mappingService!.GetKeyDisplayName(int.Parse(keyCode, CultureInfo.InvariantCulture)))
+ .ToList();
+ }
+
+ #endregion
+
+ #region IDisposable
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ if (disposing)
+ {
+ _mappingService?.Dispose();
+ _mappingService = null;
+ }
+
+ _disposed = true;
+ }
+
+ #endregion
+ }
+}
+#pragma warning restore SA1124 // Do not use regions
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Settings/EditorSettings.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Settings/EditorSettings.cs
new file mode 100644
index 0000000000..5d106bffbc
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Settings/EditorSettings.cs
@@ -0,0 +1,20 @@
+// 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.Collections.Generic;
+using KeyboardManagerEditorUI.Interop;
+
+namespace KeyboardManagerEditorUI.Settings
+{
+ public class EditorSettings
+ {
+ public Dictionary ShortcutSettingsDictionary { get; set; } = new Dictionary();
+
+ public Dictionary> ProfileDictionary { get; set; } = new Dictionary>();
+
+ public Dictionary> ShortcutsByOperationType { get; set; } = new Dictionary>();
+
+ public string ActiveProfile { get; set; } = string.Empty;
+ }
+}
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Settings/SettingsManager.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Settings/SettingsManager.cs
new file mode 100644
index 0000000000..017135ec18
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Settings/SettingsManager.cs
@@ -0,0 +1,275 @@
+// 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.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text.Json;
+using KeyboardManagerEditorUI.Interop;
+
+namespace KeyboardManagerEditorUI.Settings
+{
+ internal static class SettingsManager
+ {
+ private static readonly string _settingsDirectory = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+ "Microsoft",
+ "PowerToys",
+ "Keyboard Manager");
+
+ private static readonly string _settingsFilePath = Path.Combine(_settingsDirectory, "editorSettings.json");
+
+ private static readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions { WriteIndented = true };
+
+ private static readonly KeyboardMappingService _mappingService = new KeyboardMappingService();
+
+ public static EditorSettings EditorSettings { get; set; }
+
+ static SettingsManager()
+ {
+ EditorSettings = LoadSettings();
+ }
+
+ public static EditorSettings LoadSettings()
+ {
+ try
+ {
+ if (!File.Exists(_settingsFilePath))
+ {
+ EditorSettings createdSettings = CreateSettingsFromKeyboardManagerService();
+ WriteSettings(createdSettings);
+ return createdSettings;
+ }
+
+ string json = File.ReadAllText(_settingsFilePath);
+ return JsonSerializer.Deserialize(json, _jsonOptions) ?? new EditorSettings();
+ }
+ catch (Exception)
+ {
+ return new EditorSettings();
+ }
+ }
+
+ public static bool WriteSettings(EditorSettings editorSettings)
+ {
+ try
+ {
+ Directory.CreateDirectory(_settingsDirectory);
+ string json = JsonSerializer.Serialize(editorSettings, _jsonOptions);
+ File.WriteAllText(_settingsFilePath, json);
+ return true;
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+ }
+
+ public static bool WriteSettings() => WriteSettings(EditorSettings);
+
+ private static EditorSettings CreateSettingsFromKeyboardManagerService()
+ {
+ EditorSettings settings = new EditorSettings();
+
+ // Process all shortcut mappings (RunProgram, OpenUri, RemapShortcut, RemapText)
+ foreach (ShortcutKeyMapping mapping in _mappingService.GetShortcutMappings())
+ {
+ AddShortcutMapping(settings, mapping);
+ }
+
+ // Process single key to key mappings
+ foreach (var mapping in _mappingService.GetSingleKeyMappings())
+ {
+ var shortcutMapping = new ShortcutKeyMapping
+ {
+ OperationType = ShortcutOperationType.RemapShortcut,
+ OriginalKeys = mapping.OriginalKey.ToString(CultureInfo.InvariantCulture),
+ TargetKeys = mapping.TargetKey,
+ };
+ AddShortcutMapping(settings, shortcutMapping);
+ }
+
+ // Process single key to text mappings
+ foreach (var mapping in _mappingService.GetKeyToTextMappings())
+ {
+ var shortcutMapping = new ShortcutKeyMapping
+ {
+ OperationType = ShortcutOperationType.RemapText,
+ OriginalKeys = mapping.OriginalKey.ToString(CultureInfo.InvariantCulture),
+ TargetKeys = mapping.TargetText,
+ TargetText = mapping.TargetText,
+ };
+ AddShortcutMapping(settings, shortcutMapping);
+ }
+
+ return settings;
+ }
+
+ public static void CorrelateServiceAndEditorMappings()
+ {
+ bool shortcutSettingsChanged = false;
+
+ // Process all shortcut mappings
+ foreach (ShortcutKeyMapping mapping in _mappingService.GetShortcutMappings())
+ {
+ if (!EditorSettings.ShortcutSettingsDictionary.Values.Any(s => s.Shortcut.OriginalKeys == mapping.OriginalKeys))
+ {
+ AddShortcutMapping(EditorSettings, mapping);
+ shortcutSettingsChanged = true;
+ }
+ }
+
+ // Process single key to key mappings
+ foreach (var mapping in _mappingService.GetSingleKeyMappings())
+ {
+ var shortcutMapping = new ShortcutKeyMapping
+ {
+ OperationType = ShortcutOperationType.RemapShortcut,
+ OriginalKeys = mapping.OriginalKey.ToString(CultureInfo.InvariantCulture),
+ TargetKeys = mapping.TargetKey,
+ };
+
+ if (!MappingExists(shortcutMapping))
+ {
+ AddShortcutMapping(EditorSettings, shortcutMapping);
+ shortcutSettingsChanged = true;
+ }
+ }
+
+ // Process single key to text mappings
+ foreach (var mapping in _mappingService.GetKeyToTextMappings())
+ {
+ var shortcutMapping = new ShortcutKeyMapping
+ {
+ OperationType = ShortcutOperationType.RemapText,
+ OriginalKeys = mapping.OriginalKey.ToString(CultureInfo.InvariantCulture),
+ TargetKeys = mapping.TargetText,
+ TargetText = mapping.TargetText,
+ };
+
+ if (!EditorSettings.ShortcutSettingsDictionary.Values.Any(s => s.Shortcut.OriginalKeys == shortcutMapping.OriginalKeys))
+ {
+ AddShortcutMapping(EditorSettings, shortcutMapping);
+ shortcutSettingsChanged = true;
+ }
+ }
+
+ // Mark inactive mappings
+ var singleKeyMappings = _mappingService.GetSingleKeyMappings();
+ var keyToTextMappings = _mappingService.GetKeyToTextMappings();
+ var shortcutKeyMappings = _mappingService.GetShortcutMappings();
+
+ foreach (ShortcutSettings shortcutSettings in EditorSettings.ShortcutSettingsDictionary.Values.ToList())
+ {
+ bool foundInService = IsMappingActiveInService(
+ shortcutSettings,
+ keyToTextMappings,
+ singleKeyMappings,
+ shortcutKeyMappings);
+
+ if (!foundInService)
+ {
+ shortcutSettingsChanged = true;
+ shortcutSettings.IsActive = false;
+ }
+ }
+
+ if (shortcutSettingsChanged)
+ {
+ WriteSettings();
+ }
+ }
+
+ public static void AddShortcutKeyMappingToSettings(ShortcutKeyMapping shortcutKeyMapping)
+ {
+ AddShortcutMapping(EditorSettings, shortcutKeyMapping);
+ WriteSettings();
+ }
+
+ public static void RemoveShortcutKeyMappingFromSettings(string guid)
+ {
+ ShortcutOperationType operationType = EditorSettings.ShortcutSettingsDictionary[guid].Shortcut.OperationType;
+ EditorSettings.ShortcutSettingsDictionary.Remove(guid);
+
+ if (EditorSettings.ShortcutsByOperationType.TryGetValue(operationType, out var value))
+ {
+ value.Remove(guid);
+ }
+
+ WriteSettings();
+ }
+
+ public static void ToggleShortcutKeyMappingActiveState(string guid)
+ {
+ if (EditorSettings.ShortcutSettingsDictionary.TryGetValue(guid, out ShortcutSettings? shortcutSettings))
+ {
+ shortcutSettings.IsActive = !shortcutSettings.IsActive;
+ WriteSettings();
+ }
+ }
+
+ private static void AddShortcutMapping(EditorSettings settings, ShortcutKeyMapping mapping)
+ {
+ string guid = Guid.NewGuid().ToString();
+ var shortcutSettings = new ShortcutSettings
+ {
+ Id = guid,
+ Shortcut = mapping,
+ IsActive = true,
+ };
+
+ settings.ShortcutSettingsDictionary[guid] = shortcutSettings;
+
+ if (!settings.ShortcutsByOperationType.TryGetValue(mapping.OperationType, out System.Collections.Generic.List? value))
+ {
+ value = new System.Collections.Generic.List();
+ settings.ShortcutsByOperationType[mapping.OperationType] = value;
+ }
+
+ value.Add(guid);
+ }
+
+ private static bool MappingExists(ShortcutKeyMapping mapping)
+ {
+ return EditorSettings.ShortcutSettingsDictionary.Values.Any(s =>
+ s.Shortcut.OperationType == mapping.OperationType &&
+ s.Shortcut.OriginalKeys == mapping.OriginalKeys &&
+ s.Shortcut.TargetKeys == mapping.TargetKeys);
+ }
+
+ private static bool IsMappingActiveInService(
+ ShortcutSettings shortcutSettings,
+ List keyToTextMappings,
+ List singleKeyMappings,
+ List shortcutKeyMappings)
+ {
+ if (string.IsNullOrEmpty(shortcutSettings.Shortcut.OriginalKeys))
+ {
+ return false;
+ }
+
+ bool isSingleKey = shortcutSettings.Shortcut.OriginalKeys.Split(';').Length == 1;
+
+ if (isSingleKey && int.TryParse(shortcutSettings.Shortcut.OriginalKeys, out int keyCode))
+ {
+ if (shortcutSettings.Shortcut.OperationType == ShortcutOperationType.RemapText)
+ {
+ return keyToTextMappings.Any(m =>
+ m.OriginalKey == keyCode &&
+ m.TargetText == shortcutSettings.Shortcut.TargetText);
+ }
+ else if (shortcutSettings.Shortcut.OperationType == ShortcutOperationType.RemapShortcut)
+ {
+ return singleKeyMappings.Any(m =>
+ m.OriginalKey == keyCode &&
+ m.TargetKey == shortcutSettings.Shortcut.TargetKeys);
+ }
+ }
+
+ return shortcutKeyMappings.Any(m => m.OriginalKeys == shortcutSettings.Shortcut.OriginalKeys);
+ }
+ }
+}
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Settings/ShortcutSettings.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Settings/ShortcutSettings.cs
new file mode 100644
index 0000000000..f158c3a839
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Settings/ShortcutSettings.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 System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using KeyboardManagerEditorUI.Interop;
+
+namespace KeyboardManagerEditorUI.Settings
+{
+ public class ShortcutSettings
+ {
+ public string Id { get; set; } = string.Empty;
+
+ public ShortcutKeyMapping Shortcut { get; set; } = new ShortcutKeyMapping();
+
+ public List Profiles { get; set; } = new List();
+
+ public bool IsActive { get; set; } = true;
+ }
+}
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Strings/en-US/Resources.resw b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Strings/en-US/Resources.resw
new file mode 100644
index 0000000000..00a32af4d5
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Strings/en-US/Resources.resw
@@ -0,0 +1,295 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Add new remapping
+
+
+ Nothing mapped yet
+
+
+ Create a key or shortcut remapping to customize how your keyboard works.
+
+
+ Keys and shortcuts
+
+
+ Text
+
+
+ Programs
+
+
+ Urls
+
+
+ maps to
+
+
+ inserts
+
+
+ opens
+
+
+ in
+
+
+ Delete
+
+
+ Arguments:
+
+
+ Start in:
+
+
+ Elevation:
+
+
+ If running:
+
+
+ Window:
+
+
+ Add new remapping
+
+
+ Save
+
+
+ Cancel
+
+
+ Are you sure?
+
+
+ You are about to delete this remapping.
+
+
+ Delete
+
+
+ Cancel
+
+
+ Trigger
+
+
+ Action
+
+
+ Key or shortcut
+
+
+ Key or shortcut
+
+
+ Mouse button
+
+
+ Mouse button
+
+
+ Allow chords
+
+
+ Exact match
+
+
+ Left
+
+
+ Center
+
+
+ Right
+
+
+ Button 1
+
+
+ Button 2
+
+
+ Only apply to a specific app
+
+
+ App name
+
+
+ Enter app name (e.g., notepad.exe)
+
+
+ Remap to key or shortcut
+
+
+ Remap to key or shortcut
+
+
+ Insert text
+
+
+ Insert text
+
+
+ Open URL
+
+
+ Open URL
+
+
+ Open app
+
+
+ Open app
+
+
+ Remap to mouse click
+
+
+ Remap to mouse click
+
+
+ Text to type
+
+
+ Enter the text to type when triggered
+
+
+ URL to open
+
+
+ https://example.com
+
+
+ Program path
+
+
+ C:\Program Files\...
+
+
+ Select program path
+
+
+ Arguments (optional)
+
+
+ --arg1 value1
+
+
+ Start in directory (optional)
+
+
+ C:\Users\...
+
+
+ Select start directory
+
+
+ Run as
+
+
+ Normal
+
+
+ Elevated
+
+
+ Different user
+
+
+ If already running
+
+
+ Show window
+
+
+ Start another
+
+
+ Do nothing
+
+
+ Close
+
+
+ End task
+
+
+ Window visibility
+
+
+ Normal
+
+
+ Hidden
+
+
+ Minimized
+
+
+ Maximized
+
+
+ Mouse click action - coming soon
+
+
\ No newline at end of file
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Styles/Button.xaml b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Styles/Button.xaml
new file mode 100644
index 0000000000..ca66de1435
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Styles/Button.xaml
@@ -0,0 +1,759 @@
+
+
+
+
+
+
+
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Styles/Colors.xaml b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Styles/Colors.xaml
new file mode 100644
index 0000000000..cb4c37b56b
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Styles/Colors.xaml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardEventHandlers.cpp b/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardEventHandlers.cpp
index 179103d41b..0e8529396b 100644
--- a/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardEventHandlers.cpp
+++ b/src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardEventHandlers.cpp
@@ -538,7 +538,10 @@ namespace KeyboardEventHandlers
// Release original shortcut state (release in reverse order of shortcut to be accurate)
Helpers::SetModifierKeyEvents(it->first, it->second.modifierKeysInvoked, keyEventList, false, KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG);
- Helpers::SetTextKeyEvents(keyEventList, remapping);
+ // Send modifier release events first, then paste text via clipboard
+ ii.SendVirtualInput(keyEventList);
+ keyEventList.clear();
+ Helpers::SendTextViaClipboard(remapping);
}
it->second.isShortcutInvoked = true;
@@ -719,7 +722,8 @@ namespace KeyboardEventHandlers
else if (remapToText)
{
auto& remapping = std::get(it->second.targetShortcut);
- Helpers::SetTextKeyEvents(keyEventList, remapping);
+ Helpers::SendTextViaClipboard(remapping);
+ return 1;
}
ii.SendVirtualInput(keyEventList);
@@ -1793,9 +1797,7 @@ namespace KeyboardEventHandlers
return 0;
}
- std::vector keyEventList;
- Helpers::SetTextKeyEvents(keyEventList, *remapping);
- ii.SendVirtualInput(keyEventList);
+ Helpers::SendTextViaClipboard(*remapping);
return 1;
}
diff --git a/src/modules/keyboardmanager/KeyboardManagerEngineTest/SetKeyEventTests.cpp b/src/modules/keyboardmanager/KeyboardManagerEngineTest/SetKeyEventTests.cpp
index 75be7288de..c80d4a3cf9 100644
--- a/src/modules/keyboardmanager/KeyboardManagerEngineTest/SetKeyEventTests.cpp
+++ b/src/modules/keyboardmanager/KeyboardManagerEngineTest/SetKeyEventTests.cpp
@@ -60,4 +60,85 @@ namespace RemappingLogicTests
Assert::AreEqual(0, inputs[1].ki.wScan);
}
};
+
+ // Tests for the SetTextKeyEvents method
+ TEST_CLASS (SetTextKeyEventsTests)
+ {
+ public:
+ // Test that plain ASCII text produces KEYEVENTF_UNICODE events with correct scan codes
+ TEST_METHOD (SetTextKeyEvents_ShouldUseUnicodeFlag_WhenTextIsPlainAscii)
+ {
+ std::vector inputs;
+ std::wstring text = L"abc";
+
+ Helpers::SetTextKeyEvents(inputs, text);
+
+ // 3 characters × 2 events (down+up) = 6 events
+ Assert::AreEqual(6, inputs.size());
+ for (size_t i = 0; i < inputs.size(); i++)
+ {
+ Assert::AreEqual(true, bool(inputs[i].ki.dwFlags & KEYEVENTF_UNICODE));
+ }
+ Assert::AreEqual(L'a', inputs[0].ki.wScan);
+ Assert::AreEqual(L'b', inputs[2].ki.wScan);
+ Assert::AreEqual(L'c', inputs[4].ki.wScan);
+ }
+
+ // Test that each character generates a keydown and keyup event pair
+ TEST_METHOD (SetTextKeyEvents_ShouldGenerateDownUpPairs_WhenTextHasMultipleChars)
+ {
+ std::vector inputs;
+ std::wstring text = L"xy";
+
+ Helpers::SetTextKeyEvents(inputs, text);
+
+ Assert::AreEqual(4, inputs.size());
+ // First event: 'x' keydown (no KEYEVENTF_KEYUP flag)
+ Assert::AreEqual(L'x', inputs[0].ki.wScan);
+ Assert::IsFalse(bool(inputs[0].ki.dwFlags & KEYEVENTF_KEYUP));
+ // Second event: 'x' keyup
+ Assert::AreEqual(L'x', inputs[1].ki.wScan);
+ Assert::AreEqual(true, bool(inputs[1].ki.dwFlags & KEYEVENTF_KEYUP));
+ }
+
+ // Test that newline characters are passed through as Unicode events (actual newline handling is done via clipboard)
+ TEST_METHOD (SetTextKeyEvents_ShouldPassNewlinesAsUnicode_WhenTextContainsNewlines)
+ {
+ std::vector inputs;
+ std::wstring text = L"a\r\nb";
+
+ Helpers::SetTextKeyEvents(inputs, text);
+
+ // All 4 characters (a, \r, \n, b) × 2 events = 8 events
+ Assert::AreEqual(8, inputs.size());
+ Assert::AreEqual(L'a', inputs[0].ki.wScan);
+ Assert::AreEqual(L'\r', inputs[2].ki.wScan);
+ Assert::AreEqual(L'\n', inputs[4].ki.wScan);
+ Assert::AreEqual(L'b', inputs[6].ki.wScan);
+ }
+
+ // Test empty string produces no events
+ TEST_METHOD (SetTextKeyEvents_ShouldProduceNoEvents_WhenTextIsEmpty)
+ {
+ std::vector inputs;
+ std::wstring text = L"";
+
+ Helpers::SetTextKeyEvents(inputs, text);
+
+ Assert::AreEqual(0, inputs.size());
+ }
+
+ // Test that extraInfo flag is set correctly for KBM identification
+ TEST_METHOD (SetTextKeyEvents_ShouldSetExtraInfoFlag_WhenTextIsProvided)
+ {
+ std::vector inputs;
+ std::wstring text = L"a";
+
+ Helpers::SetTextKeyEvents(inputs, text);
+
+ Assert::AreEqual(2, inputs.size());
+ Assert::AreEqual(KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, inputs[0].ki.dwExtraInfo);
+ Assert::AreEqual(KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, inputs[1].ki.dwExtraInfo);
+ }
+ };
}
diff --git a/src/modules/keyboardmanager/common/Helpers.cpp b/src/modules/keyboardmanager/common/Helpers.cpp
index b619ae73d3..596b4343c8 100644
--- a/src/modules/keyboardmanager/common/Helpers.cpp
+++ b/src/modules/keyboardmanager/common/Helpers.cpp
@@ -1,6 +1,8 @@
#include "pch.h"
#include "Helpers.h"
#include
+#include
+#include
#include
#include
@@ -325,12 +327,161 @@ namespace Helpers
}
}
+ // Helper to set clipboard text. Returns true on success.
+ static bool SetClipboardText(const std::wstring& text)
+ {
+ if (!OpenClipboard(nullptr))
+ {
+ return false;
+ }
+
+ EmptyClipboard();
+
+ size_t byteSize = (text.size() + 1) * sizeof(wchar_t);
+ HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, byteSize);
+ if (!hMem)
+ {
+ CloseClipboard();
+ return false;
+ }
+
+ wchar_t* pMem = static_cast(GlobalLock(hMem));
+ if (!pMem)
+ {
+ GlobalFree(hMem);
+ CloseClipboard();
+ return false;
+ }
+
+ wcscpy_s(pMem, text.size() + 1, text.c_str());
+ GlobalUnlock(hMem);
+
+ if (!SetClipboardData(CF_UNICODETEXT, hMem))
+ {
+ GlobalFree(hMem);
+ CloseClipboard();
+ return false;
+ }
+
+ // Exclude this entry from clipboard history and cloud clipboard so the
+ // temporary paste text does not pollute the user's clipboard history.
+ static const UINT excludeFromHistory = RegisterClipboardFormat(L"ExcludeClipboardContentFromMonitorProcessing");
+ if (excludeFromHistory != 0)
+ {
+ HGLOBAL hExclude = GlobalAlloc(GMEM_MOVEABLE, sizeof(DWORD));
+ if (hExclude)
+ {
+ SetClipboardData(excludeFromHistory, hExclude);
+ }
+ }
+
+ CloseClipboard();
+ return true;
+ }
+
+ // Simulate Ctrl+V paste keystroke, tagged with KBM flag so our own hook
+ // passes it through without re-intercepting.
+ static void SendPasteKeystroke()
+ {
+ INPUT pasteInputs[4]{};
+
+ pasteInputs[0].type = INPUT_KEYBOARD;
+ pasteInputs[0].ki.wVk = VK_CONTROL;
+ pasteInputs[0].ki.wScan = static_cast(MapVirtualKey(VK_CONTROL, MAPVK_VK_TO_VSC));
+ pasteInputs[0].ki.dwExtraInfo = KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG;
+
+ pasteInputs[1].type = INPUT_KEYBOARD;
+ pasteInputs[1].ki.wVk = 'V';
+ pasteInputs[1].ki.wScan = static_cast(MapVirtualKey('V', MAPVK_VK_TO_VSC));
+ pasteInputs[1].ki.dwExtraInfo = KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG;
+
+ pasteInputs[2].type = INPUT_KEYBOARD;
+ pasteInputs[2].ki.wVk = 'V';
+ pasteInputs[2].ki.dwFlags = KEYEVENTF_KEYUP;
+ pasteInputs[2].ki.wScan = static_cast(MapVirtualKey('V', MAPVK_VK_TO_VSC));
+ pasteInputs[2].ki.dwExtraInfo = KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG;
+
+ pasteInputs[3].type = INPUT_KEYBOARD;
+ pasteInputs[3].ki.wVk = VK_CONTROL;
+ pasteInputs[3].ki.dwFlags = KEYEVENTF_KEYUP;
+ pasteInputs[3].ki.wScan = static_cast(MapVirtualKey(VK_CONTROL, MAPVK_VK_TO_VSC));
+ pasteInputs[3].ki.dwExtraInfo = KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG;
+
+ SendInput(ARRAYSIZE(pasteInputs), pasteInputs, sizeof(INPUT));
+ }
+
+ // Serializes clipboard operations so rapid text remappings don't race.
+ static std::mutex clipboardMutex;
+
+ // Function to send text via clipboard paste (Ctrl+V).
+ // Saves the previous clipboard content and restores it asynchronously.
+ // The clipboard entry is excluded from clipboard history via
+ // ExcludeClipboardContentFromMonitorProcessing (set in SetClipboardText).
+ bool SendTextViaClipboard(const std::wstring& text)
+ {
+ // Acquire the mutex so that the entire snapshot-paste-restore cycle
+ // is atomic with respect to other text remapping calls.
+ std::unique_lock lock(clipboardMutex);
+
+ // Snapshot current clipboard state
+ bool hadOriginalText = false;
+ std::wstring originalClipboardText;
+ if (OpenClipboard(nullptr))
+ {
+ if (IsClipboardFormatAvailable(CF_UNICODETEXT))
+ {
+ HANDLE hData = GetClipboardData(CF_UNICODETEXT);
+ if (hData)
+ {
+ wchar_t* pText = static_cast(GlobalLock(hData));
+ if (pText)
+ {
+ originalClipboardText = pText;
+ hadOriginalText = true;
+ GlobalUnlock(hData);
+ }
+ }
+ }
+ CloseClipboard();
+ }
+
+ // Place our text on the clipboard (with history exclusion)
+ if (!SetClipboardText(text))
+ {
+ return false;
+ }
+
+ SendPasteKeystroke();
+
+ // Restore clipboard after a delay on a background thread.
+ // Ctrl+V is asynchronous (SendInput queues the input), so the target
+ // app needs time to process the keystroke and read the clipboard.
+ // The lock is moved into the thread so the next call blocks until
+ // restoration completes.
+ std::thread([lock = std::move(lock), originalClipboardText = std::move(originalClipboardText), hadOriginalText]() {
+ Sleep(500);
+ if (hadOriginalText)
+ {
+ SetClipboardText(originalClipboardText);
+ }
+ else
+ {
+ if (OpenClipboard(nullptr))
+ {
+ EmptyClipboard();
+ CloseClipboard();
+ }
+ }
+ }).detach();
+
+ return true;
+ }
+
// Function to filter the key codes for artificial key codes
int32_t FilterArtificialKeys(const int32_t& key)
{
switch (key)
{
- // If a key is remapped to VK_WIN_BOTH, we send VK_LWIN instead
case CommonSharedConstants::VK_WIN_BOTH:
return VK_LWIN;
}
diff --git a/src/modules/keyboardmanager/common/Helpers.h b/src/modules/keyboardmanager/common/Helpers.h
index 8f38bbbbe4..db3cbf3eb1 100644
--- a/src/modules/keyboardmanager/common/Helpers.h
+++ b/src/modules/keyboardmanager/common/Helpers.h
@@ -41,6 +41,9 @@ namespace Helpers
// Function to set key events for remapping text.
void SetTextKeyEvents(std::vector& keyEventArray, const std::wstring& remapping);
+ // Function to send text via clipboard paste (Ctrl+V). Saves and restores previous clipboard content.
+ bool SendTextViaClipboard(const std::wstring& text);
+
// Function to return window handle for a full screen UWP app
HWND GetFullscreenUWPWindowHandle();
diff --git a/src/modules/keyboardmanager/common/Shortcut.h b/src/modules/keyboardmanager/common/Shortcut.h
index 439559c9d0..bac8dbc975 100644
--- a/src/modules/keyboardmanager/common/Shortcut.h
+++ b/src/modules/keyboardmanager/common/Shortcut.h
@@ -39,7 +39,8 @@ public:
{
RemapShortcut = 0,
RunProgram = 1,
- OpenURI = 2
+ OpenURI = 2,
+ RemapText = 3
};
enum StartWindowType
@@ -47,7 +48,7 @@ public:
Normal = 0,
Hidden = 1,
Minimized = 2,
- Maximized = 2
+ Maximized = 3
};
enum ProgramAlreadyRunningAction
diff --git a/src/modules/keyboardmanager/dll/dllmain.cpp b/src/modules/keyboardmanager/dll/dllmain.cpp
index 499b4a9693..61d5e9b0f4 100644
--- a/src/modules/keyboardmanager/dll/dllmain.cpp
+++ b/src/modules/keyboardmanager/dll/dllmain.cpp
@@ -9,6 +9,8 @@
#include
#include
#include
+#include
+#include
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
{
@@ -37,6 +39,8 @@ namespace
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";
+ const wchar_t JSON_KEY_EDITOR_SHORTCUT[] = L"EditorShortcut";
+ const wchar_t JSON_KEY_USE_NEW_EDITOR[] = L"useNewEditor";
}
// Implement the PowerToy Module Interface and all the required methods.
@@ -56,11 +60,22 @@ private:
// Hotkey for toggling the module
Hotkey m_hotkey = { .key = 0 };
+ // Hotkey for opening the editor
+ Hotkey m_editorHotkey = { .key = 0 };
+
+ // Whether to use the new WinUI3 editor
+ bool m_useNewEditor = false;
+
ULONGLONG m_lastHotkeyToggleTime = 0;
HANDLE m_hProcess = nullptr;
+ HANDLE m_hEditorProcess = nullptr;
HANDLE m_hTerminateEngineEvent = nullptr;
+ HANDLE m_open_new_editor_event_handle{ nullptr };
+ std::thread m_toggle_thread;
+ std::atomic m_toggle_thread_running{ false };
+
void refresh_process_state()
{
@@ -174,6 +189,49 @@ private:
m_hotkey.alt = false;
m_hotkey.key = 'K';
}
+
+ // Parse editor shortcut
+ if (settingsObject.GetView().Size())
+ {
+ try
+ {
+ auto jsonEditorHotkeyObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES)
+ .GetNamedObject(JSON_KEY_EDITOR_SHORTCUT);
+ m_editorHotkey.win = jsonEditorHotkeyObject.GetNamedBoolean(JSON_KEY_WIN);
+ m_editorHotkey.alt = jsonEditorHotkeyObject.GetNamedBoolean(JSON_KEY_ALT);
+ m_editorHotkey.shift = jsonEditorHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
+ m_editorHotkey.ctrl = jsonEditorHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
+ m_editorHotkey.key = static_cast(jsonEditorHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
+ }
+ catch (...)
+ {
+ Logger::error("Failed to initialize Keyboard Manager editor shortcut");
+ }
+ }
+
+ if (!m_editorHotkey.key)
+ {
+ // Set default: Win+Shift+Q
+ m_editorHotkey.win = true;
+ m_editorHotkey.shift = true;
+ m_editorHotkey.ctrl = false;
+ m_editorHotkey.alt = false;
+ m_editorHotkey.key = 'Q';
+ }
+
+ // Parse useNewEditor setting
+ if (settingsObject.GetView().Size())
+ {
+ try
+ {
+ auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
+ m_useNewEditor = propertiesObject.GetNamedBoolean(JSON_KEY_USE_NEW_EDITOR, false);
+ }
+ catch (...)
+ {
+ Logger::warn("Failed to parse useNewEditor setting, defaulting to false");
+ }
+ }
}
// Load the settings file.
@@ -214,17 +272,30 @@ public:
}
}
+ m_open_new_editor_event_handle = CreateDefaultEvent(CommonSharedConstants::OPEN_NEW_KEYBOARD_MANAGER_EVENT);
+
init_settings();
};
~KeyboardManager()
{
+ StopOpenEditorListener();
stop_engine();
if (m_hTerminateEngineEvent)
{
CloseHandle(m_hTerminateEngineEvent);
m_hTerminateEngineEvent = nullptr;
}
+ if (m_open_new_editor_event_handle)
+ {
+ CloseHandle(m_open_new_editor_event_handle);
+ m_open_new_editor_event_handle = nullptr;
+ }
+ if (m_hEditorProcess)
+ {
+ CloseHandle(m_hEditorProcess);
+ m_hEditorProcess = nullptr;
+ }
}
// Destroy the powertoy and free memory
@@ -296,6 +367,7 @@ public:
// Log telemetry
Trace::EnableKeyboardManager(true);
start_engine();
+ StartOpenEditorListener();
}
// Disable the powertoy
@@ -304,6 +376,7 @@ public:
m_enabled = false;
// Log telemetry
Trace::EnableKeyboardManager(false);
+ StopOpenEditorListener();
stop_engine();
}
@@ -319,25 +392,148 @@ public:
return false;
}
- // Return the invocation hotkey for toggling
+ // Return the invocation hotkeys for toggling and opening the editor
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
{
+ size_t count = 0;
+
+ // Hotkey 0: toggle engine
if (m_hotkey.key)
{
- if (hotkeys && buffer_size >= 1)
+ if (hotkeys && buffer_size > count)
{
- hotkeys[0] = m_hotkey;
+ hotkeys[count] = m_hotkey;
}
- return 1;
+ count++;
}
- else
+
+ // Hotkey 1: open editor (only when using new editor)
+ if (m_useNewEditor && m_editorHotkey.key)
{
- return 0;
+ if (hotkeys && buffer_size > count)
+ {
+ hotkeys[count] = m_editorHotkey;
+ }
+ count++;
+ }
+
+ return count;
+ }
+
+ void StartOpenEditorListener()
+ {
+ if (m_toggle_thread_running || !m_open_new_editor_event_handle)
+ {
+ return;
+ }
+
+ m_toggle_thread_running = true;
+ m_toggle_thread = std::thread([this]() {
+ while (m_toggle_thread_running)
+ {
+ const DWORD wait_result = WaitForSingleObject(m_open_new_editor_event_handle, 500);
+ if (!m_toggle_thread_running)
+ {
+ break;
+ }
+
+ if (wait_result == WAIT_OBJECT_0)
+ {
+ launch_editor();
+ ResetEvent(m_open_new_editor_event_handle);
+ }
+ }
+ });
+ }
+
+ void StopOpenEditorListener()
+ {
+ if (!m_toggle_thread_running)
+ {
+ return;
+ }
+
+ m_toggle_thread_running = false;
+ if (m_open_new_editor_event_handle)
+ {
+ SetEvent(m_open_new_editor_event_handle);
+ }
+ if (m_toggle_thread.joinable())
+ {
+ m_toggle_thread.join();
}
}
+ bool launch_editor()
+ {
+ // Check if editor is already running
+ if (m_hEditorProcess)
+ {
+ if (WaitForSingleObject(m_hEditorProcess, 0) == WAIT_TIMEOUT)
+ {
+ // Editor still running, bring it to front
+ DWORD editorPid = GetProcessId(m_hEditorProcess);
+ if (editorPid)
+ {
+ AllowSetForegroundWindow(editorPid);
+
+ // Find the editor's main window and set it to foreground
+ EnumWindows([](HWND hwnd, LPARAM lParam) -> BOOL {
+ DWORD windowPid = 0;
+ GetWindowThreadProcessId(hwnd, &windowPid);
+ if (windowPid == static_cast(lParam) && IsWindowVisible(hwnd))
+ {
+ SetForegroundWindow(hwnd);
+ if (IsIconic(hwnd))
+ {
+ ShowWindow(hwnd, SW_RESTORE);
+ }
+ return FALSE; // Stop enumerating
+ }
+ return TRUE;
+ }, static_cast(editorPid));
+ }
+ return true;
+ }
+ else
+ {
+ CloseHandle(m_hEditorProcess);
+ m_hEditorProcess = nullptr;
+ }
+ }
+
+ 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"WinUI3Apps\\PowerToys.KeyboardManagerEditorUI.exe";
+ sei.nShow = SW_SHOWNORMAL;
+ sei.lpParameters = executable_args.data();
+ if (ShellExecuteExW(&sei) == false)
+ {
+ Logger::error(L"Failed to start new keyboard manager editor");
+ auto message = get_last_error_message(GetLastError());
+ if (message.has_value())
+ {
+ Logger::error(message.value());
+ }
+ return false;
+ }
+
+ m_hEditorProcess = sei.hProcess;
+
+ // Log telemetry for editor launch
+ if (m_hEditorProcess)
+ {
+ Trace::LaunchEditor(true); // true = launched via hotkey/event
+ }
+
+ return m_hEditorProcess != nullptr;
+ }
+
// Process the hotkey event
- virtual bool on_hotkey(size_t /*hotkeyId*/) override
+ virtual bool on_hotkey(size_t hotkeyId) override
{
if (!m_enabled)
{
@@ -352,14 +548,23 @@ public:
}
m_lastHotkeyToggleTime = now;
- refresh_process_state();
- if (m_active)
+ if (hotkeyId == 0)
{
- stop_engine();
+ // Toggle engine on/off
+ refresh_process_state();
+ if (m_active)
+ {
+ stop_engine();
+ }
+ else
+ {
+ start_engine();
+ }
}
- else
+ else if (hotkeyId == 1)
{
- start_engine();
+ // Open the new editor (only in new editor mode)
+ launch_editor();
}
return true;
diff --git a/src/modules/keyboardmanager/dll/trace.cpp b/src/modules/keyboardmanager/dll/trace.cpp
index 82057baefb..9f78074c64 100644
--- a/src/modules/keyboardmanager/dll/trace.cpp
+++ b/src/modules/keyboardmanager/dll/trace.cpp
@@ -20,3 +20,14 @@ void Trace::EnableKeyboardManager(const bool enabled) noexcept
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingBoolean(enabled, "Enabled"));
}
+
+// Log when the editor is launched
+void Trace::LaunchEditor(const bool viaHotkey) noexcept
+{
+ TraceLoggingWriteWrapper(
+ g_hProvider,
+ "KeyboardManager_LaunchEditor",
+ ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
+ TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
+ TraceLoggingBoolean(viaHotkey, "ViaHotkey"));
+}
diff --git a/src/modules/keyboardmanager/dll/trace.h b/src/modules/keyboardmanager/dll/trace.h
index 55e5ff6867..81680f5a45 100644
--- a/src/modules/keyboardmanager/dll/trace.h
+++ b/src/modules/keyboardmanager/dll/trace.h
@@ -7,4 +7,7 @@ class Trace : public telemetry::TraceBase
public:
// Log if the user has KBM enabled or disabled - Can also be used to see how often users have to restart the keyboard hook
static void EnableKeyboardManager(const bool enabled) noexcept;
+
+ // Log when the editor is launched
+ static void LaunchEditor(const bool viaHotkey) noexcept;
};
diff --git a/src/settings-ui/QuickAccess.UI/Services/QuickAccessLauncher.cs b/src/settings-ui/QuickAccess.UI/Services/QuickAccessLauncher.cs
index fcf88fd26f..d280c15a9d 100644
--- a/src/settings-ui/QuickAccess.UI/Services/QuickAccessLauncher.cs
+++ b/src/settings-ui/QuickAccess.UI/Services/QuickAccessLauncher.cs
@@ -6,6 +6,8 @@ using System.Threading;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Controls;
using Microsoft.PowerToys.Settings.UI.Library;
+using Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events;
+using Microsoft.PowerToys.Telemetry;
using PowerToys.Interop;
namespace Microsoft.PowerToys.QuickAccess.Services
@@ -27,6 +29,12 @@ namespace Microsoft.PowerToys.QuickAccess.Services
if (moduleRun)
{
_coordinator?.OnModuleLaunched(moduleType);
+
+ // Send telemetry event for module launch from Quick Access
+ if (moduleType == ModuleType.KeyboardManager)
+ {
+ PowerToysTelemetry.Log.WriteEvent(new ModuleLaunchedFromSettingsEvent("KeyboardManagerWinUI"));
+ }
}
_coordinator?.HideFlyout();
diff --git a/src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessLauncher.cs b/src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessLauncher.cs
index 1347ce86c1..bf3ce1e960 100644
--- a/src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessLauncher.cs
+++ b/src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessLauncher.cs
@@ -126,6 +126,13 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
eventHandle.Set();
}
+ return true;
+ case ModuleType.KeyboardManager:
+ using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.OpenNewKeyboardManagerEvent()))
+ {
+ eventHandle.Set();
+ }
+
return true;
default:
return false;
diff --git a/src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessViewModel.cs b/src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessViewModel.cs
index d24edf2d0d..e32ae0d464 100644
--- a/src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessViewModel.cs
+++ b/src/settings-ui/Settings.UI.Controls/QuickAccess/QuickAccessViewModel.cs
@@ -17,6 +17,10 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
public partial class QuickAccessViewModel : Observable
{
private readonly ISettingsRepository _settingsRepository;
+
+ // Pulling in KBMSettingsRepository separately as we need to listen to changes in the
+ // UseNewEditor property to determine the visibility of the KeyboardManager quick access item.
+ private readonly SettingsRepository _kbmSettingsRepository;
private readonly IQuickAccessLauncher _launcher;
private readonly Func _isModuleGpoDisabled;
private readonly Func _isModuleGpoEnabled;
@@ -44,6 +48,9 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
_generalSettings.AddEnabledModuleChangeNotification(ModuleEnabledChanged);
_settingsRepository.SettingsChanged += OnSettingsChanged;
+ _kbmSettingsRepository = SettingsRepository.GetInstance(SettingsUtils.Default);
+ _kbmSettingsRepository.SettingsChanged += OnKbmSettingsChanged;
+
InitializeItems();
}
@@ -67,6 +74,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
AddFlyoutMenuItem(ModuleType.EnvironmentVariables);
AddFlyoutMenuItem(ModuleType.FancyZones);
AddFlyoutMenuItem(ModuleType.Hosts);
+ AddFlyoutMenuItem(ModuleType.KeyboardManager);
AddFlyoutMenuItem(ModuleType.LightSwitch);
// AddFlyoutMenuItem(ModuleType.PowerDisplay); // TEMPORARILY_DISABLED: PowerDisplay
@@ -89,7 +97,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
{
Title = _resourceLoader.GetString(Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetModuleLabelResourceName(moduleType)),
Tag = moduleType,
- Visible = _isModuleGpoEnabled(moduleType) || Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetIsModuleEnabled(_generalSettings, moduleType),
+ Visible = GetItemVisibility(moduleType),
Description = GetModuleToolTip(moduleType),
Icon = Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetModuleTypeFluentIconName(moduleType),
Command = new RelayCommand(() => _launcher.Launch(moduleType)),
@@ -115,11 +123,38 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
{
if (item.Tag is ModuleType moduleType)
{
- item.Visible = _isModuleGpoEnabled(moduleType) || Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetIsModuleEnabled(_generalSettings, moduleType);
+ bool visible = GetItemVisibility(moduleType);
+
+ item.Visible = visible;
}
}
}
+ private void OnKbmSettingsChanged(KeyboardManagerSettings newSettings)
+ {
+ if (_dispatcherQueue != null)
+ {
+ _dispatcherQueue.TryEnqueue(() =>
+ {
+ RefreshItemsVisibility();
+ });
+ }
+ }
+
+ private bool GetItemVisibility(ModuleType moduleType)
+ {
+ // Generally, if gpo is enabled or if module enabled, then quick access item is visible.
+ bool visible = _isModuleGpoEnabled(moduleType) || Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetIsModuleEnabled(_generalSettings, moduleType);
+
+ // For KeyboardManager Quick Access item is only shown when using the new editor
+ if (moduleType == ModuleType.KeyboardManager)
+ {
+ visible = visible && _kbmSettingsRepository.SettingsConfig.Properties.UseNewEditor;
+ }
+
+ return visible;
+ }
+
private string GetModuleToolTip(ModuleType moduleType)
{
return moduleType switch
@@ -127,6 +162,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
ModuleType.ColorPicker => SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ActivationShortcut.ToString(),
ModuleType.FancyZones => SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.FancyzonesEditorHotkey.Value.ToString(),
ModuleType.PowerDisplay => SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ActivationShortcut.ToString(),
+ ModuleType.KeyboardManager => SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.DefaultEditorShortcut.ToString(),
ModuleType.LightSwitch => SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ToggleThemeHotkey.Value.ToString(),
ModuleType.PowerLauncher => SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.OpenPowerLauncher.ToString(),
ModuleType.PowerOCR => SettingsRepository.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ActivationShortcut.ToString(),
diff --git a/src/settings-ui/Settings.UI.Library/KeyboardManagerProperties.cs b/src/settings-ui/Settings.UI.Library/KeyboardManagerProperties.cs
index 1da3bfd72b..a523716577 100644
--- a/src/settings-ui/Settings.UI.Library/KeyboardManagerProperties.cs
+++ b/src/settings-ui/Settings.UI.Library/KeyboardManagerProperties.cs
@@ -23,15 +23,23 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public HotkeySettings DefaultToggleShortcut => new HotkeySettings(true, false, false, true, 0x4B);
+ public HotkeySettings DefaultEditorShortcut => new HotkeySettings(true, false, false, true, 0x51);
+
public KeyboardManagerProperties()
{
ToggleShortcut = DefaultToggleShortcut;
+ EditorShortcut = DefaultEditorShortcut;
KeyboardConfigurations = new GenericProperty>(new List { "default", });
ActiveConfiguration = new GenericProperty("default");
}
public HotkeySettings ToggleShortcut { get; set; }
+ public HotkeySettings EditorShortcut { get; set; }
+
+ [JsonPropertyName("useNewEditor")]
+ public bool UseNewEditor { 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 84ba811b90..406f62cdc9 100644
--- a/src/settings-ui/Settings.UI.Library/KeyboardManagerSettings.cs
+++ b/src/settings-ui/Settings.UI.Library/KeyboardManagerSettings.cs
@@ -42,6 +42,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library
() => Properties.ToggleShortcut,
value => Properties.ToggleShortcut = value ?? Properties.DefaultToggleShortcut,
"Toggle_Shortcut"),
+ new HotkeyAccessor(
+ () => Properties.EditorShortcut,
+ value => Properties.EditorShortcut = value ?? Properties.DefaultEditorShortcut,
+ "Editor_Shortcut"),
};
return hotkeyAccessors.ToArray();
diff --git a/src/settings-ui/Settings.UI.Library/Telemetry/Events/ModuleLaunchedFromSettingsEvent.cs b/src/settings-ui/Settings.UI.Library/Telemetry/Events/ModuleLaunchedFromSettingsEvent.cs
new file mode 100644
index 0000000000..f5c7f92c1d
--- /dev/null
+++ b/src/settings-ui/Settings.UI.Library/Telemetry/Events/ModuleLaunchedFromSettingsEvent.cs
@@ -0,0 +1,26 @@
+// 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.Diagnostics.CodeAnalysis;
+using System.Diagnostics.Tracing;
+using Microsoft.PowerToys.Telemetry;
+using Microsoft.PowerToys.Telemetry.Events;
+
+namespace Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events
+{
+ [EventData]
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
+ public class ModuleLaunchedFromSettingsEvent : EventBase, IEvent
+ {
+ public string ModuleName { get; set; }
+
+ public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
+
+ public ModuleLaunchedFromSettingsEvent(string moduleName)
+ {
+ EventName = "PowerToys_ModuleLaunchedFromSettings";
+ ModuleName = moduleName;
+ }
+ }
+}
diff --git a/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/KeyboardManager.cs b/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/KeyboardManager.cs
index 89491b2b99..3d9278a7f1 100644
--- a/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/KeyboardManager.cs
+++ b/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/KeyboardManager.cs
@@ -97,7 +97,7 @@ namespace ViewModelTests
var expectedEntry = new AppSpecificKeysDataModel();
expectedEntry.OriginalKeys = entry.OriginalKeys;
expectedEntry.NewRemapKeys = entry.NewRemapKeys;
- expectedEntry.TargetApp = "All Apps";
+ expectedEntry.TargetApp = "All apps";
expectedResult.Add(expectedEntry);
Assert.AreEqual(expectedResult.Count, result.Count);
@@ -123,7 +123,7 @@ namespace ViewModelTests
var expectedEntry = new AppSpecificKeysDataModel();
expectedEntry.OriginalKeys = entry.OriginalKeys;
expectedEntry.NewRemapKeys = entry.NewRemapKeys;
- expectedEntry.TargetApp = "All Apps";
+ expectedEntry.TargetApp = "All apps";
expectedResult.Add(expectedEntry);
var x = expectedResult[0].Equals(result[0]);
Assert.AreEqual(expectedResult.Count, result.Count);
@@ -181,7 +181,7 @@ namespace ViewModelTests
var expectedFirstEntry = new AppSpecificKeysDataModel();
expectedFirstEntry.OriginalKeys = firstListEntry.OriginalKeys;
expectedFirstEntry.NewRemapKeys = firstListEntry.NewRemapKeys;
- expectedFirstEntry.TargetApp = "All Apps";
+ expectedFirstEntry.TargetApp = "All apps";
expectedResult.Add(expectedFirstEntry);
var expectedSecondEntry = new AppSpecificKeysDataModel();
expectedSecondEntry.OriginalKeys = secondListEntry.OriginalKeys;
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml
index 9f42bc45cb..9943a26959 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml
@@ -15,219 +15,276 @@
mc:Ignorable="d">
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
-
-
-
+
+
+
-
-
+
+
+
+
-
-
+
-
-
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml.cs
index fce4dfc718..16803bdf2f 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml.cs
@@ -91,5 +91,10 @@ namespace Microsoft.PowerToys.Settings.UI.Views
{
ViewModel.RefreshEnabledState();
}
+
+ private void GoBackClassic_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
+ {
+ ViewModel.UseNewEditor = false;
+ }
}
}
diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
index 8db269fed3..f2efe864b4 100644
--- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
+++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
@@ -557,6 +557,10 @@ opera.exe
Remap a key
Keyboard Manager remap keyboard button content
+
+
+ Editor
+ Keyboard Manager new editor header
Keys
@@ -571,9 +575,29 @@ opera.exe
Keyboard Manager remap keyboard header
- All Apps
+ All apps
Should be the same as EditShortcuts_AllApps from keyboard manager editor
+
+ Try the new editor
+ Keyboard Manager toggle to switch to the new unified editor
+
+
+ Switch to the new editor. You can switch back at any time.
+ Description for the new experience toggle
+
+
+ Editor
+ Keyboard Manager button to open the new unified editor
+
+
+ Set and manage your remappings
+ Description for the new editor button
+
+
+ Switch back to the classic editor
+ Keyboard Manager link to switch back to the classic editor UI
+
Shortcut
@@ -1883,12 +1907,24 @@ Made with 💗 by Microsoft and the PowerToys community.
Customize the shortcut to activate this module
- Toggle shortcut
+ Shortcut
- Use a shortcut to toggle this module on or off (note that the Settings UI will not update)
+ Enable or disable this module (Note: the Settings UI will not update)
-
+
+ Editor shortcut
+
+
+ Open editor shortcut
+
+
+ Customize the shortcut to open the Keyboard Manager editor
+
+
+ Open editor
+
+
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 0c142cf6c0..3b9fbf8d3e 100644
--- a/src/settings-ui/Settings.UI/ViewModels/KeyboardManagerViewModel.cs
+++ b/src/settings-ui/Settings.UI/ViewModels/KeyboardManagerViewModel.cs
@@ -12,16 +12,16 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
-
using global::PowerToys.GPOWrapper;
using ManagedCommon;
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.Telemetry.Events;
using Microsoft.PowerToys.Settings.UI.Library.ViewModels.Commands;
using Microsoft.PowerToys.Settings.UI.SerializationContext;
using Microsoft.PowerToys.Settings.Utilities;
-using Microsoft.Win32;
+using Microsoft.PowerToys.Telemetry;
namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
@@ -38,8 +38,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
// Default editor path. Can be removed once the new WinUI3 editor is released.
private const string KeyboardManagerEditorPath = "KeyboardManagerEditor\\PowerToys.KeyboardManagerEditor.exe";
- // New WinUI3 editor path. Still in development and do NOT use it in production.
- private const string KeyboardManagerEditorUIPath = "KeyboardManagerEditorUI\\PowerToys.KeyboardManagerEditorUI.exe";
+ private const string KeyboardManagerEditorUIPath = "WinUI3Apps\\PowerToys.KeyboardManagerEditorUI.exe";
private Process editor;
@@ -57,6 +56,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private ICommand _remapKeyboardCommand;
private ICommand _editShortcutCommand;
+ private ICommand _openNewEditorCommand;
private KeyboardManagerProfile _profile;
private Func SendConfigMSG { get; }
@@ -181,7 +181,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
var hotkeysDict = new Dictionary
{
- [ModuleName] = [ToggleShortcut],
+ [ModuleName] = [ToggleShortcut, EditorShortcut],
};
return hotkeysDict;
@@ -192,11 +192,55 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
get => Settings.Properties.ToggleShortcut;
set
{
- if (Settings.Properties.ToggleShortcut != value)
+ if (value != Settings.Properties.ToggleShortcut)
{
- Settings.Properties.ToggleShortcut = value ?? Settings.Properties.DefaultToggleShortcut;
+ Settings.Properties.ToggleShortcut = value == null ? Settings.Properties.DefaultToggleShortcut : value;
+
OnPropertyChanged(nameof(ToggleShortcut));
NotifySettingsChanged();
+
+ SendConfigMSG(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "{{ \"powertoys\": {{ \"{0}\": {1} }} }}",
+ KeyboardManagerSettings.ModuleName,
+ JsonSerializer.Serialize(Settings, SourceGenerationContextContext.Default.KeyboardManagerSettings)));
+ }
+ }
+ }
+
+ public bool UseNewEditor
+ {
+ get => Settings.Properties.UseNewEditor;
+ set
+ {
+ if (Settings.Properties.UseNewEditor != value)
+ {
+ Settings.Properties.UseNewEditor = value;
+ OnPropertyChanged(nameof(UseNewEditor));
+ NotifySettingsChanged();
+ }
+ }
+ }
+
+ public HotkeySettings EditorShortcut
+ {
+ get => Settings.Properties.EditorShortcut;
+ set
+ {
+ if (value != Settings.Properties.EditorShortcut)
+ {
+ Settings.Properties.EditorShortcut = value == null ? Settings.Properties.DefaultEditorShortcut : value;
+
+ OnPropertyChanged(nameof(EditorShortcut));
+ NotifySettingsChanged();
+
+ SendConfigMSG(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "{{ \"powertoys\": {{ \"{0}\": {1} }} }}",
+ KeyboardManagerSettings.ModuleName,
+ JsonSerializer.Serialize(Settings, SourceGenerationContextContext.Default.KeyboardManagerSettings)));
}
}
}
@@ -262,6 +306,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public ICommand EditShortcutCommand => _editShortcutCommand ?? (_editShortcutCommand = new RelayCommand(OnEditShortcut));
+ public ICommand OpenNewEditorCommand => _openNewEditorCommand ?? (_openNewEditorCommand = new RelayCommand(OnOpenNewEditor));
+
public void OnRemapKeyboard()
{
OpenEditor((int)KeyboardManagerEditorType.KeyEditor);
@@ -272,6 +318,11 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
OpenEditor((int)KeyboardManagerEditorType.ShortcutEditor);
}
+ public void OnOpenNewEditor()
+ {
+ OpenNewEditor();
+ }
+
private static void BringProcessToFront(Process process)
{
if (process == null)
@@ -305,41 +356,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
return;
}
- // Launch the new editor if:
- // 1. the experimentation toggle is enabled in the settings
- // 2. the new WinUI3 editor is enabled in the registry. The registry value does not exist by default and is only used for development purposes
- string editorPath = KeyboardManagerEditorPath;
- try
- {
- // Check if the experimentation toggle is enabled in the settings
- var settingsUtils = SettingsUtils.Default;
- bool isExperimentationEnabled = SettingsRepository.GetInstance(settingsUtils).SettingsConfig.EnableExperimentation;
-
- // Only read the registry value if the experimentation toggle is enabled
- if (isExperimentationEnabled)
- {
- // Read the registry value to determine which editor to launch
- var key = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\PowerToys\Keyboard Manager");
- if (key != null && (int?)key.GetValue("UseNewEditor") == 1)
- {
- editorPath = KeyboardManagerEditorUIPath;
- }
-
- // Close the registry key
- key?.Close();
- }
- }
- catch (Exception e)
- {
- // Fall back to the default editor path if any exception occurs
- Logger.LogError("Failed to launch the new WinUI3 Editor", e);
- }
-
- string path = Path.Combine(Environment.CurrentDirectory, editorPath);
+ string path = Path.Combine(Environment.CurrentDirectory, KeyboardManagerEditorPath);
Logger.LogInfo($"Starting {ModuleName} editor from {path}");
// InvariantCulture: type represents the KeyboardManagerEditorType enum value
- editor = Process.Start(path, $"{type.ToString(CultureInfo.InvariantCulture)} {Environment.ProcessId}");
+ ProcessStartInfo startInfo = new ProcessStartInfo(path);
+ startInfo.UseShellExecute = true; // LOAD BEARING
+ startInfo.Arguments = $"{type.ToString(CultureInfo.InvariantCulture)} {Environment.ProcessId}";
+ System.Environment.SetEnvironmentVariable("MICROSOFT_WINDOWSAPPRUNTIME_BASE_DIRECTORY", null);
+ editor = Process.Start(startInfo);
+ PowerToysTelemetry.Log.WriteEvent(new ModuleLaunchedFromSettingsEvent("KeyboardManagerClassic"));
}
catch (Exception e)
{
@@ -347,6 +373,39 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
+ private void OpenNewEditor()
+ {
+ try
+ {
+ if (editor != null && editor.HasExited)
+ {
+ Logger.LogInfo($"Previous instance of {ModuleName} editor exited");
+ editor = null;
+ }
+
+ if (editor != null)
+ {
+ Logger.LogInfo($"The {ModuleName} editor instance {editor.Id} exists. Bringing the process to the front");
+ BringProcessToFront(editor);
+ return;
+ }
+
+ string path = Path.Combine(Environment.CurrentDirectory, KeyboardManagerEditorUIPath);
+ Logger.LogInfo($"Starting {ModuleName} new editor from {path}");
+
+ System.Environment.SetEnvironmentVariable("MICROSOFT_WINDOWSAPPRUNTIME_BASE_DIRECTORY", null);
+ ProcessStartInfo startInfo = new ProcessStartInfo(path);
+ startInfo.UseShellExecute = true; // LOAD BEARING
+ startInfo.Arguments = $"{Environment.ProcessId}";
+ editor = Process.Start(startInfo);
+ PowerToysTelemetry.Log.WriteEvent(new ModuleLaunchedFromSettingsEvent("KeyboardManagerWinUI"));
+ }
+ catch (Exception e)
+ {
+ Logger.LogError($"Exception encountered when opening the new {ModuleName} editor", e);
+ }
+ }
+
public void NotifyFileChanged()
{
OnPropertyChanged(nameof(RemapKeys));