Browse Source

INCR client

feature/x11-incr
Nikita Tsukanov 1 year ago
parent
commit
6aa2d438c9
  1. 132
      src/Avalonia.X11/Clipboard/ClipboardReadSession.cs
  2. 24
      src/Avalonia.X11/Clipboard/EventStreamWindow.cs
  3. 4
      src/Avalonia.X11/Clipboard/X11Clipboard.cs

132
src/Avalonia.X11/Clipboard/ClipboardReadSession.cs

@ -1,4 +1,5 @@
using System;
using System.Buffers;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
@ -16,58 +17,73 @@ class ClipboardReadSession : IDisposable
_platform = platform;
_window = new EventStreamWindow(platform);
_x11 = _platform.Info;
XSelectInput(_x11.Display, _window.Handle, new IntPtr((int)XEventMask.PropertyChangeMask));
}
public void Dispose() => _window.Dispose();
private async Task<(IntPtr propertyData, IntPtr actualTypeAtom, int actualFormat, IntPtr nitems)?> ConvertSelectionAndGetProperty(
IntPtr target, IntPtr property)
class PropertyReadResult(IntPtr data, IntPtr actualTypeAtom, int actualFormat, IntPtr nItems)
: IDisposable
{
XConvertSelection(_platform.Display, _x11.Atoms.CLIPBOARD, target, property, _window.Handle,
IntPtr.Zero);
public IntPtr Data => data;
public IntPtr ActualTypeAtom => actualTypeAtom;
public int ActualFormat => actualFormat;
public IntPtr NItems => nItems;
public void Dispose()
{
XFree(Data);
}
}
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
);
);
if (ev == null)
return null;
var sel = ev.Value.SelectionEvent;
XGetWindowProperty(_x11.Display, _window.Handle, sel.property, IntPtr.Zero, new IntPtr (0x7fffffff), true,
return ReadProperty(sel.property);
}
private PropertyReadResult ReadProperty(IntPtr property)
{
XGetWindowProperty(_x11.Display, _window.Handle, 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 (prop, actualTypeAtom, actualFormat, nitems);
return new (prop, actualTypeAtom, actualFormat, nitems);
}
private Task<PropertyReadResult?> ConvertSelectionAndGetProperty(
IntPtr target, IntPtr property)
{
XConvertSelection(_platform.Display, _x11.Atoms.CLIPBOARD, target, property, _window.Handle,
IntPtr.Zero);
return WaitForSelectionNotifyAndGetProperty(property);
}
public async Task<IntPtr[]?> SendFormatRequest()
{
var res = await ConvertSelectionAndGetProperty(_x11.Atoms.TARGETS, _x11.Atoms.TARGETS);
using var res = await ConvertSelectionAndGetProperty(_x11.Atoms.TARGETS, _x11.Atoms.TARGETS);
if (res == null)
return null;
var (prop, _, actualFormat, nitems) = res.Value;
try
{
if (nitems == IntPtr.Zero)
return null;
if (actualFormat != 32)
return null;
else
{
var formats = new IntPtr[nitems.ToInt32()];
Marshal.Copy(prop, formats, 0, formats.Length);
return formats;
}
}
finally
if (res.NItems == IntPtr.Zero)
return null;
if (res.ActualFormat != 32)
return null;
else
{
XFree(prop);
var formats = new IntPtr[res.NItems.ToInt32()];
Marshal.Copy(res.Data, formats, 0, formats.Length);
return formats;
}
}
@ -77,34 +93,60 @@ class ClipboardReadSession : IDisposable
public byte[] AsBytes() => data ?? stream!.ToArray();
public MemoryStream AsStream() => stream ?? new MemoryStream(data!);
}
private async Task<GetDataResult?> ReadIncr(IntPtr property)
{
XFlush(_platform.Display);
var ms = new MemoryStream();
void Append(PropertyReadResult res)
{
var len = (int)res.NItems * (res.ActualFormat / 8);
var data = ArrayPool<byte>.Shared.Rent(len);
Marshal.Copy(res.Data, data, 0, len);
ms.Write(data, 0, len);
ArrayPool<byte>.Shared.Return(data);
}
IntPtr actualTypeAtom = IntPtr.Zero;
while (true)
{
var ev = await _window.WaitForEventAsync(x =>
x is { type: XEventName.PropertyNotify, PropertyEvent.state: 0 } &&
x.PropertyEvent.atom == property);
if (ev == null)
return null;
using var part = ReadProperty(property);
if (actualTypeAtom == IntPtr.Zero)
actualTypeAtom = part.ActualTypeAtom;
if(part.NItems == IntPtr.Zero)
break;
Append(part);
}
return new(null, ms, actualTypeAtom);
}
public async Task<GetDataResult?> SendDataRequest(IntPtr format)
{
var res = await ConvertSelectionAndGetProperty(format, format);
using var res = await ConvertSelectionAndGetProperty(format, format);
if (res == null)
return null;
var (prop, actualTypeAtom, actualFormat, nitems) = res.Value;
try
if (res.NItems == IntPtr.Zero)
return null;
if (res.ActualTypeAtom == _x11.Atoms.INCR)
{
if (nitems == IntPtr.Zero)
return null;
if (actualTypeAtom == _x11.Atoms.INCR)
{
// TODO: Actually implement that monstrosity
return null;
}
else
{
var data = new byte[(int)nitems * (actualFormat / 8)];
Marshal.Copy(prop, data, 0, data.Length);
return new (data, null, actualTypeAtom);
}
return await ReadIncr(format);
}
finally
else
{
XFree(prop);
var data = new byte[(int)res.NItems * (res.ActualFormat / 8)];
Marshal.Copy(res.Data, data, 0, data.Length);
return new (data, null, res.ActualTypeAtom);
}
}
}

