From d7e71b5ed00141752f5c9bbeafdc3da4b57a561b Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 22 Aug 2024 16:36:59 +0200 Subject: [PATCH] feat(Window): Allow to persist content of Clipboard after App close --- .../Platform/ClipboardImpl.cs | 3 + .../Input/Platform/IClipboard.cs | 38 +++++++++++- src/Avalonia.DesignerSupport/Remote/Stubs.cs | 1 + src/Avalonia.Native/ClipboardImpl.cs | 4 +- src/Avalonia.X11/X11Clipboard.cs | 4 +- src/Browser/Avalonia.Browser/ClipboardImpl.cs | 2 + .../HeadlessPlatformStubs.cs | 15 ++--- src/Tizen/Avalonia.Tizen/NuiClipboardImpl.cs | 2 + src/Windows/Avalonia.Win32/ClipboardImpl.cs | 61 +++++++++++++++++-- .../Interop/UnmanagedMethods.cs | 2 + .../MaskedTextBoxTests.cs | 5 ++ .../TextBoxTests.cs | 4 +- 12 files changed, 120 insertions(+), 21 deletions(-) diff --git a/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs index 028134ffad..79b3a77048 100644 --- a/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs +++ b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs @@ -55,5 +55,8 @@ namespace Avalonia.Android.Platform public Task GetFormatsAsync() => throw new PlatformNotSupportedException(); public Task GetDataAsync(string format) => throw new PlatformNotSupportedException(); + + /// + public Task SetDataObjectAsync(IDataObject data, bool copy) => throw new PlatformNotSupportedException(); } } diff --git a/src/Avalonia.Base/Input/Platform/IClipboard.cs b/src/Avalonia.Base/Input/Platform/IClipboard.cs index 3de352fc4f..03f393131f 100644 --- a/src/Avalonia.Base/Input/Platform/IClipboard.cs +++ b/src/Avalonia.Base/Input/Platform/IClipboard.cs @@ -6,16 +6,50 @@ namespace Avalonia.Input.Platform [NotClientImplementable] public interface IClipboard { + /// + /// Returns a string containing the text data on the Clipboard. + /// + /// A string containing text data in the specified data format, or an empty string if no corresponding text data is available. Task GetTextAsync(); + /// + /// Stores text data on the Clipboard. The text data to store is specified as a string. + /// + /// A string that contains the UnicodeText data to store on the Clipboard. + /// is null. Task SetTextAsync(string? text); + /// + /// Clears any data from the system Clipboard. + /// Task ClearAsync(); + /// + /// Places a specified non-persistent data object on the system Clipboard. + /// + /// A data object (an object that implements ) to place on the system Clipboard. + /// is null. Task SetDataObjectAsync(IDataObject data); - + + /// + /// Places a specified data object on the system Clipboard and accepts a Boolean parameter that indicates whether the data object should be left on the Clipboard when the application exits. + /// + /// A data object (an object that implements ) to place on the system Clipboard. + /// true to leave the data on the system Clipboard when the application exits; false to clear the data from the system Clipboard when the application exits. + /// is null. + Task SetDataObjectAsync(IDataObject data, bool copy); + + + /// + /// Get list of available Clipboard format. + /// Task GetFormatsAsync(); - + + /// + /// Retrieves data in a specified format from the Clipboard. + /// + /// A string that specifies the format of the data to retrieve. For a set of predefined data formats, see the class. + /// Task GetDataAsync(string format); } } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index e21f4dc843..e684a6f635 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -208,6 +208,7 @@ namespace Avalonia.DesignerSupport.Remote public Task ClearAsync() => Task.CompletedTask; public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask; + public Task SetDataObjectAsync(IDataObject data, bool copy) => Task.CompletedTask; public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); public Task GetDataAsync(string format) => Task.FromResult((object)null); diff --git a/src/Avalonia.Native/ClipboardImpl.cs b/src/Avalonia.Native/ClipboardImpl.cs index 0fc4d76224..8a3cb094d2 100644 --- a/src/Avalonia.Native/ClipboardImpl.cs +++ b/src/Avalonia.Native/ClipboardImpl.cs @@ -9,7 +9,6 @@ using Avalonia.Logging; using Avalonia.Native.Interop; using Avalonia.Platform.Storage; using Avalonia.Platform.Storage.FileIO; -using MicroCom.Runtime; namespace Avalonia.Native { @@ -156,6 +155,9 @@ namespace Avalonia.Native return Task.CompletedTask; } + /// + public Task SetDataObjectAsync(IDataObject data, bool copy) => throw new PlatformNotSupportedException(); + public Task GetFormatsAsync() { return Task.FromResult(GetFormats().ToArray()); diff --git a/src/Avalonia.X11/X11Clipboard.cs b/src/Avalonia.X11/X11Clipboard.cs index 637d44d617..befdd5eb81 100644 --- a/src/Avalonia.X11/X11Clipboard.cs +++ b/src/Avalonia.X11/X11Clipboard.cs @@ -302,10 +302,12 @@ namespace Avalonia.X11 public Task SetDataObjectAsync(IDataObject data) { _storedDataObject = data; - XSetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD, _handle, IntPtr.Zero); + XSetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD, _handle, IntPtr.Zero); return StoreAtomsInClipboardManager(data); } + public Task SetDataObjectAsync(IDataObject data, bool copy) => throw new PlatformNotSupportedException(); + public async Task GetFormatsAsync() { if (!HasOwner) diff --git a/src/Browser/Avalonia.Browser/ClipboardImpl.cs b/src/Browser/Avalonia.Browser/ClipboardImpl.cs index 5df09e555d..4ba8f4de8d 100644 --- a/src/Browser/Avalonia.Browser/ClipboardImpl.cs +++ b/src/Browser/Avalonia.Browser/ClipboardImpl.cs @@ -22,6 +22,8 @@ namespace Avalonia.Browser public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask; + /// + public Task SetDataObjectAsync(IDataObject data, bool copy) => Task.CompletedTask; public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); public Task GetDataAsync(string format) => Task.FromResult(null); diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs index 78fcb9ea74..4a42b0fc62 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -6,18 +6,12 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; -using Avalonia.Controls; -using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Media; -using Avalonia.Media.Fonts; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; -using Avalonia.Platform.Storage; -using Avalonia.Platform.Storage.FileIO; -using Avalonia.Utilities; namespace Avalonia.Headless { @@ -41,10 +35,11 @@ namespace Avalonia.Headless return Task.Run(() => _text = null); } - public Task SetDataObjectAsync(IDataObject data) - { - return Task.Run(() => _data = data); - } + public Task SetDataObjectAsync(IDataObject data) => + SetDataObjectAsync(data, false); + + public Task SetDataObjectAsync(IDataObject data, bool copy) => + Task.Run(() => _data = data); public Task GetFormatsAsync() { diff --git a/src/Tizen/Avalonia.Tizen/NuiClipboardImpl.cs b/src/Tizen/Avalonia.Tizen/NuiClipboardImpl.cs index 549582b034..faffa19845 100644 --- a/src/Tizen/Avalonia.Tizen/NuiClipboardImpl.cs +++ b/src/Tizen/Avalonia.Tizen/NuiClipboardImpl.cs @@ -65,6 +65,8 @@ internal class NuiClipboardImpl : IClipboard }); } + public Task SetDataObjectAsync(IDataObject data, bool copy) => throw new PlatformNotSupportedException(); + public Task GetDataAsync(string format) => throw new PlatformNotSupportedException(); diff --git a/src/Windows/Avalonia.Win32/ClipboardImpl.cs b/src/Windows/Avalonia.Win32/ClipboardImpl.cs index 1a760aeab8..d61dbcb4fa 100644 --- a/src/Windows/Avalonia.Win32/ClipboardImpl.cs +++ b/src/Windows/Avalonia.Win32/ClipboardImpl.cs @@ -1,10 +1,10 @@ using System; using System.Linq; -using Avalonia.Reactive; using System.Runtime.InteropServices; using System.Threading.Tasks; using Avalonia.Input; using Avalonia.Input.Platform; +using Avalonia.Reactive; using Avalonia.Threading; using Avalonia.Win32.Interop; using MicroCom.Runtime; @@ -15,6 +15,13 @@ namespace Avalonia.Win32 { private const int OleRetryCount = 10; private const int OleRetryDelay = 100; + /// + /// The amount of time in milliseconds to sleep before flushing the clipboard after a set. + /// + /// + /// This is mitigation for clipboard listener issues. + /// + private const int OleFlushDelay = 10; private static async Task OpenClipboard() { @@ -32,7 +39,7 @@ namespace Avalonia.Win32 public async Task GetTextAsync() { - using(await OpenClipboard()) + using (await OpenClipboard()) { IntPtr hText = UnmanagedMethods.GetClipboardData(UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT); if (hText == IntPtr.Zero) @@ -54,7 +61,7 @@ namespace Avalonia.Win32 public async Task SetTextAsync(string? text) { - using(await OpenClipboard()) + using (await OpenClipboard()) { UnmanagedMethods.EmptyClipboard(); @@ -68,13 +75,16 @@ namespace Avalonia.Win32 public async Task ClearAsync() { - using(await OpenClipboard()) + using (await OpenClipboard()) { UnmanagedMethods.EmptyClipboard(); } } - public async Task SetDataObjectAsync(IDataObject data) + public async Task SetDataObjectAsync(IDataObject data) => + await SetDataObjectAsync(data, false); + + public async Task SetDataObjectAsync(IDataObject data, bool copy) { Dispatcher.UIThread.VerifyAccess(); using var wrapper = new DataObject(data); @@ -90,9 +100,21 @@ namespace Avalonia.Win32 if (--i == 0) Marshal.ThrowExceptionForHR(hr); - + await Task.Delay(OleRetryDelay); } + + if (copy) + { + // OleSetClipboard and OleFlushClipboard both modify the clipboard + // and cause notifications to be sent to clipboard listeners. We sleep a bit here to + // mitigate issues with clipboard listeners (like TS) corrupting the clipboard contents + // as a result of these two calls being back to back. + await Task.Delay(OleFlushDelay); + + await FlushAsync(); + } + } public async Task GetFormatsAsync() @@ -142,5 +164,32 @@ namespace Avalonia.Win32 await Task.Delay(OleRetryDelay); } } + + /// + /// Permanently renders the contents of the last IDataObject that was set onto the clipboard. + /// + private static async Task FlushAsync() + { + // Retry OLE operations several times as mitigation for clipboard locking issues in TS sessions. + + int i = OleRetryCount; + + while (true) + { + var hr = UnmanagedMethods.OleFlushClipboard(); + + if (hr == 0) + { + break; + } + + if (--i == 0) + { + Marshal.ThrowExceptionForHR(hr); + } + + await Task.Delay(OleRetryDelay); + } + } } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index b575db6cdd..8d789b21a5 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1570,6 +1570,8 @@ namespace Avalonia.Win32.Interop [DllImport("ole32.dll", PreserveSig = true)] public static extern int OleGetClipboard(out IntPtr dataObject); + [DllImport("ole32.dll", PreserveSig = true)] + public static extern int OleFlushClipboard(); [DllImport("ole32.dll", PreserveSig = true)] public static extern int OleSetClipboard(IntPtr dataObject); diff --git a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs index 56f131b410..cc07937f15 100644 --- a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs @@ -990,9 +990,14 @@ namespace Avalonia.Controls.UnitTests public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask; + public Task SetDataObjectAsync(IDataObject data, bool copy) => Task.CompletedTask; + public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); public Task GetDataAsync(string format) => Task.FromResult((object)null); + + + } private class TestTopLevel : TopLevel diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 30f82533ce..6caf18506d 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -1651,10 +1651,12 @@ namespace Avalonia.Controls.UnitTests } public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask; - + public Task SetDataObjectAsync(IDataObject data, bool copy) => Task.CompletedTask; public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); public Task GetDataAsync(string format) => Task.FromResult((object)null); + + } private class TestTopLevel : TopLevel