[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

@@ -205,6 +205,7 @@ comdlg
comexp comexp
cominterop cominterop
commandpalette commandpalette
commoncontrols
compmgmt compmgmt
COMPOSITIONFULL COMPOSITIONFULL
CONFIGW CONFIGW
@@ -677,6 +678,7 @@ jpnime
Jsons Jsons
jsonval jsonval
jxr jxr
kbmcontrols
keybd keybd
KEYBDDATA KEYBDDATA
KEYBDINPUT KEYBDINPUT
@@ -1530,6 +1532,7 @@ tlc
TPMLEFTALIGN TPMLEFTALIGN
TPMRETURNCMD TPMRETURNCMD
TNP TNP
Toggleable
Toolhelp Toolhelp
toolwindow toolwindow
TOPDOWNDIB TOPDOWNDIB
@@ -2203,6 +2206,7 @@ wft
wikimedia wikimedia
wikipedia wikipedia
windowedge windowedge
WINDOWSAPPRUNTIME
windowsml windowsml
winexe winexe
winforms winforms

View File

@@ -106,7 +106,12 @@
"PowerToys.SvgThumbnailProvider.dll", "PowerToys.SvgThumbnailProvider.dll",
"PowerToys.SvgThumbnailProvider.exe", "PowerToys.SvgThumbnailProvider.exe",
"PowerToys.SvgThumbnailProviderCpp.dll", "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.HostsModuleInterface.dll",
"WinUI3Apps\\PowerToys.HostsUILib.dll", "WinUI3Apps\\PowerToys.HostsUILib.dll",
"WinUI3Apps\\PowerToys.Hosts.dll", "WinUI3Apps\\PowerToys.Hosts.dll",

View File

@@ -20,6 +20,7 @@
<NuGetAuditMode>direct</NuGetAuditMode> <NuGetAuditMode>direct</NuGetAuditMode>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion> <!-- Don't add source revision hash to the product version of binaries. --> <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion> <!-- Don't add source revision hash to the product version of binaries. -->
<PlatformTarget>$(Platform)</PlatformTarget> <PlatformTarget>$(Platform)</PlatformTarget>
<RestoreEnablePackagePruning Condition=" '$(VisualStudioVersion)' == '17.0'">false </RestoreEnablePackagePruning>
<!-- Enable Microsoft.Testing.Platform --> <!-- Enable Microsoft.Testing.Platform -->
<EnableMSTestRunner>true</EnableMSTestRunner> <EnableMSTestRunner>true</EnableMSTestRunner>

View File

@@ -497,6 +497,31 @@
<Project Path="src/modules/keyboardmanager/KeyboardManagerEngine/KeyboardManagerEngine.vcxproj" Id="ba661f5b-1d5a-4ffc-9bf1-fc39df280bdd" /> <Project Path="src/modules/keyboardmanager/KeyboardManagerEngine/KeyboardManagerEngine.vcxproj" Id="ba661f5b-1d5a-4ffc-9bf1-fc39df280bdd" />
<Project Path="src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardManagerEngineLibrary.vcxproj" Id="e496b7fc-1e99-4bab-849b-0e8367040b02" /> <Project Path="src/modules/keyboardmanager/KeyboardManagerEngineLibrary/KeyboardManagerEngineLibrary.vcxproj" Id="e496b7fc-1e99-4bab-849b-0e8367040b02" />
</Folder> </Folder>
<Folder Name="/modules/keyboardmanager/MouseUtils/">
<Project Path="src/modules/MouseUtils/CursorWrap/CursorWrap.vcxproj" Id="48a1db8c-5df8-4fb3-9e14-2b67f3f2d8b5" />
<Project Path="src/modules/MouseUtils/FindMyMouse/FindMyMouse.vcxproj" Id="e94fd11c-0591-456f-899f-efc0ca548336" />
<Project Path="src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.vcxproj" Id="782a61be-9d85-4081-b35c-1ccc9dcc1e88" />
<Project Path="src/modules/MouseUtils/MouseJump.Common/MouseJump.Common.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/MouseUtils/MouseJump/MouseJump.vcxproj" Id="8a08d663-4995-40e3-b42c-3f910625f284" />
<Project Path="src/modules/MouseUtils/MouseJumpUI/MouseJumpUI.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/MouseUtils/MousePointerCrosshairs/MousePointerCrosshairs.vcxproj" Id="eae14c0e-7a6b-45da-9080-a7d8c077ba6e" />
</Folder>
<Folder Name="/modules/keyboardmanager/MouseUtils/Tests/">
<Project Path="src/modules/MouseUtils/MouseJump.Common.UnitTests/MouseJump.Common.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/MouseUtils/MouseUtils.UITests/MouseUtils.UITests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
</Folder>
<Folder Name="/modules/keyboardmanager/Tests/"> <Folder Name="/modules/keyboardmanager/Tests/">
<Project Path="src/modules/keyboardmanager/KeyboardManagerEditorTest/KeyboardManagerEditorTest.vcxproj" Id="62173d9a-6724-4c00-a1c8-fb646480a9ec" /> <Project Path="src/modules/keyboardmanager/KeyboardManagerEditorTest/KeyboardManagerEditorTest.vcxproj" Id="62173d9a-6724-4c00-a1c8-fb646480a9ec" />
<Project Path="src/modules/keyboardmanager/KeyboardManagerEngineTest/KeyboardManagerEngineTest.vcxproj" Id="7f4b3a60-bc27-45a7-8000-68b0b6ea7466" /> <Project Path="src/modules/keyboardmanager/KeyboardManagerEngineTest/KeyboardManagerEngineTest.vcxproj" Id="7f4b3a60-bc27-45a7-8000-68b0b6ea7466" />
@@ -720,31 +745,6 @@
<Platform Solution="*|x64" Project="x64" /> <Platform Solution="*|x64" Project="x64" />
</Project> </Project>
</Folder> </Folder>
<Folder Name="/modules/MouseUtils/">
<Project Path="src/modules/MouseUtils/CursorWrap/CursorWrap.vcxproj" Id="48a1db8c-5df8-4fb3-9e14-2b67f3f2d8b5" />
<Project Path="src/modules/MouseUtils/FindMyMouse/FindMyMouse.vcxproj" Id="e94fd11c-0591-456f-899f-efc0ca548336" />
<Project Path="src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.vcxproj" Id="782a61be-9d85-4081-b35c-1ccc9dcc1e88" />
<Project Path="src/modules/MouseUtils/MouseJump.Common/MouseJump.Common.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/MouseUtils/MouseJump/MouseJump.vcxproj" Id="8a08d663-4995-40e3-b42c-3f910625f284" />
<Project Path="src/modules/MouseUtils/MouseJumpUI/MouseJumpUI.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/MouseUtils/MousePointerCrosshairs/MousePointerCrosshairs.vcxproj" Id="eae14c0e-7a6b-45da-9080-a7d8c077ba6e" />
</Folder>
<Folder Name="/modules/MouseUtils/Tests/">
<Project Path="src/modules/MouseUtils/MouseJump.Common.UnitTests/MouseJump.Common.UnitTests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/MouseUtils/MouseUtils.UITests/MouseUtils.UITests.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
</Folder>
<Folder Name="/modules/MouseWithoutBorders/"> <Folder Name="/modules/MouseWithoutBorders/">
<Project Path="src/modules/MouseWithoutBorders/App/Helper/MouseWithoutBordersHelper.csproj"> <Project Path="src/modules/MouseWithoutBorders/App/Helper/MouseWithoutBordersHelper.csproj">
<Platform Solution="*|ARM64" Project="ARM64" /> <Platform Solution="*|ARM64" Project="ARM64" />

View File

@@ -1,11 +1,11 @@
<ResourceDictionary <ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Microsoft.PowerToys.Common.UI.Controls"> xmlns:commoncontrols="using:Microsoft.PowerToys.Common.UI.Controls">
<Style BasedOn="{StaticResource DefaultKeyVisualStyle}" TargetType="local:KeyVisual" /> <Style BasedOn="{StaticResource DefaultKeyVisualStyle}" TargetType="commoncontrols:KeyVisual" />
<Style x:Key="DefaultKeyVisualStyle" TargetType="local:KeyVisual"> <Style x:Key="DefaultKeyVisualStyle" TargetType="commoncontrols:KeyVisual">
<Setter Property="MinWidth" Value="16" /> <Setter Property="MinWidth" Value="16" />
<Setter Property="AutomationProperties.AccessibilityView" Value="Raw" /> <Setter Property="AutomationProperties.AccessibilityView" Value="Raw" />
<Setter Property="IsTabStop" Value="False" /> <Setter Property="IsTabStop" Value="False" />
@@ -25,7 +25,7 @@
<Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="local:KeyVisual"> <ControlTemplate TargetType="commoncontrols:KeyVisual">
<Grid <Grid
x:Name="KeyHolder" x:Name="KeyHolder"
MinWidth="{TemplateBinding MinWidth}" MinWidth="{TemplateBinding MinWidth}"
@@ -40,7 +40,7 @@
<Grid.BackgroundTransition> <Grid.BackgroundTransition>
<BrushTransition Duration="0:0:0.083" /> <BrushTransition Duration="0:0:0.083" />
</Grid.BackgroundTransition> </Grid.BackgroundTransition>
<local:KeyCharPresenter <commoncontrols:KeyCharPresenter
x:Name="KeyPresenter" x:Name="KeyPresenter"
Margin="{TemplateBinding Padding}" Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
@@ -87,12 +87,12 @@
<Style <Style
x:Key="SubtleKeyVisualStyle" x:Key="SubtleKeyVisualStyle"
BasedOn="{StaticResource DefaultKeyVisualStyle}" BasedOn="{StaticResource DefaultKeyVisualStyle}"
TargetType="local:KeyVisual"> TargetType="commoncontrols:KeyVisual">
<Setter Property="Background" Value="{ThemeResource SubtleFillColorTransparentBrush}" /> <Setter Property="Background" Value="{ThemeResource SubtleFillColorTransparentBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource SubtleFillColorTransparentBrush}" /> <Setter Property="BorderBrush" Value="{ThemeResource SubtleFillColorTransparentBrush}" />
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="local:KeyVisual"> <ControlTemplate TargetType="commoncontrols:KeyVisual">
<Grid <Grid
x:Name="KeyHolder" x:Name="KeyHolder"
MinWidth="{TemplateBinding MinWidth}" MinWidth="{TemplateBinding MinWidth}"
@@ -106,7 +106,7 @@
<Grid.BackgroundTransition> <Grid.BackgroundTransition>
<BrushTransition Duration="0:0:0.083" /> <BrushTransition Duration="0:0:0.083" />
</Grid.BackgroundTransition> </Grid.BackgroundTransition>
<local:KeyCharPresenter <commoncontrols:KeyCharPresenter
x:Name="KeyPresenter" x:Name="KeyPresenter"
Margin="{TemplateBinding Padding}" Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
@@ -145,14 +145,14 @@
<Style <Style
x:Key="AccentKeyVisualStyle" x:Key="AccentKeyVisualStyle"
BasedOn="{StaticResource DefaultKeyVisualStyle}" BasedOn="{StaticResource DefaultKeyVisualStyle}"
TargetType="local:KeyVisual"> TargetType="commoncontrols:KeyVisual">
<Setter Property="Background" Value="{ThemeResource AccentFillColorDefaultBrush}" /> <Setter Property="Background" Value="{ThemeResource AccentFillColorDefaultBrush}" />
<Setter Property="Foreground" Value="{ThemeResource TextOnAccentFillColorPrimaryBrush}" /> <Setter Property="Foreground" Value="{ThemeResource TextOnAccentFillColorPrimaryBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource AccentControlElevationBorderBrush}" /> <Setter Property="BorderBrush" Value="{ThemeResource AccentControlElevationBorderBrush}" />
<Setter Property="BackgroundSizing" Value="OuterBorderEdge" /> <Setter Property="BackgroundSizing" Value="OuterBorderEdge" />
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
<ControlTemplate TargetType="local:KeyVisual"> <ControlTemplate TargetType="commoncontrols:KeyVisual">
<Grid <Grid
x:Name="KeyHolder" x:Name="KeyHolder"
MinWidth="{TemplateBinding MinWidth}" MinWidth="{TemplateBinding MinWidth}"
@@ -168,7 +168,7 @@
<Grid.BackgroundTransition> <Grid.BackgroundTransition>
<BrushTransition Duration="0:0:0.083" /> <BrushTransition Duration="0:0:0.083" />
</Grid.BackgroundTransition> </Grid.BackgroundTransition>
<local:KeyCharPresenter <commoncontrols:KeyCharPresenter
x:Name="KeyPresenter" x:Name="KeyPresenter"
Margin="{TemplateBinding Padding}" Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"

View File

@@ -287,4 +287,8 @@ namespace winrt::PowerToys::Interop::implementation
{ {
return CommonSharedConstants::POWER_DISPLAY_TERMINATE_APP_MESSAGE; return CommonSharedConstants::POWER_DISPLAY_TERMINATE_APP_MESSAGE;
} }
hstring Constants::OpenNewKeyboardManagerEvent()
{
return CommonSharedConstants::OPEN_NEW_KEYBOARD_MANAGER_EVENT;
}
} }

View File

@@ -75,6 +75,7 @@ namespace winrt::PowerToys::Interop::implementation
static hstring PowerDisplayToggleMessage(); static hstring PowerDisplayToggleMessage();
static hstring PowerDisplayApplyProfileMessage(); static hstring PowerDisplayApplyProfileMessage();
static hstring PowerDisplayTerminateAppMessage(); static hstring PowerDisplayTerminateAppMessage();
static hstring OpenNewKeyboardManagerEvent();
}; };
} }

View File

@@ -72,6 +72,7 @@ namespace PowerToys
static String PowerDisplayToggleMessage(); static String PowerDisplayToggleMessage();
static String PowerDisplayApplyProfileMessage(); static String PowerDisplayApplyProfileMessage();
static String PowerDisplayTerminateAppMessage(); static String PowerDisplayTerminateAppMessage();
static String OpenNewKeyboardManagerEvent();
} }
} }
} }

View File

@@ -170,6 +170,9 @@ namespace CommonSharedConstants
const wchar_t LIGHT_SWITCH_LIGHT_THEME_EVENT[] = L"Local\\PowerToysLightSwitch-LightThemeEvent-50077121-2ffc-4841-9c86-ab1bd3f9baca"; const wchar_t LIGHT_SWITCH_LIGHT_THEME_EVENT[] = L"Local\\PowerToysLightSwitch-LightThemeEvent-50077121-2ffc-4841-9c86-ab1bd3f9baca";
const wchar_t LIGHT_SWITCH_DARK_THEME_EVENT[] = L"Local\\PowerToysLightSwitch-DarkThemeEvent-b3a835c0-eaa2-49b0-b8eb-f793e3df3368"; const wchar_t LIGHT_SWITCH_DARK_THEME_EVENT[] = L"Local\\PowerToysLightSwitch-DarkThemeEvent-b3a835c0-eaa2-49b0-b8eb-f793e3df3368";
// Path to events used by Keyboard Manager
const wchar_t OPEN_NEW_KEYBOARD_MANAGER_EVENT[] = L"Local\\PowerToysOpenNewKeyboardManagerEvent-9c1d2e3f-4b5a-6c7d-8e9f-0a1b2c3d4e5f";
// used from quick access window // used from quick access window
const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a"; const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a";
const wchar_t CMDPAL_EXIT_EVENT[] = L"Local\\PowerToysCmdPal-ExitEvent-eb73f6be-3f22-4b36-aee3-62924ba40bfd"; const wchar_t CMDPAL_EXIT_EVENT[] = L"Local\\PowerToysCmdPal-ExitEvent-eb73f6be-3f22-4b36-aee3-62924ba40bfd";

View File

@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.Common.Properties {
// class via a tool like ResGen or Visual Studio. // class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen // To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project. // with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources { internal class Resources {

View File

@@ -0,0 +1,35 @@
// 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.Threading;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToys.Interop;
namespace PowerToysExtension.Commands;
/// <summary>
/// Opens the new Keyboard Manager editor.
/// </summary>
internal sealed partial class OpenNewKeyboardManagerEditorCommand : InvokableCommand
{
public OpenNewKeyboardManagerEditorCommand()
{
Name = "Open New Keyboard Manager Editor";
}
public override CommandResult Invoke()
{
try
{
using var evt = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.OpenNewKeyboardManagerEvent());
evt.Set();
return CommandResult.Dismiss();
}
catch (Exception ex)
{
return CommandResult.ShowToast($"Failed to open New Keyboard Manager Editor: {ex.Message}");
}
}
}

View File

@@ -2,7 +2,10 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands; using PowerToysExtension.Commands;
using PowerToysExtension.Helpers; using PowerToysExtension.Helpers;
@@ -18,6 +21,16 @@ internal sealed class KeyboardManagerModuleCommandProvider : ModuleCommandProvid
var title = SettingsWindow.KBM.ModuleDisplayName(); var title = SettingsWindow.KBM.ModuleDisplayName();
var icon = SettingsWindow.KBM.ModuleIcon(); var icon = SettingsWindow.KBM.ModuleIcon();
if (IsUseNewEditorEnabled())
{
yield return new ListItem(new OpenNewKeyboardManagerEditorCommand())
{
Title = Resources.KeyboardManager_OpenNewEditor_Title,
Subtitle = Resources.KeyboardManager_OpenNewEditor_Subtitle,
Icon = icon,
};
}
yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.KBM, title) { Id = "com.microsoft.powertoys.keyboardManager.openSettings" }) yield return new ListItem(new OpenInSettingsCommand(SettingsWindow.KBM, title) { Id = "com.microsoft.powertoys.keyboardManager.openSettings" })
{ {
Title = title, Title = title,
@@ -25,4 +38,37 @@ internal sealed class KeyboardManagerModuleCommandProvider : ModuleCommandProvid
Icon = icon, Icon = icon,
}; };
} }
private static bool IsUseNewEditorEnabled()
{
try
{
var settingsPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Microsoft",
"PowerToys",
"Keyboard Manager",
"settings.json");
if (!File.Exists(settingsPath))
{
return false;
}
var json = File.ReadAllText(settingsPath);
using var doc = JsonDocument.Parse(json);
if (doc.RootElement.TryGetProperty("properties", out var properties) &&
properties.TryGetProperty("useNewEditor", out var useNewEditor))
{
return useNewEditor.GetBoolean();
}
}
catch
{
// If we can't read the setting, default to not showing the command
}
return false;
}
} }

View File

