3 changed files with 402 additions and 332 deletions
@ -1,332 +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 static Avalonia.X11.XLib; |
|||
|
|||
namespace Avalonia.X11.Selections.Clipboard |
|||
{ |
|||
internal sealed class X11ClipboardImpl : IOwnedClipboardImpl |
|||
{ |
|||
private readonly AvaloniaX11Platform _platform; |
|||
private readonly X11Info _x11; |
|||
private IAsyncDataTransfer? _storedDataTransfer; |
|||
private readonly IntPtr _handle; |
|||
private TaskCompletionSource<bool>? _storeAtomTcs; |
|||
private readonly int _maximumPropertySize; |
|||
|
|||
public X11ClipboardImpl(AvaloniaX11Platform platform) |
|||
{ |
|||
_platform = platform; |
|||
_x11 = platform.Info; |
|||
_handle = CreateEventWindow(platform, OnEvent); |
|||
|
|||
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 (DataFormatHelper.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 DataFormatHelper.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 UriListHelper.FileUriListToUtf8Bytes(files); |
|||
} |
|||
|
|||
if (format is DataFormat<string> stringFormat) |
|||
{ |
|||
return dataTransfer.TryGetValueAsync(stringFormat).GetAwaiter().GetResult() is { } stringValue ? |
|||
Encoding.UTF8.GetBytes(stringValue) : |
|||
null; |
|||
} |
|||
|
|||
if (format is DataFormat<byte[]> bytesFormat) |
|||
return dataTransfer.TryGetValueAsync(bytesFormat).GetAwaiter().GetResult(); |
|||
|
|||
Logger.TryGet(LogEventLevel.Warning, LogArea.X11Platform) |
|||
?.Log(this, "Unsupported data format {Format}", format); |
|||
|
|||
return null; |
|||
} |
|||
|
|||
private async Task SendIncrDataToClientAsync(IntPtr window, IntPtr property, IntPtr target, Stream data) |
|||
{ |
|||
data.Position = 0; |
|||
using var events = new EventStreamWindow(_platform, window); |
|||
using var _ = data; |
|||
XSelectInput(_x11.Display, window, new IntPtr((int)XEventMask.PropertyChangeMask)); |
|||
var size = new IntPtr(data.Length); |
|||
XChangeProperty(_x11.Display, window, property, _x11.Atoms.INCR, 32, PropertyMode.Replace, ref size, 1); |
|||
var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min(_maximumPropertySize, data.Length)); |
|||
while (true) |
|||
{ |
|||
if (null == await events.WaitForEventAsync(x => |
|||
x.type == XEventName.PropertyNotify && x.PropertyEvent.atom == property |
|||
&& x.PropertyEvent.state == 1, TimeSpan.FromMinutes(1))) |
|||
break; |
|||
var read = await data.ReadAsync(buffer, 0, buffer.Length); |
|||
if(read == 0) |
|||
break; |
|||
XChangeProperty(_x11.Display, window, property, target, 8, PropertyMode.Replace, buffer, read); |
|||
} |
|||
ArrayPool<byte>.Shared.Return(buffer); |
|||
|
|||
// Finish the transfer
|
|||
XChangeProperty(_x11.Display, window, property, target, 8, PropertyMode.Replace, IntPtr.Zero, 0); |
|||
} |
|||
|
|||
private Task SendDataToClientAsync(IntPtr window, IntPtr property, IntPtr target, byte[] bytes) |
|||
{ |
|||
if (bytes.Length < _maximumPropertySize) |
|||
{ |
|||
XChangeProperty(_x11.Display, window, property, target, 8, |
|||
PropertyMode.Replace, |
|||
bytes, bytes.Length); |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
return SendIncrDataToClientAsync(window, property, target, new MemoryStream(bytes)); |
|||
} |
|||
|
|||
private IntPtr GetOwner() |
|||
=> XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD); |
|||
|
|||
private SelectionReadSession OpenReadSession() => ClipboardReadSessionFactory.CreateSession(_platform); |
|||
|
|||
private IntPtr[] ConvertDataTransfer(IAsyncDataTransfer? dataTransfer) |
|||
{ |
|||
var atoms = new List<IntPtr> { _x11.Atoms.TARGETS, _x11.Atoms.MULTIPLE }; |
|||
|
|||
if (dataTransfer is not null) |
|||
{ |
|||
foreach (var format in dataTransfer.Formats) |
|||
{ |
|||
foreach (var atom in DataFormatHelper.ToAtoms(format, _x11.Atoms)) |
|||
atoms.Add(atom); |
|||
} |
|||
} |
|||
|
|||
return atoms.ToArray(); |
|||
} |
|||
|
|||
private Task StoreAtomsInClipboardManager(IAsyncDataTransfer dataTransfer) |
|||
{ |
|||
if (_x11.Atoms.CLIPBOARD_MANAGER == IntPtr.Zero || _x11.Atoms.SAVE_TARGETS == IntPtr.Zero) |
|||
return Task.CompletedTask; |
|||
|
|||
var clipboardManager = XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD_MANAGER); |
|||
if (clipboardManager == IntPtr.Zero) |
|||
return Task.CompletedTask; |
|||
|
|||
// Skip storing atoms if the data object contains any non-trivial formats
|
|||
if (dataTransfer.Formats.Any(f => !DataFormat.Text.Equals(f))) |
|||
return Task.CompletedTask; |
|||
|
|||
return StoreTextCoreAsync(); |
|||
|
|||
async Task StoreTextCoreAsync() |
|||
{ |
|||
// Skip storing atoms if the trivial formats are too big
|
|||
var text = await dataTransfer.TryGetTextAsync(); |
|||
if (text is null || text.Length * 2 > 64 * 1024) |
|||
return; |
|||
|
|||
if (_storeAtomTcs is null || _storeAtomTcs.Task.IsCompleted) |
|||
_storeAtomTcs = new TaskCompletionSource<bool>(); |
|||
|
|||
var atoms = ConvertDataTransfer(dataTransfer); |
|||
XChangeProperty(_x11.Display, _handle, _x11.Atoms.AVALONIA_SAVE_TARGETS_PROPERTY_ATOM, _x11.Atoms.ATOM, 32, |
|||
PropertyMode.Replace, atoms, atoms.Length); |
|||
XConvertSelection(_x11.Display, _x11.Atoms.CLIPBOARD_MANAGER, _x11.Atoms.SAVE_TARGETS, |
|||
_x11.Atoms.AVALONIA_SAVE_TARGETS_PROPERTY_ATOM, _handle, IntPtr.Zero); |
|||
await _storeAtomTcs.Task; |
|||
} |
|||
} |
|||
|
|||
public Task ClearAsync() |
|||
{ |
|||
_storedDataTransfer = null; |
|||
XSetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD, IntPtr.Zero, IntPtr.Zero); |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
public async Task<IAsyncDataTransfer?> TryGetDataAsync() |
|||
{ |
|||
var owner = GetOwner(); |
|||
if (owner == IntPtr.Zero) |
|||
return null; |
|||
|
|||
if (owner == _handle && _storedDataTransfer is { } storedDataTransfer) |
|||
return storedDataTransfer; |
|||
|
|||
// Get the formats while we're in an async method, since IAsyncDataTransfer.GetFormats() is synchronous.
|
|||
var (dataFormats, textFormatAtoms) = await GetDataFormatsCoreAsync().ConfigureAwait(false); |
|||
if (dataFormats.Length == 0) |
|||
return null; |
|||
|
|||
// Get the items while we're in an async method. This does not get values, except for DataFormat.File.
|
|||
var reader = new ClipboardDataReader(_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, _x11.Atoms); |
|||
} |
|||
|
|||
public Task SetDataAsync(IAsyncDataTransfer dataTransfer) |
|||
{ |
|||
_storedDataTransfer = dataTransfer; |
|||
XSetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD, _handle, IntPtr.Zero); |
|||
return StoreAtomsInClipboardManager(dataTransfer); |
|||
} |
|||
|
|||
public Task<bool> IsCurrentOwnerAsync() |
|||
=> Task.FromResult(GetOwner() == _handle); |
|||
} |
|||
} |
|||
@ -0,0 +1,124 @@ |
|||
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(AvaloniaX11Platform platform) |
|||
: SelectionDataProvider(platform, platform.Info.Atoms.CLIPBOARD), IOwnedClipboardImpl |
|||
{ |
|||
private TaskCompletionSource? _storeAtomTcs; |
|||
|
|||
protected override 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) |
|||
DataTransfer = null; |
|||
|
|||
_storeAtomTcs?.TrySetResult(); |
|||
} |
|||
|
|||
else if (ev.type == XEventName.SelectionNotify) |
|||
{ |
|||
var atoms = Platform.Info.Atoms; |
|||
|
|||
if (ev.SelectionEvent.selection == atoms.CLIPBOARD_MANAGER && |
|||
ev.SelectionEvent.target == atoms.SAVE_TARGETS) |
|||
{ |
|||
_storeAtomTcs?.TrySetResult(); |
|||
} |
|||
} |
|||
|
|||
else |
|||
base.OnEvent(ref ev); |
|||
} |
|||
|
|||
private SelectionReadSession OpenReadSession() |
|||
=> ClipboardReadSessionFactory.CreateSession(Platform); |
|||
|
|||
private Task StoreAtomsInClipboardManager(IAsyncDataTransfer dataTransfer) |
|||
{ |
|||
var atoms = Platform.Info.Atoms; |
|||
|
|||
var clipboardManager = XGetSelectionOwner(Platform.Display, atoms.CLIPBOARD_MANAGER); |
|||
if (clipboardManager == IntPtr.Zero) |
|||
return Task.CompletedTask; |
|||
|
|||
// Skip storing atoms if the data object contains any non-trivial formats
|
|||
if (dataTransfer.Formats.Any(f => !DataFormat.Text.Equals(f))) |
|||
return Task.CompletedTask; |
|||
|
|||
return StoreTextCoreAsync(); |
|||
|
|||
async Task StoreTextCoreAsync() |
|||
{ |
|||
// Skip storing atoms if the trivial formats are too big
|
|||
var text = await dataTransfer.TryGetTextAsync(); |
|||
if (text is null || text.Length * 2 > 64 * 1024) |
|||
return; |
|||
|
|||
if (_storeAtomTcs is null || _storeAtomTcs.Task.IsCompleted) |
|||
_storeAtomTcs = new TaskCompletionSource(); |
|||
|
|||
var atomValues = ConvertDataTransfer(dataTransfer); |
|||
|
|||
XChangeProperty(Platform.Display, Handle, atoms.AVALONIA_SAVE_TARGETS_PROPERTY_ATOM, atoms.ATOM, 32, |
|||
PropertyMode.Replace, atomValues, atomValues.Length); |
|||
|
|||
XConvertSelection(Platform.Display, atoms.CLIPBOARD_MANAGER, atoms.SAVE_TARGETS, |
|||
atoms.AVALONIA_SAVE_TARGETS_PROPERTY_ATOM, Handle, 0); |
|||
|
|||
await _storeAtomTcs.Task; |
|||
} |
|||
} |
|||
|
|||
public Task ClearAsync() |
|||
{ |
|||
DataTransfer = null; |
|||
SetOwner(0); |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
public async Task<IAsyncDataTransfer?> TryGetDataAsync() |
|||
{ |
|||
var owner = GetOwner(); |
|||
if (owner == 0) |
|||
return null; |
|||
|
|||
if (owner == Handle && DataTransfer is { } storedDataTransfer) |
|||
return storedDataTransfer; |
|||
|
|||
// Get the formats while we're in an async method, since IAsyncDataTransfer.GetFormats() is synchronous.
|
|||
var (dataFormats, textFormatAtoms) = await GetDataFormatsCoreAsync().ConfigureAwait(false); |
|||
if (dataFormats.Length == 0) |
|||
return null; |
|||
|
|||
// Get the items while we're in an async method. This does not get values, except for DataFormat.File.
|
|||
var reader = new ClipboardDataReader(Platform, textFormatAtoms, dataFormats, owner); |
|||
var items = await reader.CreateItemsAsync(); |
|||
return new ClipboardDataTransfer(reader, dataFormats, items); |
|||
} |
|||
|
|||
private async Task<(DataFormat[] DataFormats, IntPtr[] TextFormatAtoms)> GetDataFormatsCoreAsync() |
|||
{ |
|||
using var session = OpenReadSession(); |
|||
|
|||
var formatAtoms = await session.SendFormatRequest(0) ?? []; |
|||
return DataFormatHelper.ToDataFormats(formatAtoms, Platform.Info.Atoms); |
|||
} |
|||
|
|||
public Task SetDataAsync(IAsyncDataTransfer dataTransfer) |
|||
{ |
|||
DataTransfer = dataTransfer; |
|||
SetOwner(Handle); |
|||
return StoreAtomsInClipboardManager(dataTransfer); |
|||
} |
|||
|
|||
public Task<bool> IsCurrentOwnerAsync() |
|||
=> Task.FromResult(GetOwner() == Handle); |
|||
} |
|||
@ -0,0 +1,278 @@ |
|||
using System; |
|||
using System.Buffers; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Input; |
|||
using Avalonia.Logging; |
|||
using Avalonia.X11.Selections.Clipboard; |
|||
using static Avalonia.X11.XLib; |
|||
|
|||
namespace Avalonia.X11.Selections; |
|||
|
|||
/// <summary>
|
|||
/// Provides an X11 selection (clipboard/drag-and-drop) via a dedicated window.
|
|||
/// </summary>
|
|||
internal abstract class SelectionDataProvider : IDisposable |
|||
{ |
|||
private readonly int _maximumPropertySize; |
|||
|
|||
protected AvaloniaX11Platform Platform { get; } |
|||
|
|||
protected IntPtr Selection { get; } |
|||
|
|||
protected IntPtr Handle { get; private set; } |
|||
|
|||
public IAsyncDataTransfer? DataTransfer { get; 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); |
|||
|
|||
Handle = CreateEventWindow(platform, OnEvent); |
|||
} |
|||
|
|||
protected IntPtr GetOwner() |
|||
=> XGetSelectionOwner(Platform.Display, Selection); |
|||
|
|||
protected void SetOwner(IntPtr owner) |
|||
=> XSetSelectionOwner(Platform.Display, Selection, owner, 0); |
|||
|
|||
protected virtual void OnEvent(ref XEvent ev) |
|||
{ |
|||
if (ev.type == XEventName.SelectionRequest) |
|||
{ |
|||
var sel = ev.SelectionRequestEvent; |
|||
var resp = new XEvent |
|||
{ |
|||
SelectionEvent = |
|||
{ |
|||
type = XEventName.SelectionNotify, |
|||
send_event = 1, |
|||
display = Platform.Display, |
|||
selection = sel.selection, |
|||
target = sel.target, |
|||
requestor = sel.requestor, |
|||
time = sel.time, |
|||
property = 0 |
|||
} |
|||
}; |
|||
|
|||
if (sel.selection == Selection) |
|||
{ |
|||
resp.SelectionEvent.property = WriteTargetToProperty(sel.target, sel.requestor, sel.property); |
|||
} |
|||
|
|||
XSendEvent(Platform.Display, sel.requestor, false, new IntPtr((int)EventMask.NoEventMask), ref resp); |
|||
} |
|||
|
|||
IntPtr WriteTargetToProperty(IntPtr target, IntPtr window, IntPtr property) |
|||
{ |
|||
var atoms = Platform.Info.Atoms; |
|||
|
|||
if (target == atoms.TARGETS) |
|||
{ |
|||
var atomValues = ConvertDataTransfer(DataTransfer); |
|||
XChangeProperty(Platform.Display, window, property, atoms.ATOM, 32, PropertyMode.Replace, |
|||
atomValues, atomValues.Length); |
|||
return property; |
|||
} |
|||
|
|||
if (target == atoms.SAVE_TARGETS) |
|||
{ |
|||
return property; |
|||
} |
|||
|
|||
if (DataFormatHelper.ToDataFormat(target, atoms) is { } dataFormat) |
|||
{ |
|||
if (DataTransfer is null) |
|||
return 0; |
|||
|
|||
// Our default bitmap format is image/png
|
|||
if (dataFormat.Identifier is "image/png" && DataTransfer.Contains(DataFormat.Bitmap)) |
|||
dataFormat = DataFormat.Bitmap; |
|||
|
|||
if (!DataTransfer.Contains(dataFormat)) |
|||
return 0; |
|||
|
|||
if (TryGetDataAsBytes(DataTransfer, dataFormat, target) is not { } bytes) |
|||
return 0; |
|||
|
|||
_ = SendDataToClientAsync(window, property, target, bytes); |
|||
return property; |
|||
} |
|||
|
|||
if (target == atoms.MULTIPLE) |
|||
{ |
|||
XGetWindowProperty(Platform.Display, window, property, 0, int.MaxValue, false, |
|||
atoms.ATOM_PAIR, out _, out var actualFormat, out var nitems, out _, out var prop); |
|||
|
|||
if (nitems == 0) |
|||
return 0; |
|||
|
|||
if (actualFormat == 32) |
|||
{ |
|||
unsafe |
|||
{ |
|||
var data = (IntPtr*)prop.ToPointer(); |
|||
for (var c = 0; c < nitems.ToInt32(); c += 2) |
|||
{ |
|||
var subTarget = data[c]; |
|||
var subProp = data[c + 1]; |
|||
var converted = WriteTargetToProperty(subTarget, window, subProp); |
|||
data[c + 1] = converted; |
|||
} |
|||
|
|||
XChangeProperty(Platform.Display, window, property, atoms.ATOM_PAIR, 32, PropertyMode.Replace, |
|||
prop.ToPointer(), nitems.ToInt32()); |
|||
} |
|||
} |
|||
|
|||
XFree(prop); |
|||
|
|||
return property; |
|||
} |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
} |
|||
|
|||
private byte[]? TryGetDataAsBytes(IAsyncDataTransfer dataTransfer, DataFormat format, IntPtr targetFormatAtom) |
|||
{ |
|||
if (DataFormat.Text.Equals(format)) |
|||
{ |
|||
var text = dataTransfer.TryGetValueAsync(DataFormat.Text).GetAwaiter().GetResult(); |
|||
|
|||
return DataFormatHelper.TryGetStringEncoding(targetFormatAtom, Platform.Info.Atoms) is { } encoding ? |
|||
encoding.GetBytes(text ?? string.Empty) : |
|||
null; |
|||
} |
|||
|
|||
if (DataFormat.Bitmap.Equals(format)) |
|||
{ |
|||
if (dataTransfer.TryGetValueAsync(DataFormat.Bitmap).GetAwaiter().GetResult() is not { } bitmap) |
|||
return null; |
|||
|
|||
using var stream = new MemoryStream(); |
|||
bitmap.Save(stream); |
|||
|
|||
return stream.ToArray(); |
|||
} |
|||
|
|||
if (DataFormat.File.Equals(format)) |
|||
{ |
|||
if (dataTransfer.TryGetValuesAsync(DataFormat.File).GetAwaiter().GetResult() is not { } files) |
|||
return null; |
|||
|
|||
return UriListHelper.FileUriListToUtf8Bytes(files); |
|||
} |
|||
|
|||
if (format is DataFormat<string> stringFormat) |
|||
{ |
|||
return dataTransfer.TryGetValueAsync(stringFormat).GetAwaiter().GetResult() is { } stringValue ? |
|||
Encoding.UTF8.GetBytes(stringValue) : |
|||
null; |
|||
} |
|||
|
|||
if (format is DataFormat<byte[]> bytesFormat) |
|||
return dataTransfer.TryGetValueAsync(bytesFormat).GetAwaiter().GetResult(); |
|||
|
|||
Logger.TryGet(LogEventLevel.Warning, LogArea.X11Platform) |
|||
?.Log(this, "Unsupported data format {Format}", format); |
|||
|
|||
return null; |
|||
} |
|||
|
|||
private async Task SendIncrDataToClientAsync(IntPtr window, IntPtr property, IntPtr target, Stream data) |
|||
{ |
|||
data.Position = 0; |
|||
|
|||
using var events = new EventStreamWindow(Platform, window); |
|||
using var _ = data; |
|||
|
|||
XSelectInput(Platform.Display, window, new IntPtr((int)XEventMask.PropertyChangeMask)); |
|||
|
|||
var size = new IntPtr(data.Length); |
|||
XChangeProperty(Platform.Display, window, property, Platform.Info.Atoms.INCR, 32, PropertyMode.Replace, |
|||
ref size, 1); |
|||
|
|||
var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min(_maximumPropertySize, data.Length)); |
|||
|
|||
while (true) |
|||
{ |
|||
var evt = await events.WaitForEventAsync( |
|||
x => |
|||
x.type == XEventName.PropertyNotify && |
|||
x.PropertyEvent.atom == property && |
|||
x.PropertyEvent.state == 1, |
|||
TimeSpan.FromMinutes(1)); |
|||
|
|||
if (evt is null) |
|||
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); |
|||
} |
|||
|
|||
ArrayPool<byte>.Shared.Return(buffer); |
|||
|
|||
// Finish the transfer
|
|||
XChangeProperty(Platform.Display, window, property, target, 8, PropertyMode.Replace, 0, 0); |
|||
} |
|||
|
|||
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); |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
return SendIncrDataToClientAsync(window, property, target, new MemoryStream(bytes)); |
|||
} |
|||
|
|||
protected IntPtr[] ConvertDataTransfer(IAsyncDataTransfer? dataTransfer) |
|||
{ |
|||
var atoms = Platform.Info.Atoms; |
|||
var atomValues = new List<IntPtr> { atoms.TARGETS, atoms.MULTIPLE }; |
|||
|
|||
if (dataTransfer is not null) |
|||
{ |
|||
foreach (var format in dataTransfer.Formats) |
|||
{ |
|||
foreach (var atom in DataFormatHelper.ToAtoms(format, atoms)) |
|||
atomValues.Add(atom); |
|||
} |
|||
} |
|||
|
|||
return atomValues.ToArray(); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
Dispose(true); |
|||
GC.SuppressFinalize(this); |
|||
} |
|||
|
|||
protected virtual void Dispose(bool disposing) |
|||
{ |
|||
if (Handle == 0) |
|||
return; |
|||
|
|||
XDestroyWindow(Platform.Display, Handle); |
|||
Handle = 0; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue