Browse Source

Added IClipboard:TryGetInProcessDataObjectAsyns (#18340)

* Implemented IClipboard.TryGetInProcessDataObjectAsync for X11

* [Win32] Implemented TryGetInProcessDataObjectAsync

* Rest of the platforms

* Run `nuke ValidateApiDiff`

---------

Co-authored-by: Max Katz <maxkatz6@outlook.com>
pull/18430/head
Nikita Tsukanov 11 months ago
committed by GitHub
parent
commit
c3b4d61b8a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 12
      api/Avalonia.nupkg.xml
  2. 18
      native/Avalonia.Native/src/OSX/clipboard.mm
  3. 3
      samples/ControlCatalog/Pages/ClipboardPage.xaml
  4. 46
      samples/ControlCatalog/Pages/ClipboardPage.xaml.cs
  5. 2
      src/Android/Avalonia.Android/Platform/ClipboardImpl.cs
  6. 9
      src/Avalonia.Base/Input/Platform/IClipboard.cs
  7. 2
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  8. 26
      src/Avalonia.Native/ClipboardImpl.cs
  9. 4
      src/Avalonia.Native/avn.idl
  10. 7
      src/Avalonia.X11/X11Clipboard.cs
  11. 2
      src/Browser/Avalonia.Browser/ClipboardImpl.cs
  12. 51
      src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs
  13. 2
      src/Tizen/Avalonia.Tizen/NuiClipboardImpl.cs
  14. 26
      src/Windows/Avalonia.Win32/ClipboardImpl.cs
  15. 3
      src/Windows/Avalonia.Win32/DataObject.cs
  16. 3
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  17. 2
      src/iOS/Avalonia.iOS/ClipboardImpl.cs
  18. 30
      tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs
  19. 30
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

12
api/Avalonia.nupkg.xml

@ -51,19 +51,25 @@
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Platform.Storage.IStorageFolder.GetFileAsync(System.String)</Target>
<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.Platform.Storage.IStorageFolder.GetFolderAsync(System.String)</Target>
<Target>M:Avalonia.Input.Platform.IClipboard.TryGetInProcessDataObjectAsync</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.Input.Platform.IClipboard.FlushAsync</Target>
<Target>M:Avalonia.Platform.Storage.IStorageFolder.GetFileAsync(System.String)</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.Platform.Storage.IStorageFolder.GetFolderAsync(System.String)</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>

18
native/Avalonia.Native/src/OSX/clipboard.mm

@ -148,17 +148,20 @@ public:
}
virtual HRESULT Clear() override
virtual HRESULT Clear(int64_t* rv) override
{
START_COM_CALL;
@autoreleasepool
{
if(_item != nil)
{
_item = [NSPasteboardItem new];
return 0;
}
else
{
[_pb clearContents];
*rv = [_pb clearContents];
[_pb setString:@"" forType:NSPasteboardTypeString];
}
@ -166,6 +169,17 @@ public:
}
}
virtual HRESULT GetChangeCount(int64_t* rv) override
{
START_COM_CALL;
if(_item == nil)
{
*rv = [_pb changeCount];
return S_OK;
}
return E_NOTIMPL;
}
virtual HRESULT ObtainFormats(IAvnStringArray** ppv) override
{
START_COM_CALL;

3
samples/ControlCatalog/Pages/ClipboardPage.xaml

@ -15,6 +15,9 @@
<Button Click="GetFormats" Content="Get clipboard formats" />
<Button Click="Clear" Content="Clear clipboard" />
<StackPanel Orientation="Horizontal">
<TextBlock Padding="0 0 5 0">Our DataObject is still on clipboard? <Run x:Name="OwnsClipboardDataObject"/></TextBlock>
</StackPanel>
<TextBox x:Name="ClipboardContent"
MinHeight="100"
AcceptsReturn="True"

46
samples/ControlCatalog/Pages/ClipboardPage.xaml.cs

@ -3,13 +3,17 @@ using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Controls.Notifications;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
using Avalonia.Threading;
namespace ControlCatalog.Pages
{
@ -18,8 +22,13 @@ namespace ControlCatalog.Pages
private INotificationManager? _notificationManager;
private INotificationManager NotificationManager => _notificationManager
??= new WindowNotificationManager(TopLevel.GetTopLevel(this)!);
private readonly DispatcherTimer _clipboardLastDataObjectChecker;
private DataObject? _storedDataObject;
public ClipboardPage()
{
_clipboardLastDataObjectChecker =
new DispatcherTimer(TimeSpan.FromSeconds(0.5), default, CheckLastDataObject);
InitializeComponent();
}
@ -48,7 +57,7 @@ namespace ControlCatalog.Pages
{
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
var dataObject = new DataObject();
var dataObject = _storedDataObject = new DataObject();
dataObject.Set(DataFormats.Text, ClipboardContent.Text ?? string.Empty);
await clipboard.SetDataObjectAsync(dataObject);
}
@ -96,7 +105,7 @@ namespace ControlCatalog.Pages
if (files.Count > 0)
{
var dataObject = new DataObject();
var dataObject = _storedDataObject = new DataObject();
dataObject.Set(DataFormats.Files, files);
await clipboard.SetDataObjectAsync(dataObject);
NotificationManager.Show(new Notification("Success", "Copy completated.", NotificationType.Success));
@ -135,5 +144,38 @@ namespace ControlCatalog.Pages
}
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
_clipboardLastDataObjectChecker.Start();
base.OnAttachedToVisualTree(e);
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
_clipboardLastDataObjectChecker.Stop();
base.OnDetachedFromVisualTree(e);
}
private Run OwnsClipboardDataObject => this.Get<Run>("OwnsClipboardDataObject");
private bool _checkingClipboardDataObject;
private async void CheckLastDataObject(object? sender, EventArgs e)
{
if(_checkingClipboardDataObject)
return;
try
{
_checkingClipboardDataObject = true;
var task = TopLevel.GetTopLevel(this)?.Clipboard?.TryGetInProcessDataObjectAsync();
var owns = task != null && (await task) == _storedDataObject && _storedDataObject != null;
OwnsClipboardDataObject.Text = owns ? "Yes" : "No";
OwnsClipboardDataObject.Foreground = owns ? Brushes.Green : Brushes.Red;
}
finally
{
_checkingClipboardDataObject = false;
}
}
}
}

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

