Compare commits

...

11 Commits

Author SHA1 Message Date
Craig Loewen
e40f4b0c60 Temp commit? NOt sure if this breaks anything 2024-11-21 22:28:14 -06:00
Craig Loewen
9d8972201f Added initial status info 2024-09-18 15:20:47 -07:00
Ani
fa6ddbca4f [AdvancedPaste] Improved settings window layout 2024-09-18 18:33:46 +02:00
Ani
a25fbe35e9 [AdvancedPaste] Improved paste window menu layout 2024-09-18 17:30:14 +02:00
Ani
35e6375915 Fixed typo 2024-09-18 17:26:50 +02:00
Ani
a8431528b1 Fixed typo 2024-09-18 17:25:47 +02:00
Ani
b9532186bd [AdvancedPaste] Paste as file and many other improvements 2024-09-18 14:16:27 +02:00
Ani
7dc0f7f73b Merge branch 'main' into dev/ani/advanced-paste-additional-actions 2024-09-13 20:58:34 +02:00
Ani
e61460d26e Spellcheck issue 2024-09-11 22:10:27 +02:00
Ani
ec0c300658 Merge branch 'main' into dev/ani/advanced-paste-additional-actions 2024-09-11 21:09:35 +02:00
Ani
e79d86df9b [AdvancedPaste] Additional actions, including Image to text 2024-09-11 21:08:22 +02:00
40 changed files with 1520 additions and 617 deletions

View File

@@ -63,6 +63,10 @@ namespace winrt::PowerToys::Interop::implementation
{ {
return CommonSharedConstants::ADVANCED_PASTE_JSON_MESSAGE; return CommonSharedConstants::ADVANCED_PASTE_JSON_MESSAGE;
} }
hstring Constants::AdvancedPasteAdditionalActionMessage()
{
return CommonSharedConstants::ADVANCED_PASTE_ADDITIONAL_ACTION_MESSAGE;
}
hstring Constants::AdvancedPasteCustomActionMessage() hstring Constants::AdvancedPasteCustomActionMessage()
{ {
return CommonSharedConstants::ADVANCED_PASTE_CUSTOM_ACTION_MESSAGE; return CommonSharedConstants::ADVANCED_PASTE_CUSTOM_ACTION_MESSAGE;

View File

@@ -19,6 +19,7 @@ namespace winrt::PowerToys::Interop::implementation
static hstring AdvancedPasteShowUIMessage(); static hstring AdvancedPasteShowUIMessage();
static hstring AdvancedPasteMarkdownMessage(); static hstring AdvancedPasteMarkdownMessage();
static hstring AdvancedPasteJsonMessage(); static hstring AdvancedPasteJsonMessage();
static hstring AdvancedPasteAdditionalActionMessage();
static hstring AdvancedPasteCustomActionMessage(); static hstring AdvancedPasteCustomActionMessage();
static hstring ShowPowerOCRSharedEvent(); static hstring ShowPowerOCRSharedEvent();
static hstring MouseJumpShowPreviewEvent(); static hstring MouseJumpShowPreviewEvent();

View File

@@ -16,6 +16,7 @@ namespace PowerToys
static String AdvancedPasteShowUIMessage(); static String AdvancedPasteShowUIMessage();
static String AdvancedPasteMarkdownMessage(); static String AdvancedPasteMarkdownMessage();
static String AdvancedPasteJsonMessage(); static String AdvancedPasteJsonMessage();
static String AdvancedPasteAdditionalActionMessage();
static String AdvancedPasteCustomActionMessage(); static String AdvancedPasteCustomActionMessage();
static String ShowPowerOCRSharedEvent(); static String ShowPowerOCRSharedEvent();
static String MouseJumpShowPreviewEvent(); static String MouseJumpShowPreviewEvent();

View File

@@ -32,6 +32,8 @@ namespace CommonSharedConstants
const wchar_t ADVANCED_PASTE_JSON_MESSAGE[] = L"PasteJson"; 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"; const wchar_t ADVANCED_PASTE_CUSTOM_ACTION_MESSAGE[] = L"CustomAction";
// Path to the event used to show Color Picker // Path to the event used to show Color Picker

View File

@@ -3,11 +3,15 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AdvancedPaste.Helpers; using AdvancedPaste.Helpers;
using AdvancedPaste.Models;
using AdvancedPaste.Services;
using AdvancedPaste.Settings; using AdvancedPaste.Settings;
using AdvancedPaste.ViewModels; using AdvancedPaste.ViewModels;
using ManagedCommon; using ManagedCommon;
@@ -31,6 +35,13 @@ namespace AdvancedPaste
{ {
public IHost Host { get; private set; } public IHost Host { get; private set; }
private static readonly Dictionary<string, PasteFormats> AdditionalActionIPCKeys =
typeof(PasteFormats).GetFields()
.Where(field => field.IsLiteral)
.Select(field => (Format: (PasteFormats)field.GetRawConstantValue(), field.GetCustomAttribute<PasteFormatMetadataAttribute>().IPCKey))
.Where(field => field.IPCKey != null)
.ToDictionary(field => field.IPCKey, field => field.Format);
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
private readonly OptionsViewModel viewModel; private readonly OptionsViewModel viewModel;
@@ -51,13 +62,17 @@ namespace AdvancedPaste
Host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder().UseContentRoot(AppContext.BaseDirectory).ConfigureServices((context, services) => Host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder().UseContentRoot(AppContext.BaseDirectory).ConfigureServices((context, services) =>
{ {
services.AddSingleton<OptionsViewModel>();
services.AddSingleton<IUserSettings, UserSettings>(); services.AddSingleton<IUserSettings, UserSettings>();
services.AddSingleton<AICompletionsHelper>();
services.AddSingleton<OptionsViewModel>();
services.AddSingleton<IPasteFormatExecutor, PasteFormatExecutor>();
}).Build(); }).Build();
viewModel = GetService<OptionsViewModel>(); viewModel = GetService<OptionsViewModel>();
UnhandledException += App_UnhandledException; UnhandledException += App_UnhandledException;
var throwAway = ShowWindow();
} }
public MainWindow GetMainWindow() public MainWindow GetMainWindow()
@@ -102,7 +117,7 @@ namespace AdvancedPaste
private void ProcessNamedPipe(string pipeName) 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 () => Task.Run(async () =>
{ {
@@ -111,26 +126,30 @@ namespace AdvancedPaste
}); });
} }
private void OnNamedPipeMessage(string message) private async Task OnNamedPipeMessage(string message)
{ {
var messageParts = message.Split(); var messageParts = message.Split();
var messageType = messageParts.First(); var messageType = messageParts.First();
if (messageType == PowerToys.Interop.Constants.AdvancedPasteShowUIMessage()) if (messageType == PowerToys.Interop.Constants.AdvancedPasteShowUIMessage())
{ {
OnAdvancedPasteHotkey(); await ShowWindow();
} }
else if (messageType == PowerToys.Interop.Constants.AdvancedPasteMarkdownMessage()) else if (messageType == PowerToys.Interop.Constants.AdvancedPasteMarkdownMessage())
{ {
OnAdvancedPasteMarkdownHotkey(); await viewModel.ExecutePasteFormatAsync(PasteFormats.Markdown, PasteActionSource.GlobalKeyboardShortcut);
} }
else if (messageType == PowerToys.Interop.Constants.AdvancedPasteJsonMessage()) else if (messageType == PowerToys.Interop.Constants.AdvancedPasteJsonMessage())
{ {
OnAdvancedPasteJsonHotkey(); await viewModel.ExecutePasteFormatAsync(PasteFormats.Json, PasteActionSource.GlobalKeyboardShortcut);
}
else if (messageType == PowerToys.Interop.Constants.AdvancedPasteAdditionalActionMessage())
{
await OnAdvancedPasteAdditionalActionHotkey(messageParts);
} }
else if (messageType == PowerToys.Interop.Constants.AdvancedPasteCustomActionMessage()) else if (messageType == PowerToys.Interop.Constants.AdvancedPasteCustomActionMessage())
{ {
OnAdvancedPasteCustomActionHotkey(messageParts); await OnAdvancedPasteCustomActionHotkey(messageParts);
} }
} }
@@ -139,24 +158,27 @@ namespace AdvancedPaste
Logger.LogError("Unhandled exception", e.Exception); Logger.LogError("Unhandled exception", e.Exception);
} }
private void OnAdvancedPasteJsonHotkey() private async Task OnAdvancedPasteAdditionalActionHotkey(string[] messageParts)
{ {
viewModel.ReadClipboard(); if (messageParts.Length != 2)
viewModel.ToJsonFunction(true); {
Logger.LogWarning("Unexpected additional action message");
}
else
{
if (!AdditionalActionIPCKeys.TryGetValue(messageParts[1], out PasteFormats pasteFormat))
{
Logger.LogWarning($"Unexpected additional action type {messageParts[1]}");
}
else
{
await ShowWindow();
await viewModel.ExecutePasteFormatAsync(pasteFormat, PasteActionSource.GlobalKeyboardShortcut);
}
}
} }
private void OnAdvancedPasteMarkdownHotkey() private async Task OnAdvancedPasteCustomActionHotkey(string[] messageParts)
{
viewModel.ReadClipboard();
viewModel.ToMarkdownFunction(true);
}
private void OnAdvancedPasteHotkey()
{
ShowWindow();
}
private void OnAdvancedPasteCustomActionHotkey(string[] messageParts)
{ {
if (messageParts.Length != 2) if (messageParts.Length != 2)
{ {
@@ -170,16 +192,15 @@ namespace AdvancedPaste
} }
else else
{ {
ShowWindow(); await ShowWindow();
viewModel.ReadClipboard(); await viewModel.ExecuteCustomAction(customActionId, PasteActionSource.GlobalKeyboardShortcut);
viewModel.ExecuteCustomActionWithPaste(customActionId);
} }
} }
} }
private void ShowWindow() private async Task ShowWindow()
{ {
viewModel.OnShow(); await viewModel.OnShow();
if (window is null) if (window is null)
{ {

View File

@@ -335,7 +335,7 @@
<Grid x:Name="PromptBoxGrid" Loaded="Grid_Loaded"> <Grid x:Name="PromptBoxGrid" Loaded="Grid_Loaded">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="*" /> <RowDefinition Height="*" />
<RowDefinition Height="40" /> <RowDefinition Height="60" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<local:AnimatedContentControl <local:AnimatedContentControl
x:Name="Loader" x:Name="Loader"
@@ -346,7 +346,7 @@
x:Name="InputTxtBox" x:Name="InputTxtBox"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
x:FieldModifier="public" x:FieldModifier="public"
IsEnabled="{x:Bind ViewModel.IsClipboardDataText, Mode=OneWay}" IsEnabled="{x:Bind ViewModel.ClipboardHasData, Mode=OneWay}"
KeyDown="InputTxtBox_KeyDown" KeyDown="InputTxtBox_KeyDown"
PlaceholderText="{x:Bind ViewModel.InputTxtBoxPlaceholderText, Mode=OneWay}" PlaceholderText="{x:Bind ViewModel.InputTxtBoxPlaceholderText, Mode=OneWay}"
Style="{StaticResource CustomTextBoxStyle}" Style="{StaticResource CustomTextBoxStyle}"
@@ -589,7 +589,7 @@
Background="Transparent" Background="Transparent"
Visibility="{x:Bind ViewModel.IsCustomAIEnabled, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}"> Visibility="{x:Bind ViewModel.IsCustomAIEnabled, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
<ToolTipService.ToolTip> <ToolTipService.ToolTip>
<ToolTip Content="{x:Bind ViewModel.GeneralErrorText}" /> <ToolTip Content="{x:Bind ViewModel.AIDisabledErrorText}" />
</ToolTipService.ToolTip> </ToolTipService.ToolTip>
</Grid> </Grid>
</Grid> </Grid>

View File

@@ -2,16 +2,13 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System;
using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using AdvancedPaste.Helpers; using AdvancedPaste.Helpers;
using AdvancedPaste.Settings; using AdvancedPaste.Models;
using AdvancedPaste.ViewModels; using AdvancedPaste.ViewModels;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using ManagedCommon; using ManagedCommon;
using Microsoft.PowerToys.Telemetry; using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
@@ -19,12 +16,6 @@ namespace AdvancedPaste.Controls
{ {
public sealed partial class PromptBox : Microsoft.UI.Xaml.Controls.UserControl 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 OptionsViewModel ViewModel { get; private set; }
public static readonly DependencyProperty PlaceholderTextProperty = DependencyProperty.Register( public static readonly DependencyProperty PlaceholderTextProperty = DependencyProperty.Register(
@@ -53,12 +44,31 @@ namespace AdvancedPaste.Controls
public PromptBox() public PromptBox()
{ {
this.InitializeComponent(); InitializeComponent();
_userSettings = App.GetService<IUserSettings>();
ViewModel = App.GetService<OptionsViewModel>(); ViewModel = App.GetService<OptionsViewModel>();
ViewModel.CustomActionActivated += (_, e) => GenerateCustom(e.ForcePasteCustom); ViewModel.PropertyChanged += ViewModel_PropertyChanged;
ViewModel.CustomActionActivated += ViewModel_CustomActionActivated;
}
private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
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);
}
}
private void ViewModel_CustomActionActivated(object sender, Models.CustomActionActivatedEventArgs e)
{
Logger.LogTrace();
if (!e.PasteResult)
{
PreviewGrid.Width = InputTxtBox.ActualWidth;
PreviewFlyout.ShowAt(InputTxtBox);
}
} }
private void Grid_Loaded(object sender, RoutedEventArgs e) private void Grid_Loaded(object sender, RoutedEventArgs e)
@@ -67,48 +77,7 @@ namespace AdvancedPaste.Controls
} }
[RelayCommand] [RelayCommand]
private void GenerateCustom() => GenerateCustom(false); private async Task GenerateCustom() => await ViewModel.GenerateCustomFunction(PasteActionSource.PromptBox);
private void GenerateCustom(bool forcePasteCustom)
{
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);
}
[RelayCommand] [RelayCommand]
private void Recall() private void Recall()
@@ -126,29 +95,24 @@ namespace AdvancedPaste.Controls
ClipboardHelper.SetClipboardTextContent(lastQuery.ClipboardData); 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) if (e.Key == Windows.System.VirtualKey.Enter && InputTxtBox.Text.Length > 0 && ViewModel.IsCustomAIEnabled)
{ {
GenerateCustom(); await GenerateCustom();
} }
} }
private void PreviewPasteBtn_Click(object sender, RoutedEventArgs e) private void PreviewPasteBtn_Click(object sender, RoutedEventArgs e)
{ {
ViewModel.PasteCustom(); ViewModel.PasteCustom();
InputTxtBox.Text = string.Empty;
} }
private void ThumbUpDown_Click(object sender, RoutedEventArgs e) 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; PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteCustomFormatOutputThumbUpDownEvent(result));
if (bool.TryParse(btn.CommandParameter as string, out result))
{
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteCustomFormatOutputThumbUpDownEvent(result));
}
} }
} }

