Skip to content

Commit

Permalink
feat: implement the X Drag and Drop (XDnD) extension to accept draggi…
Browse files Browse the repository at this point in the history
…ng from other windows
  • Loading branch information
ramezgerges committed May 15, 2024
1 parent 8786ee0 commit f5295cc
Show file tree
Hide file tree
Showing 4 changed files with 320 additions and 8 deletions.
6 changes: 2 additions & 4 deletions src/Uno.UI.Runtime.Skia.X11/X11ApplicationHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ static X11ApplicationHost()
ApiExtensibility.Register<FileOpenPicker>(typeof(IFileOpenPickerExtension), o => new LinuxFilePickerExtension(o));
ApiExtensibility.Register<FolderPicker>(typeof(IFolderPickerExtension), o => new LinuxFilePickerExtension(o));
ApiExtensibility.Register<FileSavePicker>(typeof(IFileSavePickerExtension), o => new LinuxFileSaverExtension(o));

ApiExtensibility.Register<DragDropManager>(typeof(Windows.ApplicationModel.DataTransfer.DragDrop.Core.IDragDropExtension), o => new X11DragDropExtension(o));
}

public X11ApplicationHost(Func<Application> appBuilder)
Expand All @@ -85,10 +87,6 @@ protected override Task RunLoop()

while (!X11XamlRootHost.AllWindowsDone())
{
if (this.Log().IsEnabled(LogLevel.Trace))
{
this.Log().Trace($"{nameof(X11ApplicationHost)} is testing for all windows closed.");
}
Thread.Sleep(100);
}

Expand Down
281 changes: 281 additions & 0 deletions src/Uno.UI.Runtime.Skia.X11/X11DragDropExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
using System;
using System.Globalization;
using System.Linq;
using System.Threading;
using Windows.ApplicationModel.DataTransfer;
using Windows.ApplicationModel.DataTransfer.DragDrop;
using Windows.ApplicationModel.DataTransfer.DragDrop.Core;
using Windows.Foundation;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Input;
using Uno.Extensions;
using Uno.Foundation.Logging;
using Uno.UI.Hosting;
namespace Uno.WinUI.Runtime.Skia.X11;