@@ -861,6 +861,24 @@ namespace PowerToysExtension.Properties {
} }
} }
/// <summary>
/// Looks up a localized string similar to Keyboard Manager: Open Editor.
/// </summary>
internal static string KeyboardManager_OpenNewEditor_Title {
get {
return ResourceManager.GetString("KeyboardManager_OpenNewEditor_Title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open the Keyboard Manager remap editor.
/// </summary>
internal static string KeyboardManager_OpenNewEditor_Subtitle {
get {
return ResourceManager.GetString("KeyboardManager_OpenNewEditor_Subtitle", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Open Light Switch settings. /// Looks up a localized string similar to Open Light Switch settings.
/// </summary> /// </summary>

View File

@@ -408,6 +408,12 @@
<data name="KeyboardManager_Settings_Subtitle" xml:space="preserve"> <data name="KeyboardManager_Settings_Subtitle" xml:space="preserve">
<value>Open Keyboard Manager settings</value> <value>Open Keyboard Manager settings</value>
</data> </data>
<data name="KeyboardManager_OpenNewEditor_Title" xml:space="preserve">
<value>Keyboard Manager: Open Editor</value>
</data>
<data name="KeyboardManager_OpenNewEditor_Subtitle" xml:space="preserve">
<value>Open the Keyboard Manager remap editor</value>
</data>
<!-- Light Switch Module --> <!-- Light Switch Module -->
<data name="LightSwitch_Toggle_Title" xml:space="preserve"> <data name="LightSwitch_Toggle_Title" xml:space="preserve">
<value>Light Switch: Toggle theme</value> <value>Light Switch: Toggle theme</value>

View File

@@ -140,12 +140,14 @@ namespace EditorHelpers
// Win+L // Win+L
if (shortcut.winKey != ModifierKey::Disabled && shortcut.ctrlKey == ModifierKey::Disabled && shortcut.altKey == ModifierKey::Disabled && shortcut.shiftKey == ModifierKey::Disabled && shortcut.actionKey == 0x4C) if (shortcut.winKey != ModifierKey::Disabled && shortcut.ctrlKey == ModifierKey::Disabled && shortcut.altKey == ModifierKey::Disabled && shortcut.shiftKey == ModifierKey::Disabled && shortcut.actionKey == 0x4C)
{ {
Logger::info(L"Illegal shortcut detected: Win+L");
return ShortcutErrorType::WinL; return ShortcutErrorType::WinL;
} }
// Ctrl+Alt+Del // Ctrl+Alt+Del
if (shortcut.winKey == ModifierKey::Disabled && shortcut.ctrlKey != ModifierKey::Disabled && shortcut.altKey != ModifierKey::Disabled && shortcut.shiftKey == ModifierKey::Disabled && shortcut.actionKey == VK_DELETE) if (shortcut.winKey == ModifierKey::Disabled && shortcut.ctrlKey != ModifierKey::Disabled && shortcut.altKey != ModifierKey::Disabled && shortcut.shiftKey == ModifierKey::Disabled && shortcut.actionKey == VK_DELETE)
{ {
Logger::info(L"Illegal shortcut detected: Ctrl+Alt+Del");
return ShortcutErrorType::CtrlAltDel; return ShortcutErrorType::CtrlAltDel;
} }

View File

@@ -198,7 +198,8 @@ ShortcutControl& ShortcutControl::AddNewShortcutControlRow(StackPanel& parent, s
textInputMargin.Bottom = EditorConstants::ShortcutTableDropDownSpacing; // compensate for a collapsed UIElement textInputMargin.Bottom = EditorConstants::ShortcutTableDropDownSpacing; // compensate for a collapsed UIElement
unicodeTextKeysInput.Margin(textInputMargin); unicodeTextKeysInput.Margin(textInputMargin);
unicodeTextKeysInput.AcceptsReturn(false); unicodeTextKeysInput.AcceptsReturn(true);
unicodeTextKeysInput.TextWrapping(TextWrapping::Wrap);
//unicodeTextKeysInput.Visibility(Visibility::Collapsed); //unicodeTextKeysInput.Visibility(Visibility::Collapsed);
unicodeTextKeysInput.Width(EditorConstants::TableDropDownHeight); unicodeTextKeysInput.Width(EditorConstants::TableDropDownHeight);

View File

@@ -58,7 +58,8 @@ SingleKeyRemapControl::SingleKeyRemapControl(StackPanel table, StackPanel row, c
textBoxMargin.Top = -EditorConstants::ShortcutTableDropDownSpacing; // compensate for a collapsed grid textBoxMargin.Top = -EditorConstants::ShortcutTableDropDownSpacing; // compensate for a collapsed grid
textBoxMargin.Bottom = EditorConstants::ShortcutTableDropDownSpacing; textBoxMargin.Bottom = EditorConstants::ShortcutTableDropDownSpacing;
textInput.Margin(textBoxMargin); textInput.Margin(textBoxMargin);
textInput.AcceptsReturn(false); textInput.AcceptsReturn(true);
textInput.TextWrapping(TextWrapping::Wrap);
textInput.Visibility(Visibility::Collapsed); textInput.Visibility(Visibility::Collapsed);
textInput.Width(EditorConstants::TableDropDownHeight); textInput.Width(EditorConstants::TableDropDownHeight);
singleKeyRemapControlLayout.as<StackPanel>().Children().Append(textInput); singleKeyRemapControlLayout.as<StackPanel>().Children().Append(textInput);

View File

@@ -1,20 +1,752 @@
#include "pch.h" #include "pch.h"
#include "KeyboardManagerEditorLibraryWrapper.h" #include "KeyboardManagerEditorLibraryWrapper.h"
#include <algorithm>
#include <cstring>
#include <vector>
#include <string>
#include <memory>
#include <common/utils/logger_helper.h> #include <common/utils/logger_helper.h>
#include <keyboardmanager/KeyboardManagerEditor/KeyboardManagerEditor.h> #include <keyboardmanager/KeyboardManagerEditor/KeyboardManagerEditor.h>
#include <keyboardmanager/KeyboardManagerEditorLibrary/EditorHelpers.h>
#include <common/interop/keyboard_layout.h>
// Test function to call the remapping helper function extern "C"
bool CheckIfRemappingsAreValid()
{ {
RemapBuffer remapBuffer; void* CreateMappingConfiguration()
{
// Mock valid key to key remappings return new MappingConfiguration();
remapBuffer.push_back(RemapBufferRow{ RemapBufferItem({ (DWORD)0x41, (DWORD)0x42 }), std::wstring() }); }
remapBuffer.push_back(RemapBufferRow{ RemapBufferItem({ (DWORD)0x42, (DWORD)0x43 }), std::wstring() });
void DestroyMappingConfiguration(void* config)
auto result = LoadingAndSavingRemappingHelper::CheckIfRemappingsAreValid(remapBuffer); {
delete static_cast<MappingConfiguration*>(config);
return result == ShortcutErrorType::NoError; }
bool LoadMappingSettings(void* config)
{
return static_cast<MappingConfiguration*>(config)->LoadSettings();
}
bool SaveMappingSettings(void* config)
{
return static_cast<MappingConfiguration*>(config)->SaveSettingsToFile();
}
wchar_t* AllocateAndCopyString(const std::wstring& str)
{
size_t len = str.length();
wchar_t* buffer = new wchar_t[len + 1];
wcscpy_s(buffer, len + 1, str.c_str());
return buffer;
}
int GetSingleKeyRemapCount(void* config)
{
auto mapping = static_cast<MappingConfiguration*>(config);
return static_cast<int>(mapping->singleKeyReMap.size());
}
bool GetSingleKeyRemap(void* config, int index, SingleKeyMapping* mapping)
{
auto mappingConfig = static_cast<MappingConfiguration*>(config);
std::vector<std::pair<DWORD, KeyShortcutTextUnion>> allMappings;
for (const auto& kv : mappingConfig->singleKeyReMap)
{
allMappings.push_back(kv);
}
if (index < 0 || index >= allMappings.size())
{
return false;
}
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());
mapping->isShortcut = true;
}
else
{
mapping->targetKey = AllocateAndCopyString(L"");
mapping->isShortcut = false;
}
return true;
}
int GetSingleKeyToTextRemapCount(void* config)
{
auto mapping = static_cast<MappingConfiguration*>(config);
return static_cast<int>(mapping->singleKeyToTextReMap.size());
}
bool GetSingleKeyToTextRemap(void* config, int index, KeyboardTextMapping* mapping)
{
auto mappingConfig = static_cast<MappingConfiguration*>(config);
if (index < 0 || index >= mappingConfig->singleKeyToTextReMap.size())
{
return false;
}
auto it = mappingConfig->singleKeyToTextReMap.begin();
std::advance(it, index);
mapping->originalKey = static_cast<int>(it->first);
std::wstring text = std::get<std::wstring>(it->second);
mapping->targetText = AllocateAndCopyString(text);
return true;
}
int GetShortcutRemapCountByType(void* config, int operationType)
{
auto mapping = static_cast<MappingConfiguration*>(config);
int count = 0;
for (const auto& kv : mapping->osLevelShortcutReMap)
{
bool shouldCount = false;
if (operationType == 0)
{
if ((kv.second.targetShortcut.index() == 0) ||
(kv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(kv.second.targetShortcut).operationType == Shortcut::OperationType::RemapShortcut))
{
shouldCount = true;
}
}
else if (operationType == 1)
{
if (kv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(kv.second.targetShortcut).operationType == Shortcut::OperationType::RunProgram)
{
shouldCount = true;
}
}
else if (operationType == 2)
{
if (kv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(kv.second.targetShortcut).operationType == Shortcut::OperationType::OpenURI)
{
shouldCount = true;
}
}
else if (operationType == 3)
{
if (kv.second.targetShortcut.index() == 2)
{
shouldCount = true;
}
}
if (shouldCount)
{
count++;
}
}
for (const auto& appMap : mapping->appSpecificShortcutReMap)
{
for (const auto& shortcutKv : appMap.second)
{
bool shouldCount = false;
if (operationType == 0)
{
if ((shortcutKv.second.targetShortcut.index() == 0) ||
(shortcutKv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(shortcutKv.second.targetShortcut).operationType == Shortcut::OperationType::RemapShortcut))
{
shouldCount = true;
}
}
else if (operationType == 1)
{
if (shortcutKv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(shortcutKv.second.targetShortcut).operationType == Shortcut::OperationType::RunProgram)
{
shouldCount = true;
}
}
else if (operationType == 2)
{
if (shortcutKv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(shortcutKv.second.targetShortcut).operationType == Shortcut::OperationType::OpenURI)
{
shouldCount = true;
}
}
else if (operationType == 3)
{
if (shortcutKv.second.targetShortcut.index() == 2)
{
shouldCount = true;
}
}
if (shouldCount)
{
count++;
}
}
}
return count;
}
bool GetShortcutRemapByType(void* config, int operationType, int index, ShortcutMapping* mapping)
{
auto mappingConfig = static_cast<MappingConfiguration*>(config);
std::vector<std::tuple<Shortcut, KeyShortcutTextUnion, std::wstring>> filteredMappings;
for (const auto& kv : mappingConfig->osLevelShortcutReMap)
{
bool shouldAdd = false;
switch (operationType)
{
case 0: // RemapShortcut
if ((kv.second.targetShortcut.index() == 0) ||
(kv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(kv.second.targetShortcut).operationType == Shortcut::OperationType::RemapShortcut))
{
shouldAdd = true;
}
break;
case 1: // RunProgram
if (kv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(kv.second.targetShortcut).operationType == Shortcut::OperationType::RunProgram)
{
shouldAdd = true;
}
break;
case 2: // OpenURI
if (kv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(kv.second.targetShortcut).operationType == Shortcut::OperationType::OpenURI)
{
shouldAdd = true;
}
break;
case 3:
if (kv.second.targetShortcut.index() == 2)
{
shouldAdd = true;
}
break;
default:
break;
}
if (shouldAdd)
{
filteredMappings.push_back(std::make_tuple(kv.first, kv.second.targetShortcut, L""));
}
}
for (const auto& appKv : mappingConfig->appSpecificShortcutReMap)
{
for (const auto& shortcutKv : appKv.second)
{
bool shouldAdd = false;
switch (operationType)
{
case 0: // RemapShortcut
if ((shortcutKv.second.targetShortcut.index() == 0) ||
(shortcutKv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(shortcutKv.second.targetShortcut).operationType == Shortcut::OperationType::RemapShortcut))
{
shouldAdd = true;
}
break;
case 1: // RunProgram
if (shortcutKv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(shortcutKv.second.targetShortcut).operationType == Shortcut::OperationType::RunProgram)
{
shouldAdd = true;
}
break;
case 2: // OpenURI
if (shortcutKv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(shortcutKv.second.targetShortcut).operationType == Shortcut::OperationType::OpenURI)
{
shouldAdd = true;
}
break;
case 3:
if (shortcutKv.second.targetShortcut.index() == 2)
{
shouldAdd = true;
}
break;
default:
break;
}
if (shouldAdd)
{
filteredMappings.push_back(std::make_tuple(
shortcutKv.first, shortcutKv.second.targetShortcut, appKv.first));
}
}
}
if (index < 0 || index >= filteredMappings.size())
{
return false;
}
const auto& [origShortcut, targetShortcutUnion, app] = filteredMappings[index];
std::wstring origKeysStr = origShortcut.ToHstringVK().c_str();
mapping->originalKeys = AllocateAndCopyString(origKeysStr);
mapping->targetApp = AllocateAndCopyString(app);
if (targetShortcutUnion.index() == 0)
{
DWORD targetKey = std::get<DWORD>(targetShortcutUnion);
mapping->targetKeys = AllocateAndCopyString(std::to_wstring(targetKey));
mapping->operationType = 0;
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->uriToOpen = AllocateAndCopyString(L"");
}
else if (targetShortcutUnion.index() == 1)
{
Shortcut targetShortcut = std::get<Shortcut>(targetShortcutUnion);
std::wstring targetKeysStr = targetShortcut.ToHstringVK().c_str();
mapping->operationType = static_cast<int>(targetShortcut.operationType);
switch (targetShortcut.operationType)
{
case Shortcut::OperationType::RunProgram:
mapping->targetKeys = AllocateAndCopyString(targetKeysStr);
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(targetShortcut.runProgramFilePath);
mapping->programArgs = AllocateAndCopyString(targetShortcut.runProgramArgs);
mapping->uriToOpen = AllocateAndCopyString(L"");
break;
case Shortcut::OperationType::OpenURI:
mapping->targetKeys = AllocateAndCopyString(targetKeysStr);
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->uriToOpen = AllocateAndCopyString(targetShortcut.uriToOpen);
break;
default:
mapping->targetKeys = AllocateAndCopyString(targetKeysStr);
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->uriToOpen = AllocateAndCopyString(L"");
break;
}
}
else if (targetShortcutUnion.index() == 2)
{
std::wstring text = std::get<std::wstring>(targetShortcutUnion);
mapping->targetKeys = AllocateAndCopyString(L"");
mapping->operationType = 0;
mapping->targetText = AllocateAndCopyString(text);
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->uriToOpen = AllocateAndCopyString(L"");
}
return true;
}
int GetShortcutRemapCount(void* config)
{
auto mapping = static_cast<MappingConfiguration*>(config);
int count = static_cast<int>(mapping->osLevelShortcutReMap.size());
for (const auto& appMap : mapping->appSpecificShortcutReMap)
{
count += static_cast<int>(appMap.second.size());
}
return count;
}
bool GetShortcutRemap(void* config, int index, ShortcutMapping* mapping)
{
auto mappingConfig = static_cast<MappingConfiguration*>(config);
std::vector<std::tuple<Shortcut, KeyShortcutTextUnion, std::wstring>> allMappings;
for (const auto& kv : mappingConfig->osLevelShortcutReMap)
{
allMappings.push_back(std::make_tuple(kv.first, kv.second.targetShortcut, L""));
}
for (const auto& appKv : mappingConfig->appSpecificShortcutReMap)
{
for (const auto& shortcutKv : appKv.second)
{
allMappings.push_back(std::make_tuple(
shortcutKv.first, shortcutKv.second.targetShortcut, appKv.first));
}
}
if (index < 0 || index >= allMappings.size())
{
return false;
}
const auto& [origShortcut, targetShortcutUnion, app] = allMappings[index];
std::wstring origKeysStr = origShortcut.ToHstringVK().c_str();
mapping->originalKeys = AllocateAndCopyString(origKeysStr);
mapping->targetApp = AllocateAndCopyString(app);
if (targetShortcutUnion.index() == 0)
{
DWORD targetKey = std::get<DWORD>(targetShortcutUnion);
mapping->targetKeys = AllocateAndCopyString(std::to_wstring(targetKey));
mapping->operationType = 0;
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->uriToOpen = AllocateAndCopyString(L"");
}
else if (targetShortcutUnion.index() == 1)
{
Shortcut targetShortcut = std::get<Shortcut>(targetShortcutUnion);
std::wstring targetKeysStr = targetShortcut.ToHstringVK().c_str();
mapping->operationType = static_cast<int>(targetShortcut.operationType);
if (targetShortcut.operationType == Shortcut::OperationType::RunProgram)
{
mapping->targetKeys = AllocateAndCopyString(targetKeysStr);
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(targetShortcut.runProgramFilePath);
mapping->programArgs = AllocateAndCopyString(targetShortcut.runProgramArgs);
mapping->uriToOpen = AllocateAndCopyString(L"");
}
else if (targetShortcut.operationType == Shortcut::OperationType::OpenURI)
{
mapping->targetKeys = AllocateAndCopyString(targetKeysStr);
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->uriToOpen = AllocateAndCopyString(targetShortcut.uriToOpen);
}
else
{
mapping->targetKeys = AllocateAndCopyString(targetKeysStr);
mapping->targetText = AllocateAndCopyString(L"");
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->uriToOpen = AllocateAndCopyString(L"");
}
}
else if (targetShortcutUnion.index() == 2)
{
std::wstring text = std::get<std::wstring>(targetShortcutUnion);
mapping->targetKeys = AllocateAndCopyString(L"");
mapping->operationType = 0;
mapping->targetText = AllocateAndCopyString(text);
mapping->programPath = AllocateAndCopyString(L"");
mapping->programArgs = AllocateAndCopyString(L"");
mapping->uriToOpen = AllocateAndCopyString(L"");
}
return true;
}
void FreeString(wchar_t* str)
{
delete[] str;
}
bool AddSingleKeyRemap(void* config, int originalKey, int targetKey)
{
auto mappingConfig = static_cast<MappingConfiguration*>(config);
return mappingConfig->AddSingleKeyRemap(static_cast<DWORD>(originalKey), static_cast<DWORD>(targetKey));
}
bool AddSingleKeyToTextRemap(void* config, int originalKey, const wchar_t* text)
{
auto mappingConfig = static_cast<MappingConfiguration*>(config);
if (text == nullptr)
{
return false;
}
return mappingConfig->AddSingleKeyToTextRemap(static_cast<DWORD>(originalKey), text);
}
bool AddSingleKeyToShortcutRemap(void* config, int originalKey, const wchar_t* targetKeys)
{
auto mappingConfig = static_cast<MappingConfiguration*>(config);
if (!targetKeys)
{
return false;
}
Shortcut targetShortcut(targetKeys);
return mappingConfig->AddSingleKeyRemap(static_cast<DWORD>(originalKey), targetShortcut);
}
bool AddShortcutRemap(void* config,
const wchar_t* originalKeys,
const wchar_t* targetKeys,
const wchar_t* targetApp,
int operationType,
const wchar_t* appPathOrUri,
const wchar_t* args,
const wchar_t* startDirectory,
int elevation,
int ifRunningAction,
int visibility)
{
auto mappingConfig = static_cast<MappingConfiguration*>(config);
Shortcut originalShortcut(originalKeys);
KeyShortcutTextUnion targetShortcut;
switch (operationType)
{
case 1:
targetShortcut = Shortcut(targetKeys);
std::get<Shortcut>(targetShortcut).runProgramFilePath = std::wstring(appPathOrUri);
if (args)
{
std::get<Shortcut>(targetShortcut).runProgramArgs = std::wstring(args);
}
if (startDirectory)
{
std::get<Shortcut>(targetShortcut).runProgramStartInDir = std::wstring(startDirectory);
}
std::get<Shortcut>(targetShortcut).elevationLevel = static_cast<Shortcut::ElevationLevel>(elevation);
std::get<Shortcut>(targetShortcut).alreadyRunningAction = static_cast<Shortcut::ProgramAlreadyRunningAction>(ifRunningAction);
std::get<Shortcut>(targetShortcut).startWindowType = static_cast<Shortcut::StartWindowType>(visibility);
std::get<Shortcut>(targetShortcut).operationType = static_cast<Shortcut::OperationType>(operationType);
break;
case 2:
targetShortcut = Shortcut(targetKeys);
std::get<Shortcut>(targetShortcut).uriToOpen = std::wstring(appPathOrUri);
std::get<Shortcut>(targetShortcut).operationType = static_cast<Shortcut::OperationType>(operationType);
break;
case 3:
targetShortcut = std::wstring(targetKeys);
break;
default:
targetShortcut = Shortcut(targetKeys);
std::get<Shortcut>(targetShortcut).operationType = static_cast<Shortcut::OperationType>(operationType);
break;
}
std::wstring app(targetApp ? targetApp : L"");
if (app.empty())
{
return mappingConfig->AddOSLevelShortcut(originalShortcut, targetShortcut);
}
else
{
return mappingConfig->AddAppSpecificShortcut(app, originalShortcut, targetShortcut);
}
}
void GetKeyDisplayName(int keyCode, wchar_t* keyName, int maxCount)
{
if (keyName == nullptr || maxCount <= 0)
{
return;
}
LayoutMap layoutMap;
std::wstring name = layoutMap.GetKeyName(static_cast<DWORD>(keyCode));
wcsncpy_s(keyName, maxCount, name.c_str(), _TRUNCATE);
}
int GetKeyCodeFromName(const wchar_t* keyName)
{
Logger::info(L"Getting key code for key name: {0}", keyName ? keyName : L"null");
if (keyName == nullptr)
{
return 0;
}
LayoutMap layoutMap;
std::wstring name(keyName);
int keyCode = static_cast<int>(layoutMap.GetKeyFromName(name));
Logger::info(L"Key code for key name {0}: {1}", keyName, keyCode);
return keyCode;
}
// Function to get the type of a key (Win, Ctrl, Alt, Shift, or Action)
int GetKeyType(int key)
{
return static_cast<int>(Helpers::GetKeyType(static_cast<DWORD>(key)));
}
// Function to check if a shortcut is illegal
bool IsShortcutIllegal(const wchar_t* shortcutKeys)
{
Logger::info(L"Checking if shortcut is illegal: {0}", shortcutKeys ? shortcutKeys : L"null");
if (!shortcutKeys)
{
return false;
}
Shortcut shortcut(shortcutKeys);
ShortcutErrorType result = EditorHelpers::IsShortcutIllegal(shortcut);
// Return true if an error was detected (anything other than NoError)
return result != ShortcutErrorType::NoError;
}
// Function to check if two shortcuts are equal
bool AreShortcutsEqual(const wchar_t* lShort, const wchar_t* rShort)
{
if (!lShort || !rShort)
{
return false;
}
Shortcut lhs(lShort);
Shortcut rhs(rShort);
return lhs == rhs;
}
// Function to delete a single key remapping
bool DeleteSingleKeyRemap(void* config, int originalKey)
{
auto mappingConfig = static_cast<MappingConfiguration*>(config);
// Find and delete the single key remapping
auto it = mappingConfig->singleKeyReMap.find(static_cast<DWORD>(originalKey));
if (it != mappingConfig->singleKeyReMap.end())
{
mappingConfig->singleKeyReMap.erase(it);
return true;
}
return false;
}
bool DeleteSingleKeyToTextRemap(void* config, int originalKey)
{
auto mappingConfig = static_cast<MappingConfiguration*>(config);
auto it = mappingConfig->singleKeyToTextReMap.find(originalKey);
if (it != mappingConfig->singleKeyToTextReMap.end())
{
mappingConfig->singleKeyToTextReMap.erase(it);
return true;
}
return false;
}
// Function to delete a shortcut remapping
bool DeleteShortcutRemap(void* config, const wchar_t* originalKeys, const wchar_t* targetApp)
{
auto mappingConfig = static_cast<MappingConfiguration*>(config);
if (originalKeys == nullptr)
{
return false;
}
std::wstring appName = targetApp ? targetApp : L"";
Shortcut shortcut(originalKeys);
// Determine the type of remapping to delete based on the app name
if (appName.empty())
{
// Delete OS level shortcut mapping
auto it = mappingConfig->osLevelShortcutReMap.find(shortcut);
if (it != mappingConfig->osLevelShortcutReMap.end())
{
mappingConfig->osLevelShortcutReMap.erase(it);
return true;
}
}
else
{
// Delete app-specific shortcut mapping
auto appIt = mappingConfig->appSpecificShortcutReMap.find(appName);
if (appIt != mappingConfig->appSpecificShortcutReMap.end())
{
auto shortcutIt = appIt->second.find(shortcut);
if (shortcutIt != appIt->second.end())
{
appIt->second.erase(shortcutIt);
// If the app-specific mapping is empty, remove the app entry
if (appIt->second.empty())
{
mappingConfig->appSpecificShortcutReMap.erase(appIt);
}
return true;
}
}
}
return false;
}
}
// Get the list of keyboard keys in Editor
int GetKeyboardKeysList(bool isShortcut, KeyNamePair* keyList, int maxCount)
{
if (keyList == nullptr || maxCount <= 0)
{
return 0;
}
LayoutMap layoutMap;
auto keyNameList = layoutMap.GetKeyNameList(isShortcut);
int count = (std::min)(static_cast<int>(keyNameList.size()), maxCount);
// Transfer the key list to the output struct format
for (int i = 0; i < count; ++i)
{
keyList[i].keyCode = static_cast<int>(keyNameList[i].first);
wcsncpy_s(keyList[i].keyName, keyNameList[i].second.c_str(), _countof(keyList[i].keyName) - 1);
}
return count;
} }

View File

@@ -4,4 +4,83 @@
#include <keyboardmanager/common/Input.h> #include <keyboardmanager/common/Input.h>
#include <keyboardmanager/common/MappingConfiguration.h> #include <keyboardmanager/common/MappingConfiguration.h>
extern "C" __declspec(dllexport) bool CheckIfRemappingsAreValid(); struct KeyNamePair
{
int keyCode;
wchar_t keyName[64];
};
struct SingleKeyMapping
{
int originalKey;
wchar_t* targetKey;
bool isShortcut;
};
struct KeyboardTextMapping
{
int originalKey;
wchar_t* targetText;
};
struct ShortcutMapping
{
wchar_t* originalKeys;
wchar_t* targetKeys;
wchar_t* targetApp;
int operationType;
wchar_t* targetText;
wchar_t* programPath;
wchar_t* programArgs;
wchar_t* uriToOpen;
};
extern "C"
{
__declspec(dllexport) void* CreateMappingConfiguration();
__declspec(dllexport) void DestroyMappingConfiguration(void* config);
__declspec(dllexport) bool LoadMappingSettings(void* config);
__declspec(dllexport) bool SaveMappingSettings(void* config);
__declspec(dllexport) int GetSingleKeyRemapCount(void* config);
__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);
__declspec(dllexport) int GetShortcutRemapCountByType(void* config, int operationType);
__declspec(dllexport) bool GetShortcutRemapByType(void* config, int operationType, int index, ShortcutMapping* mapping);
__declspec(dllexport) int GetShortcutRemapCount(void* config);
__declspec(dllexport) bool GetShortcutRemap(void* config, int index, ShortcutMapping* mapping);
__declspec(dllexport) bool AddSingleKeyRemap(void* config, int originalKey, int targetKey);
__declspec(dllexport) bool AddSingleKeyToTextRemap(void* config, int originalKey, const wchar_t* text);
__declspec(dllexport) bool AddSingleKeyToShortcutRemap(void* config,
int originalKey,
const wchar_t* targetKeys);
__declspec(dllexport) bool AddShortcutRemap(void* config,
const wchar_t* originalKeys,
const wchar_t* targetKeys,
const wchar_t* targetApp,
int operationType,
const wchar_t* appPathOrUri = nullptr,
const wchar_t* args = nullptr,
const wchar_t* startDirectory = nullptr,
int elevation = 0,
int ifRunningAction = 0,
int visibility = 0);
__declspec(dllexport) void GetKeyDisplayName(int keyCode, wchar_t* keyName, int maxCount);
__declspec(dllexport) int GetKeyCodeFromName(const wchar_t* keyName);
__declspec(dllexport) void FreeString(wchar_t* str);
__declspec(dllexport) int GetKeyType(int keyCode);
__declspec(dllexport) bool IsShortcutIllegal(const wchar_t* shortcutKeys);
__declspec(dllexport) bool AreShortcutsEqual(const wchar_t* lShort, const wchar_t* rShort);
__declspec(dllexport) bool DeleteSingleKeyRemap(void* config, int originalKey);
__declspec(dllexport) bool DeleteSingleKeyToTextRemap(void* config, int originalKey);
__declspec(dllexport) bool DeleteShortcutRemap(void* config, const wchar_t* originalKeys, const wchar_t* targetApp);
}
extern "C" __declspec(dllexport) int GetKeyboardKeysList(bool isShortcut, KeyNamePair* keyList, int maxCount);

View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<Application
x:Class="KeyboardManagerEditorUI.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:KeyboardManagerEditorUI">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<!-- Other merged dictionaries here -->
</ResourceDictionary.MergedDictionaries>
<!-- Other app resources here -->
</ResourceDictionary>
</Application.Resources>
</Application>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1,48 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:KeyboardManagerEditorUI.Controls">
<Style BasedOn="{StaticResource DefaultIconLabelControlStyle}" TargetType="local:IconLabelControl" />
<Style x:Key="DefaultIconLabelControlStyle" TargetType="local:IconLabelControl">
<Setter Property="IsTabStop" Value="False" />
<Setter Property="FontSize" Value="14" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:IconLabelControl">
<Grid
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
ColumnSpacing="4"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<FontIcon
x:Name="TypeIcon"
Grid.Column="0"
VerticalAlignment="Center"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="{TemplateBinding FontSize}"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}" />
<TextBlock
x:Name="LabelText"
Grid.Column="1"
Margin="0,-1,0,0"
VerticalAlignment="Center"
FontSize="{TemplateBinding FontSize}"
Foreground="{TemplateBinding Foreground}"
Text="{TemplateBinding Label}"
TextTrimming="CharacterEllipsis" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -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",
};
}
}
}

