Browse Source

[X11] Added INCR support (#18428)

* Refactor X11 clipboard to use session-based approach

* INCR client

* Implemented INCR server

* Detect INCR threshold

* missing return

* Handle review comments

---------

Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
pull/19325/head
Nikita Tsukanov 6 months ago
committed by GitHub
parent
commit
ec0edda7c3
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 152
      src/Avalonia.X11/Clipboard/ClipboardReadSession.cs
  2. 110
      src/Avalonia.X11/Clipboard/EventStreamWindow.cs
  3. 198
      src/Avalonia.X11/Clipboard/X11Clipboard.cs
  4. 2
      src/Avalonia.X11/X11Platform.cs
  5. 6
      src/Avalonia.X11/XLib.cs

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

@ -0,0 +1,152 @@
using System;
using System.Buffers;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using static Avalonia.X11.XLib;
namespace Avalonia.X11.Clipboard;
class ClipboardReadSession : IDisposable
{
private readonly AvaloniaX11Platform _platform;
private readonly EventStreamWindow _window;
private readonly X11Info _x11;
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)
: IDisposable
{
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;
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 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()
{
using var res = await ConvertSelectionAndGetProperty(_x11.Atoms.TARGETS, _x11.Atoms.TARGETS);
if (res == null)
return null;
if (res.NItems == IntPtr.Zero)
return null;
if (res.ActualFormat != 32)
return null;
else
{
var formats = new IntPtr[res.NItems.ToInt32()];
Marshal.Copy(res.Data, formats, 0, formats.Length);
return formats;
}
}
public class GetDataResult(byte[]? data, MemoryStream? stream, IntPtr actualTypeAtom)
{
public IntPtr TypeAtom => actualTypeAtom;
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)
{
using var res = await ConvertSelectionAndGetProperty(format, format);
if (res == null)
return null;
if (res.NItems == IntPtr.Zero)
return null;
if (res.ActualTypeAtom == _x11.Atoms.INCR)
{
return await ReadIncr(format);
}
else
{
var data = new byte[(int)res.NItems * (res.ActualFormat / 8)];
Marshal.Copy(res.Data, data, 0, data.Length);
return new (data, null, res.ActualTypeAtom);
}
}
}

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

@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Threading;
namespace Avalonia.X11;
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();
// 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 DispatcherTimer _timeoutTimer;
private readonly bool _isForeign;
private static readonly Stopwatch _time = Stopwatch.StartNew();
public EventStreamWindow(AvaloniaX11Platform platform, IntPtr? foreignWindow = null)
{
_platform = platform;
if (foreignWindow.HasValue)
{
_isForeign = true;
_handle = foreignWindow.Value;
_platform.Windows[_handle] = OnEvent;
}
else
_handle = XLib.CreateEventWindow(platform, OnEvent);
_timeoutTimer = new(TimeSpan.FromSeconds(1), DispatcherPriority.Background, OnTimer);
}
void MergeListeners()
{
_listeners.AddRange(_addedListeners);
_addedListeners.Clear();
}
private void OnTimer(object? sender, EventArgs eventArgs)
{
MergeListeners();
for (var i = 0; i < _listeners.Count; i++)
{
var (filter, tcs, timeout) = _listeners[i];
if (timeout < _time.Elapsed)
{
_listeners.RemoveAt(i);
i--;
tcs.SetResult(null);
}
}
if(_listeners.Count == 0)
_timeoutTimer.Stop();
}
private void OnEvent(ref XEvent xev)
{
MergeListeners();
for (var i = 0; i < _listeners.Count; i++)
{
var (filter, tcs, timeout) = _listeners[i];
if (filter(xev))
{
_listeners.RemoveAt(i);
i--;
tcs.SetResult(xev);
}
}
}
public Task<XEvent?> WaitForEventAsync(Func<XEvent, bool> predicate, TimeSpan? timeout = null)
{
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));
_timeoutTimer.Start();
return tcs.Task;
}
public void Dispose()
{
_timeoutTimer.Stop();
_platform.Windows.Remove(_handle);
if (_isForeign)
XLib.XSelectInput(_platform.Display, _handle, IntPtr.Zero);
else
XLib.XDestroyWindow(_platform.Display, _handle);
_handle = IntPtr.Zero;
var toDispose = _listeners.ToList();
toDispose.AddRange(_addedListeners);
_listeners.Clear();
_addedListeners.Clear();
foreach(var l in toDispose)
l.tcs.SetResult(null);
}
}

