[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>
This commit is contained in:
Ani
2024-08-22 16:17:12 +02:00
committed by GitHub
parent bfa35d65a4
commit 2a8e211cfd
30 changed files with 1581 additions and 491 deletions

View File

@@ -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;
/// <summary>
@@ -74,7 +80,7 @@ namespace AdvancedPaste
/// Invoked when the application is launched.
/// </summary>
/// <param name="args">Details about the launch request and process.</param>
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();

View File

@@ -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">
<UserControl.Resources>
@@ -323,7 +324,12 @@
</Setter.Value>
</Setter>
</Style>
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<tkconverters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<tkconverters:BoolToVisibilityConverter
x:Key="BoolToInvertedVisibilityConverter"
FalseValue="Visible"
TrueValue="Collapsed" />
<converters:CountToVisibilityConverter x:Key="CountToVisibilityConverter" />
</ResourceDictionary>
</UserControl.Resources>
<Grid x:Name="PromptBoxGrid" Loaded="Grid_Loaded">
@@ -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}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="InputTxtBoxTooltip" />
</ToolTipService.ToolTip>
@@ -531,48 +536,63 @@
<animations:OffsetAnimation Duration="0:0:1" />
</animations:Implicit.Animations>
</Button>-->
<Button
x:Name="SendBtn"
x:Uid="SendButtonAutomation"
<Grid
Width="32"
Height="32"
Margin="0,0,4,0"
Padding="0"
HorizontalAlignment="Right"
VerticalAlignment="Stretch"
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
Command="{x:Bind GenerateCustomCommand}"
Content="{ui:FontIcon Glyph=&#xE724;,
FontSize=16}"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Style="{StaticResource SubtleButtonStyle}"
TabIndex="1"
Visibility="Collapsed">
<ToolTipService.ToolTip>
<TextBlock x:Uid="SendBtnToolTip" TextWrapping="WrapWholeWords" />
</ToolTipService.ToolTip>
<animations:Implicit.ShowAnimations>
<animations:ScaleAnimation
From="0.4"
To="1"
Duration="0:0:0.167" />
<animations:OpacityAnimation
From="0.0"
To="1.0"
Duration="0:0:0.167" />
</animations:Implicit.ShowAnimations>
<animations:Implicit.HideAnimations>
<animations:ScaleAnimation
From="1"
To="0.4"
Duration="0:0:0.167" />
<animations:OpacityAnimation
From="1.0"
To="0.0"
Duration="0:0:0.167" />
</animations:Implicit.HideAnimations>
</Button>
<!--</StackPanel>-->
VerticalAlignment="Stretch">
<Button
x:Name="SendBtn"
x:Uid="SendButtonAutomation"
Padding="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
Command="{x:Bind GenerateCustomCommand}"
Content="{ui:FontIcon Glyph=&#xE724;,
FontSize=16}"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
IsEnabled="{x:Bind ViewModel.IsCustomAIEnabled, Mode=OneWay}"
Style="{StaticResource SubtleButtonStyle}"
TabIndex="1"
Visibility="{x:Bind ViewModel.Query.Length, Mode=OneWay, Converter={StaticResource CountToVisibilityConverter}}">
<ToolTipService.ToolTip>
<TextBlock x:Uid="SendBtnToolTip" TextWrapping="WrapWholeWords" />
</ToolTipService.ToolTip>
<animations:Implicit.ShowAnimations>
<animations:ScaleAnimation
From="0.4"
To="1"
Duration="0:0:0.167" />
<animations:OpacityAnimation
From="0.0"
To="1.0"
Duration="0:0:0.167" />
</animations:Implicit.ShowAnimations>
<animations:Implicit.HideAnimations>
<animations:ScaleAnimation
From="1"
To="0.4"
Duration="0:0:0.167" />
<animations:OpacityAnimation
From="1.0"
To="0.0"
Duration="0:0:0.167" />
</animations:Implicit.HideAnimations>
</Button>
<!-- Transparent overlay to show tooltip -->
<Grid
x:Name="SendBtnOverlay"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Transparent"
Visibility="{x:Bind ViewModel.IsCustomAIEnabled, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
<ToolTipService.ToolTip>
<ToolTip Content="{x:Bind ViewModel.GeneralErrorText}" />
</ToolTipService.ToolTip>
</Grid>
</Grid>
</Grid>
</local:AnimatedContentControl>
<ContentPresenter
@@ -618,7 +638,7 @@
FontWeight="SemiBold"
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ViewModel.InputTxtBoxErrorText, Mode=OneWay}" />
Text="{x:Bind ViewModel.ApiErrorText, Mode=OneWay}" />
<HyperlinkButton
x:Uid="SettingsBtn"
Grid.Column="1"
@@ -635,7 +655,6 @@
<animations:OpacityAnimation To="0.0" Duration="0:0:0.167" />
</animations:Implicit.HideAnimations>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="DefaultState" />

View File

@@ -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<IUserSettings>();
ViewModel = App.GetService<OptionsViewModel>();
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();
}

View File

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

View File

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

View File

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

View File

@@ -26,6 +26,18 @@ namespace AdvancedPaste
_userSettings = App.GetService<IUserSettings>();
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);

View File

