Compare commits

...

20 Commits

Author SHA1 Message Date
Niels Laute
85dea93a50 Design tweaks and XAML refactoring 2025-03-10 11:11:28 +01:00
chenmy77
e184808068 [FHL] Add Select Key UI in Inputcontrol (#37788)
* add press button

* add right press button

* add right press button by original button

* change toggle button position

* fix type error

* merge

* add link

* brush blue in new keys

* remove unuse annotations

* remove unuse keyvisual
2025-03-06 20:48:55 +08:00
Hao Liu
e52ac85a1b [FHL] Save Remapping Config and Data (#37787)
* load URL remapping

* Save Remapping Config and Data

* update mock data for testing

* fix app mapping

* update xaml
2025-03-06 17:56:13 +08:00
Hao Liu
1e3108efbc [FHL] Load Remapping Config and Data for Keyboard Manager new UX (#37762)
* Load remapping config and data

* display single key to shortcuts mapping properly

* get remapping with operation type so we can display them in different tabs

* clean up the xaml
2025-03-05 18:48:16 +08:00
Hao Liu
f7ed043446 [FHL] New static UX for Keyboard Manager Editor (#37747)
* Set up static new UX for Keyboard Manager Editor

* Improve the formatting and performance

* fix spelling
2025-03-04 19:09:28 +08:00
Hao Liu
d90215ee8b [FHL] Set up dev UX for Keyboard Manager Editor (#37730)
* Implement a dev version ui which is copy of current WinUI2 UX

* Implement WinUI3 dropdown and P/Invoke the key list from existing C++ Library

* Update Dllimport path
2025-03-03 10:55:44 +08:00
Hao Liu
b2dae5b48e Initial commit: Have the WinUI3 Editor up and run 2025-02-27 15:42:30 +08:00
Laszlo Nemeth
b8cef42776 [Workspaces] Fix regression when capturing minimized apps (#37599) 2025-02-26 12:51:46 +01:00
Jerry Xu
959a54bcd9 Revert "[Hosts] Add UITest Cases for Hosts Module" (#37619)
* Revert "[Hosts] Add UITest Cases for Hosts Module (#37600)"

This reverts commit c656dcc9c5.

* Matching all UITest projects and UITestAutomation project

* Add back Hosts UITests

---------

Co-authored-by: Jerry Xu <nxu@microsoft.com>
2025-02-26 07:55:15 +08:00
Smeagol
491d51afaf [QuickAccent]Add final sigma ( ς ) to the Greek character set (#37611) 2025-02-25 22:35:22 +00:00
Ani
f263042aeb [AdvancedPaste]Add paste actions to allow transcoding of media files (#37188)
* [AdvancedPaste] Additional actions, including Image to text

* Spellcheck issue

* [AdvancedPaste] Paste as file and many other improvements

* Fixed typo

* Fixed typo

* [AdvancedPaste] Improved paste window menu layout

* [AdvancedPaste] Improved settings window layout

* [AdvancedPaste] Removed AudioToText for the moment

* Code cleanup

* Minor fixes

* [AdvancedPaste] Semantic Kernel support

* Changed log-line with potentially sensitive info

* Spellcheck issues

* Various improvements for Semantic Kernel

* Spellcheck issue

* Refactored Clipboard routines

* Added integration tests for KernelService

* Extra telemetry for AdvancedPaste

* Added 'Hotkey' suffix to AdvancedPaste_Settings telemetry event

* Added IsSavedQuery

* Added KernelQueryCache

* Refactoring

* Added KernelQueryCache to BugReportTool delete list

* Added opt-n for Semantic Kernel

* Fixed bug with KernelQueryCache

* Ability to view last AI chat message on error

* Improved kernel query cache

* Used System.IO.Abstractions and improved tests

* Fixed under-count of token usage

* Used Semantic Kernel icon

* Cleanup

* Add missing EndProject line

* Fix dependency version conflicts

* Fix NOTICE.md

* Correct place of SemanticKernel in NOTICE.md

* Unlinked CustomPreview toggle from AI

* Added Microsoft.Bcl.AsyncInterfaces dependency to AdvancedPaste

* Fixed NOTICE.md order

* Moved Custom Preview to behaviour section

* Made Image to Text raise error on empty output

* Added AIServiceBatchIntegrationTests

* Updated AIServiceBatchIntegrationTests

* Added prompt moderation

* [AdvancedPaste] Media Transcoding support

* Spellcheck issue

* Improved transcoding output profile and added tests

* Moved GPO Infobar to better location

* Added cancel button and minor bug fixes

* Fixed crash

* Minor cleanups

* Improved transcoding error messages

* Used software back when transcoding fails with hardware accerlation

* Added Reencode to spellcheck

* Spellcheck issue

---------

Co-authored-by: Jaime Bernardo <jaime@janeasystems.com>
Co-authored-by: Dustin L. Howett <dustin@howett.net>
Co-authored-by: Jeremy Sinclair <4016293+snickler@users.noreply.github.com>
2025-02-25 21:33:39 +00:00
moooyo
c09a5337c4 [AOT] Refactor Logger function to improve performance and mark managedCommon as AOT compatible (#36327)
* Use function to init static value

* Replace GetFileName with GetFileNameWithoutExtension

* Add exception catch for GetCallerInfo

* Remove sourceLineNumber

* Add kernal to allow list

* Remove unused commit

* Add new folder to place source generation context

* update

* fix build issue

* Move line number back

* Use fileName to replace full path

---------

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
2025-02-26 00:12:38 +08:00
Ionuț Manța
9a658eb884 [PTRun] Disable CETCompat in Launcher (#37550)
* Disable CETCompat in Launcher

* Added comment

* Improved comment
2025-02-25 11:34:30 +00:00
Kai Tao
4eb11d6f9b [Workspaces][ARM64] Bring icon to packaged apps (#37625)
Bring icon to packaged apps
2025-02-25 19:17:41 +08:00
Dave Rayment
a5a354a70f [ImageResizer] Fix issues with blank Width and Height controls (#37373)
* Allow custom preset's dimensions to be blank in the UI while still persisted as 0.

* XAML formatting - reorder namespaces.

* Add "(auto)" text to zero-value Width/Height in Settings. Ensure Width and Height fields in flyout are formatted to empty when their value is 0.
2025-02-25 16:23:30 +08:00
Ionuț Manța
744316c400 [Settings]Fix ColorPicker dashboard shortcut (#37547)
* Fix color picker dashboard shortcut

* remove not needed code

* Remove comment
2025-02-24 23:14:45 +00:00
Jaime Bernardo
f2370912f3 [ci]Sign and fix KeyboardManagerEditorLibraryWrapper.dll (#37601)
* [ci]Sign KeyboardManagerEditorLibraryWrapper.dll

* Fix dll metadata
2025-02-24 21:12:42 +00:00
moooyo
5cc30df4db [AOT] Clean up AOT build issue in Common.UI (#36376)
* init commit

* Use path.combine

* Add useWPF and useWindowsForms back

---------

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
2025-02-25 02:56:04 +08:00
moooyo
f81f65db3d [AOT] clean up AOT issue in Settings.UI (#36559)
* Rename source generation context file

* fix build issue

* fix path bug

---------

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
2025-02-25 02:48:54 +08:00
leileizhang
9f008a65d6 [Build] Revert "[Hosts] Add UITest Cases for Hosts Module (#37600)" to fix CI issue (#37606)
Revert "[Hosts] Add UITest Cases for Hosts Module (#37600)"

This reverts commit c656dcc9c5.
2025-02-24 16:30:33 +00:00
150 changed files with 5051 additions and 324 deletions

View File

@@ -1283,7 +1283,7 @@ rectp
RECTSOURCE
recyclebin
Redist
reencode
Reencode
reencoded
REFCLSID
REFGUID

View File

@@ -128,6 +128,7 @@
"PowerToys.KeyboardManager.dll",
"KeyboardManagerEditor\\PowerToys.KeyboardManagerEditor.exe",
"KeyboardManagerEngine\\PowerToys.KeyboardManagerEngine.exe",
"PowerToys.KeyboardManagerEditorLibraryWrapper.dll",
"PowerToys.Launcher.dll",
"PowerToys.PowerLauncher.dll",

View File

@@ -15,7 +15,7 @@ Param(
$referencedFileVersionsPerDll = @{}
$totalFailures = 0
Get-ChildItem $targetDir -Recurse -Filter *.deps.json -Exclude *UITests*,MouseJump.Common.UnitTests*,*.FuzzTests* | ForEach-Object {
Get-ChildItem $targetDir -Recurse -Filter *.deps.json -Exclude *UITest*,MouseJump.Common.UnitTests*,*.FuzzTests* | ForEach-Object {
# Temporarily exclude All UI-Test, Fuzzer-Test projects because of Appium.WebDriver dependencies
$depsJsonFullFileName = $_.FullName
$depsJsonFileName = $_.Name

View File

@@ -1,6 +1,7 @@
<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.AotCompatibility.props" />
<PropertyGroup>
<UseWPF>true</UseWPF>

View File

@@ -91,20 +91,20 @@ namespace Common.UI
{
try
{
var assemblyPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
var fullPath = new DirectoryInfo(assemblyPath).FullName;
var directoryPath = System.AppContext.BaseDirectory;
if (mainExecutableIsOnTheParentFolder)
{
// Need to go into parent folder for PowerToys.exe. Likely a WinUI3 App SDK application.
fullPath = fullPath + "\\..\\PowerToys.exe";
directoryPath = Path.Combine(directoryPath, "..");
directoryPath = Path.Combine(directoryPath, "PowerToys.exe");
}
else
{
// PowerToys.exe is in the same path as the application.
fullPath = fullPath + "\\PowerToys.exe";
directoryPath = Path.Combine(directoryPath, "PowerToys.exe");
}
Process.Start(new ProcessStartInfo(fullPath) { Arguments = "--open-settings=" + SettingsWindowNameToString(window) });
Process.Start(new ProcessStartInfo(directoryPath) { Arguments = "--open-settings=" + SettingsWindowNameToString(window) });
}
catch
{

View File

@@ -11,7 +11,7 @@ using Microsoft.Win32;
namespace Common.UI
{
public class ThemeManager : IDisposable
public partial class ThemeManager : IDisposable
{
private readonly Application _app;
private const string LightTheme = "Light.Accent1";

View File

@@ -6,6 +6,7 @@ using System;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using ManagedCommon.Serialization;
namespace ManagedCommon
{
@@ -35,7 +36,7 @@ namespace ManagedCommon
inputStream.Close();
reader.Dispose();
return JsonSerializer.Deserialize<OutGoingLanguageSettings>(data).LanguageTag;
return JsonSerializer.Deserialize<OutGoingLanguageSettings>(data, SourceGenerationContext.Default.OutGoingLanguageSettings).LanguageTag;
}
catch (Exception)
{

View File

@@ -15,15 +15,23 @@ namespace ManagedCommon
{
public static class Logger
{
private static readonly Assembly Assembly = Assembly.GetExecutingAssembly();
private static readonly string Version = FileVersionInfo.GetVersionInfo(Assembly.Location).ProductVersion;
private static readonly string Error = "Error";
private static readonly string Warning = "Warning";
private static readonly string Info = "Info";
private static readonly string Debug = "Debug";
private static readonly string TraceFlag = "Trace";
private static readonly Assembly Assembly = Assembly.GetExecutingAssembly();
/*
* Please pay more attention!
* If you want to publish it with Native AOT enabled (or publish as a single file).
* You need to find another way to remove Assembly.Location usage.
*/
#pragma warning disable IL3000 // Avoid accessing Assembly file path when publishing as a single file
private static readonly string Version = FileVersionInfo.GetVersionInfo(Assembly.Location).ProductVersion;
#pragma warning restore IL3000 // Avoid accessing Assembly file path when publishing as a single file
/// <summary>
/// Initializes the logger and sets the path for logging.
/// </summary>
@@ -53,18 +61,16 @@ namespace ManagedCommon
Trace.AutoFlush = true;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void LogError(string message)
public static void LogError(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
Log(message, Error);
Log(message, Error, memberName, sourceFilePath, sourceLineNumber);
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void LogError(string message, Exception ex)
public static void LogError(string message, Exception ex, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
if (ex == null)
{
Log(message, Error);
Log(message, Error, memberName, sourceFilePath, sourceLineNumber);
}
else
{
@@ -83,38 +89,33 @@ namespace ManagedCommon
"Stack trace: " + Environment.NewLine +
ex.StackTrace;
Log(exMessage, Error);
Log(exMessage, Error, memberName, sourceFilePath, sourceLineNumber);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void LogWarning(string message)
public static void LogWarning(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
Log(message, Warning);
Log(message, Warning, memberName, sourceFilePath, sourceLineNumber);
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void LogInfo(string message)
public static void LogInfo(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
Log(message, Info);
Log(message, Info, memberName, sourceFilePath, sourceLineNumber);
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void LogDebug(string message)
public static void LogDebug(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
Log(message, Debug);
Log(message, Debug, memberName, sourceFilePath, sourceLineNumber);
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void LogTrace()
public static void LogTrace([System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
Log(string.Empty, TraceFlag);
Log(string.Empty, TraceFlag, memberName, sourceFilePath, sourceLineNumber);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void Log(string message, string type)
private static void Log(string message, string type, string memberName, string sourceFilePath, int sourceLineNumber)
{
Trace.WriteLine("[" + DateTime.Now.TimeOfDay + "] [" + type + "] " + GetCallerInfo());
Trace.WriteLine("[" + DateTime.Now.TimeOfDay + "] [" + type + "] " + GetCallerInfo(memberName, sourceFilePath, sourceLineNumber));
Trace.Indent();
if (message != string.Empty)
{
@@ -124,49 +125,27 @@ namespace ManagedCommon
Trace.Unindent();
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static string GetCallerInfo()
private static string GetCallerInfo(string memberName, string sourceFilePath, int sourceLineNumber)
{
StackTrace stackTrace = new();
var callerMethod = GetCallerMethod(stackTrace);
return $"{callerMethod?.DeclaringType?.Name}::{callerMethod.Name}";
}
private static MethodBase GetCallerMethod(StackTrace stackTrace)
{
const int topFrame = 3;
var topMethod = stackTrace.GetFrame(topFrame)?.GetMethod();
string callerFileName = "Unknown";
try
{
if (topMethod?.Name == nameof(IAsyncStateMachine.MoveNext) && typeof(IAsyncStateMachine).IsAssignableFrom(topMethod?.DeclaringType))
string fileName = Path.GetFileName(sourceFilePath);
if (!string.IsNullOrEmpty(fileName))
{
// Async method; return actual method as determined by heuristic:
// "Nearest method on stack to async state-machine's MoveNext() in same namespace but in a different type".
// There are tighter ways of determining the actual method, but this is good enough and probably faster.
for (int deepFrame = topFrame + 1; deepFrame < stackTrace.FrameCount; deepFrame++)
{
var deepMethod = stackTrace.GetFrame(deepFrame)?.GetMethod();
if (deepMethod?.DeclaringType != topMethod?.DeclaringType && deepMethod?.DeclaringType?.Namespace == topMethod?.DeclaringType?.Namespace)
{
return deepMethod;
}
}
callerFileName = fileName;
}
}
catch (Exception)
{
// Ignore exceptions in Release. The code above won't throw, but if it does, we don't want to crash the app.
callerFileName = "Unknown";
#if DEBUG
throw;
#endif
}
return topMethod;
return $"{callerFileName}::{memberName}::{sourceLineNumber}";
}
}
}

View File

@@ -1,6 +1,7 @@
<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.AotCompatibility.props" />
<PropertyGroup>
<Description>PowerToys ManagedCommon</Description>

View File

@@ -53,7 +53,7 @@ namespace ManagedCommon
internal static int Size
{
get { return Marshal.SizeOf(typeof(INPUT)); }
get { return Marshal.SizeOf<INPUT>(); }
}
}

View File

@@ -14,12 +14,11 @@ namespace ManagedCommon
{
public static class RunnerHelper
{
public static void WaitForPowerToysRunner(int powerToysPID, Action act)
public static void WaitForPowerToysRunner(int powerToysPID, Action act, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "")
{
var stackTrace = new StackTrace();
var assembly = Assembly.GetCallingAssembly().GetName();
var callingMethod = stackTrace.GetFrame(1).GetMethod().Name;
PowerToysTelemetry.Log.WriteEvent(new DebugEvent() { Message = $"[{assembly}][{callingMethod}]WaitForPowerToysRunner waiting for Event powerToysPID={powerToysPID}" });
PowerToysTelemetry.Log.WriteEvent(new DebugEvent() { Message = $"[{assembly}][{memberName}]WaitForPowerToysRunner waiting for Event powerToysPID={powerToysPID}" });
Task.Run(() =>
{
const uint INFINITE = 0xFFFFFFFF;
@@ -29,7 +28,7 @@ namespace ManagedCommon
IntPtr powerToysProcHandle = NativeMethods.OpenProcess(SYNCHRONIZE, false, powerToysPID);
if (NativeMethods.WaitForSingleObject(powerToysProcHandle, INFINITE) == WAIT_OBJECT_0)
{
PowerToysTelemetry.Log.WriteEvent(new DebugEvent() { Message = $"[{assembly}][{callingMethod}]WaitForPowerToysRunner Event Notified powerToysPID={powerToysPID}" });
PowerToysTelemetry.Log.WriteEvent(new DebugEvent() { Message = $"[{assembly}][{memberName}]WaitForPowerToysRunner Event Notified powerToysPID={powerToysPID}" });
act.Invoke();
}
});

View File

@@ -0,0 +1,13 @@
// 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.Text.Json.Serialization;
using static ManagedCommon.LanguageHelper;
namespace ManagedCommon.Serialization;
[JsonSerializable(typeof(OutGoingLanguageSettings))]
internal sealed partial class SourceGenerationContext : JsonSerializerContext
{
}

View File

@@ -14,7 +14,7 @@ namespace ManagedCommon
/// <param name="sender">Sender ThemeListener</param>
public delegate void ThemeChangedEvent(ThemeListener sender);
public class ThemeListener : IDisposable
public partial class ThemeListener : IDisposable
{
/// <summary>
/// Gets the App Theme.

View File

@@ -16,7 +16,7 @@ namespace Microsoft.PowerToys.UITest
/// <summary>
/// Represents a basic UI element in the application.
/// </summary>
public abstract class Element
public class Element
{
private WindowsElement? windowsElement;

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading.Tasks;
using AdvancedPaste.Models.KernelQueryCache;

View File

@@ -0,0 +1,14 @@
// 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;
namespace AdvancedPaste.UnitTests.Mocks;
internal sealed class NoOpProgress : IProgress<double>
{
public void Report(double value)
{
}
}

View File

@@ -8,6 +8,7 @@ using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
@@ -131,17 +132,18 @@ public sealed class AIServiceBatchIntegrationTests
{
VaultCredentialsProvider credentialsProvider = new();
PromptModerationService promptModerationService = new(credentialsProvider);
NoOpProgress progress = new();
CustomTextTransformService customTextTransformService = new(credentialsProvider, promptModerationService);
switch (format)
{
case PasteFormats.CustomTextTransformation:
return DataPackageHelpers.CreateFromText(await customTextTransformService.TransformTextAsync(batchTestInput.Prompt, batchTestInput.Clipboard));
return DataPackageHelpers.CreateFromText(await customTextTransformService.TransformTextAsync(batchTestInput.Prompt, batchTestInput.Clipboard, CancellationToken.None, progress));
case PasteFormats.KernelQuery:
var clipboardData = DataPackageHelpers.CreateFromText(batchTestInput.Clipboard).GetView();
KernelService kernelService = new(new NoOpKernelQueryCacheService(), credentialsProvider, promptModerationService, customTextTransformService);
return await kernelService.TransformClipboardAsync(batchTestInput.Prompt, clipboardData, isSavedQuery: false);
return await kernelService.TransformClipboardAsync(batchTestInput.Prompt, clipboardData, isSavedQuery: false, CancellationToken.None, progress);
default:
throw new InvalidOperationException($"Unexpected format {format}");

View File

@@ -6,6 +6,7 @@ using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
@@ -130,7 +131,7 @@ public sealed class KernelServiceIntegrationTests : IDisposable
private async Task<DataPackageView> GetKernelOutputAsync(string prompt, DataPackage input)
{
var output = await _kernelService.TransformClipboardAsync(prompt, input.GetView(), isSavedQuery: false);
var output = await _kernelService.TransformClipboardAsync(prompt, input.GetView(), isSavedQuery: false, CancellationToken.None, new NoOpProgress());
Assert.AreEqual(1, _eventListener.SemanticKernelEvents.Count);
Assert.IsTrue(_eventListener.SemanticKernelTokens > 0);

View File

@@ -0,0 +1,109 @@
// 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.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
using AdvancedPaste.Models;
using AdvancedPaste.UnitTests.Mocks;
using ManagedCommon;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Windows.Storage;
using Windows.Storage.FileProperties;
namespace AdvancedPaste.UnitTests.ServicesTests;
[TestClass]
public sealed class TranscodeHelperIntegrationTests
{
private sealed record class MediaProperties(BasicProperties Basic, MusicProperties Music, VideoProperties Video);
private const string InputRootFolder = @"%USERPROFILE%\AdvancedPasteTranscodeMediaTestData";
/// <summary> Tests transforming a folder of media files.
/// - Verifies that the output file has the same basic properties (e.g. duration) as the input file.
/// - Copies the output file to a subfolder of the input folder for manual inspection.
/// </summary>
[TestMethod]
[DataRow(@"audio", PasteFormats.TranscodeToMp3)]
[DataRow(@"video", PasteFormats.TranscodeToMp4)]
public async Task TestTransformFolder(string inputSubfolder, PasteFormats format)
{
var inputFolder = Environment.ExpandEnvironmentVariables(Path.Combine(InputRootFolder, inputSubfolder));
if (!Directory.Exists(inputFolder))
{
Assert.Inconclusive($"Skipping tests for {inputFolder} as it does not exist");
}
var outputPath = Path.Combine(inputFolder, $"test_output_{format}");
foreach (var inputPath in Directory.EnumerateFiles(inputFolder))
{
await RunTestTransformFileAsync(inputPath, outputPath, format);
}
}
private async Task RunTestTransformFileAsync(string inputPath, string finalOutputPath, PasteFormats format)
{
Logger.LogDebug($"Running {nameof(RunTestTransformFileAsync)} for {inputPath}/{format}");
Directory.CreateDirectory(finalOutputPath);
var inputPackage = await DataPackageHelpers.CreateFromFileAsync(inputPath);
var inputProperties = await GetPropertiesAsync(await StorageFile.GetFileFromPathAsync(inputPath));
var outputPackage = await TransformHelpers.TransformAsync(format, inputPackage.GetView(), CancellationToken.None, new NoOpProgress());
var outputItems = await outputPackage.GetView().GetStorageItemsAsync();
Assert.AreEqual(1, outputItems.Count);
var outputFile = outputItems.Single() as StorageFile;
Assert.IsNotNull(outputFile);
var outputProperties = await GetPropertiesAsync(outputFile);
AssertPropertiesMatch(format, inputProperties, outputProperties);
await outputFile.CopyAsync(await StorageFolder.GetFolderFromPathAsync(finalOutputPath), outputFile.Name, NameCollisionOption.ReplaceExisting);
await outputPackage.GetView().TryCleanupAfterDelayAsync(TimeSpan.Zero);
}
private static void AssertPropertiesMatch(PasteFormats format, MediaProperties inputProperties, MediaProperties outputProperties)
{
Assert.IsTrue(outputProperties.Basic.Size > 0);
Assert.AreEqual(inputProperties.Music.Title, outputProperties.Music.Title);
Assert.AreEqual(inputProperties.Music.Album, outputProperties.Music.Album);
Assert.AreEqual(inputProperties.Music.Artist, outputProperties.Music.Artist);
AssertDurationsApproxEqual(inputProperties.Music.Duration, outputProperties.Music.Duration);
if (format == PasteFormats.TranscodeToMp4)
{
Assert.AreEqual(inputProperties.Video.Title, outputProperties.Video.Title);
AssertDurationsApproxEqual(inputProperties.Video.Duration, outputProperties.Video.Duration);
var inputVideoDimensions = GetNormalizedDimensions(inputProperties.Video);
if (inputVideoDimensions != null)
{
Assert.AreEqual(inputVideoDimensions, GetNormalizedDimensions(outputProperties.Video));
}
}
}
private static async Task<MediaProperties> GetPropertiesAsync(StorageFile file) =>
new(await file.GetBasicPropertiesAsync(), await file.Properties.GetMusicPropertiesAsync(), await file.Properties.GetVideoPropertiesAsync());
private static void AssertDurationsApproxEqual(TimeSpan expected, TimeSpan actual) =>
Assert.AreEqual(expected.Ticks, actual.Ticks, delta: TimeSpan.FromSeconds(1).Ticks);
/// <summary>
/// Gets the dimensions of a video, if available. Accounts for the fact that the dimensions may sometimes be swapped.
/// </summary>
private static (uint Width, uint Height)? GetNormalizedDimensions(VideoProperties properties) =>
properties.Width == 0 || properties.Height == 0
? null
: (Math.Max(properties.Width, properties.Height), Math.Min(properties.Width, properties.Height));
}

View File

@@ -28,6 +28,7 @@
Background="Transparent"
BorderThickness="4"
CornerRadius="{TemplateBinding CornerRadius}"
IsHitTestVisible="False"
Visibility="Collapsed">
<!-- CornerRadius needs to be > 0 -->
<Grid.BorderBrush>

View File

@@ -178,17 +178,36 @@
Padding="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Image
x:Name="AIGlyphImage"
AutomationProperties.AccessibilityView="Raw"
Source="/Assets/AdvancedPaste/SemanticKernel.svg"
Visibility="{Binding DataContext.IsAdvancedAIEnabled, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource BoolToVisibilityConverter}}" />
<PathIcon
x:Name="AIGlyph"
AutomationProperties.AccessibilityView="Raw"
Data="M128 766q0-42 24-77t65-48l178-57q32-11 61-30t52-42q50-50 71-114l58-179q13-40 48-65t78-26q42 0 77 24t50 65l58 177q21 66 72 117 49 50 117 72l176 58q43 14 69 48t26 80q0 41-25 76t-64 49l-178 58q-66 21-117 72-32 32-51 73t-33 84-26 83-30 73-45 51-71 20q-42 0-77-24t-49-65l-58-178q-8-25-19-47t-28-43q-34-43-77-68t-89-41-89-27-78-29-55-45-21-75zm1149 7q-76-29-145-53t-129-60-104-88-73-138l-57-176-67 176q-18 48-42 89t-60 78q-34 34-76 61t-89 43l-177 57q75 29 144 53t127 60 103 89 73 137l57 176 67-176q37-97 103-168t168-103l177-57zm-125 759q0-31 20-57t49-36l99-32q34-11 53-34t30-51 20-59 20-54 33-41 58-16q32 0 59 19t38 50q6 20 11 40t13 40 17 38 25 34q16 17 39 26t48 18 49 16 44 20 31 32 12 50q0 33-18 60t-51 38q-19 6-39 11t-41 13-39 17-34 25q-24 25-35 62t-24 73-35 61-68 25q-32 0-59-19t-38-50q-6-18-11-39t-13-41-17-40-24-33q-18-17-41-27t-47-17-49-15-43-20-30-33-12-54zm583 4q-43-13-74-30t-55-41-40-55-32-74q-12 41-29 72t-42 55-55 42-71 31q81 23 128 71t71 129q15-43 31-74t40-54 53-40 75-32z"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Visibility="{Binding DataContext.IsAdvancedAIEnabled, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BoolToInvertedVisibilityConverter}}" />
<ProgressRing
Width="30"
Height="30"
HorizontalAlignment="Right"
VerticalAlignment="Center"
IsActive="{Binding DataContext.IsBusy, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}}"
IsIndeterminate="{Binding DataContext.HasIndeterminateTransformProgress, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}}"
Maximum="100"
Minimum="0"
Visibility="{Binding DataContext.IsBusy, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource BoolToVisibilityConverter}}"
Value="{Binding DataContext.TransformProgress, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}}" />
<StackPanel
Margin="0"
Padding="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Visibility="{Binding DataContext.IsBusy, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
<Image
x:Name="AIGlyphImage"
AutomationProperties.AccessibilityView="Raw"
Source="/Assets/AdvancedPaste/SemanticKernel.svg"
Visibility="{Binding DataContext.IsAdvancedAIEnabled, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource BoolToVisibilityConverter}}" />
<PathIcon
x:Name="AIGlyph"
AutomationProperties.AccessibilityView="Raw"
Data="M128 766q0-42 24-77t65-48l178-57q32-11 61-30t52-42q50-50 71-114l58-179q13-40 48-65t78-26q42 0 77 24t50 65l58 177q21 66 72 117 49 50 117 72l176 58q43 14 69 48t26 80q0 41-25 76t-64 49l-178 58q-66 21-117 72-32 32-51 73t-33 84-26 83-30 73-45 51-71 20q-42 0-77-24t-49-65l-58-178q-8-25-19-47t-28-43q-34-43-77-68t-89-41-89-27-78-29-55-45-21-75zm1149 7q-76-29-145-53t-129-60-104-88-73-138l-57-176-67 176q-18 48-42 89t-60 78q-34 34-76 61t-89 43l-177 57q75 29 144 53t127 60 103 89 73 137l57 176 67-176q37-97 103-168t168-103l177-57zm-125 759q0-31 20-57t49-36l99-32q34-11 53-34t30-51 20-59 20-54 33-41 58-16q32 0 59 19t38 50q6 20 11 40t13 40 17 38 25 34q16 17 39 26t48 18 49 16 44 20 31 32 12 50q0 33-18 60t-51 38q-19 6-39 11t-41 13-39 17-34 25q-24 25-35 62t-24 73-35 61-68 25q-32 0-59-19t-38-50q-6-18-11-39t-13-41-17-40-24-33q-18-17-41-27t-47-17-49-15-43-20-30-33-12-54zm583 4q-43-13-74-30t-55-41-40-55-32-74q-12 41-29 72t-42 55-55 42-71 31q81 23 128 71t71 129q15-43 31-74t40-54 53-40 75-32z"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Visibility="{Binding DataContext.IsAdvancedAIEnabled, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BoolToInvertedVisibilityConverter}}" />
</StackPanel>
</StackPanel>
</Viewbox>
<ScrollViewer
@@ -572,6 +591,24 @@
Duration="0:0:0.167" />
</animations:Implicit.HideAnimations>
</Button>
<Button
x:Name="CancelBtn"
x:Uid="CancelBtnAutomation"
Padding="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
Command="{x:Bind CancelPasteActionCommand}"
Content="{ui:FontIcon Glyph=&#xE711;,
FontSize=16}"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
IsEnabled="False"
Style="{StaticResource SubtleButtonStyle}"
Visibility="Collapsed">
<ToolTipService.ToolTip>
<TextBlock x:Uid="CancelBtnToolTip" TextWrapping="WrapWholeWords" />
</ToolTipService.ToolTip>
</Button>
<!-- Transparent overlay to show tooltip -->
<Grid
x:Name="SendBtnOverlay"
@@ -679,6 +716,10 @@
<Setter Target="Loader.IsLoading" Value="True" />
<Setter Target="InputTxtBox.IsEnabled" Value="False" />
<Setter Target="SendBtn.IsEnabled" Value="False" />
<Setter Target="SendBtn.Visibility" Value="Collapsed" />
<Setter Target="SendBtnOverlay.Visibility" Value="Collapsed" />
<Setter Target="CancelBtn.IsEnabled" Value="True" />
<Setter Target="CancelBtn.Visibility" Value="Visible" />
<Setter Target="DisclaimerPresenter.Visibility" Value="Collapsed" />
<Setter Target="LoadingText.Visibility" Value="Visible" />
</VisualState.Setters>

View File

@@ -55,9 +55,9 @@ namespace AdvancedPaste.Controls
private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(ViewModel.Busy) || e.PropertyName == nameof(ViewModel.PasteActionError))
if (e.PropertyName is nameof(ViewModel.IsBusy) or nameof(ViewModel.PasteActionError))
{
var state = ViewModel.Busy ? "LoadingState" : ViewModel.PasteActionError.HasText ? "ErrorState" : "DefaultState";
var state = ViewModel.IsBusy ? "LoadingState" : ViewModel.PasteActionError.HasText ? "ErrorState" : "DefaultState";
VisualStateManager.GoToState(this, state, true);
}
}
@@ -78,6 +78,9 @@ namespace AdvancedPaste.Controls
[RelayCommand]
private async Task GenerateCustomAIAsync() => await ViewModel.ExecuteCustomAIFormatFromCurrentQueryAsync(PasteActionSource.PromptBox);
[RelayCommand]
private async Task CancelPasteActionAsync() => await ViewModel.CancelPasteActionAsync();
private async void InputTxtBox_KeyDown(object sender, Microsoft.UI.Xaml.Input.KeyRoutedEventArgs e)
{
if (e.Key == Windows.System.VirtualKey.Enter && InputTxtBox.Text.Length > 0 && ViewModel.IsCustomAIAvailable)

View File

@@ -24,6 +24,7 @@ namespace AdvancedPaste
{
private readonly WindowMessageMonitor _msgMonitor;
private readonly IUserSettings _userSettings;
private readonly OptionsViewModel _optionsViewModel;
private bool _disposedValue;
@@ -32,8 +33,7 @@ namespace AdvancedPaste
InitializeComponent();
_userSettings = App.GetService<IUserSettings>();
var optionsViewModel = App.GetService<OptionsViewModel>();
_optionsViewModel = App.GetService<OptionsViewModel>();
var baseHeight = MinHeight;
var coreActionCount = PasteFormat.MetadataDict.Values.Count(metadata => metadata.IsCoreAction);
@@ -43,7 +43,7 @@ namespace AdvancedPaste
double GetHeight(int maxCustomActionCount) =>
baseHeight +
new PasteFormatsToHeightConverter().GetHeight(coreActionCount + _userSettings.AdditionalActions.Count) +
new PasteFormatsToHeightConverter() { MaxItems = maxCustomActionCount }.GetHeight(optionsViewModel.IsCustomAIServiceEnabled ? _userSettings.CustomActions.Count : 0);
new PasteFormatsToHeightConverter() { MaxItems = maxCustomActionCount }.GetHeight(_optionsViewModel.IsCustomAIServiceEnabled ? _userSettings.CustomActions.Count : 0);
MinHeight = GetHeight(1);
Height = GetHeight(5);
@@ -52,9 +52,9 @@ namespace AdvancedPaste
UpdateHeight();
_userSettings.Changed += (_, _) => UpdateHeight();
optionsViewModel.PropertyChanged += (_, e) =>
_optionsViewModel.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(optionsViewModel.IsCustomAIServiceEnabled))
if (e.PropertyName == nameof(_optionsViewModel.IsCustomAIServiceEnabled))
{
UpdateHeight();
}
@@ -111,8 +111,9 @@ namespace AdvancedPaste
GC.SuppressFinalize(this);
}
private void WindowEx_Closed(object sender, Microsoft.UI.Xaml.WindowEventArgs args)
private async void WindowEx_Closed(object sender, Microsoft.UI.Xaml.WindowEventArgs args)
{
await _optionsViewModel.CancelPasteActionAsync();
Hide();
args.Handled = true;
}

View File

@@ -4,10 +4,14 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using AdvancedPaste.Models;
using ManagedCommon;
using Microsoft.Win32;
using Windows.ApplicationModel.DataTransfer;
using Windows.Data.Html;
using Windows.Graphics.Imaging;
@@ -18,8 +22,6 @@ namespace AdvancedPaste.Helpers;
internal static class DataPackageHelpers
{
private static readonly Lazy<HashSet<string>> ImageFileTypes = new(GetImageFileTypes());
private static readonly (string DataFormat, ClipboardFormat ClipboardFormat)[] DataFormats =
[
(StandardDataFormats.Text, ClipboardFormat.Text),
@@ -27,6 +29,14 @@ internal static class DataPackageHelpers
(StandardDataFormats.Bitmap, ClipboardFormat.Image),
];
private static readonly Lazy<(ClipboardFormat Format, HashSet<string> FileTypes)[]> SupportedFileTypes =
new(() =>
[
(ClipboardFormat.Image, GetImageFileTypes()),
(ClipboardFormat.Audio, GetMediaFileTypes("audio")),
(ClipboardFormat.Video, GetMediaFileTypes("video")),
]);
internal static DataPackage CreateFromText(string text)
{
DataPackage dataPackage = new();
@@ -57,9 +67,12 @@ internal static class DataPackageHelpers
{
availableFormats |= ClipboardFormat.File;
if (ImageFileTypes.Value.Contains(file.FileType))
foreach (var (format, fileTypes) in SupportedFileTypes.Value)
{
availableFormats |= ClipboardFormat.Image;
if (fileTypes.Contains(file.FileType))
{
availableFormats |= format;
}
}
}
}
@@ -93,6 +106,60 @@ internal static class DataPackageHelpers
return availableFormats == ClipboardFormat.Text ? !string.IsNullOrEmpty(await dataPackageView.GetTextAsync()) : availableFormats != ClipboardFormat.None;
}
internal static async Task TryCleanupAfterDelayAsync(this DataPackageView dataPackageView, TimeSpan delay)
{
try
{
var tempFile = await GetSingleTempFileOrNullAsync(dataPackageView);
if (tempFile != null)
{
await Task.Delay(delay);
Logger.LogDebug($"Cleaning up temporary file with extension [{tempFile.Extension}] from data package after delay");
tempFile.Delete();
if (NormalizeDirectoryPath(tempFile.Directory?.Parent?.FullName) == NormalizeDirectoryPath(Path.GetTempPath()))
{
tempFile.Directory?.Delete();
}
}
}
catch (Exception ex)
{
Logger.LogError("Failed to clean up temporary files", ex);
}
}
private static async Task<FileInfo> GetSingleTempFileOrNullAsync(this DataPackageView dataPackageView)
{
if (!dataPackageView.Contains(StandardDataFormats.StorageItems))
{
return null;
}
var storageItems = await dataPackageView.GetStorageItemsAsync();
if (storageItems.Count != 1 || storageItems.Single() is not StorageFile file)
{
return null;
}
FileInfo fileInfo = new(file.Path);
var tempPathDirectory = NormalizeDirectoryPath(Path.GetTempPath());
var directoryPaths = new[] { fileInfo.Directory, fileInfo.Directory?.Parent }
.Where(directory => directory != null)
.Select(directory => NormalizeDirectoryPath(directory.FullName));
return directoryPaths.Contains(NormalizeDirectoryPath(Path.GetTempPath())) ? fileInfo : null;
}
private static string NormalizeDirectoryPath(string path) =>
Path.GetFullPath(new Uri(path).LocalPath)
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
.ToUpperInvariant();
internal static async Task<string> GetTextOrEmptyAsync(this DataPackageView dataPackageView) =>
dataPackageView.Contains(StandardDataFormats.Text) ? await dataPackageView.GetTextAsync() : string.Empty;
@@ -153,4 +220,27 @@ internal static class DataPackageHelpers
BitmapDecoder.GetDecoderInformationEnumerator()
.SelectMany(di => di.FileExtensions)
.ToHashSet(StringComparer.InvariantCultureIgnoreCase);
private static HashSet<string> GetMediaFileTypes(string mediaKind)
{
static string AssocQueryString(NativeMethods.AssocStr assocStr, string extension)
{
uint pcchOut = 0;
NativeMethods.AssocQueryString(NativeMethods.AssocF.None, assocStr, extension, null, null, ref pcchOut);
StringBuilder pszOut = new((int)pcchOut);
var hResult = NativeMethods.AssocQueryString(NativeMethods.AssocF.None, assocStr, extension, null, pszOut, ref pcchOut);
return hResult == NativeMethods.HResult.Ok ? pszOut.ToString() : string.Empty;
}
var comparison = StringComparison.OrdinalIgnoreCase;
var extensions = from extension in Registry.ClassesRoot.GetSubKeyNames()
where extension.StartsWith('.')
where AssocQueryString(NativeMethods.AssocStr.PerceivedType, extension).Equals(mediaKind, comparison) ||
AssocQueryString(NativeMethods.AssocStr.ContentType, extension).StartsWith($"{mediaKind}/", comparison)
select extension;
return extensions.ToHashSet(StringComparer.InvariantCultureIgnoreCase);
}
}

View File

@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Models;
@@ -17,6 +18,8 @@ internal static class KernelExtensions
private const string DataPackageKey = "DataPackage";
private const string LastErrorKey = "LastError";
private const string ActionChainKey = "ActionChain";
private const string CancellationTokenKey = "CancellationToken";
private const string ProgressKey = "Progress";
internal static DataPackageView GetDataPackageView(this Kernel kernel)
{
@@ -40,6 +43,14 @@ internal static class KernelExtensions
internal static void SetDataPackageView(this Kernel kernel, DataPackageView dataPackageView) => kernel.Data[DataPackageKey] = dataPackageView;
internal static CancellationToken GetCancellationToken(this Kernel kernel) => kernel.Data.TryGetValue(CancellationTokenKey, out object value) ? (CancellationToken)value : CancellationToken.None;
internal static void SetCancellationToken(this Kernel kernel, CancellationToken cancellationToken) => kernel.Data[CancellationTokenKey] = cancellationToken;
internal static IProgress<double> GetProgress(this Kernel kernel) => kernel.Data.TryGetValue(ProgressKey, out object obj) ? obj as IProgress<double> : null;
internal static void SetProgress(this Kernel kernel, IProgress<double> progress) => kernel.Data[ProgressKey] = progress;
internal static Exception GetLastError(this Kernel kernel) => kernel.Data.TryGetValue(LastErrorKey, out object obj) ? obj as Exception : null;
internal static void SetLastError(this Kernel kernel, Exception error) => kernel.Data[LastErrorKey] = error;

View File

@@ -4,6 +4,7 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace AdvancedPaste.Helpers
{
@@ -83,6 +84,68 @@ namespace AdvancedPaste.Helpers
Scancode = 0x0008,
}
public enum HResult
{
Ok = 0x0000,
False = 0x0001,
InvalidArguments = unchecked((int)0x80070057),
OutOfMemory = unchecked((int)0x8007000E),
NoInterface = unchecked((int)0x80004002),
Fail = unchecked((int)0x80004005),
ExtractionFailed = unchecked((int)0x8004B200),
ElementNotFound = unchecked((int)0x80070490),
TypeElementNotFound = unchecked((int)0x8002802B),
NoObject = unchecked((int)0x800401E5),
Win32ErrorCanceled = 1223,
Canceled = unchecked((int)0x800704C7),
ResourceInUse = unchecked((int)0x800700AA),
AccessDenied = unchecked((int)0x80030005),
}
[Flags]
public enum AssocF
{
None = 0,
Init_NoRemapCLSID = 0x1,
Init_ByExeName = 0x2,
Open_ByExeName = 0x3,
Init_DefaultToStar = 0x4,
Init_DefaultToFolder = 0x8,
NoUserSettings = 0x10,
NoTruncate = 0x20,
Verify = 0x40,
RemapRunDll = 0x80,
NoFixUps = 0x100,
IgnoreBaseClass = 0x200,
}
public enum AssocStr
{
Command = 1,
Executable,
FriendlyDocName,
FriendlyAppName,
NoOpen,
ShellNewValue,
DDECommand,
DDEIfExec,
DDEApplication,
DDETopic,
InfoTip,
QuickTip,
TileInfo,
ContentType,
DefaultIcon,
ShellExtension,
PerceivedType,
DelegateExecute,
SupportedUriProtocols,
ProgId,
AppId,
AppPublisher,
AppIconReference,
}
[DllImport("user32.dll")]
internal static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
@@ -100,5 +163,8 @@ namespace AdvancedPaste.Helpers
[DllImport("user32.dll")]
internal static extern bool GetCursorPos(out PointInter lpPoint);
[DllImport("Shlwapi.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern HResult AssocQueryString(AssocF flags, AssocStr str, string pszAssoc, string pszExtra, [Out] StringBuilder pszOut, [In][Out] ref uint pcchOut);
}
}

View File

@@ -4,6 +4,7 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Windows.Globalization;
@@ -15,11 +16,14 @@ namespace AdvancedPaste.Helpers;
public static class OcrHelpers
{
public static async Task<string> ExtractTextAsync(SoftwareBitmap bitmap)
public static async Task<string> ExtractTextAsync(SoftwareBitmap bitmap, CancellationToken cancellationToken)
{
var ocrLanguage = GetOCRLanguage() ?? throw new InvalidOperationException("Unable to determine OCR language");
cancellationToken.ThrowIfCancellationRequested();
var ocrEngine = OcrEngine.TryCreateFromLanguage(ocrLanguage) ?? throw new InvalidOperationException("Unable to create OCR engine");
cancellationToken.ThrowIfCancellationRequested();
var ocrResult = await ocrEngine.RecognizeAsync(bitmap);
return string.IsNullOrWhiteSpace(ocrResult.Text)

View File

@@ -0,0 +1,167 @@
// 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.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Models;
using ManagedCommon;
using Windows.ApplicationModel.DataTransfer;
using Windows.Media.MediaProperties;
using Windows.Media.Transcoding;
using Windows.Storage;
namespace AdvancedPaste.Helpers;
internal static class TranscodeHelpers
{
public static async Task<DataPackage> TranscodeToMp3Async(DataPackageView clipboardData, CancellationToken cancellationToken, IProgress<double> progress) =>
await TranscodeMediaAsync(clipboardData, MediaEncodingProfile.CreateMp3(AudioEncodingQuality.High), ".mp3", cancellationToken, progress);
public static async Task<DataPackage> TranscodeToMp4Async(DataPackageView clipboardData, CancellationToken cancellationToken, IProgress<double> progress) =>
await TranscodeMediaAsync(clipboardData, MediaEncodingProfile.CreateMp4(VideoEncodingQuality.HD1080p), ".mp4", cancellationToken, progress);
private static async Task<DataPackage> TranscodeMediaAsync(DataPackageView clipboardData, MediaEncodingProfile baseOutputProfile, string extension, CancellationToken cancellationToken, IProgress<double> progress)
{
Logger.LogTrace();
var inputFiles = await clipboardData.GetStorageItemsAsync();
if (inputFiles.Count != 1)
{
throw new InvalidOperationException($"{nameof(TranscodeMediaAsync)} does not support multiple files");
}
var inputFile = inputFiles.Single() as StorageFile ?? throw new InvalidOperationException($"{nameof(TranscodeMediaAsync)} only supports files");
var inputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(inputFile.Path);
var inputProfile = await MediaEncodingProfile.CreateFromFileAsync(inputFile);
var outputProfile = CreateOutputProfile(inputProfile, baseOutputProfile);
#if DEBUG
static string ProfileToString(MediaEncodingProfile profile) => System.Text.Json.JsonSerializer.Serialize(profile, options: new() { WriteIndented = true });
Logger.LogDebug($"{nameof(inputProfile)}: {ProfileToString(inputProfile)}");
Logger.LogDebug($"{nameof(outputProfile)}: {ProfileToString(outputProfile)}");
#endif
var outputFolder = await Task.Run(() => Directory.CreateTempSubdirectory("PowerToys_AdvancedPaste_"), cancellationToken);
var outputFileName = StringComparer.OrdinalIgnoreCase.Equals(Path.GetExtension(inputFile.Path), extension) ? inputFileNameWithoutExtension + "_1" : inputFileNameWithoutExtension;
var outputFilePath = Path.Combine(outputFolder.FullName, Path.ChangeExtension(outputFileName, extension));
await File.WriteAllBytesAsync(outputFilePath, [], cancellationToken); // TranscodeAsync seems to require the output file to exist
await TranscodeMediaAsync(inputFile, await StorageFile.GetFileFromPathAsync(outputFilePath), outputProfile, cancellationToken, progress);
return await DataPackageHelpers.CreateFromFileAsync(outputFilePath);
}
private static MediaEncodingProfile CreateOutputProfile(MediaEncodingProfile inputProfile, MediaEncodingProfile baseOutputProfile)
{
MediaEncodingProfile outputProfile = new()
{
Video = null,
Audio = null,
};
outputProfile.Container = baseOutputProfile.Container.Copy();
if (inputProfile.Video != null && baseOutputProfile.Video != null)
{
outputProfile.Video = baseOutputProfile.Video.Copy();
if (inputProfile.Video.Bitrate != 0)
{
outputProfile.Video.Bitrate = inputProfile.Video.Bitrate;
}
if (inputProfile.Video.FrameRate.Numerator != 0)
{
outputProfile.Video.FrameRate.Numerator = inputProfile.Video.FrameRate.Numerator;
}
if (inputProfile.Video.FrameRate.Denominator != 0)
{
outputProfile.Video.FrameRate.Denominator = inputProfile.Video.FrameRate.Denominator;
}
if (inputProfile.Video.PixelAspectRatio.Numerator != 0)
{
outputProfile.Video.PixelAspectRatio.Numerator = inputProfile.Video.PixelAspectRatio.Numerator;
}
if (inputProfile.Video.PixelAspectRatio.Denominator != 0)
{
outputProfile.Video.PixelAspectRatio.Denominator = inputProfile.Video.PixelAspectRatio.Denominator;
}
outputProfile.Video.Width = inputProfile.Video.Width;
outputProfile.Video.Height = inputProfile.Video.Height;
}
if (inputProfile.Audio != null && baseOutputProfile.Audio != null)
{
outputProfile.Audio = baseOutputProfile.Audio.Copy();
if (inputProfile.Audio.Bitrate != 0)
{
outputProfile.Audio.Bitrate = inputProfile.Audio.Bitrate;
}
if (inputProfile.Audio.BitsPerSample != 0)
{
outputProfile.Audio.BitsPerSample = inputProfile.Audio.BitsPerSample;
}
if (inputProfile.Audio.ChannelCount != 0)
{
outputProfile.Audio.ChannelCount = inputProfile.Audio.ChannelCount;
}
if (inputProfile.Audio.SampleRate != 0)
{
outputProfile.Audio.SampleRate = inputProfile.Audio.SampleRate;
}
}
return outputProfile;
}
private static async Task TranscodeMediaAsync(StorageFile inputFile, StorageFile outputFile, MediaEncodingProfile outputProfile, CancellationToken cancellationToken, IProgress<double> progress)
{
if (outputProfile.Video == null && outputProfile.Audio == null)
{
throw new InvalidOperationException("Target profile does not contain media");
}
async Task<PrepareTranscodeResult> GetPrepareResult(bool hardwareAccelerationEnabled)
{
MediaTranscoder transcoder = new()
{
AlwaysReencode = false,
HardwareAccelerationEnabled = hardwareAccelerationEnabled,
};
return await transcoder.PrepareFileTranscodeAsync(inputFile, outputFile, outputProfile);
}
var prepareResult = await GetPrepareResult(hardwareAccelerationEnabled: true);
if (!prepareResult.CanTranscode)
{
Logger.LogWarning($"Unable to transcode with hardware acceleration enabled, falling back to software; {nameof(prepareResult.FailureReason)}={prepareResult.FailureReason}");
prepareResult = await GetPrepareResult(hardwareAccelerationEnabled: false);
}
if (!prepareResult.CanTranscode)
{
var message = ResourceLoaderInstance.ResourceLoader.GetString(prepareResult.FailureReason == TranscodeFailureReason.CodecNotFound ? "TranscodeErrorUnsupportedCodec" : "TranscodeErrorGeneral");
throw new PasteActionException(message, new InvalidOperationException($"Error transcoding; {nameof(prepareResult.FailureReason)}={prepareResult.FailureReason}"));
}
await prepareResult.TranscodeAsync().AsTask(cancellationToken, progress);
}
}

View File

@@ -5,6 +5,7 @@
using System;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Models;
@@ -17,17 +18,19 @@ namespace AdvancedPaste.Helpers;
public static class TransformHelpers
{
public static async Task<DataPackage> TransformAsync(PasteFormats format, DataPackageView clipboardData)
public static async Task<DataPackage> TransformAsync(PasteFormats format, DataPackageView clipboardData, CancellationToken cancellationToken, IProgress<double> progress)
{
return format switch
{
PasteFormats.PlainText => await ToPlainTextAsync(clipboardData),
PasteFormats.Markdown => await ToMarkdownAsync(clipboardData),
PasteFormats.Json => await ToJsonAsync(clipboardData),
PasteFormats.ImageToText => await ImageToTextAsync(clipboardData),
PasteFormats.PasteAsTxtFile => await ToTxtFileAsync(clipboardData),
PasteFormats.PasteAsPngFile => await ToPngFileAsync(clipboardData),
PasteFormats.PasteAsHtmlFile => await ToHtmlFileAsync(clipboardData),
PasteFormats.ImageToText => await ImageToTextAsync(clipboardData, cancellationToken),
PasteFormats.PasteAsTxtFile => await ToTxtFileAsync(clipboardData, cancellationToken),
PasteFormats.PasteAsPngFile => await ToPngFileAsync(clipboardData, cancellationToken),
PasteFormats.PasteAsHtmlFile => await ToHtmlFileAsync(clipboardData, cancellationToken),
PasteFormats.TranscodeToMp3 => await TranscodeHelpers.TranscodeToMp3Async(clipboardData, cancellationToken, progress),
PasteFormats.TranscodeToMp4 => await TranscodeHelpers.TranscodeToMp4Async(clipboardData, cancellationToken, progress),
PasteFormats.KernelQuery => throw new ArgumentException($"Unsupported format {format}", nameof(format)),
PasteFormats.CustomTextTransformation => throw new ArgumentException($"Unsupported format {format}", nameof(format)),
_ => throw new ArgumentException($"Unknown value {format}", nameof(format)),
@@ -52,16 +55,16 @@ public static class TransformHelpers
return CreateDataPackageFromText(await JsonHelper.ToJsonFromXmlOrCsvAsync(clipboardData));
}
private static async Task<DataPackage> ImageToTextAsync(DataPackageView clipboardData)
private static async Task<DataPackage> ImageToTextAsync(DataPackageView clipboardData, CancellationToken cancellationToken)
{
Logger.LogTrace();
var bitmap = await clipboardData.GetImageContentAsync() ?? throw new ArgumentException("No image content found in clipboard", nameof(clipboardData));
var text = await OcrHelpers.ExtractTextAsync(bitmap);
var text = await OcrHelpers.ExtractTextAsync(bitmap, cancellationToken);
return CreateDataPackageFromText(text);
}
private static async Task<DataPackage> ToPngFileAsync(DataPackageView clipboardData)
private static async Task<DataPackage> ToPngFileAsync(DataPackageView clipboardData, CancellationToken cancellationToken)
{
Logger.LogTrace();
@@ -72,25 +75,25 @@ public static class TransformHelpers
encoder.SetSoftwareBitmap(clipboardBitmap);
await encoder.FlushAsync();
return await CreateDataPackageFromFileContentAsync(pngStream.AsStreamForRead(), "png");
return await CreateDataPackageFromFileContentAsync(pngStream.AsStreamForRead(), "png", cancellationToken);
}
private static async Task<DataPackage> ToTxtFileAsync(DataPackageView clipboardData)
private static async Task<DataPackage> ToTxtFileAsync(DataPackageView clipboardData, CancellationToken cancellationToken)
{
Logger.LogTrace();
var text = await clipboardData.GetTextOrHtmlTextAsync();
return await CreateDataPackageFromFileContentAsync(text, "txt");
return await CreateDataPackageFromFileContentAsync(text, "txt", cancellationToken);
}
private static async Task<DataPackage> ToHtmlFileAsync(DataPackageView clipboardData)
private static async Task<DataPackage> ToHtmlFileAsync(DataPackageView clipboardData, CancellationToken cancellationToken)
{
Logger.LogTrace();
var cfHtml = await clipboardData.GetHtmlContentAsync();
var html = RemoveHtmlMetadata(cfHtml);
return await CreateDataPackageFromFileContentAsync(html, "html");
return await CreateDataPackageFromFileContentAsync(html, "html", cancellationToken);
}
/// <summary>
@@ -114,7 +117,7 @@ public static class TransformHelpers
return (startFragmentIndex == null || endFragmentIndex == null) ? cfHtml : cfHtml[startFragmentIndex.Value..endFragmentIndex.Value];
}
private static async Task<DataPackage> CreateDataPackageFromFileContentAsync(string data, string fileExtension)
private static async Task<DataPackage> CreateDataPackageFromFileContentAsync(string data, string fileExtension, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(data))
{
@@ -123,16 +126,16 @@ public static class TransformHelpers
var path = GetPasteAsFileTempFilePath(fileExtension);
await File.WriteAllTextAsync(path, data);
await File.WriteAllTextAsync(path, data, cancellationToken);
return await DataPackageHelpers.CreateFromFileAsync(path);
}
private static async Task<DataPackage> CreateDataPackageFromFileContentAsync(Stream stream, string fileExtension)
private static async Task<DataPackage> CreateDataPackageFromFileContentAsync(Stream stream, string fileExtension, CancellationToken cancellationToken)
{
var path = GetPasteAsFileTempFilePath(fileExtension);
using var fileStream = File.Create(path);
await stream.CopyToAsync(fileStream);
await stream.CopyToAsync(fileStream, cancellationToken);
return await DataPackageHelpers.CreateFromFileAsync(path);
}

View File

@@ -108,7 +108,9 @@ namespace AdvancedPaste.Settings
(PasteFormats.ImageToText, [sourceAdditionalActions.ImageToText]),
(PasteFormats.PasteAsTxtFile, [sourceAdditionalActions.PasteAsFile, sourceAdditionalActions.PasteAsFile.PasteAsTxtFile]),
(PasteFormats.PasteAsPngFile, [sourceAdditionalActions.PasteAsFile, sourceAdditionalActions.PasteAsFile.PasteAsPngFile]),
(PasteFormats.PasteAsHtmlFile, [sourceAdditionalActions.PasteAsFile, sourceAdditionalActions.PasteAsFile.PasteAsHtmlFile])
(PasteFormats.PasteAsHtmlFile, [sourceAdditionalActions.PasteAsFile, sourceAdditionalActions.PasteAsFile.PasteAsHtmlFile]),
(PasteFormats.TranscodeToMp3, [sourceAdditionalActions.Transcode, sourceAdditionalActions.Transcode.TranscodeToMp3]),
(PasteFormats.TranscodeToMp4, [sourceAdditionalActions.Transcode, sourceAdditionalActions.Transcode.TranscodeToMp4]),
];
_additionalActions.Clear();

View File

@@ -13,6 +13,7 @@ public enum ClipboardFormat
Text = 1 << 0,
Html = 1 << 1,
Audio = 1 << 2,
Image = 1 << 3,
File = 1 << 4, // output only for now
Video = 1 << 3,
Image = 1 << 4,
File = 1 << 5, // output only for now
}

View File

@@ -30,7 +30,7 @@ public sealed class PasteActionError
public static PasteActionError FromException(Exception ex) =>
new()
{
Text = ex is PasteActionException ? ex.Message : ResourceLoaderInstance.ResourceLoader.GetString("PasteError"),
Text = ex is PasteActionException ? ex.Message : ResourceLoaderInstance.ResourceLoader.GetString(ex is OperationCanceledException ? "PasteActionCanceled" : "PasteError"),
Details = (ex as PasteActionException)?.AIServiceMessage ?? string.Empty,
};
}

View File

@@ -82,12 +82,34 @@ public enum PasteFormats
KernelFunctionDescription = "Takes HTML data in the clipboard and transforms it to an HTML file.")]
PasteAsHtmlFile,
[PasteFormatMetadata(
IsCoreAction = false,
ResourceId = "TranscodeToMp3",
IconGlyph = "\uE8D6",
RequiresAIService = false,
CanPreview = false,
SupportedClipboardFormats = ClipboardFormat.Audio | ClipboardFormat.Video,
IPCKey = AdvancedPasteTranscodeAction.PropertyNames.TranscodeToMp3,
KernelFunctionDescription = "Takes an audio or video file in the clipboard and transcodes it to MP3.")]
TranscodeToMp3,
[PasteFormatMetadata(
IsCoreAction = false,
ResourceId = "TranscodeToMp4",
IconGlyph = "\uE714",
RequiresAIService = false,
CanPreview = false,
SupportedClipboardFormats = ClipboardFormat.Video,
IPCKey = AdvancedPasteTranscodeAction.PropertyNames.TranscodeToMp4,
KernelFunctionDescription = "Takes a video file in the clipboard and transcodes it to MP4 (H.264/AAC).")]
TranscodeToMp4,
[PasteFormatMetadata(
IsCoreAction = false,
IconGlyph = "\uE945",
RequiresAIService = true,
CanPreview = true,
SupportedClipboardFormats = ClipboardFormat.Text | ClipboardFormat.Html | ClipboardFormat.Audio | ClipboardFormat.Image,
SupportedClipboardFormats = ClipboardFormat.Text | ClipboardFormat.Html | ClipboardFormat.Audio | ClipboardFormat.Video | ClipboardFormat.Image,
RequiresPrompt = true)]
KernelQuery,

View File

@@ -2,11 +2,13 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace AdvancedPaste.Services;
public interface ICustomTextTransformService
{
Task<string> TransformTextAsync(string prompt, string inputText);
Task<string> TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress<double> progress);
}

View File

@@ -2,6 +2,8 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using System.Threading.Tasks;
using Windows.ApplicationModel.DataTransfer;
@@ -10,5 +12,5 @@ namespace AdvancedPaste.Services;
public interface IKernelService
{
Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery);
Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery, CancellationToken cancellationToken, IProgress<double> progress);
}

View File

@@ -2,6 +2,8 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Models;
@@ -11,5 +13,5 @@ namespace AdvancedPaste.Services;
public interface IPasteFormatExecutor
{
Task<DataPackage> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source);
Task<DataPackage> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source, CancellationToken cancellationToken, IProgress<double> progress);
}

View File

@@ -2,11 +2,12 @@
// 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.Threading;
using System.Threading.Tasks;
namespace AdvancedPaste.Services;
public interface IPromptModerationService
{
Task ValidateAsync(string fullPrompt);
Task ValidateAsync(string fullPrompt, CancellationToken cancellationToken);
}

View File

@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
@@ -36,12 +37,14 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
protected abstract AIServiceUsage GetAIServiceUsage(ChatMessageContent chatMessage);
public async Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery)
public async Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery, CancellationToken cancellationToken, IProgress<double> progress)
{
Logger.LogTrace();
var kernel = CreateKernel();
kernel.SetDataPackageView(clipboardData);
kernel.SetCancellationToken(cancellationToken);
kernel.SetProgress(progress);
CacheKey cacheKey = new() { Prompt = prompt, AvailableFormats = await clipboardData.GetAvailableFormatsAsync() };
var maybeCacheValue = _queryCacheService.ReadOrNull(cacheKey);
@@ -51,7 +54,7 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
try
{
(chatHistory, var usage) = cacheUsed ? await ExecuteCachedActionChain(kernel, maybeCacheValue.ActionChain) : await ExecuteAICompletion(kernel, prompt);
(chatHistory, var usage) = cacheUsed ? await ExecuteCachedActionChain(kernel, maybeCacheValue.ActionChain) : await ExecuteAICompletion(kernel, prompt, cancellationToken);
LogResult(cacheUsed, isSavedQuery, kernel.GetOrAddActionChain(), usage);
@@ -84,7 +87,7 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
AdvancedPasteSemanticKernelErrorEvent errorEvent = new(ex is PasteActionModeratedException ? PasteActionModeratedException.ErrorDescription : ex.Message);
PowerToysTelemetry.Log.WriteEvent(errorEvent);
if (ex is PasteActionException)
if (ex is PasteActionException or OperationCanceledException)
{
throw;
}
@@ -127,7 +130,7 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
return $"{combinedSystemMessage}{newLine}{newLine}User instructions:{newLine}{userPromptMessage.Content}";
}
private async Task<(ChatHistory ChatHistory, AIServiceUsage Usage)> ExecuteAICompletion(Kernel kernel, string prompt)
private async Task<(ChatHistory ChatHistory, AIServiceUsage Usage)> ExecuteAICompletion(Kernel kernel, string prompt, CancellationToken cancellationToken)
{
ChatHistory chatHistory = [];
@@ -141,10 +144,10 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
chatHistory.AddSystemMessage($"Available clipboard formats: {await kernel.GetDataFormatsAsync()}");
chatHistory.AddUserMessage(prompt);
await _promptModerationService.ValidateAsync(GetFullPrompt(chatHistory));
await _promptModerationService.ValidateAsync(GetFullPrompt(chatHistory), cancellationToken);
var chatResult = await kernel.GetRequiredService<IChatCompletionService>()
.GetChatMessageContentAsync(chatHistory, PromptExecutionSettings, kernel);
.GetChatMessageContentAsync(chatHistory, PromptExecutionSettings, kernel, cancellationToken);
chatHistory.Add(chatResult);
var totalUsage = chatHistory.Select(GetAIServiceUsage)
@@ -157,6 +160,8 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
{
foreach (var item in actionChain)
{
kernel.GetCancellationToken().ThrowIfCancellationRequested();
if (item.Arguments.Count > 0)
{
await ExecutePromptTransformAsync(kernel, item.Format, item.Arguments[PromptParameterName]);
@@ -208,14 +213,14 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
async dataPackageView =>
{
var input = await dataPackageView.GetTextAsync();
string output = await GetPromptBasedOutput(format, prompt, input);
string output = await GetPromptBasedOutput(format, prompt, input, kernel.GetCancellationToken(), kernel.GetProgress());
return DataPackageHelpers.CreateFromText(output);
});
private async Task<string> GetPromptBasedOutput(PasteFormats format, string prompt, string input) =>
private async Task<string> GetPromptBasedOutput(PasteFormats format, string prompt, string input, CancellationToken cancellationToken, IProgress<double> progress) =>
format switch
{
PasteFormats.CustomTextTransformation => await _customTextTransformService.TransformTextAsync(prompt, input),
PasteFormats.CustomTextTransformation => await _customTextTransformService.TransformTextAsync(prompt, input, cancellationToken, progress),
_ => throw new ArgumentException($"Unsupported format {format} for prompt transform", nameof(format)),
};
@@ -223,7 +228,7 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
ExecuteTransformAsync(
kernel,
new ActionChainItem(format, Arguments: []),
async dataPackageView => await TransformHelpers.TransformAsync(format, dataPackageView));
async dataPackageView => await TransformHelpers.TransformAsync(format, dataPackageView, kernel.GetCancellationToken(), kernel.GetProgress()));
private static async Task<string> ExecuteTransformAsync(Kernel kernel, ActionChainItem actionChainItem, Func<DataPackageView, Task<DataPackage>> transformFunc)
{

View File

@@ -4,6 +4,7 @@
using System;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
@@ -23,11 +24,11 @@ public sealed class CustomTextTransformService(IAICredentialsProvider aiCredenti
private readonly IAICredentialsProvider _aiCredentialsProvider = aiCredentialsProvider;
private readonly IPromptModerationService _promptModerationService = promptModerationService;
private async Task<Completions> GetAICompletionAsync(string systemInstructions, string userMessage)
private async Task<Completions> GetAICompletionAsync(string systemInstructions, string userMessage, CancellationToken cancellationToken)
{
var fullPrompt = systemInstructions + "\n\n" + userMessage;
await _promptModerationService.ValidateAsync(fullPrompt);
await _promptModerationService.ValidateAsync(fullPrompt, cancellationToken);
OpenAIClient azureAIClient = new(_aiCredentialsProvider.Key);
@@ -41,7 +42,8 @@ public sealed class CustomTextTransformService(IAICredentialsProvider aiCredenti
},
Temperature = 0.01F,
MaxTokens = 2000,
});
},
cancellationToken);
if (response.Value.Choices[0].FinishReason == "length")
{
@@ -51,7 +53,7 @@ public sealed class CustomTextTransformService(IAICredentialsProvider aiCredenti
return response;
}
public async Task<string> TransformTextAsync(string prompt, string inputText)
public async Task<string> TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress<double> progress)
{
if (string.IsNullOrWhiteSpace(prompt))
{
@@ -80,7 +82,7 @@ Output:
try
{
var response = await GetAICompletionAsync(systemInstructions, userMessage);
var response = await GetAICompletionAsync(systemInstructions, userMessage, cancellationToken);
var usage = response.Usage;
AdvancedPasteGenerateCustomFormatEvent telemetryEvent = new(usage.PromptTokens, usage.CompletionTokens, ModelName);
@@ -98,7 +100,7 @@ Output:
AdvancedPasteGenerateCustomErrorEvent errorEvent = new(ex is PasteActionModeratedException ? PasteActionModeratedException.ErrorDescription : ex.Message);
PowerToysTelemetry.Log.WriteEvent(errorEvent);
if (ex is PasteActionException)
if (ex is PasteActionException or OperationCanceledException)
{
throw;
}

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.ClientModel;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
@@ -18,12 +19,12 @@ public sealed class PromptModerationService(IAICredentialsProvider aiCredentials
private readonly IAICredentialsProvider _aiCredentialsProvider = aiCredentialsProvider;
public async Task ValidateAsync(string fullPrompt)
public async Task ValidateAsync(string fullPrompt, CancellationToken cancellationToken)
{
try
{
ModerationClient moderationClient = new(ModelName, _aiCredentialsProvider.Key);
var moderationClientResult = await moderationClient.ClassifyTextAsync(fullPrompt);
var moderationClientResult = await moderationClient.ClassifyTextAsync(fullPrompt, cancellationToken);
var moderationResult = moderationClientResult.Value;
Logger.LogDebug($"{nameof(PromptModerationService)}.{nameof(ValidateAsync)} complete; {nameof(moderationResult.Flagged)}={moderationResult.Flagged}");

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
@@ -17,7 +18,7 @@ public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomTex
private readonly IKernelService _kernelService = kernelService;
private readonly ICustomTextTransformService _customTextTransformService = customTextTransformService;
public async Task<DataPackage> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source)
public async Task<DataPackage> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source, CancellationToken cancellationToken, IProgress<double> progress)
{
if (!pasteFormat.IsEnabled)
{
@@ -34,9 +35,9 @@ public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomTex
return await Task.Run(async () =>
pasteFormat.Format switch
{
PasteFormats.KernelQuery => await _kernelService.TransformClipboardAsync(pasteFormat.Prompt, clipboardData, pasteFormat.IsSavedQuery),
PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText(await _customTextTransformService.TransformTextAsync(pasteFormat.Prompt, await clipboardData.GetTextAsync())),
_ => await TransformHelpers.TransformAsync(format, clipboardData),
PasteFormats.KernelQuery => await _kernelService.TransformClipboardAsync(pasteFormat.Prompt, clipboardData, pasteFormat.IsSavedQuery, cancellationToken, progress),
PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText(await _customTextTransformService.TransformTextAsync(pasteFormat.Prompt, await clipboardData.GetTextAsync(), cancellationToken, progress)),
_ => await TransformHelpers.TransformAsync(format, clipboardData, cancellationToken, progress),
});
}

View File

@@ -135,6 +135,9 @@
<data name="OpenAIApiKeyError" xml:space="preserve">
<value>OpenAI request failed with status code: </value>
</data>
<data name="PasteActionCanceled" xml:space="preserve">
<value>The paste operation was canceled</value>
</data>
<data name="PasteError" xml:space="preserve">
<value>An error occurred during the paste operation</value>
</data>
@@ -188,7 +191,19 @@
</data>
<data name="PasteAsHtmlFile" xml:space="preserve">
<value>Paste as .html file</value>
</data>
<data name="TranscodeToMp3" xml:space="preserve">
<value>Transcode to .mp3</value>
</data>
<data name="TranscodeToMp4" xml:space="preserve">
<value>Transcode to .mp4 (H.264/AAC)</value>
</data>
<data name="TranscodeErrorGeneral" xml:space="preserve">
<value>An error occurred while transcoding media file</value>
</data>
<data name="TranscodeErrorUnsupportedCodec" xml:space="preserve">
<value>The media file contains an unsupported codec</value>
</data>
<data name="PasteButtonAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Paste</value>
</data>
@@ -207,6 +222,9 @@
<data name="SendBtnToolTip.Text" xml:space="preserve">
<value>Generate and paste data</value>
</data>
<data name="CancelBtnToolTip.Text" xml:space="preserve">
<value>Cancel paste operation</value>
</data>
<data name="RegenerateBtnToolTip.Text" xml:space="preserve">
<value>Regenerate</value>
</data>
@@ -216,6 +234,9 @@
<data name="SendButtonAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Generate and paste data</value>
</data>
<data name="CancelBtnAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Cancel paste operation</value>
</data>
<data name="SettingsBtn.Content" xml:space="preserve">
<value>Open settings</value>
</data>

View File

@@ -8,6 +8,8 @@ using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO.Abstractions;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
@@ -29,7 +31,7 @@ using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
namespace AdvancedPaste.ViewModels
{
public sealed partial class OptionsViewModel : ObservableObject, IDisposable
public sealed partial class OptionsViewModel : ObservableObject, IProgress<double>, IDisposable
{
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
private readonly DispatcherTimer _clipboardTimer;
@@ -37,6 +39,8 @@ namespace AdvancedPaste.ViewModels
private readonly IPasteFormatExecutor _pasteFormatExecutor;
private readonly IAICredentialsProvider _aiCredentialsProvider;
private CancellationTokenSource _pasteActionCancellationTokenSource;
public DataPackageView ClipboardData { get; set; }
[ObservableProperty]
@@ -65,7 +69,11 @@ namespace AdvancedPaste.ViewModels
private bool _pasteFormatsDirty;
[ObservableProperty]
private bool _busy;
private bool _isBusy;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasIndeterminateTransformProgress))]
private double _transformProgress = double.NaN;
public ObservableCollection<PasteFormat> StandardPasteFormats { get; } = [];
@@ -81,9 +89,24 @@ namespace AdvancedPaste.ViewModels
public bool ClipboardHasDataForCustomAI => PasteFormat.SupportsClipboardFormats(CustomAIFormat, AvailableClipboardFormats);
public bool HasIndeterminateTransformProgress => double.IsNaN(TransformProgress);
private PasteFormats CustomAIFormat => _userSettings.IsAdvancedAIEnabled ? PasteFormats.KernelQuery : PasteFormats.CustomTextTransformation;
private bool Visible => GetMainWindow()?.Visible is true;
private bool Visible
{
get
{
try
{
return GetMainWindow()?.Visible is true;
}
catch (COMException)
{
return false; // window is closed
}
}
}
public event EventHandler PreviewRequested;
@@ -189,7 +212,12 @@ namespace AdvancedPaste.ViewModels
void UpdateFormats(ObservableCollection<PasteFormat> collection, IEnumerable<PasteFormat> pasteFormats)
{
collection.Clear();
// Hack: Clear collection via repeated RemoveAt to avoid this crash, which seems to occasionally occur when using Clear:
// https://github.com/microsoft/microsoft-ui-xaml/issues/8684
while (collection.Count > 0)
{
collection.RemoveAt(collection.Count - 1);
}
foreach (var format in FilterAndSort(pasteFormats))
{
@@ -214,12 +242,13 @@ namespace AdvancedPaste.ViewModels
public void Dispose()
{
_clipboardTimer.Stop();
_pasteActionCancellationTokenSource?.Dispose();
GC.SuppressFinalize(this);
}
public async Task ReadClipboardAsync()
{
if (Busy)
if (IsBusy)
{
return;
}
@@ -324,6 +353,10 @@ namespace AdvancedPaste.ViewModels
{
await ClipboardHelper.TryCopyPasteAsync(package, HideWindow);
Query = string.Empty;
// Delete any temp files created. A delay is needed to ensure the file is not in use by the target application -
// for example, when pasting onto File Explorer, the paste operation will trigger a file copy.
_ = Task.Run(() => package.GetView().TryCleanupAfterDelayAsync(TimeSpan.FromSeconds(30)));
}
// Command to select the previous custom format
@@ -362,7 +395,7 @@ namespace AdvancedPaste.ViewModels
internal async Task ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source)
{
if (Busy)
if (IsBusy)
{
Logger.LogWarning($"Execution of {pasteFormat.Format} from {source} suppressed as busy");
return;
@@ -377,16 +410,18 @@ namespace AdvancedPaste.ViewModels
var elapsedWatch = Stopwatch.StartNew();
Logger.LogDebug($"Started executing {pasteFormat.Format} from source {source}");
Busy = true;
IsBusy = true;
_pasteActionCancellationTokenSource = new();
TransformProgress = double.NaN;
PasteActionError = PasteActionError.None;
Query = pasteFormat.Query;
try
{
// Minimum time to show busy spinner for AI actions when triggered by global keyboard shortcut.
var aiActionMinTaskTime = TimeSpan.FromSeconds(2);
var aiActionMinTaskTime = TimeSpan.FromSeconds(1.5);
var delayTask = (Visible && source == PasteActionSource.GlobalKeyboardShortcut) ? Task.Delay(aiActionMinTaskTime) : Task.CompletedTask;
var dataPackage = await _pasteFormatExecutor.ExecutePasteFormatAsync(pasteFormat, source);
var dataPackage = await _pasteFormatExecutor.ExecutePasteFormatAsync(pasteFormat, source, _pasteActionCancellationTokenSource.Token, this);
await delayTask;
@@ -410,7 +445,9 @@ namespace AdvancedPaste.ViewModels
PasteActionError = PasteActionError.FromException(ex);
}
Busy = false;
IsBusy = false;
_pasteActionCancellationTokenSource?.Dispose();
_pasteActionCancellationTokenSource = null;
elapsedWatch.Stop();
Logger.LogDebug($"Finished executing {pasteFormat.Format} from source {source}; timeTakenMs={elapsedWatch.ElapsedMilliseconds}");
}
@@ -484,5 +521,26 @@ namespace AdvancedPaste.ViewModels
return IsAllowedByGPO && _aiCredentialsProvider.Refresh();
}
public async Task CancelPasteActionAsync()
{
if (_pasteActionCancellationTokenSource != null)
{
await _pasteActionCancellationTokenSource.CancelAsync();
}
}
void IProgress<double>.Report(double value)
{
ReportProgress(value);
}
private void ReportProgress(double value)
{
_dispatcherQueue.TryEnqueue(() =>
{
TransformProgress = value;
});
}
}
}

View File

@@ -88,6 +88,8 @@ void Trace::AdvancedPaste_SettingsTelemetry(const PowertoyModuleIface::Hotkey& p
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"ImageToText"), "ImageToTextHotkey"),
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"PasteAsTxtFile"), "PasteAsTxtFileHotkey"),
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"PasteAsPngFile"), "PasteAsPngFileHotkey"),
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"PasteAsHtmlFile"), "PasteAsHtmlFileHotkey")
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"PasteAsHtmlFile"), "PasteAsHtmlFileHotkey"),
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"TranscodeToMp3"), "TranscodeToMp3Hotkey"),
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"TranscodeToMp4"), "TranscodeToMp4Hotkey")
);
}

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// 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.

View File

@@ -22,4 +22,4 @@
<PackageReference Include="MSTest" />
<ProjectReference Include="..\..\..\common\UITestAutomation\UITestAutomation.csproj" />
</ItemGroup>
</Project>
</Project>

View File

@@ -192,7 +192,7 @@ namespace WorkspacesCsharpLibrary.Models
else
{
string appPath = AppPath.Replace("C:\\Program Files\\WindowsApps\\", string.Empty);
Regex packagedAppPathRegex = new Regex(@"(?<APPID>[^_]*)_\d+.\d+.\d+.\d+_x64__(?<PublisherID>[^\\]*)", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
Regex packagedAppPathRegex = new Regex(@"(?<APPID>[^_]*)_\d+.\d+.\d+.\d+_(:?x64|arm64)__(?<PublisherID>[^\\]*)", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
Match match = packagedAppPathRegex.Match(appPath);
_isPackagedApp = match.Success;
if (match.Success)

View File

@@ -62,10 +62,10 @@ namespace PlacementHelper
else
{
placement.showCmd = SW_RESTORE;
ScreenToWorkAreaCoords(window, monitor, rect);
placement.rcNormalPosition = rect;
}
ScreenToWorkAreaCoords(window, monitor, rect);
placement.rcNormalPosition = rect;
placement.flags |= WPF_ASYNCWINDOWPLACEMENT;
auto result = ::SetWindowPlacement(window, &placement);

View File

@@ -21,6 +21,8 @@
<v:VisibilityBoolConverter x:Key="VisibilityBoolConverter" />
<v:EnumToIntConverter x:Key="EnumToIntConverter" />
<v:AccessTextToTextConverter x:Key="AccessTextToTextConverter" />
<v:NumberBoxValueConverter x:Key="NumberBoxValueConverter" />
<v:ZeroToEmptyStringNumberFormatter x:Key="ZeroToEmptyStringNumberFormatter" />
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -10,29 +10,55 @@ using System.Windows.Data;
using ImageResizer.Properties;
namespace ImageResizer.Views
namespace ImageResizer.Views;
/// <summary>
/// Converts between double and string for text-based controls bound to Width or Height fields.
/// Optionally returns localized "Auto" text when the underlying value is 0, letting the UI show,
/// for example "(auto) x 1024 pixels".
/// </summary>
[ValueConversion(typeof(double), typeof(string))]
internal class AutoDoubleConverter : IValueConverter
{
[ValueConversion(typeof(double), typeof(string))]
internal class AutoDoubleConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
/// <summary>
/// Converts a double to a string, optionally showing "Auto" for 0 values. NaN values are
/// converted to empty strings.
/// </summary>
/// <param name="value">The value to convert from <see cref="double"/> to
/// <see cref="string"/>.</param>
/// <param name="targetType">The conversion target type. <see cref="string"/> here.</param>
/// <param name="parameter">Set to "Auto" to return the localized "Auto" string if the
/// value is 0.</param>
/// <param name="culture">The <see cref="CultureInfo"/> to use for the number formatting.
/// </param>
/// <returns>The string representation of the passed-in value.</returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
value switch
{
var d = (double)value;
double d => d switch
{
double.NaN => "0",
0 => (string)parameter == "Auto" ? Resources.Input_Auto : "0",
_ => d.ToString(culture),
},
return d != 0
? d.ToString(culture)
: (string)parameter == "Auto"
? Resources.Input_Auto
: string.Empty;
}
_ => "0",
};
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
/// <summary>
/// Converts the string representation back to a double, returning 0 if the string is empty,
/// null or not a valid number in the specified culture.
/// </summary>
/// <param name="value">The string value to convert.</param>
/// <param name="targetType">The conversion target type. <see cref="double"/> here.</param>
/// <param name="parameter">Converter parameter. Unused.</param>
/// <param name="culture">The <see cref="CultureInfo"/> to use for the text parsing.</param>
/// <returns>The corresponding double value.</returns>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
value switch
{
var text = (string)value;
return !string.IsNullOrEmpty(text)
? double.Parse(text, culture)
: 0;
}
}
null or "" => 0,
string text when double.TryParse(text, NumberStyles.Any, culture, out double result) => result,
_ => 0,
};
}

View File

@@ -4,7 +4,8 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:m="clr-namespace:ImageResizer.Models"
xmlns:p="clr-namespace:ImageResizer.Properties"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml">
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:v="clr-namespace:ImageResizer.Views">
<Grid>
<Grid.RowDefinitions>
@@ -114,8 +115,12 @@
KeyDown="Button_KeyDown"
Minimum="0"
SpinButtonPlacementMode="Inline">
<ui:NumberBox.NumberFormatter>
<v:ZeroToEmptyStringNumberFormatter />
</ui:NumberBox.NumberFormatter>
<ui:NumberBox.Value>
<Binding
Converter="{StaticResource NumberBoxValueConverter}"
ElementName="SizeComboBox"
Mode="TwoWay"
Path="SelectedValue.Width"
@@ -143,8 +148,12 @@
Minimum="0"
SpinButtonPlacementMode="Inline"
Visibility="{Binding ElementName=SizeComboBox, Path=SelectedValue.ShowHeight, Converter={StaticResource BoolValueConverter}}">
<ui:NumberBox.NumberFormatter>
<v:ZeroToEmptyStringNumberFormatter />
</ui:NumberBox.NumberFormatter>
<ui:NumberBox.Value>
<Binding
Converter="{StaticResource NumberBoxValueConverter}"
ElementName="SizeComboBox"
Mode="TwoWay"
Path="SelectedValue.Height"

View File

@@ -0,0 +1,32 @@
// 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.Globalization;
using System.Windows.Data;
namespace ImageResizer.Views;
public class NumberBoxValueConverter : IValueConverter
{
/// <summary>
/// Converts the underlying double value to a display-friendly format. Ensures that NaN values
/// are not propagated to the UI.
/// </summary>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
value is double d && double.IsNaN(d) ? 0 : value;
/// <summary>
/// Converts the user input back to the underlying double value. If the input is not a valid
/// number, 0 is returned.
/// </summary>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
value switch
{
null => 0,
double d when double.IsNaN(d) => 0,
string str when !double.TryParse(str, out _) => 0,
_ => value,
};
}

View File

@@ -0,0 +1,37 @@
// 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.Globalization;
using Wpf.Ui.Controls;
namespace ImageResizer.Views;
public class ZeroToEmptyStringNumberFormatter : INumberFormatter, INumberParser
{
public string FormatDouble(double? value) => value switch
{
null => string.Empty,
0 => string.Empty,
_ => value.Value.ToString(CultureInfo.CurrentCulture),
};
public double? ParseDouble(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return 0;
}
return double.TryParse(value, NumberStyles.Any, CultureInfo.CurrentCulture, out double result) ? result : 0;
}
public string FormatInt(int? value) => throw new NotImplementedException();
public string FormatUInt(uint? value) => throw new NotImplementedException();
public int? ParseInt(string value) => throw new NotImplementedException();
public uint? ParseUInt(string value) => throw new NotImplementedException();
}

View File

@@ -1,11 +1,561 @@
#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 <common/interop/keyboard_layout.h>
extern "C"
{
void* CreateMappingConfiguration()
{
return new MappingConfiguration();
}
void DestroyMappingConfiguration(void* config)
{
delete static_cast<MappingConfiguration*>(config);
}
bool LoadMappingSettings(void* config)
{
return static_cast<MappingConfiguration*>(config)->LoadSettings();
}
bool SaveMappingSettings(void* config)
{
return static_cast<MappingConfiguration*>(config)->SaveSettingsToFile();
}
wchar_t* AllocateAndCopyString(const std::wstring& str)
{
size_t len = str.length();
wchar_t* buffer = new wchar_t[len + 1];
wcscpy_s(buffer, len + 1, str.c_str());
return buffer;
}
int GetSingleKeyRemapCount(void* config)
{
auto mapping = static_cast<MappingConfiguration*>(config);
return static_cast<int>(mapping->singleKeyReMap.size());
}
bool GetSingleKeyRemap(void* config, int index, KeyboardMapping* 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);
if (kv.second.index() == 0)
{
mapping->targetKey = AllocateAndCopyString(std::to_wstring(std::get<DWORD>(kv.second)));
mapping->isShortcut = false;
}
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)
{
auto mappingConfig = static_cast<MappingConfiguration*>(config);
Shortcut origShortcut(originalKeys);
Shortcut targetShortcut(targetKeys);
std::wstring app(targetApp ? targetApp : L"");
if (app.empty())
{
return mappingConfig->AddOSLevelShortcut(origShortcut, targetShortcut);
}
else
{
return mappingConfig->AddAppSpecificShortcut(app, origShortcut, 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));
}
}
// Test function to call the remapping helper function
bool CheckIfRemappingsAreValid()
{
RemapBuffer remapBuffer;
@@ -18,3 +568,26 @@ bool CheckIfRemappingsAreValid()
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,4 +4,69 @@
#include <keyboardmanager/common/Input.h>
#include <keyboardmanager/common/MappingConfiguration.h>
struct KeyNamePair
{
int keyCode;
wchar_t keyName[64];
};
struct KeyboardMapping
{
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, KeyboardMapping* 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);
__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);
}
extern "C" __declspec(dllexport) bool CheckIfRemappingsAreValid();
extern "C" __declspec(dllexport) int GetKeyboardKeysList(bool isShortcut, KeyNamePair* keyList, int maxCount);

View File

@@ -0,0 +1,40 @@
#include <windows.h>
#include "resource.h"
#include "../../../common/version/version.h"
#define APSTUDIO_READONLY_SYMBOLS
#include "winres.h"
#undef APSTUDIO_READONLY_SYMBOLS
1 VERSIONINFO
FILEVERSION FILE_VERSION
PRODUCTVERSION PRODUCT_VERSION
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_DLL
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
BEGIN
VALUE "CompanyName", COMPANY_NAME
VALUE "FileDescription", FILE_DESCRIPTION
VALUE "FileVersion", FILE_VERSION_STRING
VALUE "InternalName", INTERNAL_NAME
VALUE "LegalCopyright", COPYRIGHT_NOTE
VALUE "OriginalFilename", ORIGINAL_FILENAME
VALUE "ProductName", PRODUCT_NAME
VALUE "ProductVersion", PRODUCT_VERSION_STRING
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
END
END

View File

@@ -34,6 +34,9 @@
<RootNamespace>KeyboardManagerEditorLibraryWrapper</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<PropertyGroup>
<TargetName>PowerToys.KeyboardManagerEditorLibraryWrapper</TargetName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
@@ -218,6 +221,7 @@
<ClInclude Include="framework.h" />
<ClInclude Include="KeyboardManagerEditorLibraryWrapper.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp" />
@@ -231,6 +235,9 @@
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="KeyboardManagerEditorLibraryWrapper.rc" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>

View File

@@ -24,6 +24,9 @@
<ClInclude Include="KeyboardManagerEditorLibraryWrapper.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="resource.h">
<Filter>Resource Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp">
@@ -39,4 +42,9 @@
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="KeyboardManagerEditorLibraryWrapper.rc">
<Filter>Resource Files</Filter>
</ResourceCompile>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,13 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by AlwaysOnTopModuleInterface.rc
//////////////////////////////
// Non-localizable
#define FILE_DESCRIPTION "PowerToys Keyboard Manager Editor Library Wrapper"
#define INTERNAL_NAME "PowerToys.KeyboardManagerEditorLibraryWrapper"
#define ORIGINAL_FILENAME "PowerToys.KeyboardManagerEditorLibraryWrapper.dll"
// Non-localizable
//////////////////////////////

View File

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

View File

@@ -7,7 +7,10 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.UI;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
@@ -35,8 +38,14 @@ namespace KeyboardManagerEditorUI
public App()
{
this.InitializeComponent();
Logger.InitializeLogger("\\Keyboard Manager\\WinUI3Editor\\Logs");
Logger.LogInfo("keyboard-manager WinUI3 editor logger is initialized");
Task.Run(() =>
{
Logger.InitializeLogger("\\Keyboard Manager\\WinUI3Editor\\Logs");
Logger.LogInfo("keyboard-manager WinUI3 editor logger is initialized");
});
UnhandledException += App_UnhandledException;
}
/// <summary>
@@ -46,10 +55,38 @@ namespace KeyboardManagerEditorUI
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
window = new MainWindow();
window.Activate();
var appWindow = window.AppWindow;
var windowSize = new Windows.Graphics.SizeInt32(960, 600);
appWindow.Resize(windowSize);
Task.Run(() =>
{
App.Current.Resources.MergedDictionaries.Add(new ResourceDictionary()
{
Source = new Uri("ms-appx:///Styles/CommonStyle.xaml"),
});
}).ContinueWith(_ =>
{
window.DispatcherQueue.TryEnqueue(() =>
{
window.Activate();
window.DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () =>
{
(window.Content as FrameworkElement)?.UpdateLayout();
});
});
});
Logger.LogInfo("keyboard-manager WinUI3 editor window is launched");
}
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
Logger.LogError("Unhandled exception", e.Exception);
}
private Window? window;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -0,0 +1,25 @@
// 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 Remapping
{
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";
public bool IsEnabled { get; set; } = true;
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace KeyboardManagerEditorUI.Helpers
{
public class URLShortcut
{
public List<string> Shortcut { get; set; } = new List<string>();
public string URL { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace KeyboardManagerEditorUI.Interop
{
public class KeyMapping
{
public int OriginalKey { get; set; }
public string TargetKey { get; set; } = string.Empty;
public bool IsShortcut { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace KeyboardManagerEditorUI.Interop
{
public class KeyToTextMapping
{
public int OriginalKey { get; set; }
public string TargetText { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,134 @@
// 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";
[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);
[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 KeyboardMapping 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);
[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 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);
[DllImport(DllName)]
internal static extern int GetKeyCodeFromName(
[MarshalAs(UnmanagedType.LPWStr)] string keyName);
[DllImport(DllName)]
internal static extern void FreeString(IntPtr str);
public static string GetStringAndFree(IntPtr ptr)
{
if (ptr == IntPtr.Zero)
{
return string.Empty;
}
string? result = Marshal.PtrToStringUni(ptr);
FreeString(ptr);
return result ?? string.Empty;
}
}
[StructLayout(LayoutKind.Sequential)]
public struct KeyboardMapping
{
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

@@ -0,0 +1,189 @@
// 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 class KeyboardMappingService : IDisposable
{
private IntPtr _configHandle;
private bool _disposed;
public KeyboardMappingService()
{
_configHandle = KeyboardManagerInterop.CreateMappingConfiguration();
if (_configHandle == IntPtr.Zero)
{
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(KeyboardMapping);
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 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 AddShortcutMapping(string originalKeys, string targetKeys, string targetApp = "")
{
if (string.IsNullOrEmpty(originalKeys) || string.IsNullOrEmpty(targetKeys))
{
return false;
}
return KeyboardManagerInterop.AddShortcutRemap(_configHandle, originalKeys, targetKeys, targetApp);
}
public bool SaveSettings()
{
return KeyboardManagerInterop.SaveMappingSettings(_configHandle);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (_configHandle != IntPtr.Zero)
{
KeyboardManagerInterop.DestroyMappingConfiguration(_configHandle);
_configHandle = IntPtr.Zero;
}
_disposed = true;
}
}
~KeyboardMappingService()
{
Dispose(false);
}
}
}

View File

@@ -0,0 +1,31 @@
// 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

@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace KeyboardManagerEditorUI.Interop
{
public enum ShortcutOperationType
{
RemapShortcut = 0,
RunProgram = 1,
OpenUri = 2,
}
}

View File

@@ -18,6 +18,22 @@
<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\Shortcuts.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" />
</ItemGroup>
<ItemGroup>
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
@@ -33,6 +49,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls.Markdown" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
@@ -40,7 +57,44 @@
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Assets\" />
<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\Shortcuts.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>
</ItemGroup>
<!--

View File

@@ -6,13 +6,67 @@
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>
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal">
<Button x:Name="myButton" Click="MyButton_Click">Click Me</Button>
</StackPanel>
</Window>
<NavigationView
x:Name="RootView"
Grid.Row="1"
IsBackButtonVisible="Collapsed"
IsBackEnabled="False"
IsPaneToggleButtonVisible="False"
PaneDisplayMode="Top"
SelectionChanged="RootView_SelectionChanged">
<NavigationView.MenuItems>
<NavigationViewItem Content="Remappings" Tag="Remappings">
<NavigationViewItem.Icon>
<FontIcon Glyph="&#xEDA7;" />
</NavigationViewItem.Icon>
</NavigationViewItem>
<NavigationViewItem Content="Programs" Tag="Programs">
<NavigationViewItem.Icon>
<FontIcon Glyph="&#xECAA;" />
</NavigationViewItem.Icon>
</NavigationViewItem>
<NavigationViewItem Content="Text" Tag="Text">
<NavigationViewItem.Icon>
<FontIcon Glyph="&#xE8D2;" />
</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>

View File

@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
@@ -20,23 +21,28 @@ 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);
RootView.SelectedItem = RootView.MenuItems[0];
}
private void MyButton_Click(object sender, RoutedEventArgs e)
private void RootView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
{
// Call the C++ function to check if the current remappings are valid
myButton.Content = CheckIfRemappingsAreValid() ? "Valid" : "Invalid";
if (args.SelectedItem is NavigationViewItem selectedItem)
{
switch ((string)selectedItem.Tag)
{
case "Remappings": NavigationFrame.Navigate(typeof(Pages.Shortcuts)); break;
case "Programs": NavigationFrame.Navigate(typeof(Pages.Programs)); break;
case "Text": NavigationFrame.Navigate(typeof(Pages.Text)); break;
case "URLs": NavigationFrame.Navigate(typeof(Pages.URLs)); break;
}
}
}
}
}

View File

@@ -0,0 +1,96 @@
<?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

@@ -0,0 +1,102 @@
// 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);
[DllImport("PowerToys.KeyboardManagerEditorLibraryWrapper.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern bool CheckIfRemappingsAreValid();
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 KeyComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count > 0 && e.AddedItems[0] is KeyboardKey key)
{
Console.WriteLine($"selected key: {key.KeyName} (code: {key.KeyCode})");
}
}
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

@@ -0,0 +1,215 @@
<?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="16"
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" />
<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

@@ -0,0 +1,46 @@
// 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

@@ -0,0 +1,177 @@
<?xml version="1.0" encoding="utf-8" ?>
<Page
x:Class="KeyboardManagerEditorUI.Pages.Shortcuts"
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="Remappings allow you to reconfigure a single key or shortcut to a new key or shortcut"
TextWrapping="Wrap" />
<Button
x:Name="NewShortcutBtn"
Height="36"
Margin="0,12,0,0"
Click="NewShortcutBtn_Click">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon
FontSize="16"
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 RemappedShortcuts}"
SelectionMode="None">
<ListView.ItemTemplate>
<DataTemplate x:DataType="helper:Remapping">
<Grid Height="48">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="232" />
<ColumnDefinition Width="Auto" MinWidth="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"
IsEnabled="{x:Bind IsEnabled, Mode=OneWay}"
ItemsSource="{x:Bind OriginalKeys}">
<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>
<ItemsControl
Grid.Column="1"
Margin="0,6,0,6"
VerticalAlignment="Center"
IsEnabled="{x:Bind IsEnabled, Mode=OneWay}"
ItemsSource="{x:Bind RemappedKeys}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<styles:KeyVisual 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}" />
<ToggleSwitch
Grid.ColumnSpan="4"
Margin="0,0,-112,0"
HorizontalAlignment="Right"
IsOn="{x:Bind IsEnabled, Mode=TwoWay}"
OffContent=""
OnContent="" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</StackPanel>
<ContentDialog
x:Name="KeyDialog"
Title="Remap a shortcut"
MinWidth="600"
MaxWidth="600"
PrimaryButtonStyle="{StaticResource AccentButtonStyle}"
PrimaryButtonText="Save"
SecondaryButtonText="Cancel">
<Grid>
<styles:InputControl x:Name="ShortcutControl" />
</Grid>
</ContentDialog>
</Grid>
</Page>

View File

@@ -0,0 +1,260 @@
// 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 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;
namespace KeyboardManagerEditorUI.Pages
{
public sealed partial class Shortcuts : Page, IDisposable
{
private KeyboardMappingService? _mappingService;
private bool _disposed;
public ObservableCollection<KeyMapping> SingleKeyMappings { get; } = new ObservableCollection<KeyMapping>();
public ObservableCollection<ShortcutKeyMapping> ShortcutMappings { get; } = new ObservableCollection<ShortcutKeyMapping>();
public ObservableCollection<Remapping> RemappedShortcuts { get; set; }
[DllImport("PowerToys.KeyboardManagerEditorLibraryWrapper.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
private static extern void GetKeyDisplayName(int keyCode, [Out] StringBuilder keyName, int maxLength);
public Shortcuts()
{
this.InitializeComponent();
RemappedShortcuts = new ObservableCollection<Remapping>();
_mappingService = new KeyboardMappingService();
LoadMappings();
/*
RemappedShortcuts = new ObservableCollection<Remapping>();
RemappedShortcuts.Add(new Remapping() { OriginalKeys = new List<string>() { "Ctrl", "Shift", "F" }, RemappedKeys = new List<string>() { "Shift", "F" }, IsAllApps = true });
RemappedShortcuts.Add(new Remapping() { OriginalKeys = new List<string>() { "Ctrl (Left)" }, RemappedKeys = new List<string>() { "Ctrl (Right)" }, IsAllApps = true });
RemappedShortcuts.Add(new Remapping() { OriginalKeys = new List<string>() { "Shift", "M" }, RemappedKeys = new List<string>() { "Ctrl", "M" }, IsAllApps = true });
RemappedShortcuts.Add(new Remapping() { OriginalKeys = new List<string>() { "Shift", "Alt", "B" }, RemappedKeys = new List<string>() { "Alt", "B" }, IsAllApps = false, AppName = "outlook.exe" });
RemappedShortcuts.Add(new Remapping() { OriginalKeys = new List<string>() { "Numpad 1" }, RemappedKeys = new List<string>() { "Ctrl", "F" }, IsAllApps = true });
RemappedShortcuts.Add(new Remapping() { OriginalKeys = new List<string>() { "Numpad 2" }, RemappedKeys = new List<string>() { "Alt", "F" }, IsAllApps = true, AppName = "outlook.exe" });
*/
}
private void LoadMappings()
{
if (_mappingService == null)
{
return;
}
SingleKeyMappings.Clear();
ShortcutMappings.Clear();
RemappedShortcuts.Clear();
foreach (var mapping in _mappingService.GetSingleKeyMappings())
{
SingleKeyMappings.Add(mapping);
string[] targetKeyCode = mapping.TargetKey.Split(';');
var targetKeyNames = new List<string>();
foreach (var keyCode in targetKeyCode)
{
if (int.TryParse(keyCode, out int code))
{
targetKeyNames.Add(GetKeyDisplayName(code));
}
}
RemappedShortcuts.Add(new Remapping
{
OriginalKeys = new List<string> { GetKeyDisplayName(mapping.OriginalKey) },
RemappedKeys = targetKeyNames,
IsAllApps = true,
});
}
foreach (var mapping in _mappingService.GetShortcutMappingsByType(ShortcutOperationType.RemapShortcut))
{
ShortcutMappings.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(GetKeyDisplayName(code));
}
}
foreach (var keyCode in targetKeyCodes)
{
if (int.TryParse(keyCode, out int code))
{
targetKeyNames.Add(GetKeyDisplayName(code));
}
}
RemappedShortcuts.Add(new Remapping
{
OriginalKeys = originalKeyNames,
RemappedKeys = targetKeyNames,
IsAllApps = string.IsNullOrEmpty(mapping.TargetApp),
AppName = string.IsNullOrEmpty(mapping.TargetApp) ? "All Apps" : mapping.TargetApp,
});
}
}
public static string GetKeyDisplayName(int keyCode)
{
var keyName = new StringBuilder(64);
GetKeyDisplayName(keyCode, keyName, keyName.Capacity);
return keyName.ToString();
}
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;
}
}
private async void NewShortcutBtn_Click(object sender, RoutedEventArgs e)
{
ShortcutControl.SetOriginalKeys(new List<string>());
ShortcutControl.SetRemappedKeys(new List<string>());
ShortcutControl.SetApp(false, string.Empty);
KeyDialog.PrimaryButtonClick += KeyDialog_PrimaryButtonClick;
await KeyDialog.ShowAsync();
KeyDialog.PrimaryButtonClick -= KeyDialog_PrimaryButtonClick;
}
private void KeyDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
SaveCurrentMapping();
LoadMappings();
}
private async void ListView_ItemClick(object sender, ItemClickEventArgs e)
{
if (e.ClickedItem is Remapping selectedShortcut)
{
ShortcutControl.SetOriginalKeys(selectedShortcut.OriginalKeys);
ShortcutControl.SetRemappedKeys(selectedShortcut.RemappedKeys);
ShortcutControl.SetApp(!selectedShortcut.IsAllApps, selectedShortcut.AppName);
await KeyDialog.ShowAsync();
}
}
public static int GetKeyCode(string keyName)
{
return KeyboardManagerInterop.GetKeyCodeFromName(keyName);
}
private void SaveCurrentMapping()
{
if (_mappingService == null)
{
return;
}
try
{
List<string> originalKeys = ShortcutControl.GetOriginalKeys();
List<string> remappedKeys = ShortcutControl.GetRemappedKeys();
bool isAppSpecific = ShortcutControl.GetIsAppSpecific();
string appName = ShortcutControl.GetAppName();
// mock data
// originalKeys = ["A", "Ctrl"];
// remappedKeys = ["B"];
if (originalKeys == null || originalKeys.Count == 0 || remappedKeys == null || remappedKeys.Count == 0)
{
return;
}
if (originalKeys.Count == 1)
{
int originalKey = GetKeyCode(originalKeys[0]);
if (originalKey != 0)
{
if (remappedKeys.Count == 1)
{
int targetKey = GetKeyCode(remappedKeys[0]);
if (targetKey != 0)
{
_mappingService.AddSingleKeyMapping(originalKey, targetKey);
}
}
else
{
string targetKeys = string.Join(";", remappedKeys.Select(k => GetKeyCode(k).ToString(CultureInfo.InvariantCulture)));
_mappingService.AddSingleKeyMapping(originalKey, targetKeys);
}
}
}
else
{
string originalKeysString = string.Join(";", originalKeys.Select(k => GetKeyCode(k).ToString(CultureInfo.InvariantCulture)));
string targetKeysString = string.Join(";", remappedKeys.Select(k => GetKeyCode(k).ToString(CultureInfo.InvariantCulture)));
if (isAppSpecific && !string.IsNullOrEmpty(appName))
{
_mappingService.AddShortcutMapping(originalKeysString, targetKeysString, appName);
}
else
{
_mappingService.AddShortcutMapping(originalKeysString, targetKeysString);
}
}
_mappingService.SaveSettings();
}
catch (Exception ex)
{
Logger.LogError("Error saving shortcut mapping: " + ex.Message);
}
}
}
}

View File

@@ -0,0 +1,165 @@
<?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: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 inputfield when you use the configured text"
TextWrapping="Wrap" />
<Button
x:Name="NewShortcutBtn"
Height="36"
Margin="0,12,0,0"
Click="NewShortcutBtn_Click">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon
FontSize="16"
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="Text" />
<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>
<TextBlock
Grid.Column="1"
Grid.ColumnSpan="2"
Margin="0,0,0,0"
VerticalAlignment="Center"
FontSize="12"
Text="{x:Bind URL}"
TextTrimming="CharacterEllipsis" />
<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 text shortcut"
Width="480"
Height="360"
MinWidth="600"
MaxWidth="600"
PrimaryButtonStyle="{StaticResource AccentButtonStyle}"
PrimaryButtonText="Save"
SecondaryButtonText="Cancel">
<StackPanel
Width="360"
Height="360"
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="Shift" VisualType="SmallOutline" />
<styles:KeyVisual Content="Win" VisualType="SmallOutline" />
<styles:KeyVisual Content="M" VisualType="SmallOutline" />
</StackPanel>
</ToggleButton.Content>
</ToggleButton>
<TextBox
Height="200"
Margin="0,24,0,0"
Header="Text to insert"
Text="https://www.microsoft.com"
TextWrapping="Wrap" />
</StackPanel>
</ContentDialog>
</Grid>
</Page>

View File

@@ -0,0 +1,50 @@
// 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
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class Text : Page
{
public ObservableCollection<URLShortcut> Shortcuts { get; set; }
public Text()
{
this.InitializeComponent();
Shortcuts = new ObservableCollection<URLShortcut>();
Shortcuts.Add(new URLShortcut() { Shortcut = new List<string>() { "Alt (Left)", "H" }, URL = "Hello" });
Shortcuts.Add(new URLShortcut() { Shortcut = new List<string>() { "Win (Left)", "P", }, URL = "PowerToys" });
}
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

@@ -0,0 +1,160 @@
<?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="16"
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" />
<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

@@ -0,0 +1,117 @@
// 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

@@ -0,0 +1,203 @@
<?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

@@ -0,0 +1,111 @@
<?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:KeyboardManagerEditorUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:KeyboardManagerEditorUI.Styles"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls"
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.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>
<StackPanel Orientation="Horizontal" Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:KeyVisual Content="{Binding}" VisualType="SmallOutline" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ToggleButton.Content>
</ToggleButton>
</Grid>
<Grid Grid.Row="1" Margin="0,8,0,0">
<StackPanel
Name="KeyStackPanel" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
</StackPanel>
</Grid>
<TextBlock
Grid.Row="1"
Grid.Column="1"
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>
<StackPanel Orientation="Horizontal" Spacing="4" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:KeyVisual Content="{Binding}" VisualType="Small" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ToggleButton.Content>
</ToggleButton>
</Grid>
<Grid Grid.Row="1" Grid.Column="2" Margin="0,8,0,0">
<StackPanel
Name="NewKeyStackPanel" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
</StackPanel>
</Grid>
</Grid>
<CheckBox
x:Name="AllAppsCheckBox"
Margin="0,24,0,12"
Content="Only apply this remapping to a specific application"
IsChecked="True" />
<TextBox
x:Name="AppNameTextBox"
Header="Application name"
IsEnabled="{Binding ElementName=AllAppsCheckBox, Path=IsChecked}"
PlaceholderText="e.g.: outlook.exe" />
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,229 @@
// 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 Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
namespace KeyboardManagerEditorUI.Styles
{
public sealed partial class InputControl : UserControl
{
private List<string> pressedKeys = new List<string>();
private List<string> newPressedKeys = new List<string>();
// Define newMode as a DependencyProperty for binding
public static readonly DependencyProperty NewModeProperty =
DependencyProperty.Register(
"NewMode",
typeof(bool),
typeof(InputControl),
new PropertyMetadata(false, OnNewModeChanged));
public bool NewMode
{
get { return (bool)GetValue(NewModeProperty); }
set { SetValue(NewModeProperty, value); }
}
public InputControl()
{
this.InitializeComponent();
this.KeyDown += (sender, e) => InputControl_KeyDown(sender, e, NewMode);
this.KeyUp += (sender, e) => InputControl_KeyUp(sender, e, NewMode);
}
private static void OnNewModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = d as InputControl;
if (control != null)
{
bool newMode = (bool)e.NewValue;
control.UpdateKeyDisplay(newMode);
}
}
private void InputControl_KeyDown(object sender, KeyRoutedEventArgs e, bool newMode)
{
// Get the key name and add it to the list if it's not already there
string keyName = e.Key.ToString();
keyName = NormalizeKeyName(keyName);
var currentKeyList = newMode ? newPressedKeys : pressedKeys;
if (!currentKeyList.Contains(keyName))
{
var allowedModifiers = new[] { "Shift", "Ctrl", "LWin", "RWin", "Alt" };
if (currentKeyList.All(k => allowedModifiers.Contains(k)) && pressedKeys.Count < 4)
{
currentKeyList.Add(keyName);
UpdateKeyDisplay(newMode);
}
}
}
private void InputControl_KeyUp(object sender, KeyRoutedEventArgs e, bool newMode)
{
// Console.WriteLine(newMode);
string keyName = e.Key.ToString();
var currentKeyList = newMode ? newPressedKeys : pressedKeys;
if (!currentKeyList.Contains(keyName))
{
return;
}
// Remove the key name from the list when the key is released
// currentKeyList.Remove(keyName);
UpdateKeyDisplay(newMode);
}
private void UpdateKeyDisplay(bool newMode)
{
// Clear current UI elements
if (newMode)
{
NewKeyStackPanel.Children.Clear();
} // Assuming keyPanel2 is your second stack panel
else
{
KeyStackPanel.Children.Clear();
}
var currentKeyList = newMode ? newPressedKeys : pressedKeys;
// Add each pressed key as a TextBlock in the StackPanel
foreach (var key in currentKeyList)
{
Border keyBlockContainer = new Border
{
Background = new SolidColorBrush(Microsoft.UI.Colors.White),
Padding = new Thickness(10),
Margin = new Thickness(1),
CornerRadius = new CornerRadius(3),
BorderBrush = new SolidColorBrush(Microsoft.UI.Colors.Black),
BorderThickness = new Thickness(1),
};
TextBlock keyBlock = new TextBlock
{
Text = key,
FontSize = 12,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center,
Foreground = new SolidColorBrush(Microsoft.UI.Colors.Black),
};
// Add TextBlock inside the Border container
keyBlockContainer.Child = keyBlock;
// Add Border to StackPanel
if (newMode)
{
keyBlockContainer.Background = Application.Current.Resources["AccentButtonBackgroundPressed"] as SolidColorBrush;
keyBlockContainer.BorderBrush = Application.Current.Resources["AccentButtonBackgroundPressed"] as SolidColorBrush;
keyBlock.Foreground = new SolidColorBrush(Microsoft.UI.Colors.White);
NewKeyStackPanel.Children.Add(keyBlockContainer); // For remapping keys
}
else
{
KeyStackPanel.Children.Add(keyBlockContainer); // For normal keys
}
}
}
public void SetRemappedKeys(List<string> keys)
{
RemappedKeys.ItemsSource = keys;
}
public void SetOriginalKeys(List<string> keys)
{
OriginalKeys.ItemsSource = keys;
}
public List<string> GetOriginalKeys()
{
return pressedKeys as List<string> ?? new List<string>();
}
public List<string> GetRemappedKeys()
{
return newPressedKeys as List<string> ?? new List<string>();
}
public bool GetIsAppSpecific()
{
return AllAppsCheckBox.IsChecked ?? false;
}
public string GetAppName()
{
return AppNameTextBox.Text ?? string.Empty;
}
private void RemappedToggleBtn_Checked(object sender, RoutedEventArgs e)
{
NewMode = true;
RemappedToggleBtn.IsChecked = false;
}
private void OriginalToggleBtn_Checked(object sender, RoutedEventArgs e)
{
NewMode = false;
OriginalToggleBtn.IsChecked = false;
}
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;
}
}
private void AllAppsCheckBox_Checked(object sender, RoutedEventArgs e)
{
AppNameTextBox.Visibility = Visibility.Visible;
}
private void AllAppsCheckBox_Unchecked(object sender, RoutedEventArgs e)
{
AppNameTextBox.Visibility = Visibility.Collapsed;
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
AllAppsCheckBox.Checked += AllAppsCheckBox_Checked;
AllAppsCheckBox.Unchecked += AllAppsCheckBox_Unchecked;
}
private string NormalizeKeyName(string keyName)
{
switch (keyName)
{
case "Control":
return "Ctrl";
case "Menu":
return "Alt";
case "LeftWindows":
return "LWin";
case "RightWindows":
return "RWin";
default:
return keyName;
}
}
}
}

View File

@@ -0,0 +1,198 @@
// 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

@@ -0,0 +1,156 @@
<?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

@@ -7,6 +7,8 @@
<AssemblyTitle>PowerToys.Run</AssemblyTitle>
<OutputType>WinExe</OutputType>
<UseWPF>true</UseWPF>
<!-- For some users, it crashes in ExtractAssociatedIcon https://github.com/microsoft/PowerToys/issues/37254. Workaround suggested here https://github.com/dotnet/wpf/issues/10483 was to disable CETCompat -->
<CETCompat>false</CETCompat>
<UseWindowsForms>False</UseWindowsForms>
<StartupObject>PowerLauncher.App</StartupObject>
<ApplicationIcon>Assets\PowerLauncher\RunResource.ico</ApplicationIcon>

View File

@@ -677,7 +677,7 @@ namespace PowerAccent.Core
LetterKey.VK_O => new string[] { "ο", "ό", "ω", "ώ" },
LetterKey.VK_P => new string[] { "π", "φ", "ψ" },
LetterKey.VK_R => new string[] { "ρ" },
LetterKey.VK_S => new string[] { "σ" },
LetterKey.VK_S => new string[] { "σ", "ς" },
LetterKey.VK_T => new string[] { "τ", "θ", "ϑ" },
LetterKey.VK_U => new string[] { "υ", "ύ" },
LetterKey.VK_X => new string[] { "ξ" },

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
@@ -36,4 +37,7 @@ public sealed partial class AdvancedPasteAdditionalAction : Observable, IAdvance
get => _isShown;
set => Set(ref _isShown, value);
}
[JsonIgnore]
public IEnumerable<IAdvancedPasteAction> SubActions => [];
}

View File

@@ -14,6 +14,7 @@ public sealed class AdvancedPasteAdditionalActions
{
public const string ImageToText = "image-to-text";
public const string PasteAsFile = "paste-as-file";
public const string Transcode = "transcode";
}
[JsonPropertyName(PropertyNames.ImageToText)]
@@ -22,6 +23,22 @@ public sealed class AdvancedPasteAdditionalActions
[JsonPropertyName(PropertyNames.PasteAsFile)]
public AdvancedPastePasteAsFileAction PasteAsFile { get; init; } = new();
[JsonIgnore]
public IEnumerable<IAdvancedPasteAction> AllActions => new IAdvancedPasteAction[] { ImageToText, PasteAsFile }.Concat(PasteAsFile.SubActions);
[JsonPropertyName(PropertyNames.Transcode)]
public AdvancedPasteTranscodeAction Transcode { get; init; } = new();
public IEnumerable<IAdvancedPasteAction> GetAllActions()
{
Queue<IAdvancedPasteAction> queue = new([ImageToText, PasteAsFile, Transcode]);
while (queue.Count != 0)
{
var action = queue.Dequeue();
yield return action;
foreach (var subAction in action.SubActions)
{
queue.Enqueue(subAction);
}
}
}
}

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
@@ -98,6 +99,9 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
private set => Set(ref _isValid, value);
}
[JsonIgnore]
public IEnumerable<IAdvancedPasteAction> SubActions => [];
public object Clone()
{
AdvancedPasteCustomAction clone = new();

View File

@@ -52,5 +52,5 @@ public sealed class AdvancedPastePasteAsFileAction : Observable, IAdvancedPasteA
}
[JsonIgnore]
public IEnumerable<AdvancedPasteAdditionalAction> SubActions => [PasteAsTxtFile, PasteAsPngFile, PasteAsHtmlFile];
public IEnumerable<IAdvancedPasteAction> SubActions => [PasteAsTxtFile, PasteAsPngFile, PasteAsHtmlFile];
}

View File

@@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
namespace Microsoft.PowerToys.Settings.UI.Library;
public sealed class AdvancedPasteTranscodeAction : Observable, IAdvancedPasteAction
{
public static class PropertyNames
{
public const string TranscodeToMp3 = "transcode-to-mp3";
public const string TranscodeToMp4 = "transcode-to-mp4";
}
private AdvancedPasteAdditionalAction _transcodeToMp3 = new();
private AdvancedPasteAdditionalAction _transcodeToMp4 = new();
private bool _isShown = true;
[JsonPropertyName("isShown")]
public bool IsShown
{
get => _isShown;
set => Set(ref _isShown, value);
}
[JsonPropertyName(PropertyNames.TranscodeToMp3)]
public AdvancedPasteAdditionalAction TranscodeToMp3
{
get => _transcodeToMp3;
init => Set(ref _transcodeToMp3, value);
}
[JsonPropertyName(PropertyNames.TranscodeToMp4)]
public AdvancedPasteAdditionalAction TranscodeToMp4
{
get => _transcodeToMp4;
init => Set(ref _transcodeToMp4, value);
}
[JsonIgnore]
public IEnumerable<IAdvancedPasteAction> SubActions => [TranscodeToMp3, TranscodeToMp4];
}

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.ComponentModel;
namespace Microsoft.PowerToys.Settings.UI.Library;
@@ -9,4 +10,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library;
public interface IAdvancedPasteAction : INotifyPropertyChanged
{
public bool IsShown { get; }
public IEnumerable<IAdvancedPasteAction> SubActions { get; }
}

View File

@@ -0,0 +1,61 @@
// 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.Globalization;
using Microsoft.UI.Xaml.Data;
namespace Microsoft.PowerToys.Settings.UI.Converters;
/// <summary>
/// Converts between double and string for text-based controls bound to Width or Height fields.
/// Optionally returns localized "Auto" text when the underlying value is 0, letting the UI show,
/// for example "(auto) x 1024 pixels".
/// </summary>
public sealed partial class ImageResizerDoubleToAutoConverter : IValueConverter
{
private static readonly string AutoText =
Helpers.ResourceLoaderInstance.ResourceLoader.GetString("ImageResizer_AutoText");
/// <summary>
/// Converts a double to a string, optionally showing "Auto" for 0 values. NaN values are
/// converted to empty strings.
/// </summary>
/// <param name="value">The value to convert from <see cref="double"/> to
/// <see cref="string"/>.</param>
/// <param name="targetType">The conversion target type. <see cref="string"/> here.</param>
/// <param name="parameter">Set to "Auto" to return the localized "Auto" string if the
/// value is 0.</param>
/// <param name="language">Ignored.</param>
/// <returns>The string representation of the passed-in value.</returns>
public object Convert(object value, Type targetType, object parameter, string language) =>
value switch
{
double d => d switch
{
double.NaN => "0",
0 => (string)parameter == "Auto" ? AutoText : "0",
_ => d.ToString(CultureInfo.CurrentCulture),
},
_ => "0",
};
/// <summary>
/// Converts the string representation back to a double, returning 0 if the string is empty,
/// null or not a valid number in the specified culture.
/// </summary>
/// <param name="value">The string value to convert.</param>
/// <param name="targetType">The conversion target type. <see cref="double"/> here.</param>
/// <param name="parameter">Converter parameter. Unused.</param>
/// <param name="language">Ignored.</param>
/// <returns>The corresponding double value.</returns>
public object ConvertBack(object value, Type targetType, object parameter, string language) =>
value switch
{
null or "" => 0.0,
string text when double.TryParse(text, NumberStyles.Any, CultureInfo.CurrentCulture, out double result) => result,
_ => 0.0,
};
}

View File

@@ -0,0 +1,31 @@
// 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 Microsoft.UI.Xaml.Data;
namespace Microsoft.PowerToys.Settings.UI.Converters;
public partial class ImageResizerNumberBoxValueConverter : IValueConverter
{
/// <summary>
/// Converts the underlying double value to a display-friendly format. Ensures that NaN values
/// are not propagated to the UI.
/// </summary>
public object Convert(object value, Type targetType, object parameter, string language) =>
value is double d && double.IsNaN(d) ? 0.0 : value;
/// <summary>
/// Converts the user input back to the underlying double value. If the input is not a valid
/// number, a double with value 0 is returned.
/// </summary>
public object ConvertBack(object value, Type targetType, object parameter, string language) =>
value switch
{
null => 0.0,
double d when double.IsNaN(d) => 0.0,
string str when !double.TryParse(str, out _) => 0.0,
_ => value,
};
}

View File

@@ -0,0 +1,39 @@
// 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.Globalization;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.PowerToys.Settings.UI.Converters;
public partial class ImageResizerZeroToEmptyStringNumberFormatter
{
public string Format(long value) => throw new NotImplementedException();
public string Format(ulong value) => throw new NotImplementedException();
public string Format(double value) => throw new NotImplementedException();
public string FormatDouble(double? value) => value switch
{
null => string.Empty,
0 => string.Empty,
_ => value.Value.ToString(CultureInfo.CurrentCulture),
};
public double? ParseDouble(string text)
{
if (string.IsNullOrWhiteSpace(text))
{
return 0.0;
}
return double.TryParse(text, NumberStyles.Any, CultureInfo.CurrentCulture, out double result) ? result : 0.0;
}
public long? ParseInt(string text) => throw new NotImplementedException();
public ulong? ParseUInt(string text) => throw new NotImplementedException();
}

Some files were not shown because too many files have changed in this diff Show More