[Keyboard Manager] Functional Improvements on new KBM UX (#38652)

* Make Disable toggle button functional in Shortcuts page

* Clean up main window and aggregate Constants

* Update key remapping naming

* Fix the key visual stretch out issue when the shortcut contains too many keys

* Refactor code structure

* fix wrapping in dialog

* update naming

* Implement Keyboard hook to get the user input of remappings

* Implement Reset for InputControl to make sure InputControl can have correct status for various dialog
This commit is contained in:
Hao Liu
2025-04-09 10:30:25 +08:00
committed by GitHub
parent c641fd17d2
commit c5635c1e3e
17 changed files with 411 additions and 221 deletions

View File

@@ -46,7 +46,7 @@ extern "C"
return static_cast<int>(mapping->singleKeyReMap.size());
}
bool GetSingleKeyRemap(void* config, int index, KeyboardMapping* mapping)
bool GetSingleKeyRemap(void* config, int index, SingleKeyMapping* mapping)
{
auto mappingConfig = static_cast<MappingConfiguration*>(config);
@@ -65,11 +65,13 @@ extern "C"
const auto& kv = allMappings[index];
mapping->originalKey = static_cast<int>(kv.first);
// Remap to single key
if (kv.second.index() == 0)
{
mapping->targetKey = AllocateAndCopyString(std::to_wstring(std::get<DWORD>(kv.second)));
mapping->isShortcut = false;
}
// Remap to shortcut
else if (kv.second.index() == 1)
{
mapping->targetKey = AllocateAndCopyString(std::get<Shortcut>(kv.second).ToHstringVK().c_str());

View File

@@ -10,7 +10,7 @@ struct KeyNamePair
wchar_t keyName[64];
};
struct KeyboardMapping
struct SingleKeyMapping
{
int originalKey;
wchar_t* targetKey;
@@ -43,7 +43,7 @@ extern "C"
__declspec(dllexport) bool SaveMappingSettings(void* config);
__declspec(dllexport) int GetSingleKeyRemapCount(void* config);
__declspec(dllexport) bool GetSingleKeyRemap(void* config, int index, KeyboardMapping* mapping);
__declspec(dllexport) bool GetSingleKeyRemap(void* config, int index, SingleKeyMapping* mapping);
__declspec(dllexport) int GetSingleKeyToTextRemapCount(void* config);
__declspec(dllexport) bool GetSingleKeyToTextRemap(void* config, int index, KeyboardTextMapping* mapping);

View File

@@ -8,6 +8,7 @@ using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using KeyboardManagerEditorUI.Helpers;
using ManagedCommon;
using Microsoft.UI;
using Microsoft.UI.Dispatching;
@@ -42,7 +43,6 @@ namespace KeyboardManagerEditorUI
Task.Run(() =>
{
Logger.InitializeLogger("\\Keyboard Manager\\WinUI3Editor\\Logs");
Logger.LogInfo("keyboard-manager WinUI3 editor logger is initialized");
});
UnhandledException += App_UnhandledException;
@@ -58,30 +58,24 @@ namespace KeyboardManagerEditorUI
var appWindow = window.AppWindow;
var windowSize = new Windows.Graphics.SizeInt32(960, 600);
var windowSize = new Windows.Graphics.SizeInt32(EditorConstants.DefaultEditorWindowWidth, EditorConstants.DefaultEditorWindowHeight);
appWindow.Resize(windowSize);
Task.Run(() =>
window.DispatcherQueue.TryEnqueue(() =>
{
App.Current.Resources.MergedDictionaries.Add(new ResourceDictionary()
window.Activate();
window.DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
Source = new Uri("ms-appx:///Styles/CommonStyle.xaml"),
});
}).ContinueWith(_ =>
{
window.DispatcherQueue.TryEnqueue(() =>
{
window.Activate();
window.DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
(window.Content as FrameworkElement)?.UpdateLayout();
});
(window.Content as FrameworkElement)?.UpdateLayout();
});
});
Logger.LogInfo("keyboard-manager WinUI3 editor window is launched");
}
/// <summary>
/// Log the unhandled exception for the editor.
/// </summary>
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
Logger.LogError("Unhandled exception", e.Exception);

View File

@@ -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.Helpers
{
public static class EditorConstants
{
// Default window size
public const int DefaultEditorWindowWidth = 960;
public const int DefaultEditorWindowHeight = 600;
}
}

