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 04b4e0c634.

* fix: Address review

* fix: ValidateApiDiff

* test: Fix build issue

---------

Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
pull/18361/head
workgroupengineering 11 months ago
committed by GitHub
parent
commit
61de7e36b1
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      api/Avalonia.nupkg.xml
  2. 4
      src/Android/Avalonia.Android/Platform/ClipboardImpl.cs
  3. 36
      src/Avalonia.Base/Input/Platform/IClipboard.cs
  4. 3
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  5. 4
      src/Avalonia.Native/ClipboardImpl.cs
  6. 6
      src/Avalonia.X11/X11Clipboard.cs
  7. 4
      src/Browser/Avalonia.Browser/ClipboardImpl.cs
  8. 10
      src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs
  9. 4
      src/Tizen/Avalonia.Tizen/NuiClipboardImpl.cs
  10. 46
      src/Windows/Avalonia.Win32/ClipboardImpl.cs
  11. 2
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  12. 4
      src/iOS/Avalonia.iOS/ClipboardImpl.cs
  13. 8
      tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs
  14. 5
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

6
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
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;
}
}

36
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
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
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

6
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
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;
}
}

10
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
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;
}

46
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
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
src/iOS/Avalonia.iOS/ClipboardImpl.cs

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

8
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

5
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

Loading…
Cancel
Save