Browse Source
* 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>
(cherry picked from commit ec0edda7c3)
release/11.3.7
committed by
Julien Lebosquain
5 changed files with 370 additions and 98 deletions
@ -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); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue