Compare commits

..

18 Commits

Author SHA1 Message Date
Leilei Zhang
8c42dbc11b Refine the CmdPal launch and activation logic when opening a new app instance 2025-05-12 22:03:41 +08:00
Mike Griese
2d0d12f06c Bump toolkit version to 0.2.0 in template (#39292)
This bumps the version of the toolkit consumed by the template to 0.2.0

~Ironically, I have not yet published 0.2. I'm spinning that CI build currently. But I'll have that uploaded tomorrow morning at the latest~

EDIT: package is uploaded now
2025-05-07 16:52:26 -05:00
Mike Griese
06e5db6ff0 CmdPal: Actually observe Details (#39263)
Extensions can change the properties on their Details, and they should
be observable, but they weren't. This is because the ShellPage is
ultimately responsible for exposing the details, but it doesn't own the
details. The selected ListItemViewModel from the ListPage does.

This PR just adds a event handler on ListViewModel. We'll attach/detach
that handler to ListItemViewModels as the selection changes. In the body
of that handler, we'll let the ShellPage know when the details object
changes (by sending ShowDetails/HideDetails messages).

Closes #39216
2025-05-07 17:47:57 +08:00
Niels Laute
1a097ae09c [CmdPal] Open CmdPal settings from PT settings (#39262)
* Turning description into a hyperlink

* Update expect.txt

* Update CmdPalPage.xaml.cs

* update xaml format.

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>

* update.

Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>

* Added logger

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>

* update

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>

---------

Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>
Signed-off-by: Shawn Yuan <shuaiyuan@microsoft.com>
Co-authored-by: Shawn Yuan <shuai.yuan.zju@gmail.com>
Co-authored-by: Shawn Yuan <shuaiyuan@microsoft.com>
2025-05-07 16:34:03 +08:00
Mike Griese
980ca46cdb CmdPal: URI activate, rather than using shell:AppsFolder (#39269)
* Use a URI handler for launching

this is a test

* I think we can review this

* Add a settings URI too
2025-05-07 11:30:47 +08:00
Kai Tao
6a1999d601 [cmdpal] protect cmdpal from crash if adaptive card fails (#39264)
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? -->
## Summary of the Pull Request
     
Card = AdaptiveCard.FromJsonString(cardJson) is called in catch block, if it fails, app will crash.
2025-05-06 17:07:32 -05:00
Mike Griese
5655c61794 Lazy-fetch the settings for extensions (#38844)
This PR stops us from synchronously initializing the settings page for every extension (including built-in's) on startup. That incurs a small penalty that really adds up the more extensions a user has.

Instead, we'll now only initialize the `CommandSettings` object when we first actually need it. 

From a relatively unscientific test, this saves approximately 10% on the initialization of builtin commands, and for my setup, it trims about 28% off extension initialization (across all built-in's / extensions):

branch | Built-in load (ms) | Extension load (ms) | %Δ builtin | %Δ extensions | 
-- | -- | -- | -- | -- |
main | 1455 | 6867.6 | | |
this PR | 1309.2 | 4919 | -10.02% | -28.37%

Closes #38321
2025-05-06 04:58:44 -05:00
Mike Griese
371b7f0868 CmdPal: Cloak the window instead of hiding it (#39170)
This avoids the few frames of "flicker in" that XAML does when the window is SW_SHOW'n

closes #38384
closes #38404
closes #38438
2025-05-05 13:34:20 -05:00
Niels Laute
b6bcc92eb4 [CmdPal] MinWidth/Height and DPI-aware launch dimensions (#38637)
* MinWidth/Height and DPI-aware launch dimensions

* Making MainWindow DPI aware too

* Moving toastwindow to WinUIEx too

* Update MainWindow.xaml.cs

* Reverting back to the working logic

* Localizing settings window title

* Xaml formatting

* Update SettingsWindow.xaml.cs
2025-05-05 18:44:28 +02:00
Davide Giacometti
2ac464279a [CmdPal] Prevent maximizing (#39220)
Prevent CmdPal window from maximizing when user double-click the title bar area.

 Closes: #39096
2025-05-04 06:04:29 -05:00
Mike Griese
8ce198a47b cmdpal: fix a leak in the extension template (#39209)
There's apparently a footgun with the way we're using ComServer, which
results in us leaking the extension processes when we think we've
disposed them

The fix unfortunately has to be on the extension side. Extensions
published prior to 0.2 will need to manually fix this.

closes: #39045
2025-05-04 06:04:09 -05:00
Mike Griese
15ef9189ba cmdpal: unset the command if we don't find a command (#39208)
On a reload, the system commands fallback would leave the "restart"
fallback behind, for the same reason as what we found around
e40372c & ef264d9 in #38455
2025-05-04 06:03:01 -05:00
Mike Griese
2c555e2c2b Dismiss the details pane when the list gets emptied (#39206)
I cannot find an issue for this. I swear I filed it somewhere.

If you open winget, search for "terminal", wait till it loads, then
hit `esc`, we'll clear the search and empty the list, but never actually
hide the details pane. That looks weird.

This fixes that.

Closes _nothing i guess_.
2025-05-04 06:02:38 -05:00
Jerry Xu
83817700e1 [Infra-BuildScript] Add PowerShell Script to Enforce Shared Common.Dotnet.CsWinRT.props in all CSharp Projects under Src Sub-Folder (#37811)
* Add Powershell script to validate whether CSharp project correctly import shared props, update pipeline to enforce such validation, and fixed all projects that didn't import this shared props correctly

* add common props for fuzz test project

* update the path

* Only scans projects in src sub-folder

* Update .pipelines/verifyCommonProps.ps1

* Update csproj to include Common.Dotnet.CsWinRT.props

* Fix indentation in RegistryPreview.FuzzTests.csproj

* exclude TemplateCmdPalExtension.csproj in validation process

* exclude TemplateCmdPalExtension.csproj in validation process

---------

Co-authored-by: Leilei Zhang <leilzh@microsoft.com>
Co-authored-by: Jerry Xu <nxu@microsoft.com>
2025-05-02 20:38:11 -07:00
Mike Griese
7e92a9a5e9 CmdPal: Start extensions in parallel (#39203)
This reduces our extension startup time by approximately 70% on my
machine (I have 17 extensions). I'd guess the gains scale with the
number of extensions. That's 8s -> 3s on average, and now I also get 2.5s reloads.

This retains the order of the list of extensions, by only starting the
processes in parallel. Once we have all the command provider instances,
then actually retrieving the commands.

It also adds a timeout on startup & load, so that one misbehaving extension won't block everyone else.

closes: #38529
2025-05-02 19:43:31 -05:00
Niels Laute
fe067def65 Fix for missing CmdPal extensions docs (#39173) 2025-05-01 18:33:01 +00:00
Yu Leng
6b9c99c2f6 [cmdpal][AOT] clean up some AOT related issue (#39163)
Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
2025-04-30 22:53:06 +08:00
Yu Leng
ba6af794ac [cmdpal] Support search any file in fallback command (#38455)
## Summary of the Pull Request
1. Add new setting to control this behaviour
2. Support always on and only when file exist in the fallback command
3. Change the condition of updateFallbackCommand in toplevelvm

demo:

https://github.com/user-attachments/assets/19e4ced3-30ad-44f4-8f3a-93620f46bb3d


## PR Checklist

- [x] **Closes:** #38370

---------

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
Co-authored-by: Mike Griese <migrie@microsoft.com>
2025-04-30 06:30:23 -05:00
98 changed files with 1035 additions and 5844 deletions

View File

@@ -315,6 +315,7 @@ debugbreak
declatory
decryptor
Dedup
Deeplink
DEFAULTBOOTSTRAPPERINSTALLFOLDER
DEFAULTCOLOR
DEFAULTFLAGS

View File

@@ -127,7 +127,6 @@
"PowerToys.KeyboardManager.dll",
"KeyboardManagerEditor\\PowerToys.KeyboardManagerEditor.exe",
"KeyboardManagerEditorUI\\PowerToys.KeyboardManagerEditorUI.exe",
"KeyboardManagerEngine\\PowerToys.KeyboardManagerEngine.exe",
"PowerToys.KeyboardManagerEditorLibraryWrapper.dll",

View File

@@ -344,6 +344,11 @@ jobs:
flattenFolders: True
OverWrite: True
# Check if all projects (located in src sub-folder) import common props
- pwsh: |-
& '.pipelines/verifyCommonProps.ps1' -sourceDir '$(build.sourcesdirectory)\src'
displayName: Audit shared common props for CSharp projects in src sub-folder
# Check if deps.json files don't reference different dll versions.
- pwsh: |-
& '.pipelines/verifyDepsJsonLibraryVersions.ps1' -targetDir '$(build.sourcesdirectory)\$(BuildPlatform)\$(BuildConfiguration)'

View File

@@ -0,0 +1,54 @@
[CmdletBinding()]
Param(
[Parameter(Mandatory = $True, Position = 1)]
[string]$sourceDir
)
# scan all csharp project in the source directory
function Get-CSharpProjects {
param (
[string]$path
)
# Get all .csproj files under the specified path
return Get-ChildItem -Path $path -Recurse -Filter *.csproj | Select-Object -ExpandProperty FullName
}
# Check if the project file imports 'Common.Dotnet.CsWinRT.props'
function Test-ImportSharedCsWinRTProps {
param (
[string]$filePath
)
# Load the XML content of the .csproj file
[xml]$csprojContent = Get-Content -Path $filePath
# Check if the Import element with Project attribute containing 'Common.Dotnet.CsWinRT.props' exists
return $csprojContent.Project.Import | Where-Object { $null -ne $_.Project -and $_.Project.EndsWith('Common.Dotnet.CsWinRT.props') }
}
# Call the function with the provided source directory
$csprojFilesArray = Get-CSharpProjects -path $sourceDir
$hasInvalidCsProj = $false
# Enumerate the array of file paths and call Validate-ImportSharedCsWinRTProps for each file
foreach ($csprojFile in $csprojFilesArray) {
# Skip if the file ends with 'TemplateCmdPalExtension.csproj'
if ($csprojFile -like '*TemplateCmdPalExtension.csproj') {
continue
}
$importExists = Test-ImportSharedCsWinRTProps -filePath $csprojFile
if (!$importExists) {
Write-Output "$csprojFile need to import 'Common.Dotnet.CsWinRT.props'."
$hasInvalidCsProj = $true
}
}
if ($hasInvalidCsProj) {
exit 1
}
exit 0

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Some items may be set in Directory.Build.props in root -->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- OneFuzz does not currently support testing with .NET 9.
As a temporary workaround, create a .NET 8 project and use file links
to include the code that needs testing. -->
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
</PropertyGroup>
</Project>

View File

@@ -1,6 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.Dotnet.FuzzTest.props" />
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

View File

@@ -1,6 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.Dotnet.FuzzTest.props" />
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

View File

@@ -217,10 +217,9 @@ public:
CmdPal::m_enabled.store(true);
std::wstring packageName = L"Microsoft.CommandPalette";
std::wstring launchPath = L"shell:AppsFolder\\Microsoft.CommandPalette_8wekyb3d8bbwe!App";
std::wstring launchPath = L"x-cmdpal://background";
#ifdef IS_DEV_BRANDING
packageName = L"Microsoft.CommandPalette.Dev";
launchPath = L"shell:AppsFolder\\Microsoft.CommandPalette.Dev_8wekyb3d8bbwe!App";
#endif
if (!package::GetRegisteredPackage(packageName, false).has_value())
@@ -269,7 +268,7 @@ public:
if (!firstEnableCall)
{
Logger::trace("Not first attempt, try to launch");
LaunchApp(launchPath, L"RunFromPT", false /*no elevated*/, false /*error pop up*/);
LaunchApp(launchPath, L"", false /*no elevated*/, false /*error pop up*/);
}
else
{
@@ -297,7 +296,7 @@ public:
int retry = 0;
do
{
auto launch_result = LaunchApp(path, L"RunFromPT", false, retry < max_retry);
auto launch_result = LaunchApp(path, L"", false, retry < max_retry);
if (launch_result)
{
Logger::info(L"CmdPal launched successfully after {} retries.", retry);

View File

@@ -3,7 +3,7 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.1.0" />
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.2.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0-preview.24508.2" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.2903.40" />
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.2.46-beta" />

View File

@@ -14,11 +14,12 @@ namespace TemplateCmdPalExtension;
public class Program
{
[MTAThread]
public static async Task Main(string[] args)
public static void Main(string[] args)
{
if (args.Length > 0 && args[0] == "-RegisterProcessAsComServer")
{
await using global::Shmuelie.WinRTServer.ComServer server = new();
global::Shmuelie.WinRTServer.ComServer server = new();
ManualResetEvent extensionDisposedEvent = new(false);
// We are instantiating an extension instance once above, and returning it every time the callback in RegisterExtension below is called.
@@ -31,6 +32,8 @@ public class Program
// This will make the main thread wait until the event is signalled by the extension class.
// Since we have single instance of the extension object, we exit as soon as it is disposed.
extensionDisposedEvent.WaitOne();
server.Stop();
server.UnsafeDispose();
}
else
{

View File

@@ -190,7 +190,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
contextItem.SlowInitializeProperties();
});
if (!string.IsNullOrEmpty(model.Command.Name))
if (!string.IsNullOrEmpty(model.Command?.Name))
{
_defaultCommandContextItem = new(new CommandContextItem(model.Command!), PageContext)
{

View File

@@ -66,8 +66,10 @@ public sealed class CommandProviderWrapper
DisplayName = provider.DisplayName;
Icon = new(provider.Icon);
Icon.InitializeProperties();
// Note: explicitly not InitializeProperties()ing the settings here. If
// we do that, then we'd regress GH #38321
Settings = new(provider.Settings, this, _taskScheduler);
Settings.InitializeProperties();
Logger.LogDebug($"Initialized command provider {ProviderId}");
}
@@ -151,9 +153,11 @@ public sealed class CommandProviderWrapper
Icon = new(model.Icon);
Icon.InitializeProperties();
// Note: explicitly not InitializeProperties()ing the settings here. If
// we do that, then we'd regress GH #38321
Settings = new(model.Settings, this, _taskScheduler);
Settings.InitializeProperties();
// We do need to explicitly initialize commands though
InitializeCommands(commands, fallbacks, serviceProvider, pageContext);
Logger.LogDebug($"Loaded commands from {DisplayName} ({ProviderId})");
@@ -194,21 +198,6 @@ public sealed class CommandProviderWrapper
}
}
/* This is a View/ExtensionHost piece
* public void AllowSetForeground(bool allow)
{
if (!IsExtension)
{
return;
}
var iextn = extensionWrapper?.GetExtensionObject();
unsafe
{
PInvoke.CoAllowSetForegroundWindow(iextn);
}
}*/
public override bool Equals(object? obj) => obj is CommandProviderWrapper wrapper && isValid == wrapper.isValid;
public override int GetHashCode() => _commandProvider.GetHashCode();

View File

@@ -2,18 +2,25 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class CommandSettingsViewModel(ICommandSettings _unsafeSettings, CommandProviderWrapper provider, TaskScheduler mainThread)
public partial class CommandSettingsViewModel(ICommandSettings? _unsafeSettings, CommandProviderWrapper provider, TaskScheduler mainThread)
{
private readonly ExtensionObject<ICommandSettings> _model = new(_unsafeSettings);
public ContentPageViewModel? SettingsPage { get; private set; }
public void InitializeProperties()
public bool Initialized { get; private set; }
public bool HasSettings =>
_model.Unsafe != null && // We have a settings model AND
(!Initialized || SettingsPage != null); // we weren't initialized, OR we were, and we do have a settings page
private void UnsafeInitializeProperties()
{
var model = _model.Unsafe;
if (model == null)
@@ -27,4 +34,27 @@ public partial class CommandSettingsViewModel(ICommandSettings _unsafeSettings,
SettingsPage.InitializeProperties();
}
}
public void SafeInitializeProperties()
{
try
{
UnsafeInitializeProperties();
}
catch (Exception ex)
{
Logger.LogError($"Failed to load settings page", ex: ex);
}
Initialized = true;
}
public void DoOnUiThread(Action action)
{
Task.Factory.StartNew(
action,
CancellationToken.None,
TaskCreationOptions.None,
mainThread);
}
}

View File

@@ -6,6 +6,7 @@ using System.Text.Json;
using AdaptiveCards.ObjectModel.WinUI3;
using AdaptiveCards.Templating;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
@@ -28,43 +29,67 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
public AdaptiveCardParseResult? Card { get; private set; }
private static string Serialize(string? s) =>
JsonSerializer.Serialize(s, JsonSerializationContext.Default.String);
private static bool TryBuildCard(
string templateJson,
string dataJson,
out AdaptiveCardParseResult? card,
out Exception? error)
{
card = null;
error = null;
try
{
var template = new AdaptiveCardTemplate(templateJson);
var cardJson = template.Expand(dataJson);
card = AdaptiveCard.FromJsonString(cardJson);
return true;
}
catch (Exception ex)
{
Logger.LogError("Error building card from template: {Message}", ex.Message);
error = ex;
return false;
}
}
public override void InitializeProperties()
{
var model = _formModel.Unsafe;
if (model == null)
if (model is null)
{
return;
}
try
{
TemplateJson = model.TemplateJson;
StateJson = model.StateJson;
DataJson = model.DataJson;
TemplateJson = model.TemplateJson;
StateJson = model.StateJson;
DataJson = model.DataJson;
AdaptiveCardTemplate template = new(TemplateJson);
var cardJson = template.Expand(DataJson);
Card = AdaptiveCard.FromJsonString(cardJson);
if (TryBuildCard(TemplateJson, DataJson, out var builtCard, out var renderingError))
{
Card = builtCard;
UpdateProperty(nameof(Card));
return;
}
catch (Exception e)
{
// If we fail to parse the card JSON, then display _our own card_
// with the exception
AdaptiveCardTemplate template = new(ErrorCardJson);
var serializeString = (string? s) => JsonSerializer.Serialize(s, JsonSerializationContext.Default.String);
// todo: we could probably stick Card.Errors in there too
var dataJson = $$"""
{
"error_message": {{serializeString(e.Message)}},
"error_stack": {{serializeString(e.StackTrace)}},
"inner_exception": {{serializeString(e.InnerException?.Message)}},
"template_json": {{serializeString(TemplateJson)}},
"data_json": {{serializeString(DataJson)}}
}
""";
var cardJson = template.Expand(dataJson);
Card = AdaptiveCard.FromJsonString(cardJson);
var errorPayload = $$"""
{
"error_message": {{Serialize(renderingError!.Message)}},
"error_stack": {{Serialize(renderingError.StackTrace)}},
"inner_exception": {{Serialize(renderingError.InnerException?.Message)}},
"template_json": {{Serialize(TemplateJson)}},
"data_json": {{Serialize(DataJson)}}
}
""";
if (TryBuildCard(ErrorCardJson, errorPayload, out var errorCard, out var _))
{
Card = errorCard;
UpdateProperty(nameof(Card));
return;
}
UpdateProperty(nameof(Card));

View File

@@ -61,6 +61,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
private Task? _initializeItemsTask;
private CancellationTokenSource? _cancellationTokenSource;
private ListItemViewModel? _lastSelectedItem;
public override bool IsInitialized
{
get => base.IsInitialized; protected set
@@ -328,7 +330,24 @@ public partial class ListViewModel : PageViewModel, IDisposable
}
[RelayCommand]
private void UpdateSelectedItem(ListItemViewModel item)
private void UpdateSelectedItem(ListItemViewModel? item)
{
if (_lastSelectedItem != null)
{
_lastSelectedItem.PropertyChanged -= SelectedItemPropertyChanged;
}
if (item != null)
{
SetSelectedItem(item);
}
else
{
ClearSelectedItem();
}
}
private void SetSelectedItem(ListItemViewModel item)
{
if (!item.SafeSlowInit())
{
@@ -355,6 +374,60 @@ public partial class ListViewModel : PageViewModel, IDisposable
TextToSuggest = item.TextToSuggest;
});
_lastSelectedItem = item;
_lastSelectedItem.PropertyChanged += SelectedItemPropertyChanged;
}
private void SelectedItemPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var item = _lastSelectedItem;
if (item == null)
{
return;
}
// already on the UI thread here
switch (e.PropertyName)
{
case nameof(item.Command):
case nameof(item.SecondaryCommand):
case nameof(item.AllCommands):
case nameof(item.Name):
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(item));
break;
case nameof(item.Details):
if (ShowDetails && item.HasDetails)
{
WeakReferenceMessenger.Default.Send<ShowDetailsMessage>(new(item.Details));
}
else
{
WeakReferenceMessenger.Default.Send<HideDetailsMessage>();
}
break;
case nameof(item.TextToSuggest):
TextToSuggest = item.TextToSuggest;
break;
}
}
private void ClearSelectedItem()
{
// GH #322:
// For inexplicable reasons, if you try updating the command bar and
// the details on the same UI thread tick as updating the list, we'll
// explode
DoOnUiThread(
() =>
{
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null));
WeakReferenceMessenger.Default.Send<HideDetailsMessage>();
TextToSuggest = string.Empty;
});
}
public override void InitializeProperties()

View File

@@ -18,6 +18,8 @@ public partial class ProviderSettingsViewModel(
IServiceProvider _serviceProvider) : ObservableObject
{
private readonly SettingsModel _settings = _serviceProvider.GetService<SettingsModel>()!;
private readonly Lock _initializeSettingsLock = new();
private Task? _initializeSettingsTask;
public string DisplayName => _provider.DisplayName;
@@ -34,6 +36,9 @@ public partial class ProviderSettingsViewModel(
public IconInfoViewModel Icon => _provider.Icon;
[ObservableProperty]
public partial bool LoadingSettings { get; set; } = _provider.Settings?.HasSettings ?? false;
public bool IsEnabled
{
get => _providerSettings.IsEnabled;
@@ -56,15 +61,60 @@ public partial class ProviderSettingsViewModel(
}
}
private void Provider_CommandsChanged(CommandProviderWrapper sender, CommandPalette.Extensions.IItemsChangedEventArgs args)
/// <summary>
/// Gets a value indicating whether returns true if we have a settings page
/// that's initialized, or we are still working on initializing that
/// settings page. If we don't have a settings object, or that settings
/// object doesn't have a settings page, then we'll return false.
/// </summary>
public bool HasSettings
{
OnPropertyChanged(nameof(ExtensionSubtext));
OnPropertyChanged(nameof(TopLevelCommands));
get
{
if (_provider.Settings == null)
{
return false;
}
if (_provider.Settings.Initialized)
{
return _provider.Settings.HasSettings;
}
// settings still need to be loaded.
return LoadingSettings;
}
}
public bool HasSettings => _provider.Settings != null && _provider.Settings.SettingsPage != null;
/// <summary>
/// Gets will return the settings page, if we have one, and have initialized it.
/// If we haven't initialized it, this will kick off a thread to start
/// initializing it.
/// </summary>
public ContentPageViewModel? SettingsPage
{
get
{
if (_provider.Settings == null)
{
return null;
}
public ContentPageViewModel? SettingsPage => HasSettings ? _provider?.Settings?.SettingsPage : null;
if (_provider.Settings.Initialized)
{
LoadingSettings = false;
return _provider.Settings.SettingsPage;
}
// Don't load the settings if we're already working on it
lock (_initializeSettingsLock)
{
_initializeSettingsTask ??= Task.Run(InitializeSettingsPage);
}
return null;
}
}
[field: AllowNull]
public List<TopLevelViewModel> TopLevelCommands
@@ -90,4 +140,30 @@ public partial class ProviderSettingsViewModel(
}
private void Save() => SettingsModel.SaveSettings(_settings);
private void InitializeSettingsPage()
{
if (_provider.Settings == null)
{
return;
}
_provider.Settings.SafeInitializeProperties();
_provider.Settings.DoOnUiThread(() =>
{
// Changing these properties will try to update XAML, and that has
// to be handled on the UI thread, so we need to raise them on the
// UI thread
LoadingSettings = false;
OnPropertyChanged(nameof(HasSettings));
OnPropertyChanged(nameof(LoadingSettings));
OnPropertyChanged(nameof(SettingsPage));
});
}
private void Provider_CommandsChanged(CommandProviderWrapper sender, CommandPalette.Extensions.IItemsChangedEventArgs args)
{
OnPropertyChanged(nameof(ExtensionSubtext));
OnPropertyChanged(nameof(TopLevelCommands));
}
}

View File

@@ -4,6 +4,7 @@
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
@@ -44,6 +45,9 @@ public partial class TopLevelCommandManager : ObservableObject,
public async Task<bool> LoadBuiltinsAsync()
{
var s = new Stopwatch();
s.Start();
_builtInCommands.Clear();
// Load built-In commands first. These are all in-proc, and
@@ -53,53 +57,48 @@ public partial class TopLevelCommandManager : ObservableObject,
{
CommandProviderWrapper wrapper = new(provider, _taskScheduler);
_builtInCommands.Add(wrapper);
await LoadTopLevelCommandsFromProvider(wrapper);
var commands = await LoadTopLevelCommandsFromProvider(wrapper);
lock (TopLevelCommands)
{
foreach (var c in commands)
{
TopLevelCommands.Add(c);
}
}
}
s.Stop();
Logger.LogDebug($"Loading built-ins took {s.ElapsedMilliseconds}ms");
return true;
}
// May be called from a background thread
private async Task LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
private async Task<IEnumerable<TopLevelViewModel>> LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
{
WeakReference<IPageContext> weakSelf = new(this);
await commandProvider.LoadTopLevelCommands(_serviceProvider, weakSelf);
var settings = _serviceProvider.GetService<SettingsModel>()!;
var makeAndAdd = (ICommandItem? i, bool fallback) =>
List<TopLevelViewModel> commands = [];
foreach (var item in commandProvider.TopLevelItems)
{
var commandItemViewModel = new CommandItemViewModel(new(i), weakSelf);
var topLevelViewModel = new TopLevelViewModel(commandItemViewModel, fallback, commandProvider.ExtensionHost, commandProvider.ProviderId, settings, _serviceProvider);
commands.Add(item);
}
lock (TopLevelCommands)
{
TopLevelCommands.Add(topLevelViewModel);
}
};
await Task.Factory.StartNew(
() =>
{
lock (TopLevelCommands)
{
foreach (var item in commandProvider.TopLevelItems)
{
TopLevelCommands.Add(item);
}
foreach (var item in commandProvider.FallbackItems)
{
TopLevelCommands.Add(item);
}
}
},
CancellationToken.None,
TaskCreationOptions.None,
_taskScheduler);
foreach (var item in commandProvider.FallbackItems)
{
commands.Add(item);
}
commandProvider.CommandsChanged -= CommandProvider_CommandsChanged;
commandProvider.CommandsChanged += CommandProvider_CommandsChanged;
return commands;
}
// By all accounts, we're already on a background thread (the COM call
@@ -239,25 +238,71 @@ public partial class TopLevelCommandManager : ObservableObject,
private async Task StartExtensionsAndGetCommands(IEnumerable<IExtensionWrapper> extensions)
{
// TODO This most definitely needs a lock
foreach (var extension in extensions)
{
Logger.LogDebug($"Starting {extension.PackageFullName}");
try
{
// start it ...
await extension.StartExtensionAsync();
var timer = new Stopwatch();
timer.Start();
// ... and fetch the command provider from it.
CommandProviderWrapper wrapper = new(extension, _taskScheduler);
_extensionCommandProviders.Add(wrapper);
await LoadTopLevelCommandsFromProvider(wrapper);
}
catch (Exception ex)
// Start all extensions in parallel
var startTasks = extensions.Select(StartExtensionWithTimeoutAsync);
// Wait for all extensions to start
var wrappers = (await Task.WhenAll(startTasks)).Where(wrapper => wrapper != null).Select(w => w!).ToList();
foreach (var wrapper in wrappers)
{
_extensionCommandProviders.Add(wrapper!);
}
// Load the commands from the providers in parallel
var loadTasks = wrappers.Select(LoadCommandsWithTimeoutAsync);
var commandSets = (await Task.WhenAll(loadTasks)).Where(results => results != null).Select(r => r!).ToList();
lock (TopLevelCommands)
{
foreach (var commands in commandSets)
{
Logger.LogError(ex.ToString());
foreach (var c in commands)
{
TopLevelCommands.Add(c);
}
}
}
timer.Stop();
Logger.LogDebug($"Loading extensions took {timer.ElapsedMilliseconds} ms");
}
private async Task<CommandProviderWrapper?> StartExtensionWithTimeoutAsync(IExtensionWrapper extension)
{
Logger.LogDebug($"Starting {extension.PackageFullName}");
try
{
await extension.StartExtensionAsync().WaitAsync(TimeSpan.FromSeconds(10));
return new CommandProviderWrapper(extension, _taskScheduler);
}
catch (Exception ex)
{
Logger.LogError($"Failed to start extension {extension.PackageFullName}: {ex}");
return null; // Return null for failed extensions
}
}
private async Task<IEnumerable<TopLevelViewModel>?> LoadCommandsWithTimeoutAsync(CommandProviderWrapper wrapper)
{
try
{
return await LoadTopLevelCommandsFromProvider(wrapper!).WaitAsync(TimeSpan.FromSeconds(10));
}
catch (TimeoutException)
{
Logger.LogError($"Loading commands from {wrapper!.ExtensionHost?.Extension?.PackageFullName} timed out");
}
catch (Exception ex)
{
Logger.LogError($"Failed to load commands for extension {wrapper!.ExtensionHost?.Extension?.PackageFullName}: {ex}");
}
return null;
}
private void ExtensionService_OnExtensionRemoved(IExtensionService sender, IEnumerable<IExtensionWrapper> extensions)

View File

@@ -73,26 +73,12 @@ public partial class App : Application
/// Invoked when the application is launched.
/// </summary>
/// <param name="args">Details about the launch request and process.</param>
protected override void OnLaunched(LaunchActivatedEventArgs args)
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
AppWindow = new MainWindow();
var cmdArgs = Environment.GetCommandLineArgs();
var runFromPT = false;
foreach (var arg in cmdArgs)
{
if (arg == "RunFromPT")
{
runFromPT = true;
break;
}
}
if (!runFromPT)
{
AppWindow.Activate();
}
var activatedEventArgs = Microsoft.Windows.AppLifecycle.AppInstance.GetCurrent().GetActivatedEventArgs();
((MainWindow)AppWindow).HandleLaunch(activatedEventArgs);
}
/// <summary>

View File

@@ -124,14 +124,12 @@ public sealed partial class ListPage : Page,
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS is too aggressive at pruning methods bound in XAML")]
private void ItemsList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (ItemsList.SelectedItem is ListItemViewModel item)
var vm = ViewModel;
var li = ItemsList.SelectedItem as ListItemViewModel;
_ = Task.Run(() =>
{
var vm = ViewModel;
_ = Task.Run(() =>
{
vm?.UpdateSelectedItemCommand.Execute(item);
});
}
vm?.UpdateSelectedItemCommand.Execute(li);
});
// There's mysterious behavior here, where the selection seemingly
// changes to _nothing_ when we're backspacing to a single character.

View File

@@ -1,4 +1,4 @@
<Window
<winuiex:WindowEx
x:Class="Microsoft.CmdPal.UI.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@@ -6,8 +6,13 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:pages="using:Microsoft.CmdPal.UI.Pages"
xmlns:viewmodels="using:Microsoft.CmdPal.UI.ViewModels"
xmlns:winuiex="using:WinUIEx"
Width="800"
Height="480"
MinWidth="320"
MinHeight="240"
Activated="MainWindow_Activated"
Closed="MainWindow_Closed"
mc:Ignorable="d">
<pages:ShellPage x:Name="RootShellPage" />
</Window>
</winuiex:WindowEx>

View File

@@ -19,21 +19,25 @@ using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Input;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle;
using Windows.ApplicationModel.Activation;
using Windows.Foundation;
using Windows.Graphics;
using Windows.UI;
using Windows.UI.WindowManagement;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Dwm;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using Windows.Win32.UI.Shell;
using Windows.Win32.UI.WindowsAndMessaging;
using WinRT;
using WinUIEx;
using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance;
namespace Microsoft.CmdPal.UI;
public sealed partial class MainWindow : Window,
public sealed partial class MainWindow : WindowEx,
IRecipient<DismissMessage>,
IRecipient<ShowWindowMessage>,
IRecipient<HideWindowMessage>,
@@ -82,7 +86,6 @@ public sealed partial class MainWindow : Window,
this.SetIcon();
AppWindow.Title = RS_.GetString("AppName");
AppWindow.Resize(new SizeInt32 { Width = 1000, Height = 620 });
PositionCentered();
SetAcrylic();
@@ -232,6 +235,16 @@ public sealed partial class MainWindow : Window,
PositionCentered(display);
PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_SHOW);
// instead of showing the window, uncloak it from DWM
// This will make it visible to the user, without the animation or frames for
// loading XAML with composition
unsafe
{
BOOL value = false;
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAK, &value, (uint)sizeof(BOOL));
}
PInvoke.SetForegroundWindow(hwnd);
PInvoke.SetActiveWindow(hwnd);
}
@@ -289,7 +302,7 @@ public sealed partial class MainWindow : Window,
ShowHwnd(message.Hwnd, settings.SummonOn);
}
public void Receive(HideWindowMessage message) => PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
public void Receive(HideWindowMessage message) => HideWindow();
public void Receive(QuitMessage message) =>
@@ -297,7 +310,21 @@ public sealed partial class MainWindow : Window,
DispatcherQueue.TryEnqueue(() => Close());
public void Receive(DismissMessage message) =>
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
HideWindow();
private void HideWindow()
{
// Hide our window
// Instead of hiding the window, cloak it from DWM
// This will make it invisible to the user, such that we can show it again
// by uncloaking it, which avoids an unnecessary "flicker in" that XAML does
unsafe
{
BOOL value = true;
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAK, &value, (uint)sizeof(BOOL));
}
}
internal void MainWindow_Closed(object sender, WindowEventArgs args)
{
@@ -386,7 +413,9 @@ public sealed partial class MainWindow : Window,
return;
}
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
// This will DWM cloak our window:
HideWindow();
PowerToysTelemetry.Log.WriteEvent(new CmdPalDismissedOnLostFocus());
}
@@ -396,6 +425,40 @@ public sealed partial class MainWindow : Window,
}
}
public void HandleLaunch(AppActivationArguments? activatedEventArgs)
{
if (activatedEventArgs == null)
{
Summon(string.Empty);
return;
}
if (activatedEventArgs.Kind == Microsoft.Windows.AppLifecycle.ExtendedActivationKind.Protocol)
{
if (activatedEventArgs.Data is IProtocolActivatedEventArgs protocolArgs)
{
if (protocolArgs.Uri.ToString() is string uri)
{
// was the URI "x-cmdpal://background" ?
if (uri.StartsWith("x-cmdpal://background", StringComparison.OrdinalIgnoreCase))
{
// we're running, we don't want to activate our window. bail
return;
}
else if (uri.StartsWith("x-cmdpal://settings", StringComparison.OrdinalIgnoreCase))
{
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>(new());
return;
}
}
return;
}
}
Activate();
}
public void Summon(string commandId) =>
// The actual showing and hiding of the window will be done by the
@@ -404,11 +467,6 @@ public sealed partial class MainWindow : Window,
// know till the message is being handled.
WeakReferenceMessenger.Default.Send<HotkeySummonMessage>(new(commandId, _hwnd));
#pragma warning disable SA1310 // Field names should not contain underscore
private const uint DOT_KEY = 0xBE;
private const uint WM_HOTKEY = 0x0312;
#pragma warning restore SA1310 // Field names should not contain underscore
private void UnregisterHotkeys()
{
_keyboardListener.ClearHotkeys();
@@ -479,10 +537,24 @@ public sealed partial class MainWindow : Window,
var isRootHotkey = string.IsNullOrEmpty(commandId);
PowerToysTelemetry.Log.WriteEvent(new CmdPalHotkeySummoned(isRootHotkey));
var isVisible = this.Visible;
unsafe
{
// We need to check if our window is cloaked or not. A cloaked window is still
// technically visible, because SHOW/HIDE != iconic (minimized) != cloaked
// (these are all separate states)
long attr = 0;
PInvoke.DwmGetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAKED, &attr, sizeof(long));
if (attr == 1 /* DWM_CLOAKED_APP */)
{
isVisible = false;
}
}
// Note to future us: the wParam will have the index of the hotkey we registered.
// We can use that in the future to differentiate the hotkeys we've pressed
// so that we can bind hotkeys to individual commands
if (!this.Visible || !isRootHotkey)
if (!isVisible || !isRootHotkey)
{
Activate();
@@ -490,7 +562,16 @@ public sealed partial class MainWindow : Window,
}
else if (isRootHotkey)
{
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
// If there's a debugger attached...
if (System.Diagnostics.Debugger.IsAttached)
{
// ... then manually hide our window. When debugged, we won't get the cool cloaking,
// but that's the price to pay for having the HWND not light-dismiss while we're debugging.
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
return;
}
HideWindow();
}
}
@@ -502,7 +583,10 @@ public sealed partial class MainWindow : Window,
{
switch (uMsg)
{
case WM_HOTKEY:
// Prevent the window from maximizing when double-clicking the title bar area
case PInvoke.WM_NCLBUTTONDBLCLK:
return (LRESULT)IntPtr.Zero;
case PInvoke.WM_HOTKEY:
{
var hotkeyIndex = (int)wParam.Value;
if (hotkeyIndex < _hotkeys.Count)
@@ -518,22 +602,6 @@ public sealed partial class MainWindow : Window,
var hotkey = _hotkeys[hotkeyIndex];
HandleSummon(hotkey.CommandId);
// var isRootHotkey = string.IsNullOrEmpty(hotkey.CommandId);
// // Note to future us: the wParam will have the index of the hotkey we registered.
// // We can use that in the future to differentiate the hotkeys we've pressed
// // so that we can bind hotkeys to individual commands
// if (!this.Visible || !isRootHotkey)
// {
// Activate();
// Summon(hotkey.CommandId);
// }
// else if (isRootHotkey)
// {
// PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_HIDE);
// }
}
return (LRESULT)IntPtr.Zero;

View File

@@ -66,7 +66,7 @@
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" />
<PackageReference Include="WinUIEx" />
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>

View File

@@ -25,6 +25,8 @@ SHCreateStreamOnFileEx
CoAllowSetForegroundWindow
SHCreateStreamOnFileEx
SHLoadIndirectString
WM_HOTKEY
WM_NCLBUTTONDBLCLK
Shell_NotifyIcon
LoadIcon
@@ -36,3 +38,8 @@ ExtractIconEx
WM_RBUTTONUP
WM_LBUTTONUP
WM_LBUTTONDBLCLK
MessageBox
DwmGetWindowAttribute
DwmSetWindowAttribute
DWM_CLOAKED_APP

View File

@@ -70,6 +70,13 @@
DisplayName="ms-resource:StartupTaskNameDev" />
</uap5:Extension>
<uap:Extension Category="windows.protocol">
<uap:Protocol Name="x-cmdpal">
<uap:Logo>Assets\StoreLogo.png</uap:Logo>
<uap:DisplayName>Command Palette Dev URI scheme</uap:DisplayName>
</uap:Protocol>
</uap:Extension>
</Extensions>
</Application>

View File

@@ -70,6 +70,14 @@
DisplayName="ms-resource:StartupTaskName" />
</uap5:Extension>
<uap:Extension Category="windows.protocol">
<uap:Protocol Name="x-cmdpal">
<uap:Logo>Assets\StoreLogo.png</uap:Logo>
<uap:DisplayName>Command Palette URI scheme</uap:DisplayName>
</uap:Protocol>
</uap:Extension>
</Extensions>
</Application>

View File

@@ -418,18 +418,20 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
{
_ = DispatcherQueue.TryEnqueue(() =>
{
// Also hide our details pane about here, if we had one
HideDetails();
if (_settingsWindow == null)
{
_settingsWindow = new SettingsWindow();
}
_settingsWindow.Activate();
OpenSettings();
});
}
public void OpenSettings()
{
if (_settingsWindow == null)
{
_settingsWindow = new SettingsWindow();
}
_settingsWindow.Activate();
}
public void Receive(ShowDetailsMessage message)
{
// TERRIBLE HACK TODO GH #245

View File

@@ -2,10 +2,14 @@
// 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.Runtime.InteropServices;
using ManagedCommon;
using Microsoft.CmdPal.UI.Events;
using Microsoft.PowerToys.Telemetry;
using Microsoft.Windows.AppLifecycle;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Microsoft.CmdPal.UI;
@@ -30,21 +34,51 @@ internal sealed class Program
return 0;
}
Logger.InitializeLogger("\\CmdPal\\Logs\\");
WinRT.ComWrappersSupport.InitializeComWrappers();
var isRedirect = DecideRedirection();
if (isRedirect)
{
return 0;
}
try
{
Logger.InitializeLogger("\\CmdPal\\Logs\\");
}
catch (COMException e)
{
// This is unexpected. For the sake of debugging:
// pop a message box
PInvoke.MessageBox(
(HWND)IntPtr.Zero,
$"Failed to initialize the logger. COMException: \r{e.Message}",
"Command Palette",
MESSAGEBOX_STYLE.MB_OK | MESSAGEBOX_STYLE.MB_ICONERROR);
return 0;
}
catch (Exception e2)
{
// This is unexpected. For the sake of debugging:
// pop a message box
PInvoke.MessageBox(
(HWND)IntPtr.Zero,
$"Failed to initialize the logger. Unknown Exception: \r{e2.Message}",
"Command Palette",
MESSAGEBOX_STYLE.MB_OK | MESSAGEBOX_STYLE.MB_ICONERROR);
return 0;
}
Logger.LogDebug($"Starting at {DateTime.UtcNow}");
PowerToysTelemetry.Log.WriteEvent(new CmdPalProcessStarted());
WinRT.ComWrappersSupport.InitializeComWrappers();
var isRedirect = DecideRedirection();
if (!isRedirect)
Microsoft.UI.Xaml.Application.Start((p) =>
{
Microsoft.UI.Xaml.Application.Start((p) =>
{
Microsoft.UI.Dispatching.DispatcherQueueSynchronizationContext context = new(Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread());
SynchronizationContext.SetSynchronizationContext(context);
app = new App();
});
}
Microsoft.UI.Dispatching.DispatcherQueueSynchronizationContext context = new(Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread());
SynchronizationContext.SetSynchronizationContext(context);
app = new App();
});
return 0;
}
@@ -64,7 +98,7 @@ internal sealed class Program
{
isRedirect = true;
PowerToysTelemetry.Log.WriteEvent(new ReactivateInstance());
keyInstance.RedirectActivationToAsync(args).AsTask().ConfigureAwait(false);
keyInstance.RedirectActivationToAsync(args).AsTask().GetAwaiter().GetResult();
}
return isRedirect;
@@ -79,7 +113,12 @@ internal sealed class Program
if (thisApp.AppWindow is not null and
MainWindow mainWindow)
{
mainWindow.Summon(string.Empty);
mainWindow.DispatcherQueue.TryEnqueue(() =>
{
mainWindow.HandleLaunch(args);
});
// mainWindow.Summon(string.Empty);
}
}
}

View File

@@ -2,7 +2,8 @@
"profiles": {
"Microsoft.CmdPal.UI (Package)": {
"commandName": "MsixPackage",
"nativeDebugging": false
"nativeDebugging": false,
"doNotLaunchApp": false
},
"Microsoft.CmdPal.UI (Unpackaged)": {
"commandName": "Project"

View File

@@ -113,7 +113,24 @@
Visibility="{x:Bind ViewModel.HasSettings}" />
<Frame x:Name="SettingsFrame" Visibility="{x:Bind ViewModel.HasSettings}">
<cmdpalUI:ContentPage ViewModel="{x:Bind ViewModel.SettingsPage, Mode=OneWay}" />
<controls:SwitchPresenter
HorizontalAlignment="Stretch"
TargetType="x:Boolean"
Value="{x:Bind ViewModel.LoadingSettings, Mode=OneWay}">
<controls:Case Value="True">
<ProgressRing
Width="36"
Height="36"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsIndeterminate="True" />
</controls:Case>
<controls:Case Value="False">
<cmdpalUI:ContentPage ViewModel="{x:Bind ViewModel.SettingsPage, Mode=OneWay}" />
</controls:Case>
</controls:SwitchPresenter>
</Frame>
<TextBlock

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8" ?>
<Window
<winuiex:WindowEx
x:Class="Microsoft.CmdPal.UI.Settings.SettingsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@@ -7,13 +7,18 @@
xmlns:local="using:Microsoft.CmdPal.UI.Settings"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:winuiex="using:WinUIEx"
Title="SettingsWindow"
Width="1280"
Height="720"
MinWidth="480"
MinHeight="480"
Activated="Window_Activated"
Closed="Window_Closed"
mc:Ignorable="d">
<Window.SystemBackdrop>
<winuiex:WindowEx.SystemBackdrop>
<MicaBackdrop />
</Window.SystemBackdrop>
</winuiex:WindowEx.SystemBackdrop>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@@ -37,10 +42,10 @@
Height="16"
Source="ms-appx:///Assets/icon.svg" />
<TextBlock
x:Uid="CmdPalSettingsHeader"
Margin="12,0,0,0"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="Command Palette Settings" />
Style="{StaticResource CaptionTextBlockStyle}" />
</StackPanel>
<NavigationView
x:Name="NavView"
@@ -100,4 +105,4 @@
</Grid>
</NavigationView>
</Grid>
</Window>
</winuiex:WindowEx>

View File

@@ -11,11 +11,12 @@ using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.Graphics;
using WinUIEx;
using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance;
namespace Microsoft.CmdPal.UI.Settings;
public sealed partial class SettingsWindow : Window,
public sealed partial class SettingsWindow : WindowEx,
IRecipient<NavigateToExtensionSettingsMessage>,
IRecipient<QuitMessage>
{
@@ -70,7 +71,6 @@ public sealed partial class SettingsWindow : Window,
private void PositionCentered()
{
AppWindow.Resize(new SizeInt32 { Width = 1280, Height = 720 });
var displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest);
if (displayArea is not null)
{

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8" ?>
<Window
<winuiex:WindowEx
x:Class="Microsoft.CmdPal.UI.ToastWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@@ -7,17 +7,18 @@
xmlns:local="using:Microsoft.CmdPal.UI"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:winuiex="using:WinUIEx"
Title="Command Palette Toast"
mc:Ignorable="d">
<Window.SystemBackdrop>
<winuiex:WindowEx.SystemBackdrop>
<DesktopAcrylicBackdrop />
</Window.SystemBackdrop>
</winuiex:WindowEx.SystemBackdrop>
<Grid x:Name="ToastGrid">
<!-- This padding is used to calculate the dimensions of the ToastWindow -->
<TextBlock
x:Name="ToastText"
Padding="16,16,36,24"
Padding="12,12,24,20"
Text="{x:Bind ViewModel.ToastMessage, Mode=OneWay}"
TextAlignment="Center" />
</Grid>
</Window>
</winuiex:WindowEx>

View File

@@ -17,11 +17,12 @@ using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Gdi;
using Windows.Win32.UI.HiDpi;
using Windows.Win32.UI.WindowsAndMessaging;
using WinUIEx;
using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance;
namespace Microsoft.CmdPal.UI;
public sealed partial class ToastWindow : Window,
public sealed partial class ToastWindow : WindowEx,
IRecipient<QuitMessage>
{
private readonly HWND _hwnd;
@@ -64,19 +65,7 @@ public sealed partial class ToastWindow : Window,
private void PositionCentered()
{
var intSize = new SizeInt32
{
Width = Convert.ToInt32(ToastText.ActualWidth),
Height = Convert.ToInt32(ToastText.ActualHeight),
};
var scaleAdjustment = GetScaleFactor(_hwnd);
var scaled = new SizeInt32
{
Width = (int)Math.Round(intSize.Width * scaleAdjustment),
Height = (int)Math.Round(intSize.Height * scaleAdjustment),
};
AppWindow.Resize(scaled);
this.SetWindowSize(ToastText.ActualWidth, ToastText.ActualHeight);
var displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest);
if (displayArea is not null)
@@ -86,7 +75,7 @@ public sealed partial class ToastWindow : Window,
var monitorHeight = displayArea.WorkArea.Height;
var windowHeight = AppWindow.Size.Height;
centeredPosition.Y = monitorHeight - (windowHeight * 2);
centeredPosition.Y = monitorHeight - (windowHeight + 8); // Align with other shell toasts, like the volume indicator.
AppWindow.Move(centeredPosition);
}
}

View File

@@ -9,7 +9,7 @@ By default, CmdPal is bound to <kbd>Win+Alt+Space</kbd>.
The fastest way to get started is just to run the "Create extension" command in the palette itself. That'll prompt you for a project name and a Display Name, and where you want to place your project. Then just open the `sln` it produces. You should be ready to go 🙂.
The official API documentation can be found [on this docs site](TODO! Add docs link when we have one)
The official API documentation can be found [on this docs site](https://learn.microsoft.com/windows/powertoys/command-palette/extensibility-overview).
We've also got samples, so that you can see how the APIs in-action.

View File

@@ -12,7 +12,7 @@ using Microsoft.CmdPal.Ext.Apps.Utils;
namespace Microsoft.CmdPal.Ext.Apps;
public sealed class AppCache : IDisposable
public sealed partial class AppCache : IDisposable
{
private Win32ProgramFileSystemWatchers _win32ProgramRepositoryHelper;

View File

@@ -319,7 +319,7 @@ public static class ReparsePoint
public static AppExecutionAliasMetadata FromPersistedRepresentationIntPtr(IntPtr reparseDataBufferPtr, AppExecutionAliasReparseTagBufferLayoutVersion version)
{
var dataOffset = Marshal.SizeOf(typeof(AppExecutionAliasReparseTagHeader));
var dataOffset = Marshal.SizeOf<AppExecutionAliasReparseTagHeader>();
var dataBufferPtr = reparseDataBufferPtr + dataOffset;
string? packageFullName = null;

View File

@@ -8,7 +8,7 @@ using System.IO;
namespace Microsoft.CmdPal.Ext.Apps.Storage;
// File System Watcher Wrapper class which implements the IFileSystemWatcherWrapper interface
public sealed class FileSystemWatcherWrapper : FileSystemWatcher, IFileSystemWatcherWrapper
public sealed partial class FileSystemWatcherWrapper : FileSystemWatcher, IFileSystemWatcherWrapper
{
public FileSystemWatcherWrapper()
{

View File

@@ -15,7 +15,7 @@ namespace Microsoft.CmdPal.Ext.Apps.Storage;
/// A repository for storing packaged applications such as UWP apps or appx packaged desktop apps.
/// This repository will also monitor for changes to the PackageCatalog and update the repository accordingly
/// </summary>
internal sealed class PackageRepository : ListRepository<UWPApplication>, IProgramRepository
internal sealed partial class PackageRepository : ListRepository<UWPApplication>, IProgramRepository
{
private readonly IPackageCatalog _packageCatalog;

View File

@@ -9,7 +9,7 @@ using System.Linq;
namespace Microsoft.CmdPal.Ext.Apps.Storage;
internal sealed class Win32ProgramFileSystemWatchers : IDisposable
internal sealed partial class Win32ProgramFileSystemWatchers : IDisposable
{
public string[] PathsToWatch { get; set; }

View File

@@ -14,7 +14,7 @@ using Win32Program = Microsoft.CmdPal.Ext.Apps.Programs.Win32Program;
namespace Microsoft.CmdPal.Ext.Apps.Storage;
internal sealed class Win32ProgramRepository : ListRepository<Programs.Win32Program>, IProgramRepository
internal sealed partial class Win32ProgramRepository : ListRepository<Programs.Win32Program>, IProgramRepository
{
private static readonly IFileSystem FileSystem = new FileSystem();
private static readonly IPath Path = FileSystem.Path;

View File

@@ -2,7 +2,10 @@
// 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.Globalization;
using System.IO;
using System.Text;
using Microsoft.CmdPal.Ext.Indexer.Data;
using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -10,8 +13,14 @@ using Windows.Storage.Streams;
namespace Microsoft.CmdPal.Ext.Indexer;
internal sealed partial class FallbackOpenFileItem : FallbackCommandItem
internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System.IDisposable
{
private readonly CompositeFormat fallbackItemSearchPageTitleCompositeFormat = CompositeFormat.Parse(Resources.Indexer_fallback_searchPage_title);
private readonly SearchEngine _searchEngine = new();
private uint _queryCookie = 10;
public FallbackOpenFileItem()
: base(new NoOpCommand(), Resources.Indexer_Find_Path_fallback_display_title)
{
@@ -21,8 +30,20 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem
public override void UpdateQuery(string query)
{
if (string.IsNullOrWhiteSpace(query))
{
Command = new NoOpCommand();
Title = string.Empty;
Subtitle = string.Empty;
Icon = null;
MoreCommands = null;
return;
}
if (Path.Exists(query))
{
// Exit 1: The query is a direct path to a file. Great! Return it.
var item = new IndexerItem() { FullPath = query, FileName = Path.GetFileName(query) };
var listItemForUs = new IndexerListItem(item, IncludeBrowseCommand.AsDefault);
Command = listItemForUs.Command;
@@ -43,12 +64,65 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem
catch
{
}
return;
}
else
{
Title = string.Empty;
Subtitle = string.Empty;
Command = new NoOpCommand();
_queryCookie++;
try
{
_searchEngine.Query(query, _queryCookie);
var results = _searchEngine.FetchItems(0, 20, _queryCookie, out var _);
if (results.Count == 0 || ((results[0] as IndexerListItem) == null))
{
// Exit 2: We searched for the file, and found nothing. Oh well.
// Hide ourselves.
Title = string.Empty;
Subtitle = string.Empty;
Command = new NoOpCommand();
return;
}
if (results.Count == 1)
{
// Exit 3: We searched for the file, and found exactly one thing. Awesome!
// Return it.
Title = results[0].Title;
Subtitle = results[0].Subtitle;
Icon = results[0].Icon;
Command = results[0].Command;
MoreCommands = results[0].MoreCommands;
return;
}
// Exit 4: We found more than one result. Make our command take
// us to the file search page, prepopulated with this search.
var indexerPage = new IndexerPage(query, _searchEngine, _queryCookie, results);
Title = string.Format(CultureInfo.CurrentCulture, fallbackItemSearchPageTitleCompositeFormat, query);
Icon = Icons.FileExplorer;
Subtitle = Resources.Indexer_Subtitle;
Command = indexerPage;
return;
}
catch
{
Title = string.Empty;
Subtitle = string.Empty;
Icon = null;
Command = new NoOpCommand();
MoreCommands = null;
}
}
}
public void Dispose()
{
_searchEngine.Dispose();
GC.SuppressFinalize(this);
}
}

View File

@@ -4,25 +4,22 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Indexer.Data;
using Microsoft.CmdPal.Ext.Indexer.Indexer;
using Microsoft.CmdPal.Ext.Indexer.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Storage.Streams;
namespace Microsoft.CmdPal.Ext.Indexer;
internal sealed partial class IndexerPage : DynamicListPage, IDisposable
{
private readonly List<IListItem> _indexerListItems = [];
private readonly SearchEngine _searchEngine;
private readonly bool disposeSearchEngine = true;
private SearchQuery _searchQuery = new();
private uint _queryCookie;
private uint _queryCookie = 10;
private string initialQuery = string.Empty;
public IndexerPage()
{
@@ -30,16 +27,31 @@ internal sealed partial class IndexerPage : DynamicListPage, IDisposable
Icon = Icons.FileExplorer;
Name = Resources.Indexer_Title;
PlaceholderText = Resources.Indexer_PlaceholderText;
_searchEngine = new();
_queryCookie = 10;
}
public IndexerPage(string query, SearchEngine searchEngine, uint queryCookie, IList<IListItem> firstPageData)
{
Icon = Icons.FileExplorer;
Name = Resources.Indexer_Title;
_searchEngine = searchEngine;
_queryCookie = queryCookie;
_indexerListItems.AddRange(firstPageData);
initialQuery = query;
SearchText = query;
disposeSearchEngine = false;
}
public override void UpdateSearchText(string oldSearch, string newSearch)
{
if (oldSearch != newSearch)
if (oldSearch != newSearch && newSearch != initialQuery)
{
_ = Task.Run(() =>
{
Query(newSearch);
LoadMore();
initialQuery = string.Empty;
});
}
}
@@ -49,7 +61,9 @@ internal sealed partial class IndexerPage : DynamicListPage, IDisposable
public override void LoadMore()
{
IsLoading = true;
FetchItems(20);
var results = _searchEngine.FetchItems(_indexerListItems.Count, 20, _queryCookie, out var hasMore);
_indexerListItems.AddRange(results);
HasMoreItems = hasMore;
IsLoading = false;
RaiseItemsChanged(_indexerListItems.Count);
}
@@ -58,70 +72,16 @@ internal sealed partial class IndexerPage : DynamicListPage, IDisposable
{
++_queryCookie;
_indexerListItems.Clear();
_searchQuery.SearchResults.Clear();
_searchQuery.CancelOutstandingQueries();
if (query == string.Empty)
{
return;
}
Stopwatch stopwatch = new();
stopwatch.Start();
_searchQuery.Execute(query, _queryCookie);
stopwatch.Stop();
Logger.LogDebug($"Query time: {stopwatch.ElapsedMilliseconds} ms, query: \"{query}\"");
}
private void FetchItems(int limit)
{
if (_searchQuery != null)
{
var cookie = _searchQuery.Cookie;
if (cookie == _queryCookie)
{
var index = 0;
SearchResult result;
var hasMoreItems = _searchQuery.FetchRows(_indexerListItems.Count, limit);
while (!_searchQuery.SearchResults.IsEmpty && _searchQuery.SearchResults.TryDequeue(out result) && ++index <= limit)
{
IconInfo icon = null;
try
{
var stream = ThumbnailHelper.GetThumbnail(result.LaunchUri).Result;
if (stream != null)
{
var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream));
icon = new IconInfo(data, data);
}
}
catch (Exception ex)
{
Logger.LogError("Failed to get the icon.", ex);
}
_indexerListItems.Add(new IndexerListItem(new IndexerItem
{
FileName = result.ItemDisplayName,
FullPath = result.LaunchUri,
})
{
Icon = icon,
});
}
HasMoreItems = hasMoreItems;
}
}
_searchEngine.Query(query, _queryCookie);
}
public void Dispose()
{
_searchQuery = null;
GC.SuppressFinalize(this);
if (disposeSearchEngine)
{
_searchEngine.Dispose();
GC.SuppressFinalize(this);
}
}
}

View File

@@ -123,6 +123,15 @@ namespace Microsoft.CmdPal.Ext.Indexer.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Search for &quot;{0}&quot; in files.
/// </summary>
internal static string Indexer_fallback_searchPage_title {
get {
return ResourceManager.GetString("Indexer_fallback_searchPage_title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to This file doesn&apos;t exist.
/// </summary>
@@ -168,6 +177,42 @@ namespace Microsoft.CmdPal.Ext.Indexer.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Always on.
/// </summary>
internal static string Indexer_Settings_FallbackCommand_AlwaysOn {
get {
return ResourceManager.GetString("Indexer_Settings_FallbackCommand_AlwaysOn", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Only when file path exist.
/// </summary>
internal static string Indexer_Settings_FallbackCommand_FilePathExist {
get {
return ResourceManager.GetString("Indexer_Settings_FallbackCommand_FilePathExist", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Shows file search results on the top-level search results.
/// </summary>
internal static string Indexer_Settings_FallbackCommand_Mode {
get {
return ResourceManager.GetString("Indexer_Settings_FallbackCommand_Mode", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Always off.
/// </summary>
internal static string Indexer_Settings_FallbackCommand_Off {
get {
return ResourceManager.GetString("Indexer_Settings_FallbackCommand_Off", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Search files on this device.
/// </summary>

View File

@@ -156,10 +156,25 @@
<data name="Indexer_PlaceholderText" xml:space="preserve">
<value>Search for files and folders...</value>
</data>
<data name="Indexer_Settings_FallbackCommand_AlwaysOn" xml:space="preserve">
<value>Always on</value>
</data>
<data name="Indexer_Settings_FallbackCommand_Mode" xml:space="preserve">
<value>Shows file search results on the top-level search results</value>
</data>
<data name="Indexer_Settings_FallbackCommand_Off" xml:space="preserve">
<value>Always off</value>
</data>
<data name="Indexer_Settings_FallbackCommand_FilePathExist" xml:space="preserve">
<value>Only when file path exist</value>
</data>
<data name="Indexer_Subtitle" xml:space="preserve">
<value>Search files on this device</value>
</data>
<data name="Indexer_Title" xml:space="preserve">
<value>Search files</value>
</data>
<data name="Indexer_fallback_searchPage_title" xml:space="preserve">
<value>Search for "{0}" in files</value>
</data>
</root>

View File

@@ -0,0 +1,95 @@
// 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.Diagnostics;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Indexer.Data;
using Microsoft.CmdPal.Ext.Indexer.Indexer;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Storage.Streams;
namespace Microsoft.CmdPal.Ext.Indexer;
public sealed partial class SearchEngine : IDisposable
{
private SearchQuery _searchQuery = new();
public void Query(string query, uint queryCookie)
{
// _indexerListItems.Clear();
_searchQuery.SearchResults.Clear();
_searchQuery.CancelOutstandingQueries();
if (query == string.Empty)
{
return;
}
Stopwatch stopwatch = new();
stopwatch.Start();
_searchQuery.Execute(query, queryCookie);
stopwatch.Stop();
Logger.LogDebug($"Query time: {stopwatch.ElapsedMilliseconds} ms, query: \"{query}\"");
}
public IList<IListItem> FetchItems(int offset, int limit, uint queryCookie, out bool hasMore)
{
hasMore = false;
var results = new List<IListItem>();
if (_searchQuery != null)
{
var cookie = _searchQuery.Cookie;
if (cookie == queryCookie)
{
var index = 0;
SearchResult result;
// var hasMoreItems = _searchQuery.FetchRows(_indexerListItems.Count, limit);
var hasMoreItems = _searchQuery.FetchRows(offset, limit);
while (!_searchQuery.SearchResults.IsEmpty && _searchQuery.SearchResults.TryDequeue(out result) && ++index <= limit)
{
IconInfo icon = null;
try
{
var stream = ThumbnailHelper.GetThumbnail(result.LaunchUri).Result;
if (stream != null)
{
var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream));
icon = new IconInfo(data, data);
}
}
catch (Exception ex)
{
Logger.LogError("Failed to get the icon.", ex);
}
results.Add(new IndexerListItem(new IndexerItem
{
FileName = result.ItemDisplayName,
FullPath = result.LaunchUri,
})
{
Icon = icon,
});
}
hasMore = hasMoreItems;
}
}
return results;
}
public void Dispose()
{
_searchQuery = null;
GC.SuppressFinalize(this);
}
}

View File

@@ -32,6 +32,7 @@ internal sealed partial class FallbackSystemCommandItem : FallbackCommandItem
{
if (string.IsNullOrWhiteSpace(query))
{
Command = null;
Title = string.Empty;
Subtitle = string.Empty;
return;
@@ -58,6 +59,7 @@ internal sealed partial class FallbackSystemCommandItem : FallbackCommandItem
if (result == null)
{
Command = null;
Title = string.Empty;
Subtitle = string.Empty;

View File

@@ -10,12 +10,12 @@ namespace Microsoft.CmdPal.Ext.System.Pages;
public sealed partial class SystemCommandPage : ListPage
{
private SettingsManager _settingsManager;
private readonly SettingsManager _settingsManager;
public SystemCommandPage(SettingsManager settingsManager)
{
Title = Resources.Microsoft_plugin_ext_system_page_name;
Name = Resources.Microsoft_plugin_ext_system_page_name;
Title = Resources.Microsoft_plugin_ext_system_page_title;
Name = Resources.Microsoft_plugin_command_name_open;
Icon = IconHelpers.FromRelativePath("Assets\\SystemCommand.svg");
_settingsManager = settingsManager;
ShowDetails = true;

View File

@@ -205,7 +205,7 @@ namespace Microsoft.CmdPal.Ext.System {
}
/// <summary>
/// Looks up a localized string similar to Windows System Command.
/// Looks up a localized string similar to System Commands.
/// </summary>
public static string Microsoft_plugin_ext_system_page_name {
get {
@@ -213,6 +213,15 @@ namespace Microsoft.CmdPal.Ext.System {
}
}
/// <summary>
/// Looks up a localized string similar to Windows System Commands.
/// </summary>
public static string Microsoft_plugin_ext_system_page_title {
get {
return ResourceManager.GetString("Microsoft_plugin_ext_system_page_title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Adapter name.
/// </summary>

View File

@@ -160,8 +160,11 @@
<data name="Microsoft_plugin_ext_settings_hideDisconnectedNetworkInfo" xml:space="preserve">
<value>Hide disconnected network info</value>
</data>
<data name="Microsoft_plugin_ext_system_page_title" xml:space="preserve">
<value>Windows System Commands</value>
</data>
<data name="Microsoft_plugin_ext_system_page_name" xml:space="preserve">
<value>Windows System Command</value>
<value>System Commands</value>
</data>
<data name="Microsoft_plugin_sys_AdapterName" xml:space="preserve">
<value>Adapter name</value>

View File

@@ -14,7 +14,7 @@ public partial class SystemCommandExtensionProvider : CommandProvider
private readonly ICommandItem[] _commands;
private static readonly SettingsManager _settingsManager = new();
public static readonly SystemCommandPage Page = new(_settingsManager);
private readonly FallbackSystemCommandItem _fallbackFileItem = new(_settingsManager);
private readonly FallbackSystemCommandItem _fallbackSystemItem = new(_settingsManager);
public SystemCommandExtensionProvider()
{
@@ -23,7 +23,7 @@ public partial class SystemCommandExtensionProvider : CommandProvider
_commands = [
new CommandItem(Page)
{
Title = Resources.Microsoft_plugin_ext_system_page_name,
Title = Resources.Microsoft_plugin_ext_system_page_title,
Icon = Page.Icon,
MoreCommands = [new CommandContextItem(_settingsManager.Settings.SettingsPage)],
},
@@ -38,5 +38,5 @@ public partial class SystemCommandExtensionProvider : CommandProvider
return _commands;
}
public override IFallbackCommandItem[] FallbackCommands() => [_fallbackFileItem];
public override IFallbackCommandItem[] FallbackCommands() => [_fallbackSystemItem];
}

View File

@@ -1,709 +1,20 @@
#include "pch.h"
#include "KeyboardManagerEditorLibraryWrapper.h"
#include <algorithm>
#include <cstring>
#include <vector>
#include <string>
#include <memory>
#include <common/utils/logger_helper.h>
#include <keyboardmanager/KeyboardManagerEditor/KeyboardManagerEditor.h>
#include <keyboardmanager/KeyboardManagerEditorLibrary/EditorHelpers.h>
#include <common/interop/keyboard_layout.h>
extern "C"
// Test function to call the remapping helper function
bool CheckIfRemappingsAreValid()
{
void* CreateMappingConfiguration()
{
return new MappingConfiguration();
}
RemapBuffer remapBuffer;
void DestroyMappingConfiguration(void* config)
{
delete static_cast<MappingConfiguration*>(config);
}
// Mock valid key to key remappings
remapBuffer.push_back(RemapBufferRow{ RemapBufferItem({ (DWORD)0x41, (DWORD)0x42 }), std::wstring() });
remapBuffer.push_back(RemapBufferRow{ RemapBufferItem({ (DWORD)0x42, (DWORD)0x43 }), std::wstring() });
bool LoadMappingSettings(void* config)
{
return static_cast<MappingConfiguration*>(config)->LoadSettings();
}
auto result = LoadingAndSavingRemappingHelper::CheckIfRemappingsAreValid(remapBuffer);
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;
if (operationType == 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;
}
}
else if (operationType == 1) // RunProgram
{
if (kv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(kv.second.targetShortcut).operationType == Shortcut::OperationType::RunProgram)
{
shouldAdd = true;
}
}
else if (operationType == 2) // OpenURI
{
if (kv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(kv.second.targetShortcut).operationType == Shortcut::OperationType::OpenURI)
{
shouldAdd = true;
}
}
else if (operationType == 3)
{
if (kv.second.targetShortcut.index() == 2)
{
shouldAdd = true;
}
}
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;
if (operationType == 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;
}
}
else if (operationType == 1) // RunProgram
{
if (shortcutKv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(shortcutKv.second.targetShortcut).operationType == Shortcut::OperationType::RunProgram)
{
shouldAdd = true;
}
}
else if (operationType == 2) // OpenURI
{
if (shortcutKv.second.targetShortcut.index() == 1 &&
std::get<Shortcut>(shortcutKv.second.targetShortcut).operationType == Shortcut::OperationType::OpenURI)
{
shouldAdd = true;
}
}
else if (operationType == 3)
{
if (shortcutKv.second.targetShortcut.index() == 2)
{
shouldAdd = true;
}
}
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);
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;
}
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 = 0)
{
auto mappingConfig = static_cast<MappingConfiguration*>(config);
Shortcut originalShortcut(originalKeys);
KeyShortcutTextUnion targetShortcut;
switch (operationType)
{
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)
{
if (keyName == nullptr)
{
return 0;
}
LayoutMap layoutMap;
std::wstring name(keyName);
return static_cast<int>(layoutMap.GetKeyFromName(name));
}
// 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)
{
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;
}
return result == ShortcutErrorType::NoError;
}
// 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,77 +4,4 @@
#include <keyboardmanager/common/Input.h>
#include <keyboardmanager/common/MappingConfiguration.h>
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);
__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);
extern "C" __declspec(dllexport) bool CheckIfRemappingsAreValid();

View File

@@ -8,8 +8,7 @@
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<ResourceDictionary Source="/Styles/KeyVisual.xaml" />
<ResourceDictionary Source="/Styles/CommonStyle.xaml" />
<!-- Other merged dictionaries here -->
</ResourceDictionary.MergedDictionaries>
<!-- Other app resources here -->
</ResourceDictionary>

View File

@@ -7,11 +7,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using KeyboardManagerEditorUI.Helpers;
using ManagedCommon;
using Microsoft.UI;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
@@ -39,13 +35,8 @@ namespace KeyboardManagerEditorUI
public App()
{
this.InitializeComponent();
Task.Run(() =>
{
Logger.InitializeLogger("\\Keyboard Manager\\WinUI3Editor\\Logs");
});
UnhandledException += App_UnhandledException;
Logger.InitializeLogger("\\Keyboard Manager\\WinUI3Editor\\Logs");
Logger.LogInfo("keyboard-manager WinUI3 editor logger is initialized");
}
/// <summary>
@@ -55,37 +46,10 @@ namespace KeyboardManagerEditorUI
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
window = new MainWindow();
var appWindow = window.AppWindow;
var windowSize = new Windows.Graphics.SizeInt32(EditorConstants.DefaultEditorWindowWidth, EditorConstants.DefaultEditorWindowHeight);
appWindow.Resize(windowSize);
window.DispatcherQueue.TryEnqueue(() =>
{
window.Activate();
window.DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
(window.Content as FrameworkElement)?.UpdateLayout();
});
});
window.Activate();
Logger.LogInfo("keyboard-manager WinUI3 editor window is launched");
}
/// <summary>
/// Log the unhandled exception for the editor.
/// </summary>
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
Logger.LogError("Unhandled exception", e.Exception);
}
public Window? GetWindow()
{
return window;
}
private Window? window;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -1,22 +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.Linq;
using System.Text;
using System.Threading.Tasks;
namespace KeyboardManagerEditorUI.Helpers
{
public static class EditorConstants
{
// Default window size
public const int DefaultEditorWindowWidth = 960;
public const int DefaultEditorWindowHeight = 600;
// Default notification timeout
public const int DefaultNotificationTimeout = 1500;
}
}

View File

@@ -1,12 +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.
namespace KeyboardManagerEditorUI.Helpers
{
public enum KeyInputMode
{
OriginalKeys,
RemappedKeys,
}
}

View File

@@ -1,239 +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.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;
foreach (var key in _keyPressOrder)
{
if (!_currentlyPressedKeys.Contains(key))
{
continue;
}
if (RemappingHelper.IsModifierKey(key))
{
if (!modifierKeys.Contains(key))
{
modifierKeys.Add(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));
}
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
{
void OnKeyDown(VirtualKey key, List<string> formattedKeys);
void OnKeyUp(VirtualKey key, List<string> formattedKeys)
{
}
void ClearKeys();
void OnInputLimitReached();
}
}

View File

@@ -1,47 +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.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace KeyboardManagerEditorUI.Helpers
{
public partial class Remapping : INotifyPropertyChanged
{
public List<string> OriginalKeys { 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; } = "All Apps";
private bool IsEnabledValue { get; set; } = true;
public event PropertyChangedEventHandler? PropertyChanged;
public bool IsEnabled
{
get => IsEnabledValue;
set
{
if (IsEnabledValue != value)
{
IsEnabledValue = value;
OnPropertyChanged();
}
}
}
private void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@@ -1,144 +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.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using KeyboardManagerEditorUI.Interop;
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)
{
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)
{
if (remappedKeys.Count == 1)
{
int targetKey = mappingService.GetKeyCodeFromName(remappedKeys[0]);
if (targetKey != 0)
{
mappingService.AddSingleKeyMapping(originalKey, targetKey);
}
}
else
{
string targetKeys = string.Join(";", remappedKeys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
mappingService.AddSingleKeyMapping(originalKey, targetKeys);
}
}
}
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)));
if (isAppSpecific && !string.IsNullOrEmpty(appName))
{
mappingService.AddShortcutMapping(originalKeysString, targetKeysString, appName);
}
else
{
mappingService.AddShortcutMapping(originalKeysString, targetKeysString);
}
}
return mappingService.SaveSettings();
}
catch (Exception ex)
{
Logger.LogError("Error saving mapping: " + ex.Message);
return false;
}
}
public static bool DeleteRemapping(KeyboardMappingService mappingService, Remapping remapping)
{
if (mappingService == null)
{
return false;
}
try
{
if (remapping.OriginalKeys.Count == 1)
{
// Single key mapping
int originalKey = mappingService.GetKeyCodeFromName(remapping.OriginalKeys[0]);
if (originalKey != 0)
{
if (mappingService.DeleteSingleKeyMapping(originalKey))
{
// Save settings after successful deletion
return mappingService.SaveSettings();
}
}
}
else if (remapping.OriginalKeys.Count > 1)
{
// Shortcut mapping
string originalKeysString = string.Join(";", remapping.OriginalKeys.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);
}
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

@@ -1,23 +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.Linq;
using System.Text;
using System.Threading.Tasks;
namespace KeyboardManagerEditorUI.Helpers
{
public class TextMapping
{
public List<string> Keys { get; set; } = new List<string>();
public string Text { get; set; } = string.Empty;
public bool IsAllApps { get; set; } = true;
public string AppName { get; set; } = "All Apps";
}
}

View File

@@ -1,19 +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.Linq;
using System.Text;
using System.Threading.Tasks;
namespace KeyboardManagerEditorUI.Helpers
{
public class URLShortcut
{
public List<string> Shortcut { get; set; } = new List<string>();
public string URL { get; set; } = string.Empty;
}
}

View File

@@ -1,25 +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.Linq;
using System.Text;
using System.Threading.Tasks;
namespace KeyboardManagerEditorUI.Helpers
{
public enum ValidationErrorType
{
NoError,
EmptyOriginalKeys,
EmptyRemappedKeys,
ModifierOnly,
EmptyAppName,
IllegalShortcut,
DuplicateMapping,
SelfMapping,
EmptyTargetText,
}
}

View File

@@ -1,299 +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.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using KeyboardManagerEditorUI.Interop;
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.") },
};
public static ValidationErrorType ValidateKeyMapping(
List<string> originalKeys,
List<string> remappedKeys,
bool isAppSpecific,
string appName,
KeyboardMappingService mappingService,
bool isEditMode = false,
Remapping? editingRemapping = null)
{
// Check if original keys are empty
if (originalKeys == null || originalKeys.Count == 0)
{
return ValidationErrorType.EmptyOriginalKeys;
}
// Check if remapped keys are empty
if (remappedKeys == null || remappedKeys.Count == 0)
{
return ValidationErrorType.EmptyRemappedKeys;
}
// Check if shortcut contains only modifier keys
if ((originalKeys.Count > 1 && ContainsOnlyModifierKeys(originalKeys)) ||
(remappedKeys.Count > 1 && ContainsOnlyModifierKeys(remappedKeys)))
{
return ValidationErrorType.ModifierOnly;
}
// Check if app specific is checked but no app name is provided
if (isAppSpecific && string.IsNullOrWhiteSpace(appName))
{
return ValidationErrorType.EmptyAppName;
}
// Check if this is a shortcut (multiple keys) and if it's an illegal combination
if (originalKeys.Count > 1)
{
string shortcutKeysString = string.Join(";", originalKeys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
if (KeyboardManagerInterop.IsShortcutIllegal(shortcutKeysString))
{
return ValidationErrorType.IllegalShortcut;
}
}
// Check for duplicate mappings
if (IsDuplicateMapping(originalKeys, isAppSpecific, appName, mappingService, isEditMode, editingRemapping))
{
return ValidationErrorType.DuplicateMapping;
}
// Check for self-mapping
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)
{
// Check if original keys are empty
if (keys == null || keys.Count == 0)
{
return ValidationErrorType.EmptyOriginalKeys;
}
// Check if text content is empty
if (string.IsNullOrWhiteSpace(textContent))
{
return ValidationErrorType.EmptyTargetText;
}
// Check if shortcut contains only modifier keys
if (keys.Count > 1 && ContainsOnlyModifierKeys(keys))
{
return ValidationErrorType.ModifierOnly;
}
// Check if app specific is checked but no app name is provided
if (isAppSpecific && string.IsNullOrWhiteSpace(appName))
{
return ValidationErrorType.EmptyAppName;
}
// Check if this is a shortcut (multiple keys) and if it's an illegal combination
if (keys.Count > 1)
{
string shortcutKeysString = string.Join(";", keys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
if (KeyboardManagerInterop.IsShortcutIllegal(shortcutKeysString))
{
return ValidationErrorType.IllegalShortcut;
}
}
// No errors found
return ValidationErrorType.NoError;
}
public static bool IsDuplicateMapping(
List<string> originalKeys,
bool isAppSpecific,
string appName,
KeyboardMappingService mappingService,
bool isEditMode = false,
Remapping? editingRemapping = null)
{
if (mappingService == null || originalKeys == null || originalKeys.Count == 0)
{
return false;
}
// For single key remapping
if (originalKeys.Count == 1)
{
int originalKeyCode = mappingService.GetKeyCodeFromName(originalKeys[0]);
if (originalKeyCode == 0)
{
return false;
}
// Check if the key is already remapped
foreach (var mapping in mappingService.GetSingleKeyMappings())
{
if (mapping.OriginalKey == originalKeyCode)
{
// Skip if the remapping is the same as the one being edited
if (isEditMode && editingRemapping != null &&
editingRemapping.OriginalKeys.Count == 1 &&
mappingService.GetKeyCodeFromName(editingRemapping.OriginalKeys[0]) == originalKeyCode)
{
continue;
}
return true;
}
}
}
// For shortcut remapping
else
{
string originalKeysString = string.Join(";", originalKeys.Select(
k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
// Don't check for duplicates if the original keys are the same as the remapping being edited
bool isEditingExistingRemapping = false;
if (isEditMode && editingRemapping != null)
{
string editingOriginalKeysString = string.Join(";", editingRemapping.OriginalKeys.Select(k =>
mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
if (KeyboardManagerInterop.AreShortcutsEqual(originalKeysString, editingOriginalKeysString))
{
isEditingExistingRemapping = true;
}
}
// Check if the shortcut is already remapped in the same app context
foreach (var mapping in mappingService.GetShortcutMappingsByType(ShortcutOperationType.RemapShortcut))
{
if (KeyboardManagerInterop.AreShortcutsEqual(originalKeysString, mapping.OriginalKeys))
{
// If both are global (all apps)
if (!isAppSpecific && string.IsNullOrEmpty(mapping.TargetApp))
{
// Skip if the remapping is the same as the one being edited
if (editingRemapping != null && editingRemapping.OriginalKeys.Count > 1 && editingRemapping.IsAllApps && isEditingExistingRemapping)
{
continue;
}
return true;
}
// If both are for the same specific app
else if (isAppSpecific && !string.IsNullOrEmpty(mapping.TargetApp)
&& string.Equals(mapping.TargetApp, appName, StringComparison.OrdinalIgnoreCase))
{
// Skip if the remapping is the same as the one being edited
if (editingRemapping != null && editingRemapping.OriginalKeys.Count > 1 && !editingRemapping.IsAllApps &&
string.Equals(editingRemapping.AppName, appName, StringComparison.OrdinalIgnoreCase) && isEditingExistingRemapping)
{
continue;
}
return true;
}
}
}
}
return false;
}
public static bool IsSelfMapping(List<string> originalKeys, List<string> remappedKeys, KeyboardMappingService mappingService)
{
if (mappingService == null)
{
return false;
}
// If either list is empty, it's not a self-mapping
if (originalKeys == null || remappedKeys == null ||
originalKeys.Count == 0 || remappedKeys.Count == 0)
{
return false;
}
string originalKeysString = string.Join(";", originalKeys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
string remappedKeysString = string.Join(";", remappedKeys.Select(k => mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
return KeyboardManagerInterop.AreShortcutsEqual(originalKeysString, remappedKeysString);
}
public static bool ContainsOnlyModifierKeys(List<string> keys)
{
if (keys == null || keys.Count == 0)
{
return false;
}
foreach (string key in keys)
{
int keyCode = KeyboardManagerInterop.GetKeyCodeFromName(key);
var keyType = (KeyType)KeyboardManagerInterop.GetKeyType(keyCode);
// If any key is an action key, return false
if (keyType == KeyType.Action)
{
return false;
}
}
// All keys are modifier keys
return true;
}
public static bool IsKeyOrphaned(int originalKey, KeyboardMappingService mappingService)
{
// Check all single key mappings
foreach (var mapping in mappingService.GetSingleKeyMappings())
{
if (!mapping.IsShortcut && int.TryParse(mapping.TargetKey, out int targetKey) && targetKey == originalKey)
{
return false;
}
}
// Check all 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;
}
}
// No mapping found for the original key
return true;
}
}
}

View File

@@ -1,21 +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.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

@@ -1,19 +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.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

@@ -1,21 +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.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

@@ -1,159 +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.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace KeyboardManagerEditorUI.Interop
{
public static class KeyboardManagerInterop
{
private const string DllName = "Powertoys.KeyboardManagerEditorLibraryWrapper.dll";
// Configuration Management
[DllImport(DllName)]
internal static extern IntPtr CreateMappingConfiguration();
[DllImport(DllName)]
internal static extern void DestroyMappingConfiguration(IntPtr config);
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool LoadMappingSettings(IntPtr config);
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool SaveMappingSettings(IntPtr config);
// Get Mapping Functions
[DllImport(DllName)]
internal static extern int GetSingleKeyRemapCount(IntPtr config);
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetSingleKeyRemap(IntPtr config, int index, ref SingleKeyMapping mapping);
[DllImport(DllName)]
internal static extern int GetSingleKeyToTextRemapCount(IntPtr config);
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetSingleKeyToTextRemap(IntPtr config, int index, ref KeyboardTextMapping mapping);
[DllImport(DllName)]
internal static extern int GetShortcutRemapCount(IntPtr config);
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetShortcutRemap(IntPtr config, int index, ref ShortcutMapping mapping);
[DllImport(DllName)]
internal static extern int GetShortcutRemapCountByType(IntPtr config, int operationType);
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetShortcutRemapByType(IntPtr config, int operationType, int index, ref ShortcutMapping mapping);
// Add Mapping Functions
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool AddSingleKeyRemap(IntPtr config, int originalKey, int targetKey);
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool AddSingleKeyToTextRemap(IntPtr config, int originalKey, [MarshalAs(UnmanagedType.LPWStr)] string targetText);
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool AddSingleKeyToShortcutRemap(IntPtr config, int originalKey, [MarshalAs(UnmanagedType.LPWStr)] string targetKeys);
[DllImport(DllName)]
[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);
// Delete Mapping Functions
[DllImport(DllName)]
internal static extern bool DeleteSingleKeyRemap(IntPtr mappingConfiguration, int originalKey);
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool DeleteSingleKeyToTextRemap(IntPtr config, int originalKey);
[DllImport(DllName)]
internal static extern bool DeleteShortcutRemap(IntPtr mappingConfiguration, [MarshalAs(UnmanagedType.LPWStr)] string originalKeys, [MarshalAs(UnmanagedType.LPWStr)] string targetApp);
// Key Utility Functions
[DllImport(DllName)]
internal static extern int GetKeyCodeFromName([MarshalAs(UnmanagedType.LPWStr)] string keyName);
[DllImport(DllName, CharSet = CharSet.Unicode)]
internal static extern void GetKeyDisplayName(int keyCode, [Out] StringBuilder keyName, int maxLength);
[DllImport(DllName)]
internal static extern int GetKeyType(int keyCode);
// Validation Functions
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool IsShortcutIllegal([MarshalAs(UnmanagedType.LPWStr)] string shortcutKeys);
[DllImport(DllName)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool AreShortcutsEqual([MarshalAs(UnmanagedType.LPWStr)] string lShort, [MarshalAs(UnmanagedType.LPWStr)] string rShortcut);
// String Management Functions
[DllImport(DllName)]
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)]
public struct SingleKeyMapping
{
public int OriginalKey;
public IntPtr TargetKey;
[MarshalAs(UnmanagedType.Bool)]
public bool IsShortcut;
}
[StructLayout(LayoutKind.Sequential)]
public struct KeyboardTextMapping
{
public int OriginalKey;
public IntPtr TargetText;
}
[StructLayout(LayoutKind.Sequential)]
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

@@ -1,243 +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.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;
}
return KeyboardManagerInterop.GetKeyCodeFromName(keyName);
}
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 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

@@ -1,31 +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.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 UriToOpen { get; set; } = string.Empty;
}
}

View File

@@ -1,20 +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.Linq;
using System.Text;
using System.Threading.Tasks;
namespace KeyboardManagerEditorUI.Interop
{
public enum ShortcutOperationType
{
RemapShortcut = 0,
RunProgram = 1,
OpenUri = 2,
RemapText = 3,
}
}

View File

@@ -18,22 +18,6 @@
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\$(MSBuildProjectName)</OutputPath>
</PropertyGroup>
<PropertyGroup>
<EnableDefaultXamlItems>true</EnableDefaultXamlItems>
<EnableXamlJitOptimization>true</EnableXamlJitOptimization>
</PropertyGroup>
<ItemGroup>
<None Remove="Pages\ExistingUI.xaml" />
<None Remove="Pages\Programs.xaml" />
<None Remove="Pages\Text.xaml" />
<None Remove="Pages\URLs.xaml" />
<None Remove="Styles\CommonStyle.xaml" />
<None Remove="Styles\InputControl.xaml" />
<None Remove="Styles\KeyVisual.xaml" />
<None Remove="Styles\TextPageInputControl.xaml" />
</ItemGroup>
<ItemGroup>
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
@@ -49,7 +33,6 @@
<ItemGroup>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls.Markdown" />
<!-- 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" />
</ItemGroup>
@@ -59,49 +42,7 @@
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
</ItemGroup>
<ItemGroup>
<Page Update="Styles\TextPageInputControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Styles\CommonStyle.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Pages\URLs.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Pages\Text.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Pages\Programs.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Pages\Remappings.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Styles\InputControl.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Styles\KeyVisual.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Pages\ExistingUI.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Folder Include="Assets\" />
</ItemGroup>
<!--

View File

@@ -6,68 +6,13 @@
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"
Title="KeyboardManagerEditorUI"
mc:Ignorable="d">
<Window.SystemBackdrop>
<MicaBackdrop />
</Window.SystemBackdrop>
<Grid
x:Name="LayoutRoot"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="32" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid x:Name="titleBar">
<StackPanel
Grid.ColumnSpan="2"
Margin="16,8,8,8"
VerticalAlignment="Top"
Orientation="Horizontal">
<Image Width="16" Source="ms-appx:///Assets/FluentIconsKeyboardManager.png" />
<TextBlock
Margin="12,0,0,0"
Style="{StaticResource CaptionTextBlockStyle}"
Text="Keyboard Manager" />
</StackPanel>
</Grid>
<NavigationView
x:Name="RootView"
Grid.Row="1"
IsBackButtonVisible="Collapsed"
IsBackEnabled="False"
IsPaneToggleButtonVisible="False"
IsSettingsVisible="False"
PaneDisplayMode="Top"
SelectionChanged="RootView_SelectionChanged">
<NavigationView.MenuItems>
<NavigationViewItem Content="Remappings" Tag="Remappings">
<NavigationViewItem.Icon>
<FontIcon Glyph="&#xEDA7;" />
</NavigationViewItem.Icon>
</NavigationViewItem>
<NavigationViewItem Content="Text" Tag="Text">
<NavigationViewItem.Icon>
<FontIcon Glyph="&#xE8D2;" />
</NavigationViewItem.Icon>
</NavigationViewItem>
<NavigationViewItem Content="Programs" Tag="Programs">
<NavigationViewItem.Icon>
<FontIcon Glyph="&#xECAA;" />
</NavigationViewItem.Icon>
</NavigationViewItem>
<NavigationViewItem Content="URLs" Tag="URLs">
<NavigationViewItem.Icon>
<FontIcon Glyph="&#xE8A7;" />
</NavigationViewItem.Icon>
</NavigationViewItem>
</NavigationView.MenuItems>
<NavigationView.Content>
<Frame x:Name="NavigationFrame" Margin="0,0,0,0" />
</NavigationView.Content>
</NavigationView>
</Grid>
</Window>
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal">
<Button x:Name="myButton" Click="MyButton_Click">Click Me</Button>
</StackPanel>
</Window>

View File

@@ -4,12 +4,10 @@
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.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
@@ -22,52 +20,23 @@ 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();
this.ExtendsContentIntoTitleBar = true;
this.SetTitleBar(titleBar);
this.Activated += MainWindow_Activated;
this.Closed += MainWindow_Closed;
// Set the default page
RootView.SelectedItem = RootView.MenuItems[0];
}
private void MainWindow_Activated(object sender, WindowActivatedEventArgs args)
private void MyButton_Click(object sender, RoutedEventArgs e)
{
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;
}
private void RootView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
{
// Cleanup the keyboard hook when the selected page changes
KeyboardHookHelper.Instance.CleanupHook();
if (args.SelectedItem is NavigationViewItem selectedItem)
{
switch ((string)selectedItem.Tag)
{
case "Remappings": NavigationFrame.Navigate(typeof(Pages.Remappings)); break;
case "Programs": NavigationFrame.Navigate(typeof(Pages.Programs)); break;
case "Text": NavigationFrame.Navigate(typeof(Pages.Text)); break;
case "URLs": NavigationFrame.Navigate(typeof(Pages.URLs)); break;
}
}
// Call the C++ function to check if the current remappings are valid
myButton.Content = CheckIfRemappingsAreValid() ? "Valid" : "Invalid";
}
}
}

View File

@@ -1,96 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="KeyboardManagerEditorUI.Pages.ExistingUI"
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.Pages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<!-- WinUI3 implementation of the existing Keyboard Manager UI -->
<Grid Padding="16">
<StackPanel>
<TextBlock
Margin="10"
FontSize="24"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Text="Remap keys" />
<TextBlock
Margin="10"
FontSize="16"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Select the key you want to change and then configure the key, shortcut or text you want it to send."
TextWrapping="Wrap" />
<TextBlock
Margin="10"
FontSize="16"
FontStyle="Italic"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Example of a remapping: Select A and send &quot;Ctrl+C&quot;, &quot;A&quot; would be your &quot;Select&quot; and &quot;Ctrl+C&quot; would be your &quot;To send&quot; command."
TextWrapping="Wrap" />
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="240" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="240" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock
Margin="0,12,0,0"
FontSize="16"
FontWeight="Bold"
Text="Selected:" />
<Grid Grid.Row="1" Margin="0,8,0,0">
<StackPanel>
<ToggleButton Click="Button_Click" Content="Select" />
<ComboBox
x:Name="keyComboBox"
Width="250"
PlaceholderText="select keys please" />
</StackPanel>
</Grid>
<TextBlock
Grid.Row="1"
Grid.Column="1"
Margin="24,0,24,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
Text="&#xE0AB;" />
<TextBlock
Grid.Column="2"
Margin="0,12,0,0"
FontSize="16"
FontWeight="Bold"
Text="To Send:" />
<Grid
Grid.Row="1"
Grid.Column="2"
Margin="0,8,0,0">
<StackPanel>
<ToggleButton Click="Button_Click" Content="Click to select" />
<ComboBox
x:Name="newKeyComboBox"
Width="250"
PlaceholderText="select keys please" />
</StackPanel>
</Grid>
</Grid>
<Button
Margin="10"
Content="+ Add Key Remapping"
Style="{StaticResource AccentButtonStyle}" />
</StackPanel>
</Grid>
</UserControl>

View File

@@ -1,91 +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.Collections.ObjectModel;
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;
// WinUI3 implementation of the Existing Keyboard Manager UI
namespace KeyboardManagerEditorUI.Pages
{
public sealed partial class ExistingUI : UserControl
{
public class KeyboardKey
{
public int KeyCode { get; set; }
public string KeyName { get; set; } = string.Empty;
public override string ToString() => KeyName;
}
// Struct to hold key code and name pairs
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct KeyNamePair
{
public int KeyCode;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
public string KeyName;
}
[DllImport("PowerToys.KeyboardManagerEditorLibraryWrapper.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern int GetKeyboardKeysList(bool isShortcut, [Out] KeyNamePair[] keyList, int maxCount);
public List<KeyboardKey> KeysList { get; private set; } = new List<KeyboardKey>();
public ExistingUI()
{
this.InitializeComponent();
LoadKeyboardKeys();
keyComboBox.ItemsSource = KeysList;
keyComboBox.DisplayMemberPath = "KeyName";
newKeyComboBox.ItemsSource = KeysList;
newKeyComboBox.DisplayMemberPath = "KeyCode";
}
private void LoadKeyboardKeys()
{
const int MaxKeys = 300;
KeyNamePair[] keyNamePairs = new KeyNamePair[MaxKeys];
int count = GetKeyboardKeysList(false, keyNamePairs, MaxKeys);
KeysList = new List<KeyboardKey>(count);
for (int i = 0; i < count; i++)
{
KeysList.Add(new KeyboardKey
{
KeyCode = keyNamePairs[i].KeyCode,
KeyName = keyNamePairs[i].KeyName,
});
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
var button = sender as Button;
if (button == null)
{
// button.Background = (SolidColorBrush)Application.Current.Resources["SystemControlBackgroundAccentBrush"];
return;
}
}
}
}

View File

@@ -1,221 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="KeyboardManagerEditorUI.Pages.Programs"
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:helper="using:KeyboardManagerEditorUI.Helpers"
xmlns:local="using:KeyboardManagerEditorUI.Pages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:styles="using:KeyboardManagerEditorUI.Styles"
mc:Ignorable="d">
<Grid Padding="16">
<StackPanel Orientation="Vertical" Spacing="12">
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Program shortcuts allow you to open specific applications when you use the configured shortcut"
TextWrapping="Wrap" />
<Button
x:Name="NewShortcutBtn"
Height="36"
Margin="0,12,0,0"
Click="NewShortcutBtn_Click">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon
FontSize="14"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="&#xE109;" />
<TextBlock VerticalAlignment="Center" Text="New" />
</StackPanel>
</Button>
<Grid
HorizontalAlignment="Stretch"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="{StaticResource OverlayCornerRadius}">
<Grid.RowDefinitions>
<RowDefinition Height="48" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="236" />
<ColumnDefinition Width="236" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="84" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Margin="16,-2,0,0"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Shortcut" />
<AppBarSeparator
Grid.Column="1"
Margin="-6,4,0,4"
HorizontalAlignment="Left" />
<TextBlock
Grid.Column="1"
Margin="12,-2,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Program" />
<Rectangle
Grid.ColumnSpan="4"
Height="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Fill="{ThemeResource CardStrokeColorDefaultBrush}" />
<ListView
Grid.Row="1"
Grid.ColumnSpan="4"
IsItemClickEnabled="True"
ItemClick="ListView_ItemClick"
ItemsSource="{x:Bind Shortcuts}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="helper:URLShortcut">
<Grid Height="48">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="232" />
<ColumnDefinition Width="238" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="84" />
</Grid.ColumnDefinitions>
<Rectangle
Grid.ColumnSpan="5"
Height="1"
Margin="-16,0,-16,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Fill="{ThemeResource CardStrokeColorDefaultBrush}"
Opacity="0.8" />
<ItemsControl
Grid.Column="0"
VerticalAlignment="Center"
ItemsSource="{x:Bind Shortcut}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<styles:KeyVisual
HorizontalAlignment="Left"
Content="{Binding}"
VisualType="SmallOutline" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<StackPanel
Grid.Column="1"
Orientation="Horizontal"
Spacing="8">
<TextBlock VerticalAlignment="Center" Text="WindowsTerminal.exe" />
<FontIcon
VerticalAlignment="Center"
FontSize="14"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="&#xE946;">
<ToolTipService.ToolTip>
<TextBlock>
<Run FontWeight="SemiBold" Text="Arguments:" />
<Run Text="-config json" />
<LineBreak />
<Run FontWeight="SemiBold" Text="Starting directory:" />
<Run Text="C:\Users\Dev\PowerToys\bin" />
<LineBreak />
<Run FontWeight="SemiBold" Text="Launch state:" />
<Run Text="Minimized" />
</TextBlock>
</ToolTipService.ToolTip>
</FontIcon>
</StackPanel>
<ToggleSwitch
Grid.ColumnSpan="4"
Margin="0,0,-112,0"
HorizontalAlignment="Right"
IsOn="True"
OffContent=""
OnContent="" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</StackPanel>
<ContentDialog
x:Name="KeyDialog"
Title="New program shortcut"
Width="480"
MinWidth="600"
MaxWidth="600"
PrimaryButtonStyle="{StaticResource AccentButtonStyle}"
PrimaryButtonText="Save"
SecondaryButtonText="Cancel">
<StackPanel Width="480" Orientation="Vertical">
<TextBlock Margin="0,12,0,8" Text="Shortcut" />
<ToggleButton
x:Name="OriginalToggleBtn"
Padding="0,24,0,24"
HorizontalAlignment="Stretch"
Style="{StaticResource CustomShortcutToggleButtonStyle}">
<ToggleButton.Content>
<StackPanel Orientation="Horizontal" Spacing="8">
<styles:KeyVisual Content="Win (Left)" VisualType="SmallOutline" />
<styles:KeyVisual Content="T" VisualType="SmallOutline" />
</StackPanel>
</ToggleButton.Content>
</ToggleButton>
<Grid
Margin="0,24,0,0"
ColumnSpacing="4"
RowSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBox Header="Application" Text="WindowsTerminal.exe" />
<Button
Grid.Column="1"
Height="32"
VerticalAlignment="Bottom">
<FontIcon FontSize="14" Glyph="&#xE8DA;" />
</Button>
<TextBox
Grid.Row="1"
Header="Arguments"
Text="-config" />
<TextBox
Grid.Row="2"
Header="Start in directory"
Text="C:\Program Files\WindowsApps\Microsoft.WindowsTerminal_1.21.10351.0_x64__8wekyb3d8bbwe" />
<Button
Grid.Row="2"
Grid.Column="1"
Height="32"
VerticalAlignment="Bottom">
<FontIcon FontSize="14" Glyph="&#xE8DA;" />
</Button>
<ComboBox
Grid.Row="3"
Header="Launch state"
SelectedIndex="0">
<ComboBoxItem Content="Show window" />
<ComboBoxItem Content="Minimized" />
</ComboBox>
</Grid>
</StackPanel>
</ContentDialog>
</Grid>
</Page>

View File

@@ -1,46 +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.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using KeyboardManagerEditorUI.Helpers;
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.Pages
{
public sealed partial class Programs : Page
{
public ObservableCollection<URLShortcut> Shortcuts { get; set; }
public Programs()
{
this.InitializeComponent();
Shortcuts = new ObservableCollection<URLShortcut>();
Shortcuts.Add(new URLShortcut() { Shortcut = new List<string>() { "Win (Left)", "T", }, URL = "https://www.bing.com" });
}
private async void NewShortcutBtn_Click(object sender, RoutedEventArgs e)
{
await KeyDialog.ShowAsync();
}
private async void ListView_ItemClick(object sender, ItemClickEventArgs e)
{
await KeyDialog.ShowAsync();
}
}
}

View File

@@ -1,221 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="KeyboardManagerEditorUI.Pages.Remappings"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helper="using:KeyboardManagerEditorUI.Helpers"
xmlns:local="using:KeyboardManagerEditorUI.Pages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:styles="using:KeyboardManagerEditorUI.Styles"
mc:Ignorable="d">
<Grid Padding="16">
<StackPanel Orientation="Vertical" Spacing="12">
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Remappings allow you to reconfigure a single key or shortcut to a new key or shortcut"
TextWrapping="Wrap" />
<Button
x:Name="NewRemappingBtn"
Height="36"
Margin="0,12,0,0"
Click="NewRemappingBtn_Click">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon
FontSize="14"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="&#xE109;" />
<TextBlock VerticalAlignment="Center" Text="New" />
</StackPanel>
</Button>
<Grid
HorizontalAlignment="Stretch"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="{StaticResource OverlayCornerRadius}">
<Grid.RowDefinitions>
<RowDefinition Height="48" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="236" />
<ColumnDefinition Width="236" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="84" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Margin="16,-2,0,0"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Original key(s)" />
<AppBarSeparator
Grid.Column="1"
Margin="-6,4,0,4"
HorizontalAlignment="Left" />
<TextBlock
Grid.Column="1"
Margin="12,-2,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="New key(s)" />
<AppBarSeparator
Grid.Column="2"
Margin="-6,4,0,4"
HorizontalAlignment="Left" />
<TextBlock
Grid.Column="2"
Margin="12,-2,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Applicable apps" />
<Rectangle
Grid.ColumnSpan="4"
Height="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Fill="{ThemeResource CardStrokeColorDefaultBrush}" />
<ListView
Grid.Row="1"
Grid.ColumnSpan="4"
IsItemClickEnabled="True"
ItemClick="ListView_ItemClick"
ItemsSource="{x:Bind RemappingList}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="helper:Remapping">
<Grid Height="Auto" MinHeight="48">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="236" />
<ColumnDefinition Width="Auto" MinWidth="236" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="84" />
</Grid.ColumnDefinitions>
<Rectangle
Grid.ColumnSpan="5"
Height="1"
Margin="-16,0,-16,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Fill="{ThemeResource CardStrokeColorDefaultBrush}"
Opacity="0.8" />
<ItemsControl
Grid.Column="0"
VerticalAlignment="Center"
IsEnabled="{x:Bind IsEnabled, Mode=OneWay}"
ItemsSource="{x:Bind OriginalKeys}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:WrapPanel
MaxWidth="230"
HorizontalSpacing="4"
Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<styles:KeyVisual
HorizontalAlignment="Left"
Content="{Binding}"
VisualType="SmallOutline" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl
Grid.Column="1"
Margin="0,6,0,6"
VerticalAlignment="Center"
IsEnabled="{x:Bind IsEnabled, Mode=OneWay}"
ItemsSource="{x:Bind RemappedKeys}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:WrapPanel
MaxWidth="230"
HorizontalSpacing="4"
Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<styles:KeyVisual
HorizontalAlignment="Left"
Content="{Binding}"
VisualType="Small" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock
Grid.Column="2"
Margin="-4,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="12"
Text="{x:Bind AppName}" />
<Button
Grid.Column="3"
Margin="0,0,4,0"
Padding="8,4"
HorizontalAlignment="Right"
VerticalAlignment="Center"
AutomationProperties.Name="Delete remapping"
Background="Transparent"
BorderThickness="0"
Click="DeleteButton_Click"
ToolTipService.ToolTip="Delete remapping">
<FontIcon
FontFamily="{StaticResource SymbolThemeFontFamily}"
FontSize="16"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Glyph="&#xE74D;" />
</Button>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</StackPanel>
<ContentDialog
x:Name="KeyDialog"
Title="Remappings"
MinWidth="600"
MaxWidth="600"
PrimaryButtonStyle="{StaticResource AccentButtonStyle}"
PrimaryButtonText="Save"
SecondaryButtonText="Cancel">
<Grid>
<styles:InputControl x:Name="RemappingControl" />
</Grid>
</ContentDialog>
<TeachingTip
x:Name="OrphanedKeysTeachingTip"
Title="Orphaned Keys Warning"
ActionButtonClick="OrphanedKeysTeachingTip_ActionButtonClick"
ActionButtonContent="Continue anyway"
ActionButtonStyle="{StaticResource AccentButtonStyle}"
CloseButtonClick="OrphanedKeysTeachingTip_CloseButtonClick"
CloseButtonContent="Cancel"
IsLightDismissEnabled="False"
PreferredPlacement="Center">
<TeachingTip.IconSource>
<SymbolIconSource Symbol="Important" />
</TeachingTip.IconSource>
</TeachingTip>
<TeachingTip
x:Name="ValidationTeachingTip"
CloseButtonClick="ValidationTeachingTip_CloseButtonClick"
CloseButtonContent="OK"
IsLightDismissEnabled="True"
PreferredPlacement="Center">
<TeachingTip.IconSource>
<SymbolIconSource Symbol="Important" />
</TeachingTip.IconSource>
</TeachingTip>
</Grid>
</Page>

View File

@@ -1,370 +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.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text;
using System.Threading.Tasks;
using KeyboardManagerEditorUI.Helpers;
using KeyboardManagerEditorUI.Interop;
using ManagedCommon;
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 static KeyboardManagerEditorUI.Helpers.ValidationHelper;
namespace KeyboardManagerEditorUI.Pages
{
/// <summary>
/// The Remapping page that allow users to configure a single key or shortcut to a new key or shortcut
/// </summary>
public sealed partial class Remappings : Page, IDisposable
{
private KeyboardMappingService? _mappingService;
// Flag to indicate if the user is editing an existing remapping
private bool _isEditMode;
private Remapping? _editingRemapping;
private bool _disposed;
// The list of single key mappings
public ObservableCollection<KeyMapping> SingleKeyMappings { get; } = new ObservableCollection<KeyMapping>();
// The list of shortcut key mappings
public ObservableCollection<ShortcutKeyMapping> ShortcutKeyMappings { get; } = new ObservableCollection<ShortcutKeyMapping>();
// The full list of remappings
public ObservableCollection<Remapping> RemappingList { get; set; }
public Remappings()
{
this.InitializeComponent();
RemappingList = new ObservableCollection<Remapping>();
_mappingService = new KeyboardMappingService();
// Load all existing remappings
LoadMappings();
this.Unloaded += Remappings_Unloaded;
}
private void Remappings_Unloaded(object sender, RoutedEventArgs e)
{
// Make sure we unregister the handler when the page is unloaded
UnregisterWindowActivationHandler();
KeyboardHookHelper.Instance.CleanupHook();
}
private void RegisterWindowActivationHandler()
{
// Get the current window that contains this page
var app = Application.Current as App;
if (app?.GetWindow() is Window window)
{
// Register for window activation events
window.Activated += Dialog_WindowActivated;
}
}
private void UnregisterWindowActivationHandler()
{
var app = Application.Current as App;
if (app?.GetWindow() is Window window)
{
// Unregister to prevent memory leaks
window.Activated -= Dialog_WindowActivated;
}
}
private void Dialog_WindowActivated(object sender, WindowActivatedEventArgs args)
{
// When window is deactivated (user switched to another app)
if (args.WindowActivationState == WindowActivationState.Deactivated)
{
// Make sure to cleanup the keyboard hook when the window loses focus
KeyboardHookHelper.Instance.CleanupHook();
RemappingControl.ResetToggleButtons();
RemappingControl.UpdateAllAppsCheckBoxState();
}
}
private async void NewRemappingBtn_Click(object sender, RoutedEventArgs e)
{
_isEditMode = false;
_editingRemapping = null;
RemappingControl.SetOriginalKeys(new List<string>());
RemappingControl.SetRemappedKeys(new List<string>());
RemappingControl.SetApp(false, string.Empty);
RemappingControl.SetUpToggleButtonInitialStatus();
RegisterWindowActivationHandler();
// Show the dialog to add a new remapping
KeyDialog.PrimaryButtonClick += KeyDialog_PrimaryButtonClick;
await KeyDialog.ShowAsync();
KeyDialog.PrimaryButtonClick -= KeyDialog_PrimaryButtonClick;
UnregisterWindowActivationHandler();
KeyboardHookHelper.Instance.CleanupHook();
}
private async void ListView_ItemClick(object sender, ItemClickEventArgs e)
{
if (e.ClickedItem is Remapping selectedRemapping && selectedRemapping.IsEnabled)
{
// Set to edit mode
_isEditMode = true;
_editingRemapping = selectedRemapping;
RemappingControl.SetOriginalKeys(selectedRemapping.OriginalKeys);
RemappingControl.SetRemappedKeys(selectedRemapping.RemappedKeys);
RemappingControl.SetApp(!selectedRemapping.IsAllApps, selectedRemapping.AppName);
RemappingControl.SetUpToggleButtonInitialStatus();
RegisterWindowActivationHandler();
KeyDialog.PrimaryButtonClick += KeyDialog_PrimaryButtonClick;
await KeyDialog.ShowAsync();
KeyDialog.PrimaryButtonClick -= KeyDialog_PrimaryButtonClick;
UnregisterWindowActivationHandler();
KeyboardHookHelper.Instance.CleanupHook();
// Reset the edit status
_isEditMode = false;
_editingRemapping = null;
}
}
private void KeyDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
List<string> originalKeys = RemappingControl.GetOriginalKeys();
List<string> remappedKeys = RemappingControl.GetRemappedKeys();
bool isAppSpecific = RemappingControl.GetIsAppSpecific();
string appName = RemappingControl.GetAppName();
// Make sure _mappingService is not null before validating and saving
if (_mappingService == null)
{
Logger.LogError("Mapping service is null, cannot validate mapping");
return;
}
// Validate the remapping
ValidationErrorType errorType = ValidationHelper.ValidateKeyMapping(
originalKeys, remappedKeys, isAppSpecific, appName, _mappingService, _isEditMode, _editingRemapping);
if (errorType != ValidationErrorType.NoError)
{
ShowValidationError(errorType, args);
return;
}
// Check for orphaned keys
if (originalKeys.Count == 1 && _mappingService != null)
{
int originalKeyCode = _mappingService.GetKeyCodeFromName(originalKeys[0]);
if (IsKeyOrphaned(originalKeyCode, _mappingService))
{
string keyName = _mappingService.GetKeyDisplayName(originalKeyCode);
OrphanedKeysTeachingTip.Target = RemappingControl;
OrphanedKeysTeachingTip.Subtitle = $"The key {keyName} will become orphaned (inaccessible) after remapping. Please confirm if you want to proceed.";
OrphanedKeysTeachingTip.Tag = args;
OrphanedKeysTeachingTip.IsOpen = true;
args.Cancel = true;
return;
}
}
// If in edit mode, delete the existing remapping before saving the new one
if (_isEditMode && _editingRemapping != null)
{
if (!RemappingHelper.DeleteRemapping(_mappingService!, _editingRemapping))
{
return;
}
}
// If no errors, proceed to save the remapping
bool saved = RemappingHelper.SaveMapping(_mappingService!, originalKeys, remappedKeys, isAppSpecific, appName);
if (saved)
{
// Display the remapping in the list after saving
LoadMappings();
}
}
private void DeleteButton_Click(object sender, RoutedEventArgs e)
{
if (sender is Button button && button.DataContext is Remapping remapping)
{
if (RemappingHelper.DeleteRemapping(_mappingService!, remapping))
{
LoadMappings();
}
}
}
private void ValidationTeachingTip_CloseButtonClick(TeachingTip sender, object args)
{
sender.IsOpen = false;
}
private void OrphanedKeysTeachingTip_ActionButtonClick(TeachingTip sender, object args)
{
// User pressed continue anyway button
sender.IsOpen = false;
if (_isEditMode && _editingRemapping != null)
{
if (!RemappingHelper.DeleteRemapping(_mappingService!, _editingRemapping))
{
return;
}
}
bool saved = RemappingHelper.SaveMapping(
_mappingService!, RemappingControl.GetOriginalKeys(), RemappingControl.GetRemappedKeys(), RemappingControl.GetIsAppSpecific(), RemappingControl.GetAppName());
if (saved)
{
KeyDialog.Hide();
LoadMappings();
}
}
private void OrphanedKeysTeachingTip_CloseButtonClick(TeachingTip sender, object args)
{
// Just close the teaching tip if the user canceled
sender.IsOpen = false;
}
private void LoadMappings()
{
if (_mappingService == null)
{
return;
}
SingleKeyMappings.Clear();
ShortcutKeyMappings.Clear();
RemappingList.Clear();
// Load all single key mappings
foreach (var mapping in _mappingService.GetSingleKeyMappings())
{
SingleKeyMappings.Add(mapping);
string[] targetKeyCodes = mapping.TargetKey.Split(';');
var targetKeyNames = new List<string>();
foreach (var keyCode in targetKeyCodes)
{
if (int.TryParse(keyCode, out int code))
{
targetKeyNames.Add(_mappingService.GetKeyDisplayName(code));
}
}
RemappingList.Add(new Remapping
{
OriginalKeys = new List<string> { _mappingService.GetKeyDisplayName(mapping.OriginalKey) },
RemappedKeys = targetKeyNames,
IsAllApps = true,
});
}
// Load all shortcut key mappings
foreach (var mapping in _mappingService.GetShortcutMappingsByType(ShortcutOperationType.RemapShortcut))
{
ShortcutKeyMappings.Add(mapping);
string[] originalKeyCodes = mapping.OriginalKeys.Split(';');
string[] targetKeyCodes = mapping.TargetKeys.Split(';');
var originalKeyNames = new List<string>();
var targetKeyNames = new List<string>();
foreach (var keyCode in originalKeyCodes)
{
if (int.TryParse(keyCode, out int code))
{
originalKeyNames.Add(_mappingService.GetKeyDisplayName(code));
}
}
foreach (var keyCode in targetKeyCodes)
{
if (int.TryParse(keyCode, out int code))
{
targetKeyNames.Add(_mappingService.GetKeyDisplayName(code));
}
}
RemappingList.Add(new Remapping
{
OriginalKeys = originalKeyNames,
RemappedKeys = targetKeyNames,
IsAllApps = string.IsNullOrEmpty(mapping.TargetApp),
AppName = string.IsNullOrEmpty(mapping.TargetApp) ? "All Apps" : mapping.TargetApp,
});
}
}
private bool ShowValidationError(ValidationErrorType errorType, ContentDialogButtonClickEventArgs args)
{
var (title, message) = ValidationMessages[errorType];
ValidationTeachingTip.Title = title;
ValidationTeachingTip.Subtitle = message;
ValidationTeachingTip.Target = RemappingControl;
ValidationTeachingTip.Tag = args;
ValidationTeachingTip.IsOpen = true;
args.Cancel = true;
return false;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Dispose managed resources
_mappingService?.Dispose();
_mappingService = null;
}
_disposed = true;
}
}
}
}

View File

@@ -1,207 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="KeyboardManagerEditorUI.Pages.Text"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helper="using:KeyboardManagerEditorUI.Helpers"
xmlns:local="using:KeyboardManagerEditorUI.Pages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:styles="using:KeyboardManagerEditorUI.Styles"
mc:Ignorable="d">
<Grid Padding="16">
<StackPanel Orientation="Vertical" Spacing="12">
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Text shortcuts allow you to insert text in any input field when you use the configured keyboard shortcut"
TextWrapping="Wrap" />
<Button
x:Name="NewShortcutBtn"
Height="36"
Margin="0,12,0,0"
Click="NewShortcutBtn_Click">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon
FontSize="14"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="&#xE109;" />
<TextBlock VerticalAlignment="Center" Text="New" />
</StackPanel>
</Button>
<Grid
HorizontalAlignment="Stretch"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="{StaticResource OverlayCornerRadius}">
<Grid.RowDefinitions>
<RowDefinition Height="48" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="236" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="120" />
<ColumnDefinition Width="84" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Margin="16,-2,0,0"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Original key(s)" />
<AppBarSeparator
Grid.Column="1"
Margin="-6,4,0,4"
HorizontalAlignment="Left" />
<TextBlock
Grid.Column="1"
Margin="12,-2,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Text" />
<AppBarSeparator
Grid.Column="2"
Margin="-6,4,0,4"
HorizontalAlignment="Left" />
<TextBlock
Grid.Column="2"
Margin="12,-2,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Applicable apps" />
<Rectangle
Grid.Row="0"
Grid.ColumnSpan="4"
Height="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Fill="{ThemeResource CardStrokeColorDefaultBrush}" />
<ListView
x:Name="MappingsList"
Grid.Row="1"
Grid.ColumnSpan="4"
IsItemClickEnabled="True"
ItemClick="ListView_ItemClick"
ItemsSource="{x:Bind TextMappings}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="helper:TextMapping">
<Grid Height="Auto" MinHeight="48">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="236" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="120" />
<ColumnDefinition Width="48" />
</Grid.ColumnDefinitions>
<Rectangle
Grid.ColumnSpan="5"
Height="1"
Margin="-16,0,-16,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Fill="{ThemeResource CardStrokeColorDefaultBrush}"
Opacity="0.8" />
<!-- Shortcut keys -->
<ItemsControl
Grid.Column="0"
VerticalAlignment="Center"
ItemsSource="{x:Bind Keys}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:WrapPanel
MaxWidth="230"
HorizontalSpacing="4"
Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<styles:KeyVisual
HorizontalAlignment="Left"
Content="{Binding}"
VisualType="SmallOutline" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Text content -->
<TextBlock
Grid.Column="1"
Margin="-4,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="12"
Text="{x:Bind Text}"
TextTrimming="CharacterEllipsis" />
<!-- App name -->
<TextBlock
Grid.Column="2"
Margin="-4,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="{x:Bind AppName}"
TextTrimming="CharacterEllipsis" />
<!-- Delete button -->
<Button
Grid.Column="3"
Margin="0,0,4,0"
Padding="8,4"
HorizontalAlignment="Right"
VerticalAlignment="Center"
AutomationProperties.Name="Delete remapping"
Background="Transparent"
BorderThickness="0"
Click="DeleteButton_Click"
ToolTipService.ToolTip="Delete">
<FontIcon
FontFamily="{StaticResource SymbolThemeFontFamily}"
FontSize="16"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Glyph="&#xE74D;" />
</Button>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</StackPanel>
<ContentDialog
x:Name="KeyDialog"
Title="Text Shortcut"
Width="480"
Height="360"
MinWidth="600"
MaxWidth="600"
PrimaryButtonClick="KeyDialog_PrimaryButtonClick"
PrimaryButtonStyle="{StaticResource AccentButtonStyle}"
PrimaryButtonText="Save"
SecondaryButtonText="Cancel">
<Grid>
<styles:TextPageInputControl x:Name="TextInputControl" />
</Grid>
</ContentDialog>
<TeachingTip
x:Name="ValidationTip"
CloseButtonContent="OK"
IsLightDismissEnabled="True"
PreferredPlacement="Center">
<TeachingTip.IconSource>
<SymbolIconSource Symbol="Important" />
</TeachingTip.IconSource>
</TeachingTip>
</Grid>
</Page>

View File

@@ -1,278 +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.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using KeyboardManagerEditorUI.Helpers;
using KeyboardManagerEditorUI.Interop;
using ManagedCommon;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
namespace KeyboardManagerEditorUI.Pages
{
public sealed partial class Text : Page, IDisposable
{
private KeyboardMappingService? _mappingService;
// Flag to indicate if the user is editing an existing mapping
private bool _isEditMode;
private TextMapping? _editingMapping;
private bool _disposed;
// The list of text mappings
public ObservableCollection<TextMapping> TextMappings { get; } = new ObservableCollection<TextMapping>();
public Text()
{
this.InitializeComponent();
try
{
_mappingService = new KeyboardMappingService();
LoadTextMappings();
}
catch (Exception ex)
{
Logger.LogError("Failed to initialize KeyboardMappingService: " + ex.Message);
}
this.Unloaded += Text_Unloaded;
}
private void Text_Unloaded(object sender, RoutedEventArgs e)
{
Dispose();
}
private void LoadTextMappings()
{
if (_mappingService == null)
{
return;
}
TextMappings.Clear();
// Load key-to-text mappings
var keyToTextMappings = _mappingService.GetKeyToTextMappings();
foreach (var mapping in keyToTextMappings)
{
TextMappings.Add(new TextMapping
{
Keys = new List<string> { _mappingService.GetKeyDisplayName(mapping.OriginalKey) },
Text = mapping.TargetText,
IsAllApps = true,
AppName = "All Apps",
});
}
// Load shortcut-to-text mappings
foreach (var mapping in _mappingService.GetShortcutMappingsByType(ShortcutOperationType.RemapText))
{
string[] originalKeyCodes = mapping.OriginalKeys.Split(';');
var originalKeyNames = new List<string>();
foreach (var keyCode in originalKeyCodes)
{
if (int.TryParse(keyCode, out int code))
{
originalKeyNames.Add(_mappingService.GetKeyDisplayName(code));
}
}
TextMappings.Add(new TextMapping
{
Keys = originalKeyNames,
Text = mapping.TargetText,
IsAllApps = string.IsNullOrEmpty(mapping.TargetApp),
AppName = string.IsNullOrEmpty(mapping.TargetApp) ? "All Apps" : mapping.TargetApp,
});
}
}
private async void NewShortcutBtn_Click(object sender, RoutedEventArgs e)
{
_isEditMode = false;
_editingMapping = null;
TextInputControl.ClearKeys();
TextInputControl.SetTextContent(string.Empty);
TextInputControl.SetAppSpecific(false, string.Empty);
await KeyDialog.ShowAsync();
}
private async void ListView_ItemClick(object sender, ItemClickEventArgs e)
{
if (e.ClickedItem is TextMapping selectedMapping)
{
_isEditMode = true;
_editingMapping = selectedMapping;
TextInputControl.SetShortcutKeys(selectedMapping.Keys);
TextInputControl.SetTextContent(selectedMapping.Text);
TextInputControl.SetAppSpecific(!selectedMapping.IsAllApps, selectedMapping.AppName);
await KeyDialog.ShowAsync();
}
}
private void KeyDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
if (_mappingService == null)
{
return;
}
List<string> keys = TextInputControl.GetShortcutKeys();
string textContent = TextInputControl.GetTextContent();
bool isAppSpecific = TextInputControl.GetIsAppSpecific();
string appName = TextInputControl.GetAppName();
// Validate inputs
ValidationErrorType errorType = ValidationHelper.ValidateTextMapping(
keys, textContent, isAppSpecific, appName, _mappingService);
if (errorType != ValidationErrorType.NoError)
{
ShowValidationError(errorType, args);
return;
}
bool saved = false;
try
{
// Delete existing mapping if in edit mode
if (_isEditMode && _editingMapping != null)
{
if (_editingMapping.Keys.Count == 1)
{
int originalKey = _mappingService.GetKeyCodeFromName(_editingMapping.Keys[0]);
if (originalKey != 0)
{
_mappingService.DeleteSingleKeyToTextMapping(originalKey);
}
}
else
{
string originalKeys = string.Join(";", _editingMapping.Keys.Select(k => _mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
_mappingService.DeleteShortcutMapping(originalKeys, _editingMapping.IsAllApps ? string.Empty : _editingMapping.AppName);
}
}
// Add new mapping
if (keys.Count == 1)
{
// Single key to text mapping
int originalKey = _mappingService.GetKeyCodeFromName(keys[0]);
if (originalKey != 0)
{
saved = _mappingService.AddSingleKeyToTextMapping(originalKey, textContent);
}
}
else
{
// Shortcut to text mapping
string originalKeysString = string.Join(";", keys.Select(k => _mappingService.GetKeyCodeFromName(k).ToString(CultureInfo.InvariantCulture)));
if (isAppSpecific && !string.IsNullOrEmpty(appName))
{
saved = _mappingService.AddShortcutMapping(originalKeysString, textContent, appName, ShortcutOperationType.RemapText);
}
else
{
saved = _mappingService.AddShortcutMapping(originalKeysString, textContent, operationType: ShortcutOperationType.RemapText);
}
}
if (saved)
{
_mappingService.SaveSettings();
LoadTextMappings(); // Refresh the list
}
}
catch (Exception ex)
{
Logger.LogError("Error saving text mapping: " + ex.Message);
args.Cancel = true;
}
}
private void DeleteButton_Click(object sender, RoutedEventArgs e)
{
if (_mappingService == null || !(sender is Button button) || !(button.DataContext is TextMapping mapping))
{
return;
}
try
{
bool deleted = false;
if (mapping.Keys.Count == 1)
{
// Single key mapping
int originalKey = _mappingService.GetKeyCodeFromName(mapping.Keys[0]);
if (originalKey != 0)
{
deleted = _mappingService.DeleteSingleKeyToTextMapping(originalKey);
}
}
else
{
// Shortcut mapping
string originalKeys = string.Join(";", mapping.Keys.Select(k => _mappingService.GetKeyCodeFromName(k)));
deleted = _mappingService.DeleteShortcutMapping(originalKeys, mapping.IsAllApps ? string.Empty : mapping.AppName);
}
if (deleted)
{
_mappingService.SaveSettings();
TextMappings.Remove(mapping);
}
}
catch (Exception ex)
{
Logger.LogError("Error deleting text mapping: " + ex.Message);
}
}
private void ShowValidationError(ValidationErrorType errorType, ContentDialogButtonClickEventArgs args)
{
if (ValidationHelper.ValidationMessages.TryGetValue(errorType, out (string Title, string Message) error))
{
ValidationTip.Title = error.Title;
ValidationTip.Subtitle = error.Message;
ValidationTip.IsOpen = true;
args.Cancel = true;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_mappingService?.Dispose();
_mappingService = null;
}
_disposed = true;
}
}
}
}

View File

@@ -1,165 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="KeyboardManagerEditorUI.Pages.URLs"
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:helper="using:KeyboardManagerEditorUI.Helpers"
xmlns:local="using:KeyboardManagerEditorUI.Pages"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:styles="using:KeyboardManagerEditorUI.Styles"
mc:Ignorable="d">
<Grid Padding="16">
<StackPanel Orientation="Vertical" Spacing="12">
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="URL shortcuts allow you to open a URL when you use the configured shortcut"
TextWrapping="Wrap" />
<Button
x:Name="NewShortcutBtn"
Height="36"
Margin="0,12,0,0"
Click="NewShortcutBtn_Click">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon
FontSize="14"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="&#xE109;" />
<TextBlock VerticalAlignment="Center" Text="New" />
</StackPanel>
</Button>
<Grid
HorizontalAlignment="Stretch"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="{ThemeResource OverlayCornerRadius}">
<Grid.RowDefinitions>
<RowDefinition Height="48" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="236" />
<ColumnDefinition Width="236" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="84" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Margin="16,-2,0,0"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Shortcut" />
<AppBarSeparator
Grid.Column="1"
Margin="-6,4,0,4"
HorizontalAlignment="Left" />
<TextBlock
Grid.Column="1"
Margin="12,-2,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="URL" />
<Rectangle
Grid.ColumnSpan="4"
Height="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Fill="{ThemeResource CardStrokeColorDefaultBrush}" />
<ListView
Grid.Row="1"
Grid.ColumnSpan="4"
IsItemClickEnabled="True"
ItemClick="ListView_ItemClick"
ItemsSource="{x:Bind Shortcuts}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="helper:URLShortcut">
<Grid Height="48">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="232" />
<ColumnDefinition Width="238" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="84" />
</Grid.ColumnDefinitions>
<Rectangle
Grid.ColumnSpan="5"
Height="1"
Margin="-16,0,-16,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Fill="{ThemeResource CardStrokeColorDefaultBrush}"
Opacity="0.8" />
<ItemsControl
Grid.Column="0"
VerticalAlignment="Center"
ItemsSource="{x:Bind Shortcut}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<styles:KeyVisual
HorizontalAlignment="Left"
Content="{Binding}"
VisualType="SmallOutline" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<HyperlinkButton
Grid.Column="1"
Margin="-12,0,0,0"
Content="{x:Bind URL}" />
<ToggleSwitch
Grid.ColumnSpan="4"
Margin="0,0,-112,0"
HorizontalAlignment="Right"
IsOn="True"
OffContent=""
OnContent="" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</StackPanel>
<ContentDialog
x:Name="KeyDialog"
Title="New URL shortcut"
Width="480"
Height="360"
MinWidth="600"
MaxWidth="600"
PrimaryButtonStyle="{StaticResource AccentButtonStyle}"
PrimaryButtonText="Save"
SecondaryButtonText="Cancel">
<StackPanel
Width="320"
Height="264"
Orientation="Vertical">
<TextBlock Margin="0,12,0,8" Text="Shortcut" />
<ToggleButton
x:Name="OriginalToggleBtn"
Padding="0,24,0,24"
HorizontalAlignment="Stretch"
Style="{StaticResource CustomShortcutToggleButtonStyle}">
<ToggleButton.Content>
<StackPanel Orientation="Horizontal" Spacing="8">
<styles:KeyVisual Content="Win (Left)" VisualType="SmallOutline" />
<styles:KeyVisual Content="B" VisualType="SmallOutline" />
</StackPanel>
</ToggleButton.Content>
</ToggleButton>
<TextBox
Margin="0,24,0,0"
Header="URL to open"
Text="https://www.bing.com" />
</StackPanel>
</ContentDialog>
</Grid>
</Page>

View File

@@ -1,117 +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.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text;
using KeyboardManagerEditorUI.Helpers;
using KeyboardManagerEditorUI.Interop;
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.Pages
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class URLs : Page, IDisposable
{
private KeyboardMappingService? _mappingService;
private bool _disposed;
public ObservableCollection<URLShortcut> Shortcuts { get; set; }
[DllImport("PowerToys.KeyboardManagerEditorLibraryWrapper.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
private static extern void GetKeyDisplayName(int keyCode, [Out] StringBuilder keyName, int maxLength);
public URLs()
{
this.InitializeComponent();
Shortcuts = new ObservableCollection<URLShortcut>();
_mappingService = new KeyboardMappingService();
foreach (var mapping in _mappingService.GetShortcutMappingsByType(ShortcutOperationType.OpenUri))
{
string[] originalKeyCodes = mapping.OriginalKeys.Split(';');
var originalKeyNames = new List<string>();
foreach (var keyCode in originalKeyCodes)
{
if (int.TryParse(keyCode, out int code))
{
originalKeyNames.Add(GetKeyDisplayName(code));
}
}
var shortcut = new URLShortcut
{
Shortcut = originalKeyNames,
URL = mapping.UriToOpen,
};
Shortcuts.Add(shortcut);
}
/*
Shortcuts.Add(new URLShortcut() { Shortcut = new List<string>() { "Shift", "Win", "M" }, URL = "https://www.microsoft.com" });
Shortcuts.Add(new URLShortcut() { Shortcut = new List<string>() { "Win", "P", }, URL = "https://www.bing.com" });
Shortcuts.Add(new URLShortcut() { Shortcut = new List<string>() { "Shift", "Win", "M" }, URL = "https://www.windows.com" });
Shortcuts.Add(new URLShortcut() { Shortcut = new List<string>() { "Win", "U", }, URL = "https://www.bing.com" });
Shortcuts.Add(new URLShortcut() { Shortcut = new List<string>() { "Ctrl", "P" }, URL = "https://www.surface.com" });
Shortcuts.Add(new URLShortcut() { Shortcut = new List<string>() { "Alt", "Ctrl", "Shift" }, URL = "https://www.bing.com" });
*/
}
public static string GetKeyDisplayName(int keyCode)
{
var keyName = new StringBuilder(64);
GetKeyDisplayName(keyCode, keyName, keyName.Capacity);
return keyName.ToString();
}
private async void NewShortcutBtn_Click(object sender, RoutedEventArgs e)
{
await KeyDialog.ShowAsync();
}
private async void ListView_ItemClick(object sender, ItemClickEventArgs e)
{
await KeyDialog.ShowAsync();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Dispose managed resources
_mappingService?.Dispose();
_mappingService = null;
}
_disposed = true;
}
}
}
}

View File

@@ -1,203 +0,0 @@
<?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"
xmlns:local="using:KeyboardManagerEditorUI.Styles">
<x:Double x:Key="ContentDialogMaxWidth">960</x:Double>
<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>
</ResourceDictionary>

View File

@@ -1,100 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="KeyboardManagerEditorUI.Styles.InputControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:KeyboardManagerEditorUI.Styles"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Loaded="UserControl_Loaded"
mc:Ignorable="d">
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="240" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="240" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Margin="0,12,0,0" Text="Original key(s)" />
<Grid Grid.Column="2">
<TextBlock Margin="0,12,0,0" Text="New key(s)" />
</Grid>
<Grid Grid.Row="1" Margin="0,8,0,0">
<ToggleButton
x:Name="OriginalToggleBtn"
Padding="0,24,0,24"
HorizontalAlignment="Stretch"
Checked="OriginalToggleBtn_Checked"
Style="{StaticResource CustomShortcutToggleButtonStyle}">
<ToggleButton.Content>
<ItemsControl x:Name="OriginalKeys">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:WrapPanel HorizontalSpacing="4" Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:KeyVisual Content="{Binding}" VisualType="SmallOutline" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ToggleButton.Content>
</ToggleButton>
</Grid>
<TextBlock
Grid.Row="1"
Grid.Column="1"
Margin="24,0,24,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
Text="&#xE0AB;" />
<Grid
Grid.Row="1"
Grid.Column="2"
Margin="0,8,0,0">
<ToggleButton
x:Name="RemappedToggleBtn"
Padding="0,24,0,24"
HorizontalAlignment="Stretch"
Checked="RemappedToggleBtn_Checked"
Style="{StaticResource CustomShortcutToggleButtonStyle}">
<ToggleButton.Content>
<ItemsControl x:Name="RemappedKeys">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:WrapPanel HorizontalSpacing="4" Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:KeyVisual Content="{Binding}" VisualType="Small" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ToggleButton.Content>
</ToggleButton>
</Grid>
</Grid>
<CheckBox
x:Name="AllAppsCheckBox"
Margin="0,24,0,12"
Content="Only apply this remapping to a specific application" />
<TextBox
x:Name="AppNameTextBox"
Header="Application name"
IsEnabled="{Binding ElementName=AllAppsCheckBox, Path=IsChecked}"
PlaceholderText="e.g.: outlook.exe" />
</StackPanel>
</UserControl>

View File

@@ -1,419 +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.Collections.ObjectModel;
using System.Linq;
using KeyboardManagerEditorUI.Helpers;
using KeyboardManagerEditorUI.Interop;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Windows.System;
namespace KeyboardManagerEditorUI.Styles
{
public sealed partial class InputControl : UserControl, IDisposable, IKeyboardHookTarget
{
// Collection to store original and remapped keys
private ObservableCollection<string> _originalKeys = new ObservableCollection<string>();
private ObservableCollection<string> _remappedKeys = new ObservableCollection<string>();
// TeachingTip for notifications
private TeachingTip? currentNotification;
private DispatcherTimer? notificationTimer;
private bool _disposed;
public static readonly DependencyProperty InputModeProperty =
DependencyProperty.Register(
"InputMode",
typeof(KeyInputMode),
typeof(InputControl),
new PropertyMetadata(KeyInputMode.OriginalKeys));
public KeyInputMode InputMode
{
get { return (KeyInputMode)GetValue(InputModeProperty); }
set { SetValue(InputModeProperty, value); }
}
public InputControl()
{
this.InitializeComponent();
this.OriginalKeys.ItemsSource = _originalKeys;
this.RemappedKeys.ItemsSource = _remappedKeys;
this.Unloaded += InputControl_Unloaded;
// Set the default focus state
OriginalToggleBtn.IsChecked = true;
// Ensure AllAppsCheckBox is in the correct state initially
UpdateAllAppsCheckBoxState();
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
AllAppsCheckBox.Checked += AllAppsCheckBox_Checked;
AllAppsCheckBox.Unchecked += AllAppsCheckBox_Unchecked;
AppNameTextBox.GotFocus += AppNameTextBox_GotFocus;
}
private void InputControl_Unloaded(object sender, RoutedEventArgs e)
{
// Reset the control when it is unloaded
Reset();
}
public void OnKeyDown(VirtualKey key, List<string> formattedKeys)
{
if (InputMode == KeyInputMode.RemappedKeys)
{
_remappedKeys.Clear();
foreach (var keyName in formattedKeys)
{
_remappedKeys.Add(keyName);
}
}
else
{
_originalKeys.Clear();
foreach (var keyName in formattedKeys)
{
_originalKeys.Add(keyName);
}
}
UpdateAllAppsCheckBoxState();
}
public void ClearKeys()
{
if (InputMode == KeyInputMode.RemappedKeys)
{
_remappedKeys.Clear();
}
else
{
_originalKeys.Clear();
}
}
public void OnInputLimitReached()
{
ShowNotificationTip("Shortcuts can only have up to 4 modifier keys");
}
public void CleanupKeyboardHook()
{
KeyboardHookHelper.Instance.CleanupHook();
}
private void RemappedToggleBtn_Checked(object sender, RoutedEventArgs e)
{
// Only set NewMode to true if RemappedToggleBtn is checked
if (RemappedToggleBtn.IsChecked == true)
{
InputMode = KeyInputMode.RemappedKeys;
// Make sure OriginalToggleBtn is unchecked
if (OriginalToggleBtn.IsChecked == true)
{
OriginalToggleBtn.IsChecked = false;
}
KeyboardHookHelper.Instance.ActivateHook(this);
}
else
{
CleanupKeyboardHook();
}
}
private void OriginalToggleBtn_Checked(object sender, RoutedEventArgs e)
{
// Only set NewMode to false if OriginalToggleBtn is checked
if (OriginalToggleBtn.IsChecked == true)
{
InputMode = KeyInputMode.OriginalKeys;
// Make sure RemappedToggleBtn is unchecked
if (RemappedToggleBtn.IsChecked == true)
{
RemappedToggleBtn.IsChecked = false;
}
KeyboardHookHelper.Instance.ActivateHook(this);
}
}
private void AllAppsCheckBox_Checked(object sender, RoutedEventArgs e)
{
if (RemappedToggleBtn != null && RemappedToggleBtn.IsChecked == true)
{
RemappedToggleBtn.IsChecked = false;
}
if (OriginalToggleBtn != null && OriginalToggleBtn.IsChecked == true)
{
OriginalToggleBtn.IsChecked = false;
}
CleanupKeyboardHook();
AppNameTextBox.Visibility = Visibility.Visible;
}
private void AllAppsCheckBox_Unchecked(object sender, RoutedEventArgs e)
{
AppNameTextBox.Visibility = Visibility.Collapsed;
}
private void AppNameTextBox_GotFocus(object sender, RoutedEventArgs e)
{
// Reset the focus state when the AppNameTextBox is focused
if (RemappedToggleBtn != null && RemappedToggleBtn.IsChecked == true)
{
RemappedToggleBtn.IsChecked = false;
}
if (OriginalToggleBtn != null && OriginalToggleBtn.IsChecked == true)
{
OriginalToggleBtn.IsChecked = false;
}
CleanupKeyboardHook();
}
public void SetRemappedKeys(List<string> keys)
{
_remappedKeys.Clear();
if (keys != null)
{
foreach (var key in keys)
{
_remappedKeys.Add(key);
}
}
UpdateAllAppsCheckBoxState();
}
public void SetOriginalKeys(List<string> keys)
{
_originalKeys.Clear();
if (keys != null)
{
foreach (var key in keys)
{
_originalKeys.Add(key);
}
}
}
public void SetApp(bool isSpecificApp, string appName)
{
if (isSpecificApp)
{
AllAppsCheckBox.IsChecked = true;
AppNameTextBox.Text = appName;
AppNameTextBox.Visibility = Visibility.Visible;
}
else
{
AllAppsCheckBox.IsChecked = false;
AppNameTextBox.Visibility = Visibility.Collapsed;
}
}
public List<string> GetOriginalKeys()
{
return _originalKeys.ToList();
}
public List<string> GetRemappedKeys()
{
return _remappedKeys.ToList();
}
public bool GetIsAppSpecific()
{
return AllAppsCheckBox.IsChecked ?? false;
}
public string GetAppName()
{
return AppNameTextBox.Text ?? string.Empty;
}
public void SetUpToggleButtonInitialStatus()
{
// Ensure OriginalToggleBtn is checked
if (OriginalToggleBtn != null && OriginalToggleBtn.IsChecked != true)
{
OriginalToggleBtn.IsChecked = true;
}
// Make sure RemappedToggleBtn is not checked
if (RemappedToggleBtn != null && RemappedToggleBtn.IsChecked == true)
{
RemappedToggleBtn.IsChecked = false;
}
}
public void UpdateAllAppsCheckBoxState()
{
// Only enable app-specific remapping for shortcuts (multiple keys)
bool isShortcut = _originalKeys.Count > 1;
AllAppsCheckBox.IsEnabled = isShortcut;
// If it's not a shortcut, ensure the checkbox is unchecked and app textbox is hidden
if (!isShortcut)
{
AllAppsCheckBox.IsChecked = false;
AppNameTextBox.Visibility = Visibility.Collapsed;
}
}
public void ShowNotificationTip(string message)
{
// If there's already an active notification, close and remove it first
CloseExistingNotification();
// Create a new notification
currentNotification = new TeachingTip
{
Title = "Input Limit Reached",
Subtitle = message,
IsLightDismissEnabled = true,
PreferredPlacement = TeachingTipPlacementMode.Top,
XamlRoot = this.XamlRoot,
IconSource = new SymbolIconSource { Symbol = Symbol.Important },
};
// Target the toggle button that triggered the notification
currentNotification.Target = InputMode == KeyInputMode.RemappedKeys ? RemappedToggleBtn : OriginalToggleBtn;
// Add the notification to the root panel and show it
if (this.Content is Panel rootPanel)
{
rootPanel.Children.Add(currentNotification);
currentNotification.IsOpen = true;
// Create a timer to auto-dismiss the notification
notificationTimer = new DispatcherTimer();
notificationTimer.Interval = TimeSpan.FromMilliseconds(EditorConstants.DefaultNotificationTimeout);
notificationTimer.Tick += (s, e) =>
{
CloseExistingNotification();
notificationTimer = null;
};
notificationTimer.Start();
}
}
// Helper method to close existing notifications
private void CloseExistingNotification()
{
// Stop any running timer
if (notificationTimer != null)
{
notificationTimer.Stop();
notificationTimer = null;
}
// Close and remove any existing notification
if (currentNotification != null && currentNotification.IsOpen)
{
currentNotification.IsOpen = false;
if (this.Content is Panel rootPanel)
{
rootPanel.Children.Remove(currentNotification);
}
currentNotification = null;
}
}
public void ResetToggleButtons()
{
// Reset toggle button status without clearing the key displays
if (RemappedToggleBtn != null)
{
RemappedToggleBtn.IsChecked = false;
}
if (OriginalToggleBtn != null)
{
OriginalToggleBtn.IsChecked = false;
}
}
public void Reset()
{
// Reset displayed keys
_originalKeys.Clear();
_remappedKeys.Clear();
// Reset toggle button status
if (RemappedToggleBtn != null)
{
RemappedToggleBtn.IsChecked = false;
}
if (OriginalToggleBtn != null)
{
OriginalToggleBtn.IsChecked = false;
}
InputMode = KeyInputMode.OriginalKeys;
// Reset app name text box
if (AppNameTextBox != null)
{
AppNameTextBox.Text = string.Empty;
}
UpdateAllAppsCheckBoxState();
// Close any existing notifications
CloseExistingNotification();
// Reset the focus status
if (this.FocusState != FocusState.Unfocused)
{
this.IsTabStop = false;
this.IsTabStop = true;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
CleanupKeyboardHook();
CloseExistingNotification();
Reset();
}
_disposed = true;
}
}
}
}

View File

@@ -1,198 +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.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Markup;
using Microsoft.UI.Xaml.Media;
using Windows.System;
namespace KeyboardManagerEditorUI.Styles
{
[TemplatePart(Name = KeyPresenter, Type = typeof(ContentPresenter))]
[TemplateVisualState(Name = "Normal", GroupName = "CommonStates")]
[TemplateVisualState(Name = "Disabled", GroupName = "CommonStates")]
[TemplateVisualState(Name = "Default", GroupName = "StateStates")]
[TemplateVisualState(Name = "Error", GroupName = "StateStates")]
public sealed partial class KeyVisual : Control
{
private const string KeyPresenter = "KeyPresenter";
private KeyVisual? _keyVisual;
private ContentPresenter? _keyPresenter;
public object Content
{
get => (object)GetValue(ContentProperty);
set => SetValue(ContentProperty, value);
}
public static readonly DependencyProperty ContentProperty = DependencyProperty.Register("Content", typeof(object), typeof(KeyVisual), new PropertyMetadata(default(string), OnContentChanged));
public VisualType VisualType
{
get => (VisualType)GetValue(VisualTypeProperty);
set => SetValue(VisualTypeProperty, value);
}
public static readonly DependencyProperty VisualTypeProperty = DependencyProperty.Register("VisualType", typeof(VisualType), typeof(KeyVisual), new PropertyMetadata(default(VisualType), OnSizeChanged));
public bool IsError
{
get => (bool)GetValue(IsErrorProperty);
set => SetValue(IsErrorProperty, value);
}
public static readonly DependencyProperty IsErrorProperty = DependencyProperty.Register("IsError", typeof(bool), typeof(KeyVisual), new PropertyMetadata(false, OnIsErrorChanged));
public KeyVisual()
{
this.DefaultStyleKey = typeof(KeyVisual);
this.Style = GetStyleSize("TextKeyVisualStyle");
}
protected override void OnApplyTemplate()
{
IsEnabledChanged -= KeyVisual_IsEnabledChanged;
_keyVisual = (KeyVisual)this;
_keyPresenter = (ContentPresenter)_keyVisual.GetTemplateChild(KeyPresenter);
Update();
SetEnabledState();
SetErrorState();
IsEnabledChanged += KeyVisual_IsEnabledChanged;
base.OnApplyTemplate();
}
private static void OnContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((KeyVisual)d).Update();
}
private static void OnSizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((KeyVisual)d).Update();
}
private static void OnIsErrorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((KeyVisual)d).SetErrorState();
}
private void Update()
{
if (_keyVisual == null || _keyVisual._keyPresenter == null)
{
return;
}
if (_keyVisual.Content != null)
{
if (_keyVisual.Content.GetType() == typeof(string))
{
_keyVisual.Style = GetStyleSize("TextKeyVisualStyle");
_keyVisual._keyPresenter.Content = _keyVisual.Content;
}
else
{
_keyVisual.Style = GetStyleSize("IconKeyVisualStyle");
switch ((int)_keyVisual.Content)
{
/* We can enable other glyphs in the future
case 13: // The Enter key or button.
_keyVisual._keyPresenter.Content = "\uE751"; break;
case 8: // The Back key or button.
_keyVisual._keyPresenter.Content = "\uE750"; break;
case 16: // The right Shift key or button.
case 160: // The left Shift key or button.
case 161: // The Shift key or button.
_keyVisual._keyPresenter.Content = "\uE752"; break; */
case 38: _keyVisual._keyPresenter.Content = "\uE0E4"; break; // The Up Arrow key or button.
case 40: _keyVisual._keyPresenter.Content = "\uE0E5"; break; // The Down Arrow key or button.
case 37: _keyVisual._keyPresenter.Content = "\uE0E2"; break; // The Left Arrow key or button.
case 39: _keyVisual._keyPresenter.Content = "\uE0E3"; break; // The Right Arrow key or button.
case 91: // The left Windows key
case 92: // The right Windows key
PathIcon winIcon = (PathIcon)XamlReader.Load(@"<PathIcon xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"" Data=""M9,17V9h8v8ZM0,17V9H8v8ZM9,8V0h8V8ZM0,8V0H8V8Z"" />") as PathIcon;
Viewbox winIconContainer = new Viewbox();
winIconContainer.Child = winIcon;
winIconContainer.HorizontalAlignment = HorizontalAlignment.Center;
winIconContainer.VerticalAlignment = VerticalAlignment.Center;
double iconDimensions = GetIconSize();
winIconContainer.Height = iconDimensions;
winIconContainer.Width = iconDimensions;
_keyVisual._keyPresenter.Content = winIconContainer;
break;
default: _keyVisual._keyPresenter.Content = ((VirtualKey)_keyVisual.Content).ToString(); break;
}
}
}
}
public Style GetStyleSize(string styleName)
{
if (VisualType == VisualType.Small)
{
return (Style)App.Current.Resources["Small" + styleName];
}
else if (VisualType == VisualType.SmallOutline)
{
return (Style)App.Current.Resources["SmallOutline" + styleName];
}
else if (VisualType == VisualType.LargeOutline)
{
return (Style)App.Current.Resources["LargeOutline" + styleName];
}
else
{
return (Style)App.Current.Resources["Default" + styleName];
}
}
public double GetIconSize()
{
if (VisualType == VisualType.Small || VisualType == VisualType.SmallOutline)
{
return (double)App.Current.Resources["SmallIconSize"];
}
else
{
return (double)App.Current.Resources["DefaultIconSize"];
}
}
private void KeyVisual_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
{
SetEnabledState();
}
private void SetErrorState()
{
VisualStateManager.GoToState(this, IsError ? "Error" : "Default", true);
}
private void SetEnabledState()
{
VisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled", true);
}
}
public enum VisualType
{
Small,
SmallOutline,
Large,
LargeOutline,
}
}

View File

@@ -1,156 +0,0 @@
<?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"
xmlns:local="using:KeyboardManagerEditorUI.Styles">
<x:Double x:Key="DefaultIconSize">16</x:Double>
<x:Double x:Key="SmallIconSize">12</x:Double>
<Style x:Key="DefaultTextKeyVisualStyle" TargetType="local:KeyVisual">
<Setter Property="MinWidth" Value="32" />
<Setter Property="MinHeight" Value="32" />
<Setter Property="Background" Value="{ThemeResource AccentButtonBackgroundPressed}" />
<Setter Property="Foreground" Value="{ThemeResource TextOnAccentFillColorPrimaryBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource ButtonBorderBrush}" />
<Setter Property="BorderThickness" Value="{ThemeResource ButtonBorderThemeThickness}" />
<Setter Property="Padding" Value="4,0,0,4" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="FontSize" Value="12" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:KeyVisual">
<Grid>
<Grid Height="{TemplateBinding MinHeight}">
<Border
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4" />
<Rectangle
x:Name="ContentHolder"
Fill="{TemplateBinding Background}"
RadiusX="4"
RadiusY="4" />
<ContentPresenter
x:Name="KeyPresenter"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="Center"
Content="{TemplateBinding Content}"
FontSize="12"
FontWeight="Normal"
Foreground="{TemplateBinding Foreground}" />
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Target="ContentHolder.Fill" Value="{ThemeResource AccentButtonBackgroundDisabled}" />
<Setter Target="KeyPresenter.Foreground" Value="{ThemeResource AccentButtonForegroundDisabled}" />
<Setter Target="ContentHolder.Stroke" Value="{ThemeResource AccentButtonBorderBrushDisabled}" />
<!--<Setter Target="ContentHolder.StrokeThickness" Value="{TemplateBinding BorderThickness}" />-->
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="StateStates">
<VisualState x:Name="Default" />
<VisualState x:Name="Error">
<VisualState.Setters>
<Setter Target="ContentHolder.Fill" Value="{ThemeResource InfoBarErrorSeverityBackgroundBrush}" />
<Setter Target="KeyPresenter.Foreground" Value="{ThemeResource InfoBarErrorSeverityIconBackground}" />
<Setter Target="ContentHolder.Stroke" Value="{ThemeResource InfoBarErrorSeverityIconBackground}" />
<Setter Target="ContentHolder.StrokeThickness" Value="2" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style
x:Key="SmallTextKeyVisualStyle"
BasedOn="{StaticResource DefaultTextKeyVisualStyle}"
TargetType="local:KeyVisual">
<Setter Property="MinWidth" Value="40" />
<Setter Property="Height" Value="36" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Padding" Value="12,0,12,2" />
<Setter Property="FontSize" Value="14" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
<Style
x:Key="LargeOutlineTextKeyVisualStyle"
BasedOn="{StaticResource DefaultTextKeyVisualStyle}"
TargetType="local:KeyVisual">
<Setter Property="Background" Value="{ThemeResource ControlFillColorDefaultBrush}" />
<Setter Property="Foreground" Value="{ThemeResource ButtonForeground}" />
<Setter Property="BorderBrush" Value="{ThemeResource ControlStrongStrokeColorDefaultBrush}" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
<Style
x:Key="SmallOutlineTextKeyVisualStyle"
BasedOn="{StaticResource DefaultTextKeyVisualStyle}"
TargetType="local:KeyVisual">
<Setter Property="MinWidth" Value="40" />
<Setter Property="Background" Value="{ThemeResource ControlFillColorDefaultBrush}" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorSecondaryBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource ControlStrongStrokeColorDefaultBrush}" />
<Setter Property="Height" Value="36" />
<Setter Property="BorderThickness" Value="1,1,1,1" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Padding" Value="8,0,8,2" />
<Setter Property="FontSize" Value="13" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
<Style
x:Key="DefaultIconKeyVisualStyle"
BasedOn="{StaticResource DefaultTextKeyVisualStyle}"
TargetType="local:KeyVisual">
<Setter Property="MinWidth" Value="56" />
<Setter Property="MinHeight" Value="48" />
<Setter Property="FontFamily" Value="{ThemeResource SymbolThemeFontFamily}" />
<Setter Property="Padding" Value="16,8,16,8" />
<Setter Property="FontSize" Value="14" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
<Style
x:Key="SmallIconKeyVisualStyle"
BasedOn="{StaticResource DefaultTextKeyVisualStyle}"
TargetType="local:KeyVisual">
<Setter Property="MinWidth" Value="40" />
<Setter Property="Height" Value="36" />
<Setter Property="FontFamily" Value="{ThemeResource SymbolThemeFontFamily}" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="Padding" Value="0" />
<Setter Property="FontSize" Value="10" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
<Style
x:Key="SmallOutlineIconKeyVisualStyle"
BasedOn="{StaticResource DefaultTextKeyVisualStyle}"
TargetType="local:KeyVisual">
<Setter Property="MinWidth" Value="40" />
<Setter Property="Background" Value="{ThemeResource ButtonBackground}" />
<Setter Property="Foreground" Value="{ThemeResource ButtonForeground}" />
<Setter Property="BorderBrush" Value="{ThemeResource ButtonBorderBrush}" />
<Setter Property="FontFamily" Value="{ThemeResource SymbolThemeFontFamily}" />
<Setter Property="Height" Value="36" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Padding" Value="0" />
<Setter Property="FontSize" Value="9" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
</Style>
</ResourceDictionary>

View File

@@ -1,74 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="KeyboardManagerEditorUI.Styles.TextPageInputControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:KeyboardManagerEditorUI.Styles"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Loaded="UserControl_Loaded"
mc:Ignorable="d">
<StackPanel
Width="360"
Height="360"
Orientation="Vertical">
<!-- Shortcut section -->
<TextBlock
Margin="0,12,0,8"
FontWeight="SemiBold"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
Text="Shortcut key(s)" />
<ToggleButton
x:Name="ShortcutToggleBtn"
Padding="0,24,0,24"
HorizontalAlignment="Stretch"
Checked="ShortcutToggleBtn_Checked"
Style="{StaticResource CustomShortcutToggleButtonStyle}">
<ToggleButton.Content>
<ItemsControl x:Name="ShortcutKeys">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:WrapPanel HorizontalSpacing="4" Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:KeyVisual Content="{Binding}" VisualType="SmallOutline" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ToggleButton.Content>
</ToggleButton>
<!-- Text section -->
<TextBox
x:Name="TextContentBox"
Height="120"
Margin="0,8,0,0"
AcceptsReturn="True"
Background="{ThemeResource TextControlBackgroundFocused}"
BorderBrush="{ThemeResource ControlStrokeColorDefaultBrush}"
FontSize="13"
Header="Text to insert"
PlaceholderText="Enter text that will be inserted when the shortcut is pressed"
TextWrapping="Wrap" />
<!-- App specific section -->
<CheckBox
x:Name="AllAppsCheckBox"
Margin="0,8,0,0"
Content="Only apply this text shortcut to a specific application"
Foreground="{ThemeResource TextFillColorPrimaryBrush}" />
<TextBox
x:Name="AppNameTextBox"
Background="{ThemeResource TextControlBackgroundFocused}"
BorderBrush="{ThemeResource ControlStrokeColorDefaultBrush}"
Header="Application name"
IsEnabled="{Binding ElementName=AllAppsCheckBox, Path=IsChecked}"
PlaceholderText="e.g.: outlook.exe" />
</StackPanel>
</UserControl>

View File

@@ -1,252 +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.Collections.ObjectModel;
using KeyboardManagerEditorUI.Helpers;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.System;
namespace KeyboardManagerEditorUI.Styles
{
public sealed partial class TextPageInputControl : UserControl, IKeyboardHookTarget
{
private ObservableCollection<string> _shortcutKeys = new ObservableCollection<string>();
private TeachingTip? currentNotification;
private DispatcherTimer? notificationTimer;
private bool _internalUpdate;
public TextPageInputControl()
{
this.InitializeComponent();
this.ShortcutKeys.ItemsSource = _shortcutKeys;
ShortcutToggleBtn.IsChecked = true;
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
KeyboardHookHelper.Instance.ActivateHook(this);
TextContentBox.GotFocus += TextContentBox_GotFocus;
AllAppsCheckBox.Checked += AllAppsCheckBox_Changed;
AllAppsCheckBox.Unchecked += AllAppsCheckBox_Changed;
AppNameTextBox.GotFocus += AppNameTextBox_GotFocus;
AppNameTextBox.Visibility = AllAppsCheckBox.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
}
private void ShortcutToggleBtn_Checked(object sender, RoutedEventArgs e)
{
if (ShortcutToggleBtn.IsChecked == true)
{
KeyboardHookHelper.Instance.ActivateHook(this);
}
else
{
KeyboardHookHelper.Instance.CleanupHook();
}
}
public void OnKeyDown(VirtualKey key, List<string> formattedKeys)
{
_shortcutKeys.Clear();
foreach (var keyName in formattedKeys)
{
_shortcutKeys.Add(keyName);
}
UpdateAllAppsCheckBoxState();
}
private void TextContentBox_GotFocus(object sender, RoutedEventArgs e)
{
// Clean up the keyboard hook when the text box gains focus
KeyboardHookHelper.Instance.CleanupHook();
if (ShortcutToggleBtn != null && ShortcutToggleBtn.IsChecked == true)
{
ShortcutToggleBtn.IsChecked = false;
}
}
public void OnInputLimitReached()
{
ShowNotificationTip("Shortcuts can only have up to 4 modifier keys");
}
public void UpdateAllAppsCheckBoxState()
{
// Only enable app-specific remapping for shortcuts (multiple keys)
bool isShortcut = _shortcutKeys.Count > 1;
AllAppsCheckBox.IsEnabled = isShortcut;
// If it's not a shortcut, ensure the checkbox is unchecked and app textbox is hidden
try
{
if (!isShortcut)
{
_internalUpdate = true;
AllAppsCheckBox.IsChecked = false;
AppNameTextBox.Visibility = Visibility.Collapsed;
}
else if (AllAppsCheckBox.IsChecked == true)
{
AppNameTextBox.Visibility = Visibility.Visible;
}
}
finally
{
_internalUpdate = false;
}
}
private void AllAppsCheckBox_Changed(object sender, RoutedEventArgs e)
{
if (_internalUpdate)
{
return;
}
KeyboardHookHelper.Instance.CleanupHook();
if (ShortcutToggleBtn != null && ShortcutToggleBtn.IsChecked == true)
{
ShortcutToggleBtn.IsChecked = false;
}
AppNameTextBox.Visibility = AllAppsCheckBox.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
}
private void AppNameTextBox_GotFocus(object sender, RoutedEventArgs e)
{
if (_internalUpdate)
{
return;
}
KeyboardHookHelper.Instance.CleanupHook();
if (ShortcutToggleBtn != null && ShortcutToggleBtn.IsChecked == true)
{
ShortcutToggleBtn.IsChecked = false;
}
}
public void ShowNotificationTip(string message)
{
CloseExistingNotification();
currentNotification = new TeachingTip
{
Title = "Input Limit",
Subtitle = message,
IsLightDismissEnabled = true,
PreferredPlacement = TeachingTipPlacementMode.Top,
XamlRoot = this.XamlRoot,
IconSource = new SymbolIconSource { Symbol = Symbol.Important },
Target = ShortcutToggleBtn,
};
if (this.Content is Panel rootPanel)
{
rootPanel.Children.Add(currentNotification);
currentNotification.IsOpen = true;
notificationTimer = new DispatcherTimer();
notificationTimer.Interval = TimeSpan.FromMilliseconds(EditorConstants.DefaultNotificationTimeout);
notificationTimer.Tick += (s, e) =>
{
CloseExistingNotification();
};
notificationTimer.Start();
}
}
private void CloseExistingNotification()
{
if (notificationTimer != null)
{
notificationTimer.Stop();
notificationTimer = null;
}
if (currentNotification != null && currentNotification.IsOpen)
{
currentNotification.IsOpen = false;
if (this.Content is Panel rootPanel && rootPanel.Children.Contains(currentNotification))
{
rootPanel.Children.Remove(currentNotification);
}
currentNotification = null;
}
}
public void ClearKeys()
{
_shortcutKeys.Clear();
UpdateAllAppsCheckBoxState();
}
public List<string> GetShortcutKeys()
{
List<string> keys = new List<string>();
foreach (var key in _shortcutKeys)
{
keys.Add(key);
}
return keys;
}
public string GetTextContent()
{
return TextContentBox.Text;
}
public bool GetIsAppSpecific()
{
return AllAppsCheckBox.IsChecked ?? false;
}
public string GetAppName()
{
return AllAppsCheckBox.IsChecked == true ? AppNameTextBox.Text : string.Empty;
}
public void SetShortcutKeys(List<string> keys)
{
if (keys != null)
{
_shortcutKeys.Clear();
foreach (var key in keys)
{
_shortcutKeys.Add(key);
}
}
UpdateAllAppsCheckBoxState();
}
public void SetTextContent(string text)
{
TextContentBox.Text = text;
}
public void SetAppSpecific(bool isAppSpecific, string appName)
{
AllAppsCheckBox.IsChecked = isAppSpecific;
if (isAppSpecific)
{
AppNameTextBox.Text = appName;
}
}
}
}

View File

@@ -1,9 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\Common.Dotnet.FuzzTest.props" />
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<Platforms>x64;ARM64</Platforms>
<LangVersion>latest</LangVersion>
<Platforms>x64;ARM64</Platforms>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

View File

@@ -27,12 +27,17 @@
Severity="Informational" />
<controls:SettingsGroup x:Uid="CmdPal_Activation_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsCard x:Uid="CmdPal_ActivationShortcut" HeaderIcon="{ui:FontIcon Glyph=&#xEDA7;}">
<controls:ShortcutControl
MinWidth="{StaticResource SettingActionControlMinWidth}"
HotkeySettings="{x:Bind Path=ViewModel.Hotkey, Mode=OneWay}"
IsEnabled="False" />
<tkcontrols:SettingsCard.Description>
<HyperlinkButton
x:Name="CmdPalSettingsDeeplink"
x:Uid="CmdPal_DeeplinkContent"
Click="CmdPalSettingsDeeplink_Click" />
</tkcontrols:SettingsCard.Description>
</tkcontrols:SettingsCard>
</controls:SettingsGroup>
</StackPanel>

View File

@@ -2,6 +2,10 @@
// 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.Diagnostics;
using System.IO;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.ViewModels;
@@ -29,5 +33,37 @@ namespace Microsoft.PowerToys.Settings.UI.Views
{
ViewModel.RefreshEnabledState();
}
private void LaunchApp(string appPath)
{
try
{
string dir = Path.GetDirectoryName(appPath);
var processStartInfo = new ProcessStartInfo
{
FileName = appPath,
Arguments = string.Empty,
WorkingDirectory = dir,
UseShellExecute = true,
Verb = "open",
CreateNoWindow = false,
};
Process process = Process.Start(processStartInfo) ?? throw new InvalidOperationException("Failed to start the process.");
process.WaitForInputIdle();
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to launch CmdPal settings: {ex.Message}");
}
}
private void CmdPalSettingsDeeplink_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
// Launch CmdPal settings window
string launchPath = "x-cmdpal://settings";
LaunchApp(launchPath);
}
}
}

View File

@@ -4996,8 +4996,8 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
<data name="CmdPal_ActivationShortcut.Header" xml:space="preserve">
<value>Activation shortcut</value>
</data>
<data name="CmdPal_ActivationShortcut.Description" xml:space="preserve">
<value>Go to Command Palette settings to customize the activation shortcut.</value>
<data name="CmdPal_DeeplinkContent.Content" xml:space="preserve">
<value>Open Command Palette settings to customize the activation shortcut</value>
</data>
<data name="Help_chromaCIE" xml:space="preserve">
<value>chroma (CIE LCh)</value>

View File

@@ -297,7 +297,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
Logger.LogError("Failed to launch the new WinUI3 Editor", e);
}
string path = Path.Combine(Directory.GetParent(Environment.CurrentDirectory).ToString(), editorPath);
string path = Path.Combine(Environment.CurrentDirectory, editorPath);
Logger.LogInfo($"Starting {PowerToyName} editor from {path}");
// InvariantCulture: type represents the KeyboardManagerEditorType enum value