View File

@@ -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;
using System.Collections;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace AdvancedPaste.Converters;
public sealed partial class InfoBadgeStyleConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
return string.IsNullOrEmpty(value as string) ? Visibility.Collapsed : Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace AdvancedPaste.Converters;
public sealed partial class PasteFormatsToHeightConverter : IValueConverter
{
private const int ItemHeight = 40;
public int MaxItems { get; set; } = 5;
public object Convert(object value, Type targetType, object parameter, string language) =>
new GridLength(Convert((value is ICollection collection) ? collection.Count : (value is int intValue) ? intValue : 0));
public int Convert(int itemCount) => Math.Min(MaxItems, itemCount) * ItemHeight;
public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException();
}

View File

@@ -8,9 +8,9 @@
xmlns:pages="using:AdvancedPaste.Pages" xmlns:pages="using:AdvancedPaste.Pages"
xmlns:winuiex="using:WinUIEx" xmlns:winuiex="using:WinUIEx"
Width="420" Width="420"
Height="308" Height="188"
MinWidth="420" MinWidth="420"
MinHeight="308" MinHeight="188"
Closed="WindowEx_Closed" Closed="WindowEx_Closed"
IsAlwaysOnTop="True" IsAlwaysOnTop="True"
IsMaximizable="False" IsMaximizable="False"

View File

