diff --git a/src/Avalonia.X11/Selections/DragDrop/DragDropDataProvider.cs b/src/Avalonia.X11/Selections/DragDrop/DragDropDataProvider.cs index d01cf8a6bc..c8727f0c87 100644 --- a/src/Avalonia.X11/Selections/DragDrop/DragDropDataProvider.cs +++ b/src/Avalonia.X11/Selections/DragDrop/DragDropDataProvider.cs @@ -5,11 +5,11 @@ namespace Avalonia.X11.Selections.DragDrop; internal sealed class DragDropDataProvider : SelectionDataProvider { + public Action? Activity { get; set; } + public DragDropDataProvider(AvaloniaX11Platform platform, IAsyncDataTransfer dataTransfer) : base(platform, platform.Info.Atoms.XdndSelection) - { - DataTransfer = dataTransfer; - } + => DataTransfer = dataTransfer; public new IntPtr GetOwner() => base.GetOwner(); @@ -17,8 +17,13 @@ internal sealed class DragDropDataProvider : SelectionDataProvider public new void SetOwner(IntPtr window) => base.SetOwner(window); + protected override void OnActivity() + => Activity?.Invoke(); + public override void Dispose() { + Activity = null; + DataTransfer?.Dispose(); DataTransfer = null; } diff --git a/src/Avalonia.X11/Selections/DragDrop/DragDropTimeoutManager.cs b/src/Avalonia.X11/Selections/DragDrop/DragDropTimeoutManager.cs new file mode 100644 index 0000000000..673eb49ec4 --- /dev/null +++ b/src/Avalonia.X11/Selections/DragDrop/DragDropTimeoutManager.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading; + +namespace Avalonia.X11.Selections.DragDrop; + +internal sealed class DragDropTimeoutManager : IDisposable +{ + private readonly TimeSpan _timeout; + private readonly Timer _timer; + + public DragDropTimeoutManager(TimeSpan timeout, Action onTimeout) + { + _timeout = timeout; + + _timer = new Timer( + static state => ((Action)state!)(), + onTimeout, + Timeout.InfiniteTimeSpan, + Timeout.InfiniteTimeSpan); + } + + public void Restart() + { + try + { + _timer.Change(_timeout, Timeout.InfiniteTimeSpan); + } + catch (ObjectDisposedException) + { + } + } + + public void Dispose() + => _timer.Dispose(); +} diff --git a/src/Avalonia.X11/Selections/DragDrop/X11DragSource.cs b/src/Avalonia.X11/Selections/DragDrop/X11DragSource.cs index 1fe1fdfc42..52d7ad5095 100644 --- a/src/Avalonia.X11/Selections/DragDrop/X11DragSource.cs +++ b/src/Avalonia.X11/Selections/DragDrop/X11DragSource.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Input; @@ -49,6 +50,7 @@ internal sealed class X11DragSource(AvaloniaX11Platform platform) : IPlatformDra private bool _pointerGrabbed; private XdndTargetInfo? _lastTarget; private DragDropEffects _currentEffects; + private DragDropTimeoutManager? _timeoutManager; public Handler( AvaloniaX11Platform platform, @@ -68,7 +70,7 @@ internal sealed class X11DragSource(AvaloniaX11Platform platform) : IPlatformDra if (!platform.Windows.TryGetValue(sourceWindow, out var sourceWindowInfo)) { _formatAtoms = []; - _completionSource.TrySetResult(DragDropEffects.None); + Complete(DragDropEffects.None); return; } @@ -92,7 +94,7 @@ internal sealed class X11DragSource(AvaloniaX11Platform platform) : IPlatformDra if (grabResult != GrabResult.GrabSuccess) { _formatAtoms = []; - _completionSource.TrySetResult(DragDropEffects.None); + Complete(DragDropEffects.None); return; } @@ -198,11 +200,22 @@ internal sealed class X11DragSource(AvaloniaX11Platform platform) : IPlatformDra var rootPosition = new PixelPoint(button.x_root, button.y_root); ProcessRawDragEvent(lastTarget.InProcessWindow, RawDragEventType.Drop, rootPosition); _lastTarget = null; - _completionSource.TrySetResult(_currentEffects); + Complete(_currentEffects); } else { _dataProvider.SetOwner(_sourceWindow); + + Debug.Assert(_timeoutManager is null); + + _timeoutManager = new DragDropTimeoutManager(SelectionHelper.Timeout, () => + { + DisposeTimeoutManager(); + Complete(DragDropEffects.None); + }); + + _dataProvider.Activity = _timeoutManager.Restart; + SendXdndDrop(lastTarget, button.time); } } @@ -224,6 +237,7 @@ internal sealed class X11DragSource(AvaloniaX11Platform platform) : IPlatformDra return; _lastTarget = null; + DisposeTimeoutManager(); if (lastTarget.Version >= 5) { @@ -233,7 +247,7 @@ internal sealed class X11DragSource(AvaloniaX11Platform platform) : IPlatformDra UpdateCurrentEffects(effects & _allowedEffects); } - _completionSource.TrySetResult(_currentEffects); + Complete(_currentEffects); } private XdndTargetInfo? FindXdndTarget(PixelPoint rootPosition) @@ -433,6 +447,16 @@ internal sealed class X11DragSource(AvaloniaX11Platform platform) : IPlatformDra return _platform.Info.DefaultCursor; } + private void Complete(DragDropEffects resultEffects) + => _completionSource.TrySetResult(resultEffects); + + private void DisposeTimeoutManager() + { + _dataProvider.Activity = null; + _timeoutManager?.Dispose(); + _timeoutManager = null; + } + public void Dispose() { if (_pointerGrabbed) @@ -448,6 +472,8 @@ internal sealed class X11DragSource(AvaloniaX11Platform platform) : IPlatformDra _lastTarget = null; _currentEffects = DragDropEffects.None; + DisposeTimeoutManager(); + if (_dataProvider.GetOwner() == _sourceWindow) _dataProvider.SetOwner(0); diff --git a/src/Avalonia.X11/Selections/SelectionDataProvider.cs b/src/Avalonia.X11/Selections/SelectionDataProvider.cs index d3642e58e1..c1a7be87fb 100644 --- a/src/Avalonia.X11/Selections/SelectionDataProvider.cs +++ b/src/Avalonia.X11/Selections/SelectionDataProvider.cs @@ -63,6 +63,7 @@ internal abstract class SelectionDataProvider : IDisposable } XSendEvent(Platform.Display, request.requestor, false, new IntPtr((int)EventMask.NoEventMask), ref response); + OnActivity(); IntPtr WriteTargetToProperty(IntPtr target, IntPtr window, IntPtr property) { @@ -196,6 +197,7 @@ internal abstract class SelectionDataProvider : IDisposable ref size, 1); var buffer = ArrayPool.Shared.Rent((int)Math.Min(_maximumPropertySize, data.Length)); + var gotTimeout = false; while (true) { @@ -204,22 +206,29 @@ internal abstract class SelectionDataProvider : IDisposable x.type == XEventName.PropertyNotify && x.PropertyEvent.atom == property && x.PropertyEvent.state == 1, - TimeSpan.FromMinutes(1)); + SelectionHelper.Timeout); if (evt is null) + { + gotTimeout = true; break; + } var read = await data.ReadAsync(buffer, 0, buffer.Length); if (read == 0) break; XChangeProperty(Platform.Display, window, property, target, 8, PropertyMode.Replace, buffer, read); + OnActivity(); } ArrayPool.Shared.Return(buffer); // Finish the transfer XChangeProperty(Platform.Display, window, property, target, 8, PropertyMode.Replace, 0, 0); + + if (!gotTimeout) + OnActivity(); } private Task SendDataToClientAsync(IntPtr window, IntPtr property, IntPtr target, byte[] bytes) @@ -228,6 +237,7 @@ internal abstract class SelectionDataProvider : IDisposable { XChangeProperty(Platform.Display, window, property, target, 8, PropertyMode.Replace, bytes, bytes.Length); + OnActivity(); return Task.CompletedTask; } @@ -242,5 +252,9 @@ internal abstract class SelectionDataProvider : IDisposable return [atoms.TARGETS, atoms.MULTIPLE, ..formatAtoms]; } + protected virtual void OnActivity() + { + } + public abstract void Dispose(); } diff --git a/src/Avalonia.X11/Selections/SelectionHelper.cs b/src/Avalonia.X11/Selections/SelectionHelper.cs new file mode 100644 index 0000000000..1a6cd81f8c --- /dev/null +++ b/src/Avalonia.X11/Selections/SelectionHelper.cs @@ -0,0 +1,8 @@ +using System; + +namespace Avalonia.X11.Selections; + +internal static class SelectionHelper +{ + public static TimeSpan Timeout { get; } = TimeSpan.FromSeconds(5); +} diff --git a/src/Avalonia.X11/Selections/SelectionReadSession.cs b/src/Avalonia.X11/Selections/SelectionReadSession.cs index 5123244691..207af91309 100644 --- a/src/Avalonia.X11/Selections/SelectionReadSession.cs +++ b/src/Avalonia.X11/Selections/SelectionReadSession.cs @@ -42,7 +42,7 @@ internal sealed class SelectionReadSession( ev.SelectionEvent.requestor == window && ev.SelectionEvent.selection == selection && ev.SelectionEvent.property == property, - TimeSpan.FromSeconds(5)); + SelectionHelper.Timeout); if (ev == null) return null; @@ -108,7 +108,7 @@ internal sealed class SelectionReadSession( x => x is { type: XEventName.PropertyNotify, PropertyEvent.state: 0 } && x.PropertyEvent.window == window && x.PropertyEvent.atom == property, - TimeSpan.FromSeconds(5)); + SelectionHelper.Timeout); if (ev == null) return null;