@@ -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">
<Page.Resources>
<converters:ListViewIndexConverter x:Name="listViewIndexConverter" />
<tkconverters:BoolToVisibilityConverter x:Name="BoolToVisibilityConverter" />
<converters:CountToVisibilityConverter x:Name="countToVisibilityConverter" />
<converters:CountToDoubleConverter
x:Name="customActionsCountToMinHeightConverter"
ValueIfNonZero="40"
ValueIfZero="0" />
<Style
x:Key="PaddingLessFlyoutPresenterStyle"
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
@@ -21,6 +28,38 @@
<Setter Property="Padding" Value="0" />
</Style.Setters>
</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>
</DataTemplate>
</Page.Resources>
<Page.KeyboardAccelerators>
<KeyboardAccelerator Key="Escape" Invoked="KeyboardAccelerator_Invoked" />
@@ -36,6 +75,30 @@
Key="Number3"
Invoked="KeyboardAccelerator_Invoked"
Modifiers="Control" />
<KeyboardAccelerator
Key="Number4"
Invoked="KeyboardAccelerator_Invoked"
Modifiers="Control" />
<KeyboardAccelerator
Key="Number5"
Invoked="KeyboardAccelerator_Invoked"
Modifiers="Control" />
<KeyboardAccelerator
Key="Number6"
Invoked="KeyboardAccelerator_Invoked"
Modifiers="Control" />
<KeyboardAccelerator
Key="Number7"
Invoked="KeyboardAccelerator_Invoked"
Modifiers="Control" />
<KeyboardAccelerator
Key="Number8"
Invoked="KeyboardAccelerator_Invoked"
Modifiers="Control" />
<KeyboardAccelerator
Key="Number9"
Invoked="KeyboardAccelerator_Invoked"
Modifiers="Control" />
</Page.KeyboardAccelerators>
<Grid>
<Grid.RowDefinitions>
@@ -103,73 +166,55 @@
BorderThickness="0,1,0,0"
RowSpacing="4">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" MinHeight="{x:Bind ViewModel.CustomActionPasteFormats.Count, Mode=OneWay, Converter={StaticResource customActionsCountToMinHeightConverter}}" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListView
x:Name="PasteOptionsListView"
Grid.Row="0"
VerticalAlignment="Bottom"
IsEnabled="{x:Bind ViewModel.IsClipboardDataText, Mode=OneWay}"
IsItemClickEnabled="True"
ItemClick="PasteOptionsListView_ItemClick"
ItemClick="ListView_Click"
ItemContainerTransitions="{x:Null}"
ItemsSource="{x:Bind pasteFormats, Mode=OneWay}"
ItemTemplate="{StaticResource PasteFormatTemplate}"
ItemsSource="{x:Bind ViewModel.StandardPasteFormats, Mode=OneWay}"
SelectionMode="None"
TabIndex="1">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:PasteFormat">
<Grid>
<ToolTipService.ToolTip>
<TextBlock>
<Run Text="{x:Bind Name}" />
<Run Text="(" /><Run Text="Ctrl" /><Run Text="+" /><Run Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource listViewIndexConverter}}" /><Run Text=")" />
</TextBlock>
</ToolTipService.ToolTip>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="26" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Viewbox
x:Name="IconHolderBox"
MaxWidth="16"
MaxHeight="16"
HorizontalAlignment="Left"
VerticalAlignment="Center">
<ContentPresenter
x:Name="IconHolder"
x:Phase="2"
Content="{x:Bind Icon}" />
</Viewbox>
<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}">
<Run Text="Ctrl" /><Run Text="+" /><Run Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource listViewIndexConverter}}" />
</TextBlock>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
TabIndex="1" />
<Rectangle
Grid.Row="1"
Height="1"
HorizontalAlignment="Stretch"
Fill="{ThemeResource DividerStrokeColorDefaultBrush}"
Visibility="{x:Bind ViewModel.CustomActionPasteFormats.Count, Mode=OneWay, Converter={StaticResource countToVisibilityConverter}}" />
<ListView
x:Name="CustomActionsListView"
Grid.Row="2"
VerticalAlignment="Top"
IsEnabled="{x:Bind ViewModel.IsCustomAIEnabled, Mode=OneWay}"
IsItemClickEnabled="True"
ItemClick="ListView_Click"
ItemContainerTransitions="{x:Null}"
ItemTemplate="{StaticResource PasteFormatTemplate}"
ItemsSource="{x:Bind ViewModel.CustomActionPasteFormats, Mode=OneWay}"
ScrollViewer.VerticalScrollBarVisibility="Visible"
ScrollViewer.VerticalScrollMode="Auto"
SelectionMode="None"
TabIndex="2" />
<Rectangle
Grid.Row="3"
Height="1"
HorizontalAlignment="Stretch"
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
<!-- x:Uid="ClipboardHistoryButton" -->
<Button
Grid.Row="2"
Grid.Row="4"
Height="32"
Margin="4,0,4,4"
Padding="{StaticResource ButtonPadding}"

View File

@@ -6,7 +6,6 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
using AdvancedPaste.Models;
@@ -25,7 +24,6 @@ namespace AdvancedPaste.Pages
public sealed partial class MainPage : Page
{
private readonly ObservableCollection<ClipboardItem> clipboardHistory;
private readonly ObservableCollection<PasteFormat> pasteFormats;
private readonly Microsoft.UI.Dispatching.DispatcherQueue _dispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread();
public OptionsViewModel ViewModel { get; private set; }
@@ -34,13 +32,6 @@ namespace AdvancedPaste.Pages
{
this.InitializeComponent();
pasteFormats =
[
new PasteFormat { Icon = new FontIcon() { Glyph = "\uE8E9" }, Name = ResourceLoaderInstance.ResourceLoader.GetString("PasteAsPlainText"), Format = PasteFormats.PlainText },
new PasteFormat { Icon = new FontIcon() { Glyph = "\ue8a5" }, Name = ResourceLoaderInstance.ResourceLoader.GetString("PasteAsMarkdown"), Format = PasteFormats.Markdown },
new PasteFormat { Icon = new FontIcon() { Glyph = "\uE943" }, Name = ResourceLoaderInstance.ResourceLoader.GetString("PasteAsJson"), Format = PasteFormats.Json },
];
ViewModel = App.GetService<OptionsViewModel>();
clipboardHistory = new ObservableCollection<ClipboardItem>();
@@ -121,6 +112,8 @@ namespace AdvancedPaste.Pages
}
}
private static MainWindow GetMainWindow() => (App.Current as App)?.GetMainWindow();
private void ClipboardHistoryItemDeleteButton_Click(object sender, RoutedEventArgs e)
{
Logger.LogTrace();
@@ -135,83 +128,40 @@ namespace AdvancedPaste.Pages
}
}
private void PasteAsPlain()
{
ViewModel.ToPlainTextFunction();
}
private void PasteAsMarkdown()
{
ViewModel.ToMarkdownFunction();
}
private void PasteAsJson()
{
ViewModel.ToJsonFunction();
}
private void PasteOptionsListView_ItemClick(object sender, ItemClickEventArgs e)
private void ListView_Click(object sender, ItemClickEventArgs e)
{
if (e.ClickedItem is PasteFormat format)
{
switch (format.Format)
{
case PasteFormats.PlainText:
{
PasteAsPlain();
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteFormatClickedEvent(PasteFormats.PlainText));
break;
}
case PasteFormats.Markdown:
{
PasteAsMarkdown();
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteFormatClickedEvent(PasteFormats.Markdown));
break;
}
case PasteFormats.Json:
{
PasteAsJson();
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteFormatClickedEvent(PasteFormats.Json));
break;
}
}
ViewModel.ExecutePasteFormat(format);
}
}
private void KeyboardAccelerator_Invoked(Microsoft.UI.Xaml.Input.KeyboardAccelerator sender, Microsoft.UI.Xaml.Input.KeyboardAcceleratorInvokedEventArgs args)
{
if (GetMainWindow()?.Visible is false)
{
return;
}
Logger.LogTrace();
switch (sender.Key)
{
case VirtualKey.Escape:
{
(App.Current as App).GetMainWindow().Close();
break;
}
GetMainWindow()?.Close();
break;
case VirtualKey.Number1:
{
PasteAsPlain();
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteInAppKeyboardShortcutEvent(PasteFormats.PlainText));
break;
}
case VirtualKey.Number2:
{
PasteAsMarkdown();
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteInAppKeyboardShortcutEvent(PasteFormats.Markdown));
break;
}
case VirtualKey.Number3:
{
PasteAsJson();
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteInAppKeyboardShortcutEvent(PasteFormats.Json));
break;
}
case VirtualKey.Number4:
case VirtualKey.Number5:
case VirtualKey.Number6:
case VirtualKey.Number7:
case VirtualKey.Number8:
case VirtualKey.Number9:
ViewModel.ExecutePasteFormat(sender.Key);
break;
default:
break;
@@ -222,7 +172,7 @@ namespace AdvancedPaste.Pages
{
if (e.Key == VirtualKey.Escape)
{
(App.Current as App).GetMainWindow().Close();
GetMainWindow()?.Close();
}
}

