committed by
GitHub
45 changed files with 2022 additions and 660 deletions
@ -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(); |
|||
} |
|||
@ -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,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; |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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() |
|||
{ |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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(); |
|||
} |
|||
@ -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(); |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.X11.Selections; |
|||
|
|||
internal static class SelectionHelper |
|||
{ |
|||
public static TimeSpan Timeout { get; } = TimeSpan.FromSeconds(5); |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
Loading…
Reference in new issue