Browse Source

Merge 6e377abbbb into d5c30b6270

pull/20926/merge
Julien Lebosquain 10 hours ago
committed by GitHub
parent
commit
d2b4972fb5
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      samples/ControlCatalog/Pages/DragAndDropPage.xaml
  2. 38
      samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
  3. 3
      src/Avalonia.Base/Input/AsyncDataTransferExtensions.cs
  4. 43
      src/Avalonia.Base/Input/SyncToAsyncDataTransfer.cs
  5. 21
      src/Avalonia.Base/Input/SyncToAsyncDataTransferItem.cs
  6. 1
      src/Avalonia.Controls/Platform/InProcessDragSource.cs
  7. 82
      src/Avalonia.X11/Clipboard/ClipboardDataReader.cs
  8. 414
      src/Avalonia.X11/Clipboard/X11Clipboard.cs
  9. 10
      src/Avalonia.X11/Dispatching/X11EventDispatcher.cs
  10. 39
      src/Avalonia.X11/Selections/Clipboard/ClipboardDataReader.cs
  11. 12
      src/Avalonia.X11/Selections/Clipboard/ClipboardDataTransfer.cs
  12. 9
      src/Avalonia.X11/Selections/Clipboard/ClipboardDataTransferItem.cs
  13. 20
      src/Avalonia.X11/Selections/Clipboard/ClipboardReadSessionFactory.cs
  14. 19
      src/Avalonia.X11/Selections/Clipboard/EventStreamWindow.cs
  15. 140
      src/Avalonia.X11/Selections/Clipboard/X11ClipboardImpl.cs
  16. 65
      src/Avalonia.X11/Selections/DataFormatHelper.cs
  17. 30
      src/Avalonia.X11/Selections/DragDrop/DragDropDataProvider.cs
  18. 49
      src/Avalonia.X11/Selections/DragDrop/DragDropDataReader.cs
  19. 40
      src/Avalonia.X11/Selections/DragDrop/DragDropDataTransfer.cs
  20. 31
      src/Avalonia.X11/Selections/DragDrop/DragDropDataTransferItem.cs
  21. 35
      src/Avalonia.X11/Selections/DragDrop/DragDropTimeoutManager.cs
  22. 13
      src/Avalonia.X11/Selections/DragDrop/IXdndWindow.cs
  23. 124
      src/Avalonia.X11/Selections/DragDrop/SynchronousXEventWaiter.cs
  24. 494
      src/Avalonia.X11/Selections/DragDrop/X11DragSource.cs
  25. 204
      src/Avalonia.X11/Selections/DragDrop/X11DropTarget.cs
  26. 29
      src/Avalonia.X11/Selections/DragDrop/XdndActionHelper.cs
  27. 8
      src/Avalonia.X11/Selections/DragDrop/XdndConstants.cs
  28. 9
      src/Avalonia.X11/Selections/IXEventWaiter.cs
  29. 260
      src/Avalonia.X11/Selections/SelectionDataProvider.cs
  30. 108
      src/Avalonia.X11/Selections/SelectionDataReader.cs
  31. 8
      src/Avalonia.X11/Selections/SelectionHelper.cs
  32. 74
      src/Avalonia.X11/Selections/SelectionReadSession.cs
  33. 6
      src/Avalonia.X11/Selections/UriListHelper.cs
  34. 19
      src/Avalonia.X11/X11Atoms.cs
  35. 36
      src/Avalonia.X11/X11CursorFactory.cs
  36. 30
      src/Avalonia.X11/X11EnumExtensions.cs
  37. 2
      src/Avalonia.X11/X11FocusProxy.cs
  38. 4
      src/Avalonia.X11/X11Globals.cs
  39. 8
      src/Avalonia.X11/X11Platform.cs
  40. 2
      src/Avalonia.X11/X11Window.Ime.cs
  41. 71
      src/Avalonia.X11/X11Window.cs
  42. 7
      src/Avalonia.X11/X11WindowInfo.cs
  43. 31
      src/Avalonia.X11/XLib.Helpers.cs
  44. 28
      src/Avalonia.X11/XLib.cs
  45. 2
      src/tools/DevGenerators/X11AtomsGenerator.cs

4
samples/ControlCatalog/Pages/DragAndDropPage.xaml

@ -43,14 +43,14 @@
MaxWidth="260"
Background="{DynamicResource SystemAccentColorDark1}"
DragDrop.AllowDrop="True">
<TextBlock TextWrapping="Wrap">Drop some text or files here (Copy)</TextBlock>
<TextBlock TextWrapping="Wrap">Drop some text, files, bitmap or custom format here (Copy)</TextBlock>
</Border>
<Border Name="MoveTarget"
Padding="16"
MaxWidth="260"
Background="{DynamicResource SystemAccentColorDark1}"
DragDrop.AllowDrop="True">
<TextBlock TextWrapping="Wrap">Drop some text or files here (Move)</TextBlock>
<TextBlock TextWrapping="Wrap">Drop some text or custom format here (Move)</TextBlock>
</Border>
</StackPanel>
</WrapPanel>

38
samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs

@ -51,6 +51,9 @@ namespace ControlCatalog.Pages
"Bitmap",
d => d.Add(DataTransferItem.Create(DataFormat.Bitmap, new Bitmap(AssetLoader.Open(new Uri("avares://ControlCatalog/Assets/image1.jpg"))))),
DragDropEffects.Copy);
AddHandlers(CopyTarget, DragDropEffects.Copy);
AddHandlers(MoveTarget, DragDropEffects.Move);
}
private void SetupDnd(string suffix, Action<DataTransfer> factory, DragDropEffects effects) =>
@ -94,16 +97,18 @@ namespace ControlCatalog.Pages
}
}
dragMe.PointerPressed += DoDrag;
}
private void AddHandlers(Control target, DragDropEffects allowedEffects)
{
DragDrop.AddDragEnterHandler(target, DragOver);
DragDrop.AddDragOverHandler(target, DragOver);
DragDrop.AddDropHandler(target, Drop);
void DragOver(object? sender, DragEventArgs e)
{
if (e.Source is Control c && c.Name == "MoveTarget")
{
e.DragEffects = e.DragEffects & (DragDropEffects.Move);
}
else
{
e.DragEffects = e.DragEffects & (DragDropEffects.Copy);
}
e.DragEffects &= allowedEffects;
// Only allow if the dragged data contains text or filenames.
if (!e.DataTransfer.Contains(DataFormat.Text)
@ -115,14 +120,10 @@ namespace ControlCatalog.Pages
async void Drop(object? sender, DragEventArgs e)
{
if (e.Source is Control c && c.Name == "MoveTarget")
{
e.DragEffects = e.DragEffects & (DragDropEffects.Move);
}
else
{
e.DragEffects = e.DragEffects & (DragDropEffects.Copy);
}
e.DragEffects &= allowedEffects;
if (e.DragEffects == DragDropEffects.None)
return;
if (e.DataTransfer.Contains(DataFormat.Text))
{
@ -165,11 +166,6 @@ namespace ControlCatalog.Pages
DropState.Content = "Custom: " + e.DataTransfer.TryGetValue(_customFormat);
}
}
dragMe.PointerPressed += DoDrag;
AddHandler(DragDrop.DropEvent, Drop);
AddHandler(DragDrop.DragOverEvent, DragOver);
}
}
}

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

1
src/Avalonia.Controls/Platform/InProcessDragSource.cs

@ -63,6 +63,7 @@ namespace Avalonia.Platform
using (_result.Subscribe(new AnonymousObserver<DragDropEffects>(tcs)))
{
var effect = await tcs.Task;
dataTransfer.Dispose();
return effect;
}
}

82
src/Avalonia.X11/Clipboard/ClipboardDataReader.cs

@ -1,82 +0,0 @@
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Input;
using Avalonia.Media.Imaging;
using static Avalonia.X11.XLib;
namespace Avalonia.X11.Clipboard;
/// <summary>
/// An object used to read values, converted to the correct format, from the X11 clipboard.
/// </summary>
internal sealed class ClipboardDataReader(
X11Info x11,
AvaloniaX11Platform platform,
IntPtr[] textFormatAtoms,
IntPtr owner,
DataFormat[] dataFormats)
: IDisposable
{
private readonly X11Info _x11 = x11;
private readonly AvaloniaX11Platform _platform = platform;
private readonly IntPtr[] _textFormatAtoms = textFormatAtoms;
private IntPtr _owner = owner;
private bool IsOwnerStillValid()
=> _owner != IntPtr.Zero && XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD) == _owner;
public async Task<object?> TryGetAsync(DataFormat format)
{
if (!IsOwnerStillValid())
return null;
var formatAtom = ClipboardDataFormatHelper.ToAtom(format, _textFormatAtoms, _x11.Atoms, dataFormats);
if (formatAtom == IntPtr.Zero)
return null;
using var session = new ClipboardReadSession(_platform);
var result = await session.SendDataRequest(formatAtom).ConfigureAwait(false);
return ConvertDataResult(result, format, formatAtom);
}
private object? ConvertDataResult(ClipboardReadSession.GetDataResult? result, DataFormat format, IntPtr formatAtom)
{
if (result is null)
return null;
if (DataFormat.Text.Equals(format))
{
return ClipboardDataFormatHelper.TryGetStringEncoding(result.TypeAtom, _x11.Atoms) is { } textEncoding ?
textEncoding.GetString(result.AsBytes()) :
null;
}
if(DataFormat.Bitmap.Equals(format))
{
using var data = result.AsStream();
return new Bitmap(data);
}
if (DataFormat.File.Equals(format))
{
// text/uri-list might not be supported
return formatAtom != IntPtr.Zero && result.TypeAtom == formatAtom ?
ClipboardUriListHelper.Utf8BytesToFileUriList(result.AsBytes()) :
null;
}
if (format is DataFormat<string>)
return Encoding.UTF8.GetString(result.AsBytes());
if (format is DataFormat<byte[]>)
return result.AsBytes();
return null;
}
public void Dispose()
=> _owner = IntPtr.Zero;
}

414
src/Avalonia.X11/Clipboard/X11Clipboard.cs