@@ -3,7 +3,10 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Linq;
using AdvancedPaste.Converters;
using AdvancedPaste.Helpers; using AdvancedPaste.Helpers;
using AdvancedPaste.Models;
using AdvancedPaste.Settings; using AdvancedPaste.Settings;
using ManagedCommon; using ManagedCommon;
using Microsoft.UI.Windowing; using Microsoft.UI.Windowing;
@@ -22,21 +25,27 @@ namespace AdvancedPaste
public MainWindow() public MainWindow()
{ {
this.InitializeComponent(); InitializeComponent();
_userSettings = App.GetService<IUserSettings>(); _userSettings = App.GetService<IUserSettings>();
var baseHeight = MinHeight; var baseHeight = MinHeight;
var coreActionCount = PasteFormat.MetadataDict.Values.Count(metadata => metadata.IsCoreAction);
void UpdateHeight() void UpdateHeight()
{ {
var trimmedCustomActionCount = Math.Min(_userSettings.CustomActions.Count, 5); double GetHeight(int maxCustomActionCount) =>
Height = MinHeight = baseHeight + (trimmedCustomActionCount * 40); baseHeight +
new PasteFormatsToHeightConverter().Convert(coreActionCount + _userSettings.AdditionalActions.Count) +
new PasteFormatsToHeightConverter() { MaxItems = maxCustomActionCount }.Convert(_userSettings.CustomActions.Count);
MinHeight = GetHeight(1);
Height = GetHeight(5);
} }
UpdateHeight(); UpdateHeight();
_userSettings.CustomActions.CollectionChanged += (_, _) => UpdateHeight(); _userSettings.Changed += (_, _) => UpdateHeight();
AppWindow.SetIcon("Assets/AdvancedPaste/AdvancedPaste.ico"); AppWindow.SetIcon("Assets/AdvancedPaste/AdvancedPaste.ico");
this.ExtendsContentIntoTitleBar = true; this.ExtendsContentIntoTitleBar = true;

View File

@@ -16,8 +16,10 @@
<Page.Resources> <Page.Resources>
<tkconverters:BoolToVisibilityConverter x:Name="BoolToVisibilityConverter" /> <tkconverters:BoolToVisibilityConverter x:Name="BoolToVisibilityConverter" />
<converters:CountToVisibilityConverter x:Name="countToVisibilityConverter" /> <converters:CountToVisibilityConverter x:Name="countToVisibilityConverter" />
<converters:PasteFormatsToHeightConverter x:Name="standardPasteFormatsToHeightConverter" />
<converters:InfoBadgeStyleConverter x:Name="infoBadgeStyleConverter" />
<converters:CountToDoubleConverter <converters:CountToDoubleConverter
x:Name="customActionsCountToMinHeightConverter" x:Name="customActionsToMinHeightConverter"
ValueIfNonZero="40" ValueIfNonZero="40"
ValueIfZero="0" /> ValueIfZero="0" />
<Style <Style
@@ -29,36 +31,49 @@
</Style.Setters> </Style.Setters>
</Style> </Style>
<DataTemplate x:Key="PasteFormatTemplate" x:DataType="local:PasteFormat"> <DataTemplate x:Key="PasteFormatTemplate" x:DataType="local:PasteFormat">
<Grid> <Button
<ToolTipService.ToolTip> Margin="0"
<TextBlock Text="{x:Bind ToolTip}" /> Padding="5,0,5,0"
</ToolTipService.ToolTip> HorizontalAlignment="Stretch"
<Grid.ColumnDefinitions> VerticalAlignment="Stretch"
<ColumnDefinition Width="26" /> HorizontalContentAlignment="Stretch"
<ColumnDefinition Width="*" /> VerticalContentAlignment="Stretch"
<ColumnDefinition Width="Auto" /> AllowFocusOnInteraction="False"
</Grid.ColumnDefinitions> BorderThickness="0"
<FontIcon Click="ListView_Button_Click"
Margin="0,0,0,0" IsEnabled="{x:Bind IsEnabled, Mode=OneWay}">
VerticalAlignment="Center" <Grid Opacity="{x:Bind Opacity, Mode=OneWay}">
AutomationProperties.AccessibilityView="Raw" <Grid.ColumnDefinitions>
FontSize="16" <ColumnDefinition Width="26" />
Glyph="{x:Bind IconGlyph}" /> <ColumnDefinition Width="*" />
<TextBlock <ColumnDefinition Width="Auto" />
Grid.Column="1" </Grid.ColumnDefinitions>
VerticalAlignment="Center"
x:Phase="1" <ToolTipService.ToolTip>
Text="{x:Bind Name}" /> <TextBlock Text="{x:Bind ToolTip, Mode=OneWay}" />
<TextBlock </ToolTipService.ToolTip>
Grid.Column="2" <FontIcon
Margin="0,0,8,0" Margin="0,0,0,0"
HorizontalAlignment="Right" VerticalAlignment="Center"
VerticalAlignment="Center" AutomationProperties.AccessibilityView="Raw"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" FontSize="16"
Style="{StaticResource CaptionTextBlockStyle}" Glyph="{x:Bind IconGlyph, Mode=OneWay}" />
Text="{x:Bind ShortcutText, Mode=OneWay}" <TextBlock
Visibility="{x:Bind ShortcutText.Length, Mode=OneWay, Converter={StaticResource countToVisibilityConverter}}" /> Grid.Column="1"
</Grid> VerticalAlignment="Center"
x:Phase="1"
Text="{x:Bind Name, Mode=OneWay}" />
<TextBlock
Grid.Column="2"
Margin="0,0,8,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ShortcutText, Mode=OneWay}"
Visibility="{x:Bind ShortcutText.Length, Mode=OneWay, Converter={StaticResource countToVisibilityConverter}}" />
</Grid>
</Button>
</DataTemplate> </DataTemplate>
</Page.Resources> </Page.Resources>
<Page.KeyboardAccelerators> <Page.KeyboardAccelerators>
@@ -109,53 +124,77 @@
<controls:PromptBox <controls:PromptBox
x:Name="CustomFormatTextBox" x:Name="CustomFormatTextBox"
x:Uid="CustomFormatTextBox" x:Uid="CustomFormatTextBox"
Margin="8,4,8,0" Margin="8,4,8,-12"
x:FieldModifier="public" x:FieldModifier="public"
TabIndex="0"> TabIndex="0">
<controls:PromptBox.Footer> <controls:PromptBox.Footer>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Vertical">
<TextBlock <StackPanel Orientation="Horizontal">
Margin="0,0,2,0" <TextBlock
HorizontalAlignment="Left" Margin="0,0,2,0"
VerticalAlignment="Center" HorizontalAlignment="Left"
Style="{StaticResource CaptionTextBlockStyle}"> VerticalAlignment="Center"
<Run x:Uid="AIMistakeNote" Foreground="{ThemeResource TextFillColorSecondaryBrush}" /> Style="{StaticResource CaptionTextBlockStyle}">
</TextBlock> <Run x:Uid="AIMistakeNote" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<TextBlock </TextBlock>
Margin="4,0,2,0" <TextBlock
HorizontalAlignment="Left" Margin="4,0,2,0"
VerticalAlignment="Center" HorizontalAlignment="Left"
Style="{StaticResource CaptionTextBlockStyle}"> VerticalAlignment="Center"
<Hyperlink Style="{StaticResource CaptionTextBlockStyle}">
x:Name="TermsHyperlink" <Hyperlink
NavigateUri="https://openai.com/policies/terms-of-use" x:Name="TermsHyperlink"
TabIndex="3"> NavigateUri="https://openai.com/policies/terms-of-use"
<Run x:Uid="TermsLink" /> TabIndex="3">
</Hyperlink> <Run x:Uid="TermsLink" />
<ToolTipService.ToolTip> </Hyperlink>
<TextBlock Text="https://openai.com/policies/terms-of-use" /> <ToolTipService.ToolTip>
</ToolTipService.ToolTip> <TextBlock Text="https://openai.com/policies/terms-of-use" />
</TextBlock> </ToolTipService.ToolTip>
<TextBlock </TextBlock>
Margin="0,0,2,0" <TextBlock
HorizontalAlignment="Left" Margin="0,0,2,0"
VerticalAlignment="Center" HorizontalAlignment="Left"
Style="{StaticResource CaptionTextBlockStyle}" VerticalAlignment="Center"
ToolTipService.ToolTip=""> Style="{StaticResource CaptionTextBlockStyle}"
<Run x:Uid="AIFooterSeparator" Foreground="{ThemeResource TextFillColorSecondaryBrush}">|</Run> ToolTipService.ToolTip="">
</TextBlock> <Run x:Uid="AIFooterSeparator" Foreground="{ThemeResource TextFillColorSecondaryBrush}">|</Run>
<TextBlock </TextBlock>
Margin="0,0,2,0" <TextBlock
HorizontalAlignment="Left" Margin="0,0,2,0"
VerticalAlignment="Center" HorizontalAlignment="Left"
Style="{StaticResource CaptionTextBlockStyle}"> VerticalAlignment="Center"
<Hyperlink NavigateUri="https://openai.com/policies/privacy-policy" TabIndex="3"> Style="{StaticResource CaptionTextBlockStyle}">
<Run x:Uid="PrivacyLink" /> <Hyperlink NavigateUri="https://openai.com/policies/privacy-policy" TabIndex="3">
</Hyperlink> <Run x:Uid="PrivacyLink" />
<ToolTipService.ToolTip> </Hyperlink>
<TextBlock Text="https://openai.com/policies/privacy-policy" /> <ToolTipService.ToolTip>
</ToolTipService.ToolTip> <TextBlock Text="https://openai.com/policies/privacy-policy" />
</TextBlock> </ToolTipService.ToolTip>
</TextBlock>
</StackPanel>
<StackPanel Margin="-5 5 0 0" Orientation="Horizontal">
<TextBlock Foreground="#CCCCCC" Style="{StaticResource CaptionTextBlockStyle}">Choose a paste action. Clipboard has: </TextBlock>
<ItemsControl ItemsSource="{x:Bind ViewModel.AvailableFormatsText, Mode=OneWay}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="2 0 0 0">
<TextBlock Style="{StaticResource CaptionTextBlockStyle}" Text="{Binding Item1}" ToolTipService.ToolTip="{Binding Item2}" />
<InfoBadge Style="{ThemeResource InformationalDotInfoBadgeStyle}" Width="4" Height="4" Margin="-2 0 0 8">
<InfoBadge.Visibility>
<Binding Path="Item2" Converter="{StaticResource infoBadgeStyleConverter}" />
</InfoBadge.Visibility>
</InfoBadge>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</StackPanel> </StackPanel>
</controls:PromptBox.Footer> </controls:PromptBox.Footer>
</controls:PromptBox> </controls:PromptBox>
@@ -166,9 +205,9 @@
BorderThickness="0,1,0,0" BorderThickness="0,1,0,0"
RowSpacing="4"> RowSpacing="4">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="{x:Bind ViewModel.StandardPasteFormats.Count, Mode=OneWay, Converter={StaticResource standardPasteFormatsToHeightConverter}}" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="*" MinHeight="{x:Bind ViewModel.CustomActionPasteFormats.Count, Mode=OneWay, Converter={StaticResource customActionsToMinHeightConverter}}" />
<RowDefinition Height="*" MinHeight="{x:Bind ViewModel.CustomActionPasteFormats.Count, Mode=OneWay, Converter={StaticResource customActionsCountToMinHeightConverter}}" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
@@ -176,14 +215,23 @@
x:Name="PasteOptionsListView" x:Name="PasteOptionsListView"
Grid.Row="0" Grid.Row="0"
VerticalAlignment="Bottom" VerticalAlignment="Bottom"
IsEnabled="{x:Bind ViewModel.IsClipboardDataText, Mode=OneWay}" IsItemClickEnabled="False"
IsItemClickEnabled="True"
ItemClick="ListView_Click"
ItemContainerTransitions="{x:Null}" ItemContainerTransitions="{x:Null}"
ItemTemplate="{StaticResource PasteFormatTemplate}" ItemTemplate="{StaticResource PasteFormatTemplate}"
ItemsSource="{x:Bind ViewModel.StandardPasteFormats, Mode=OneWay}" ItemsSource="{x:Bind ViewModel.StandardPasteFormats, Mode=OneWay}"
ScrollViewer.VerticalScrollBarVisibility="Visible"
ScrollViewer.VerticalScrollMode="Auto"
SelectionMode="None" SelectionMode="None"
TabIndex="1" /> TabIndex="1">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="0" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Stretch" />
</Style>
</ListView.ItemContainerStyle>
</ListView>
<Rectangle <Rectangle
Grid.Row="1" Grid.Row="1"
@@ -196,16 +244,23 @@
x:Name="CustomActionsListView" x:Name="CustomActionsListView"
Grid.Row="2" Grid.Row="2"
VerticalAlignment="Top" VerticalAlignment="Top"
IsEnabled="{x:Bind ViewModel.IsCustomAIEnabled, Mode=OneWay}" IsItemClickEnabled="False"
IsItemClickEnabled="True"
ItemClick="ListView_Click"
ItemContainerTransitions="{x:Null}" ItemContainerTransitions="{x:Null}"
ItemTemplate="{StaticResource PasteFormatTemplate}" ItemTemplate="{StaticResource PasteFormatTemplate}"
ItemsSource="{x:Bind ViewModel.CustomActionPasteFormats, Mode=OneWay}" ItemsSource="{x:Bind ViewModel.CustomActionPasteFormats, Mode=OneWay}"
ScrollViewer.VerticalScrollBarVisibility="Visible" ScrollViewer.VerticalScrollBarVisibility="Visible"
ScrollViewer.VerticalScrollMode="Auto" ScrollViewer.VerticalScrollMode="Auto"
SelectionMode="None" SelectionMode="None"
TabIndex="2" /> TabIndex="2">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="0" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Stretch" />
</Style>
</ListView.ItemContainerStyle>
</ListView>
<Rectangle <Rectangle
Grid.Row="3" Grid.Row="3"

View File

@@ -129,15 +129,15 @@ namespace AdvancedPaste.Pages
} }
} }
private void ListView_Click(object sender, ItemClickEventArgs e) private async void ListView_Button_Click(object sender, RoutedEventArgs e)
{ {
if (e.ClickedItem is PasteFormat format) 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) if (GetMainWindow()?.Visible is false)
{ {
@@ -170,7 +170,7 @@ namespace AdvancedPaste.Pages
case VirtualKey.Number7: case VirtualKey.Number7:
case VirtualKey.Number8: case VirtualKey.Number8:
case VirtualKey.Number9: case VirtualKey.Number9:
ViewModel.ExecutePasteFormat(sender.Key); await ViewModel.ExecutePasteFormat(sender.Key);
break; break;
default: default:

View File

@@ -3,11 +3,15 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Threading; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using AdvancedPaste.Models;
using ManagedCommon; using ManagedCommon;
using Microsoft.UI.Xaml.Media.Imaging;
using Windows.ApplicationModel.DataTransfer; using Windows.ApplicationModel.DataTransfer;
using Windows.Data.Html;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Streams; using Windows.Storage.Streams;
using Windows.System; using Windows.System;
@@ -15,6 +19,34 @@ namespace AdvancedPaste.Helpers
{ {
internal static class ClipboardHelper internal static class ClipboardHelper
{ {
private static readonly HashSet<string> 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<ClipboardFormat> 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) internal static void SetClipboardTextContent(string text)
{ {
Logger.LogTrace(); Logger.LogTrace();
@@ -25,31 +57,41 @@ namespace AdvancedPaste.Helpers
output.SetText(text); output.SetText(text);
Clipboard.SetContentWithOptions(output, null); Clipboard.SetContentWithOptions(output, null);
// TODO(stefan): For some reason Flush() fails from time to time when directly activated via hotkey. Flush();
// Calling inside a loop makes it work. }
bool flushed = false; }
for (int i = 0; i < 5; i++)
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
{ {
if (flushed) Task.Run(Clipboard.Flush).Wait();
{ return true;
break; }
} catch (Exception ex)
{
try Logger.LogError($"{nameof(Clipboard)}.{nameof(Flush)}() failed", ex);
{
Task.Run(() =>
{
Clipboard.Flush();
}).Wait();
flushed = true;
}
catch (Exception ex)
{
Logger.LogError("Clipboard.Flush() failed", ex);
}
} }
} }
return false;
}
private static async Task<bool> 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) internal static void SetClipboardImageContent(RandomAccessStreamReference image)
@@ -62,30 +104,7 @@ namespace AdvancedPaste.Helpers
output.SetBitmap(image); output.SetBitmap(image);
Clipboard.SetContentWithOptions(output, null); Clipboard.SetContentWithOptions(output, null);
// TODO(stefan): For some reason Flush() fails from time to time when directly activated via hotkey. Flush();
// 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);
}
}
} }
} }
@@ -135,5 +154,63 @@ namespace AdvancedPaste.Helpers
Logger.LogInfo("Paste sent"); Logger.LogInfo("Paste sent");
} }
internal static async Task<string> 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<string> GetClipboardHtmlContent(DataPackageView clipboardData) =>
clipboardData.Contains(StandardDataFormats.Html) ? await clipboardData.GetHtmlFormatAsync() : string.Empty;
internal static async Task<string> GetClipboardTextContent(DataPackageView clipboardData)
{
return clipboardData.Contains(StandardDataFormats.Text) ? await clipboardData.GetTextAsync() : string.Empty;
}
internal static async Task<SoftwareBitmap> 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<IRandomAccessStream> GetClipboardImageStreamAsync(DataPackageView clipboardData)
{
if (clipboardData.Contains(StandardDataFormats.StorageItems))
{
var storageItems = await clipboardData.GetStorageItemsAsync();
var file = storageItems.Count == 1 ? storageItems[0] as StorageFile : null;
if (file != null)
{
return await file.OpenReadAsync();
}
}
if (clipboardData.Contains(StandardDataFormats.Bitmap))
{
var bitmap = await clipboardData.GetBitmapAsync();
return await bitmap.OpenReadAsync();
}
return null;
}
} }
} }

