[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
<img width="1071" height="671" alt="image"
src="https://github.com/user-attachments/assets/d2e81de0-6d92-4189-9a33-32e94cce74f7"
/>
<img width="2142" height="1341" alt="image"
src="https://github.com/user-attachments/assets/0e4e5685-fdf1-4dfd-ba52-a2e5bc9a66db"
/>



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 <liuhao3418@gmail.com>
Co-authored-by: chenmy77 <162882040+chenmy77@users.noreply.github.com>
Co-authored-by: Niels Laute <niels.laute@live.nl>
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 <duhowett@microsoft.com>
This commit is contained in:
Zach Teutsch
2026-03-04 15:46:42 -05:00
committed by GitHub
parent d20ae940d5
commit f651d1a611
85 changed files with 8080 additions and 399 deletions

View File

@@ -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<ValidationErrorType, (string Title, string Message)> 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<string> originalKeys,
List<string> 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<string> 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<string> 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<string> 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<string> 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<string> originalKeys, List<string> 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<string> 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<string> 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<string> 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<string> keys, KeyboardMappingService mappingService)
{
return string.Join(";", keys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
}
}
}