瀏覽代碼

Added IClipboard:TryGetInProcessDataObjectAsyns (#18340)

* Implemented IClipboard.TryGetInProcessDataObjectAsync for X11

* [Win32] Implemented TryGetInProcessDataObjectAsync

* Rest of the platforms

* Run `nuke ValidateApiDiff`

---------

Co-authored-by: Max Katz <[email protected]>
Nikita Tsukanov 7 月之前
父節點
當前提交
c3b4d61b8a

+ 9 - 3
api/Avalonia.nupkg.xml

@@ -51,19 +51,25 @@
   </Suppression>
   </Suppression>
   <Suppression>
   <Suppression>
     <DiagnosticId>CP0006</DiagnosticId>
     <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>
     <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
     <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
     <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
   </Suppression>
   </Suppression>
   <Suppression>
   <Suppression>
     <DiagnosticId>CP0006</DiagnosticId>
     <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>
     <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
     <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
     <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
   </Suppression>
   </Suppression>
   <Suppression>
   <Suppression>
     <DiagnosticId>CP0006</DiagnosticId>
     <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>
     <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
     <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
     <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
   </Suppression>
   </Suppression>

+ 16 - 2
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;
         START_COM_CALL;
         
         
         @autoreleasepool
         @autoreleasepool
         {
         {
             if(_item != nil)
             if(_item != nil)
+            {
                 _item = [NSPasteboardItem new];
                 _item = [NSPasteboardItem new];
+                return 0;
+            }
             else
             else
             {
             {
-                [_pb clearContents];
+                *rv = [_pb clearContents];
                 [_pb setString:@"" forType:NSPasteboardTypeString];
                 [_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
     virtual HRESULT ObtainFormats(IAvnStringArray** ppv) override
     {
     {
         START_COM_CALL;
         START_COM_CALL;

+ 3 - 0
samples/ControlCatalog/Pages/ClipboardPage.xaml

@@ -15,6 +15,9 @@
     <Button Click="GetFormats" Content="Get clipboard formats" />
     <Button Click="GetFormats" Content="Get clipboard formats" />
     <Button Click="Clear" Content="Clear clipboard" />
     <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"
     <TextBox x:Name="ClipboardContent"
              MinHeight="100"
              MinHeight="100"
              AcceptsReturn="True"
              AcceptsReturn="True"

+ 44 - 2
samples/ControlCatalog/Pages/ClipboardPage.xaml.cs

@@ -3,13 +3,17 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using Avalonia;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls;
+using Avalonia.Controls.Documents;
 using Avalonia.Controls.Notifications;
 using Avalonia.Controls.Notifications;
 using Avalonia.Input;
 using Avalonia.Input;
+using Avalonia.Input.Platform;
 using Avalonia.Interactivity;
 using Avalonia.Interactivity;
 using Avalonia.Markup.Xaml;
 using Avalonia.Markup.Xaml;
+using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Platform;
 using Avalonia.Platform.Storage;
 using Avalonia.Platform.Storage;
 using Avalonia.Platform.Storage.FileIO;
 using Avalonia.Platform.Storage.FileIO;
+using Avalonia.Threading;
 
 
 namespace ControlCatalog.Pages
 namespace ControlCatalog.Pages
 {
 {
@@ -18,8 +22,13 @@ namespace ControlCatalog.Pages
         private INotificationManager? _notificationManager;
         private INotificationManager? _notificationManager;
         private INotificationManager NotificationManager => _notificationManager
         private INotificationManager NotificationManager => _notificationManager
             ??= new WindowNotificationManager(TopLevel.GetTopLevel(this)!);
             ??= new WindowNotificationManager(TopLevel.GetTopLevel(this)!);
+
+        private readonly DispatcherTimer _clipboardLastDataObjectChecker;
+        private DataObject? _storedDataObject;
         public ClipboardPage()
         public ClipboardPage()
         {
         {
+            _clipboardLastDataObjectChecker =
+                new DispatcherTimer(TimeSpan.FromSeconds(0.5), default, CheckLastDataObject);
             InitializeComponent();
             InitializeComponent();
         }
         }
 
 
@@ -48,7 +57,7 @@ namespace ControlCatalog.Pages
         {
         {
             if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
             if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
             {
             {
-                var dataObject = new DataObject();
+                var dataObject =  _storedDataObject = new DataObject();
                 dataObject.Set(DataFormats.Text, ClipboardContent.Text ?? string.Empty);
                 dataObject.Set(DataFormats.Text, ClipboardContent.Text ?? string.Empty);
                 await clipboard.SetDataObjectAsync(dataObject);
                 await clipboard.SetDataObjectAsync(dataObject);
             }
             }
@@ -96,7 +105,7 @@ namespace ControlCatalog.Pages
 
 
                 if (files.Count > 0)
                 if (files.Count > 0)
                 {
                 {
-                    var dataObject = new DataObject();
+                    var dataObject = _storedDataObject = new DataObject();
                     dataObject.Set(DataFormats.Files, files);
                     dataObject.Set(DataFormats.Files, files);
                     await clipboard.SetDataObjectAsync(dataObject);
                     await clipboard.SetDataObjectAsync(dataObject);
                     NotificationManager.Show(new Notification("Success", "Copy completated.", NotificationType.Success));
                     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 - 0
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<object?> GetDataAsync(string format) => throw new PlatformNotSupportedException();
 
 
+        public Task<IDataObject?> TryGetInProcessDataObjectAsync() => Task.FromResult<IDataObject?>(null);
+
         /// <inheritdoc />
         /// <inheritdoc />
         public Task FlushAsync() =>
         public Task FlushAsync() =>
             Task.CompletedTask;
             Task.CompletedTask;

+ 9 - 0
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>
         /// <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>
         /// <returns></returns>
         Task<object?> GetDataAsync(string format);
         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 - 0
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<object> GetDataAsync(string format) => Task.FromResult((object)null);
 
 
+        public Task<IDataObject> TryGetInProcessDataObjectAsync() => Task.FromResult<IDataObject>(null);
+
         public Task FlushAsync() =>
         public Task FlushAsync() =>
             Task.CompletedTask;
             Task.CompletedTask;
     }
     }

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

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

+ 3 - 1
src/Avalonia.Native/avn.idl

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

+ 7 - 0
src/Avalonia.X11/X11Clipboard.cs

@@ -314,6 +314,13 @@ namespace Avalonia.X11
             return StoreAtomsInClipboardManager(data);
             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()
         public async Task<string[]> GetFormatsAsync()
         {
         {
             if (!HasOwner)
             if (!HasOwner)

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

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

+ 16 - 35
src/Headless/Avalonia.Headless/HeadlessPlatformStubs.cs

@@ -17,66 +17,47 @@ namespace Avalonia.Headless
 {
 {
     internal class HeadlessClipboardStub : IClipboard
     internal class HeadlessClipboardStub : IClipboard
     {
     {
-        private string? _text;
         private IDataObject? _data;
         private IDataObject? _data;
 
 
         public Task<string?> GetTextAsync()
         public Task<string?> GetTextAsync()
         {
         {
-            return Task.Run(() => _text);
+            return Task.FromResult(_data?.GetText());
         }
         }
 
 
         public Task SetTextAsync(string? text)
         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()
         public Task ClearAsync()
         {
         {
-            return Task.Run(() => _text = null);
+            _data = null;
+            return Task.CompletedTask;
         }
         }
 
 
         public Task SetDataObjectAsync(IDataObject data)
         public Task SetDataObjectAsync(IDataObject data)
         {
         {
-            return Task.Run(() => _data = data);
+            _data = data;
+            return Task.CompletedTask;
         }
         }
 
 
         public Task<string[]> GetFormatsAsync()
         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 />
         /// <inheritdoc />
         public Task FlushAsync() =>
         public Task FlushAsync() =>
             Task.CompletedTask;
             Task.CompletedTask;

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

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

+ 26 - 0
src/Windows/Avalonia.Win32/ClipboardImpl.cs

@@ -15,6 +15,11 @@ namespace Avalonia.Win32
     {
     {
         private const int OleRetryCount = 10;
         private const int OleRetryCount = 10;
         private const int OleRetryDelay = 100;
         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>
         /// <summary>
         /// The amount of time in milliseconds to sleep before flushing the clipboard after a set.
         /// The amount of time in milliseconds to sleep before flushing the clipboard after a set.
         /// </summary>
         /// </summary>
@@ -93,7 +98,18 @@ namespace Avalonia.Win32
                 var hr = UnmanagedMethods.OleSetClipboard(ptr);
                 var hr = UnmanagedMethods.OleSetClipboard(ptr);
 
 
                 if (hr == 0)
                 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;
                     break;
+                }
 
 
                 if (--i == 0)
                 if (--i == 0)
                     Marshal.ThrowExceptionForHR(hr);
                     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>
         /// <summary>
         /// Permanently renders the contents of the last IDataObject that was set onto the clipboard.
         /// Permanently renders the contents of the last IDataObject that was set onto the clipboard.
         /// </summary>
         /// </summary>

+ 3 - 0
src/Windows/Avalonia.Win32/DataObject.cs

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

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

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

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

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

+ 1 - 29
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 class TestTopLevel : TopLevel
         {
         {
             private readonly ILayoutManager _layoutManager;
             private readonly ILayoutManager _layoutManager;
@@ -1048,7 +1020,7 @@ namespace Avalonia.Controls.UnitTests
             var clipboard = new Mock<ITopLevelImpl>();
             var clipboard = new Mock<ITopLevelImpl>();
             clipboard.Setup(x => x.Compositor).Returns(RendererMocks.CreateDummyCompositor());
             clipboard.Setup(x => x.Compositor).Returns(RendererMocks.CreateDummyCompositor());
             clipboard.Setup(r => r.TryGetFeature(typeof(IClipboard)))
             clipboard.Setup(r => r.TryGetFeature(typeof(IClipboard)))
-                .Returns(new ClipboardStub());
+                .Returns(new HeadlessClipboardStub());
             clipboard.SetupGet(x => x.RenderScaling).Returns(1);
             clipboard.SetupGet(x => x.RenderScaling).Returns(1);
             return clipboard;
             return clipboard;
         }
         }

+ 1 - 29
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 class TestTopLevel : TopLevel
         {
         {
             private readonly ILayoutManager _layoutManager;
             private readonly ILayoutManager _layoutManager;
@@ -2193,7 +2165,7 @@ namespace Avalonia.Controls.UnitTests
             var clipboard = new Mock<ITopLevelImpl>();
             var clipboard = new Mock<ITopLevelImpl>();
             clipboard.Setup(x => x.Compositor).Returns(RendererMocks.CreateDummyCompositor());
             clipboard.Setup(x => x.Compositor).Returns(RendererMocks.CreateDummyCompositor());
             clipboard.Setup(r => r.TryGetFeature(typeof(IClipboard)))
             clipboard.Setup(r => r.TryGetFeature(typeof(IClipboard)))
-                .Returns(new ClipboardStub());
+                .Returns(new HeadlessClipboardStub());
             clipboard.SetupGet(x => x.RenderScaling).Returns(1);
             clipboard.SetupGet(x => x.RenderScaling).Returns(1);
             return clipboard;
             return clipboard;
         }
         }