View File

@@ -0,0 +1,387 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="KeyboardManagerEditorUI.Controls.UnifiedMappingControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:commoncontrols="using:Microsoft.PowerToys.Common.UI.Controls"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:ui="using:CommunityToolkit.WinUI"
Loaded="UserControl_Loaded"
mc:Ignorable="d">
<Grid
Width="720"
MinHeight="480"
ColumnSpacing="24">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Left Column: Original Input (Trigger) -->
<StackPanel
Grid.Row="0"
Grid.Column="0"
Orientation="Vertical"
Spacing="8">
<TextBlock x:Uid="TriggerLabel" FontWeight="SemiBold" />
<ComboBox
x:Name="TriggerTypeComboBox"
HorizontalAlignment="Stretch"
SelectionChanged="TriggerTypeComboBox_SelectionChanged">
<ComboBoxItem
x:Uid="TriggerType_KeyOrShortcut"
IsSelected="True"
Tag="KeyOrShortcut">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="14" Glyph="&#xEDA7;" />
<TextBlock x:Uid="TriggerType_KeyOrShortcut_Text" />
</StackPanel>
</ComboBoxItem>
<!--
<ComboBoxItem x:Uid="TriggerType_Mouse" Tag="Mouse">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="16" Glyph="&#xE962;" />
<TextBlock x:Uid="TriggerType_Mouse_Text" />
</StackPanel>
</ComboBoxItem>
-->
</ComboBox>
<Rectangle
Height="1"
Margin="0,12,0,12"
HorizontalAlignment="Stretch"
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
<tkcontrols:SwitchPresenter
x:Name="TriggerSwitchPresenter"
TargetType="x:String"
Value="{Binding SelectedItem.Tag, ElementName=TriggerTypeComboBox}">
<!-- Key or Shortcut Trigger -->
<tkcontrols:Case Value="KeyOrShortcut">
<StackPanel Orientation="Vertical" Spacing="8">
<ToggleButton
x:Name="TriggerKeyToggleBtn"
MinHeight="86"
Padding="8,24,8,24"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Checked="TriggerKeyToggleBtn_Checked"
Style="{StaticResource CustomShortcutToggleButtonStyle}"
Unchecked="TriggerKeyToggleBtn_Unchecked">
<ToggleButton.Content>
<ItemsControl x:Name="TriggerKeys">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:WrapPanel
HorizontalSpacing="4"
Orientation="Horizontal"
VerticalSpacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<commoncontrols:KeyVisual
Padding="8"
Background="{ThemeResource ControlFillColorDefaultBrush}"
BorderThickness="1"
Content="{Binding}"
CornerRadius="{StaticResource OverlayCornerRadius}"
FontSize="16"
Style="{StaticResource DefaultKeyVisualStyle}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ToggleButton.Content>
</ToggleButton>
<CheckBox
x:Name="AllowChordsCheckBox"
x:Uid="AllowChordsCheckBox"
Click="AllowChordsCheckBox_Click"
IsChecked="True" />
</StackPanel>
</tkcontrols:Case>
<!-- Mouse Button Trigger -->
<tkcontrols:Case Value="Mouse">
<ComboBox
x:Name="MouseTriggerComboBox"
MinHeight="86"
HorizontalAlignment="Stretch"
VerticalContentAlignment="Center">
<ComboBoxItem x:Uid="MouseButton_Left" Tag="LeftMouse" />
<ComboBoxItem x:Uid="MouseButton_Center" Tag="CenterMouse" />
<ComboBoxItem x:Uid="MouseButton_Right" Tag="RightMouse" />
<ComboBoxItem x:Uid="MouseButton_Button1" Tag="Button1" />
<ComboBoxItem x:Uid="MouseButton_Button2" Tag="Button2" />
</ComboBox>
</tkcontrols:Case>
</tkcontrols:SwitchPresenter>
<CheckBox
x:Name="AppSpecificCheckBox"
x:Uid="AppSpecificCheckBox"
IsEnabled="False" />
<TextBox
x:Name="AppNameTextBox"
x:Uid="AppNameTextBox"
Background="{ThemeResource TextControlBackgroundFocused}"
BorderBrush="{ThemeResource ControlStrokeColorDefaultBrush}"
GotFocus="AppNameTextBox_GotFocus"
Visibility="Collapsed" />
</StackPanel>
<!-- Arrow Separator -->
<Grid
Grid.Row="0"
Grid.Column="1"
RowSpacing="8">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Rectangle
Width="1"
HorizontalAlignment="Center"
VerticalAlignment="Stretch"
Fill="{ThemeResource TextFillColorDisabledBrush}" />
<Viewbox
Grid.Row="1"
Width="12"
Height="12">
<PathIcon Data="{StaticResource ArrowIconData}" Foreground="{ThemeResource TextFillColorDisabledBrush}" />
</Viewbox>
<Rectangle
Grid.Row="2"
Width="1"
HorizontalAlignment="Center"
VerticalAlignment="Stretch"
Fill="{ThemeResource TextFillColorDisabledBrush}" />
</Grid>
<!-- Right Column: Action Output -->
<StackPanel
Grid.Row="0"
Grid.Column="2"
Orientation="Vertical"
Spacing="8">
<TextBlock x:Uid="ActionLabel" FontWeight="SemiBold" />
<ComboBox
x:Name="ActionTypeComboBox"
HorizontalAlignment="Stretch"
SelectionChanged="ActionTypeComboBox_SelectionChanged">
<ComboBoxItem
x:Uid="ActionType_KeyOrShortcut"
IsSelected="True"
Tag="KeyOrShortcut">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="14" Glyph="&#xEDA7;" />
<TextBlock x:Uid="ActionType_KeyOrShortcut_Text" />
</StackPanel>
</ComboBoxItem>
<ComboBoxItem x:Uid="ActionType_Text" Tag="Text">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="14" Glyph="&#xE8D2;" />
<TextBlock x:Uid="ActionType_Text_Text" />
</StackPanel>
</ComboBoxItem>
<ComboBoxItem x:Uid="ActionType_OpenUrl" Tag="OpenUrl">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="14" Glyph="&#xE774;" />
<TextBlock x:Uid="ActionType_OpenUrl_Text" />
</StackPanel>
</ComboBoxItem>
<ComboBoxItem x:Uid="ActionType_OpenApp" Tag="OpenApp">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="14" Glyph="&#xECAA;" />
<TextBlock x:Uid="ActionType_OpenApp_Text" />
</StackPanel>
</ComboBoxItem>
<!--
<ComboBoxItem x:Uid="ActionType_MouseClick" Tag="MouseClick">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="14" Glyph="&#xE962;" />
<TextBlock x:Uid="ActionType_MouseClick_Text" />
</StackPanel>
</ComboBoxItem>
-->
</ComboBox>
<Rectangle
Height="1"
Margin="0,12,0,12"
HorizontalAlignment="Stretch"
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
<tkcontrols:SwitchPresenter
x:Name="ActionSwitchPresenter"
TargetType="x:String"
Value="{Binding SelectedItem.Tag, ElementName=ActionTypeComboBox}">
<!-- Key or Shortcut Action -->
<tkcontrols:Case Value="KeyOrShortcut">
<ToggleButton
x:Name="ActionKeyToggleBtn"
MinHeight="86"
Padding="8,24,8,24"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Checked="ActionKeyToggleBtn_Checked"
Style="{StaticResource CustomShortcutToggleButtonStyle}"
Unchecked="ActionKeyToggleBtn_Unchecked">
<ToggleButton.Content>
<ItemsControl x:Name="ActionKeys">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:WrapPanel
HorizontalSpacing="4"
Orientation="Horizontal"
VerticalSpacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<commoncontrols:KeyVisual
Padding="8"
Background="{ThemeResource CustomAccentBackgroundBrush}"
BorderThickness="0"
Content="{Binding}"
CornerRadius="{StaticResource OverlayCornerRadius}"
FontSize="16"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Style="{StaticResource DefaultKeyVisualStyle}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ToggleButton.Content>
</ToggleButton>
</tkcontrols:Case>
<!-- Text Action -->
<tkcontrols:Case Value="Text">
<TextBox
x:Name="TextContentBox"
x:Uid="TextContentBox"
MinHeight="120"
MaxHeight="240"
AcceptsReturn="True"
Background="{ThemeResource TextControlBackgroundFocused}"
BorderBrush="{ThemeResource ControlStrokeColorDefaultBrush}"
GotFocus="TextContentBox_GotFocus"
TextChanged="TextContentBox_TextChanged"
TextWrapping="Wrap" />
</tkcontrols:Case>
<!-- Open URL Action -->
<tkcontrols:Case Value="OpenUrl">
<TextBox
x:Name="UrlPathInput"
x:Uid="UrlPathInput"
Background="{ThemeResource TextControlBackgroundFocused}"
BorderBrush="{ThemeResource ControlStrokeColorDefaultBrush}"
GotFocus="UrlPathInput_GotFocus"
TextChanged="UrlPathInput_TextChanged" />
</tkcontrols:Case>
<!-- Open App Action -->
<tkcontrols:Case Value="OpenApp">
<StackPanel Orientation="Vertical" Spacing="16">
<Grid ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox
x:Name="ProgramPathInput"
x:Uid="ProgramPathInput"
HorizontalAlignment="Stretch"
GotFocus="ProgramPathInput_GotFocus"
TextChanged="ProgramPathInput_TextChanged" />
<Button
x:Name="ProgramPathSelectButton"
x:Uid="ProgramPathSelectButton"
Grid.Column="1"
VerticalAlignment="Bottom"
Click="ProgramPathSelectButton_Click"
Content="{ui:FontIcon Glyph=&#xE8DA;,
FontSize=16}"
Style="{StaticResource SubtleButtonStyle}" />
</Grid>
<TextBox
x:Name="ProgramArgsInput"
x:Uid="ProgramArgsInput"
GotFocus="ProgramArgsInput_GotFocus" />
<Grid ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox
x:Name="StartInPathInput"
x:Uid="StartInPathInput"
HorizontalAlignment="Stretch"
GotFocus="StartInPathInput_GotFocus" />
<Button
x:Name="StartInSelectButton"
x:Uid="StartInSelectButton"
Grid.Column="1"
VerticalAlignment="Bottom"
Click="StartInSelectButton_Click"
Content="{ui:FontIcon Glyph=&#xE8DA;,
FontSize=16}"
Style="{StaticResource SubtleButtonStyle}" />
</Grid>
<ComboBox
x:Name="ElevationComboBox"
x:Uid="ElevationComboBox"
HorizontalAlignment="Stretch"
SelectedIndex="0">
<ComboBoxItem x:Uid="Elevation_Normal" />
<ComboBoxItem x:Uid="Elevation_Elevated" />
<ComboBoxItem x:Uid="Elevation_DifferentUser" />
</ComboBox>
<ComboBox
x:Name="IfRunningComboBox"
x:Uid="IfRunningComboBox"
HorizontalAlignment="Stretch"
SelectedIndex="0">
<ComboBoxItem x:Uid="IfRunning_ShowWindow" />
<ComboBoxItem x:Uid="IfRunning_StartAnother" />
<ComboBoxItem x:Uid="IfRunning_DoNothing" />
<ComboBoxItem x:Uid="IfRunning_Close" />
<ComboBoxItem x:Uid="IfRunning_EndTask" />
</ComboBox>
<ComboBox
x:Name="VisibilityComboBox"
x:Uid="VisibilityComboBox"
HorizontalAlignment="Stretch"
SelectedIndex="0">
<ComboBoxItem x:Uid="Visibility_Normal" />
<ComboBoxItem x:Uid="Visibility_Hidden" />
<ComboBoxItem x:Uid="Visibility_Minimized" />
<ComboBoxItem x:Uid="Visibility_Maximized" />
</ComboBox>
</StackPanel>
</tkcontrols:Case>
<!-- Mouse Click Action (Placeholder) -->
<tkcontrols:Case Value="MouseClick">
<TextBlock
x:Uid="MouseClickPlaceholder"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap" />
</tkcontrols:Case>
</tkcontrols:SwitchPresenter>
</StackPanel>
<!-- Validation InfoBar spanning all columns -->
<InfoBar
x:Name="ValidationInfoBar"
Grid.Row="1"
Grid.ColumnSpan="3"
Margin="0,16,0,0"
IsClosable="False"
IsOpen="False"
Severity="Warning" />
</Grid>
</UserControl>