View File

@@ -2,7 +2,9 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // 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; using Microsoft.PowerToys.Settings.UI.Library;
namespace AdvancedPaste.Settings namespace AdvancedPaste.Settings
@@ -15,6 +17,10 @@ namespace AdvancedPaste.Settings
public bool CloseAfterLosingFocus { get; } public bool CloseAfterLosingFocus { get; }
public ObservableCollection<AdvancedPasteCustomAction> CustomActions { get; } public IReadOnlyList<AdvancedPasteCustomAction> CustomActions { get; }
public IReadOnlyList<PasteFormats> AdditionalActions { get; }
public event EventHandler Changed;
} }
} }

View File

@@ -0,0 +1,39 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.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<string> 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();
}
}

View File

@@ -3,10 +3,12 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.ObjectModel; using System.Collections.Generic;
using System.IO.Abstractions; using System.IO.Abstractions;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AdvancedPaste.Models;
using ManagedCommon; using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Utilities; using Microsoft.PowerToys.Settings.UI.Library.Utilities;
@@ -19,6 +21,8 @@ namespace AdvancedPaste.Settings
private readonly TaskScheduler _taskScheduler; private readonly TaskScheduler _taskScheduler;
private readonly IFileSystemWatcher _watcher; private readonly IFileSystemWatcher _watcher;
private readonly object _loadingSettingsLock = new(); private readonly object _loadingSettingsLock = new();
private readonly List<AdvancedPasteCustomAction> _customActions;
private readonly List<PasteFormats> _additionalActions;
private const string AdvancedPasteModuleName = "AdvancedPaste"; private const string AdvancedPasteModuleName = "AdvancedPaste";
private const int MaxNumberOfRetry = 5; private const int MaxNumberOfRetry = 5;
@@ -26,13 +30,17 @@ namespace AdvancedPaste.Settings
private bool _disposedValue; private bool _disposedValue;
private CancellationTokenSource _cancellationTokenSource; private CancellationTokenSource _cancellationTokenSource;
public event EventHandler Changed;
public bool ShowCustomPreview { get; private set; } public bool ShowCustomPreview { get; private set; }
public bool SendPasteKeyCombination { get; private set; } public bool SendPasteKeyCombination { get; private set; }
public bool CloseAfterLosingFocus { get; private set; } public bool CloseAfterLosingFocus { get; private set; }
public ObservableCollection<AdvancedPasteCustomAction> CustomActions { get; private set; } public IReadOnlyList<PasteFormats> AdditionalActions => _additionalActions;
public IReadOnlyList<AdvancedPasteCustomAction> CustomActions => _customActions;
public UserSettings() public UserSettings()
{ {
@@ -41,8 +49,8 @@ namespace AdvancedPaste.Settings
ShowCustomPreview = true; ShowCustomPreview = true;
SendPasteKeyCombination = true; SendPasteKeyCombination = true;
CloseAfterLosingFocus = false; CloseAfterLosingFocus = false;
CustomActions = []; _additionalActions = [];
_customActions = [];
_taskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); _taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
LoadSettingsFromJson(); LoadSettingsFromJson();
@@ -87,18 +95,30 @@ namespace AdvancedPaste.Settings
{ {
void UpdateSettings() void UpdateSettings()
{ {
ShowCustomPreview = settings.Properties.ShowCustomPreview; var properties = settings.Properties;
SendPasteKeyCombination = settings.Properties.SendPasteKeyCombination;
CloseAfterLosingFocus = settings.Properties.CloseAfterLosingFocus;
CustomActions.Clear(); ShowCustomPreview = properties.ShowCustomPreview;
foreach (var customAction in settings.Properties.CustomActions.Value) SendPasteKeyCombination = properties.SendPasteKeyCombination;
{ CloseAfterLosingFocus = properties.CloseAfterLosingFocus;
if (customAction.IsShown && customAction.IsValid)
{ var sourceAdditionalActions = properties.AdditionalActions;
CustomActions.Add(customAction); (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 Task.Factory

View File

@@ -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 << 0,
Html = 1 << 1,
Audio = 1 << 2,
Image = 1 << 3,
ImageFile = 1 << 4,
}

View File

@@ -5,14 +5,13 @@
using Microsoft.UI.Xaml.Media.Imaging; using Microsoft.UI.Xaml.Media.Imaging;
using Windows.ApplicationModel.DataTransfer; 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; }
}
} }

View File

@@ -6,9 +6,9 @@ using System;
namespace AdvancedPaste.Models; 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 string Text { get; private set; } = text;
public bool ForcePasteCustom { get; private set; } = forcePasteCustom; public bool PasteResult { get; private set; } = pasteResult;
} }

View File

@@ -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)
{
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace AdvancedPaste.Models;
public enum PasteActionSource
{
ContextMenu,
InAppKeyboardShortcut,
GlobalKeyboardShortcut,
PromptBox,
}

View File

@@ -2,38 +2,60 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // 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; using Microsoft.PowerToys.Settings.UI.Library;
namespace AdvancedPaste.Models; namespace AdvancedPaste.Models;
public partial class PasteFormat : ObservableObject [DebuggerDisplay("{Name} IsEnabled={IsEnabled} ShortcutText={ShortcutText}")]
public sealed class PasteFormat
{ {
[ObservableProperty] public static readonly IReadOnlyDictionary<PasteFormats, PasteFormatMetadataAttribute> MetadataDict =
private string _shortcutText = string.Empty; typeof(PasteFormats).GetFields()
.Where(field => field.IsLiteral)
.ToDictionary(field => (PasteFormats)field.GetRawConstantValue(), field => field.GetCustomAttribute<PasteFormatMetadataAttribute>());
[ObservableProperty] private PasteFormat(PasteFormats format, ClipboardFormat clipboardFormats, bool isCustomAIEnabled)
private string _toolTip = string.Empty;
public PasteFormat()
{ {
Format = format;
IsEnabled = ((clipboardFormats & Metadata.SupportedClipboardFormats) != ClipboardFormat.None) && (isCustomAIEnabled || !Metadata.RequiresAIService);
} }
public PasteFormat(AdvancedPasteCustomAction customAction, string shortcutText) public PasteFormat(PasteFormats format, ClipboardFormat clipboardFormats, bool isCustomAIEnabled, Func<string, string> resourceLoader)
: this(format, clipboardFormats, isCustomAIEnabled)
{
Name = Metadata.ResourceId == null ? string.Empty : resourceLoader(Metadata.ResourceId);
Prompt = string.Empty;
}
public PasteFormat(AdvancedPasteCustomAction customAction, ClipboardFormat clipboardFormats, bool isCustomAIEnabled)
: this(PasteFormats.Custom, clipboardFormats, isCustomAIEnabled)
{ {
IconGlyph = "\uE945";
Name = customAction.Name; Name = customAction.Name;
Prompt = customAction.Prompt; Prompt = customAction.Prompt;
Format = PasteFormats.Custom;
ShortcutText = shortcutText;
ToolTip = customAction.Prompt;
} }
public string IconGlyph { get; init; } public PasteFormatMetadataAttribute Metadata => MetadataDict[Format];
public string Name { get; init; } public string IconGlyph => Metadata.IconGlyph;
public PasteFormats Format { get; init; } public string Name { get; private init; }
public string Prompt { get; init; } = string.Empty; public PasteFormats Format { get; private init; }
public string Prompt { get; private init; }
public bool IsEnabled { get; private init; }
public double Opacity => IsEnabled ? 1 : 0.5;
public string ToolTip => string.IsNullOrEmpty(Prompt) ? $"{Name} ({ShortcutText})" : Prompt;
public string Query => string.IsNullOrEmpty(Prompt) ? Name : Prompt;
public string ShortcutText { get; set; } = string.Empty;
} }

View File

@@ -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; }
}

View File

@@ -2,13 +2,36 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // 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 [PasteFormatMetadata(IsCoreAction = true, ResourceId = "PasteAsPlainText", IconGlyph = "\uE8E9", RequiresAIService = false, SupportedClipboardFormats = ClipboardFormat.Text)]
{ PlainText,
PlainText,
Markdown, [PasteFormatMetadata(IsCoreAction = true, ResourceId = "PasteAsMarkdown", IconGlyph = "\ue8a5", RequiresAIService = false, SupportedClipboardFormats = ClipboardFormat.Text)]
Json, Markdown,
Custom,
} [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.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 | ClipboardFormat.ImageFile, IPCKey = AdvancedPastePasteAsFileAction.PropertyNames.PasteAsPngFile)]
PasteAsPngFile,
[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)]
Custom,
} }

View File

@@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Threading.Tasks;
using AdvancedPaste.Models;
namespace AdvancedPaste.Services;
public interface IPasteFormatExecutor
{
Task<string> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source);
}

View File

@@ -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<string> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source)
{
if (!pasteFormat.IsEnabled)
{
return null;
}
WriteTelemetry(pasteFormat.Format, source);
return await ExecutePasteFormatCoreAsync(pasteFormat, Clipboard.GetContent());
}
private async Task<string> 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");
}
/// <summary>
/// Removes leading CF_HTML metadata from HTML clipboard data.
/// See: https://learn.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format
/// </summary>
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<string> 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),
};
}

View File

