Browse Source

Xdnd: send messages

pull/20926/head
Julien Lebosquain 7 months ago
parent
commit
17563ac0ea
No known key found for this signature in database GPG Key ID: 1833CAD10ACC46FD
  1. 3
      src/Avalonia.Base/Input/AsyncDataTransferExtensions.cs
  2. 43
      src/Avalonia.Base/Input/SyncToAsyncDataTransfer.cs
  3. 21
      src/Avalonia.Base/Input/SyncToAsyncDataTransferItem.cs
  4. 12
      src/Avalonia.X11/Selections/Clipboard/X11ClipboardImpl.cs
  5. 15
      src/Avalonia.X11/Selections/DragDrop/DragDropDataProvider.cs
  6. 294
      src/Avalonia.X11/Selections/DragDrop/X11DragSource.cs
  7. 26
      src/Avalonia.X11/Selections/DragDrop/X11DropTarget.cs
  8. 29
      src/Avalonia.X11/Selections/DragDrop/XdndActionHelper.cs
  9. 16
      src/Avalonia.X11/Selections/SelectionDataProvider.cs

3
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);
/// <summary>
/// Gets whether a <see cref="IAsyncDataTransfer"/> supports a specific format.
/// </summary>

43
src/Avalonia.Base/Input/SyncToAsyncDataTransfer.cs

@ -0,0 +1,43 @@
using System.Collections.Generic;
namespace Avalonia.Input;
/// <summary>
/// Wraps a <see cref="IDataTransfer"/> into a <see cref="IAsyncDataTransfer"/>.
/// </summary>
/// <param name="dataTransfer">The sync object to wrap.</param>
internal sealed class SyncToAsyncDataTransfer(IDataTransfer dataTransfer)
: IDataTransfer, IAsyncDataTransfer
{
private SyncToAsyncDataTransferItem[]? _items;
public IReadOnlyList<DataFormat> Formats
=> dataTransfer.Formats;
public IReadOnlyList<SyncToAsyncDataTransferItem> Items
=> _items ??= ProvideItems();
IReadOnlyList<IDataTransferItem> IDataTransfer.Items
=> dataTransfer.Items;
IReadOnlyList<IAsyncDataTransferItem> 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();
}

21
src/Avalonia.Base/Input/SyncToAsyncDataTransferItem.cs

@ -0,0 +1,21 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Avalonia.Input;
/// <summary>
/// Wraps a <see cref="IDataTransferItem"/> into a <see cref="IAsyncDataTransferItem"/>.
/// </summary>
/// <param name="dataTransferItem">The sync item to wrap.</param>
internal sealed class SyncToAsyncDataTransferItem(IDataTransferItem dataTransferItem)
: IDataTransferItem, IAsyncDataTransferItem
{
public IReadOnlyList<DataFormat> Formats
=> dataTransferItem.Formats;
public object? TryGetRaw(DataFormat format)
=> dataTransferItem.TryGetRaw(format);
public Task<object?> TryGetRawAsync(DataFormat format)
=> Task.FromResult(dataTransferItem.TryGetRaw(format));
}

12
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<bool> IsCurrentOwnerAsync()
=> Task.FromResult(GetOwner() == Handle);
=> Task.FromResult(GetOwner() == Window);
}

15
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);
}

294
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<ICursorFactory>();
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<DragDropEffects> _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<DragDropEffects> Completion
=> _completionSource.Task;
private IntPtr[] DataFormatAtoms
{
get
{
return field ??= CalcDataFormatAtoms();
IntPtr[] CalcDataFormatAtoms()
{
var atomValues = new List<IntPtr>(_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);
}
}

26
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();

29
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;
}
}

16
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;
}
}

Loading…
Cancel
Save