internal class X11DragDropExtension : IDragDropExtension
{
private static readonly long _fakePointerId = Pointer.CreateUniqueIdForUnknownPointer();

private readonly X11XamlRootHost _host;
private readonly CoreDragDropManager _coreDragDropManager;
private readonly DragDropManager _manager;
private XdndSession? _currentSession;

private IntPtr XdndSelection => X11Helper.GetAtom(_host.X11Window.Display, X11Helper.XdndSelection);

public X11DragDropExtension(DragDropManager manager)
{
if (manager.ContentRoot.GetOrCreateXamlRoot().HostWindow is not { } window)
{
throw new InvalidOperationException($"Couldn't find a window associated with the {nameof(X11DragDropExtension)}");
}
_host = X11XamlRootHost.GetHostFromWindow(window) ?? throw new InvalidOperationException($"Couldn't find an {nameof(X11XamlRootHost)} associated with the {nameof(X11DragDropExtension)}");
_manager = manager;
_coreDragDropManager = XamlRoot.GetCoreDragDropManager(((IXamlRootHost)_host).RootElement!.XamlRoot);

var display = _host.X11Window.Display;
using var _1 = X11Helper.XLock(display);
var _2 = XLib.XChangeProperty(
display,
_host.X11Window.Window,
X11Helper.GetAtom(display, X11Helper.XdndAware),
X11Helper.GetAtom(display, X11Helper.XA_ATOM),
32,
PropertyMode.Replace,
new byte[] { 5 }, // version 5
1);
var _3 = XLib.XFlush(display);

_host.SetDragDropExtension(this);
}

public void ProcessXdndMessage(XClientMessageEvent ev)
{
using var _1 = X11Helper.XLock(_host.X11Window.Display);

if (this.Log().IsEnabled(LogLevel.Trace))
{
this.Log().Trace($"XDnd EVENT for window {ev.window}: message_type={XLib.GetAtomName(_host.X11Window.Display, ev.message_type)} data: {ev.ptr1.ToString("X", CultureInfo.InvariantCulture)}, {ev.ptr2.ToString("X", CultureInfo.InvariantCulture)}, {ev.ptr3.ToString("X", CultureInfo.InvariantCulture)}, {ev.ptr4.ToString("X", CultureInfo.InvariantCulture)}, {ev.ptr5.ToString("X", CultureInfo.InvariantCulture)}");
}

if (ev.message_type == X11Helper.GetAtom(_host.X11Window.Display, X11Helper.XdndEnter))
{
ProcessXdndEnter(ev);
}
else if (ev.message_type == X11Helper.GetAtom(_host.X11Window.Display, X11Helper.XdndPosition))
{
ProcessXdndPosition(ev);
}
else if (ev.message_type == X11Helper.GetAtom(_host.X11Window.Display, X11Helper.XdndLeave))
{
ProcessXdndLeave(ev);
}
else if (ev.message_type == X11Helper.GetAtom(_host.X11Window.Display, X11Helper.XdndDrop))
{
ProcessXdndDrop(ev);
}
else
{
throw new ArgumentException($"{nameof(ProcessXdndMessage)} only accepts XDnD messages.");
}
}

private void ProcessXdndEnter(XClientMessageEvent ev)
{
var sourceWindow = ev.ptr1;
var version = ev.ptr2 >> 24;

if (this.Log().IsEnabled(LogLevel.Trace))
{
this.Log().Trace($"XDndEnter: version={version}, sourceWindow={sourceWindow}, types in message: [{XLib.GetAtomName(_host.X11Window.Display, ev.ptr3)}, {XLib.GetAtomName(_host.X11Window.Display, ev.ptr4)}, {XLib.GetAtomName(_host.X11Window.Display, ev.ptr5)}]");
}

var moreThan3Types = ev.ptr2 & 1;

var types = moreThan3Types == IntPtr.Zero ?
new[] { ev.ptr3, ev.ptr4, ev.ptr5 } :
X11ClipboardExtension.WaitForFormats(_host.X11Window, XdndSelection);

if (this.Log().IsEnabled(LogLevel.Trace))
{
this.Log().Trace($"XDndEnter: total types received {types.Select(t => XLib.GetAtomName(_host.X11Window.Display, t)).ToList()}");
}

_currentSession = new XdndSession(version, sourceWindow, types, false, null, null, null);
}

private void ProcessXdndPosition(XClientMessageEvent ev)
{
if (_currentSession is null)
{
if (this.Log().IsEnabled(LogLevel.Error))
{
this.Log().Error($"Received a XdndPosition message without a XdndEnter preceding it. Ignoring.");
}

return;
}

var display = _host.X11Window.Display;

var sourceWindow = ev.ptr1;
var rootX = (int)(ev.ptr3 >> 16);
var rootY = (int)(ev.ptr3 & 0xffff);

var _1 = XLib.XQueryTree(display, XLib.XDefaultRootWindow(display), out IntPtr root, out _, out _, out _);
XWindowAttributes windowAttrs = default;
var _2 = XLib.XGetWindowAttributes(display, _host.X11Window.Window, ref windowAttrs);
XLib.XTranslateCoordinates(display, root, _host.X11Window.Window, rootX, rootY, out var x, out var y, out _);
x += windowAttrs.x;
y += windowAttrs.y;

if (!_currentSession.Value.EnterFired)
{
// Note how we synchronously retrieve and cache the data, unlike copying/pasting from CLIPBOARD, which asynchronously gets the data only when used.
var package = new DataPackage();
var formats = _currentSession.Value.AvailableFormats;
if (formats.FirstOrDefault(f => X11ClipboardExtension.TextFormats.ContainsKey(XLib.GetAtomName(display, f))) is var f2 && f2 != IntPtr.Zero)
{
package.SetText(X11ClipboardExtension.WaitForText(_host.X11Window, f2, XdndSelection));
}

// TODO: other operations
var operations = DataPackageOperation.Copy;

var src = new DragEventSource(x, y);
var info = new CoreDragInfo(src, package.GetView(), operations);

if (this.Log().IsEnabled(LogLevel.Trace))
{
this.Log().Trace($"XDndPosition: first position event, firing DragStarted with available operations {operations}. Found available formats {formats.Select(f => XLib.GetAtomName(display, f)).ToList()}.");
}

_coreDragDropManager.DragStarted(info);
// Note: No needs to _manager.ProcessMove, the DragStarted will actually have the same effect

_currentSession = _currentSession.Value with { EnterFired = true, Package = package, Operations = operations, LastPosition = new Point(x, y) };
}

var acceptedOperations = _manager.ProcessMoved(new DragEventSource(x, y));

if (this.Log().IsEnabled(LogLevel.Trace))
{
this.Log().Trace($"XDndPosition: sent ProcessMoved to DragDropManager: acceptedOperations={acceptedOperations}.");
}

XClientMessageEvent m = default;
m.type = XEventName.ClientMessage;
m.display = display;
m.window = sourceWindow;
m.message_type = X11Helper.GetAtom(display, X11Helper.XdndStatus);
m.format = 32;
m.ptr1 = _host.X11Window.Window;
m.ptr2 = acceptedOperations != DataPackageOperation.None ? 1 : 0;
// This is an optimization mechanism that tells Xdnd not to send new XdndPosition events until the pointer exits the widget it's in.
// We skip this with an empty rectangle.
m.ptr3 = 0;
m.ptr4 = 0;
m.ptr5 = X11Helper.GetAtom(display, X11Helper.XdndActionCopy); // TODO: support other actions and choose action from acceptedOperations

XEvent xev = default;
xev.ClientMessageEvent = m;
var _3 = XLib.XSendEvent(display, ev.ptr1, false, IntPtr.Zero /* NoEventMask */, ref xev);
var _4 = XLib.XFlush(display);

if (this.Log().IsEnabled(LogLevel.Trace))
{
this.Log().Trace($"XDndPosition: responded with XdndStatus message.");
}
}

private void ProcessXdndLeave(XClientMessageEvent ev)
{
var pos = _currentSession!.Value.LastPosition!.Value;
_manager.ProcessAborted(new DragEventSource((int)pos.X, (int)pos.Y));
_currentSession = null;

if (this.Log().IsEnabled(LogLevel.Trace))
{
this.Log().Trace($"XDndLeave: aborted current dragging session.");
}
}

private void ProcessXdndDrop(XClientMessageEvent ev)
{
var pos = _currentSession!.Value.LastPosition!.Value;
var acceptedOperation = _manager.ProcessDropped(new DragEventSource((int)pos.X, (int)pos.Y));
_currentSession = null;

if (this.Log().IsEnabled(LogLevel.Trace))
{
this.Log().Trace($"XDndDrop: called DragDropManager.ProcessDropped and received acceptedOperation={acceptedOperation}.");
}

var sourceWindow = ev.ptr1;

var display = _host.X11Window.Display;

// We already cached the data, so no need to first retrieve it. We directly send XdndFinished
XClientMessageEvent m = default;
m.type = XEventName.ClientMessage;
m.display = display;
m.window = sourceWindow;
m.message_type = X11Helper.GetAtom(display, X11Helper.XdndFinished);
m.format = 32;
m.ptr1 = _host.X11Window.Window;
m.ptr2 = 0;
// TODO: support other actions and read from acceptedOperation
m.ptr3 = acceptedOperation is not DataPackageOperation.None ? X11Helper.GetAtom(display, X11Helper.XdndActionCopy) : X11Helper.None;

XEvent xev = default;
xev.ClientMessageEvent = m;
var _1 = XLib.XSendEvent(display, ev.ptr1, false, IntPtr.Zero /* NoEventMask */, ref xev);
var _2 = XLib.XFlush(display);

if (this.Log().IsEnabled(LogLevel.Trace))
{
this.Log().Trace($"XDndDrop: responded with XdndFinished.");
}
}

// TODO: uno-to-outside dragging
public void StartNativeDrag(CoreDragInfo info) => throw new System.NotImplementedException();

private class DragEventSource(int x, int y) : IDragEventSource
{
private static long _nextFrameId;
private readonly Point _location = new Point(x, y);

public long Id => _fakePointerId;

public uint FrameId { get; } = (uint)Interlocked.Increment(ref _nextFrameId);

/// <inheritdoc />
public (Point location, DragDropModifiers modifier) GetState() => (_location, DragDropModifiers.None);

/// <inheritdoc />
public Point GetPosition(object? relativeTo)
{
if (relativeTo is null)
{
return _location;
}

if (relativeTo is UIElement elt)
{
var eltToRoot = UIElement.GetTransform(elt, null);
var rootToElt = eltToRoot.Inverse();

return rootToElt.Transform(_location);
}

throw new InvalidOperationException("The relative to must be a UIElement.");
}
}

// From the spec: "If (the target window) retrieved the data, it should cache it so it does not need to be retrieved again when the actual drop occurs.
// XdndEnter doesn't provide pointer coords, so we fire DragEntered with the first XdndPosition that comes after XdndEnter
// We store the last position because XdndLeave doesn't send coordinates
private record struct XdndSession(IntPtr Version, IntPtr SourceWindow, IntPtr[] AvailableFormats, bool EnterFired, DataPackage? Package, DataPackageOperation? Operations, Point? LastPosition);
}
22 changes: 19 additions & 3 deletions src/Uno.UI.Runtime.Skia.X11/X11XamlRootHost.x11events.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ internal partial class X11XamlRootHost
private Thread? _eventsThread;
private X11PointerInputSource? _pointerSource;
private X11KeyboardInputSource? _keyboardSource;
private X11DragDropExtension? _dragDrop;
private X11DisplayInformationExtension? _displayInformationExtension;

