mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 09:46:54 +02:00
[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:
@@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user