diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml b/samples/ControlCatalog/Pages/DragAndDropPage.xaml index 7982ddc1d0..9b4290ad04 100644 --- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml +++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml @@ -43,14 +43,14 @@ MaxWidth="260" Background="{DynamicResource SystemAccentColorDark1}" DragDrop.AllowDrop="True"> - Drop some text or files here (Copy) + Drop some text, files, bitmap or custom format here (Copy) - Drop some text or files here (Move) + Drop some text or custom format here (Move) diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs index fe2b306477..99e319d819 100644 --- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs +++ b/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 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); } } } diff --git a/src/Avalonia.Base/Input/AsyncDataTransferExtensions.cs b/src/Avalonia.Base/Input/AsyncDataTransferExtensions.cs index cf9d320310..a71a1e744d 100644 --- a/src/Avalonia.Base/Input/AsyncDataTransferExtensions.cs +++ b/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); + /// /// Gets whether a supports a specific format. /// diff --git a/src/Avalonia.Base/Input/SyncToAsyncDataTransfer.cs b/src/Avalonia.Base/Input/SyncToAsyncDataTransfer.cs new file mode 100644 index 0000000000..c228e8b8db --- /dev/null +++ b/src/Avalonia.Base/Input/SyncToAsyncDataTransfer.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; + +namespace Avalonia.Input; + +/// +/// Wraps a into a . +/// +/// The sync object to wrap. +internal sealed class SyncToAsyncDataTransfer(IDataTransfer dataTransfer) + : IDataTransfer, IAsyncDataTransfer +{ + private SyncToAsyncDataTransferItem[]? _items; + + public IReadOnlyList Formats + => dataTransfer.Formats; + + public IReadOnlyList Items + => _items ??= ProvideItems(); + + IReadOnlyList IDataTransfer.Items + => dataTransfer.Items; + + IReadOnlyList 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(); +} diff --git a/src/Avalonia.Base/Input/SyncToAsyncDataTransferItem.cs b/src/Avalonia.Base/Input/SyncToAsyncDataTransferItem.cs new file mode 100644 index 0000000000..705ce0bdee --- /dev/null +++ b/src/Avalonia.Base/Input/SyncToAsyncDataTransferItem.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Avalonia.Input; + +/// +/// Wraps a into a . +/// +/// The sync item to wrap. +internal sealed class SyncToAsyncDataTransferItem(IDataTransferItem dataTransferItem) + : IDataTransferItem, IAsyncDataTransferItem +{ + public IReadOnlyList Formats + => dataTransferItem.Formats; + + public object? TryGetRaw(DataFormat format) + => dataTransferItem.TryGetRaw(format); + + public Task TryGetRawAsync(DataFormat format) + => Task.FromResult(dataTransferItem.TryGetRaw(format)); +} diff --git a/src/Avalonia.Controls/Platform/InProcessDragSource.cs b/src/Avalonia.Controls/Platform/InProcessDragSource.cs index 1a7b719499..e4f41e61ad 100644 --- a/src/Avalonia.Controls/Platform/InProcessDragSource.cs +++ b/src/Avalonia.Controls/Platform/InProcessDragSource.cs @@ -63,6 +63,7 @@ namespace Avalonia.Platform using (_result.Subscribe(new AnonymousObserver(tcs))) { var effect = await tcs.Task; + dataTransfer.Dispose(); return effect; } } diff --git a/src/Avalonia.X11/Clipboard/ClipboardDataReader.cs b/src/Avalonia.X11/Clipboard/ClipboardDataReader.cs deleted file mode 100644 index 910b76385c..0000000000 --- a/src/Avalonia.X11/Clipboard/ClipboardDataReader.cs +++ /dev/null @@ -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; - -/// -/// An object used to read values, converted to the correct format, from the X11 clipboard. -/// -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 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) - return Encoding.UTF8.GetString(result.AsBytes()); - - if (format is DataFormat) - return result.AsBytes(); - - return null; - } - - public void Dispose() - => _owner = IntPtr.Zero; -} diff --git a/src/Avalonia.X11/Clipboard/X11Clipboard.cs b/src/Avalonia.X11/Clipboard/X11Clipboard.cs deleted file mode 100644 index 35abbf11c1..0000000000 --- a/src/Avalonia.X11/Clipboard/X11Clipboard.cs +++ /dev/null @@ -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? _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 stringFormat) - { - return dataTransfer.TryGetValueAsync(stringFormat).GetAwaiter().GetResult() is { } stringValue ? - Encoding.UTF8.GetBytes(stringValue) : - null; - } - - if (format is DataFormat 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.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.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 { _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(); - - 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 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(formatAtoms.Length); - List? 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 CreateItemsAsync(ClipboardDataReader reader, DataFormat[] formats) - { - List? nonFileFormats = null; - var items = new List(); - 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 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 IsCurrentOwnerAsync() - => Task.FromResult(GetOwner() == _handle); - } -} diff --git a/src/Avalonia.X11/Dispatching/X11EventDispatcher.cs b/src/Avalonia.X11/Dispatching/X11EventDispatcher.cs index 36625a61cd..1c1e042334 100644 --- a/src/Avalonia.X11/Dispatching/X11EventDispatcher.cs +++ b/src/Avalonia.X11/Dispatching/X11EventDispatcher.cs @@ -8,16 +8,16 @@ internal class X11EventDispatcher { private readonly AvaloniaX11Platform _platform; private readonly IntPtr _display; + private readonly Dictionary _windows; public delegate void EventHandler(ref XEvent xev); public int Fd { get; } - private readonly Dictionary _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); -} \ No newline at end of file +} diff --git a/src/Avalonia.X11/Selections/Clipboard/ClipboardDataReader.cs b/src/Avalonia.X11/Selections/Clipboard/ClipboardDataReader.cs new file mode 100644 index 0000000000..d3dafbcc84 --- /dev/null +++ b/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; + +/// +/// An object used to read values, converted to the correct format, from the X11 clipboard. +/// +internal sealed class ClipboardDataReader( + AvaloniaX11Platform platform, + IntPtr[] textFormatAtoms, + DataFormat[] dataFormats, + IntPtr owner) + : SelectionDataReader(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 TryGetAsync(DataFormat format) + { + if (!IsOwnerStillValid()) + return Task.FromResult(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; +} diff --git a/src/Avalonia.X11/Clipboard/ClipboardDataTransfer.cs b/src/Avalonia.X11/Selections/Clipboard/ClipboardDataTransfer.cs similarity index 75% rename from src/Avalonia.X11/Clipboard/ClipboardDataTransfer.cs rename to src/Avalonia.X11/Selections/Clipboard/ClipboardDataTransfer.cs index 3c9ef5b821..569a157aa3 100644 --- a/src/Avalonia.X11/Clipboard/ClipboardDataTransfer.cs +++ b/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; /// /// Implementation of 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(); } diff --git a/src/Avalonia.X11/Clipboard/ClipboardDataTransferItem.cs b/src/Avalonia.X11/Selections/Clipboard/ClipboardDataTransferItem.cs similarity index 73% rename from src/Avalonia.X11/Clipboard/ClipboardDataTransferItem.cs rename to src/Avalonia.X11/Selections/Clipboard/ClipboardDataTransferItem.cs index 1b2fcbf5a4..a9c6bb1b39 100644 --- a/src/Avalonia.X11/Clipboard/ClipboardDataTransferItem.cs +++ b/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; /// /// Implementation of 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 TryGetRawCoreAsync(DataFormat format) - => _reader.TryGetAsync(format); + => reader.TryGetAsync(format); } diff --git a/src/Avalonia.X11/Selections/Clipboard/ClipboardReadSessionFactory.cs b/src/Avalonia.X11/Selections/Clipboard/ClipboardReadSessionFactory.cs new file mode 100644 index 0000000000..9233b87c3a --- /dev/null +++ b/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); + } +} diff --git a/src/Avalonia.X11/Clipboard/EventStreamWindow.cs b/src/Avalonia.X11/Selections/Clipboard/EventStreamWindow.cs similarity index 87% rename from src/Avalonia.X11/Clipboard/EventStreamWindow.cs rename to src/Avalonia.X11/Selections/Clipboard/EventStreamWindow.cs index 1c9edbde49..42729de647 100644 --- a/src/Avalonia.X11/Clipboard/EventStreamWindow.cs +++ b/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 WaitForEventAsync(Func predicate, TimeSpan? timeout = null) + public Task WaitForEventAsync(Func 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(); - _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(); diff --git a/src/Avalonia.X11/Selections/Clipboard/X11ClipboardImpl.cs b/src/Avalonia.X11/Selections/Clipboard/X11ClipboardImpl.cs new file mode 100644 index 0000000000..a2503ef893 --- /dev/null +++ b/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 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 IsCurrentOwnerAsync() + => Task.FromResult(GetOwner() == _handle); + + public override void Dispose() + { + if (_handle == 0) + return; + + Platform.Windows.Remove(_handle); + XDestroyWindow(Platform.Display, _handle); + _handle = 0; + } +} diff --git a/src/Avalonia.X11/Clipboard/ClipboardDataFormatHelper.cs b/src/Avalonia.X11/Selections/DataFormatHelper.cs similarity index 66% rename from src/Avalonia.X11/Clipboard/ClipboardDataFormatHelper.cs rename to src/Avalonia.X11/Selections/DataFormatHelper.cs index 5e53ff96c1..aac23afb80 100644 --- a/src/Avalonia.X11/Clipboard/ClipboardDataFormatHelper.cs +++ b/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(formatAtoms.Length); + List? 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 formats, X11Atoms atoms) + { + var atomValues = new List(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 preferredFormats = [atoms.UTF16_STRING, atoms.UTF8_STRING, atoms.STRING]; diff --git a/src/Avalonia.X11/Selections/DragDrop/DragDropDataProvider.cs b/src/Avalonia.X11/Selections/DragDrop/DragDropDataProvider.cs new file mode 100644 index 0000000000..c8727f0c87 --- /dev/null +++ b/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; + } +} diff --git a/src/Avalonia.X11/Selections/DragDrop/DragDropDataReader.cs b/src/Avalonia.X11/Selections/DragDrop/DragDropDataReader.cs new file mode 100644 index 0000000000..379851751a --- /dev/null +++ b/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; + +/// +/// An object used to read values, converted to the correct format, from a Xdnd selection. +/// +internal sealed class DragDropDataReader( + X11Atoms atoms, + IntPtr[] textFormatAtoms, + DataFormat[] dataFormats, + IntPtr display, + IntPtr targetWindow) + : SelectionDataReader(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() + { + } +} diff --git a/src/Avalonia.X11/Selections/DragDrop/DragDropDataTransfer.cs b/src/Avalonia.X11/Selections/DragDrop/DragDropDataTransfer.cs new file mode 100644 index 0000000000..b1dc030800 --- /dev/null +++ b/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; + +/// +/// Implementation of for data being dragged into an Avalonia window via Xdnd. +/// +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(); +} diff --git a/src/Avalonia.X11/Selections/DragDrop/DragDropDataTransferItem.cs b/src/Avalonia.X11/Selections/DragDrop/DragDropDataTransferItem.cs new file mode 100644 index 0000000000..b6c87bf6dc --- /dev/null +++ b/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; + +/// +/// Implementation of for Xdnd. +/// +/// The object used to read values. +/// The formats. +internal sealed class DragDropDataTransferItem(DragDropDataReader reader, DataFormat[] formats) + : PlatformDataTransferItem +{ + private Dictionary? _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; + } +} diff --git a/src/Avalonia.X11/Selections/DragDrop/DragDropTimeoutManager.cs b/src/Avalonia.X11/Selections/DragDrop/DragDropTimeoutManager.cs new file mode 100644 index 0000000000..673eb49ec4 --- /dev/null +++ b/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(); +} diff --git a/src/Avalonia.X11/Selections/DragDrop/IXdndWindow.cs b/src/Avalonia.X11/Selections/DragDrop/IXdndWindow.cs new file mode 100644 index 0000000000..32a2e130fe --- /dev/null +++ b/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); +} diff --git a/src/Avalonia.X11/Selections/DragDrop/SynchronousXEventWaiter.cs b/src/Avalonia.X11/Selections/DragDrop/SynchronousXEventWaiter.cs new file mode 100644 index 0000000000..9234207062 --- /dev/null +++ b/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; + +/// +/// An implementation of that waits for an event synchronously by using its own event loop. +/// Unprocessed events are put back onto the main queue after the wait. +/// +internal sealed partial class SynchronousXEventWaiter(IntPtr display) : IXEventWaiter +{ + private const short POLLIN = 0x0001; + + public XEvent? WaitForEvent(Func predicate, TimeSpan timeout) + { + var startingTimestamp = Stopwatch.GetTimestamp(); + TimeSpan elapsed; + List? 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 IXEventWaiter.WaitForEventAsync(Func 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); +} diff --git a/src/Avalonia.X11/Selections/DragDrop/X11DragSource.cs b/src/Avalonia.X11/Selections/DragDrop/X11DragSource.cs new file mode 100644 index 0000000000..ebbbe0a78f --- /dev/null +++ b/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; + +/// +/// Implementation of for X11 using XDND. +/// Specs: https://www.freedesktop.org/wiki/Specifications/XDND/ +/// +internal sealed class X11DragSource(AvaloniaX11Platform platform) : IPlatformDragSource +{ + public async Task 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() 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 _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 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); + } +} diff --git a/src/Avalonia.X11/Selections/DragDrop/X11DropTarget.cs b/src/Avalonia.X11/Selections/DragDrop/X11DropTarget.cs new file mode 100644 index 0000000000..26a910ec90 --- /dev/null +++ b/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; + +/// +/// Manages an XDND target for a given X11 window. +/// Specs: https://www.freedesktop.org/wiki/Specifications/XDND/ +/// +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(); + + 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(); + } +} diff --git a/src/Avalonia.X11/Selections/DragDrop/XdndActionHelper.cs b/src/Avalonia.X11/Selections/DragDrop/XdndActionHelper.cs new file mode 100644 index 0000000000..b8fbc17e36 --- /dev/null +++ b/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; + } +} diff --git a/src/Avalonia.X11/Selections/DragDrop/XdndConstants.cs b/src/Avalonia.X11/Selections/DragDrop/XdndConstants.cs new file mode 100644 index 0000000000..53223b3f36 --- /dev/null +++ b/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; +} diff --git a/src/Avalonia.X11/Selections/IXEventWaiter.cs b/src/Avalonia.X11/Selections/IXEventWaiter.cs new file mode 100644 index 0000000000..98398b615f --- /dev/null +++ b/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 WaitForEventAsync(Func predicate, TimeSpan timeout); +} diff --git a/src/Avalonia.X11/Selections/SelectionDataProvider.cs b/src/Avalonia.X11/Selections/SelectionDataProvider.cs new file mode 100644 index 0000000000..c1a7be87fb --- /dev/null +++ b/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; + +/// +/// Provides an X11 selection (clipboard/drag-and-drop). +/// +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 stringFormat) + { + return dataTransfer.TryGetValueAsync(stringFormat).GetAwaiter().GetResult() is { } stringValue ? + Encoding.UTF8.GetBytes(stringValue) : + null; + } + + if (format is DataFormat 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.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.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(); +} diff --git a/src/Avalonia.X11/Selections/SelectionDataReader.cs b/src/Avalonia.X11/Selections/SelectionDataReader.cs new file mode 100644 index 0000000000..de6ff13653 --- /dev/null +++ b/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; + +/// +/// An object used to read values, converted to the correct format, from an X11 selection (clipboard/drag-and-drop). +/// +internal abstract class SelectionDataReader( + X11Atoms atoms, + IntPtr[] textFormatAtoms, + DataFormat[] dataFormats) + : IDisposable + where TItem : class +{ + protected X11Atoms Atoms { get; } = atoms; + + public async Task CreateItemsAsync() + { + List? nonFileFormats = null; + var items = new List(); + var hasFiles = false; + + foreach (var format in dataFormats) + { + if (DataFormat.File.Equals(format)) + { + if (hasFiles) + continue; + + if (await TryGetAsync(format) is IEnumerable 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 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) + return Encoding.UTF8.GetString(result.AsBytes()); + + if (format is DataFormat) + return result.AsBytes(); + + return null; + } + + public abstract void Dispose(); +} diff --git a/src/Avalonia.X11/Selections/SelectionHelper.cs b/src/Avalonia.X11/Selections/SelectionHelper.cs new file mode 100644 index 0000000000..1a6cd81f8c --- /dev/null +++ b/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); +} diff --git a/src/Avalonia.X11/Clipboard/ClipboardReadSession.cs b/src/Avalonia.X11/Selections/SelectionReadSession.cs similarity index 64% rename from src/Avalonia.X11/Clipboard/ClipboardReadSession.cs rename to src/Avalonia.X11/Selections/SelectionReadSession.cs index 7c83ecea40..207af91309 100644 --- a/src/Avalonia.X11/Clipboard/ClipboardReadSession.cs +++ b/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 +/// +/// A session used to read a X11 selection (Clipboard/Drag-and-Drop) from a given window. +/// +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 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 ConvertSelectionAndGetProperty( - IntPtr target, IntPtr property) + private Task 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 SendFormatRequest() + public async Task 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 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 SendDataRequest(IntPtr format) + public async Task 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); } diff --git a/src/Avalonia.X11/Clipboard/ClipboardUriListHelper.cs b/src/Avalonia.X11/Selections/UriListHelper.cs similarity index 93% rename from src/Avalonia.X11/Clipboard/ClipboardUriListHelper.cs rename to src/Avalonia.X11/Selections/UriListHelper.cs index da175c5828..cb838a1661 100644 --- a/src/Avalonia.X11/Clipboard/ClipboardUriListHelper.cs +++ b/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(); } } diff --git a/src/Avalonia.X11/X11Atoms.cs b/src/Avalonia.X11/X11Atoms.cs index 64b00c411b..5fbc87a549 100644 --- a/src/Avalonia.X11/X11Atoms.cs +++ b/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 _namesToAtoms = new Dictionary(); private readonly Dictionary _atomsToNames = new Dictionary(); + + 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) diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index 1ac4c1fe8d..d68eed8c15 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -61,16 +61,28 @@ namespace Avalonia.X11 _cursors = new Dictionary(); } + // 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) diff --git a/src/Avalonia.X11/X11EnumExtensions.cs b/src/Avalonia.X11/X11EnumExtensions.cs new file mode 100644 index 0000000000..a374a24bbd --- /dev/null +++ b/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; + } +} diff --git a/src/Avalonia.X11/X11FocusProxy.cs b/src/Avalonia.X11/X11FocusProxy.cs index 4856dbef65..7bcede3a53 100644 --- a/src/Avalonia.X11/X11FocusProxy.cs +++ b/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() diff --git a/src/Avalonia.X11/X11Globals.cs b/src/Avalonia.X11/X11Globals.cs index b9e4058b2d..9f2fb69c93 100644 --- a/src/Avalonia.X11/X11Globals.cs +++ b/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))); } diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 566b0d907a..fac9e32ef7 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/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 Windows { get; } = new (); + public Dictionary 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().ToConstant(new X11CursorFactory(Display)) .Bind().ToConstant(clipboardImpl) .Bind().ToConstant(clipboard) + .Bind().ToConstant(new X11DragSource(this)) .Bind().ToSingleton() .Bind().ToConstant(new X11IconLoader()) .Bind().ToConstant(new LinuxMountedVolumeInfoProvider()) diff --git a/src/Avalonia.X11/X11Window.Ime.cs b/src/Avalonia.X11/X11Window.Ime.cs index 768a79af3d..e51934e65d 100644 --- a/src/Avalonia.X11/X11Window.Ime.cs +++ b/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 ? diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index bf20600a18..37eff383db 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/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() 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 diff --git a/src/Avalonia.X11/X11WindowInfo.cs b/src/Avalonia.X11/X11WindowInfo.cs new file mode 100644 index 0000000000..37a19a8874 --- /dev/null +++ b/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; +} diff --git a/src/Avalonia.X11/XLib.Helpers.cs b/src/Avalonia.X11/XLib.Helpers.cs index b32d5e3b26..813a00e88d 100644 --- a/src/Avalonia.X11/XLib.Helpers.cs +++ b/src/Avalonia.X11/XLib.Helpers.cs @@ -23,7 +23,34 @@ internal static partial class XLib } finally { - XFree(prop); + if (prop != 0) + XFree(prop); } } -} \ No newline at end of file + + 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); + } + } +} diff --git a/src/Avalonia.X11/XLib.cs b/src/Avalonia.X11/XLib.cs index 595e996733..089ab03c0b 100644 --- a/src/Avalonia.X11/XLib.cs +++ b/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, + } } } diff --git a/src/tools/DevGenerators/X11AtomsGenerator.cs b/src/tools/DevGenerators/X11AtomsGenerator.cs index 920b3477dc..e91016af3e 100644 --- a/src/tools/DevGenerators/X11AtomsGenerator.cs +++ b/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(", \"")