@ -1,414 +0,0 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Logging;
using Avalonia.Platform.Storage;
using static Avalonia.X11.XLib;
namespace Avalonia.X11.Clipboard
{
internal sealed class X11ClipboardImpl : IOwnedClipboardImpl
{
private readonly AvaloniaX11Platform _platform;
private readonly X11Info _x11;
private IAsyncDataTransfer? _storedDataTransfer;
private readonly IntPtr _handle;
private TaskCompletionSource<bool>? _storeAtomTcs;
private readonly IntPtr[] _textAtoms;
private readonly IntPtr _avaloniaSaveTargetsAtom;
private readonly int _maximumPropertySize;
public X11ClipboardImpl(AvaloniaX11Platform platform)
{
_platform = platform;
_x11 = platform.Info;
_handle = CreateEventWindow(platform, OnEvent);
_avaloniaSaveTargetsAtom = XInternAtom(_x11.Display, "AVALONIA_SAVE_TARGETS_PROPERTY_ATOM", false);
_textAtoms = new[]
{
_x11.Atoms.STRING,
_x11.Atoms.OEMTEXT,
_x11.Atoms.UTF8_STRING,
_x11.Atoms.UTF16_STRING
}.Where(a => a != IntPtr.Zero).ToArray();
var extendedMaxRequestSize = XExtendedMaxRequestSize(_platform.Display);
var maxRequestSize = XMaxRequestSize(_platform.Display);
_maximumPropertySize =
(int)Math.Min(0x100000, (extendedMaxRequestSize == IntPtr.Zero
? maxRequestSize
: extendedMaxRequestSize).ToInt64() - 0x100);
}
private unsafe void OnEvent(ref XEvent ev)
{
if (ev.type == XEventName.SelectionClear)
{
// We night have already regained the clipboard ownership by the time a SelectionClear message arrives.
if (GetOwner() != _handle)
_storedDataTransfer = null;
_storeAtomTcs?.TrySetResult(true);
}
else if (ev.type == XEventName.SelectionRequest)
{
var sel = ev.SelectionRequestEvent;
var resp = new XEvent
{
SelectionEvent =
{
type = XEventName.SelectionNotify,
send_event = 1,
display = _x11.Display,
selection = sel.selection,
target = sel.target,
requestor = sel.requestor,
time = sel.time,
property = IntPtr.Zero
}
};
if (sel.selection == _x11.Atoms.CLIPBOARD)
{
resp.SelectionEvent.property = WriteTargetToProperty(sel.target, sel.requestor, sel.property);
}
XSendEvent(_x11.Display, sel.requestor, false, new IntPtr((int)EventMask.NoEventMask), ref resp);
}
else if (ev.type == XEventName.SelectionNotify)
{
if (ev.SelectionEvent.selection == _x11.Atoms.CLIPBOARD_MANAGER &&
ev.SelectionEvent.target == _x11.Atoms.SAVE_TARGETS &&
_x11.Atoms.CLIPBOARD_MANAGER != IntPtr.Zero &&
_x11.Atoms.SAVE_TARGETS != IntPtr.Zero)
{
_storeAtomTcs?.TrySetResult(true);
}
}
IntPtr WriteTargetToProperty(IntPtr target, IntPtr window, IntPtr property)
{
if (target == _x11.Atoms.TARGETS)
{
var atoms = ConvertDataTransfer(_storedDataTransfer);
XChangeProperty(_x11.Display, window, property,
_x11.Atoms.ATOM, 32, PropertyMode.Replace, atoms, atoms.Length);
return property;
}
else if (target == _x11.Atoms.SAVE_TARGETS && _x11.Atoms.SAVE_TARGETS != IntPtr.Zero)
{
return property;
}
else if (ClipboardDataFormatHelper.ToDataFormat(target, _x11.Atoms) is { } dataFormat)
{
if (_storedDataTransfer is null)
return IntPtr.Zero;
// Our default bitmap format is image/png
if (dataFormat.Identifier is "image/png" && _storedDataTransfer.Contains(DataFormat.Bitmap))
dataFormat = DataFormat.Bitmap;
if (!_storedDataTransfer.Contains(dataFormat))
return IntPtr.Zero;
if (TryGetDataAsBytes(_storedDataTransfer, dataFormat, target) is not { } bytes)
return IntPtr.Zero;
_ = SendDataToClientAsync(window, property, target, bytes);
return property;
}
else if (target == _x11.Atoms.MULTIPLE && _x11.Atoms.MULTIPLE != IntPtr.Zero)
{
XGetWindowProperty(_x11.Display, window, property, IntPtr.Zero, new IntPtr(0x7fffffff), false,
_x11.Atoms.ATOM_PAIR, out _, out var actualFormat, out var nitems, out _, out var prop);
if (nitems == IntPtr.Zero)
return IntPtr.Zero;
if (actualFormat == 32)
{
var data = (IntPtr*)prop.ToPointer();
for (var c = 0; c < nitems.ToInt32(); c += 2)
{
var subTarget = data[c];
var subProp = data[c + 1];
var converted = WriteTargetToProperty(subTarget, window, subProp);
data[c + 1] = converted;
}
XChangeProperty(_x11.Display, window, property, _x11.Atoms.ATOM_PAIR, 32, PropertyMode.Replace,
prop.ToPointer(), nitems.ToInt32());
}
XFree(prop);
return property;
}
else
return IntPtr.Zero;
}
}
private byte[]? TryGetDataAsBytes(IAsyncDataTransfer dataTransfer, DataFormat format, IntPtr targetFormatAtom)
{
if (DataFormat.Text.Equals(format))
{
var text = dataTransfer.TryGetValueAsync(DataFormat.Text).GetAwaiter().GetResult();
return ClipboardDataFormatHelper.TryGetStringEncoding(targetFormatAtom, _x11.Atoms) is { } encoding ?
encoding.GetBytes(text ?? string.Empty) :
null;
}
if (DataFormat.Bitmap.Equals(format))
{
if (dataTransfer.TryGetValueAsync(DataFormat.Bitmap).GetAwaiter().GetResult() is not { } bitmap)
return null;
using var stream = new MemoryStream();
bitmap.Save(stream);
return stream.ToArray();
}
if (DataFormat.File.Equals(format))
{
if (dataTransfer.TryGetValuesAsync(DataFormat.File).GetAwaiter().GetResult() is not { } files)
return null;
return ClipboardUriListHelper.FileUriListToUtf8Bytes(files);
}
if (format is DataFormat<string> stringFormat)
{
return dataTransfer.TryGetValueAsync(stringFormat).GetAwaiter().GetResult() is { } stringValue ?
Encoding.UTF8.GetBytes(stringValue) :
null;
}
if (format is DataFormat<byte[]> bytesFormat)
return dataTransfer.TryGetValueAsync(bytesFormat).GetAwaiter().GetResult();
Logger.TryGet(LogEventLevel.Warning, LogArea.X11Platform)
?.Log(this, "Unsupported data format {Format}", format);
return null;
}
private async Task SendIncrDataToClientAsync(IntPtr window, IntPtr property, IntPtr target, Stream data)
{
data.Position = 0;
using var events = new EventStreamWindow(_platform, window);
using var _ = data;
XSelectInput(_x11.Display, window, new IntPtr((int)XEventMask.PropertyChangeMask));
var size = new IntPtr(data.Length);
XChangeProperty(_x11.Display, window, property, _x11.Atoms.INCR, 32, PropertyMode.Replace, ref size, 1);
var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min(_maximumPropertySize, data.Length));
while (true)
{
if (null == await events.WaitForEventAsync(x =>
x.type == XEventName.PropertyNotify && x.PropertyEvent.atom == property
&& x.PropertyEvent.state == 1, TimeSpan.FromMinutes(1)))
break;
var read = await data.ReadAsync(buffer, 0, buffer.Length);
if(read == 0)
break;
XChangeProperty(_x11.Display, window, property, target, 8, PropertyMode.Replace, buffer, read);
}
ArrayPool<byte>.Shared.Return(buffer);
// Finish the transfer
XChangeProperty(_x11.Display, window, property, target, 8, PropertyMode.Replace, IntPtr.Zero, 0);
}
private Task SendDataToClientAsync(IntPtr window, IntPtr property, IntPtr target, byte[] bytes)
{
if (bytes.Length < _maximumPropertySize)
{
XChangeProperty(_x11.Display, window, property, target, 8,
PropertyMode.Replace,
bytes, bytes.Length);
return Task.CompletedTask;
}
return SendIncrDataToClientAsync(window, property, target, new MemoryStream(bytes));
}
private IntPtr GetOwner()
=> XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD);
private ClipboardReadSession OpenReadSession() => new(_platform);
private IntPtr[] ConvertDataTransfer(IAsyncDataTransfer? dataTransfer)
{
var atoms = new List<IntPtr> { _x11.Atoms.TARGETS, _x11.Atoms.MULTIPLE };
if (dataTransfer is not null)
{
foreach (var format in dataTransfer.Formats)
{
foreach (var atom in ClipboardDataFormatHelper.ToAtoms(format, _textAtoms, _x11.Atoms))
atoms.Add(atom);
}
}
return atoms.ToArray();
}
private Task StoreAtomsInClipboardManager(IAsyncDataTransfer dataTransfer)
{
if (_x11.Atoms.CLIPBOARD_MANAGER == IntPtr.Zero || _x11.Atoms.SAVE_TARGETS == IntPtr.Zero)
return Task.CompletedTask;
var clipboardManager = XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD_MANAGER);
if (clipboardManager == IntPtr.Zero)
return Task.CompletedTask;
// Skip storing atoms if the data object contains any non-trivial formats
if (dataTransfer.Formats.Any(f => !DataFormat.Text.Equals(f)))
return Task.CompletedTask;
return StoreTextCoreAsync();
async Task StoreTextCoreAsync()
{
// Skip storing atoms if the trivial formats are too big
var text = await dataTransfer.TryGetTextAsync();
if (text is null || text.Length * 2 > 64 * 1024)
return;
if (_storeAtomTcs is null || _storeAtomTcs.Task.IsCompleted)
_storeAtomTcs = new TaskCompletionSource<bool>();
var atoms = ConvertDataTransfer(dataTransfer);
XChangeProperty(_x11.Display, _handle, _avaloniaSaveTargetsAtom, _x11.Atoms.ATOM, 32,
PropertyMode.Replace, atoms, atoms.Length);
XConvertSelection(_x11.Display, _x11.Atoms.CLIPBOARD_MANAGER, _x11.Atoms.SAVE_TARGETS,
_avaloniaSaveTargetsAtom, _handle, IntPtr.Zero);
await _storeAtomTcs.Task;
}
}
public Task ClearAsync()
{
_storedDataTransfer = null;
XSetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD, IntPtr.Zero, IntPtr.Zero);
return Task.CompletedTask;
}
public async Task<IAsyncDataTransfer?> TryGetDataAsync()
{
var owner = GetOwner();
if (owner == IntPtr.Zero)
return null;
if (owner == _handle && _storedDataTransfer is { } storedDataTransfer)
return storedDataTransfer;
// Get the formats while we're in an async method, since IAsyncDataTransfer.GetFormats() is synchronous.
var (dataFormats, textFormatAtoms) = await GetDataFormatsCoreAsync().ConfigureAwait(false);
if (dataFormats.Length == 0)
return null;
// Get the items while we're in an async method. This does not get values, except for DataFormat.File.
var reader = new ClipboardDataReader(_x11, _platform, textFormatAtoms, owner, dataFormats);
var items = await CreateItemsAsync(reader, dataFormats);
return new ClipboardDataTransfer(reader, dataFormats, items);
}
private async Task<(DataFormat[] DataFormats, IntPtr[] TextFormatAtoms)> GetDataFormatsCoreAsync()
{
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() ?? []);
}
private static async Task<IAsyncDataTransferItem[]> CreateItemsAsync(ClipboardDataReader reader, DataFormat[] formats)
{
List<DataFormat>? nonFileFormats = null;
var items = new List<IAsyncDataTransferItem>();
var hasFiles = false;
foreach (var format in formats)
{
if (DataFormat.File.Equals(format))
{
if (hasFiles)
continue;
// We're reading the filenames ahead of time to generate the appropriate items.
// This is async, so it should be fine.
if (await reader.TryGetAsync(format) is IEnumerable<IStorageItem> storageItems)
{
hasFiles = true;
foreach (var storageItem in storageItems)
items.Add(PlatformDataTransferItem.Create(DataFormat.File, storageItem));
}
}
else
(nonFileFormats ??= new()).Add(format);
}
// Single item containing all formats except for DataFormat.File.
if (nonFileFormats is not null)
items.Add(new ClipboardDataTransferItem(reader, formats));
return items.ToArray();
}
public Task SetDataAsync(IAsyncDataTransfer dataTransfer)
{
_storedDataTransfer = dataTransfer;
XSetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD, _handle, IntPtr.Zero);
return StoreAtomsInClipboardManager(dataTransfer);
}
public Task<bool> IsCurrentOwnerAsync()
=> Task.FromResult(GetOwner() == _handle);
}
}

10
src/Avalonia.X11/Dispatching/X11EventDispatcher.cs

@ -8,16 +8,16 @@ internal class X11EventDispatcher
{
private readonly AvaloniaX11Platform _platform;
private readonly IntPtr _display;
private readonly Dictionary<IntPtr, X11WindowInfo> _windows;
public delegate void EventHandler(ref XEvent xev);
public int Fd { get; }
private readonly Dictionary<IntPtr, EventHandler> _eventHandlers;
public X11EventDispatcher(AvaloniaX11Platform platform)
{
_platform = platform;
_display = platform.Display;
_eventHandlers = platform.Windows;
_windows = platform.Windows;
Fd = XLib.XConnectionNumber(_display);
}
@ -46,8 +46,8 @@ internal class X11EventDispatcher
_platform.XI2.OnEvent((XIEvent*)xev.GenericEventCookie.data);
}
}
else if (_eventHandlers.TryGetValue(xev.AnyEvent.window, out var handler))
handler(ref xev);
else if (_windows.TryGetValue(xev.AnyEvent.window, out var windowInfo))
windowInfo.EventHandler(ref xev);
}
finally
{
@ -59,4 +59,4 @@ internal class X11EventDispatcher
}
public void Flush() => XFlush(_display);
}
}

39
src/Avalonia.X11/Selections/Clipboard/ClipboardDataReader.cs

@ -0,0 +1,39 @@
using System;
using System.Threading.Tasks;
using Avalonia.Input;
using static Avalonia.X11.XLib;
namespace Avalonia.X11.Selections.Clipboard;
/// <summary>
/// An object used to read values, converted to the correct format, from the X11 clipboard.
/// </summary>
internal sealed class ClipboardDataReader(
AvaloniaX11Platform platform,
IntPtr[] textFormatAtoms,
DataFormat[] dataFormats,
IntPtr owner)
: SelectionDataReader<IAsyncDataTransferItem>(platform.Info.Atoms, textFormatAtoms, dataFormats)
{
private IntPtr _owner = owner;
private bool IsOwnerStillValid()
=> _owner != IntPtr.Zero && XGetSelectionOwner(platform.Display, platform.Info.Atoms.CLIPBOARD) == _owner;
public override Task<object?> TryGetAsync(DataFormat format)
{
if (!IsOwnerStillValid())
return Task.FromResult<object?>(null);
return base.TryGetAsync(format);
}
protected override IAsyncDataTransferItem CreateSingleItem(DataFormat[] nonFileFormats)
=> new ClipboardDataTransferItem(this, nonFileFormats);
protected override SelectionReadSession CreateReadSession()
=> ClipboardReadSessionFactory.CreateSession(platform);
public override void Dispose()
=> _owner = IntPtr.Zero;
}