@@ -120,9 +120,12 @@
<data name="AIMistakeNote.Text" xml:space="preserve"> <data name="AIMistakeNote.Text" xml:space="preserve">
<value>AI can make mistakes.</value> <value>AI can make mistakes.</value>
</data> </data>
<data name="ClipboardDataTypeMismatchWarning" xml:space="preserve"> <data name="ClipboardEmptyWarning" xml:space="preserve">
<value>Clipboard data is not text</value> <value>Clipboard is empty</value>
</data> </data>
<data name="ClipboardDataNotTextWarning" xml:space="preserve">
<value>Clipboard data is not text</value>
</data>
<data name="OpenAINotConfigured" xml:space="preserve"> <data name="OpenAINotConfigured" xml:space="preserve">
<value>To custom with AI is not enabled</value> <value>To custom with AI is not enabled</value>
</data> </data>
@@ -135,6 +138,9 @@
<data name="OpenAIApiKeyError" xml:space="preserve"> <data name="OpenAIApiKeyError" xml:space="preserve">
<value>OpenAI request failed with status code: </value> <value>OpenAI request failed with status code: </value>
</data> </data>
<data name="PasteError" xml:space="preserve">
<value>An error occurred during the paste operation</value>
</data>
<data name="ClipboardHistoryButton.Text" xml:space="preserve"> <data name="ClipboardHistoryButton.Text" xml:space="preserve">
<value>Clipboard history</value> <value>Clipboard history</value>
</data> </data>
@@ -151,7 +157,7 @@
<value>Privacy</value> <value>Privacy</value>
</data> </data>
<data name="LoadingText.Text" xml:space="preserve"> <data name="LoadingText.Text" xml:space="preserve">
<value>Connecting to AI services and generating output..</value> <value>Generating output...</value>
</data> </data>
<data name="PasteAsJson" xml:space="preserve"> <data name="PasteAsJson" xml:space="preserve">
<value>Paste as JSON</value> <value>Paste as JSON</value>
@@ -162,6 +168,21 @@
<data name="PasteAsPlainText" xml:space="preserve"> <data name="PasteAsPlainText" xml:space="preserve">
<value>Paste as plain text</value> <value>Paste as plain text</value>
</data> </data>
<data name="AudioToText" xml:space="preserve">
<value>Audio to text</value>
</data>
<data name="ImageToText" xml:space="preserve">
<value>Image to text</value>
</data>
<data name="PasteAsTxtFile" xml:space="preserve">
<value>Paste as .txt file</value>
</data>
<data name="PasteAsPngFile" xml:space="preserve">
<value>Paste as .png file</value>
</data>
<data name="PasteAsHtmlFile" xml:space="preserve">
<value>Paste as .html file</value>
</data>
<data name="PasteButtonAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve"> <data name="PasteButtonAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Paste</value> <value>Paste</value>
</data> </data>
@@ -228,4 +249,7 @@
<data name="CtrlKey" xml:space="preserve"> <data name="CtrlKey" xml:space="preserve">
<value>Ctrl</value> <value>Ctrl</value>
</data> </data>
<data name="PasteAsFile_FilePrefix" xml:space="preserve">
<value>PowerToys_Paste_</value>
</data>
</root> </root>

View File

