[AdvancedPaste]Additional actions - Image to text, Paste as file (txt, png, html) (#35167)

* [AdvancedPaste] Additional actions, including Image to text

* Spellcheck issue

* [AdvancedPaste] Paste as file and many other improvements

* Fixed typo

* Fixed typo

* [AdvancedPaste] Improved paste window menu layout

* [AdvancedPaste] Improved settings window layout

* [AdvancedPaste] Removed AudioToText for the moment

* Code cleanup

* Minor fixes

* Changed log-line with potentially sensitive info

* Extra telemetry for AdvancedPaste

* Added 'Hotkey' suffix to AdvancedPaste_Settings telemetry event
This commit is contained in:
Ani
2024-10-18 15:34:09 +02:00
committed by GitHub
parent 14139affc5
commit dd5cd2a570
41 changed files with 1435 additions and 624 deletions

View File

@@ -0,0 +1,39 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
namespace Microsoft.PowerToys.Settings.UI.Library;
public sealed partial class AdvancedPasteAdditionalAction : Observable, IAdvancedPasteAction
{
private HotkeySettings _shortcut = new();
private bool _isShown = true;
[JsonPropertyName("shortcut")]
public HotkeySettings Shortcut
{
get => _shortcut;
set
{
if (_shortcut != value)
{
// We null-coalesce here rather than outside this branch as we want to raise PropertyChanged when the setter is called
// with null; the ShortcutControl depends on this.
_shortcut = value ?? new();
OnPropertyChanged();
}
}
}
[JsonPropertyName("isShown")]
public bool IsShown
{
get => _isShown;
set => Set(ref _isShown, value);
}
}

View File

@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
namespace Microsoft.PowerToys.Settings.UI.Library;
public sealed class AdvancedPasteAdditionalActions
{
public static class PropertyNames
{
public const string ImageToText = "image-to-text";
public const string PasteAsFile = "paste-as-file";
}
[JsonPropertyName(PropertyNames.ImageToText)]
public AdvancedPasteAdditionalAction ImageToText { get; init; } = new();
[JsonPropertyName(PropertyNames.PasteAsFile)]
public AdvancedPastePasteAsFileAction PasteAsFile { get; init; } = new();
[JsonIgnore]
public IEnumerable<IAdvancedPasteAction> AllActions => new IAdvancedPasteAction[] { ImageToText, PasteAsFile }.Concat(PasteAsFile.SubActions);
}

View File

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

View File

@@ -0,0 +1,56 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
namespace Microsoft.PowerToys.Settings.UI.Library;
public sealed class AdvancedPastePasteAsFileAction : Observable, IAdvancedPasteAction
{
public static class PropertyNames
{
public const string PasteAsTxtFile = "paste-as-txt-file";
public const string PasteAsPngFile = "paste-as-png-file";
public const string PasteAsHtmlFile = "paste-as-html-file";
}
private AdvancedPasteAdditionalAction _pasteAsTxtFile = new();
private AdvancedPasteAdditionalAction _pasteAsPngFile = new();
private AdvancedPasteAdditionalAction _pasteAsHtmlFile = new();
private bool _isShown = true;
[JsonPropertyName("isShown")]
public bool IsShown
{
get => _isShown;
set => Set(ref _isShown, value);
}
[JsonPropertyName(PropertyNames.PasteAsTxtFile)]
public AdvancedPasteAdditionalAction PasteAsTxtFile
{
get => _pasteAsTxtFile;
init => Set(ref _pasteAsTxtFile, value);
}
[JsonPropertyName(PropertyNames.PasteAsPngFile)]
public AdvancedPasteAdditionalAction PasteAsPngFile
{
get => _pasteAsPngFile;
init => Set(ref _pasteAsPngFile, value);
}
[JsonPropertyName(PropertyNames.PasteAsHtmlFile)]
public AdvancedPasteAdditionalAction PasteAsHtmlFile
{
get => _pasteAsHtmlFile;
init => Set(ref _pasteAsHtmlFile, value);
}
[JsonIgnore]
public IEnumerable<AdvancedPasteAdditionalAction> SubActions => [PasteAsTxtFile, PasteAsPngFile, PasteAsHtmlFile];
}

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.ComponentModel;
namespace Microsoft.PowerToys.Settings.UI.Library;
public interface IAdvancedPasteAction : INotifyPropertyChanged
{
public bool IsShown { get; }
}

View File

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

View File

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

View File

@@ -38,6 +38,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private readonly object _delayedActionLock = new object();
private readonly AdvancedPasteSettings _advancedPasteSettings;
private readonly AdvancedPasteAdditionalActions _additionalActions;
private readonly ObservableCollection<AdvancedPasteCustomAction> _customActions;
private Timer _delayedTimer;
@@ -69,6 +70,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
_advancedPasteSettings = advancedPasteSettingsRepository.SettingsConfig;
_additionalActions = _advancedPasteSettings.Properties.AdditionalActions;
_customActions = _advancedPasteSettings.Properties.CustomActions.Value;
InitializeEnabledValue();
@@ -81,6 +83,11 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
_delayedTimer.Elapsed += DelayedTimer_Tick;
_delayedTimer.AutoReset = false;
foreach (var action in _additionalActions.AllActions)
{
action.PropertyChanged += OnAdditionalActionPropertyChanged;
}
foreach (var customAction in _customActions)
{
customAction.PropertyChanged += OnCustomActionPropertyChanged;
@@ -143,6 +150,8 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public ObservableCollection<AdvancedPasteCustomAction> CustomActions => _customActions;
public AdvancedPasteAdditionalActions AdditionalActions => _additionalActions;
private bool OpenAIKeyExists()
{
PasswordVault vault = new PasswordVault();
@@ -336,12 +345,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
public bool IsConflictingCopyShortcut
{
get => _customActions.Select(customAction => customAction.Shortcut)
.Concat([PasteAsPlainTextShortcut, AdvancedPasteUIShortcut, PasteAsMarkdownShortcut, PasteAsJsonShortcut])
.Any(hotkey => WarnHotkeys.Contains(hotkey.ToString()));
}
public bool IsConflictingCopyShortcut =>
_customActions.Select(customAction => customAction.Shortcut)
.Concat([PasteAsPlainTextShortcut, AdvancedPasteUIShortcut, PasteAsMarkdownShortcut, PasteAsJsonShortcut])
.Any(hotkey => WarnHotkeys.Contains(hotkey.ToString()));
public bool IsAdditionalActionConflictingCopyShortcut =>
_additionalActions.AllActions
.OfType<AdvancedPasteAdditionalAction>()
.Select(additionalAction => additionalAction.Shortcut)
.Any(hotkey => WarnHotkeys.Contains(hotkey.ToString()));
private void DelayedTimer_Tick(object sender, EventArgs e)
{
@@ -461,6 +474,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
NotifySettingsChanged();
}
private void OnAdditionalActionPropertyChanged(object sender, PropertyChangedEventArgs e)
{
SaveAndNotifySettings();
if (e.PropertyName == nameof(AdvancedPasteAdditionalAction.Shortcut))
{
OnPropertyChanged(nameof(IsAdditionalActionConflictingCopyShortcut));
}
}
private void OnCustomActionPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (typeof(AdvancedPasteCustomAction).GetProperty(e.PropertyName).GetCustomAttribute<JsonIgnoreAttribute>() == null)