12
src/Avalonia.X11/Clipboard/ClipboardDataTransfer.cs → src/Avalonia.X11/Selections/Clipboard/ClipboardDataTransfer.cs

@ -1,7 +1,7 @@
using Avalonia.Input;
using Avalonia.Input.Platform;
namespace Avalonia.X11.Clipboard;
namespace Avalonia.X11.Selections.Clipboard;
/// <summary>
/// Implementation of <see cref="IAsyncDataTransfer"/> for the X11 clipboard.
@ -19,16 +19,12 @@ internal sealed class ClipboardDataTransfer(
IAsyncDataTransferItem[] items)
: PlatformAsyncDataTransfer
{
private readonly ClipboardDataReader _reader = reader;
private readonly DataFormat[] _formats = formats;
private readonly IAsyncDataTransferItem[] _items = items;
protected override DataFormat[] ProvideFormats()
=> _formats;
=> formats;
protected override IAsyncDataTransferItem[] ProvideItems()
=> _items;
=> items;
public override void Dispose()
=> _reader.Dispose();
=> reader.Dispose();
}

9
src/Avalonia.X11/Clipboard/ClipboardDataTransferItem.cs → src/Avalonia.X11/Selections/Clipboard/ClipboardDataTransferItem.cs

@ -2,7 +2,7 @@ using System.Threading.Tasks;
using Avalonia.Input;
using Avalonia.Input.Platform;
namespace Avalonia.X11.Clipboard;
namespace Avalonia.X11.Selections.Clipboard;
/// <summary>
/// Implementation of <see cref="IAsyncDataTransferItem"/> for the X11 clipboard.
@ -12,12 +12,9 @@ namespace Avalonia.X11.Clipboard;
internal sealed class ClipboardDataTransferItem(ClipboardDataReader reader, DataFormat[] formats)
: PlatformAsyncDataTransferItem
{
private readonly ClipboardDataReader _reader = reader;
private readonly DataFormat[] _formats = formats;
protected override DataFormat[] ProvideFormats()
=> _formats;
=> formats;
protected override Task<object?> TryGetRawCoreAsync(DataFormat format)
=> _reader.TryGetAsync(format);
=> reader.TryGetAsync(format);
}

20
src/Avalonia.X11/Selections/Clipboard/ClipboardReadSessionFactory.cs

@ -0,0 +1,20 @@
using System;
using static Avalonia.X11.XLib;
namespace Avalonia.X11.Selections.Clipboard;
internal static class ClipboardReadSessionFactory
{
public static SelectionReadSession CreateSession(AvaloniaX11Platform platform)
{
var window = new EventStreamWindow(platform);
XSelectInput(platform.Display, window.Handle, new IntPtr((int)XEventMask.PropertyChangeMask));
return new SelectionReadSession(
platform.Display,
window.Handle,
platform.Info.Atoms.CLIPBOARD,
window,
platform.Info.Atoms);
}
}

19
src/Avalonia.X11/Clipboard/EventStreamWindow.cs → src/Avalonia.X11/Selections/Clipboard/EventStreamWindow.cs