@@ -3,20 +3,19 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using AdvancedPaste.Helpers; using AdvancedPaste.Helpers;
using AdvancedPaste.Models; using AdvancedPaste.Models;
using AdvancedPaste.Services;
using AdvancedPaste.Settings; using AdvancedPaste.Settings;
using Common.UI; using Common.UI;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using ManagedCommon; using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.Win32; using Microsoft.Win32;
using Windows.ApplicationModel.DataTransfer; using Windows.ApplicationModel.DataTransfer;
@@ -26,64 +25,64 @@ using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
namespace AdvancedPaste.ViewModels namespace AdvancedPaste.ViewModels
{ {
public partial class OptionsViewModel : ObservableObject, IDisposable public sealed partial class OptionsViewModel : ObservableObject, IDisposable
{ {
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
private readonly DispatcherTimer _clipboardTimer; private readonly DispatcherTimer _clipboardTimer;
private readonly IUserSettings _userSettings; private readonly IUserSettings _userSettings;
private readonly AICompletionsHelper aiHelper; private readonly IPasteFormatExecutor _pasteFormatExecutor;
private readonly AICompletionsHelper _aiHelper;
private readonly App app = App.Current as App; private readonly App app = App.Current as App;
private readonly PasteFormat[] _allStandardPasteFormats;
public DataPackageView ClipboardData { get; set; } public DataPackageView ClipboardData { get; set; }
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(InputTxtBoxPlaceholderText))]
[NotifyPropertyChangedFor(nameof(GeneralErrorText))]
[NotifyPropertyChangedFor(nameof(IsCustomAIEnabled))] [NotifyPropertyChangedFor(nameof(IsCustomAIEnabled))]
private bool _isClipboardDataText; [NotifyPropertyChangedFor(nameof(ClipboardHasData))]
[NotifyPropertyChangedFor(nameof(InputTxtBoxPlaceholderText))]
[NotifyPropertyChangedFor(nameof(AIDisabledErrorText))]
private ClipboardFormat _availableClipboardFormats;
[ObservableProperty] [ObservableProperty]
private bool _clipboardHistoryEnabled; private bool _clipboardHistoryEnabled;
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(InputTxtBoxPlaceholderText))] [NotifyPropertyChangedFor(nameof(AIDisabledErrorText))]
[NotifyPropertyChangedFor(nameof(GeneralErrorText))]
[NotifyPropertyChangedFor(nameof(IsCustomAIEnabled))] [NotifyPropertyChangedFor(nameof(IsCustomAIEnabled))]
private bool _isAllowedByGPO; private bool _isAllowedByGPO;
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(ApiErrorText))] private string _apiErrorText;
private int _apiRequestStatus;
[ObservableProperty] [ObservableProperty]
private string _query = string.Empty; private string _query = string.Empty;
private bool _pasteFormatsDirty; private bool _pasteFormatsDirty;
[ObservableProperty]
private bool _busy;
public ObservableCollection<PasteFormat> StandardPasteFormats { get; } = []; public ObservableCollection<PasteFormat> StandardPasteFormats { get; } = [];
public ObservableCollection<PasteFormat> CustomActionPasteFormats { get; } = []; public ObservableCollection<PasteFormat> CustomActionPasteFormats { get; } = [];
public bool IsCustomAIEnabled => IsAllowedByGPO && IsClipboardDataText && 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<CustomActionActivatedEventArgs> CustomActionActivated; public event EventHandler<CustomActionActivatedEventArgs> CustomActionActivated;
public OptionsViewModel(IUserSettings userSettings) public OptionsViewModel(AICompletionsHelper aiHelper, IUserSettings userSettings, IPasteFormatExecutor pasteFormatExecutor)
{ {
aiHelper = new AICompletionsHelper(); _aiHelper = aiHelper;
_userSettings = userSettings; _userSettings = userSettings;
_pasteFormatExecutor = pasteFormatExecutor;
ApiRequestStatus = (int)HttpStatusCode.OK; GeneratedResponses = [];
_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<string>();
GeneratedResponses.CollectionChanged += (s, e) => GeneratedResponses.CollectionChanged += (s, e) =>
{ {
OnPropertyChanged(nameof(HasMultipleResponses)); OnPropertyChanged(nameof(HasMultipleResponses));
@@ -91,27 +90,28 @@ namespace AdvancedPaste.ViewModels
}; };
ClipboardHistoryEnabled = IsClipboardHistoryEnabled(); ClipboardHistoryEnabled = IsClipboardHistoryEnabled();
ReadClipboard();
_clipboardTimer = new() { Interval = TimeSpan.FromSeconds(1) }; _clipboardTimer = new() { Interval = TimeSpan.FromSeconds(1) };
_clipboardTimer.Tick += ClipboardTimer_Tick; _clipboardTimer.Tick += ClipboardTimer_Tick;
_clipboardTimer.Start(); _clipboardTimer.Start();
RefreshPasteFormats(); RefreshPasteFormats();
_userSettings.CustomActions.CollectionChanged += (_, _) => EnqueueRefreshPasteFormats(); _userSettings.Changed += (_, _) => EnqueueRefreshPasteFormats();
PropertyChanged += (_, e) => PropertyChanged += (_, e) =>
{ {
if (e.PropertyName == nameof(Query)) string[] dirtyingProperties = [nameof(Query), nameof(IsCustomAIEnabled), nameof(AvailableClipboardFormats)];
if (dirtyingProperties.Contains(e.PropertyName))
{ {
EnqueueRefreshPasteFormats(); EnqueueRefreshPasteFormats();
} }
}; };
} }
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(); UpdateAllowedByGPO();
} }
} }
@@ -131,10 +131,12 @@ namespace AdvancedPaste.ViewModels
}); });
} }
private PasteFormat CreatePasteFormat(PasteFormats format) => new(format, AvailableClipboardFormats, IsCustomAIEnabled, ResourceLoaderInstance.ResourceLoader.GetString);
private PasteFormat CreatePasteFormat(AdvancedPasteCustomAction customAction) => new(customAction, AvailableClipboardFormats, IsCustomAIEnabled);
private void RefreshPasteFormats() private void RefreshPasteFormats()
{ {
bool Filter(string text) => text.Contains(Query, StringComparison.CurrentCultureIgnoreCase);
var ctrlString = ResourceLoaderInstance.ResourceLoader.GetString("CtrlKey"); var ctrlString = ResourceLoaderInstance.ResourceLoader.GetString("CtrlKey");
int shortcutNum = 0; int shortcutNum = 0;
@@ -144,25 +146,33 @@ namespace AdvancedPaste.ViewModels
return shortcutNum <= 9 ? $"{ctrlString}+{shortcutNum}" : string.Empty; return shortcutNum <= 9 ? $"{ctrlString}+{shortcutNum}" : string.Empty;
} }
StandardPasteFormats.Clear(); IEnumerable<PasteFormat> FilterAndSort(IEnumerable<PasteFormat> pasteFormats) =>
foreach (var format in _allStandardPasteFormats) 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;
void UpdateFormats(ObservableCollection<PasteFormat> collection, IEnumerable<PasteFormat> pasteFormats)
{ {
if (Filter(format.Name)) collection.Clear();
foreach (var format in FilterAndSort(pasteFormats))
{ {
format.ShortcutText = GetNextShortcutText(); if (format.IsEnabled)
format.ToolTip = $"{format.Name} ({format.ShortcutText})"; {
StandardPasteFormats.Add(format); format.ShortcutText = GetNextShortcutText();
}
collection.Add(format);
} }
} }
CustomActionPasteFormats.Clear(); UpdateFormats(StandardPasteFormats, Enum.GetValues<PasteFormats>()
foreach (var customAction in _userSettings.CustomActions) .Where(format => PasteFormat.MetadataDict[format].IsCoreAction || _userSettings.AdditionalActions.Contains(format))
{ .Select(CreatePasteFormat));
if (Filter(customAction.Name) || Filter(customAction.Prompt))
{ UpdateFormats(CustomActionPasteFormats, _userSettings.CustomActions.Select(CreatePasteFormat));
CustomActionPasteFormats.Add(new PasteFormat(customAction, GetNextShortcutText()));
}
}
} }
public void Dispose() public void Dispose()
@@ -171,38 +181,46 @@ namespace AdvancedPaste.ViewModels
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
public void ReadClipboard() public async Task ReadClipboard()
{ {
if (Busy)
{
return;
}
ClipboardData = Clipboard.GetContent(); ClipboardData = Clipboard.GetContent();
IsClipboardDataText = ClipboardData.Contains(StandardDataFormats.Text); AvailableClipboardFormats = await ClipboardHelper.GetAvailableClipboardFormats(ClipboardData);
} }
public void OnShow() public async Task OnShow()
{ {
ReadClipboard(); ApiErrorText = string.Empty;
Query = string.Empty;
await ReadClipboard();
UpdateAllowedByGPO(); UpdateAllowedByGPO();
if (IsAllowedByGPO) if (IsAllowedByGPO)
{ {
var openAIKey = AICompletionsHelper.LoadOpenAIKey(); var openAIKey = AICompletionsHelper.LoadOpenAIKey();
var currentKey = aiHelper.GetKey(); var currentKey = _aiHelper.GetKey();
bool keyChanged = openAIKey != currentKey; bool keyChanged = openAIKey != currentKey;
if (keyChanged) if (keyChanged)
{ {
app.GetMainWindow().StartLoading(); app.GetMainWindow().StartLoading();
Task.Run(() => await Task.Run(() =>
{ {
aiHelper.SetOpenAIKey(openAIKey); _aiHelper.SetOpenAIKey(openAIKey);
}).ContinueWith( }).ContinueWith(
(t) => (t) =>
{ {
_dispatcherQueue.TryEnqueue(() => _dispatcherQueue.TryEnqueue(() =>
{ {
app.GetMainWindow().FinishLoading(aiHelper.IsAIEnabled); app.GetMainWindow().FinishLoading(_aiHelper.IsAIEnabled);
OnPropertyChanged(nameof(InputTxtBoxPlaceholderText)); OnPropertyChanged(nameof(InputTxtBoxPlaceholderText));
OnPropertyChanged(nameof(GeneralErrorText)); OnPropertyChanged(nameof(AIDisabledErrorText));
OnPropertyChanged(nameof(IsCustomAIEnabled)); OnPropertyChanged(nameof(IsCustomAIEnabled));
}); });
}, },
@@ -212,10 +230,65 @@ namespace AdvancedPaste.ViewModels
ClipboardHistoryEnabled = IsClipboardHistoryEnabled(); ClipboardHistoryEnabled = IsClipboardHistoryEnabled();
GeneratedResponses.Clear(); GeneratedResponses.Clear();
await GenerateAvailableFormatsText();
}
public async Task<bool> GenerateAvailableFormatsText()
{
AvailableFormatsText.Clear();
List<Tuple<ClipboardFormat, string>> formatQueryList = new()
{
new Tuple<ClipboardFormat, string>(ClipboardFormat.Text, "Text,"),
new Tuple<ClipboardFormat, string>(ClipboardFormat.Html, "Html,"),
new Tuple<ClipboardFormat, string>(ClipboardFormat.Audio, "Audio,"),
new Tuple<ClipboardFormat, string>(ClipboardFormat.Image, "Image,"),
new Tuple<ClipboardFormat, string>(ClipboardFormat.ImageFile, "ImageFile,"),
};
ObservableCollection<Tuple<string, string>> returnList = new();
foreach (var formatQuery in formatQueryList)
{
if (AvailableClipboardFormats.HasFlag(formatQuery.Item1))
{
string presentedString = formatQuery.Item2;
string tooltipContent = null;
if (formatQuery.Item1 == ClipboardFormat.Text)
{
tooltipContent = await ClipboardHelper.GetClipboardTextContent(ClipboardData);
}
else if (formatQuery.Item1 == ClipboardFormat.Html)
{
tooltipContent = await ClipboardHelper.GetClipboardHtmlContent(ClipboardData);
}
returnList.Add(new Tuple<string, string>(formatQuery.Item2, tooltipContent));
}
}
// Remove comma from last item
if (returnList.Count > 0)
{
Tuple<string, string> lastItem = returnList.Last();
if (!string.IsNullOrEmpty(lastItem.Item1))
{
lastItem = new Tuple<string, string>(lastItem.Item1.Substring(0, lastItem.Item1.Length - 1), lastItem.Item2);
returnList[returnList.Count - 1] = lastItem;
}
}
foreach (var item in returnList)
{
AvailableFormatsText.Add(item);
}
return true;
} }
// List to store generated responses // List to store generated responses
public ObservableCollection<string> GeneratedResponses { get; set; } = new ObservableCollection<string>(); public ObservableCollection<string> GeneratedResponses { get; set; } = [];
// Index to keep track of the current response // Index to keep track of the current response
private int _currentResponseIndex; private int _currentResponseIndex;
@@ -234,30 +307,20 @@ namespace AdvancedPaste.ViewModels
} }
} }
public bool HasMultipleResponses public bool HasMultipleResponses => GeneratedResponses.Count > 1;
{
get => GeneratedResponses.Count > 1;
}
public string CurrentIndexDisplay => $"{CurrentResponseIndex + 1}/{GeneratedResponses.Count}"; public string CurrentIndexDisplay => $"{CurrentResponseIndex + 1}/{GeneratedResponses.Count}";
public string InputTxtBoxPlaceholderText public string InputTxtBoxPlaceholderText
=> ResourceLoaderInstance.ResourceLoader.GetString(ClipboardHasData ? "CustomFormatTextBox/PlaceholderText" : "ClipboardEmptyWarning");
public string AIDisabledErrorText
{ {
get get
{ {
app.GetMainWindow().ClearInputText(); if (!ClipboardHasText)
return IsClipboardDataText ? ResourceLoaderInstance.ResourceLoader.GetString("CustomFormatTextBox/PlaceholderText") : GeneralErrorText;
}
}
public string GeneralErrorText
{
get
{
if (!IsClipboardDataText)
{ {
return ResourceLoaderInstance.ResourceLoader.GetString("ClipboardDataTypeMismatchWarning"); return ResourceLoaderInstance.ResourceLoader.GetString("ClipboardDataNotTextWarning");
} }
if (!IsAllowedByGPO) if (!IsAllowedByGPO)
@@ -265,7 +328,7 @@ namespace AdvancedPaste.ViewModels
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAIGpoDisabled"); return ResourceLoaderInstance.ResourceLoader.GetString("OpenAIGpoDisabled");
} }
if (!aiHelper.IsAIEnabled) if (!_aiHelper.IsAIEnabled)
{ {
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAINotConfigured"); return ResourceLoaderInstance.ResourceLoader.GetString("OpenAINotConfigured");
} }
@@ -276,16 +339,7 @@ namespace AdvancedPaste.ViewModels
} }
} }
public string ApiErrorText public ObservableCollection<Tuple<string, string>> AvailableFormatsText { get; } = [];
{
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] [ObservableProperty]
private string _customFormatResult; private string _customFormatResult;
@@ -295,9 +349,17 @@ namespace AdvancedPaste.ViewModels
{ {
var text = GeneratedResponses.ElementAtOrDefault(CurrentResponseIndex); 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;
} }
} }
@@ -329,187 +391,115 @@ namespace AdvancedPaste.ViewModels
(App.Current as App).GetMainWindow().Close(); (App.Current as App).GetMainWindow().Close();
} }
private void SetClipboardContentAndHideWindow(string content) internal async Task ExecutePasteFormatAsync(PasteFormats format, PasteActionSource source)
{ {
if (!string.IsNullOrEmpty(content)) await ReadClipboard();
{ await ExecutePasteFormatAsync(CreatePasteFormat(format), source);
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);
}
} }
internal void ToPlainTextFunction() internal async Task ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source)
{ {
try if (Busy)
{ {
Logger.LogTrace(); Logger.LogWarning($"Execution of {pasteFormat.Name} from {source} suppressed as busy");
return;
string outputString = MarkdownHelper.PasteAsPlainTextFromClipboard(ClipboardData);
SetClipboardContentAndHideWindow(outputString);
if (_userSettings.SendPasteKeyCombination)
{
ClipboardHelper.SendPasteKeyCombination();
}
} }
catch
{
}
}
internal void ToMarkdownFunction(bool pasteAlways = false) if (!pasteFormat.IsEnabled)
{
try
{
Logger.LogTrace();
string outputString = MarkdownHelper.ToMarkdown(ClipboardData);
SetClipboardContentAndHideWindow(outputString);
if (pasteAlways || _userSettings.SendPasteKeyCombination)
{
ClipboardHelper.SendPasteKeyCombination();
}
}
catch
{
}
}
internal void ToJsonFunction(bool pasteAlways = false)
{
try
{
Logger.LogTrace();
string jsonText = JsonHelper.ToJsonFromXmlOrCsv(ClipboardData);
SetClipboardContentAndHideWindow(jsonText);
if (pasteAlways || _userSettings.SendPasteKeyCombination)
{
ClipboardHelper.SendPasteKeyCombination();
}
}
catch
{
}
}
internal void ExecutePasteFormat(VirtualKey key)
{
var index = key - VirtualKey.Number1;
var pasteFormat = StandardPasteFormats.ElementAtOrDefault(index) ?? CustomActionPasteFormats.ElementAtOrDefault(index - StandardPasteFormats.Count);
if (pasteFormat != null)
{
ExecutePasteFormat(pasteFormat);
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteInAppKeyboardShortcutEvent(pasteFormat.Format));
}
}
internal void ExecutePasteFormat(PasteFormat pasteFormat)
{
if (!IsClipboardDataText || (pasteFormat.Format == PasteFormats.Custom && !IsCustomAIEnabled))
{ {
return; return;
} }
switch (pasteFormat.Format) Busy = true;
ApiErrorText = string.Empty;
Query = pasteFormat.Query;
if (pasteFormat.Format == PasteFormats.Custom)
{ {
case PasteFormats.PlainText: SaveQuery(Query);
ToPlainTextFunction(); }
break;
case PasteFormats.Markdown: try
ToMarkdownFunction(); {
break; // 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);
case PasteFormats.Json: await delayTask;
ToJsonFunction();
break;
case PasteFormats.Custom: if (pasteFormat.Format != PasteFormats.Custom)
Query = pasteFormat.Prompt; {
CustomActionActivated?.Invoke(this, new CustomActionActivatedEventArgs(pasteFormat.Prompt, false)); HideWindow();
break;
if (source == PasteActionSource.GlobalKeyboardShortcut || _userSettings.SendPasteKeyCombination)
{
ClipboardHelper.SendPasteKeyCombination();
}
}
else
{
var pasteResult = source == PasteActionSource.GlobalKeyboardShortcut || !_userSettings.ShowCustomPreview;
GeneratedResponses.Add(aiOutput);
CurrentResponseIndex = GeneratedResponses.Count - 1;
CustomActionActivated?.Invoke(this, new CustomActionActivatedEventArgs(pasteFormat.Prompt, pasteResult));
if (pasteResult)
{
PasteCustom();
}
}
}
catch (Exception ex)
{
Logger.LogError("Error executing paste format", ex);
ApiErrorText = ex is PasteActionException ? ex.Message : ResourceLoaderInstance.ResourceLoader.GetString("PasteError");
}
Busy = false;
}
internal async Task ExecutePasteFormat(VirtualKey key)
{
var pasteFormat = StandardPasteFormats.Concat(CustomActionPasteFormats)
.Where(pasteFormat => pasteFormat.IsEnabled)
.ElementAtOrDefault(key - VirtualKey.Number1);
if (pasteFormat != null)
{
await ExecutePasteFormatAsync(pasteFormat, PasteActionSource.InAppKeyboardShortcut);
} }
} }
internal void ExecuteCustomActionWithPaste(int customActionId) internal async Task ExecuteCustomAction(int customActionId, PasteActionSource source)
{ {
Logger.LogTrace(); Logger.LogTrace();
await ReadClipboard();
var customAction = _userSettings.CustomActions.FirstOrDefault(customAction => customAction.Id == customActionId); var customAction = _userSettings.CustomActions.FirstOrDefault(customAction => customAction.Id == customActionId);
if (customAction != null) if (customAction != null)
{ {
Query = customAction.Prompt; await ExecutePasteFormatAsync(CreatePasteFormat(customAction), source);
CustomActionActivated?.Invoke(this, new CustomActionActivatedEventArgs(customAction.Prompt, true));
} }
} }
internal async Task<string> GenerateCustomFunction(string inputInstructions) internal async Task GenerateCustomFunction(PasteActionSource triggerSource)
{ {
Logger.LogTrace(); AdvancedPasteCustomAction customAction = new() { Name = "Default", Prompt = Query };
await ExecutePasteFormatAsync(CreatePasteFormat(customAction), triggerSource);
if (string.IsNullOrWhiteSpace(inputInstructions))
{
return string.Empty;
}
if (!IsClipboardDataText)
{
Logger.LogWarning("Clipboard does not contain text data");
return string.Empty;
}
string currentClipboardText = await Task.Run(async () =>
{
try
{
string text = await ClipboardData.GetTextAsync() as string;
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;
} }
internal void PasteCustomFunction(string text) private void HideWindow()
{ {
Logger.LogTrace(); var mainWindow = app.GetMainWindow();
SetClipboardContentAndHideWindow(text); if (mainWindow != null)
if (_userSettings.SendPasteKeyCombination)
{ {
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);
} }
} }
@@ -530,11 +520,7 @@ namespace AdvancedPaste.ViewModels
return; return;
} }
string currentClipboardText = Task.Run(async () => var currentClipboardText = Task.Run(async () => await clipboardData.GetTextAsync()).Result;
{
string text = await clipboardData.GetTextAsync() as string;
return text;
}).Result;
var queryData = new CustomQuery var queryData = new CustomQuery
{ {
@@ -542,13 +528,13 @@ namespace AdvancedPaste.ViewModels
ClipboardData = currentClipboardText, ClipboardData = currentClipboardText,
}; };
SettingsUtils utils = new SettingsUtils(); SettingsUtils utils = new();
utils.SaveSettings(queryData.ToString(), Constants.AdvancedPasteModuleName, Constants.LastQueryJsonFileName); utils.SaveSettings(queryData.ToString(), Constants.AdvancedPasteModuleName, Constants.LastQueryJsonFileName);
} }
internal CustomQuery LoadPreviousQuery() internal CustomQuery LoadPreviousQuery()
{ {
SettingsUtils utils = new SettingsUtils(); SettingsUtils utils = new();
var query = utils.GetSettings<CustomQuery>(Constants.AdvancedPasteModuleName, Constants.LastQueryJsonFileName); var query = utils.GetSettings<CustomQuery>(Constants.AdvancedPasteModuleName, Constants.LastQueryJsonFileName);
return query; return query;
} }

