Browse Source

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

pull/16778/head
Giuseppe Lippolis 2 years ago
parent
commit
d7e71b5ed0
  1. 3
      src/Android/Avalonia.Android/Platform/ClipboardImpl.cs
  2. 38
      src/Avalonia.Base/Input/Platform/IClipboard.cs
  3. 1
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  4. 4
      src/Avalonia.Native/ClipboardImpl.cs
  5. 4
      src/Avalonia.X11/X11Clipboard.cs
  6. 2
      src/Browser/Avalonia.Browser/ClipboardImpl.cs
  7. 15
      src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs
  8. 2
      src/Tizen/Avalonia.Tizen/NuiClipboardImpl.cs
  9. 61
      src/Windows/Avalonia.Win32/ClipboardImpl.cs
  10. 2
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  11. 5
      tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs
  12. 4
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

3
src/Android/Avalonia.Android/Platform/ClipboardImpl.cs

@ -55,5 +55,8 @@ namespace Avalonia.Android.Platform
public Task<string[]> GetFormatsAsync() => throw new PlatformNotSupportedException();
public Task<object?> GetDataAsync(string format) => throw new PlatformNotSupportedException();
/// <inheritdoc />
public Task SetDataObjectAsync(IDataObject data, bool copy) => throw new PlatformNotSupportedException();
}
}

38
src/Avalonia.Base/Input/Platform/IClipboard.cs

@ -6,16 +6,50 @@ 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>
/// 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.
/// </summary>
/// <param name="data">A data object (an object that implements <see cref="IDataObject"/>) to place on the system Clipboard.</param>
/// <param name="copy"><c>true</c> 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.</param>
/// <exception cref="System.ArgumentNullException"><paramref name="data"/> is null.</exception>
Task SetDataObjectAsync(IDataObject data, bool copy);
/// <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);
}
}

1
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<string[]> GetFormatsAsync() => Task.FromResult(Array.Empty<string>());
public Task<object> GetDataAsync(string format) => Task.FromResult((object)null);

4
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<string[]> GetFormatsAsync()
{
return Task.FromResult(GetFormats().ToArray());

4
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<string[]> GetFormatsAsync()
{
if (!HasOwner)

2
src/Browser/Avalonia.Browser/ClipboardImpl.cs

@ -22,6 +22,8 @@ namespace Avalonia.Browser
public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask;
/// <inheritdoc />
public Task SetDataObjectAsync(IDataObject data, bool copy) => Task.CompletedTask;
public Task<string[]> GetFormatsAsync() => Task.FromResult(Array.Empty<string>());
public Task<object?> GetDataAsync(string format) => Task.FromResult<object?>(null);

15
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<string[]> GetFormatsAsync()
{

2
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<object?> GetDataAsync(string format) =>
throw new PlatformNotSupportedException();

61
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,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<string[]> GetFormatsAsync()
@ -142,5 +164,32 @@ namespace Avalonia.Win32
await Task.Delay(OleRetryDelay);
}
}
/// <summary>
/// Permanently renders the contents of the last IDataObject that was set onto the clipboard.
/// </summary>
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);
}
}
}
}

2
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);

5
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<string[]> GetFormatsAsync() => Task.FromResult(Array.Empty<string>());
public Task<object> GetDataAsync(string format) => Task.FromResult((object)null);
}
private class TestTopLevel : TopLevel

4
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<string[]> GetFormatsAsync() => Task.FromResult(Array.Empty<string>());
public Task<object> GetDataAsync(string format) => Task.FromResult((object)null);
}
private class TestTopLevel : TopLevel

Loading…
Cancel
Save