View File

@@ -2,6 +2,9 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.ObjectModel;
using Microsoft.PowerToys.Settings.UI.Library;
namespace AdvancedPaste.Settings
{
public interface IUserSettings
@@ -11,5 +14,7 @@ namespace AdvancedPaste.Settings
public bool SendPasteKeyCombination { get; }
public bool CloseAfterLosingFocus { get; }
public ObservableCollection<AdvancedPasteCustomAction> CustomActions { get; }
}
}

View File

@@ -0,0 +1,37 @@
// 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.IO;
using System.IO.Pipes;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace AdvancedPaste.Helpers;
public static class NamedPipeProcessor
{
public static async Task ProcessNamedPipeAsync(string pipeName, TimeSpan connectTimeout, Action<string> messageHandler, CancellationToken cancellationToken)
{
using NamedPipeClientStream pipeClient = new(".", pipeName, PipeDirection.In);
await pipeClient.ConnectAsync(connectTimeout, cancellationToken);
using StreamReader streamReader = new(pipeClient, Encoding.Unicode);
while (true)
{
var message = await streamReader.ReadLineAsync(cancellationToken);
if (message != null)
{
messageHandler(message);
}
var intraMessageDelay = TimeSpan.FromMilliseconds(10);
await Task.Delay(intraMessageDelay, cancellationToken);
}
}
}

View File

@@ -1,29 +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 System.Threading;
using Microsoft.UI.Dispatching;
namespace AdvancedPaste.Helpers
{
public static class NativeEventWaiter
{
public static void WaitForEventLoop(string eventName, Action callback)
{
var dispatcherQueue = DispatcherQueue.GetForCurrentThread();
new Thread(() =>
{
var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, eventName);
while (true)
{
if (eventHandle.WaitOne())
{
dispatcherQueue.TryEnqueue(() => callback());
}
}
}).Start();
}
}
}

View File

@@ -3,29 +3,37 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.ObjectModel;
using System.IO.Abstractions;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Utilities;
namespace AdvancedPaste.Settings
{
internal sealed class UserSettings : IUserSettings
internal sealed class UserSettings : IUserSettings, IDisposable
{
private readonly SettingsUtils _settingsUtils;
private readonly TaskScheduler _taskScheduler;
private readonly IFileSystemWatcher _watcher;
private readonly object _loadingSettingsLock = new object();
private readonly object _loadingSettingsLock = new();
private const string AdvancedPasteModuleName = "AdvancedPaste";
private const int MaxNumberOfRetry = 5;
private bool _disposedValue;
private CancellationTokenSource _cancellationTokenSource;
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 UserSettings()
{
_settingsUtils = new SettingsUtils();
@@ -33,10 +41,25 @@ namespace AdvancedPaste.Settings
ShowCustomPreview = true;
SendPasteKeyCombination = true;
CloseAfterLosingFocus = false;
CustomActions = [];
_taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
LoadSettingsFromJson();
_watcher = Helper.GetFileWatcher(AdvancedPasteModuleName, "settings.json", () => LoadSettingsFromJson());
_watcher = Helper.GetFileWatcher(AdvancedPasteModuleName, "settings.json", OnSettingsFileChanged);
}
private void OnSettingsFileChanged()
{
lock (_loadingSettingsLock)
{
_cancellationTokenSource?.Cancel();
_cancellationTokenSource = new CancellationTokenSource();
Task.Delay(TimeSpan.FromMilliseconds(500))
.ContinueWith(_ => LoadSettingsFromJson(), _cancellationTokenSource.Token, TaskContinuationOptions.NotOnCanceled, TaskScheduler.Default);
}
}
private void LoadSettingsFromJson()
@@ -62,9 +85,25 @@ namespace AdvancedPaste.Settings
var settings = _settingsUtils.GetSettingsOrDefault<AdvancedPasteSettings>(AdvancedPasteModuleName);
if (settings != null)
{
ShowCustomPreview = settings.Properties.ShowCustomPreview;
SendPasteKeyCombination = settings.Properties.SendPasteKeyCombination;
CloseAfterLosingFocus = settings.Properties.CloseAfterLosingFocus;
void UpdateSettings()
{
ShowCustomPreview = settings.Properties.ShowCustomPreview;
SendPasteKeyCombination = settings.Properties.SendPasteKeyCombination;
CloseAfterLosingFocus = settings.Properties.CloseAfterLosingFocus;
CustomActions.Clear();
foreach (var customAction in settings.Properties.CustomActions.Value)
{
if (customAction.IsShown && customAction.IsValid)
{
CustomActions.Add(customAction);
}
}
}
Task.Factory
.StartNew(UpdateSettings, CancellationToken.None, TaskCreationOptions.None, _taskScheduler)
.Wait();
}
retry = false;
@@ -82,5 +121,30 @@ namespace AdvancedPaste.Settings
}
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_cancellationTokenSource.Dispose();
_watcher.Dispose();
}
_disposedValue = true;
}
}
~UserSettings()
{
Dispose(false);
}
}
}