View File

@@ -40,6 +40,7 @@ namespace
{ {
const wchar_t JSON_KEY_PROPERTIES[] = L"properties"; const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
const wchar_t JSON_KEY_CUSTOM_ACTIONS[] = L"custom-actions"; 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_SHORTCUT[] = L"shortcut";
const wchar_t JSON_KEY_IS_SHOWN[] = L"isShown"; const wchar_t JSON_KEY_IS_SHOWN[] = L"isShown";
const wchar_t JSON_KEY_ID[] = L"id"; const wchar_t JSON_KEY_ID[] = L"id";
@@ -68,7 +69,6 @@ private:
HANDLE m_hProcess; HANDLE m_hProcess;
std::thread create_pipe_thread;
std::unique_ptr<CAtlFile> m_write_pipe; std::unique_ptr<CAtlFile> m_write_pipe;
// Time to wait for process to close after sending WM_CLOSE signal // Time to wait for process to close after sending WM_CLOSE signal
@@ -81,8 +81,18 @@ private:
Hotkey m_paste_as_markdown_hotkey{}; Hotkey m_paste_as_markdown_hotkey{};
Hotkey m_paste_as_json_hotkey{}; Hotkey m_paste_as_json_hotkey{};
std::vector<Hotkey> m_custom_action_hotkeys; template<class Id>
std::vector<int> m_custom_action_ids; struct ActionData
{
Id id;
Hotkey hotkey;
};
using AdditionalAction = ActionData<std::wstring>;
std::vector<AdditionalAction> m_additional_actions;
using CustomAction = ActionData<int>;
std::vector<CustomAction> m_custom_actions;
bool m_preview_custom_format_output = true; bool m_preview_custom_format_output = true;
@@ -164,6 +174,39 @@ private:
return false; 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) void parse_hotkeys(PowerToysSettings::PowerToyValues& settings)
{ {
auto settingsObject = settings.get_raw_json(); auto settingsObject = settings.get_raw_json();
@@ -206,13 +249,23 @@ private:
*hotkey = parse_single_hotkey(keyName, settingsObject); *hotkey = parse_single_hotkey(keyName, settingsObject);
} }
m_custom_action_hotkeys.clear(); m_additional_actions.clear();
m_custom_action_ids.clear(); m_custom_actions.clear();
if (settingsObject.HasKey(JSON_KEY_PROPERTIES)) if (settingsObject.HasKey(JSON_KEY_PROPERTIES))
{ {
const auto propertiesObject = settingsObject.GetNamedObject(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)) if (propertiesObject.HasKey(JSON_KEY_CUSTOM_ACTIONS))
{ {
const auto customActions = propertiesObject.GetNamedObject(JSON_KEY_CUSTOM_ACTIONS).GetNamedArray(JSON_KEY_VALUE); const auto customActions = propertiesObject.GetNamedObject(JSON_KEY_CUSTOM_ACTIONS).GetNamedArray(JSON_KEY_VALUE);
@@ -223,8 +276,13 @@ private:
if (object.GetNamedBoolean(JSON_KEY_IS_SHOWN, false)) if (object.GetNamedBoolean(JSON_KEY_IS_SHOWN, false))
{ {
m_custom_action_hotkeys.push_back(parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT))); const CustomAction customActionData
m_custom_action_ids.push_back(static_cast<int>(object.GetNamedNumber(JSON_KEY_ID))); {
static_cast<int>(object.GetNamedNumber(JSON_KEY_ID)),
parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT))
};
m_custom_actions.push_back(customActionData);
} }
} }
} }
@@ -296,7 +354,7 @@ private:
return; 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()); launch_process(pipe_name.value());
create_pipe_thread.join(); create_pipe_thread.join();
} }
@@ -789,11 +847,22 @@ public:
return true; 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_custom_action_ids.at(custom_action_index); 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_actions.at(custom_action_index).id;
Logger::trace(L"Starting custom action id={}", id); Logger::trace(L"Starting custom action id={}", id);
@@ -807,7 +876,7 @@ public:
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override 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) if (hotkeys && buffer_size >= num_hotkeys)
{ {
@@ -815,9 +884,11 @@ public:
m_advanced_paste_ui_hotkey, m_advanced_paste_ui_hotkey,
m_paste_as_markdown_hotkey, m_paste_as_markdown_hotkey,
m_paste_as_json_hotkey }; m_paste_as_json_hotkey };
std::copy(default_hotkeys.begin(), default_hotkeys.end(), hotkeys); 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; return num_hotkeys;

View File

@@ -0,0 +1,39 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.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);
}
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.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<IAdvancedPasteAction> AllActions => new IAdvancedPasteAction[] { AudioToText, ImageToText, PasteAsFile }.Concat(PasteAsFile.SubActions);
}

View File

@@ -3,14 +3,13 @@
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System; using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
namespace Microsoft.PowerToys.Settings.UI.Library; namespace Microsoft.PowerToys.Settings.UI.Library;
public sealed class AdvancedPasteCustomAction : INotifyPropertyChanged, ICloneable public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction, ICloneable
{ {
private int _id; private int _id;
private string _name = string.Empty; private string _name = string.Empty;
@@ -25,14 +24,7 @@ public sealed class AdvancedPasteCustomAction : INotifyPropertyChanged, ICloneab
public int Id public int Id
{ {
get => _id; get => _id;
set set => Set(ref _id, value);
{
if (_id != value)
{
_id = value;
OnPropertyChanged();
}
}
} }
[JsonPropertyName("name")] [JsonPropertyName("name")]
@@ -41,10 +33,8 @@ public sealed class AdvancedPasteCustomAction : INotifyPropertyChanged, ICloneab
get => _name; get => _name;
set set
{ {
if (_name != value) if (Set(ref _name, value))
{ {
_name = value;
OnPropertyChanged();
UpdateIsValid(); UpdateIsValid();
} }
} }
@@ -56,10 +46,8 @@ public sealed class AdvancedPasteCustomAction : INotifyPropertyChanged, ICloneab
get => _prompt; get => _prompt;
set set
{ {
if (_prompt != value) if (Set(ref _prompt, value))
{ {
_prompt = value;
OnPropertyChanged();
UpdateIsValid(); UpdateIsValid();
} }
} }
@@ -86,62 +74,30 @@ public sealed class AdvancedPasteCustomAction : INotifyPropertyChanged, ICloneab
public bool IsShown public bool IsShown
{ {
get => _isShown; get => _isShown;
set set => Set(ref _isShown, value);
{
if (_isShown != value)
{
_isShown = value;
OnPropertyChanged();
}
}
} }
[JsonIgnore] [JsonIgnore]
public bool CanMoveUp public bool CanMoveUp
{ {
get => _canMoveUp; get => _canMoveUp;
set set => Set(ref _canMoveUp, value);
{
if (_canMoveUp != value)
{
_canMoveUp = value;
OnPropertyChanged();
}
}
} }
[JsonIgnore] [JsonIgnore]
public bool CanMoveDown public bool CanMoveDown
{ {
get => _canMoveDown; get => _canMoveDown;
set set => Set(ref _canMoveDown, value);
{
if (_canMoveDown != value)
{
_canMoveDown = value;
OnPropertyChanged();
}
}
} }
[JsonIgnore] [JsonIgnore]
public bool IsValid public bool IsValid
{ {
get => _isValid; get => _isValid;
private set private set => Set(ref _isValid, value);
{
if (_isValid != value)
{
_isValid = value;
OnPropertyChanged();
}
}
} }
public event PropertyChangedEventHandler PropertyChanged;
public string ToJsonString() => JsonSerializer.Serialize(this);
public object Clone() public object Clone()
{ {
AdvancedPasteCustomAction clone = new(); AdvancedPasteCustomAction clone = new();
@@ -160,11 +116,6 @@ public sealed class AdvancedPasteCustomAction : INotifyPropertyChanged, ICloneab
CanMoveDown = other.CanMoveDown; CanMoveDown = other.CanMoveDown;
} }
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private HotkeySettings GetShortcutClone() private HotkeySettings GetShortcutClone()
{ {
object shortcut = null; object shortcut = null;

View File

@@ -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<AdvancedPasteAdditionalAction> SubActions => [PasteAsTxtFile, PasteAsPngFile, PasteAsHtmlFile];
}

View File

@@ -21,6 +21,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
PasteAsMarkdownShortcut = new(); PasteAsMarkdownShortcut = new();
PasteAsJsonShortcut = new(); PasteAsJsonShortcut = new();
CustomActions = new(); CustomActions = new();
AdditionalActions = new();
ShowCustomPreview = true; ShowCustomPreview = true;
SendPasteKeyCombination = true; SendPasteKeyCombination = true;
CloseAfterLosingFocus = false; CloseAfterLosingFocus = false;
@@ -50,7 +51,11 @@ namespace Microsoft.PowerToys.Settings.UI.Library
[JsonPropertyName("custom-actions")] [JsonPropertyName("custom-actions")]
[CmdConfigureIgnoreAttribute] [CmdConfigureIgnoreAttribute]
public AdvancedPasteCustomActions CustomActions { get; set; } public AdvancedPasteCustomActions CustomActions { get; init; }
[JsonPropertyName("additional-actions")]
[CmdConfigureIgnoreAttribute]
public AdvancedPasteAdditionalActions AdditionalActions { get; init; }
public override string ToString() public override string ToString()
=> JsonSerializer.Serialize(this); => JsonSerializer.Serialize(this);

View File

@@ -11,17 +11,19 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Helpers
{ {
public event PropertyChangedEventHandler PropertyChanged; public event PropertyChangedEventHandler PropertyChanged;
protected void Set<T>(ref T storage, T value, [CallerMemberName] string propertyName = null) protected bool Set<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{ {
if (Equals(storage, value)) if (Equals(storage, value))
{ {
return; return false;
} }
storage = value; storage = value;
OnPropertyChanged(propertyName); 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));
} }
} }

View File

@@ -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; }
}