@ -5,9 +5,9 @@ using System.Linq;
using System.Threading.Tasks;
using Avalonia.Threading;
namespace Avalonia.X11.Clipboard;
namespace Avalonia.X11.Selections.Clipboard;
internal class EventStreamWindow : IDisposable
internal sealed class EventStreamWindow : IXEventWaiter
{
private readonly AvaloniaX11Platform _platform;
private IntPtr _handle;
@ -27,7 +27,7 @@ internal class EventStreamWindow : IDisposable
{
_isForeign = true;
_handle = foreignWindow.Value;
_platform.Windows[_handle] = OnEvent;
_platform.Windows[_handle] = new X11WindowInfo(OnEvent, null);
}
else
_handle = XLib.CreateEventWindow(platform, OnEvent);
@ -73,22 +73,15 @@ internal class EventStreamWindow : IDisposable
}
}
public Task<XEvent?> WaitForEventAsync(Func<XEvent, bool> predicate, TimeSpan? timeout = null)
public Task<XEvent?> WaitForEventAsync(Func<XEvent, bool> predicate, TimeSpan timeout)
{
timeout ??= TimeSpan.FromSeconds(5);
if (timeout < TimeSpan.Zero)
throw new TimeoutException();
if(timeout > TimeSpan.FromDays(1))
throw new ArgumentOutOfRangeException(nameof(timeout));
var tcs = new TaskCompletionSource<XEvent?>();
_addedListeners.Add((predicate, tcs, _time.Elapsed + timeout.Value));
_addedListeners.Add((predicate, tcs, _time.Elapsed + timeout));
_timeoutTimer.Start();
return tcs.Task;
}
public void Dispose()
{
_timeoutTimer.Stop();

140
src/Avalonia.X11/Selections/Clipboard/X11ClipboardImpl.cs

@ -0,0 +1,140 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Input;
using Avalonia.Input.Platform;
using static Avalonia.X11.XLib;
namespace Avalonia.X11.Selections.Clipboard;
internal sealed class X11ClipboardImpl : SelectionDataProvider, IOwnedClipboardImpl
{
private TaskCompletionSource? _storeAtomTcs;
private IntPtr _handle;
public X11ClipboardImpl(AvaloniaX11Platform platform)
: base(platform, platform.Info.Atoms.CLIPBOARD)
{
_handle = CreateEventWindow(platform, OnEvent);
}
private void OnEvent(ref XEvent evt)
{
if (evt.type == XEventName.SelectionClear)
{
// We might have already regained the clipboard ownership by the time a SelectionClear message arrives.
if (GetOwner() != _handle)
DataTransfer = null;
_storeAtomTcs?.TrySetResult();
}
else if (evt.type == XEventName.SelectionNotify)
{
var atoms = Platform.Info.Atoms;
if (evt.SelectionEvent.selection == atoms.CLIPBOARD_MANAGER &&
evt.SelectionEvent.target == atoms.SAVE_TARGETS)
{
_storeAtomTcs?.TrySetResult();
}
}
else if (evt.type == XEventName.SelectionRequest)
OnSelectionRequest(in evt.SelectionRequestEvent);
}
private SelectionReadSession OpenReadSession()
=> ClipboardReadSessionFactory.CreateSession(Platform);
private Task StoreAtomsInClipboardManager(IAsyncDataTransfer dataTransfer)
{
var atoms = Platform.Info.Atoms;
var clipboardManager = XGetSelectionOwner(Platform.Display, atoms.CLIPBOARD_MANAGER);
if (clipboardManager == IntPtr.Zero)
return Task.CompletedTask;
// Skip storing atoms if the data object contains any non-trivial formats
if (dataTransfer.Formats.Any(f => !DataFormat.Text.Equals(f)))
return Task.CompletedTask;
return StoreTextCoreAsync();
async Task StoreTextCoreAsync()
{
// Skip storing atoms if the trivial formats are too big
var text = await dataTransfer.TryGetTextAsync();
if (text is null || text.Length * 2 > 64 * 1024)
return;
if (_storeAtomTcs is null || _storeAtomTcs.Task.IsCompleted)
_storeAtomTcs = new TaskCompletionSource();
var atomValues = ConvertDataTransfer(dataTransfer);
XChangeProperty(Platform.Display, _handle, 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);
await _storeAtomTcs.Task;
}
}
public Task ClearAsync()
{
DataTransfer = null;
SetOwner(0);
return Task.CompletedTask;
}
public async Task<IAsyncDataTransfer?> TryGetDataAsync()
{
var owner = GetOwner();
if (owner == 0)
return null;
if (owner == _handle && DataTransfer is { } storedDataTransfer)
return storedDataTransfer;
// Get the formats while we're in an async method, since IAsyncDataTransfer.GetFormats() is synchronous.
var (dataFormats, textFormatAtoms) = await GetDataFormatsCoreAsync().ConfigureAwait(false);
if (dataFormats.Length == 0)
return null;
// Get the items while we're in an async method. This does not get values, except for DataFormat.File.
var reader = new ClipboardDataReader(Platform, textFormatAtoms, dataFormats, owner);
var items = await reader.CreateItemsAsync();
return new ClipboardDataTransfer(reader, dataFormats, items);
}
private async Task<(DataFormat[] DataFormats, IntPtr[] TextFormatAtoms)> GetDataFormatsCoreAsync()
{
using var session = OpenReadSession();
var formatAtoms = await session.SendFormatRequest(0) ?? [];
return DataFormatHelper.ToDataFormats(formatAtoms, Platform.Info.Atoms);
}
public Task SetDataAsync(IAsyncDataTransfer dataTransfer)
{
DataTransfer = dataTransfer;
SetOwner(_handle);
return StoreAtomsInClipboardManager(dataTransfer);
}
public Task<bool> IsCurrentOwnerAsync()
=> Task.FromResult(GetOwner() == _handle);
public override void Dispose()
{
if (_handle == 0)
return;
Platform.Windows.Remove(_handle);
XDestroyWindow(Platform.Display, _handle);
_handle = 0;
}
}

65
src/Avalonia.X11/Clipboard/ClipboardDataFormatHelper.cs → src/Avalonia.X11/Selections/DataFormatHelper.cs

@ -1,11 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Controls;
using Avalonia.Input;
namespace Avalonia.X11.Clipboard;
namespace Avalonia.X11.Selections;
internal static class ClipboardDataFormatHelper
internal static class DataFormatHelper
{
private const string MimeTypeTextUriList = "text/uri-list";
private const string AppPrefix = "application/avn-fmt.";
@ -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))
@ -75,10 +117,10 @@ internal static class ClipboardDataFormatHelper
return atoms.GetAtom(systemName);
}
public static IntPtr[] ToAtoms(DataFormat format, IntPtr[] textFormatAtoms, X11Atoms atoms)
public static IntPtr[] ToAtoms(DataFormat format, X11Atoms atoms)
{
if (DataFormat.Text.Equals(format))
return textFormatAtoms;
return atoms.TextFormats;
if (DataFormat.File.Equals(format))
return [atoms.GetAtom(MimeTypeTextUriList)];
@ -90,6 +132,19 @@ internal static class ClipboardDataFormatHelper
return [atoms.GetAtom(systemName)];
}
public static IntPtr[] ToAtoms(IReadOnlyList<DataFormat> formats, X11Atoms atoms)
{
var atomValues = new List<IntPtr>(formats.Count);
foreach (var format in formats)
{
foreach (var atom in ToAtoms(format, atoms))
atomValues.Add(atom);
}
return atomValues.ToArray();
}
private static IntPtr GetPreferredStringFormatAtom(IntPtr[] textFormatAtoms, X11Atoms atoms)
{
ReadOnlySpan<IntPtr> preferredFormats = [atoms.UTF16_STRING, atoms.UTF8_STRING, atoms.STRING];

30
src/Avalonia.X11/Selections/DragDrop/DragDropDataProvider.cs

@ -0,0 +1,30 @@
using System;
using Avalonia.Input;
namespace Avalonia.X11.Selections.DragDrop;
internal sealed class DragDropDataProvider : SelectionDataProvider
{
public Action? Activity { get; set; }
public DragDropDataProvider(AvaloniaX11Platform platform, IAsyncDataTransfer dataTransfer)
: base(platform, platform.Info.Atoms.XdndSelection)
=> DataTransfer = dataTransfer;
public new IntPtr GetOwner()
=> base.GetOwner();
public new void SetOwner(IntPtr window)
=> base.SetOwner(window);
protected override void OnActivity()
=> Activity?.Invoke();
public override void Dispose()
{
Activity = null;
DataTransfer?.Dispose();
DataTransfer = null;
}
}

49
src/Avalonia.X11/Selections/DragDrop/DragDropDataReader.cs

@ -0,0 +1,49 @@
using System;
using System.Diagnostics;
using Avalonia.Input;
using Avalonia.Input.Platform;
namespace Avalonia.X11.Selections.DragDrop;
/// <summary>
/// An object used to read values, converted to the correct format, from a Xdnd selection.
/// </summary>
internal sealed class DragDropDataReader(
X11Atoms atoms,
IntPtr[] textFormatAtoms,
DataFormat[] dataFormats,
IntPtr display,
IntPtr targetWindow)
: SelectionDataReader<PlatformDataTransferItem>(atoms, textFormatAtoms, dataFormats)
{
protected override SelectionReadSession CreateReadSession()
{
var eventWaiter = new SynchronousXEventWaiter(display);
return new SelectionReadSession(display, targetWindow, Atoms.XdndSelection, eventWaiter, Atoms);
}
protected override PlatformDataTransferItem CreateSingleItem(DataFormat[] nonFileFormats)
=> new DragDropDataTransferItem(this, nonFileFormats);
public PlatformDataTransferItem[] CreateItems()
{
// Note: this doesn't cause any deadlock, CreateItemsAsync() will always complete synchronously
// thanks to the SynchronousXEventWaiter used in the SelectionReadSession.
var task = CreateItemsAsync();
Debug.Assert(task.IsCompleted);
return task.GetAwaiter().GetResult();
}
public object? TryGet(DataFormat format)
{
// Note: this doesn't cause any deadlock, TryGetAsync() will always complete synchronously
// thanks to the SynchronousXEventWaiter used in the SelectionReadSession.
var task = TryGetAsync(format);
Debug.Assert(task.IsCompleted);
return task.GetAwaiter().GetResult();
}
public override void Dispose()
{
}
}

40
src/Avalonia.X11/Selections/DragDrop/DragDropDataTransfer.cs

@ -0,0 +1,40 @@
using System;
using Avalonia.Input;
using Avalonia.Input.Platform;
namespace Avalonia.X11.Selections.DragDrop;
/// <summary>
/// Implementation of <see cref="IDataTransfer"/> for data being dragged into an Avalonia window via Xdnd.
/// </summary>
internal sealed class DragDropDataTransfer(
DragDropDataReader reader,
DataFormat[] dataFormats,
IntPtr sourceWindow,
IntPtr targetWindow,
IInputRoot inputRoot)
: PlatformDataTransfer
{
public IntPtr SourceWindow { get; } = sourceWindow;
public IntPtr TargetWindow { get; } = targetWindow;
public IInputRoot InputRoot { get; } = inputRoot;
public Point? LastPosition { get; set; }
public IntPtr LastTimestamp { get; set; }
public DragDropEffects ResultEffects { get; set; } = DragDropEffects.None;
public bool Dropped { get; set; }
protected override DataFormat[] ProvideFormats()
=> dataFormats;
protected override PlatformDataTransferItem[] ProvideItems()
=> reader.CreateItems();
public override void Dispose()
=> reader.Dispose();
}

31
src/Avalonia.X11/Selections/DragDrop/DragDropDataTransferItem.cs

@ -0,0 +1,31 @@
using System.Collections.Generic;
using Avalonia.Input;
using Avalonia.Input.Platform;
namespace Avalonia.X11.Selections.DragDrop;
/// <summary>
/// Implementation of <see cref="IAsyncDataTransferItem"/> for Xdnd.
/// </summary>
/// <param name="reader">The object used to read values.</param>
/// <param name="formats">The formats.</param>
internal sealed class DragDropDataTransferItem(DragDropDataReader reader, DataFormat[] formats)
: PlatformDataTransferItem
{
private Dictionary<DataFormat, object?>? _cachedValues;
protected override DataFormat[] ProvideFormats()
=> formats;
protected override object? TryGetRawCore(DataFormat format)
{
if (_cachedValues is null || !_cachedValues.TryGetValue(format, out var value))
{
value = reader.TryGet(format);
_cachedValues ??= [];
_cachedValues[format] = value;
}
return value;
}
}

35
src/Avalonia.X11/Selections/DragDrop/DragDropTimeoutManager.cs

@ -0,0 +1,35 @@
using System;
using System.Threading;
namespace Avalonia.X11.Selections.DragDrop;
internal sealed class DragDropTimeoutManager : IDisposable
{
private readonly TimeSpan _timeout;
private readonly Timer _timer;
public DragDropTimeoutManager(TimeSpan timeout, Action onTimeout)
{
_timeout = timeout;
_timer = new Timer(
static state => ((Action)state!)(),
onTimeout,
Timeout.InfiniteTimeSpan,
Timeout.InfiniteTimeSpan);
}
public void Restart()
{
try
{
_timer.Change(_timeout, Timeout.InfiniteTimeSpan);
}
catch (ObjectDisposedException)
{
}
}
public void Dispose()
=> _timer.Dispose();
}

13
src/Avalonia.X11/Selections/DragDrop/IXdndWindow.cs

@ -0,0 +1,13 @@
using System;
using Avalonia.Input;
namespace Avalonia.X11.Selections.DragDrop;
internal interface IXdndWindow
{
IntPtr Handle { get; }
IInputRoot? InputRoot { get; }
Point PointToClient(PixelPoint point);
}

124
src/Avalonia.X11/Selections/DragDrop/SynchronousXEventWaiter.cs

@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using static Avalonia.X11.XLib;
namespace Avalonia.X11.Selections.DragDrop;
/// <summary>
/// An implementation of <see cref="IXEventWaiter"/> that waits for an event synchronously by using its own event loop.
/// Unprocessed events are put back onto the main queue after the wait.
/// </summary>
internal sealed partial class SynchronousXEventWaiter(IntPtr display) : IXEventWaiter
{
private const short POLLIN = 0x0001;
public XEvent? WaitForEvent(Func<XEvent, bool> predicate, TimeSpan timeout)
{
var startingTimestamp = Stopwatch.GetTimestamp();
TimeSpan elapsed;
List<XEvent>? stashedEvents = null;
var fd = 0;
try {
while (true)
{
if (!CheckTimeout())
return null;
// First, wait until there's some event in the queue
while (XPending(display) == 0)
{
if (fd == 0)
fd = XConnectionNumber(display);
if (!CheckTimeout())
return null;
var remainingTimeout = timeout - elapsed;
var pollfd = new PollFd
{
fd = fd,
events = POLLIN
};
unsafe
{
if (poll(&pollfd, 1, (int)remainingTimeout.TotalMilliseconds) != 1)
return null;
}
}
if (!CheckTimeout())
return null;
// Second, check if there's an event we're interested in.
// Use XNextEvent and stash the unrelated events, then put them back onto the queue.
// We can't use XIfEvent because it could block indefinitely.
// We can't use XCheckIfEvent either: by leaving the event in the queue, the next poll()
// returns immediately, eating CPU, and we don't want to add arbitrary thread sleeps.
XNextEvent(display, out var evt);
if (evt.type == XEventName.GenericEvent)
{
unsafe
{
XGetEventData(display, &evt.GenericEventCookie);
}
}
if (predicate(evt))
return evt;
stashedEvents ??= [];
stashedEvents.Add(evt);
}
}
finally
{
if (stashedEvents is not null)
{
foreach (var evt in stashedEvents)
{
XPutBackEvent(display, evt);
if (evt.type == XEventName.GenericEvent)
{
unsafe
{
if (evt.GenericEventCookie.data != null)
XFreeEventData(display, &evt.GenericEventCookie);
}
}
}
}
}
bool CheckTimeout()
{
elapsed = Stopwatch.GetElapsedTime(startingTimestamp);
return elapsed < timeout;
}
}
Task<XEvent?> IXEventWaiter.WaitForEventAsync(Func<XEvent, bool> predicate, TimeSpan timeout)
=> Task.FromResult(WaitForEvent(predicate, timeout));
void IDisposable.Dispose()
{
}
[StructLayout(LayoutKind.Sequential)]
private struct PollFd
{
public int fd;
public short events;
public short revents;
}
[LibraryImport("libc", SetLastError = true)]
private static unsafe partial int poll(PollFd* fds, nint nfds, int timeout);
}

494
src/Avalonia.X11/Selections/DragDrop/X11DragSource.cs

@ -0,0 +1,494 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Input.Raw;
using Avalonia.Platform;
using Avalonia.Threading;
using static Avalonia.X11.Selections.DragDrop.XdndConstants;
using static Avalonia.X11.XLib;
namespace Avalonia.X11.Selections.DragDrop;
/// <summary>
/// Implementation of <see cref="IPlatformDragSource"/> for X11 using XDND.
/// Specs: https://www.freedesktop.org/wiki/Specifications/XDND/
/// </summary>
internal sealed class X11DragSource(AvaloniaX11Platform platform) : IPlatformDragSource
{
public async Task<DragDropEffects> DoDragDropAsync(
PointerEventArgs triggerEvent,
IDataTransfer dataTransfer,
DragDropEffects allowedEffects)
{
Dispatcher.UIThread.VerifyAccess();
if (TopLevel.GetTopLevel(triggerEvent.Source as Visual)?.PlatformImpl is not IXdndWindow window)
throw new ArgumentOutOfRangeException(nameof(triggerEvent), "Invalid drag source");
triggerEvent.Pointer.Capture(null);
var cursorFactory = AvaloniaLocator.Current.GetService<ICursorFactory>() as X11CursorFactory;
using var handler = new Handler(platform, window.Handle, dataTransfer, allowedEffects, cursorFactory);
return await handler.Completion;
}
private sealed class Handler : IDisposable
{
private readonly AvaloniaX11Platform _platform;
private readonly IntPtr _sourceWindow;
private readonly IDataTransfer _dataTransfer;
private readonly DragDropEffects _allowedEffects;
private readonly X11CursorFactory? _cursorFactory;
private readonly DragDropDataProvider _dataProvider;
private readonly TaskCompletionSource<DragDropEffects> _completionSource = new();
private readonly IntPtr[] _formatAtoms;
private X11WindowInfo? _originalSourceWindowInfo;
private bool _pointerGrabbed;
private XdndTargetInfo? _lastTarget;
private DragDropEffects _currentEffects;
private DragDropTimeoutManager? _timeoutManager;
public Handler(
AvaloniaX11Platform platform,
IntPtr sourceWindow,
IDataTransfer dataTransfer,
DragDropEffects allowedEffects,
X11CursorFactory? cursorFactory)
{
_platform = platform;
_sourceWindow = sourceWindow;
_dataTransfer = dataTransfer;
_allowedEffects = allowedEffects;
_cursorFactory = cursorFactory;
_currentEffects = allowedEffects;
_dataProvider = new DragDropDataProvider(platform, dataTransfer.ToAsynchronous());
if (!platform.Windows.TryGetValue(sourceWindow, out var sourceWindowInfo))
{
_formatAtoms = [];
Complete(DragDropEffects.None);
return;
}
// Note: in the standard case (starting a drop-drop operation on pointer pressed), X11 already has an
// implicit capture. However, the capture is from our child render window when GL is used, instead of
// the parent window. For now, release any existing capture and start our own.
// TODO: make the render window invisible from input using XShape.
XUngrabPointer(platform.Display, 0);
var grabResult = XGrabPointer(
platform.Display,
_sourceWindow,
false,
EventMask.ButtonPressMask | EventMask.ButtonReleaseMask | EventMask.PointerMotionMask,
GrabMode.GrabModeAsync,
GrabMode.GrabModeAsync,
0,
GetCursor(allowedEffects),
0);
if (grabResult != GrabResult.GrabSuccess)
{
_formatAtoms = [];
Complete(DragDropEffects.None);
return;
}
_pointerGrabbed = true;
// Replace the window event handler with our own during the drag operation
_originalSourceWindowInfo = sourceWindowInfo;
_platform.Windows[_sourceWindow] = new X11WindowInfo(OnEvent, sourceWindowInfo.Window);
var atoms = _platform.Info.Atoms;
_formatAtoms = DataFormatHelper.ToAtoms(dataTransfer.Formats, atoms);
XChangeProperty(
_platform.Display,
_sourceWindow,
atoms.XdndTypeList,
atoms.ATOM,
32,
PropertyMode.Replace,
_formatAtoms,
_formatAtoms.Length);
}
public Task<DragDropEffects> Completion
=> _completionSource.Task;
private void OnEvent(ref XEvent evt)
{
if (evt.type == XEventName.MotionNotify && _pointerGrabbed)
OnMotionNotify(in evt.MotionEvent);
else if (evt.type == XEventName.ButtonRelease && _pointerGrabbed)
OnButtonRelease(in evt.ButtonEvent);
else if (evt.type == XEventName.ClientMessage)
{
ref var message = ref evt.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
_originalSourceWindowInfo?.EventHandler(ref evt);
}
else if (evt.type == XEventName.SelectionRequest)
_dataProvider.OnSelectionRequest(in evt.SelectionRequestEvent);
else
_originalSourceWindowInfo?.EventHandler(ref evt);
}
private void OnMotionNotify(in XMotionEvent motion)
{
var rootPosition = new PixelPoint(motion.x_root, motion.y_root);
var target = FindXdndTarget(rootPosition);
if (_lastTarget != target)
{
if (_lastTarget is { } lastTarget)
{
if (lastTarget.InProcessWindow is { } window)
ProcessRawDragEvent(window, RawDragEventType.DragLeave, rootPosition, motion.state);
else
SendXdndLeave(lastTarget);
}
_lastTarget = target;
UpdateCurrentEffects(_allowedEffects);
if (target is { } newTarget)
{
if (newTarget.InProcessWindow is { } window)
ProcessRawDragEvent(window, RawDragEventType.DragEnter, rootPosition, motion.state);
else
SendXdndEnter(newTarget);
}
}
if (target is { } currentTarget)
{
if (currentTarget.InProcessWindow is { } window)
ProcessRawDragEvent(window, RawDragEventType.DragOver, rootPosition, motion.state);
else
{
var action = XdndActionHelper.EffectsToAction(_allowedEffects, _platform.Info.Atoms);
SendXdndPosition(currentTarget, rootPosition, motion.time, action);
}
}
}
private void OnButtonRelease(in XButtonEvent button)
{
UngrabPointer();
if (_lastTarget is not { } lastTarget)
return;
if (lastTarget.InProcessWindow is not null)
{
var rootPosition = new PixelPoint(button.x_root, button.y_root);
ProcessRawDragEvent(lastTarget.InProcessWindow, RawDragEventType.Drop, rootPosition, button.state);
_lastTarget = null;
Complete(_currentEffects);
}
else
{
_dataProvider.SetOwner(_sourceWindow);
Debug.Assert(_timeoutManager is null);
_timeoutManager = new DragDropTimeoutManager(SelectionHelper.Timeout, () =>
{
DisposeTimeoutManager();
Complete(DragDropEffects.None);
});
_dataProvider.Activity = _timeoutManager.Restart;
SendXdndDrop(lastTarget, button.time);
}
}
private void OnXdndStatus(in XClientMessageEvent message)
{
if (_lastTarget is not { } lastTarget || message.ptr1 != lastTarget.TargetWindow)
return;
var accepted = (message.ptr2 & 1) == 1;
var action = accepted ? message.ptr5 : 0;
var effects = XdndActionHelper.ActionToEffects(action, _platform.Info.Atoms);
UpdateCurrentEffects(effects & _allowedEffects);
}
private void OnXdndFinished(in XClientMessageEvent message)
{
if (_lastTarget is not { } lastTarget || message.ptr1 != lastTarget.TargetWindow)
return;
_lastTarget = null;
DisposeTimeoutManager();
if (lastTarget.Version >= 5)
{
var accepted = (message.ptr2 & 1) == 1;
var action = accepted ? message.ptr3 : 0;
var effects = XdndActionHelper.ActionToEffects(action, _platform.Info.Atoms);
UpdateCurrentEffects(effects & _allowedEffects);
}
Complete(_currentEffects);
}
private XdndTargetInfo? FindXdndTarget(PixelPoint rootPosition)
{
var display = _platform.Display;
var rootWindow = _platform.Info.RootWindow;
var currentWindow = rootWindow;
while (currentWindow != 0)
{
if (TryGetXdndTargetInfo(currentWindow) is { } info)
return info;
if (!XTranslateCoordinates(display, rootWindow, currentWindow, rootPosition.X, rootPosition.Y,
out _, out _, out var childWindow))
{
return null;
}
currentWindow = childWindow;
}
return null;
}
private XdndTargetInfo? TryGetXdndTargetInfo(IntPtr window)
{
// Special case our own windows: we don't need to go through X for them.
if (_platform.Windows.TryGetValue(window, out var windowInfo)
&& windowInfo.Window is { } inProcessWindow)
{
return new XdndTargetInfo(XdndVersion, window, window, inProcessWindow);
}
var proxyWindow = GetXdndProxyWindow(window);
var messageWindow = proxyWindow != 0 ? proxyWindow : window;
var version = GetXdndVersion(messageWindow);
return version != 0 ? new XdndTargetInfo(version, window, messageWindow, null) : null;
}
private IntPtr GetXdndProxyWindow(IntPtr window)
{
var atoms = _platform.Info.Atoms;
// Spec: If this window property exists, it must be of type XA_WINDOW and must contain the ID of the proxy window
// that should be checked for XdndAware and that should receive all the client messages, etc.
var proxyWindow = XGetWindowPropertyAsIntPtr(_platform.Display, window, atoms.XdndProxy, atoms.WINDOW) ?? 0;
if (proxyWindow == 0)
return 0;
// Spec: The proxy window must have the XdndProxy property set to point to itself.
var proxyOnProxy = XGetWindowPropertyAsIntPtr(_platform.Display, proxyWindow, atoms.XdndProxy, atoms.WINDOW) ?? 0;
if (proxyOnProxy != proxyWindow)
return 0;
return proxyWindow;
}
private byte GetXdndVersion(IntPtr window)
{
var atoms = _platform.Info.Atoms;
var version = XGetWindowPropertyAsIntPtr(_platform.Display, window, atoms.XdndAware, atoms.ATOM) ?? 0;
if (version is < MinXdndVersion or > byte.MaxValue)
version = 0;
return (byte)version;
}
private void SendXdndEnter(XdndTargetInfo target)
{
var version = Math.Min(target.Version, XdndVersion);
var hasMoreFormats = _formatAtoms.Length > 3;
SendXdndMessage(
_platform.Info.Atoms.XdndEnter,
target.MessageWindow,
(version << 24) | (hasMoreFormats ? 1 : 0),
_formatAtoms.Length >= 1 ? _formatAtoms[0] : 0,
_formatAtoms.Length >= 2 ? _formatAtoms[1] : 0,
_formatAtoms.Length >= 3 ? _formatAtoms[2] : 0);
}
private void SendXdndPosition(XdndTargetInfo target, PixelPoint rootPosition, IntPtr timestamp, IntPtr action)
{
SendXdndMessage(
_platform.Info.Atoms.XdndPosition,
target.MessageWindow,
0,
(rootPosition.X << 16) | rootPosition.Y,
timestamp,
action);
}
private void SendXdndLeave(XdndTargetInfo target)
{
SendXdndMessage(_platform.Info.Atoms.XdndLeave, target.MessageWindow, 0, 0, 0, 0);
}
private void SendXdndDrop(XdndTargetInfo target, IntPtr timestamp)
{
SendXdndMessage(_platform.Info.Atoms.XdndDrop, target.MessageWindow, 0, timestamp, 0, 0);
}
private void SendXdndMessage(
IntPtr messageType,
IntPtr messageWindow,
IntPtr ptr2,
IntPtr ptr3,
IntPtr ptr4,
IntPtr ptr5)
{
var evt = new XEvent
{
ClientMessageEvent = new XClientMessageEvent
{
type = XEventName.ClientMessage,
display = _platform.Display,
window = messageWindow,
message_type = messageType,
format = 32,
ptr1 = _sourceWindow,
ptr2 = ptr2,
ptr3 = ptr3,
ptr4 = ptr4,
ptr5 = ptr5
}
};
XSendEvent(_platform.Display, messageWindow, false, (IntPtr)EventMask.NoEventMask, ref evt);
XFlush(_platform.Display);
}
private void ProcessRawDragEvent(
X11Window targetWindow,
RawDragEventType eventType,
PixelPoint rootPosition,
XModifierMask modifierMask)
{
if (targetWindow.DragDropDevice is not { } dragDropDevice)
{
UpdateCurrentEffects(DragDropEffects.None);
return;
}
var localPosition = targetWindow.PointToClient(rootPosition);
var modifiers = modifierMask.ToRawInputModifiers();
var dragEvent = new RawDragEvent(
dragDropDevice,
eventType,
targetWindow.InputRoot,
localPosition,
_dataTransfer,
_allowedEffects,
modifiers);
dragDropDevice.ProcessRawEvent(dragEvent);
UpdateCurrentEffects(dragEvent.Effects & _allowedEffects);
}
private void UngrabPointer()
{
_pointerGrabbed = false;
XUngrabPointer(_platform.Display, 0);
XFlush(_platform.Display);
}
private void UpdateCurrentEffects(DragDropEffects effects)
{
if (_currentEffects == effects)
return;
_currentEffects = effects;
if (_pointerGrabbed)
{
XChangeActivePointerGrab(
_platform.Display,
EventMask.ButtonPressMask | EventMask.ButtonReleaseMask | EventMask.PointerMotionMask,
GetCursor(effects),
0);
}
}
private IntPtr GetCursor(DragDropEffects effects)
{
if (_cursorFactory is not null)
{
if ((effects & DragDropEffects.Copy) != 0)
return _cursorFactory.GetCursorHandle(StandardCursorType.DragCopy);
if ((effects & DragDropEffects.Move) != 0)
return _cursorFactory.GetCursorHandle(StandardCursorType.DragMove);
if ((effects & DragDropEffects.Link) != 0)
return _cursorFactory.GetCursorHandle(StandardCursorType.DragLink);
return _cursorFactory.DragNoDropCursorHandle;
}
return _platform.Info.DefaultCursor;
}
private void Complete(DragDropEffects resultEffects)
=> _completionSource.TrySetResult(resultEffects);
private void DisposeTimeoutManager()
{
_dataProvider.Activity = null;
_timeoutManager?.Dispose();
_timeoutManager = null;
}
public void Dispose()
{
if (_pointerGrabbed)
UngrabPointer();
if (_originalSourceWindowInfo is { } originalSourceWindowInfo &&
_platform.Windows.ContainsKey(_sourceWindow))
{
_platform.Windows[_sourceWindow] = originalSourceWindowInfo;
_originalSourceWindowInfo = null;
}
_lastTarget = null;
_currentEffects = DragDropEffects.None;
DisposeTimeoutManager();
if (_dataProvider.GetOwner() == _sourceWindow)
_dataProvider.SetOwner(0);
_dataProvider.Dispose();
}
private readonly record struct XdndTargetInfo(
byte Version,
IntPtr TargetWindow,
IntPtr MessageWindow,
X11Window? InProcessWindow);
}
}

204
src/Avalonia.X11/Selections/DragDrop/X11DropTarget.cs

@ -0,0 +1,204 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Input;
using Avalonia.Input.Raw;
using static Avalonia.X11.Selections.DragDrop.XdndConstants;
using static Avalonia.X11.XLib;
namespace Avalonia.X11.Selections.DragDrop;
/// <summary>
/// Manages an XDND target for a given X11 window.
/// Specs: https://www.freedesktop.org/wiki/Specifications/XDND/
/// </summary>
internal sealed class X11DropTarget
{
private readonly IDragDropDevice _dragDropDevice;
private readonly IXdndWindow _window;
private readonly IntPtr _display;
private readonly X11Atoms _atoms;
private DragDropDataTransfer? _currentDrag;
public X11DropTarget(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.ATOM, 32, PropertyMode.Replace, ref version, 1);
}
public void OnXdndEnter(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 is < MinXdndVersion or > 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 &&
XGetWindowPropertyAsIntPtrArray(_display, sourceWindow, _atoms.XdndTypeList, _atoms.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, textFormats) = DataFormatHelper.ToDataFormats(formats.ToArray(), _atoms);
var reader = new DragDropDataReader(_atoms, textFormats, dataFormats, _display, _window.Handle);
_currentDrag = new DragDropDataTransfer(reader, dataFormats, sourceWindow, _window.Handle, inputRoot);
}
public void OnXdndPosition(in XClientMessageEvent message)
{
if (_currentDrag is not { } drag || message.ptr1 != drag.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 = XdndActionHelper.ActionToEffects(message.ptr5, _atoms);
var eventType = drag.LastPosition is null ? RawDragEventType.DragEnter : RawDragEventType.DragOver;
var modifiers = GetModifiers();
drag.LastPosition = position;
drag.LastTimestamp = message.ptr4;
var dragEvent = new RawDragEvent(
_dragDropDevice,
eventType,
drag.InputRoot,
position,
drag,
requestedEffects,
modifiers);
_dragDropDevice.ProcessRawEvent(dragEvent);
drag.ResultEffects = dragEvent.Effects;
var resultAction = XdndActionHelper.EffectsToAction(dragEvent.Effects, _atoms);
SendXdndMessage(_atoms.XdndStatus, drag, resultAction == 0 ? 0 : 1, 0, 0, resultAction);
}
public void OnXdndLeave(in XClientMessageEvent message)
{
if (_currentDrag is not { } drag || message.ptr1 != drag.SourceWindow)
return;
var modifiers = GetModifiers();
var dragLeave = new RawDragEvent(
_dragDropDevice,
RawDragEventType.DragLeave,
drag.InputRoot,
default,
drag,
DragDropEffects.None,
modifiers);
_dragDropDevice.ProcessRawEvent(dragLeave);
DisposeCurrentDrag();
}
public void OnXdndDrop(in XClientMessageEvent message)
{
if (_currentDrag is not { } drag || message.ptr1 != drag.SourceWindow)
return;
var modifiers = GetModifiers();
var drop = new RawDragEvent(
_dragDropDevice,
RawDragEventType.Drop,
drag.InputRoot,
drag.LastPosition ?? default,
drag,
drag.ResultEffects,
modifiers);
_dragDropDevice.ProcessRawEvent(drop);
drag.ResultEffects = drop.Effects;
drag.Dropped = true;
DisposeCurrentDrag();
}
private void SendXdndMessage(
IntPtr messageType,
DragDropDataTransfer drag,
IntPtr ptr2,
IntPtr ptr3,
IntPtr ptr4,
IntPtr ptr5)
{
var evt = new XEvent
{
ClientMessageEvent = new XClientMessageEvent
{
type = XEventName.ClientMessage,
display = _display,
window = drag.SourceWindow,
message_type = messageType,
format = 32,
ptr1 = drag.TargetWindow,
ptr2 = ptr2,
ptr3 = ptr3,
ptr4 = ptr4,
ptr5 = ptr5
}
};
XSendEvent(_display, drag.SourceWindow, false, (IntPtr)EventMask.NoEventMask, ref evt);
XFlush(_display);
}
private RawInputModifiers GetModifiers()
=> XQueryPointer(_display, _window.Handle, out _, out _, out _, out _, out _, out _, out var mask) ?
mask.ToRawInputModifiers() :
RawInputModifiers.None;
private void DisposeCurrentDrag()
{
if (_currentDrag is not { } drag)
return;
_currentDrag = null;
if (drag.Dropped)
{
var resultAction = XdndActionHelper.EffectsToAction(drag.ResultEffects, _atoms);
SendXdndMessage(_atoms.XdndFinished, drag, resultAction == 0 ? 0 : 1, resultAction, 0, 0);
}
drag.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;
}
}

8
src/Avalonia.X11/Selections/DragDrop/XdndConstants.cs

@ -0,0 +1,8 @@
namespace Avalonia.X11.Selections.DragDrop;
internal static class XdndConstants
{
// Spec: every application that supports XDND version N must also support all previous versions (3 to N-1).
public const byte MinXdndVersion = 3;
public const byte XdndVersion = 5;
}

9
src/Avalonia.X11/Selections/IXEventWaiter.cs

@ -0,0 +1,9 @@
using System;
using System.Threading.Tasks;
namespace Avalonia.X11.Selections;
internal interface IXEventWaiter : IDisposable
{
Task<XEvent?> WaitForEventAsync(Func<XEvent, bool> predicate, TimeSpan timeout);
}

260
src/Avalonia.X11/Selections/SelectionDataProvider.cs

@ -0,0 +1,260 @@
using System;
using System.Buffers;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Input;
using Avalonia.Logging;
using Avalonia.X11.Selections.Clipboard;
using static Avalonia.X11.XLib;
namespace Avalonia.X11.Selections;
/// <summary>
/// Provides an X11 selection (clipboard/drag-and-drop).
/// </summary>
internal abstract class SelectionDataProvider : IDisposable
{
private readonly IntPtr _selection;
private readonly int _maximumPropertySize;
protected AvaloniaX11Platform Platform { get; }
public IAsyncDataTransfer? DataTransfer { get; protected set; }
protected SelectionDataProvider(AvaloniaX11Platform platform, IntPtr selection)
{
Platform = platform;
_selection = selection;
var maxRequestSize = XExtendedMaxRequestSize(Platform.Display);
if (maxRequestSize == 0)
maxRequestSize = XMaxRequestSize(Platform.Display);
_maximumPropertySize = (int)Math.Min(0x100000, maxRequestSize - 0x100);
}
protected IntPtr GetOwner()
=> XGetSelectionOwner(Platform.Display, _selection);
protected void SetOwner(IntPtr owner)
=> XSetSelectionOwner(Platform.Display, _selection, owner, 0);
public void OnSelectionRequest(in XSelectionRequestEvent request)
{
var response = new XEvent
{
SelectionEvent =
{
type = XEventName.SelectionNotify,
send_event = 1,
display = Platform.Display,
selection = request.selection,
target = request.target,
requestor = request.requestor,
time = request.time,
property = 0
}
};
if (request.selection == _selection)
{
response.SelectionEvent.property = WriteTargetToProperty(request.target, request.requestor, request.property);
}
XSendEvent(Platform.Display, request.requestor, false, new IntPtr((int)EventMask.NoEventMask), ref response);
OnActivity();
IntPtr WriteTargetToProperty(IntPtr target, IntPtr window, IntPtr property)
{
var atoms = Platform.Info.Atoms;
if (target == atoms.TARGETS)
{
var atomValues = ConvertDataTransfer(DataTransfer);
XChangeProperty(Platform.Display, window, property, atoms.ATOM, 32, PropertyMode.Replace,
atomValues, atomValues.Length);
return property;
}
if (target == atoms.SAVE_TARGETS)
{
return property;
}
if (DataFormatHelper.ToDataFormat(target, atoms) is { } dataFormat)
{
if (DataTransfer is null)
return 0;
// Our default bitmap format is image/png
if (dataFormat.Identifier is "image/png" && DataTransfer.Contains(DataFormat.Bitmap))
dataFormat = DataFormat.Bitmap;
if (!DataTransfer.Contains(dataFormat))
return 0;
if (TryGetDataAsBytes(DataTransfer, dataFormat, target) is not { } bytes)
return 0;
_ = SendDataToClientAsync(window, property, target, bytes);
return property;
}
if (target == atoms.MULTIPLE)
{
XGetWindowProperty(Platform.Display, window, property, 0, int.MaxValue, false,
atoms.ATOM_PAIR, out _, out var actualFormat, out var nitems, out _, out var prop);
if (nitems == 0)
return 0;
if (actualFormat == 32)
{
unsafe
{
var data = (IntPtr*)prop.ToPointer();
for (var c = 0; c < nitems.ToInt32(); c += 2)
{
var subTarget = data[c];
var subProp = data[c + 1];
var converted = WriteTargetToProperty(subTarget, window, subProp);
data[c + 1] = converted;
}
XChangeProperty(Platform.Display, window, property, atoms.ATOM_PAIR, 32, PropertyMode.Replace,
prop.ToPointer(), nitems.ToInt32());
}
}
XFree(prop);
return property;
}
return 0;
}
}
private byte[]? TryGetDataAsBytes(IAsyncDataTransfer dataTransfer, DataFormat format, IntPtr targetFormatAtom)
{
if (DataFormat.Text.Equals(format))
{
var text = dataTransfer.TryGetValueAsync(DataFormat.Text).GetAwaiter().GetResult();
return DataFormatHelper.TryGetStringEncoding(targetFormatAtom, Platform.Info.Atoms) is { } encoding ?
encoding.GetBytes(text ?? string.Empty) :
null;
}
if (DataFormat.Bitmap.Equals(format))
{
if (dataTransfer.TryGetValueAsync(DataFormat.Bitmap).GetAwaiter().GetResult() is not { } bitmap)
return null;
using var stream = new MemoryStream();
bitmap.Save(stream);
return stream.ToArray();
}
if (DataFormat.File.Equals(format))
{
if (dataTransfer.TryGetValuesAsync(DataFormat.File).GetAwaiter().GetResult() is not { } files)
return null;
return UriListHelper.FileUriListToUtf8Bytes(files);
}
if (format is DataFormat<string> stringFormat)
{
return dataTransfer.TryGetValueAsync(stringFormat).GetAwaiter().GetResult() is { } stringValue ?
Encoding.UTF8.GetBytes(stringValue) :
null;
}
if (format is DataFormat<byte[]> bytesFormat)
return dataTransfer.TryGetValueAsync(bytesFormat).GetAwaiter().GetResult();
Logger.TryGet(LogEventLevel.Warning, LogArea.X11Platform)
?.Log(this, "Unsupported data format {Format}", format);
return null;
}
private async Task SendIncrDataToClientAsync(IntPtr window, IntPtr property, IntPtr target, Stream data)
{
data.Position = 0;
using var events = new EventStreamWindow(Platform, window);
using var _ = data;
XSelectInput(Platform.Display, window, new IntPtr((int)XEventMask.PropertyChangeMask));
var size = new IntPtr(data.Length);
XChangeProperty(Platform.Display, window, property, Platform.Info.Atoms.INCR, 32, PropertyMode.Replace,
ref size, 1);
var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min(_maximumPropertySize, data.Length));
var gotTimeout = false;
while (true)
{
var evt = await events.WaitForEventAsync(
x =>
x.type == XEventName.PropertyNotify &&
x.PropertyEvent.atom == property &&
x.PropertyEvent.state == 1,
SelectionHelper.Timeout);
if (evt is null)
{
gotTimeout = true;
break;
}
var read = await data.ReadAsync(buffer, 0, buffer.Length);
if (read == 0)
break;
XChangeProperty(Platform.Display, window, property, target, 8, PropertyMode.Replace, buffer, read);
OnActivity();
}
ArrayPool<byte>.Shared.Return(buffer);
// Finish the transfer
XChangeProperty(Platform.Display, window, property, target, 8, PropertyMode.Replace, 0, 0);
if (!gotTimeout)
OnActivity();
}
private Task SendDataToClientAsync(IntPtr window, IntPtr property, IntPtr target, byte[] bytes)
{
if (bytes.Length < _maximumPropertySize)
{
XChangeProperty(Platform.Display, window, property, target, 8, PropertyMode.Replace,
bytes, bytes.Length);
OnActivity();
return Task.CompletedTask;
}
return SendIncrDataToClientAsync(window, property, target, new MemoryStream(bytes));
}
protected IntPtr[] ConvertDataTransfer(IAsyncDataTransfer? dataTransfer)
{
var atoms = Platform.Info.Atoms;
var formatAtoms = DataFormatHelper.ToAtoms(dataTransfer?.Formats ?? [], atoms);
return [atoms.TARGETS, atoms.MULTIPLE, ..formatAtoms];
}
protected virtual void OnActivity()
{
}
public abstract void Dispose();
}

