diff --git a/src/Avalonia.X11/Clipboard/ClipboardDataFormatHelper.cs b/src/Avalonia.X11/Clipboard/ClipboardDataFormatHelper.cs index 5e53ff96c1..ce17646812 100644 --- a/src/Avalonia.X11/Clipboard/ClipboardDataFormatHelper.cs +++ b/src/Avalonia.X11/Clipboard/ClipboardDataFormatHelper.cs @@ -1,6 +1,6 @@ using System; +using System.Collections.Generic; using System.Text; -using Avalonia.Controls; using Avalonia.Input; namespace Avalonia.X11.Clipboard; @@ -41,6 +41,48 @@ internal static class ClipboardDataFormatHelper return null; } + public static (DataFormat[] DataFormats, IntPtr[] TextFormatAtoms) ToDataFormats(IntPtr[] formatAtoms, X11Atoms atoms) + { + if (formatAtoms.Length == 0) + return ([], []); + + var formats = new List(formatAtoms.Length); + List? textFormatAtoms = null; + + var hasImage = false; + + foreach (var formatAtom in formatAtoms) + { + if (ToDataFormat(formatAtom, atoms) is not { } format) + continue; + + if (DataFormat.Text.Equals(format)) + { + if (textFormatAtoms is null) + { + formats.Add(format); + textFormatAtoms = []; + } + textFormatAtoms.Add(formatAtom); + } + else + { + formats.Add(format); + + if(!hasImage) + { + if (format.Identifier is JpegFormatMimeType or PngFormatMimeType) + hasImage = true; + } + } + } + + if (hasImage) + formats.Add(DataFormat.Bitmap); + + return (formats.ToArray(), textFormatAtoms?.ToArray() ?? []); + } + public static IntPtr ToAtom(DataFormat format, IntPtr[] textFormatAtoms, X11Atoms atoms, DataFormat[] dataFormats) { if (DataFormat.Text.Equals(format)) diff --git a/src/Avalonia.X11/Clipboard/ClipboardDataReader.cs b/src/Avalonia.X11/Clipboard/ClipboardDataReader.cs index 910b76385c..929da2a43f 100644 --- a/src/Avalonia.X11/Clipboard/ClipboardDataReader.cs +++ b/src/Avalonia.X11/Clipboard/ClipboardDataReader.cs @@ -53,7 +53,7 @@ internal sealed class ClipboardDataReader( null; } - if(DataFormat.Bitmap.Equals(format)) + if (DataFormat.Bitmap.Equals(format)) { using var data = result.AsStream(); diff --git a/src/Avalonia.X11/Clipboard/X11Clipboard.cs b/src/Avalonia.X11/Clipboard/X11Clipboard.cs index 35abbf11c1..46cf85acec 100644 --- a/src/Avalonia.X11/Clipboard/X11Clipboard.cs +++ b/src/Avalonia.X11/Clipboard/X11Clipboard.cs @@ -326,45 +326,8 @@ namespace Avalonia.X11.Clipboard { using var session = OpenReadSession(); - var formatAtoms = await session.SendFormatRequest(); - if (formatAtoms is null) - return ([], []); - - var formats = new List(formatAtoms.Length); - List? textFormatAtoms = null; - - var hasImage = false; - - foreach (var formatAtom in formatAtoms) - { - if (ClipboardDataFormatHelper.ToDataFormat(formatAtom, _x11.Atoms) is not { } format) - continue; - - if (DataFormat.Text.Equals(format)) - { - if (textFormatAtoms is null) - { - formats.Add(format); - textFormatAtoms = []; - } - textFormatAtoms.Add(formatAtom); - } - else - { - formats.Add(format); - - if(!hasImage) - { - if (format.Identifier is ClipboardDataFormatHelper.JpegFormatMimeType or ClipboardDataFormatHelper.PngFormatMimeType) - hasImage = true; - } - } - } - - if (hasImage) - formats.Add(DataFormat.Bitmap); - - return (formats.ToArray(), textFormatAtoms?.ToArray() ?? []); + var formatAtoms = await session.SendFormatRequest() ?? []; + return ClipboardDataFormatHelper.ToDataFormats(formatAtoms, _x11.Atoms); } private static async Task CreateItemsAsync(ClipboardDataReader reader, DataFormat[] formats) diff --git a/src/Avalonia.X11/DragDrop/DragDropDataTransfer.cs b/src/Avalonia.X11/DragDrop/DragDropDataTransfer.cs new file mode 100644 index 0000000000..338d5e784d --- /dev/null +++ b/src/Avalonia.X11/DragDrop/DragDropDataTransfer.cs @@ -0,0 +1,56 @@ +using System; +using Avalonia.Input; +using Avalonia.Input.Platform; +using static Avalonia.X11.XLib; + +namespace Avalonia.X11.DragDrop; + +/// +/// Implementation of for data being dragged into an Avalonia window via Xdnd. +/// +internal sealed class DragDropDataTransfer( + IntPtr display, + IntPtr sourceWindow, + IntPtr targetWindow, + IInputRoot inputRoot, + DataFormat[] dataFormats, + X11Atoms atoms) + : PlatformDataTransfer +{ + public IntPtr SourceWindow { get; } = sourceWindow; + + public IInputRoot InputRoot { get; } = inputRoot; + + public Point? LastPosition { get; set; } + + protected override DataFormat[] ProvideFormats() + => dataFormats; + + protected override PlatformDataTransferItem[] ProvideItems() + { + return []; + } + + public override void Dispose() + { + var evt = new XEvent + { + ClientMessageEvent = new XClientMessageEvent + { + type = XEventName.ClientMessage, + display = display, + window = SourceWindow, + message_type = atoms.XdndFinished, + format = 32, + ptr1 = targetWindow, + ptr2 = 0, + ptr3 = 0, + ptr4 = 0, + ptr5 = 0 + } + }; + + XSendEvent(display, SourceWindow, false, (IntPtr)EventMask.NoEventMask, ref evt); + XFlush(display); + } +} diff --git a/src/Avalonia.X11/DragDrop/DragDropDataTransferItem.cs b/src/Avalonia.X11/DragDrop/DragDropDataTransferItem.cs new file mode 100644 index 0000000000..683bae10da --- /dev/null +++ b/src/Avalonia.X11/DragDrop/DragDropDataTransferItem.cs @@ -0,0 +1,15 @@ +using Avalonia.Input; +using Avalonia.Input.Platform; + +namespace Avalonia.X11.DragDrop; + +internal sealed class DragDropDataTransferItem(DataFormat[] formats) : PlatformDataTransferItem +{ + protected override DataFormat[] ProvideFormats() + => formats; + + protected override object? TryGetRawCore(DataFormat format) + { + throw new System.NotImplementedException(); + } +} diff --git a/src/Avalonia.X11/DragDrop/IXdndWindow.cs b/src/Avalonia.X11/DragDrop/IXdndWindow.cs new file mode 100644 index 0000000000..c7e5be11c3 --- /dev/null +++ b/src/Avalonia.X11/DragDrop/IXdndWindow.cs @@ -0,0 +1,13 @@ +using System; +using Avalonia.Input; + +namespace Avalonia.X11.DragDrop; + +internal interface IXdndWindow +{ + IntPtr Handle { get; } + + IInputRoot? InputRoot { get; } + + Point PointToClient(PixelPoint point); +} diff --git a/src/Avalonia.X11/DragDrop/XdndHandler.cs b/src/Avalonia.X11/DragDrop/XdndHandler.cs new file mode 100644 index 0000000000..0cb8969917 --- /dev/null +++ b/src/Avalonia.X11/DragDrop/XdndHandler.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.X11.Clipboard; +using static Avalonia.X11.XLib; + +namespace Avalonia.X11.DragDrop; + +/// +/// Manages XDND (drag and drop) for a given X11 window. +/// Specs: https://www.freedesktop.org/wiki/Specifications/XDND/ +/// +internal sealed class XdndHandler +{ + private const byte XdndVersion = 5; + + private readonly IDragDropDevice _dragDropDevice; + private readonly IXdndWindow _window; + private readonly IntPtr _display; + private readonly X11Atoms _atoms; + private DragDropDataTransfer? _currentDrag; + + public XdndHandler(IDragDropDevice dragDropDevice, IXdndWindow window, IntPtr _display, X11Atoms atoms) + { + _dragDropDevice = dragDropDevice; + _window = window; + this._display = _display; + _atoms = atoms; + + IntPtr version = XdndVersion; + XChangeProperty(_display, _window.Handle, _atoms.XdndAware, _atoms.XA_ATOM, 32, PropertyMode.Replace, ref version, 1); + } + + public void HandleXdndEnter(in XClientMessageEvent message) + { + if (_window.InputRoot is not { } inputRoot) + return; + + // Spec: If the version number in the XdndEnter message is higher than what the target can support, + // the target should ignore the source. + var version = (byte)((message.ptr2 >> 24) & 0xFF); + if (version > XdndVersion) + return; + + // If we ever receive a new XdndEnter message while a drag is in progress, it means something went wrong. + // In this case, assume the old drag is stale. + DisposeCurrentDrag(); + + var sourceWindow = message.ptr1; + var hasExtraFormats = (message.ptr2 & 1) == 1; + + var formats = new HashSet(); + + if (hasExtraFormats) + { + if (XGetWindowPropertyAsIntPtrArray(_display, sourceWindow, _atoms.XdndTypeList, _atoms.XA_ATOM) + is { } formatList) + { + foreach (var format in formatList) + { + if (format != 0) + formats.Add(format); + } + } + } + else + { + if (message.ptr3 != 0) + formats.Add(message.ptr3); + if (message.ptr4 != 0) + formats.Add(message.ptr4); + if (message.ptr5 != 0) + formats.Add(message.ptr5); + } + + var (dataFormats, _) = ClipboardDataFormatHelper.ToDataFormats(formats.ToArray(), _atoms); + var drag = new DragDropDataTransfer(_display, sourceWindow, _window.Handle, inputRoot, dataFormats, _atoms); + + _currentDrag = drag; + } + + public void HandleXdndPosition(in XClientMessageEvent message) + { + if (_currentDrag is null) + return; + + var sourceWindow = message.ptr1; + if (sourceWindow != _currentDrag.SourceWindow) + return; + + 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 eventType = _currentDrag.LastPosition is null ? RawDragEventType.DragEnter : RawDragEventType.DragOver; + + _currentDrag.LastPosition = position; + + var dragEnter = new RawDragEvent( + _dragDropDevice, + eventType, + _currentDrag.InputRoot, + position, + _currentDrag, + requestedEffects, + RawInputModifiers.None); + + _dragDropDevice.ProcessRawEvent(dragEnter); + + var resultAction = EffectsToAction(dragEnter.Effects); + + var evt = new XEvent + { + ClientMessageEvent = + { + type = XEventName.ClientMessage, + send_event = 1, + window = sourceWindow, + message_type = _atoms.XdndStatus, + format = 32, + ptr1 = _window.Handle, + ptr2 = resultAction == 0 ? 0 : 1, + ptr3 = 0, + ptr4 = 0, + ptr5 = resultAction + } + }; + + XSendEvent(_display, sourceWindow, false, (IntPtr)EventMask.NoEventMask, ref evt); + XFlush(_display); + } + + public void HandleXdndLeave(in XClientMessageEvent message) + { + if (_currentDrag is null) + return; + + var sourceWindow = message.ptr1; + if (sourceWindow != _currentDrag.SourceWindow) + return; + + var dragLeave = new RawDragEvent( + _dragDropDevice, + RawDragEventType.DragLeave, + _currentDrag.InputRoot, + default, + _currentDrag, + DragDropEffects.None, + RawInputModifiers.None); + + _dragDropDevice.ProcessRawEvent(dragLeave); + + DisposeCurrentDrag(); + } + + public void HandleXdndDrop(in XClientMessageEvent message) + { + if (_currentDrag is null) + return; + + var sourceWindow = message.ptr1; + if (sourceWindow != _currentDrag.SourceWindow) + return; + + var drop = new RawDragEvent( + _dragDropDevice, + RawDragEventType.Drop, + _currentDrag.InputRoot, + _currentDrag.LastPosition ?? default, + _currentDrag, + DragDropEffects.None, + RawInputModifiers.None); + + _dragDropDevice.ProcessRawEvent(drop); + + 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(); + _currentDrag = null; + } +} diff --git a/src/Avalonia.X11/X11Atoms.cs b/src/Avalonia.X11/X11Atoms.cs index afe3121cfc..047234b9d1 100644 --- a/src/Avalonia.X11/X11Atoms.cs +++ b/src/Avalonia.X11/X11Atoms.cs @@ -197,6 +197,20 @@ namespace Avalonia.X11 public IntPtr INCR; public IntPtr _NET_WM_STATE_FOCUSED; + public IntPtr XdndActionCopy; + public IntPtr XdndActionLink; + public IntPtr XdndActionMove; + public IntPtr XdndAware; + public IntPtr XdndDrop; + public IntPtr XdndEnter; + public IntPtr XdndFinished; + public IntPtr XdndLeave; + public IntPtr XdndPosition; + public IntPtr XdndProxy; + public IntPtr XdndSelection; + public IntPtr XdndStatus; + public IntPtr XdndTypeList; + private readonly Dictionary _namesToAtoms = new Dictionary(); private readonly Dictionary _atomsToNames = new Dictionary(); public X11Atoms(IntPtr display) diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 661c720f40..cad9a39287 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -27,6 +27,7 @@ using Avalonia.Input.Platform; using System.Runtime.InteropServices; using Avalonia.Dialogs; using Avalonia.Platform.Storage.FileIO; +using Avalonia.X11.DragDrop; // ReSharper disable IdentifierTypo // ReSharper disable StringLiteralTypo @@ -34,7 +35,7 @@ using Avalonia.Platform.Storage.FileIO; namespace Avalonia.X11 { internal unsafe partial class X11Window : IWindowImpl, IPopupImpl, IXI2Client, - IX11OptionsToplevelImplFeature + IX11OptionsToplevelImplFeature, IXdndWindow { private readonly AvaloniaX11Platform _platform; private readonly bool _popup; @@ -72,6 +73,7 @@ namespace Avalonia.X11 private bool _usePositioningFlags = false; private X11WindowMode _mode; private IWindowIconImpl? _iconImpl; + private readonly XdndHandler? _xdndHandler; private enum XSyncState { @@ -279,6 +281,9 @@ namespace Avalonia.X11 : null) }); + if (AvaloniaLocator.Current.GetService() is { } dragDropDevice) + _xdndHandler = new XdndHandler(dragDropDevice, this, _x11.Display, _x11.Atoms); + platform.X11Screens.Changed += OnScreensChanged; } @@ -681,7 +686,8 @@ namespace Avalonia.X11 } else if (ev.type == XEventName.ClientMessage) { - if (ev.ClientMessageEvent.message_type == _x11.Atoms.WM_PROTOCOLS) + var messageType = ev.ClientMessageEvent.message_type; + if (messageType == _x11.Atoms.WM_PROTOCOLS) { if (ev.ClientMessageEvent.ptr1 == _x11.Atoms.WM_DELETE_WINDOW) { @@ -695,6 +701,14 @@ namespace Avalonia.X11 _xSyncState = XSyncState.WaitConfigure; } } + else if (messageType == _x11.Atoms.XdndEnter) + _xdndHandler?.HandleXdndEnter(ev.ClientMessageEvent); + else if (messageType == _x11.Atoms.XdndPosition) + _xdndHandler?.HandleXdndPosition(ev.ClientMessageEvent); + else if (messageType == _x11.Atoms.XdndLeave) + _xdndHandler?.HandleXdndLeave(ev.ClientMessageEvent); + else if (messageType == _x11.Atoms.XdndDrop) + _xdndHandler?.HandleXdndDrop(ev.ClientMessageEvent); } else if (ev.type == XEventName.KeyPress || ev.type == XEventName.KeyRelease) { @@ -1302,7 +1316,9 @@ namespace Avalonia.X11 } public IPlatformHandle Handle { get; } - + + IntPtr IXdndWindow.Handle => _handle; + public PixelPoint Position { get diff --git a/src/Avalonia.X11/XLib.Helpers.cs b/src/Avalonia.X11/XLib.Helpers.cs index b32d5e3b26..813a00e88d 100644 --- a/src/Avalonia.X11/XLib.Helpers.cs +++ b/src/Avalonia.X11/XLib.Helpers.cs @@ -23,7 +23,34 @@ internal static partial class XLib } finally { - XFree(prop); + if (prop != 0) + XFree(prop); } } -} \ No newline at end of file + + public static IntPtr? XGetWindowPropertyAsIntPtr(IntPtr display, IntPtr window, IntPtr atom, IntPtr reqType) + { + if ((Status)XGetWindowProperty( + display, window, atom, 0, 1, false, reqType, + out var actualType, out var actualFormat, out var itemCount, out _, out var prop) != Status.Success) + { + return null; + } + + try + { + if (actualType != reqType || actualFormat != 32 || itemCount != 1) + return null; + + unsafe + { + return *(IntPtr*)prop; + } + } + finally + { + if (prop != 0) + XFree(prop); + } + } +}