From 2a8e211cfd1d329b35fad043e5d476c47ce36fab Mon Sep 17 00:00:00 2001
From: Ani <115020168+drawbyperpetual@users.noreply.github.com>
Date: Thu, 22 Aug 2024 16:17:12 +0200
Subject: [PATCH] [AdvancedPaste] Custom Actions (#34395)
* [AdvancedPaste] Custom Actions
* Renamed pipe name to make spellcheck happy
* Improved settings page for Custom Actions
* UI improvements, disabled standard paste actions when no clipboard text, update clipboard text and gpo state every second
* Bug fixes, single query/prompt box, Ctrl+num shortcuts for custom actions, error box
* Spellcheck issue
* Bug fixes and used Advanced Paste Window as wait indicator for keyboard shortcuts
* Improvements to PromptBox, incluing show error message as tooltip
* Refactoring
* Fixed issue where ESC sometimes didn't close paste window
* Update src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
Co-authored-by: Stefan Markovic <57057282+stefansjfw@users.noreply.github.com>
---------
Co-authored-by: Stefan Markovic <57057282+stefansjfw@users.noreply.github.com>
---
src/common/interop/Constants.cpp | 16 +-
src/common/interop/Constants.h | 7 +-
src/common/interop/Constants.idl | 7 +-
src/common/interop/shared_constants.h | 10 +-
.../AdvancedPasteXAML/App.xaml.cs | 83 ++++-
.../AdvancedPasteXAML/Controls/PromptBox.xaml | 109 ++++---
.../Controls/PromptBox.xaml.cs | 41 +--
.../Converters/CountToDoubleConverter.cs | 25 ++
.../Converters/CountToVisibilityConverter.cs | 33 ++
.../Converters/ListViewIndexConverter.cs | 32 --
.../AdvancedPasteXAML/MainWindow.xaml.cs | 12 +
.../AdvancedPasteXAML/Pages/MainPage.xaml | 147 ++++++---
.../AdvancedPasteXAML/Pages/MainPage.xaml.cs | 90 ++----
.../AdvancedPaste/Helpers/IUserSettings.cs | 5 +
.../Helpers/NamedPipeProcessor.cs | 37 +++
.../Helpers/NativeEventWaiter.cs | 29 --
.../AdvancedPaste/Helpers/UserSettings.cs | 76 ++++-
.../Models/CustomActionActivatedEventArgs.cs | 14 +
.../AdvancedPaste/Models/PasteFormat.cs | 38 ++-
.../Strings/en-us/Resources.resw | 10 +-
.../ViewModels/OptionsViewModel.cs | 293 +++++++++++++----
.../AdvancedPasteModuleInterface/dllmain.cpp | 300 +++++++++++++-----
.../AdvancedPasteCustomAction.cs | 183 +++++++++++
.../AdvancedPasteCustomActions.cs | 26 ++
.../AdvancedPasteProperties.cs | 5 +
.../ShortcutControl/ShortcutControl.xaml.cs | 34 +-
.../SettingsXAML/Views/AdvancedPaste.xaml | 142 +++++++--
.../SettingsXAML/Views/AdvancedPaste.xaml.cs | 79 ++++-
.../Settings.UI/Strings/en-us/Resources.resw | 36 ++-
.../ViewModels/AdvancedPasteViewModel.cs | 153 ++++++++-
30 files changed, 1581 insertions(+), 491 deletions(-)
create mode 100644 src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Converters/CountToDoubleConverter.cs
create mode 100644 src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Converters/CountToVisibilityConverter.cs
delete mode 100644 src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Converters/ListViewIndexConverter.cs
create mode 100644 src/modules/AdvancedPaste/AdvancedPaste/Helpers/NamedPipeProcessor.cs
delete mode 100644 src/modules/AdvancedPaste/AdvancedPaste/Helpers/NativeEventWaiter.cs
create mode 100644 src/modules/AdvancedPaste/AdvancedPaste/Models/CustomActionActivatedEventArgs.cs
create mode 100644 src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs
create mode 100644 src/settings-ui/Settings.UI.Library/AdvancedPasteCustomActions.cs
diff --git a/src/common/interop/Constants.cpp b/src/common/interop/Constants.cpp
index 57ebaa5661..30969a75b4 100644
--- a/src/common/interop/Constants.cpp
+++ b/src/common/interop/Constants.cpp
@@ -51,17 +51,21 @@ namespace winrt::PowerToys::Interop::implementation
{
return CommonSharedConstants::SHOW_COLOR_PICKER_SHARED_EVENT;
}
- hstring Constants::ShowAdvancedPasteSharedEvent()
+ hstring Constants::AdvancedPasteShowUIMessage()
{
- return CommonSharedConstants::SHOW_ADVANCED_PASTE_SHARED_EVENT;
+ return CommonSharedConstants::ADVANCED_PASTE_SHOW_UI_MESSAGE;
}
- hstring Constants::AdvancedPasteMarkdownEvent()
+ hstring Constants::AdvancedPasteMarkdownMessage()
{
- return CommonSharedConstants::ADVANCED_PASTE_MARKDOWN_EVENT;
+ return CommonSharedConstants::ADVANCED_PASTE_MARKDOWN_MESSAGE;
}
- hstring Constants::AdvancedPasteJsonEvent()
+ hstring Constants::AdvancedPasteJsonMessage()
{
- return CommonSharedConstants::ADVANCED_PASTE_JSON_EVENT;
+ return CommonSharedConstants::ADVANCED_PASTE_JSON_MESSAGE;
+ }
+ hstring Constants::AdvancedPasteCustomActionMessage()
+ {
+ return CommonSharedConstants::ADVANCED_PASTE_CUSTOM_ACTION_MESSAGE;
}
hstring Constants::ShowPowerOCRSharedEvent()
{
diff --git a/src/common/interop/Constants.h b/src/common/interop/Constants.h
index 319c6b76b3..cc8ebc01b1 100644
--- a/src/common/interop/Constants.h
+++ b/src/common/interop/Constants.h
@@ -16,9 +16,10 @@ namespace winrt::PowerToys::Interop::implementation
static hstring FZEToggleEvent();
static hstring ColorPickerSendSettingsTelemetryEvent();
static hstring ShowColorPickerSharedEvent();
- static hstring ShowAdvancedPasteSharedEvent();
- static hstring AdvancedPasteMarkdownEvent();
- static hstring AdvancedPasteJsonEvent();
+ static hstring AdvancedPasteShowUIMessage();
+ static hstring AdvancedPasteMarkdownMessage();
+ static hstring AdvancedPasteJsonMessage();
+ static hstring AdvancedPasteCustomActionMessage();
static hstring ShowPowerOCRSharedEvent();
static hstring MouseJumpShowPreviewEvent();
static hstring AwakeExitEvent();
diff --git a/src/common/interop/Constants.idl b/src/common/interop/Constants.idl
index 0f33274a93..72d9fc58a0 100644
--- a/src/common/interop/Constants.idl
+++ b/src/common/interop/Constants.idl
@@ -13,9 +13,10 @@ namespace PowerToys
static String FZEToggleEvent();
static String ColorPickerSendSettingsTelemetryEvent();
static String ShowColorPickerSharedEvent();
- static String ShowAdvancedPasteSharedEvent();
- static String AdvancedPasteMarkdownEvent();
- static String AdvancedPasteJsonEvent();
+ static String AdvancedPasteShowUIMessage();
+ static String AdvancedPasteMarkdownMessage();
+ static String AdvancedPasteJsonMessage();
+ static String AdvancedPasteCustomActionMessage();
static String ShowPowerOCRSharedEvent();
static String MouseJumpShowPreviewEvent();
static String AwakeExitEvent();
diff --git a/src/common/interop/shared_constants.h b/src/common/interop/shared_constants.h
index 675b6f7830..c5d44524c9 100644
--- a/src/common/interop/shared_constants.h
+++ b/src/common/interop/shared_constants.h
@@ -25,12 +25,14 @@ namespace CommonSharedConstants
const wchar_t COLOR_PICKER_SEND_SETTINGS_TELEMETRY_EVENT[] = L"Local\\ColorPickerSettingsTelemetryEvent-6c7071d8-4014-46ec-b687-913bd8a422f1";
- // Path to the event used to show Advanced Paste UI
- const wchar_t SHOW_ADVANCED_PASTE_SHARED_EVENT[] = L"Local\\ShowAdvancedPasteEvent-9a46be2a-3e05-4186-b56b-4ae986ef2526";
+ // IPC Messages used in Advanced Paste
+ const wchar_t ADVANCED_PASTE_SHOW_UI_MESSAGE[] = L"ShowUI";
- const wchar_t ADVANCED_PASTE_MARKDOWN_EVENT[] = L"Local\\AdvancedPasteJsonEvent-a18c0798-3ee6-4fc5-bb9f-114c57ac0d47";
+ const wchar_t ADVANCED_PASTE_MARKDOWN_MESSAGE[] = L"PasteMarkdown";
- const wchar_t ADVANCED_PASTE_JSON_EVENT[] = L"Local\\AdvancedPasteJsonEvent-9ed021ab-b711-4cf3-9f33-135a698a9d21";
+ const wchar_t ADVANCED_PASTE_JSON_MESSAGE[] = L"PasteJson";
+
+ const wchar_t ADVANCED_PASTE_CUSTOM_ACTION_MESSAGE[] = L"CustomAction";
// Path to the event used to show Color Picker
const wchar_t SHOW_COLOR_PICKER_SHARED_EVENT[] = L"Local\\ShowColorPickerEvent-8c46be2a-3e05-4186-b56b-4ae986ef2525";
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs
index c0224a0c55..e7cefe644f 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs
@@ -3,6 +3,10 @@
// See the LICENSE file in the project root for more information.
using System;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
using AdvancedPaste.Helpers;
using AdvancedPaste.Settings;
using AdvancedPaste.ViewModels;
@@ -14,6 +18,7 @@ using Microsoft.UI.Xaml;
using Windows.Graphics;
using WinUIEx;
using static AdvancedPaste.Helpers.NativeMethods;
+using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
@@ -26,12 +31,13 @@ namespace AdvancedPaste
{
public IHost Host { get; private set; }
+ private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
+ private readonly OptionsViewModel viewModel;
+
private MainWindow window;
private nint windowHwnd;
- private OptionsViewModel viewModel;
-
private bool disposedValue;
///
@@ -74,7 +80,7 @@ namespace AdvancedPaste
/// Invoked when the application is launched.
///
/// Details about the launch request and process.
- protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
+ protected override void OnLaunched(LaunchActivatedEventArgs args)
{
var cmdArgs = Environment.GetCommandLineArgs();
if (cmdArgs?.Length > 1)
@@ -88,9 +94,44 @@ namespace AdvancedPaste
}
}
- NativeEventWaiter.WaitForEventLoop(PowerToys.Interop.Constants.ShowAdvancedPasteSharedEvent(), OnAdvancedPasteHotkey);
- NativeEventWaiter.WaitForEventLoop(PowerToys.Interop.Constants.AdvancedPasteMarkdownEvent(), OnAdvancedPasteMarkdownHotkey);
- NativeEventWaiter.WaitForEventLoop(PowerToys.Interop.Constants.AdvancedPasteJsonEvent(), OnAdvancedPasteJsonHotkey);
+ if (cmdArgs?.Length > 2)
+ {
+ ProcessNamedPipe(cmdArgs[2]);
+ }
+ }
+
+ private void ProcessNamedPipe(string pipeName)
+ {
+ void OnMessage(string message) => _dispatcherQueue.TryEnqueue(() => OnNamedPipeMessage(message));
+
+ Task.Run(async () =>
+ {
+ var connectTimeout = TimeSpan.FromSeconds(10);
+ await NamedPipeProcessor.ProcessNamedPipeAsync(pipeName, connectTimeout, OnMessage, CancellationToken.None);
+ });
+ }
+
+ private void OnNamedPipeMessage(string message)
+ {
+ var messageParts = message.Split();
+ var messageType = messageParts.First();
+
+ if (messageType == PowerToys.Interop.Constants.AdvancedPasteShowUIMessage())
+ {
+ OnAdvancedPasteHotkey();
+ }
+ else if (messageType == PowerToys.Interop.Constants.AdvancedPasteMarkdownMessage())
+ {
+ OnAdvancedPasteMarkdownHotkey();
+ }
+ else if (messageType == PowerToys.Interop.Constants.AdvancedPasteJsonMessage())
+ {
+ OnAdvancedPasteJsonHotkey();
+ }
+ else if (messageType == PowerToys.Interop.Constants.AdvancedPasteCustomActionMessage())
+ {
+ OnAdvancedPasteCustomActionHotkey(messageParts);
+ }
}
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
@@ -100,17 +141,43 @@ namespace AdvancedPaste
private void OnAdvancedPasteJsonHotkey()
{
- viewModel.GetClipboardData();
+ viewModel.ReadClipboard();
viewModel.ToJsonFunction(true);
}
private void OnAdvancedPasteMarkdownHotkey()
{
- viewModel.GetClipboardData();
+ viewModel.ReadClipboard();
viewModel.ToMarkdownFunction(true);
}
private void OnAdvancedPasteHotkey()
+ {
+ ShowWindow();
+ }
+
+ private void OnAdvancedPasteCustomActionHotkey(string[] messageParts)
+ {
+ if (messageParts.Length != 2)
+ {
+ Logger.LogWarning("Unexpected custom action message");
+ }
+ else
+ {
+ if (!int.TryParse(messageParts[1], CultureInfo.InvariantCulture, out int customActionId))
+ {
+ Logger.LogWarning($"Unexpected custom action message id {messageParts[1]}");
+ }
+ else
+ {
+ ShowWindow();
+ viewModel.ReadClipboard();
+ viewModel.ExecuteCustomActionWithPaste(customActionId);
+ }
+ }
+ }
+
+ private void ShowWindow()
{
viewModel.OnShow();
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml
index e139ac4c31..27891312ac 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml
+++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml
@@ -3,10 +3,11 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:animations="using:CommunityToolkit.WinUI.Animations"
- xmlns:converters="using:CommunityToolkit.WinUI.Converters"
+ xmlns:converters="using:AdvancedPaste.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:AdvancedPaste.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters"
xmlns:ui="using:CommunityToolkit.WinUI"
mc:Ignorable="d">
@@ -323,7 +324,12 @@
-
+
+
+
@@ -340,13 +346,12 @@
x:Name="InputTxtBox"
HorizontalAlignment="Stretch"
x:FieldModifier="public"
- IsEnabled="{x:Bind ViewModel.IsCustomAIEnabled, Mode=OneWay}"
+ IsEnabled="{x:Bind ViewModel.IsClipboardDataText, Mode=OneWay}"
KeyDown="InputTxtBox_KeyDown"
PlaceholderText="{x:Bind ViewModel.InputTxtBoxPlaceholderText, Mode=OneWay}"
Style="{StaticResource CustomTextBoxStyle}"
TabIndex="0"
- Text="{x:Bind Prompt, Mode=TwoWay}"
- TextChanging="InputTxtBox_TextChanging">
+ Text="{x:Bind ViewModel.Query, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
@@ -531,48 +536,63 @@
-->
-
-
+ VerticalAlignment="Stretch">
+
+
+
+
+
+
+
+
+ Text="{x:Bind ViewModel.ApiErrorText, Mode=OneWay}" />
-
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml.cs
index 01e4db575f..457052d619 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml.cs
@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System;
using System.Net;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
@@ -18,23 +19,14 @@ 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 static readonly DependencyProperty PromptProperty = DependencyProperty.Register(
- nameof(Prompt),
- typeof(string),
- typeof(PromptBox),
- new PropertyMetadata(defaultValue: string.Empty));
-
public OptionsViewModel ViewModel { get; private set; }
- public string Prompt
- {
- get => (string)GetValue(PromptProperty);
- set => SetValue(PromptProperty, value);
- }
-
public static readonly DependencyProperty PlaceholderTextProperty = DependencyProperty.Register(
nameof(PlaceholderText),
typeof(string),
@@ -66,6 +58,7 @@ namespace AdvancedPaste.Controls
_userSettings = App.GetService();
ViewModel = App.GetService();
+ ViewModel.CustomActionActivated += (_, e) => GenerateCustom(e.ForcePasteCustom);
}
private void Grid_Loaded(object sender, RoutedEventArgs e)
@@ -74,27 +67,30 @@ namespace AdvancedPaste.Controls
}
[RelayCommand]
- private void GenerateCustom()
+ private void GenerateCustom() => GenerateCustom(false);
+
+ private void GenerateCustom(bool forcePasteCustom)
{
Logger.LogTrace();
VisualStateManager.GoToState(this, "LoadingState", true);
- string inputInstructions = InputTxtBox.Text;
+ string inputInstructions = ViewModel.Query;
ViewModel.SaveQuery(inputInstructions);
var customFormatTask = ViewModel.GenerateCustomFunction(inputInstructions);
-
- customFormatTask.ContinueWith(
- t =>
+ var delayTask = forcePasteCustom ? Task.Delay(MinTaskTime) : Task.CompletedTask;
+ Task.WhenAll(customFormatTask, delayTask)
+ .ContinueWith(
+ _ =>
{
_dispatcherQueue.TryEnqueue(() =>
{
- ViewModel.CustomFormatResult = t.Result;
+ ViewModel.CustomFormatResult = customFormatTask.Result;
if (ViewModel.ApiRequestStatus == (int)HttpStatusCode.OK)
{
VisualStateManager.GoToState(this, "DefaultState", true);
- if (_userSettings.ShowCustomPreview)
+ if (_userSettings.ShowCustomPreview && !forcePasteCustom)
{
PreviewGrid.Width = InputTxtBox.ActualWidth;
PreviewFlyout.ShowAt(InputTxtBox);
@@ -130,14 +126,9 @@ namespace AdvancedPaste.Controls
ClipboardHelper.SetClipboardTextContent(lastQuery.ClipboardData);
}
- private void InputTxtBox_TextChanging(Microsoft.UI.Xaml.Controls.TextBox sender, TextBoxTextChangingEventArgs args)
- {
- SendBtn.Visibility = InputTxtBox.Text.Length > 0 ? Visibility.Visible : Visibility.Collapsed;
- }
-
private void InputTxtBox_KeyDown(object sender, Microsoft.UI.Xaml.Input.KeyRoutedEventArgs e)
{
- if (e.Key == Windows.System.VirtualKey.Enter && InputTxtBox.Text.Length > 0)
+ if (e.Key == Windows.System.VirtualKey.Enter && InputTxtBox.Text.Length > 0 && ViewModel.IsCustomAIEnabled)
{
GenerateCustom();
}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Converters/CountToDoubleConverter.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Converters/CountToDoubleConverter.cs
new file mode 100644
index 0000000000..5226bf320d
--- /dev/null
+++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Converters/CountToDoubleConverter.cs
@@ -0,0 +1,25 @@
+// 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.Data;
+
+namespace AdvancedPaste.Converters;
+
+public sealed class CountToDoubleConverter : IValueConverter
+{
+ public double ValueIfZero { get; set; }
+
+ public double ValueIfNonZero { get; set; }
+
+ public object Convert(object value, Type targetType, object parameter, string language)
+ {
+ bool hasCount = ((value is int intValue) && intValue > 0) || (value is IEnumerable collection && collection.GetEnumerator().MoveNext());
+
+ return hasCount ? ValueIfNonZero : ValueIfZero;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException();
+}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Converters/CountToVisibilityConverter.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Converters/CountToVisibilityConverter.cs
new file mode 100644
index 0000000000..41b3968e03
--- /dev/null
+++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Converters/CountToVisibilityConverter.cs
@@ -0,0 +1,33 @@
+// 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 class CountToVisibilityConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, string language)
+ {
+ bool hasCount = ((value is int intValue) && intValue > 0) || (value is IEnumerable collection && collection.GetEnumerator().MoveNext());
+
+ if (targetType == typeof(Visibility))
+ {
+ return hasCount ? Visibility.Visible : Visibility.Collapsed;
+ }
+ else if (targetType == typeof(bool))
+ {
+ return hasCount;
+ }
+ else
+ {
+ throw new ArgumentOutOfRangeException(nameof(targetType));
+ }
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException();
+}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Converters/ListViewIndexConverter.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Converters/ListViewIndexConverter.cs
deleted file mode 100644
index a50ec1fc0e..0000000000
--- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Converters/ListViewIndexConverter.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-// 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 Microsoft.UI.Xaml.Controls;
-using Microsoft.UI.Xaml.Controls.Primitives;
-using Microsoft.UI.Xaml.Data;
-using Microsoft.UI.Xaml.Media;
-
-namespace AdvancedPaste.Converters
-{
- public sealed class ListViewIndexConverter : IValueConverter
- {
- public object Convert(object value, Type targetType, object parameter, string language)
- {
- var presenter = value as ListViewItemPresenter;
- var item = VisualTreeHelper.GetParent(presenter) as ListViewItem;
-
- var listView = ItemsControl.ItemsControlFromItemContainer(item);
- int index = listView.IndexFromContainer(item) + 1;
-#pragma warning disable CA1305 // Specify IFormatProvider
- return index.ToString();
-#pragma warning restore CA1305 // Specify IFormatProvider
- }
-
- public object ConvertBack(object value, Type targetType, object parameter, string language)
- {
- throw new NotImplementedException();
- }
- }
-}
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/MainWindow.xaml.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/MainWindow.xaml.cs
index 401fac840a..252aa9fa48 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/MainWindow.xaml.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/MainWindow.xaml.cs
@@ -26,6 +26,18 @@ namespace AdvancedPaste
_userSettings = App.GetService();
+ var baseHeight = MinHeight;
+
+ void UpdateHeight()
+ {
+ var trimmedCustomActionCount = Math.Min(_userSettings.CustomActions.Count, 5);
+ Height = MinHeight = baseHeight + (trimmedCustomActionCount * 40);
+ }
+
+ UpdateHeight();
+
+ _userSettings.CustomActions.CollectionChanged += (_, _) => UpdateHeight();
+
AppWindow.SetIcon("Assets/AdvancedPaste/AdvancedPaste.ico");
this.ExtendsContentIntoTitleBar = true;
this.SetTitleBar(titleBar);
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml
index 2979155c1f..f45a222866 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml
+++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Pages/MainPage.xaml
@@ -5,14 +5,21 @@
xmlns:controls="using:AdvancedPaste.Controls"
xmlns:converters="using:AdvancedPaste.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+ xmlns:library="using:Microsoft.PowerToys.Settings.UI.Library"
xmlns:local="using:AdvancedPaste.Models"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters"
xmlns:ui="using:CommunityToolkit.WinUI"
KeyDown="Page_KeyDown"
KeyboardAcceleratorPlacementMode="Hidden"
mc:Ignorable="d">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -36,6 +75,30 @@
Key="Number3"
Invoked="KeyboardAccelerator_Invoked"
Modifiers="Control" />
+
+
+
+
+
+
@@ -103,73 +166,55 @@
BorderThickness="0,1,0,0"
RowSpacing="4">
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ TabIndex="1" />
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs
index adf504e83e..d24d13dabc 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
+using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using Microsoft.PowerToys.Settings.UI.Helpers;
@@ -10,7 +11,6 @@ using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
-using Windows.Security.Credentials;
namespace Microsoft.PowerToys.Settings.UI.Views
{
@@ -69,14 +69,85 @@ namespace Microsoft.PowerToys.Settings.UI.Views
private void AdvancedPaste_EnableAIDialogOpenAIApiKey_TextChanged(object sender, TextChangedEventArgs e)
{
- if (AdvancedPaste_EnableAIDialogOpenAIApiKey.Text.Length > 0)
+ EnableAIDialog.IsPrimaryButtonEnabled = AdvancedPaste_EnableAIDialogOpenAIApiKey.Text.Length > 0;
+ }
+
+ public async void DeleteCustomActionButton_Click(object sender, RoutedEventArgs e)
+ {
+ var customAction = GetBoundCustomAction(sender);
+ var resourceLoader = ResourceLoaderInstance.ResourceLoader;
+
+ ContentDialog dialog = new()
{
- EnableAIDialog.IsPrimaryButtonEnabled = true;
+ XamlRoot = RootPage.XamlRoot,
+ Title = customAction.Name,
+ PrimaryButtonText = resourceLoader.GetString("Yes"),
+ CloseButtonText = resourceLoader.GetString("No"),
+ DefaultButton = ContentDialogButton.Primary,
+ Content = new TextBlock() { Text = resourceLoader.GetString("Delete_Dialog_Description") },
+ };
+
+ dialog.PrimaryButtonClick += (_, _) => ViewModel.DeleteCustomAction(customAction);
+
+ await dialog.ShowAsync();
+ }
+
+ private async void AddCustomActionButton_Click(object sender, RoutedEventArgs e)
+ {
+ var resourceLoader = ResourceLoaderInstance.ResourceLoader;
+
+ CustomActionDialog.Title = resourceLoader.GetString("AddCustomAction");
+ CustomActionDialog.DataContext = ViewModel.GetNewCustomAction(resourceLoader.GetString("AdvancedPasteUI_NewCustomActionPrefix"));
+ CustomActionDialog.PrimaryButtonText = resourceLoader.GetString("CustomActionSave");
+ await CustomActionDialog.ShowAsync();
+ }
+
+ private async void EditCustomActionButton_Click(object sender, RoutedEventArgs e)
+ {
+ var resourceLoader = ResourceLoaderInstance.ResourceLoader;
+
+ CustomActionDialog.Title = resourceLoader.GetString("EditCustomAction");
+ CustomActionDialog.DataContext = GetBoundCustomAction(sender).Clone();
+ CustomActionDialog.PrimaryButtonText = resourceLoader.GetString("CustomActionUpdate");
+ await CustomActionDialog.ShowAsync();
+ }
+
+ private void ReorderButtonDown_Click(object sender, RoutedEventArgs e)
+ {
+ var index = ViewModel.CustomActions.IndexOf(GetBoundCustomAction(sender));
+ ViewModel.CustomActions.Move(index, index + 1);
+ }
+
+ private void ReorderButtonUp_Click(object sender, RoutedEventArgs e)
+ {
+ var index = ViewModel.CustomActions.IndexOf(GetBoundCustomAction(sender));
+ ViewModel.CustomActions.Move(index, index - 1);
+ }
+
+ private void CustomActionDialog_Closed(ContentDialog sender, ContentDialogClosedEventArgs args)
+ {
+ if (args.Result != ContentDialogResult.Primary)
+ {
+ return;
+ }
+
+ var dialogCustomAction = GetBoundCustomAction(sender);
+ var existingCustomAction = ViewModel.CustomActions.FirstOrDefault(candidate => candidate.Id == dialogCustomAction.Id);
+
+ if (existingCustomAction == null)
+ {
+ ViewModel.AddCustomAction(dialogCustomAction);
+
+ var element = (ContentPresenter)CustomActions.ContainerFromIndex(CustomActions.Items.Count - 1);
+ element.StartBringIntoView(new BringIntoViewOptions { VerticalOffset = -60, AnimationDesired = true });
+ element.Focus(FocusState.Programmatic);
}
else
{
- EnableAIDialog.IsPrimaryButtonEnabled = false;
+ existingCustomAction.Update(dialogCustomAction);
}
}
+
+ private static AdvancedPasteCustomAction GetBoundCustomAction(object sender) => (AdvancedPasteCustomAction)((FrameworkElement)sender).DataContext;
}
}
diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
index 5ff1ea6a11..817f8d9a43 100644
--- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
+++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
@@ -720,7 +720,7 @@
Save multiple items to your clipboard. This is an OS feature.
- Shortcuts
+ Actions
Current Key Remappings
@@ -1996,9 +1996,43 @@ Made with 💗 by Microsoft and the PowerToys community.
Paste as plain text directly
+
+ Actions
+
+
+ Create and manage advanced paste custom actions
+
+
+ Add custom action
+
Open Advanced Paste window
+
+ New custom action
+ First part of the default name of new custom action that can be added in PT's settings ui.
+
+
+ Name
+
+
+ Prompt
+
+
+ Cancel
+
+
+ Add custom action
+
+
+ Save
+
+
+ Edit custom action
+
+
+ Update
+
Paste as Markdown directly
diff --git a/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs
index 884061f01c..8d0bacacec 100644
--- a/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs
+++ b/src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs
@@ -3,8 +3,15 @@
// See the LICENSE file in the project root for more information.
using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.ComponentModel;
using System.Globalization;
+using System.Linq;
+using System.Reflection;
using System.Text.Json;
+using System.Text.Json.Serialization;
using System.Timers;
using global::PowerToys.GPOWrapper;
using Microsoft.PowerToys.Settings.UI.Library;
@@ -17,6 +24,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
public class AdvancedPasteViewModel : Observable, IDisposable
{
+ private static readonly HashSet WarnHotkeys = ["Ctrl + V", "Ctrl + Shift + V"];
+
private bool disposedValue;
// Delay saving of settings in order to avoid calling save multiple times and hitting file in use exception. If there is no other request to save settings in given interval, we proceed to save it, otherwise we schedule saving it after this interval
@@ -28,6 +37,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private readonly object _delayedActionLock = new object();
private readonly AdvancedPasteSettings _advancedPasteSettings;
+ private readonly ObservableCollection _customActions;
private Timer _delayedTimer;
private GpoRuleConfigured _enabledGpoRuleConfiguration;
@@ -58,6 +68,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
_advancedPasteSettings = advancedPasteSettingsRepository.SettingsConfig;
+ _customActions = _advancedPasteSettings.Properties.CustomActions.Value;
+
InitializeEnabledValue();
// set the callback functions value to handle outgoing IPC message.
@@ -67,6 +79,14 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
_delayedTimer.Interval = SaveSettingsDelayInMs;
_delayedTimer.Elapsed += DelayedTimer_Tick;
_delayedTimer.AutoReset = false;
+
+ foreach (var customAction in _customActions)
+ {
+ customAction.PropertyChanged += OnCustomActionPropertyChanged;
+ }
+
+ _customActions.CollectionChanged += OnCustomActionsCollectionChanged;
+ UpdateCustomActionsCanMoveUpDown();
}
private void InitializeEnabledValue()
@@ -120,6 +140,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
+ public ObservableCollection CustomActions => _customActions;
+
private bool OpenAIKeyExists()
{
PasswordVault vault = new PasswordVault();
@@ -234,8 +256,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
OnPropertyChanged(nameof(AdvancedPasteUIShortcut));
OnPropertyChanged(nameof(IsConflictingCopyShortcut));
- _settingsUtils.SaveSettings(_advancedPasteSettings.ToJsonString(), AdvancedPasteSettings.ModuleName);
- NotifySettingsChanged();
+ SaveAndNotifySettings();
}
}
}
@@ -251,8 +272,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
OnPropertyChanged(nameof(PasteAsPlainTextShortcut));
OnPropertyChanged(nameof(IsConflictingCopyShortcut));
- _settingsUtils.SaveSettings(_advancedPasteSettings.ToJsonString(), AdvancedPasteSettings.ModuleName);
- NotifySettingsChanged();
+ SaveAndNotifySettings();
}
}
}
@@ -268,8 +288,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
OnPropertyChanged(nameof(PasteAsMarkdownShortcut));
OnPropertyChanged(nameof(IsConflictingCopyShortcut));
- _settingsUtils.SaveSettings(_advancedPasteSettings.ToJsonString(), AdvancedPasteSettings.ModuleName);
- NotifySettingsChanged();
+ SaveAndNotifySettings();
}
}
}
@@ -285,8 +304,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
OnPropertyChanged(nameof(PasteAsJsonShortcut));
OnPropertyChanged(nameof(IsConflictingCopyShortcut));
- _settingsUtils.SaveSettings(_advancedPasteSettings.ToJsonString(), AdvancedPasteSettings.ModuleName);
- NotifySettingsChanged();
+ SaveAndNotifySettings();
}
}
}
@@ -319,13 +337,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public bool IsConflictingCopyShortcut
{
- get
- {
- return PasteAsPlainTextShortcut.ToString() == "Ctrl + V" || PasteAsPlainTextShortcut.ToString() == "Ctrl + Shift + V" ||
- AdvancedPasteUIShortcut.ToString() == "Ctrl + V" || AdvancedPasteUIShortcut.ToString() == "Ctrl + Shift + V" ||
- PasteAsMarkdownShortcut.ToString() == "Ctrl + V" || PasteAsMarkdownShortcut.ToString() == "Ctrl + Shift + V" ||
- PasteAsJsonShortcut.ToString() == "Ctrl + V" || PasteAsJsonShortcut.ToString() == "Ctrl + Shift + V";
- }
+ get => _customActions.Select(customAction => customAction.Shortcut)
+ .Concat([PasteAsPlainTextShortcut, AdvancedPasteUIShortcut, PasteAsMarkdownShortcut, PasteAsJsonShortcut])
+ .Any(hotkey => WarnHotkeys.Contains(hotkey.ToString()));
}
private void DelayedTimer_Tick(object sender, EventArgs e)
@@ -402,5 +416,114 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
{
}
}
+
+ internal AdvancedPasteCustomAction GetNewCustomAction(string namePrefix)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(namePrefix);
+
+ var maxUsedPrefix = _customActions.Select(customAction => customAction.Name)
+ .Where(name => name.StartsWith(namePrefix, StringComparison.InvariantCulture))
+ .Select(name => int.TryParse(name.AsSpan(namePrefix.Length), out int number) ? number : 0)
+ .DefaultIfEmpty(0)
+ .Max();
+
+ var maxUsedId = _customActions.Select(customAction => customAction.Id)
+ .DefaultIfEmpty(-1)
+ .Max();
+ return new()
+ {
+ Id = maxUsedId + 1,
+ Name = $"{namePrefix} {maxUsedPrefix + 1}",
+ IsShown = true,
+ };
+ }
+
+ internal void AddCustomAction(AdvancedPasteCustomAction customAction)
+ {
+ if (_customActions.Any(existingCustomAction => existingCustomAction.Id == customAction.Id))
+ {
+ throw new ArgumentException("Duplicate custom action", nameof(customAction));
+ }
+
+ _customActions.Add(customAction);
+ }
+
+ internal void DeleteCustomAction(AdvancedPasteCustomAction customAction) => _customActions.Remove(customAction);
+
+ private void SaveCustomActions() => SaveAndNotifySettings();
+
+ private void SaveAndNotifySettings()
+ {
+ _settingsUtils.SaveSettings(_advancedPasteSettings.ToJsonString(), AdvancedPasteSettings.ModuleName);
+ NotifySettingsChanged();
+ }
+
+ private void OnCustomActionPropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (typeof(AdvancedPasteCustomAction).GetProperty(e.PropertyName).GetCustomAttribute() == null)
+ {
+ SaveCustomActions();
+ }
+
+ if (e.PropertyName == nameof(AdvancedPasteCustomAction.Shortcut))
+ {
+ OnPropertyChanged(nameof(IsConflictingCopyShortcut));
+ }
+ }
+
+ private void OnCustomActionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ void AddRange(System.Collections.IList items)
+ {
+ foreach (AdvancedPasteCustomAction item in items)
+ {
+ item.PropertyChanged += OnCustomActionPropertyChanged;
+ }
+ }
+
+ void RemoveRange(System.Collections.IList items)
+ {
+ foreach (AdvancedPasteCustomAction item in items)
+ {
+ item.PropertyChanged -= OnCustomActionPropertyChanged;
+ }
+ }
+
+ switch (e.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ AddRange(e.NewItems);
+ break;
+
+ case NotifyCollectionChangedAction.Remove:
+ RemoveRange(e.OldItems);
+ break;
+
+ case NotifyCollectionChangedAction.Replace:
+ AddRange(e.NewItems);
+ RemoveRange(e.OldItems);
+ break;
+
+ case NotifyCollectionChangedAction.Move:
+ break;
+
+ default:
+ throw new ArgumentException($"Unsupported {nameof(e.Action)} {e.Action}", nameof(e));
+ }
+
+ OnPropertyChanged(nameof(IsConflictingCopyShortcut));
+ UpdateCustomActionsCanMoveUpDown();
+ SaveCustomActions();
+ }
+
+ private void UpdateCustomActionsCanMoveUpDown()
+ {
+ for (int index = 0; index < _customActions.Count; index++)
+ {
+ var customAction = _customActions[index];
+ customAction.CanMoveUp = index != 0;
+ customAction.CanMoveDown = index != _customActions.Count - 1;
+ }
+ }
}
}