@ -56,6 +56,8 @@ namespace Avalonia.Android.Platform
public Task<object?> GetDataAsync(string format) => throw new PlatformNotSupportedException();
public Task<IDataObject?> TryGetInProcessDataObjectAsync() => Task.FromResult<IDataObject?>(null);
/// <inheritdoc />
public Task FlushAsync() =>
Task.CompletedTask;

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

@ -49,5 +49,14 @@ namespace Avalonia.Input.Platform
/// <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);
/// <summary>
/// If clipboard contains the IDataObject that was set by a previous call to <see cref="SetDataObjectAsync"/>,
/// return said IDataObject instance. Otherwise, return null.
/// Note that not every platform supports that method, on unsupported platforms this method will always return
/// null
/// </summary>
/// <returns></returns>
Task<IDataObject?> TryGetInProcessDataObjectAsync();
}
}

2
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@ -226,6 +226,8 @@ namespace Avalonia.DesignerSupport.Remote
public Task<object> GetDataAsync(string format) => Task.FromResult((object)null);
public Task<IDataObject> TryGetInProcessDataObjectAsync() => Task.FromResult<IDataObject>(null);
public Task FlushAsync() =>
Task.CompletedTask;
}

26
src/Avalonia.Native/ClipboardImpl.cs

@ -17,6 +17,8 @@ namespace Avalonia.Native
class ClipboardImpl : IClipboard, IDisposable
{
private IAvnClipboard? _native;
private IDataObject? _savedDataObject;
private long _lastClearChangeCount;
// TODO hide native types behind IAvnClipboard abstraction, so managed side won't depend on macOS.
private const string NSPasteboardTypeString = "public.utf8-plain-text";
@ -30,10 +32,15 @@ namespace Avalonia.Native
private IAvnClipboard Native
=> _native ?? throw new ObjectDisposedException(nameof(ClipboardImpl));
private void ClearCore()
{
_savedDataObject = null;
_lastClearChangeCount = Native.Clear();
}
public Task ClearAsync()
{
Native.Clear();
ClearCore();
return Task.CompletedTask;
}
@ -47,7 +54,7 @@ namespace Avalonia.Native
{
var native = Native;
native.Clear();
ClearCore();
if (text != null)
native.SetText(NSPasteboardTypeString, text);
@ -83,6 +90,7 @@ namespace Avalonia.Native
public void Dispose()
{
_savedDataObject = null;
_native?.Dispose();
_native = null;
}
@ -111,7 +119,7 @@ namespace Avalonia.Native
public unsafe Task SetDataObjectAsync(IDataObject data)
{
Native.Clear();
ClearCore();
// If there is multiple values with the same "to" format, prefer these that were not mapped.
var formats = data.GetDataFormats().Select(f =>
@ -163,6 +171,8 @@ namespace Avalonia.Native
break;
}
}
_savedDataObject = data;
return Task.CompletedTask;
}
@ -183,9 +193,17 @@ namespace Avalonia.Native
return n.Bytes;
}
public Task<IDataObject?> TryGetInProcessDataObjectAsync()
{
if (Native.ChangeCount != _lastClearChangeCount)
_savedDataObject = null;
return Task.FromResult(_savedDataObject);
}
/// <inheritdoc />
public Task FlushAsync() =>
Task.CompletedTask;
}
class ClipboardDataObject : IDataObject, IDisposable

