From 87f11a4eabfb9a580a10da063191078acfe4581d Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Fri, 13 Mar 2026 12:49:57 +0100 Subject: [PATCH] Xdnd: implement synchronous event waiter --- .../Clipboard/ClipboardDataReader.cs | 16 +-- .../Clipboard/ClipboardReadSessionFactory.cs | 20 +++ .../Clipboard/EventStreamWindow.cs | 16 +-- ...ReadSession.cs => SelectionReadSession.cs} | 68 +++++----- src/Avalonia.X11/Clipboard/X11Clipboard.cs | 4 +- .../DragDrop/DragDropDataTransfer.cs | 9 ++ src/Avalonia.X11/DragDrop/IEventWaiter.cs | 9 ++ .../DragDrop/SynchronousEventWaiter.cs | 120 ++++++++++++++++++ src/Avalonia.X11/DragDrop/XdndHandler.cs | 7 +- src/Avalonia.X11/XLib.cs | 7 +- 10 files changed, 212 insertions(+), 64 deletions(-) create mode 100644 src/Avalonia.X11/Clipboard/ClipboardReadSessionFactory.cs rename src/Avalonia.X11/Clipboard/{ClipboardReadSession.cs => SelectionReadSession.cs} (66%) create mode 100644 src/Avalonia.X11/DragDrop/IEventWaiter.cs create mode 100644 src/Avalonia.X11/DragDrop/SynchronousEventWaiter.cs diff --git a/src/Avalonia.X11/Clipboard/ClipboardDataReader.cs b/src/Avalonia.X11/Clipboard/ClipboardDataReader.cs index 929da2a43f..223d7aa3dd 100644 --- a/src/Avalonia.X11/Clipboard/ClipboardDataReader.cs +++ b/src/Avalonia.X11/Clipboard/ClipboardDataReader.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Text; using System.Threading.Tasks; using Avalonia.Input; @@ -19,36 +18,33 @@ internal sealed class ClipboardDataReader( DataFormat[] dataFormats) : IDisposable { - private readonly X11Info _x11 = x11; - private readonly AvaloniaX11Platform _platform = platform; - private readonly IntPtr[] _textFormatAtoms = textFormatAtoms; private IntPtr _owner = owner; private bool IsOwnerStillValid() - => _owner != IntPtr.Zero && XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD) == _owner; + => _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); + var formatAtom = ClipboardDataFormatHelper.ToAtom(format, textFormatAtoms, x11.Atoms, dataFormats); if (formatAtom == IntPtr.Zero) return null; - using var session = new ClipboardReadSession(_platform); - var result = await session.SendDataRequest(formatAtom).ConfigureAwait(false); + using var session = ClipboardReadSessionFactory.CreateSession(platform); + var result = await session.SendDataRequest(formatAtom, 0).ConfigureAwait(false); return ConvertDataResult(result, format, formatAtom); } - private object? ConvertDataResult(ClipboardReadSession.GetDataResult? result, DataFormat format, IntPtr 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 ? + return ClipboardDataFormatHelper.TryGetStringEncoding(result.TypeAtom, x11.Atoms) is { } textEncoding ? textEncoding.GetString(result.AsBytes()) : null; } diff --git a/src/Avalonia.X11/Clipboard/ClipboardReadSessionFactory.cs b/src/Avalonia.X11/Clipboard/ClipboardReadSessionFactory.cs new file mode 100644 index 0000000000..b3935bd35a --- /dev/null +++ b/src/Avalonia.X11/Clipboard/ClipboardReadSessionFactory.cs @@ -0,0 +1,20 @@ +using System; +using static Avalonia.X11.XLib; + +namespace Avalonia.X11.Clipboard; + +internal static class ClipboardReadSessionFactory +{ + public static SelectionReadSession CreateSession(AvaloniaX11Platform platform) + { + var window = new EventStreamWindow(platform); + XSelectInput(platform.Display, window.Handle, new IntPtr((int)XEventMask.PropertyChangeMask)); + + return new SelectionReadSession( + platform.Display, + window.Handle, + platform.Info.Atoms.CLIPBOARD, + window, + platform.Info.Atoms); + } +} diff --git a/src/Avalonia.X11/Clipboard/EventStreamWindow.cs b/src/Avalonia.X11/Clipboard/EventStreamWindow.cs index 1c9edbde49..3441806e97 100644 --- a/src/Avalonia.X11/Clipboard/EventStreamWindow.cs +++ b/src/Avalonia.X11/Clipboard/EventStreamWindow.cs @@ -4,10 +4,11 @@ using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Avalonia.Threading; +using Avalonia.X11.DragDrop; namespace Avalonia.X11.Clipboard; -internal class EventStreamWindow : IDisposable +internal sealed class EventStreamWindow : IEventWaiter { private readonly AvaloniaX11Platform _platform; private IntPtr _handle; @@ -73,22 +74,15 @@ internal class EventStreamWindow : IDisposable } } - public Task WaitForEventAsync(Func predicate, TimeSpan? timeout = null) + public Task WaitForEventAsync(Func predicate, TimeSpan timeout) { - timeout ??= TimeSpan.FromSeconds(5); - - if (timeout < TimeSpan.Zero) - throw new TimeoutException(); - if(timeout > TimeSpan.FromDays(1)) - throw new ArgumentOutOfRangeException(nameof(timeout)); - var tcs = new TaskCompletionSource(); - _addedListeners.Add((predicate, tcs, _time.Elapsed + timeout.Value)); + _addedListeners.Add((predicate, tcs, _time.Elapsed + timeout)); _timeoutTimer.Start(); return tcs.Task; } - + public void Dispose() { _timeoutTimer.Stop(); diff --git a/src/Avalonia.X11/Clipboard/ClipboardReadSession.cs b/src/Avalonia.X11/Clipboard/SelectionReadSession.cs similarity index 66% rename from src/Avalonia.X11/Clipboard/ClipboardReadSession.cs rename to src/Avalonia.X11/Clipboard/SelectionReadSession.cs index 7c83ecea40..9be5c9ca92 100644 --- a/src/Avalonia.X11/Clipboard/ClipboardReadSession.cs +++ b/src/Avalonia.X11/Clipboard/SelectionReadSession.cs @@ -3,27 +3,22 @@ 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; -class ClipboardReadSession : IDisposable +internal sealed class SelectionReadSession( + IntPtr display, + IntPtr window, + IntPtr selection, + IEventWaiter eventWaiter, + X11Atoms atoms) + : IDisposable { - private readonly AvaloniaX11Platform _platform; - private readonly EventStreamWindow _window; - private readonly X11Info _x11; + public void Dispose() => eventWaiter.Dispose(); - public ClipboardReadSession(AvaloniaX11Platform platform) - { - _platform = platform; - _window = new EventStreamWindow(platform); - _x11 = _platform.Info; - XSelectInput(_x11.Display, _window.Handle, new IntPtr((int)XEventMask.PropertyChangeMask)); - } - - public void Dispose() => _window.Dispose(); - - class PropertyReadResult(IntPtr data, IntPtr actualTypeAtom, int actualFormat, IntPtr nItems) + private sealed class PropertyReadResult(IntPtr data, IntPtr actualTypeAtom, int actualFormat, IntPtr nItems) : IDisposable { public IntPtr Data => data; @@ -40,39 +35,36 @@ class ClipboardReadSession : IDisposable private async Task WaitForSelectionNotifyAndGetProperty(IntPtr property) { - var ev = await _window.WaitForEventAsync(ev => - ev.type == XEventName.SelectionNotify - && ev.SelectionEvent.selection == _x11.Atoms.CLIPBOARD - && ev.SelectionEvent.property == property - ); + var ev = await eventWaiter.WaitForEventAsync( + ev => ev.type == XEventName.SelectionNotify && + ev.SelectionEvent.requestor == window && + ev.SelectionEvent.selection == selection && + ev.SelectionEvent.property == property, + TimeSpan.FromSeconds(5)); if (ev == null) return null; - var sel = ev.Value.SelectionEvent; - - return ReadProperty(sel.property); + return ReadProperty(property); } private PropertyReadResult ReadProperty(IntPtr property) { - XGetWindowProperty(_x11.Display, _window.Handle, property, IntPtr.Zero, new IntPtr (0x7fffffff), true, + 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); return new (prop, actualTypeAtom, actualFormat, nitems); } - private Task ConvertSelectionAndGetProperty( - IntPtr target, IntPtr property) + private Task ConvertSelectionAndGetProperty(IntPtr target, IntPtr property, IntPtr timestamp) { - XConvertSelection(_platform.Display, _x11.Atoms.CLIPBOARD, target, property, _window.Handle, - IntPtr.Zero); + XConvertSelection(display, selection, target, property, window, timestamp); return WaitForSelectionNotifyAndGetProperty(property); } - public async Task SendFormatRequest() + public async Task SendFormatRequest(IntPtr targetsAtom) { - using var res = await ConvertSelectionAndGetProperty(_x11.Atoms.TARGETS, _x11.Atoms.TARGETS); + using var res = await ConvertSelectionAndGetProperty(atoms.TARGETS, atoms.TARGETS, 0); if (res == null) return null; @@ -97,7 +89,7 @@ class ClipboardReadSession : IDisposable private async Task ReadIncr(IntPtr property) { - XFlush(_platform.Display); + XFlush(display); var ms = new MemoryStream(); void Append(PropertyReadResult res) { @@ -110,9 +102,11 @@ class ClipboardReadSession : IDisposable IntPtr actualTypeAtom = IntPtr.Zero; while (true) { - var ev = await _window.WaitForEventAsync(x => - x is { type: XEventName.PropertyNotify, PropertyEvent.state: 0 } && - x.PropertyEvent.atom == property); + var ev = await eventWaiter.WaitForEventAsync( + x => x is { type: XEventName.PropertyNotify, PropertyEvent.state: 0 } && + x.PropertyEvent.window == window && + x.PropertyEvent.atom == property, + TimeSpan.FromSeconds(5)); if (ev == null) return null; @@ -131,15 +125,15 @@ class ClipboardReadSession : IDisposable return new(null, ms, actualTypeAtom); } - public async Task SendDataRequest(IntPtr format) + public async Task SendDataRequest(IntPtr format, IntPtr timestamp) { - using var res = await ConvertSelectionAndGetProperty(format, format); + using var res = await ConvertSelectionAndGetProperty(format, format, timestamp); if (res == null) return null; if (res.NItems == IntPtr.Zero) return null; - if (res.ActualTypeAtom == _x11.Atoms.INCR) + if (res.ActualTypeAtom == atoms.INCR) { return await ReadIncr(format); } diff --git a/src/Avalonia.X11/Clipboard/X11Clipboard.cs b/src/Avalonia.X11/Clipboard/X11Clipboard.cs index 46cf85acec..03da5cabfb 100644 --- a/src/Avalonia.X11/Clipboard/X11Clipboard.cs +++ b/src/Avalonia.X11/Clipboard/X11Clipboard.cs @@ -243,7 +243,7 @@ namespace Avalonia.X11.Clipboard private IntPtr GetOwner() => XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD); - private ClipboardReadSession OpenReadSession() => new(_platform); + private SelectionReadSession OpenReadSession() => ClipboardReadSessionFactory.CreateSession(_platform); private IntPtr[] ConvertDataTransfer(IAsyncDataTransfer? dataTransfer) { @@ -326,7 +326,7 @@ namespace Avalonia.X11.Clipboard { using var session = OpenReadSession(); - var formatAtoms = await session.SendFormatRequest() ?? []; + var formatAtoms = await session.SendFormatRequest(0) ?? []; return ClipboardDataFormatHelper.ToDataFormats(formatAtoms, _x11.Atoms); } diff --git a/src/Avalonia.X11/DragDrop/DragDropDataTransfer.cs b/src/Avalonia.X11/DragDrop/DragDropDataTransfer.cs index 338d5e784d..85fe66f476 100644 --- a/src/Avalonia.X11/DragDrop/DragDropDataTransfer.cs +++ b/src/Avalonia.X11/DragDrop/DragDropDataTransfer.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Input; using Avalonia.Input.Platform; +using Avalonia.X11.Clipboard; using static Avalonia.X11.XLib; namespace Avalonia.X11.DragDrop; @@ -23,6 +24,8 @@ internal sealed class DragDropDataTransfer( public Point? LastPosition { get; set; } + public IntPtr LastTimestamp { get; set; } + protected override DataFormat[] ProvideFormats() => dataFormats; @@ -31,6 +34,12 @@ internal sealed class DragDropDataTransfer( return []; } + private SelectionReadSession CreateSession() + { + var eventWaiter = new SynchronousEventWaiter(display); + return new SelectionReadSession(display, targetWindow, atoms.XdndSelection, eventWaiter, atoms); + } + public override void Dispose() { var evt = new XEvent diff --git a/src/Avalonia.X11/DragDrop/IEventWaiter.cs b/src/Avalonia.X11/DragDrop/IEventWaiter.cs new file mode 100644 index 0000000000..6cf420da92 --- /dev/null +++ b/src/Avalonia.X11/DragDrop/IEventWaiter.cs @@ -0,0 +1,9 @@ +using System; +using System.Threading.Tasks; + +namespace Avalonia.X11.DragDrop; + +internal interface IEventWaiter : IDisposable +{ + Task WaitForEventAsync(Func predicate, TimeSpan timeout); +} diff --git a/src/Avalonia.X11/DragDrop/SynchronousEventWaiter.cs b/src/Avalonia.X11/DragDrop/SynchronousEventWaiter.cs new file mode 100644 index 0000000000..6b8e7b552e --- /dev/null +++ b/src/Avalonia.X11/DragDrop/SynchronousEventWaiter.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using static Avalonia.X11.XLib; + +namespace Avalonia.X11.DragDrop; + +internal sealed partial class SynchronousEventWaiter(IntPtr display) : IEventWaiter +{ + private const short POLLIN = 0x0001; + + public XEvent? WaitForEvent(Func predicate, TimeSpan timeout) + { + var startingTimestamp = Stopwatch.GetTimestamp(); + TimeSpan elapsed; + List? stashedEvents = null; + var fd = 0; + + try { + while (true) + { + if (!CheckTimeout()) + return null; + + // First, wait until there's some event in the queue + while (XPending(display) == 0) + { + if (fd == 0) + fd = XConnectionNumber(display); + + if (!CheckTimeout()) + return null; + + var remainingTimeout = timeout - elapsed; + + var pollfd = new PollFd + { + fd = fd, + events = POLLIN + }; + + unsafe + { + if (poll(&pollfd, 1, (int)remainingTimeout.TotalMilliseconds) != 1) + return null; + } + } + + if (!CheckTimeout()) + return null; + + // Second, check if there's an event we're interested in. + // Use XNextEvent and stash the unrelated events, then put them back onto the queue. + // We can't use XIfEvent because it could block indefinitely. + // We can't use XCheckIfEvent either: by leaving the event in the queue, the next poll() + // returns immediately, eating CPU, and we don't want to add arbitrary thread sleeps. + XNextEvent(display, out var evt); + + if (evt.type == XEventName.GenericEvent) + { + unsafe + { + XGetEventData(display, &evt.GenericEventCookie); + } + } + + if (predicate(evt)) + return evt; + + stashedEvents ??= []; + stashedEvents.Add(evt); + } + } + finally + { + if (stashedEvents is not null) + { + foreach (var evt in stashedEvents) + { + XPutBackEvent(display, evt); + + if (evt.type == XEventName.GenericEvent) + { + unsafe + { + if (evt.GenericEventCookie.data != null) + XFreeEventData(display, &evt.GenericEventCookie); + } + } + } + } + } + + bool CheckTimeout() + { + elapsed = Stopwatch.GetElapsedTime(startingTimestamp); + return elapsed < timeout; + } + } + + Task IEventWaiter.WaitForEventAsync(Func predicate, TimeSpan timeout) + => Task.FromResult(WaitForEvent(predicate, timeout)); + + void IDisposable.Dispose() + { + } + + [StructLayout(LayoutKind.Sequential)] + private struct PollFd + { + public int fd; + public short events; + public short revents; + } + + [LibraryImport("libc", SetLastError = true)] + private static unsafe partial int poll(PollFd* fds, nint nfds, int timeout); +} diff --git a/src/Avalonia.X11/DragDrop/XdndHandler.cs b/src/Avalonia.X11/DragDrop/XdndHandler.cs index 0cb8969917..611217fc16 100644 --- a/src/Avalonia.X11/DragDrop/XdndHandler.cs +++ b/src/Avalonia.X11/DragDrop/XdndHandler.cs @@ -30,7 +30,7 @@ internal sealed class XdndHandler _atoms = atoms; IntPtr version = XdndVersion; - XChangeProperty(_display, _window.Handle, _atoms.XdndAware, _atoms.XA_ATOM, 32, PropertyMode.Replace, ref version, 1); + XChangeProperty(_display, _window.Handle, _atoms.XdndAware, _atoms.ATOM, 32, PropertyMode.Replace, ref version, 1); } public void HandleXdndEnter(in XClientMessageEvent message) @@ -55,7 +55,7 @@ internal sealed class XdndHandler if (hasExtraFormats) { - if (XGetWindowPropertyAsIntPtrArray(_display, sourceWindow, _atoms.XdndTypeList, _atoms.XA_ATOM) + if (XGetWindowPropertyAsIntPtrArray(_display, sourceWindow, _atoms.XdndTypeList, _atoms.ATOM) is { } formatList) { foreach (var format in formatList) @@ -97,6 +97,7 @@ internal sealed class XdndHandler var eventType = _currentDrag.LastPosition is null ? RawDragEventType.DragEnter : RawDragEventType.DragOver; _currentDrag.LastPosition = position; + _currentDrag.LastTimestamp = message.ptr4; var dragEnter = new RawDragEvent( _dragDropDevice, @@ -130,6 +131,8 @@ 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/XLib.cs b/src/Avalonia.X11/XLib.cs index 595e996733..a37a28b08f 100644 --- a/src/Avalonia.X11/XLib.cs +++ b/src/Avalonia.X11/XLib.cs @@ -60,10 +60,13 @@ namespace Avalonia.X11 public static extern IntPtr XDefaultRootWindow(IntPtr display); [DllImport(libX11)] - public static extern IntPtr XNextEvent(IntPtr display, out XEvent xevent); + public static extern int XNextEvent(IntPtr display, out XEvent xevent); [DllImport(libX11)] - public static extern IntPtr XNextEvent(IntPtr display, XEvent* xevent); + public static extern int XNextEvent(IntPtr display, XEvent* xevent); + + [LibraryImport(libX11)] + public static partial int XPutBackEvent(IntPtr display, in XEvent evt); [DllImport(libX11)] public static extern int XConnectionNumber(IntPtr diplay);