View File

@@ -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
{
/// <summary>
/// Unified control that consolidates all mapping input types:
/// - Key/Shortcut remapping (InputControl)
/// - Text output (TextPageInputControl)
/// - URL opening (UrlPageInputControl)
/// - App launching (AppPageInputControl)
/// </summary>
public sealed partial class UnifiedMappingControl : UserControl, IDisposable, IKeyboardHookTarget
{
#region Fields
private readonly ObservableCollection<string> _triggerKeys = new();
private readonly ObservableCollection<string> _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
/// <summary>
/// Raised whenever the validation state of the control changes (inputs filled/cleared).
/// </summary>
public event EventHandler? ValidationStateChanged;
#endregion
#region Enums
/// <summary>
/// Defines the type of trigger for the mapping.
/// </summary>
public enum TriggerType
{
KeyOrShortcut,
Mouse,
}
/// <summary>
/// Defines the type of action to perform.
/// </summary>
public enum ActionType
{
KeyOrShortcut,
Text,
OpenUrl,
OpenApp,
MouseClick,
}
/// <summary>
/// Defines the mouse button options.
/// </summary>
public enum MouseButton
{
LeftMouse,
RightMouse,
ScrollUp,
ScrollDown,
}
#endregion
#region Properties
/// <summary>
/// Gets the current trigger type.
/// </summary>
public TriggerType CurrentTriggerType
{
get
{
if (TriggerTypeComboBox?.SelectedItem is ComboBoxItem item)
{
return item.Tag?.ToString() switch
{
"Mouse" => TriggerType.Mouse,
_ => TriggerType.KeyOrShortcut,
};
}
return TriggerType.KeyOrShortcut;
}
}
/// <summary>
/// Gets the current action type.
/// </summary>
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<string> 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
/// <summary>
/// Gets the trigger keys.
/// </summary>
public List<string> GetTriggerKeys() => _triggerKeys.ToList();
/// <summary>
/// Gets the action keys (for Key/Shortcut action type).
/// </summary>
public List<string> GetActionKeys() => _actionKeys.ToList();
/// <summary>
/// Gets the selected mouse trigger.
/// </summary>
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;
}
/// <summary>
/// Gets the text content (for Text action type).
/// </summary>
public string GetTextContent() => TextContentBox?.Text ?? string.Empty;
/// <summary>
/// Gets the URL (for OpenUrl action type).
/// </summary>
public string GetUrl() => UrlPathInput?.Text ?? string.Empty;
/// <summary>
/// Gets the program path (for OpenApp action type).
/// </summary>
public string GetProgramPath() => ProgramPathInput?.Text ?? string.Empty;
/// <summary>
/// Gets the program arguments (for OpenApp action type).
/// </summary>
public string GetProgramArgs() => ProgramArgsInput?.Text ?? string.Empty;
/// <summary>
/// Gets the start-in directory (for OpenApp action type).
/// </summary>
public string GetStartInDirectory() => StartInPathInput?.Text ?? string.Empty;
/// <summary>
/// Gets whether the mapping is app-specific.
/// </summary>
public bool GetIsAppSpecific()
{
return AppSpecificCheckBox?.IsChecked ?? false;
}
/// <summary>
/// Gets the app name for app-specific mappings.
/// </summary>
public string GetAppName()
{
return GetIsAppSpecific() ? (AppNameTextBox?.Text ?? string.Empty) : string.Empty;
}
/// <summary>
/// Gets the elevation level (for OpenApp action type).
/// </summary>
public ElevationLevel GetElevationLevel() => (ElevationLevel)(ElevationComboBox?.SelectedIndex ?? 0);
/// <summary>
/// Gets the window visibility (for OpenApp action type).
/// </summary>
public StartWindowType GetVisibility() => (StartWindowType)(VisibilityComboBox?.SelectedIndex ?? 0);
/// <summary>
/// Gets the if-running action (for OpenApp action type).
/// </summary>
public ProgramAlreadyRunningAction GetIfRunningAction() => (ProgramAlreadyRunningAction)(IfRunningComboBox?.SelectedIndex ?? 0);
#endregion
#region Public API - Validation
/// <summary>
/// Returns true when all required fields for the current action type are filled.
/// </summary>
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
/// <summary>
/// Sets the trigger keys.
/// </summary>
public void SetTriggerKeys(List<string> keys)
{
_triggerKeys.Clear();
if (keys != null)
{
foreach (var key in keys)
{
_triggerKeys.Add(key);
}
}
UpdateAppSpecificCheckBoxState();
}
/// <summary>
/// Sets the action keys.
/// </summary>
public void SetActionKeys(List<string> keys)
{
_actionKeys.Clear();
if (keys != null)
{
foreach (var key in keys)
{
_actionKeys.Add(key);
}
}
}
/// <summary>
/// Sets the action type.
/// </summary>
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;
}
}
/// <summary>
/// Sets the text content (for Text action type).
/// </summary>
public void SetTextContent(string text)
{
if (TextContentBox != null)
{
TextContentBox.Text = text;
}
}
/// <summary>
/// Sets the URL (for OpenUrl action type).
/// </summary>
public void SetUrl(string url)
{
if (UrlPathInput != null)
{
UrlPathInput.Text = url;
}
}
/// <summary>
/// Sets the program path (for OpenApp action type).
/// </summary>
public void SetProgramPath(string path)
{
if (ProgramPathInput != null)
{
ProgramPathInput.Text = path;
}
}
/// <summary>
/// Sets the program arguments (for OpenApp action type).
/// </summary>
public void SetProgramArgs(string args)
{
if (ProgramArgsInput != null)
{
ProgramArgsInput.Text = args;
}
}
/// <summary>
/// Sets the start-in directory (for OpenApp action type).
/// </summary>
public void SetStartInDirectory(string path)
{
if (StartInPathInput != null)
{
StartInPathInput.Text = path;
}
}
/// <summary>
/// Sets the elevation level (for OpenApp action type).
/// </summary>
public void SetElevationLevel(ElevationLevel elevationLevel)
{
if (ElevationComboBox != null)
{
ElevationComboBox.SelectedIndex = (int)elevationLevel;
}
}
/// <summary>
/// Sets the window visibility (for OpenApp action type).
/// </summary>
public void SetVisibility(StartWindowType visibility)
{
if (VisibilityComboBox != null)
{
VisibilityComboBox.SelectedIndex = (int)visibility;
}
}
/// <summary>
/// Sets the if-already-running action (for OpenApp action type).
/// </summary>
public void SetIfRunningAction(ProgramAlreadyRunningAction ifRunningAction)
{
if (IfRunningComboBox != null)
{
IfRunningComboBox.SelectedIndex = (int)ifRunningAction;
}
}
/// <summary>
/// Sets whether the mapping is app-specific.
/// </summary>
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);
}
/// <summary>
/// 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).
/// </summary>
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();
}
/// <summary>
/// Resets all inputs to their default state.
/// </summary>
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();
}
/// <summary>
/// Resets only the toggle buttons without clearing the key displays.
/// </summary>
public void ResetToggleButtons()
{
UncheckAllToggleButtons();
}
#endregion
#region Notifications
/// <summary>
/// Shows a warning notification in the InfoBar.
/// </summary>
public void ShowNotificationTip(string message)
{
ShowValidationMessage("Warning", message, InfoBarSeverity.Warning);
}
/// <summary>
/// Shows an error in the InfoBar with title and message.
/// </summary>
public void ShowValidationError(string title, string message)
{
ShowValidationMessage(title, message, InfoBarSeverity.Error);
}
/// <summary>
/// Shows a validation error based on the error type.
/// </summary>
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.");
}
}
/// <summary>
/// Shows a message in the InfoBar with the specified severity.
/// </summary>
private void ShowValidationMessage(string title, string message, InfoBarSeverity severity)
{
if (ValidationInfoBar != null)
{
ValidationInfoBar.Title = title;
ValidationInfoBar.Message = message;
ValidationInfoBar.Severity = severity;
ValidationInfoBar.IsOpen = true;
}
}
/// <summary>
/// Hides the validation InfoBar.
/// </summary>
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

View File

@@ -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,
}
}

View File

@@ -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;
}
}

View File

@@ -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<string> Shortcut { get; set; }
bool IsActive { get; set; }
string Id { get; set; }
string AppName { get; set; }
}
}

View File

@@ -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,
}
}

View File

@@ -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<VirtualKey> _currentlyPressedKeys = new();
private List<VirtualKey> _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<string> GetFormattedKeyList()
{
if (_activeTarget == null)
{
return new List<string>();
}
List<string> keyList = new List<string>();
List<VirtualKey> modifierKeys = new List<VirtualKey>();
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<string> formattedKeys);
void OnKeyUp(VirtualKey key, List<string> formattedKeys)
{
}
void ClearKeys();
void OnInputLimitReached();
}
}

View File

@@ -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<string> Shortcut { get; set; } = new List<string>();
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;
}
}

View File

@@ -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<string> Shortcut { get; set; } = new List<string>();
public List<string> RemappedKeys { get; set; } = new List<string>();
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));
}
}
}

View File

@@ -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<string> originalKeys, List<string> 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;
}
}
}

View File

@@ -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<string> Shortcut { get; set; } = new List<string>();
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;
}
}

View File

@@ -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<string> Shortcut { get; set; } = new List<string>();
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;
}
}

View File

@@ -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,
}
}

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)));
}
}
}

View File

@@ -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; }
}
}

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.Interop
{
public class KeyToTextMapping
{
public int OriginalKey { get; set; }
public string TargetText { get; set; } = string.Empty;
}
}

View File

@@ -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,
}
}

View File

@@ -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;
}
}

View File

@@ -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<KeyMapping> GetSingleKeyMappings()
{
var result = new List<KeyMapping>();
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<ShortcutKeyMapping> GetShortcutMappings()
{
var result = new List<ShortcutKeyMapping>();
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<ShortcutKeyMapping> GetShortcutMappingsByType(ShortcutOperationType operationType)
{
var result = new List<ShortcutKeyMapping>();
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<KeyToTextMapping> GetKeyToTextMappings()
{
var result = new List<KeyToTextMapping>();
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);
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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,
}
}

View File

@@ -8,16 +8,36 @@
<RootNamespace>KeyboardManagerEditorUI</RootNamespace> <RootNamespace>KeyboardManagerEditorUI</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<UseWinUI>true</UseWinUI> <UseWinUI>true</UseWinUI>
<EnableMsixTooling>true</EnableMsixTooling> <EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<WindowsPackageType>None</WindowsPackageType> <WindowsPackageType>None</WindowsPackageType>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks> <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath> <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<AssemblyName>PowerToys.KeyboardManagerEditorUI</AssemblyName> <AssemblyName>PowerToys.KeyboardManagerEditorUI</AssemblyName>
<OutputPath>$(RepoRoot)$(Platform)\$(Configuration)\$(MSBuildProjectName)</OutputPath> <OutputPath>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps</OutputPath>
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
<ProjectPriFileName>PowerToys.KeyboardManagerEditorUI.pri</ProjectPriFileName>
</PropertyGroup> </PropertyGroup>
<PropertyGroup>
<EnableDefaultXamlItems>true</EnableDefaultXamlItems>
<EnableXamlJitOptimization>true</EnableXamlJitOptimization>
</PropertyGroup>
<ItemGroup>
<None Remove="Assets\KeyboardManagerEditor\Keyboard.ico" />
<None Remove="Styles\Colors.xaml" />
</ItemGroup>
<ItemGroup>
<Page Remove="KeyboardManagerEditorXAML\App.xaml" />
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="KeyboardManagerEditorXAML\App.xaml" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Manifest Include="$(ApplicationManifest)" /> <Manifest Include="$(ApplicationManifest)" />
</ItemGroup> </ItemGroup>
@@ -27,12 +47,16 @@
Tools extension to be activated for this project even if the Windows App SDK Nuget Tools extension to be activated for this project even if the Windows App SDK Nuget
package has not yet been restored. package has not yet been restored.
--> -->
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'"> <ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnablePreviewMsixTooling)'=='true'">
<ProjectCapability Include="Msix" /> <ProjectCapability Include="Msix" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CommunityToolkit.Common" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" /> <PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
<PackageReference Include="Microsoft.WindowsAppSDK" /> <PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="WinUIEx" />
<!-- This line forces the WebView2 version used by Windows App SDK to be the one we expect from Directory.Packages.props . --> <!-- This line forces the WebView2 version used by Windows App SDK to be the one we expect from Directory.Packages.props . -->
<PackageReference Include="Microsoft.Web.WebView2" /> <PackageReference Include="Microsoft.Web.WebView2" />
</ItemGroup> </ItemGroup>
@@ -40,9 +64,26 @@
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" /> <ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" /> <ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" /> <ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
<ProjectReference Include="..\..\..\common\Common.UI.Controls\Common.UI.Controls.csproj" />
<ProjectReference Include="..\KeyboardManagerEditorLibraryWrapper\KeyboardManagerEditorLibraryWrapper.vcxproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Assets\" /> <Content Update="Assets\KeyboardManagerEditor\Keyboard.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Assets\KeyboardManagerEditor\Square150x150Logo.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Page Update="Styles\Colors.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Styles\Button.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup> </ItemGroup>
<!-- <!--
@@ -50,7 +91,7 @@
Explorer "Package and Publish" context menu entry to be enabled for this project even if Explorer "Package and Publish" context menu entry to be enabled for this project even if
the Windows App SDK Nuget package has not yet been restored. the Windows App SDK Nuget package has not yet been restored.
--> -->
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'"> <PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnablePreviewMsixTooling)'=='true'">
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu> <HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8" ?>
<Application
x:Class="KeyboardManagerEditorUI.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:KeyboardManagerEditorUI">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<ResourceDictionary Source="/Controls/IconLabelControl.xaml" />
<ResourceDictionary Source="/Styles/Button.xaml" />
<ResourceDictionary Source="/Styles/Colors.xaml" />
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/KeyVisual/KeyVisual.xaml" />
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/KeyVisual/KeyCharPresenter.xaml" />
</ResourceDictionary.MergedDictionaries>
<x:Double x:Key="ContentDialogMaxWidth">960</x:Double>
<!-- Icons -->
<x:String x:Key="ArrowIconData">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</x:String>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -7,7 +7,12 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime; using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using KeyboardManagerEditorUI.Helpers;
using KeyboardManagerEditorUI.Settings;
using ManagedCommon; using ManagedCommon;
using Microsoft.UI;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives; using Microsoft.UI.Xaml.Controls.Primitives;
@@ -29,14 +34,22 @@ namespace KeyboardManagerEditorUI
public partial class App : Application public partial class App : Application
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="App"/> class.
/// Initializes the singleton application object. This is the first line of authored code /// 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(). /// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary> /// </summary>
public App() public App()
{ {
this.InitializeComponent(); this.InitializeComponent();
Task.Run(() =>
{
Logger.InitializeLogger("\\Keyboard Manager\\WinUI3Editor\\Logs"); Logger.InitializeLogger("\\Keyboard Manager\\WinUI3Editor\\Logs");
Logger.LogInfo("keyboard-manager WinUI3 editor logger is initialized"); });
UnhandledException += App_UnhandledException;
SettingsManager.CorrelateServiceAndEditorMappings();
} }
/// <summary> /// <summary>
@@ -45,11 +58,28 @@ namespace KeyboardManagerEditorUI
/// <param name="args">Details about the launch request and process.</param> /// <param name="args">Details about the launch request and process.</param>
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{ {
window = new MainWindow(); MainWindow = new MainWindow();
window.Activate();
MainWindow.DispatcherQueue.TryEnqueue(() =>
{
MainWindow.Activate();
MainWindow.DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
(MainWindow.Content as FrameworkElement)?.UpdateLayout();
});
});
Logger.LogInfo("keyboard-manager WinUI3 editor window is launched"); Logger.LogInfo("keyboard-manager WinUI3 editor window is launched");
} }
private Window? window; /// <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);
}
internal static MainWindow MainWindow { get; private set; } = null!;
} }
} }

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8" ?>
<winuiex:WindowEx
x:Class="KeyboardManagerEditorUI.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:KeyboardManagerEditorUI"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:pages="using:KeyboardManagerEditorUI.Pages"
xmlns:winuiex="using:WinUIEx"
Title="KeyboardManagerEditorUI"
Width="1440"
Height="900"
MinWidth="860"
MinHeight="320"
mc:Ignorable="d">
<Window.SystemBackdrop>
<MicaBackdrop />
</Window.SystemBackdrop>
<Grid
x:Name="LayoutRoot"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TitleBar x:Name="titleBar" Title="Keyboard Manager">
<TitleBar.IconSource>
<ImageIconSource ImageSource="/Assets/KeyboardManagerEditor/FluentIconsKeyboardManager.png" />
</TitleBar.IconSource>
</TitleBar>
<pages:MainPage
Grid.Row="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" />
</Grid>
</winuiex:WindowEx>

