diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index 6b0f1c59cc..08c27e16f2 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -61,6 +61,12 @@ baseline/netstandard2.0/Avalonia.Base.dll target/netstandard2.0/Avalonia.Base.dll + + CP0006 + M:Avalonia.Input.Platform.IClipboard.FlushAsync + baseline/netstandard2.0/Avalonia.Base.dll + target/netstandard2.0/Avalonia.Base.dll + CP0006 M:Avalonia.Controls.Notifications.IManagedNotificationManager.Close(System.Object) diff --git a/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs index 028134ffad..163e0c53ec 100644 --- a/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs +++ b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs @@ -55,5 +55,9 @@ namespace Avalonia.Android.Platform public Task GetFormatsAsync() => throw new PlatformNotSupportedException(); public Task GetDataAsync(string format) => throw new PlatformNotSupportedException(); + + /// + public Task FlushAsync() => + Task.CompletedTask; } } diff --git a/src/Avalonia.Base/Input/Platform/IClipboard.cs b/src/Avalonia.Base/Input/Platform/IClipboard.cs index 3de352fc4f..f042537387 100644 --- a/src/Avalonia.Base/Input/Platform/IClipboard.cs +++ b/src/Avalonia.Base/Input/Platform/IClipboard.cs @@ -6,16 +6,48 @@ 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); - + + /// + /// Permanently adds the data that is on the Clipboard so that it is available after the data's original application closes. + /// + /// + /// This method works only on Windows platform, on other platforms it does nothing. + Task FlushAsync(); + + /// + /// 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 e3117ab8b2..dc7ab400cf 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -225,6 +225,9 @@ namespace Avalonia.DesignerSupport.Remote public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); public Task GetDataAsync(string format) => Task.FromResult((object)null); + + public Task FlushAsync() => + Task.CompletedTask; } class CursorFactoryStub : ICursorFactory diff --git a/src/Avalonia.Native/ClipboardImpl.cs b/src/Avalonia.Native/ClipboardImpl.cs index 34b47ce236..96370895a8 100644 --- a/src/Avalonia.Native/ClipboardImpl.cs +++ b/src/Avalonia.Native/ClipboardImpl.cs @@ -182,6 +182,10 @@ namespace Avalonia.Native using (var n = Native.GetBytes(format)) return n.Bytes; } + + /// + public Task FlushAsync() => + Task.CompletedTask; } class ClipboardDataObject : IDataObject, IDisposable diff --git a/src/Avalonia.X11/X11Clipboard.cs b/src/Avalonia.X11/X11Clipboard.cs index da079458d9..bfa72dfeec 100644 --- a/src/Avalonia.X11/X11Clipboard.cs +++ b/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); } + + /// + public Task FlushAsync() => + Task.CompletedTask; } } diff --git a/src/Browser/Avalonia.Browser/ClipboardImpl.cs b/src/Browser/Avalonia.Browser/ClipboardImpl.cs index 5df09e555d..b02a0f804f 100644 --- a/src/Browser/Avalonia.Browser/ClipboardImpl.cs +++ b/src/Browser/Avalonia.Browser/ClipboardImpl.cs @@ -25,5 +25,9 @@ namespace Avalonia.Browser public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); public Task GetDataAsync(string format) => Task.FromResult(null); + + /// + public Task FlushAsync() => + Task.CompletedTask; } } diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs index 6bfe8562dc..47ae308115 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 { @@ -82,6 +76,10 @@ namespace Avalonia.Headless return (object?)_data; }); } + + /// + public Task FlushAsync() => + Task.CompletedTask; } internal class HeadlessCursorFactoryStub : ICursorFactory diff --git a/src/Tizen/Avalonia.Tizen/NuiClipboardImpl.cs b/src/Tizen/Avalonia.Tizen/NuiClipboardImpl.cs index 549582b034..d08eb6f36e 100644 --- a/src/Tizen/Avalonia.Tizen/NuiClipboardImpl.cs +++ b/src/Tizen/Avalonia.Tizen/NuiClipboardImpl.cs @@ -73,4 +73,8 @@ internal class NuiClipboardImpl : IClipboard public Task GetFormatsAsync() => throw new PlatformNotSupportedException(); + + /// + public Task FlushAsync() => + Task.CompletedTask; } diff --git a/src/Windows/Avalonia.Win32/ClipboardImpl.cs b/src/Windows/Avalonia.Win32/ClipboardImpl.cs index 1a760aeab8..5bea02b2ff 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,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); } } + + /// + /// Permanently renders the contents of the last IDataObject that was set onto the clipboard. + /// + 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); + } + } } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 5ebc165dd5..cfb818e682 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/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); diff --git a/src/iOS/Avalonia.iOS/ClipboardImpl.cs b/src/iOS/Avalonia.iOS/ClipboardImpl.cs index 8c6cadee0e..6061e006ee 100644 --- a/src/iOS/Avalonia.iOS/ClipboardImpl.cs +++ b/src/iOS/Avalonia.iOS/ClipboardImpl.cs @@ -57,5 +57,9 @@ namespace Avalonia.iOS return Task.FromResult(null); } + + /// + public Task FlushAsync() => + Task.CompletedTask; } } diff --git a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs index 70c6969021..71ff289c22 100644 --- a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs +++ b/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(); + var texts = new System.Collections.Generic.List(); target.PropertyChanged += (_, e) => { @@ -1028,6 +1025,9 @@ namespace Avalonia.Controls.UnitTests public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); public Task GetDataAsync(string format) => Task.FromResult((object)null); + + public Task FlushAsync() => + Task.CompletedTask; } private class TestTopLevel : TopLevel diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 66108952ad..36a9c8cc68 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -1995,7 +1995,10 @@ namespace Avalonia.Controls.UnitTests public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); - public Task GetDataAsync(string format) => Task.FromResult((object?)null); + public Task GetDataAsync(string format) => Task.FromResult((object)null); + + public Task FlushAsync() => + Task.CompletedTask; } private class TestTopLevel : TopLevel