mirror of
https://github.com/microsoft/PowerToys.git
synced 2024-11-27 14:59:16 +08:00
[AdvancedPaste]Additional actions - Image to text, Paste as file (txt, png, html) (#35167)
* [AdvancedPaste] Additional actions, including Image to text * Spellcheck issue * [AdvancedPaste] Paste as file and many other improvements * Fixed typo * Fixed typo * [AdvancedPaste] Improved paste window menu layout * [AdvancedPaste] Improved settings window layout * [AdvancedPaste] Removed AudioToText for the moment * Code cleanup * Minor fixes * Changed log-line with potentially sensitive info * Extra telemetry for AdvancedPaste * Added 'Hotkey' suffix to AdvancedPaste_Settings telemetry event
This commit is contained in:
parent
14139affc5
commit
dd5cd2a570
@ -63,6 +63,10 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
{
|
||||
return CommonSharedConstants::ADVANCED_PASTE_JSON_MESSAGE;
|
||||
}
|
||||
hstring Constants::AdvancedPasteAdditionalActionMessage()
|
||||
{
|
||||
return CommonSharedConstants::ADVANCED_PASTE_ADDITIONAL_ACTION_MESSAGE;
|
||||
}
|
||||
hstring Constants::AdvancedPasteCustomActionMessage()
|
||||
{
|
||||
return CommonSharedConstants::ADVANCED_PASTE_CUSTOM_ACTION_MESSAGE;
|
||||
|
@ -19,6 +19,7 @@ namespace winrt::PowerToys::Interop::implementation
|
||||
static hstring AdvancedPasteShowUIMessage();
|
||||
static hstring AdvancedPasteMarkdownMessage();
|
||||
static hstring AdvancedPasteJsonMessage();
|
||||
static hstring AdvancedPasteAdditionalActionMessage();
|
||||
static hstring AdvancedPasteCustomActionMessage();
|
||||
static hstring ShowPowerOCRSharedEvent();
|
||||
static hstring MouseJumpShowPreviewEvent();
|
||||
|
@ -16,6 +16,7 @@ namespace PowerToys
|
||||
static String AdvancedPasteShowUIMessage();
|
||||
static String AdvancedPasteMarkdownMessage();
|
||||
static String AdvancedPasteJsonMessage();
|
||||
static String AdvancedPasteAdditionalActionMessage();
|
||||
static String AdvancedPasteCustomActionMessage();
|
||||
static String ShowPowerOCRSharedEvent();
|
||||
static String MouseJumpShowPreviewEvent();
|
||||
|
@ -32,6 +32,8 @@ namespace CommonSharedConstants
|
||||
|
||||
const wchar_t ADVANCED_PASTE_JSON_MESSAGE[] = L"PasteJson";
|
||||
|
||||
const wchar_t ADVANCED_PASTE_ADDITIONAL_ACTION_MESSAGE[] = L"AdditionalAction";
|
||||
|
||||
const wchar_t ADVANCED_PASTE_CUSTOM_ACTION_MESSAGE[] = L"CustomAction";
|
||||
|
||||
// Path to the event used to show Color Picker
|
||||
|
@ -3,12 +3,16 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using AdvancedPaste.Services;
|
||||
using AdvancedPaste.Settings;
|
||||
using AdvancedPaste.ViewModels;
|
||||
using ManagedCommon;
|
||||
@ -34,6 +38,13 @@ namespace AdvancedPaste
|
||||
{
|
||||
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 OptionsViewModel viewModel;
|
||||
|
||||
@ -60,8 +71,10 @@ namespace AdvancedPaste
|
||||
|
||||
Host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder().UseContentRoot(AppContext.BaseDirectory).ConfigureServices((context, services) =>
|
||||
{
|
||||
services.AddSingleton<OptionsViewModel>();
|
||||
services.AddSingleton<IUserSettings, UserSettings>();
|
||||
services.AddSingleton<AICompletionsHelper>();
|
||||
services.AddSingleton<OptionsViewModel>();
|
||||
services.AddSingleton<IPasteFormatExecutor, PasteFormatExecutor>();
|
||||
}).Build();
|
||||
|
||||
viewModel = GetService<OptionsViewModel>();
|
||||
@ -111,35 +124,35 @@ namespace AdvancedPaste
|
||||
|
||||
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 () =>
|
||||
{
|
||||
var connectTimeout = TimeSpan.FromSeconds(10);
|
||||
await NamedPipeProcessor.ProcessNamedPipeAsync(pipeName, connectTimeout, OnMessage, CancellationToken.None);
|
||||
});
|
||||
Task.Run(async () => await NamedPipeProcessor.ProcessNamedPipeAsync(pipeName, connectTimeout: TimeSpan.FromSeconds(10), OnMessage, CancellationToken.None));
|
||||
}
|
||||
|
||||
private void OnNamedPipeMessage(string message)
|
||||
private async Task OnNamedPipeMessage(string message)
|
||||
{
|
||||
var messageParts = message.Split();
|
||||
var messageType = messageParts.First();
|
||||
|
||||
if (messageType == PowerToys.Interop.Constants.AdvancedPasteShowUIMessage())
|
||||
{
|
||||
OnAdvancedPasteHotkey();
|
||||
await ShowWindow();
|
||||
}
|
||||
else if (messageType == PowerToys.Interop.Constants.AdvancedPasteMarkdownMessage())
|
||||
{
|
||||
OnAdvancedPasteMarkdownHotkey();
|
||||
await viewModel.ExecutePasteFormatAsync(PasteFormats.Markdown, PasteActionSource.GlobalKeyboardShortcut);
|
||||
}
|
||||
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())
|
||||
{
|
||||
OnAdvancedPasteCustomActionHotkey(messageParts);
|
||||
await OnAdvancedPasteCustomActionHotkey(messageParts);
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,24 +161,27 @@ namespace AdvancedPaste
|
||||
Logger.LogError("Unhandled exception", e.Exception);
|
||||
}
|
||||
|
||||
private void OnAdvancedPasteJsonHotkey()
|
||||
private async Task OnAdvancedPasteAdditionalActionHotkey(string[] messageParts)
|
||||
{
|
||||
viewModel.ReadClipboard();
|
||||
viewModel.ToJsonFunction(true);
|
||||
if (messageParts.Length != 2)
|
||||
{
|
||||
Logger.LogWarning("Unexpected additional action message");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!AdditionalActionIPCKeys.TryGetValue(messageParts[1], out PasteFormats pasteFormat))
|
||||
{
|
||||
Logger.LogWarning($"Unexpected additional action type {messageParts[1]}");
|
||||
}
|
||||
else
|
||||
{
|
||||
await ShowWindow();
|
||||
await viewModel.ExecutePasteFormatAsync(pasteFormat, PasteActionSource.GlobalKeyboardShortcut);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAdvancedPasteMarkdownHotkey()
|
||||
{
|
||||
viewModel.ReadClipboard();
|
||||
viewModel.ToMarkdownFunction(true);
|
||||
}
|
||||
|
||||
private void OnAdvancedPasteHotkey()
|
||||
{
|
||||
ShowWindow();
|
||||
}
|
||||
|
||||
private void OnAdvancedPasteCustomActionHotkey(string[] messageParts)
|
||||
private async Task OnAdvancedPasteCustomActionHotkey(string[] messageParts)
|
||||
{
|
||||
if (messageParts.Length != 2)
|
||||
{
|
||||
@ -179,16 +195,15 @@ namespace AdvancedPaste
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowWindow();
|
||||
viewModel.ReadClipboard();
|
||||
viewModel.ExecuteCustomActionWithPaste(customActionId);
|
||||
await ShowWindow();
|
||||
await viewModel.ExecuteCustomActionAsync(customActionId, PasteActionSource.GlobalKeyboardShortcut);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowWindow()
|
||||
private async Task ShowWindow()
|
||||
{
|
||||
viewModel.OnShow();
|
||||
await viewModel.OnShowAsync();
|
||||
|
||||
if (window is null)
|
||||
{
|
||||
|
@ -346,7 +346,7 @@
|
||||
x:Name="InputTxtBox"
|
||||
HorizontalAlignment="Stretch"
|
||||
x:FieldModifier="public"
|
||||
IsEnabled="{x:Bind ViewModel.IsClipboardDataText, Mode=OneWay}"
|
||||
IsEnabled="{x:Bind ViewModel.ClipboardHasData, Mode=OneWay}"
|
||||
KeyDown="InputTxtBox_KeyDown"
|
||||
PlaceholderText="{x:Bind ViewModel.InputTxtBoxPlaceholderText, Mode=OneWay}"
|
||||
Style="{StaticResource CustomTextBoxStyle}"
|
||||
@ -589,7 +589,7 @@
|
||||
Background="Transparent"
|
||||
Visibility="{x:Bind ViewModel.IsCustomAIEnabled, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||
<ToolTipService.ToolTip>
|
||||
<ToolTip Content="{x:Bind ViewModel.GeneralErrorText}" />
|
||||
<ToolTip Content="{x:Bind ViewModel.AIDisabledErrorText}" />
|
||||
</ToolTipService.ToolTip>
|
||||
</Grid>
|
||||
</Grid>
|
||||
@ -638,7 +638,7 @@
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.ApiErrorText, Mode=OneWay}" />
|
||||
Text="{x:Bind ViewModel.PasteOperationErrorText, Mode=OneWay}" />
|
||||
<HyperlinkButton
|
||||
x:Uid="SettingsBtn"
|
||||
Grid.Column="1"
|
||||
|
@ -2,17 +2,15 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.ComponentModel;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Settings;
|
||||
using AdvancedPaste.Models;
|
||||
using AdvancedPaste.ViewModels;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
@ -20,12 +18,6 @@ namespace AdvancedPaste.Controls
|
||||
{
|
||||
public sealed partial class PromptBox : Microsoft.UI.Xaml.Controls.UserControl
|
||||
{
|
||||
// Minimum time to show spinner when generating custom format using forcePasteCustom
|
||||
private static readonly TimeSpan MinTaskTime = TimeSpan.FromSeconds(2);
|
||||
|
||||
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
private readonly IUserSettings _userSettings;
|
||||
|
||||
public OptionsViewModel ViewModel { get; private set; }
|
||||
|
||||
public static readonly DependencyProperty PlaceholderTextProperty = DependencyProperty.Register(
|
||||
@ -54,12 +46,31 @@ namespace AdvancedPaste.Controls
|
||||
|
||||
public PromptBox()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
_userSettings = App.GetService<IUserSettings>();
|
||||
InitializeComponent();
|
||||
|
||||
ViewModel = App.GetService<OptionsViewModel>();
|
||||
ViewModel.CustomActionActivated += (_, e) => GenerateCustom(e.ForcePasteCustom);
|
||||
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
|
||||
ViewModel.CustomActionActivated += ViewModel_CustomActionActivated;
|
||||
}
|
||||
|
||||
private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(ViewModel.Busy) || e.PropertyName == nameof(ViewModel.PasteOperationErrorText))
|
||||
{
|
||||
var state = ViewModel.Busy ? "LoadingState" : string.IsNullOrEmpty(ViewModel.PasteOperationErrorText) ? "DefaultState" : "ErrorState";
|
||||
VisualStateManager.GoToState(this, state, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void ViewModel_CustomActionActivated(object sender, CustomActionActivatedEventArgs e)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
if (!e.PasteResult)
|
||||
{
|
||||
PreviewGrid.Width = InputTxtBox.ActualWidth;
|
||||
PreviewFlyout.ShowAt(InputTxtBox);
|
||||
}
|
||||
}
|
||||
|
||||
private void Grid_Loaded(object sender, RoutedEventArgs e)
|
||||
@ -68,48 +79,7 @@ namespace AdvancedPaste.Controls
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void GenerateCustom() => GenerateCustom(false);
|
||||
|
||||
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);
|
||||
}
|
||||
private async Task GenerateCustomAsync() => await ViewModel.GenerateCustomFunctionAsync(PasteActionSource.PromptBox);
|
||||
|
||||
[RelayCommand]
|
||||
private void Recall()
|
||||
@ -127,29 +97,24 @@ namespace AdvancedPaste.Controls
|
||||
ClipboardHelper.SetClipboardTextContent(lastQuery.ClipboardData);
|
||||
}
|
||||
|
||||
private void InputTxtBox_KeyDown(object sender, Microsoft.UI.Xaml.Input.KeyRoutedEventArgs e)
|
||||
private async void InputTxtBox_KeyDown(object sender, Microsoft.UI.Xaml.Input.KeyRoutedEventArgs e)
|
||||
{
|
||||
if (e.Key == Windows.System.VirtualKey.Enter && InputTxtBox.Text.Length > 0 && ViewModel.IsCustomAIEnabled)
|
||||
{
|
||||
GenerateCustom();
|
||||
await GenerateCustomAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void PreviewPasteBtn_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel.PasteCustom();
|
||||
InputTxtBox.Text = string.Empty;
|
||||
}
|
||||
|
||||
private void ThumbUpDown_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button btn)
|
||||
if (sender is Button btn && bool.TryParse(btn.CommandParameter as string, out bool result))
|
||||
{
|
||||
bool result;
|
||||
if (bool.TryParse(btn.CommandParameter as string, out result))
|
||||
{
|
||||
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteCustomFormatOutputThumbUpDownEvent(result));
|
||||
}
|
||||
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteCustomFormatOutputThumbUpDownEvent(result));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(GetHeight((value is ICollection collection) ? collection.Count : (value is int intValue) ? intValue : 0));
|
||||
|
||||
public int GetHeight(int itemCount) => Math.Min(MaxItems, itemCount) * ItemHeight;
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException();
|
||||
}
|
@ -8,9 +8,9 @@
|
||||
xmlns:pages="using:AdvancedPaste.Pages"
|
||||
xmlns:winuiex="using:WinUIEx"
|
||||
Width="420"
|
||||
Height="308"
|
||||
Height="188"
|
||||
MinWidth="420"
|
||||
MinHeight="308"
|
||||
MinHeight="188"
|
||||
Closed="WindowEx_Closed"
|
||||
IsAlwaysOnTop="True"
|
||||
IsMaximizable="False"
|
||||
|
@ -4,9 +4,13 @@
|
||||
|
||||
using System;
|
||||
|
||||
using System.Linq;
|
||||
using AdvancedPaste.Converters;
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using AdvancedPaste.Settings;
|
||||
using AdvancedPaste.ViewModels;
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
@ -24,25 +28,32 @@ namespace AdvancedPaste
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
InitializeComponent();
|
||||
|
||||
_userSettings = App.GetService<IUserSettings>();
|
||||
|
||||
var optionsViewModel = App.GetService<OptionsViewModel>();
|
||||
|
||||
var baseHeight = MinHeight;
|
||||
var coreActionCount = PasteFormat.MetadataDict.Values.Count(metadata => metadata.IsCoreAction);
|
||||
|
||||
void UpdateHeight()
|
||||
{
|
||||
var trimmedCustomActionCount = optionsViewModel.IsPasteWithAIEnabled ? Math.Min(_userSettings.CustomActions.Count, 5) : 0;
|
||||
Height = MinHeight = baseHeight + (trimmedCustomActionCount * 40);
|
||||
double GetHeight(int maxCustomActionCount) =>
|
||||
baseHeight +
|
||||
new PasteFormatsToHeightConverter().GetHeight(coreActionCount + _userSettings.AdditionalActions.Count) +
|
||||
new PasteFormatsToHeightConverter() { MaxItems = maxCustomActionCount }.GetHeight(optionsViewModel.IsAIServiceEnabled ? _userSettings.CustomActions.Count : 0);
|
||||
|
||||
MinHeight = GetHeight(1);
|
||||
Height = GetHeight(5);
|
||||
}
|
||||
|
||||
UpdateHeight();
|
||||
|
||||
_userSettings.CustomActions.CollectionChanged += (_, _) => UpdateHeight();
|
||||
_userSettings.Changed += (_, _) => UpdateHeight();
|
||||
optionsViewModel.PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(optionsViewModel.IsPasteWithAIEnabled))
|
||||
if (e.PropertyName == nameof(optionsViewModel.IsAIServiceEnabled))
|
||||
{
|
||||
UpdateHeight();
|
||||
}
|
||||
|
@ -16,8 +16,9 @@
|
||||
<Page.Resources>
|
||||
<tkconverters:BoolToVisibilityConverter x:Name="BoolToVisibilityConverter" />
|
||||
<converters:CountToVisibilityConverter x:Name="countToVisibilityConverter" />
|
||||
<converters:PasteFormatsToHeightConverter x:Name="standardPasteFormatsToHeightConverter" />
|
||||
<converters:CountToDoubleConverter
|
||||
x:Name="customActionsCountToMinHeightConverter"
|
||||
x:Name="customActionsToMinHeightConverter"
|
||||
ValueIfNonZero="40"
|
||||
ValueIfZero="0" />
|
||||
<Style
|
||||
@ -28,37 +29,56 @@
|
||||
<Setter Property="Padding" Value="0" />
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
<Style x:Key="PasteFormatListViewItemStyle" TargetType="ListViewItem">
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch" />
|
||||
</Style>
|
||||
<DataTemplate x:Key="PasteFormatTemplate" x:DataType="local:PasteFormat">
|
||||
<Grid>
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock Text="{x:Bind ToolTip}" />
|
||||
</ToolTipService.ToolTip>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="26" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<FontIcon
|
||||
Margin="0,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
FontSize="16"
|
||||
Glyph="{x:Bind IconGlyph}" />
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
x:Phase="1"
|
||||
Text="{x:Bind Name}" />
|
||||
<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
|
||||
Margin="0"
|
||||
Padding="5,0,5,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
AllowFocusOnInteraction="False"
|
||||
BorderThickness="0"
|
||||
Click="ListView_Button_Click"
|
||||
IsEnabled="{x:Bind IsEnabled, Mode=OneWay}">
|
||||
<Grid Opacity="{x:Bind Opacity, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="26" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock Text="{x:Bind ToolTip, Mode=OneWay}" />
|
||||
</ToolTipService.ToolTip>
|
||||
<FontIcon
|
||||
Margin="0,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
FontSize="16"
|
||||
Glyph="{x:Bind IconGlyph, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
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>
|
||||
</Page.Resources>
|
||||
<Page.KeyboardAccelerators>
|
||||
@ -166,9 +186,9 @@
|
||||
BorderThickness="0,1,0,0"
|
||||
RowSpacing="4">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="{x:Bind ViewModel.StandardPasteFormats.Count, Mode=OneWay, Converter={StaticResource standardPasteFormatsToHeightConverter}}" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" MinHeight="{x:Bind ViewModel.CustomActionPasteFormats.Count, Mode=OneWay, Converter={StaticResource customActionsCountToMinHeightConverter}}" />
|
||||
<RowDefinition Height="*" MinHeight="{x:Bind ViewModel.CustomActionPasteFormats.Count, Mode=OneWay, Converter={StaticResource customActionsToMinHeightConverter}}" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
@ -176,12 +196,13 @@
|
||||
x:Name="PasteOptionsListView"
|
||||
Grid.Row="0"
|
||||
VerticalAlignment="Bottom"
|
||||
IsEnabled="{x:Bind ViewModel.IsClipboardDataText, Mode=OneWay}"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="ListView_Click"
|
||||
IsItemClickEnabled="False"
|
||||
ItemContainerStyle="{StaticResource PasteFormatListViewItemStyle}"
|
||||
ItemContainerTransitions="{x:Null}"
|
||||
ItemTemplate="{StaticResource PasteFormatTemplate}"
|
||||
ItemsSource="{x:Bind ViewModel.StandardPasteFormats, Mode=OneWay}"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Visible"
|
||||
ScrollViewer.VerticalScrollMode="Auto"
|
||||
SelectionMode="None"
|
||||
TabIndex="1" />
|
||||
|
||||
@ -196,9 +217,8 @@
|
||||
x:Name="CustomActionsListView"
|
||||
Grid.Row="2"
|
||||
VerticalAlignment="Top"
|
||||
IsEnabled="{x:Bind ViewModel.IsCustomAIEnabled, Mode=OneWay}"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="ListView_Click"
|
||||
IsItemClickEnabled="False"
|
||||
ItemContainerStyle="{StaticResource PasteFormatListViewItemStyle}"
|
||||
ItemContainerTransitions="{x:Null}"
|
||||
ItemTemplate="{StaticResource PasteFormatTemplate}"
|
||||
ItemsSource="{x:Bind ViewModel.CustomActionPasteFormats, Mode=OneWay}"
|
||||
|
@ -130,15 +130,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)
|
||||
{
|
||||
@ -171,7 +171,7 @@ namespace AdvancedPaste.Pages
|
||||
case VirtualKey.Number7:
|
||||
case VirtualKey.Number8:
|
||||
case VirtualKey.Number9:
|
||||
ViewModel.ExecutePasteFormat(sender.Key);
|
||||
await ViewModel.ExecutePasteFormatAsync(sender.Key);
|
||||
break;
|
||||
|
||||
default:
|
||||
@ -199,8 +199,7 @@ namespace AdvancedPaste.Pages
|
||||
}
|
||||
else if (item.Image is not null)
|
||||
{
|
||||
RandomAccessStreamReference image = null;
|
||||
image = await item.Item.Content.GetBitmapAsync();
|
||||
RandomAccessStreamReference image = await item.Item.Content.GetBitmapAsync();
|
||||
ClipboardHelper.SetClipboardImageContent(image);
|
||||
}
|
||||
}
|
||||
|
@ -3,12 +3,16 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Models;
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Data.Html;
|
||||
using Windows.Graphics.Imaging;
|
||||
using Windows.Storage;
|
||||
using Windows.Storage.Streams;
|
||||
using Windows.System;
|
||||
|
||||
@ -16,6 +20,34 @@ namespace AdvancedPaste.Helpers
|
||||
{
|
||||
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> GetAvailableClipboardFormatsAsync(DataPackageView clipboardData)
|
||||
{
|
||||
var availableClipboardFormats = DataFormats.Aggregate(
|
||||
ClipboardFormat.None,
|
||||
(result, formatPair) => clipboardData.Contains(formatPair.DataFormat) ? (result | formatPair.ClipboardFormat) : result);
|
||||
|
||||
if (clipboardData.Contains(StandardDataFormats.StorageItems))
|
||||
{
|
||||
var storageItems = await clipboardData.GetStorageItemsAsync();
|
||||
|
||||
if (storageItems.Count == 1 && storageItems.Single() is StorageFile file && ImageFileTypes.Contains(file.FileType))
|
||||
{
|
||||
availableClipboardFormats |= ClipboardFormat.ImageFile;
|
||||
}
|
||||
}
|
||||
|
||||
return availableClipboardFormats;
|
||||
}
|
||||
|
||||
internal static void SetClipboardTextContent(string text)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
@ -26,31 +58,45 @@ namespace AdvancedPaste.Helpers
|
||||
output.SetText(text);
|
||||
Clipboard.SetContentWithOptions(output, null);
|
||||
|
||||
// TODO(stefan): For some reason Flush() fails from time to time when directly activated via hotkey.
|
||||
// Calling inside a loop makes it work.
|
||||
bool flushed = false;
|
||||
for (int i = 0; i < 5; i++)
|
||||
Flush();
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
const int maxAttempts = 5;
|
||||
for (int i = 1; i <= maxAttempts; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (flushed)
|
||||
Task.Run(Clipboard.Flush).Wait();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (i == maxAttempts)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
Clipboard.Flush();
|
||||
}).Wait();
|
||||
|
||||
flushed = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Clipboard.Flush() failed", ex);
|
||||
Logger.LogError($"{nameof(Clipboard)}.{nameof(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)
|
||||
@ -63,30 +109,7 @@ namespace AdvancedPaste.Helpers
|
||||
output.SetBitmap(image);
|
||||
Clipboard.SetContentWithOptions(output, null);
|
||||
|
||||
// TODO(stefan): For some reason Flush() fails from time to time when directly activated via hotkey.
|
||||
// Calling inside a loop makes it work.
|
||||
bool flushed = false;
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
if (flushed)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
Clipboard.Flush();
|
||||
}).Wait();
|
||||
|
||||
flushed = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Clipboard.Flush() failed", ex);
|
||||
}
|
||||
}
|
||||
Flush();
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,5 +159,58 @@ namespace AdvancedPaste.Helpers
|
||||
|
||||
Logger.LogInfo("Paste sent");
|
||||
}
|
||||
|
||||
internal static async Task<string> GetClipboardTextOrHtmlTextAsync(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> GetClipboardHtmlContentAsync(DataPackageView clipboardData) =>
|
||||
clipboardData.Contains(StandardDataFormats.Html) ? await clipboardData.GetHtmlFormatAsync() : 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,10 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using AdvancedPaste.Models;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
namespace AdvancedPaste.Settings
|
||||
@ -16,6 +18,10 @@ namespace AdvancedPaste.Settings
|
||||
|
||||
public bool CloseAfterLosingFocus { get; }
|
||||
|
||||
public ObservableCollection<AdvancedPasteCustomAction> CustomActions { get; }
|
||||
public IReadOnlyList<AdvancedPasteCustomAction> CustomActions { get; }
|
||||
|
||||
public IReadOnlyList<PasteFormats> AdditionalActions { get; }
|
||||
|
||||
public event EventHandler Changed;
|
||||
}
|
||||
}
|
||||
|
@ -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> ExtractTextAsync(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();
|
||||
}
|
||||
}
|
@ -3,11 +3,13 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Models;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
|
||||
@ -20,6 +22,8 @@ namespace AdvancedPaste.Settings
|
||||
private readonly TaskScheduler _taskScheduler;
|
||||
private readonly IFileSystemWatcher _watcher;
|
||||
private readonly object _loadingSettingsLock = new();
|
||||
private readonly List<PasteFormats> _additionalActions;
|
||||
private readonly List<AdvancedPasteCustomAction> _customActions;
|
||||
|
||||
private const string AdvancedPasteModuleName = "AdvancedPaste";
|
||||
private const int MaxNumberOfRetry = 5;
|
||||
@ -27,13 +31,17 @@ namespace AdvancedPaste.Settings
|
||||
private bool _disposedValue;
|
||||
private CancellationTokenSource _cancellationTokenSource;
|
||||
|
||||
public event EventHandler Changed;
|
||||
|
||||
public bool ShowCustomPreview { get; private set; }
|
||||
|
||||
public bool SendPasteKeyCombination { get; private set; }
|
||||
|
||||
public bool CloseAfterLosingFocus { get; private set; }
|
||||
|
||||
public ObservableCollection<AdvancedPasteCustomAction> CustomActions { get; private set; }
|
||||
public IReadOnlyList<PasteFormats> AdditionalActions => _additionalActions;
|
||||
|
||||
public IReadOnlyList<AdvancedPasteCustomAction> CustomActions => _customActions;
|
||||
|
||||
public UserSettings()
|
||||
{
|
||||
@ -42,8 +50,8 @@ namespace AdvancedPaste.Settings
|
||||
ShowCustomPreview = true;
|
||||
SendPasteKeyCombination = true;
|
||||
CloseAfterLosingFocus = false;
|
||||
CustomActions = [];
|
||||
|
||||
_additionalActions = [];
|
||||
_customActions = [];
|
||||
_taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
|
||||
|
||||
LoadSettingsFromJson();
|
||||
@ -88,18 +96,29 @@ namespace AdvancedPaste.Settings
|
||||
{
|
||||
void UpdateSettings()
|
||||
{
|
||||
ShowCustomPreview = settings.Properties.ShowCustomPreview;
|
||||
SendPasteKeyCombination = settings.Properties.SendPasteKeyCombination;
|
||||
CloseAfterLosingFocus = settings.Properties.CloseAfterLosingFocus;
|
||||
var properties = settings.Properties;
|
||||
|
||||
CustomActions.Clear();
|
||||
foreach (var customAction in settings.Properties.CustomActions.Value)
|
||||
{
|
||||
if (customAction.IsShown && customAction.IsValid)
|
||||
{
|
||||
CustomActions.Add(customAction);
|
||||
}
|
||||
}
|
||||
ShowCustomPreview = properties.ShowCustomPreview;
|
||||
SendPasteKeyCombination = properties.SendPasteKeyCombination;
|
||||
CloseAfterLosingFocus = properties.CloseAfterLosingFocus;
|
||||
|
||||
var sourceAdditionalActions = properties.AdditionalActions;
|
||||
(PasteFormats Format, IAdvancedPasteAction[] Actions)[] additionalActionFormats =
|
||||
[
|
||||
(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
|
||||
|
@ -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,
|
||||
}
|
@ -5,14 +5,13 @@
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
|
||||
namespace AdvancedPaste.Models
|
||||
namespace AdvancedPaste.Models;
|
||||
|
||||
public class ClipboardItem
|
||||
{
|
||||
public class ClipboardItem
|
||||
{
|
||||
public string Content { get; set; }
|
||||
public string Content { get; set; }
|
||||
|
||||
public ClipboardHistoryItem Item { get; set; }
|
||||
public ClipboardHistoryItem Item { get; set; }
|
||||
|
||||
public BitmapImage Image { get; set; }
|
||||
}
|
||||
public BitmapImage Image { get; set; }
|
||||
}
|
||||
|
@ -6,9 +6,9 @@ using System;
|
||||
|
||||
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 init; } = text;
|
||||
|
||||
public bool ForcePasteCustom { get; private set; } = forcePasteCustom;
|
||||
public bool PasteResult { get; private init; } = pasteResult;
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
}
|
@ -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,
|
||||
}
|
@ -2,38 +2,62 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
namespace AdvancedPaste.Models;
|
||||
|
||||
public partial class PasteFormat : ObservableObject
|
||||
[DebuggerDisplay("{Name} IsEnabled={IsEnabled} ShortcutText={ShortcutText}")]
|
||||
public sealed class PasteFormat
|
||||
{
|
||||
[ObservableProperty]
|
||||
private string _shortcutText = string.Empty;
|
||||
public static readonly IReadOnlyDictionary<PasteFormats, PasteFormatMetadataAttribute> MetadataDict =
|
||||
typeof(PasteFormats).GetFields()
|
||||
.Where(field => field.IsLiteral)
|
||||
.ToDictionary(field => (PasteFormats)field.GetRawConstantValue(), field => field.GetCustomAttribute<PasteFormatMetadataAttribute>());
|
||||
|
||||
[ObservableProperty]
|
||||
private string _toolTip = string.Empty;
|
||||
|
||||
public PasteFormat()
|
||||
private PasteFormat(PasteFormats format, ClipboardFormat clipboardFormats, bool isAIServiceEnabled)
|
||||
{
|
||||
Format = format;
|
||||
IsEnabled = SupportsClipboardFormats(clipboardFormats) && (isAIServiceEnabled || !Metadata.RequiresAIService);
|
||||
}
|
||||
|
||||
public PasteFormat(AdvancedPasteCustomAction customAction, string shortcutText)
|
||||
public PasteFormat(PasteFormats format, ClipboardFormat clipboardFormats, bool isAIServiceEnabled, Func<string, string> resourceLoader)
|
||||
: this(format, clipboardFormats, isAIServiceEnabled)
|
||||
{
|
||||
Name = Metadata.ResourceId == null ? string.Empty : resourceLoader(Metadata.ResourceId);
|
||||
Prompt = string.Empty;
|
||||
}
|
||||
|
||||
public PasteFormat(AdvancedPasteCustomAction customAction, ClipboardFormat clipboardFormats, bool isAIServiceEnabled)
|
||||
: this(PasteFormats.Custom, clipboardFormats, isAIServiceEnabled)
|
||||
{
|
||||
IconGlyph = "\uE945";
|
||||
Name = customAction.Name;
|
||||
Prompt = customAction.Prompt;
|
||||
Format = PasteFormats.Custom;
|
||||
ShortcutText = shortcutText;
|
||||
ToolTip = customAction.Prompt;
|
||||
}
|
||||
|
||||
public string IconGlyph { get; init; }
|
||||
public PasteFormatMetadataAttribute Metadata => MetadataDict[Format];
|
||||
|
||||
public string 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;
|
||||
|
||||
public bool SupportsClipboardFormats(ClipboardFormat clipboardFormats) => (clipboardFormats & Metadata.SupportedClipboardFormats) != ClipboardFormat.None;
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
@ -2,13 +2,33 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace AdvancedPaste.Models
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
namespace AdvancedPaste.Models;
|
||||
|
||||
public enum PasteFormats
|
||||
{
|
||||
public enum PasteFormats
|
||||
{
|
||||
PlainText,
|
||||
Markdown,
|
||||
Json,
|
||||
Custom,
|
||||
}
|
||||
[PasteFormatMetadata(IsCoreAction = true, ResourceId = "PasteAsPlainText", IconGlyph = "\uE8E9", RequiresAIService = false, SupportedClipboardFormats = ClipboardFormat.Text)]
|
||||
PlainText,
|
||||
|
||||
[PasteFormatMetadata(IsCoreAction = true, ResourceId = "PasteAsMarkdown", IconGlyph = "\ue8a5", RequiresAIService = false, SupportedClipboardFormats = ClipboardFormat.Text)]
|
||||
Markdown,
|
||||
|
||||
[PasteFormatMetadata(IsCoreAction = true, ResourceId = "PasteAsJson", IconGlyph = "\uE943", RequiresAIService = false, SupportedClipboardFormats = ClipboardFormat.Text)]
|
||||
Json,
|
||||
|
||||
[PasteFormatMetadata(IsCoreAction = false, ResourceId = "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,
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -0,0 +1,253 @@
|
||||
// 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.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 {pasteFormat.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.ExtractTextAsync(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.GetClipboardTextOrHtmlTextAsync(clipboardData);
|
||||
await SetClipboardFileContentAsync(text, "txt");
|
||||
}
|
||||
|
||||
private async Task ToHtmlFileAsync(DataPackageView clipboardData)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
var cfHtml = await ClipboardHelper.GetClipboardHtmlContentAsync(clipboardData);
|
||||
var html = RemoveHtmlMetadata(cfHtml);
|
||||
|
||||
await SetClipboardFileContentAsync(html, "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 cfHtml)
|
||||
{
|
||||
int? GetIntTagValue(string tagName)
|
||||
{
|
||||
var tagNameWithColon = tagName + ":";
|
||||
int tagStartPos = cfHtml.IndexOf(tagNameWithColon, StringComparison.InvariantCulture);
|
||||
|
||||
const int tagValueLength = 10;
|
||||
return tagStartPos != -1 && int.TryParse(cfHtml.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) ? cfHtml : cfHtml[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),
|
||||
};
|
||||
}
|
@ -120,9 +120,12 @@
|
||||
<data name="AIMistakeNote.Text" xml:space="preserve">
|
||||
<value>AI can make mistakes.</value>
|
||||
</data>
|
||||
<data name="ClipboardDataTypeMismatchWarning" xml:space="preserve">
|
||||
<value>Clipboard data is not text</value>
|
||||
<data name="ClipboardEmptyWarning" xml:space="preserve">
|
||||
<value>Clipboard does not contain any usable formats</value>
|
||||
</data>
|
||||
<data name="ClipboardDataNotTextWarning" xml:space="preserve">
|
||||
<value>Clipboard data is not text</value>
|
||||
</data>
|
||||
<data name="OpenAINotConfigured" xml:space="preserve">
|
||||
<value>To custom with AI is not enabled</value>
|
||||
</data>
|
||||
@ -135,6 +138,9 @@
|
||||
<data name="OpenAIApiKeyError" xml:space="preserve">
|
||||
<value>OpenAI request failed with status code: </value>
|
||||
</data>
|
||||
<data name="PasteError" xml:space="preserve">
|
||||
<value>An error occurred during the paste operation</value>
|
||||
</data>
|
||||
<data name="ClipboardHistoryButton.Text" xml:space="preserve">
|
||||
<value>Clipboard history</value>
|
||||
</data>
|
||||
@ -151,7 +157,7 @@
|
||||
<value>Privacy</value>
|
||||
</data>
|
||||
<data name="LoadingText.Text" xml:space="preserve">
|
||||
<value>Connecting to AI services and generating output..</value>
|
||||
<value>Generating output...</value>
|
||||
</data>
|
||||
<data name="PasteAsJson" xml:space="preserve">
|
||||
<value>Paste as JSON</value>
|
||||
@ -162,6 +168,18 @@
|
||||
<data name="PasteAsPlainText" xml:space="preserve">
|
||||
<value>Paste as plain 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">
|
||||
<value>Paste</value>
|
||||
</data>
|
||||
@ -228,4 +246,7 @@
|
||||
<data name="CtrlKey" xml:space="preserve">
|
||||
<value>Ctrl</value>
|
||||
</data>
|
||||
<data name="PasteAsFile_FilePrefix" xml:space="preserve">
|
||||
<value>PowerToys_Paste_</value>
|
||||
</data>
|
||||
</root>
|
@ -3,21 +3,20 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using AdvancedPaste.Services;
|
||||
using AdvancedPaste.Settings;
|
||||
using Common.UI;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.Win32;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
@ -28,67 +27,66 @@ using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
|
||||
|
||||
namespace AdvancedPaste.ViewModels
|
||||
{
|
||||
public partial class OptionsViewModel : ObservableObject, IDisposable
|
||||
public sealed partial class OptionsViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
private readonly DispatcherTimer _clipboardTimer;
|
||||
private readonly IUserSettings _userSettings;
|
||||
private readonly AICompletionsHelper aiHelper;
|
||||
private readonly App app = App.Current as App;
|
||||
private readonly PasteFormat[] _allStandardPasteFormats;
|
||||
private readonly IPasteFormatExecutor _pasteFormatExecutor;
|
||||
private readonly AICompletionsHelper _aiHelper;
|
||||
|
||||
public DataPackageView ClipboardData { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(InputTxtBoxPlaceholderText))]
|
||||
[NotifyPropertyChangedFor(nameof(GeneralErrorText))]
|
||||
[NotifyPropertyChangedFor(nameof(IsCustomAIEnabled))]
|
||||
private bool _isClipboardDataText;
|
||||
[NotifyPropertyChangedFor(nameof(ClipboardHasData))]
|
||||
[NotifyPropertyChangedFor(nameof(InputTxtBoxPlaceholderText))]
|
||||
[NotifyPropertyChangedFor(nameof(AIDisabledErrorText))]
|
||||
private ClipboardFormat _availableClipboardFormats;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _clipboardHistoryEnabled;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(InputTxtBoxPlaceholderText))]
|
||||
[NotifyPropertyChangedFor(nameof(GeneralErrorText))]
|
||||
[NotifyPropertyChangedFor(nameof(IsPasteWithAIEnabled))]
|
||||
[NotifyPropertyChangedFor(nameof(AIDisabledErrorText))]
|
||||
[NotifyPropertyChangedFor(nameof(IsAIServiceEnabled))]
|
||||
[NotifyPropertyChangedFor(nameof(IsCustomAIEnabled))]
|
||||
private bool _isAllowedByGPO;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ApiErrorText))]
|
||||
private int _apiRequestStatus;
|
||||
private string _pasteOperationErrorText;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _query = string.Empty;
|
||||
|
||||
private bool _pasteFormatsDirty;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _busy;
|
||||
|
||||
public ObservableCollection<PasteFormat> StandardPasteFormats { get; } = [];
|
||||
|
||||
public ObservableCollection<PasteFormat> CustomActionPasteFormats { get; } = [];
|
||||
|
||||
public bool IsPasteWithAIEnabled => IsAllowedByGPO && aiHelper.IsAIEnabled;
|
||||
public bool IsAIServiceEnabled => IsAllowedByGPO && _aiHelper.IsAIEnabled;
|
||||
|
||||
public bool IsCustomAIEnabled => IsPasteWithAIEnabled && IsClipboardDataText;
|
||||
public bool IsCustomAIEnabled => IsAIServiceEnabled && ClipboardHasText;
|
||||
|
||||
public bool ClipboardHasData => AvailableClipboardFormats != ClipboardFormat.None;
|
||||
|
||||
private bool ClipboardHasText => AvailableClipboardFormats.HasFlag(ClipboardFormat.Text);
|
||||
|
||||
private bool Visible => GetMainWindow()?.Visible is true;
|
||||
|
||||
public event EventHandler<CustomActionActivatedEventArgs> CustomActionActivated;
|
||||
|
||||
public OptionsViewModel(IUserSettings userSettings)
|
||||
public OptionsViewModel(AICompletionsHelper aiHelper, IUserSettings userSettings, IPasteFormatExecutor pasteFormatExecutor)
|
||||
{
|
||||
aiHelper = new AICompletionsHelper();
|
||||
_aiHelper = aiHelper;
|
||||
_userSettings = userSettings;
|
||||
_pasteFormatExecutor = pasteFormatExecutor;
|
||||
|
||||
ApiRequestStatus = (int)HttpStatusCode.OK;
|
||||
|
||||
_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 = [];
|
||||
GeneratedResponses.CollectionChanged += (s, e) =>
|
||||
{
|
||||
OnPropertyChanged(nameof(HasMultipleResponses));
|
||||
@ -96,28 +94,31 @@ namespace AdvancedPaste.ViewModels
|
||||
};
|
||||
|
||||
ClipboardHistoryEnabled = IsClipboardHistoryEnabled();
|
||||
ReadClipboard();
|
||||
UpdateOpenAIKey();
|
||||
_clipboardTimer = new() { Interval = TimeSpan.FromSeconds(1) };
|
||||
_clipboardTimer.Tick += ClipboardTimer_Tick;
|
||||
_clipboardTimer.Start();
|
||||
|
||||
RefreshPasteFormats();
|
||||
_userSettings.CustomActions.CollectionChanged += (_, _) => EnqueueRefreshPasteFormats();
|
||||
_userSettings.Changed += (_, _) => EnqueueRefreshPasteFormats();
|
||||
PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(Query) || e.PropertyName == nameof(IsPasteWithAIEnabled))
|
||||
string[] dirtyingProperties = [nameof(Query), nameof(IsAIServiceEnabled), nameof(IsCustomAIEnabled), nameof(AvailableClipboardFormats)];
|
||||
|
||||
if (dirtyingProperties.Contains(e.PropertyName))
|
||||
{
|
||||
EnqueueRefreshPasteFormats();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void ClipboardTimer_Tick(object sender, object e)
|
||||
private static MainWindow GetMainWindow() => (App.Current as App)?.GetMainWindow();
|
||||
|
||||
private async void ClipboardTimer_Tick(object sender, object e)
|
||||
{
|
||||
if (app.GetMainWindow()?.Visible is true)
|
||||
if (Visible)
|
||||
{
|
||||
ReadClipboard();
|
||||
await ReadClipboardAsync();
|
||||
UpdateAllowedByGPO();
|
||||
}
|
||||
}
|
||||
@ -137,10 +138,12 @@ namespace AdvancedPaste.ViewModels
|
||||
});
|
||||
}
|
||||
|
||||
private PasteFormat CreatePasteFormat(PasteFormats format) => new(format, AvailableClipboardFormats, IsAIServiceEnabled, ResourceLoaderInstance.ResourceLoader.GetString);
|
||||
|
||||
private PasteFormat CreatePasteFormat(AdvancedPasteCustomAction customAction) => new(customAction, AvailableClipboardFormats, IsAIServiceEnabled);
|
||||
|
||||
private void RefreshPasteFormats()
|
||||
{
|
||||
bool Filter(string text) => text.Contains(Query, StringComparison.CurrentCultureIgnoreCase);
|
||||
|
||||
var ctrlString = ResourceLoaderInstance.ResourceLoader.GetString("CtrlKey");
|
||||
int shortcutNum = 0;
|
||||
|
||||
@ -150,28 +153,33 @@ namespace AdvancedPaste.ViewModels
|
||||
return shortcutNum <= 9 ? $"{ctrlString}+{shortcutNum}" : string.Empty;
|
||||
}
|
||||
|
||||
StandardPasteFormats.Clear();
|
||||
foreach (var format in _allStandardPasteFormats)
|
||||
IEnumerable<PasteFormat> FilterAndSort(IEnumerable<PasteFormat> pasteFormats) =>
|
||||
from pasteFormat in pasteFormats
|
||||
let comparison = StringComparison.CurrentCultureIgnoreCase
|
||||
where pasteFormat.Name.Contains(Query, comparison) || pasteFormat.Prompt.Contains(Query, comparison)
|
||||
orderby pasteFormat.IsEnabled descending
|
||||
select pasteFormat;
|
||||
|
||||
void UpdateFormats(ObservableCollection<PasteFormat> collection, IEnumerable<PasteFormat> pasteFormats)
|
||||
{
|
||||
if (Filter(format.Name))
|
||||
collection.Clear();
|
||||
|
||||
foreach (var format in FilterAndSort(pasteFormats))
|
||||
{
|
||||
format.ShortcutText = GetNextShortcutText();
|
||||
format.ToolTip = $"{format.Name} ({format.ShortcutText})";
|
||||
StandardPasteFormats.Add(format);
|
||||
if (format.IsEnabled)
|
||||
{
|
||||
format.ShortcutText = GetNextShortcutText();
|
||||
}
|
||||
|
||||
collection.Add(format);
|
||||
}
|
||||
}
|
||||
|
||||
CustomActionPasteFormats.Clear();
|
||||
if (IsPasteWithAIEnabled)
|
||||
{
|
||||
foreach (var customAction in _userSettings.CustomActions)
|
||||
{
|
||||
if (Filter(customAction.Name) || Filter(customAction.Prompt))
|
||||
{
|
||||
CustomActionPasteFormats.Add(new PasteFormat(customAction, GetNextShortcutText()));
|
||||
}
|
||||
}
|
||||
}
|
||||
UpdateFormats(StandardPasteFormats, Enum.GetValues<PasteFormats>()
|
||||
.Where(format => PasteFormat.MetadataDict[format].IsCoreAction || _userSettings.AdditionalActions.Contains(format))
|
||||
.Select(CreatePasteFormat));
|
||||
|
||||
UpdateFormats(CustomActionPasteFormats, IsAIServiceEnabled ? _userSettings.CustomActions.Select(CreatePasteFormat) : []);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@ -180,26 +188,34 @@ namespace AdvancedPaste.ViewModels
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public void ReadClipboard()
|
||||
public async Task ReadClipboardAsync()
|
||||
{
|
||||
if (Busy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClipboardData = Clipboard.GetContent();
|
||||
IsClipboardDataText = ClipboardData.Contains(StandardDataFormats.Text);
|
||||
AvailableClipboardFormats = await ClipboardHelper.GetAvailableClipboardFormatsAsync(ClipboardData);
|
||||
}
|
||||
|
||||
public void OnShow()
|
||||
public async Task OnShowAsync()
|
||||
{
|
||||
ReadClipboard();
|
||||
PasteOperationErrorText = string.Empty;
|
||||
Query = string.Empty;
|
||||
|
||||
await ReadClipboardAsync();
|
||||
|
||||
if (UpdateOpenAIKey())
|
||||
{
|
||||
app.GetMainWindow()?.StartLoading();
|
||||
GetMainWindow()?.StartLoading();
|
||||
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
app.GetMainWindow()?.FinishLoading(aiHelper.IsAIEnabled);
|
||||
GetMainWindow()?.FinishLoading(_aiHelper.IsAIEnabled);
|
||||
OnPropertyChanged(nameof(InputTxtBoxPlaceholderText));
|
||||
OnPropertyChanged(nameof(GeneralErrorText));
|
||||
OnPropertyChanged(nameof(IsPasteWithAIEnabled));
|
||||
OnPropertyChanged(nameof(AIDisabledErrorText));
|
||||
OnPropertyChanged(nameof(IsAIServiceEnabled));
|
||||
OnPropertyChanged(nameof(IsCustomAIEnabled));
|
||||
});
|
||||
}
|
||||
@ -209,7 +225,7 @@ namespace AdvancedPaste.ViewModels
|
||||
}
|
||||
|
||||
// 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
|
||||
private int _currentResponseIndex;
|
||||
@ -228,30 +244,20 @@ namespace AdvancedPaste.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasMultipleResponses
|
||||
{
|
||||
get => GeneratedResponses.Count > 1;
|
||||
}
|
||||
public bool HasMultipleResponses => GeneratedResponses.Count > 1;
|
||||
|
||||
public string CurrentIndexDisplay => $"{CurrentResponseIndex + 1}/{GeneratedResponses.Count}";
|
||||
|
||||
public string InputTxtBoxPlaceholderText
|
||||
=> ResourceLoaderInstance.ResourceLoader.GetString(ClipboardHasData ? "CustomFormatTextBox/PlaceholderText" : "ClipboardEmptyWarning");
|
||||
|
||||
public string AIDisabledErrorText
|
||||
{
|
||||
get
|
||||
{
|
||||
app.GetMainWindow().ClearInputText();
|
||||
|
||||
return IsClipboardDataText ? ResourceLoaderInstance.ResourceLoader.GetString("CustomFormatTextBox/PlaceholderText") : GeneralErrorText;
|
||||
}
|
||||
}
|
||||
|
||||
public string GeneralErrorText
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsClipboardDataText)
|
||||
if (!ClipboardHasText)
|
||||
{
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("ClipboardDataTypeMismatchWarning");
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("ClipboardDataNotTextWarning");
|
||||
}
|
||||
|
||||
if (!IsAllowedByGPO)
|
||||
@ -259,7 +265,7 @@ namespace AdvancedPaste.ViewModels
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAIGpoDisabled");
|
||||
}
|
||||
|
||||
if (!aiHelper.IsAIEnabled)
|
||||
if (!_aiHelper.IsAIEnabled)
|
||||
{
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAINotConfigured");
|
||||
}
|
||||
@ -270,17 +276,6 @@ namespace AdvancedPaste.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public string ApiErrorText
|
||||
{
|
||||
get => (HttpStatusCode)ApiRequestStatus switch
|
||||
{
|
||||
HttpStatusCode.TooManyRequests => ResourceLoaderInstance.ResourceLoader.GetString("OpenAIApiKeyTooManyRequests"),
|
||||
HttpStatusCode.Unauthorized => ResourceLoaderInstance.ResourceLoader.GetString("OpenAIApiKeyUnauthorized"),
|
||||
HttpStatusCode.OK => string.Empty,
|
||||
_ => ResourceLoaderInstance.ResourceLoader.GetString("OpenAIApiKeyError") + ApiRequestStatus.ToString(CultureInfo.InvariantCulture),
|
||||
};
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
private string _customFormatResult;
|
||||
|
||||
@ -289,9 +284,17 @@ namespace AdvancedPaste.ViewModels
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -320,190 +323,120 @@ namespace AdvancedPaste.ViewModels
|
||||
public void OpenSettings()
|
||||
{
|
||||
SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.AdvancedPaste, true);
|
||||
(App.Current as App).GetMainWindow().Close();
|
||||
GetMainWindow()?.Close();
|
||||
}
|
||||
|
||||
private void SetClipboardContentAndHideWindow(string content)
|
||||
internal async Task ExecutePasteFormatAsync(PasteFormats format, PasteActionSource source)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(content))
|
||||
{
|
||||
ClipboardHelper.SetClipboardTextContent(content);
|
||||
}
|
||||
|
||||
if (app.GetMainWindow() != null)
|
||||
{
|
||||
Windows.Win32.Foundation.HWND hwnd = (Windows.Win32.Foundation.HWND)app.GetMainWindow().GetWindowHandle();
|
||||
Windows.Win32.PInvoke.ShowWindow(hwnd, Windows.Win32.UI.WindowsAndMessaging.SHOW_WINDOW_CMD.SW_HIDE);
|
||||
}
|
||||
await ReadClipboardAsync();
|
||||
await ExecutePasteFormatAsync(CreatePasteFormat(format), source);
|
||||
}
|
||||
|
||||
internal void ToPlainTextFunction()
|
||||
internal async Task ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
string outputString = MarkdownHelper.PasteAsPlainTextFromClipboard(ClipboardData);
|
||||
|
||||
SetClipboardContentAndHideWindow(outputString);
|
||||
|
||||
if (_userSettings.SendPasteKeyCombination)
|
||||
{
|
||||
ClipboardHelper.SendPasteKeyCombination();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
internal void ToMarkdownFunction(bool pasteAlways = false)
|
||||
{
|
||||
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))
|
||||
if (Busy)
|
||||
{
|
||||
Logger.LogWarning($"Execution of {pasteFormat.Format} from {source} suppressed as busy");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (pasteFormat.Format)
|
||||
if (!pasteFormat.IsEnabled)
|
||||
{
|
||||
case PasteFormats.PlainText:
|
||||
ToPlainTextFunction();
|
||||
break;
|
||||
var resourceId = pasteFormat.SupportsClipboardFormats(AvailableClipboardFormats) ? "PasteError" : "ClipboardEmptyWarning";
|
||||
PasteOperationErrorText = ResourceLoaderInstance.ResourceLoader.GetString(resourceId);
|
||||
return;
|
||||
}
|
||||
|
||||
case PasteFormats.Markdown:
|
||||
ToMarkdownFunction();
|
||||
break;
|
||||
Busy = true;
|
||||
PasteOperationErrorText = string.Empty;
|
||||
Query = pasteFormat.Query;
|
||||
|
||||
case PasteFormats.Json:
|
||||
ToJsonFunction();
|
||||
break;
|
||||
if (pasteFormat.Format == PasteFormats.Custom)
|
||||
{
|
||||
SaveQuery(Query);
|
||||
}
|
||||
|
||||
case PasteFormats.Custom:
|
||||
Query = pasteFormat.Prompt;
|
||||
CustomActionActivated?.Invoke(this, new CustomActionActivatedEventArgs(pasteFormat.Prompt, false));
|
||||
break;
|
||||
try
|
||||
{
|
||||
// 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);
|
||||
|
||||
await delayTask;
|
||||
|
||||
if (pasteFormat.Format != PasteFormats.Custom)
|
||||
{
|
||||
HideWindow();
|
||||
|
||||
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);
|
||||
PasteOperationErrorText = ex is PasteActionException ? ex.Message : ResourceLoaderInstance.ResourceLoader.GetString("PasteError");
|
||||
}
|
||||
|
||||
Busy = false;
|
||||
}
|
||||
|
||||
internal async Task ExecutePasteFormatAsync(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 ExecuteCustomActionAsync(int customActionId, PasteActionSource source)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
await ReadClipboardAsync();
|
||||
|
||||
var customAction = _userSettings.CustomActions.FirstOrDefault(customAction => customAction.Id == customActionId);
|
||||
|
||||
if (customAction != null)
|
||||
{
|
||||
Query = customAction.Prompt;
|
||||
CustomActionActivated?.Invoke(this, new CustomActionActivatedEventArgs(customAction.Prompt, true));
|
||||
await ExecutePasteFormatAsync(CreatePasteFormat(customAction), source);
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task<string> GenerateCustomFunction(string inputInstructions)
|
||||
internal async Task GenerateCustomFunctionAsync(PasteActionSource triggerSource)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(inputInstructions) || !IsCustomAIEnabled)
|
||||
{
|
||||
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;
|
||||
AdvancedPasteCustomAction customAction = new() { Name = "Default", Prompt = Query };
|
||||
await ExecutePasteFormatAsync(CreatePasteFormat(customAction), triggerSource);
|
||||
}
|
||||
|
||||
internal void PasteCustomFunction(string text)
|
||||
private void HideWindow()
|
||||
{
|
||||
Logger.LogTrace();
|
||||
var mainWindow = GetMainWindow();
|
||||
|
||||
SetClipboardContentAndHideWindow(text);
|
||||
|
||||
if (_userSettings.SendPasteKeyCombination)
|
||||
if (mainWindow != null)
|
||||
{
|
||||
ClipboardHelper.SendPasteKeyCombination();
|
||||
Windows.Win32.Foundation.HWND hwnd = (Windows.Win32.Foundation.HWND)mainWindow.GetWindowHandle();
|
||||
Windows.Win32.PInvoke.ShowWindow(hwnd, Windows.Win32.UI.WindowsAndMessaging.SHOW_WINDOW_CMD.SW_HIDE);
|
||||
}
|
||||
}
|
||||
|
||||
@ -524,11 +457,7 @@ namespace AdvancedPaste.ViewModels
|
||||
return;
|
||||
}
|
||||
|
||||
string currentClipboardText = Task.Run(async () =>
|
||||
{
|
||||
string text = await clipboardData.GetTextAsync() as string;
|
||||
return text;
|
||||
}).Result;
|
||||
var currentClipboardText = Task.Run(async () => await clipboardData.GetTextAsync()).Result;
|
||||
|
||||
var queryData = new CustomQuery
|
||||
{
|
||||
@ -536,13 +465,13 @@ namespace AdvancedPaste.ViewModels
|
||||
ClipboardData = currentClipboardText,
|
||||
};
|
||||
|
||||
SettingsUtils utils = new SettingsUtils();
|
||||
SettingsUtils utils = new();
|
||||
utils.SaveSettings(queryData.ToString(), Constants.AdvancedPasteModuleName, Constants.LastQueryJsonFileName);
|
||||
}
|
||||
|
||||
internal CustomQuery LoadPreviousQuery()
|
||||
{
|
||||
SettingsUtils utils = new SettingsUtils();
|
||||
SettingsUtils utils = new();
|
||||
var query = utils.GetSettings<CustomQuery>(Constants.AdvancedPasteModuleName, Constants.LastQueryJsonFileName);
|
||||
return query;
|
||||
}
|
||||
@ -572,9 +501,9 @@ namespace AdvancedPaste.ViewModels
|
||||
|
||||
if (IsAllowedByGPO)
|
||||
{
|
||||
var oldKey = aiHelper.GetKey();
|
||||
var oldKey = _aiHelper.GetKey();
|
||||
var newKey = AICompletionsHelper.LoadOpenAIKey();
|
||||
aiHelper.SetOpenAIKey(newKey);
|
||||
_aiHelper.SetOpenAIKey(newKey);
|
||||
return newKey != oldKey;
|
||||
}
|
||||
|
||||
|
@ -42,6 +42,7 @@ namespace
|
||||
{
|
||||
const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
|
||||
const wchar_t JSON_KEY_CUSTOM_ACTIONS[] = L"custom-actions";
|
||||
const wchar_t JSON_KEY_ADDITIONAL_ACTIONS[] = L"additional-actions";
|
||||
const wchar_t JSON_KEY_SHORTCUT[] = L"shortcut";
|
||||
const wchar_t JSON_KEY_IS_SHOWN[] = L"isShown";
|
||||
const wchar_t JSON_KEY_ID[] = L"id";
|
||||
@ -73,7 +74,6 @@ private:
|
||||
|
||||
HANDLE m_hProcess;
|
||||
|
||||
std::thread create_pipe_thread;
|
||||
std::unique_ptr<CAtlFile> m_write_pipe;
|
||||
|
||||
// Time to wait for process to close after sending WM_CLOSE signal
|
||||
@ -86,8 +86,18 @@ private:
|
||||
Hotkey m_paste_as_markdown_hotkey{};
|
||||
Hotkey m_paste_as_json_hotkey{};
|
||||
|
||||
std::vector<Hotkey> m_custom_action_hotkeys;
|
||||
std::vector<int> m_custom_action_ids;
|
||||
template<class Id>
|
||||
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;
|
||||
|
||||
@ -166,6 +176,34 @@ private:
|
||||
open_ai_key_exists();
|
||||
}
|
||||
|
||||
static std::wstring kebab_to_pascal_case(const std::wstring& kebab_str)
|
||||
{
|
||||
std::wstring result;
|
||||
bool capitalize_next = true;
|
||||
|
||||
for (const auto ch : kebab_str)
|
||||
{
|
||||
if (ch == L'-')
|
||||
{
|
||||
capitalize_next = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (capitalize_next)
|
||||
{
|
||||
result += std::towupper(ch);
|
||||
capitalize_next = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
result += ch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool migrate_data_and_remove_data_file(Hotkey& old_paste_as_plain_hotkey)
|
||||
{
|
||||
const wchar_t OLD_JSON_KEY_ACTIVATION_SHORTCUT[] = L"ActivationShortcut";
|
||||
@ -197,6 +235,39 @@ private:
|
||||
return false;
|
||||
}
|
||||
|
||||
void process_additional_action(const winrt::hstring& actionName, const winrt::Windows::Data::Json::IJsonValue& actionValue)
|
||||
{
|
||||
if (actionValue.ValueType() != winrt::Windows::Data::Json::JsonValueType::Object)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const auto action = actionValue.GetObjectW();
|
||||
|
||||
if (!action.GetNamedBoolean(JSON_KEY_IS_SHOWN, false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.HasKey(JSON_KEY_SHORTCUT))
|
||||
{
|
||||
const AdditionalAction additionalAction
|
||||
{
|
||||
actionName.c_str(),
|
||||
parse_single_hotkey(action.GetNamedObject(JSON_KEY_SHORTCUT))
|
||||
};
|
||||
|
||||
m_additional_actions.push_back(additionalAction);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (const auto& [subActionName, subAction] : action)
|
||||
{
|
||||
process_additional_action(subActionName, subAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void parse_hotkeys(PowerToysSettings::PowerToyValues& settings)
|
||||
{
|
||||
auto settingsObject = settings.get_raw_json();
|
||||
@ -239,13 +310,23 @@ private:
|
||||
*hotkey = parse_single_hotkey(keyName, settingsObject);
|
||||
}
|
||||
|
||||
m_custom_action_hotkeys.clear();
|
||||
m_custom_action_ids.clear();
|
||||
m_additional_actions.clear();
|
||||
m_custom_actions.clear();
|
||||
|
||||
if (settingsObject.HasKey(JSON_KEY_PROPERTIES))
|
||||
{
|
||||
const auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
|
||||
|
||||
if (propertiesObject.HasKey(JSON_KEY_ADDITIONAL_ACTIONS))
|
||||
{
|
||||
const auto additionalActions = propertiesObject.GetNamedObject(JSON_KEY_ADDITIONAL_ACTIONS);
|
||||
|
||||
for (const auto& [actionName, additionalAction] : additionalActions)
|
||||
{
|
||||
process_additional_action(actionName, additionalAction);
|
||||
}
|
||||
}
|
||||
|
||||
if (propertiesObject.HasKey(JSON_KEY_CUSTOM_ACTIONS))
|
||||
{
|
||||
const auto customActions = propertiesObject.GetNamedObject(JSON_KEY_CUSTOM_ACTIONS).GetNamedArray(JSON_KEY_VALUE);
|
||||
@ -257,8 +338,13 @@ private:
|
||||
|
||||
if (object.GetNamedBoolean(JSON_KEY_IS_SHOWN, false))
|
||||
{
|
||||
m_custom_action_hotkeys.push_back(parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT)));
|
||||
m_custom_action_ids.push_back(static_cast<int>(object.GetNamedNumber(JSON_KEY_ID)));
|
||||
const CustomAction customActionData
|
||||
{
|
||||
static_cast<int>(object.GetNamedNumber(JSON_KEY_ID)),
|
||||
parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT))
|
||||
};
|
||||
|
||||
m_custom_actions.push_back(customActionData);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -331,7 +417,7 @@ private:
|
||||
return;
|
||||
}
|
||||
|
||||
create_pipe_thread = std::thread([&] { start_named_pipe_server(pipe_name.value()); });
|
||||
std::thread create_pipe_thread ([&]{ start_named_pipe_server(pipe_name.value()); });
|
||||
launch_process(pipe_name.value());
|
||||
create_pipe_thread.join();
|
||||
}
|
||||
@ -730,12 +816,19 @@ public:
|
||||
m_preview_custom_format_output = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_SHOW_CUSTOM_PREVIEW).GetNamedBoolean(JSON_KEY_VALUE);
|
||||
}
|
||||
|
||||
std::unordered_map<std::wstring, Hotkey> additionalActionMap;
|
||||
for (const auto& action : m_additional_actions)
|
||||
{
|
||||
additionalActionMap[kebab_to_pascal_case(action.id)] = action.hotkey;
|
||||
}
|
||||
|
||||
// order of args matter
|
||||
Trace::AdvancedPaste_SettingsTelemetry(m_paste_as_plain_hotkey,
|
||||
m_advanced_paste_ui_hotkey,
|
||||
m_paste_as_markdown_hotkey,
|
||||
m_paste_as_json_hotkey,
|
||||
m_preview_custom_format_output);
|
||||
m_preview_custom_format_output,
|
||||
additionalActionMap);
|
||||
|
||||
// If you don't need to do any custom processing of the settings, proceed
|
||||
// to persists the values calling:
|
||||
@ -825,11 +918,24 @@ public:
|
||||
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);
|
||||
|
||||
Trace::AdvancedPaste_Invoked(std::format(L"{}Direct", kebab_to_pascal_case(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);
|
||||
|
||||
@ -844,7 +950,7 @@ public:
|
||||
|
||||
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)
|
||||
{
|
||||
@ -852,9 +958,11 @@ public:
|
||||
m_advanced_paste_ui_hotkey,
|
||||
m_paste_as_markdown_hotkey,
|
||||
m_paste_as_json_hotkey };
|
||||
|
||||
std::copy(default_hotkeys.begin(), default_hotkeys.end(), hotkeys);
|
||||
std::copy(m_custom_action_hotkeys.begin(), m_custom_action_hotkeys.end(), hotkeys + NUM_DEFAULT_HOTKEYS);
|
||||
|
||||
const auto get_action_hotkey = [](const auto& action) { return action.hotkey; };
|
||||
std::transform(m_additional_actions.begin(), m_additional_actions.end(), hotkeys + NUM_DEFAULT_HOTKEYS, get_action_hotkey);
|
||||
std::transform(m_custom_actions.begin(), m_custom_actions.end(), hotkeys + NUM_DEFAULT_HOTKEYS + m_additional_actions.size(), get_action_hotkey);
|
||||
}
|
||||
|
||||
return num_hotkeys;
|
||||
|
@ -58,45 +58,44 @@ void Trace::AdvancedPaste_SettingsTelemetry(const PowertoyModuleIface::Hotkey& p
|
||||
const PowertoyModuleIface::Hotkey& advancedPasteUIHotkey,
|
||||
const PowertoyModuleIface::Hotkey& pasteMarkdownHotkey,
|
||||
const PowertoyModuleIface::Hotkey& pasteJsonHotkey,
|
||||
const bool preview_custom_format_output) noexcept
|
||||
const bool preview_custom_format_output,
|
||||
const std::unordered_map<std::wstring, PowertoyModuleIface::Hotkey>& additionalActionsHotkeys) noexcept
|
||||
{
|
||||
std::wstring pastePlainHotkeyStr =
|
||||
std::wstring(pastePlainHotkey.win ? L"Win + " : L"") +
|
||||
std::wstring(pastePlainHotkey.ctrl ? L"Ctrl + " : L"") +
|
||||
std::wstring(pastePlainHotkey.shift ? L"Shift + " : L"") +
|
||||
std::wstring(pastePlainHotkey.alt ? L"Alt + " : L"") +
|
||||
std::wstring(L"VK ") + std::to_wstring(pastePlainHotkey.key);
|
||||
const auto getHotKeyStr = [](const PowertoyModuleIface::Hotkey& hotKey)
|
||||
{
|
||||
return std::wstring(hotKey.win ? L"Win + " : L"") +
|
||||
std::wstring(hotKey.ctrl ? L"Ctrl + " : L"") +
|
||||
std::wstring(hotKey.shift ? L"Shift + " : L"") +
|
||||
std::wstring(hotKey.alt ? L"Alt + " : L"") +
|
||||
std::wstring(L"VK ") + std::to_wstring(hotKey.key);
|
||||
};
|
||||
|
||||
std::wstring advancedPasteUIHotkeyStr =
|
||||
std::wstring(advancedPasteUIHotkey.win ? L"Win + " : L"") +
|
||||
std::wstring(advancedPasteUIHotkey.ctrl ? L"Ctrl + " : L"") +
|
||||
std::wstring(advancedPasteUIHotkey.shift ? L"Shift + " : L"") +
|
||||
std::wstring(advancedPasteUIHotkey.alt ? L"Alt + " : L"") +
|
||||
std::wstring(L"VK ") + std::to_wstring(advancedPasteUIHotkey.key);
|
||||
std::vector<std::wstring> hotkeyStrs;
|
||||
const auto getHotkeyCStr = [&](const PowertoyModuleIface::Hotkey& hotkey)
|
||||
{
|
||||
hotkeyStrs.push_back(getHotKeyStr(hotkey)); // Probably unnecessary, but offers protection against the macro TraceLoggingWideString expanding to something that would invalidate the pointer
|
||||
return hotkeyStrs.back().c_str();
|
||||
};
|
||||
|
||||
std::wstring pasteMarkdownHotkeyStr =
|
||||
std::wstring(pasteMarkdownHotkey.win ? L"Win + " : L"") +
|
||||
std::wstring(pasteMarkdownHotkey.ctrl ? L"Ctrl + " : L"") +
|
||||
std::wstring(pasteMarkdownHotkey.shift ? L"Shift + " : L"") +
|
||||
std::wstring(pasteMarkdownHotkey.alt ? L"Alt + " : L"") +
|
||||
std::wstring(L"VK ") + std::to_wstring(pasteMarkdownHotkey.key);
|
||||
|
||||
std::wstring pasteJsonHotkeyStr =
|
||||
std::wstring(pasteJsonHotkey.win ? L"Win + " : L"") +
|
||||
std::wstring(pasteJsonHotkey.ctrl ? L"Ctrl + " : L"") +
|
||||
std::wstring(pasteJsonHotkey.shift ? L"Shift + " : L"") +
|
||||
std::wstring(pasteJsonHotkey.alt ? L"Alt + " : L"") +
|
||||
std::wstring(L"VK ") + std::to_wstring(pasteJsonHotkey.key);
|
||||
const auto getAdditionalActionHotkeyCStr = [&](const std::wstring& name)
|
||||
{
|
||||
const auto it = additionalActionsHotkeys.find(name);
|
||||
return it != additionalActionsHotkeys.end() ? getHotkeyCStr(it->second) : L"";
|
||||
};
|
||||
|
||||
TraceLoggingWrite(
|
||||
g_hProvider,
|
||||
"AdvancedPaste_Settings",
|
||||
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
|
||||
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
|
||||
TraceLoggingWideString(pastePlainHotkeyStr.c_str(), "PastePlainHotkey"),
|
||||
TraceLoggingWideString(advancedPasteUIHotkeyStr.c_str(), "AdvancedPasteUIHotkey"),
|
||||
TraceLoggingWideString(pasteMarkdownHotkeyStr.c_str(), "PasteMarkdownHotkey"),
|
||||
TraceLoggingWideString(pasteJsonHotkeyStr.c_str(), "PasteJsonHotkey"),
|
||||
TraceLoggingBoolean(preview_custom_format_output, "ShowCustomPreview")
|
||||
TraceLoggingWideString(getHotkeyCStr(pastePlainHotkey), "PastePlainHotkey"),
|
||||
TraceLoggingWideString(getHotkeyCStr(advancedPasteUIHotkey), "AdvancedPasteUIHotkey"),
|
||||
TraceLoggingWideString(getHotkeyCStr(pasteMarkdownHotkey), "PasteMarkdownHotkey"),
|
||||
TraceLoggingWideString(getHotkeyCStr(pasteJsonHotkey), "PasteJsonHotkey"),
|
||||
TraceLoggingBoolean(preview_custom_format_output, "ShowCustomPreview"),
|
||||
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"ImageToText"), "ImageToTextHotkey"),
|
||||
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"PasteAsTxtFile"), "PasteAsTxtFileHotkey"),
|
||||
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"PasteAsPngFile"), "PasteAsPngFileHotkey"),
|
||||
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"PasteAsHtmlFile"), "PasteAsHtmlFileHotkey")
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include <interface/powertoy_module_interface.h>
|
||||
#include <unordered_map>
|
||||
|
||||
class Trace
|
||||
{
|
||||
@ -21,5 +22,6 @@ public:
|
||||
const PowertoyModuleIface::Hotkey& advancedPasteUIHotkey,
|
||||
const PowertoyModuleIface::Hotkey& pasteMarkdownHotkey,
|
||||
const PowertoyModuleIface::Hotkey& pasteJsonHotkey,
|
||||
const bool preview_custom_format_output) noexcept;
|
||||
const bool preview_custom_format_output,
|
||||
const std::unordered_map<std::wstring, PowertoyModuleIface::Hotkey>& additionalActionsHotkeys) noexcept;
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
// 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 ImageToText = "image-to-text";
|
||||
public const string PasteAsFile = "paste-as-file";
|
||||
}
|
||||
|
||||
[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[] { ImageToText, PasteAsFile }.Concat(PasteAsFile.SubActions);
|
||||
}
|
@ -3,14 +3,13 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
public sealed class AdvancedPasteCustomAction : INotifyPropertyChanged, ICloneable
|
||||
public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction, ICloneable
|
||||
{
|
||||
private int _id;
|
||||
private string _name = string.Empty;
|
||||
@ -25,14 +24,7 @@ public sealed class AdvancedPasteCustomAction : INotifyPropertyChanged, ICloneab
|
||||
public int Id
|
||||
{
|
||||
get => _id;
|
||||
set
|
||||
{
|
||||
if (_id != value)
|
||||
{
|
||||
_id = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
set => Set(ref _id, value);
|
||||
}
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
@ -41,10 +33,8 @@ public sealed class AdvancedPasteCustomAction : INotifyPropertyChanged, ICloneab
|
||||
get => _name;
|
||||
set
|
||||
{
|
||||
if (_name != value)
|
||||
if (Set(ref _name, value))
|
||||
{
|
||||
_name = value;
|
||||
OnPropertyChanged();
|
||||
UpdateIsValid();
|
||||
}
|
||||
}
|
||||
@ -56,10 +46,8 @@ public sealed class AdvancedPasteCustomAction : INotifyPropertyChanged, ICloneab
|
||||
get => _prompt;
|
||||
set
|
||||
{
|
||||
if (_prompt != value)
|
||||
if (Set(ref _prompt, value))
|
||||
{
|
||||
_prompt = value;
|
||||
OnPropertyChanged();
|
||||
UpdateIsValid();
|
||||
}
|
||||
}
|
||||
@ -86,62 +74,30 @@ public sealed class AdvancedPasteCustomAction : INotifyPropertyChanged, ICloneab
|
||||
public bool IsShown
|
||||
{
|
||||
get => _isShown;
|
||||
set
|
||||
{
|
||||
if (_isShown != value)
|
||||
{
|
||||
_isShown = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
set => Set(ref _isShown, value);
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public bool CanMoveUp
|
||||
{
|
||||
get => _canMoveUp;
|
||||
set
|
||||
{
|
||||
if (_canMoveUp != value)
|
||||
{
|
||||
_canMoveUp = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
set => Set(ref _canMoveUp, value);
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public bool CanMoveDown
|
||||
{
|
||||
get => _canMoveDown;
|
||||
set
|
||||
{
|
||||
if (_canMoveDown != value)
|
||||
{
|
||||
_canMoveDown = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
set => Set(ref _canMoveDown, value);
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsValid
|
||||
{
|
||||
get => _isValid;
|
||||
private set
|
||||
{
|
||||
if (_isValid != value)
|
||||
{
|
||||
_isValid = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
private set => Set(ref _isValid, value);
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
public string ToJsonString() => JsonSerializer.Serialize(this);
|
||||
|
||||
public object Clone()
|
||||
{
|
||||
AdvancedPasteCustomAction clone = new();
|
||||
@ -160,11 +116,6 @@ public sealed class AdvancedPasteCustomAction : INotifyPropertyChanged, ICloneab
|
||||
CanMoveDown = other.CanMoveDown;
|
||||
}
|
||||
|
||||
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
private HotkeySettings GetShortcutClone()
|
||||
{
|
||||
object shortcut = null;
|
||||
|
@ -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];
|
||||
}
|
@ -22,6 +22,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
PasteAsMarkdownShortcut = new();
|
||||
PasteAsJsonShortcut = new();
|
||||
CustomActions = new();
|
||||
AdditionalActions = new();
|
||||
ShowCustomPreview = true;
|
||||
SendPasteKeyCombination = true;
|
||||
CloseAfterLosingFocus = false;
|
||||
@ -51,7 +52,11 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
[JsonPropertyName("custom-actions")]
|
||||
[CmdConfigureIgnoreAttribute]
|
||||
public AdvancedPasteCustomActions CustomActions { get; set; }
|
||||
public AdvancedPasteCustomActions CustomActions { get; init; }
|
||||
|
||||
[JsonPropertyName("additional-actions")]
|
||||
[CmdConfigureIgnoreAttribute]
|
||||
public AdvancedPasteAdditionalActions AdditionalActions { get; init; }
|
||||
|
||||
public override string ToString()
|
||||
=> JsonSerializer.Serialize(this);
|
||||
|
@ -11,17 +11,19 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Helpers
|
||||
{
|
||||
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))
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
storage = value;
|
||||
OnPropertyChanged(propertyName);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
protected void OnPropertyChanged([CallerMemberName] string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
|
12
src/settings-ui/Settings.UI.Library/IAdvancedPasteAction.cs
Normal file
12
src/settings-ui/Settings.UI.Library/IAdvancedPasteAction.cs
Normal 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; }
|
||||
}
|
@ -24,6 +24,18 @@
|
||||
<ImageSource x:Key="DialogHeaderImage">ms-appx:///Assets/Settings/Modules/APDialog.light.png</ImageSource>
|
||||
</ResourceDictionary>
|
||||
</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>
|
||||
</Page.Resources>
|
||||
<Grid>
|
||||
@ -129,6 +141,7 @@
|
||||
AllowDisable="True"
|
||||
HotkeySettings="{x:Bind Path=ViewModel.PasteAsJsonShortcut, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
|
||||
<ItemsControl
|
||||
x:Name="CustomActions"
|
||||
x:Uid="CustomActions"
|
||||
@ -155,7 +168,7 @@
|
||||
HotkeySettings="{x:Bind Path=Shortcut, Mode=TwoWay}" />
|
||||
<ToggleSwitch
|
||||
x:Uid="Enable_CustomAction"
|
||||
AutomationProperties.HelpText="{x:Bind Name}"
|
||||
AutomationProperties.HelpText="{x:Bind Name, Mode=OneWay}"
|
||||
IsOn="{x:Bind IsShown, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
OffContent=""
|
||||
OnContent="" />
|
||||
@ -202,6 +215,56 @@
|
||||
IsTabStop="{x:Bind ViewModel.IsConflictingCopyShortcut, Mode=OneWay}"
|
||||
Severity="Warning" />
|
||||
</controls:SettingsGroup>
|
||||
|
||||
<controls:SettingsGroup x:Uid="AdvancedPaste_Additional_Actions_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
||||
<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, Mode=OneWay}"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
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}"
|
||||
IsEnabled="{x:Bind ViewModel.AdditionalActions.PasteAsFile.IsShown, Mode=OneWay}">
|
||||
<ContentControl ContentTemplate="{StaticResource AdditionalActionTemplate}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard
|
||||
x:Uid="PasteAsPngFile"
|
||||
DataContext="{Binding PasteAsPngFile, Mode=TwoWay}"
|
||||
IsEnabled="{x:Bind ViewModel.AdditionalActions.PasteAsFile.IsShown, Mode=OneWay}">
|
||||
<ContentControl ContentTemplate="{StaticResource AdditionalActionTemplate}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard
|
||||
x:Uid="PasteAsHtmlFile"
|
||||
DataContext="{Binding PasteAsHtmlFile, Mode=TwoWay}"
|
||||
IsEnabled="{x:Bind ViewModel.AdditionalActions.PasteAsFile.IsShown, Mode=OneWay}">
|
||||
<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>
|
||||
</controls:SettingsPageControl.ModuleContent>
|
||||
<controls:SettingsPageControl.PrimaryLinks>
|
||||
|
@ -725,6 +725,9 @@
|
||||
<data name="AdvancedPaste_Direct_Access_Hotkeys_GroupSettings.Header" xml:space="preserve">
|
||||
<value>Actions</value>
|
||||
</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">
|
||||
<value>Current Key Remappings</value>
|
||||
</data>
|
||||
@ -2046,6 +2049,21 @@ Made with 💗 by Microsoft and the PowerToys community.</value>
|
||||
<data name="PasteAsCustom_Shortcut.Header" xml:space="preserve">
|
||||
<value>Paste as Custom with AI directly</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">
|
||||
<value>OpenAI API key:</value>
|
||||
</data>
|
||||
|
@ -38,6 +38,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
private readonly object _delayedActionLock = new object();
|
||||
|
||||
private readonly AdvancedPasteSettings _advancedPasteSettings;
|
||||
private readonly AdvancedPasteAdditionalActions _additionalActions;
|
||||
private readonly ObservableCollection<AdvancedPasteCustomAction> _customActions;
|
||||
private Timer _delayedTimer;
|
||||
|
||||
@ -69,6 +70,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
_advancedPasteSettings = advancedPasteSettingsRepository.SettingsConfig;
|
||||
|
||||
_additionalActions = _advancedPasteSettings.Properties.AdditionalActions;
|
||||
_customActions = _advancedPasteSettings.Properties.CustomActions.Value;
|
||||
|
||||
InitializeEnabledValue();
|
||||
@ -81,6 +83,11 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
_delayedTimer.Elapsed += DelayedTimer_Tick;
|
||||
_delayedTimer.AutoReset = false;
|
||||
|
||||
foreach (var action in _additionalActions.AllActions)
|
||||
{
|
||||
action.PropertyChanged += OnAdditionalActionPropertyChanged;
|
||||
}
|
||||
|
||||
foreach (var customAction in _customActions)
|
||||
{
|
||||
customAction.PropertyChanged += OnCustomActionPropertyChanged;
|
||||
@ -143,6 +150,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
public ObservableCollection<AdvancedPasteCustomAction> CustomActions => _customActions;
|
||||
|
||||
public AdvancedPasteAdditionalActions AdditionalActions => _additionalActions;
|
||||
|
||||
private bool OpenAIKeyExists()
|
||||
{
|
||||
PasswordVault vault = new PasswordVault();
|
||||
@ -336,12 +345,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsConflictingCopyShortcut
|
||||
{
|
||||
get => _customActions.Select(customAction => customAction.Shortcut)
|
||||
.Concat([PasteAsPlainTextShortcut, AdvancedPasteUIShortcut, PasteAsMarkdownShortcut, PasteAsJsonShortcut])
|
||||
.Any(hotkey => WarnHotkeys.Contains(hotkey.ToString()));
|
||||
}
|
||||
public bool IsConflictingCopyShortcut =>
|
||||
_customActions.Select(customAction => customAction.Shortcut)
|
||||
.Concat([PasteAsPlainTextShortcut, AdvancedPasteUIShortcut, PasteAsMarkdownShortcut, PasteAsJsonShortcut])
|
||||
.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)
|
||||
{
|
||||
@ -461,6 +474,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
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)
|
||||
{
|
||||
if (typeof(AdvancedPasteCustomAction).GetProperty(e.PropertyName).GetCustomAttribute<JsonIgnoreAttribute>() == null)
|
||||
|
Loading…
Reference in New Issue
Block a user