View File

@@ -4,13 +4,15 @@
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 class Remapping
public partial class Remapping : INotifyPropertyChanged
{
public List<string> OriginalKeys { get; set; } = new List<string>();
@@ -20,6 +22,26 @@ namespace KeyboardManagerEditorUI.Helpers
public string AppName { get; set; } = "All Apps";
public bool IsEnabled { get; set; } = true;
private bool IsEnabledValue { 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));
}
}
}

View File

@@ -34,17 +34,14 @@ namespace KeyboardManagerEditorUI.Interop
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetSingleKeyRemap(IntPtr config, int index, ref KeyboardMapping mapping);
internal static extern bool GetSingleKeyRemap(IntPtr config, int index, ref SingleKeyMapping mapping);
[DllImport(DllName)]
internal static extern int GetSingleKeyToTextRemapCount(IntPtr config);
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetSingleKeyToTextRemap(
IntPtr config,
int index,
ref KeyboardTextMapping mapping);
internal static extern bool GetSingleKeyToTextRemap(IntPtr config, int index, ref KeyboardTextMapping mapping);
[DllImport(DllName)]
internal static extern int GetShortcutRemapCount(IntPtr config);
@@ -58,11 +55,7 @@ namespace KeyboardManagerEditorUI.Interop
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetShortcutRemapByType(
IntPtr config,
int operationType,
int index,
ref ShortcutMapping mapping);
internal static extern bool GetShortcutRemapByType(IntPtr config, int operationType, int index, ref ShortcutMapping mapping);
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
@@ -70,10 +63,7 @@ namespace KeyboardManagerEditorUI.Interop
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool AddSingleKeyToShortcutRemap(
IntPtr config,
int originalKey,
[MarshalAs(UnmanagedType.LPWStr)] string targetKeys);
internal static extern bool AddSingleKeyToShortcutRemap(IntPtr config, int originalKey, [MarshalAs(UnmanagedType.LPWStr)] string targetKeys);
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
@@ -84,27 +74,29 @@ namespace KeyboardManagerEditorUI.Interop
[MarshalAs(UnmanagedType.LPWStr)] string targetApp);
[DllImport(DllName)]
internal static extern int GetKeyCodeFromName(
[MarshalAs(UnmanagedType.LPWStr)] string keyName);
internal static extern int GetKeyCodeFromName([MarshalAs(UnmanagedType.LPWStr)] string keyName);
[DllImport(DllName, CharSet = CharSet.Unicode)]
internal static extern void GetKeyDisplayName(int keyCode, [Out] StringBuilder keyName, int maxLength);
[DllImport(DllName)]
internal static extern void FreeString(IntPtr str);
public static string GetStringAndFree(IntPtr ptr)
public static string GetStringAndFree(IntPtr handle)
{
if (ptr == IntPtr.Zero)
if (handle == IntPtr.Zero)
{
return string.Empty;
}
string? result = Marshal.PtrToStringUni(ptr);
FreeString(ptr);
string? result = Marshal.PtrToStringUni(handle);
FreeString(handle);
return result ?? string.Empty;
}
}
[StructLayout(LayoutKind.Sequential)]
public struct KeyboardMapping
public struct SingleKeyMapping
{
public int OriginalKey;
public IntPtr TargetKey;

View File

@@ -34,7 +34,7 @@ namespace KeyboardManagerEditorUI.Interop
for (int i = 0; i < count; i++)
{
var mapping = default(KeyboardMapping);
var mapping = default(SingleKeyMapping);
if (KeyboardManagerInterop.GetSingleKeyRemap(_configHandle, i, ref mapping))
{
result.Add(new KeyMapping
@@ -124,6 +124,13 @@ namespace KeyboardManagerEditorUI.Interop
return result;
}
public string GetKeyDisplayName(int keyCode)
{
var keyName = new StringBuilder(64);
KeyboardManagerInterop.GetKeyDisplayName(keyCode, keyName, keyName.Capacity);
return keyName.ToString();
}
public bool AddSingleKeyMapping(int originalKey, int targetKey)
{
return KeyboardManagerInterop.AddSingleKeyRemap(_configHandle, originalKey, targetKey);

View File

@@ -26,7 +26,6 @@
<ItemGroup>
<None Remove="Pages\ExistingUI.xaml" />
<None Remove="Pages\Programs.xaml" />
<None Remove="Pages\Shortcuts.xaml" />
<None Remove="Pages\Text.xaml" />
<None Remove="Pages\URLs.xaml" />
<None Remove="Styles\CommonStyle.xaml" />
@@ -77,7 +76,7 @@
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Pages\Shortcuts.xaml">
<Page Update="Pages\Remappings.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>

View File

@@ -40,6 +40,7 @@
IsBackButtonVisible="Collapsed"
IsBackEnabled="False"
IsPaneToggleButtonVisible="False"
IsSettingsVisible="False"
PaneDisplayMode="Top"
SelectionChanged="RootView_SelectionChanged">
<NavigationView.MenuItems>

View File

@@ -28,6 +28,8 @@ namespace KeyboardManagerEditorUI
this.InitializeComponent();
this.ExtendsContentIntoTitleBar = true;
this.SetTitleBar(titleBar);
// Set the default page
RootView.SelectedItem = RootView.MenuItems[0];
}
@@ -37,7 +39,7 @@ namespace KeyboardManagerEditorUI
{
switch ((string)selectedItem.Tag)
{
case "Remappings": NavigationFrame.Navigate(typeof(Pages.Shortcuts)); break;
case "Remappings": NavigationFrame.Navigate(typeof(Pages.Remappings)); break;
case "Programs": NavigationFrame.Navigate(typeof(Pages.Programs)); break;
case "Text": NavigationFrame.Navigate(typeof(Pages.Text)); break;
case "URLs": NavigationFrame.Navigate(typeof(Pages.URLs)); break;

View File

@@ -23,7 +23,7 @@
Click="NewShortcutBtn_Click">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon
FontSize="16"
FontSize="14"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="&#xE109;" />
<TextBlock VerticalAlignment="Center" Text="New" />

View File

@@ -1,8 +1,9 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="KeyboardManagerEditorUI.Pages.Shortcuts"
x:Class="KeyboardManagerEditorUI.Pages.Remappings"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helper="using:KeyboardManagerEditorUI.Helpers"
xmlns:local="using:KeyboardManagerEditorUI.Pages"
@@ -17,13 +18,13 @@
Text="Remappings allow you to reconfigure a single key or shortcut to a new key or shortcut"
TextWrapping="Wrap" />
<Button
x:Name="NewShortcutBtn"
x:Name="NewRemappingBtn"
Height="36"
Margin="0,12,0,0"
Click="NewShortcutBtn_Click">
Click="NewRemappingBtn_Click">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon
FontSize="16"
FontSize="14"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="&#xE109;" />
<TextBlock VerticalAlignment="Center" Text="New" />
@@ -85,14 +86,14 @@
Grid.ColumnSpan="4"
IsItemClickEnabled="True"
ItemClick="ListView_ItemClick"
ItemsSource="{x:Bind RemappedShortcuts}"
ItemsSource="{x:Bind RemappingList}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="helper:Remapping">
<Grid Height="48">
<Grid Height="Auto" MinHeight="48">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="232" />
<ColumnDefinition Width="Auto" MinWidth="238" />
<ColumnDefinition Width="Auto" MinWidth="236" />
<ColumnDefinition Width="Auto" MinWidth="236" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="84" />
</Grid.ColumnDefinitions>
@@ -112,7 +113,10 @@
ItemsSource="{x:Bind OriginalKeys}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="4" />
<controls:WrapPanel
MaxWidth="230"
HorizontalSpacing="4"
Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
@@ -132,12 +136,18 @@
ItemsSource="{x:Bind RemappedKeys}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="4" />
<controls:WrapPanel
MaxWidth="230"
HorizontalSpacing="4"
Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<styles:KeyVisual Content="{Binding}" VisualType="Small" />
<styles:KeyVisual
HorizontalAlignment="Left"
Content="{Binding}"
VisualType="Small" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
@@ -163,14 +173,14 @@
</StackPanel>
<ContentDialog
x:Name="KeyDialog"
Title="Remap a shortcut"
Title="Remappings"
MinWidth="600"
MaxWidth="600"
PrimaryButtonStyle="{StaticResource AccentButtonStyle}"
PrimaryButtonText="Save"
SecondaryButtonText="Cancel">
<Grid>
<styles:InputControl x:Name="ShortcutControl" />
<styles:InputControl x:Name="RemappingControl" />
</Grid>
</ContentDialog>
</Grid>

View File

@@ -27,38 +27,33 @@ using Windows.Foundation.Collections;
namespace KeyboardManagerEditorUI.Pages
{
public sealed partial class Shortcuts : Page, IDisposable
/// <summary>
/// The Remapping page that allow users to configure a single key or shortcut to a new key or shortcut
/// </summary>
public sealed partial class Remappings : Page, IDisposable
{
private KeyboardMappingService? _mappingService;
private bool _disposed;
// The list of single key mappings
public ObservableCollection<KeyMapping> SingleKeyMappings { get; } = new ObservableCollection<KeyMapping>();
public ObservableCollection<ShortcutKeyMapping> ShortcutMappings { get; } = new ObservableCollection<ShortcutKeyMapping>();
// The list of shortcut key mappings
public ObservableCollection<ShortcutKeyMapping> ShortcutKeyMappings { get; } = new ObservableCollection<ShortcutKeyMapping>();
public ObservableCollection<Remapping> RemappedShortcuts { get; set; }
// The full list of remappings
public ObservableCollection<Remapping> RemappingList { get; set; }
[DllImport("PowerToys.KeyboardManagerEditorLibraryWrapper.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
private static extern void GetKeyDisplayName(int keyCode, [Out] StringBuilder keyName, int maxLength);
public Shortcuts()
public Remappings()
{
this.InitializeComponent();
RemappedShortcuts = new ObservableCollection<Remapping>();
RemappingList = new ObservableCollection<Remapping>();
_mappingService = new KeyboardMappingService();
LoadMappings();
/*
RemappedShortcuts = new ObservableCollection<Remapping>();
RemappedShortcuts.Add(new Remapping() { OriginalKeys = new List<string>() { "Ctrl", "Shift", "F" }, RemappedKeys = new List<string>() { "Shift", "F" }, IsAllApps = true });
RemappedShortcuts.Add(new Remapping() { OriginalKeys = new List<string>() { "Ctrl (Left)" }, RemappedKeys = new List<string>() { "Ctrl (Right)" }, IsAllApps = true });
RemappedShortcuts.Add(new Remapping() { OriginalKeys = new List<string>() { "Shift", "M" }, RemappedKeys = new List<string>() { "Ctrl", "M" }, IsAllApps = true });
RemappedShortcuts.Add(new Remapping() { OriginalKeys = new List<string>() { "Shift", "Alt", "B" }, RemappedKeys = new List<string>() { "Alt", "B" }, IsAllApps = false, AppName = "outlook.exe" });
RemappedShortcuts.Add(new Remapping() { OriginalKeys = new List<string>() { "Numpad 1" }, RemappedKeys = new List<string>() { "Ctrl", "F" }, IsAllApps = true });
RemappedShortcuts.Add(new Remapping() { OriginalKeys = new List<string>() { "Numpad 2" }, RemappedKeys = new List<string>() { "Alt", "F" }, IsAllApps = true, AppName = "outlook.exe" });
*/
// Load all existing remappings
LoadMappings();
}
private void LoadMappings()
@@ -69,35 +64,37 @@ namespace KeyboardManagerEditorUI.Pages
}
SingleKeyMappings.Clear();
ShortcutMappings.Clear();
RemappedShortcuts.Clear();
ShortcutKeyMappings.Clear();
RemappingList.Clear();
// Load all single key mappings
foreach (var mapping in _mappingService.GetSingleKeyMappings())
{
SingleKeyMappings.Add(mapping);
string[] targetKeyCode = mapping.TargetKey.Split(';');
string[] targetKeyCodes = mapping.TargetKey.Split(';');
var targetKeyNames = new List<string>();
foreach (var keyCode in targetKeyCode)
foreach (var keyCode in targetKeyCodes)
{
if (int.TryParse(keyCode, out int code))
{
targetKeyNames.Add(GetKeyDisplayName(code));
targetKeyNames.Add(_mappingService.GetKeyDisplayName(code));
}
}
RemappedShortcuts.Add(new Remapping
RemappingList.Add(new Remapping
{
OriginalKeys = new List<string> { GetKeyDisplayName(mapping.OriginalKey) },
OriginalKeys = new List<string> { _mappingService.GetKeyDisplayName(mapping.OriginalKey) },
RemappedKeys = targetKeyNames,
IsAllApps = true,
});
}
// Load all shortcut key mappings
foreach (var mapping in _mappingService.GetShortcutMappingsByType(ShortcutOperationType.RemapShortcut))
{
ShortcutMappings.Add(mapping);
ShortcutKeyMappings.Add(mapping);
string[] originalKeyCodes = mapping.OriginalKeys.Split(';');
string[] targetKeyCodes = mapping.TargetKeys.Split(';');
@@ -109,7 +106,7 @@ namespace KeyboardManagerEditorUI.Pages
{
if (int.TryParse(keyCode, out int code))
{
originalKeyNames.Add(GetKeyDisplayName(code));
originalKeyNames.Add(_mappingService.GetKeyDisplayName(code));
}
}
@@ -117,11 +114,11 @@ namespace KeyboardManagerEditorUI.Pages
{
if (int.TryParse(keyCode, out int code))
{
targetKeyNames.Add(GetKeyDisplayName(code));
targetKeyNames.Add(_mappingService.GetKeyDisplayName(code));
}
}
RemappedShortcuts.Add(new Remapping
RemappingList.Add(new Remapping
{
OriginalKeys = originalKeyNames,
RemappedKeys = targetKeyNames,
@@ -131,13 +128,6 @@ namespace KeyboardManagerEditorUI.Pages
}
}
public static string GetKeyDisplayName(int keyCode)
{
var keyName = new StringBuilder(64);
GetKeyDisplayName(keyCode, keyName, keyName.Capacity);
return keyName.ToString();
}
public void Dispose()
{
Dispose(true);
@@ -159,15 +149,20 @@ namespace KeyboardManagerEditorUI.Pages
}
}
private async void NewShortcutBtn_Click(object sender, RoutedEventArgs e)
private async void NewRemappingBtn_Click(object sender, RoutedEventArgs e)
{
ShortcutControl.SetOriginalKeys(new List<string>());
ShortcutControl.SetRemappedKeys(new List<string>());
ShortcutControl.SetApp(false, string.Empty);
RemappingControl.SetOriginalKeys(new List<string>());
RemappingControl.SetRemappedKeys(new List<string>());
RemappingControl.SetApp(false, string.Empty);
RemappingControl.SetKeyboardHook();
// Show the dialog to add a new remapping
KeyDialog.PrimaryButtonClick += KeyDialog_PrimaryButtonClick;
await KeyDialog.ShowAsync();
KeyDialog.PrimaryButtonClick -= KeyDialog_PrimaryButtonClick;
RemappingControl.CleanupKeyboardHook();
}
private void KeyDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
@@ -178,12 +173,19 @@ namespace KeyboardManagerEditorUI.Pages
private async void ListView_ItemClick(object sender, ItemClickEventArgs e)
{
if (e.ClickedItem is Remapping selectedShortcut)
if (e.ClickedItem is Remapping selectedRemapping && selectedRemapping.IsEnabled)
{
ShortcutControl.SetOriginalKeys(selectedShortcut.OriginalKeys);
ShortcutControl.SetRemappedKeys(selectedShortcut.RemappedKeys);
ShortcutControl.SetApp(!selectedShortcut.IsAllApps, selectedShortcut.AppName);
RemappingControl.SetOriginalKeys(selectedRemapping.OriginalKeys);
RemappingControl.SetRemappedKeys(selectedRemapping.RemappedKeys);
RemappingControl.SetApp(!selectedRemapping.IsAllApps, selectedRemapping.AppName);
RemappingControl.SetKeyboardHook();
KeyDialog.PrimaryButtonClick += KeyDialog_PrimaryButtonClick;
await KeyDialog.ShowAsync();
KeyDialog.PrimaryButtonClick -= KeyDialog_PrimaryButtonClick;
RemappingControl.CleanupKeyboardHook();
}
}
@@ -201,10 +203,10 @@ namespace KeyboardManagerEditorUI.Pages
try
{
List<string> originalKeys = ShortcutControl.GetOriginalKeys();
List<string> remappedKeys = ShortcutControl.GetRemappedKeys();
bool isAppSpecific = ShortcutControl.GetIsAppSpecific();
string appName = ShortcutControl.GetAppName();
List<string> originalKeys = RemappingControl.GetOriginalKeys();
List<string> remappedKeys = RemappingControl.GetRemappedKeys();
bool isAppSpecific = RemappingControl.GetIsAppSpecific();
string appName = RemappingControl.GetAppName();
// mock data
// originalKeys = ["A", "Ctrl"];

View File

@@ -23,7 +23,7 @@
Click="NewShortcutBtn_Click">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon
FontSize="16"
FontSize="14"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="&#xE109;" />
<TextBlock VerticalAlignment="Center" Text="New" />

View File

@@ -23,7 +23,7 @@
Click="NewShortcutBtn_Click">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon
FontSize="16"
FontSize="14"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="&#xE109;" />
<TextBlock VerticalAlignment="Center" Text="New" />

View File

@@ -3,11 +3,11 @@
x:Class="KeyboardManagerEditorUI.Styles.InputControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:KeyboardManagerEditorUI.Controls"
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:KeyboardManagerEditorUI.Styles"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
Loaded="UserControl_Loaded"
mc:Ignorable="d">
<StackPanel>
@@ -24,6 +24,9 @@
</Grid.RowDefinitions>
<TextBlock Margin="0,12,0,0" Text="Original key(s)" />
<Grid Grid.Column="2">
<TextBlock Margin="0,12,0,0" Text="New key(s)" />
</Grid>
<Grid Grid.Row="1" Margin="0,8,0,0">
<ToggleButton
x:Name="OriginalToggleBtn"
@@ -35,7 +38,7 @@
<ItemsControl x:Name="OriginalKeys">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="4" />
<controls:WrapPanel HorizontalSpacing="4" Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
@@ -48,12 +51,6 @@
</ToggleButton>
</Grid>
<Grid Grid.Row="1" Margin="0,8,0,0">
<StackPanel
Name="KeyStackPanel" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
</StackPanel>
</Grid>
<TextBlock
Grid.Row="1"
Grid.Column="1"
@@ -77,7 +74,7 @@
<ItemsControl x:Name="RemappedKeys">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="4" />
<controls:WrapPanel HorizontalSpacing="4" Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
@@ -89,12 +86,6 @@
</ToggleButton.Content>
</ToggleButton>
</Grid>
<Grid Grid.Row="1" Grid.Column="2" Margin="0,8,0,0">
<StackPanel
Name="NewKeyStackPanel" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
</StackPanel>
</Grid>
</Grid>
<CheckBox

View File

@@ -3,18 +3,32 @@
// 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.Interop;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Windows.System;
namespace KeyboardManagerEditorUI.Styles
{
public sealed partial class InputControl : UserControl
public sealed partial class InputControl : UserControl, IDisposable
{
private List<string> pressedKeys = new List<string>();
private List<string> newPressedKeys = new List<string>();
// List to store pressed keys
private HashSet<VirtualKey> _currentlyPressedKeys = new HashSet<VirtualKey>();
// List to store order of remapped keys
private List<VirtualKey> _keyPressOrder = new List<VirtualKey>();
// Collection to store original and remapped keys
private ObservableCollection<string> _originalKeys = new ObservableCollection<string>();
private ObservableCollection<string> _remappedKeys = new ObservableCollection<string>();
private HotkeySettingsControlHook? _keyboardHook;
private bool _disposed;
// Define newMode as a DependencyProperty for binding
public static readonly DependencyProperty NewModeProperty =
@@ -33,127 +47,200 @@ namespace KeyboardManagerEditorUI.Styles
public InputControl()
{
this.InitializeComponent();
this.KeyDown += (sender, e) => InputControl_KeyDown(sender, e, NewMode);
this.KeyUp += (sender, e) => InputControl_KeyUp(sender, e, NewMode);
this.OriginalKeys.ItemsSource = _originalKeys;
this.RemappedKeys.ItemsSource = _remappedKeys;
this.Unloaded += InputControl_Unloaded;
}
private void InputControl_Unloaded(object sender, RoutedEventArgs e)
{
// Reset the control when it is unloaded
Reset();
}
private void InputControl_KeyDown(int key)
{
// if no keys are pressed, clear the lists when a new key is pressed
if (_currentlyPressedKeys.Count == 0)
{
if (NewMode)
{
_remappedKeys.Clear();
}
else
{
_originalKeys.Clear();
}
_keyPressOrder.Clear();
}
VirtualKey virtualKey = (VirtualKey)key;
if (_currentlyPressedKeys.Add(virtualKey))
{
_keyPressOrder.Add(virtualKey);
UpdateKeysDisplay();
}
}
private void InputControl_KeyUp(int key)
{
VirtualKey virtualKey = (VirtualKey)key;
if (_currentlyPressedKeys.Remove(virtualKey))
{
_keyPressOrder.Remove(virtualKey);
}
}
private static void OnNewModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = d as InputControl;
if (control != null)
if (d is InputControl control)
{
bool newMode = (bool)e.NewValue;
control.UpdateKeyDisplay(newMode);
// Clear the lists when the mode changes
control._currentlyPressedKeys.Clear();
control._keyPressOrder.Clear();
}
}
private void InputControl_KeyDown(object sender, KeyRoutedEventArgs e, bool newMode)
public void SetKeyboardHook()
{
// Get the key name and add it to the list if it's not already there
string keyName = e.Key.ToString();
keyName = NormalizeKeyName(keyName);
CleanupKeyboardHook();
var currentKeyList = newMode ? newPressedKeys : pressedKeys;
_keyboardHook = new HotkeySettingsControlHook(
InputControl_KeyDown,
InputControl_KeyUp,
() => true,
(key, extraInfo) => true);
}
if (!currentKeyList.Contains(keyName))
public void CleanupKeyboardHook()
{
if (_keyboardHook != null)
{
var allowedModifiers = new[] { "Shift", "Ctrl", "LWin", "RWin", "Alt" };
if (currentKeyList.All(k => allowedModifiers.Contains(k)) && pressedKeys.Count < 4)
_keyboardHook.Dispose();
_keyboardHook = null;
}
}
private void UpdateKeysDisplay()
{
var formattedKeys = GetFormattedKeyList();
if (NewMode)
{
_remappedKeys.Clear();
foreach (var key in formattedKeys)
{
currentKeyList.Add(keyName);
UpdateKeyDisplay(newMode);
_remappedKeys.Add(key);
}
}
else
{
_originalKeys.Clear();
foreach (var key in formattedKeys)
{
_originalKeys.Add(key);
}
}
}
private void InputControl_KeyUp(object sender, KeyRoutedEventArgs e, bool newMode)
private List<string> GetFormattedKeyList()
{
// Console.WriteLine(newMode);
string keyName = e.Key.ToString();
var currentKeyList = newMode ? newPressedKeys : pressedKeys;
List<string> keyList = new List<string>();
List<VirtualKey> modifierKeys = new List<VirtualKey>();
VirtualKey? actionKey = null;
if (!currentKeyList.Contains(keyName))
foreach (var key in _keyPressOrder)
{
return;
}
// Remove the key name from the list when the key is released
// currentKeyList.Remove(keyName);
UpdateKeyDisplay(newMode);
}
private void UpdateKeyDisplay(bool newMode)
{
// Clear current UI elements
if (newMode)
{
NewKeyStackPanel.Children.Clear();
} // Assuming keyPanel2 is your second stack panel
else
{
KeyStackPanel.Children.Clear();
}
var currentKeyList = newMode ? newPressedKeys : pressedKeys;
// Add each pressed key as a TextBlock in the StackPanel
foreach (var key in currentKeyList)
{
Border keyBlockContainer = new Border
if (!_currentlyPressedKeys.Contains(key))
{
Background = new SolidColorBrush(Microsoft.UI.Colors.White),
Padding = new Thickness(10),
Margin = new Thickness(1),
CornerRadius = new CornerRadius(3),
BorderBrush = new SolidColorBrush(Microsoft.UI.Colors.Black),
BorderThickness = new Thickness(1),
};
continue;
}
TextBlock keyBlock = new TextBlock
if (IsModifierKey(key))
{
Text = key,
FontSize = 12,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center,
Foreground = new SolidColorBrush(Microsoft.UI.Colors.Black),
};
// Add TextBlock inside the Border container
keyBlockContainer.Child = keyBlock;
// Add Border to StackPanel
if (newMode)
{
keyBlockContainer.Background = Application.Current.Resources["AccentButtonBackgroundPressed"] as SolidColorBrush;
keyBlockContainer.BorderBrush = Application.Current.Resources["AccentButtonBackgroundPressed"] as SolidColorBrush;
keyBlock.Foreground = new SolidColorBrush(Microsoft.UI.Colors.White);
NewKeyStackPanel.Children.Add(keyBlockContainer); // For remapping keys
if (!modifierKeys.Contains(key))
{
modifierKeys.Add(key);
}
}
else
{
KeyStackPanel.Children.Add(keyBlockContainer); // For normal keys
actionKey = key;
}
}
foreach (var key in modifierKeys)
{
keyList.Add(GetKeyDisplayName((int)key));
}
if (actionKey.HasValue)
{
keyList.Add(GetKeyDisplayName((int)actionKey.Value));
}
return keyList;
}
private string GetKeyDisplayName(int keyCode)
{
var keyName = new System.Text.StringBuilder(64);
KeyboardManagerInterop.GetKeyDisplayName(keyCode, keyName, keyName.Capacity);
return keyName.ToString();
}
private 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;
}
public void SetRemappedKeys(List<string> keys)
{
RemappedKeys.ItemsSource = keys;
_remappedKeys.Clear();
if (keys != null)
{
foreach (var key in keys)
{
_remappedKeys.Add(key);
}
}
}
public void SetOriginalKeys(List<string> keys)
{
OriginalKeys.ItemsSource = keys;
_originalKeys.Clear();
if (keys != null)
{
foreach (var key in keys)
{
_originalKeys.Add(key);
}
}
}
public List<string> GetOriginalKeys()
{
return pressedKeys as List<string> ?? new List<string>();
return _originalKeys.ToList();
}
public List<string> GetRemappedKeys()
{
return newPressedKeys as List<string> ?? new List<string>();
return _remappedKeys.ToList();
}
public bool GetIsAppSpecific()
@@ -168,14 +255,36 @@ namespace KeyboardManagerEditorUI.Styles
private void RemappedToggleBtn_Checked(object sender, RoutedEventArgs e)
{
NewMode = true;
RemappedToggleBtn.IsChecked = false;
// Only set NewMode to true if RemappedToggleBtn is checked
if (RemappedToggleBtn.IsChecked == true)
{
NewMode = true;
// Make sure OriginalToggleBtn is unchecked
if (OriginalToggleBtn.IsChecked == true)
{
OriginalToggleBtn.IsChecked = false;
}
}
this.Focus(FocusState.Programmatic);
}
private void OriginalToggleBtn_Checked(object sender, RoutedEventArgs e)
{
NewMode = false;
OriginalToggleBtn.IsChecked = false;
// Only set NewMode to false if OriginalToggleBtn is checked
if (OriginalToggleBtn.IsChecked == true)
{
NewMode = false;
// Make sure RemappedToggleBtn is unchecked
if (RemappedToggleBtn.IsChecked == true)
{
RemappedToggleBtn.IsChecked = false;
}
}
this.Focus(FocusState.Programmatic);
}
public void SetApp(bool isSpecificApp, string appName)
@@ -209,20 +318,60 @@ namespace KeyboardManagerEditorUI.Styles
AllAppsCheckBox.Unchecked += AllAppsCheckBox_Unchecked;
}
private string NormalizeKeyName(string keyName)
public void Dispose()
{
switch (keyName)
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!_disposed)
{
case "Control":
return "Ctrl";
case "Menu":
return "Alt";
case "LeftWindows":
return "LWin";
case "RightWindows":
return "RWin";
default:
return keyName;
if (disposing)
{
CleanupKeyboardHook();
Reset();
}
_disposed = true;
}
}
public void Reset()
{
// Reset key status
_currentlyPressedKeys.Clear();
_keyPressOrder.Clear();
// Reset displayed keys
_originalKeys.Clear();
_remappedKeys.Clear();
// Reset toggle button status
if (RemappedToggleBtn != null)
{
RemappedToggleBtn.IsChecked = false;
}
if (OriginalToggleBtn != null)
{
OriginalToggleBtn.IsChecked = false;
}
NewMode = false;
// Reset app name text box
if (AppNameTextBox != null)
{
AppNameTextBox.Text = string.Empty;
}
// Reset the focus status
if (this.FocusState != FocusState.Unfocused)
{
this.IsTabStop = false;
this.IsTabStop = true;
}
}
}