Browse Source

feat(Window): Allow to persist content of Clipboard after App close (#16778)

* feat(Window): Allow to persist content of Clipboard after App close

* fix: missing iOS Platform

* fix(Api Validation): Missing suppression

* Revert "fix(Api Validation): Missing suppression"

This reverts commit 04b4e0c634ca70759969e098b581d7e1bbae802a.

* fix: Address review

* fix: ValidateApiDiff

* test: Fix build issue

---------

Co-authored-by: Julien Lebosquain <[email protected]>
workgroupengineering 7 months ago
parent
commit
61de7e36b1

+ 6 - 0
api/Avalonia.nupkg.xml

@@ -61,6 +61,12 @@
     <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
     <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
   </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0006</DiagnosticId>
+    <Target>M:Avalonia.Input.Platform.IClipboard.FlushAsync</Target>
+    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
+    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
+  </Suppression>
   <Suppression>
     <DiagnosticId>CP0006</DiagnosticId>
     <Target>M:Avalonia.Controls.Notifications.IManagedNotificationManager.Close(System.Object)</Target>

+ 4 - 0
src/Android/Avalonia.Android/Platform/ClipboardImpl.cs

@@ -55,5 +55,9 @@ namespace Avalonia.Android.Platform
         public Task<string[]> GetFormatsAsync() => throw new PlatformNotSupportedException();
 
         public Task<object?> GetDataAsync(string format) => throw new PlatformNotSupportedException();
+
+        /// <inheritdoc />
+        public Task FlushAsync() =>
+            Task.CompletedTask;
     }
 }

+ 34 - 2
src/Avalonia.Base/Input/Platform/IClipboard.cs

@@ -6,16 +6,48 @@ namespace Avalonia.Input.Platform
     [NotClientImplementable]
     public interface IClipboard
     {
+        /// <summary>
+        /// Returns a string containing the text data on the Clipboard.
+        /// </summary>
+        /// <returns>A string containing text data in the specified data format, or an empty string if no corresponding text data is available.</returns>
         Task<string?> GetTextAsync();
 
+        /// <summary>
+        /// Stores text data on the Clipboard. The text data to store is specified as a string.
+        /// </summary>
+        /// <param name="text">A string that contains the UnicodeText data to store on the Clipboard.</param>
+        /// <exception cref="System.ArgumentNullException"><paramref name="text"/> is null.</exception>
         Task SetTextAsync(string? text);
 
+        /// <summary>
+        /// Clears any data from the system Clipboard.
+        /// </summary>
         Task ClearAsync();
 
+        /// <summary>
+        /// Places a specified non-persistent data object on the system Clipboard.
+        /// </summary>
+        /// <param name="data">A data object (an object that implements <see cref="IDataObject"/>) to place on the system Clipboard.</param>
+        /// <exception cref="System.ArgumentNullException"><paramref name="data"/> is null.</exception>
         Task SetDataObjectAsync(IDataObject data);
-        
+
+        /// <summary>
+        /// Permanently adds the data that is on the Clipboard so that it is available after the data's original application closes.
+        /// </summary>
+        /// <returns></returns>
+        /// <remarks>This method works only on Windows platform, on other platforms it does nothing.</remarks>
+        Task FlushAsync();
+
+        /// <summary>
+        /// Get list of available Clipboard format.
+        /// </summary>
         Task<string[]> GetFormatsAsync();
-        
+
+        /// <summary>
+        /// Retrieves data in a specified format from the Clipboard.
+        /// </summary>
+        /// <param name="format">A string that specifies the format of the data to retrieve. For a set of predefined data formats, see the <see cref="DataFormats"/> class.</param>
+        /// <returns></returns>
         Task<object?> GetDataAsync(string format);
     }
 }

+ 3 - 0
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@@ -225,6 +225,9 @@ namespace Avalonia.DesignerSupport.Remote
         public Task<string[]> GetFormatsAsync() => Task.FromResult(Array.Empty<string>());
 
         public Task<object> GetDataAsync(string format) => Task.FromResult((object)null);
+
+        public Task FlushAsync() =>
+            Task.CompletedTask;
     }
 
     class CursorFactoryStub : ICursorFactory

+ 4 - 0
src/Avalonia.Native/ClipboardImpl.cs

@@ -182,6 +182,10 @@ namespace Avalonia.Native
             using (var n = Native.GetBytes(format))
                 return n.Bytes;
         }
+
+        /// <inheritdoc />
+        public Task FlushAsync() =>
+            Task.CompletedTask;
     }
     
     class ClipboardDataObject : IDataObject, IDisposable

+ 5 - 1
src/Avalonia.X11/X11Clipboard.cs

@@ -310,7 +310,7 @@ 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);
         }
 
@@ -350,5 +350,9 @@ namespace Avalonia.X11
             
             return await SendDataRequest(formatAtom);
         }
+
+        /// <inheritdoc />
+        public Task FlushAsync() =>
+            Task.CompletedTask;
     }
 }

