From e79d86df9b27d87a0a2136fa2184b0ac02356e43 Mon Sep 17 00:00:00 2001 From: Ani <115020168+drawbyperpetual@users.noreply.github.com> Date: Wed, 11 Sep 2024 21:08:22 +0200 Subject: [PATCH 01/51] [AdvancedPaste] Additional actions, including Image to text --- src/common/interop/Constants.cpp | 4 + src/common/interop/Constants.h | 1 + src/common/interop/Constants.idl | 1 + src/common/interop/shared_constants.h | 2 + .../AdvancedPasteXAML/App.xaml.cs | 35 ++++ .../AdvancedPasteXAML/Controls/PromptBox.xaml | 2 +- .../AdvancedPasteXAML/MainWindow.xaml.cs | 10 +- .../AdvancedPasteXAML/Pages/MainPage.xaml | 103 +++++++----- .../AdvancedPasteXAML/Pages/MainPage.xaml.cs | 4 +- .../AdvancedPaste/Helpers/ClipboardHelper.cs | 37 ++++- .../AdvancedPaste/Helpers/IUserSettings.cs | 10 +- .../AdvancedPaste/Helpers/OcrHelpers.cs | 39 +++++ .../AdvancedPaste/Helpers/UserSettings.cs | 52 ++++-- .../AdvancedPaste/Models/ClipboardFormat.cs | 18 +++ .../AdvancedPaste/Models/ClipboardItem.cs | 13 +- .../AdvancedPaste/Models/PasteFormat.cs | 52 ++++-- .../Models/PasteFormatMetadataAttribute.cs | 23 +++ .../AdvancedPaste/Models/PasteFormats.cs | 39 ++++- .../Strings/en-us/Resources.resw | 22 ++- .../ViewModels/OptionsViewModel.cs | 152 +++++++++++++----- .../AdvancedPasteModuleInterface/dllmain.cpp | 99 ++++++++++-- .../AdvancedPasteAdditionalAction.cs | 39 +++++ .../AdvancedPasteAdditionalActions.cs | 31 ++++ .../AdvancedPasteCustomAction.cs | 69 ++------ .../AdvancedPastePasteAsFileAction.cs | 56 +++++++ .../AdvancedPasteProperties.cs | 7 +- .../Settings.UI.Library/Helpers/Observable.cs | 8 +- .../IAdvancedPasteAction.cs | 12 ++ .../SettingsXAML/Views/AdvancedPaste.xaml | 49 ++++++ .../Settings.UI/Strings/en-us/Resources.resw | 18 +++ .../ViewModels/AdvancedPasteViewModel.cs | 9 ++ 31 files changed, 799 insertions(+), 217 deletions(-) create mode 100644 src/modules/AdvancedPaste/AdvancedPaste/Helpers/OcrHelpers.cs create mode 100644 src/modules/AdvancedPaste/AdvancedPaste/Models/ClipboardFormat.cs create mode 100644 src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormatMetadataAttribute.cs create mode 100644 src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalAction.cs create mode 100644 src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalActions.cs create mode 100644 src/settings-ui/Settings.UI.Library/AdvancedPastePasteAsFileAction.cs create mode 100644 src/settings-ui/Settings.UI.Library/IAdvancedPasteAction.cs diff --git a/src/common/interop/Constants.cpp b/src/common/interop/Constants.cpp index 63e978234610..6388d07f34bf 100644 --- a/src/common/interop/Constants.cpp +++ b/src/common/interop/Constants.cpp @@ -63,6 +63,10 @@ namespace winrt::PowerToys::Interop::implementation { return CommonSharedConstants::ADVANCED_PASTE_JSON_MESSAGE; } + hstring Constants::AdvancedPasteAdditionalActionMessage() + { + return CommonSharedConstants::ADVANCED_PASTE_ADDITIONAL_ACTION_MESSAGE; + } hstring Constants::AdvancedPasteCustomActionMessage() { return CommonSharedConstants::ADVANCED_PASTE_CUSTOM_ACTION_MESSAGE; diff --git a/src/common/interop/Constants.h b/src/common/interop/Constants.h index 978ca8ab6062..c0fac4fb68a7 100644 --- a/src/common/interop/Constants.h +++ b/src/common/interop/Constants.h @@ -19,6 +19,7 @@ namespace winrt::PowerToys::Interop::implementation static hstring AdvancedPasteShowUIMessage(); static hstring AdvancedPasteMarkdownMessage(); static hstring AdvancedPasteJsonMessage(); + static hstring AdvancedPasteAdditionalActionMessage(); static hstring AdvancedPasteCustomActionMessage(); static hstring ShowPowerOCRSharedEvent(); static hstring MouseJumpShowPreviewEvent(); diff --git a/src/common/interop/Constants.idl b/src/common/interop/Constants.idl index 4c4125b7dbc5..8168d7680cd0 100644 --- a/src/common/interop/Constants.idl +++ b/src/common/interop/Constants.idl @@ -16,6 +16,7 @@ namespace PowerToys static String AdvancedPasteShowUIMessage(); static String AdvancedPasteMarkdownMessage(); static String AdvancedPasteJsonMessage(); + static String AdvancedPasteAdditionalActionMessage(); static String AdvancedPasteCustomActionMessage(); static String ShowPowerOCRSharedEvent(); static String MouseJumpShowPreviewEvent(); diff --git a/src/common/interop/shared_constants.h b/src/common/interop/shared_constants.h index 5237c1573775..25e78524b5a0 100644 --- a/src/common/interop/shared_constants.h +++ b/src/common/interop/shared_constants.h @@ -32,6 +32,8 @@ namespace CommonSharedConstants const wchar_t ADVANCED_PASTE_JSON_MESSAGE[] = L"PasteJson"; + const wchar_t ADVANCED_PASTE_ADDITIONAL_ACTION_MESSAGE[] = L"AdditionalAction"; + const wchar_t ADVANCED_PASTE_CUSTOM_ACTION_MESSAGE[] = L"CustomAction"; // Path to the event used to show Color Picker diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs index e7cefe644fc5..a937be3e3126 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs @@ -3,11 +3,14 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using AdvancedPaste.Helpers; +using AdvancedPaste.Models; using AdvancedPaste.Settings; using AdvancedPaste.ViewModels; using ManagedCommon; @@ -31,6 +34,13 @@ public partial class App : Application, IDisposable { public IHost Host { get; private set; } + private static readonly Dictionary AdditionalActionIPCKeys = + typeof(PasteFormats).GetFields() + .Where(field => field.IsLiteral) + .Select(field => (Format: (PasteFormats)field.GetRawConstantValue(), field.GetCustomAttribute().IPCKey)) + .Where(field => field.IPCKey != null) + .ToDictionary(field => field.IPCKey, field => field.Format); + private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); private readonly OptionsViewModel viewModel; @@ -128,6 +138,10 @@ private void OnNamedPipeMessage(string message) { OnAdvancedPasteJsonHotkey(); } + else if (messageType == PowerToys.Interop.Constants.AdvancedPasteAdditionalActionMessage()) + { + OnAdvancedPasteAdditionalActionHotkey(messageParts); + } else if (messageType == PowerToys.Interop.Constants.AdvancedPasteCustomActionMessage()) { OnAdvancedPasteCustomActionHotkey(messageParts); @@ -156,6 +170,27 @@ private void OnAdvancedPasteHotkey() ShowWindow(); } + private void OnAdvancedPasteAdditionalActionHotkey(string[] messageParts) + { + if (messageParts.Length != 2) + { + Logger.LogWarning("Unexpected additional action message"); + } + else + { + if (!AdditionalActionIPCKeys.TryGetValue(messageParts[1], out PasteFormats pasteFormat)) + { + Logger.LogWarning($"Unexpected additional action type {messageParts[1]}"); + } + else + { + ShowWindow(); + viewModel.ReadClipboard(); + viewModel.ExecuteAdditionalAction(pasteFormat); + } + } + } + private void OnAdvancedPasteCustomActionHotkey(string[] messageParts) { if (messageParts.Length != 2) diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml index 27891312ac5b..6da59f66c414 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml @@ -346,7 +346,7 @@ x:Name="InputTxtBox" HorizontalAlignment="Stretch" x:FieldModifier="public" - IsEnabled="{x:Bind ViewModel.IsClipboardDataText, Mode=OneWay}" + IsEnabled="{x:Bind ViewModel.ClipboardHasData, Mode=OneWay}" KeyDown="InputTxtBox_KeyDown" PlaceholderText="{x:Bind ViewModel.InputTxtBoxPlaceholderText, Mode=OneWay}" Style="{StaticResource CustomTextBoxStyle}" diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/MainWindow.xaml.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/MainWindow.xaml.cs index 252aa9fa48d1..949f3b727f7d 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/MainWindow.xaml.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/MainWindow.xaml.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Linq; using AdvancedPaste.Helpers; using AdvancedPaste.Settings; using ManagedCommon; @@ -30,13 +31,16 @@ public MainWindow() void UpdateHeight() { - var trimmedCustomActionCount = Math.Min(_userSettings.CustomActions.Count, 5); - Height = MinHeight = baseHeight + (trimmedCustomActionCount * 40); + double GetHeight(int maxCustomActionCount) => + baseHeight + (40 * (_userSettings.AdditionalActions.Count + Math.Min(_userSettings.CustomActions.Count, maxCustomActionCount))); + + MinHeight = GetHeight(1); + Height = GetHeight(5); } UpdateHeight(); - _userSettings.CustomActions.CollectionChanged += (_, _) => UpdateHeight(); + _userSettings.Changed += (_, _) => UpdateHeight(); AppWindow.SetIcon("Assets/AdvancedPaste/AdvancedPaste.ico"); this.ExtendsContentIntoTitleBar = true; diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml index f45a2228664a..b975333b71d0 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml @@ -29,36 +29,49 @@ - - - - - - - - - - - - - + @@ -176,14 +189,21 @@ x:Name="PasteOptionsListView" Grid.Row="0" VerticalAlignment="Bottom" - IsEnabled="{x:Bind ViewModel.IsClipboardDataText, Mode=OneWay}" - IsItemClickEnabled="True" - ItemClick="ListView_Click" + IsItemClickEnabled="False" ItemContainerTransitions="{x:Null}" ItemTemplate="{StaticResource PasteFormatTemplate}" ItemsSource="{x:Bind ViewModel.StandardPasteFormats, Mode=OneWay}" SelectionMode="None" - TabIndex="1" /> + TabIndex="1"> + + + + + TabIndex="2"> + + + + GetClipboardImageContentAsync(DataPackageView clipboardData) + { + using var stream = await GetClipboardImageStreamAsync(clipboardData); + if (stream != null) + { + var decoder = await BitmapDecoder.CreateAsync(stream); + return await decoder.GetSoftwareBitmapAsync(); + } + + return null; + } + + private static async Task GetClipboardImageStreamAsync(DataPackageView clipboardData) + { + if (clipboardData.Contains(StandardDataFormats.StorageItems)) + { + var storageItems = await clipboardData.GetStorageItemsAsync(); + var file = storageItems[0] as StorageFile; + if (file != null) + { + return await file.OpenReadAsync(); + } + } + + if (clipboardData.Contains(StandardDataFormats.Bitmap)) + { + var imageStreamReference = await clipboardData.GetBitmapAsync(); + return await imageStreamReference.OpenReadAsync(); + } + + return null; + } } } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs index cf38974a7919..3d0ccc3c4f82 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs @@ -2,7 +2,9 @@ // 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.ObjectModel; +using System; +using System.Collections.Generic; +using AdvancedPaste.Models; using Microsoft.PowerToys.Settings.UI.Library; namespace AdvancedPaste.Settings @@ -15,6 +17,10 @@ public interface IUserSettings public bool CloseAfterLosingFocus { get; } - public ObservableCollection CustomActions { get; } + public IReadOnlyList CustomActions { get; } + + public IReadOnlyList AdditionalActions { get; } + + public event EventHandler Changed; } } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/OcrHelpers.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/OcrHelpers.cs new file mode 100644 index 000000000000..9e64fb2fc26c --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/OcrHelpers.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.Linq; +using System.Threading.Tasks; +using Windows.Globalization; +using Windows.Graphics.Imaging; +using Windows.Media.Ocr; +using Windows.System.UserProfile; + +namespace AdvancedPaste.Helpers; + +public static class OcrHelpers +{ + public static async Task GetTextAsync(SoftwareBitmap bitmap) + { + var ocrLanguage = GetOCRLanguage() ?? throw new InvalidOperationException("Unable to determine OCR language"); + + var ocrEngine = OcrEngine.TryCreateFromLanguage(ocrLanguage) ?? throw new InvalidOperationException("Unable to create OCR engine"); + var ocrResult = await ocrEngine.RecognizeAsync(bitmap); + + return ocrResult.Text; + } + + private static Language GetOCRLanguage() + { + var userLanguageTags = GlobalizationPreferences.Languages.ToList(); + + var languages = from language in OcrEngine.AvailableRecognizerLanguages + let tag = language.LanguageTag + where userLanguageTags.Contains(tag) + orderby userLanguageTags.IndexOf(tag) + select language; + + return languages.FirstOrDefault(); + } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs index 0dc208b2049f..cb3e016165e8 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs @@ -3,10 +3,12 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.ObjectModel; +using System.Collections.Generic; using System.IO.Abstractions; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using AdvancedPaste.Models; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Utilities; @@ -19,6 +21,8 @@ internal sealed class UserSettings : IUserSettings, IDisposable private readonly TaskScheduler _taskScheduler; private readonly IFileSystemWatcher _watcher; private readonly object _loadingSettingsLock = new(); + private readonly List _customActions; + private readonly List _additionalActions; private const string AdvancedPasteModuleName = "AdvancedPaste"; private const int MaxNumberOfRetry = 5; @@ -26,13 +30,17 @@ internal sealed class UserSettings : IUserSettings, IDisposable private bool _disposedValue; private CancellationTokenSource _cancellationTokenSource; + public event EventHandler Changed; + public bool ShowCustomPreview { get; private set; } public bool SendPasteKeyCombination { get; private set; } public bool CloseAfterLosingFocus { get; private set; } - public ObservableCollection CustomActions { get; private set; } + public IReadOnlyList AdditionalActions => _additionalActions; + + public IReadOnlyList CustomActions => _customActions; public UserSettings() { @@ -41,8 +49,8 @@ public UserSettings() ShowCustomPreview = true; SendPasteKeyCombination = true; CloseAfterLosingFocus = false; - CustomActions = []; - + _additionalActions = []; + _customActions = []; _taskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); LoadSettingsFromJson(); @@ -87,18 +95,30 @@ private void LoadSettingsFromJson() { void UpdateSettings() { - ShowCustomPreview = settings.Properties.ShowCustomPreview; - SendPasteKeyCombination = settings.Properties.SendPasteKeyCombination; - CloseAfterLosingFocus = settings.Properties.CloseAfterLosingFocus; - - CustomActions.Clear(); - foreach (var customAction in settings.Properties.CustomActions.Value) - { - if (customAction.IsShown && customAction.IsValid) - { - CustomActions.Add(customAction); - } - } + var properties = settings.Properties; + + ShowCustomPreview = properties.ShowCustomPreview; + SendPasteKeyCombination = properties.SendPasteKeyCombination; + CloseAfterLosingFocus = properties.CloseAfterLosingFocus; + + var sourceAdditionalActions = properties.AdditionalActions; + (PasteFormats Format, IAdvancedPasteAction[] Actions)[] additionalActionFormats = + [ + (PasteFormats.AudioToText, [sourceAdditionalActions.AudioToText]), + (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]) + ]; + + _additionalActions.Clear(); + _additionalActions.AddRange(additionalActionFormats.Where(tuple => tuple.Actions.All(action => action.IsShown)) + .Select(tuple => tuple.Format)); + + _customActions.Clear(); + _customActions.AddRange(properties.CustomActions.Value.Where(customAction => customAction.IsShown && customAction.IsValid)); + + Changed?.Invoke(this, EventArgs.Empty); } Task.Factory diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Models/ClipboardFormat.cs b/src/modules/AdvancedPaste/AdvancedPaste/Models/ClipboardFormat.cs new file mode 100644 index 000000000000..837b2b2b1e7e --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/Models/ClipboardFormat.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace AdvancedPaste.Models; + +[Flags] +public enum ClipboardFormat +{ + None, + Text = 1, + Html = 1 << 1, + Audio = 1 << 2, + Image = 1 << 3, + File = 1 << 4, +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Models/ClipboardItem.cs b/src/modules/AdvancedPaste/AdvancedPaste/Models/ClipboardItem.cs index 1ff749753ec2..392794906417 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Models/ClipboardItem.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Models/ClipboardItem.cs @@ -5,14 +5,13 @@ using Microsoft.UI.Xaml.Media.Imaging; using Windows.ApplicationModel.DataTransfer; -namespace AdvancedPaste.Models +namespace AdvancedPaste.Models; + +public class ClipboardItem { - public class ClipboardItem - { - public string Content { get; set; } + public string Content { get; set; } - public ClipboardHistoryItem Item { get; set; } + public ClipboardHistoryItem Item { get; set; } - public BitmapImage Image { get; set; } - } + public BitmapImage Image { get; set; } } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormat.cs b/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormat.cs index 6f5103be3e7b..b691b141c660 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormat.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormat.cs @@ -2,38 +2,58 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using CommunityToolkit.Mvvm.ComponentModel; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; using Microsoft.PowerToys.Settings.UI.Library; namespace AdvancedPaste.Models; -public partial class PasteFormat : ObservableObject +[DebuggerDisplay("{Name} IsEnabled={IsEnabled} ShortcutText={ShortcutText}")] +public sealed class PasteFormat { - [ObservableProperty] - private string _shortcutText = string.Empty; + public static readonly IReadOnlyDictionary MetadataDict = + typeof(PasteFormats).GetFields() + .Where(field => field.IsLiteral) + .ToDictionary(field => (PasteFormats)field.GetRawConstantValue(), field => field.GetCustomAttribute()); - [ObservableProperty] - private string _toolTip = string.Empty; + private PasteFormat(PasteFormats format, ClipboardFormat clipboardFormats, bool isCustomAIEnabled) + { + Format = format; + IsEnabled = ((clipboardFormats & Metadata.SupportedClipboardFormats) != ClipboardFormat.None) && (isCustomAIEnabled || !Metadata.RequiresAIService); + } - public PasteFormat() + public PasteFormat(PasteFormats format, ClipboardFormat clipboardFormats, bool isCustomAIEnabled, Func resourceLoader) + : this(format, clipboardFormats, isCustomAIEnabled) { + Name = Metadata.ResourceId == null ? string.Empty : resourceLoader(Metadata.ResourceId); + Prompt = string.Empty; } - public PasteFormat(AdvancedPasteCustomAction customAction, string shortcutText) + public PasteFormat(AdvancedPasteCustomAction customAction, ClipboardFormat clipboardFormats, bool isCustomAIEnabled) + : this(PasteFormats.Custom, clipboardFormats, isCustomAIEnabled) { - IconGlyph = "\uE945"; Name = customAction.Name; Prompt = customAction.Prompt; - Format = PasteFormats.Custom; - ShortcutText = shortcutText; - ToolTip = customAction.Prompt; } - public string IconGlyph { get; init; } + public PasteFormatMetadataAttribute Metadata => MetadataDict[Format]; + + public string IconGlyph => Metadata.IconGlyph; + + public string Name { get; private init; } + + public PasteFormats Format { get; private init; } + + public string Prompt { get; private init; } + + public bool IsEnabled { get; private init; } - public string Name { get; init; } + public double Opacity => IsEnabled ? 1 : 0.5; - public PasteFormats Format { get; init; } + public string ToolTip => string.IsNullOrEmpty(Prompt) ? $"{Name} ({ShortcutText})" : Prompt; - public string Prompt { get; init; } = string.Empty; + public string ShortcutText { get; set; } = string.Empty; } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormatMetadataAttribute.cs b/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormatMetadataAttribute.cs new file mode 100644 index 000000000000..cb3a8a954e37 --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormatMetadataAttribute.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace AdvancedPaste.Models; + +[AttributeUsage(AttributeTargets.Field)] +public sealed class PasteFormatMetadataAttribute : Attribute +{ + public bool IsCoreAction { get; init; } + + public string ResourceId { get; init; } + + public string IconGlyph { get; init; } + + public bool RequiresAIService { get; init; } + + public ClipboardFormat SupportedClipboardFormats { get; init; } + + public string IPCKey { get; init; } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormats.cs b/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormats.cs index ba48fe6586a7..9723ed94bc16 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormats.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormats.cs @@ -2,13 +2,36 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace AdvancedPaste.Models +using Microsoft.PowerToys.Settings.UI.Library; + +namespace AdvancedPaste.Models; + +public enum PasteFormats { - public enum PasteFormats - { - PlainText, - Markdown, - Json, - Custom, - } + [PasteFormatMetadata(IsCoreAction = true, ResourceId = "PasteAsPlainText", IconGlyph = "\uE8E9", RequiresAIService = false, SupportedClipboardFormats = ClipboardFormat.Text)] + PlainText, + + [PasteFormatMetadata(IsCoreAction = true, ResourceId = "PasteAsMarkdown", IconGlyph = "\ue8a5", RequiresAIService = false, SupportedClipboardFormats = ClipboardFormat.Text)] + Markdown, + + [PasteFormatMetadata(IsCoreAction = true, ResourceId = "PasteAsJson", IconGlyph = "\uE943", RequiresAIService = false, SupportedClipboardFormats = ClipboardFormat.Text)] + Json, + + [PasteFormatMetadata(IsCoreAction = false, ResourceId = "AudioToText", IconGlyph = "\uF8B1", RequiresAIService = true, SupportedClipboardFormats = ClipboardFormat.Audio, IPCKey = AdvancedPasteAdditionalActions.PropertyNames.AudioToText)] + AudioToText, + + [PasteFormatMetadata(IsCoreAction = false, ResourceId = "ImageToText", IconGlyph = "\uE91B", RequiresAIService = false, SupportedClipboardFormats = ClipboardFormat.Image | ClipboardFormat.File, IPCKey = AdvancedPasteAdditionalActions.PropertyNames.ImageToText)] + ImageToText, + + [PasteFormatMetadata(IsCoreAction = false, ResourceId = "PasteAsTxtFile", IconGlyph = "\uE8D2", RequiresAIService = false, SupportedClipboardFormats = ClipboardFormat.Text | ClipboardFormat.Html, IPCKey = AdvancedPastePasteAsFileAction.PropertyNames.PasteAsTxtFile)] + PasteAsTxtFile, + + [PasteFormatMetadata(IsCoreAction = false, ResourceId = "PasteAsPngFile", IconGlyph = "\uE8B9", RequiresAIService = false, SupportedClipboardFormats = ClipboardFormat.Image, IPCKey = AdvancedPastePasteAsFileAction.PropertyNames.PasteAsPngFile)] + PasteAsPngFile, + + [PasteFormatMetadata(IsCoreAction = false, ResourceId = "PasteAsHtmlFile", IconGlyph = "\uF6FA", RequiresAIService = false, SupportedClipboardFormats = ClipboardFormat.Text | ClipboardFormat.Html, IPCKey = AdvancedPastePasteAsFileAction.PropertyNames.PasteAsHtmlFile)] + PasteAsHtmlFile, + + [PasteFormatMetadata(IsCoreAction = false, IconGlyph = "\uE945", RequiresAIService = true, SupportedClipboardFormats = ClipboardFormat.Text)] + Custom, } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw b/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw index dd2720732cb5..428d5050c1bf 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw +++ b/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw @@ -120,8 +120,8 @@ AI can make mistakes. - - Clipboard data is not text + + Clipboard is empty To custom with AI is not enabled @@ -135,6 +135,9 @@ OpenAI request failed with status code: + + An error occurred during the paste operation + Clipboard history @@ -162,6 +165,21 @@ Paste as plain text + + Audio to text + + + Image to text + + + Paste as .txt file + + + Paste as .png file + + + Paste as .html file + Paste diff --git a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs index b0cacd027423..67abea2c745e 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs @@ -3,10 +3,12 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Net; +using System.Threading; using System.Threading.Tasks; using AdvancedPaste.Helpers; using AdvancedPaste.Models; @@ -33,15 +35,14 @@ public partial class OptionsViewModel : ObservableObject, IDisposable private readonly IUserSettings _userSettings; private readonly AICompletionsHelper aiHelper; private readonly App app = App.Current as App; - private readonly PasteFormat[] _allStandardPasteFormats; public DataPackageView ClipboardData { get; set; } [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ClipboardHasData))] [NotifyPropertyChangedFor(nameof(InputTxtBoxPlaceholderText))] [NotifyPropertyChangedFor(nameof(GeneralErrorText))] - [NotifyPropertyChangedFor(nameof(IsCustomAIEnabled))] - private bool _isClipboardDataText; + private ClipboardFormat _availableClipboardFormats; [ObservableProperty] private bool _clipboardHistoryEnabled; @@ -65,7 +66,9 @@ public partial class OptionsViewModel : ObservableObject, IDisposable public ObservableCollection CustomActionPasteFormats { get; } = []; - public bool IsCustomAIEnabled => IsAllowedByGPO && IsClipboardDataText && aiHelper.IsAIEnabled; + public bool IsCustomAIEnabled => IsAllowedByGPO && aiHelper.IsAIEnabled; + + public bool ClipboardHasData => AvailableClipboardFormats != ClipboardFormat.None; public event EventHandler CustomActionActivated; @@ -76,13 +79,6 @@ public OptionsViewModel(IUserSettings userSettings) ApiRequestStatus = (int)HttpStatusCode.OK; - _allStandardPasteFormats = - [ - new PasteFormat { IconGlyph = "\uE8E9", Name = ResourceLoaderInstance.ResourceLoader.GetString("PasteAsPlainText"), Format = PasteFormats.PlainText }, - new PasteFormat { IconGlyph = "\ue8a5", Name = ResourceLoaderInstance.ResourceLoader.GetString("PasteAsMarkdown"), Format = PasteFormats.Markdown }, - new PasteFormat { IconGlyph = "\uE943", Name = ResourceLoaderInstance.ResourceLoader.GetString("PasteAsJson"), Format = PasteFormats.Json }, - ]; - GeneratedResponses = new ObservableCollection(); GeneratedResponses.CollectionChanged += (s, e) => { @@ -97,10 +93,12 @@ public OptionsViewModel(IUserSettings userSettings) _clipboardTimer.Start(); RefreshPasteFormats(); - _userSettings.CustomActions.CollectionChanged += (_, _) => EnqueueRefreshPasteFormats(); + _userSettings.Changed += (_, _) => EnqueueRefreshPasteFormats(); PropertyChanged += (_, e) => { - if (e.PropertyName == nameof(Query)) + string[] dirtyingProperties = [nameof(Query), nameof(IsCustomAIEnabled), nameof(AvailableClipboardFormats)]; + + if (dirtyingProperties.Contains(e.PropertyName)) { EnqueueRefreshPasteFormats(); } @@ -131,10 +129,12 @@ private void EnqueueRefreshPasteFormats() }); } + private PasteFormat CreatePasteFormat(PasteFormats format) => new(format, AvailableClipboardFormats, IsCustomAIEnabled, ResourceLoaderInstance.ResourceLoader.GetString); + + private PasteFormat CreatePasteFormat(AdvancedPasteCustomAction customAction) => new(customAction, AvailableClipboardFormats, IsCustomAIEnabled); + private void RefreshPasteFormats() { - bool Filter(string text) => text.Contains(Query, StringComparison.CurrentCultureIgnoreCase); - var ctrlString = ResourceLoaderInstance.ResourceLoader.GetString("CtrlKey"); int shortcutNum = 0; @@ -144,25 +144,33 @@ string GetNextShortcutText() return shortcutNum <= 9 ? $"{ctrlString}+{shortcutNum}" : string.Empty; } - StandardPasteFormats.Clear(); - foreach (var format in _allStandardPasteFormats) - { - if (Filter(format.Name)) - { - format.ShortcutText = GetNextShortcutText(); - format.ToolTip = $"{format.Name} ({format.ShortcutText})"; - StandardPasteFormats.Add(format); - } - } + IEnumerable FilterAndSort(IEnumerable pasteFormats) => + from pasteFormat in pasteFormats + let comparison = StringComparison.CurrentCultureIgnoreCase + where pasteFormat.Name.Contains(Query, comparison) || pasteFormat.Prompt.Contains(Query, comparison) + orderby pasteFormat.IsEnabled descending + select pasteFormat; - CustomActionPasteFormats.Clear(); - foreach (var customAction in _userSettings.CustomActions) + void UpdateFormats(ObservableCollection collection, IEnumerable pasteFormats) { - if (Filter(customAction.Name) || Filter(customAction.Prompt)) + collection.Clear(); + + foreach (var format in FilterAndSort(pasteFormats)) { - CustomActionPasteFormats.Add(new PasteFormat(customAction, GetNextShortcutText())); + if (format.IsEnabled) + { + format.ShortcutText = GetNextShortcutText(); + } + + collection.Add(format); } } + + UpdateFormats(StandardPasteFormats, Enum.GetValues() + .Where(format => PasteFormat.MetadataDict[format].IsCoreAction || _userSettings.AdditionalActions.Contains(format)) + .Select(CreatePasteFormat)); + + UpdateFormats(CustomActionPasteFormats, _userSettings.CustomActions.Select(CreatePasteFormat)); } public void Dispose() @@ -174,7 +182,18 @@ public void Dispose() public void ReadClipboard() { ClipboardData = Clipboard.GetContent(); - IsClipboardDataText = ClipboardData.Contains(StandardDataFormats.Text); + + (string DataFormat, ClipboardFormat ClipboardFormat)[] formats = + [ + (StandardDataFormats.Text, ClipboardFormat.Text), + (StandardDataFormats.Html, ClipboardFormat.Html), + (StandardDataFormats.Bitmap, ClipboardFormat.Image), + (StandardDataFormats.StorageItems, ClipboardFormat.File), + ]; + + AvailableClipboardFormats = formats.Aggregate( + ClipboardFormat.None, + (result, formatTuple) => ClipboardData.Contains(formatTuple.DataFormat) ? (result | formatTuple.ClipboardFormat) : result); } public void OnShow() @@ -247,7 +266,7 @@ public string InputTxtBoxPlaceholderText { app.GetMainWindow().ClearInputText(); - return IsClipboardDataText ? ResourceLoaderInstance.ResourceLoader.GetString("CustomFormatTextBox/PlaceholderText") : GeneralErrorText; + return ClipboardHasData ? ResourceLoaderInstance.ResourceLoader.GetString("CustomFormatTextBox/PlaceholderText") : GeneralErrorText; } } @@ -255,9 +274,9 @@ public string GeneralErrorText { get { - if (!IsClipboardDataText) + if (!ClipboardHasData) { - return ResourceLoaderInstance.ResourceLoader.GetString("ClipboardDataTypeMismatchWarning"); + return ResourceLoaderInstance.ResourceLoader.GetString("ClipboardEmptyWarning"); } if (!IsAllowedByGPO) @@ -403,10 +422,40 @@ internal void ToJsonFunction(bool pasteAlways = false) } } + internal void ImageToTextFunction() + { + Task.Factory + .StartNew(async () => await ImageToTextFunctionAsync(), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.FromCurrentSynchronizationContext()); + } + + internal async Task ImageToTextFunctionAsync(bool pasteAlways = false) + { + try + { + Logger.LogTrace(); + + var bitmap = await ClipboardHelper.GetClipboardImageContentAsync(ClipboardData); + var text = await OcrHelpers.GetTextAsync(bitmap); + SetClipboardContentAndHideWindow(text); + + if (pasteAlways || _userSettings.SendPasteKeyCombination) + { + ClipboardHelper.SendPasteKeyCombination(); + } + } + catch (Exception ex) + { + Logger.LogError("Unable to extract text from image", ex); + + await app.GetMainWindow().ShowMessageDialogAsync(ResourceLoaderInstance.ResourceLoader.GetString("PasteError")); + } + } + internal void ExecutePasteFormat(VirtualKey key) { - var index = key - VirtualKey.Number1; - var pasteFormat = StandardPasteFormats.ElementAtOrDefault(index) ?? CustomActionPasteFormats.ElementAtOrDefault(index - StandardPasteFormats.Count); + var pasteFormat = StandardPasteFormats.Concat(CustomActionPasteFormats) + .Where(pasteFormat => pasteFormat.IsEnabled) + .ElementAtOrDefault(key - VirtualKey.Number1); if (pasteFormat != null) { @@ -417,7 +466,7 @@ internal void ExecutePasteFormat(VirtualKey key) internal void ExecutePasteFormat(PasteFormat pasteFormat) { - if (!IsClipboardDataText || (pasteFormat.Format == PasteFormats.Custom && !IsCustomAIEnabled)) + if (!pasteFormat.IsEnabled) { return; } @@ -436,6 +485,22 @@ internal void ExecutePasteFormat(PasteFormat pasteFormat) ToJsonFunction(); break; + case PasteFormats.AudioToText: + throw new NotImplementedException(); + + case PasteFormats.ImageToText: + ImageToTextFunction(); + break; + + case PasteFormats.PasteAsTxtFile: + throw new NotImplementedException(); + + case PasteFormats.PasteAsPngFile: + throw new NotImplementedException(); + + case PasteFormats.PasteAsHtmlFile: + throw new NotImplementedException(); + case PasteFormats.Custom: Query = pasteFormat.Prompt; CustomActionActivated?.Invoke(this, new CustomActionActivatedEventArgs(pasteFormat.Prompt, false)); @@ -443,6 +508,11 @@ internal void ExecutePasteFormat(PasteFormat pasteFormat) } } + internal void ExecuteAdditionalAction(PasteFormats format) + { + ExecutePasteFormat(CreatePasteFormat(format)); + } + internal void ExecuteCustomActionWithPaste(int customActionId) { Logger.LogTrace(); @@ -465,7 +535,7 @@ internal async Task GenerateCustomFunction(string inputInstructions) return string.Empty; } - if (!IsClipboardDataText) + if (!AvailableClipboardFormats.HasFlag(ClipboardFormat.Text)) { Logger.LogWarning("Clipboard does not contain text data"); return string.Empty; @@ -475,7 +545,7 @@ internal async Task GenerateCustomFunction(string inputInstructions) { try { - string text = await ClipboardData.GetTextAsync() as string; + string text = await ClipboardData.GetTextAsync(); return text; } catch (Exception) @@ -530,11 +600,7 @@ internal void SaveQuery(string inputQuery) return; } - string currentClipboardText = Task.Run(async () => - { - string text = await clipboardData.GetTextAsync() as string; - return text; - }).Result; + var currentClipboardText = Task.Run(async () => await clipboardData.GetTextAsync()).Result; var queryData = new CustomQuery { diff --git a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp index bb6f2f952a68..ef7ca6bd2887 100644 --- a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp +++ b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp @@ -40,6 +40,7 @@ namespace { const wchar_t JSON_KEY_PROPERTIES[] = L"properties"; const wchar_t JSON_KEY_CUSTOM_ACTIONS[] = L"custom-actions"; + const wchar_t JSON_KEY_ADDITIONAL_ACTIONS[] = L"additional-actions"; const wchar_t JSON_KEY_SHORTCUT[] = L"shortcut"; const wchar_t JSON_KEY_IS_SHOWN[] = L"isShown"; const wchar_t JSON_KEY_ID[] = L"id"; @@ -68,7 +69,6 @@ class AdvancedPaste : public PowertoyModuleIface HANDLE m_hProcess; - std::thread create_pipe_thread; std::unique_ptr m_write_pipe; // Time to wait for process to close after sending WM_CLOSE signal @@ -81,8 +81,18 @@ class AdvancedPaste : public PowertoyModuleIface Hotkey m_paste_as_markdown_hotkey{}; Hotkey m_paste_as_json_hotkey{}; - std::vector m_custom_action_hotkeys; - std::vector m_custom_action_ids; + template + struct ActionData + { + TKey id; + Hotkey hotkey; + }; + + using AdditionalAction = ActionData; + std::vector m_additional_actions; + + using CustomAction = ActionData; + std::vector m_custom_actions; bool m_preview_custom_format_output = true; @@ -164,6 +174,39 @@ class AdvancedPaste : public PowertoyModuleIface return false; } + void process_additional_action(const winrt::hstring& actionName, const winrt::Windows::Data::Json::IJsonValue& actionValue) + { + if (actionValue.ValueType() != winrt::Windows::Data::Json::JsonValueType::Object) + { + return; + } + + const auto action = actionValue.GetObjectW(); + + if (!action.GetNamedBoolean(JSON_KEY_IS_SHOWN, false)) + { + return; + } + + if (action.HasKey(JSON_KEY_SHORTCUT)) + { + const AdditionalAction additionalAction + { + actionName.c_str(), + parse_single_hotkey(action.GetNamedObject(JSON_KEY_SHORTCUT)) + }; + + m_additional_actions.push_back(additionalAction); + } + else + { + for (const auto& [subActionName, subAction] : action) + { + process_additional_action(subActionName, subAction); + } + } + } + void parse_hotkeys(PowerToysSettings::PowerToyValues& settings) { auto settingsObject = settings.get_raw_json(); @@ -206,13 +249,23 @@ class AdvancedPaste : public PowertoyModuleIface *hotkey = parse_single_hotkey(keyName, settingsObject); } - m_custom_action_hotkeys.clear(); - m_custom_action_ids.clear(); + m_additional_actions.clear(); + m_custom_actions.clear(); if (settingsObject.HasKey(JSON_KEY_PROPERTIES)) { const auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES); + if (propertiesObject.HasKey(JSON_KEY_ADDITIONAL_ACTIONS)) + { + const auto additionalActions = propertiesObject.GetNamedObject(JSON_KEY_ADDITIONAL_ACTIONS); + + for (const auto& [actionName, additionalAction] : additionalActions) + { + process_additional_action(actionName, additionalAction); + } + } + if (propertiesObject.HasKey(JSON_KEY_CUSTOM_ACTIONS)) { const auto customActions = propertiesObject.GetNamedObject(JSON_KEY_CUSTOM_ACTIONS).GetNamedArray(JSON_KEY_VALUE); @@ -223,8 +276,13 @@ class AdvancedPaste : public PowertoyModuleIface if (object.GetNamedBoolean(JSON_KEY_IS_SHOWN, false)) { - m_custom_action_hotkeys.push_back(parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT))); - m_custom_action_ids.push_back(static_cast(object.GetNamedNumber(JSON_KEY_ID))); + const CustomAction customActionData + { + static_cast(object.GetNamedNumber(JSON_KEY_ID)), + parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT)) + }; + + m_custom_actions.push_back(customActionData); } } } @@ -296,7 +354,7 @@ class AdvancedPaste : public PowertoyModuleIface return; } - create_pipe_thread = std::thread([&] { start_named_pipe_server(pipe_name.value()); }); + std::thread create_pipe_thread ([&]{ start_named_pipe_server(pipe_name.value()); }); launch_process(pipe_name.value()); create_pipe_thread.join(); } @@ -789,11 +847,22 @@ class AdvancedPaste : public PowertoyModuleIface return true; } - const auto custom_action_index = hotkeyId - NUM_DEFAULT_HOTKEYS; - if (custom_action_index < m_custom_action_ids.size()) + const auto additional_action_index = hotkeyId - NUM_DEFAULT_HOTKEYS; + if (additional_action_index < m_additional_actions.size()) + { + const auto& id = m_additional_actions.at(additional_action_index).id; + + Logger::trace(L"Starting additional action id={}", id); + + send_named_pipe_message(CommonSharedConstants::ADVANCED_PASTE_ADDITIONAL_ACTION_MESSAGE, id); + return true; + } + + const auto custom_action_index = additional_action_index - m_additional_actions.size(); + if (custom_action_index < m_custom_actions.size()) { - const auto id = m_custom_action_ids.at(custom_action_index); + const auto id = m_custom_actions.at(custom_action_index).id; Logger::trace(L"Starting custom action id={}", id); @@ -807,7 +876,7 @@ class AdvancedPaste : public PowertoyModuleIface virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override { - const size_t num_hotkeys = NUM_DEFAULT_HOTKEYS + m_custom_action_hotkeys.size(); + const size_t num_hotkeys = NUM_DEFAULT_HOTKEYS + m_additional_actions.size() + m_custom_actions.size(); if (hotkeys && buffer_size >= num_hotkeys) { @@ -815,9 +884,11 @@ class AdvancedPaste : public PowertoyModuleIface m_advanced_paste_ui_hotkey, m_paste_as_markdown_hotkey, m_paste_as_json_hotkey }; - std::copy(default_hotkeys.begin(), default_hotkeys.end(), hotkeys); - std::copy(m_custom_action_hotkeys.begin(), m_custom_action_hotkeys.end(), hotkeys + NUM_DEFAULT_HOTKEYS); + + const auto get_action_hotkey = [](const auto& action) { return action.hotkey; }; + std::transform(m_additional_actions.begin(), m_additional_actions.end(), hotkeys + NUM_DEFAULT_HOTKEYS, get_action_hotkey); + std::transform(m_custom_actions.begin(), m_custom_actions.end(), hotkeys + NUM_DEFAULT_HOTKEYS + m_additional_actions.size(), get_action_hotkey); } return num_hotkeys; diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalAction.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalAction.cs new file mode 100644 index 000000000000..7a6fd3081aa1 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalAction.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.Text.Json.Serialization; + +using Microsoft.PowerToys.Settings.UI.Library.Helpers; + +namespace Microsoft.PowerToys.Settings.UI.Library; + +public sealed partial class AdvancedPasteAdditionalAction : Observable, IAdvancedPasteAction +{ + private HotkeySettings _shortcut = new(); + private bool _isShown = true; + + [JsonPropertyName("shortcut")] + public HotkeySettings Shortcut + { + get => _shortcut; + set + { + if (_shortcut != value) + { + // We null-coalesce here rather than outside this branch as we want to raise PropertyChanged when the setter is called + // with null; the ShortcutControl depends on this. + _shortcut = value ?? new(); + + OnPropertyChanged(); + } + } + } + + [JsonPropertyName("isShown")] + public bool IsShown + { + get => _isShown; + set => Set(ref _isShown, value); + } +} diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalActions.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalActions.cs new file mode 100644 index 000000000000..1f7a18f40270 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteAdditionalActions.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.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; + +namespace Microsoft.PowerToys.Settings.UI.Library; + +public sealed class AdvancedPasteAdditionalActions +{ + public static class PropertyNames + { + public const string AudioToText = "audio-to-text"; + public const string ImageToText = "image-to-text"; + public const string PasteAsFile = "paste-as-file"; + } + + [JsonPropertyName(PropertyNames.AudioToText)] + public AdvancedPasteAdditionalAction AudioToText { get; init; } = new(); + + [JsonPropertyName(PropertyNames.ImageToText)] + public AdvancedPasteAdditionalAction ImageToText { get; init; } = new(); + + [JsonPropertyName(PropertyNames.PasteAsFile)] + public AdvancedPastePasteAsFileAction PasteAsFile { get; init; } = new(); + + [JsonIgnore] + public IEnumerable AllActions => new IAdvancedPasteAction[] { AudioToText, ImageToText, PasteAsFile }.Concat(PasteAsFile.SubActions); +} diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs index 585663026bb0..f3bb4431ca29 100644 --- a/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs +++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs @@ -3,14 +3,13 @@ // See the LICENSE file in the project root for more information. using System; -using System.ComponentModel; -using System.Runtime.CompilerServices; -using System.Text.Json; using System.Text.Json.Serialization; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; + namespace Microsoft.PowerToys.Settings.UI.Library; -public sealed class AdvancedPasteCustomAction : INotifyPropertyChanged, ICloneable +public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction, ICloneable { private int _id; private string _name = string.Empty; @@ -25,14 +24,7 @@ public sealed class AdvancedPasteCustomAction : INotifyPropertyChanged, ICloneab public int Id { get => _id; - set - { - if (_id != value) - { - _id = value; - OnPropertyChanged(); - } - } + set => Set(ref _id, value); } [JsonPropertyName("name")] @@ -41,10 +33,8 @@ public string Name get => _name; set { - if (_name != value) + if (Set(ref _name, value)) { - _name = value; - OnPropertyChanged(); UpdateIsValid(); } } @@ -56,10 +46,8 @@ public string Prompt get => _prompt; set { - if (_prompt != value) + if (Set(ref _prompt, value)) { - _prompt = value; - OnPropertyChanged(); UpdateIsValid(); } } @@ -86,62 +74,30 @@ public HotkeySettings Shortcut public bool IsShown { get => _isShown; - set - { - if (_isShown != value) - { - _isShown = value; - OnPropertyChanged(); - } - } + set => Set(ref _isShown, value); } [JsonIgnore] public bool CanMoveUp { get => _canMoveUp; - set - { - if (_canMoveUp != value) - { - _canMoveUp = value; - OnPropertyChanged(); - } - } + set => Set(ref _canMoveUp, value); } [JsonIgnore] public bool CanMoveDown { get => _canMoveDown; - set - { - if (_canMoveDown != value) - { - _canMoveDown = value; - OnPropertyChanged(); - } - } + set => Set(ref _canMoveDown, value); } [JsonIgnore] public bool IsValid { get => _isValid; - private set - { - if (_isValid != value) - { - _isValid = value; - OnPropertyChanged(); - } - } + private set => Set(ref _isValid, value); } - public event PropertyChangedEventHandler PropertyChanged; - - public string ToJsonString() => JsonSerializer.Serialize(this); - public object Clone() { AdvancedPasteCustomAction clone = new(); @@ -160,11 +116,6 @@ public void Update(AdvancedPasteCustomAction other) CanMoveDown = other.CanMoveDown; } - private void OnPropertyChanged([CallerMemberName] string propertyName = null) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - private HotkeySettings GetShortcutClone() { object shortcut = null; diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPastePasteAsFileAction.cs b/src/settings-ui/Settings.UI.Library/AdvancedPastePasteAsFileAction.cs new file mode 100644 index 000000000000..979e967d4a0a --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/AdvancedPastePasteAsFileAction.cs @@ -0,0 +1,56 @@ +// 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 AdvancedPastePasteAsFileAction : Observable, IAdvancedPasteAction +{ + public static class PropertyNames + { + public const string PasteAsTxtFile = "paste-as-txt-file"; + public const string PasteAsPngFile = "paste-as-png-file"; + public const string PasteAsHtmlFile = "paste-as-html-file"; + } + + private AdvancedPasteAdditionalAction _pasteAsTxtFile = new(); + private AdvancedPasteAdditionalAction _pasteAsPngFile = new(); + private AdvancedPasteAdditionalAction _pasteAsHtmlFile = new(); + private bool _isShown = true; + + [JsonPropertyName("isShown")] + public bool IsShown + { + get => _isShown; + set => Set(ref _isShown, value); + } + + [JsonPropertyName(PropertyNames.PasteAsTxtFile)] + public AdvancedPasteAdditionalAction PasteAsTxtFile + { + get => _pasteAsTxtFile; + init => Set(ref _pasteAsTxtFile, value); + } + + [JsonPropertyName(PropertyNames.PasteAsPngFile)] + public AdvancedPasteAdditionalAction PasteAsPngFile + { + get => _pasteAsPngFile; + init => Set(ref _pasteAsPngFile, value); + } + + [JsonPropertyName(PropertyNames.PasteAsHtmlFile)] + public AdvancedPasteAdditionalAction PasteAsHtmlFile + { + get => _pasteAsHtmlFile; + init => Set(ref _pasteAsHtmlFile, value); + } + + [JsonIgnore] + public IEnumerable SubActions => [PasteAsTxtFile, PasteAsPngFile, PasteAsHtmlFile]; +} diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs index 9af393065b5e..3953380c82c4 100644 --- a/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs +++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs @@ -21,6 +21,7 @@ public AdvancedPasteProperties() PasteAsMarkdownShortcut = new(); PasteAsJsonShortcut = new(); CustomActions = new(); + AdditionalActions = new(); ShowCustomPreview = true; SendPasteKeyCombination = true; CloseAfterLosingFocus = false; @@ -50,7 +51,11 @@ public AdvancedPasteProperties() [JsonPropertyName("custom-actions")] [CmdConfigureIgnoreAttribute] - public AdvancedPasteCustomActions CustomActions { get; set; } + public AdvancedPasteCustomActions CustomActions { get; init; } + + [JsonPropertyName("additional-actions")] + [CmdConfigureIgnoreAttribute] + public AdvancedPasteAdditionalActions AdditionalActions { get; init; } public override string ToString() => JsonSerializer.Serialize(this); diff --git a/src/settings-ui/Settings.UI.Library/Helpers/Observable.cs b/src/settings-ui/Settings.UI.Library/Helpers/Observable.cs index a77099b4b853..79b4e9d2ee6d 100644 --- a/src/settings-ui/Settings.UI.Library/Helpers/Observable.cs +++ b/src/settings-ui/Settings.UI.Library/Helpers/Observable.cs @@ -11,17 +11,19 @@ public class Observable : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; - protected void Set(ref T storage, T value, [CallerMemberName] string propertyName = null) + protected bool Set(ref T storage, T value, [CallerMemberName] string propertyName = null) { if (Equals(storage, value)) { - return; + return false; } storage = value; OnPropertyChanged(propertyName); + + return true; } - protected void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + protected void OnPropertyChanged([CallerMemberName] string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } diff --git a/src/settings-ui/Settings.UI.Library/IAdvancedPasteAction.cs b/src/settings-ui/Settings.UI.Library/IAdvancedPasteAction.cs new file mode 100644 index 000000000000..4c31557010c9 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/IAdvancedPasteAction.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.ComponentModel; + +namespace Microsoft.PowerToys.Settings.UI.Library; + +public interface IAdvancedPasteAction : INotifyPropertyChanged +{ + public bool IsShown { get; } +} diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml index ed7a634388a4..e2aff674bcfa 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml @@ -24,6 +24,18 @@ ms-appx:///Assets/Settings/Modules/APDialog.light.png + + + + + + @@ -126,6 +138,43 @@ AllowDisable="True" HotkeySettings="{x:Bind Path=ViewModel.PasteAsJsonShortcut, Mode=TwoWay}" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Paste as Custom with AI directly + + Audio to text + + + Image to text + + + Paste as file + + + Paste as .txt file + + + Paste as .png file + + + Paste as .html file + OpenAI API key: diff --git a/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs index 8d0bacacec50..e1196ff35eb4 100644 --- a/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs @@ -38,6 +38,7 @@ public class AdvancedPasteViewModel : Observable, IDisposable private readonly AdvancedPasteSettings _advancedPasteSettings; private readonly ObservableCollection _customActions; + private readonly AdvancedPasteAdditionalActions _additionalActions; private Timer _delayedTimer; private GpoRuleConfigured _enabledGpoRuleConfiguration; @@ -69,6 +70,7 @@ public AdvancedPasteViewModel( _advancedPasteSettings = advancedPasteSettingsRepository.SettingsConfig; _customActions = _advancedPasteSettings.Properties.CustomActions.Value; + _additionalActions = _advancedPasteSettings.Properties.AdditionalActions; InitializeEnabledValue(); @@ -87,6 +89,11 @@ public AdvancedPasteViewModel( _customActions.CollectionChanged += OnCustomActionsCollectionChanged; UpdateCustomActionsCanMoveUpDown(); + + foreach (var action in _additionalActions.AllActions) + { + action.PropertyChanged += (_, _) => SaveAndNotifySettings(); + } } private void InitializeEnabledValue() @@ -142,6 +149,8 @@ public bool IsEnabled public ObservableCollection CustomActions => _customActions; + public AdvancedPasteAdditionalActions AdditionalActions => _additionalActions; + private bool OpenAIKeyExists() { PasswordVault vault = new PasswordVault(); From e61460d26e009c464e28a008b597b5c9eb2c9b16 Mon Sep 17 00:00:00 2001 From: Ani <115020168+drawbyperpetual@users.noreply.github.com> Date: Wed, 11 Sep 2024 22:10:27 +0200 Subject: [PATCH 02/51] Spellcheck issue --- .../AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp index ef7ca6bd2887..7d8c9cfaa749 100644 --- a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp +++ b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp @@ -81,10 +81,10 @@ class AdvancedPaste : public PowertoyModuleIface Hotkey m_paste_as_markdown_hotkey{}; Hotkey m_paste_as_json_hotkey{}; - template + template struct ActionData { - TKey id; + Id id; Hotkey hotkey; }; From b9532186bdc27492a8d872f862e97390f07aaeb8 Mon Sep 17 00:00:00 2001 From: Ani <115020168+drawbyperpetual@users.noreply.github.com> Date: Wed, 18 Sep 2024 14:16:27 +0200 Subject: [PATCH 03/51] [AdvancedPaste] Paste as file and many other improvements --- .../AdvancedPasteXAML/App.xaml.cs | 54 +-- .../AdvancedPasteXAML/Controls/PromptBox.xaml | 2 +- .../Controls/PromptBox.xaml.cs | 92 ++--- .../AdvancedPasteXAML/Pages/MainPage.xaml.cs | 8 +- .../AdvancedPaste/Helpers/ClipboardHelper.cs | 137 ++++--- .../AdvancedPaste/Models/ClipboardFormat.cs | 4 +- .../Models/CustomActionActivatedEventArgs.cs | 4 +- .../Models/PasteActionException.cs | 11 + .../AdvancedPaste/Models/PasteActionSource.cs | 13 + .../AdvancedPaste/Models/PasteFormat.cs | 2 + .../AdvancedPaste/Models/PasteFormats.cs | 6 +- .../Services/IPasteFormatExecutor.cs | 13 + .../Services/PasteFormatExecutor.cs | 256 ++++++++++++ .../Strings/en-us/Resources.resw | 8 +- .../ViewModels/OptionsViewModel.cs | 365 ++++++------------ 15 files changed, 563 insertions(+), 412 deletions(-) create mode 100644 src/modules/AdvancedPaste/AdvancedPaste/Models/PasteActionException.cs create mode 100644 src/modules/AdvancedPaste/AdvancedPaste/Models/PasteActionSource.cs create mode 100644 src/modules/AdvancedPaste/AdvancedPaste/Services/IPasteFormatExecutor.cs create mode 100644 src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs index a937be3e3126..960b36ae5082 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using AdvancedPaste.Helpers; using AdvancedPaste.Models; +using AdvancedPaste.Services; using AdvancedPaste.Settings; using AdvancedPaste.ViewModels; using ManagedCommon; @@ -61,8 +62,10 @@ public App() Host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder().UseContentRoot(AppContext.BaseDirectory).ConfigureServices((context, services) => { - services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); }).Build(); viewModel = GetService(); @@ -112,7 +115,7 @@ protected override void OnLaunched(LaunchActivatedEventArgs args) private void ProcessNamedPipe(string pipeName) { - void OnMessage(string message) => _dispatcherQueue.TryEnqueue(() => OnNamedPipeMessage(message)); + void OnMessage(string message) => _dispatcherQueue.TryEnqueue(async () => await OnNamedPipeMessage(message)); Task.Run(async () => { @@ -121,30 +124,30 @@ private void ProcessNamedPipe(string pipeName) }); } - private void OnNamedPipeMessage(string message) + private async Task OnNamedPipeMessage(string message) { var messageParts = message.Split(); var messageType = messageParts.First(); if (messageType == PowerToys.Interop.Constants.AdvancedPasteShowUIMessage()) { - OnAdvancedPasteHotkey(); + await ShowWindow(); } else if (messageType == PowerToys.Interop.Constants.AdvancedPasteMarkdownMessage()) { - OnAdvancedPasteMarkdownHotkey(); + await viewModel.ExceutePasteFormatAsync(PasteFormats.Markdown, PasteActionSource.GlobalKeyboardShortcut); } else if (messageType == PowerToys.Interop.Constants.AdvancedPasteJsonMessage()) { - OnAdvancedPasteJsonHotkey(); + await viewModel.ExceutePasteFormatAsync(PasteFormats.Json, PasteActionSource.GlobalKeyboardShortcut); } else if (messageType == PowerToys.Interop.Constants.AdvancedPasteAdditionalActionMessage()) { - OnAdvancedPasteAdditionalActionHotkey(messageParts); + await OnAdvancedPasteAdditionalActionHotkey(messageParts); } else if (messageType == PowerToys.Interop.Constants.AdvancedPasteCustomActionMessage()) { - OnAdvancedPasteCustomActionHotkey(messageParts); + await OnAdvancedPasteCustomActionHotkey(messageParts); } } @@ -153,24 +156,7 @@ private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledEx Logger.LogError("Unhandled exception", e.Exception); } - private void OnAdvancedPasteJsonHotkey() - { - viewModel.ReadClipboard(); - viewModel.ToJsonFunction(true); - } - - private void OnAdvancedPasteMarkdownHotkey() - { - viewModel.ReadClipboard(); - viewModel.ToMarkdownFunction(true); - } - - private void OnAdvancedPasteHotkey() - { - ShowWindow(); - } - - private void OnAdvancedPasteAdditionalActionHotkey(string[] messageParts) + private async Task OnAdvancedPasteAdditionalActionHotkey(string[] messageParts) { if (messageParts.Length != 2) { @@ -184,14 +170,13 @@ private void OnAdvancedPasteAdditionalActionHotkey(string[] messageParts) } else { - ShowWindow(); - viewModel.ReadClipboard(); - viewModel.ExecuteAdditionalAction(pasteFormat); + await ShowWindow(); + await viewModel.ExceutePasteFormatAsync(pasteFormat, PasteActionSource.GlobalKeyboardShortcut); } } } - private void OnAdvancedPasteCustomActionHotkey(string[] messageParts) + private async Task OnAdvancedPasteCustomActionHotkey(string[] messageParts) { if (messageParts.Length != 2) { @@ -205,16 +190,15 @@ private void OnAdvancedPasteCustomActionHotkey(string[] messageParts) } else { - ShowWindow(); - viewModel.ReadClipboard(); - viewModel.ExecuteCustomActionWithPaste(customActionId); + await ShowWindow(); + await viewModel.ExecuteCustomAction(customActionId, PasteActionSource.GlobalKeyboardShortcut); } } } - private void ShowWindow() + private async Task ShowWindow() { - viewModel.OnShow(); + await viewModel.OnShow(); if (window is null) { diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml index 6da59f66c414..74021ebb80bb 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml @@ -589,7 +589,7 @@ Background="Transparent" Visibility="{x:Bind ViewModel.IsCustomAIEnabled, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}"> - + diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml.cs index 457052d61937..7e57a4eeeb6a 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml.cs @@ -2,16 +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.Net; using System.Threading.Tasks; using AdvancedPaste.Helpers; -using AdvancedPaste.Settings; +using AdvancedPaste.Models; using AdvancedPaste.ViewModels; using CommunityToolkit.Mvvm.Input; using ManagedCommon; using Microsoft.PowerToys.Telemetry; -using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -19,12 +16,6 @@ namespace AdvancedPaste.Controls { public sealed partial class PromptBox : Microsoft.UI.Xaml.Controls.UserControl { - // Minimum time to show spinner when generating custom format using forcePasteCustom - private static readonly TimeSpan MinTaskTime = TimeSpan.FromSeconds(2); - - private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); - private readonly IUserSettings _userSettings; - public OptionsViewModel ViewModel { get; private set; } public static readonly DependencyProperty PlaceholderTextProperty = DependencyProperty.Register( @@ -53,63 +44,41 @@ public object Footer public PromptBox() { - this.InitializeComponent(); - - _userSettings = App.GetService(); + InitializeComponent(); ViewModel = App.GetService(); - ViewModel.CustomActionActivated += (_, e) => GenerateCustom(e.ForcePasteCustom); + ViewModel.PropertyChanged += ViewModel_PropertyChanged; + ViewModel.CustomActionActivated += ViewModel_CustomActionActivated; } - private void Grid_Loaded(object sender, RoutedEventArgs e) + private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { - InputTxtBox.Focus(FocusState.Programmatic); + if (e.PropertyName == nameof(ViewModel.Busy) || e.PropertyName == nameof(ViewModel.ApiErrorText)) + { + var state = ViewModel.Busy ? "LoadingState" : string.IsNullOrEmpty(ViewModel.ApiErrorText) ? "DefaultState" : "ErrorState"; + VisualStateManager.GoToState(this, state, true); + } } - [RelayCommand] - private void GenerateCustom() => GenerateCustom(false); - - private void GenerateCustom(bool forcePasteCustom) + private void ViewModel_CustomActionActivated(object sender, Models.CustomActionActivatedEventArgs e) { Logger.LogTrace(); - VisualStateManager.GoToState(this, "LoadingState", true); - string inputInstructions = ViewModel.Query; - ViewModel.SaveQuery(inputInstructions); - - var customFormatTask = ViewModel.GenerateCustomFunction(inputInstructions); - var delayTask = forcePasteCustom ? Task.Delay(MinTaskTime) : Task.CompletedTask; - Task.WhenAll(customFormatTask, delayTask) - .ContinueWith( - _ => - { - _dispatcherQueue.TryEnqueue(() => - { - ViewModel.CustomFormatResult = customFormatTask.Result; - - if (ViewModel.ApiRequestStatus == (int)HttpStatusCode.OK) - { - VisualStateManager.GoToState(this, "DefaultState", true); - if (_userSettings.ShowCustomPreview && !forcePasteCustom) - { - PreviewGrid.Width = InputTxtBox.ActualWidth; - PreviewFlyout.ShowAt(InputTxtBox); - } - else - { - ViewModel.PasteCustom(); - InputTxtBox.Text = string.Empty; - } - } - else - { - VisualStateManager.GoToState(this, "ErrorState", true); - } - }); - }, - TaskScheduler.Default); + if (!e.PasteResult) + { + PreviewGrid.Width = InputTxtBox.ActualWidth; + PreviewFlyout.ShowAt(InputTxtBox); + } } + private void Grid_Loaded(object sender, RoutedEventArgs e) + { + InputTxtBox.Focus(FocusState.Programmatic); + } + + [RelayCommand] + private async Task GenerateCustom() => await ViewModel.GenerateCustomFunction(PasteActionSource.PromptBox); + [RelayCommand] private void Recall() { @@ -126,29 +95,24 @@ private void Recall() ClipboardHelper.SetClipboardTextContent(lastQuery.ClipboardData); } - private void InputTxtBox_KeyDown(object sender, Microsoft.UI.Xaml.Input.KeyRoutedEventArgs e) + 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.IsCustomAIEnabled) { - GenerateCustom(); + await GenerateCustom(); } } private void PreviewPasteBtn_Click(object sender, RoutedEventArgs e) { ViewModel.PasteCustom(); - InputTxtBox.Text = string.Empty; } private void ThumbUpDown_Click(object sender, RoutedEventArgs e) { - if (sender is Button btn) + if (sender is Button btn && bool.TryParse(btn.CommandParameter as string, out bool result)) { - bool result; - if (bool.TryParse(btn.CommandParameter as string, out result)) - { - PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteCustomFormatOutputThumbUpDownEvent(result)); - } + PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteCustomFormatOutputThumbUpDownEvent(result)); } } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml.cs index 25c9fc43ee79..39f0a71c467e 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml.cs @@ -129,15 +129,15 @@ private void ClipboardHistoryItemDeleteButton_Click(object sender, RoutedEventAr } } - private void ListView_Button_Click(object sender, RoutedEventArgs e) + private async void ListView_Button_Click(object sender, RoutedEventArgs e) { if (sender is Button { DataContext: PasteFormat format }) { - ViewModel.ExecutePasteFormat(format); + await ViewModel.ExecutePasteFormatAsync(format, PasteActionSource.ContextMenu); } } - private void KeyboardAccelerator_Invoked(Microsoft.UI.Xaml.Input.KeyboardAccelerator sender, Microsoft.UI.Xaml.Input.KeyboardAcceleratorInvokedEventArgs args) + private async void KeyboardAccelerator_Invoked(Microsoft.UI.Xaml.Input.KeyboardAccelerator sender, Microsoft.UI.Xaml.Input.KeyboardAcceleratorInvokedEventArgs args) { if (GetMainWindow()?.Visible is false) { @@ -170,7 +170,7 @@ private void KeyboardAccelerator_Invoked(Microsoft.UI.Xaml.Input.KeyboardAcceler case VirtualKey.Number7: case VirtualKey.Number8: case VirtualKey.Number9: - ViewModel.ExecutePasteFormat(sender.Key); + await ViewModel.ExecutePasteFormat(sender.Key); break; default: diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/ClipboardHelper.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/ClipboardHelper.cs index d2612032569b..3c2af0be167e 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/ClipboardHelper.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/ClipboardHelper.cs @@ -3,9 +3,13 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using AdvancedPaste.Models; using ManagedCommon; using Windows.ApplicationModel.DataTransfer; +using Windows.Data.Html; using Windows.Graphics.Imaging; using Windows.Storage; using Windows.Storage.Streams; @@ -15,6 +19,34 @@ namespace AdvancedPaste.Helpers { internal static class ClipboardHelper { + private static readonly HashSet ImageFileTypes = new(StringComparer.InvariantCultureIgnoreCase) { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico", ".svg" }; + + private static readonly (string DataFormat, ClipboardFormat ClipboardFormat)[] DataFormats = + [ + (StandardDataFormats.Text, ClipboardFormat.Text), + (StandardDataFormats.Html, ClipboardFormat.Html), + (StandardDataFormats.Bitmap, ClipboardFormat.Image), + ]; + + internal static async Task GetAvailableClipboardFormats(DataPackageView clipboardData) + { + var availableClipboardFormats = DataFormats.Aggregate( + ClipboardFormat.None, + (result, formatPair) => clipboardData.Contains(formatPair.DataFormat) ? (result | formatPair.ClipboardFormat) : result); + + if (clipboardData.Contains(StandardDataFormats.StorageItems)) + { + var storageItems = await clipboardData.GetStorageItemsAsync(); + + if (storageItems.Count == 1 && storageItems.Single() is StorageFile file && ImageFileTypes.Contains(file.FileType)) + { + availableClipboardFormats |= ClipboardFormat.ImageFile; + } + } + + return availableClipboardFormats; + } + internal static void SetClipboardTextContent(string text) { Logger.LogTrace(); @@ -25,31 +57,41 @@ internal static void SetClipboardTextContent(string text) output.SetText(text); Clipboard.SetContentWithOptions(output, null); - // TODO(stefan): For some reason Flush() fails from time to time when directly activated via hotkey. - // Calling inside a loop makes it work. - bool flushed = false; - for (int i = 0; i < 5; i++) - { - if (flushed) - { - break; - } + Flush(); + } + } - try - { - Task.Run(() => - { - Clipboard.Flush(); - }).Wait(); - - flushed = true; - } - catch (Exception ex) - { - Logger.LogError("Clipboard.Flush() failed", ex); - } + private static bool Flush() + { + // TODO(stefan): For some reason Flush() fails from time to time when directly activated via hotkey. + // Calling inside a loop makes it work. + for (int i = 0; i < 5; i++) + { + try + { + Task.Run(Clipboard.Flush).Wait(); + return true; + } + catch (Exception ex) + { + Logger.LogError($"{nameof(Clipboard)}.{nameof(Flush)}() failed", ex); } } + + return false; + } + + private static async Task FlushAsync() => await Task.Run(Flush); + + internal static async Task SetClipboardFileContentAsync(string fileName) + { + var storageFile = await StorageFile.GetFileFromPathAsync(fileName); + + DataPackage output = new(); + output.SetStorageItems([storageFile]); + Clipboard.SetContent(output); + + await FlushAsync(); } internal static void SetClipboardImageContent(RandomAccessStreamReference image) @@ -62,30 +104,7 @@ internal static void SetClipboardImageContent(RandomAccessStreamReference image) output.SetBitmap(image); Clipboard.SetContentWithOptions(output, null); - // TODO(stefan): For some reason Flush() fails from time to time when directly activated via hotkey. - // Calling inside a loop makes it work. - bool flushed = false; - for (int i = 0; i < 5; i++) - { - if (flushed) - { - break; - } - - try - { - Task.Run(() => - { - Clipboard.Flush(); - }).Wait(); - - flushed = true; - } - catch (Exception ex) - { - Logger.LogError("Clipboard.Flush() failed", ex); - } - } + Flush(); } } @@ -136,6 +155,26 @@ internal static void SendPasteKeyCombination() Logger.LogInfo("Paste sent"); } + internal static async Task GetClipboardTextOrHtmlText(DataPackageView clipboardData) + { + if (clipboardData.Contains(StandardDataFormats.Text)) + { + return await clipboardData.GetTextAsync(); + } + else if (clipboardData.Contains(StandardDataFormats.Html)) + { + var html = await clipboardData.GetHtmlFormatAsync(); + return HtmlUtilities.ConvertToText(html); + } + else + { + return string.Empty; + } + } + + internal static async Task GetClipboardHtmlContent(DataPackageView clipboardData) => + clipboardData.Contains(StandardDataFormats.Html) ? await clipboardData.GetHtmlFormatAsync() : string.Empty; + internal static async Task GetClipboardImageContentAsync(DataPackageView clipboardData) { using var stream = await GetClipboardImageStreamAsync(clipboardData); @@ -153,7 +192,7 @@ private static async Task GetClipboardImageStreamAsync(Data if (clipboardData.Contains(StandardDataFormats.StorageItems)) { var storageItems = await clipboardData.GetStorageItemsAsync(); - var file = storageItems[0] as StorageFile; + var file = storageItems.Count == 1 ? storageItems[0] as StorageFile : null; if (file != null) { return await file.OpenReadAsync(); @@ -162,8 +201,8 @@ private static async Task GetClipboardImageStreamAsync(Data if (clipboardData.Contains(StandardDataFormats.Bitmap)) { - var imageStreamReference = await clipboardData.GetBitmapAsync(); - return await imageStreamReference.OpenReadAsync(); + var bitmap = await clipboardData.GetBitmapAsync(); + return await bitmap.OpenReadAsync(); } return null; diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Models/ClipboardFormat.cs b/src/modules/AdvancedPaste/AdvancedPaste/Models/ClipboardFormat.cs index 837b2b2b1e7e..a63e79735ed9 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Models/ClipboardFormat.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Models/ClipboardFormat.cs @@ -10,9 +10,9 @@ namespace AdvancedPaste.Models; public enum ClipboardFormat { None, - Text = 1, + Text = 1 << 0, Html = 1 << 1, Audio = 1 << 2, Image = 1 << 3, - File = 1 << 4, + ImageFile = 1 << 4, } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Models/CustomActionActivatedEventArgs.cs b/src/modules/AdvancedPaste/AdvancedPaste/Models/CustomActionActivatedEventArgs.cs index d321cb01f64d..a7e8ba8d6d40 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Models/CustomActionActivatedEventArgs.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Models/CustomActionActivatedEventArgs.cs @@ -6,9 +6,9 @@ namespace AdvancedPaste.Models; -public sealed class CustomActionActivatedEventArgs(string text, bool forcePasteCustom) : EventArgs +public sealed class CustomActionActivatedEventArgs(string text, bool pasteResult) : EventArgs { public string Text { get; private set; } = text; - public bool ForcePasteCustom { get; private set; } = forcePasteCustom; + public bool PasteResult { get; private set; } = pasteResult; } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteActionException.cs b/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteActionException.cs new file mode 100644 index 000000000000..fed4e24c501a --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteActionException.cs @@ -0,0 +1,11 @@ +// 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.Models; + +public sealed class PasteActionException(string message) : Exception(message) +{ +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteActionSource.cs b/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteActionSource.cs new file mode 100644 index 000000000000..bdfabfbcc3b9 --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteActionSource.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. + +namespace AdvancedPaste.Models; + +public enum PasteActionSource +{ + ContextMenu, + InAppKeyboardShortcut, + GlobalKeyboardShortcut, + PromptBox, +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormat.cs b/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormat.cs index b691b141c660..3eb8fa055d94 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormat.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormat.cs @@ -55,5 +55,7 @@ public PasteFormat(AdvancedPasteCustomAction customAction, ClipboardFormat clipb public string ToolTip => string.IsNullOrEmpty(Prompt) ? $"{Name} ({ShortcutText})" : Prompt; + public string Query => string.IsNullOrEmpty(Prompt) ? Name : Prompt; + public string ShortcutText { get; set; } = string.Empty; } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormats.cs b/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormats.cs index 9723ed94bc16..7f8d5ad9ca89 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormats.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormats.cs @@ -20,16 +20,16 @@ public enum PasteFormats [PasteFormatMetadata(IsCoreAction = false, ResourceId = "AudioToText", IconGlyph = "\uF8B1", RequiresAIService = true, SupportedClipboardFormats = ClipboardFormat.Audio, IPCKey = AdvancedPasteAdditionalActions.PropertyNames.AudioToText)] AudioToText, - [PasteFormatMetadata(IsCoreAction = false, ResourceId = "ImageToText", IconGlyph = "\uE91B", RequiresAIService = false, SupportedClipboardFormats = ClipboardFormat.Image | ClipboardFormat.File, IPCKey = AdvancedPasteAdditionalActions.PropertyNames.ImageToText)] + [PasteFormatMetadata(IsCoreAction = false, ResourceId = "ImageToText", IconGlyph = "\uE91B", RequiresAIService = false, SupportedClipboardFormats = ClipboardFormat.Image | ClipboardFormat.ImageFile, IPCKey = AdvancedPasteAdditionalActions.PropertyNames.ImageToText)] ImageToText, [PasteFormatMetadata(IsCoreAction = false, ResourceId = "PasteAsTxtFile", IconGlyph = "\uE8D2", RequiresAIService = false, SupportedClipboardFormats = ClipboardFormat.Text | ClipboardFormat.Html, IPCKey = AdvancedPastePasteAsFileAction.PropertyNames.PasteAsTxtFile)] PasteAsTxtFile, - [PasteFormatMetadata(IsCoreAction = false, ResourceId = "PasteAsPngFile", IconGlyph = "\uE8B9", RequiresAIService = false, SupportedClipboardFormats = ClipboardFormat.Image, IPCKey = AdvancedPastePasteAsFileAction.PropertyNames.PasteAsPngFile)] + [PasteFormatMetadata(IsCoreAction = false, ResourceId = "PasteAsPngFile", IconGlyph = "\uE8B9", RequiresAIService = false, SupportedClipboardFormats = ClipboardFormat.Image | ClipboardFormat.ImageFile, IPCKey = AdvancedPastePasteAsFileAction.PropertyNames.PasteAsPngFile)] PasteAsPngFile, - [PasteFormatMetadata(IsCoreAction = false, ResourceId = "PasteAsHtmlFile", IconGlyph = "\uF6FA", RequiresAIService = false, SupportedClipboardFormats = ClipboardFormat.Text | ClipboardFormat.Html, IPCKey = AdvancedPastePasteAsFileAction.PropertyNames.PasteAsHtmlFile)] + [PasteFormatMetadata(IsCoreAction = false, ResourceId = "PasteAsHtmlFile", IconGlyph = "\uF6FA", RequiresAIService = false, SupportedClipboardFormats = ClipboardFormat.Html, IPCKey = AdvancedPastePasteAsFileAction.PropertyNames.PasteAsHtmlFile)] PasteAsHtmlFile, [PasteFormatMetadata(IsCoreAction = false, IconGlyph = "\uE945", RequiresAIService = true, SupportedClipboardFormats = ClipboardFormat.Text)] diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/IPasteFormatExecutor.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/IPasteFormatExecutor.cs new file mode 100644 index 000000000000..e0bb39ab7c74 --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/IPasteFormatExecutor.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.Threading.Tasks; +using AdvancedPaste.Models; + +namespace AdvancedPaste.Services; + +public interface IPasteFormatExecutor +{ + Task ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source); +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs new file mode 100644 index 000000000000..b92ec607e6a6 --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs @@ -0,0 +1,256 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Globalization; +using System.IO; +using System.Net; +using System.Threading.Tasks; +using AdvancedPaste.Helpers; +using AdvancedPaste.Models; +using ManagedCommon; +using Microsoft.PowerToys.Telemetry; +using Windows.ApplicationModel.DataTransfer; +using Windows.Graphics.Imaging; +using Windows.Storage.Streams; + +namespace AdvancedPaste.Services; + +public sealed class PasteFormatExecutor(AICompletionsHelper aiHelper) : IPasteFormatExecutor +{ + private readonly AICompletionsHelper _aiHelper = aiHelper; + + public async Task ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source) + { + if (!pasteFormat.IsEnabled) + { + return null; + } + + WriteTelemetry(pasteFormat.Format, source); + + return await ExecutePasteFormatCoreAsync(pasteFormat, Clipboard.GetContent()); + } + + private async Task ExecutePasteFormatCoreAsync(PasteFormat pasteFormat, DataPackageView clipboardData) + { + switch (pasteFormat.Format) + { + case PasteFormats.PlainText: + ToPlainText(clipboardData); + return null; + + case PasteFormats.Markdown: + ToMarkdown(clipboardData); + return null; + + case PasteFormats.Json: + ToJson(clipboardData); + return null; + + case PasteFormats.AudioToText: + throw new NotImplementedException(); + + case PasteFormats.ImageToText: + await ImageToTextAsync(clipboardData); + return null; + + case PasteFormats.PasteAsTxtFile: + await ToTxtFileAsync(clipboardData); + return null; + + case PasteFormats.PasteAsPngFile: + await ToPngFileAsync(clipboardData); + return null; + + case PasteFormats.PasteAsHtmlFile: + await ToHtmlFileAsync(clipboardData); + return null; + + case PasteFormats.Custom: + return await ToCustomAsync(pasteFormat.Prompt, clipboardData); + + default: + throw new ArgumentException("Unknown paste format", nameof(pasteFormat)); + } + } + + private static void WriteTelemetry(PasteFormats format, PasteActionSource source) + { + switch (source) + { + case PasteActionSource.ContextMenu: + PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteFormatClickedEvent(format)); + break; + + case PasteActionSource.InAppKeyboardShortcut: + PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteInAppKeyboardShortcutEvent(format)); + break; + + case PasteActionSource.GlobalKeyboardShortcut: + case PasteActionSource.PromptBox: + break; // no telemetry yet for these sources + + default: + throw new ArgumentOutOfRangeException(nameof(format)); + } + } + + private void ToPlainText(DataPackageView clipboardData) + { + Logger.LogTrace(); + SetClipboardTextContent(MarkdownHelper.PasteAsPlainTextFromClipboard(clipboardData)); + } + + private void ToMarkdown(DataPackageView clipboardData) + { + Logger.LogTrace(); + SetClipboardTextContent(MarkdownHelper.ToMarkdown(clipboardData)); + } + + private void ToJson(DataPackageView clipboardData) + { + Logger.LogTrace(); + SetClipboardTextContent(JsonHelper.ToJsonFromXmlOrCsv(clipboardData)); + } + + private async Task ImageToTextAsync(DataPackageView clipboardData) + { + Logger.LogTrace(); + + var bitmap = await ClipboardHelper.GetClipboardImageContentAsync(clipboardData); + var text = await OcrHelpers.GetTextAsync(bitmap); + SetClipboardTextContent(text); + } + + private async Task ToPngFileAsync(DataPackageView clipboardData) + { + Logger.LogTrace(); + + var clipboardBitmap = await ClipboardHelper.GetClipboardImageContentAsync(clipboardData); + + using var pngStream = new InMemoryRandomAccessStream(); + var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, pngStream); + encoder.SetSoftwareBitmap(clipboardBitmap); + await encoder.FlushAsync(); + + await SetClipboardFileContentAsync(pngStream.AsStreamForRead(), "png"); + } + + private async Task ToTxtFileAsync(DataPackageView clipboardData) + { + Logger.LogTrace(); + + var text = await ClipboardHelper.GetClipboardTextOrHtmlText(clipboardData); + await SetClipboardFileContentAsync(text, "txt"); + } + + private async Task ToHtmlFileAsync(DataPackageView clipboardData) + { + Logger.LogTrace(); + + var html = await ClipboardHelper.GetClipboardHtmlContent(clipboardData); + var cleanedHtml = RemoveHtmlMetadata(html); + + await SetClipboardFileContentAsync(cleanedHtml, "html"); + } + + /// + /// Removes leading CF_HTML metadata from HTML clipboard data. + /// See: https://learn.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format + /// + private static string RemoveHtmlMetadata(string htmlFormat) + { + int? GetIntTagValue(string tagName) + { + var tagNameWithColon = tagName + ":"; + int tagStartPos = htmlFormat.IndexOf(tagNameWithColon, StringComparison.InvariantCulture); + + const int tagValueLength = 10; + return tagStartPos != -1 && int.TryParse(htmlFormat.AsSpan(tagStartPos + tagNameWithColon.Length, tagValueLength), CultureInfo.InvariantCulture, out int result) ? result : null; + } + + var startFragmentIndex = GetIntTagValue("StartFragment"); + var endFragmentIndex = GetIntTagValue("EndFragment"); + + return (startFragmentIndex == null || endFragmentIndex == null) ? htmlFormat : htmlFormat[startFragmentIndex.Value..endFragmentIndex.Value]; + } + + private static async Task SetClipboardFileContentAsync(string data, string fileExtension) + { + if (string.IsNullOrEmpty(data)) + { + throw new ArgumentException($"Empty value in {nameof(SetClipboardFileContentAsync)}", nameof(data)); + } + + var path = GetPasteAsFileTempFilePath(fileExtension); + + await File.WriteAllTextAsync(path, data); + await ClipboardHelper.SetClipboardFileContentAsync(path); + } + + private static async Task SetClipboardFileContentAsync(Stream stream, string fileExtension) + { + var path = GetPasteAsFileTempFilePath(fileExtension); + + using var fileStream = File.Create(path); + await stream.CopyToAsync(fileStream); + + await ClipboardHelper.SetClipboardFileContentAsync(path); + } + + private static string GetPasteAsFileTempFilePath(string fileExtension) + { + var prefix = ResourceLoaderInstance.ResourceLoader.GetString("PasteAsFile_FilePrefix"); + var timestamp = DateTime.Now.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture); + + return Path.Combine(Path.GetTempPath(), $"{prefix}{timestamp}.{fileExtension}"); + } + + private async Task ToCustomAsync(string prompt, DataPackageView clipboardData) + { + Logger.LogTrace(); + + if (string.IsNullOrWhiteSpace(prompt)) + { + return string.Empty; + } + + if (!clipboardData.Contains(StandardDataFormats.Text)) + { + Logger.LogWarning("Clipboard does not contain text data"); + return string.Empty; + } + + var currentClipboardText = await clipboardData.GetTextAsync(); + + if (string.IsNullOrWhiteSpace(currentClipboardText)) + { + Logger.LogWarning("Clipboard has no usable text data"); + return string.Empty; + } + + var aiResponse = await Task.Run(() => _aiHelper.AIFormatString(prompt, currentClipboardText)); + + return aiResponse.ApiRequestStatus == (int)HttpStatusCode.OK + ? aiResponse.Response + : throw new PasteActionException(TranslateErrorText(aiResponse.ApiRequestStatus)); + } + + private void SetClipboardTextContent(string content) + { + if (!string.IsNullOrEmpty(content)) + { + ClipboardHelper.SetClipboardTextContent(content); + } + } + + private static string TranslateErrorText(int apiRequestStatus) => (HttpStatusCode)apiRequestStatus switch + { + HttpStatusCode.TooManyRequests => ResourceLoaderInstance.ResourceLoader.GetString("OpenAIApiKeyTooManyRequests"), + HttpStatusCode.Unauthorized => ResourceLoaderInstance.ResourceLoader.GetString("OpenAIApiKeyUnauthorized"), + HttpStatusCode.OK => string.Empty, + _ => ResourceLoaderInstance.ResourceLoader.GetString("OpenAIApiKeyError") + apiRequestStatus.ToString(CultureInfo.InvariantCulture), + }; +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw b/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw index 428d5050c1bf..dbd43fc7f4c7 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw +++ b/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw @@ -123,6 +123,9 @@ Clipboard is empty + + Clipboard data is not text + To custom with AI is not enabled @@ -154,7 +157,7 @@ Privacy - Connecting to AI services and generating output.. + Generating output... Paste as JSON @@ -246,4 +249,7 @@ Ctrl + + PowerToys_Paste_ + \ No newline at end of file diff --git a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs index 67abea2c745e..376ea49d8f89 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs @@ -5,20 +5,17 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Globalization; using System.Linq; -using System.Net; -using System.Threading; using System.Threading.Tasks; using AdvancedPaste.Helpers; using AdvancedPaste.Models; +using AdvancedPaste.Services; using AdvancedPaste.Settings; using Common.UI; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library; -using Microsoft.PowerToys.Telemetry; using Microsoft.UI.Xaml; using Microsoft.Win32; using Windows.ApplicationModel.DataTransfer; @@ -28,58 +25,64 @@ namespace AdvancedPaste.ViewModels { - public partial class OptionsViewModel : ObservableObject, IDisposable + public sealed partial class OptionsViewModel : ObservableObject, IDisposable { private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); private readonly DispatcherTimer _clipboardTimer; private readonly IUserSettings _userSettings; - private readonly AICompletionsHelper aiHelper; + private readonly IPasteFormatExecutor _pasteFormatExecutor; + private readonly AICompletionsHelper _aiHelper; private readonly App app = App.Current as App; public DataPackageView ClipboardData { get; set; } [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsCustomAIEnabled))] [NotifyPropertyChangedFor(nameof(ClipboardHasData))] [NotifyPropertyChangedFor(nameof(InputTxtBoxPlaceholderText))] - [NotifyPropertyChangedFor(nameof(GeneralErrorText))] + [NotifyPropertyChangedFor(nameof(AIDisabledErrorText))] private ClipboardFormat _availableClipboardFormats; [ObservableProperty] private bool _clipboardHistoryEnabled; [ObservableProperty] - [NotifyPropertyChangedFor(nameof(InputTxtBoxPlaceholderText))] - [NotifyPropertyChangedFor(nameof(GeneralErrorText))] + [NotifyPropertyChangedFor(nameof(AIDisabledErrorText))] [NotifyPropertyChangedFor(nameof(IsCustomAIEnabled))] private bool _isAllowedByGPO; [ObservableProperty] - [NotifyPropertyChangedFor(nameof(ApiErrorText))] - private int _apiRequestStatus; + private string _apiErrorText; [ObservableProperty] private string _query = string.Empty; private bool _pasteFormatsDirty; + [ObservableProperty] + private bool _busy; + public ObservableCollection StandardPasteFormats { get; } = []; public ObservableCollection CustomActionPasteFormats { get; } = []; - public bool IsCustomAIEnabled => IsAllowedByGPO && aiHelper.IsAIEnabled; + public bool IsCustomAIEnabled => IsAllowedByGPO && _aiHelper.IsAIEnabled && ClipboardHasText; public bool ClipboardHasData => AvailableClipboardFormats != ClipboardFormat.None; + private bool ClipboardHasText => AvailableClipboardFormats.HasFlag(ClipboardFormat.Text); + + private bool Visible => app?.GetMainWindow()?.Visible is true; + public event EventHandler CustomActionActivated; - public OptionsViewModel(IUserSettings userSettings) + public OptionsViewModel(AICompletionsHelper aiHelper, IUserSettings userSettings, IPasteFormatExecutor pasteFormatExecutor) { - aiHelper = new AICompletionsHelper(); + _aiHelper = aiHelper; _userSettings = userSettings; + _pasteFormatExecutor = pasteFormatExecutor; - ApiRequestStatus = (int)HttpStatusCode.OK; - - GeneratedResponses = new ObservableCollection(); + GeneratedResponses = []; GeneratedResponses.CollectionChanged += (s, e) => { OnPropertyChanged(nameof(HasMultipleResponses)); @@ -87,7 +90,6 @@ public OptionsViewModel(IUserSettings userSettings) }; ClipboardHistoryEnabled = IsClipboardHistoryEnabled(); - ReadClipboard(); _clipboardTimer = new() { Interval = TimeSpan.FromSeconds(1) }; _clipboardTimer.Tick += ClipboardTimer_Tick; _clipboardTimer.Start(); @@ -105,11 +107,11 @@ public OptionsViewModel(IUserSettings userSettings) }; } - private void ClipboardTimer_Tick(object sender, object e) + private async void ClipboardTimer_Tick(object sender, object e) { - if (app.GetMainWindow()?.Visible is true) + if (Visible) { - ReadClipboard(); + await ReadClipboard(); UpdateAllowedByGPO(); } } @@ -179,49 +181,46 @@ public void Dispose() GC.SuppressFinalize(this); } - public void ReadClipboard() + public async Task ReadClipboard() { - ClipboardData = Clipboard.GetContent(); + if (Busy) + { + return; + } - (string DataFormat, ClipboardFormat ClipboardFormat)[] formats = - [ - (StandardDataFormats.Text, ClipboardFormat.Text), - (StandardDataFormats.Html, ClipboardFormat.Html), - (StandardDataFormats.Bitmap, ClipboardFormat.Image), - (StandardDataFormats.StorageItems, ClipboardFormat.File), - ]; - - AvailableClipboardFormats = formats.Aggregate( - ClipboardFormat.None, - (result, formatTuple) => ClipboardData.Contains(formatTuple.DataFormat) ? (result | formatTuple.ClipboardFormat) : result); + ClipboardData = Clipboard.GetContent(); + AvailableClipboardFormats = await ClipboardHelper.GetAvailableClipboardFormats(ClipboardData); } - public void OnShow() + public async Task OnShow() { - ReadClipboard(); + ApiErrorText = string.Empty; + Query = string.Empty; + + await ReadClipboard(); UpdateAllowedByGPO(); if (IsAllowedByGPO) { var openAIKey = AICompletionsHelper.LoadOpenAIKey(); - var currentKey = aiHelper.GetKey(); + var currentKey = _aiHelper.GetKey(); bool keyChanged = openAIKey != currentKey; if (keyChanged) { app.GetMainWindow().StartLoading(); - Task.Run(() => + await Task.Run(() => { - aiHelper.SetOpenAIKey(openAIKey); + _aiHelper.SetOpenAIKey(openAIKey); }).ContinueWith( (t) => { _dispatcherQueue.TryEnqueue(() => { - app.GetMainWindow().FinishLoading(aiHelper.IsAIEnabled); + app.GetMainWindow().FinishLoading(_aiHelper.IsAIEnabled); OnPropertyChanged(nameof(InputTxtBoxPlaceholderText)); - OnPropertyChanged(nameof(GeneralErrorText)); + OnPropertyChanged(nameof(AIDisabledErrorText)); OnPropertyChanged(nameof(IsCustomAIEnabled)); }); }, @@ -234,7 +233,7 @@ public void OnShow() } // List to store generated responses - public ObservableCollection GeneratedResponses { get; set; } = new ObservableCollection(); + public ObservableCollection GeneratedResponses { get; set; } = []; // Index to keep track of the current response private int _currentResponseIndex; @@ -253,30 +252,20 @@ public int CurrentResponseIndex } } - public bool HasMultipleResponses - { - get => GeneratedResponses.Count > 1; - } + public bool HasMultipleResponses => GeneratedResponses.Count > 1; public string CurrentIndexDisplay => $"{CurrentResponseIndex + 1}/{GeneratedResponses.Count}"; public string InputTxtBoxPlaceholderText - { - get - { - app.GetMainWindow().ClearInputText(); + => ResourceLoaderInstance.ResourceLoader.GetString(ClipboardHasData ? "CustomFormatTextBox/PlaceholderText" : "ClipboardEmptyWarning"); - return ClipboardHasData ? ResourceLoaderInstance.ResourceLoader.GetString("CustomFormatTextBox/PlaceholderText") : GeneralErrorText; - } - } - - public string GeneralErrorText + public string AIDisabledErrorText { get { - if (!ClipboardHasData) + if (!ClipboardHasText) { - return ResourceLoaderInstance.ResourceLoader.GetString("ClipboardEmptyWarning"); + return ResourceLoaderInstance.ResourceLoader.GetString("ClipboardDataNotTextWarning"); } if (!IsAllowedByGPO) @@ -284,7 +273,7 @@ public string GeneralErrorText return ResourceLoaderInstance.ResourceLoader.GetString("OpenAIGpoDisabled"); } - if (!aiHelper.IsAIEnabled) + if (!_aiHelper.IsAIEnabled) { return ResourceLoaderInstance.ResourceLoader.GetString("OpenAINotConfigured"); } @@ -295,17 +284,6 @@ public string GeneralErrorText } } - public string ApiErrorText - { - get => (HttpStatusCode)ApiRequestStatus switch - { - HttpStatusCode.TooManyRequests => ResourceLoaderInstance.ResourceLoader.GetString("OpenAIApiKeyTooManyRequests"), - HttpStatusCode.Unauthorized => ResourceLoaderInstance.ResourceLoader.GetString("OpenAIApiKeyUnauthorized"), - HttpStatusCode.OK => string.Empty, - _ => ResourceLoaderInstance.ResourceLoader.GetString("OpenAIApiKeyError") + ApiRequestStatus.ToString(CultureInfo.InvariantCulture), - }; - } - [ObservableProperty] private string _customFormatResult; @@ -314,9 +292,17 @@ public void PasteCustom() { var text = GeneratedResponses.ElementAtOrDefault(CurrentResponseIndex); - if (text != null) + if (!string.IsNullOrEmpty(text)) { - PasteCustomFunction(text); + ClipboardHelper.SetClipboardTextContent(text); + HideWindow(); + + if (_userSettings.SendPasteKeyCombination) + { + ClipboardHelper.SendPasteKeyCombination(); + } + + Query = string.Empty; } } @@ -348,110 +334,76 @@ public void OpenSettings() (App.Current as App).GetMainWindow().Close(); } - private void SetClipboardContentAndHideWindow(string content) + internal async Task ExceutePasteFormatAsync(PasteFormats format, PasteActionSource source) { - if (!string.IsNullOrEmpty(content)) - { - ClipboardHelper.SetClipboardTextContent(content); - } - - if (app.GetMainWindow() != null) - { - Windows.Win32.Foundation.HWND hwnd = (Windows.Win32.Foundation.HWND)app.GetMainWindow().GetWindowHandle(); - Windows.Win32.PInvoke.ShowWindow(hwnd, Windows.Win32.UI.WindowsAndMessaging.SHOW_WINDOW_CMD.SW_HIDE); - } + await ReadClipboard(); + await ExecutePasteFormatAsync(CreatePasteFormat(format), source); } - internal void ToPlainTextFunction() + internal async Task ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source) { - try - { - Logger.LogTrace(); - - string outputString = MarkdownHelper.PasteAsPlainTextFromClipboard(ClipboardData); - - SetClipboardContentAndHideWindow(outputString); - - if (_userSettings.SendPasteKeyCombination) - { - ClipboardHelper.SendPasteKeyCombination(); - } - } - catch + if (Busy) { + Logger.LogWarning($"Execution of {pasteFormat.Name} from {source} suppressed as busy"); + return; } - } - internal void ToMarkdownFunction(bool pasteAlways = false) - { - try + if (!pasteFormat.IsEnabled) { - Logger.LogTrace(); - - string outputString = MarkdownHelper.ToMarkdown(ClipboardData); + return; + } - SetClipboardContentAndHideWindow(outputString); + Busy = true; + ApiErrorText = string.Empty; + Query = pasteFormat.Query; - if (pasteAlways || _userSettings.SendPasteKeyCombination) - { - ClipboardHelper.SendPasteKeyCombination(); - } - } - catch + if (pasteFormat.Format == PasteFormats.Custom) { + SaveQuery(Query); } - } - internal void ToJsonFunction(bool pasteAlways = false) - { try { - Logger.LogTrace(); - - string jsonText = JsonHelper.ToJsonFromXmlOrCsv(ClipboardData); + // Minimum time to show busy spinner for AI actions when triggered by global keyboard shortcut. + var aiActionMinTaskTime = TimeSpan.FromSeconds(2); + var delayTask = (Visible && source == PasteActionSource.GlobalKeyboardShortcut) ? Task.Delay(aiActionMinTaskTime) : Task.CompletedTask; + var aiOutput = await _pasteFormatExecutor.ExecutePasteFormatAsync(pasteFormat, source); - SetClipboardContentAndHideWindow(jsonText); + await delayTask; - if (pasteAlways || _userSettings.SendPasteKeyCombination) + if (pasteFormat.Format != PasteFormats.Custom) { - ClipboardHelper.SendPasteKeyCombination(); - } - } - catch - { - } - } - - internal void ImageToTextFunction() - { - Task.Factory - .StartNew(async () => await ImageToTextFunctionAsync(), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.FromCurrentSynchronizationContext()); - } + HideWindow(); - internal async Task ImageToTextFunctionAsync(bool pasteAlways = false) - { - try - { - Logger.LogTrace(); + if (source == PasteActionSource.GlobalKeyboardShortcut || _userSettings.SendPasteKeyCombination) + { + ClipboardHelper.SendPasteKeyCombination(); + } + } + else + { + var pasteResult = source == PasteActionSource.GlobalKeyboardShortcut || !_userSettings.ShowCustomPreview; - var bitmap = await ClipboardHelper.GetClipboardImageContentAsync(ClipboardData); - var text = await OcrHelpers.GetTextAsync(bitmap); - SetClipboardContentAndHideWindow(text); + GeneratedResponses.Add(aiOutput); + CurrentResponseIndex = GeneratedResponses.Count - 1; + CustomActionActivated?.Invoke(this, new CustomActionActivatedEventArgs(pasteFormat.Prompt, pasteResult)); - if (pasteAlways || _userSettings.SendPasteKeyCombination) - { - ClipboardHelper.SendPasteKeyCombination(); + if (pasteResult) + { + PasteCustom(); + } } } catch (Exception ex) { - Logger.LogError("Unable to extract text from image", ex); - - await app.GetMainWindow().ShowMessageDialogAsync(ResourceLoaderInstance.ResourceLoader.GetString("PasteError")); + Logger.LogError("Error executing paste format", ex); + ApiErrorText = ex is PasteActionException ? ex.Message : ResourceLoaderInstance.ResourceLoader.GetString("PasteError"); } + + Busy = false; } - internal void ExecutePasteFormat(VirtualKey key) + internal async Task ExecutePasteFormat(VirtualKey key) { var pasteFormat = StandardPasteFormats.Concat(CustomActionPasteFormats) .Where(pasteFormat => pasteFormat.IsEnabled) @@ -459,127 +411,38 @@ internal void ExecutePasteFormat(VirtualKey key) if (pasteFormat != null) { - ExecutePasteFormat(pasteFormat); - PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteInAppKeyboardShortcutEvent(pasteFormat.Format)); - } - } - - internal void ExecutePasteFormat(PasteFormat pasteFormat) - { - if (!pasteFormat.IsEnabled) - { - return; - } - - switch (pasteFormat.Format) - { - case PasteFormats.PlainText: - ToPlainTextFunction(); - break; - - case PasteFormats.Markdown: - ToMarkdownFunction(); - break; - - case PasteFormats.Json: - ToJsonFunction(); - break; - - case PasteFormats.AudioToText: - throw new NotImplementedException(); - - case PasteFormats.ImageToText: - ImageToTextFunction(); - break; - - case PasteFormats.PasteAsTxtFile: - throw new NotImplementedException(); - - case PasteFormats.PasteAsPngFile: - throw new NotImplementedException(); - - case PasteFormats.PasteAsHtmlFile: - throw new NotImplementedException(); - - case PasteFormats.Custom: - Query = pasteFormat.Prompt; - CustomActionActivated?.Invoke(this, new CustomActionActivatedEventArgs(pasteFormat.Prompt, false)); - break; + await ExecutePasteFormatAsync(pasteFormat, PasteActionSource.InAppKeyboardShortcut); } } - internal void ExecuteAdditionalAction(PasteFormats format) - { - ExecutePasteFormat(CreatePasteFormat(format)); - } - - internal void ExecuteCustomActionWithPaste(int customActionId) + internal async Task ExecuteCustomAction(int customActionId, PasteActionSource source) { Logger.LogTrace(); + await ReadClipboard(); + var customAction = _userSettings.CustomActions.FirstOrDefault(customAction => customAction.Id == customActionId); if (customAction != null) { - Query = customAction.Prompt; - CustomActionActivated?.Invoke(this, new CustomActionActivatedEventArgs(customAction.Prompt, true)); + await ExecutePasteFormatAsync(CreatePasteFormat(customAction), source); } } - internal async Task GenerateCustomFunction(string inputInstructions) + internal async Task GenerateCustomFunction(PasteActionSource triggerSource) { - Logger.LogTrace(); - - if (string.IsNullOrWhiteSpace(inputInstructions)) - { - return string.Empty; - } - - if (!AvailableClipboardFormats.HasFlag(ClipboardFormat.Text)) - { - Logger.LogWarning("Clipboard does not contain text data"); - return string.Empty; - } - - string currentClipboardText = await Task.Run(async () => - { - try - { - string text = await ClipboardData.GetTextAsync(); - return text; - } - catch (Exception) - { - // Couldn't get text from the clipboard. Resume with empty text. - return string.Empty; - } - }); - - if (string.IsNullOrWhiteSpace(currentClipboardText)) - { - Logger.LogWarning("Clipboard has no usable text data"); - return string.Empty; - } - - var aiResponse = await Task.Run(() => aiHelper.AIFormatString(inputInstructions, currentClipboardText)); - - string aiOutput = aiResponse.Response; - ApiRequestStatus = aiResponse.ApiRequestStatus; - - GeneratedResponses.Add(aiOutput); - CurrentResponseIndex = GeneratedResponses.Count - 1; - return aiOutput; + AdvancedPasteCustomAction customAction = new() { Name = "Default", Prompt = Query }; + await ExecutePasteFormatAsync(CreatePasteFormat(customAction), triggerSource); } - internal void PasteCustomFunction(string text) + private void HideWindow() { - Logger.LogTrace(); + var mainWindow = app.GetMainWindow(); - SetClipboardContentAndHideWindow(text); - - if (_userSettings.SendPasteKeyCombination) + if (mainWindow != null) { - ClipboardHelper.SendPasteKeyCombination(); + Windows.Win32.Foundation.HWND hwnd = (Windows.Win32.Foundation.HWND)mainWindow.GetWindowHandle(); + Windows.Win32.PInvoke.ShowWindow(hwnd, Windows.Win32.UI.WindowsAndMessaging.SHOW_WINDOW_CMD.SW_HIDE); } } @@ -608,13 +471,13 @@ internal void SaveQuery(string inputQuery) ClipboardData = currentClipboardText, }; - SettingsUtils utils = new SettingsUtils(); + SettingsUtils utils = new(); utils.SaveSettings(queryData.ToString(), Constants.AdvancedPasteModuleName, Constants.LastQueryJsonFileName); } internal CustomQuery LoadPreviousQuery() { - SettingsUtils utils = new SettingsUtils(); + SettingsUtils utils = new(); var query = utils.GetSettings(Constants.AdvancedPasteModuleName, Constants.LastQueryJsonFileName); return query; } From a8431528b17738e618a20de7ef984f7ec0c51a59 Mon Sep 17 00:00:00 2001 From: Ani <115020168+drawbyperpetual@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:25:47 +0200 Subject: [PATCH 04/51] Fixed typo --- .../AdvancedPaste/AdvancedPasteXAML/App.xaml.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs index 960b36ae5082..5c63f999fcff 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs @@ -135,11 +135,11 @@ private async Task OnNamedPipeMessage(string message) } else if (messageType == PowerToys.Interop.Constants.AdvancedPasteMarkdownMessage()) { - await viewModel.ExceutePasteFormatAsync(PasteFormats.Markdown, PasteActionSource.GlobalKeyboardShortcut); + await viewModel.ExecutePasteFormatAsync(PasteFormats.Markdown, PasteActionSource.GlobalKeyboardShortcut); } else if (messageType == PowerToys.Interop.Constants.AdvancedPasteJsonMessage()) { - await viewModel.ExceutePasteFormatAsync(PasteFormats.Json, PasteActionSource.GlobalKeyboardShortcut); + await viewModel.ExecutePasteFormatAsync(PasteFormats.Json, PasteActionSource.GlobalKeyboardShortcut); } else if (messageType == PowerToys.Interop.Constants.AdvancedPasteAdditionalActionMessage()) { @@ -171,7 +171,7 @@ private async Task OnAdvancedPasteAdditionalActionHotkey(string[] messageParts) else { await ShowWindow(); - await viewModel.ExceutePasteFormatAsync(pasteFormat, PasteActionSource.GlobalKeyboardShortcut); + await viewModel.ExecutePasteFormatAsync(pasteFormat, PasteActionSource.GlobalKeyboardShortcut); } } } From 35e637591539423892c515a217032478c068225c Mon Sep 17 00:00:00 2001 From: Ani <115020168+drawbyperpetual@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:26:50 +0200 Subject: [PATCH 05/51] Fixed typo --- .../AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs index 376ea49d8f89..ae47e870f68d 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs @@ -334,7 +334,7 @@ public void OpenSettings() (App.Current as App).GetMainWindow().Close(); } - internal async Task ExceutePasteFormatAsync(PasteFormats format, PasteActionSource source) + internal async Task ExecutePasteFormatAsync(PasteFormats format, PasteActionSource source) { await ReadClipboard(); await ExecutePasteFormatAsync(CreatePasteFormat(format), source); From a25fbe35e994a5776876928641ca7a4070105931 Mon Sep 17 00:00:00 2001 From: Ani <115020168+drawbyperpetual@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:30:14 +0200 Subject: [PATCH 06/51] [AdvancedPaste] Improved paste window menu layout --- .../PasteFormatsToHeightConverter.cs | 24 +++++++++++++++++++ .../AdvancedPasteXAML/MainWindow.xaml | 4 ++-- .../AdvancedPasteXAML/MainWindow.xaml.cs | 9 +++++-- .../AdvancedPasteXAML/Pages/MainPage.xaml | 9 ++++--- 4 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Converters/PasteFormatsToHeightConverter.cs diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Converters/PasteFormatsToHeightConverter.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Converters/PasteFormatsToHeightConverter.cs new file mode 100644 index 000000000000..403843737de0 --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Converters/PasteFormatsToHeightConverter.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Data; + +namespace AdvancedPaste.Converters; + +public sealed partial class PasteFormatsToHeightConverter : IValueConverter +{ + private const int ItemHeight = 40; + + public int MaxItems { get; set; } = 5; + + public object Convert(object value, Type targetType, object parameter, string language) => + new GridLength(Convert((value is ICollection collection) ? collection.Count : (value is int intValue) ? intValue : 0)); + + public int Convert(int itemCount) => Math.Min(MaxItems, itemCount) * ItemHeight; + + public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException(); +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/MainWindow.xaml b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/MainWindow.xaml index 8aa111219cb6..33a0dec49ffd 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/MainWindow.xaml +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/MainWindow.xaml @@ -8,9 +8,9 @@ xmlns:pages="using:AdvancedPaste.Pages" xmlns:winuiex="using:WinUIEx" Width="420" - Height="308" + Height="188" MinWidth="420" - MinHeight="308" + MinHeight="188" Closed="WindowEx_Closed" IsAlwaysOnTop="True" IsMaximizable="False" diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/MainWindow.xaml.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/MainWindow.xaml.cs index 949f3b727f7d..691684518d36 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/MainWindow.xaml.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/MainWindow.xaml.cs @@ -4,7 +4,9 @@ using System; using System.Linq; +using AdvancedPaste.Converters; using AdvancedPaste.Helpers; +using AdvancedPaste.Models; using AdvancedPaste.Settings; using ManagedCommon; using Microsoft.UI.Windowing; @@ -23,16 +25,19 @@ public sealed partial class MainWindow : WindowEx, IDisposable public MainWindow() { - this.InitializeComponent(); + InitializeComponent(); _userSettings = App.GetService(); var baseHeight = MinHeight; + var coreActionCount = PasteFormat.MetadataDict.Values.Count(metadata => metadata.IsCoreAction); void UpdateHeight() { double GetHeight(int maxCustomActionCount) => - baseHeight + (40 * (_userSettings.AdditionalActions.Count + Math.Min(_userSettings.CustomActions.Count, maxCustomActionCount))); + baseHeight + + new PasteFormatsToHeightConverter().Convert(coreActionCount + _userSettings.AdditionalActions.Count) + + new PasteFormatsToHeightConverter() { MaxItems = maxCustomActionCount }.Convert(_userSettings.CustomActions.Count); MinHeight = GetHeight(1); Height = GetHeight(5); diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml index b975333b71d0..839c48f8731e 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml @@ -16,8 +16,9 @@ + + + + + + + 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 18295f231567..0f88d58b6d72 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(); } @@ -110,8 +110,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/KernelExtensions.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/KernelExtensions.cs index e967550e4798..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,7 +18,8 @@ internal static class KernelExtensions private const string DataPackageKey = "DataPackage"; private const string LastErrorKey = "LastError"; private const string ActionChainKey = "ActionChain"; - private const string ProgressKey = "IProgress"; + private const string CancellationTokenKey = "CancellationToken"; + private const string ProgressKey = "Progress"; internal static DataPackageView GetDataPackageView(this Kernel kernel) { @@ -41,6 +43,10 @@ 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; 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 index 38cc215b53fb..9c60ca77ef5b 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/TranscodeHelpers.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/TranscodeHelpers.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using ManagedCommon; @@ -17,13 +18,13 @@ namespace AdvancedPaste.Helpers; internal static class TranscodeHelpers { - public static async Task TranscodeToMp3Async(DataPackageView clipboardData, IProgress progress) => - await TranscodeMediaAsync(clipboardData, MediaEncodingProfile.CreateMp3(AudioEncodingQuality.High), ".mp3", progress); + 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, IProgress progress) => - await TranscodeMediaAsync(clipboardData, MediaEncodingProfile.CreateMp4(VideoEncodingQuality.HD1080p), ".mp4", 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, IProgress progress) + private static async Task TranscodeMediaAsync(DataPackageView clipboardData, MediaEncodingProfile baseOutputProfile, string extension, CancellationToken cancellationToken, IProgress progress) { Logger.LogTrace(); @@ -46,12 +47,12 @@ private static async Task TranscodeMediaAsync(DataPackageView clipb Logger.LogDebug($"{nameof(outputProfile)}: {ProfileToString(outputProfile)}"); #endif - var outputFolder = await Task.Run(() => Directory.CreateTempSubdirectory("PowerToys_AdvancedPaste_")); + 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, []); // TranscodeAsync seems to require the output file to exist + await File.WriteAllBytesAsync(outputFilePath, [], cancellationToken); // TranscodeAsync seems to require the output file to exist - await TranscodeMediaAsync(inputFile, await StorageFile.GetFileFromPathAsync(outputFilePath), outputProfile, progress); + await TranscodeMediaAsync(inputFile, await StorageFile.GetFileFromPathAsync(outputFilePath), outputProfile, cancellationToken, progress); return await DataPackageHelpers.CreateFromFileAsync(outputFilePath); } @@ -127,7 +128,7 @@ private static MediaEncodingProfile CreateOutputProfile(MediaEncodingProfile inp return outputProfile; } - private static async Task TranscodeMediaAsync(StorageFile inputFile, StorageFile outputFile, MediaEncodingProfile outputProfile, IProgress progress) + private static async Task TranscodeMediaAsync(StorageFile inputFile, StorageFile outputFile, MediaEncodingProfile outputProfile, CancellationToken cancellationToken, IProgress progress) { if (outputProfile.Video == null && outputProfile.Audio == null) { @@ -141,9 +142,6 @@ private static async Task TranscodeMediaAsync(StorageFile inputFile, StorageFile throw new InvalidOperationException($"Error transcoding; {nameof(prepareOp.FailureReason)}={prepareOp.FailureReason}"); } - var transcodeOp = prepareOp.TranscodeAsync(); - transcodeOp.Progress = (_, args) => progress.Report(args); - - await transcodeOp; + await prepareOp.TranscodeAsync().AsTask(cancellationToken, progress); } } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/TransformHelpers.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/TransformHelpers.cs index ef0c2abaeb62..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,19 +18,19 @@ namespace AdvancedPaste.Helpers; public static class TransformHelpers { - public static async Task TransformAsync(PasteFormats format, DataPackageView clipboardData, IProgress progress) + 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.TranscodeToMp3 => await TranscodeHelpers.TranscodeToMp3Async(clipboardData, progress), - PasteFormats.TranscodeToMp4 => await TranscodeHelpers.TranscodeToMp4Async(clipboardData, progress), + 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)), @@ -54,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(); @@ -74,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); } /// @@ -116,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)) { @@ -125,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/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/Services/ICustomTextTransformService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/ICustomTextTransformService.cs index faa0abe46c50..75f1df259e8c 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Services/ICustomTextTransformService.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/ICustomTextTransformService.cs @@ -3,11 +3,12 @@ // 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, IProgress progress); + 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 e92db9ceee65..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, System.IProgress progress); + 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 09fb64f5c662..3b3237faffee 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Services/IPasteFormatExecutor.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/IPasteFormatExecutor.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.Models; @@ -12,5 +13,5 @@ namespace AdvancedPaste.Services; public interface IPasteFormatExecutor { - Task ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source, IProgress progress); + 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 acf45d4a3a6d..ba3e5eeb0496 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,13 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi protected abstract AIServiceUsage GetAIServiceUsage(ChatMessageContent chatMessage); - public async Task TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery, IProgress progress) + 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() }; @@ -52,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); @@ -85,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; } @@ -128,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 = []; @@ -142,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) @@ -158,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]); @@ -210,14 +214,14 @@ private Task ExecutePromptTransformAsync(Kernel kernel, PasteFormats for async dataPackageView => { var input = await dataPackageView.GetTextAsync(); - string output = await GetPromptBasedOutput(format, prompt, input, kernel.GetProgress()); + 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, IProgress progress) => + private async Task GetPromptBasedOutput(PasteFormats format, string prompt, string input, CancellationToken cancellationToken, IProgress progress) => format switch { - PasteFormats.CustomTextTransformation => await _customTextTransformService.TransformTextAsync(prompt, input, progress), + PasteFormats.CustomTextTransformation => await _customTextTransformService.TransformTextAsync(prompt, input, cancellationToken, progress), _ => throw new ArgumentException($"Unsupported format {format} for prompt transform", nameof(format)), }; @@ -225,7 +229,7 @@ private Task ExecuteStandardTransformAsync(Kernel kernel, PasteFormats f ExecuteTransformAsync( kernel, new ActionChainItem(format, Arguments: []), - async dataPackageView => await TransformHelpers.TransformAsync(format, dataPackageView, kernel.GetProgress())); + 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 e83d28022be9..13f218a8957d 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, IProgress progress) + 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, IP 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, IP 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 a72d1336a138..f247bbe40558 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, IProgress progress) + 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, progress), - PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText(await _customTextTransformService.TransformTextAsync(pasteFormat.Prompt, await clipboardData.GetTextAsync(), progress)), - _ => await TransformHelpers.TransformAsync(format, clipboardData, progress), + 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 ce9a616ec683..3c3d9e931726 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 @@ -204,6 +207,9 @@ Generate and paste data + + Cancel paste operation + Regenerate @@ -213,6 +219,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 3f705f0f6574..91d8e69724c4 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; @@ -37,6 +39,8 @@ public sealed partial class OptionsViewModel : ObservableObject, IProgress _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; @@ -220,12 +237,13 @@ void UpdateFormats(ObservableCollection collection, IEnumerable package.GetView().TryCleanupAfterDelayAsync(TimeSpan.FromSeconds(30))); } @@ -369,7 +390,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; @@ -384,7 +405,8 @@ 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; @@ -392,9 +414,9 @@ internal async Task ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteAction 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, this); + var dataPackage = await _pasteFormatExecutor.ExecutePasteFormatAsync(pasteFormat, source, _pasteActionCancellationTokenSource.Token, this); await delayTask; @@ -418,7 +440,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}"); } @@ -493,7 +517,20 @@ private bool UpdateOpenAIKey() return IsAllowedByGPO && _aiCredentialsProvider.Refresh(); } - public void Report(double value) + public async Task CancelPasteActionAsync() + { + if (_pasteActionCancellationTokenSource != null) + { + await _pasteActionCancellationTokenSource.CancelAsync(); + } + } + + void IProgress.Report(double value) + { + ReportProgress(value); + } + + private void ReportProgress(double value) { _dispatcherQueue.TryEnqueue(() => { From 873028bb5ce081f08b5d120ebc37629c8c2c3b10 Mon Sep 17 00:00:00 2001 From: Ani <115020168+drawbyperpetual@users.noreply.github.com> Date: Thu, 16 Jan 2025 01:48:24 +0100 Subject: [PATCH 49/51] Fixed crash --- .../AdvancedPaste/ViewModels/OptionsViewModel.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs index 91d8e69724c4..0db41186d4a4 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs @@ -212,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)) { From 1d1cc2dc7e6f636f1686af73c8b380bbae0475ce Mon Sep 17 00:00:00 2001 From: Ani <115020168+drawbyperpetual@users.noreply.github.com> Date: Thu, 30 Jan 2025 23:42:50 +0100 Subject: [PATCH 50/51] Minor cleanups --- .../Helpers/DataPackageHelpers.cs | 37 ++++++++----------- .../AdvancedPaste/Models/PasteFormats.cs | 2 +- .../Services/PasteFormatExecutor.cs | 6 +-- .../ViewModels/OptionsViewModel.cs | 6 +-- 4 files changed, 23 insertions(+), 28 deletions(-) diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/DataPackageHelpers.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/DataPackageHelpers.cs index 97af3bbab12a..529773f9a6d5 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/DataPackageHelpers.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/DataPackageHelpers.cs @@ -22,12 +22,6 @@ namespace AdvancedPaste.Helpers; internal static class DataPackageHelpers { - private static readonly Lazy> ImageFileTypes = new(GetImageFileTypes()); - - private static readonly Lazy> AudioFileTypes = new(GetMediaFileTypes("audio")); - - private static readonly Lazy> VideoFileTypes = new(GetMediaFileTypes("video")); - private static readonly (string DataFormat, ClipboardFormat ClipboardFormat)[] DataFormats = [ (StandardDataFormats.Text, ClipboardFormat.Text), @@ -35,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(); @@ -65,19 +67,12 @@ internal static async Task GetAvailableFormatsAsync(this DataPa { availableFormats |= ClipboardFormat.File; - if (ImageFileTypes.Value.Contains(file.FileType)) - { - availableFormats |= ClipboardFormat.Image; - } - - if (AudioFileTypes.Value.Contains(file.FileType)) - { - availableFormats |= ClipboardFormat.Audio; - } - - if (VideoFileTypes.Value.Contains(file.FileType)) + foreach (var (format, fileTypes) in SupportedFileTypes.Value) { - availableFormats |= ClipboardFormat.Video; + if (fileTypes.Contains(file.FileType)) + { + availableFormats |= format; + } } } } @@ -228,7 +223,7 @@ private static HashSet GetImageFileTypes() => private static HashSet GetMediaFileTypes(string mediaKind) { - static string AssocQueryStringValue(NativeMethods.AssocStr assocStr, string extension) + static string AssocQueryString(NativeMethods.AssocStr assocStr, string extension) { uint pcchOut = 0; @@ -242,8 +237,8 @@ static string AssocQueryStringValue(NativeMethods.AssocStr assocStr, string exte var comparison = StringComparison.OrdinalIgnoreCase; var extensions = from extension in Registry.ClassesRoot.GetSubKeyNames() where extension.StartsWith('.') - where AssocQueryStringValue(NativeMethods.AssocStr.PerceivedType, extension).Equals(mediaKind, comparison) || - AssocQueryStringValue(NativeMethods.AssocStr.ContentType, extension).StartsWith($"{mediaKind}/", comparison) + 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/Models/PasteFormats.cs b/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormats.cs index b7903ca00a29..99243ebb5efa 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormats.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Models/PasteFormats.cs @@ -101,7 +101,7 @@ public enum PasteFormats CanPreview = false, SupportedClipboardFormats = ClipboardFormat.Video, IPCKey = AdvancedPasteTranscodeAction.PropertyNames.TranscodeToMp4, - KernelFunctionDescription = "Takes a video file in the clipboard and transcodes it to MP4.")] + KernelFunctionDescription = "Takes a video file in the clipboard and transcodes it to MP4 (H.264/AAC).")] TranscodeToMp4, [PasteFormatMetadata( diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs index f247bbe40558..5d6740977bb5 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs @@ -35,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, cancellationToken, progress), - PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText(await _customTextTransformService.TransformTextAsync(pasteFormat.Prompt, await clipboardData.GetTextAsync(), cancellationToken, progress)), - _ => await TransformHelpers.TransformAsync(format, clipboardData, cancellationToken, progress), + 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/ViewModels/OptionsViewModel.cs b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs index 0db41186d4a4..688c3047e236 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs @@ -103,7 +103,7 @@ private bool Visible } catch (COMException) { - return false; // Window is closed + return false; // window is closed } } } @@ -354,8 +354,8 @@ private async Task CopyPasteAndHideAsync(DataPackage package) await ClipboardHelper.TryCopyPasteAsync(package, HideWindow); Query = string.Empty; - // Delete any temp files created. A the delay is needed to ensure the file is not in use by the target application - - // for example, when pasting into Explorer, the paste operation will trigger a file copy. + // Delete any temp files created. A delay is needed to ensure the file is not in use by the target application - + // for example, when pasting onto File Explorer, the paste operation will trigger a file copy. _ = Task.Run(() => package.GetView().TryCleanupAfterDelayAsync(TimeSpan.FromSeconds(30))); } From 2f63493478111adf446a28aeb056cc74353bfa37 Mon Sep 17 00:00:00 2001 From: Ani <115020168+drawbyperpetual@users.noreply.github.com> Date: Mon, 24 Feb 2025 21:02:24 +0100 Subject: [PATCH 51/51] Improved transcoding error messages --- .../AdvancedPaste/AdvancedPaste/Helpers/TranscodeHelpers.cs | 4 +++- .../AdvancedPaste/Strings/en-us/Resources.resw | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/TranscodeHelpers.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/TranscodeHelpers.cs index 9c60ca77ef5b..f526bb7f64ac 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/TranscodeHelpers.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/TranscodeHelpers.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; +using AdvancedPaste.Models; using ManagedCommon; using Windows.ApplicationModel.DataTransfer; using Windows.Media.MediaProperties; @@ -139,7 +140,8 @@ private static async Task TranscodeMediaAsync(StorageFile inputFile, StorageFile if (!prepareOp.CanTranscode) { - throw new InvalidOperationException($"Error transcoding; {nameof(prepareOp.FailureReason)}={prepareOp.FailureReason}"); + var message = ResourceLoaderInstance.ResourceLoader.GetString(prepareOp.FailureReason == TranscodeFailureReason.CodecNotFound ? "TranscodeErrorUnsupportedCodec" : "TranscodeErrorGeneral"); + throw new PasteActionException(message, new InvalidOperationException($"Error transcoding; {nameof(prepareOp.FailureReason)}={prepareOp.FailureReason}")); } await prepareOp.TranscodeAsync().AsTask(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 b9a984907803..30b46190e329 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw +++ b/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw @@ -198,6 +198,12 @@ Transcode to .mp4 (H.264/AAC) + + An error occurred while transcoding media file + + + The media file contains an unsupported codec + Paste