From a5a354a70fe2743a5f30f0ca6b60a6afa058dc58 Mon Sep 17 00:00:00 2001 From: Dave Rayment Date: Tue, 25 Feb 2025 08:23:30 +0000 Subject: [PATCH 01/10] [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. --- src/modules/imageresizer/ui/App.xaml | 2 + .../ui/Views/AutoDoubleConverter.cs | 66 +++++++++++++------ .../imageresizer/ui/Views/InputPage.xaml | 11 +++- .../ui/Views/NumberBoxValueConverter.cs | 32 +++++++++ .../Views/ZeroToEmptyStringNumberFormatter.cs | 37 +++++++++++ .../ImageResizerDoubleToAutoConverter.cs | 61 +++++++++++++++++ .../ImageResizerNumberBoxValueConverter.cs | 31 +++++++++ ...ResizerZeroToEmptyStringNumberFormatter.cs | 39 +++++++++++ .../ImageResizerDimensionsNumberBox.cs | 38 +++++++++++ .../SettingsXAML/Views/ImageResizerPage.xaml | 15 +++-- .../Settings.UI/Strings/en-us/Resources.resw | 4 ++ 11 files changed, 309 insertions(+), 27 deletions(-) create mode 100644 src/modules/imageresizer/ui/Views/NumberBoxValueConverter.cs create mode 100644 src/modules/imageresizer/ui/Views/ZeroToEmptyStringNumberFormatter.cs create mode 100644 src/settings-ui/Settings.UI/Converters/ImageResizerDoubleToAutoConverter.cs create mode 100644 src/settings-ui/Settings.UI/Converters/ImageResizerNumberBoxValueConverter.cs create mode 100644 src/settings-ui/Settings.UI/Converters/ImageResizerZeroToEmptyStringNumberFormatter.cs create mode 100644 src/settings-ui/Settings.UI/SettingsXAML/Controls/ImageResizerDimensionsNumberBox.cs diff --git a/src/modules/imageresizer/ui/App.xaml b/src/modules/imageresizer/ui/App.xaml index 73bec9c4429e..be9316dbd8bb 100644 --- a/src/modules/imageresizer/ui/App.xaml +++ b/src/modules/imageresizer/ui/App.xaml @@ -21,6 +21,8 @@ + + \ No newline at end of file diff --git a/src/modules/imageresizer/ui/Views/AutoDoubleConverter.cs b/src/modules/imageresizer/ui/Views/AutoDoubleConverter.cs index 69f886036b1e..631416a7c0ba 100644 --- a/src/modules/imageresizer/ui/Views/AutoDoubleConverter.cs +++ b/src/modules/imageresizer/ui/Views/AutoDoubleConverter.cs @@ -10,29 +10,55 @@ using ImageResizer.Properties; -namespace ImageResizer.Views +namespace ImageResizer.Views; + +/// +/// 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". +/// +[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) + /// + /// Converts a double to a string, optionally showing "Auto" for 0 values. NaN values are + /// converted to empty strings. + /// + /// The value to convert from to + /// . + /// The conversion target type. here. + /// Set to "Auto" to return the localized "Auto" string if the + /// value is 0. + /// The to use for the number formatting. + /// + /// The string representation of the passed-in value. + 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) + /// + /// 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. + /// + /// The string value to convert. + /// The conversion target type. here. + /// Converter parameter. Unused. + /// The to use for the text parsing. + /// The corresponding double value. + 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, + }; } diff --git a/src/modules/imageresizer/ui/Views/InputPage.xaml b/src/modules/imageresizer/ui/Views/InputPage.xaml index a9b8c3dc301e..ca42f6d793fb 100644 --- a/src/modules/imageresizer/ui/Views/InputPage.xaml +++ b/src/modules/imageresizer/ui/Views/InputPage.xaml @@ -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"> @@ -114,8 +115,12 @@ KeyDown="Button_KeyDown" Minimum="0" SpinButtonPlacementMode="Inline"> + + + + + + + /// Converts the underlying double value to a display-friendly format. Ensures that NaN values + /// are not propagated to the UI. + /// + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => + value is double d && double.IsNaN(d) ? 0 : value; + + /// + /// Converts the user input back to the underlying double value. If the input is not a valid + /// number, 0 is returned. + /// + 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, + }; +} diff --git a/src/modules/imageresizer/ui/Views/ZeroToEmptyStringNumberFormatter.cs b/src/modules/imageresizer/ui/Views/ZeroToEmptyStringNumberFormatter.cs new file mode 100644 index 000000000000..a753895e58bf --- /dev/null +++ b/src/modules/imageresizer/ui/Views/ZeroToEmptyStringNumberFormatter.cs @@ -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(); +} diff --git a/src/settings-ui/Settings.UI/Converters/ImageResizerDoubleToAutoConverter.cs b/src/settings-ui/Settings.UI/Converters/ImageResizerDoubleToAutoConverter.cs new file mode 100644 index 000000000000..63ba8cde9278 --- /dev/null +++ b/src/settings-ui/Settings.UI/Converters/ImageResizerDoubleToAutoConverter.cs @@ -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; + +/// +/// 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". +/// +public sealed partial class ImageResizerDoubleToAutoConverter : IValueConverter +{ + private static readonly string AutoText = + Helpers.ResourceLoaderInstance.ResourceLoader.GetString("ImageResizer_AutoText"); + + /// + /// Converts a double to a string, optionally showing "Auto" for 0 values. NaN values are + /// converted to empty strings. + /// + /// The value to convert from to + /// . + /// The conversion target type. here. + /// Set to "Auto" to return the localized "Auto" string if the + /// value is 0. + /// Ignored. + /// The string representation of the passed-in value. + 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", + }; + + /// + /// 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. + /// + /// The string value to convert. + /// The conversion target type. here. + /// Converter parameter. Unused. + /// Ignored. + /// The corresponding double value. + 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, + }; +} diff --git a/src/settings-ui/Settings.UI/Converters/ImageResizerNumberBoxValueConverter.cs b/src/settings-ui/Settings.UI/Converters/ImageResizerNumberBoxValueConverter.cs new file mode 100644 index 000000000000..777657186df2 --- /dev/null +++ b/src/settings-ui/Settings.UI/Converters/ImageResizerNumberBoxValueConverter.cs @@ -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 +{ + /// + /// Converts the underlying double value to a display-friendly format. Ensures that NaN values + /// are not propagated to the UI. + /// + public object Convert(object value, Type targetType, object parameter, string language) => + value is double d && double.IsNaN(d) ? 0.0 : value; + + /// + /// 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. + /// + 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, + }; +} diff --git a/src/settings-ui/Settings.UI/Converters/ImageResizerZeroToEmptyStringNumberFormatter.cs b/src/settings-ui/Settings.UI/Converters/ImageResizerZeroToEmptyStringNumberFormatter.cs new file mode 100644 index 000000000000..241210f6c056 --- /dev/null +++ b/src/settings-ui/Settings.UI/Converters/ImageResizerZeroToEmptyStringNumberFormatter.cs @@ -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(); +} diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ImageResizerDimensionsNumberBox.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ImageResizerDimensionsNumberBox.cs new file mode 100644 index 000000000000..a310f2d675c1 --- /dev/null +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ImageResizerDimensionsNumberBox.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace Microsoft.PowerToys.Settings.UI.Controls; + +public partial class ImageResizerDimensionsNumberBox : NumberBox +{ + public ImageResizerDimensionsNumberBox() + { + this.Loaded += (_, _) => UpdateDisplayText(); + + this.ValueChanged += (_, _) => UpdateDisplayText(); + + this.GotFocus += (s, e) => + { + // Show "0" in the UI when focused on the empty value. This ensures that the spinbutton + // controls are usable. + if (Value is double.NaN) + { + Value = 0.0; + } + }; + + this.LostFocus += (_, _) => UpdateDisplayText(); + } + + private void UpdateDisplayText() + { + if (FocusState == FocusState.Unfocused && Value == 0) + { + Text = string.Empty; + } + } +} diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/ImageResizerPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/ImageResizerPage.xaml index 7e7d956ce91b..2db21e91adfb 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/ImageResizerPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/ImageResizerPage.xaml @@ -19,6 +19,9 @@ + + + + Text="{x:Bind Width, Mode=OneWay, Converter={StaticResource ImageResizerDoubleToAutoConverter}, ConverterParameter=Auto}" /> - + Value="{x:Bind Width, Mode=TwoWay, Converter={StaticResource ImageResizerNumberBoxValueConverter}}" /> - + Value="{x:Bind Height, Mode=TwoWay, Converter={StaticResource ImageResizerNumberBoxValueConverter}}" /> TIFF compression {Locked="TIFF"} + + (auto) + Displayed on the preset card when the Width or Height property is zero. The same as "Input_Auto" in the ImageResizerUI project's resources. + File as in a computer file From 4eb11d6f9b87806ea70219275bc73c8832ced2f5 Mon Sep 17 00:00:00 2001 From: Kai Tao <69313318+vanzue@users.noreply.github.com> Date: Tue, 25 Feb 2025 19:17:41 +0800 Subject: [PATCH 02/10] [Workspaces][ARM64] Bring icon to packaged apps (#37625) Bring icon to packaged apps --- .../WorkspacesCsharpLibrary/Models/BaseApplication.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Workspaces/WorkspacesCsharpLibrary/Models/BaseApplication.cs b/src/modules/Workspaces/WorkspacesCsharpLibrary/Models/BaseApplication.cs index 45bc2ef89e50..e3c0bff5081e 100644 --- a/src/modules/Workspaces/WorkspacesCsharpLibrary/Models/BaseApplication.cs +++ b/src/modules/Workspaces/WorkspacesCsharpLibrary/Models/BaseApplication.cs @@ -192,7 +192,7 @@ public bool IsPackagedApp else { string appPath = AppPath.Replace("C:\\Program Files\\WindowsApps\\", string.Empty); - Regex packagedAppPathRegex = new Regex(@"(?[^_]*)_\d+.\d+.\d+.\d+_x64__(?[^\\]*)", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + Regex packagedAppPathRegex = new Regex(@"(?[^_]*)_\d+.\d+.\d+.\d+_(:?x64|arm64)__(?[^\\]*)", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); Match match = packagedAppPathRegex.Match(appPath); _isPackagedApp = match.Success; if (match.Success) From 9a658eb884a5bea9d6af3a7c3befc4f9731c2738 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionu=C8=9B=20Man=C8=9Ba?= Date: Tue, 25 Feb 2025 03:34:30 -0800 Subject: [PATCH 03/10] [PTRun] Disable CETCompat in Launcher (#37550) * Disable CETCompat in Launcher * Added comment * Improved comment --- src/modules/launcher/PowerLauncher/PowerLauncher.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/modules/launcher/PowerLauncher/PowerLauncher.csproj b/src/modules/launcher/PowerLauncher/PowerLauncher.csproj index 07852dc804c0..0953b4d5a189 100644 --- a/src/modules/launcher/PowerLauncher/PowerLauncher.csproj +++ b/src/modules/launcher/PowerLauncher/PowerLauncher.csproj @@ -7,6 +7,8 @@ PowerToys.Run WinExe true + + false False PowerLauncher.App Assets\PowerLauncher\RunResource.ico From c09a5337c4481c612fd23474f4a4279463a37726 Mon Sep 17 00:00:00 2001 From: moooyo <42196638+moooyo@users.noreply.github.com> Date: Wed, 26 Feb 2025 00:12:38 +0800 Subject: [PATCH 04/10] [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) --- src/common/ManagedCommon/LanguageHelper.cs | 3 +- src/common/ManagedCommon/Logger.cs | 87 +++++++------------ src/common/ManagedCommon/ManagedCommon.csproj | 1 + src/common/ManagedCommon/NativeMethods.cs | 2 +- src/common/ManagedCommon/RunnerHelper.cs | 7 +- .../SourceGenerationContext.cs | 13 +++ src/common/ManagedCommon/ThemeListener.cs | 2 +- 7 files changed, 54 insertions(+), 61 deletions(-) create mode 100644 src/common/ManagedCommon/SerializationContext/SourceGenerationContext.cs diff --git a/src/common/ManagedCommon/LanguageHelper.cs b/src/common/ManagedCommon/LanguageHelper.cs index 85cdcd1c335e..89b48e7ccbc2 100644 --- a/src/common/ManagedCommon/LanguageHelper.cs +++ b/src/common/ManagedCommon/LanguageHelper.cs @@ -6,6 +6,7 @@ using System.IO; using System.Text.Json; using System.Text.Json.Serialization; +using ManagedCommon.Serialization; namespace ManagedCommon { @@ -35,7 +36,7 @@ public static string LoadLanguage() inputStream.Close(); reader.Dispose(); - return JsonSerializer.Deserialize(data).LanguageTag; + return JsonSerializer.Deserialize(data, SourceGenerationContext.Default.OutGoingLanguageSettings).LanguageTag; } catch (Exception) { diff --git a/src/common/ManagedCommon/Logger.cs b/src/common/ManagedCommon/Logger.cs index 78b27afefcfc..b67d63c3b573 100644 --- a/src/common/ManagedCommon/Logger.cs +++ b/src/common/ManagedCommon/Logger.cs @@ -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 + /// /// Initializes the logger and sets the path for logging. /// @@ -53,18 +61,16 @@ public static void InitializeLogger(string applicationLogPath, bool isLocalLow = 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 @@ public static void LogError(string message, Exception ex) "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 @@ private static void Log(string message, string type) 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}"; } } } diff --git a/src/common/ManagedCommon/ManagedCommon.csproj b/src/common/ManagedCommon/ManagedCommon.csproj index f3b149616c4c..bd7425307356 100644 --- a/src/common/ManagedCommon/ManagedCommon.csproj +++ b/src/common/ManagedCommon/ManagedCommon.csproj @@ -1,6 +1,7 @@  + PowerToys ManagedCommon diff --git a/src/common/ManagedCommon/NativeMethods.cs b/src/common/ManagedCommon/NativeMethods.cs index 998010f10a29..a8d05b7a4716 100644 --- a/src/common/ManagedCommon/NativeMethods.cs +++ b/src/common/ManagedCommon/NativeMethods.cs @@ -53,7 +53,7 @@ public struct INPUT internal static int Size { - get { return Marshal.SizeOf(typeof(INPUT)); } + get { return Marshal.SizeOf(); } } } diff --git a/src/common/ManagedCommon/RunnerHelper.cs b/src/common/ManagedCommon/RunnerHelper.cs index 67ca0856aec2..16ff45428efe 100644 --- a/src/common/ManagedCommon/RunnerHelper.cs +++ b/src/common/ManagedCommon/RunnerHelper.cs @@ -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 @@ public static void WaitForPowerToysRunner(int powerToysPID, Action act) 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(); } }); diff --git a/src/common/ManagedCommon/SerializationContext/SourceGenerationContext.cs b/src/common/ManagedCommon/SerializationContext/SourceGenerationContext.cs new file mode 100644 index 000000000000..24a46f68d9a4 --- /dev/null +++ b/src/common/ManagedCommon/SerializationContext/SourceGenerationContext.cs @@ -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 +{ +} diff --git a/src/common/ManagedCommon/ThemeListener.cs b/src/common/ManagedCommon/ThemeListener.cs index 4e7475458155..945ab37debab 100644 --- a/src/common/ManagedCommon/ThemeListener.cs +++ b/src/common/ManagedCommon/ThemeListener.cs @@ -14,7 +14,7 @@ namespace ManagedCommon /// Sender ThemeListener public delegate void ThemeChangedEvent(ThemeListener sender); - public class ThemeListener : IDisposable + public partial class ThemeListener : IDisposable { /// /// Gets the App Theme. From f263042aebf84c5b8d9571c0db4ff02e374607c8 Mon Sep 17 00:00:00 2001 From: Ani <115020168+drawbyperpetual@users.noreply.github.com> Date: Tue, 25 Feb 2025 22:33:39 +0100 Subject: [PATCH 05/10] [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 Co-authored-by: Dustin L. Howett Co-authored-by: Jeremy Sinclair <4016293+snickler@users.noreply.github.com> --- .github/actions/spell-check/expect.txt | 2 +- .../Mocks/NoOpKernelQueryCacheService.cs | 1 + .../Mocks/NoOpProgress.cs | 14 ++ .../AIServiceBatchIntegrationTests.cs | 6 +- .../KernelServiceIntegrationTests.cs | 3 +- .../TranscodeHelperIntegrationTests.cs | 109 ++++++++++++ .../AnimatedContentControl.xaml | 1 + .../AdvancedPasteXAML/Controls/PromptBox.xaml | 63 +++++-- .../Controls/PromptBox.xaml.cs | 7 +- .../AdvancedPasteXAML/MainWindow.xaml.cs | 13 +- .../Helpers/DataPackageHelpers.cs | 98 +++++++++- .../AdvancedPaste/Helpers/KernelExtensions.cs | 11 ++ .../AdvancedPaste/Helpers/NativeMethods.cs | 66 +++++++ .../AdvancedPaste/Helpers/OcrHelpers.cs | 6 +- .../AdvancedPaste/Helpers/TranscodeHelpers.cs | 167 ++++++++++++++++++ .../AdvancedPaste/Helpers/TransformHelpers.cs | 37 ++-- .../AdvancedPaste/Helpers/UserSettings.cs | 4 +- .../AdvancedPaste/Models/ClipboardFormat.cs | 5 +- .../AdvancedPaste/Models/PasteActionError.cs | 2 +- .../AdvancedPaste/Models/PasteFormats.cs | 24 ++- .../Services/ICustomTextTransformService.cs | 4 +- .../AdvancedPaste/Services/IKernelService.cs | 4 +- .../Services/IPasteFormatExecutor.cs | 4 +- .../Services/IPromptModerationService.cs | 3 +- .../Services/KernelServiceBase.cs | 25 +-- .../OpenAI/CustomTextTransformService.cs | 14 +- .../OpenAI/PromptModerationService.cs | 5 +- .../Services/PasteFormatExecutor.cs | 9 +- .../Strings/en-us/Resources.resw | 21 +++ .../ViewModels/OptionsViewModel.cs | 78 ++++++-- .../AdvancedPasteModuleInterface/trace.cpp | 4 +- .../AdvancedPasteAdditionalAction.cs | 4 + .../AdvancedPasteAdditionalActions.cs | 21 ++- .../AdvancedPasteCustomAction.cs | 4 + .../AdvancedPastePasteAsFileAction.cs | 2 +- .../AdvancedPasteTranscodeAction.cs | 47 +++++ .../IAdvancedPasteAction.cs | 3 + .../SettingsXAML/Views/AdvancedPaste.xaml | 31 ++++ .../Settings.UI/Strings/en-us/Resources.resw | 9 + .../ViewModels/AdvancedPasteViewModel.cs | 4 +- 40 files changed, 843 insertions(+), 92 deletions(-) create mode 100644 src/modules/AdvancedPaste/AdvancedPaste.UnitTests/Mocks/NoOpProgress.cs create mode 100644 src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/TranscodeHelperIntegrationTests.cs create mode 100644 src/modules/AdvancedPaste/AdvancedPaste/Helpers/TranscodeHelpers.cs create mode 100644 src/settings-ui/Settings.UI.Library/AdvancedPasteTranscodeAction.cs diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index efebf0ae9da4..4771acfefc24 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -1283,7 +1283,7 @@ rectp RECTSOURCE recyclebin Redist -reencode +Reencode reencoded REFCLSID REFGUID diff --git a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/Mocks/NoOpKernelQueryCacheService.cs b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/Mocks/NoOpKernelQueryCacheService.cs index e7ba121c13c6..0d0446dc926f 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/Mocks/NoOpKernelQueryCacheService.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/Mocks/NoOpKernelQueryCacheService.cs @@ -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; diff --git a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/Mocks/NoOpProgress.cs b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/Mocks/NoOpProgress.cs new file mode 100644 index 000000000000..b5e55469a549 --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/Mocks/NoOpProgress.cs @@ -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 +{ + public void Report(double value) + { + } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/AIServiceBatchIntegrationTests.cs b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/AIServiceBatchIntegrationTests.cs index 224ec9f99fa1..20aaf77709b7 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/AIServiceBatchIntegrationTests.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/AIServiceBatchIntegrationTests.cs @@ -8,6 +8,7 @@ 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 @@ private static async Task GetOutputDataPackageAsync(BatchTestInput { 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}"); diff --git a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/KernelServiceIntegrationTests.cs b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/KernelServiceIntegrationTests.cs index 14eb5100a8b7..998534cf5e89 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/KernelServiceIntegrationTests.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/KernelServiceIntegrationTests.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using AdvancedPaste.Helpers; @@ -130,7 +131,7 @@ private static async Task CreatePackageAsync(ClipboardFormat format private async Task 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); diff --git a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/TranscodeHelperIntegrationTests.cs b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/TranscodeHelperIntegrationTests.cs new file mode 100644 index 000000000000..514cefeb830a --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/TranscodeHelperIntegrationTests.cs @@ -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"; + + /// 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. + /// + [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 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); + + /// + /// Gets the dimensions of a video, if available. Accounts for the fact that the dimensions may sometimes be swapped. + /// + 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)); +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/AnimatedContentControl/AnimatedContentControl.xaml b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/AnimatedContentControl/AnimatedContentControl.xaml index 00d672c6f0a1..fd42be2b3ba8 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/AnimatedContentControl/AnimatedContentControl.xaml +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/AnimatedContentControl/AnimatedContentControl.xaml @@ -28,6 +28,7 @@ Background="Transparent" BorderThickness="4" CornerRadius="{TemplateBinding CornerRadius}" + IsHitTestVisible="False" Visibility="Collapsed"> diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml index 69f675d4cb06..dd09c717b064 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml @@ -178,17 +178,36 @@ Padding="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> - - + + + + + + + + + + + diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml.cs index 3383af5292aa..19c0fd8ce67e 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml.cs @@ -55,9 +55,9 @@ public PromptBox() 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 @@ private void Grid_Loaded(object sender, RoutedEventArgs e) [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) diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/MainWindow.xaml.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/MainWindow.xaml.cs index 7743e6764bb2..7d90cf62dce0 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/MainWindow.xaml.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/MainWindow.xaml.cs @@ -24,6 +24,7 @@ public sealed partial class MainWindow : WindowEx, IDisposable { private readonly WindowMessageMonitor _msgMonitor; private readonly IUserSettings _userSettings; + private readonly OptionsViewModel _optionsViewModel; private bool _disposedValue; @@ -32,8 +33,7 @@ public MainWindow() InitializeComponent(); _userSettings = App.GetService(); - - var optionsViewModel = App.GetService(); + _optionsViewModel = App.GetService(); var baseHeight = MinHeight; var coreActionCount = PasteFormat.MetadataDict.Values.Count(metadata => metadata.IsCoreAction); @@ -43,7 +43,7 @@ void UpdateHeight() 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 @@ double GetHeight(int maxCustomActionCount) => 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 @@ public void Dispose() 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; } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/DataPackageHelpers.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/DataPackageHelpers.cs index 4aacd0c1153c..529773f9a6d5 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/DataPackageHelpers.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/DataPackageHelpers.cs @@ -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> ImageFileTypes = new(GetImageFileTypes()); - private static readonly (string DataFormat, ClipboardFormat ClipboardFormat)[] DataFormats = [ (StandardDataFormats.Text, ClipboardFormat.Text), @@ -27,6 +29,14 @@ private static readonly (string DataFormat, ClipboardFormat ClipboardFormat)[] D (StandardDataFormats.Bitmap, ClipboardFormat.Image), ]; + private static readonly Lazy<(ClipboardFormat Format, HashSet 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 async Task GetAvailableFormatsAsync(this DataPa { 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 async Task HasUsableDataAsync(this DataPackageView dataPac 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 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 GetTextOrEmptyAsync(this DataPackageView dataPackageView) => dataPackageView.Contains(StandardDataFormats.Text) ? await dataPackageView.GetTextAsync() : string.Empty; @@ -153,4 +220,27 @@ private static HashSet GetImageFileTypes() => BitmapDecoder.GetDecoderInformationEnumerator() .SelectMany(di => di.FileExtensions) .ToHashSet(StringComparer.InvariantCultureIgnoreCase); + + private static HashSet 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); + } } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/KernelExtensions.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/KernelExtensions.cs index 2708e2ca6946..886d24d721d2 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/KernelExtensions.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/KernelExtensions.cs @@ -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 async Task GetDataFormatsAsync(this Kernel kernel) 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 GetProgress(this Kernel kernel) => kernel.Data.TryGetValue(ProgressKey, out object obj) ? obj as IProgress : null; + + internal static void SetProgress(this Kernel kernel, IProgress 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; diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/NativeMethods.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/NativeMethods.cs index a28626ca1f21..6e53e9b61890 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/NativeMethods.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/NativeMethods.cs @@ -4,6 +4,7 @@ using System; using System.Runtime.InteropServices; +using System.Text; namespace AdvancedPaste.Helpers { @@ -83,6 +84,68 @@ internal enum KeyEventF 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 @@ internal struct PointInter [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); } } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/OcrHelpers.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/OcrHelpers.cs index 1ed0665f9d9b..b56868ece8cb 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/OcrHelpers.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/OcrHelpers.cs @@ -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 ExtractTextAsync(SoftwareBitmap bitmap) + public static async Task 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) diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/TranscodeHelpers.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/TranscodeHelpers.cs new file mode 100644 index 000000000000..8d0f735bade4 --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/TranscodeHelpers.cs @@ -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 TranscodeToMp3Async(DataPackageView clipboardData, CancellationToken cancellationToken, IProgress progress) => + await TranscodeMediaAsync(clipboardData, MediaEncodingProfile.CreateMp3(AudioEncodingQuality.High), ".mp3", cancellationToken, progress); + + public static async Task TranscodeToMp4Async(DataPackageView clipboardData, CancellationToken cancellationToken, IProgress progress) => + await TranscodeMediaAsync(clipboardData, MediaEncodingProfile.CreateMp4(VideoEncodingQuality.HD1080p), ".mp4", cancellationToken, progress); + + private static async Task TranscodeMediaAsync(DataPackageView clipboardData, MediaEncodingProfile baseOutputProfile, string extension, CancellationToken cancellationToken, IProgress 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 progress) + { + if (outputProfile.Video == null && outputProfile.Audio == null) + { + throw new InvalidOperationException("Target profile does not contain media"); + } + + async Task 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); + } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/TransformHelpers.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/TransformHelpers.cs index 2c8f442cd7d0..fe66ba6cbbfb 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/TransformHelpers.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/TransformHelpers.cs @@ -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 TransformAsync(PasteFormats format, DataPackageView clipboardData) + public static async Task TransformAsync(PasteFormats format, DataPackageView clipboardData, CancellationToken cancellationToken, IProgress 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 @@ private static async Task ToJsonAsync(DataPackageView clipboardData return CreateDataPackageFromText(await JsonHelper.ToJsonFromXmlOrCsvAsync(clipboardData)); } - private static async Task ImageToTextAsync(DataPackageView clipboardData) + private static async Task 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 ToPngFileAsync(DataPackageView clipboardData) + private static async Task ToPngFileAsync(DataPackageView clipboardData, CancellationToken cancellationToken) { Logger.LogTrace(); @@ -72,25 +75,25 @@ private static async Task ToPngFileAsync(DataPackageView clipboardD encoder.SetSoftwareBitmap(clipboardBitmap); await encoder.FlushAsync(); - return await CreateDataPackageFromFileContentAsync(pngStream.AsStreamForRead(), "png"); + return await CreateDataPackageFromFileContentAsync(pngStream.AsStreamForRead(), "png", cancellationToken); } - private static async Task ToTxtFileAsync(DataPackageView clipboardData) + private static async Task 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 ToHtmlFileAsync(DataPackageView clipboardData) + private static async Task 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); } /// @@ -114,7 +117,7 @@ private static string RemoveHtmlMetadata(string cfHtml) return (startFragmentIndex == null || endFragmentIndex == null) ? cfHtml : cfHtml[startFragmentIndex.Value..endFragmentIndex.Value]; } - private static async Task CreateDataPackageFromFileContentAsync(string data, string fileExtension) + private static async Task CreateDataPackageFromFileContentAsync(string data, string fileExtension, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(data)) { @@ -123,16 +126,16 @@ private static async Task CreateDataPackageFromFileContentAsync(str var path = GetPasteAsFileTempFilePath(fileExtension); - await File.WriteAllTextAsync(path, data); + await File.WriteAllTextAsync(path, data, cancellationToken); return await DataPackageHelpers.CreateFromFileAsync(path); } - private static async Task CreateDataPackageFromFileContentAsync(Stream stream, string fileExtension) + private static async Task 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); } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs index 70a4cf0f9e4d..8a25b70f07ea 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs @@ -108,7 +108,9 @@ void UpdateSettings() (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(); diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Models/ClipboardFormat.cs b/src/modules/AdvancedPaste/AdvancedPaste/Models/ClipboardFormat.cs index 63c935b63ed6..af445e85b045 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Models/ClipboardFormat.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Models/ClipboardFormat.cs @@ -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 } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteActionError.cs b/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteActionError.cs index 6ec9a49028f0..701ffe54bb18 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteActionError.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteActionError.cs @@ -30,7 +30,7 @@ public static PasteActionError FromResourceId(string resourceId) => 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, }; } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormats.cs b/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormats.cs index 588f10250629..99243ebb5efa 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormats.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormats.cs @@ -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, diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/ICustomTextTransformService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/ICustomTextTransformService.cs index 800f7b04161a..75f1df259e8c 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Services/ICustomTextTransformService.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/ICustomTextTransformService.cs @@ -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 TransformTextAsync(string prompt, string inputText); + Task TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress progress); } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/IKernelService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/IKernelService.cs index ae99fccf443a..beb62fb293d2 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Services/IKernelService.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/IKernelService.cs @@ -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 TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery); + Task TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery, CancellationToken cancellationToken, IProgress progress); } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/IPasteFormatExecutor.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/IPasteFormatExecutor.cs index 9df354e3d185..3b3237faffee 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Services/IPasteFormatExecutor.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/IPasteFormatExecutor.cs @@ -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 ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source); + Task ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source, CancellationToken cancellationToken, IProgress progress); } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/IPromptModerationService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/IPromptModerationService.cs index bd7963ac7829..f80b8d30ab7d 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Services/IPromptModerationService.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/IPromptModerationService.cs @@ -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); } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/KernelServiceBase.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/KernelServiceBase.cs index c988d2f8ced8..e921b21e54a0 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Services/KernelServiceBase.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/KernelServiceBase.cs @@ -6,6 +6,7 @@ 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 TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery) + public async Task TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery, CancellationToken cancellationToken, IProgress 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 async Task TransformClipboardAsync(string prompt, DataPackag 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 async Task TransformClipboardAsync(string prompt, DataPackag 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 @@ private static string GetFullPrompt(ChatHistory initialHistory) 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 @@ The user will put in a request to format their clipboard data and you will fulfi 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() - .GetChatMessageContentAsync(chatHistory, PromptExecutionSettings, kernel); + .GetChatMessageContentAsync(chatHistory, PromptExecutionSettings, kernel, cancellationToken); chatHistory.Add(chatResult); var totalUsage = chatHistory.Select(GetAIServiceUsage) @@ -157,6 +160,8 @@ The user will put in a request to format their clipboard data and you will fulfi { 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 @@ private Task ExecutePromptTransformAsync(Kernel kernel, PasteFormats for 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 GetPromptBasedOutput(PasteFormats format, string prompt, string input) => + private async Task GetPromptBasedOutput(PasteFormats format, string prompt, string input, CancellationToken cancellationToken, IProgress 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 @@ private Task ExecuteStandardTransformAsync(Kernel kernel, PasteFormats f 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 ExecuteTransformAsync(Kernel kernel, ActionChainItem actionChainItem, Func> transformFunc) { diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/CustomTextTransformService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/CustomTextTransformService.cs index 95823d8d246f..b6aa156b9df1 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/CustomTextTransformService.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/CustomTextTransformService.cs @@ -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 GetAICompletionAsync(string systemInstructions, string userMessage) + private async Task 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 @@ private async Task GetAICompletionAsync(string systemInstructions, }, Temperature = 0.01F, MaxTokens = 2000, - }); + }, + cancellationToken); if (response.Value.Choices[0].FinishReason == "length") { @@ -51,7 +53,7 @@ private async Task GetAICompletionAsync(string systemInstructions, return response; } - public async Task TransformTextAsync(string prompt, string inputText) + public async Task TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress progress) { if (string.IsNullOrWhiteSpace(prompt)) { @@ -80,7 +82,7 @@ public async Task TransformTextAsync(string prompt, string inputText) 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 @@ public async Task TransformTextAsync(string prompt, string inputText) 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; } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/PromptModerationService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/PromptModerationService.cs index e78a44b533a1..0ca15e4161de 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/PromptModerationService.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/PromptModerationService.cs @@ -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}"); diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs index e7e7f9b4cf89..5d6740977bb5 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs @@ -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 ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source) + public async Task ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source, CancellationToken cancellationToken, IProgress progress) { if (!pasteFormat.IsEnabled) { @@ -34,9 +35,9 @@ public async Task ExecutePasteFormatAsync(PasteFormat pasteFormat, 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), }); } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw b/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw index 1c2839d06424..30b46190e329 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw +++ b/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw @@ -135,6 +135,9 @@ OpenAI request failed with status code: + + The paste operation was canceled + An error occurred during the paste operation @@ -188,7 +191,19 @@ Paste as .html file + + + Transcode to .mp3 + + Transcode to .mp4 (H.264/AAC) + + + An error occurred while transcoding media file + + + The media file contains an unsupported codec + Paste @@ -207,6 +222,9 @@ Generate and paste data + + Cancel paste operation + Regenerate @@ -216,6 +234,9 @@ Generate and paste data + + Cancel paste operation + Open settings diff --git a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs index 30e4a6f359eb..688c3047e236 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs @@ -8,6 +8,8 @@ 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 @@ namespace AdvancedPaste.ViewModels { - public sealed partial class OptionsViewModel : ObservableObject, IDisposable + public sealed partial class OptionsViewModel : ObservableObject, IProgress, IDisposable { private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); private readonly DispatcherTimer _clipboardTimer; @@ -37,6 +39,8 @@ public sealed partial class OptionsViewModel : ObservableObject, IDisposable private readonly IPasteFormatExecutor _pasteFormatExecutor; private readonly IAICredentialsProvider _aiCredentialsProvider; + private CancellationTokenSource _pasteActionCancellationTokenSource; + public DataPackageView ClipboardData { get; set; } [ObservableProperty] @@ -65,7 +69,11 @@ public sealed partial class OptionsViewModel : ObservableObject, IDisposable private bool _pasteFormatsDirty; [ObservableProperty] - private bool _busy; + private bool _isBusy; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasIndeterminateTransformProgress))] + private double _transformProgress = double.NaN; public ObservableCollection StandardPasteFormats { get; } = []; @@ -81,9 +89,24 @@ public sealed partial class OptionsViewModel : ObservableObject, IDisposable 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 @@ orderby pasteFormat.IsEnabled descending void UpdateFormats(ObservableCollection collection, IEnumerable 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 @@ void UpdateFormats(ObservableCollection collection, IEnumerable package.GetView().TryCleanupAfterDelayAsync(TimeSpan.FromSeconds(30))); } // Command to select the previous custom format @@ -362,7 +395,7 @@ internal async Task ExecutePasteFormatAsync(PasteFormats format, PasteActionSour 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 @@ internal async Task ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteAction 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 @@ internal async Task ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteAction 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 @@ private bool UpdateOpenAIKey() return IsAllowedByGPO && _aiCredentialsProvider.Refresh(); } + + public async Task CancelPasteActionAsync() + { + if (_pasteActionCancellationTokenSource != null) + { + await _pasteActionCancellationTokenSource.CancelAsync(); + } + } + + void IProgress.Report(double value) + { + ReportProgress(value); + } + + private void ReportProgress(double value) + { + _dispatcherQueue.TryEnqueue(() => + { + TransformProgress = value; + }); + } } } diff --git a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/trace.cpp b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/trace.cpp index c5af4f231e89..c6b1bfa0a9cd 100644 --- a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/trace.cpp +++ b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/trace.cpp @@ -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") ); } diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalAction.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalAction.cs index 7a6fd3081aa1..28bed9201292 100644 --- a/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalAction.cs +++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalAction.cs @@ -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 bool IsShown get => _isShown; set => Set(ref _isShown, value); } + + [JsonIgnore] + public IEnumerable SubActions => []; } diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalActions.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalActions.cs index ce26962b020e..3b1a859364a3 100644 --- a/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalActions.cs +++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalActions.cs @@ -14,6 +14,7 @@ public static class PropertyNames { 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 static class PropertyNames [JsonPropertyName(PropertyNames.PasteAsFile)] public AdvancedPastePasteAsFileAction PasteAsFile { get; init; } = new(); - [JsonIgnore] - public IEnumerable AllActions => new IAdvancedPasteAction[] { ImageToText, PasteAsFile }.Concat(PasteAsFile.SubActions); + [JsonPropertyName(PropertyNames.Transcode)] + public AdvancedPasteTranscodeAction Transcode { get; init; } = new(); + + public IEnumerable GetAllActions() + { + Queue 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); + } + } + } } diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs index f3bb4431ca29..971d24c93b69 100644 --- a/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs +++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs @@ -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 bool IsValid private set => Set(ref _isValid, value); } + [JsonIgnore] + public IEnumerable SubActions => []; + public object Clone() { AdvancedPasteCustomAction clone = new(); diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPastePasteAsFileAction.cs b/src/settings-ui/Settings.UI.Library/AdvancedPastePasteAsFileAction.cs index 979e967d4a0a..c4489eaaf762 100644 --- a/src/settings-ui/Settings.UI.Library/AdvancedPastePasteAsFileAction.cs +++ b/src/settings-ui/Settings.UI.Library/AdvancedPastePasteAsFileAction.cs @@ -52,5 +52,5 @@ public AdvancedPasteAdditionalAction PasteAsHtmlFile } [JsonIgnore] - public IEnumerable SubActions => [PasteAsTxtFile, PasteAsPngFile, PasteAsHtmlFile]; + public IEnumerable SubActions => [PasteAsTxtFile, PasteAsPngFile, PasteAsHtmlFile]; } diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteTranscodeAction.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteTranscodeAction.cs new file mode 100644 index 000000000000..82ea4d09f566 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteTranscodeAction.cs @@ -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 SubActions => [TranscodeToMp3, TranscodeToMp4]; +} diff --git a/src/settings-ui/Settings.UI.Library/IAdvancedPasteAction.cs b/src/settings-ui/Settings.UI.Library/IAdvancedPasteAction.cs index 4c31557010c9..6571853be54a 100644 --- a/src/settings-ui/Settings.UI.Library/IAdvancedPasteAction.cs +++ b/src/settings-ui/Settings.UI.Library/IAdvancedPasteAction.cs @@ -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 SubActions { get; } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml index f6308feba6bc..7aa817662141 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml @@ -276,6 +276,37 @@ + + + + + + + + + + + + + + + + + + Paste as .html file + + Transcode audio / video + + + Transcode to .mp3 + + + Transcode to .mp4 (H.264/AAC) + OpenAI API key: diff --git a/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs index a84006a30544..ae75fe5bf6dc 100644 --- a/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs @@ -84,7 +84,7 @@ public AdvancedPasteViewModel( _delayedTimer.Elapsed += DelayedTimer_Tick; _delayedTimer.AutoReset = false; - foreach (var action in _additionalActions.AllActions) + foreach (var action in _additionalActions.GetAllActions()) { action.PropertyChanged += OnAdditionalActionPropertyChanged; } @@ -366,7 +366,7 @@ public bool CloseAfterLosingFocus .Any(hotkey => WarnHotkeys.Contains(hotkey.ToString())); public bool IsAdditionalActionConflictingCopyShortcut => - _additionalActions.AllActions + _additionalActions.GetAllActions() .OfType() .Select(additionalAction => additionalAction.Shortcut) .Any(hotkey => WarnHotkeys.Contains(hotkey.ToString())); From 491d51afafae8e64e17c25a4b56faeb555e8055c Mon Sep 17 00:00:00 2001 From: Smeagol Date: Tue, 25 Feb 2025 17:35:22 -0500 Subject: [PATCH 06/10] =?UTF-8?q?[QuickAccent]Add=20final=20sigma=20(=20?= =?UTF-8?q?=CF=82=20)=20to=20the=20Greek=20character=20set=20(#37611)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/poweraccent/PowerAccent.Core/Languages.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/poweraccent/PowerAccent.Core/Languages.cs b/src/modules/poweraccent/PowerAccent.Core/Languages.cs index 60264e8e859a..542af1b5996d 100644 --- a/src/modules/poweraccent/PowerAccent.Core/Languages.cs +++ b/src/modules/poweraccent/PowerAccent.Core/Languages.cs @@ -677,7 +677,7 @@ private static string[] GetDefaultLetterKeyEL(LetterKey letter) 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[] { "ξ" }, From 959a54bcd9021095c12c453a6ce4013277dd6610 Mon Sep 17 00:00:00 2001 From: Jerry Xu Date: Tue, 25 Feb 2025 15:55:15 -0800 Subject: [PATCH 07/10] Revert "[Hosts] Add UITest Cases for Hosts Module" (#37619) * Revert "[Hosts] Add UITest Cases for Hosts Module (#37600)" This reverts commit c656dcc9c590dad0bffb5527825d30edbef14aa8. * Matching all UITest projects and UITestAutomation project * Add back Hosts UITests --------- Co-authored-by: Jerry Xu --- .pipelines/verifyDepsJsonLibraryVersions.ps1 | 2 +- PowerToys.sln | 17 ++- .../Hosts/Hosts.UITests/HostModuleTests.cs | 108 ++++++++++++++++++ .../Hosts/Hosts.UITests/Hosts.UITests.csproj | 25 ++++ 4 files changed, 145 insertions(+), 7 deletions(-) create mode 100644 src/modules/Hosts/Hosts.UITests/HostModuleTests.cs create mode 100644 src/modules/Hosts/Hosts.UITests/Hosts.UITests.csproj diff --git a/.pipelines/verifyDepsJsonLibraryVersions.ps1 b/.pipelines/verifyDepsJsonLibraryVersions.ps1 index b91a8a566caa..e4f1733ac622 100644 --- a/.pipelines/verifyDepsJsonLibraryVersions.ps1 +++ b/.pipelines/verifyDepsJsonLibraryVersions.ps1 @@ -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 diff --git a/PowerToys.sln b/PowerToys.sln index 7a5f2831d876..c5d1f9176f69 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -644,6 +644,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "KeyboardManagerEditorLibrar EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hosts.FuzzTests", "src\modules\Hosts\Hosts.FuzzTests\Hosts.FuzzTests.csproj", "{EBED240C-8702-452D-B764-6DB9DA9179AF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hosts.UITests", "src\modules\Hosts\Hosts.UITests\Hosts.UITests.csproj", "{4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -2252,20 +2254,14 @@ Global {CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Release|ARM64.Build.0 = Release|ARM64 {CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Release|x64.ActiveCfg = Release|x64 {CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Release|x64.Build.0 = Release|x64 - {CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Release|x86.ActiveCfg = Release|x64 - {CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Release|x86.Build.0 = Release|x64 {A558C25D-2007-498E-8B6F-43405AFAE9E2}.Debug|ARM64.ActiveCfg = Debug|ARM64 {A558C25D-2007-498E-8B6F-43405AFAE9E2}.Debug|ARM64.Build.0 = Debug|ARM64 {A558C25D-2007-498E-8B6F-43405AFAE9E2}.Debug|x64.ActiveCfg = Debug|x64 {A558C25D-2007-498E-8B6F-43405AFAE9E2}.Debug|x64.Build.0 = Debug|x64 - {A558C25D-2007-498E-8B6F-43405AFAE9E2}.Debug|x86.ActiveCfg = Debug|x64 - {A558C25D-2007-498E-8B6F-43405AFAE9E2}.Debug|x86.Build.0 = Debug|x64 {A558C25D-2007-498E-8B6F-43405AFAE9E2}.Release|ARM64.ActiveCfg = Release|ARM64 {A558C25D-2007-498E-8B6F-43405AFAE9E2}.Release|ARM64.Build.0 = Release|ARM64 {A558C25D-2007-498E-8B6F-43405AFAE9E2}.Release|x64.ActiveCfg = Release|x64 {A558C25D-2007-498E-8B6F-43405AFAE9E2}.Release|x64.Build.0 = Release|x64 - {A558C25D-2007-498E-8B6F-43405AFAE9E2}.Release|x86.ActiveCfg = Release|x64 - {A558C25D-2007-498E-8B6F-43405AFAE9E2}.Release|x86.Build.0 = Release|x64 {08F9155D-B6DC-46E5-9C83-AF60B655898B}.Debug|ARM64.ActiveCfg = Debug|ARM64 {08F9155D-B6DC-46E5-9C83-AF60B655898B}.Debug|ARM64.Build.0 = Debug|ARM64 {08F9155D-B6DC-46E5-9C83-AF60B655898B}.Debug|x64.ActiveCfg = Debug|x64 @@ -2290,6 +2286,14 @@ Global {EBED240C-8702-452D-B764-6DB9DA9179AF}.Release|ARM64.Build.0 = Release|ARM64 {EBED240C-8702-452D-B764-6DB9DA9179AF}.Release|x64.ActiveCfg = Release|x64 {EBED240C-8702-452D-B764-6DB9DA9179AF}.Release|x64.Build.0 = Release|x64 + {4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}.Debug|ARM64.Build.0 = Debug|ARM64 + {4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}.Debug|x64.ActiveCfg = Debug|x64 + {4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}.Debug|x64.Build.0 = Debug|x64 + {4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}.Release|ARM64.ActiveCfg = Release|ARM64 + {4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}.Release|ARM64.Build.0 = Release|ARM64 + {4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}.Release|x64.ActiveCfg = Release|x64 + {4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2529,6 +2533,7 @@ Global {08F9155D-B6DC-46E5-9C83-AF60B655898B} = {38BDB927-829B-4C65-9CD9-93FB05D66D65} {4382A954-179A-4078-92AF-715187DFFF50} = {38BDB927-829B-4C65-9CD9-93FB05D66D65} {EBED240C-8702-452D-B764-6DB9DA9179AF} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA} + {4E0AE3A4-2EE0-44D7-A2D0-8769977254A0} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} diff --git a/src/modules/Hosts/Hosts.UITests/HostModuleTests.cs b/src/modules/Hosts/Hosts.UITests/HostModuleTests.cs new file mode 100644 index 000000000000..35d49a5b64cd --- /dev/null +++ b/src/modules/Hosts/Hosts.UITests/HostModuleTests.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading.Tasks; +using Microsoft.PowerToys.UITest; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Hosts.UITests +{ + [TestClass] + public class HostModuleTests : UITestBase + { + public HostModuleTests() + : base(PowerToysModule.Hosts) + { + } + + /// + /// Test if Empty-view is shown when no entries are present. + /// And 'Add an entry' button from Empty-view is functional. + /// + [TestMethod] + public void TestEmptyView() + { + this.CloseWarningDialog(); + this.RemoveAllEntries(); + + // 'Add an entry' button (only show-up when list is empty) should be visible + Assert.IsTrue(this.FindAll