Clipboard rework (#19347)

* New clipboard with IDataTransfer

* Move legacy format handling to DataObject wrappers

* macOS clipboard rework

* Browser clipboard rework

* Android clipboard rework

* iOS new clipboard

* X11 clipboard rework

* Simplify IDataTransfer API

* Simplify IClipboardImpl API

* Add DataFormat documentation

* Make DataFormat.SystemName platform specific

* Fix clipboard/DnD samples

* Fix native clipboard UTI conversion

* Adjust IDataTransfer namespaces

* Add Obsolete attributes to IDataObject related methods

* Better API for DataTransferItem

* Tizen clipboard rework

* Add missing clipboard extension methods

* Split IDataTransferItem into IAsyncDataTransferItem and ISyncDataTransferItem

* Rename back ISyncDataTransfer to IDataTransfer

* Added IClipboard API suppressions

* Make IPlatformDragSource NotClientImplementable

* Rename DataFormatKinds

* Added DataTransferItem.CreateText/File

* Implemented typed DataFormat<T>

* Fix X11 text/uri-list encoding

* Add API suppressions

* Adjust ClipboardUriListHelper stream ownership

* Fix legacy clipboard BinaryFormatter deserialization

* Fix macOS build
This commit is contained in:
Julien Lebosquain
2025-10-01 13:47:09 +02:00
committed by GitHub
parent d060f96d52
commit 1f404646fa
137 changed files with 5961 additions and 1922 deletions
@@ -6,12 +6,10 @@
<Button Click="CopyText" Content="Copy text to clipboard" />
<Button Click="PasteText" Content="Paste text from clipboard" />
<Button Click="CopyTextDataObject" Content="Copy text to clipboard (data object)" />
<Button Click="PasteTextDataObject" Content="Paste text from clipboard (data object)" />
<Button Click="CopyFilesDataObject" Content="Copy files to clipboard (data object)" />
<Button Click="PasteFilesDataObject" Content="Paste files from clipboard (data object)" />
<Button Click="CopyFiles" Content="Copy files to clipboard" />
<Button Click="PasteFiles" Content="Paste files from clipboard" />
<Button Click="CopyBinaryData" Content="Copy bytes to clipboard" />
<Button Click="PasteBinaryData" Content="Paste bytes from clipboard" />
<Button Click="GetFormats" Content="Get clipboard formats" />
<Button Click="Clear" Content="Clear clipboard" />
@@ -10,21 +10,22 @@ 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
{
public partial class ClipboardPage : UserControl
{
private readonly DataFormat<byte[]> _customBinaryDataFormat =
DataFormat.CreateBytesApplicationFormat("controlcatalog-binary-data");
private INotificationManager? _notificationManager;
private INotificationManager NotificationManager => _notificationManager
??= new WindowNotificationManager(TopLevel.GetTopLevel(this)!);
private readonly DispatcherTimer _clipboardLastDataObjectChecker;
private DataObject? _storedDataObject;
private DataTransfer? _storedDataTransfer;
public ClipboardPage()
{
_clipboardLastDataObjectChecker =
@@ -41,37 +42,19 @@ namespace ControlCatalog.Pages
private async void CopyText(object? sender, RoutedEventArgs args)
{
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard && ClipboardContent is { } clipboardContent)
await clipboard.SetTextAsync(clipboardContent.Text ?? String.Empty);
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
await clipboard.SetTextAsync(ClipboardContent.Text ?? string.Empty);
}
private async void PasteText(object? sender, RoutedEventArgs args)
{
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
ClipboardContent.Text = await clipboard.GetTextAsync();
ClipboardContent.Text = await clipboard.TryGetTextAsync();
}
}
private async void CopyTextDataObject(object? sender, RoutedEventArgs args)
{
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
var dataObject = _storedDataObject = new DataObject();
dataObject.Set(DataFormats.Text, ClipboardContent.Text ?? string.Empty);
await clipboard.SetDataObjectAsync(dataObject);
}
}
private async void PasteTextDataObject(object? sender, RoutedEventArgs args)
{
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
ClipboardContent.Text = await clipboard.GetDataAsync(DataFormats.Text) as string ?? string.Empty;
}
}
private async void CopyFilesDataObject(object? sender, RoutedEventArgs args)
private async void CopyFiles(object? sender, RoutedEventArgs args)
{
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
@@ -105,10 +88,11 @@ namespace ControlCatalog.Pages
if (files.Count > 0)
{
var dataObject = _storedDataObject = new DataObject();
dataObject.Set(DataFormats.Files, files);
await clipboard.SetDataObjectAsync(dataObject);
NotificationManager.Show(new Notification("Success", "Copy completated.", NotificationType.Success));
var dataTransfer = _storedDataTransfer = new DataTransfer();
foreach (var file in files)
dataTransfer.Add(DataTransferItem.Create(DataFormat.File, file));
await clipboard.SetDataAsync(dataTransfer);
NotificationManager.Show(new Notification("Success", "Copy completed.", NotificationType.Success));
}
else
{
@@ -117,11 +101,11 @@ namespace ControlCatalog.Pages
}
}
private async void PasteFilesDataObject(object? sender, RoutedEventArgs args)
private async void PasteFiles(object? sender, RoutedEventArgs args)
{
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
var files = await clipboard.GetDataAsync(DataFormats.Files) as IEnumerable<Avalonia.Platform.Storage.IStorageItem>;
var files = await clipboard.TryGetFilesAsync();
ClipboardContent.Text = files != null ? string.Join(Environment.NewLine, files.Select(f => f.TryGetLocalPath() ?? f.Name)) : string.Empty;
}
@@ -131,11 +115,32 @@ namespace ControlCatalog.Pages
{
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
var formats = await clipboard.GetFormatsAsync();
var formats = await clipboard.GetDataFormatsAsync();
ClipboardContent.Text = string.Join(Environment.NewLine, formats);
}
}
private async void CopyBinaryData(object? sender, RoutedEventArgs args)
{
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
var dataTransfer = _storedDataTransfer = new DataTransfer();
var bytes = new byte[10 * 1024 * 1024];
new Random().NextBytes(bytes);
dataTransfer.Add(DataTransferItem.Create(_customBinaryDataFormat, bytes));
await clipboard.SetDataAsync(dataTransfer);
}
}
private async void PasteBinaryData(object? sender, RoutedEventArgs args)
{
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
var bytes = await clipboard.TryGetValueAsync(_customBinaryDataFormat);
ClipboardContent.Text = bytes is null ? "<null>" : $"{bytes.Length} bytes";
}
}
private async void Clear(object sender, RoutedEventArgs args)
{
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
@@ -159,22 +164,28 @@ namespace ControlCatalog.Pages
}
private Run OwnsClipboardDataObject => this.Get<Run>("OwnsClipboardDataObject");
private bool _checkingClipboardDataObject;
private bool _checkingClipboardDataTransfer;
private async void CheckLastDataObject(object? sender, EventArgs e)
{
if(_checkingClipboardDataObject)
if(_checkingClipboardDataTransfer)
return;
try
{
_checkingClipboardDataObject = true;
var task = TopLevel.GetTopLevel(this)?.Clipboard?.TryGetInProcessDataObjectAsync();
var owns = task != null && (await task) == _storedDataObject && _storedDataObject != null;
_checkingClipboardDataTransfer = true;
var owns = false;
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
var dataTransfer = await clipboard.TryGetInProcessDataAsync();
owns = dataTransfer == _storedDataTransfer && dataTransfer is not null;
}
OwnsClipboardDataObject.Text = owns ? "Yes" : "No";
OwnsClipboardDataObject.Foreground = owns ? Brushes.Green : Brushes.Red;
}
finally
{
_checkingClipboardDataObject = false;
_checkingClipboardDataTransfer = false;
}
}
}
@@ -1,9 +1,11 @@
using System;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Markup.Xaml;
using Avalonia.Platform.Storage;
@@ -12,7 +14,10 @@ namespace ControlCatalog.Pages
public class DragAndDropPage : UserControl
{
private readonly TextBlock _dropState;
private const string CustomFormat = "application/xxx-avalonia-controlcatalog-custom";
private readonly DataFormat<string> _customFormat =
DataFormat.CreateStringApplicationFormat("xxx-avalonia-controlcatalog-custom");
public DragAndDropPage()
{
InitializeComponent();
@@ -22,13 +27,13 @@ namespace ControlCatalog.Pages
SetupDnd(
"Text",
d => d.Set(DataFormats.Text, $"Text was dragged {++textCount} times"),
d => d.Add(DataTransferItem.Create(DataFormat.Text, $"Text was dragged {++textCount} times")),
DragDropEffects.Copy | DragDropEffects.Move | DragDropEffects.Link);
SetupDnd(
"Custom",
d => d.Set(CustomFormat, "Test123"),
DragDropEffects.Move);
d => d.Add(DataTransferItem.Create(_customFormat, "Test123")),
DragDropEffects.Copy | DragDropEffects.Move);
SetupDnd(
"Files",
@@ -38,13 +43,13 @@ namespace ControlCatalog.Pages
TopLevel.GetTopLevel(this) is { } topLevel &&
await topLevel.StorageProvider.TryGetFileFromPathAsync(name) is { } storageFile)
{
d.Set(DataFormats.Files, new[] { storageFile });
d.Add(DataTransferItem.Create(DataFormat.File, storageFile));
}
},
DragDropEffects.Copy);
}
private void SetupDnd(string suffix, Action<DataObject> factory, DragDropEffects effects) =>
private void SetupDnd(string suffix, Action<DataTransfer> factory, DragDropEffects effects) =>
SetupDnd(
suffix,
o =>
@@ -54,17 +59,17 @@ namespace ControlCatalog.Pages
},
effects);
private void SetupDnd(string suffix, Func<DataObject, Task> factory, DragDropEffects effects)
private void SetupDnd(string suffix, Func<DataTransfer, Task> factory, DragDropEffects effects)
{
var dragMe = this.Get<Border>("DragMe" + suffix);
var dragState = this.Get<TextBlock>("DragState" + suffix);
async void DoDrag(object? sender, PointerPressedEventArgs e)
{
var dragData = new DataObject();
var dragData = new DataTransfer();
await factory(dragData);
var result = await DragDrop.DoDragDrop(e, dragData, effects);
var result = await DragDrop.DoDragDropAsync(e, dragData, effects);
switch (result)
{
case DragDropEffects.Move:
@@ -97,9 +102,9 @@ namespace ControlCatalog.Pages
}
// Only allow if the dragged data contains text or filenames.
if (!e.Data.Contains(DataFormats.Text)
&& !e.Data.Contains(DataFormats.Files)
&& !e.Data.Contains(CustomFormat))
if (!e.DataTransfer.Contains(DataFormat.Text)
&& !e.DataTransfer.Contains(DataFormat.File)
&& !e.DataTransfer.Contains(_customFormat))
e.DragEffects = DragDropEffects.None;
}
@@ -114,13 +119,13 @@ namespace ControlCatalog.Pages
e.DragEffects = e.DragEffects & (DragDropEffects.Copy);
}
if (e.Data.Contains(DataFormats.Text))
if (e.DataTransfer.Contains(DataFormat.Text))
{
_dropState.Text = e.Data.GetText();
_dropState.Text = e.DataTransfer.TryGetText();
}
else if (e.Data.Contains(DataFormats.Files))
else if (e.DataTransfer.Contains(DataFormat.File))
{
var files = e.Data.GetFiles() ?? Array.Empty<IStorageItem>();
var files = e.DataTransfer.TryGetFiles() ?? [];
var contentStr = "";
foreach (var item in files)
@@ -143,16 +148,9 @@ namespace ControlCatalog.Pages
_dropState.Text = contentStr;
}
#pragma warning disable CS0618 // Type or member is obsolete
else if (e.Data.Contains(DataFormats.FileNames))
else if (e.DataTransfer.Contains(_customFormat))
{
var files = e.Data.GetFileNames();
_dropState.Text = string.Join(Environment.NewLine, files ?? Array.Empty<string>());
}
#pragma warning restore CS0618 // Type or member is obsolete
else if (e.Data.Contains(CustomFormat))
{
_dropState.Text = "Custom: " + e.Data.Get(CustomFormat);
_dropState.Text = "Custom: " + e.DataTransfer.TryGetValue(_customFormat);
}
}