4
src/Avalonia.Native/avn.idl

@ -2,6 +2,7 @@
@clr-access internal
@clr-map bool int
@clr-map u_int64_t ulong
@clr-map int64_t long
@clr-map long IntPtr
@cpp-preamble @@
#pragma once
@ -972,7 +973,8 @@ interface IAvnClipboard : IUnknown
HRESULT SetBytes(char* type, void* utf8Text, int len);
HRESULT GetBytes(char* type, IAvnString**ppv);
HRESULT Clear();
HRESULT Clear(int64_t* ret);
HRESULT GetChangeCount(int64_t* ret);
}
[uuid(3f998545-f027-4d4d-bd2a-1a80926d984e)]

7
src/Avalonia.X11/X11Clipboard.cs

@ -314,6 +314,13 @@ namespace Avalonia.X11
return StoreAtomsInClipboardManager(data);
}
public Task<IDataObject?> TryGetInProcessDataObjectAsync()
{
if (XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD) == _handle)
return Task.FromResult(_storedDataObject);
return Task.FromResult<IDataObject?>(null);
}
public async Task<string[]> GetFormatsAsync()
{
if (!HasOwner)

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

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

51
src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs

@ -17,66 +17,47 @@ namespace Avalonia.Headless
{
internal class HeadlessClipboardStub : IClipboard
{
private string? _text;
private IDataObject? _data;
public Task<string?> GetTextAsync()
{
return Task.Run(() => _text);
return Task.FromResult(_data?.GetText());
}
public Task SetTextAsync(string? text)
{
return Task.Run(() => _text = text);
var data = new DataObject();
if (text != null)
data.Set(DataFormats.Text, text);
_data = data;
return Task.CompletedTask;
}
public Task ClearAsync()
{
return Task.Run(() => _text = null);
_data = null;
return Task.CompletedTask;
}
public Task SetDataObjectAsync(IDataObject data)
{
return Task.Run(() => _data = data);
_data = data;
return Task.CompletedTask;
}
public Task<string[]> GetFormatsAsync()
{
return Task.Run(() =>
{
if (_data is not null)
{
return _data.GetDataFormats().ToArray();
}
if (_text is not null)
{
return new[] { DataFormats.Text };
}
return Array.Empty<string>();
});
return Task.FromResult<string[]>(_data?.GetDataFormats().ToArray() ?? []);
}
public async Task<object?> GetDataAsync(string format)
public Task<object?> GetDataAsync(string format)
{
return await Task.Run(() =>
{
if (format == DataFormats.Text)
return _text;
if (format == DataFormats.Files && _data is not null)
return _data.GetFiles();
if (format == DataFormats.FileNames && _data is not null)
{
#pragma warning disable CS0618 // Type or member is obsolete
return _data.GetFileNames();
#pragma warning restore CS0618 // Type or member is obsolete
}
else
return (object?)_data;
});
return Task.FromResult(_data?.Get(format));
}
public Task<IDataObject?> TryGetInProcessDataObjectAsync() => Task.FromResult(_data);
/// <inheritdoc />
public Task FlushAsync() =>
Task.CompletedTask;

2
src/Tizen/Avalonia.Tizen/NuiClipboardImpl.cs

@ -73,6 +73,8 @@ internal class NuiClipboardImpl : IClipboard
public Task<string[]> GetFormatsAsync() =>
throw new PlatformNotSupportedException();
public Task<IDataObject?> TryGetInProcessDataObjectAsync() => Task.FromResult<IDataObject?>(null);
/// <inheritdoc />
public Task FlushAsync() =>

26
src/Windows/Avalonia.Win32/ClipboardImpl.cs

@ -15,6 +15,11 @@ namespace Avalonia.Win32
{
private const int OleRetryCount = 10;
private const int OleRetryDelay = 100;
private DataObject? _lastStoredDataObject;
// We can't currently rely on GetNativeIntPtr due to a bug in MicroCom 0.11, so we store the raw CCW reference instead
private IntPtr _lastStoredDataObjectIntPtr;
/// <summary>
/// The amount of time in milliseconds to sleep before flushing the clipboard after a set.
/// </summary>
@ -93,7 +98,18 @@ namespace Avalonia.Win32
var hr = UnmanagedMethods.OleSetClipboard(ptr);
if (hr == 0)
{
_lastStoredDataObject = wrapper;
// TODO: Replace with GetNativeIntPtr in TryGetInProcessDataObjectAsync
// once MicroCom is fixed
_lastStoredDataObjectIntPtr = ptr;
wrapper.OnDestroyed += delegate
{
if (_lastStoredDataObjectIntPtr == ptr)
_lastStoredDataObjectIntPtr = IntPtr.Zero;
};
break;
}
if (--i == 0)
Marshal.ThrowExceptionForHR(hr);
@ -150,6 +166,16 @@ namespace Avalonia.Win32
}
}
public Task<IDataObject?> TryGetInProcessDataObjectAsync()
{
if (_lastStoredDataObject?.IsDisposed != false
|| _lastStoredDataObjectIntPtr == IntPtr.Zero
|| UnmanagedMethods.OleIsCurrentClipboard(_lastStoredDataObjectIntPtr) != 0)
return Task.FromResult<IDataObject?>(null);
return Task.FromResult<IDataObject?>(_lastStoredDataObject.Wrapped);
}
/// <summary>
/// Permanently renders the contents of the last IDataObject that was set onto the clipboard.
/// </summary>

3
src/Windows/Avalonia.Win32/DataObject.cs

@ -104,6 +104,8 @@ namespace Avalonia.Win32
private IDataObject _wrapped;
public IDataObject Wrapped => _wrapped;
public bool IsDisposed => _wrapped == null!;
public event Action? OnDestroyed;
public DataObject(IDataObject wrapped)
{
@ -375,6 +377,7 @@ namespace Avalonia.Win32
protected override void Destroyed()
{
OnDestroyed?.Invoke();
ReleaseWrapped();
}

3
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -1578,6 +1578,9 @@ namespace Avalonia.Win32.Interop
[DllImport("ole32.dll", PreserveSig = true)]
public static extern int OleSetClipboard(IntPtr dataObject);
[DllImport("ole32.dll", PreserveSig = true)]
public static extern int OleIsCurrentClipboard(IntPtr dataObject);
[DllImport("kernel32.dll", ExactSpelling = true)]
public static extern IntPtr GlobalLock(IntPtr handle);

2
src/iOS/Avalonia.iOS/ClipboardImpl.cs

@ -57,6 +57,8 @@ namespace Avalonia.iOS
return Task.FromResult<object?>(null);
}
public Task<IDataObject?> TryGetInProcessDataObjectAsync() => Task.FromResult<IDataObject?>(null);
/// <inheritdoc />
public Task FlushAsync() =>

30
tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs

@ -1002,34 +1002,6 @@ namespace Avalonia.Controls.UnitTests
}
}
internal class ClipboardStub : IClipboard // in order to get tests working that use the clipboard
{
private string _text;
public Task<string> GetTextAsync() => Task.FromResult(_text);
public Task SetTextAsync(string text)
{
_text = text;
return Task.CompletedTask;
}
public Task ClearAsync()
{
_text = null;
return Task.CompletedTask;
}
public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask;
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
{
private readonly ILayoutManager _layoutManager;
@ -1048,7 +1020,7 @@ namespace Avalonia.Controls.UnitTests
var clipboard = new Mock<ITopLevelImpl>();
clipboard.Setup(x => x.Compositor).Returns(RendererMocks.CreateDummyCompositor());
clipboard.Setup(r => r.TryGetFeature(typeof(IClipboard)))
.Returns(new ClipboardStub());
.Returns(new HeadlessClipboardStub());
clipboard.SetupGet(x => x.RenderScaling).Returns(1);
return clipboard;
}

30
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@ -2147,34 +2147,6 @@ namespace Avalonia.Controls.UnitTests
}
}
private class ClipboardStub : IClipboard // in order to get tests working that use the clipboard
{
private string? _text;
public Task<string?> GetTextAsync() => Task.FromResult(_text);
public Task SetTextAsync(string? text)
{
_text = text;
return Task.CompletedTask;
}
public Task ClearAsync()
{
_text = null;
return Task.CompletedTask;
}
public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask;
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
{
private readonly ILayoutManager _layoutManager;
@ -2193,7 +2165,7 @@ namespace Avalonia.Controls.UnitTests
var clipboard = new Mock<ITopLevelImpl>();
clipboard.Setup(x => x.Compositor).Returns(RendererMocks.CreateDummyCompositor());
clipboard.Setup(r => r.TryGetFeature(typeof(IClipboard)))
.Returns(new ClipboardStub());
.Returns(new HeadlessClipboardStub());
clipboard.SetupGet(x => x.RenderScaling).Returns(1);
return clipboard;
}

Loading…
Cancel
Save