View File

@@ -0,0 +1,14 @@
// 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 CustomActionActivatedEventArgs(string text, bool forcePasteCustom) : EventArgs
{
public string Text { get; private set; } = text;
public bool ForcePasteCustom { get; private set; } = forcePasteCustom;
}

View File

@@ -2,16 +2,38 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.UI.Xaml.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.PowerToys.Settings.UI.Library;
namespace AdvancedPaste.Models
namespace AdvancedPaste.Models;
public partial class PasteFormat : ObservableObject
{
public class PasteFormat
[ObservableProperty]
private string _shortcutText = string.Empty;
[ObservableProperty]
private string _toolTip = string.Empty;
public PasteFormat()
{
public IconElement Icon { get; set; }
public string Name { get; set; }
public PasteFormats Format { get; set; }
}
public PasteFormat(AdvancedPasteCustomAction customAction, string shortcutText)
{
IconGlyph = "\uE945";
Name = customAction.Name;
Prompt = customAction.Prompt;
Format = PasteFormats.Custom;
ShortcutText = shortcutText;
ToolTip = customAction.Prompt;
}
public string IconGlyph { get; init; }
public string Name { get; init; }
public PasteFormats Format { get; init; }
public string Prompt { get; init; } = string.Empty;
}

View File

@@ -59,10 +59,7 @@
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root"
xmlns=""
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
@@ -228,4 +225,7 @@
<data name="OpenAIGpoDisabled" xml:space="preserve">
<value>To custom with AI is disabled by your organization</value>
</data>
</root>
<data name="CtrlKey" xml:space="preserve">
<value>Ctrl</value>
</data>
</root>

View File

