diff --git a/src/Avalonia.X11/Clipboard/EventStreamWindow.cs b/src/Avalonia.X11/Clipboard/EventStreamWindow.cs index 1ae9fcaab5..3364280537 100644 --- a/src/Avalonia.X11/Clipboard/EventStreamWindow.cs +++ b/src/Avalonia.X11/Clipboard/EventStreamWindow.cs @@ -17,12 +17,21 @@ internal class EventStreamWindow : IDisposable // 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 DispatcherTimer _timeoutTimer; + private readonly bool _isForeign; private static readonly Stopwatch _time = Stopwatch.StartNew(); - public EventStreamWindow(AvaloniaX11Platform platform) + public EventStreamWindow(AvaloniaX11Platform platform, IntPtr? foreignWindow = null) { _platform = platform; - _handle = XLib.CreateEventWindow(platform, OnEvent); + 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); } @@ -51,7 +60,6 @@ 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++) { @@ -83,7 +91,12 @@ internal class EventStreamWindow : IDisposable public void Dispose() { - XLib.XDestroyWindow(_platform.Display, _handle); + _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); diff --git a/src/Avalonia.X11/Clipboard/X11Clipboard.cs b/src/Avalonia.X11/Clipboard/X11Clipboard.cs index 98c5d21338..4974b338bb 100644 --- a/src/Avalonia.X11/Clipboard/X11Clipboard.cs +++ b/src/Avalonia.X11/Clipboard/X11Clipboard.cs @@ -1,5 +1,7 @@ using System; +using System.Buffers; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; @@ -101,10 +103,8 @@ namespace Avalonia.X11 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) @@ -145,9 +145,7 @@ 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 @@ -156,6 +154,46 @@ namespace Avalonia.X11 } + private const int IncrChunkSize = 0x10;//0000; // Change to 0x10 or smth for debugging + private const int IncrThreshold = 0x20;//0000; // Change to 0x10 or smth for debugging + + async void SendIncrDataToClient(IntPtr window, IntPtr property, IntPtr target, Stream data) + { + 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.Shared.Rent((int)Math.Min(IncrChunkSize, 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.Shared.Return(buffer); + + // Finish the transfer + XChangeProperty(_x11.Display, window, property, target, 8, PropertyMode.Replace, IntPtr.Zero, 0); + } + + void SendDataToClient(IntPtr window, IntPtr property, IntPtr target, byte[] bytes) + { + if (bytes.Length < IncrThreshold) // change to 0 to debug INCR + { + XChangeProperty(_x11.Display, window, property, target, 8, + PropertyMode.Replace, + bytes, bytes.Length); + } + SendIncrDataToClient(window, property, target, new MemoryStream(bytes)); + } + private bool HasOwner => XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD) != IntPtr.Zero; private ClipboardReadSession OpenReadSession() => new(_platform); @@ -164,6 +202,9 @@ namespace Avalonia.X11 { if (!HasOwner) return null; + if (TryGetInProcessDataObject() is { } inProc) + return inProc.GetText(); + using var session = OpenReadSession(); var res = await session.SendFormatRequest(); var target = _x11.Atoms.UTF8_STRING; @@ -255,18 +296,22 @@ namespace Avalonia.X11 return StoreAtomsInClipboardManager(data); } - public Task TryGetInProcessDataObjectAsync() + private IDataObject? TryGetInProcessDataObject() { if (XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD) == _handle) - return Task.FromResult(_storedDataObject); - return Task.FromResult(null); + return _storedDataObject; + return null; } + public Task TryGetInProcessDataObjectAsync() => Task.FromResult(TryGetInProcessDataObject()); + public async Task GetFormatsAsync() { if (!HasOwner) return []; - + if (TryGetInProcessDataObject() is { } inProc) + inProc.GetDataFormats(); + using var session = OpenReadSession(); var res = await session.SendFormatRequest(); if (res == null) @@ -289,6 +334,10 @@ namespace Avalonia.X11 { if (!HasOwner) return null; + + if(TryGetInProcessDataObject() is {} inProc) + return inProc.Get(format); + if (format == DataFormats.Text) return await GetTextAsync();