View File

@@ -24,6 +24,18 @@
<ImageSource x:Key="DialogHeaderImage">ms-appx:///Assets/Settings/Modules/APDialog.light.png</ImageSource> <ImageSource x:Key="DialogHeaderImage">ms-appx:///Assets/Settings/Modules/APDialog.light.png</ImageSource>
</ResourceDictionary> </ResourceDictionary>
</ResourceDictionary.ThemeDictionaries> </ResourceDictionary.ThemeDictionaries>
<DataTemplate x:Key="AdditionalActionTemplate" x:DataType="models:AdvancedPasteAdditionalAction">
<StackPanel Orientation="Horizontal" Spacing="4">
<controls:ShortcutControl
MinWidth="{StaticResource SettingActionControlMinWidth}"
AllowDisable="True"
HotkeySettings="{x:Bind Shortcut, Mode=TwoWay}" />
<ToggleSwitch
IsOn="{x:Bind IsShown, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
OffContent=""
OnContent="" />
</StackPanel>
</DataTemplate>
</ResourceDictionary> </ResourceDictionary>
</Page.Resources> </Page.Resources>
<Grid> <Grid>
@@ -126,6 +138,7 @@
AllowDisable="True" AllowDisable="True"
HotkeySettings="{x:Bind Path=ViewModel.PasteAsJsonShortcut, Mode=TwoWay}" /> HotkeySettings="{x:Bind Path=ViewModel.PasteAsJsonShortcut, Mode=TwoWay}" />
</tkcontrols:SettingsCard> </tkcontrols:SettingsCard>
<ItemsControl <ItemsControl
x:Name="CustomActions" x:Name="CustomActions"
x:Uid="CustomActions" x:Uid="CustomActions"
@@ -198,6 +211,51 @@
IsTabStop="{x:Bind ViewModel.IsConflictingCopyShortcut, Mode=OneWay}" IsTabStop="{x:Bind ViewModel.IsConflictingCopyShortcut, Mode=OneWay}"
Severity="Warning" /> Severity="Warning" />
</controls:SettingsGroup> </controls:SettingsGroup>
<controls:SettingsGroup x:Uid="AdvancedPaste_Additional_Actions_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
<tkcontrols:SettingsCard x:Uid="AudioToText" DataContext="{x:Bind ViewModel.AdditionalActions.AudioToText, Mode=OneWay}">
<ContentControl ContentTemplate="{StaticResource AdditionalActionTemplate}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="ImageToText" DataContext="{x:Bind ViewModel.AdditionalActions.ImageToText, Mode=OneWay}">
<ContentControl ContentTemplate="{StaticResource AdditionalActionTemplate}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsExpander
x:Uid="PasteAsFile"
DataContext="{x:Bind ViewModel.AdditionalActions.PasteAsFile}"
HeaderIcon="{ui:FontIcon Glyph=&#xEC50;}"
IsExpanded="{Binding IsShown, Mode=OneWay}">
<tkcontrols:SettingsExpander.Content>
<ToggleSwitch
IsOn="{Binding IsShown, Mode=TwoWay}"
OffContent=""
OnContent="" />
</tkcontrols:SettingsExpander.Content>
<tkcontrols:SettingsExpander.Items>
<!-- HACK: For some weird reason, a ShortcutControl does not work correctly if it's the first or last item in the expander, so we add an invisible card. -->
<tkcontrols:SettingsCard Visibility="Collapsed" />
<tkcontrols:SettingsCard x:Uid="PasteAsTxtFile" DataContext="{Binding PasteAsTxtFile, Mode=TwoWay}">
<ContentControl ContentTemplate="{StaticResource AdditionalActionTemplate}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PasteAsPngFile" DataContext="{Binding PasteAsPngFile, Mode=TwoWay}">
<ContentControl ContentTemplate="{StaticResource AdditionalActionTemplate}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard x:Uid="PasteAsHtmlFile" DataContext="{Binding PasteAsHtmlFile, Mode=TwoWay}">
<ContentControl ContentTemplate="{StaticResource AdditionalActionTemplate}" />
</tkcontrols:SettingsCard>
<!-- HACK: For some weird reason, a ShortcutControl does not work correctly if it's the first or last item in the expander, so we add an invisible card. -->
<tkcontrols:SettingsCard Visibility="Collapsed" />
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
<InfoBar
x:Uid="AdvancedPaste_ShortcutWarning"
IsClosable="False"
IsOpen="{x:Bind ViewModel.IsAdditionalActionConflictingCopyShortcut, Mode=OneWay}"
IsTabStop="{x:Bind ViewModel.IsAdditionalActionConflictingCopyShortcut, Mode=OneWay}"
Severity="Warning" />
</controls:SettingsGroup>
</StackPanel> </StackPanel>
</controls:SettingsPageControl.ModuleContent> </controls:SettingsPageControl.ModuleContent>
<controls:SettingsPageControl.PrimaryLinks> <controls:SettingsPageControl.PrimaryLinks>

View File

@@ -722,6 +722,9 @@
<data name="AdvancedPaste_Direct_Access_Hotkeys_GroupSettings.Header" xml:space="preserve"> <data name="AdvancedPaste_Direct_Access_Hotkeys_GroupSettings.Header" xml:space="preserve">
<value>Actions</value> <value>Actions</value>
</data> </data>
<data name="AdvancedPaste_Additional_Actions_GroupSettings.Header" xml:space="preserve">
<value>Additional actions</value>
</data>
<data name="RemapKeysList.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve"> <data name="RemapKeysList.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Current Key Remappings</value> <value>Current Key Remappings</value>
</data> </data>
@@ -2042,6 +2045,24 @@ Made with 💗 by Microsoft and the PowerToys community.</value>
<data name="PasteAsCustom_Shortcut.Header" xml:space="preserve"> <data name="PasteAsCustom_Shortcut.Header" xml:space="preserve">
<value>Paste as Custom with AI directly</value> <value>Paste as Custom with AI directly</value>
</data> </data>
<data name="AudioToText.Header" xml:space="preserve">
<value>Audio to text</value>
</data>
<data name="ImageToText.Header" xml:space="preserve">
<value>Image to text</value>
</data>
<data name="PasteAsFile.Header" xml:space="preserve">
<value>Paste as file</value>
</data>
<data name="PasteAsTxtFile.Header" xml:space="preserve">
<value>Paste as .txt file</value>
</data>
<data name="PasteAsPngFile.Header" xml:space="preserve">
<value>Paste as .png file</value>
</data>
<data name="PasteAsHtmlFile.Header" xml:space="preserve">
<value>Paste as .html file</value>
</data>
<data name="AdvancedPaste_EnableAIDialogOpenAIApiKey.Text" xml:space="preserve"> <data name="AdvancedPaste_EnableAIDialogOpenAIApiKey.Text" xml:space="preserve">
<value>OpenAI API key:</value> <value>OpenAI API key:</value>
</data> </data>

View File

@@ -38,6 +38,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private readonly AdvancedPasteSettings _advancedPasteSettings; private readonly AdvancedPasteSettings _advancedPasteSettings;
private readonly ObservableCollection<AdvancedPasteCustomAction> _customActions; private readonly ObservableCollection<AdvancedPasteCustomAction> _customActions;
private readonly AdvancedPasteAdditionalActions _additionalActions;
private Timer _delayedTimer; private Timer _delayedTimer;
private GpoRuleConfigured _enabledGpoRuleConfiguration; private GpoRuleConfigured _enabledGpoRuleConfiguration;
@@ -69,6 +70,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
_advancedPasteSettings = advancedPasteSettingsRepository.SettingsConfig; _advancedPasteSettings = advancedPasteSettingsRepository.SettingsConfig;
_customActions = _advancedPasteSettings.Properties.CustomActions.Value; _customActions = _advancedPasteSettings.Properties.CustomActions.Value;
_additionalActions = _advancedPasteSettings.Properties.AdditionalActions;
InitializeEnabledValue(); InitializeEnabledValue();
@@ -87,6 +89,11 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
_customActions.CollectionChanged += OnCustomActionsCollectionChanged; _customActions.CollectionChanged += OnCustomActionsCollectionChanged;
UpdateCustomActionsCanMoveUpDown(); UpdateCustomActionsCanMoveUpDown();
foreach (var action in _additionalActions.AllActions)
{
action.PropertyChanged += OnAdditionalActionPropertyChanged;
}
} }
private void InitializeEnabledValue() private void InitializeEnabledValue()
@@ -142,6 +149,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public ObservableCollection<AdvancedPasteCustomAction> CustomActions => _customActions; public ObservableCollection<AdvancedPasteCustomAction> CustomActions => _customActions;
public AdvancedPasteAdditionalActions AdditionalActions => _additionalActions;
private bool OpenAIKeyExists() private bool OpenAIKeyExists()
{ {
PasswordVault vault = new PasswordVault(); PasswordVault vault = new PasswordVault();
@@ -335,12 +344,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
} }
} }
public bool IsConflictingCopyShortcut public bool IsConflictingCopyShortcut =>
{ _customActions.Select(customAction => customAction.Shortcut)
get => _customActions.Select(customAction => customAction.Shortcut) .Concat([PasteAsPlainTextShortcut, AdvancedPasteUIShortcut, PasteAsMarkdownShortcut, PasteAsJsonShortcut])
.Concat([PasteAsPlainTextShortcut, AdvancedPasteUIShortcut, PasteAsMarkdownShortcut, PasteAsJsonShortcut]) .Any(hotkey => WarnHotkeys.Contains(hotkey.ToString()));
.Any(hotkey => WarnHotkeys.Contains(hotkey.ToString()));
} public bool IsAdditionalActionConflictingCopyShortcut =>
_additionalActions.AllActions
.OfType<AdvancedPasteAdditionalAction>()
.Select(additionalAction => additionalAction.Shortcut)
.Any(hotkey => WarnHotkeys.Contains(hotkey.ToString()));
private void DelayedTimer_Tick(object sender, EventArgs e) private void DelayedTimer_Tick(object sender, EventArgs e)
{ {
@@ -458,6 +471,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
NotifySettingsChanged(); NotifySettingsChanged();
} }
private void OnAdditionalActionPropertyChanged(object sender, PropertyChangedEventArgs e)
{
SaveAndNotifySettings();
if (e.PropertyName == nameof(AdvancedPasteAdditionalAction.Shortcut))
{
OnPropertyChanged(nameof(IsAdditionalActionConflictingCopyShortcut));
}
}
private void OnCustomActionPropertyChanged(object sender, PropertyChangedEventArgs e) private void OnCustomActionPropertyChanged(object sender, PropertyChangedEventArgs e)
{ {
if (typeof(AdvancedPasteCustomAction).GetProperty(e.PropertyName).GetCustomAttribute<JsonIgnoreAttribute>() == null) if (typeof(AdvancedPasteCustomAction).GetProperty(e.PropertyName).GetCustomAttribute<JsonIgnoreAttribute>() == null)