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();