+ 4 - 0
src/Browser/Avalonia.Browser/ClipboardImpl.cs

@@ -25,5 +25,9 @@ namespace Avalonia.Browser
         public Task<string[]> GetFormatsAsync() => Task.FromResult(Array.Empty<string>());
 
         public Task<object?> GetDataAsync(string format) => Task.FromResult<object?>(null);
+
+        /// <inheritdoc />
+        public Task FlushAsync() =>
+            Task.CompletedTask;
     }
 }

+ 4 - 6
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
 {
@@ -82,6 +76,10 @@ namespace Avalonia.Headless
                     return (object?)_data;
             });
         }
+
+        /// <inheritdoc />
+        public Task FlushAsync() =>
+            Task.CompletedTask;
     }
 
     internal class HeadlessCursorFactoryStub : ICursorFactory

+ 4 - 0
src/Tizen/Avalonia.Tizen/NuiClipboardImpl.cs

@@ -73,4 +73,8 @@ internal class NuiClipboardImpl : IClipboard
 
     public Task<string[]> GetFormatsAsync() =>
         throw new PlatformNotSupportedException();
+
+    /// <inheritdoc />
+    public Task FlushAsync() => 
+        Task.CompletedTask;
 }

+ 41 - 5
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;
+        /// <summary>
+        /// The amount of time in milliseconds to sleep before flushing the clipboard after a set.
+        /// </summary>
+        /// <remarks>
+        /// This is mitigation for clipboard listener issues.
+        /// </remarks>
+        private const int OleFlushDelay = 10;
 
         private static async Task<IDisposable> OpenClipboard()
         {
@@ -32,7 +39,7 @@ namespace Avalonia.Win32
 
         public async Task<string?> 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,7 +75,7 @@ namespace Avalonia.Win32
 
         public async Task ClearAsync()
         {
-            using(await OpenClipboard())
+            using (await OpenClipboard())
             {
                 UnmanagedMethods.EmptyClipboard();
             }
@@ -90,7 +97,7 @@ namespace Avalonia.Win32
 
                 if (--i == 0)
                     Marshal.ThrowExceptionForHR(hr);
-                
+
                 await Task.Delay(OleRetryDelay);
             }
         }
@@ -142,5 +149,34 @@ namespace Avalonia.Win32
                 await Task.Delay(OleRetryDelay);
             }
         }
+
+        /// <summary>
+        /// Permanently renders the contents of the last IDataObject that was set onto the clipboard.
+        /// </summary>
+        public async Task FlushAsync()
+        {
+            await Task.Delay(OleFlushDelay);
+
+            // 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);
+            }
+        }
     }
 }

+ 2 - 0
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@@ -1573,6 +1573,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);

+ 4 - 0
src/iOS/Avalonia.iOS/ClipboardImpl.cs

@@ -57,5 +57,9 @@ namespace Avalonia.iOS
 
             return Task.FromResult<object?>(null);
         }
+
+        /// <inheritdoc />
+        public Task FlushAsync() =>
+            Task.CompletedTask;
     }
 }

+ 4 - 4
tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs

@@ -1,5 +1,4 @@
 using System;
-using System.Collections.Generic;
 using System.Globalization;
 using System.Reactive.Linq;
 using System.Threading.Tasks;
@@ -13,8 +12,6 @@ using Avalonia.Input.Platform;
 using Avalonia.Layout;
 using Avalonia.Media;
 using Avalonia.Platform;
-using Avalonia.Rendering;
-using Avalonia.Rendering.Composition;
 using Avalonia.UnitTests;
 using Moq;
 using Xunit;
@@ -906,7 +903,7 @@ namespace Avalonia.Controls.UnitTests
                 topLevel.ApplyTemplate();
                 topLevel.LayoutManager.ExecuteInitialLayoutPass();
 
-                var texts = new List<string>();
+                var texts = new System.Collections.Generic.List<string>();
 
                 target.PropertyChanged += (_, e) =>
                 {
@@ -1028,6 +1025,9 @@ namespace Avalonia.Controls.UnitTests
             public Task<string[]> GetFormatsAsync() => Task.FromResult(Array.Empty<string>());
 
             public Task<object> GetDataAsync(string format) => Task.FromResult((object)null);
+
+            public Task FlushAsync() =>
+                Task.CompletedTask;
         }
 
         private class TestTopLevel : TopLevel

+ 4 - 1
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@@ -1995,7 +1995,10 @@ namespace Avalonia.Controls.UnitTests
 
             public Task<string[]> GetFormatsAsync() => Task.FromResult(Array.Empty<string>());
 
-            public Task<object?> GetDataAsync(string format) => Task.FromResult((object?)null);
+            public Task<object> GetDataAsync(string format) => Task.FromResult((object)null);
+
+            public Task FlushAsync() =>
+                Task.CompletedTask;
         }
 
         private class TestTopLevel : TopLevel