View File

@@ -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;
}
}
}

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<Window
x:Class="KeyboardManagerEditorUI.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:KeyboardManagerEditorUI"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="KeyboardManagerEditorUI"
mc:Ignorable="d">
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal">
<Button x:Name="myButton" Click="MyButton_Click">Click Me</Button>
</StackPanel>
</Window>

View File

@@ -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
{
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
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";
}
}
}

View File

@@ -15,9 +15,9 @@
<mp:PhoneIdentity PhoneProductId="edb1d2cd-ef93-4f89-9db6-4edf04ff20a5" PhonePublisherId="00000000-0000-0000-0000-000000000000"/> <mp:PhoneIdentity PhoneProductId="edb1d2cd-ef93-4f89-9db6-4edf04ff20a5" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties> <Properties>
<DisplayName>KeyboardManagerEditorUI</DisplayName> <DisplayName>Keyboard Manager</DisplayName>
<PublisherDisplayName>haoliuu</PublisherDisplayName> <PublisherDisplayName>haoliuu</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo> <Logo>Assets\KeyboardManagerEditor\StoreLogo.png</Logo>
</Properties> </Properties>
<Dependencies> <Dependencies>
@@ -34,13 +34,13 @@
Executable="$targetnametoken$.exe" Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$"> EntryPoint="$targetentrypoint$">
<uap:VisualElements <uap:VisualElements
DisplayName="KeyboardManagerEditorUI" DisplayName="Keyboard Manager"
Description="KeyboardManagerEditorUI" Description="Keyboard Manager"
BackgroundColor="transparent" BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png" Square150x150Logo="Assets\KeyboardManagerEditor\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png"> Square44x44Logo="Assets\KeyboardManagerEditor\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" /> <uap:DefaultTile Wide310x150Logo="Assets\KeyboardManagerEditor\Wide310x150Logo.png" />
<uap:SplashScreen Image="Assets\SplashScreen.png" /> <uap:SplashScreen Image="Assets\KeyboardManagerEditor\SplashScreen.png" />
</uap:VisualElements> </uap:VisualElements>
</Application> </Application>
</Applications> </Applications>

View File

@@ -0,0 +1,548 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="KeyboardManagerEditorUI.Pages.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:commoncontrols="using:Microsoft.PowerToys.Common.UI.Controls"
xmlns:controls="using:KeyboardManagerEditorUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helper="using:KeyboardManagerEditorUI.Helpers"
xmlns:local="using:KeyboardManagerEditorUI.Pages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
<Page.Resources>
<x:Double x:Key="ContentDialogMaxWidth">800</x:Double>
<x:Double x:Key="ContentDialogMaxHeight">800</x:Double>
<Style
x:Key="OriginalKeyVisualStyle"
BasedOn="{StaticResource DefaultKeyVisualStyle}"
TargetType="commoncontrols:KeyVisual">
<Setter Property="Padding" Value="6" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Background" Value="{ThemeResource ControlFillColorDefaultBrush}" />
<Setter Property="CornerRadius" Value="{StaticResource ControlCornerRadius}" />
<Setter Property="FontSize" Value="12" />
<Setter Property="IsTabStop" Value="False" />
<Setter Property="RenderKeyAsGlyph" Value="True" />
</Style>
<Style
x:Key="RemappedKeyVisualStyle"
BasedOn="{StaticResource OriginalKeyVisualStyle}"
TargetType="commoncontrols:KeyVisual">
<Setter Property="Background" Value="{ThemeResource CustomAccentBackgroundBrush}" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Foreground" Value="{ThemeResource AccentTextFillColorPrimaryBrush}" />
</Style>
<Style
x:Key="RemappedIconLabelControlStyle"
BasedOn="{StaticResource DefaultIconLabelControlStyle}"
TargetType="controls:IconLabelControl">
<Setter Property="Background" Value="{ThemeResource CustomAccentBackgroundBrush}" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="6" />
<Setter Property="FontSize" Value="12" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="CornerRadius" Value="{StaticResource ControlCornerRadius}" />
<Setter Property="Foreground" Value="{ThemeResource AccentTextFillColorPrimaryBrush}" />
</Style>
<Style x:Key="ItemDividerStyle" TargetType="Rectangle">
<Style.Setters>
<Setter Property="Grid.ColumnSpan" Value="2" />
<Setter Property="Height" Value="1" />
<Setter Property="Margin" Value="-16,0,-16,-8" />
<Setter Property="Fill" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Bottom" />
</Style.Setters>
</Style>
<tkconverters:DoubleToVisibilityConverter
x:Key="CountToVisibilityConverter"
FalseValue="Collapsed"
GreaterThan="0"
TrueValue="Visible" />
<tkconverters:StringVisibilityConverter
x:Key="StringVisibilityConverter"
EmptyValue="Collapsed"
NotEmptyValue="Visible" />
</Page.Resources>
<Grid Padding="16" RowSpacing="16">
<Grid.RowDefinitions>
<RowDefinition Height="48" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid VerticalAlignment="Top">
<Button
x:Name="NewRemappingBtn"
VerticalAlignment="Top"
Click="NewRemappingBtn_Click">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon FontSize="14" Glyph="&#xE710;" />
<TextBlock x:Uid="NewRemappingBtn_Text" VerticalAlignment="Center" />
</StackPanel>
</Button>
</Grid>
<Grid
Grid.Row="1"
Background="{ThemeResource LayerFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="{ThemeResource OverlayCornerRadius}">
<tkcontrols:SwitchPresenter
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
TargetType="x:String"
Value="{x:Bind MappingState, Mode=OneWay}">
<tkcontrols:Case Value="Empty">
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="8">
<FontIcon
HorizontalAlignment="Center"
FontSize="24"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Glyph="&#xEDA7;" />
<TextBlock
x:Uid="EmptyStateTitle"
VerticalAlignment="Center"
CharacterSpacing="12"
TextAlignment="Center"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
<TextBlock
x:Uid="EmptyStateDescription"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
TextAlignment="Center"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
</StackPanel>
</tkcontrols:Case>
<tkcontrols:Case Value="HasMappings">
<ScrollViewer VerticalAlignment="Stretch" VerticalScrollBarVisibility="Auto">
<Grid>
<StackPanel Orientation="Vertical" Spacing="24">
<!-- Remappings Section -->
<StackPanel Orientation="Vertical" Visibility="{x:Bind RemappingList.Count, Mode=OneWay, Converter={StaticResource CountToVisibilityConverter}}">
<TextBlock
x:Uid="RemappingsHeader"
Margin="16,16,0,8"
Style="{StaticResource BodyStrongTextBlockStyle}" />
<Rectangle
Height="1"
HorizontalAlignment="Stretch"
Fill="{ThemeResource CardStrokeColorDefaultBrush}" />
<ListView
IsItemClickEnabled="True"
ItemClick="RemappingsList_ItemClick"
ItemsSource="{x:Bind RemappingList}"
ScrollViewer.VerticalScrollMode="Disabled"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="helper:Remapping">
<Grid MinHeight="48" Padding="0,8,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Rectangle Style="{StaticResource ItemDividerStyle}" />
<StackPanel Orientation="Horizontal" Spacing="8">
<ItemsControl VerticalAlignment="Center" ItemsSource="{x:Bind Shortcut}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<commoncontrols:KeyVisual Content="{Binding}" Style="{StaticResource OriginalKeyVisualStyle}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock
x:Uid="MapsToText"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<ItemsControl VerticalAlignment="Center" ItemsSource="{x:Bind RemappedKeys}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<commoncontrols:KeyVisual Content="{Binding}" Style="{StaticResource RemappedKeyVisualStyle}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<StackPanel
Orientation="Horizontal"
Spacing="8"
Visibility="{x:Bind AppName, Converter={StaticResource StringVisibilityConverter}}">
<TextBlock
x:Uid="InText"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<controls:IconLabelControl ActionType="Program" Label="{x:Bind AppName}" />
</StackPanel>
</StackPanel>
<StackPanel
Grid.Column="1"
Orientation="Horizontal"
Spacing="8">
<ToggleSwitch
IsOn="{x:Bind IsActive}"
Style="{StaticResource RightAlignedCompactToggleSwitchStyle}"
Toggled="ToggleSwitch_Toggled" />
<Button
VerticalAlignment="Center"
Content="{ui:FontIcon Glyph=&#xE712;,
FontSize=14}"
Style="{StaticResource SubtleButtonStyle}">
<Button.Flyout>
<MenuFlyout>
<MenuFlyoutItem
x:Uid="DeleteMenuItem"
Click="DeleteMapping_Click"
Icon="{ui:FontIcon Glyph=&#xE74D;,
FontSize=14}"
Tag="{x:Bind}" />
</MenuFlyout>
</Button.Flyout>
</Button>
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
<!-- Text Section -->
<StackPanel Orientation="Vertical" Visibility="{x:Bind TextMappings.Count, Mode=OneWay, Converter={StaticResource CountToVisibilityConverter}}">
<TextBlock
x:Uid="TextMappingsHeader"
Margin="16,16,0,8"
Style="{StaticResource BodyStrongTextBlockStyle}" />
<Rectangle
Height="1"
HorizontalAlignment="Stretch"
Fill="{ThemeResource CardStrokeColorDefaultBrush}" />
<ListView
IsItemClickEnabled="True"
ItemClick="TextMappingsList_ItemClick"
ItemsSource="{x:Bind TextMappings}"
ScrollViewer.VerticalScrollMode="Disabled"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="helper:TextMapping">
<Grid MinHeight="48" Padding="0,8,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Rectangle Style="{StaticResource ItemDividerStyle}" />
<StackPanel Orientation="Horizontal" Spacing="8">
<ItemsControl VerticalAlignment="Center" ItemsSource="{x:Bind Shortcut}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<commoncontrols:KeyVisual Content="{Binding}" Style="{StaticResource OriginalKeyVisualStyle}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock
x:Uid="InsertsText"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<controls:IconLabelControl
ActionType="Text"
Label="{x:Bind Text}"
Style="{StaticResource RemappedIconLabelControlStyle}" />
<StackPanel
Orientation="Horizontal"
Spacing="8"
Visibility="{x:Bind AppName, Converter={StaticResource StringVisibilityConverter}}">
<TextBlock
x:Uid="InText"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<controls:IconLabelControl ActionType="Program" Label="{x:Bind AppName}" />
</StackPanel>
</StackPanel>
<StackPanel
Grid.Column="1"
Orientation="Horizontal"
Spacing="8">
<ToggleSwitch
IsOn="{x:Bind IsActive}"
Style="{StaticResource RightAlignedCompactToggleSwitchStyle}"
Toggled="ToggleSwitch_Toggled" />
<Button
VerticalAlignment="Center"
Content="{ui:FontIcon Glyph=&#xE712;,
FontSize=14}"
Style="{StaticResource SubtleButtonStyle}">
<Button.Flyout>
<MenuFlyout>
<MenuFlyoutItem
x:Uid="DeleteMenuItem"
Click="DeleteMapping_Click"
Icon="{ui:FontIcon Glyph=&#xE74D;,
FontSize=14}"
Tag="{x:Bind}" />
</MenuFlyout>
</Button.Flyout>
</Button>
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
<!-- Programs Section -->
<StackPanel Orientation="Vertical" Visibility="{x:Bind ProgramShortcuts.Count, Mode=OneWay, Converter={StaticResource CountToVisibilityConverter}}">
<TextBlock
x:Uid="ProgramsHeader"
Margin="16,16,0,8"
Style="{StaticResource BodyStrongTextBlockStyle}" />
<Rectangle
Height="1"
HorizontalAlignment="Stretch"
Fill="{ThemeResource CardStrokeColorDefaultBrush}" />
<ListView
IsItemClickEnabled="True"
ItemClick="ProgramShortcutsList_ItemClick"
ItemsSource="{x:Bind ProgramShortcuts}"
ScrollViewer.VerticalScrollMode="Disabled"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="helper:ProgramShortcut">
<Grid MinHeight="48" Padding="0,8,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Rectangle Style="{StaticResource ItemDividerStyle}" />
<StackPanel Orientation="Horizontal" Spacing="8">
<ToolTipService.ToolTip>
<ToolTip>
<StackPanel Spacing="4">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock x:Uid="TooltipArguments" FontWeight="SemiBold" />
<TextBlock Text="{x:Bind Args}" />
</StackPanel>
<StackPanel
Orientation="Horizontal"
Spacing="8"
Visibility="{x:Bind StartInDirectory, Converter={StaticResource StringVisibilityConverter}}">
<TextBlock x:Uid="TooltipStartIn" FontWeight="SemiBold" />
<TextBlock Text="{x:Bind StartInDirectory}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock x:Uid="TooltipElevation" FontWeight="SemiBold" />
<TextBlock Text="{x:Bind Elevation}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock x:Uid="TooltipIfRunning" FontWeight="SemiBold" />
<TextBlock Text="{x:Bind IfRunningAction}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock x:Uid="TooltipWindow" FontWeight="SemiBold" />
<TextBlock Text="{x:Bind Visibility}" />
</StackPanel>
</StackPanel>
</ToolTip>
</ToolTipService.ToolTip>
<ItemsControl VerticalAlignment="Center" ItemsSource="{x:Bind Shortcut}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<commoncontrols:KeyVisual Content="{Binding}" Style="{StaticResource OriginalKeyVisualStyle}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock
x:Uid="OpensText"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<controls:IconLabelControl
ActionType="Program"
Label="{x:Bind AppToRun}"
Style="{StaticResource RemappedIconLabelControlStyle}" />
<StackPanel
Orientation="Horizontal"
Spacing="8"
Visibility="{x:Bind AppName, Converter={StaticResource StringVisibilityConverter}}">
<TextBlock
x:Uid="InText"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<controls:IconLabelControl ActionType="Program" Label="{x:Bind AppName}" />
</StackPanel>
</StackPanel>
<StackPanel
Grid.Column="1"
VerticalAlignment="Center"
Orientation="Horizontal"
Spacing="8">
<ToggleSwitch
IsOn="{x:Bind IsActive}"
Style="{StaticResource RightAlignedCompactToggleSwitchStyle}"
Toggled="ToggleSwitch_Toggled" />
<Button
VerticalAlignment="Center"
Content="{ui:FontIcon Glyph=&#xE712;,
FontSize=14}"
Style="{StaticResource SubtleButtonStyle}">
<Button.Flyout>
<MenuFlyout>
<MenuFlyoutItem
x:Uid="DeleteMenuItem"
Click="DeleteMapping_Click"
Icon="{ui:FontIcon Glyph=&#xE74D;,
FontSize=14}"
Tag="{x:Bind}" />
</MenuFlyout>
</Button.Flyout>
</Button>
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
<!-- URLs Section -->
<StackPanel Orientation="Vertical" Visibility="{x:Bind UrlShortcuts.Count, Mode=OneWay, Converter={StaticResource CountToVisibilityConverter}}">
<TextBlock
x:Uid="UrlsHeader"
Margin="16,16,0,8"
Style="{StaticResource BodyStrongTextBlockStyle}" />
<Rectangle
Height="1"
HorizontalAlignment="Stretch"
Fill="{ThemeResource CardStrokeColorDefaultBrush}" />
<ListView
IsItemClickEnabled="True"
ItemClick="UrlShortcutsList_ItemClick"
ItemsSource="{x:Bind UrlShortcuts}"
ScrollViewer.VerticalScrollMode="Disabled"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="helper:URLShortcut">
<Grid MinHeight="48" Padding="0,8,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Rectangle Style="{StaticResource ItemDividerStyle}" />
<StackPanel Orientation="Horizontal" Spacing="8">
<ItemsControl VerticalAlignment="Center" ItemsSource="{x:Bind Shortcut}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<commoncontrols:KeyVisual Content="{Binding}" Style="{StaticResource OriginalKeyVisualStyle}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock
x:Uid="OpensText"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<controls:IconLabelControl
ActionType="Url"
Label="{x:Bind URL}"
Style="{StaticResource RemappedIconLabelControlStyle}" />
<StackPanel
Orientation="Horizontal"
Spacing="8"
Visibility="{x:Bind AppName, Converter={StaticResource StringVisibilityConverter}}">
<TextBlock
x:Uid="InText"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<controls:IconLabelControl ActionType="Program" Label="{x:Bind AppName}" />
</StackPanel>
</StackPanel>
<StackPanel
Grid.Column="1"
Orientation="Horizontal"
Spacing="8">
<ToggleSwitch
IsOn="{x:Bind IsActive}"
Style="{StaticResource RightAlignedCompactToggleSwitchStyle}"
Toggled="ToggleSwitch_Toggled" />
<Button
VerticalAlignment="Center"
Content="{ui:FontIcon Glyph=&#xE712;,
FontSize=14}"
Style="{StaticResource SubtleButtonStyle}">
<Button.Flyout>
<MenuFlyout>
<MenuFlyoutItem
x:Uid="DeleteMenuItem"
Click="DeleteMapping_Click"
Icon="{ui:FontIcon Glyph=&#xE74D;,
FontSize=14}"
Tag="{x:Bind}" />
</MenuFlyout>
</Button.Flyout>
</Button>
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</StackPanel>
</Grid>
</ScrollViewer>
</tkcontrols:Case>
</tkcontrols:SwitchPresenter>
</Grid>
<!-- Content Dialog for new remapping -->
<ContentDialog
x:Name="RemappingDialog"
x:Uid="RemappingDialog"
Width="760"
MinWidth="800"
MinHeight="500"
MaxWidth="900"
DefaultButton="Primary"
IsPrimaryButtonEnabled="False"
PrimaryButtonStyle="{StaticResource AccentButtonStyle}">
<controls:UnifiedMappingControl x:Name="UnifiedMappingControl" Margin="0,16,0,0" />
</ContentDialog>
<!-- Confirmation Dialog for delete -->
<ContentDialog
x:Name="DeleteConfirmationDialog"
x:Uid="DeleteConfirmationDialog"
DefaultButton="Primary">
<TextBlock x:Uid="DeleteConfirmationDialogContent" TextWrapping="Wrap" />
</ContentDialog>
</Grid>
</Page>

View File

