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;
using System.IO;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Input; using Avalonia.Input;
@ -19,36 +18,33 @@ internal sealed class ClipboardDataReader(
DataFormat[] dataFormats) DataFormat[] dataFormats)
: IDisposable : IDisposable
{ {
private readonly X11Info _x11 = x11;
private readonly AvaloniaX11Platform _platform = platform;
private readonly IntPtr[] _textFormatAtoms = textFormatAtoms;
private IntPtr _owner = owner; private IntPtr _owner = owner;
private bool IsOwnerStillValid() 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) public async Task<object?> TryGetAsync(DataFormat format)
{ {
if (!IsOwnerStillValid()) if (!IsOwnerStillValid())
return null; return null;
var formatAtom = ClipboardDataFormatHelper.ToAtom(format, _textFormatAtoms, _x11.Atoms, dataFormats); var formatAtom = ClipboardDataFormatHelper.ToAtom(format, textFormatAtoms, x11.Atoms, dataFormats);
if (formatAtom == IntPtr.Zero) if (formatAtom == IntPtr.Zero)
return null; return null;
using var session = new ClipboardReadSession(_platform); using var session = ClipboardReadSessionFactory.CreateSession(platform);
var result = await session.SendDataRequest(formatAtom).ConfigureAwait(false); var result = await session.SendDataRequest(formatAtom, 0).ConfigureAwait(false);
return ConvertDataResult(result, format, formatAtom); 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) if (result is null)
return null; return null;
if (DataFormat.Text.Equals(format)) 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()) : textEncoding.GetString(result.AsBytes()) :
null; 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.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.X11.DragDrop;
namespace Avalonia.X11.Clipboard; namespace Avalonia.X11.Clipboard;
internal class EventStreamWindow : IDisposable internal sealed class EventStreamWindow : IEventWaiter
{ {
private readonly AvaloniaX11Platform _platform; private readonly AvaloniaX11Platform _platform;
private IntPtr _handle; 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?>(); var tcs = new TaskCompletionSource<XEvent?>();
_addedListeners.Add((predicate, tcs, _time.Elapsed + timeout.Value)); _addedListeners.Add((predicate, tcs, _time.Elapsed + timeout));
_timeoutTimer.Start(); _timeoutTimer.Start();
return tcs.Task; return tcs.Task;
} }
public void Dispose() public void Dispose()
{ {
_timeoutTimer.Stop(); _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.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.X11.DragDrop;
using static Avalonia.X11.XLib; using static Avalonia.X11.XLib;
namespace Avalonia.X11.Clipboard; 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; public void Dispose() => eventWaiter.Dispose();
private readonly EventStreamWindow _window;
private readonly X11Info _x11;
public ClipboardReadSession(AvaloniaX11Platform platform) private sealed class PropertyReadResult(IntPtr data, IntPtr actualTypeAtom, int actualFormat, IntPtr nItems)
{
_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)
: IDisposable : IDisposable
{ {
public IntPtr Data => data; public IntPtr Data => data;
@ -40,39 +35,36 @@ class ClipboardReadSession : IDisposable
private async Task<PropertyReadResult?> private async Task<PropertyReadResult?>
WaitForSelectionNotifyAndGetProperty(IntPtr property) WaitForSelectionNotifyAndGetProperty(IntPtr property)
{ {
var ev = await _window.WaitForEventAsync(ev => var ev = await eventWaiter.WaitForEventAsync(
ev.type == XEventName.SelectionNotify ev => ev.type == XEventName.SelectionNotify &&
&& ev.SelectionEvent.selection == _x11.Atoms.CLIPBOARD ev.SelectionEvent.requestor == window &&
&& ev.SelectionEvent.property == property ev.SelectionEvent.selection == selection &&
); ev.SelectionEvent.property == property,
TimeSpan.FromSeconds(5));
if (ev == null) if (ev == null)
return null; return null;
var sel = ev.Value.SelectionEvent; return ReadProperty(property);
return ReadProperty(sel.property);
} }
private PropertyReadResult ReadProperty(IntPtr 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, (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 var bytes_after, out var prop);
return new (prop, actualTypeAtom, actualFormat, nitems); return new (prop, actualTypeAtom, actualFormat, nitems);
} }
private Task<PropertyReadResult?> ConvertSelectionAndGetProperty( private Task<PropertyReadResult?> ConvertSelectionAndGetProperty(IntPtr target, IntPtr property, IntPtr timestamp)
IntPtr target, IntPtr property)
{ {
XConvertSelection(_platform.Display, _x11.Atoms.CLIPBOARD, target, property, _window.Handle, XConvertSelection(display, selection, target, property, window, timestamp);
IntPtr.Zero);
return WaitForSelectionNotifyAndGetProperty(property); 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) if (res == null)
return null; return null;
@ -97,7 +89,7 @@ class ClipboardReadSession : IDisposable
private async Task<GetDataResult?> ReadIncr(IntPtr property) private async Task<GetDataResult?> ReadIncr(IntPtr property)
{ {
XFlush(_platform.Display); XFlush(display);
var ms = new MemoryStream(); var ms = new MemoryStream();
void Append(PropertyReadResult res) void Append(PropertyReadResult res)
{ {
@ -110,9 +102,11 @@ class ClipboardReadSession : IDisposable
IntPtr actualTypeAtom = IntPtr.Zero; IntPtr actualTypeAtom = IntPtr.Zero;
while (true) while (true)
{ {
var ev = await _window.WaitForEventAsync(x => var ev = await eventWaiter.WaitForEventAsync(
x is { type: XEventName.PropertyNotify, PropertyEvent.state: 0 } && x => x is { type: XEventName.PropertyNotify, PropertyEvent.state: 0 } &&
x.PropertyEvent.atom == property); x.PropertyEvent.window == window &&
x.PropertyEvent.atom == property,
TimeSpan.FromSeconds(5));
if (ev == null) if (ev == null)
return null; return null;
@ -131,15 +125,15 @@ class ClipboardReadSession : IDisposable
return new(null, ms, actualTypeAtom); 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) if (res == null)
return null; return null;
if (res.NItems == IntPtr.Zero) if (res.NItems == IntPtr.Zero)
return null; return null;
if (res.ActualTypeAtom == _x11.Atoms.INCR) if (res.ActualTypeAtom == atoms.INCR)
{ {
return await ReadIncr(format); return await ReadIncr(format);
} }

4
src/Avalonia.X11/Clipboard/X11Clipboard.cs

@ -243,7 +243,7 @@ namespace Avalonia.X11.Clipboard
private IntPtr GetOwner() private IntPtr GetOwner()
=> XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD); => XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD);
private ClipboardReadSession OpenReadSession() => new(_platform); private SelectionReadSession OpenReadSession() => ClipboardReadSessionFactory.CreateSession(_platform);
private IntPtr[] ConvertDataTransfer(IAsyncDataTransfer? dataTransfer) private IntPtr[] ConvertDataTransfer(IAsyncDataTransfer? dataTransfer)
{ {
@ -326,7 +326,7 @@ namespace Avalonia.X11.Clipboard
{ {
using var session = OpenReadSession(); using var session = OpenReadSession();
var formatAtoms = await session.SendFormatRequest() ?? []; var formatAtoms = await session.SendFormatRequest(0) ?? [];
return ClipboardDataFormatHelper.ToDataFormats(formatAtoms, _x11.Atoms); return ClipboardDataFormatHelper.ToDataFormats(formatAtoms, _x11.Atoms);
} }

9
src/Avalonia.X11/DragDrop/DragDropDataTransfer.cs

@ -1,6 +1,7 @@
using System; using System;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Platform; using Avalonia.Input.Platform;
using Avalonia.X11.Clipboard;
using static Avalonia.X11.XLib; using static Avalonia.X11.XLib;
namespace Avalonia.X11.DragDrop; namespace Avalonia.X11.DragDrop;
@ -23,6 +24,8 @@ internal sealed class DragDropDataTransfer(
public Point? LastPosition { get; set; } public Point? LastPosition { get; set; }
public IntPtr LastTimestamp { get; set; }
protected override DataFormat[] ProvideFormats() protected override DataFormat[] ProvideFormats()
=> dataFormats; => dataFormats;
@ -31,6 +34,12 @@ internal sealed class DragDropDataTransfer(
return []; return [];
} }
private SelectionReadSession CreateSession()
{
var eventWaiter = new SynchronousEventWaiter(display);
return new SelectionReadSession(display, targetWindow, atoms.XdndSelection, eventWaiter, atoms);
}
public override void Dispose() public override void Dispose()
{ {
var evt = new XEvent 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; _atoms = atoms;
IntPtr version = XdndVersion; 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) public void HandleXdndEnter(in XClientMessageEvent message)
@ -55,7 +55,7 @@ internal sealed class XdndHandler
if (hasExtraFormats) if (hasExtraFormats)
{ {
if (XGetWindowPropertyAsIntPtrArray(_display, sourceWindow, _atoms.XdndTypeList, _atoms.XA_ATOM) if (XGetWindowPropertyAsIntPtrArray(_display, sourceWindow, _atoms.XdndTypeList, _atoms.ATOM)
is { } formatList) is { } formatList)
{ {
foreach (var format in formatList) foreach (var format in formatList)
@ -97,6 +97,7 @@ internal sealed class XdndHandler
var eventType = _currentDrag.LastPosition is null ? RawDragEventType.DragEnter : RawDragEventType.DragOver; var eventType = _currentDrag.LastPosition is null ? RawDragEventType.DragEnter : RawDragEventType.DragOver;
_currentDrag.LastPosition = position; _currentDrag.LastPosition = position;
_currentDrag.LastTimestamp = message.ptr4;
var dragEnter = new RawDragEvent( var dragEnter = new RawDragEvent(
_dragDropDevice, _dragDropDevice,
@ -130,6 +131,8 @@ internal sealed class XdndHandler
XSendEvent(_display, sourceWindow, false, (IntPtr)EventMask.NoEventMask, ref evt); XSendEvent(_display, sourceWindow, false, (IntPtr)EventMask.NoEventMask, ref evt);
XFlush(_display); XFlush(_display);
_currentDrag.ReadText();
} }
public void HandleXdndLeave(in XClientMessageEvent message) 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); public static extern IntPtr XDefaultRootWindow(IntPtr display);
[DllImport(libX11)] [DllImport(libX11)]
public static extern IntPtr XNextEvent(IntPtr display, out XEvent xevent); public static extern int XNextEvent(IntPtr display, out XEvent xevent);
[DllImport(libX11)] [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)] [DllImport(libX11)]
public static extern int XConnectionNumber(IntPtr diplay); public static extern int XConnectionNumber(IntPtr diplay);

Loading…
Cancel
Save