private void InitializeX11EventsThread()
Expand Down Expand Up @@ -54,6 +55,15 @@ public void SetKeyboardSource(X11KeyboardInputSource keyboardSource)
_keyboardSource = keyboardSource;
}

public void SetDragDropExtension(X11DragDropExtension dragDrop)
{
if (_dragDrop is not null)
{
throw new InvalidOperationException($"{nameof(X11DragDropExtension)} is set twice.");
}
_dragDrop = dragDrop;
}

public void SetDisplayInformationExtension(X11DisplayInformationExtension extension)
{
if (_displayInformationExtension is not null)
Expand Down Expand Up @@ -133,16 +143,22 @@ static IEnumerable<XEvent> GetEvents(IntPtr display)
switch (@event.type)
{
case XEventName.ClientMessage:
// no locking needed, WM_DELETE_WINDOW is already cached
IntPtr deleteWindow = X11Helper.GetAtom(X11Window.Display, X11Helper.WM_DELETE_WINDOW);
if (@event.ClientMessageEvent.ptr1 == deleteWindow)
if (@event.ClientMessageEvent.ptr1 == X11Helper.GetAtom(X11Window.Display, X11Helper.WM_DELETE_WINDOW))
{
// This happens when we click the titlebar X, not like xkill,
// which, according to the source code, just calls XKillClient
// https://gitlab.freedesktop.org/xorg/app/xkill/-/blob/a5f704e4cd30f03859f66bafd609a75aae27cc8c/xkill.c#L234
// In the case of xkill, we can't really do much, it's similar to a SIGKILL but for x connections
QueueAction(this, _closingCallback);
}
else if (@event.ClientMessageEvent.message_type == X11Helper.GetAtom(X11Window.Display, X11Helper.XdndEnter) ||
@event.ClientMessageEvent.message_type == X11Helper.GetAtom(X11Window.Display, X11Helper.XdndPosition) ||
@event.ClientMessageEvent.message_type == X11Helper.GetAtom(X11Window.Display, X11Helper.XdndPosition) ||
@event.ClientMessageEvent.message_type == X11Helper.GetAtom(X11Window.Display, X11Helper.XdndLeave) ||
@event.ClientMessageEvent.message_type == X11Helper.GetAtom(X11Window.Display, X11Helper.XdndDrop))
{
QueueAction(this, () => _dragDrop?.ProcessXdndMessage(@event.ClientMessageEvent));
}
break;
case XEventName.ConfigureNotify:
var configureEvent = @event.ConfigureEvent;
Expand Down
19 changes: 18 additions & 1 deletion src/Uno.UI.Runtime.Skia.X11/X11_Bindings/X11Helper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,19 @@ internal static partial class X11Helper
public const string XA_CARDINAL = "CARDINAL";
public const string ATOM_PAIR = "ATOM_PAIR";
public const string INCR = "INCR";
public const string XdndAware = "XdndAware";
public const string XdndStatus = "XdndStatus";
public const string XdndEnter = "XdndEnter";
public const string XdndPosition = "XdndPosition";
public const string XdndTypeList = "XdndTypeList";
public const string XdndActionCopy = "XdndActionCopy";
public const string XdndActionLink = "XdndActionLink";
public const string XdndActionMove = "XdndActionMove";
public const string XdndDrop = "XdndDrop";
public const string XdndLeave = "XdndLeave";
public const string XdndFinished = "XdndFinished";
public const string XdndSelection = "XdndSelection";
public const string XdndProxy = "XdndProxy";

public const int POLLIN = 0x001; /* There is data to read. */
public const int POLLPRI = 0x002; /* There is urgent data to read. */
Expand Down Expand Up @@ -300,7 +313,11 @@ private unsafe static void SetMotifWMHints(X11Window x11Window, bool on, IntPtr?
}

private static Func<IntPtr, string, bool, IntPtr> _getAtom = Funcs.CreateMemoized<IntPtr, string, bool, IntPtr>(XLib.XInternAtom);
public static IntPtr GetAtom(IntPtr display, string name, bool only_if_exists = false) => _getAtom(display, name, only_if_exists);
public static IntPtr GetAtom(IntPtr display, string name, bool only_if_exists = false)
{
using var _ = XLock(display);
return _getAtom(display, name, only_if_exists);
}

[LibraryImport("libc")]
public unsafe static partial int poll(Pollfd* __fds, IntPtr __nfds, int __timeout);
Expand Down

0 comments on commit f5295cc

Please sign in to comment.