@@ -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
{
/// <summary>
/// A consolidated page that displays all mappings from Remappings, Text, Programs, and URLs pages.
/// </summary>
#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<Remapping> RemappingList { get; } = new();
public ObservableCollection<TextMapping> TextMappings { get; } = new();
public ObservableCollection<ProgramShortcut> ProgramShortcuts { get; } = new();
public ObservableCollection<URLShortcut> 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<string> 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<string> 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<string> 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<string> 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<string> originalKeys, string targetApp = "")
{
bool deleted = originalKeys.Count == 1
? DeleteSingleKeyToTextMapping(originalKeys[0])
: DeleteMultiKeyMapping(originalKeys, targetApp);
if (deleted)
{
_mappingService!.SaveSettings();
}
}
private bool DeleteMultiKeyMapping(List<string> 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<string> triggerKeys)
{
List<string> 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<string> 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<string> 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<string> 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<string> 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<string> 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

View File

@@ -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<string, ShortcutSettings> ShortcutSettingsDictionary { get; set; } = new Dictionary<string, ShortcutSettings>();
public Dictionary<string, List<string>> ProfileDictionary { get; set; } = new Dictionary<string, List<string>>();
public Dictionary<ShortcutOperationType, List<string>> ShortcutsByOperationType { get; set; } = new Dictionary<ShortcutOperationType, List<string>>();
public string ActiveProfile { get; set; } = string.Empty;
}
}

View File

@@ -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<EditorSettings>(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<string>? value))
{
value = new System.Collections.Generic.List<string>();
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<KeyToTextMapping> keyToTextMappings,
List<KeyMapping> singleKeyMappings,
List<ShortcutKeyMapping> 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);
}
}
}

View File

@@ -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<string> Profiles { get; set; } = new List<string>();
public bool IsActive { get; set; } = true;
}
}

View File

@@ -0,0 +1,295 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="NewRemappingBtn_Text.Text" xml:space="preserve">
<value>Add new remapping</value>
</data>
<data name="EmptyStateTitle.Text" xml:space="preserve">
<value>Nothing mapped yet</value>
</data>
<data name="EmptyStateDescription.Text" xml:space="preserve">
<value>Create a key or shortcut remapping to customize how your keyboard works.</value>
</data>
<data name="RemappingsHeader.Text" xml:space="preserve">
<value>Keys and shortcuts</value>
</data>
<data name="TextMappingsHeader.Text" xml:space="preserve">
<value>Text</value>
</data>
<data name="ProgramsHeader.Text" xml:space="preserve">
<value>Programs</value>
</data>
<data name="UrlsHeader.Text" xml:space="preserve">
<value>Urls</value>
</data>
<data name="MapsToText.Text" xml:space="preserve">
<value>maps to</value>
</data>
<data name="InsertsText.Text" xml:space="preserve">
<value>inserts</value>
</data>
<data name="OpensText.Text" xml:space="preserve">
<value>opens</value>
</data>
<data name="InText.Text" xml:space="preserve">
<value>in</value>
</data>
<data name="DeleteMenuItem.Text" xml:space="preserve">
<value>Delete</value>
</data>
<data name="TooltipArguments.Text" xml:space="preserve">
<value>Arguments:</value>
</data>
<data name="TooltipStartIn.Text" xml:space="preserve">
<value>Start in:</value>
</data>
<data name="TooltipElevation.Text" xml:space="preserve">
<value>Elevation:</value>
</data>
<data name="TooltipIfRunning.Text" xml:space="preserve">
<value>If running:</value>
</data>
<data name="TooltipWindow.Text" xml:space="preserve">
<value>Window:</value>
</data>
<data name="RemappingDialog.Title" xml:space="preserve">
<value>Add new remapping</value>
</data>
<data name="RemappingDialog.PrimaryButtonText" xml:space="preserve">
<value>Save</value>
</data>
<data name="RemappingDialog.CloseButtonText" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="DeleteConfirmationDialog.Title" xml:space="preserve">
<value>Are you sure?</value>
</data>
<data name="DeleteConfirmationDialogContent.Text" xml:space="preserve">
<value>You are about to delete this remapping.</value>
</data>
<data name="DeleteConfirmationDialog.PrimaryButtonText" xml:space="preserve">
<value>Delete</value>
</data>
<data name="DeleteConfirmationDialog.CloseButtonText" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="TriggerLabel.Text" xml:space="preserve">
<value>Trigger</value>
</data>
<data name="ActionLabel.Text" xml:space="preserve">
<value>Action</value>
</data>
<data name="TriggerType_KeyOrShortcut.AutomationProperties.Name" xml:space="preserve">
<value>Key or shortcut</value>
</data>
<data name="TriggerType_KeyOrShortcut_Text.Text" xml:space="preserve">
<value>Key or shortcut</value>
</data>
<data name="TriggerType_Mouse.AutomationProperties.Name" xml:space="preserve">
<value>Mouse button</value>
</data>
<data name="TriggerType_Mouse_Text.Text" xml:space="preserve">
<value>Mouse button</value>
</data>
<data name="AllowChordsCheckBox.Content" xml:space="preserve">
<value>Allow chords</value>
</data>
<data name="ExactMatchCheckBox.Content" xml:space="preserve">
<value>Exact match</value>
</data>
<data name="MouseButton_Left.Content" xml:space="preserve">
<value>Left</value>
</data>
<data name="MouseButton_Center.Content" xml:space="preserve">
<value>Center</value>
</data>
<data name="MouseButton_Right.Content" xml:space="preserve">
<value>Right</value>
</data>
<data name="MouseButton_Button1.Content" xml:space="preserve">
<value>Button 1</value>
</data>
<data name="MouseButton_Button2.Content" xml:space="preserve">
<value>Button 2</value>
</data>
<data name="AppSpecificCheckBox.Content" xml:space="preserve">
<value>Only apply to a specific app</value>
</data>
<data name="AppNameTextBox.Header" xml:space="preserve">
<value>App name</value>
</data>
<data name="AppNameTextBox.PlaceholderText" xml:space="preserve">
<value>Enter app name (e.g., notepad.exe)</value>
</data>
<data name="ActionType_KeyOrShortcut.AutomationProperties.Name" xml:space="preserve">
<value>Remap to key or shortcut</value>
</data>
<data name="ActionType_KeyOrShortcut_Text.Text" xml:space="preserve">
<value>Remap to key or shortcut</value>
</data>
<data name="ActionType_Text.AutomationProperties.Name" xml:space="preserve">
<value>Insert text</value>
</data>
<data name="ActionType_Text_Text.Text" xml:space="preserve">
<value>Insert text</value>
</data>
<data name="ActionType_OpenUrl.AutomationProperties.Name" xml:space="preserve">
<value>Open URL</value>
</data>
<data name="ActionType_OpenUrl_Text.Text" xml:space="preserve">
<value>Open URL</value>
</data>
<data name="ActionType_OpenApp.AutomationProperties.Name" xml:space="preserve">
<value>Open app</value>
</data>
<data name="ActionType_OpenApp_Text.Text" xml:space="preserve">
<value>Open app</value>
</data>
<data name="ActionType_MouseClick.AutomationProperties.Name" xml:space="preserve">
<value>Remap to mouse click</value>
</data>
<data name="ActionType_MouseClick_Text.Text" xml:space="preserve">
<value>Remap to mouse click</value>
</data>
<data name="TextContentBox.Header" xml:space="preserve">
<value>Text to type</value>
</data>
<data name="TextContentBox.PlaceholderText" xml:space="preserve">
<value>Enter the text to type when triggered</value>
</data>
<data name="UrlPathInput.Header" xml:space="preserve">
<value>URL to open</value>
</data>
<data name="UrlPathInput.PlaceholderText" xml:space="preserve">
<value>https://example.com</value>
</data>
<data name="ProgramPathInput.Header" xml:space="preserve">
<value>Program path</value>
</data>
<data name="ProgramPathInput.PlaceholderText" xml:space="preserve">
<value>C:\Program Files\...</value>
</data>
<data name="ProgramPathSelectButton.AutomationProperties.Name" xml:space="preserve">
<value>Select program path</value>
</data>
<data name="ProgramArgsInput.Header" xml:space="preserve">
<value>Arguments (optional)</value>
</data>
<data name="ProgramArgsInput.PlaceholderText" xml:space="preserve">
<value>--arg1 value1</value>
</data>
<data name="StartInPathInput.Header" xml:space="preserve">
<value>Start in directory (optional)</value>
</data>
<data name="StartInPathInput.PlaceholderText" xml:space="preserve">
<value>C:\Users\...</value>
</data>
<data name="StartInSelectButton.AutomationProperties.Name" xml:space="preserve">
<value>Select start directory</value>
</data>
<data name="ElevationComboBox.Header" xml:space="preserve">
<value>Run as</value>
</data>
<data name="Elevation_Normal.Content" xml:space="preserve">
<value>Normal</value>
</data>
<data name="Elevation_Elevated.Content" xml:space="preserve">
<value>Elevated</value>
</data>
<data name="Elevation_DifferentUser.Content" xml:space="preserve">
<value>Different user</value>
</data>
<data name="IfRunningComboBox.Header" xml:space="preserve">
<value>If already running</value>
</data>
<data name="IfRunning_ShowWindow.Content" xml:space="preserve">
<value>Show window</value>
</data>
<data name="IfRunning_StartAnother.Content" xml:space="preserve">
<value>Start another</value>
</data>
<data name="IfRunning_DoNothing.Content" xml:space="preserve">
<value>Do nothing</value>
</data>
<data name="IfRunning_Close.Content" xml:space="preserve">
<value>Close</value>
</data>
<data name="IfRunning_EndTask.Content" xml:space="preserve">
<value>End task</value>
</data>
<data name="VisibilityComboBox.Header" xml:space="preserve">
<value>Window visibility</value>
</data>
<data name="Visibility_Normal.Content" xml:space="preserve">
<value>Normal</value>
</data>
<data name="Visibility_Hidden.Content" xml:space="preserve">
<value>Hidden</value>
</data>
<data name="Visibility_Minimized.Content" xml:space="preserve">
<value>Minimized</value>
</data>
<data name="Visibility_Maximized.Content" xml:space="preserve">
<value>Maximized</value>
</data>
<data name="MouseClickPlaceholder.Text" xml:space="preserve">
<value>Mouse click action - coming soon</value>
</data>
</root>

View File

@@ -0,0 +1,759 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="CustomShortcutToggleButtonStyle" TargetType="ToggleButton">
<Setter Property="Background" Value="{ThemeResource ToggleButtonBackground}" />
<Setter Property="Foreground" Value="{ThemeResource ToggleButtonForeground}" />
<Setter Property="BorderBrush" Value="{ThemeResource ToggleButtonBorderBrush}" />
<Setter Property="BorderThickness" Value="1,1,1,1" />
<Setter Property="Padding" Value="{StaticResource ButtonPadding}" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="FocusVisualMargin" Value="-3" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<ContentPresenter
x:Name="ContentPresenter"
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
AutomationProperties.AccessibilityView="Raw"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}"
CornerRadius="{TemplateBinding CornerRadius}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBorderBrushPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBorderBrushPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundPressed}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBorderBrushDisabled}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Checked">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="Transparent" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundChecked}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundChecked}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderThickness">
<DiscreteObjectKeyFrame KeyTime="0" Value="1,1,1,4" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="CheckedPointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundCheckedPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundCheckedPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderThickness">
<DiscreteObjectKeyFrame KeyTime="0" Value="1,1,1,4" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="CheckedPressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundCheckedPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundCheckedPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderThickness">
<DiscreteObjectKeyFrame KeyTime="0" Value="1,1,1,4" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="CheckedDisabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundCheckedDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundCheckedDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBorderBrushCheckedDisabled}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Indeterminate">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundIndeterminate}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundIndeterminate}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBorderBrushIndeterminate}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="IndeterminatePointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundIndeterminatePointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBorderBrushIndeterminatePointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundIndeterminatePointerOver}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="IndeterminatePressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundIndeterminatePressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBorderBrushIndeterminatePressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundIndeterminatePressed}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="IndeterminateDisabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundIndeterminateDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundIndeterminateDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBorderBrushIndeterminateDisabled}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</ContentPresenter>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
x:Key="RightAlignedCompactToggleSwitchStyle"
BasedOn="{StaticResource DefaultToggleSwitchStyle}"
TargetType="ToggleSwitch">
<Style.Setters>
<Setter Property="MinWidth" Value="0" />
<Setter Property="Height" Value="36" />
<Setter Property="HorizontalContentAlignment" Value="Right" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleSwitch">
<Grid
VerticalAlignment="Stretch"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Control.IsTemplateFocusTarget="True"
CornerRadius="{TemplateBinding CornerRadius}">
<ContentPresenter
x:Name="HeaderContentPresenter"
Grid.Row="0"
Margin="{ThemeResource ToggleSwitchTopHeaderMargin}"
VerticalAlignment="Top"
x:DeferLoadStrategy="Lazy"
AutomationProperties.AccessibilityView="Raw"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
Foreground="{ThemeResource ToggleSwitchHeaderForeground}"
IsHitTestVisible="False"
TextWrapping="Wrap"
Visibility="Collapsed" />
<Grid
Grid.Row="1"
HorizontalAlignment="Right"
VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="12" MaxWidth="12" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid
x:Name="SwitchAreaGrid"
Grid.RowSpan="3"
Grid.ColumnSpan="3"
Margin="0,5"
Background="{ThemeResource ToggleSwitchContainerBackground}"
CornerRadius="{TemplateBinding CornerRadius}" />
<ContentPresenter
x:Name="OffContentPresenter"
Grid.RowSpan="3"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
AutomationProperties.AccessibilityView="Raw"
Content="{TemplateBinding OffContent}"
ContentTemplate="{TemplateBinding OffContentTemplate}"
Foreground="{TemplateBinding Foreground}"
IsHitTestVisible="False"
Opacity="0" />
<ContentPresenter
x:Name="OnContentPresenter"
Grid.RowSpan="3"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
AutomationProperties.AccessibilityView="Raw"
Content="{TemplateBinding OnContent}"
ContentTemplate="{TemplateBinding OnContentTemplate}"
Foreground="{TemplateBinding Foreground}"
IsHitTestVisible="False"
Opacity="0" />
<Rectangle
x:Name="OuterBorder"
Grid.Row="1"
Grid.Column="2"
Width="40"
Height="20"
Fill="{ThemeResource ToggleSwitchFillOff}"
RadiusX="10"
RadiusY="10"
Stroke="{ThemeResource ToggleSwitchStrokeOff}"
StrokeThickness="{ThemeResource ToggleSwitchOuterBorderStrokeThickness}" />
<Rectangle
x:Name="SwitchKnobBounds"
Grid.Row="1"
Grid.Column="2"
Width="40"
Height="20"
Fill="{ThemeResource ToggleSwitchFillOn}"
Opacity="0"
RadiusX="10"
RadiusY="10"
Stroke="{ThemeResource ToggleSwitchStrokeOn}"
StrokeThickness="{ThemeResource ToggleSwitchOnStrokeThickness}" />
<Grid
x:Name="SwitchKnob"
Grid.Row="1"
Grid.Column="2"
Width="20"
Height="20"
HorizontalAlignment="Left">
<Border
x:Name="SwitchKnobOn"
Width="12"
Height="12"
Margin="0,0,3,0"
HorizontalAlignment="Right"
Background="{ThemeResource ToggleSwitchKnobFillOn}"
BackgroundSizing="OuterBorderEdge"
BorderBrush="{ThemeResource ToggleSwitchKnobStrokeOn}"
CornerRadius="7"
Opacity="0"
RenderTransformOrigin="0.5, 0.5">
<Border.RenderTransform>
<CompositeTransform />
</Border.RenderTransform>
</Border>
<Rectangle
x:Name="SwitchKnobOff"
Width="12"
Height="12"
Margin="3,0,0,0"
HorizontalAlignment="Left"
Fill="{ThemeResource ToggleSwitchKnobFillOff}"
RadiusX="7"
RadiusY="7"
RenderTransformOrigin="0.5, 0.5">
<Rectangle.RenderTransform>
<CompositeTransform />
</Rectangle.RenderTransform>
</Rectangle>
<Grid.RenderTransform>
<TranslateTransform x:Name="KnobTranslateTransform" />
</Grid.RenderTransform>
</Grid>
<Thumb
x:Name="SwitchThumb"
Grid.RowSpan="3"
Grid.ColumnSpan="3"
AutomationProperties.AccessibilityView="Raw">
<Thumb.Template>
<ControlTemplate TargetType="Thumb">
<Rectangle Fill="Transparent" />
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchStrokeOff}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchFillOff}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOff}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOn}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchFillOn}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchStrokeOn}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchAreaGrid" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchContainerBackground}" />
</ObjectAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="12" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="12" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="12" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="12" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="PointerOver">
<Storyboard>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="(Shape.Stroke).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchStrokeOffPointerOver}" />
</ColorAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchFillOffPointerOver}" />
</ColorAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOffPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOnPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchFillOnPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchStrokeOnPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="SwitchAreaGrid" Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchContainerBackgroundPointerOver}" />
</ColorAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="14" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="14" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="14" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="14" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Target="SwitchKnobOn.HorizontalAlignment" Value="Right" />
<Setter Target="SwitchKnobOn.Margin" Value="0,0,3,0" />
<Setter Target="SwitchKnobOff.HorizontalAlignment" Value="Left" />
<Setter Target="SwitchKnobOff.Margin" Value="3,0,0,0" />
</VisualState.Setters>
<Storyboard>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="(Shape.Stroke).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchStrokeOffPressed}" />
</ColorAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchFillOffPressed}" />
</ColorAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchFillOnPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchStrokeOnPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOffPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOnPressed}" />
</ObjectAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="SwitchAreaGrid" Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchContainerBackgroundPressed}" />
</ColorAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="17" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="14" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="17" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlFasterAnimationDuration}"
Value="14" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HeaderContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchHeaderForegroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OffContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchContentForegroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OnContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchContentForegroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="(Shape.Stroke).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchStrokeOffDisabled}" />
</ColorAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchFillOffDisabled}" />
</ColorAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchFillOnDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Stroke">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchStrokeOnDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOffDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleSwitchKnobFillOnDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ColorAnimationUsingKeyFrames Storyboard.TargetName="SwitchAreaGrid" Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)">
<LinearColorKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="{ThemeResource ToggleSwitchContainerBackgroundDisabled}" />
</ColorAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlNormalAnimationDuration}"
Value="12" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOn"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlNormalAnimationDuration}"
Value="12" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Width">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlNormalAnimationDuration}"
Value="12" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames
EnableDependentAnimation="True"
Storyboard.TargetName="SwitchKnobOff"
Storyboard.TargetProperty="Height">
<SplineDoubleKeyFrame
KeySpline="{StaticResource ControlFastOutSlowInKeySpline}"
KeyTime="{StaticResource ControlNormalAnimationDuration}"
Value="12" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="ToggleStates">
<VisualStateGroup.Transitions>
<VisualTransition
x:Name="DraggingToOnTransition"
GeneratedDuration="0"
From="Dragging"
To="On">
<Storyboard>
<RepositionThemeAnimation FromHorizontalOffset="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.KnobCurrentToOnOffset}" TargetName="SwitchKnob" />
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition
x:Name="OnToDraggingTransition"
GeneratedDuration="0"
From="On"
To="Dragging">
<Storyboard>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="0" Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="0" Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="0" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition
x:Name="DraggingToOffTransition"
GeneratedDuration="0"
From="Dragging"
To="Off">
<Storyboard>
<RepositionThemeAnimation FromHorizontalOffset="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.KnobCurrentToOffOffset}" TargetName="SwitchKnob" />
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
<VisualTransition
x:Name="OnToOffTransition"
GeneratedDuration="0"
From="On"
To="Off">
<Storyboard>
<RepositionThemeAnimation FromHorizontalOffset="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.KnobOnToOffOffset}" TargetName="SwitchKnob" />
</Storyboard>
</VisualTransition>
<VisualTransition
x:Name="OffToOnTransition"
GeneratedDuration="0"
From="Off"
To="On">
<Storyboard>
<RepositionThemeAnimation FromHorizontalOffset="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.KnobOffToOnOffset}" TargetName="SwitchKnob" />
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="Dragging" />
<VisualState x:Name="Off" />
<VisualState x:Name="On">
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="KnobTranslateTransform"
Storyboard.TargetProperty="X"
To="20"
Duration="0" />
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobBounds" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="OuterBorder" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOn" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="1" />
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="SwitchKnobOff" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="{StaticResource ControlFasterAnimationDuration}" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="ContentStates">
<VisualState x:Name="OffContent">
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="OffContentPresenter"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0" />
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OffContentPresenter" Storyboard.TargetProperty="IsHitTestVisible">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<x:Boolean>True</x:Boolean>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="OnContent">
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="OnContentPresenter"
Storyboard.TargetProperty="Opacity"
To="1"
Duration="0" />
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="OnContentPresenter" Storyboard.TargetProperty="IsHitTestVisible">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<x:Boolean>True</x:Boolean>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default">
<SolidColorBrush
x:Key="CustomAccentBackgroundBrush"
Opacity="0.3"
Color="{ThemeResource SystemAccentColorLight2}" />
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<SolidColorBrush
x:Key="CustomAccentBackgroundBrush"
Opacity="0.2"
Color="{ThemeResource SystemAccentColorDark1}" />
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<SolidColorBrush x:Key="CustomAccentBackgroundBrush" Color="{ThemeResource SystemColorWindowColor}" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>

