From bec4bec6e705c26c88d46068c965572a8b6faaf9 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Fri, 13 Mar 2026 19:25:18 +0100 Subject: [PATCH] Xdnd: share common DataReader/ReadSession with clipboard --- .../Clipboard/ClipboardDataReader.cs | 78 ------------- .../DragDrop/DragDropDataTransferItem.cs | 15 --- .../Clipboard/ClipboardDataReader.cs | 39 +++++++ .../Clipboard/ClipboardDataTransfer.cs | 12 +- .../Clipboard/ClipboardDataTransferItem.cs | 9 +- .../Clipboard/ClipboardReadSessionFactory.cs | 2 +- .../Clipboard/EventStreamWindow.cs | 5 +- .../Clipboard/X11Clipboard.cs | 50 ++------ .../DataFormatHelper.cs} | 4 +- .../Selections/DragDrop/DragDropDataReader.cs | 49 ++++++++ .../DragDrop/DragDropDataTransfer.cs | 22 ++-- .../DragDrop/DragDropDataTransferItem.cs | 31 +++++ .../{ => Selections}/DragDrop/IXdndWindow.cs | 2 +- .../DragDrop/SynchronousXEventWaiter.cs} | 10 +- .../{ => Selections}/DragDrop/XdndHandler.cs | 32 +++--- .../IXEventWaiter.cs} | 4 +- .../Selections/SelectionDataReader.cs | 108 ++++++++++++++++++ .../SelectionReadSession.cs | 10 +- .../UriListHelper.cs} | 4 +- src/Avalonia.X11/X11Platform.cs | 4 +- src/Avalonia.X11/X11Window.cs | 16 ++- 21 files changed, 299 insertions(+), 207 deletions(-) delete mode 100644 src/Avalonia.X11/Clipboard/ClipboardDataReader.cs delete mode 100644 src/Avalonia.X11/DragDrop/DragDropDataTransferItem.cs create mode 100644 src/Avalonia.X11/Selections/Clipboard/ClipboardDataReader.cs rename src/Avalonia.X11/{ => Selections}/Clipboard/ClipboardDataTransfer.cs (75%) rename src/Avalonia.X11/{ => Selections}/Clipboard/ClipboardDataTransferItem.cs (73%) rename src/Avalonia.X11/{ => Selections}/Clipboard/ClipboardReadSessionFactory.cs (92%) rename src/Avalonia.X11/{ => Selections}/Clipboard/EventStreamWindow.cs (96%) rename src/Avalonia.X11/{ => Selections}/Clipboard/X11Clipboard.cs (87%) rename src/Avalonia.X11/{Clipboard/ClipboardDataFormatHelper.cs => Selections/DataFormatHelper.cs} (98%) create mode 100644 src/Avalonia.X11/Selections/DragDrop/DragDropDataReader.cs rename src/Avalonia.X11/{ => Selections}/DragDrop/DragDropDataTransfer.cs (79%) create mode 100644 src/Avalonia.X11/Selections/DragDrop/DragDropDataTransferItem.cs rename src/Avalonia.X11/{ => Selections}/DragDrop/IXdndWindow.cs (80%) rename src/Avalonia.X11/{DragDrop/SynchronousEventWaiter.cs => Selections/DragDrop/SynchronousXEventWaiter.cs} (88%) rename src/Avalonia.X11/{ => Selections}/DragDrop/XdndHandler.cs (88%) rename src/Avalonia.X11/{DragDrop/IEventWaiter.cs => Selections/IXEventWaiter.cs} (62%) create mode 100644 src/Avalonia.X11/Selections/SelectionDataReader.cs rename src/Avalonia.X11/{Clipboard => Selections}/SelectionReadSession.cs (95%) rename src/Avalonia.X11/{Clipboard/ClipboardUriListHelper.cs => Selections/UriListHelper.cs} (94%) diff --git a/src/Avalonia.X11/Clipboard/ClipboardDataReader.cs b/src/Avalonia.X11/Clipboard/ClipboardDataReader.cs deleted file mode 100644 index 223d7aa3dd..0000000000 --- a/src/Avalonia.X11/Clipboard/ClipboardDataReader.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Text; -using System.Threading.Tasks; -using Avalonia.Input; -using Avalonia.Media.Imaging; -using static Avalonia.X11.XLib; - -namespace Avalonia.X11.Clipboard; - -/// -/// An object used to read values, converted to the correct format, from the X11 clipboard. -/// -internal sealed class ClipboardDataReader( - X11Info x11, - AvaloniaX11Platform platform, - IntPtr[] textFormatAtoms, - IntPtr owner, - DataFormat[] dataFormats) - : IDisposable -{ - private IntPtr _owner = owner; - - private bool IsOwnerStillValid() - => _owner != IntPtr.Zero && XGetSelectionOwner(x11.Display, x11.Atoms.CLIPBOARD) == _owner; - - public async Task TryGetAsync(DataFormat format) - { - if (!IsOwnerStillValid()) - return null; - - var formatAtom = ClipboardDataFormatHelper.ToAtom(format, textFormatAtoms, x11.Atoms, dataFormats); - if (formatAtom == IntPtr.Zero) - return null; - - using var session = ClipboardReadSessionFactory.CreateSession(platform); - var result = await session.SendDataRequest(formatAtom, 0).ConfigureAwait(false); - return ConvertDataResult(result, format, formatAtom); - } - - private object? ConvertDataResult(SelectionReadSession.GetDataResult? result, DataFormat format, IntPtr formatAtom) - { - if (result is null) - return null; - - if (DataFormat.Text.Equals(format)) - { - return ClipboardDataFormatHelper.TryGetStringEncoding(result.TypeAtom, x11.Atoms) is { } textEncoding ? - textEncoding.GetString(result.AsBytes()) : - null; - } - - if (DataFormat.Bitmap.Equals(format)) - { - using var data = result.AsStream(); - - return new Bitmap(data); - } - - if (DataFormat.File.Equals(format)) - { - // text/uri-list might not be supported - return formatAtom != IntPtr.Zero && result.TypeAtom == formatAtom ? - ClipboardUriListHelper.Utf8BytesToFileUriList(result.AsBytes()) : - null; - } - - if (format is DataFormat) - return Encoding.UTF8.GetString(result.AsBytes()); - - if (format is DataFormat) - return result.AsBytes(); - - return null; - } - - public void Dispose() - => _owner = IntPtr.Zero; -} diff --git a/src/Avalonia.X11/DragDrop/DragDropDataTransferItem.cs b/src/Avalonia.X11/DragDrop/DragDropDataTransferItem.cs deleted file mode 100644 index 683bae10da..0000000000 --- a/src/Avalonia.X11/DragDrop/DragDropDataTransferItem.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Avalonia.Input; -using Avalonia.Input.Platform; - -namespace Avalonia.X11.DragDrop; - -internal sealed class DragDropDataTransferItem(DataFormat[] formats) : PlatformDataTransferItem -{ - protected override DataFormat[] ProvideFormats() - => formats; - - protected override object? TryGetRawCore(DataFormat format) - { - throw new System.NotImplementedException(); - } -} diff --git a/src/Avalonia.X11/Selections/Clipboard/ClipboardDataReader.cs b/src/Avalonia.X11/Selections/Clipboard/ClipboardDataReader.cs new file mode 100644 index 0000000000..d3dafbcc84 --- /dev/null +++ b/src/Avalonia.X11/Selections/Clipboard/ClipboardDataReader.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading.Tasks; +using Avalonia.Input; +using static Avalonia.X11.XLib; + +namespace Avalonia.X11.Selections.Clipboard; + +/// +/// An object used to read values, converted to the correct format, from the X11 clipboard. +/// +internal sealed class ClipboardDataReader( + AvaloniaX11Platform platform, + IntPtr[] textFormatAtoms, + DataFormat[] dataFormats, + IntPtr owner) + : SelectionDataReader(platform.Info.Atoms, textFormatAtoms, dataFormats) +{ + private IntPtr _owner = owner; + + private bool IsOwnerStillValid() + => _owner != IntPtr.Zero && XGetSelectionOwner(platform.Display, platform.Info.Atoms.CLIPBOARD) == _owner; + + public override Task TryGetAsync(DataFormat format) + { + if (!IsOwnerStillValid()) + return Task.FromResult(null); + + return base.TryGetAsync(format); + } + + protected override IAsyncDataTransferItem CreateSingleItem(DataFormat[] nonFileFormats) + => new ClipboardDataTransferItem(this, nonFileFormats); + + protected override SelectionReadSession CreateReadSession() + => ClipboardReadSessionFactory.CreateSession(platform); + + public override void Dispose() + => _owner = IntPtr.Zero; +} diff --git a/src/Avalonia.X11/Clipboard/ClipboardDataTransfer.cs b/src/Avalonia.X11/Selections/Clipboard/ClipboardDataTransfer.cs similarity index 75% rename from src/Avalonia.X11/Clipboard/ClipboardDataTransfer.cs rename to src/Avalonia.X11/Selections/Clipboard/ClipboardDataTransfer.cs index 3c9ef5b821..569a157aa3 100644 --- a/src/Avalonia.X11/Clipboard/ClipboardDataTransfer.cs +++ b/src/Avalonia.X11/Selections/Clipboard/ClipboardDataTransfer.cs @@ -1,7 +1,7 @@ using Avalonia.Input; using Avalonia.Input.Platform; -namespace Avalonia.X11.Clipboard; +namespace Avalonia.X11.Selections.Clipboard; /// /// Implementation of for the X11 clipboard. @@ -19,16 +19,12 @@ internal sealed class ClipboardDataTransfer( IAsyncDataTransferItem[] items) : PlatformAsyncDataTransfer { - private readonly ClipboardDataReader _reader = reader; - private readonly DataFormat[] _formats = formats; - private readonly IAsyncDataTransferItem[] _items = items; - protected override DataFormat[] ProvideFormats() - => _formats; + => formats; protected override IAsyncDataTransferItem[] ProvideItems() - => _items; + => items; public override void Dispose() - => _reader.Dispose(); + => reader.Dispose(); } diff --git a/src/Avalonia.X11/Clipboard/ClipboardDataTransferItem.cs b/src/Avalonia.X11/Selections/Clipboard/ClipboardDataTransferItem.cs similarity index 73% rename from src/Avalonia.X11/Clipboard/ClipboardDataTransferItem.cs rename to src/Avalonia.X11/Selections/Clipboard/ClipboardDataTransferItem.cs index 1b2fcbf5a4..a9c6bb1b39 100644 --- a/src/Avalonia.X11/Clipboard/ClipboardDataTransferItem.cs +++ b/src/Avalonia.X11/Selections/Clipboard/ClipboardDataTransferItem.cs @@ -2,7 +2,7 @@ using System.Threading.Tasks; using Avalonia.Input; using Avalonia.Input.Platform; -namespace Avalonia.X11.Clipboard; +namespace Avalonia.X11.Selections.Clipboard; /// /// Implementation of for the X11 clipboard. @@ -12,12 +12,9 @@ namespace Avalonia.X11.Clipboard; internal sealed class ClipboardDataTransferItem(ClipboardDataReader reader, DataFormat[] formats) : PlatformAsyncDataTransferItem { - private readonly ClipboardDataReader _reader = reader; - private readonly DataFormat[] _formats = formats; - protected override DataFormat[] ProvideFormats() - => _formats; + => formats; protected override Task TryGetRawCoreAsync(DataFormat format) - => _reader.TryGetAsync(format); + => reader.TryGetAsync(format); } diff --git a/src/Avalonia.X11/Clipboard/ClipboardReadSessionFactory.cs b/src/Avalonia.X11/Selections/Clipboard/ClipboardReadSessionFactory.cs similarity index 92% rename from src/Avalonia.X11/Clipboard/ClipboardReadSessionFactory.cs rename to src/Avalonia.X11/Selections/Clipboard/ClipboardReadSessionFactory.cs index b3935bd35a..9233b87c3a 100644 --- a/src/Avalonia.X11/Clipboard/ClipboardReadSessionFactory.cs +++ b/src/Avalonia.X11/Selections/Clipboard/ClipboardReadSessionFactory.cs @@ -1,7 +1,7 @@ using System; using static Avalonia.X11.XLib; -namespace Avalonia.X11.Clipboard; +namespace Avalonia.X11.Selections.Clipboard; internal static class ClipboardReadSessionFactory { diff --git a/src/Avalonia.X11/Clipboard/EventStreamWindow.cs b/src/Avalonia.X11/Selections/Clipboard/EventStreamWindow.cs similarity index 96% rename from src/Avalonia.X11/Clipboard/EventStreamWindow.cs rename to src/Avalonia.X11/Selections/Clipboard/EventStreamWindow.cs index 3441806e97..73ddbbdb2d 100644 --- a/src/Avalonia.X11/Clipboard/EventStreamWindow.cs +++ b/src/Avalonia.X11/Selections/Clipboard/EventStreamWindow.cs @@ -4,11 +4,10 @@ using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Avalonia.Threading; -using Avalonia.X11.DragDrop; -namespace Avalonia.X11.Clipboard; +namespace Avalonia.X11.Selections.Clipboard; -internal sealed class EventStreamWindow : IEventWaiter +internal sealed class EventStreamWindow : IXEventWaiter { private readonly AvaloniaX11Platform _platform; private IntPtr _handle; diff --git a/src/Avalonia.X11/Clipboard/X11Clipboard.cs b/src/Avalonia.X11/Selections/Clipboard/X11Clipboard.cs similarity index 87% rename from src/Avalonia.X11/Clipboard/X11Clipboard.cs rename to src/Avalonia.X11/Selections/Clipboard/X11Clipboard.cs index 03da5cabfb..04431e9e33 100644 --- a/src/Avalonia.X11/Clipboard/X11Clipboard.cs +++ b/src/Avalonia.X11/Selections/Clipboard/X11Clipboard.cs @@ -11,7 +11,7 @@ using Avalonia.Logging; using Avalonia.Platform.Storage; using static Avalonia.X11.XLib; -namespace Avalonia.X11.Clipboard +namespace Avalonia.X11.Selections.Clipboard { internal sealed class X11ClipboardImpl : IOwnedClipboardImpl { @@ -106,7 +106,7 @@ namespace Avalonia.X11.Clipboard { return property; } - else if (ClipboardDataFormatHelper.ToDataFormat(target, _x11.Atoms) is { } dataFormat) + else if (DataFormatHelper.ToDataFormat(target, _x11.Atoms) is { } dataFormat) { if (_storedDataTransfer is null) return IntPtr.Zero; @@ -161,7 +161,7 @@ namespace Avalonia.X11.Clipboard { var text = dataTransfer.TryGetValueAsync(DataFormat.Text).GetAwaiter().GetResult(); - return ClipboardDataFormatHelper.TryGetStringEncoding(targetFormatAtom, _x11.Atoms) is { } encoding ? + return DataFormatHelper.TryGetStringEncoding(targetFormatAtom, _x11.Atoms) is { } encoding ? encoding.GetBytes(text ?? string.Empty) : null; } @@ -182,7 +182,7 @@ namespace Avalonia.X11.Clipboard if (dataTransfer.TryGetValuesAsync(DataFormat.File).GetAwaiter().GetResult() is not { } files) return null; - return ClipboardUriListHelper.FileUriListToUtf8Bytes(files); + return UriListHelper.FileUriListToUtf8Bytes(files); } if (format is DataFormat stringFormat) @@ -253,7 +253,7 @@ namespace Avalonia.X11.Clipboard { foreach (var format in dataTransfer.Formats) { - foreach (var atom in ClipboardDataFormatHelper.ToAtoms(format, _textAtoms, _x11.Atoms)) + foreach (var atom in DataFormatHelper.ToAtoms(format, _textAtoms, _x11.Atoms)) atoms.Add(atom); } } @@ -317,8 +317,8 @@ namespace Avalonia.X11.Clipboard return null; // Get the items while we're in an async method. This does not get values, except for DataFormat.File. - var reader = new ClipboardDataReader(_x11, _platform, textFormatAtoms, owner, dataFormats); - var items = await CreateItemsAsync(reader, dataFormats); + var reader = new ClipboardDataReader(_platform, textFormatAtoms, dataFormats, owner); + var items = await reader.CreateItemsAsync(); return new ClipboardDataTransfer(reader, dataFormats, items); } @@ -327,41 +327,7 @@ namespace Avalonia.X11.Clipboard using var session = OpenReadSession(); var formatAtoms = await session.SendFormatRequest(0) ?? []; - return ClipboardDataFormatHelper.ToDataFormats(formatAtoms, _x11.Atoms); - } - - private static async Task CreateItemsAsync(ClipboardDataReader reader, DataFormat[] formats) - { - List? nonFileFormats = null; - var items = new List(); - var hasFiles = false; - - foreach (var format in formats) - { - if (DataFormat.File.Equals(format)) - { - if (hasFiles) - continue; - - // We're reading the filenames ahead of time to generate the appropriate items. - // This is async, so it should be fine. - if (await reader.TryGetAsync(format) is IEnumerable storageItems) - { - hasFiles = true; - - foreach (var storageItem in storageItems) - items.Add(PlatformDataTransferItem.Create(DataFormat.File, storageItem)); - } - } - else - (nonFileFormats ??= new()).Add(format); - } - - // Single item containing all formats except for DataFormat.File. - if (nonFileFormats is not null) - items.Add(new ClipboardDataTransferItem(reader, formats)); - - return items.ToArray(); + return DataFormatHelper.ToDataFormats(formatAtoms, _x11.Atoms); } public Task SetDataAsync(IAsyncDataTransfer dataTransfer) diff --git a/src/Avalonia.X11/Clipboard/ClipboardDataFormatHelper.cs b/src/Avalonia.X11/Selections/DataFormatHelper.cs similarity index 98% rename from src/Avalonia.X11/Clipboard/ClipboardDataFormatHelper.cs rename to src/Avalonia.X11/Selections/DataFormatHelper.cs index ce17646812..d80e5d65cf 100644 --- a/src/Avalonia.X11/Clipboard/ClipboardDataFormatHelper.cs +++ b/src/Avalonia.X11/Selections/DataFormatHelper.cs @@ -3,9 +3,9 @@ using System.Collections.Generic; using System.Text; using Avalonia.Input; -namespace Avalonia.X11.Clipboard; +namespace Avalonia.X11.Selections; -internal static class ClipboardDataFormatHelper +internal static class DataFormatHelper { private const string MimeTypeTextUriList = "text/uri-list"; private const string AppPrefix = "application/avn-fmt."; diff --git a/src/Avalonia.X11/Selections/DragDrop/DragDropDataReader.cs b/src/Avalonia.X11/Selections/DragDrop/DragDropDataReader.cs new file mode 100644 index 0000000000..379851751a --- /dev/null +++ b/src/Avalonia.X11/Selections/DragDrop/DragDropDataReader.cs @@ -0,0 +1,49 @@ +using System; +using System.Diagnostics; +using Avalonia.Input; +using Avalonia.Input.Platform; + +namespace Avalonia.X11.Selections.DragDrop; + +/// +/// An object used to read values, converted to the correct format, from a Xdnd selection. +/// +internal sealed class DragDropDataReader( + X11Atoms atoms, + IntPtr[] textFormatAtoms, + DataFormat[] dataFormats, + IntPtr display, + IntPtr targetWindow) + : SelectionDataReader(atoms, textFormatAtoms, dataFormats) +{ + protected override SelectionReadSession CreateReadSession() + { + var eventWaiter = new SynchronousXEventWaiter(display); + return new SelectionReadSession(display, targetWindow, Atoms.XdndSelection, eventWaiter, Atoms); + } + + protected override PlatformDataTransferItem CreateSingleItem(DataFormat[] nonFileFormats) + => new DragDropDataTransferItem(this, nonFileFormats); + + public PlatformDataTransferItem[] CreateItems() + { + // Note: this doesn't cause any deadlock, CreateItemsAsync() will always complete synchronously + // thanks to the SynchronousXEventWaiter used in the SelectionReadSession. + var task = CreateItemsAsync(); + Debug.Assert(task.IsCompleted); + return task.GetAwaiter().GetResult(); + } + + public object? TryGet(DataFormat format) + { + // Note: this doesn't cause any deadlock, TryGetAsync() will always complete synchronously + // thanks to the SynchronousXEventWaiter used in the SelectionReadSession. + var task = TryGetAsync(format); + Debug.Assert(task.IsCompleted); + return task.GetAwaiter().GetResult(); + } + + public override void Dispose() + { + } +} diff --git a/src/Avalonia.X11/DragDrop/DragDropDataTransfer.cs b/src/Avalonia.X11/Selections/DragDrop/DragDropDataTransfer.cs similarity index 79% rename from src/Avalonia.X11/DragDrop/DragDropDataTransfer.cs rename to src/Avalonia.X11/Selections/DragDrop/DragDropDataTransfer.cs index 85fe66f476..aa626643ff 100644 --- a/src/Avalonia.X11/DragDrop/DragDropDataTransfer.cs +++ b/src/Avalonia.X11/Selections/DragDrop/DragDropDataTransfer.cs @@ -1,21 +1,21 @@ using System; using Avalonia.Input; using Avalonia.Input.Platform; -using Avalonia.X11.Clipboard; using static Avalonia.X11.XLib; -namespace Avalonia.X11.DragDrop; +namespace Avalonia.X11.Selections.DragDrop; /// /// Implementation of for data being dragged into an Avalonia window via Xdnd. /// internal sealed class DragDropDataTransfer( + DragDropDataReader reader, + X11Atoms atoms, + DataFormat[] dataFormats, IntPtr display, IntPtr sourceWindow, IntPtr targetWindow, - IInputRoot inputRoot, - DataFormat[] dataFormats, - X11Atoms atoms) + IInputRoot inputRoot) : PlatformDataTransfer { public IntPtr SourceWindow { get; } = sourceWindow; @@ -30,18 +30,12 @@ internal sealed class DragDropDataTransfer( => dataFormats; protected override PlatformDataTransferItem[] ProvideItems() - { - return []; - } - - private SelectionReadSession CreateSession() - { - var eventWaiter = new SynchronousEventWaiter(display); - return new SelectionReadSession(display, targetWindow, atoms.XdndSelection, eventWaiter, atoms); - } + => reader.CreateItems(); public override void Dispose() { + reader.Dispose(); + var evt = new XEvent { ClientMessageEvent = new XClientMessageEvent diff --git a/src/Avalonia.X11/Selections/DragDrop/DragDropDataTransferItem.cs b/src/Avalonia.X11/Selections/DragDrop/DragDropDataTransferItem.cs new file mode 100644 index 0000000000..b6c87bf6dc --- /dev/null +++ b/src/Avalonia.X11/Selections/DragDrop/DragDropDataTransferItem.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using Avalonia.Input; +using Avalonia.Input.Platform; + +namespace Avalonia.X11.Selections.DragDrop; + +/// +/// Implementation of for Xdnd. +/// +/// The object used to read values. +/// The formats. +internal sealed class DragDropDataTransferItem(DragDropDataReader reader, DataFormat[] formats) + : PlatformDataTransferItem +{ + private Dictionary? _cachedValues; + + protected override DataFormat[] ProvideFormats() + => formats; + + protected override object? TryGetRawCore(DataFormat format) + { + if (_cachedValues is null || !_cachedValues.TryGetValue(format, out var value)) + { + value = reader.TryGet(format); + _cachedValues ??= []; + _cachedValues[format] = value; + } + + return value; + } +} diff --git a/src/Avalonia.X11/DragDrop/IXdndWindow.cs b/src/Avalonia.X11/Selections/DragDrop/IXdndWindow.cs similarity index 80% rename from src/Avalonia.X11/DragDrop/IXdndWindow.cs rename to src/Avalonia.X11/Selections/DragDrop/IXdndWindow.cs index c7e5be11c3..32a2e130fe 100644 --- a/src/Avalonia.X11/DragDrop/IXdndWindow.cs +++ b/src/Avalonia.X11/Selections/DragDrop/IXdndWindow.cs @@ -1,7 +1,7 @@ using System; using Avalonia.Input; -namespace Avalonia.X11.DragDrop; +namespace Avalonia.X11.Selections.DragDrop; internal interface IXdndWindow { diff --git a/src/Avalonia.X11/DragDrop/SynchronousEventWaiter.cs b/src/Avalonia.X11/Selections/DragDrop/SynchronousXEventWaiter.cs similarity index 88% rename from src/Avalonia.X11/DragDrop/SynchronousEventWaiter.cs rename to src/Avalonia.X11/Selections/DragDrop/SynchronousXEventWaiter.cs index 6b8e7b552e..9234207062 100644 --- a/src/Avalonia.X11/DragDrop/SynchronousEventWaiter.cs +++ b/src/Avalonia.X11/Selections/DragDrop/SynchronousXEventWaiter.cs @@ -5,9 +5,13 @@ using System.Runtime.InteropServices; using System.Threading.Tasks; using static Avalonia.X11.XLib; -namespace Avalonia.X11.DragDrop; +namespace Avalonia.X11.Selections.DragDrop; -internal sealed partial class SynchronousEventWaiter(IntPtr display) : IEventWaiter +/// +/// An implementation of that waits for an event synchronously by using its own event loop. +/// Unprocessed events are put back onto the main queue after the wait. +/// +internal sealed partial class SynchronousXEventWaiter(IntPtr display) : IXEventWaiter { private const short POLLIN = 0x0001; @@ -100,7 +104,7 @@ internal sealed partial class SynchronousEventWaiter(IntPtr display) : IEventWai } } - Task IEventWaiter.WaitForEventAsync(Func predicate, TimeSpan timeout) + Task IXEventWaiter.WaitForEventAsync(Func predicate, TimeSpan timeout) => Task.FromResult(WaitForEvent(predicate, timeout)); void IDisposable.Dispose() diff --git a/src/Avalonia.X11/DragDrop/XdndHandler.cs b/src/Avalonia.X11/Selections/DragDrop/XdndHandler.cs similarity index 88% rename from src/Avalonia.X11/DragDrop/XdndHandler.cs rename to src/Avalonia.X11/Selections/DragDrop/XdndHandler.cs index 611217fc16..d657b23894 100644 --- a/src/Avalonia.X11/DragDrop/XdndHandler.cs +++ b/src/Avalonia.X11/Selections/DragDrop/XdndHandler.cs @@ -3,10 +3,9 @@ using System.Collections.Generic; using System.Linq; using Avalonia.Input; using Avalonia.Input.Raw; -using Avalonia.X11.Clipboard; using static Avalonia.X11.XLib; -namespace Avalonia.X11.DragDrop; +namespace Avalonia.X11.Selections.DragDrop; /// /// Manages XDND (drag and drop) for a given X11 window. @@ -53,16 +52,13 @@ internal sealed class XdndHandler var formats = new HashSet(); - if (hasExtraFormats) + if (hasExtraFormats && + XGetWindowPropertyAsIntPtrArray(_display, sourceWindow, _atoms.XdndTypeList, _atoms.ATOM) is { } formatList) { - if (XGetWindowPropertyAsIntPtrArray(_display, sourceWindow, _atoms.XdndTypeList, _atoms.ATOM) - is { } formatList) + foreach (var format in formatList) { - foreach (var format in formatList) - { - if (format != 0) - formats.Add(format); - } + if (format != 0) + formats.Add(format); } } else @@ -75,10 +71,18 @@ internal sealed class XdndHandler formats.Add(message.ptr5); } - var (dataFormats, _) = ClipboardDataFormatHelper.ToDataFormats(formats.ToArray(), _atoms); - var drag = new DragDropDataTransfer(_display, sourceWindow, _window.Handle, inputRoot, dataFormats, _atoms); + var (dataFormats, textFormats) = DataFormatHelper.ToDataFormats(formats.ToArray(), _atoms); - _currentDrag = drag; + var reader = new DragDropDataReader(_atoms, textFormats, dataFormats, _display, _window.Handle); + + _currentDrag = new DragDropDataTransfer( + reader, + _atoms, + dataFormats, + _display, + sourceWindow, + _window.Handle, + inputRoot); } public void HandleXdndPosition(in XClientMessageEvent message) @@ -131,8 +135,6 @@ internal sealed class XdndHandler XSendEvent(_display, sourceWindow, false, (IntPtr)EventMask.NoEventMask, ref evt); XFlush(_display); - - _currentDrag.ReadText(); } public void HandleXdndLeave(in XClientMessageEvent message) diff --git a/src/Avalonia.X11/DragDrop/IEventWaiter.cs b/src/Avalonia.X11/Selections/IXEventWaiter.cs similarity index 62% rename from src/Avalonia.X11/DragDrop/IEventWaiter.cs rename to src/Avalonia.X11/Selections/IXEventWaiter.cs index 6cf420da92..98398b615f 100644 --- a/src/Avalonia.X11/DragDrop/IEventWaiter.cs +++ b/src/Avalonia.X11/Selections/IXEventWaiter.cs @@ -1,9 +1,9 @@ using System; using System.Threading.Tasks; -namespace Avalonia.X11.DragDrop; +namespace Avalonia.X11.Selections; -internal interface IEventWaiter : IDisposable +internal interface IXEventWaiter : IDisposable { Task WaitForEventAsync(Func predicate, TimeSpan timeout); } diff --git a/src/Avalonia.X11/Selections/SelectionDataReader.cs b/src/Avalonia.X11/Selections/SelectionDataReader.cs new file mode 100644 index 0000000000..de6ff13653 --- /dev/null +++ b/src/Avalonia.X11/Selections/SelectionDataReader.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Media.Imaging; +using Avalonia.Platform.Storage; + +namespace Avalonia.X11.Selections; + +/// +/// An object used to read values, converted to the correct format, from an X11 selection (clipboard/drag-and-drop). +/// +internal abstract class SelectionDataReader( + X11Atoms atoms, + IntPtr[] textFormatAtoms, + DataFormat[] dataFormats) + : IDisposable + where TItem : class +{ + protected X11Atoms Atoms { get; } = atoms; + + public async Task CreateItemsAsync() + { + List? nonFileFormats = null; + var items = new List(); + var hasFiles = false; + + foreach (var format in dataFormats) + { + if (DataFormat.File.Equals(format)) + { + if (hasFiles) + continue; + + if (await TryGetAsync(format) is IEnumerable storageItems) + { + hasFiles = true; + + foreach (var storageItem in storageItems) + items.Add((TItem)(object)PlatformDataTransferItem.Create(DataFormat.File, storageItem)); + } + } + else + (nonFileFormats ??= []).Add(format); + } + + // Single item containing all formats except for DataFormat.File. + if (nonFileFormats is not null) + items.Add(CreateSingleItem(nonFileFormats.ToArray())); + + return items.ToArray(); + } + + public virtual async Task TryGetAsync(DataFormat format) + { + var formatAtom = DataFormatHelper.ToAtom(format, textFormatAtoms, Atoms, dataFormats); + if (formatAtom == IntPtr.Zero) + return null; + + using var session = CreateReadSession(); + var result = await session.SendDataRequest(formatAtom, 0).ConfigureAwait(false); + return ConvertDataResult(result, format, formatAtom); + } + + protected abstract TItem CreateSingleItem(DataFormat[] nonFileFormats); + + protected abstract SelectionReadSession CreateReadSession(); + + private object? ConvertDataResult(SelectionReadSession.GetDataResult? result, DataFormat format, IntPtr formatAtom) + { + if (result is null) + return null; + + if (DataFormat.Text.Equals(format)) + { + return DataFormatHelper.TryGetStringEncoding(result.TypeAtom, Atoms) is { } textEncoding ? + textEncoding.GetString(result.AsBytes()) : + null; + } + + if (DataFormat.Bitmap.Equals(format)) + { + using var data = result.AsStream(); + + return new Bitmap(data); + } + + if (DataFormat.File.Equals(format)) + { + // text/uri-list might not be supported + return formatAtom != IntPtr.Zero && result.TypeAtom == formatAtom ? + UriListHelper.Utf8BytesToFileUriList(result.AsBytes()) : + null; + } + + if (format is DataFormat) + return Encoding.UTF8.GetString(result.AsBytes()); + + if (format is DataFormat) + return result.AsBytes(); + + return null; + } + + public abstract void Dispose(); +} diff --git a/src/Avalonia.X11/Clipboard/SelectionReadSession.cs b/src/Avalonia.X11/Selections/SelectionReadSession.cs similarity index 95% rename from src/Avalonia.X11/Clipboard/SelectionReadSession.cs rename to src/Avalonia.X11/Selections/SelectionReadSession.cs index 9be5c9ca92..5123244691 100644 --- a/src/Avalonia.X11/Clipboard/SelectionReadSession.cs +++ b/src/Avalonia.X11/Selections/SelectionReadSession.cs @@ -3,16 +3,18 @@ using System.Buffers; using System.IO; using System.Runtime.InteropServices; using System.Threading.Tasks; -using Avalonia.X11.DragDrop; using static Avalonia.X11.XLib; -namespace Avalonia.X11.Clipboard; +namespace Avalonia.X11.Selections; +/// +/// A session used to read a X11 selection (Clipboard/Drag-and-Drop) from a given window. +/// internal sealed class SelectionReadSession( IntPtr display, IntPtr window, IntPtr selection, - IEventWaiter eventWaiter, + IXEventWaiter eventWaiter, X11Atoms atoms) : IDisposable { @@ -52,7 +54,7 @@ internal sealed class SelectionReadSession( { XGetWindowProperty(display, window, property, IntPtr.Zero, new IntPtr (0x7fffffff), true, (IntPtr)Atom.AnyPropertyType, - out var actualTypeAtom, out var actualFormat, out var nitems, out var bytes_after, out var prop); + out var actualTypeAtom, out var actualFormat, out var nitems, out _, out var prop); return new (prop, actualTypeAtom, actualFormat, nitems); } diff --git a/src/Avalonia.X11/Clipboard/ClipboardUriListHelper.cs b/src/Avalonia.X11/Selections/UriListHelper.cs similarity index 94% rename from src/Avalonia.X11/Clipboard/ClipboardUriListHelper.cs rename to src/Avalonia.X11/Selections/UriListHelper.cs index da175c5828..774a6ce87a 100644 --- a/src/Avalonia.X11/Clipboard/ClipboardUriListHelper.cs +++ b/src/Avalonia.X11/Selections/UriListHelper.cs @@ -5,9 +5,9 @@ using System.Text; using Avalonia.Platform.Storage; using Avalonia.Platform.Storage.FileIO; -namespace Avalonia.X11.Clipboard; +namespace Avalonia.X11.Selections; -internal static class ClipboardUriListHelper +internal static class UriListHelper { private static readonly Encoding s_utf8NoBomEncoding = new UTF8Encoding(false); diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index a3f98265f4..cb821d22a9 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -17,11 +17,11 @@ using Avalonia.Rendering.Composition; using Avalonia.Threading; using Avalonia.Vulkan; using Avalonia.X11; -using Avalonia.X11.Clipboard; using Avalonia.X11.Dispatching; using Avalonia.X11.Glx; -using Avalonia.X11.Vulkan; using Avalonia.X11.Screens; +using Avalonia.X11.Selections.Clipboard; +using Avalonia.X11.Vulkan; using static Avalonia.X11.XLib; namespace Avalonia.X11 diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index cad9a39287..97f2cf9bb0 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -2,32 +2,30 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using Avalonia.Reactive; +using System.Runtime.InteropServices; using System.Text; -using System.Threading.Tasks; using System.Threading; +using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.Dialogs; using Avalonia.FreeDesktop; using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; -using Avalonia.OpenGL; using Avalonia.OpenGL.Egl; using Avalonia.Platform; -using Avalonia.Platform.Surfaces; using Avalonia.Platform.Storage; +using Avalonia.Platform.Storage.FileIO; +using Avalonia.Platform.Surfaces; using Avalonia.Rendering.Composition; using Avalonia.Threading; using Avalonia.X11.Glx; using Avalonia.X11.NativeDialogs; +using Avalonia.X11.Selections.DragDrop; using static Avalonia.X11.XLib; -using Avalonia.Input.Platform; -using System.Runtime.InteropServices; -using Avalonia.Dialogs; -using Avalonia.Platform.Storage.FileIO; -using Avalonia.X11.DragDrop; // ReSharper disable IdentifierTypo // ReSharper disable StringLiteralTypo