diff --git a/src/Avalonia.Base/Input/AsyncDataTransferExtensions.cs b/src/Avalonia.Base/Input/AsyncDataTransferExtensions.cs index cf9d320310..a71a1e744d 100644 --- a/src/Avalonia.Base/Input/AsyncDataTransferExtensions.cs +++ b/src/Avalonia.Base/Input/AsyncDataTransferExtensions.cs @@ -27,6 +27,9 @@ public static class AsyncDataTransferExtensions return new AsyncToSyncDataTransfer(asyncDataTransfer); } + internal static IAsyncDataTransfer ToAsynchronous(this IDataTransfer dataTransfer) + => dataTransfer as IAsyncDataTransfer ?? new SyncToAsyncDataTransfer(dataTransfer); + /// /// Gets whether a supports a specific format. /// diff --git a/src/Avalonia.Base/Input/SyncToAsyncDataTransfer.cs b/src/Avalonia.Base/Input/SyncToAsyncDataTransfer.cs new file mode 100644 index 0000000000..c228e8b8db --- /dev/null +++ b/src/Avalonia.Base/Input/SyncToAsyncDataTransfer.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; + +namespace Avalonia.Input; + +/// +/// Wraps a into a . +/// +/// The sync object to wrap. +internal sealed class SyncToAsyncDataTransfer(IDataTransfer dataTransfer) + : IDataTransfer, IAsyncDataTransfer +{ + private SyncToAsyncDataTransferItem[]? _items; + + public IReadOnlyList Formats + => dataTransfer.Formats; + + public IReadOnlyList Items + => _items ??= ProvideItems(); + + IReadOnlyList IDataTransfer.Items + => dataTransfer.Items; + + IReadOnlyList IAsyncDataTransfer.Items + => Items; + + private SyncToAsyncDataTransferItem[] ProvideItems() + { + var asyncItems = dataTransfer.Items; + var count = asyncItems.Count; + var syncItems = new SyncToAsyncDataTransferItem[count]; + + for (var i = 0; i < count; ++i) + { + var asyncItem = asyncItems[i]; + syncItems[i] = new SyncToAsyncDataTransferItem(asyncItem); + } + + return syncItems; + } + + public void Dispose() + => dataTransfer.Dispose(); +} diff --git a/src/Avalonia.Base/Input/SyncToAsyncDataTransferItem.cs b/src/Avalonia.Base/Input/SyncToAsyncDataTransferItem.cs new file mode 100644 index 0000000000..705ce0bdee --- /dev/null +++ b/src/Avalonia.Base/Input/SyncToAsyncDataTransferItem.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Avalonia.Input; + +/// +/// Wraps a into a . +/// +/// The sync item to wrap. +internal sealed class SyncToAsyncDataTransferItem(IDataTransferItem dataTransferItem) + : IDataTransferItem, IAsyncDataTransferItem +{ + public IReadOnlyList Formats + => dataTransferItem.Formats; + + public object? TryGetRaw(DataFormat format) + => dataTransferItem.TryGetRaw(format); + + public Task TryGetRawAsync(DataFormat format) + => Task.FromResult(dataTransferItem.TryGetRaw(format)); +} diff --git a/src/Avalonia.X11/Selections/Clipboard/X11ClipboardImpl.cs b/src/Avalonia.X11/Selections/Clipboard/X11ClipboardImpl.cs index 61efa3c0d0..a322b1c3a4 100644 --- a/src/Avalonia.X11/Selections/Clipboard/X11ClipboardImpl.cs +++ b/src/Avalonia.X11/Selections/Clipboard/X11ClipboardImpl.cs @@ -17,7 +17,7 @@ internal sealed class X11ClipboardImpl(AvaloniaX11Platform platform) if (ev.type == XEventName.SelectionClear) { // We night have already regained the clipboard ownership by the time a SelectionClear message arrives. - if (GetOwner() != Handle) + if (GetOwner() != Window) DataTransfer = null; _storeAtomTcs?.TrySetResult(); @@ -67,11 +67,11 @@ internal sealed class X11ClipboardImpl(AvaloniaX11Platform platform) var atomValues = ConvertDataTransfer(dataTransfer); - XChangeProperty(Platform.Display, Handle, atoms.AVALONIA_SAVE_TARGETS_PROPERTY_ATOM, atoms.ATOM, 32, + XChangeProperty(Platform.Display, Window, atoms.AVALONIA_SAVE_TARGETS_PROPERTY_ATOM, atoms.ATOM, 32, PropertyMode.Replace, atomValues, atomValues.Length); XConvertSelection(Platform.Display, atoms.CLIPBOARD_MANAGER, atoms.SAVE_TARGETS, - atoms.AVALONIA_SAVE_TARGETS_PROPERTY_ATOM, Handle, 0); + atoms.AVALONIA_SAVE_TARGETS_PROPERTY_ATOM, Window, 0); await _storeAtomTcs.Task; } @@ -90,7 +90,7 @@ internal sealed class X11ClipboardImpl(AvaloniaX11Platform platform) if (owner == 0) return null; - if (owner == Handle && DataTransfer is { } storedDataTransfer) + if (owner == Window && DataTransfer is { } storedDataTransfer) return storedDataTransfer; // Get the formats while we're in an async method, since IAsyncDataTransfer.GetFormats() is synchronous. @@ -115,10 +115,10 @@ internal sealed class X11ClipboardImpl(AvaloniaX11Platform platform) public Task SetDataAsync(IAsyncDataTransfer dataTransfer) { DataTransfer = dataTransfer; - SetOwner(Handle); + SetOwner(Window); return StoreAtomsInClipboardManager(dataTransfer); } public Task IsCurrentOwnerAsync() - => Task.FromResult(GetOwner() == Handle); + => Task.FromResult(GetOwner() == Window); } diff --git a/src/Avalonia.X11/Selections/DragDrop/DragDropDataProvider.cs b/src/Avalonia.X11/Selections/DragDrop/DragDropDataProvider.cs new file mode 100644 index 0000000000..7c4a822b95 --- /dev/null +++ b/src/Avalonia.X11/Selections/DragDrop/DragDropDataProvider.cs @@ -0,0 +1,15 @@ +using Avalonia.Input; + +namespace Avalonia.X11.Selections.DragDrop; + +internal sealed class DragDropDataProvider : SelectionDataProvider +{ + public DragDropDataProvider(AvaloniaX11Platform platform, IAsyncDataTransfer dataTransfer) + : base(platform, platform.Info.Atoms.XdndSelection) + { + DataTransfer = dataTransfer; + } + + public void SetAsOwner() + => SetOwner(Window); +} diff --git a/src/Avalonia.X11/Selections/DragDrop/X11DragSource.cs b/src/Avalonia.X11/Selections/DragDrop/X11DragSource.cs index cd1e1e4730..28fbf80f34 100644 --- a/src/Avalonia.X11/Selections/DragDrop/X11DragSource.cs +++ b/src/Avalonia.X11/Selections/DragDrop/X11DragSource.cs @@ -1,10 +1,12 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Platform; using Avalonia.Threading; +using static Avalonia.X11.Selections.DragDrop.XdndConstants; using static Avalonia.X11.XLib; namespace Avalonia.X11.Selections.DragDrop; @@ -29,7 +31,7 @@ internal sealed class X11DragSource(AvaloniaX11Platform platform) : IPlatformDra var cursorFactory = AvaloniaLocator.Current.GetService(); - using var handler = new Handler(platform, window.Handle, cursorFactory); + using var handler = new Handler(platform, window.Handle, dataTransfer.ToAsynchronous(), allowedEffects, cursorFactory); await handler.Completion; return DragDropEffects.None; @@ -39,21 +41,30 @@ internal sealed class X11DragSource(AvaloniaX11Platform platform) : IPlatformDra { private readonly AvaloniaX11Platform _platform; private readonly IntPtr _sourceWindow; - private readonly TaskCompletionSource _completionSource = new(); - private readonly X11EventDispatcher.EventHandler? _originalEventHandler; + private readonly IAsyncDataTransfer _dataTransfer; + private readonly DragDropEffects _allowedEffects; + private readonly TaskCompletionSource _completionSource = new(); + private X11EventDispatcher.EventHandler? _originalEventHandler; + private bool _pointerGrabbed; private XdndTargetInfo? _lastTarget; - - public Task Completion - => _completionSource.Task; - - public Handler(AvaloniaX11Platform platform, IntPtr sourceWindow, ICursorFactory? cursorFactory) + private DragDropDataProvider? _dataProvider; + private IntPtr _lastStatusAction; + + public Handler( + AvaloniaX11Platform platform, + IntPtr sourceWindow, + IAsyncDataTransfer dataTransfer, + DragDropEffects allowedEffects, + ICursorFactory? cursorFactory) { _platform = platform; _sourceWindow = sourceWindow; + _dataTransfer = dataTransfer; + _allowedEffects = allowedEffects; if (!platform.Windows.TryGetValue(sourceWindow, out _originalEventHandler)) { - _completionSource.TrySetResult(); + _completionSource.TrySetResult(DragDropEffects.None); return; } @@ -80,37 +91,167 @@ internal sealed class X11DragSource(AvaloniaX11Platform platform) : IPlatformDra { Console.WriteLine($"Could not grab: {grabResult}"); - _completionSource.TrySetResult(); + _completionSource.TrySetResult(DragDropEffects.None); return; } Console.WriteLine("Grabbed!"); + _pointerGrabbed = true; _platform.Windows[_sourceWindow] = OnEvent; } + public Task Completion + => _completionSource.Task; + + private IntPtr[] DataFormatAtoms + { + get + { + return field ??= CalcDataFormatAtoms(); + + IntPtr[] CalcDataFormatAtoms() + { + var atomValues = new List(_dataTransfer.Formats.Count); + + foreach (var format in _dataTransfer.Formats) + { + foreach (var atom in DataFormatHelper.ToAtoms(format, _platform.Info.Atoms)) + atomValues.Add(atom); + } + + return atomValues.ToArray(); + } + } + } + + private DragDropDataProvider DataProvider + { + get + { + return _dataProvider ??= CreateDataProvider(); + + DragDropDataProvider CreateDataProvider() + { + var dataProvider = new DragDropDataProvider(_platform, _dataTransfer); + var formats = DataFormatAtoms; + + XChangeProperty( + _platform.Display, + dataProvider.Window, + _platform.Info.Atoms.XdndTypeList, + _platform.Info.Atoms.ATOM, + 32, + PropertyMode.Replace, + formats, + formats.Length); + + return dataProvider; + } + } + } + private void OnEvent(ref XEvent xev) { - Console.WriteLine($"Event on drag window: {xev.type}"); + if (xev.type == XEventName.MotionNotify && _pointerGrabbed) + OnMotionNotify(in xev.MotionEvent); + + else if (xev.type == XEventName.ButtonRelease && _pointerGrabbed) + OnButtonRelease(in xev.ButtonEvent); + + else if (xev.type == XEventName.ClientMessage) + { + ref var message = ref xev.ClientMessageEvent; + var atoms = _platform.Info.Atoms; + + if (message.message_type == atoms.XdndStatus) + OnXdndStatus(in message); + else if (message.message_type == atoms.XdndFinished) + OnXdndFinished(in message); + else + _originalEventHandler?.Invoke(ref xev); + } + + else + _originalEventHandler?.Invoke(ref xev); + } + + private void OnMotionNotify(in XMotionEvent motion) + { + var target = FindXdndTarget(motion.x_root, motion.y_root); - if (xev.type == XEventName.MotionNotify) + if (_lastTarget != target) { - var m = xev.MotionEvent; - var target = FindXdndTarget(m.x_root, m.y_root); + if (_lastTarget is { } lastTarget) + SendXdndLeave(lastTarget); + + _lastTarget = target; - Console.WriteLine($"Motion on 0x{m.subwindow:x} at {m.x},{m.y} (root={m.x_root},{m.y_root}); XDND: win=0x{target?.MessageWindow:x}; version={target?.Version}"); + if (target is { } newTarget) + SendXdndEnter(newTarget); } - else if (xev.type == XEventName.ButtonRelease) + if (target is { } currentTarget) { - Console.WriteLine("Completing"); - _completionSource.TrySetResult(); + var action = XdndActionHelper.EffectsToAction(_allowedEffects, _platform.Info.Atoms); + SendXdndPosition(currentTarget, motion.x_root, motion.y_root, motion.time, action); + } + + Console.WriteLine($"Motion on 0x{motion.subwindow:x} at {motion.x},{motion.y} (root={motion.x_root},{motion.y_root}); XDND: win=0x{target?.MessageWindow:x}; version={target?.Version}"); + } + + private void OnButtonRelease(in XButtonEvent button) + { + UngrabPointer(); + + if (_lastTarget is { } lastTarget) + SendXdndDrop(lastTarget, button.time); + } + + private void OnXdndStatus(in XClientMessageEvent message) + { + Console.WriteLine("Received XdndStatus"); + + if (_lastTarget is null) + return; + + var targetWindow = message.ptr1; + if (targetWindow == 0 || targetWindow != _dataProvider?.Window) + return; + + var accepted = (message.ptr2 & 1) == 1; + _lastStatusAction = accepted ? message.ptr5 : 0; + + Console.WriteLine($"Status updated: {XdndActionHelper.ActionToEffects(_lastStatusAction, _platform.Info.Atoms)}"); + } + + private void OnXdndFinished(in XClientMessageEvent message) + { + Console.WriteLine("Received XdndFinished"); + + if (_lastTarget is not { } lastTarget) + return; + + var targetWindow = message.ptr1; + if (targetWindow == 0 || targetWindow != _dataProvider?.Window) + return; + + _lastTarget = null; + + IntPtr action; + if (lastTarget.Version >= 5) + { + var accepted = (message.ptr2 & 1) == 1; + action = accepted ? message.ptr3 : 0; } else - _originalEventHandler!(ref xev); + action = _lastStatusAction; + + var effects = XdndActionHelper.ActionToEffects(action, _platform.Info.Atoms); + _completionSource.TrySetResult(effects); } - private XdndTargetInfo? FindXdndTarget(int rootX, int rootY) + private XdndTargetInfo? FindXdndTarget(int x, int y) { var display = _platform.Display; var rootWindow = _platform.Info.RootWindow; @@ -121,7 +262,7 @@ internal sealed class X11DragSource(AvaloniaX11Platform platform) : IPlatformDra if (TryGetXdndTargetInfo(currentWindow) is { } info) return info; - if (!XTranslateCoordinates(display, rootWindow, currentWindow, rootX, rootY, out _, out _, out var childWindow)) + if (!XTranslateCoordinates(display, rootWindow, currentWindow, x, y, out _, out _, out var childWindow)) return null; currentWindow = childWindow; @@ -162,13 +303,80 @@ internal sealed class X11DragSource(AvaloniaX11Platform platform) : IPlatformDra var atoms = _platform.Info.Atoms; var version = XGetWindowPropertyAsIntPtr(_platform.Display, window, atoms.XdndAware, atoms.ATOM) ?? 0; - if (version is < XdndConstants.MinXdndVersion or > byte.MaxValue) + if (version is < MinXdndVersion or > byte.MaxValue) version = 0; return (byte)version; } private void SendXdndEnter(XdndTargetInfo target) + { + Console.WriteLine("Sending XdndEnter"); + + var version = Math.Min(target.Version, XdndVersion); + var formats = DataFormatAtoms; + var hasMoreFormats = formats.Length > 3; + + SendXdndMessage( + _platform.Info.Atoms.XdndEnter, + target, + DataProvider.Window, + (version << 24) | (hasMoreFormats ? 1 : 0), + formats.Length >= 1 ? formats[0] : 0, + formats.Length >= 2 ? formats[1] : 0, + formats.Length >= 3 ? formats[2] : 0); + } + + private void SendXdndPosition(XdndTargetInfo target, int x, int y, IntPtr timestamp, IntPtr action) + { + Console.WriteLine("Sending XdndPosition"); + + SendXdndMessage( + _platform.Info.Atoms.XdndPosition, + target, + DataProvider.Window, + 0, + (x << 16) | y, + timestamp, + action); + } + + private void SendXdndLeave(XdndTargetInfo target) + { + Console.WriteLine("Sending XdndLeave"); + + SendXdndMessage( + _platform.Info.Atoms.XdndLeave, + target, + DataProvider.Window, + 0, + 0, + 0, + 0); + } + + private void SendXdndDrop(XdndTargetInfo target, IntPtr timestamp) + { + Console.WriteLine("Sending XdndDrop"); + + SendXdndMessage( + _platform.Info.Atoms.XdndDrop, + target, + DataProvider.Window, + 0, + timestamp, + 0, + 0); + } + + private void SendXdndMessage( + IntPtr messageType, + XdndTargetInfo target, + IntPtr sourceWindow, + IntPtr ptr2, + IntPtr ptr3, + IntPtr ptr4, + IntPtr ptr5) { var evt = new XEvent { @@ -177,13 +385,13 @@ internal sealed class X11DragSource(AvaloniaX11Platform platform) : IPlatformDra type = XEventName.ClientMessage, display = _platform.Display, window = target.MessageWindow, - message_type = _platform.Info.Atoms.XdndEnter, + message_type = messageType, format = 32, - ptr1 = target.TargetWindow, - ptr2 = 0, - ptr3 = 0, - ptr4 = 0, - ptr5 = 0 + ptr1 = sourceWindow, + ptr2 = ptr2, + ptr3 = ptr3, + ptr4 = ptr4, + ptr5 = ptr5 } }; @@ -191,19 +399,35 @@ internal sealed class X11DragSource(AvaloniaX11Platform platform) : IPlatformDra XFlush(_platform.Display); } - public void Dispose() + private void UngrabPointer() { + _pointerGrabbed = false; + XUngrabPointer(_platform.Display, 0); + XFlush(_platform.Display); + } + + public void Dispose() + { + if (_pointerGrabbed) + UngrabPointer(); if (_originalEventHandler is not null && _platform.Windows.ContainsKey(_sourceWindow)) + { _platform.Windows[_sourceWindow] = _originalEventHandler; - } + _originalEventHandler = null; + } - private readonly struct XdndTargetInfo(byte version, IntPtr targetWindow, IntPtr messageWindow) - { - public readonly byte Version = version; - public readonly IntPtr TargetWindow = targetWindow; - public readonly IntPtr MessageWindow = messageWindow; + _lastTarget = null; + _lastStatusAction = 0; + + _dataProvider?.Dispose(); + _dataProvider = null; } + + private readonly record struct XdndTargetInfo( + byte Version, + IntPtr TargetWindow, + IntPtr MessageWindow); } } diff --git a/src/Avalonia.X11/Selections/DragDrop/X11DropTarget.cs b/src/Avalonia.X11/Selections/DragDrop/X11DropTarget.cs index 1d8a402fdc..8d8670caa7 100644 --- a/src/Avalonia.X11/Selections/DragDrop/X11DropTarget.cs +++ b/src/Avalonia.X11/Selections/DragDrop/X11DropTarget.cs @@ -96,7 +96,7 @@ internal sealed class X11DropTarget var screenX = (ushort)((message.ptr3 >> 16) & 0xFFFF); var screenY = (ushort)(message.ptr3 & 0xFFFF); var position = _window.PointToClient(new PixelPoint(screenX, screenY)); - var requestedEffects = ActionToEffects(message.ptr5); + var requestedEffects = XdndActionHelper.ActionToEffects(message.ptr5, _atoms); var eventType = _currentDrag.LastPosition is null ? RawDragEventType.DragEnter : RawDragEventType.DragOver; _currentDrag.LastPosition = position; @@ -113,7 +113,7 @@ internal sealed class X11DropTarget _dragDropDevice.ProcessRawEvent(dragEnter); - var resultAction = EffectsToAction(dragEnter.Effects); + var resultAction = XdndActionHelper.EffectsToAction(dragEnter.Effects, _atoms); var evt = new XEvent { @@ -182,28 +182,6 @@ internal sealed class X11DropTarget DisposeCurrentDrag(); } - private DragDropEffects ActionToEffects(IntPtr action) - { - if (action == _atoms.XdndActionCopy) - return DragDropEffects.Copy; - if (action == _atoms.XdndActionMove) - return DragDropEffects.Move; - if (action == _atoms.XdndActionLink) - return DragDropEffects.Link; - return DragDropEffects.None; - } - - private IntPtr EffectsToAction(DragDropEffects effects) - { - if ((effects & DragDropEffects.Copy) != 0) - return _atoms.XdndActionCopy; - if ((effects & DragDropEffects.Move) != 0) - return _atoms.XdndActionMove; - if ((effects & DragDropEffects.Link) != 0) - return _atoms.XdndActionLink; - return 0; - } - private void DisposeCurrentDrag() { _currentDrag?.Dispose(); diff --git a/src/Avalonia.X11/Selections/DragDrop/XdndActionHelper.cs b/src/Avalonia.X11/Selections/DragDrop/XdndActionHelper.cs new file mode 100644 index 0000000000..b8fbc17e36 --- /dev/null +++ b/src/Avalonia.X11/Selections/DragDrop/XdndActionHelper.cs @@ -0,0 +1,29 @@ +using System; +using Avalonia.Input; + +namespace Avalonia.X11.Selections.DragDrop; + +internal static class XdndActionHelper +{ + public static DragDropEffects ActionToEffects(IntPtr action, X11Atoms atoms) + { + if (action == atoms.XdndActionCopy) + return DragDropEffects.Copy; + if (action == atoms.XdndActionMove) + return DragDropEffects.Move; + if (action == atoms.XdndActionLink) + return DragDropEffects.Link; + return DragDropEffects.None; + } + + public static IntPtr EffectsToAction(DragDropEffects effects, X11Atoms atoms) + { + if ((effects & DragDropEffects.Copy) != 0) + return atoms.XdndActionCopy; + if ((effects & DragDropEffects.Move) != 0) + return atoms.XdndActionMove; + if ((effects & DragDropEffects.Link) != 0) + return atoms.XdndActionLink; + return 0; + } +} diff --git a/src/Avalonia.X11/Selections/SelectionDataProvider.cs b/src/Avalonia.X11/Selections/SelectionDataProvider.cs index 1d47d5b3bb..e9c947f0e1 100644 --- a/src/Avalonia.X11/Selections/SelectionDataProvider.cs +++ b/src/Avalonia.X11/Selections/SelectionDataProvider.cs @@ -18,13 +18,13 @@ internal abstract class SelectionDataProvider : IDisposable { private readonly int _maximumPropertySize; - protected AvaloniaX11Platform Platform { get; } + public AvaloniaX11Platform Platform { get; } - protected IntPtr Selection { get; } + public IntPtr Selection { get; } - protected IntPtr Handle { get; private set; } + public IntPtr Window { get; private set; } - public IAsyncDataTransfer? DataTransfer { get; set; } + public IAsyncDataTransfer? DataTransfer { get; protected set; } protected SelectionDataProvider(AvaloniaX11Platform platform, IntPtr selection) { @@ -37,7 +37,7 @@ internal abstract class SelectionDataProvider : IDisposable _maximumPropertySize = (int)Math.Min(0x100000, maxRequestSize - 0x100); - Handle = CreateEventWindow(platform, OnEvent); + Window = CreateEventWindow(platform, OnEvent); } protected IntPtr GetOwner() @@ -269,10 +269,10 @@ internal abstract class SelectionDataProvider : IDisposable protected virtual void Dispose(bool disposing) { - if (Handle == 0) + if (Window == 0) return; - XDestroyWindow(Platform.Display, Handle); - Handle = 0; + XDestroyWindow(Platform.Display, Window); + Window = 0; } }