108
src/Avalonia.X11/Selections/SelectionDataReader.cs

@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage;
namespace Avalonia.X11.Selections;
/// <summary>
/// An object used to read values, converted to the correct format, from an X11 selection (clipboard/drag-and-drop).
/// </summary>
internal abstract class SelectionDataReader<TItem>(
X11Atoms atoms,
IntPtr[] textFormatAtoms,
DataFormat[] dataFormats)
: IDisposable
where TItem : class
{
protected X11Atoms Atoms { get; } = atoms;
public async Task<TItem[]> CreateItemsAsync()
{
List<DataFormat>? nonFileFormats = null;
var items = new List<TItem>();
var hasFiles = false;
foreach (var format in dataFormats)
{
if (DataFormat.File.Equals(format))
{
if (hasFiles)
continue;
if (await TryGetAsync(format) is IEnumerable<IStorageItem> storageItems)
{
hasFiles = true;
foreach (var storageItem in storageItems)
items.Add((TItem)(object)PlatformDataTransferItem.Create(DataFormat.File, storageItem));
}
}
else
(nonFileFormats ??= []).Add(format);
}
// Single item containing all formats except for DataFormat.File.
if (nonFileFormats is not null)
items.Add(CreateSingleItem(nonFileFormats.ToArray()));
return items.ToArray();
}
public virtual async Task<object?> TryGetAsync(DataFormat format)
{
var formatAtom = DataFormatHelper.ToAtom(format, textFormatAtoms, Atoms, dataFormats);
if (formatAtom == IntPtr.Zero)
return null;
using var session = CreateReadSession();
var result = await session.SendDataRequest(formatAtom, 0).ConfigureAwait(false);
return ConvertDataResult(result, format, formatAtom);
}
protected abstract TItem CreateSingleItem(DataFormat[] nonFileFormats);
protected abstract SelectionReadSession CreateReadSession();
private object? ConvertDataResult(SelectionReadSession.GetDataResult? result, DataFormat format, IntPtr formatAtom)
{
if (result is null)
return null;
if (DataFormat.Text.Equals(format))
{
return DataFormatHelper.TryGetStringEncoding(result.TypeAtom, Atoms) is { } textEncoding ?
textEncoding.GetString(result.AsBytes()) :
null;
}
if (DataFormat.Bitmap.Equals(format))
{
using var data = result.AsStream();
return new Bitmap(data);
}
if (DataFormat.File.Equals(format))
{
// text/uri-list might not be supported
return formatAtom != IntPtr.Zero && result.TypeAtom == formatAtom ?
UriListHelper.Utf8BytesToFileUriList(result.AsBytes()) :
null;
}
if (format is DataFormat<string>)
return Encoding.UTF8.GetString(result.AsBytes());
if (format is DataFormat<byte[]>)
return result.AsBytes();
return null;
}
public abstract void Dispose();
}

