Browse Source

Xdnd: implement synchronous event waiter

pull/20926/head
Julien Lebosquain 7 months ago
parent
commit
87f11a4eab
No known key found for this signature in database GPG Key ID: 1833CAD10ACC46FD
  1. 16
      src/Avalonia.X11/Clipboard/ClipboardDataReader.cs
  2. 20
      src/Avalonia.X11/Clipboard/ClipboardReadSessionFactory.cs
  3. 16
      src/Avalonia.X11/Clipboard/EventStreamWindow.cs
  4. 68
      src/Avalonia.X11/Clipboard/SelectionReadSession.cs
  5. 4
      src/Avalonia.X11/Clipboard/X11Clipboard.cs
  6. 9
      src/Avalonia.X11/DragDrop/DragDropDataTransfer.cs
  7. 9
      src/Avalonia.X11/DragDrop/IEventWaiter.cs
  8. 120
      src/Avalonia.X11/DragDrop/SynchronousEventWaiter.cs
  9. 7
      src/Avalonia.X11/DragDrop/XdndHandler.cs
  10. 7
      src/Avalonia.X11/XLib.cs

16
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<object?> 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;
}

20
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);
}
}

16
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<XEvent?> WaitForEventAsync(Func<XEvent, bool> predicate, TimeSpan? timeout = null)
public Task<XEvent?> WaitForEventAsync(Func<XEvent, bool> 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<XEvent?>();
_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();

68
src/Avalonia.X11/Clipboard/ClipboardReadSession.cs → 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<PropertyReadResult?>
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<PropertyReadResult?> ConvertSelectionAndGetProperty(
IntPtr target, IntPtr property)
private Task<PropertyReadResult?> 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<IntPtr[]?> SendFormatRequest()
public async Task<IntPtr[]?> 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<GetDataResult?> 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<GetDataResult?> SendDataRequest(IntPtr format)
public async Task<GetDataResult?> 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);
}

4
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);
}

9
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

9
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<XEvent?> WaitForEventAsync(Func<XEvent, bool> predicate, TimeSpan timeout);
}

120
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<XEvent, bool> predicate, TimeSpan timeout)
{
var startingTimestamp = Stopwatch.GetTimestamp();
TimeSpan elapsed;
List<XEvent>? 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<XEvent?> IEventWaiter.WaitForEventAsync(Func<XEvent, bool> 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);
}

7
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)

7
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);

Loading…
Cancel
Save