From 6aa2d438c9056b2b72e93be00bb2779a63a9a01d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 12 Mar 2025 21:58:56 +0500 Subject: [PATCH] INCR client --- .../Clipboard/ClipboardReadSession.cs | 132 ++++++++++++------ .../Clipboard/EventStreamWindow.cs | 24 ++-- src/Avalonia.X11/Clipboard/X11Clipboard.cs | 4 +- 3 files changed, 101 insertions(+), 59 deletions(-) diff --git a/src/Avalonia.X11/Clipboard/ClipboardReadSession.cs b/src/Avalonia.X11/Clipboard/ClipboardReadSession.cs index 3241cc9d47..7f964e8007 100644 --- a/src/Avalonia.X11/Clipboard/ClipboardReadSession.cs +++ b/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 + 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 ConvertSelectionAndGetProperty( + IntPtr target, IntPtr property) + { + XConvertSelection(_platform.Display, _x11.Atoms.CLIPBOARD, target, property, _window.Handle, + IntPtr.Zero); + return WaitForSelectionNotifyAndGetProperty(property); } public async Task 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 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.Shared.Rent(len); + Marshal.Copy(res.Data, data, 0, len); + ms.Write(data, 0, len); + ArrayPool.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 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); } + } } \ No newline at end of file diff --git a/src/Avalonia.X11/Clipboard/EventStreamWindow.cs b/src/Avalonia.X11/Clipboard/EventStreamWindow.cs index c2842d38fa..1ae9fcaab5 100644 --- a/src/Avalonia.X11/Clipboard/EventStreamWindow.cs +++ b/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 filter, TaskCompletionSource tcs, TimeSpan? timeout)> _listeners = new(); + private readonly List<(Func filter, TaskCompletionSource 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 filter, TaskCompletionSource tcs, TimeSpan? timeout)> _addedListeners = new(); + private readonly List<(Func filter, TaskCompletionSource 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 WaitForEventAsync(Func 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(); - _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; } diff --git a/src/Avalonia.X11/Clipboard/X11Clipboard.cs b/src/Avalonia.X11/Clipboard/X11Clipboard.cs index 6773eff2f9..98c5d21338 100644 --- a/src/Avalonia.X11/Clipboard/X11Clipboard.cs +++ b/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)); } ///