8
src/Avalonia.X11/Selections/SelectionHelper.cs

@ -0,0 +1,8 @@
using System;
namespace Avalonia.X11.Selections;
internal static class SelectionHelper
{
public static TimeSpan Timeout { get; } = TimeSpan.FromSeconds(5);
}

74
src/Avalonia.X11/Clipboard/ClipboardReadSession.cs → src/Avalonia.X11/Selections/SelectionReadSession.cs

@ -5,25 +5,22 @@ using System.Runtime.InteropServices;
using System.Threading.Tasks;
using static Avalonia.X11.XLib;
namespace Avalonia.X11.Clipboard;
namespace Avalonia.X11.Selections;
class ClipboardReadSession : IDisposable
/// <summary>
/// A session used to read a X11 selection (Clipboard/Drag-and-Drop) from a given window.
/// </summary>
internal sealed class SelectionReadSession(
IntPtr display,
IntPtr window,
IntPtr selection,
IXEventWaiter eventWaiter,
X11Atoms atoms)
: IDisposable
{
private readonly AvaloniaX11Platform _platform;
private readonly EventStreamWindow _window;
private readonly X11Info _x11;
public void Dispose() => eventWaiter.Dispose();
public ClipboardReadSession(AvaloniaX11Platform platform)
{
_platform = platform;
_window = new EventStreamWindow(platform);
_x11 = _platform.Info;
XSelectInput(_x11.Display, _window.Handle, new IntPtr((int)XEventMask.PropertyChangeMask));
}
public void Dispose() => _window.Dispose();
class PropertyReadResult(IntPtr data, IntPtr actualTypeAtom, int actualFormat, IntPtr nItems)
private sealed class PropertyReadResult(IntPtr data, IntPtr actualTypeAtom, int actualFormat, IntPtr nItems)
: IDisposable
{
public IntPtr Data => data;
@ -40,39 +37,36 @@ class ClipboardReadSession : IDisposable
private async Task<PropertyReadResult?>
WaitForSelectionNotifyAndGetProperty(IntPtr property)
{
var ev = await _window.WaitForEventAsync(ev =>
ev.type == XEventName.SelectionNotify
&& ev.SelectionEvent.selection == _x11.Atoms.CLIPBOARD
&& ev.SelectionEvent.property == property
);
var ev = await eventWaiter.WaitForEventAsync(
ev => ev.type == XEventName.SelectionNotify &&
ev.SelectionEvent.requestor == window &&
ev.SelectionEvent.selection == selection &&
ev.SelectionEvent.property == property,
SelectionHelper.Timeout);
if (ev == null)
return null;
var sel = ev.Value.SelectionEvent;
return ReadProperty(sel.property);
return ReadProperty(property);
}
private PropertyReadResult ReadProperty(IntPtr property)
{
XGetWindowProperty(_x11.Display, _window.Handle, property, IntPtr.Zero, new IntPtr (0x7fffffff), true,
XGetWindowProperty(display, window, property, IntPtr.Zero, new IntPtr (0x7fffffff), true,
(IntPtr)Atom.AnyPropertyType,
out var actualTypeAtom, out var actualFormat, out var nitems, out var bytes_after, out var prop);
out var actualTypeAtom, out var actualFormat, out var nitems, out _, out var prop);
return new (prop, actualTypeAtom, actualFormat, nitems);
}
private Task<PropertyReadResult?> ConvertSelectionAndGetProperty(
IntPtr target, IntPtr property)
private Task<PropertyReadResult?> ConvertSelectionAndGetProperty(IntPtr target, IntPtr property, IntPtr timestamp)
{
XConvertSelection(_platform.Display, _x11.Atoms.CLIPBOARD, target, property, _window.Handle,
IntPtr.Zero);
XConvertSelection(display, selection, target, property, window, timestamp);
return WaitForSelectionNotifyAndGetProperty(property);
}
public async Task<IntPtr[]?> SendFormatRequest()
public async Task<IntPtr[]?> SendFormatRequest(IntPtr targetsAtom)
{
using var res = await ConvertSelectionAndGetProperty(_x11.Atoms.TARGETS, _x11.Atoms.TARGETS);
using var res = await ConvertSelectionAndGetProperty(atoms.TARGETS, atoms.TARGETS, 0);
if (res == null)
return null;
@ -97,7 +91,7 @@ class ClipboardReadSession : IDisposable
private async Task<GetDataResult?> ReadIncr(IntPtr property)
{
XFlush(_platform.Display);
XFlush(display);
var ms = new MemoryStream();
void Append(PropertyReadResult res)
{
@ -110,9 +104,11 @@ class ClipboardReadSession : IDisposable
IntPtr actualTypeAtom = IntPtr.Zero;
while (true)
{
var ev = await _window.WaitForEventAsync(x =>
x is { type: XEventName.PropertyNotify, PropertyEvent.state: 0 } &&
x.PropertyEvent.atom == property);
var ev = await eventWaiter.WaitForEventAsync(
x => x is { type: XEventName.PropertyNotify, PropertyEvent.state: 0 } &&
x.PropertyEvent.window == window &&
x.PropertyEvent.atom == property,
SelectionHelper.Timeout);
if (ev == null)
return null;
@ -131,15 +127,15 @@ class ClipboardReadSession : IDisposable
return new(null, ms, actualTypeAtom);
}
public async Task<GetDataResult?> SendDataRequest(IntPtr format)
public async Task<GetDataResult?> SendDataRequest(IntPtr format, IntPtr timestamp)
{
using var res = await ConvertSelectionAndGetProperty(format, format);
using var res = await ConvertSelectionAndGetProperty(format, format, timestamp);
if (res == null)
return null;
if (res.NItems == IntPtr.Zero)
return null;
if (res.ActualTypeAtom == _x11.Atoms.INCR)
if (res.ActualTypeAtom == atoms.INCR)
{
return await ReadIncr(format);
}

6
src/Avalonia.X11/Clipboard/ClipboardUriListHelper.cs → src/Avalonia.X11/Selections/UriListHelper.cs

@ -5,9 +5,9 @@ using System.Text;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
namespace Avalonia.X11.Clipboard;
namespace Avalonia.X11.Selections;
internal static class ClipboardUriListHelper
internal static class UriListHelper
{
private static readonly Encoding s_utf8NoBomEncoding = new UTF8Encoding(false);
@ -47,6 +47,8 @@ internal static class ClipboardUriListHelper
foreach (var item in items)
writer.WriteLine(item.Path.AbsoluteUri);
writer.Flush();
return stream.ToArray();
}
}

19
src/Avalonia.X11/X11Atoms.cs

@ -192,13 +192,32 @@ namespace Avalonia.X11
public IntPtr _KDE_NET_WM_BLUR_BEHIND_REGION;
public IntPtr INCR;
public IntPtr _NET_WM_STATE_FOCUSED;
public IntPtr AVALONIA_SAVE_TARGETS_PROPERTY_ATOM;
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 IntPtr[] TextFormats { get; }
public X11Atoms(IntPtr display)
{
_display = display;
PopulateAtoms(display);
TextFormats = [STRING, OEMTEXT, UTF8_STRING, UTF16_STRING];
}
private void InitAtom(ref IntPtr field, string name, IntPtr value)

36
src/Avalonia.X11/X11CursorFactory.cs

@ -61,16 +61,28 @@ namespace Avalonia.X11
_cursors = new Dictionary<StandardCursorType, IntPtr>();
}
// We don't have a "DragNo" standard cursor type, but Xcursor provides one
public IntPtr DragNoDropCursorHandle
{
get
{
if (field == 0)
field = LoadDragNoDropCursor();
return field;
}
}
public ICursorImpl GetCursor(StandardCursorType cursorType)
{
IntPtr handle;
if (cursorType == StandardCursorType.None)
handle = _nullCursor;
else
handle = GetCursorHandleCached(cursorType);
var handle = GetCursorHandle(cursorType);
return new CursorImpl(handle);
}
public IntPtr GetCursorHandle(StandardCursorType cursorType)
{
return cursorType == StandardCursorType.None ? _nullCursor : GetCursorHandleCached(cursorType);
}
public unsafe ICursorImpl CreateCursor(Bitmap cursor, PixelPoint hotSpot)
{
return new XImageCursor(_display, cursor, hotSpot);
@ -84,6 +96,15 @@ namespace Avalonia.X11
return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0);
}
private IntPtr LoadDragNoDropCursor()
{
var handle = XLib.XcursorLibraryLoadCursor(_display, "dnd-no-drop");
if (handle == 0)
handle = GetCursorHandleCached(StandardCursorType.No);
return handle;
}
private unsafe class XImageCursor : CursorImpl, IPlatformHandle
{
private readonly IntPtr _display;
@ -130,9 +151,10 @@ namespace Avalonia.X11
{
if (!_cursors.TryGetValue(type, out var handle))
{
if(s_libraryCursors.TryGetValue(type, out var cursorName))
if (s_libraryCursors.TryGetValue(type, out var cursorName))
handle = XLib.XcursorLibraryLoadCursor(_display, cursorName);
else if(s_mapping.TryGetValue(type, out var cursorShape))
if (handle == 0 && s_mapping.TryGetValue(type, out var cursorShape))
handle = XLib.XCreateFontCursor(_display, cursorShape);
if (handle == IntPtr.Zero)

30
src/Avalonia.X11/X11EnumExtensions.cs

@ -0,0 +1,30 @@
using Avalonia.Input;
namespace Avalonia.X11;
internal static class X11EnumExtensions
{
public static RawInputModifiers ToRawInputModifiers(this XModifierMask state)
{
var rv = default(RawInputModifiers);
if (state.HasAllFlags(XModifierMask.Button1Mask))
rv |= RawInputModifiers.LeftMouseButton;
if (state.HasAllFlags(XModifierMask.Button2Mask))
rv |= RawInputModifiers.RightMouseButton;
if (state.HasAllFlags(XModifierMask.Button3Mask))
rv |= RawInputModifiers.MiddleMouseButton;
if (state.HasAllFlags(XModifierMask.Button4Mask))
rv |= RawInputModifiers.XButton1MouseButton;
if (state.HasAllFlags(XModifierMask.Button5Mask))
rv |= RawInputModifiers.XButton2MouseButton;
if (state.HasAllFlags(XModifierMask.ShiftMask))
rv |= RawInputModifiers.Shift;
if (state.HasAllFlags(XModifierMask.ControlMask))
rv |= RawInputModifiers.Control;
if (state.HasAllFlags(XModifierMask.Mod1Mask))
rv |= RawInputModifiers.Alt;
if (state.HasAllFlags(XModifierMask.Mod4Mask))
rv |= RawInputModifiers.Meta;
return rv;
}
}

2
src/Avalonia.X11/X11FocusProxy.cs

@ -41,7 +41,7 @@ namespace Avalonia.X11
_handle = PrepareXWindow(platform.Info.Display, parent);
_platform = platform;
_ownerEventHandler = eventHandler;
_platform.Windows[_handle] = OnEvent;
_platform.Windows[_handle] = new X11WindowInfo(OnEvent, null);
}
internal void Cleanup()

4
src/Avalonia.X11/X11Globals.cs

@ -38,7 +38,7 @@ namespace Avalonia.X11
_x11 = plat.Info;
_screenNumber = XDefaultScreen(_x11.Display);
_rootWindow = XRootWindow(_x11.Display, _screenNumber);
plat.Windows[_rootWindow] = OnRootWindowEvent;
plat.Windows[_rootWindow] = new X11WindowInfo(OnRootWindowEvent, null);
XSelectInput(_x11.Display, _rootWindow,
new IntPtr((int)(EventMask.StructureNotifyMask | EventMask.PropertyChangeMask)));
@ -144,7 +144,7 @@ namespace Avalonia.X11
CompositionAtomOwner = newOwner;
if (CompositionAtomOwner != IntPtr.Zero)
{
_plat.Windows[newOwner] = HandleCompositionAtomOwnerEvents;
_plat.Windows[newOwner] = new X11WindowInfo(HandleCompositionAtomOwnerEvents, null);
XSelectInput(_x11.Display, CompositionAtomOwner, new IntPtr((int)(EventMask.StructureNotifyMask)));
}

8
src/Avalonia.X11/X11Platform.cs

@ -17,11 +17,12 @@ using Avalonia.Rendering.Composition;
using Avalonia.Threading;
using Avalonia.Vulkan;
using Avalonia.X11;
using Avalonia.X11.Clipboard;
using Avalonia.X11.Dispatching;
using Avalonia.X11.Glx;
using Avalonia.X11.Vulkan;
using Avalonia.X11.Screens;
using Avalonia.X11.Selections.Clipboard;
using Avalonia.X11.Selections.DragDrop;
using Avalonia.X11.Vulkan;
using static Avalonia.X11.XLib;
namespace Avalonia.X11
@ -32,7 +33,7 @@ namespace Avalonia.X11
private X11AtSpiAccessibility? _accessibility;
internal AtSpiServer? AtSpiServer => _accessibility?.Server;
public KeyboardDevice KeyboardDevice => _keyboardDevice.Value;
public Dictionary<IntPtr, X11EventDispatcher.EventHandler> Windows { get; } = new ();
public Dictionary<IntPtr, X11WindowInfo> Windows { get; } = new ();
public XI2Manager? XI2 { get; private set; }
public X11Info Info { get; private set; } = null!;
public X11Screens X11Screens { get; private set; } = null!;
@ -94,6 +95,7 @@ namespace Avalonia.X11
.Bind<ICursorFactory>().ToConstant(new X11CursorFactory(Display))
.Bind<IClipboardImpl>().ToConstant(clipboardImpl)
.Bind<IClipboard>().ToConstant(clipboard)
.Bind<IPlatformDragSource>().ToConstant(new X11DragSource(this))
.Bind<IPlatformSettings>().ToSingleton<DBusPlatformSettings>()
.Bind<IPlatformIconLoader>().ToConstant(new X11IconLoader())
.Bind<IMountedVolumeInfoProvider>().ToConstant(new LinuxMountedVolumeInfoProvider())

2
src/Avalonia.X11/X11Window.Ime.cs

@ -116,7 +116,7 @@ namespace Avalonia.X11
{
var physicalKey = X11KeyTransform.PhysicalKeyFromScanCode(ev.KeyEvent.keycode);
var (x11Key, key, symbol) = LookupKey(ref ev.KeyEvent, physicalKey);
var modifiers = TranslateModifiers(ev.KeyEvent.state);
var modifiers = ev.KeyEvent.state.ToRawInputModifiers();
var timestamp = (ulong)ev.KeyEvent.time.ToInt64();
var args = ev.type == XEventName.KeyPress ?

71
src/Avalonia.X11/X11Window.cs

@ -2,31 +2,30 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Avalonia.Reactive;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Dialogs;
using Avalonia.FreeDesktop;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.OpenGL;
using Avalonia.OpenGL.Egl;
using Avalonia.Platform;
using Avalonia.Platform.Surfaces;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
using Avalonia.Platform.Surfaces;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
using Avalonia.X11.Glx;
using Avalonia.X11.NativeDialogs;
using Avalonia.X11.Selections.DragDrop;
using static Avalonia.X11.XLib;
using Avalonia.Input.Platform;
using System.Runtime.InteropServices;
using Avalonia.Dialogs;
using Avalonia.Platform.Storage.FileIO;
// ReSharper disable IdentifierTypo
// ReSharper disable StringLiteralTypo
@ -34,7 +33,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 +71,7 @@ namespace Avalonia.X11
private bool _usePositioningFlags = false;
private X11WindowMode _mode;
private IWindowIconImpl? _iconImpl;
private readonly X11DropTarget? _dropTarget;
private enum XSyncState
{
@ -193,7 +193,7 @@ namespace Avalonia.X11
_mode.OnHandleCreated(_handle);
_realSize = new PixelSize(defaultWidth, defaultHeight);
platform.Windows[_handle] = OnEvent;
platform.Windows[_handle] = new X11WindowInfo(OnEvent, this);
XEventMask ignoredMask = XEventMask.SubstructureRedirectMask
| XEventMask.ResizeRedirectMask
| XEventMask.PointerMotionHintMask;
@ -276,6 +276,12 @@ namespace Avalonia.X11
: null)
});
if (AvaloniaLocator.Current.GetService<IDragDropDevice>() is { } dragDropDevice)
{
DragDropDevice = dragDropDevice;
_dropTarget = new X11DropTarget(dragDropDevice, this, _x11.Display, _x11.Atoms);
}
platform.X11Screens.Changed += OnScreensChanged;
}
@ -512,6 +518,8 @@ namespace Avalonia.X11
public Action? LostFocus { get; set; }
public Compositor Compositor => _platform.Compositor;
public IDragDropDevice? DragDropDevice { get; }
private void OnEvent(ref XEvent ev)
{
@ -572,7 +580,7 @@ namespace Avalonia.X11
: new Vector(-1, 0);
ScheduleInput(new RawMouseWheelEventArgs(_mouse, (ulong)ev.ButtonEvent.time.ToInt64(),
_inputRoot, new Point(ev.ButtonEvent.x, ev.ButtonEvent.y), delta,
TranslateModifiers(ev.ButtonEvent.state)), ref ev);
ev.ButtonEvent.state.ToRawInputModifiers()), ref ev);
}
}
@ -651,7 +659,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)
{
@ -665,6 +674,14 @@ namespace Avalonia.X11
_xSyncState = XSyncState.WaitConfigure;
}
}
else if (messageType == _x11.Atoms.XdndEnter)
_dropTarget?.OnXdndEnter(ev.ClientMessageEvent);
else if (messageType == _x11.Atoms.XdndPosition)
_dropTarget?.OnXdndPosition(ev.ClientMessageEvent);
else if (messageType == _x11.Atoms.XdndLeave)
_dropTarget?.OnXdndLeave(ev.ClientMessageEvent);
else if (messageType == _x11.Atoms.XdndDrop)
_dropTarget?.OnXdndDrop(ev.ClientMessageEvent);
}
else if (ev.type == XEventName.KeyPress || ev.type == XEventName.KeyRelease)
{
@ -838,30 +855,6 @@ namespace Avalonia.X11
}
private static RawInputModifiers TranslateModifiers(XModifierMask state)
{
var rv = default(RawInputModifiers);
if (state.HasAllFlags(XModifierMask.Button1Mask))
rv |= RawInputModifiers.LeftMouseButton;
if (state.HasAllFlags(XModifierMask.Button2Mask))
rv |= RawInputModifiers.RightMouseButton;
if (state.HasAllFlags(XModifierMask.Button3Mask))
rv |= RawInputModifiers.MiddleMouseButton;
if (state.HasAllFlags(XModifierMask.Button4Mask))
rv |= RawInputModifiers.XButton1MouseButton;
if (state.HasAllFlags(XModifierMask.Button5Mask))
rv |= RawInputModifiers.XButton2MouseButton;
if (state.HasAllFlags(XModifierMask.ShiftMask))
rv |= RawInputModifiers.Shift;
if (state.HasAllFlags(XModifierMask.ControlMask))
rv |= RawInputModifiers.Control;
if (state.HasAllFlags(XModifierMask.Mod1Mask))
rv |= RawInputModifiers.Alt;
if (state.HasAllFlags(XModifierMask.Mod4Mask))
rv |= RawInputModifiers.Meta;
return rv;
}
private WindowDecorations _requestedWindowDecorations = WindowDecorations.Full;
private WindowDecorations _windowDecorations = WindowDecorations.Full;
private bool _canResize = true;
@ -969,7 +962,7 @@ namespace Avalonia.X11
return;
var mev = new RawPointerEventArgs(
_mouse, (ulong)ev.ButtonEvent.time.ToInt64(), _inputRoot,
type, new Point(ev.ButtonEvent.x, ev.ButtonEvent.y), TranslateModifiers(mods));
type, new Point(ev.ButtonEvent.x, ev.ButtonEvent.y), mods.ToRawInputModifiers());
ScheduleInput(mev, ref ev);
}
@ -1269,7 +1262,9 @@ namespace Avalonia.X11
}
public IPlatformHandle Handle { get; }
IntPtr IXdndWindow.Handle => _handle;
public PixelPoint Position
{
get

7
src/Avalonia.X11/X11WindowInfo.cs

@ -0,0 +1,7 @@
namespace Avalonia.X11;
internal readonly struct X11WindowInfo(X11EventDispatcher.EventHandler eventHandler, X11Window? window)
{
public X11EventDispatcher.EventHandler EventHandler { get; } = eventHandler;
public X11Window? Window { get; } = window;
}

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

28
src/Avalonia.X11/XLib.cs

@ -60,10 +60,13 @@ namespace Avalonia.X11
public static extern IntPtr XDefaultRootWindow(IntPtr display);
[DllImport(libX11)]
public static extern IntPtr XNextEvent(IntPtr display, out XEvent xevent);
public static extern int XNextEvent(IntPtr display, out XEvent xevent);
[DllImport(libX11)]
public static extern IntPtr XNextEvent(IntPtr display, XEvent* xevent);
public static extern int XNextEvent(IntPtr display, XEvent* xevent);
[LibraryImport(libX11)]
public static partial int XPutBackEvent(IntPtr display, in XEvent evt);
[DllImport(libX11)]
public static extern int XConnectionNumber(IntPtr diplay);
@ -164,7 +167,7 @@ namespace Avalonia.X11
public static extern int XSetWMProtocols(IntPtr display, IntPtr window, IntPtr[] protocols, int count);
[DllImport(libX11)]
public static extern int XGrabPointer(IntPtr display, IntPtr window, bool owner_events, EventMask event_mask,
public static extern GrabResult XGrabPointer(IntPtr display, IntPtr window, bool owner_events, EventMask event_mask,
GrabMode pointer_mode, GrabMode keyboard_mode, IntPtr confine_to, IntPtr cursor, IntPtr timestamp);
[DllImport(libX11)]
@ -172,11 +175,11 @@ namespace Avalonia.X11
[DllImport(libX11)]
public static extern bool XQueryPointer(IntPtr display, IntPtr window, out IntPtr root, out IntPtr child,
out int root_x, out int root_y, out int win_x, out int win_y, out int keys_buttons);
out int root_x, out int root_y, out int win_x, out int win_y, out XModifierMask mask);
[DllImport(libX11)]
public static extern bool XTranslateCoordinates(IntPtr display, IntPtr src_w, IntPtr dest_w, int src_x,
int src_y, out int intdest_x_return, out int dest_y_return, out IntPtr child_return);
int src_y, out int dest_x_return, out int dest_y_return, out IntPtr child_return);
[DllImport(libX11)]
public static extern bool XGetGeometry(IntPtr display, IntPtr window, out IntPtr root, out int x, out int y,
@ -691,7 +694,7 @@ namespace Avalonia.X11
public static void QueryPointer (IntPtr display, IntPtr w, out IntPtr root, out IntPtr child,
out int root_x, out int root_y, out int child_x, out int child_y,
out int mask)
out XModifierMask mask)
{
IntPtr c;
@ -726,7 +729,7 @@ namespace Avalonia.X11
int root_y;
int win_x;
int win_y;
int keys_buttons;
XModifierMask keys_buttons;
@ -748,7 +751,7 @@ namespace Avalonia.X11
{
var win = XCreateSimpleWindow(plat.Display, plat.Info.DefaultRootWindow,
0, 0, 1, 1, 0, IntPtr.Zero, IntPtr.Zero);
plat.Windows[win] = handler;
plat.Windows[win] = new X11WindowInfo(handler, null);
return win;
}
@ -757,5 +760,14 @@ namespace Avalonia.X11
public static int XkbSetGroupForCoreState(int state, int newGroup)
=> (state & ~(0x3 << 13)) | ((newGroup & 0x3) << 13);
public enum GrabResult
{
GrabSuccess = 0,
AlreadyGrabbed = 1,
GrabInvalidTime = 2,
GrabNotViewable = 3,
GrabFrozen = 4,
}
}
}

2
src/tools/DevGenerators/X11AtomsGenerator.cs

@ -72,7 +72,7 @@ public class X11AtomsGenerator : IIncrementalGenerator
classBuilder.Pad(3).Append("\"").Append(writeableFields[c].Name).AppendLine("\",");
classBuilder.Pad(2).AppendLine("};");
classBuilder.Pad(2).AppendLine("XInternAtoms(display, atomNames, atomNames.Length, true, atoms);");
classBuilder.Pad(2).AppendLine("XInternAtoms(display, atomNames, atomNames.Length, false, atoms);");
for (int c = 0; c < writeableFields.Count; c++)
classBuilder.Pad(2).Append("InitAtom(ref ").Append(writeableFields[c].Name).Append(", \"")

Loading…
Cancel
Save