21 changed files with 299 additions and 207 deletions
@ -1,78 +0,0 @@ |
|||
using System; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Input; |
|||
using Avalonia.Media.Imaging; |
|||
using static Avalonia.X11.XLib; |
|||
|
|||
namespace Avalonia.X11.Clipboard; |
|||
|
|||
/// <summary>
|
|||
/// An object used to read values, converted to the correct format, from the X11 clipboard.
|
|||
/// </summary>
|
|||
internal sealed class ClipboardDataReader( |
|||
X11Info x11, |
|||
AvaloniaX11Platform platform, |
|||
IntPtr[] textFormatAtoms, |
|||
IntPtr owner, |
|||
DataFormat[] dataFormats) |
|||
: IDisposable |
|||
{ |
|||
private IntPtr _owner = owner; |
|||
|
|||
private bool IsOwnerStillValid() |
|||
=> _owner != IntPtr.Zero && XGetSelectionOwner(x11.Display, x11.Atoms.CLIPBOARD) == _owner; |
|||
|
|||
public async Task<object?> TryGetAsync(DataFormat format) |
|||
{ |
|||
if (!IsOwnerStillValid()) |
|||
return null; |
|||
|
|||
var formatAtom = ClipboardDataFormatHelper.ToAtom(format, textFormatAtoms, x11.Atoms, dataFormats); |
|||
if (formatAtom == IntPtr.Zero) |
|||
return null; |
|||
|
|||
using var session = ClipboardReadSessionFactory.CreateSession(platform); |
|||
var result = await session.SendDataRequest(formatAtom, 0).ConfigureAwait(false); |
|||
return ConvertDataResult(result, format, formatAtom); |
|||
} |
|||
|
|||
private object? ConvertDataResult(SelectionReadSession.GetDataResult? result, DataFormat format, IntPtr formatAtom) |
|||
{ |
|||
if (result is null) |
|||
return null; |
|||
|
|||
if (DataFormat.Text.Equals(format)) |
|||
{ |
|||
return ClipboardDataFormatHelper.TryGetStringEncoding(result.TypeAtom, x11.Atoms) is { } textEncoding ? |
|||
textEncoding.GetString(result.AsBytes()) : |
|||
null; |
|||
} |
|||
|
|||
if (DataFormat.Bitmap.Equals(format)) |
|||
{ |
|||
using var data = result.AsStream(); |
|||
|
|||
return new Bitmap(data); |
|||
} |
|||
|
|||
if (DataFormat.File.Equals(format)) |
|||
{ |
|||
// text/uri-list might not be supported
|
|||
return formatAtom != IntPtr.Zero && result.TypeAtom == formatAtom ? |
|||
ClipboardUriListHelper.Utf8BytesToFileUriList(result.AsBytes()) : |
|||
null; |
|||
} |
|||
|
|||
if (format is DataFormat<string>) |
|||
return Encoding.UTF8.GetString(result.AsBytes()); |
|||
|
|||
if (format is DataFormat<byte[]>) |
|||
return result.AsBytes(); |
|||
|
|||
return null; |
|||
} |
|||
|
|||
public void Dispose() |
|||
=> _owner = IntPtr.Zero; |
|||
} |
|||
@ -1,15 +0,0 @@ |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
|
|||
namespace Avalonia.X11.DragDrop; |
|||
|
|||
internal sealed class DragDropDataTransferItem(DataFormat[] formats) : PlatformDataTransferItem |
|||
{ |
|||
protected override DataFormat[] ProvideFormats() |
|||
=> formats; |
|||
|
|||
protected override object? TryGetRawCore(DataFormat format) |
|||
{ |
|||
throw new System.NotImplementedException(); |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Input; |
|||
using static Avalonia.X11.XLib; |
|||
|
|||
namespace Avalonia.X11.Selections.Clipboard; |
|||
|
|||
/// <summary>
|
|||
/// An object used to read values, converted to the correct format, from the X11 clipboard.
|
|||
/// </summary>
|
|||
internal sealed class ClipboardDataReader( |
|||
AvaloniaX11Platform platform, |
|||
IntPtr[] textFormatAtoms, |
|||
DataFormat[] dataFormats, |
|||
IntPtr owner) |
|||
: SelectionDataReader<IAsyncDataTransferItem>(platform.Info.Atoms, textFormatAtoms, dataFormats) |
|||
{ |
|||
private IntPtr _owner = owner; |
|||
|
|||
private bool IsOwnerStillValid() |
|||
=> _owner != IntPtr.Zero && XGetSelectionOwner(platform.Display, platform.Info.Atoms.CLIPBOARD) == _owner; |
|||
|
|||
public override Task<object?> TryGetAsync(DataFormat format) |
|||
{ |
|||
if (!IsOwnerStillValid()) |
|||
return Task.FromResult<object?>(null); |
|||
|
|||
return base.TryGetAsync(format); |
|||
} |
|||
|
|||
protected override IAsyncDataTransferItem CreateSingleItem(DataFormat[] nonFileFormats) |
|||
=> new ClipboardDataTransferItem(this, nonFileFormats); |
|||
|
|||
protected override SelectionReadSession CreateReadSession() |
|||
=> ClipboardReadSessionFactory.CreateSession(platform); |
|||
|
|||
public override void Dispose() |
|||
=> _owner = IntPtr.Zero; |
|||
} |
|||
@ -1,7 +1,7 @@ |
|||
using System; |
|||
using static Avalonia.X11.XLib; |
|||
|
|||
namespace Avalonia.X11.Clipboard; |
|||
namespace Avalonia.X11.Selections.Clipboard; |
|||
|
|||
internal static class ClipboardReadSessionFactory |
|||
{ |
|||
@ -0,0 +1,49 @@ |
|||
using System; |
|||
using System.Diagnostics; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
|
|||
namespace Avalonia.X11.Selections.DragDrop; |
|||
|
|||
/// <summary>
|
|||
/// An object used to read values, converted to the correct format, from a Xdnd selection.
|
|||
/// </summary>
|
|||
internal sealed class DragDropDataReader( |
|||
X11Atoms atoms, |
|||
IntPtr[] textFormatAtoms, |
|||
DataFormat[] dataFormats, |
|||
IntPtr display, |
|||
IntPtr targetWindow) |
|||
: SelectionDataReader<PlatformDataTransferItem>(atoms, textFormatAtoms, dataFormats) |
|||
{ |
|||
protected override SelectionReadSession CreateReadSession() |
|||
{ |
|||
var eventWaiter = new SynchronousXEventWaiter(display); |
|||
return new SelectionReadSession(display, targetWindow, Atoms.XdndSelection, eventWaiter, Atoms); |
|||
} |
|||
|
|||
protected override PlatformDataTransferItem CreateSingleItem(DataFormat[] nonFileFormats) |
|||
=> new DragDropDataTransferItem(this, nonFileFormats); |
|||
|
|||
public PlatformDataTransferItem[] CreateItems() |
|||
{ |
|||
// Note: this doesn't cause any deadlock, CreateItemsAsync() will always complete synchronously
|
|||
// thanks to the SynchronousXEventWaiter used in the SelectionReadSession.
|
|||
var task = CreateItemsAsync(); |
|||
Debug.Assert(task.IsCompleted); |
|||
return task.GetAwaiter().GetResult(); |
|||
} |
|||
|
|||
public object? TryGet(DataFormat format) |
|||
{ |
|||
// Note: this doesn't cause any deadlock, TryGetAsync() will always complete synchronously
|
|||
// thanks to the SynchronousXEventWaiter used in the SelectionReadSession.
|
|||
var task = TryGetAsync(format); |
|||
Debug.Assert(task.IsCompleted); |
|||
return task.GetAwaiter().GetResult(); |
|||
} |
|||
|
|||
public override void Dispose() |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
using System.Collections.Generic; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
|
|||
namespace Avalonia.X11.Selections.DragDrop; |
|||
|
|||
/// <summary>
|
|||
/// Implementation of <see cref="IAsyncDataTransferItem"/> for Xdnd.
|
|||
/// </summary>
|
|||
/// <param name="reader">The object used to read values.</param>
|
|||
/// <param name="formats">The formats.</param>
|
|||
internal sealed class DragDropDataTransferItem(DragDropDataReader reader, DataFormat[] formats) |
|||
: PlatformDataTransferItem |
|||
{ |
|||
private Dictionary<DataFormat, object?>? _cachedValues; |
|||
|
|||
protected override DataFormat[] ProvideFormats() |
|||
=> formats; |
|||
|
|||
protected override object? TryGetRawCore(DataFormat format) |
|||
{ |
|||
if (_cachedValues is null || !_cachedValues.TryGetValue(format, out var value)) |
|||
{ |
|||
value = reader.TryGet(format); |
|||
_cachedValues ??= []; |
|||
_cachedValues[format] = value; |
|||
} |
|||
|
|||
return value; |
|||
} |
|||
} |
|||
@ -1,7 +1,7 @@ |
|||
using System; |
|||
using Avalonia.Input; |
|||
|
|||
namespace Avalonia.X11.DragDrop; |
|||
namespace Avalonia.X11.Selections.DragDrop; |
|||
|
|||
internal interface IXdndWindow |
|||
{ |
|||
@ -1,9 +1,9 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Avalonia.X11.DragDrop; |
|||
namespace Avalonia.X11.Selections; |
|||
|
|||
internal interface IEventWaiter : IDisposable |
|||
internal interface IXEventWaiter : IDisposable |
|||
{ |
|||
Task<XEvent?> WaitForEventAsync(Func<XEvent, bool> predicate, TimeSpan timeout); |
|||
} |
|||
@ -0,0 +1,108 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
using Avalonia.Media.Imaging; |
|||
using Avalonia.Platform.Storage; |
|||
|
|||
namespace Avalonia.X11.Selections; |
|||
|
|||
/// <summary>
|
|||
/// An object used to read values, converted to the correct format, from an X11 selection (clipboard/drag-and-drop).
|
|||
/// </summary>
|
|||
internal abstract class SelectionDataReader<TItem>( |
|||
X11Atoms atoms, |
|||
IntPtr[] textFormatAtoms, |
|||
DataFormat[] dataFormats) |
|||
: IDisposable |
|||
where TItem : class |
|||
{ |
|||
protected X11Atoms Atoms { get; } = atoms; |
|||
|
|||
public async Task<TItem[]> CreateItemsAsync() |
|||
{ |
|||
List<DataFormat>? nonFileFormats = null; |
|||
var items = new List<TItem>(); |
|||
var hasFiles = false; |
|||
|
|||
foreach (var format in dataFormats) |
|||
{ |
|||
if (DataFormat.File.Equals(format)) |
|||
{ |
|||
if (hasFiles) |
|||
continue; |
|||
|
|||
if (await TryGetAsync(format) is IEnumerable<IStorageItem> storageItems) |
|||
{ |
|||
hasFiles = true; |
|||
|
|||
foreach (var storageItem in storageItems) |
|||
items.Add((TItem)(object)PlatformDataTransferItem.Create(DataFormat.File, storageItem)); |
|||
} |
|||
} |
|||
else |
|||
(nonFileFormats ??= []).Add(format); |
|||
} |
|||
|
|||
// Single item containing all formats except for DataFormat.File.
|
|||
if (nonFileFormats is not null) |
|||
items.Add(CreateSingleItem(nonFileFormats.ToArray())); |
|||
|
|||
return items.ToArray(); |
|||
} |
|||
|
|||
public virtual async Task<object?> TryGetAsync(DataFormat format) |
|||
{ |
|||
var formatAtom = DataFormatHelper.ToAtom(format, textFormatAtoms, Atoms, dataFormats); |
|||
if (formatAtom == IntPtr.Zero) |
|||
return null; |
|||
|
|||
using var session = CreateReadSession(); |
|||
var result = await session.SendDataRequest(formatAtom, 0).ConfigureAwait(false); |
|||
return ConvertDataResult(result, format, formatAtom); |
|||
} |
|||
|
|||
protected abstract TItem CreateSingleItem(DataFormat[] nonFileFormats); |
|||
|
|||
protected abstract SelectionReadSession CreateReadSession(); |
|||
|
|||
private object? ConvertDataResult(SelectionReadSession.GetDataResult? result, DataFormat format, IntPtr formatAtom) |
|||
{ |
|||
if (result is null) |
|||
return null; |
|||
|
|||
if (DataFormat.Text.Equals(format)) |
|||
{ |
|||
return DataFormatHelper.TryGetStringEncoding(result.TypeAtom, Atoms) is { } textEncoding ? |
|||
textEncoding.GetString(result.AsBytes()) : |
|||
null; |
|||
} |
|||
|
|||
if (DataFormat.Bitmap.Equals(format)) |
|||
{ |
|||
using var data = result.AsStream(); |
|||
|
|||
return new Bitmap(data); |
|||
} |
|||
|
|||
if (DataFormat.File.Equals(format)) |
|||
{ |
|||
// text/uri-list might not be supported
|
|||
return formatAtom != IntPtr.Zero && result.TypeAtom == formatAtom ? |
|||
UriListHelper.Utf8BytesToFileUriList(result.AsBytes()) : |
|||
null; |
|||
} |
|||
|
|||
if (format is DataFormat<string>) |
|||
return Encoding.UTF8.GetString(result.AsBytes()); |
|||
|
|||
if (format is DataFormat<byte[]>) |
|||
return result.AsBytes(); |
|||
|
|||
return null; |
|||
} |
|||
|
|||
public abstract void Dispose(); |
|||
} |
|||
Loading…
Reference in new issue