View File

@@ -538,7 +538,10 @@ namespace KeyboardEventHandlers
// Release original shortcut state (release in reverse order of shortcut to be accurate) // 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::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; it->second.isShortcutInvoked = true;
@@ -719,7 +722,8 @@ namespace KeyboardEventHandlers
else if (remapToText) else if (remapToText)
{ {
auto& remapping = std::get<std::wstring>(it->second.targetShortcut); auto& remapping = std::get<std::wstring>(it->second.targetShortcut);
Helpers::SetTextKeyEvents(keyEventList, remapping); Helpers::SendTextViaClipboard(remapping);
return 1;
} }
ii.SendVirtualInput(keyEventList); ii.SendVirtualInput(keyEventList);
@@ -1793,9 +1797,7 @@ namespace KeyboardEventHandlers
return 0; return 0;
} }
std::vector<INPUT> keyEventList; Helpers::SendTextViaClipboard(*remapping);
Helpers::SetTextKeyEvents(keyEventList, *remapping);
ii.SendVirtualInput(keyEventList);
return 1; return 1;
} }

View File

@@ -60,4 +60,85 @@ namespace RemappingLogicTests
Assert::AreEqual<unsigned int>(0, inputs[1].ki.wScan); Assert::AreEqual<unsigned int>(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<INPUT> inputs;
std::wstring text = L"abc";
Helpers::SetTextKeyEvents(inputs, text);
// 3 characters × 2 events (down+up) = 6 events
Assert::AreEqual<size_t>(6, inputs.size());
for (size_t i = 0; i < inputs.size(); i++)
{
Assert::AreEqual(true, bool(inputs[i].ki.dwFlags & KEYEVENTF_UNICODE));
}
Assert::AreEqual<unsigned short>(L'a', inputs[0].ki.wScan);
Assert::AreEqual<unsigned short>(L'b', inputs[2].ki.wScan);
Assert::AreEqual<unsigned short>(L'c', inputs[4].ki.wScan);
}
// Test that each character generates a keydown and keyup event pair
TEST_METHOD (SetTextKeyEvents_ShouldGenerateDownUpPairs_WhenTextHasMultipleChars)
{
std::vector<INPUT> inputs;
std::wstring text = L"xy";
Helpers::SetTextKeyEvents(inputs, text);
Assert::AreEqual<size_t>(4, inputs.size());
// First event: 'x' keydown (no KEYEVENTF_KEYUP flag)
Assert::AreEqual<unsigned short>(L'x', inputs[0].ki.wScan);
Assert::IsFalse(bool(inputs[0].ki.dwFlags & KEYEVENTF_KEYUP));
// Second event: 'x' keyup
Assert::AreEqual<unsigned short>(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<INPUT> 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<size_t>(8, inputs.size());
Assert::AreEqual<unsigned short>(L'a', inputs[0].ki.wScan);
Assert::AreEqual<unsigned short>(L'\r', inputs[2].ki.wScan);
Assert::AreEqual<unsigned short>(L'\n', inputs[4].ki.wScan);
Assert::AreEqual<unsigned short>(L'b', inputs[6].ki.wScan);
}
// Test empty string produces no events
TEST_METHOD (SetTextKeyEvents_ShouldProduceNoEvents_WhenTextIsEmpty)
{
std::vector<INPUT> inputs;
std::wstring text = L"";
Helpers::SetTextKeyEvents(inputs, text);
Assert::AreEqual<size_t>(0, inputs.size());
}
// Test that extraInfo flag is set correctly for KBM identification
TEST_METHOD (SetTextKeyEvents_ShouldSetExtraInfoFlag_WhenTextIsProvided)
{
std::vector<INPUT> inputs;
std::wstring text = L"a";
Helpers::SetTextKeyEvents(inputs, text);
Assert::AreEqual<size_t>(2, inputs.size());
Assert::AreEqual(KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, inputs[0].ki.dwExtraInfo);
Assert::AreEqual(KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG, inputs[1].ki.dwExtraInfo);
}
};
} }

View File

@@ -1,6 +1,8 @@
#include "pch.h" #include "pch.h"
#include "Helpers.h" #include "Helpers.h"
#include <sstream> #include <sstream>
#include <mutex>
#include <thread>
#include <common/interop/shared_constants.h> #include <common/interop/shared_constants.h>
#include <common/utils/process_path.h> #include <common/utils/process_path.h>
@@ -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<wchar_t*>(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<WORD>(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<WORD>(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<WORD>(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<WORD>(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<std::mutex> 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<wchar_t*>(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 // Function to filter the key codes for artificial key codes
int32_t FilterArtificialKeys(const int32_t& key) int32_t FilterArtificialKeys(const int32_t& key)
{ {
switch (key) switch (key)
{ {
// If a key is remapped to VK_WIN_BOTH, we send VK_LWIN instead
case CommonSharedConstants::VK_WIN_BOTH: case CommonSharedConstants::VK_WIN_BOTH:
return VK_LWIN; return VK_LWIN;
} }

View File

@@ -41,6 +41,9 @@ namespace Helpers
// Function to set key events for remapping text. // Function to set key events for remapping text.
void SetTextKeyEvents(std::vector<INPUT>& keyEventArray, const std::wstring& remapping); void SetTextKeyEvents(std::vector<INPUT>& 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 // Function to return window handle for a full screen UWP app
HWND GetFullscreenUWPWindowHandle(); HWND GetFullscreenUWPWindowHandle();

View File

@@ -39,7 +39,8 @@ public:
{ {
RemapShortcut = 0, RemapShortcut = 0,
RunProgram = 1, RunProgram = 1,
OpenURI = 2 OpenURI = 2,
RemapText = 3
}; };
enum StartWindowType enum StartWindowType
@@ -47,7 +48,7 @@ public:
Normal = 0, Normal = 0,
Hidden = 1, Hidden = 1,
Minimized = 2, Minimized = 2,
Maximized = 2 Maximized = 3
}; };
enum ProgramAlreadyRunningAction enum ProgramAlreadyRunningAction

View File

@@ -9,6 +9,8 @@
#include <shellapi.h> #include <shellapi.h>
#include <common/utils/logger_helper.h> #include <common/utils/logger_helper.h>
#include <common/interop/shared_constants.h> #include <common/interop/shared_constants.h>
#include <thread>
#include <atomic>
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/) 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_SHIFT[] = L"shift";
const wchar_t JSON_KEY_CODE[] = L"code"; const wchar_t JSON_KEY_CODE[] = L"code";
const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"ToggleShortcut"; 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. // Implement the PowerToy Module Interface and all the required methods.
@@ -56,11 +60,22 @@ private:
// Hotkey for toggling the module // Hotkey for toggling the module
Hotkey m_hotkey = { .key = 0 }; 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; ULONGLONG m_lastHotkeyToggleTime = 0;
HANDLE m_hProcess = nullptr; HANDLE m_hProcess = nullptr;
HANDLE m_hEditorProcess = nullptr;
HANDLE m_hTerminateEngineEvent = nullptr; HANDLE m_hTerminateEngineEvent = nullptr;
HANDLE m_open_new_editor_event_handle{ nullptr };
std::thread m_toggle_thread;
std::atomic<bool> m_toggle_thread_running{ false };
void refresh_process_state() void refresh_process_state()
{ {
@@ -174,6 +189,49 @@ private:
m_hotkey.alt = false; m_hotkey.alt = false;
m_hotkey.key = 'K'; 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<unsigned char>(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. // Load the settings file.
@@ -214,17 +272,30 @@ public:
} }
} }
m_open_new_editor_event_handle = CreateDefaultEvent(CommonSharedConstants::OPEN_NEW_KEYBOARD_MANAGER_EVENT);
init_settings(); init_settings();
}; };
~KeyboardManager() ~KeyboardManager()
{ {
StopOpenEditorListener();
stop_engine(); stop_engine();
if (m_hTerminateEngineEvent) if (m_hTerminateEngineEvent)
{ {
CloseHandle(m_hTerminateEngineEvent); CloseHandle(m_hTerminateEngineEvent);
m_hTerminateEngineEvent = nullptr; 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 // Destroy the powertoy and free memory
@@ -296,6 +367,7 @@ public:
// Log telemetry // Log telemetry
Trace::EnableKeyboardManager(true); Trace::EnableKeyboardManager(true);
start_engine(); start_engine();
StartOpenEditorListener();
} }
// Disable the powertoy // Disable the powertoy
@@ -304,6 +376,7 @@ public:
m_enabled = false; m_enabled = false;
// Log telemetry // Log telemetry
Trace::EnableKeyboardManager(false); Trace::EnableKeyboardManager(false);
StopOpenEditorListener();
stop_engine(); stop_engine();
} }
@@ -319,25 +392,148 @@ public:
return false; 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 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 (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<DWORD>(lParam) && IsWindowVisible(hwnd))
{
SetForegroundWindow(hwnd);
if (IsIconic(hwnd))
{
ShowWindow(hwnd, SW_RESTORE);
}
return FALSE; // Stop enumerating
}
return TRUE;
}, static_cast<LPARAM>(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 // Process the hotkey event
virtual bool on_hotkey(size_t /*hotkeyId*/) override virtual bool on_hotkey(size_t hotkeyId) override
{ {
if (!m_enabled) if (!m_enabled)
{ {
@@ -352,6 +548,9 @@ public:
} }
m_lastHotkeyToggleTime = now; m_lastHotkeyToggleTime = now;
if (hotkeyId == 0)
{
// Toggle engine on/off
refresh_process_state(); refresh_process_state();
if (m_active) if (m_active)
{ {
@@ -361,6 +560,12 @@ public:
{ {
start_engine(); start_engine();
} }
}
else if (hotkeyId == 1)
{
// Open the new editor (only in new editor mode)
launch_editor();
}
return true; return true;
} }

View File

@@ -20,3 +20,14 @@ void Trace::EnableKeyboardManager(const bool enabled) noexcept
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE), TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingBoolean(enabled, "Enabled")); 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"));
}

View File

@@ -7,4 +7,7 @@ class Trace : public telemetry::TraceBase
public: 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 // 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; static void EnableKeyboardManager(const bool enabled) noexcept;
// Log when the editor is launched
static void LaunchEditor(const bool viaHotkey) noexcept;
}; };

View File

@@ -6,6 +6,8 @@ using System.Threading;
using ManagedCommon; using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Controls; using Microsoft.PowerToys.Settings.UI.Controls;
using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events;
using Microsoft.PowerToys.Telemetry;
using PowerToys.Interop; using PowerToys.Interop;
namespace Microsoft.PowerToys.QuickAccess.Services namespace Microsoft.PowerToys.QuickAccess.Services
@@ -27,6 +29,12 @@ namespace Microsoft.PowerToys.QuickAccess.Services
if (moduleRun) if (moduleRun)
{ {
_coordinator?.OnModuleLaunched(moduleType); _coordinator?.OnModuleLaunched(moduleType);
// Send telemetry event for module launch from Quick Access
if (moduleType == ModuleType.KeyboardManager)
{
PowerToysTelemetry.Log.WriteEvent(new ModuleLaunchedFromSettingsEvent("KeyboardManagerWinUI"));
}
} }
_coordinator?.HideFlyout(); _coordinator?.HideFlyout();

View File

@@ -126,6 +126,13 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
eventHandle.Set(); eventHandle.Set();
} }
return true;
case ModuleType.KeyboardManager:
using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.OpenNewKeyboardManagerEvent()))
{
eventHandle.Set();
}
return true; return true;
default: default:
return false; return false;

View File

@@ -17,6 +17,10 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
public partial class QuickAccessViewModel : Observable public partial class QuickAccessViewModel : Observable
{ {
private readonly ISettingsRepository<GeneralSettings> _settingsRepository; private readonly ISettingsRepository<GeneralSettings> _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<KeyboardManagerSettings> _kbmSettingsRepository;
private readonly IQuickAccessLauncher _launcher; private readonly IQuickAccessLauncher _launcher;
private readonly Func<ModuleType, bool> _isModuleGpoDisabled; private readonly Func<ModuleType, bool> _isModuleGpoDisabled;
private readonly Func<ModuleType, bool> _isModuleGpoEnabled; private readonly Func<ModuleType, bool> _isModuleGpoEnabled;
@@ -44,6 +48,9 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
_generalSettings.AddEnabledModuleChangeNotification(ModuleEnabledChanged); _generalSettings.AddEnabledModuleChangeNotification(ModuleEnabledChanged);
_settingsRepository.SettingsChanged += OnSettingsChanged; _settingsRepository.SettingsChanged += OnSettingsChanged;
_kbmSettingsRepository = SettingsRepository<KeyboardManagerSettings>.GetInstance(SettingsUtils.Default);
_kbmSettingsRepository.SettingsChanged += OnKbmSettingsChanged;
InitializeItems(); InitializeItems();
} }
@@ -67,6 +74,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
AddFlyoutMenuItem(ModuleType.EnvironmentVariables); AddFlyoutMenuItem(ModuleType.EnvironmentVariables);
AddFlyoutMenuItem(ModuleType.FancyZones); AddFlyoutMenuItem(ModuleType.FancyZones);
AddFlyoutMenuItem(ModuleType.Hosts); AddFlyoutMenuItem(ModuleType.Hosts);
AddFlyoutMenuItem(ModuleType.KeyboardManager);
AddFlyoutMenuItem(ModuleType.LightSwitch); AddFlyoutMenuItem(ModuleType.LightSwitch);
// AddFlyoutMenuItem(ModuleType.PowerDisplay); // TEMPORARILY_DISABLED: PowerDisplay // 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)), Title = _resourceLoader.GetString(Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetModuleLabelResourceName(moduleType)),
Tag = moduleType, Tag = moduleType,
Visible = _isModuleGpoEnabled(moduleType) || Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetIsModuleEnabled(_generalSettings, moduleType), Visible = GetItemVisibility(moduleType),
Description = GetModuleToolTip(moduleType), Description = GetModuleToolTip(moduleType),
Icon = Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetModuleTypeFluentIconName(moduleType), Icon = Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetModuleTypeFluentIconName(moduleType),
Command = new RelayCommand(() => _launcher.Launch(moduleType)), Command = new RelayCommand(() => _launcher.Launch(moduleType)),
@@ -115,11 +123,38 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
{ {
if (item.Tag is ModuleType moduleType) 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) private string GetModuleToolTip(ModuleType moduleType)
{ {
return moduleType switch return moduleType switch
@@ -127,6 +162,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
ModuleType.ColorPicker => SettingsRepository<ColorPickerSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ActivationShortcut.ToString(), ModuleType.ColorPicker => SettingsRepository<ColorPickerSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ActivationShortcut.ToString(),
ModuleType.FancyZones => SettingsRepository<FancyZonesSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.FancyzonesEditorHotkey.Value.ToString(), ModuleType.FancyZones => SettingsRepository<FancyZonesSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.FancyzonesEditorHotkey.Value.ToString(),
ModuleType.PowerDisplay => SettingsRepository<PowerDisplaySettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ActivationShortcut.ToString(), ModuleType.PowerDisplay => SettingsRepository<PowerDisplaySettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ActivationShortcut.ToString(),
ModuleType.KeyboardManager => SettingsRepository<KeyboardManagerSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.DefaultEditorShortcut.ToString(),
ModuleType.LightSwitch => SettingsRepository<LightSwitchSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ToggleThemeHotkey.Value.ToString(), ModuleType.LightSwitch => SettingsRepository<LightSwitchSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ToggleThemeHotkey.Value.ToString(),
ModuleType.PowerLauncher => SettingsRepository<PowerLauncherSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.OpenPowerLauncher.ToString(), ModuleType.PowerLauncher => SettingsRepository<PowerLauncherSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.OpenPowerLauncher.ToString(),
ModuleType.PowerOCR => SettingsRepository<PowerOcrSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ActivationShortcut.ToString(), ModuleType.PowerOCR => SettingsRepository<PowerOcrSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ActivationShortcut.ToString(),

View File

@@ -23,15 +23,23 @@ namespace Microsoft.PowerToys.Settings.UI.Library
public HotkeySettings DefaultToggleShortcut => new HotkeySettings(true, false, false, true, 0x4B); public HotkeySettings DefaultToggleShortcut => new HotkeySettings(true, false, false, true, 0x4B);
public HotkeySettings DefaultEditorShortcut => new HotkeySettings(true, false, false, true, 0x51);
public KeyboardManagerProperties() public KeyboardManagerProperties()
{ {
ToggleShortcut = DefaultToggleShortcut; ToggleShortcut = DefaultToggleShortcut;
EditorShortcut = DefaultEditorShortcut;
KeyboardConfigurations = new GenericProperty<List<string>>(new List<string> { "default", }); KeyboardConfigurations = new GenericProperty<List<string>>(new List<string> { "default", });
ActiveConfiguration = new GenericProperty<string>("default"); ActiveConfiguration = new GenericProperty<string>("default");
} }
public HotkeySettings ToggleShortcut { get; set; } public HotkeySettings ToggleShortcut { get; set; }
public HotkeySettings EditorShortcut { get; set; }
[JsonPropertyName("useNewEditor")]
public bool UseNewEditor { get; set; }
public string ToJsonString() public string ToJsonString()
{ {
return JsonSerializer.Serialize(this); return JsonSerializer.Serialize(this);

View File

@@ -42,6 +42,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library
() => Properties.ToggleShortcut, () => Properties.ToggleShortcut,
value => Properties.ToggleShortcut = value ?? Properties.DefaultToggleShortcut, value => Properties.ToggleShortcut = value ?? Properties.DefaultToggleShortcut,
"Toggle_Shortcut"), "Toggle_Shortcut"),
new HotkeyAccessor(
() => Properties.EditorShortcut,
value => Properties.EditorShortcut = value ?? Properties.DefaultEditorShortcut,
"Editor_Shortcut"),
}; };
return hotkeyAccessors.ToArray(); return hotkeyAccessors.ToArray();

View File

@@ -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;
}
}
}

View File