@@ -5,6 +5,7 @@
using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
@@ -15,49 +16,79 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Dispatching;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Xaml;
using Microsoft.Win32;
using Windows.ApplicationModel.DataTransfer;
using Windows.System;
using WinUIEx;
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
namespace AdvancedPaste.ViewModels
{
public partial class OptionsViewModel : ObservableObject
public partial class OptionsViewModel : ObservableObject, IDisposable
{
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
private readonly DispatcherTimer _clipboardTimer;
private readonly IUserSettings _userSettings;
private App app = App.Current as App;
private AICompletionsHelper aiHelper;
private readonly AICompletionsHelper aiHelper;
private readonly App app = App.Current as App;
private readonly PasteFormat[] _allStandardPasteFormats;
public DataPackageView ClipboardData { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(InputTxtBoxPlaceholderText))]
[NotifyPropertyChangedFor(nameof(GeneralErrorText))]
[NotifyPropertyChangedFor(nameof(IsCustomAIEnabled))]
private bool _isClipboardDataText;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(InputTxtBoxPlaceholderText))]
private bool _isCustomAIEnabled;
[ObservableProperty]
private bool _clipboardHistoryEnabled;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(InputTxtBoxErrorText))]
[NotifyPropertyChangedFor(nameof(InputTxtBoxPlaceholderText))]
[NotifyPropertyChangedFor(nameof(GeneralErrorText))]
[NotifyPropertyChangedFor(nameof(IsCustomAIEnabled))]
private bool _isAllowedByGPO;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ApiErrorText))]
private int _apiRequestStatus;
[ObservableProperty]
private string _query = string.Empty;
private bool _pasteFormatsDirty;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsCustomAIEnabled))]
private bool _isCustomAIEnabledOverride = false;
public ObservableCollection<PasteFormat> StandardPasteFormats { get; } = [];
public ObservableCollection<PasteFormat> CustomActionPasteFormats { get; } = [];
public bool IsCustomAIEnabled => IsCustomAIEnabledOverride || IsCustomAIEnabledCore;
private bool IsCustomAIEnabledCore => IsAllowedByGPO && IsClipboardDataText && aiHelper.IsAIEnabled;
public event EventHandler<CustomActionActivatedEventArgs> CustomActionActivated;
public OptionsViewModel(IUserSettings userSettings)
{
aiHelper = new AICompletionsHelper();
_userSettings = userSettings;
IsCustomAIEnabled = IsClipboardDataText && aiHelper.IsAIEnabled;
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.CollectionChanged += (s, e) =>
{
@@ -66,10 +97,87 @@ namespace AdvancedPaste.ViewModels
};
ClipboardHistoryEnabled = IsClipboardHistoryEnabled();
GetClipboardData();
ReadClipboard();
_clipboardTimer = new() { Interval = TimeSpan.FromSeconds(1) };
_clipboardTimer.Tick += ClipboardTimer_Tick;
_clipboardTimer.Start();
RefreshPasteFormats();
_userSettings.CustomActions.CollectionChanged += (_, _) => EnqueueRefreshPasteFormats();
PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(Query))
{
EnqueueRefreshPasteFormats();
}
};
}
public void GetClipboardData()
private void ClipboardTimer_Tick(object sender, object e)
{
if (app.GetMainWindow()?.Visible is true)
{
ReadClipboard();
UpdateAllowedByGPO();
}
}
private void EnqueueRefreshPasteFormats()
{
if (_pasteFormatsDirty)
{
return;
}
_pasteFormatsDirty = true;
_dispatcherQueue.TryEnqueue(() =>
{
RefreshPasteFormats();
_pasteFormatsDirty = false;
});
}
private void RefreshPasteFormats()
{
bool Filter(string text) => text.Contains(Query, StringComparison.CurrentCultureIgnoreCase);
var ctrlString = ResourceLoaderInstance.ResourceLoader.GetString("CtrlKey");
int shortcutNum = 0;
string GetNextShortcutText()
{
shortcutNum++;
return shortcutNum <= 9 ? $"{ctrlString}+{shortcutNum}" : string.Empty;
}
StandardPasteFormats.Clear();
foreach (var format in _allStandardPasteFormats)
{
if (Filter(format.Name))
{
format.ShortcutText = GetNextShortcutText();
format.ToolTip = $"{format.Name} ({format.ShortcutText})";
StandardPasteFormats.Add(format);
}
}
CustomActionPasteFormats.Clear();
foreach (var customAction in _userSettings.CustomActions)
{
if (Filter(customAction.Name) || Filter(customAction.Prompt))
{
CustomActionPasteFormats.Add(new PasteFormat(customAction, GetNextShortcutText()));
}
}
}
public void Dispose()
{
_clipboardTimer.Stop();
GC.SuppressFinalize(this);
}
public void ReadClipboard()
{
ClipboardData = Clipboard.GetContent();
IsClipboardDataText = ClipboardData.Contains(StandardDataFormats.Text);
@@ -77,14 +185,10 @@ namespace AdvancedPaste.ViewModels
public void OnShow()
{
GetClipboardData();
ReadClipboard();
UpdateAllowedByGPO();
if (PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteOnlineAIModelsValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
{
IsCustomAIEnabled = false;
OnPropertyChanged(nameof(InputTxtBoxPlaceholderText));
}
else
if (IsAllowedByGPO)
{
var openAIKey = AICompletionsHelper.LoadOpenAIKey();
var currentKey = aiHelper.GetKey();
@@ -104,19 +208,30 @@ namespace AdvancedPaste.ViewModels
{
app.GetMainWindow().FinishLoading(aiHelper.IsAIEnabled);
OnPropertyChanged(nameof(InputTxtBoxPlaceholderText));
IsCustomAIEnabled = IsClipboardDataText && aiHelper.IsAIEnabled;
OnPropertyChanged(nameof(GeneralErrorText));
OnPropertyChanged(nameof(IsCustomAIEnabled));
});
},
TaskScheduler.Default);
}
else
{
IsCustomAIEnabled = IsClipboardDataText && aiHelper.IsAIEnabled;
}
}
ClipboardHistoryEnabled = IsClipboardHistoryEnabled();
GeneratedResponses.Clear();
_dispatcherQueue.TryEnqueue(async () =>
{
// Work-around for ListViews being disabled but sometimes not appearing grayed out.
// It appears that this is sometimes only triggered by a change event. This
// work-around sometimes still doesn't work, but it's better than not having it.
await Task.Delay(5);
IsClipboardDataText = true;
IsCustomAIEnabledOverride = true;
await Task.Delay(150);
ReadClipboard();
IsCustomAIEnabledOverride = false;
});
}
// List to store generated responses
@@ -152,47 +267,44 @@ namespace AdvancedPaste.ViewModels
{
app.GetMainWindow().ClearInputText();
if (PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteOnlineAIModelsValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
{
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAIGpoDisabled");
}
else if (!aiHelper.IsAIEnabled)
{
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAINotConfigured");
}
else if (!IsClipboardDataText)
return IsClipboardDataText ? ResourceLoaderInstance.ResourceLoader.GetString("CustomFormatTextBox/PlaceholderText") : GeneralErrorText;
}
}
public string GeneralErrorText
{
get
{
if (!IsClipboardDataText)
{
return ResourceLoaderInstance.ResourceLoader.GetString("ClipboardDataTypeMismatchWarning");
}
if (!IsAllowedByGPO)
{
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAIGpoDisabled");
}
if (!aiHelper.IsAIEnabled)
{
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAINotConfigured");
}
else
{
return ResourceLoaderInstance.ResourceLoader.GetString("CustomFormatTextBox/PlaceholderText");
return string.Empty;
}
}
}
public string InputTxtBoxErrorText
public string ApiErrorText
{
get
get => (HttpStatusCode)ApiRequestStatus switch
{
if (ApiRequestStatus != (int)HttpStatusCode.OK)
{
if (ApiRequestStatus == (int)HttpStatusCode.TooManyRequests)
{
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAIApiKeyTooManyRequests");
}
else if (ApiRequestStatus == (int)HttpStatusCode.Unauthorized)
{
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAIApiKeyUnauthorized");
}
else
{
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAIApiKeyError") + ApiRequestStatus.ToString(CultureInfo.InvariantCulture);
}
}
return string.Empty;
}
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]
@@ -201,7 +313,12 @@ namespace AdvancedPaste.ViewModels
[RelayCommand]
public void PasteCustom()
{
PasteCustomFunction(GeneratedResponses[CurrentResponseIndex]);
var text = GeneratedResponses.ElementAtOrDefault(CurrentResponseIndex);
if (text != null)
{
PasteCustomFunction(text);
}
}
// Command to select the previous custom format
@@ -306,6 +423,59 @@ namespace AdvancedPaste.ViewModels
}
}
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 && !IsCustomAIEnabledCore))
{
return;
}
switch (pasteFormat.Format)
{
case PasteFormats.PlainText:
ToPlainTextFunction();
break;
case PasteFormats.Markdown:
ToMarkdownFunction();
break;
case PasteFormats.Json:
ToJsonFunction();
break;
case PasteFormats.Custom:
Query = pasteFormat.Prompt;
CustomActionActivated?.Invoke(this, new CustomActionActivatedEventArgs(pasteFormat.Prompt, false));
break;
}
}
internal void ExecuteCustomActionWithPaste(int customActionId)
{
Logger.LogTrace();
var customAction = _userSettings.CustomActions.FirstOrDefault(customAction => customAction.Id == customActionId);
if (customAction != null)
{
Query = customAction.Prompt;
CustomActionActivated?.Invoke(this, new CustomActionActivatedEventArgs(customAction.Prompt, true));
}
}
internal async Task<string> GenerateCustomFunction(string inputInstructions)
{
Logger.LogTrace();
@@ -315,7 +485,7 @@ namespace AdvancedPaste.ViewModels
return string.Empty;
}
if (ClipboardData == null || !ClipboardData.Contains(StandardDataFormats.Text))
if (!IsClipboardDataText)
{
Logger.LogWarning("Clipboard does not contain text data");
return string.Empty;
@@ -416,5 +586,10 @@ namespace AdvancedPaste.ViewModels
return false;
}
}
private void UpdateAllowedByGPO()
{
IsAllowedByGPO = PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteOnlineAIModelsValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled;
}
}
}

