diff --git a/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs index 6559e013d2..2699488eda 100644 --- a/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs +++ b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs @@ -39,6 +39,7 @@ namespace Avalonia.Android.Platform return; var mimeTypes = dataTransfer.Formats + .Where(f => f.Kind != DataFormatKind.InProcess) .Select(AndroidDataFormatHelper.DataFormatToMimeType) .ToArray(); @@ -80,6 +81,9 @@ namespace Avalonia.Android.Platform // Create the item from the first format returning a supported value. foreach (var dataFormat in item.Formats) { + if (dataFormat.Kind == DataFormatKind.InProcess) + continue; + hasFormats = true; if (DataFormat.Text.Equals(dataFormat)) diff --git a/src/Avalonia.Base/Input/DataFormat.cs b/src/Avalonia.Base/Input/DataFormat.cs index 14d1d4a30b..3531567724 100644 --- a/src/Avalonia.Base/Input/DataFormat.cs +++ b/src/Avalonia.Base/Input/DataFormat.cs @@ -62,7 +62,7 @@ public abstract class DataFormat : IEquatable { DataFormatKind.Application => applicationPrefix + Identifier, DataFormatKind.Platform => Identifier, - _ => throw new InvalidOperationException($"Cannot get system name for universal format {Identifier}") + _ => throw new InvalidOperationException($"Cannot get system name for {Kind} format {Identifier}") }; } @@ -137,6 +137,23 @@ public abstract class DataFormat : IEquatable public static DataFormat CreateStringApplicationFormat(string identifier) => CreateApplicationFormat(identifier); + /// + /// Creates a new format that stays within the current process and is never serialized to a platform clipboard or drag-and-drop operation. + /// + /// The data type. Can be any reference type. + /// + /// The format identifier. This value is only used for equality comparisons within the process + /// and is never passed to the underlying platform. + /// + /// A new . + public static DataFormat CreateInProcessFormat(string identifier) + where T : class + { + ThrowHelper.ThrowIfNullOrEmpty(identifier); + + return new(DataFormatKind.InProcess, identifier); + } + private static DataFormat CreateApplicationFormat(string identifier) where T : class { diff --git a/src/Avalonia.Base/Input/DataFormatKind.cs b/src/Avalonia.Base/Input/DataFormatKind.cs index 17f778e189..af7b1465ea 100644 --- a/src/Avalonia.Base/Input/DataFormatKind.cs +++ b/src/Avalonia.Base/Input/DataFormatKind.cs @@ -42,5 +42,17 @@ public enum DataFormatKind /// It is not possible to create such a format directly. /// /// - Universal + Universal, + + /// + /// + /// The data format is only usable within the current process. + /// It never crosses process or serialization boundaries. + /// + /// + /// Such a format is created using . + /// + /// + /// + InProcess } diff --git a/src/Avalonia.Base/Input/DataFormatOfT.cs b/src/Avalonia.Base/Input/DataFormatOfT.cs index 2036f49fb0..06a0980c4f 100644 --- a/src/Avalonia.Base/Input/DataFormatOfT.cs +++ b/src/Avalonia.Base/Input/DataFormatOfT.cs @@ -10,8 +10,9 @@ namespace Avalonia.Input; /// This class cannot be instantiated directly. /// Use universal formats such as and , /// or create custom formats using , -/// , -/// or . +/// , , +/// , +/// or . /// [SuppressMessage("ReSharper", "UnusedTypeParameter", Justification = "Used to resolve typed overloads.")] public sealed class DataFormat : DataFormat diff --git a/src/Avalonia.Native/DataTransferItemToAvnClipboardDataItemWrapper.cs b/src/Avalonia.Native/DataTransferItemToAvnClipboardDataItemWrapper.cs index e9be474598..4b659a3cc4 100644 --- a/src/Avalonia.Native/DataTransferItemToAvnClipboardDataItemWrapper.cs +++ b/src/Avalonia.Native/DataTransferItemToAvnClipboardDataItemWrapper.cs @@ -18,7 +18,7 @@ internal sealed class DataTransferItemToAvnClipboardDataItemWrapper(IDataTransfe private readonly IDataTransferItem _item = item; IAvnStringArray IAvnClipboardDataItem.ProvideFormats() - => new AvnStringArray(_item.Formats.Select(ClipboardDataFormatHelper.ToNativeFormat)); + => new AvnStringArray(_item.Formats.Where(f => f.Kind != DataFormatKind.InProcess).Select(ClipboardDataFormatHelper.ToNativeFormat)); IAvnClipboardDataValue? IAvnClipboardDataItem.GetValue(string format) { @@ -63,6 +63,8 @@ internal sealed class DataTransferItemToAvnClipboardDataItemWrapper(IDataTransfe for (var i = 0; i < count; i++) { var format = formats[i]; + if (format.Kind == DataFormatKind.InProcess) + continue; if (ClipboardDataFormatHelper.ToNativeFormat(format) == nativeFormat) return format; } diff --git a/src/Avalonia.X11/Clipboard/X11Clipboard.cs b/src/Avalonia.X11/Clipboard/X11Clipboard.cs index 35abbf11c1..48d50d076a 100644 --- a/src/Avalonia.X11/Clipboard/X11Clipboard.cs +++ b/src/Avalonia.X11/Clipboard/X11Clipboard.cs @@ -253,6 +253,9 @@ namespace Avalonia.X11.Clipboard { foreach (var format in dataTransfer.Formats) { + if (format.Kind == DataFormatKind.InProcess) + continue; + foreach (var atom in ClipboardDataFormatHelper.ToAtoms(format, _textAtoms, _x11.Atoms)) atoms.Add(atom); } diff --git a/src/Browser/Avalonia.Browser/ClipboardImpl.cs b/src/Browser/Avalonia.Browser/ClipboardImpl.cs index 75f50a5e93..3a4d44c278 100644 --- a/src/Browser/Avalonia.Browser/ClipboardImpl.cs +++ b/src/Browser/Avalonia.Browser/ClipboardImpl.cs @@ -40,6 +40,9 @@ internal sealed class ClipboardImpl : IClipboardImpl { foreach (var format in dataTransferItem.Formats) { + if (format.Kind == DataFormatKind.InProcess) + continue; + var formatString = ToBrowserFormat(format); if (!IsClipboardFormatSupported(formatString)) continue; diff --git a/src/Windows/Avalonia.Win32/DataTransferToOleDataObjectWrapper.cs b/src/Windows/Avalonia.Win32/DataTransferToOleDataObjectWrapper.cs index f8f1784180..b5c7f193eb 100644 --- a/src/Windows/Avalonia.Win32/DataTransferToOleDataObjectWrapper.cs +++ b/src/Windows/Avalonia.Win32/DataTransferToOleDataObjectWrapper.cs @@ -193,6 +193,9 @@ internal class DataTransferToOleDataObjectWrapper(IDataTransfer dataTransfer) foreach (var dataFormat in DataTransfer.Formats) { + if (dataFormat.Kind == DataFormatKind.InProcess) + continue; + if (DataFormat.Bitmap.Equals(dataFormat)) { // We add extra formats for bitmaps diff --git a/src/iOS/Avalonia.iOS/Clipboard/ClipboardImpl.cs b/src/iOS/Avalonia.iOS/Clipboard/ClipboardImpl.cs index 20722e6314..57ff3c9043 100644 --- a/src/iOS/Avalonia.iOS/Clipboard/ClipboardImpl.cs +++ b/src/iOS/Avalonia.iOS/Clipboard/ClipboardImpl.cs @@ -65,6 +65,9 @@ internal sealed class ClipboardImpl(UIPasteboard pasteboard) foreach (var dataFormat in dataTransferItem.Formats) { + if (dataFormat.Kind == DataFormatKind.InProcess) + continue; + var data = await TryGetFoundationDataAsync(dataTransferItem, dataFormat); if (data is null) continue; diff --git a/tests/Avalonia.Base.UnitTests/Input/DataFormatTests.cs b/tests/Avalonia.Base.UnitTests/Input/DataFormatTests.cs new file mode 100644 index 0000000000..e4eb52ecfc --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Input/DataFormatTests.cs @@ -0,0 +1,110 @@ +using System; +using Avalonia.Input; +using Xunit; + +namespace Avalonia.Base.UnitTests.Input; + +public sealed class DataFormatTests +{ + [Fact] + public void CreateInProcessFormat_Returns_Format_With_InProcess_Kind() + { + var format = DataFormat.CreateInProcessFormat("my-format"); + + Assert.Equal(DataFormatKind.InProcess, format.Kind); + } + + [Fact] + public void CreateInProcessFormat_Returns_Format_With_Correct_Identifier() + { + var format = DataFormat.CreateInProcessFormat("my-format"); + + Assert.Equal("my-format", format.Identifier); + } + + [Fact] + public void CreateInProcessFormat_Throws_On_Null_Identifier() + { + Assert.Throws(() => DataFormat.CreateInProcessFormat(null!)); + } + + [Fact] + public void CreateInProcessFormat_Throws_On_Empty_Identifier() + { + Assert.Throws(() => DataFormat.CreateInProcessFormat(string.Empty)); + } + + [Fact] + public void CreateInProcessFormat_Allows_Non_ASCII_Identifiers() + { + var format = DataFormat.CreateInProcessFormat("日本語フォーマット"); + + Assert.Equal("日本語フォーマット", format.Identifier); + } + + [Fact] + public void ToSystemName_Throws_For_InProcess() + { + var format = DataFormat.CreateInProcessFormat("test"); + + Assert.Throws(() => format.ToSystemName("prefix.")); + } + + [Fact] + public void InProcess_Format_Equality_Same_Identifier() + { + var format1 = DataFormat.CreateInProcessFormat("my-format"); + var format2 = DataFormat.CreateInProcessFormat("my-format"); + + Assert.Equal(format1, format2); + Assert.True(format1 == format2); + } + + [Fact] + public void InProcess_Format_Inequality_Different_Identifier() + { + var format1 = DataFormat.CreateInProcessFormat("format-a"); + var format2 = DataFormat.CreateInProcessFormat("format-b"); + + Assert.NotEqual(format1, format2); + Assert.True(format1 != format2); + } + + [Fact] + public void InProcess_Format_Inequality_Different_Kind_Same_Identifier() + { + var inProcess = DataFormat.CreateInProcessFormat("test-format"); + var application = DataFormat.CreateStringApplicationFormat("test-format"); + + Assert.NotEqual(inProcess, application); + } + + [Fact] + public void InProcess_Format_Works_With_DataTransferItem_Set_And_Get() + { + var format = DataFormat.CreateInProcessFormat("my-inprocess"); + var item = new DataTransferItem(); + item.Set(format, "hello"); + + var value = item.TryGetValue(format); + + Assert.Equal("hello", value); + } + + [Fact] + public void InProcess_Format_Coexists_With_Other_Formats_In_DataTransfer() + { + var inProcessFormat = DataFormat.CreateInProcessFormat("my-inprocess"); + var item = new DataTransferItem(); + item.SetText("plain text"); + item.Set(inProcessFormat, "in-process data"); + + var dataTransfer = new DataTransfer(); + dataTransfer.Add(item); + + Assert.Contains(DataFormat.Text, dataTransfer.Formats); + Assert.Contains(inProcessFormat, (System.Collections.Generic.IEnumerable)dataTransfer.Formats); + Assert.Equal("plain text", item.TryGetValue(DataFormat.Text)); + Assert.Equal("in-process data", item.TryGetValue(inProcessFormat)); + } +}