24
src/Avalonia.X11/Clipboard/EventStreamWindow.cs

@ -12,10 +12,10 @@ internal class EventStreamWindow : IDisposable
private readonly AvaloniaX11Platform _platform;
private IntPtr _handle;
public IntPtr Handle => _handle;
private readonly List<(Func<XEvent, bool> filter, TaskCompletionSource<XEvent?> tcs, TimeSpan? timeout)> _listeners = new();
private readonly List<(Func<XEvent, bool> filter, TaskCompletionSource<XEvent?> tcs, TimeSpan timeout)> _listeners = new();
// We are adding listeners to an intermediate collection to avoid freshly added listeners to be called
// in the same event loop iteration and potentially processing an event that was not meant for them.
private readonly List<(Func<XEvent, bool> filter, TaskCompletionSource<XEvent?> tcs, TimeSpan? timeout)> _addedListeners = new();
private readonly List<(Func<XEvent, bool> filter, TaskCompletionSource<XEvent?> tcs, TimeSpan timeout)> _addedListeners = new();
private readonly DispatcherTimer _timeoutTimer;
private static readonly Stopwatch _time = Stopwatch.StartNew();
@ -51,6 +51,7 @@ internal class EventStreamWindow : IDisposable
private void OnEvent(ref XEvent xev)
{
Console.WriteLine(xev.type + " " + xev.SelectionEvent.property);
MergeListeners();
for (var i = 0; i < _listeners.Count; i++)
{
@ -66,18 +67,17 @@ internal class EventStreamWindow : IDisposable
public Task<XEvent?> WaitForEventAsync(Func<XEvent, bool> predicate, TimeSpan? timeout = null)
{
if (timeout.HasValue)
{
if (timeout < TimeSpan.Zero)
throw new TimeoutException();
if(timeout > TimeSpan.FromDays(1))
throw new ArgumentOutOfRangeException(nameof(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));
if(timeout.HasValue)
_timeoutTimer.Start();
_addedListeners.Add((predicate, tcs, _time.Elapsed + timeout.Value));
_timeoutTimer.Start();
return tcs.Task;
}

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

@ -297,8 +297,8 @@ namespace Avalonia.X11
var res = await session.SendFormatRequest();
if (res is null || !res.Contains(formatAtom))
return null;
return await session.SendDataRequest(formatAtom);
return ConvertData(await session.SendDataRequest(formatAtom));
}
/// <inheritdoc />

Loading…
Cancel
Save