@@ -97,7 +97,7 @@ namespace ViewModelTests
var expectedEntry = new AppSpecificKeysDataModel(); var expectedEntry = new AppSpecificKeysDataModel();
expectedEntry.OriginalKeys = entry.OriginalKeys; expectedEntry.OriginalKeys = entry.OriginalKeys;
expectedEntry.NewRemapKeys = entry.NewRemapKeys; expectedEntry.NewRemapKeys = entry.NewRemapKeys;
expectedEntry.TargetApp = "All Apps"; expectedEntry.TargetApp = "All apps";
expectedResult.Add(expectedEntry); expectedResult.Add(expectedEntry);
Assert.AreEqual(expectedResult.Count, result.Count); Assert.AreEqual(expectedResult.Count, result.Count);
@@ -123,7 +123,7 @@ namespace ViewModelTests
var expectedEntry = new AppSpecificKeysDataModel(); var expectedEntry = new AppSpecificKeysDataModel();
expectedEntry.OriginalKeys = entry.OriginalKeys; expectedEntry.OriginalKeys = entry.OriginalKeys;
expectedEntry.NewRemapKeys = entry.NewRemapKeys; expectedEntry.NewRemapKeys = entry.NewRemapKeys;
expectedEntry.TargetApp = "All Apps"; expectedEntry.TargetApp = "All apps";
expectedResult.Add(expectedEntry); expectedResult.Add(expectedEntry);
var x = expectedResult[0].Equals(result[0]); var x = expectedResult[0].Equals(result[0]);
Assert.AreEqual(expectedResult.Count, result.Count); Assert.AreEqual(expectedResult.Count, result.Count);
@@ -181,7 +181,7 @@ namespace ViewModelTests
var expectedFirstEntry = new AppSpecificKeysDataModel(); var expectedFirstEntry = new AppSpecificKeysDataModel();
expectedFirstEntry.OriginalKeys = firstListEntry.OriginalKeys; expectedFirstEntry.OriginalKeys = firstListEntry.OriginalKeys;
expectedFirstEntry.NewRemapKeys = firstListEntry.NewRemapKeys; expectedFirstEntry.NewRemapKeys = firstListEntry.NewRemapKeys;
expectedFirstEntry.TargetApp = "All Apps"; expectedFirstEntry.TargetApp = "All apps";
expectedResult.Add(expectedFirstEntry); expectedResult.Add(expectedFirstEntry);
var expectedSecondEntry = new AppSpecificKeysDataModel(); var expectedSecondEntry = new AppSpecificKeysDataModel();
expectedSecondEntry.OriginalKeys = secondListEntry.OriginalKeys; expectedSecondEntry.OriginalKeys = secondListEntry.OriginalKeys;

View File

@@ -15,6 +15,27 @@
mc:Ignorable="d"> mc:Ignorable="d">
<local:NavigablePage.Resources> <local:NavigablePage.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default">
<LinearGradientBrush x:Key="CardGradient2Brush" StartPoint="0,0" EndPoint="0.5, 1">
<GradientStop Offset="0" Color="#38C8AEC4" />
<GradientStop Offset="1" Color="#383286EE" />
</LinearGradientBrush>
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<LinearGradientBrush x:Key="CardGradient2Brush" StartPoint="0,0" EndPoint="1, 1">
<GradientStop Offset="0.0" Color="#FFF6F9FF" />
<GradientStop Offset="0.4" Color="#FFEFF5FF" />
<GradientStop Offset="0.7" Color="#FFF7FAFD" />
<GradientStop Offset="1.0" Color="#FFF5F8FA" />
</LinearGradientBrush>
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<SolidColorBrush x:Key="CardGradient2Brush" Color="Transparent" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<tkconverters:CollectionVisibilityConverter x:Key="CollectionVisibilityConverter" /> <tkconverters:CollectionVisibilityConverter x:Key="CollectionVisibilityConverter" />
<Style x:Name="KeysListViewContainerStyle" TargetType="ListViewItem"> <Style x:Name="KeysListViewContainerStyle" TargetType="ListViewItem">
<Setter Property="IsTabStop" Value="False" /> <Setter Property="IsTabStop" Value="False" />
@@ -34,50 +55,49 @@
CornerRadius="{StaticResource ControlCornerRadius}" CornerRadius="{StaticResource ControlCornerRadius}"
Style="{StaticResource AccentKeyVisualStyle}" /> Style="{StaticResource AccentKeyVisualStyle}" />
</DataTemplate> </DataTemplate>
</ResourceDictionary>
<!--<DataTemplate x:Name="KeysListViewTemplate" x:DataType="Lib:KeysDataModel">
<StackPanel
Name="KeyboardManager_RemappedKeysListItem"
x:Uid="KeyboardManager_RemappedKeysListItem"
Orientation="Horizontal"
Height="56">
</StackPanel>
</DataTemplate>-->
<!--<DataTemplate x:Name="ShortcutKeysListViewTemplate" x:DataType="Lib:AppSpecificKeysDataModel">
<StackPanel
Name="KeyboardManager_RemappedShortcutsListItem"
x:Uid="KeyboardManager_RemappedShortcutsListItem"
Orientation="Horizontal"
Height="56">
</DataTemplate>-->
</local:NavigablePage.Resources> </local:NavigablePage.Resources>
<controls:SettingsPageControl x:Uid="KeyboardManager" ModuleImageSource="ms-appx:///Assets/Settings/Modules/KBM.png"> <controls:SettingsPageControl x:Uid="KeyboardManager" ModuleImageSource="ms-appx:///Assets/Settings/Modules/KBM.png">
<controls:SettingsPageControl.ModuleContent> <controls:SettingsPageControl.ModuleContent>
<StackPanel ChildrenTransitions="{StaticResource SettingsCardsAnimations}" Orientation="Vertical"> <StackPanel ChildrenTransitions="{StaticResource SettingsCardsAnimations}" Orientation="Vertical">
<controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsEnabledGpoConfigured, Mode=OneWay}"> <controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsEnabledGpoConfigured, Mode=OneWay}">
<tkcontrols:SettingsCard <tkcontrols:SettingsExpander
Name="KeyboardManagerEnableToggle" Name="KeyboardManagerEnableToggle"
x:Uid="KeyboardManager_EnableToggle" x:Uid="KeyboardManager_EnableToggle"
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/KeyboardManager.png}"> HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/KeyboardManager.png}">
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.Enabled, Mode=TwoWay}" /> <ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.Enabled, Mode=TwoWay}" />
<tkcontrols:SettingsCard.Description> <tkcontrols:SettingsExpander.Description>
<HyperlinkButton NavigateUri="https://aka.ms/powerToysCannotRemapKeys"> <HyperlinkButton NavigateUri="https://aka.ms/powerToysCannotRemapKeys">
<TextBlock x:Uid="KBM_KeysCannotBeRemapped" FontWeight="SemiBold" /> <TextBlock x:Uid="KBM_KeysCannotBeRemapped" FontWeight="SemiBold" />
</HyperlinkButton> </HyperlinkButton>
</tkcontrols:SettingsCard.Description> </tkcontrols:SettingsExpander.Description>
</tkcontrols:SettingsCard> <tkcontrols:SettingsExpander.Items>
</controls:GPOInfoControl>
<tkcontrols:SettingsCard <tkcontrols:SettingsCard
Name="ToggleShortcut" Name="ToggleShortcut"
x:Uid="KeyboardManager_Toggle_Shortcut" x:Uid="KeyboardManager_Toggle_Shortcut"
HeaderIcon="{ui:FontIcon Glyph=&#xEDA7;}"> IsEnabled="{x:Bind ViewModel.Enabled, Mode=OneWay}">
<controls:ShortcutControl MinWidth="{StaticResource SettingActionControlMinWidth}" HotkeySettings="{x:Bind Path=ViewModel.ToggleShortcut, Mode=TwoWay}" /> <controls:ShortcutControl
MinWidth="{StaticResource SettingActionControlMinWidth}"
AllowDisable="True"
HotkeySettings="{x:Bind Path=ViewModel.ToggleShortcut, Mode=TwoWay}" />
</tkcontrols:SettingsCard> </tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
</controls:GPOInfoControl>
<tkcontrols:SwitchPresenter TargetType="x:Boolean" Value="{x:Bind ViewModel.UseNewEditor, Mode=OneWay}">
<tkcontrols:Case Value="False">
<StackPanel Orientation="Vertical">
<tkcontrols:SettingsCard
x:Uid="KeyboardManager_TryNewExperience"
MinHeight="64"
Margin="0,0,0,24"
Background="{ThemeResource CardGradient2Brush}"
HeaderIcon="{ui:FontIcon Glyph=&#xF133;}"
IsEnabled="{x:Bind ViewModel.Enabled, Mode=OneWay}">
<ToggleSwitch IsOn="{x:Bind ViewModel.UseNewEditor, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<controls:SettingsGroup x:Uid="KeyboardManager_Keys" IsEnabled="{x:Bind ViewModel.Enabled, Mode=OneWay}"> <controls:SettingsGroup x:Uid="KeyboardManager_Keys" IsEnabled="{x:Bind ViewModel.Enabled, Mode=OneWay}">
<tkcontrols:SettingsCard <tkcontrols:SettingsCard
Name="KeyboardManagerRemapKeyboardButton" Name="KeyboardManagerRemapKeyboardButton"
@@ -229,6 +249,43 @@
</ListView> </ListView>
</controls:SettingsGroup> </controls:SettingsGroup>
</StackPanel> </StackPanel>
</tkcontrols:Case>
<tkcontrols:Case Value="True">
<controls:SettingsGroup x:Uid="KeyboardManager_Editor" IsEnabled="{x:Bind ViewModel.Enabled, Mode=OneWay}">
<StackPanel Orientation="Vertical" Spacing="16">
<tkcontrols:SettingsExpander
x:Uid="KeyboardManager_OpenNewEditor"
HeaderIcon="{ui:FontIcon Glyph=&#xEB3C;}"
IsEnabled="{x:Bind ViewModel.Enabled, Mode=OneWay}"
IsExpanded="True">
<Button
x:Uid="KeyboardManager_LaunchEditor_Button"
Command="{Binding Path=OpenNewEditorCommand}"
Style="{StaticResource AccentButtonStyle}" />
<tkcontrols:SettingsExpander.Items>
<!-- HACK: ShortcutControl does not work correctly if it's the first or last item in the expander, so we add invisible cards. -->
<tkcontrols:SettingsCard Visibility="Collapsed" />
<tkcontrols:SettingsCard Name="KBMEditorOpenShortcut" x:Uid="KeyboardManager_Editor_Open_Shortcut">
<controls:ShortcutControl
MinWidth="{StaticResource SettingActionControlMinWidth}"
AllowDisable="True"
HotkeySettings="{x:Bind Path=ViewModel.EditorShortcut, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Visibility="Collapsed" />
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
<HyperlinkButton
x:Uid="KeyboardManager_GoBackClassic"
Padding="0"
Click="GoBackClassic_Click"
FontSize="12"
FontWeight="SemiBold" />
</StackPanel>
</controls:SettingsGroup>
</tkcontrols:Case>
</tkcontrols:SwitchPresenter>
</StackPanel>
</controls:SettingsPageControl.ModuleContent> </controls:SettingsPageControl.ModuleContent>
<controls:SettingsPageControl.PrimaryLinks> <controls:SettingsPageControl.PrimaryLinks>
<controls:PageLink x:Uid="LearnMore_KBM" Link="https://aka.ms/PowerToysOverview_KeyboardManager" /> <controls:PageLink x:Uid="LearnMore_KBM" Link="https://aka.ms/PowerToysOverview_KeyboardManager" />

View File

@@ -91,5 +91,10 @@ namespace Microsoft.PowerToys.Settings.UI.Views
{ {
ViewModel.RefreshEnabledState(); ViewModel.RefreshEnabledState();
} }
private void GoBackClassic_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
ViewModel.UseNewEditor = false;
}
} }
} }

View File

@@ -557,6 +557,10 @@ opera.exe</value>
<data name="KeyboardManager_RemapKeyboardButton.Header" xml:space="preserve"> <data name="KeyboardManager_RemapKeyboardButton.Header" xml:space="preserve">
<value>Remap a key</value> <value>Remap a key</value>
<comment>Keyboard Manager remap keyboard button content</comment> <comment>Keyboard Manager remap keyboard button content</comment>
</data>
<data name="KeyboardManager_Editor.Header" xml:space="preserve">
<value>Editor</value>
<comment>Keyboard Manager new editor header</comment>
</data> </data>
<data name="KeyboardManager_Keys.Header" xml:space="preserve"> <data name="KeyboardManager_Keys.Header" xml:space="preserve">
<value>Keys</value> <value>Keys</value>
@@ -571,9 +575,29 @@ opera.exe</value>
<comment>Keyboard Manager remap keyboard header</comment> <comment>Keyboard Manager remap keyboard header</comment>
</data> </data>
<data name="KeyboardManager_All_Apps_Description" xml:space="preserve"> <data name="KeyboardManager_All_Apps_Description" xml:space="preserve">
<value>All Apps</value> <value>All apps</value>
<comment>Should be the same as EditShortcuts_AllApps from keyboard manager editor</comment> <comment>Should be the same as EditShortcuts_AllApps from keyboard manager editor</comment>
</data> </data>
<data name="KeyboardManager_TryNewExperience.Header" xml:space="preserve">
<value>Try the new editor</value>
<comment>Keyboard Manager toggle to switch to the new unified editor</comment>
</data>
<data name="KeyboardManager_TryNewExperience.Description" xml:space="preserve">
<value>Switch to the new editor. You can switch back at any time.</value>
<comment>Description for the new experience toggle</comment>
</data>
<data name="KeyboardManager_OpenNewEditor.Header" xml:space="preserve">
<value>Editor</value>
<comment>Keyboard Manager button to open the new unified editor</comment>
</data>
<data name="KeyboardManager_OpenNewEditor.Description" xml:space="preserve">
<value>Set and manage your remappings</value>
<comment>Description for the new editor button</comment>
</data>
<data name="KeyboardManager_GoBackClassic.Content" xml:space="preserve">
<value>Switch back to the classic editor</value>
<comment>Keyboard Manager link to switch back to the classic editor UI</comment>
</data>
<data name="Shortcut.Header" xml:space="preserve"> <data name="Shortcut.Header" xml:space="preserve">
<value>Shortcut</value> <value>Shortcut</value>
</data> </data>
@@ -1883,10 +1907,22 @@ Made with 💗 by Microsoft and the PowerToys community.</value>
<value>Customize the shortcut to activate this module</value> <value>Customize the shortcut to activate this module</value>
</data> </data>
<data name="KeyboardManager_Toggle_Shortcut.Header" xml:space="preserve"> <data name="KeyboardManager_Toggle_Shortcut.Header" xml:space="preserve">
<value>Toggle shortcut</value> <value>Shortcut</value>
</data> </data>
<data name="KeyboardManager_Toggle_Shortcut.Description" xml:space="preserve"> <data name="KeyboardManager_Toggle_Shortcut.Description" xml:space="preserve">
<value>Use a shortcut to toggle this module on or off (note that the Settings UI will not update)</value> <value>Enable or disable this module (Note: the Settings UI will not update)</value>
</data>
<data name="KeyboardManager_Editor_Shortcut.Header" xml:space="preserve">
<value>Editor shortcut</value>
</data>
<data name="KeyboardManager_Editor_Open_Shortcut.Header" xml:space="preserve">
<value>Open editor shortcut</value>
</data>
<data name="KeyboardManager_Editor_Open_Shortcut.Description" xml:space="preserve">
<value>Customize the shortcut to open the Keyboard Manager editor</value>
</data>
<data name="KeyboardManager_LaunchEditor_Button.Content" xml:space="preserve">
<value>Open editor</value>
</data> </data>
<data name="PasteAsPlainText_Shortcut.Header" xml:space="preserve"> <data name="PasteAsPlainText_Shortcut.Header" xml:space="preserve">
<value>Paste as plain text directly</value> <value>Paste as plain text directly</value>

View File

@@ -12,16 +12,16 @@ using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Input; using System.Windows.Input;
using global::PowerToys.GPOWrapper; using global::PowerToys.GPOWrapper;
using ManagedCommon; using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; 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.Library.ViewModels.Commands;
using Microsoft.PowerToys.Settings.UI.SerializationContext; using Microsoft.PowerToys.Settings.UI.SerializationContext;
using Microsoft.PowerToys.Settings.Utilities; using Microsoft.PowerToys.Settings.Utilities;
using Microsoft.Win32; using Microsoft.PowerToys.Telemetry;
namespace Microsoft.PowerToys.Settings.UI.ViewModels 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. // Default editor path. Can be removed once the new WinUI3 editor is released.
private const string KeyboardManagerEditorPath = "KeyboardManagerEditor\\PowerToys.KeyboardManagerEditor.exe"; 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 = "WinUI3Apps\\PowerToys.KeyboardManagerEditorUI.exe";
private const string KeyboardManagerEditorUIPath = "KeyboardManagerEditorUI\\PowerToys.KeyboardManagerEditorUI.exe";
private Process editor; private Process editor;
@@ -57,6 +56,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private ICommand _remapKeyboardCommand; private ICommand _remapKeyboardCommand;
private ICommand _editShortcutCommand; private ICommand _editShortcutCommand;
private ICommand _openNewEditorCommand;
private KeyboardManagerProfile _profile; private KeyboardManagerProfile _profile;
private Func<string, int> SendConfigMSG { get; } private Func<string, int> SendConfigMSG { get; }
@@ -181,7 +181,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{ {
var hotkeysDict = new Dictionary<string, HotkeySettings[]> var hotkeysDict = new Dictionary<string, HotkeySettings[]>
{ {
[ModuleName] = [ToggleShortcut], [ModuleName] = [ToggleShortcut, EditorShortcut],
}; };
return hotkeysDict; return hotkeysDict;
@@ -192,11 +192,55 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
get => Settings.Properties.ToggleShortcut; get => Settings.Properties.ToggleShortcut;
set 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)); OnPropertyChanged(nameof(ToggleShortcut));
NotifySettingsChanged(); 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 EditShortcutCommand => _editShortcutCommand ?? (_editShortcutCommand = new RelayCommand(OnEditShortcut));
public ICommand OpenNewEditorCommand => _openNewEditorCommand ?? (_openNewEditorCommand = new RelayCommand(OnOpenNewEditor));
public void OnRemapKeyboard() public void OnRemapKeyboard()
{ {
OpenEditor((int)KeyboardManagerEditorType.KeyEditor); OpenEditor((int)KeyboardManagerEditorType.KeyEditor);
@@ -272,6 +318,11 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
OpenEditor((int)KeyboardManagerEditorType.ShortcutEditor); OpenEditor((int)KeyboardManagerEditorType.ShortcutEditor);
} }
public void OnOpenNewEditor()
{
OpenNewEditor();
}
private static void BringProcessToFront(Process process) private static void BringProcessToFront(Process process)
{ {
if (process == null) if (process == null)
@@ -305,41 +356,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
return; return;
} }
// Launch the new editor if: string path = Path.Combine(Environment.CurrentDirectory, KeyboardManagerEditorPath);
// 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<GeneralSettings>.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);
Logger.LogInfo($"Starting {ModuleName} editor from {path}"); Logger.LogInfo($"Starting {ModuleName} editor from {path}");
// InvariantCulture: type represents the KeyboardManagerEditorType enum value // 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) 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() public void NotifyFileChanged()
{ {
OnPropertyChanged(nameof(RemapKeys)); OnPropertyChanged(nameof(RemapKeys));