View File

@@ -14,6 +14,10 @@
#include <common/utils/logger_helper.h>
#include <common/utils/winapi_error.h>
#include <atlfile.h>
#include <atlstr.h>
#include <vector>
BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
{
switch (ul_reason_for_call)
@@ -35,6 +39,9 @@ BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lp
namespace
{
const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
const wchar_t JSON_KEY_CUSTOM_ACTIONS[] = L"custom-actions";
const wchar_t JSON_KEY_SHORTCUT[] = L"shortcut";
const wchar_t JSON_KEY_ID[] = L"id";
const wchar_t JSON_KEY_WIN[] = L"win";
const wchar_t JSON_KEY_ALT[] = L"alt";
const wchar_t JSON_KEY_CTRL[] = L"ctrl";
@@ -60,33 +67,30 @@ 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
static const int MAX_WAIT_MILLISEC = 10000;
static const constexpr int MAX_WAIT_MILLISEC = 10000;
static const constexpr int NUM_DEFAULT_HOTKEYS = 4;
Hotkey m_paste_as_plain_hotkey = { .win = true, .ctrl = true, .shift = false, .alt = true, .key = 'V' };
Hotkey m_advanced_paste_ui_hotkey = { .win = true, .ctrl = false, .shift = true, .alt = false, .key = 'V' };
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;
bool m_preview_custom_format_output = true;
// Handle to event used to invoke AdvancedPaste
HANDLE m_hShowUIEvent;
HANDLE m_hPasteMarkdownEvent;
HANDLE m_hPasteJsonEvent;
Hotkey parse_single_hotkey(const wchar_t* hotkey, const winrt::Windows::Data::Json::JsonObject& settingsObject)
Hotkey parse_single_hotkey(const wchar_t* keyName, const winrt::Windows::Data::Json::JsonObject& settingsObject)
{
try
{
Hotkey _temp_paste_as_plain;
auto jsonHotkeyObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(hotkey);
_temp_paste_as_plain.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN);
_temp_paste_as_plain.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT);
_temp_paste_as_plain.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
_temp_paste_as_plain.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
_temp_paste_as_plain.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
return _temp_paste_as_plain;
const auto jsonHotkeyObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(keyName);
return parse_single_hotkey(jsonHotkeyObject);
}
catch (...)
{
@@ -96,6 +100,38 @@ private:
return {};
}
static Hotkey parse_single_hotkey(const winrt::Windows::Data::Json::JsonObject& jsonHotkeyObject)
{
try
{
Hotkey hotkey;
hotkey.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN);
hotkey.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT);
hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
return hotkey;
}
catch (...)
{
Logger::error("Failed to initialize AdvancedPaste shortcut from settings. Value will keep unchanged.");
}
return {};
}
static json::JsonObject to_json_object(const Hotkey& hotkey)
{
json::JsonObject jsonObject;
jsonObject.SetNamedValue(JSON_KEY_WIN, json::value(hotkey.win));
jsonObject.SetNamedValue(JSON_KEY_ALT, json::value(hotkey.alt));
jsonObject.SetNamedValue(JSON_KEY_SHIFT, json::value(hotkey.shift));
jsonObject.SetNamedValue(JSON_KEY_CTRL, json::value(hotkey.ctrl));
jsonObject.SetNamedValue(JSON_KEY_CODE, json::value(hotkey.key));
return jsonObject;
}
bool migrate_data_and_remove_data_file(Hotkey& old_paste_as_plain_hotkey)
{
const wchar_t OLD_JSON_KEY_ACTIVATION_SHORTCUT[] = L"ActivationShortcut";
@@ -131,7 +167,7 @@ private:
{
auto settingsObject = settings.get_raw_json();
// Migrate Paste As PLain text shortcut
// Migrate Paste As Plain text shortcut
Hotkey old_paste_as_plain_hotkey;
bool old_data_migrated = migrate_data_and_remove_data_file(old_paste_as_plain_hotkey);
if (old_data_migrated)
@@ -139,12 +175,7 @@ private:
m_paste_as_plain_hotkey = old_paste_as_plain_hotkey;
// override settings file
json::JsonObject new_hotkey_value;
new_hotkey_value.SetNamedValue(JSON_KEY_WIN, json::value(old_paste_as_plain_hotkey.win));
new_hotkey_value.SetNamedValue(JSON_KEY_ALT, json::value(old_paste_as_plain_hotkey.alt));
new_hotkey_value.SetNamedValue(JSON_KEY_SHIFT, json::value(old_paste_as_plain_hotkey.shift));
new_hotkey_value.SetNamedValue(JSON_KEY_CTRL, json::value(old_paste_as_plain_hotkey.ctrl));
new_hotkey_value.SetNamedValue(JSON_KEY_CODE, json::value(old_paste_as_plain_hotkey.key));
const auto new_hotkey_value = to_json_object(old_paste_as_plain_hotkey);
if (!settingsObject.HasKey(JSON_KEY_PROPERTIES))
{
@@ -153,13 +184,7 @@ private:
settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).SetNamedValue(JSON_KEY_PASTE_AS_PLAIN_HOTKEY, new_hotkey_value);
json::JsonObject ui_hotkey;
ui_hotkey.SetNamedValue(JSON_KEY_WIN, json::value(m_advanced_paste_ui_hotkey.win));
ui_hotkey.SetNamedValue(JSON_KEY_ALT, json::value(m_advanced_paste_ui_hotkey.alt));
ui_hotkey.SetNamedValue(JSON_KEY_SHIFT, json::value(m_advanced_paste_ui_hotkey.shift));
ui_hotkey.SetNamedValue(JSON_KEY_CTRL, json::value(m_advanced_paste_ui_hotkey.ctrl));
ui_hotkey.SetNamedValue(JSON_KEY_CODE, json::value(m_advanced_paste_ui_hotkey.key));
const auto ui_hotkey = to_json_object(m_advanced_paste_ui_hotkey);
settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).SetNamedValue(JSON_KEY_ADVANCED_PASTE_UI_HOTKEY, ui_hotkey);
settings.save_to_settings_file();
@@ -168,40 +193,53 @@ private:
{
if (settingsObject.GetView().Size())
{
if (settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).HasKey(JSON_KEY_PASTE_AS_PLAIN_HOTKEY))
const std::array<std::pair<Hotkey*, LPCWSTR>, NUM_DEFAULT_HOTKEYS> defaultHotkeys{
{ { &m_paste_as_plain_hotkey, JSON_KEY_PASTE_AS_PLAIN_HOTKEY },
{ &m_advanced_paste_ui_hotkey, JSON_KEY_ADVANCED_PASTE_UI_HOTKEY },
{ &m_paste_as_markdown_hotkey, JSON_KEY_PASTE_AS_MARKDOWN_HOTKEY },
{ &m_paste_as_json_hotkey, JSON_KEY_PASTE_AS_JSON_HOTKEY } }
};
for (auto& [hotkey, keyName] : defaultHotkeys)
{
m_paste_as_plain_hotkey = parse_single_hotkey(JSON_KEY_PASTE_AS_PLAIN_HOTKEY, settingsObject);
*hotkey = parse_single_hotkey(keyName, settingsObject);
}
if (settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).HasKey(JSON_KEY_ADVANCED_PASTE_UI_HOTKEY))
m_custom_action_hotkeys.clear();
m_custom_action_ids.clear();
if (settingsObject.HasKey(JSON_KEY_PROPERTIES))
{
m_advanced_paste_ui_hotkey = parse_single_hotkey(JSON_KEY_ADVANCED_PASTE_UI_HOTKEY, settingsObject);
}
if (settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).HasKey(JSON_KEY_PASTE_AS_MARKDOWN_HOTKEY))
{
m_paste_as_markdown_hotkey = parse_single_hotkey(JSON_KEY_PASTE_AS_MARKDOWN_HOTKEY, settingsObject);
}
if (settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).HasKey(JSON_KEY_PASTE_AS_JSON_HOTKEY))
{
m_paste_as_json_hotkey = parse_single_hotkey(JSON_KEY_PASTE_AS_JSON_HOTKEY, settingsObject);
const auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
if (propertiesObject.HasKey(JSON_KEY_CUSTOM_ACTIONS))
{
const auto customActions = propertiesObject.GetNamedObject(JSON_KEY_CUSTOM_ACTIONS).GetNamedArray(JSON_KEY_VALUE);
for (const auto& customAction : customActions)
{
const auto object = customAction.GetObjectW();
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)));
}
}
}
}
}
}
bool is_process_running()
bool is_process_running() const
{
return WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT;
}
void launch_process(const std::wstring& arg = L"")
void launch_process(const std::wstring& pipe_name)
{
Logger::trace(L"Starting AdvancedPaste process");
unsigned long powertoys_pid = GetCurrentProcessId();
const unsigned long powertoys_pid = GetCurrentProcessId();
std::wstring executable_args = L"";
executable_args.append(std::to_wstring(powertoys_pid));
executable_args += L" " + arg;
const auto executable_args = std::format(L"{} {}", std::to_wstring(powertoys_pid), pipe_name);
SHELLEXECUTEINFOW sei{ sizeof(sei) };
sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI };
@@ -221,6 +259,55 @@ private:
m_hProcess = sei.hProcess;
}
std::optional<std::wstring> get_pipe_name(const std::wstring& prefix) const
{
UUID temp_uuid;
wchar_t* uuid_chars = nullptr;
if (UuidCreate(&temp_uuid) == RPC_S_UUID_NO_ADDRESS)
{
const auto val = get_last_error_message(GetLastError());
Logger::error(L"UuidCreate cannot create guid. {}", val.has_value() ? val.value() : L"");
return std::nullopt;
}
else if (UuidToString(&temp_uuid, reinterpret_cast<RPC_WSTR*>(&uuid_chars)) != RPC_S_OK)
{
const auto val = get_last_error_message(GetLastError());
Logger::error(L"UuidToString cannot convert to string. {}", val.has_value() ? val.value() : L"");
return std::nullopt;
}
const auto pipe_name = std::format(L"{}{}", prefix, std::wstring(uuid_chars));
RpcStringFree(reinterpret_cast<RPC_WSTR*>(&uuid_chars));
return pipe_name;
}
void launch_process_and_named_pipe()
{
const auto pipe_name = get_pipe_name(L"powertoys_advanced_paste_");
if (!pipe_name)
{
return;
}
create_pipe_thread = std::thread([&] { start_named_pipe_server(pipe_name.value()); });
launch_process(pipe_name.value());
create_pipe_thread.join();
}
void send_named_pipe_message(const std::wstring& message_type, const std::wstring& message_arg = L"")
{
if (m_write_pipe)
{
const auto message = message_arg.empty() ? std::format(L"{}\r\n", message_type) : std::format(L"{} {}\r\n", message_type, message_arg);
const CString file_name(message.c_str());
m_write_pipe->Write(file_name, file_name.GetLength() * sizeof(TCHAR));
}
}
// Load the settings file.
void init_settings()
{
@@ -258,7 +345,7 @@ private:
}
}
void try_inject_modifier_key_restore(std::vector<INPUT> &inputs, short modifier)
void try_inject_modifier_key_restore(std::vector<INPUT>& inputs, short modifier)
{
// Most significant bit is set if key is down
if ((GetAsyncKeyState(static_cast<int>(modifier)) & 0x8000) != 0)
@@ -487,15 +574,54 @@ private:
EnumWindows(enum_windows, (LPARAM)m_hProcess);
}
HRESULT start_named_pipe_server(const std::wstring& pipe_name)
{
const constexpr DWORD BUFSIZE = 4096 * 4;
const auto full_pipe_name = std::format(L"\\\\.\\pipe\\{}", pipe_name);
const auto hPipe = CreateNamedPipe(
full_pipe_name.c_str(), // pipe name
PIPE_ACCESS_OUTBOUND, // write access
PIPE_TYPE_MESSAGE | // message type pipe
PIPE_READMODE_MESSAGE | // message-read mode
PIPE_WAIT, // blocking mode
1, // max. instances
BUFSIZE, // output buffer size
0, // input buffer size
0, // client time-out
NULL); // default security attribute
if (hPipe == NULL || hPipe == INVALID_HANDLE_VALUE)
{
return E_FAIL;
}
// This call blocks until a client process connects to the pipe
BOOL connected = ConnectNamedPipe(hPipe, NULL);
if (!connected)
{
if (GetLastError() == ERROR_PIPE_CONNECTED)
{
return S_OK;
}
else
{
CloseHandle(hPipe);
}
return E_FAIL;
}
m_write_pipe = std::make_unique<CAtlFile>(hPipe);
return S_OK;
}
public:
AdvancedPaste()
{
app_name = GET_RESOURCE_STRING(IDS_ADVANCED_PASTE_NAME);
app_key = AdvancedPasteConstants::ModuleKey;
LoggerHelpers::init_logger(app_key, L"ModuleInterface", "AdvancedPaste");
m_hShowUIEvent = CreateDefaultEvent(CommonSharedConstants::SHOW_ADVANCED_PASTE_SHARED_EVENT);
m_hPasteMarkdownEvent = CreateDefaultEvent(CommonSharedConstants::ADVANCED_PASTE_MARKDOWN_EVENT);
m_hPasteJsonEvent = CreateDefaultEvent(CommonSharedConstants::ADVANCED_PASTE_JSON_EVENT);
init_settings();
}
@@ -559,7 +685,7 @@ public:
parse_hotkeys(values);
auto settingsObject = values.get_raw_json();
const auto settingsObject = values.get_raw_json();
if (settingsObject.GetView().Size() && settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).HasKey(JSON_KEY_SHOW_CUSTOM_PREVIEW))
{
m_preview_custom_format_output = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_SHOW_CUSTOM_PREVIEW).GetNamedBoolean(JSON_KEY_VALUE);
@@ -567,10 +693,10 @@ public:
// 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_advanced_paste_ui_hotkey,
m_paste_as_markdown_hotkey,
m_paste_as_json_hotkey,
m_preview_custom_format_output);
// If you don't need to do any custom processing of the settings, proceed
// to persists the values calling:
@@ -588,12 +714,9 @@ public:
{
Logger::trace("AdvancedPaste::enable()");
Trace::AdvancedPaste_Enable(true);
ResetEvent(m_hShowUIEvent);
ResetEvent(m_hPasteMarkdownEvent);
ResetEvent(m_hPasteJsonEvent);
m_enabled = true;
launch_process();
launch_process_and_named_pipe();
};
virtual void disable()
@@ -601,9 +724,8 @@ public:
Logger::trace("AdvancedPaste::disable()");
if (m_enabled)
{
ResetEvent(m_hShowUIEvent);
ResetEvent(m_hPasteMarkdownEvent);
ResetEvent(m_hPasteJsonEvent);
m_write_pipe = nullptr;
TerminateProcess(m_hProcess, 1);
Trace::AdvancedPaste_Enable(false);
@@ -622,13 +744,14 @@ public:
if (!is_process_running())
{
Logger::trace(L"Launching new process");
launch_process();
launch_process_and_named_pipe();
Trace::AdvancedPaste_Invoked(L"AdvancedPasteUI");
}
// hotkeyId in same order as set by get_hotkeys
if (hotkeyId == 0) { // m_paste_as_plain_hotkey
if (hotkeyId == 0)
{ // m_paste_as_plain_hotkey
Logger::trace(L"Paste as plain text hotkey pressed");
std::thread([=]() {
@@ -641,21 +764,36 @@ public:
return true;
}
if (hotkeyId == 1) { // m_advanced_paste_ui_hotkey
if (hotkeyId == 1)
{ // m_advanced_paste_ui_hotkey
Logger::trace(L"Setting start up event");
bring_process_to_front();
SetEvent(m_hShowUIEvent);
send_named_pipe_message(CommonSharedConstants::ADVANCED_PASTE_SHOW_UI_MESSAGE);
return true;
}
if (hotkeyId == 2) { // m_paste_as_markdown_hotkey
if (hotkeyId == 2)
{ // m_paste_as_markdown_hotkey
Logger::trace(L"Starting paste as markdown directly");
SetEvent(m_hPasteMarkdownEvent);
send_named_pipe_message(CommonSharedConstants::ADVANCED_PASTE_MARKDOWN_MESSAGE);
return true;
}
if (hotkeyId == 3) { // m_paste_as_json_hotkey
if (hotkeyId == 3)
{ // m_paste_as_json_hotkey
Logger::trace(L"Starting paste as json directly");
SetEvent(m_hPasteJsonEvent);
send_named_pipe_message(CommonSharedConstants::ADVANCED_PASTE_JSON_MESSAGE);
return true;
}
const auto custom_action_index = hotkeyId - NUM_DEFAULT_HOTKEYS;
if (custom_action_index < m_custom_action_ids.size())
{
const auto id = m_custom_action_ids.at(custom_action_index);
Logger::trace(L"Starting custom action id={}", id);
send_named_pipe_message(CommonSharedConstants::ADVANCED_PASTE_CUSTOM_ACTION_MESSAGE, std::to_wstring(id));
return true;
}
}
@@ -665,14 +803,20 @@ public:
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
{
if (hotkeys && buffer_size >= 4)
const size_t num_hotkeys = NUM_DEFAULT_HOTKEYS + m_custom_action_hotkeys.size();
if (hotkeys && buffer_size >= num_hotkeys)
{
hotkeys[0] = m_paste_as_plain_hotkey;
hotkeys[1] = m_advanced_paste_ui_hotkey;
hotkeys[2] = m_paste_as_markdown_hotkey;
hotkeys[3] = m_paste_as_json_hotkey;
const std::array default_hotkeys = { m_paste_as_plain_hotkey,
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);
}
return 4;
return num_hotkeys;
}
virtual bool is_enabled() override