diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs index a937be3e312..960b36ae508 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 6da59f66c41..74021ebb80b 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 457052d6193..7e57a4eeeb6 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 25c9fc43ee7..39f0a71c467 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 d2612032569..3c2af0be167 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 837b2b2b1e7..a63e79735ed 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 d321cb01f64..a7e8ba8d6d4 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 00000000000..fed4e24c501 --- /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 00000000000..bdfabfbcc3b --- /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 b691b141c66..3eb8fa055d9 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 9723ed94bc1..7f8d5ad9ca8 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 00000000000..e0bb39ab7c7 --- /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 00000000000..b92ec607e6a --- /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 428d5050c1b..dbd43fc7f4c 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 67abea2c745..376ea49d8f8 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; }