diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/UnifiedMappingControl.xaml b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/UnifiedMappingControl.xaml
new file mode 100644
index 0000000000..7738db7e0c
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/UnifiedMappingControl.xaml
@@ -0,0 +1,314 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Normal
+ Elevated
+ Different user
+
+
+
+ Show window
+ Start another
+ Do nothing
+ Close
+ End task
+
+
+
+ Normal
+ Hidden
+ Minimized
+ Maximized
+
+
+
+
+
+
+
+
+
+
+
+
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..95d182d9c8
--- /dev/null
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Controls/UnifiedMappingControl.xaml.cs
@@ -0,0 +1,943 @@
+// 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 TeachingTip? _currentNotification;
+ private DispatcherTimer? _notificationTimer;
+
+ private bool _disposed;
+ private bool _internalUpdate;
+
+ private KeyInputMode _currentInputMode = KeyInputMode.OriginalKeys;
+
+ #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;
+
+ this.Unloaded += UnifiedMappingControl_Unloaded;
+ }
+
+ #endregion
+
+ #region Lifecycle Events
+
+ private void UserControl_Loaded(object sender, RoutedEventArgs e)
+ {
+ // Set up event handlers for app-specific checkboxes
+ AppSpecificCheckBox.Checked += AppSpecificCheckBox_Changed;
+ AppSpecificCheckBox.Unchecked += AppSpecificCheckBox_Changed;
+ TextAppSpecificCheckBox.Checked += TextAppSpecificCheckBox_Changed;
+ TextAppSpecificCheckBox.Unchecked += TextAppSpecificCheckBox_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;
+ }
+ }
+ }
+ }
+
+ 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 TextAppSpecificCheckBox_Changed(object sender, RoutedEventArgs e)
+ {
+ if (_internalUpdate)
+ {
+ return;
+ }
+
+ CleanupKeyboardHook();
+ UncheckAllToggleButtons();
+
+ TextAppNameTextBox.Visibility = TextAppSpecificCheckBox.IsChecked == true
+ ? Visibility.Visible
+ : Visibility.Collapsed;
+ }
+
+ private void UpdateAppSpecificCheckBoxState()
+ {
+ // Only enable app-specific remapping for shortcuts (multiple keys)
+ bool isShortcut = _triggerKeys.Count > 1;
+
+ try
+ {
+ _internalUpdate = true;
+
+ // Update Key/Shortcut action checkbox
+ AppSpecificCheckBox.IsEnabled = isShortcut;
+ if (!isShortcut)
+ {
+ AppSpecificCheckBox.IsChecked = false;
+ AppNameTextBox.Visibility = Visibility.Collapsed;
+ }
+
+ // Update Text action checkbox
+ TextAppSpecificCheckBox.IsEnabled = isShortcut;
+ if (!isShortcut)
+ {
+ TextAppSpecificCheckBox.IsChecked = false;
+ TextAppNameTextBox.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 TextAppNameTextBox_GotFocus(object sender, RoutedEventArgs e)
+ {
+ CleanupKeyboardHook();
+ UncheckAllToggleButtons();
+ }
+
+ private void UrlPathInput_GotFocus(object sender, RoutedEventArgs e)
+ {
+ CleanupKeyboardHook();
+ UncheckAllToggleButtons();
+ }
+
+ private void ProgramPathInput_GotFocus(object sender, RoutedEventArgs e)
+ {
+ CleanupKeyboardHook();
+ UncheckAllToggleButtons();
+ }
+
+ 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;
+ }
+ }
+
+ 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 CurrentActionType switch
+ {
+ ActionType.KeyOrShortcut => AppSpecificCheckBox?.IsChecked ?? false,
+ ActionType.Text => TextAppSpecificCheckBox?.IsChecked ?? false,
+ _ => false,
+ };
+ }
+
+ ///
+ /// Gets the app name for app-specific mappings.
+ ///
+ public string GetAppName()
+ {
+ return CurrentActionType switch
+ {
+ ActionType.KeyOrShortcut => GetIsAppSpecific() ? (AppNameTextBox?.Text ?? string.Empty) : string.Empty,
+ ActionType.Text => GetIsAppSpecific() ? (TextAppNameTextBox?.Text ?? string.Empty) : 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 - 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)
+ {
+ switch (CurrentActionType)
+ {
+ case ActionType.KeyOrShortcut:
+ if (AppSpecificCheckBox != null)
+ {
+ AppSpecificCheckBox.IsChecked = isAppSpecific;
+ if (isAppSpecific && AppNameTextBox != null)
+ {
+ AppNameTextBox.Text = appName;
+ AppNameTextBox.Visibility = Visibility.Visible;
+ }
+ }
+
+ break;
+
+ case ActionType.Text:
+ if (TextAppSpecificCheckBox != null)
+ {
+ TextAppSpecificCheckBox.IsChecked = isAppSpecific;
+ if (isAppSpecific && TextAppNameTextBox != null)
+ {
+ TextAppNameTextBox.Text = appName;
+ TextAppNameTextBox.Visibility = Visibility.Visible;
+ }
+ }
+
+ break;
+ }
+ }
+
+ #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();
+ }
+
+ ///
+ /// Resets all inputs to their default state.
+ ///
+ public void Reset()
+ {
+ _triggerKeys.Clear();
+ _actionKeys.Clear();
+
+ UncheckAllToggleButtons();
+
+ _currentInputMode = KeyInputMode.OriginalKeys;
+
+ // 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;
+ }
+
+ if (TextAppNameTextBox != null)
+ {
+ TextAppNameTextBox.Text = string.Empty;
+ TextAppNameTextBox.Visibility = Visibility.Collapsed;
+ }
+
+ // Reset checkboxes
+ if (AppSpecificCheckBox != null)
+ {
+ AppSpecificCheckBox.IsChecked = false;
+ AppSpecificCheckBox.IsEnabled = false;
+ }
+
+ if (TextAppSpecificCheckBox != null)
+ {
+ TextAppSpecificCheckBox.IsChecked = false;
+ TextAppSpecificCheckBox.IsEnabled = false;
+ }
+
+ // Reset app combo boxes
+ if (ElevationComboBox != null)
+ {
+ ElevationComboBox.SelectedIndex = 0;
+ }
+
+ if (IfRunningComboBox != null)
+ {
+ IfRunningComboBox.SelectedIndex = 0;
+ }
+
+ if (VisibilityComboBox != null)
+ {
+ VisibilityComboBox.SelectedIndex = 0;
+ }
+
+ CloseExistingNotification();
+ }
+
+ ///
+ /// Resets only the toggle buttons without clearing the key displays.
+ ///
+ public void ResetToggleButtons()
+ {
+ UncheckAllToggleButtons();
+ }
+
+ #endregion
+
+ #region Notifications
+
+ public void ShowNotificationTip(string message)
+ {
+ CloseExistingNotification();
+
+ _currentNotification = new TeachingTip
+ {
+ Title = "Input Limit Reached",
+ Subtitle = message,
+ IsLightDismissEnabled = true,
+ PreferredPlacement = TeachingTipPlacementMode.Top,
+ XamlRoot = this.XamlRoot,
+ IconSource = new SymbolIconSource { Symbol = Symbol.Important },
+ };
+
+ // Target the appropriate toggle button
+ _currentNotification.Target = _currentInputMode == KeyInputMode.RemappedKeys
+ ? ActionKeyToggleBtn
+ : TriggerKeyToggleBtn;
+
+ if (this.Content is Panel rootPanel)
+ {
+ rootPanel.Children.Add(_currentNotification);
+ _currentNotification.IsOpen = true;
+
+ _notificationTimer = new DispatcherTimer
+ {
+ Interval = TimeSpan.FromMilliseconds(EditorConstants.DefaultNotificationTimeout),
+ };
+ _notificationTimer.Tick += (s, e) => CloseExistingNotification();
+ _notificationTimer.Start();
+ }
+ }
+
+ private void CloseExistingNotification()
+ {
+ _notificationTimer?.Stop();
+ _notificationTimer = null;
+
+ if (_currentNotification != null && _currentNotification.IsOpen)
+ {
+ _currentNotification.IsOpen = false;
+
+ if (this.Content is Panel rootPanel && rootPanel.Children.Contains(_currentNotification))
+ {
+ rootPanel.Children.Remove(_currentNotification);
+ }
+
+ _currentNotification = null;
+ }
+ }
+
+ #endregion
+
+ #region IDisposable
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ CleanupKeyboardHook();
+ CloseExistingNotification();
+ Reset();
+ }
+
+ _disposed = true;
+ }
+ }
+
+ #endregion
+ }
+}
+
+#pragma warning restore SA1124 // Do not use regions
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/KeyboardManagerEditorUI.csproj b/src/modules/keyboardmanager/KeyboardManagerEditorUI/KeyboardManagerEditorUI.csproj
index f181e11432..72102bbc32 100644
--- a/src/modules/keyboardmanager/KeyboardManagerEditorUI/KeyboardManagerEditorUI.csproj
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/KeyboardManagerEditorUI.csproj
@@ -46,6 +46,7 @@
+
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/All.xaml b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/All.xaml
index 16b4f77c5d..6151891a85 100644
--- a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/All.xaml
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/All.xaml
@@ -12,14 +12,33 @@
mc:Ignorable="d">
-
+
+
+
+
+
+
+
+
+
-
+
-
+
@@ -152,11 +172,9 @@
-
+
-
+
@@ -273,11 +292,9 @@
-
+
-
+
@@ -362,9 +380,7 @@
Grid.Column="1"
Orientation="Horizontal"
Spacing="8">
-
+
-
+
@@ -387,11 +401,9 @@
-
+
-
+
@@ -485,5 +498,19 @@
+
+
+
+
+
diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/All.xaml.cs b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/All.xaml.cs
index 965dbc5e2d..928d7c386f 100644
--- a/src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/All.xaml.cs
+++ b/src/modules/keyboardmanager/KeyboardManagerEditorUI/Pages/All.xaml.cs
@@ -5,24 +5,34 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+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 All : Page, IDisposable
{
private KeyboardMappingService? _mappingService;
private bool _disposed;
+ // Edit mode tracking
+ private bool _isEditMode;
+ private EditingItem? _editingItem;
+
public ObservableCollection RemappingList { get; } = new ObservableCollection();
public ObservableCollection TextMappings { get; } = new ObservableCollection();
@@ -34,6 +44,30 @@ namespace KeyboardManagerEditorUI.Pages
[DllImport("PowerToys.KeyboardManagerEditorLibraryWrapper.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
private static extern void GetKeyDisplayName(int keyCode, [Out] StringBuilder keyName, int maxLength);
+ ///
+ /// Tracks what item is being edited and its type.
+ ///
+ 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 All()
{
this.InitializeComponent();
@@ -51,11 +85,444 @@ namespace KeyboardManagerEditorUI.Pages
this.Unloaded += All_Unloaded;
}
- private void All_Unloaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
+ 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;
+
+ // Reset the control before showing
+ UnifiedMappingControl.Reset();
+ RemappingDialog.Title = "Add new remapping";
+
+ await ShowRemappingDialog();
+ }
+
+ private async void RemappingsList_ItemClick(object sender, ItemClickEventArgs e)
+ {
+ if (e.ClickedItem is Remapping remapping)
+ {
+ _isEditMode = true;
+ _editingItem = new EditingItem
+ {
+ Type = EditingItem.ItemType.Remapping,
+ Item = remapping,
+ OriginalTriggerKeys = remapping.OriginalKeys.ToList(),
+ AppName = remapping.AppName,
+ IsAllApps = remapping.IsAllApps,
+ };
+
+ UnifiedMappingControl.Reset();
+ UnifiedMappingControl.SetTriggerKeys(remapping.OriginalKeys.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 TextMapping textMapping)
+ {
+ _isEditMode = true;
+ _editingItem = new EditingItem
+ {
+ Type = EditingItem.ItemType.TextMapping,
+ Item = textMapping,
+ OriginalTriggerKeys = textMapping.Keys.ToList(),
+ AppName = textMapping.AppName,
+ IsAllApps = textMapping.IsAllApps,
+ };
+
+ UnifiedMappingControl.Reset();
+ UnifiedMappingControl.SetTriggerKeys(textMapping.Keys.ToList());
+ UnifiedMappingControl.SetActionType(UnifiedMappingControl.ActionType.Text);
+ UnifiedMappingControl.SetTextContent(textMapping.Text);
+ UnifiedMappingControl.SetAppSpecific(!textMapping.IsAllApps, textMapping.AppName);
+
+ RemappingDialog.Title = "Edit text mapping";
+ await ShowRemappingDialog();
+ }
+ }
+
+ private async void ProgramShortcutsList_ItemClick(object sender, ItemClickEventArgs e)
+ {
+ if (e.ClickedItem is ProgramShortcut programShortcut)
+ {
+ _isEditMode = true;
+ _editingItem = new EditingItem
+ {
+ Type = EditingItem.ItemType.ProgramShortcut,
+ Item = programShortcut,
+ OriginalTriggerKeys = programShortcut.Shortcut.ToList(),
+ };
+
+ UnifiedMappingControl.Reset();
+ UnifiedMappingControl.SetTriggerKeys(programShortcut.Shortcut.ToList());
+ UnifiedMappingControl.SetActionType(UnifiedMappingControl.ActionType.OpenApp);
+ UnifiedMappingControl.SetProgramPath(programShortcut.AppToRun);
+ UnifiedMappingControl.SetProgramArgs(programShortcut.Args);
+
+ // Load additional settings from SettingsManager if available
+ 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);
+ }
+
+ RemappingDialog.Title = "Edit program shortcut";
+ await ShowRemappingDialog();
+ }
+ }
+
+ private async void UrlShortcutsList_ItemClick(object sender, ItemClickEventArgs e)
+ {
+ if (e.ClickedItem is URLShortcut urlShortcut)
+ {
+ _isEditMode = true;
+ _editingItem = new EditingItem
+ {
+ Type = EditingItem.ItemType.UrlShortcut,
+ Item = urlShortcut,
+ OriginalTriggerKeys = urlShortcut.Shortcut.ToList(),
+ };
+
+ UnifiedMappingControl.Reset();
+ UnifiedMappingControl.SetTriggerKeys(urlShortcut.Shortcut.ToList());
+ UnifiedMappingControl.SetActionType(UnifiedMappingControl.ActionType.OpenUrl);
+ UnifiedMappingControl.SetUrl(urlShortcut.URL);
+
+ RemappingDialog.Title = "Edit URL shortcut";
+ await ShowRemappingDialog();
+ }
+ }
+
+ private async System.Threading.Tasks.Task ShowRemappingDialog()
+ {
+ // Hook up the primary button click handler
+ RemappingDialog.PrimaryButtonClick += RemappingDialog_PrimaryButtonClick;
+
+ // Show the dialog
+ await RemappingDialog.ShowAsync();
+
+ // Unhook the handler
+ RemappingDialog.PrimaryButtonClick -= RemappingDialog_PrimaryButtonClick;
+
+ // Reset edit mode
+ _isEditMode = false;
+ _editingItem = null;
+
+ // Cleanup keyboard hook after dialog closes
+ KeyboardHookHelper.Instance.CleanupHook();
+ }
+
+ #endregion
+
+ #region Save Logic
+ private void RemappingDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
+ {
+ if (_mappingService == null)
+ {
+ Logger.LogError("Mapping service is null, cannot save mapping");
+ args.Cancel = true;
+ return;
+ }
+
+ try
+ {
+ bool saved = false;
+ var actionType = UnifiedMappingControl.CurrentActionType;
+ List triggerKeys = UnifiedMappingControl.GetTriggerKeys();
+
+ if (triggerKeys == null || triggerKeys.Count == 0)
+ {
+ // No trigger keys specified
+ args.Cancel = true;
+ return;
+ }
+
+ // If in edit mode, delete the existing mapping first
+ if (_isEditMode && _editingItem != null)
+ {
+ DeleteExistingMapping();
+ }
+
+ switch (actionType)
+ {
+ case UnifiedMappingControl.ActionType.KeyOrShortcut:
+ saved = SaveKeyOrShortcutMapping(triggerKeys);
+ break;
+
+ case UnifiedMappingControl.ActionType.Text:
+ saved = SaveTextMapping(triggerKeys);
+ break;
+
+ case UnifiedMappingControl.ActionType.OpenUrl:
+ saved = SaveUrlMapping(triggerKeys);
+ break;
+
+ case UnifiedMappingControl.ActionType.OpenApp:
+ saved = SaveProgramMapping(triggerKeys);
+ break;
+
+ case UnifiedMappingControl.ActionType.MouseClick:
+ // Not implemented yet
+ args.Cancel = true;
+ return;
+ }
+
+ if (saved)
+ {
+ LoadAllMappings();
+ }
+ else
+ {
+ args.Cancel = true;
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Error saving mapping: " + ex.Message);
+ args.Cancel = true;
+ }
+ }
+
+ private void DeleteExistingMapping()
+ {
+ if (_editingItem == null || _mappingService == null)
+ {
+ return;
+ }
+
+ try
+ {
+ var originalKeys = _editingItem.OriginalTriggerKeys;
+
+ switch (_editingItem.Type)
+ {
+ case EditingItem.ItemType.Remapping:
+ if (_editingItem.Item is Remapping remapping)
+ {
+ RemappingHelper.DeleteRemapping(_mappingService, remapping);
+ }
+
+ break;
+
+ case EditingItem.ItemType.TextMapping:
+ if (originalKeys.Count == 1)
+ {
+ int originalKey = _mappingService.GetKeyCodeFromName(originalKeys[0]);
+ if (originalKey != 0)
+ {
+ _mappingService.DeleteSingleKeyToTextMapping(originalKey);
+ }
+ }
+ else
+ {
+ string originalKeysString = string.Join(";", originalKeys.Select(k => _mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
+ _mappingService.DeleteShortcutMapping(originalKeysString, _editingItem.IsAllApps ? string.Empty : _editingItem.AppName ?? string.Empty);
+ }
+
+ break;
+
+ case EditingItem.ItemType.ProgramShortcut:
+ if (_editingItem.Item is ProgramShortcut programShortcut)
+ {
+ if (originalKeys.Count == 1)
+ {
+ int originalKey = _mappingService.GetKeyCodeFromName(originalKeys[0]);
+ if (originalKey != 0)
+ {
+ _mappingService.DeleteSingleKeyMapping(originalKey);
+ }
+ }
+ else
+ {
+ string originalKeysString = string.Join(";", originalKeys.Select(k => _mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
+ _mappingService.DeleteShortcutMapping(originalKeysString);
+ }
+
+ if (!string.IsNullOrEmpty(programShortcut.Id))
+ {
+ SettingsManager.RemoveShortcutKeyMappingFromSettings(programShortcut.Id);
+ }
+ }
+
+ break;
+
+ case EditingItem.ItemType.UrlShortcut:
+ if (originalKeys.Count == 1)
+ {
+ int originalKey = _mappingService.GetKeyCodeFromName(originalKeys[0]);
+ if (originalKey != 0)
+ {
+ _mappingService.DeleteSingleKeyMapping(originalKey);
+ }
+ }
+ else
+ {
+ string originalKeysString = string.Join(";", originalKeys.Select(k => _mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
+ _mappingService.DeleteShortcutMapping(originalKeysString);
+ }
+
+ break;
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Error deleting existing mapping: " + ex.Message);
+ }
+ }
+
+ private bool SaveKeyOrShortcutMapping(List triggerKeys)
+ {
+ List actionKeys = UnifiedMappingControl.GetActionKeys();
+ bool isAppSpecific = UnifiedMappingControl.GetIsAppSpecific();
+ string appName = UnifiedMappingControl.GetAppName();
+
+ if (actionKeys == null || actionKeys.Count == 0)
+ {
+ return false;
+ }
+
+ return RemappingHelper.SaveMapping(_mappingService!, triggerKeys, actionKeys, isAppSpecific, appName);
+ }
+
+ private bool SaveTextMapping(List triggerKeys)
+ {
+ string textContent = UnifiedMappingControl.GetTextContent();
+ bool isAppSpecific = UnifiedMappingControl.GetIsAppSpecific();
+ string appName = UnifiedMappingControl.GetAppName();
+
+ if (string.IsNullOrEmpty(textContent))
+ {
+ return false;
+ }
+
+ if (triggerKeys.Count == 1)
+ {
+ // Single key to text mapping
+ int originalKey = _mappingService!.GetKeyCodeFromName(triggerKeys[0]);
+ if (originalKey != 0)
+ {
+ bool saved = _mappingService.AddSingleKeyToTextMapping(originalKey, textContent);
+ if (saved)
+ {
+ return _mappingService.SaveSettings();
+ }
+ }
+ }
+ else
+ {
+ // Shortcut to text mapping
+ string originalKeysString = string.Join(";", triggerKeys.Select(k => _mappingService!.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
+
+ bool saved;
+ if (isAppSpecific && !string.IsNullOrEmpty(appName))
+ {
+ saved = _mappingService!.AddShortcutMapping(originalKeysString, textContent, appName, ShortcutOperationType.RemapText);
+ }
+ else
+ {
+ saved = _mappingService!.AddShortcutMapping(originalKeysString, textContent, operationType: ShortcutOperationType.RemapText);
+ }
+
+ if (saved)
+ {
+ return _mappingService.SaveSettings();
+ }
+ }
+
+ return false;
+ }
+
+ 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)));
+
+ ShortcutKeyMapping shortcutKeyMapping = new ShortcutKeyMapping()
+ {
+ OperationType = ShortcutOperationType.OpenUri,
+ OriginalKeys = originalKeysString,
+ TargetKeys = originalKeysString,
+ UriToOpen = url,
+ };
+
+ bool saved = _mappingService!.AddShorcutMapping(shortcutKeyMapping);
+
+ if (saved)
+ {
+ return _mappingService.SaveSettings();
+ }
+
+ return false;
+ }
+
+ private bool SaveProgramMapping(List triggerKeys)
+ {
+ string programPath = UnifiedMappingControl.GetProgramPath();
+ string programArgs = UnifiedMappingControl.GetProgramArgs();
+ string startInDir = UnifiedMappingControl.GetStartInDirectory();
+ ElevationLevel elevationLevel = UnifiedMappingControl.GetElevationLevel();
+ StartWindowType visibility = UnifiedMappingControl.GetVisibility();
+ ProgramAlreadyRunningAction ifRunningAction = UnifiedMappingControl.GetIfRunningAction();
+
+ if (string.IsNullOrEmpty(programPath))
+ {
+ return false;
+ }
+
+ string originalKeysString = string.Join(";", triggerKeys.Select(k => _mappingService!.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
+
+ ShortcutKeyMapping shortcutKeyMapping = new ShortcutKeyMapping()
+ {
+ OperationType = ShortcutOperationType.RunProgram,
+ OriginalKeys = originalKeysString,
+ TargetKeys = originalKeysString,
+ ProgramPath = programPath,
+ ProgramArgs = programArgs,
+ StartInDirectory = startInDir,
+ IfRunningAction = ifRunningAction,
+ Visibility = visibility,
+ Elevation = elevationLevel,
+ };
+
+ bool saved = _mappingService!.AddShorcutMapping(shortcutKeyMapping);
+
+ if (saved)
+ {
+ _mappingService.SaveSettings();
+ SettingsManager.AddShortcutKeyMappingToSettings(shortcutKeyMapping);
+ return true;
+ }
+
+ return false;
+ }
+
+ #endregion
+
+ #region Load Methods
+
private void LoadAllMappings()
{
LoadRemappings();
@@ -244,6 +711,10 @@ namespace KeyboardManagerEditorUI.Pages
return keyName.ToString();
}
+ #endregion
+
+ #region IDisposable
+
public void Dispose()
{
Dispose(true);
@@ -263,5 +734,8 @@ namespace KeyboardManagerEditorUI.Pages
_disposed = true;
}
}
+
+ #endregion
}
}
+#pragma warning restore SA1124 // Do not use regions