198
src/Avalonia.X11/X11Clipboard.cs → src/Avalonia.X11/Clipboard/X11Clipboard.cs

@ -1,27 +1,31 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.X11.Clipboard;
using static Avalonia.X11.XLib;
namespace Avalonia.X11
{
internal class X11Clipboard : IClipboard
{
private readonly AvaloniaX11Platform _platform;
private readonly X11Info _x11;
private IDataObject? _storedDataObject;
private IntPtr _handle;
private TaskCompletionSource<bool>? _storeAtomTcs;
private TaskCompletionSource<IntPtr[]?>? _requestedFormatsTcs;
private TaskCompletionSource<object?>? _requestedDataTcs;
private readonly IntPtr[] _textAtoms;
private readonly IntPtr _avaloniaSaveTargetsAtom;
private int _maximumPropertySize;
public X11Clipboard(AvaloniaX11Platform platform)
{
_platform = platform;
_x11 = platform.Info;
_handle = CreateEventWindow(platform, OnEvent);
_avaloniaSaveTargetsAtom = XInternAtom(_x11.Display, "AVALONIA_SAVE_TARGETS_PROPERTY_ATOM", false);
@ -32,13 +36,15 @@ namespace Avalonia.X11
_x11.Atoms.UTF8_STRING,
_x11.Atoms.UTF16_STRING
}.Where(a => a != IntPtr.Zero).ToArray();
}
private bool IsStringAtom(IntPtr atom)
{
return _textAtoms.Contains(atom);
var extendedMaxRequestSize = XExtendedMaxRequestSize(_platform.Display);
var maxRequestSize = XMaxRequestSize(_platform.Display);
_maximumPropertySize =
(int)Math.Min(0x100000, (extendedMaxRequestSize == IntPtr.Zero
? maxRequestSize
: extendedMaxRequestSize).ToInt64() - 0x100);
}
private Encoding? GetStringEncoding(IntPtr atom)
{
return (atom == _x11.Atoms.XA_STRING
@ -50,17 +56,17 @@ namespace Avalonia.X11
? Encoding.Unicode
: null;
}
private unsafe void OnEvent(ref XEvent ev)
{
if (ev.type == XEventName.SelectionClear)
{
{
_storeAtomTcs?.TrySetResult(true);
return;
}
if (ev.type == XEventName.SelectionRequest)
{
{
var sel = ev.SelectionRequestEvent;
var resp = new XEvent
{
@ -80,7 +86,7 @@ namespace Avalonia.X11
{
resp.SelectionEvent.property = WriteTargetToProperty(sel.target, sel.requestor, sel.property);
}
XSendEvent(_x11.Display, sel.requestor, false, new IntPtr((int)EventMask.NoEventMask), ref resp);
}
@ -94,21 +100,19 @@ namespace Avalonia.X11
_x11.Atoms.XA_ATOM, 32, PropertyMode.Replace, atoms, atoms.Length);
return property;
}
else if(target == _x11.Atoms.SAVE_TARGETS && _x11.Atoms.SAVE_TARGETS != IntPtr.Zero)
else if (target == _x11.Atoms.SAVE_TARGETS && _x11.Atoms.SAVE_TARGETS != IntPtr.Zero)
{
return property;
}
else if ((textEnc = GetStringEncoding(target)) != null
else if ((textEnc = GetStringEncoding(target)) != null
&& _storedDataObject?.Contains(DataFormats.Text) == true)
{
var text = _storedDataObject.GetText();
if(text == null)
if (text == null)
return IntPtr.Zero;
var data = textEnc.GetBytes(text);
fixed (void* pdata = data)
XChangeProperty(_x11.Display, window, property, target, 8,
PropertyMode.Replace,
pdata, data.Length);
SendDataToClient(window, property, target, data);
return property;
}
else if (target == _x11.Atoms.MULTIPLE && _x11.Atoms.MULTIPLE != IntPtr.Zero)
@ -136,11 +140,12 @@ namespace Avalonia.X11
return property;
}
else if(_x11.Atoms.GetAtomName(target) is { } atomName && _storedDataObject?.Contains(atomName) == true)
else if (_x11.Atoms.GetAtomName(target) is { } atomName &&
_storedDataObject?.Contains(atomName) == true)
{
var objValue = _storedDataObject.Get(atomName);
if(!(objValue is byte[] bytes))
if (!(objValue is byte[] bytes))
{
if (objValue is string s)
bytes = Encoding.UTF8.GetBytes(s);
@ -148,93 +153,66 @@ namespace Avalonia.X11
return IntPtr.Zero;
}
XChangeProperty(_x11.Display, window, property, target, 8,
PropertyMode.Replace,
bytes, bytes.Length);
SendDataToClient(window, property, target, bytes);
return property;
}
else
return IntPtr.Zero;
}
if (ev.type == XEventName.SelectionNotify && ev.SelectionEvent.selection == _x11.Atoms.CLIPBOARD)
{
var sel = ev.SelectionEvent;
if (sel.property == IntPtr.Zero)
{
_requestedFormatsTcs?.TrySetResult(null);
_requestedDataTcs?.TrySetResult(null);
}
XGetWindowProperty(_x11.Display, _handle, sel.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);
Encoding? textEnc;
if (nitems == IntPtr.Zero)
{
_requestedFormatsTcs?.TrySetResult(null);
_requestedDataTcs?.TrySetResult(null);
}
else
{
if (sel.property == _x11.Atoms.TARGETS)
{
if (actualFormat != 32)
_requestedFormatsTcs?.TrySetResult(null);
else
{
var formats = new IntPtr[nitems.ToInt32()];
Marshal.Copy(prop, formats, 0, formats.Length);
_requestedFormatsTcs?.TrySetResult(formats);
}
}
else if ((textEnc = GetStringEncoding(actualTypeAtom)) != null)
{
var text = textEnc.GetString((byte*)prop.ToPointer(), nitems.ToInt32());
_requestedDataTcs?.TrySetResult(text);
}
else
{
if (actualTypeAtom == _x11.Atoms.INCR)
{
// TODO: Actually implement that monstrosity
_requestedDataTcs?.TrySetResult(null);
}
else
{
var data = new byte[(int)nitems * (actualFormat / 8)];
Marshal.Copy(prop, data, 0, data.Length);
_requestedDataTcs?.TrySetResult(data);
}
}
}
XFree(prop);
}
}
private Task<IntPtr[]?> SendFormatRequest()
async void SendIncrDataToClient(IntPtr window, IntPtr property, IntPtr target, Stream data)
{
if (_requestedFormatsTcs == null || _requestedFormatsTcs.Task.IsCompleted)
_requestedFormatsTcs = new TaskCompletionSource<IntPtr[]?>();
XConvertSelection(_x11.Display, _x11.Atoms.CLIPBOARD, _x11.Atoms.TARGETS, _x11.Atoms.TARGETS, _handle,
IntPtr.Zero);
return _requestedFormatsTcs.Task;
data.Position = 0;
using var events = new EventStreamWindow(_platform, window);
using var _ = data;
XSelectInput(_x11.Display, window, new IntPtr((int)XEventMask.PropertyChangeMask));
var size = new IntPtr(data.Length);
XChangeProperty(_x11.Display, window, property, _x11.Atoms.INCR, 32, PropertyMode.Replace, ref size, 1);
var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min(_maximumPropertySize, data.Length));
while (true)
{
if (null == await events.WaitForEventAsync(x =>
x.type == XEventName.PropertyNotify && x.PropertyEvent.atom == property
&& x.PropertyEvent.state == 1, TimeSpan.FromMinutes(1)))
break;
var read = await data.ReadAsync(buffer, 0, buffer.Length);
if(read == 0)
break;
XChangeProperty(_x11.Display, window, property, target, 8, PropertyMode.Replace, buffer, read);
}
ArrayPool<byte>.Shared.Return(buffer);
// Finish the transfer
XChangeProperty(_x11.Display, window, property, target, 8, PropertyMode.Replace, IntPtr.Zero, 0);
}
private Task<object?> SendDataRequest(IntPtr format)
void SendDataToClient(IntPtr window, IntPtr property, IntPtr target, byte[] bytes)
{
if (_requestedDataTcs == null || _requestedDataTcs.Task.IsCompleted)
_requestedDataTcs = new TaskCompletionSource<object?>();
XConvertSelection(_x11.Display, _x11.Atoms.CLIPBOARD, format, format, _handle, IntPtr.Zero);
return _requestedDataTcs.Task;
if (bytes.Length < _maximumPropertySize)
{
XChangeProperty(_x11.Display, window, property, target, 8,
PropertyMode.Replace,
bytes, bytes.Length);
}
else
SendIncrDataToClient(window, property, target, new MemoryStream(bytes));
}
private bool HasOwner => XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD) != IntPtr.Zero;
private ClipboardReadSession OpenReadSession() => new(_platform);
public async Task<string?> GetTextAsync()
{
if (!HasOwner)
return null;
var res = await SendFormatRequest();
if (TryGetInProcessDataObject() is { } inProc)
return inProc.GetText();
using var session = OpenReadSession();
var res = await session.SendFormatRequest();
var target = _x11.Atoms.UTF8_STRING;
if (res != null)
{
@ -247,7 +225,17 @@ namespace Avalonia.X11
}
}
return (string?)await SendDataRequest(target);
return ConvertData(await session.SendDataRequest(target)) as string;
}
private object? ConvertData(ClipboardReadSession.GetDataResult? result)
{
if (result == null)
return null;
if (GetStringEncoding(result.TypeAtom) is { } textEncoding)
return textEncoding.GetString(result.AsBytes());
// TODO: image encoding
return result.AsBytes();
}
@ -272,6 +260,12 @@ namespace Avalonia.X11
private Task StoreAtomsInClipboardManager(IDataObject data)
{
// Skip storing atoms if the data object contains any non-trivial formats or trivial formats are too big
if (data.GetDataFormats().Any(f => f != DataFormats.Text)
|| data.GetText()?.Length * 2 > 64 * 1024
)
return Task.CompletedTask;
if (_x11.Atoms.CLIPBOARD_MANAGER != IntPtr.Zero && _x11.Atoms.SAVE_TARGETS != IntPtr.Zero)
{
var clipboardManager = XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD_MANAGER);
@ -314,19 +308,24 @@ namespace Avalonia.X11
return StoreAtomsInClipboardManager(data);
}
public Task<IDataObject?> TryGetInProcessDataObjectAsync()
private IDataObject? TryGetInProcessDataObject()
{
if (XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD) == _handle)
return Task.FromResult(_storedDataObject);
return Task.FromResult<IDataObject?>(null);
return _storedDataObject;
return null;
}
public Task<IDataObject?> TryGetInProcessDataObjectAsync() => Task.FromResult(TryGetInProcessDataObject());
public async Task<string[]> GetFormatsAsync()
{
if (!HasOwner)
return [];
var res = await SendFormatRequest();
if (TryGetInProcessDataObject() is { } inProc)
return inProc.GetDataFormats().ToArray();
using var session = OpenReadSession();
var res = await session.SendFormatRequest();
if (res == null)
return [];
@ -347,15 +346,20 @@ namespace Avalonia.X11
{
if (!HasOwner)
return null;
if(TryGetInProcessDataObject() is {} inProc)
return inProc.Get(format);
if (format == DataFormats.Text)
return await GetTextAsync();
var formatAtom = _x11.Atoms.GetAtom(format);
var res = await SendFormatRequest();
using var session = OpenReadSession();
var res = await session.SendFormatRequest();
if (res is null || !res.Contains(formatAtom))
return null;
return await SendDataRequest(formatAtom);
return ConvertData(await session.SendDataRequest(formatAtom));
}
/// <inheritdoc />

2
src/Avalonia.X11/X11Platform.cs

@ -81,7 +81,7 @@ namespace Avalonia.X11
.Bind<KeyGestureFormatInfo>().ToConstant(new KeyGestureFormatInfo(new Dictionary<Key, string>() { }, meta: "Super"))
.Bind<IKeyboardDevice>().ToFunc(() => KeyboardDevice)
.Bind<ICursorFactory>().ToConstant(new X11CursorFactory(Display))
.Bind<IClipboard>().ToConstant(new X11Clipboard(this))
.Bind<IClipboard>().ToLazy(() => new X11Clipboard(this))
.Bind<IPlatformSettings>().ToSingleton<DBusPlatformSettings>()
.Bind<IPlatformIconLoader>().ToConstant(new X11IconLoader())
.Bind<IMountedVolumeInfoProvider>().ToConstant(new LinuxMountedVolumeInfoProvider())

6
src/Avalonia.X11/XLib.cs

@ -559,6 +559,12 @@ namespace Avalonia.X11
[DllImport(libX11)]
public static extern void XFreeEventData(IntPtr display, void* cookie);
[DllImport(libX11)]
public static extern IntPtr XMaxRequestSize(IntPtr display);
[DllImport(libX11)]
public static extern IntPtr XExtendedMaxRequestSize(IntPtr display);
[DllImport(libX11Randr)]
public static extern int XRRQueryExtension (IntPtr dpy,
out int event_base_return,

Loading…
Cancel
Save