Browse Source

Xdnd: handle events

pull/20926/head
Julien Lebosquain 7 months ago
parent
commit
6952b93711
No known key found for this signature in database GPG Key ID: 1833CAD10ACC46FD
  1. 44
      src/Avalonia.X11/Clipboard/ClipboardDataFormatHelper.cs
  2. 2
      src/Avalonia.X11/Clipboard/ClipboardDataReader.cs
  3. 41
      src/Avalonia.X11/Clipboard/X11Clipboard.cs
  4. 56
      src/Avalonia.X11/DragDrop/DragDropDataTransfer.cs
  5. 15
      src/Avalonia.X11/DragDrop/DragDropDataTransferItem.cs
  6. 13
      src/Avalonia.X11/DragDrop/IXdndWindow.cs
  7. 208
      src/Avalonia.X11/DragDrop/XdndHandler.cs
  8. 14
      src/Avalonia.X11/X11Atoms.cs
  9. 22
      src/Avalonia.X11/X11Window.cs
  10. 31
      src/Avalonia.X11/XLib.Helpers.cs

44
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<DataFormat>(formatAtoms.Length);
List<IntPtr>? 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))

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

41
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<DataFormat>(formatAtoms.Length);
List<IntPtr>? 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<IAsyncDataTransferItem[]> CreateItemsAsync(ClipboardDataReader reader, DataFormat[] formats)

56
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;
/// <summary>
/// Implementation of <see cref="IDataTransfer"/> for data being dragged into an Avalonia window via Xdnd.
/// </summary>
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);
}
}

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

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

208
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;
/// <summary>
/// Manages XDND (drag and drop) for a given X11 window.
/// Specs: https://www.freedesktop.org/wiki/Specifications/XDND/
/// </summary>
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<IntPtr>();
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;
}
}

14
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<string, IntPtr> _namesToAtoms = new Dictionary<string, IntPtr>();
private readonly Dictionary<IntPtr, string> _atomsToNames = new Dictionary<IntPtr, string>();
public X11Atoms(IntPtr display)

22
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<IDragDropDevice>() 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

31
src/Avalonia.X11/XLib.Helpers.cs

@ -23,7 +23,34 @@ internal static partial class XLib
}
finally
{
XFree(prop);
if (prop != 0)
XFree(prop);
}
}
}
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);
}
}
}

Loading…
Cancel
Save