Browse Source
* New clipboard with IDataTransfer
* Move legacy format handling to DataObject wrappers
* macOS clipboard rework
* Browser clipboard rework
* Android clipboard rework
* iOS new clipboard
* X11 clipboard rework
* Simplify IDataTransfer API
* Simplify IClipboardImpl API
* Add DataFormat documentation
* Make DataFormat.SystemName platform specific
* Fix clipboard/DnD samples
* Fix native clipboard UTI conversion
* Adjust IDataTransfer namespaces
* Add Obsolete attributes to IDataObject related methods
* Better API for DataTransferItem
* Tizen clipboard rework
* Add missing clipboard extension methods
* Split IDataTransferItem into IAsyncDataTransferItem and ISyncDataTransferItem
* Rename back ISyncDataTransfer to IDataTransfer
* Added IClipboard API suppressions
* Make IPlatformDragSource NotClientImplementable
* Rename DataFormatKinds
* Added DataTransferItem.CreateText/File
* Implemented typed DataFormat<T>
* Fix X11 text/uri-list encoding
* Add API suppressions
* Adjust ClipboardUriListHelper stream ownership
* Fix legacy clipboard BinaryFormatter deserialization
* Fix macOS build
(cherry picked from commit 1f404646fa)
release/11.3.7
137 changed files with 5918 additions and 1883 deletions
@ -0,0 +1,7 @@ |
|||
#pragma once |
|||
|
|||
#include "common.h" |
|||
|
|||
@interface WriteableClipboardItem : NSObject <NSPasteboardWriting> |
|||
- (nonnull instancetype) initWithItem:(nonnull IAvnClipboardDataItem*)item source:(nonnull IAvnClipboardDataSource*)source; |
|||
@end |
|||
@ -1,206 +1,295 @@ |
|||
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h> |
|||
#include "common.h" |
|||
#include "clipboard.h" |
|||
#include "AvnString.h" |
|||
|
|||
class Clipboard : public ComSingleObject<IAvnClipboard, &IID_IAvnClipboard> |
|||
{ |
|||
private: |
|||
NSPasteboard* _pb; |
|||
NSPasteboardItem* _item; |
|||
NSPasteboard* _pasteboard; |
|||
public: |
|||
FORWARD_IUNKNOWN() |
|||
|
|||
Clipboard(NSPasteboard* pasteboard, NSPasteboardItem* item) |
|||
Clipboard(NSPasteboard* pasteboard) |
|||
{ |
|||
if(pasteboard == nil && item == nil) |
|||
if (pasteboard == nil) |
|||
pasteboard = [NSPasteboard generalPasteboard]; |
|||
|
|||
_pb = pasteboard; |
|||
_item = item; |
|||
_pasteboard = pasteboard; |
|||
} |
|||
|
|||
NSPasteboardItem* TryGetItem() |
|||
virtual HRESULT GetFormats(int64_t changeCount, IAvnStringArray** ret) override |
|||
{ |
|||
return _item; |
|||
START_COM_ARP_CALL; |
|||
|
|||
if (ret == nullptr) |
|||
return E_POINTER; |
|||
|
|||
if (changeCount != [_pasteboard changeCount]) |
|||
return COR_E_OBJECTDISPOSED; |
|||
|
|||
auto types = [_pasteboard types]; |
|||
*ret = types == nil ? nullptr : CreateAvnStringArray(types); |
|||
return S_OK; |
|||
} |
|||
|
|||
virtual HRESULT GetText (char* type, IAvnString**ppv) override |
|||
|
|||
virtual HRESULT GetItemCount(int64_t changeCount, int* ret) override |
|||
{ |
|||
START_COM_CALL; |
|||
START_COM_ARP_CALL; |
|||
|
|||
@autoreleasepool |
|||
{ |
|||
if(ppv == nullptr) |
|||
{ |
|||
return E_POINTER; |
|||
} |
|||
NSString* typeString = [NSString stringWithUTF8String:(const char*)type]; |
|||
NSString* string = _item == nil ? [_pb stringForType:typeString] : [_item stringForType:typeString]; |
|||
|
|||
*ppv = CreateAvnString(string); |
|||
|
|||
return S_OK; |
|||
} |
|||
if (ret == nullptr) |
|||
return E_POINTER; |
|||
|
|||
if (changeCount != [_pasteboard changeCount]) |
|||
return COR_E_OBJECTDISPOSED; |
|||
|
|||
auto items = [_pasteboard pasteboardItems]; |
|||
*ret = items == nil ? 0 : (int)[items count]; |
|||
return S_OK; |
|||
} |
|||
|
|||
virtual HRESULT SetStrings(char* type, IAvnStringArray*ppv) override |
|||
virtual HRESULT GetItemFormats(int index, int64_t changeCount, IAvnStringArray** ret) override |
|||
{ |
|||
START_COM_CALL; |
|||
START_COM_ARP_CALL; |
|||
|
|||
@autoreleasepool |
|||
{ |
|||
NSArray<NSString*>* data = GetNSArrayOfStringsAndRelease(ppv); |
|||
NSString* typeString = [NSString stringWithUTF8String:(const char*)type]; |
|||
if(_item == nil) |
|||
[_pb setPropertyList: data forType: typeString]; |
|||
else |
|||
[_item setPropertyList: data forType:typeString]; |
|||
return S_OK; |
|||
} |
|||
if (ret == nullptr) |
|||
return E_POINTER; |
|||
|
|||
if (changeCount != [_pasteboard changeCount]) |
|||
return COR_E_OBJECTDISPOSED; |
|||
|
|||
auto item = [[_pasteboard pasteboardItems] objectAtIndex:index]; |
|||
auto types = [item types]; |
|||
*ret = types == nil ? nullptr : CreateAvnStringArray(types); |
|||
return S_OK; |
|||
} |
|||
|
|||
virtual HRESULT GetStrings(char* type, IAvnStringArray**ppv) override |
|||
virtual HRESULT GetItemValueAsString(int index, int64_t changeCount, const char* format, IAvnString** ret) override |
|||
{ |
|||
START_COM_CALL; |
|||
START_COM_ARP_CALL; |
|||
|
|||
@autoreleasepool |
|||
{ |
|||
*ppv= nil; |
|||
NSString* typeString = [NSString stringWithUTF8String:(const char*)type]; |
|||
NSObject* data = _item == nil ? [_pb propertyListForType: typeString] : [_item propertyListForType: typeString]; |
|||
if(data == nil) |
|||
return S_OK; |
|||
|
|||
if([data isKindOfClass: [NSString class]]) |
|||
{ |
|||
*ppv = CreateAvnStringArray((NSString*) data); |
|||
return S_OK; |
|||
} |
|||
|
|||
NSArray<NSString*>* arr = (NSArray*)data; |
|||
|
|||
for(int c = 0; c < [arr count]; c++) |
|||
if(![[arr objectAtIndex:c] isKindOfClass:[NSString class]]) |
|||
return E_INVALIDARG; |
|||
|
|||
*ppv = CreateAvnStringArray(arr); |
|||
return S_OK; |
|||
} |
|||
if (ret == nullptr) |
|||
return E_POINTER; |
|||
|
|||
if (changeCount != [_pasteboard changeCount]) |
|||
return COR_E_OBJECTDISPOSED; |
|||
|
|||
auto item = [[_pasteboard pasteboardItems] objectAtIndex:index]; |
|||
auto value = [item stringForType:[NSString stringWithUTF8String:format]]; |
|||
*ret = value == nil ? nullptr : CreateAvnString(value); |
|||
return S_OK; |
|||
} |
|||
|
|||
virtual HRESULT SetText (char* type, char* utf8String) override |
|||
virtual HRESULT GetItemValueAsBytes(int index, int64_t changeCount, const char* format, IAvnString** ret) override |
|||
{ |
|||
START_COM_CALL; |
|||
START_COM_ARP_CALL; |
|||
|
|||
@autoreleasepool |
|||
{ |
|||
auto string = [NSString stringWithUTF8String:(const char*)utf8String]; |
|||
auto typeString = [NSString stringWithUTF8String:(const char*)type]; |
|||
if(_item == nil) |
|||
[_pb setString: string forType: typeString]; |
|||
else |
|||
[_item setString: string forType:typeString]; |
|||
if (ret == nullptr) |
|||
return E_POINTER; |
|||
|
|||
return S_OK; |
|||
} |
|||
if (changeCount != [_pasteboard changeCount]) |
|||
return COR_E_OBJECTDISPOSED; |
|||
|
|||
auto item = [[_pasteboard pasteboardItems] objectAtIndex:index]; |
|||
auto value = [item dataForType:[NSString stringWithUTF8String:format]]; |
|||
|
|||
*ret = value == nil || [value length] == 0 |
|||
? nullptr |
|||
: CreateByteArray((void*)[value bytes], (int)[value length]); |
|||
return S_OK; |
|||
} |
|||
|
|||
virtual HRESULT Clear(int64_t* ret) override |
|||
{ |
|||
START_COM_ARP_CALL; |
|||
|
|||
*ret = [_pasteboard clearContents]; |
|||
return S_OK; |
|||
} |
|||
|
|||
virtual HRESULT SetBytes(char* type, void* bytes, int len) override |
|||
virtual HRESULT GetChangeCount(int64_t* ret) override |
|||
{ |
|||
START_COM_CALL; |
|||
START_COM_ARP_CALL; |
|||
|
|||
@autoreleasepool |
|||
*ret = [_pasteboard changeCount]; |
|||
return S_OK; |
|||
} |
|||
|
|||
virtual HRESULT SetData(IAvnClipboardDataSource* source) override |
|||
{ |
|||
START_COM_ARP_CALL; |
|||
|
|||
auto count = source->GetItemCount(); |
|||
auto writeableItems = [NSMutableArray<WriteableClipboardItem*> arrayWithCapacity:count]; |
|||
|
|||
for (auto i = 0; i < count; ++i) |
|||
{ |
|||
auto typeString = [NSString stringWithUTF8String:(const char*)type]; |
|||
auto data = [NSData dataWithBytes:bytes length:len]; |
|||
if(_item == nil) |
|||
[_pb setData:data forType:typeString]; |
|||
else |
|||
[_item setData:data forType:typeString]; |
|||
return S_OK; |
|||
auto item = source->GetItem(i); |
|||
auto writeableItem = [[WriteableClipboardItem alloc] initWithItem:item source:source]; |
|||
[writeableItems addObject:writeableItem]; |
|||
} |
|||
|
|||
[_pasteboard writeObjects:writeableItems]; |
|||
return S_OK; |
|||
} |
|||
|
|||
virtual HRESULT GetBytes(char* type, IAvnString**ppv) override |
|||
|
|||
virtual bool IsTextFormat(const char *format) override |
|||
{ |
|||
START_COM_CALL; |
|||
START_COM_ARP_CALL; |
|||
|
|||
auto formatString = [NSString stringWithUTF8String:format]; |
|||
|
|||
@autoreleasepool |
|||
if (@available(macOS 11.0, *)) |
|||
{ |
|||
*ppv = nil; |
|||
auto typeString = [NSString stringWithUTF8String:(const char*)type]; |
|||
NSData*data; |
|||
@try |
|||
{ |
|||
if(_item) |
|||
data = [_item dataForType:typeString]; |
|||
else |
|||
data = [_pb dataForType:typeString]; |
|||
if(data == nil) |
|||
return E_FAIL; |
|||
} |
|||
@catch(NSException* e) |
|||
{ |
|||
return E_FAIL; |
|||
} |
|||
*ppv = CreateByteArray((void*)data.bytes, (int)data.length); |
|||
return S_OK; |
|||
auto type = [UTType typeWithIdentifier:formatString]; |
|||
return type != nil && [type conformsToType:UTTypeText]; |
|||
} |
|||
else |
|||
{ |
|||
return UTTypeConformsTo((__bridge CFStringRef)formatString, kUTTypeText); |
|||
} |
|||
} |
|||
}; |
|||
|
|||
|
|||
extern IAvnClipboard* CreateClipboard(NSPasteboard* pb) |
|||
{ |
|||
return new Clipboard(pb); |
|||
} |
|||
|
|||
|
|||
@implementation WriteableClipboardItem |
|||
{ |
|||
IAvnClipboardDataItem* _item; |
|||
IAvnClipboardDataSource* _source; |
|||
} |
|||
|
|||
- (nonnull WriteableClipboardItem*) initWithItem:(nonnull IAvnClipboardDataItem*)item source:(nonnull IAvnClipboardDataSource*)source |
|||
{ |
|||
self = [super init]; |
|||
_item = item; |
|||
_source = source; |
|||
|
|||
// Each item references its source so it doesn't get disposed too early. |
|||
source->AddRef(); |
|||
|
|||
return self; |
|||
} |
|||
|
|||
virtual HRESULT Clear(int64_t* rv) override |
|||
NSString* TryConvertFormatToUti(NSString* format) |
|||
{ |
|||
if (@available(macOS 11.0, *)) |
|||
{ |
|||
START_COM_CALL; |
|||
|
|||
@autoreleasepool |
|||
auto type = [UTType typeWithIdentifier:format]; |
|||
if (type == nil) |
|||
{ |
|||
if(_item != nil) |
|||
{ |
|||
_item = [NSPasteboardItem new]; |
|||
return 0; |
|||
} |
|||
if ([format containsString:@"/"]) |
|||
type = [UTType typeWithMIMEType:format]; |
|||
else |
|||
type = [UTType exportedTypeWithIdentifier:format]; |
|||
|
|||
if (type == nil) |
|||
{ |
|||
*rv = [_pb clearContents]; |
|||
[_pb setString:@"" forType:NSPasteboardTypeString]; |
|||
// For now, we need to use the deprecated UTTypeCreatePreferredIdentifierForTag to create a dynamic UTI for arbitrary strings. |
|||
// This is only necessary because the old IDataObject can provide arbitrary types that aren't UTIs nor mime types. |
|||
// With the new DataFormat: |
|||
// - If the format is an application format, the managed side provides a UTI like net.avaloniaui.app.uti.xxx. |
|||
// - If the format is an OS format, the user has been warned that they MUST provide a name which is valid for the OS. |
|||
// TODO12: remove! |
|||
auto fromPasteboardType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassNSPboardType, (__bridge CFStringRef)format, nil); |
|||
if (fromPasteboardType != nil) |
|||
return (__bridge_transfer NSString*)fromPasteboardType; |
|||
} |
|||
|
|||
return S_OK; |
|||
} |
|||
|
|||
return type == nil ? nil : [type identifier]; |
|||
} |
|||
|
|||
virtual HRESULT GetChangeCount(int64_t* rv) override |
|||
else |
|||
{ |
|||
START_COM_CALL; |
|||
if(_item == nil) |
|||
{ |
|||
*rv = [_pb changeCount]; |
|||
return S_OK; |
|||
} |
|||
return E_NOTIMPL; |
|||
auto bridgedFormat = (__bridge CFStringRef)format; |
|||
if (UTTypeIsDeclared(bridgedFormat)) |
|||
return format; |
|||
|
|||
auto fromMimeType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, bridgedFormat, nil); |
|||
if (fromMimeType != nil) |
|||
return (__bridge_transfer NSString*)fromMimeType; |
|||
|
|||
auto fromPasteboardType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassNSPboardType, bridgedFormat, nil); |
|||
if (fromPasteboardType != nil) |
|||
return (__bridge_transfer NSString*)fromPasteboardType; |
|||
|
|||
return nil; |
|||
} |
|||
} |
|||
|
|||
- (nonnull NSArray<NSPasteboardType>*) writableTypesForPasteboard:(nonnull NSPasteboard*)pasteboard |
|||
{ |
|||
auto formats = _item->ProvideFormats(); |
|||
if (formats == nullptr) |
|||
return [NSArray array]; |
|||
|
|||
virtual HRESULT ObtainFormats(IAvnStringArray** ppv) override |
|||
auto count = formats->GetCount(); |
|||
if (count == 0) |
|||
return [NSArray array]; |
|||
|
|||
auto utis = [NSMutableArray arrayWithCapacity:count]; |
|||
IAvnString* format; |
|||
for (auto i = 0; i < count; ++i) |
|||
{ |
|||
START_COM_CALL; |
|||
|
|||
@autoreleasepool |
|||
{ |
|||
*ppv = CreateAvnStringArray(_item == nil ? [_pb types] : [_item types]); |
|||
return S_OK; |
|||
} |
|||
if (formats->Get(i, &format) != S_OK) |
|||
continue; |
|||
|
|||
// Only UTIs must be returned from writableTypesForPasteboard or an exception will be thrown |
|||
auto formatString = GetNSStringAndRelease(format); |
|||
auto uti = TryConvertFormatToUti(formatString); |
|||
if (uti != nil) |
|||
[utis addObject:uti]; |
|||
} |
|||
}; |
|||
formats->Release(); |
|||
|
|||
[utis addObject:GetAvnCustomDataType()]; |
|||
|
|||
return utis; |
|||
} |
|||
|
|||
extern IAvnClipboard* CreateClipboard(NSPasteboard* pb, NSPasteboardItem* item) |
|||
- (NSPasteboardWritingOptions) writingOptionsForType:(NSPasteboardType)type pasteboard:(NSPasteboard*)pasteboard |
|||
{ |
|||
return new Clipboard(pb, item); |
|||
return [type isEqualToString:NSPasteboardTypeString] || [type isEqualToString:GetAvnCustomDataType()] |
|||
? 0 |
|||
: NSPasteboardWritingPromised; |
|||
} |
|||
|
|||
extern NSPasteboardItem* TryGetPasteboardItem(IAvnClipboard*cb) |
|||
- (nullable id) pasteboardPropertyListForType:(nonnull NSPasteboardType)type |
|||
{ |
|||
auto clipboard = dynamic_cast<Clipboard*>(cb); |
|||
if(clipboard == nil) |
|||
if ([type isEqualToString:GetAvnCustomDataType()]) |
|||
return @""; |
|||
|
|||
ComPtr<IAvnClipboardDataValue> value(_item->GetValue([type UTF8String]), true); |
|||
if (value.getRaw() == nullptr) |
|||
return nil; |
|||
return clipboard->TryGetItem(); |
|||
|
|||
if (value->IsString()) |
|||
return GetNSStringAndRelease(value->AsString()); |
|||
|
|||
auto length = value->GetByteLength(); |
|||
auto buffer = malloc(length); |
|||
value->CopyBytesTo(buffer); |
|||
return [NSData dataWithBytesNoCopy:buffer length:length]; |
|||
} |
|||
|
|||
- (void) dealloc |
|||
{ |
|||
if (_item != nullptr) |
|||
{ |
|||
_item->Release(); |
|||
_item = nullptr; |
|||
} |
|||
|
|||
if (_source != nullptr) |
|||
{ |
|||
_source->Release(); |
|||
_source = nullptr; |
|||
} |
|||
} |
|||
|
|||
@end |
|||
|
|||
@ -0,0 +1,36 @@ |
|||
using System; |
|||
using Android.Content; |
|||
using Avalonia.Input; |
|||
|
|||
namespace Avalonia.Android.Platform; |
|||
|
|||
internal static class AndroidDataFormatHelper |
|||
{ |
|||
private const string AppPrefix = "application/avn-fmt."; |
|||
|
|||
public static DataFormat MimeTypeToDataFormat(string mimeType) |
|||
{ |
|||
if (mimeType == ClipDescription.MimetypeTextPlain) |
|||
return DataFormat.Text; |
|||
|
|||
if (mimeType == ClipDescription.MimetypeTextUrilist) |
|||
return DataFormat.File; |
|||
|
|||
if (mimeType.StartsWith("text/", StringComparison.OrdinalIgnoreCase)) |
|||
return DataFormat.FromSystemName<string>(mimeType, AppPrefix); |
|||
|
|||
return DataFormat.FromSystemName<byte[]>(mimeType, AppPrefix); |
|||
} |
|||
|
|||
public static string DataFormatToMimeType(DataFormat format) |
|||
{ |
|||
if (DataFormat.Text.Equals(format)) |
|||
return ClipDescription.MimetypeTextPlain; |
|||
|
|||
if (DataFormat.File.Equals(format)) |
|||
return ClipDescription.MimetypeTextUrilist; |
|||
|
|||
return format.ToSystemName(AppPrefix); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
using System; |
|||
using Android.App; |
|||
using Android.Content; |
|||
using Avalonia.Android.Platform.Storage; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
|
|||
namespace Avalonia.Android.Platform; |
|||
|
|||
/// <summary>
|
|||
/// Wraps a <see cref="ClipData.Item"/> into a <see cref="IDataTransferItem"/>.
|
|||
/// </summary>
|
|||
/// <param name="item">The clip data item.</param>
|
|||
/// <param name="owner">The data transfer owning this item.</param>
|
|||
internal sealed class ClipDataItemToDataTransferItemWrapper(ClipData.Item item, ClipDataToDataTransferWrapper owner) |
|||
: PlatformDataTransferItem |
|||
{ |
|||
private readonly ClipData.Item _item = item; |
|||
private readonly ClipDataToDataTransferWrapper _owner = owner; |
|||
|
|||
protected override DataFormat[] ProvideFormats() |
|||
=> _owner.Formats; // There's no "format per item", assume each item handle all formats
|
|||
|
|||
protected override object? TryGetRawCore(DataFormat format) |
|||
{ |
|||
if (DataFormat.Text.Equals(format)) |
|||
return _item.CoerceToText(_owner.Context); |
|||
|
|||
if (DataFormat.File.Equals(format)) |
|||
{ |
|||
return _item.Uri is { Scheme: "file" or "content" } fileUri && _owner.Context is Activity activity ? |
|||
AndroidStorageItem.CreateItem(activity, fileUri) : |
|||
null; |
|||
} |
|||
|
|||
if (format is DataFormat<string>) |
|||
return TryGetAsString(); |
|||
|
|||
return null; |
|||
} |
|||
|
|||
private string? TryGetAsString() |
|||
{ |
|||
if (_item.Text is { } text) |
|||
return text; |
|||
|
|||
if (_item.HtmlText is { } htmlText) |
|||
return htmlText; |
|||
|
|||
if (_item.Uri is { } uri) |
|||
return uri.ToString(); |
|||
|
|||
if (_item.Intent is { } intent) |
|||
return intent.ToUri(IntentUriType.Scheme); |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
using Android.Content; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
|
|||
namespace Avalonia.Android.Platform; |
|||
|
|||
/// <summary>
|
|||
/// Wraps a <see cref="ClipData"/> into a <see cref="IDataTransfer"/>.
|
|||
/// </summary>
|
|||
/// <param name="clipData">The clip data.</param>
|
|||
/// <param name="context">The application context.</param>
|
|||
internal sealed class ClipDataToDataTransferWrapper(ClipData clipData, Context? context) |
|||
: PlatformDataTransfer |
|||
{ |
|||
private readonly ClipData _clipData = clipData; |
|||
|
|||
public Context? Context { get; } = context; |
|||
|
|||
protected override DataFormat[] ProvideFormats() |
|||
{ |
|||
if (_clipData.Description is not { MimeTypeCount: > 0 and var count } clipDescription) |
|||
return []; |
|||
|
|||
var formats = new DataFormat[count]; |
|||
|
|||
for (var i = 0; i < count; ++i) |
|||
formats[i] = AndroidDataFormatHelper.MimeTypeToDataFormat(clipDescription.GetMimeType(i)!); |
|||
|
|||
return formats; |
|||
} |
|||
|
|||
protected override PlatformDataTransferItem[] ProvideItems() |
|||
{ |
|||
var count = _clipData.ItemCount; |
|||
var items = new PlatformDataTransferItem[count]; |
|||
|
|||
for (var i = 0; i < count; ++i) |
|||
items[i] = new ClipDataItemToDataTransferItemWrapper(_clipData.GetItemAt(i)!, this); |
|||
|
|||
return items; |
|||
} |
|||
|
|||
public override void Dispose() |
|||
{ |
|||
} |
|||
} |
|||
@ -1,65 +1,145 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Android.Content; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
using Avalonia.Logging; |
|||
using AndroidUri = Android.Net.Uri; |
|||
|
|||
namespace Avalonia.Android.Platform |
|||
{ |
|||
internal class ClipboardImpl : IClipboard |
|||
internal sealed class ClipboardImpl(ClipboardManager? clipboardManager, Context? context) |
|||
: IClipboardImpl |
|||
{ |
|||
private readonly ClipboardManager? _clipboardManager; |
|||
private readonly ClipboardManager? _clipboardManager = clipboardManager; |
|||
private readonly Context? _context = context; |
|||
|
|||
internal ClipboardImpl(ClipboardManager? value) |
|||
public Task<IAsyncDataTransfer?> TryGetDataAsync() |
|||
{ |
|||
_clipboardManager = value; |
|||
try |
|||
{ |
|||
return Task.FromResult<IAsyncDataTransfer?>(TryGetData()); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
return Task.FromException<IAsyncDataTransfer?>(ex); |
|||
} |
|||
} |
|||
|
|||
public Task<string?> GetTextAsync() |
|||
private ClipDataToDataTransferWrapper? TryGetData() |
|||
=> _clipboardManager?.PrimaryClip is { } clipData ? |
|||
new ClipDataToDataTransferWrapper(clipData, _context) : |
|||
null; |
|||
|
|||
public async Task SetDataAsync(IAsyncDataTransfer dataTransfer) |
|||
{ |
|||
if (_clipboardManager?.HasPrimaryClip == true) |
|||
if (_clipboardManager is null) |
|||
return; |
|||
|
|||
var mimeTypes = dataTransfer.Formats |
|||
.Select(AndroidDataFormatHelper.DataFormatToMimeType) |
|||
.ToArray(); |
|||
|
|||
ClipData.Item? firstItem = null; |
|||
List<ClipData.Item>? additionalItems = null; |
|||
|
|||
foreach (var dataTransferItem in dataTransfer.Items) |
|||
{ |
|||
return Task.FromResult(_clipboardManager.PrimaryClip?.GetItemAt(0)?.Text); |
|||
if (await TryCreateDataItemAsync(dataTransferItem) is not { } clipDataItem) |
|||
continue; |
|||
|
|||
if (firstItem is null) |
|||
firstItem = clipDataItem; |
|||
else |
|||
(additionalItems ??= new()).Add(clipDataItem); |
|||
} |
|||
|
|||
return Task.FromResult<string?>(null); |
|||
if (firstItem is null) |
|||
{ |
|||
Clear(); |
|||
return; |
|||
} |
|||
|
|||
var clipData = new ClipData((string?)null, mimeTypes, firstItem); |
|||
|
|||
if (additionalItems is not null) |
|||
{ |
|||
foreach (var additionalItem in additionalItems) |
|||
clipData.AddItem(additionalItem); |
|||
} |
|||
|
|||
_clipboardManager.PrimaryClip = clipData; |
|||
} |
|||
|
|||
public Task SetTextAsync(string? text) |
|||
private async Task<ClipData.Item?> TryCreateDataItemAsync(IAsyncDataTransferItem item) |
|||
{ |
|||
if(_clipboardManager == null) |
|||
var hasFormats = false; |
|||
|
|||
// Create the item from the first format returning a supported value.
|
|||
foreach (var dataFormat in item.Formats) |
|||
{ |
|||
return Task.CompletedTask; |
|||
hasFormats = true; |
|||
|
|||
if (DataFormat.Text.Equals(dataFormat)) |
|||
{ |
|||
var text = await item.TryGetValueAsync(DataFormat.Text); |
|||
return new ClipData.Item(text, string.Empty); |
|||
} |
|||
|
|||
if (DataFormat.File.Equals(dataFormat)) |
|||
{ |
|||
var storageItem = await item.TryGetValueAsync(DataFormat.File); |
|||
if (storageItem is null) |
|||
continue; |
|||
|
|||
return new ClipData.Item(AndroidUri.Parse(storageItem.Path.OriginalString)); |
|||
} |
|||
|
|||
if (dataFormat is DataFormat<string> stringFormat) |
|||
{ |
|||
var stringValue = await item.TryGetValueAsync(stringFormat); |
|||
if (stringValue is null) |
|||
continue; |
|||
|
|||
return new ClipData.Item(stringValue); |
|||
} |
|||
} |
|||
|
|||
var clip = ClipData.NewPlainText("text", text); |
|||
_clipboardManager.PrimaryClip = clip; |
|||
if (hasFormats) |
|||
{ |
|||
Logger.TryGet(LogEventLevel.Warning, LogArea.AndroidPlatform)?.Log( |
|||
this, |
|||
"No compatible value found for data transfer item with formats {Formats}", |
|||
string.Join(", ", item.Formats)); |
|||
} |
|||
|
|||
return Task.CompletedTask; |
|||
return null; |
|||
} |
|||
|
|||
public Task ClearAsync() |
|||
{ |
|||
if (_clipboardManager == null) |
|||
try |
|||
{ |
|||
Clear(); |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
_clipboardManager.PrimaryClip = null; |
|||
|
|||
return Task.CompletedTask; |
|||
catch (Exception ex) |
|||
{ |
|||
return Task.FromException(ex); |
|||
} |
|||
} |
|||
|
|||
public Task SetDataObjectAsync(IDataObject data) => throw new PlatformNotSupportedException(); |
|||
|
|||
public Task<string[]> GetFormatsAsync() => throw new PlatformNotSupportedException(); |
|||
|
|||
public Task<object?> GetDataAsync(string format) => throw new PlatformNotSupportedException(); |
|||
|
|||
public Task<IDataObject?> TryGetInProcessDataObjectAsync() => Task.FromResult<IDataObject?>(null); |
|||
private void Clear() |
|||
{ |
|||
if (_clipboardManager is null) |
|||
return; |
|||
|
|||
/// <inheritdoc />
|
|||
public Task FlushAsync() => |
|||
Task.CompletedTask; |
|||
if (OperatingSystem.IsAndroidVersionAtLeast(28)) |
|||
_clipboardManager.ClearPrimaryClip(); |
|||
else |
|||
_clipboardManager.PrimaryClip = ClipData.NewPlainText(null, string.Empty); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,139 @@ |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Logging; |
|||
using Avalonia.Platform.Storage; |
|||
|
|||
namespace Avalonia.Input; |
|||
|
|||
// Keep AsyncDataTransferExtensions.TryGetXxxAsync methods in sync with DataTransferExtensions.TryGetXxx ones.
|
|||
|
|||
/// <summary>
|
|||
/// Contains extension methods for <see cref="IAsyncDataTransfer"/>.
|
|||
/// </summary>
|
|||
public static class AsyncDataTransferExtensions |
|||
{ |
|||
internal static IDataTransfer ToSynchronous(this IAsyncDataTransfer asyncDataTransfer, string logArea) |
|||
{ |
|||
if (asyncDataTransfer is IDataTransfer dataTransfer) |
|||
return dataTransfer; |
|||
|
|||
Logger.TryGet(LogEventLevel.Warning, logArea)?.Log( |
|||
null, |
|||
$"Using a synchronous wrapper for {nameof(IAsyncDataTransferItem)} {{Type}}. Consider implementing {nameof(IDataTransfer)} instead.", |
|||
asyncDataTransfer.GetType()); |
|||
|
|||
return new AsyncToSyncDataTransfer(asyncDataTransfer); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets whether a <see cref="IAsyncDataTransfer"/> supports a specific format.
|
|||
/// </summary>
|
|||
/// <param name="dataTransfer">The <see cref="IAsyncDataTransfer"/> instance.</param>
|
|||
/// <param name="format">The format to check.</param>
|
|||
/// <returns>true if <paramref name="format"/> is supported, false otherwise.</returns>
|
|||
public static bool Contains(this IAsyncDataTransfer dataTransfer, DataFormat format) |
|||
{ |
|||
var formats = dataTransfer.Formats; |
|||
var count = formats.Count; |
|||
|
|||
for (var i = 0; i < count; ++i) |
|||
{ |
|||
if (format == formats[i]) |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the list of <see cref="IAsyncDataTransferItem"/> contained in this object, filtered by a given format.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// <para>
|
|||
/// Some platforms (such as Windows and X11) may only support a single data item for all formats
|
|||
/// except <see cref="DataFormat.File"/>.
|
|||
/// </para>
|
|||
/// <para>Items returned by this property must stay valid until the <see cref="IAsyncDataTransfer"/> is disposed.</para>
|
|||
/// </remarks>
|
|||
public static IEnumerable<IAsyncDataTransferItem> GetItems(this IAsyncDataTransfer dataTransfer, DataFormat format) |
|||
{ |
|||
var items = dataTransfer.Items; |
|||
var count = items.Count; |
|||
|
|||
for (var i = 0; i < count; ++i) |
|||
{ |
|||
var item = items[i]; |
|||
if (item.Contains(format)) |
|||
yield return item; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tries to get a value for a given format from a <see cref="IAsyncDataTransfer"/>.
|
|||
/// </summary>
|
|||
/// <param name="dataTransfer">The <see cref="IAsyncDataTransfer"/> instance.</param>
|
|||
/// <param name="format">The format to retrieve.</param>
|
|||
/// <returns>A value for <paramref name="format"/>, or null if the format is not supported.</returns>
|
|||
/// <remarks>
|
|||
/// If the <see cref="IAsyncDataTransfer"/> contains several items supporting <paramref name="format"/>,
|
|||
/// the first matching one will be returned.
|
|||
/// </remarks>
|
|||
public static Task<T?> TryGetValueAsync<T>(this IAsyncDataTransfer dataTransfer, DataFormat<T> format) |
|||
where T : class |
|||
=> dataTransfer.GetItems(format).FirstOrDefault() is { } item ? |
|||
item.TryGetValueAsync(format) : |
|||
Task.FromResult<T?>(null); |
|||
|
|||
/// <summary>
|
|||
/// Tries to get multiple values for a given format from a <see cref="IAsyncDataTransfer"/>.
|
|||
/// </summary>
|
|||
/// <param name="dataTransfer">The <see cref="IAsyncDataTransfer"/> instance.</param>
|
|||
/// <param name="format">The format to retrieve.</param>
|
|||
/// <returns>A list of values for <paramref name="format"/>, or null if the format is not supported.</returns>
|
|||
public static async Task<T[]?> TryGetValuesAsync<T>(this IAsyncDataTransfer dataTransfer, DataFormat<T> format) |
|||
where T : class |
|||
{ |
|||
List<T>? results = null; |
|||
|
|||
foreach (var item in dataTransfer.GetItems(format)) |
|||
{ |
|||
// No ConfigureAwait(false) here: we want TryGetAsync() for next items to be called on the initial thread.
|
|||
var result = await item.TryGetValueAsync(format); |
|||
if (result is null) |
|||
continue; |
|||
|
|||
results ??= []; |
|||
results.Add(result); |
|||
} |
|||
|
|||
return results?.ToArray(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a text, if available, from a <see cref="IAsyncDataTransfer"/> instance.
|
|||
/// </summary>
|
|||
/// <param name="dataTransfer">The data transfer instance.</param>
|
|||
/// <returns>A string, or null if the format isn't available.</returns>
|
|||
/// <seealso cref="DataFormat.Text"/>.
|
|||
public static Task<string?> TryGetTextAsync(this IAsyncDataTransfer dataTransfer) |
|||
=> dataTransfer.TryGetValueAsync(DataFormat.Text); |
|||
|
|||
/// <summary>
|
|||
/// Returns a file, if available, from a <see cref="IAsyncDataTransfer"/> instance.
|
|||
/// </summary>
|
|||
/// <param name="dataTransfer">The data transfer instance.</param>
|
|||
/// <returns>An <see cref="IStorageItem"/> (file or folder), or null if the format isn't available.</returns>
|
|||
/// <seealso cref="DataFormat.File"/>.
|
|||
public static Task<IStorageItem?> TryGetFileAsync(this IAsyncDataTransfer dataTransfer) |
|||
=> dataTransfer.TryGetValueAsync(DataFormat.File); |
|||
|
|||
/// <summary>
|
|||
/// Returns a list of files, if available, from a <see cref="IAsyncDataTransfer"/> instance.
|
|||
/// </summary>
|
|||
/// <param name="dataTransfer">The data transfer instance.</param>
|
|||
/// <returns>An array of <see cref="IStorageItem"/> (files or folders), or null if the format isn't available.</returns>
|
|||
/// <seealso cref="DataFormat.File"/>.
|
|||
public static Task<IStorageItem[]?> TryGetFilesAsync(this IAsyncDataTransfer dataTransfer) |
|||
=> dataTransfer.TryGetValuesAsync(DataFormat.File); |
|||
} |
|||
@ -0,0 +1,58 @@ |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Platform.Storage; |
|||
|
|||
namespace Avalonia.Input; |
|||
|
|||
/// <summary>
|
|||
/// Contains extension methods for <see cref="IAsyncDataTransferItem"/>.
|
|||
/// </summary>
|
|||
public static class AsyncDataTransferItemExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Gets whether a <see cref="IAsyncDataTransferItem"/> supports a specific format.
|
|||
/// </summary>
|
|||
/// <param name="dataTransferItem">The <see cref="IAsyncDataTransferItem"/> instance.</param>
|
|||
/// <param name="format">The format to check.</param>
|
|||
/// <returns>true if <paramref name="format"/> is supported, false otherwise.</returns>
|
|||
public static bool Contains(this IAsyncDataTransferItem dataTransferItem, DataFormat format) |
|||
{ |
|||
var formats = dataTransferItem.Formats; |
|||
var count = formats.Count; |
|||
|
|||
for (var i = 0; i < count; ++i) |
|||
{ |
|||
if (format == formats[i]) |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tries to get a value for a given format from a <see cref="IAsyncDataTransferItem"/>.
|
|||
/// </summary>
|
|||
/// <param name="dataTransferItem">The <see cref="IAsyncDataTransferItem"/> instance.</param>
|
|||
/// <param name="format">The format to retrieve.</param>
|
|||
/// <returns>A value for <paramref name="format"/>, or null if the format is not supported.</returns>
|
|||
public static async Task<T?> TryGetValueAsync<T>(this IAsyncDataTransferItem dataTransferItem, DataFormat<T> format) |
|||
where T : class |
|||
=> await dataTransferItem.TryGetRawAsync(format).ConfigureAwait(false) as T; |
|||
|
|||
/// <summary>
|
|||
/// Returns a text, if available, from a <see cref="IAsyncDataTransferItem"/> instance.
|
|||
/// </summary>
|
|||
/// <param name="dataTransferItem">The data transfer instance.</param>
|
|||
/// <returns>A string, or null if the format isn't available.</returns>
|
|||
/// <seealso cref="DataFormat.Text"/>.
|
|||
public static Task<string?> TryGetTextAsync(this IAsyncDataTransferItem dataTransferItem) |
|||
=> dataTransferItem.TryGetValueAsync(DataFormat.Text); |
|||
|
|||
/// <summary>
|
|||
/// Returns a file, if available, from a <see cref="IAsyncDataTransferItem"/> instance.
|
|||
/// </summary>
|
|||
/// <param name="dataTransferItem">The data transfer instance.</param>
|
|||
/// <returns>An <see cref="IStorageItem"/> (file or folder), or null if the format isn't available.</returns>
|
|||
/// <seealso cref="DataFormat.File"/>.
|
|||
public static Task<IStorageItem?> TryGetFileAsync(this IAsyncDataTransferItem dataTransferItem) |
|||
=> dataTransferItem.TryGetValueAsync(DataFormat.File); |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.Input; |
|||
|
|||
/// <summary>
|
|||
/// Wraps a <see cref="IAsyncDataTransfer"/> into a <see cref="IDataTransfer"/>.
|
|||
/// </summary>
|
|||
/// <param name="asyncDataTransfer">The async object to wrap.</param>
|
|||
/// <remarks>Using this type should be a last resort!</remarks>
|
|||
internal sealed class AsyncToSyncDataTransfer(IAsyncDataTransfer asyncDataTransfer) |
|||
: IDataTransfer, IAsyncDataTransfer |
|||
{ |
|||
private readonly IAsyncDataTransfer _asyncDataTransfer = asyncDataTransfer; |
|||
private AsyncToSyncDataTransferItem[]? _items; |
|||
|
|||
public IReadOnlyList<DataFormat> Formats |
|||
=> _asyncDataTransfer.Formats; |
|||
|
|||
public IReadOnlyList<AsyncToSyncDataTransferItem> Items |
|||
=> _items ??= ProvideItems(); |
|||
|
|||
IReadOnlyList<IDataTransferItem> IDataTransfer.Items |
|||
=> Items; |
|||
|
|||
IReadOnlyList<IAsyncDataTransferItem> IAsyncDataTransfer.Items |
|||
=> _asyncDataTransfer.Items; |
|||
|
|||
private AsyncToSyncDataTransferItem[] ProvideItems() |
|||
{ |
|||
var asyncItems = _asyncDataTransfer.Items; |
|||
var count = asyncItems.Count; |
|||
var syncItems = new AsyncToSyncDataTransferItem[count]; |
|||
|
|||
for (var i = 0; i < count; ++i) |
|||
{ |
|||
var asyncItem = asyncItems[i]; |
|||
syncItems[i] = new AsyncToSyncDataTransferItem(asyncItem); |
|||
} |
|||
|
|||
return syncItems; |
|||
} |
|||
|
|||
public void Dispose() |
|||
=> _asyncDataTransfer.Dispose(); |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Avalonia.Input; |
|||
|
|||
/// <summary>
|
|||
/// Wraps a <see cref="IAsyncDataTransferItem"/> into a <see cref="IDataTransferItem"/>.
|
|||
/// </summary>
|
|||
/// <param name="asyncDataTransferItem">The async item to wrap.</param>
|
|||
/// <remarks>Using this type should be a last resort!</remarks>
|
|||
internal sealed class AsyncToSyncDataTransferItem(IAsyncDataTransferItem asyncDataTransferItem) |
|||
: IDataTransferItem, IAsyncDataTransferItem |
|||
{ |
|||
private readonly IAsyncDataTransferItem _asyncDataTransferItem = asyncDataTransferItem; |
|||
|
|||
public IReadOnlyList<DataFormat> Formats |
|||
=> _asyncDataTransferItem.Formats; |
|||
|
|||
public object? TryGetRaw(DataFormat format) |
|||
=> _asyncDataTransferItem.TryGetRawAsync(format).GetAwaiter().GetResult(); |
|||
|
|||
public Task<object?> TryGetRawAsync(DataFormat format) |
|||
=> _asyncDataTransferItem.TryGetRawAsync(format); |
|||
} |
|||
@ -0,0 +1,227 @@ |
|||
using System; |
|||
using Avalonia.Metadata; |
|||
using Avalonia.Platform.Storage; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Input; |
|||
|
|||
/// <summary>
|
|||
/// Represents a format usable with the clipboard and drag-and-drop.
|
|||
/// </summary>
|
|||
public abstract class DataFormat : IEquatable<DataFormat> |
|||
{ |
|||
private protected DataFormat(DataFormatKind kind, string identifier) |
|||
{ |
|||
Kind = kind; |
|||
Identifier = identifier; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the kind of the data format.
|
|||
/// </summary>
|
|||
public DataFormatKind Kind { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the identifier of the data format.
|
|||
/// </summary>
|
|||
public string Identifier { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a data format representing plain text.
|
|||
/// Its data type is <see cref="string"/>.
|
|||
/// </summary>
|
|||
public static DataFormat<string> Text { get; } = CreateUniversalFormat<string>("Text"); |
|||
|
|||
/// <summary>
|
|||
/// Gets a data format representing a single file.
|
|||
/// Its data type is <see cref="IStorageItem"/>.
|
|||
/// </summary>
|
|||
public static DataFormat<IStorageItem> File { get; } = CreateUniversalFormat<IStorageItem>("File"); |
|||
|
|||
/// <summary>
|
|||
/// Creates a name for this format, usable by the underlying platform.
|
|||
/// </summary>
|
|||
/// <param name="applicationPrefix">The system prefix used to recognize the name as an application format.</param>
|
|||
/// <returns>A system name for the format.</returns>
|
|||
/// <remarks>
|
|||
/// This method can only be called if <see cref="Kind"/> is
|
|||
/// <see cref="DataFormatKind.Application"/> or <see cref="DataFormatKind.Platform"/>.
|
|||
/// </remarks>
|
|||
public string ToSystemName(string applicationPrefix) |
|||
{ |
|||
ThrowHelper.ThrowIfNull(applicationPrefix); |
|||
|
|||
return Kind switch |
|||
{ |
|||
DataFormatKind.Application => applicationPrefix + Identifier, |
|||
DataFormatKind.Platform => Identifier, |
|||
_ => throw new InvalidOperationException($"Cannot get system name for universal format {Identifier}") |
|||
}; |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public bool Equals(DataFormat? other) |
|||
{ |
|||
if (other is null) |
|||
return false; |
|||
if (ReferenceEquals(this, other)) |
|||
return true; |
|||
return Kind == other.Kind && Identifier == other.Identifier; |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public override bool Equals(object? obj) |
|||
=> Equals(obj as DataFormat); |
|||
|
|||
/// <inheritdoc />
|
|||
public override int GetHashCode() |
|||
=> ((int)Kind * 397) ^ Identifier.GetHashCode(); |
|||
|
|||
/// <summary>
|
|||
/// Compares two instances of <see cref="DataFormat"/> for equality.
|
|||
/// </summary>
|
|||
/// <param name="left">The first instance.</param>
|
|||
/// <param name="right">The second instance.</param>
|
|||
/// <returns>true if the two instances are equal; otherwise false.</returns>
|
|||
public static bool operator ==(DataFormat? left, DataFormat? right) |
|||
=> Equals(left, right); |
|||
|
|||
/// <summary>
|
|||
/// Compares two instances of <see cref="DataFormat"/> for inequality.
|
|||
/// </summary>
|
|||
/// <param name="left">The first instance.</param>
|
|||
/// <param name="right">The second instance.</param>
|
|||
/// <returns>true if the two instances are not equal; otherwise false.</returns>
|
|||
public static bool operator !=(DataFormat? left, DataFormat? right) |
|||
=> !Equals(left, right); |
|||
|
|||
private static DataFormat<T> CreateUniversalFormat<T>(string identifier) where T : class |
|||
=> new(DataFormatKind.Universal, identifier); |
|||
|
|||
/// <summary>
|
|||
/// Creates a new format specific to the application that returns an array of <see cref="byte"/>.
|
|||
/// </summary>
|
|||
/// <param name="identifier">
|
|||
/// <para>
|
|||
/// The format identifier. To avoid conflicts with system identifiers, this value isn't passed to the underlying
|
|||
/// platform directly. However, two different applications using the same identifier
|
|||
/// with <see cref="CreateBytesApplicationFormat"/> or <see cref="CreateStringApplicationFormat"/>
|
|||
/// are able to share data using this format.
|
|||
/// </para>
|
|||
/// <para>Only ASCII letters (A-Z, a-z), digits (0-9), the dot (.) and the hyphen (-) are accepted.</para>
|
|||
/// </param>
|
|||
/// <returns>A new <see cref="DataFormat"/>.</returns>
|
|||
public static DataFormat<byte[]> CreateBytesApplicationFormat(string identifier) |
|||
=> CreateApplicationFormat<byte[]>(identifier); |
|||
|
|||
/// <summary>
|
|||
/// Creates a new format specific to the application that returns a <see cref="string"/>.
|
|||
/// </summary>
|
|||
/// <param name="identifier">
|
|||
/// <para>
|
|||
/// The format identifier. To avoid conflicts with system identifiers, this value isn't passed to the underlying
|
|||
/// platform directly. However, two different applications using the same identifier
|
|||
/// with <see cref="CreateBytesApplicationFormat"/> or <see cref="CreateStringApplicationFormat"/>
|
|||
/// are able to share data using this format.
|
|||
/// </para>
|
|||
/// <para>Only ASCII letters (A-Z, a-z), digits (0-9), the dot (.) and the hyphen (-) are accepted.</para>
|
|||
/// </param>
|
|||
/// <returns>A new <see cref="DataFormat"/>.</returns>
|
|||
public static DataFormat<string> CreateStringApplicationFormat(string identifier) |
|||
=> CreateApplicationFormat<string>(identifier); |
|||
|
|||
private static DataFormat<T> CreateApplicationFormat<T>(string identifier) |
|||
where T : class |
|||
{ |
|||
if (!IsValidApplicationFormatIdentifier(identifier)) |
|||
throw new ArgumentException("Invalid application identifier", nameof(identifier)); |
|||
|
|||
return new(DataFormatKind.Application, identifier); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a new format for the current platform that returns an array of <see cref="byte"/>.
|
|||
/// </summary>
|
|||
/// <param name="identifier">
|
|||
/// The format identifier. This value is not validated and is passed AS IS to the underlying platform.
|
|||
/// Most systems use mime types, but macOS requires Uniform Type Identifiers (UTI).
|
|||
/// </param>
|
|||
/// <returns>A new <see cref="DataFormat"/>.</returns>
|
|||
public static DataFormat<byte[]> CreateBytesPlatformFormat(string identifier) |
|||
=> CreatePlatformFormat<byte[]>(identifier); |
|||
|
|||
/// <summary>
|
|||
/// Creates a new format for the current platform that returns a <see cref="string"/>.
|
|||
/// </summary>
|
|||
/// <param name="identifier">
|
|||
/// The format identifier. This value is not validated and is passed AS IS to the underlying platform.
|
|||
/// Most systems use mime types, but macOS requires Uniform Type Identifiers (UTI).
|
|||
/// </param>
|
|||
/// <returns>A new <see cref="DataFormat"/>.</returns>
|
|||
public static DataFormat<string> CreateStringPlatformFormat(string identifier) |
|||
=> CreatePlatformFormat<string>(identifier); |
|||
|
|||
private static DataFormat<T> CreatePlatformFormat<T>(string identifier) |
|||
where T : class |
|||
{ |
|||
ThrowHelper.ThrowIfNullOrEmpty(identifier); |
|||
|
|||
return new(DataFormatKind.Platform, identifier); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a <see cref="DataFormat"/> from a name coming from the underlying platform.
|
|||
/// </summary>
|
|||
/// <param name="systemName">The name.</param>
|
|||
/// <param name="applicationPrefix">The system prefix used to recognize the name as an application format.</param>
|
|||
/// <returns>A <see cref="DataFormat"/> corresponding to <paramref name="systemName"/>.</returns>
|
|||
[PrivateApi] |
|||
public static DataFormat<T> FromSystemName<T>(string systemName, string applicationPrefix) |
|||
where T : class |
|||
{ |
|||
ThrowHelper.ThrowIfNull(systemName); |
|||
ThrowHelper.ThrowIfNull(applicationPrefix); |
|||
|
|||
if (systemName.StartsWith(applicationPrefix, StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
var identifier = systemName.Substring(applicationPrefix.Length); |
|||
if (IsValidApplicationFormatIdentifier(identifier)) |
|||
return new(DataFormatKind.Application, identifier); |
|||
} |
|||
|
|||
return new(DataFormatKind.Platform, systemName); |
|||
} |
|||
|
|||
private static bool IsValidApplicationFormatIdentifier(string identifier) |
|||
{ |
|||
if (string.IsNullOrEmpty(identifier)) |
|||
return false; |
|||
|
|||
foreach (var c in identifier) |
|||
{ |
|||
if (!IsValidChar(c)) |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
|
|||
static bool IsValidChar(char c) |
|||
=> IsAsciiLetterOrDigit(c) || c == '.' || c == '-'; |
|||
|
|||
static bool IsAsciiLetterOrDigit(char c) |
|||
{ |
|||
#if NET8_0_OR_GREATER
|
|||
return char.IsAsciiLetterOrDigit(c); |
|||
#else
|
|||
return c is |
|||
(>= '0' and <= '9') or |
|||
(>= 'A' and <= 'Z') or |
|||
(>= 'a' and <= 'z'); |
|||
#endif
|
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public override string ToString() |
|||
=> $"{Kind}: {Identifier}"; |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
namespace Avalonia.Input; |
|||
|
|||
/// <summary>
|
|||
/// Represents the kind of a <see cref="DataFormat"/>.
|
|||
/// </summary>
|
|||
public enum DataFormatKind |
|||
{ |
|||
/// <summary>
|
|||
/// <para>
|
|||
/// The data format is specific to the application.
|
|||
/// The exact format name used internally by Avalonia will vary depending on the platform.
|
|||
/// </para>
|
|||
/// <para>
|
|||
/// Such a format is created using <see cref="DataFormat.CreateBytesApplicationFormat"/>
|
|||
/// or <see cref="DataFormat.CreateStringApplicationFormat"/>.
|
|||
/// </para>
|
|||
/// </summary>
|
|||
/// <seealso cref="DataFormat.CreateBytesApplicationFormat"/>
|
|||
/// <seealso cref="DataFormat.CreateStringApplicationFormat"/>
|
|||
Application, |
|||
|
|||
/// <summary>
|
|||
/// <para>
|
|||
/// The data format is specific to the current platform.
|
|||
/// Any other application using the same identifier will be able to access it.
|
|||
/// </para>
|
|||
/// <para>
|
|||
/// Such a format is created using <see cref="DataFormat.CreateBytesPlatformFormat"/>
|
|||
/// or <see cref="DataFormat.CreateStringPlatformFormat"/>.
|
|||
/// </para>
|
|||
/// </summary>
|
|||
/// <seealso cref="DataFormat.CreateBytesPlatformFormat"/>
|
|||
/// <seealso cref="DataFormat.CreateStringPlatformFormat"/>
|
|||
Platform, |
|||
|
|||
/// <summary>
|
|||
/// <para>
|
|||
/// The data format is cross-platform and supported directly by Avalonia.
|
|||
/// Such formats include <see cref="DataFormat.Text"/> and <see cref="DataFormat.File"/>.
|
|||
/// </para>
|
|||
/// <para>
|
|||
/// It is not possible to create such a format directly.
|
|||
/// </para>
|
|||
/// </summary>
|
|||
Universal |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
using System.Diagnostics.CodeAnalysis; |
|||
|
|||
namespace Avalonia.Input; |
|||
|
|||
/// <summary>
|
|||
/// Represents a format usable with the clipboard and drag-and-drop, with a data type.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The data type.</typeparam>
|
|||
/// <remarks>
|
|||
/// This class cannot be instantiated directly.
|
|||
/// Use universal formats such as <see cref="DataFormat.Text"/> and <see cref="DataFormat.File"/>,
|
|||
/// or create custom formats using <see cref="DataFormat.CreateBytesApplicationFormat"/>,
|
|||
/// <see cref="DataFormat.CreateStringApplicationFormat"/>, <see cref="DataFormat.CreateBytesPlatformFormat"/>
|
|||
/// or <see cref="DataFormat.CreateStringPlatformFormat"/>.
|
|||
/// </remarks>
|
|||
[SuppressMessage("ReSharper", "UnusedTypeParameter", Justification = "Used to resolve typed overloads.")] |
|||
public sealed class DataFormat<T> : DataFormat |
|||
where T : class |
|||
{ |
|||
internal DataFormat(DataFormatKind kind, string identifier) |
|||
: base(kind, identifier) |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Input; |
|||
|
|||
/// <summary>
|
|||
/// A mutable implementation of <see cref="IDataTransfer"/> and <see cref="IAsyncDataTransfer"/>.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// While it also implements <see cref="IAsyncDataTransfer"/>, this class always returns data synchronously.
|
|||
/// For advanced usages, consider implementing <see cref="IAsyncDataTransfer"/> directly.
|
|||
/// </remarks>
|
|||
public sealed class DataTransfer : IDataTransfer, IAsyncDataTransfer |
|||
{ |
|||
private readonly List<DataTransferItem> _items = []; |
|||
private DataFormat[]? _formats; |
|||
|
|||
/// <inheritdoc cref="IDataTransferItem.Formats" />
|
|||
public IReadOnlyList<DataFormat> Formats |
|||
{ |
|||
get |
|||
{ |
|||
return _formats ??= GetFormatsCore(); |
|||
|
|||
DataFormat[] GetFormatsCore() |
|||
=> Items.SelectMany(item => item.Formats).Distinct().ToArray(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a list of <see cref="DataTransferItem"/> contained in this object.
|
|||
/// </summary>
|
|||
public IReadOnlyList<DataTransferItem> Items |
|||
=> _items; |
|||
|
|||
IReadOnlyList<IDataTransferItem> IDataTransfer.Items |
|||
=> Items; |
|||
|
|||
IReadOnlyList<IAsyncDataTransferItem> IAsyncDataTransfer.Items |
|||
=> Items; |
|||
|
|||
/// <summary>
|
|||
/// Adds an existing <see cref="DataTransferItem"/> to this object.
|
|||
/// </summary>
|
|||
/// <param name="item">The item to add.</param>
|
|||
public void Add(DataTransferItem item) |
|||
{ |
|||
ThrowHelper.ThrowIfNull(item); |
|||
|
|||
_formats = null; |
|||
_items.Add(item); |
|||
} |
|||
|
|||
void IDisposable.Dispose() |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,130 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Input.Platform; |
|||
using Avalonia.Platform.Storage; |
|||
|
|||
namespace Avalonia.Input; |
|||
|
|||
// Keep DataTransferExtensions.TryGetXxx methods in sync with AsyncDataTransferExtensions.TryGetXxxAsync ones.
|
|||
|
|||
/// <summary>
|
|||
/// Contains extension methods for <see cref="IDataTransfer"/>.
|
|||
/// </summary>
|
|||
public static class DataTransferExtensions |
|||
{ |
|||
[Obsolete] |
|||
internal static IDataObject ToLegacyDataObject(this IDataTransfer dataTransfer) |
|||
=> (dataTransfer as DataObjectToDataTransferWrapper)?.DataObject |
|||
?? new DataTransferToDataObjectWrapper(dataTransfer); |
|||
|
|||
/// <summary>
|
|||
/// Gets whether a <see cref="IDataTransfer"/> supports a specific format.
|
|||
/// </summary>
|
|||
/// <param name="dataTransfer">The <see cref="IDataTransfer"/> instance.</param>
|
|||
/// <param name="format">The format to check.</param>
|
|||
/// <returns>true if <paramref name="format"/> is supported, false otherwise.</returns>
|
|||
public static bool Contains(this IDataTransfer dataTransfer, DataFormat format) |
|||
{ |
|||
var formats = dataTransfer.Formats; |
|||
var count = formats.Count; |
|||
|
|||
for (var i = 0; i < count; ++i) |
|||
{ |
|||
if (format == formats[i]) |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the list of <see cref="IDataTransferItem"/> contained in this object, filtered by a given format.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// <para>
|
|||
/// Some platforms (such as Windows and X11) may only support a single data item for all formats
|
|||
/// except <see cref="DataFormat.File"/>.
|
|||
/// </para>
|
|||
/// <para>Items returned by this property must stay valid until the <see cref="IDataTransfer"/> is disposed.</para>
|
|||
/// </remarks>
|
|||
public static IEnumerable<IDataTransferItem> GetItems(this IDataTransfer dataTransfer, DataFormat format) |
|||
{ |
|||
var items = dataTransfer.Items; |
|||
var count = items.Count; |
|||
|
|||
for (var i = 0; i < count; ++i) |
|||
{ |
|||
var item = items[i]; |
|||
if (item.Contains(format)) |
|||
yield return item; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tries to get a value for a given format from a <see cref="IDataTransfer"/>.
|
|||
/// </summary>
|
|||
/// <param name="dataTransfer">The <see cref="IDataTransfer"/> instance.</param>
|
|||
/// <param name="format">The format to retrieve.</param>
|
|||
/// <returns>A value for <paramref name="format"/>, or null if the format is not supported.</returns>
|
|||
/// <remarks>
|
|||
/// If the <see cref="IDataTransfer"/> contains several items supporting <paramref name="format"/>,
|
|||
/// the first matching one will be returned.
|
|||
/// </remarks>
|
|||
public static T? TryGetValue<T>(this IDataTransfer dataTransfer, DataFormat<T> format) |
|||
where T : class |
|||
=> dataTransfer.GetItems(format).FirstOrDefault() is { } item ? |
|||
item.TryGetValue(format) : |
|||
null; |
|||
|
|||
/// <summary>
|
|||
/// Tries to get multiple values for a given format from a <see cref="IDataTransfer"/>.
|
|||
/// </summary>
|
|||
/// <param name="dataTransfer">The <see cref="IDataTransfer"/> instance.</param>
|
|||
/// <param name="format">The format to retrieve.</param>
|
|||
/// <returns>A list of values for <paramref name="format"/>, or null if the format is not supported.</returns>
|
|||
public static T[]? TryGetValues<T>(this IDataTransfer dataTransfer, DataFormat<T> format) |
|||
where T : class |
|||
{ |
|||
List<T>? results = null; |
|||
|
|||
foreach (var item in dataTransfer.GetItems(format)) |
|||
{ |
|||
var result = item.TryGetValue(format); |
|||
if (result is null) |
|||
continue; |
|||
|
|||
results ??= []; |
|||
results.Add(result); |
|||
} |
|||
|
|||
return results?.ToArray(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a text, if available, from a <see cref="IDataTransfer"/> instance.
|
|||
/// </summary>
|
|||
/// <param name="dataTransfer">The data transfer instance.</param>
|
|||
/// <returns>A string, or null if the format isn't available.</returns>
|
|||
/// <seealso cref="DataFormat.Text"/>.
|
|||
public static string? TryGetText(this IDataTransfer dataTransfer) |
|||
=> dataTransfer.TryGetValue(DataFormat.Text); |
|||
|
|||
/// <summary>
|
|||
/// Returns a file, if available, from a <see cref="IDataTransfer"/> instance.
|
|||
/// </summary>
|
|||
/// <param name="dataTransfer">The data transfer instance.</param>
|
|||
/// <returns>An <see cref="IStorageItem"/> (file or folder), or null if the format isn't available.</returns>
|
|||
/// <seealso cref="DataFormat.File"/>.
|
|||
public static IStorageItem? TryGetFile(this IDataTransfer dataTransfer) |
|||
=> dataTransfer.TryGetValue(DataFormat.File); |
|||
|
|||
/// <summary>
|
|||
/// Returns a list of files, if available, from a <see cref="IDataTransfer"/> instance.
|
|||
/// </summary>
|
|||
/// <param name="dataTransfer">The data transfer instance.</param>
|
|||
/// <returns>An array of <see cref="IStorageItem"/> (files or folders), or null if the format isn't available.</returns>
|
|||
/// <seealso cref="DataFormat.File"/>.
|
|||
public static IStorageItem[]? TryGetFiles(this IDataTransfer dataTransfer) |
|||
=> dataTransfer.TryGetValues(DataFormat.File); |
|||
} |
|||
@ -0,0 +1,222 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Platform.Storage; |
|||
using Avalonia.Utilities; |
|||
|
|||
namespace Avalonia.Input; |
|||
|
|||
/// <summary>
|
|||
/// A mutable implementation of <see cref="IDataTransferItem"/> and <see cref="IAsyncDataTransferItem"/>.
|
|||
/// This class also provides several static methods to easily create a <see cref="DataTransferItem"/> for common usages.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// While it also implements <see cref="IAsyncDataTransferItem"/>, this class always returns data synchronously.
|
|||
/// For advanced usages, consider implementing <see cref="IAsyncDataTransferItem"/> directly.
|
|||
/// </remarks>
|
|||
public sealed class DataTransferItem : IDataTransferItem, IAsyncDataTransferItem |
|||
{ |
|||
private Dictionary<DataFormat, DataAccessor>? _accessorByFormat; // used for 2+ formats
|
|||
private KeyValuePair<DataFormat, DataAccessor>? _singleItem; // used for the common single format case
|
|||
private DataFormat[]? _formats; |
|||
|
|||
/// <inheritdoc cref="IDataTransferItem.Formats" />
|
|||
public IReadOnlyList<DataFormat> Formats |
|||
{ |
|||
get |
|||
{ |
|||
return _formats ??= ComputeFormats(); |
|||
|
|||
DataFormat[] ComputeFormats() |
|||
{ |
|||
if (_accessorByFormat is not null) |
|||
return _accessorByFormat.Keys.ToArray(); |
|||
if (_singleItem is { } singleItem) |
|||
return [singleItem.Key]; |
|||
return []; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public object? TryGetRaw(DataFormat format) |
|||
=> FindAccessor(format)?.GetValue(); |
|||
|
|||
Task<object?> IAsyncDataTransferItem.TryGetRawAsync(DataFormat format) |
|||
{ |
|||
try |
|||
{ |
|||
return Task.FromResult(TryGetRaw(format)); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
return Task.FromException<object?>(ex); |
|||
} |
|||
} |
|||
|
|||
private DataAccessor? FindAccessor(DataFormat format) |
|||
{ |
|||
if (_accessorByFormat is not null) |
|||
return _accessorByFormat.TryGetValue(format, out var accessor) ? accessor : null; |
|||
|
|||
if (_singleItem is { } singleItem) |
|||
return singleItem.Value; |
|||
|
|||
return null; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets the value for a given format.
|
|||
/// </summary>
|
|||
/// <param name="format">The format.</param>
|
|||
/// <param name="value">
|
|||
/// The value corresponding to <paramref name="format"/>.
|
|||
/// If null, the format won't be part of the <see cref="DataTransferItem"/>.
|
|||
/// </param>
|
|||
public void Set<T>(DataFormat<T> format, T? value) |
|||
where T : class |
|||
{ |
|||
ThrowHelper.ThrowIfNull(format); |
|||
|
|||
if (value is null) |
|||
RemoveCore(format); |
|||
else |
|||
SetCore(format, new DataAccessor(static state => state, value)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets a value created on demand for a given format.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The value type.</typeparam>
|
|||
/// <param name="format">The format.</param>
|
|||
/// <param name="getValue">A function returning the value corresponding to <paramref name="format"/>.</param>
|
|||
public void Set<T>(DataFormat<T> format, Func<T?> getValue) |
|||
where T : class |
|||
{ |
|||
ThrowHelper.ThrowIfNull(format); |
|||
ThrowHelper.ThrowIfNull(getValue); |
|||
|
|||
SetCore(format, new DataAccessor(static state => ((Func<T?>)state)(), getValue)); |
|||
} |
|||
|
|||
private void SetCore(DataFormat format, DataAccessor accessor) |
|||
{ |
|||
if (_accessorByFormat is not null) |
|||
_accessorByFormat[format] = accessor; |
|||
else if (_singleItem is { } singleItem && !singleItem.Key.Equals(format)) |
|||
{ |
|||
_accessorByFormat = new() |
|||
{ |
|||
[singleItem.Key] = singleItem.Value, |
|||
[format] = accessor |
|||
}; |
|||
_singleItem = null; |
|||
} |
|||
else |
|||
_singleItem = new(format, accessor); |
|||
|
|||
_formats = null; |
|||
} |
|||
|
|||
private void RemoveCore(DataFormat format) |
|||
{ |
|||
bool removed; |
|||
|
|||
if (_accessorByFormat is not null) |
|||
removed = _accessorByFormat.Remove(format); |
|||
else if (_singleItem is { } singleItem && singleItem.Key.Equals(format)) |
|||
{ |
|||
_singleItem = null; |
|||
removed = true; |
|||
} |
|||
else |
|||
removed = false; |
|||
|
|||
if (removed) |
|||
_formats = null; |
|||
} |
|||
|
|||
|
|||
/// <summary>
|
|||
/// Sets the value for the <see cref="DataFormat.Text"/> format.
|
|||
/// </summary>
|
|||
/// <param name="value">
|
|||
/// The value corresponding to the <see cref="DataFormat.Text"/> format.
|
|||
/// If null, the format won't be part of the <see cref="DataTransferItem"/>.
|
|||
/// </param>
|
|||
public void SetText(string? value) |
|||
=> Set(DataFormat.Text, value); |
|||
|
|||
/// <summary>
|
|||
/// Sets the value for the <see cref="DataFormat.File"/> format.
|
|||
/// </summary>
|
|||
/// <param name="value">
|
|||
/// The value corresponding to the <see cref="DataFormat.File"/> format.
|
|||
/// If null, the format won't be part of the <see cref="DataTransferItem"/>.
|
|||
/// </param>
|
|||
public void SetFile(IStorageItem? value) |
|||
=> Set(DataFormat.File, value); |
|||
|
|||
/// <summary>
|
|||
/// Creates a new <see cref="DataTransferItem"/> for a single format with a given value.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The value type.</typeparam>
|
|||
/// <param name="format">The format.</param>
|
|||
/// <param name="value">
|
|||
/// The value corresponding to <paramref name="format"/>.
|
|||
/// If null, the format won't be part of the <see cref="DataTransferItem"/>.
|
|||
/// </param>
|
|||
/// <returns>A <see cref="DataTransferItem"/> instance.</returns>
|
|||
public static DataTransferItem Create<T>(DataFormat<T> format, T? value) |
|||
where T : class |
|||
{ |
|||
var item = new DataTransferItem(); |
|||
item.Set(format, value); |
|||
return item; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a new <see cref="DataTransferItem"/> for a single format with a given value created on demand.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The value type.</typeparam>
|
|||
/// <param name="format">The format.</param>
|
|||
/// <param name="getValue">A function returning the value corresponding to <paramref name="format"/>.</param>
|
|||
/// <returns>A <see cref="DataTransferItem"/> instance.</returns>
|
|||
public static DataTransferItem Create<T>(DataFormat<T> format, Func<T?> getValue) |
|||
where T : class |
|||
{ |
|||
var item = new DataTransferItem(); |
|||
item.Set(format, getValue); |
|||
return item; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a new <see cref="DataTransferItem"/> with <see cref="DataFormat.Text"/> as a single format.
|
|||
/// </summary>
|
|||
/// <param name="value">
|
|||
/// The value corresponding to the <see cref="DataFormat.Text"/> format.
|
|||
/// If null, the format won't be part of the <see cref="DataTransferItem"/>.
|
|||
/// </param>
|
|||
/// <returns>A <see cref="DataTransferItem"/> instance.</returns>
|
|||
public static DataTransferItem CreateText(string? value) |
|||
=> Create(DataFormat.Text, value); |
|||
|
|||
/// <summary>
|
|||
/// Creates a new <see cref="DataTransferItem"/> with <see cref="DataFormat.File"/> as a single format.
|
|||
/// </summary>
|
|||
/// <param name="value">
|
|||
/// The value corresponding to the <see cref="DataFormat.File"/> format.
|
|||
/// If null, the format won't be part of the <see cref="DataTransferItem"/>.
|
|||
/// </param>
|
|||
/// <returns>A <see cref="DataTransferItem"/> instance.</returns>
|
|||
public static DataTransferItem CreateFile(IStorageItem? value) |
|||
=> Create(DataFormat.File, value); |
|||
|
|||
private readonly struct DataAccessor(Func<object, object?> getValue, object state) |
|||
{ |
|||
public object? GetValue() |
|||
=> getValue(state); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,57 @@ |
|||
using Avalonia.Platform.Storage; |
|||
|
|||
namespace Avalonia.Input; |
|||
|
|||
/// <summary>
|
|||
/// Contains extension methods for <see cref="IDataTransferItem"/>.
|
|||
/// </summary>
|
|||
public static class DataTransferItemExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Gets whether a <see cref="IDataTransferItem"/> supports a specific format.
|
|||
/// </summary>
|
|||
/// <param name="dataTransferItem">The <see cref="IDataTransferItem"/> instance.</param>
|
|||
/// <param name="format">The format to check.</param>
|
|||
/// <returns>true if <paramref name="format"/> is supported, false otherwise.</returns>
|
|||
public static bool Contains(this IDataTransferItem dataTransferItem, DataFormat format) |
|||
{ |
|||
var formats = dataTransferItem.Formats; |
|||
var count = formats.Count; |
|||
|
|||
for (var i = 0; i < count; ++i) |
|||
{ |
|||
if (format == formats[i]) |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tries to get a value for a given format from a <see cref="IDataTransferItem"/>.
|
|||
/// </summary>
|
|||
/// <param name="dataTransferItem">The <see cref="IDataTransferItem"/> instance.</param>
|
|||
/// <param name="format">The format to retrieve.</param>
|
|||
/// <returns>A value for <paramref name="format"/>, or null if the format is not supported.</returns>
|
|||
public static T? TryGetValue<T>(this IDataTransferItem dataTransferItem, DataFormat<T> format) |
|||
where T : class |
|||
=> dataTransferItem.TryGetRaw(format) as T; |
|||
|
|||
/// <summary>
|
|||
/// Returns a text, if available, from a <see cref="IDataTransferItem"/> instance.
|
|||
/// </summary>
|
|||
/// <param name="dataTransferItem">The data transfer instance.</param>
|
|||
/// <returns>A string, or null if the format isn't available.</returns>
|
|||
/// <seealso cref="DataFormat.Text"/>.
|
|||
public static string? TryGetText(this IDataTransferItem dataTransferItem) |
|||
=> dataTransferItem.TryGetValue(DataFormat.Text); |
|||
|
|||
/// <summary>
|
|||
/// Returns a file, if available, from a <see cref="IDataTransferItem"/> instance.
|
|||
/// </summary>
|
|||
/// <param name="dataTransferItem">The data transfer instance.</param>
|
|||
/// <returns>An <see cref="IStorageItem"/> (file or folder), or null if the format isn't available.</returns>
|
|||
/// <seealso cref="DataFormat.File"/>.
|
|||
public static IStorageItem? TryGetFile(this IDataTransferItem dataTransferItem) |
|||
=> dataTransferItem.TryGetValue(DataFormat.File); |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Input.Platform; |
|||
|
|||
namespace Avalonia.Input; |
|||
|
|||
/// <summary>
|
|||
/// Represents an object providing a list of <see cref="IAsyncDataTransferItem"/> usable by the clipboard.
|
|||
/// </summary>
|
|||
/// <seealso cref="DataTransfer"/>
|
|||
/// <remarks>
|
|||
/// <list type="bullet">
|
|||
/// <item>
|
|||
/// When an implementation of this interface is put into the clipboard using <see cref="IClipboard.SetDataAsync"/>,
|
|||
/// it must NOT be disposed by the caller. The system will dispose of it automatically when it becomes unused.
|
|||
/// </item>
|
|||
/// <item>
|
|||
/// When an implementation of this interface is returned from the clipboard via <see cref="IClipboard.TryGetDataAsync"/>,
|
|||
/// it MUST be disposed the caller.
|
|||
/// </item>
|
|||
/// <item>
|
|||
/// This interface is mostly used during clipboard operations. However, several platforms only support synchronous
|
|||
/// clipboard manipulation and will try to use <see cref="IDataTransfer"/> if the underlying type also implements it.
|
|||
/// For this reason, custom implementations should ideally implement both <see cref="IAsyncDataTransfer"/> and
|
|||
/// <see cref="IDataTransfer"/>.
|
|||
/// </item>
|
|||
/// </list>
|
|||
/// </remarks>
|
|||
public interface IAsyncDataTransfer : IDisposable |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the formats supported by a <see cref="IAsyncDataTransfer"/>.
|
|||
/// </summary>
|
|||
IReadOnlyList<DataFormat> Formats { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a list of <see cref="IAsyncDataTransferItem"/> contained in this object.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// <para>
|
|||
/// Some platforms (such as Windows and X11) may only support a single data item for all formats
|
|||
/// except <see cref="DataFormat.File"/>.
|
|||
/// </para>
|
|||
/// <para>Items returned by this property must stay valid until the <see cref="IAsyncDataTransfer"/> is disposed.</para>
|
|||
/// </remarks>
|
|||
IReadOnlyList<IAsyncDataTransferItem> Items { get; } |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Avalonia.Input; |
|||
|
|||
/// <summary>
|
|||
/// Represent an item inside a <see cref="IAsyncDataTransfer"/>.
|
|||
/// An item may support several formats and can return the value of a given format on demand.
|
|||
/// </summary>
|
|||
/// <seealso cref="DataTransferItem"/>
|
|||
public interface IAsyncDataTransferItem |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the formats supported by this item.
|
|||
/// </summary>
|
|||
IReadOnlyList<DataFormat> Formats { get; } |
|||
|
|||
/// <summary>
|
|||
/// Tries to get a value for a given format.
|
|||
/// </summary>
|
|||
/// <param name="format">The format to retrieve.</param>
|
|||
/// <returns>A value for <paramref name="format"/>, or null if the format is not supported.</returns>
|
|||
/// <remarks>
|
|||
/// <para>
|
|||
/// Implementations of this method are expected to return a value matching the exact type
|
|||
/// of the generic argument of the underlying <see cref="DataFormat{T}"/>.
|
|||
/// </para>
|
|||
/// <para>
|
|||
/// To retrieve a typed value, use <see cref="DataTransferItemExtensions.TryGetValue"/>.
|
|||
/// </para>
|
|||
/// </remarks>
|
|||
/// <seealso cref="AsyncDataTransferItemExtensions.TryGetValueAsync"/>
|
|||
/// <seealso cref="AsyncDataTransferExtensions.TryGetValueAsync"/>
|
|||
Task<object?> TryGetRawAsync(DataFormat format); |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.Input; |
|||
|
|||
/// <summary>
|
|||
/// Represents an object providing a list of <see cref="IDataTransferItem"/> usableduring a drag and drop operation.
|
|||
/// </summary>
|
|||
/// <seealso cref="DataTransfer"/>
|
|||
/// <remarks>
|
|||
/// When an implementation of this interface is used as a drag source using <see cref="DragDrop.DoDragDropAsync"/>,
|
|||
/// it must NOT be disposed by the caller. The system will dispose of it automatically when the drag operation completes.
|
|||
/// </remarks>
|
|||
public interface IDataTransfer : IDisposable |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the formats supported by a <see cref="IDataTransfer"/>.
|
|||
/// </summary>
|
|||
IReadOnlyList<DataFormat> Formats { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a list of <see cref="IDataTransferItem"/> contained in this object.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// <para>
|
|||
/// Some platforms (such as Windows and X11) may only support a single data item for all formats
|
|||
/// except <see cref="DataFormat.File"/>.
|
|||
/// </para>
|
|||
/// <para>Items returned by this property must stay valid until the <see cref="IDataTransfer"/> is disposed.</para>
|
|||
/// </remarks>
|
|||
IReadOnlyList<IDataTransferItem> Items { get; } |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.Input; |
|||
|
|||
/// <summary>
|
|||
/// Represent an item inside a <see cref="IDataTransfer"/>.
|
|||
/// An item may support several formats and can return the value of a given format on demand.
|
|||
/// </summary>
|
|||
/// <seealso cref="DataTransferItem"/>
|
|||
public interface IDataTransferItem |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the formats supported by this item.
|
|||
/// </summary>
|
|||
IReadOnlyList<DataFormat> Formats { get; } |
|||
|
|||
/// <summary>
|
|||
/// Tries to get a value for a given format.
|
|||
/// </summary>
|
|||
/// <param name="format">The format to retrieve.</param>
|
|||
/// <returns>A value for <paramref name="format"/>, or null if the format is not supported.</returns>
|
|||
/// <remarks>
|
|||
/// <para>
|
|||
/// Implementations of this method are expected to return a value matching the exact type
|
|||
/// of the generic argument of the underlying <see cref="DataFormat{T}"/>.
|
|||
/// </para>
|
|||
/// <para>
|
|||
/// To retrieve a typed value, use <see cref="DataTransferItemExtensions.TryGetValue"/>.
|
|||
/// </para>
|
|||
/// </remarks>
|
|||
/// <seealso cref="DataTransferItemExtensions.TryGetValue"/>
|
|||
/// <seealso cref="DataTransferExtensions.TryGetValue"/>
|
|||
object? TryGetRaw(DataFormat format); |
|||
} |
|||
@ -0,0 +1,72 @@ |
|||
using System; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.IO; |
|||
using System.Runtime.Serialization.Formatters.Binary; |
|||
using Avalonia.Compatibility; |
|||
using Avalonia.Logging; |
|||
|
|||
namespace Avalonia.Input.Platform; |
|||
|
|||
// TODO12: remove
|
|||
[Obsolete("Remove in v12")] |
|||
internal static class BinaryFormatterHelper |
|||
{ |
|||
// Compatibility with WinForms + WPF...
|
|||
private static ReadOnlySpan<byte> SerializedObjectGuid |
|||
=> [ |
|||
// FD9EA796-3B13-4370-A679-56106BB288FB
|
|||
0x96, 0xa7, 0x9e, 0xfd, |
|||
0x13, 0x3b, |
|||
0x70, 0x43, |
|||
0xa6, 0x79, 0x56, 0x10, 0x6b, 0xb2, 0x88, 0xfb |
|||
]; |
|||
|
|||
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "We still use BinaryFormatter for WinForms drag and drop compatibility")] |
|||
[UnconditionalSuppressMessage("Trimming", "IL3050", Justification = "We still use BinaryFormatter for WinForms drag and drop compatibility")] |
|||
public static byte[]? TrySerializeUsingBinaryFormatter(object data, DataFormat dataFormat) |
|||
{ |
|||
if (!OperatingSystemEx.IsWindows()) |
|||
return null; |
|||
|
|||
Logger.TryGet(LogEventLevel.Warning, LogArea.Win32Platform)?.Log( |
|||
null, |
|||
"Using BinaryFormatter to serialize data format {Format}. This won't be supported in Avalonia v12. Prefer passing a byte[] or Stream instead.", |
|||
dataFormat); |
|||
|
|||
var stream = new MemoryStream(); |
|||
var serializedGuid = SerializedObjectGuid; |
|||
|
|||
#if NET6_0_OR_GREATER
|
|||
stream.Write(serializedGuid); |
|||
#else
|
|||
stream.Write(serializedGuid.ToArray(), 0, serializedGuid.Length); |
|||
#endif
|
|||
|
|||
#pragma warning disable SYSLIB0011 // Type or member is obsolete
|
|||
new BinaryFormatter().Serialize(stream, data); |
|||
#pragma warning restore SYSLIB0011 // Type or member is obsolete
|
|||
|
|||
return stream.GetBuffer(); |
|||
} |
|||
|
|||
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "We still use BinaryFormatter for WinForms drag and drop compatibility")] |
|||
[UnconditionalSuppressMessage("Trimming", "IL3050", Justification = "We still use BinaryFormatter for WinForms drag and drop compatibility")] |
|||
public static object? TryDeserializeUsingBinaryFormatter(byte[]? bytes) |
|||
{ |
|||
var serializedObjectGuid = SerializedObjectGuid; |
|||
|
|||
// Our Win32 backend used to automatically serialize/deserialize objects using the BinaryFormatter.
|
|||
// Only keep that behavior for compatibility with IDataObject.
|
|||
if (OperatingSystemEx.IsWindows() && bytes is not null && bytes.AsSpan().StartsWith(serializedObjectGuid)) |
|||
{ |
|||
using var stream = new MemoryStream(bytes); |
|||
stream.Position = serializedObjectGuid.Length; |
|||
|
|||
#pragma warning disable SYSLIB0011 // Type or member is obsolete
|
|||
return new BinaryFormatter().Deserialize(stream); |
|||
#pragma warning restore SYSLIB0011 // Type or member is obsolete
|
|||
} |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
@ -0,0 +1,103 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Compatibility; |
|||
using Avalonia.Platform.Storage; |
|||
|
|||
namespace Avalonia.Input.Platform; |
|||
|
|||
/// <summary>
|
|||
/// Implementation of <see cref="IClipboard"/>
|
|||
/// </summary>
|
|||
internal sealed class Clipboard(IClipboardImpl clipboardImpl) : IClipboard |
|||
{ |
|||
private readonly IClipboardImpl _clipboardImpl = clipboardImpl; |
|||
private IAsyncDataTransfer? _lastDataTransfer; |
|||
|
|||
Task<string?> IClipboard.GetTextAsync() |
|||
=> this.TryGetTextAsync(); |
|||
|
|||
Task IClipboard.SetTextAsync(string? text) |
|||
=> this.SetValueAsync(DataFormat.Text, text); |
|||
|
|||
public Task ClearAsync() |
|||
{ |
|||
_lastDataTransfer?.Dispose(); |
|||
_lastDataTransfer = null; |
|||
|
|||
return _clipboardImpl.ClearAsync(); |
|||
} |
|||
|
|||
[Obsolete($"Use {nameof(SetDataAsync)} instead.")] |
|||
Task IClipboard.SetDataObjectAsync(IDataObject data) |
|||
=> SetDataAsync(new DataObjectToDataTransferWrapper(data)); |
|||
|
|||
public Task SetDataAsync(IAsyncDataTransfer? dataTransfer) |
|||
{ |
|||
if (dataTransfer is null) |
|||
return ClearAsync(); |
|||
|
|||
if (_clipboardImpl is IOwnedClipboardImpl) |
|||
_lastDataTransfer = dataTransfer; |
|||
|
|||
return _clipboardImpl.SetDataAsync(dataTransfer); |
|||
} |
|||
|
|||
public Task FlushAsync() |
|||
=> _clipboardImpl is IFlushableClipboardImpl flushable ? flushable.FlushAsync() : Task.CompletedTask; |
|||
|
|||
async Task<string[]> IClipboard.GetFormatsAsync() |
|||
{ |
|||
var dataTransfer = await TryGetDataAsync(); |
|||
return dataTransfer is null ? [] : dataTransfer.Formats.Select(DataFormats.ToString).ToArray(); |
|||
} |
|||
|
|||
[Obsolete($"Use {nameof(TryGetDataAsync)} instead.")] |
|||
async Task<object?> IClipboard.GetDataAsync(string format) |
|||
{ |
|||
// No ConfigureAwait(false) here: we want TryGetXxxAsync() below to be called on the initial thread.
|
|||
using var dataTransfer = await TryGetDataAsync(); |
|||
if (dataTransfer is null) |
|||
return null; |
|||
|
|||
if (format == DataFormats.Text) |
|||
return await dataTransfer.TryGetTextAsync().ConfigureAwait(false); |
|||
|
|||
if (format == DataFormats.Files) |
|||
return await dataTransfer.TryGetFilesAsync().ConfigureAwait(false); |
|||
|
|||
if (format == DataFormats.FileNames) |
|||
{ |
|||
return (await dataTransfer.TryGetFilesAsync().ConfigureAwait(false)) |
|||
?.Select(file => file.TryGetLocalPath()) |
|||
.Where(path => path is not null) |
|||
.ToArray(); |
|||
} |
|||
|
|||
var typedFormat = DataFormat.CreateBytesPlatformFormat(format); |
|||
var bytes = await dataTransfer.TryGetValueAsync(typedFormat).ConfigureAwait(false); |
|||
return BinaryFormatterHelper.TryDeserializeUsingBinaryFormatter(bytes) ?? bytes; |
|||
} |
|||
|
|||
public Task<IAsyncDataTransfer?> TryGetDataAsync() |
|||
=> _clipboardImpl.TryGetDataAsync(); |
|||
|
|||
[Obsolete($"Use {nameof(TryGetInProcessDataAsync)} instead.")] |
|||
async Task<IDataObject?> IClipboard.TryGetInProcessDataObjectAsync() |
|||
{ |
|||
var dataTransfer = await TryGetInProcessDataAsync().ConfigureAwait(false); |
|||
return (dataTransfer as DataObjectToDataTransferWrapper)?.DataObject; |
|||
} |
|||
|
|||
public async Task<IAsyncDataTransfer?> TryGetInProcessDataAsync() |
|||
{ |
|||
if (_lastDataTransfer is null || _clipboardImpl is not IOwnedClipboardImpl ownedClipboardImpl) |
|||
return null; |
|||
|
|||
if (!await ownedClipboardImpl.IsCurrentOwnerAsync()) |
|||
_lastDataTransfer = null; |
|||
|
|||
return _lastDataTransfer; |
|||
} |
|||
} |
|||
@ -0,0 +1,189 @@ |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Platform.Storage; |
|||
|
|||
namespace Avalonia.Input.Platform; |
|||
|
|||
/// <summary>
|
|||
/// Contains extension methods related to <see cref="IClipboard"/>.
|
|||
/// </summary>
|
|||
public static class ClipboardExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Gets a list containing the formats currently available from the clipboard.
|
|||
/// </summary>
|
|||
/// <returns>A list of formats. It can be empty if the clipboard is empty.</returns>
|
|||
public static async Task<IReadOnlyList<DataFormat>> GetDataFormatsAsync(this IClipboard clipboard) |
|||
{ |
|||
using var dataTransfer = await clipboard.TryGetDataAsync(); |
|||
return dataTransfer is null ? [] : dataTransfer.Formats; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tries to get a value for a given format from the clipboard.
|
|||
/// </summary>
|
|||
/// <param name="clipboard">The <see cref="IClipboard"/> instance.</param>
|
|||
/// <param name="format">The format to retrieve.</param>
|
|||
/// <returns>A value for <paramref name="format"/>, or null if the format is not supported.</returns>
|
|||
/// <remarks>
|
|||
/// If the <see cref="IClipboard"/> contains several items supporting <paramref name="format"/>,
|
|||
/// the first matching one will be returned.
|
|||
/// </remarks>
|
|||
public static async Task<T?> TryGetValueAsync<T>(this IClipboard clipboard, DataFormat<T> format) |
|||
where T : class |
|||
{ |
|||
// No ConfigureAwait(false) here: we want TryGetValueAsync() below to be called on the initial thread.
|
|||
using var dataTransfer = await clipboard.TryGetDataAsync(); |
|||
if (dataTransfer is null) |
|||
return null; |
|||
|
|||
// However, ConfigureAwait(false) is fine here: we're not doing anything after.
|
|||
return await dataTransfer.TryGetValueAsync(format).ConfigureAwait(false); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tries to get multiple values for a given format from the clipboard.
|
|||
/// </summary>
|
|||
/// <param name="clipboard">The <see cref="IClipboard"/> instance.</param>
|
|||
/// <param name="format">The format to retrieve.</param>
|
|||
/// <returns>A list of values for <paramref name="format"/>, or null if the format is not supported.</returns>
|
|||
public static async Task<T[]?> TryGetValuesAsync<T>(this IClipboard clipboard, DataFormat<T> format) |
|||
where T : class |
|||
{ |
|||
// No ConfigureAwait(false) here: we want TryGetValuesAsync() below to be called on the initial thread.
|
|||
using var dataTransfer = await clipboard.TryGetDataAsync(); |
|||
if (dataTransfer is null) |
|||
return null; |
|||
|
|||
// However, ConfigureAwait(false) is fine here: we're not doing anything after.
|
|||
return await dataTransfer.TryGetValuesAsync(format).ConfigureAwait(false); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Places a single value on the clipboard in the specified format.
|
|||
/// </summary>
|
|||
/// <param name="clipboard">The clipboard instance.</param>
|
|||
/// <param name="format">The data format.</param>
|
|||
/// <param name="value">The value to place on the clipboard.</param>
|
|||
/// <remarks>
|
|||
/// <para>By calling this method, the clipboard will get cleared of any possible previous data.</para>
|
|||
/// <para>
|
|||
/// If <paramref name="value"/> is null, nothing will get placed on the clipboard and this method
|
|||
/// will be equivalent to <see cref="IClipboard.ClearAsync"/>.
|
|||
/// </para>
|
|||
/// </remarks>
|
|||
public static Task SetValueAsync<T>(this IClipboard clipboard, DataFormat<T> format, T? value) |
|||
where T : class |
|||
{ |
|||
if (value is null) |
|||
return clipboard.ClearAsync(); |
|||
|
|||
var dataTransfer = new DataTransfer(); |
|||
dataTransfer.Add(DataTransferItem.Create(format, value)); |
|||
return clipboard.SetDataAsync(dataTransfer); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Places multiple values on the clipboard in the specified format.
|
|||
/// </summary>
|
|||
/// <param name="clipboard">The clipboard instance.</param>
|
|||
/// <param name="format">The data format.</param>
|
|||
/// <param name="values">The values to place on the clipboard.</param>
|
|||
/// <remarks>
|
|||
/// <para>By calling this method, the clipboard will get cleared of any possible previous data.</para>
|
|||
/// <para>
|
|||
/// If <paramref name="values"/> is null or empty, nothing will get placed on the clipboard and this method
|
|||
/// will be equivalent to <see cref="IClipboard.ClearAsync"/>.
|
|||
/// </para>
|
|||
/// </remarks>
|
|||
public static Task SetValuesAsync<T>(this IClipboard clipboard, DataFormat<T> format, IEnumerable<T>? values) |
|||
where T : class |
|||
{ |
|||
if (values is null) |
|||
return clipboard.ClearAsync(); |
|||
|
|||
var dataTransfer = new DataTransfer(); |
|||
|
|||
foreach (var value in values) |
|||
dataTransfer.Add(DataTransferItem.Create(format, value)); |
|||
|
|||
return dataTransfer.Items.Count == 0 |
|||
? clipboard.ClearAsync() |
|||
: clipboard.SetDataAsync(dataTransfer); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a text, if available, from the clipboard.
|
|||
/// </summary>
|
|||
/// <param name="clipboard">The clipboard instance.</param>
|
|||
/// <returns>A string, or null if the format isn't available.</returns>
|
|||
/// <seealso cref="DataFormat.Text"/>
|
|||
public static Task<string?> TryGetTextAsync(this IClipboard clipboard) |
|||
=> clipboard.TryGetValueAsync(DataFormat.Text); |
|||
|
|||
/// <summary>
|
|||
/// Returns a file, if available, from the clipboard.
|
|||
/// </summary>
|
|||
/// <param name="clipboard">The clipboard instance.</param>
|
|||
/// <returns>An <see cref="IStorageItem"/> (file or folder), or null if the format isn't available.</returns>
|
|||
/// <seealso cref="DataFormat.File"/>.
|
|||
public static Task<IStorageItem?> TryGetFileAsync(this IClipboard clipboard) |
|||
=> clipboard.TryGetValueAsync(DataFormat.File); |
|||
|
|||
/// <summary>
|
|||
/// Returns a list of files, if available, from the clipboard.
|
|||
/// </summary>
|
|||
/// <param name="clipboard">The data transfer instance.</param>
|
|||
/// <returns>An array of <see cref="IStorageItem"/> (files or folders), or null if the format isn't available.</returns>
|
|||
/// <seealso cref="DataFormat.File"/>.
|
|||
public static Task<IStorageItem[]?> TryGetFilesAsync(this IClipboard clipboard) |
|||
=> clipboard.TryGetValuesAsync(DataFormat.File); |
|||
|
|||
/// <summary>
|
|||
/// Places a text on the clipboard.
|
|||
/// </summary>
|
|||
/// <param name="clipboard">The clipboard instance.</param>
|
|||
/// <param name="text">The value to place on the clipboard.</param>
|
|||
/// <remarks>
|
|||
/// <para>By calling this method, the clipboard will get cleared of any possible previous data.</para>
|
|||
/// <para>
|
|||
/// If <paramref name="text"/> is null, nothing will get placed on the clipboard and this method
|
|||
/// will be equivalent to <see cref="IClipboard.ClearAsync"/>.
|
|||
/// </para>
|
|||
/// </remarks>
|
|||
/// <seealso cref="DataFormat.Text"/>
|
|||
public static Task SetTextAsync(this IClipboard clipboard, string? text) |
|||
=> clipboard.SetValueAsync(DataFormat.Text, text); |
|||
|
|||
/// <summary>
|
|||
/// Places a file on the clipboard.
|
|||
/// </summary>
|
|||
/// <param name="clipboard">The clipboard instance.</param>
|
|||
/// <param name="file">The file to place on the clipboard.</param>
|
|||
/// <remarks>
|
|||
/// <para>By calling this method, the clipboard will get cleared of any possible previous data.</para>
|
|||
/// <para>
|
|||
/// If <paramref name="file"/> is null, nothing will get placed on the clipboard and this method
|
|||
/// will be equivalent to <see cref="IClipboard.ClearAsync"/>.
|
|||
/// </para>
|
|||
/// </remarks>
|
|||
/// <seealso cref="DataFormat.File"/>
|
|||
public static Task SetFileAsync(this IClipboard clipboard, IStorageItem? file) |
|||
=> clipboard.SetValueAsync(DataFormat.File, file); |
|||
|
|||
/// <summary>
|
|||
/// Places a list of files on the clipboard.
|
|||
/// </summary>
|
|||
/// <param name="clipboard">The clipboard instance.</param>
|
|||
/// <param name="files">The files to place on the clipboard.</param>
|
|||
/// <remarks>
|
|||
/// <para>By calling this method, the clipboard will get cleared of any possible previous data.</para>
|
|||
/// <para>
|
|||
/// If <paramref name="files"/> is null or empty, nothing will get placed on the clipboard and this method
|
|||
/// will be equivalent to <see cref="IClipboard.ClearAsync"/>.
|
|||
/// </para>
|
|||
/// </remarks>
|
|||
/// <seealso cref="DataFormat.File"/>
|
|||
public static Task SetFilesAsync(this IClipboard clipboard, IEnumerable<IStorageItem>? files) |
|||
=> clipboard.SetValuesAsync(DataFormat.File, files); |
|||
} |
|||
@ -0,0 +1,79 @@ |
|||
using System; |
|||
using System.Diagnostics; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.IO; |
|||
using System.Runtime.Serialization.Formatters.Binary; |
|||
using System.Text; |
|||
using Avalonia.Compatibility; |
|||
using Avalonia.Logging; |
|||
|
|||
namespace Avalonia.Input.Platform; |
|||
|
|||
/// <summary>
|
|||
/// Wraps a legacy <see cref="IDataObject"/> into a <see cref="IDataTransferItem"/>.
|
|||
/// </summary>
|
|||
[Obsolete] |
|||
internal sealed class DataObjectToDataTransferItemWrapper( |
|||
IDataObject dataObject, |
|||
DataFormat[] formats, |
|||
string[] formatStrings) |
|||
: PlatformDataTransferItem |
|||
{ |
|||
private readonly IDataObject _dataObject = dataObject; |
|||
private readonly DataFormat[] _formats = formats; |
|||
private readonly string[] _formatStrings = formatStrings; |
|||
|
|||
protected override DataFormat[] ProvideFormats() |
|||
=> _formats; |
|||
|
|||
protected override object? TryGetRawCore(DataFormat format) |
|||
{ |
|||
var index = Array.IndexOf(Formats, format); |
|||
if (index < 0) |
|||
return null; |
|||
|
|||
// We should never have DataFormat.File here, it's been handled by DataObjectToDataTransferWrapper.
|
|||
Debug.Assert(!DataFormat.File.Equals(format)); |
|||
|
|||
var formatString = _formatStrings[index]; |
|||
var data = _dataObject.Get(formatString); |
|||
|
|||
if (DataFormat.Text.Equals(format)) |
|||
return Convert.ToString(data) ?? string.Empty; |
|||
|
|||
if (format is DataFormat<string>) |
|||
return Convert.ToString(data); |
|||
|
|||
if (format is DataFormat<byte[]>) |
|||
return ConvertLegacyDataToBytes(format, data); |
|||
|
|||
return null; |
|||
} |
|||
|
|||
private static byte[]? ConvertLegacyDataToBytes(DataFormat format, object? data) |
|||
{ |
|||
switch (data) |
|||
{ |
|||
case null: |
|||
return null; |
|||
|
|||
case byte[] bytes: |
|||
return bytes; |
|||
|
|||
case string str: |
|||
return OperatingSystemEx.IsWindows() || OperatingSystemEx.IsMacOS() || OperatingSystemEx.IsIOS() ? |
|||
Encoding.Unicode.GetBytes(str) : |
|||
Encoding.UTF8.GetBytes(str); |
|||
|
|||
case Stream stream: |
|||
var length = (int)(stream.Length - stream.Position); |
|||
var buffer = new byte[length]; |
|||
|
|||
stream.ReadExactly(buffer, 0, length); |
|||
return buffer; |
|||
|
|||
default: |
|||
return BinaryFormatterHelper.TrySerializeUsingBinaryFormatter(data, format); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,84 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.Linq; |
|||
using Avalonia.Platform.Storage; |
|||
using Avalonia.Platform.Storage.FileIO; |
|||
|
|||
namespace Avalonia.Input.Platform; |
|||
|
|||
#pragma warning disable CS0618 // Type or member is obsolete: usages of IDataObject and DataFormats
|
|||
|
|||
// TODO12: remove
|
|||
/// <summary>
|
|||
/// Wraps a legacy <see cref="IDataObject"/> into a <see cref="IDataTransfer"/>.
|
|||
/// </summary>
|
|||
[Obsolete] |
|||
internal sealed class DataObjectToDataTransferWrapper(IDataObject dataObject) |
|||
: PlatformDataTransfer |
|||
{ |
|||
public IDataObject DataObject { get; } = dataObject; |
|||
|
|||
protected override DataFormat[] ProvideFormats() |
|||
=> DataObject.GetDataFormats().Select(DataFormats.ToDataFormat).Distinct().ToArray(); |
|||
|
|||
protected override PlatformDataTransferItem[] ProvideItems() |
|||
{ |
|||
var items = new List<PlatformDataTransferItem>(); |
|||
var nonFileFormats = new List<DataFormat>(); |
|||
var nonFileFormatStrings = new List<string>(); |
|||
|
|||
foreach (var formatString in DataObject.GetDataFormats()) |
|||
{ |
|||
var format = DataFormats.ToDataFormat(formatString); |
|||
|
|||
if (formatString == DataFormats.Files) |
|||
{ |
|||
// This is not ideal as we're reading the filenames ahead of time to generate the appropriate items.
|
|||
// We don't really care about that for this legacy wrapper.
|
|||
if (DataObject.Get(formatString) is IEnumerable<IStorageItem> storageItems) |
|||
{ |
|||
foreach (var storageItem in storageItems) |
|||
items.Add(PlatformDataTransferItem.Create(DataFormat.File, storageItem)); |
|||
} |
|||
} |
|||
else if (formatString == DataFormats.FileNames) |
|||
{ |
|||
if (DataObject.Get(formatString) is IEnumerable<string> fileNames) |
|||
{ |
|||
foreach (var fileName in fileNames) |
|||
{ |
|||
if (StorageProviderHelpers.TryCreateBclStorageItem(fileName) is { } storageItem) |
|||
items.Add(PlatformDataTransferItem.Create(DataFormat.File, storageItem)); |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
nonFileFormats.Add(format); |
|||
nonFileFormatStrings.Add(formatString); |
|||
} |
|||
} |
|||
|
|||
if (nonFileFormats.Count > 0) |
|||
{ |
|||
Debug.Assert(nonFileFormats.Count == nonFileFormatStrings.Count); |
|||
|
|||
// Single item containing all formats except for DataFormat.File.
|
|||
items.Add(new DataObjectToDataTransferItemWrapper( |
|||
DataObject, |
|||
nonFileFormats.ToArray(), |
|||
nonFileFormatStrings.ToArray())); |
|||
} |
|||
|
|||
return items.ToArray(); |
|||
} |
|||
|
|||
[SuppressMessage( |
|||
"ReSharper", |
|||
"SuspiciousTypeConversion.Global", |
|||
Justification = "IDisposable may be implemented externally by the IDataObject instance.")] |
|||
public override void Dispose() |
|||
=> (DataObject as IDisposable)?.Dispose(); |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Platform.Storage; |
|||
|
|||
namespace Avalonia.Input.Platform; |
|||
|
|||
/// <summary>
|
|||
/// Wraps a <see cref="IDataTransfer"/> into a legacy <see cref="IDataObject"/>.
|
|||
/// </summary>
|
|||
[Obsolete] |
|||
internal sealed class DataTransferToDataObjectWrapper(IDataTransfer dataTransfer) : IDataObject |
|||
{ |
|||
public IDataTransfer DataTransfer { get; } = dataTransfer; |
|||
|
|||
public IEnumerable<string> GetDataFormats() |
|||
=> DataTransfer.Formats.Select(DataFormats.ToString); |
|||
|
|||
public bool Contains(string dataFormat) |
|||
=> DataTransfer.Contains(DataFormats.ToDataFormat(dataFormat)); |
|||
|
|||
public object? Get(string dataFormat) |
|||
{ |
|||
if (dataFormat == DataFormats.Text) |
|||
return DataTransfer.TryGetText(); |
|||
|
|||
if (dataFormat == DataFormats.Files) |
|||
return DataTransfer.TryGetFiles(); |
|||
|
|||
if (dataFormat == DataFormats.FileNames) |
|||
{ |
|||
return DataTransfer |
|||
.TryGetFiles() |
|||
?.Select(file => file.TryGetLocalPath()) |
|||
.Where(path => path is not null) |
|||
.ToArray(); |
|||
} |
|||
|
|||
var typedFormat = DataFormat.CreateBytesPlatformFormat(dataFormat); |
|||
var bytes = DataTransfer.TryGetValue(typedFormat); |
|||
return BinaryFormatterHelper.TryDeserializeUsingBinaryFormatter(bytes) ?? bytes; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Metadata; |
|||
|
|||
namespace Avalonia.Input.Platform; |
|||
|
|||
/// <summary>
|
|||
/// Represents a platform-specific implementation of the clipboard.
|
|||
/// </summary>
|
|||
[PrivateApi] |
|||
public interface IClipboardImpl |
|||
{ |
|||
/// <inheritdoc cref="IClipboard.TryGetDataAsync"/>
|
|||
Task<IAsyncDataTransfer?> TryGetDataAsync(); |
|||
|
|||
/// <inheritdoc cref="IClipboard.SetDataAsync"/>
|
|||
Task SetDataAsync(IAsyncDataTransfer dataTransfer); |
|||
|
|||
/// <inheritdoc cref="IClipboard.ClearAsync"/>
|
|||
Task ClearAsync(); |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Metadata; |
|||
|
|||
namespace Avalonia.Input.Platform; |
|||
|
|||
/// <summary>
|
|||
/// Represents a platform-specific implementation of the clipboard that can be flushed.
|
|||
/// </summary>
|
|||
[PrivateApi] |
|||
public interface IFlushableClipboardImpl : IClipboardImpl |
|||
{ |
|||
/// <inheritdoc cref="IClipboard.FlushAsync"/>
|
|||
Task FlushAsync(); |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Metadata; |
|||
|
|||
namespace Avalonia.Input.Platform; |
|||
|
|||
/// <summary>
|
|||
/// Represents a platform-specific implementation of the clipboard that keeps track of its current owner.
|
|||
/// </summary>
|
|||
[PrivateApi] |
|||
public interface IOwnedClipboardImpl : IClipboardImpl |
|||
{ |
|||
/// <summary>
|
|||
/// Gets whether the current instance still owns the system clipboard.
|
|||
/// </summary>
|
|||
Task<bool> IsCurrentOwnerAsync(); |
|||
} |
|||
@ -1,11 +1,21 @@ |
|||
using System.Threading.Tasks; |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Metadata; |
|||
|
|||
namespace Avalonia.Input.Platform |
|||
{ |
|||
[Unstable] |
|||
[NotClientImplementable] |
|||
public interface IPlatformDragSource |
|||
{ |
|||
Task<DragDropEffects> DoDragDrop(PointerEventArgs triggerEvent, IDataObject data, DragDropEffects allowedEffects); |
|||
[Obsolete($"Use {nameof(DoDragDropAsync)} instead.")] |
|||
Task<DragDropEffects> DoDragDrop( |
|||
PointerEventArgs triggerEvent, |
|||
IDataObject data, |
|||
DragDropEffects allowedEffects); |
|||
|
|||
Task<DragDropEffects> DoDragDropAsync( |
|||
PointerEventArgs triggerEvent, |
|||
IDataTransfer dataTransfer, |
|||
DragDropEffects allowedEffects); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,37 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.Input.Platform; |
|||
|
|||
/// <summary>
|
|||
/// Abstract implementation of <see cref="IAsyncDataTransfer"/> used by platform implementations.
|
|||
/// </summary>
|
|||
/// <remarks>Use this class when the platform can only provide the underlying data asynchronously.</remarks>
|
|||
internal abstract class PlatformAsyncDataTransfer : IAsyncDataTransfer |
|||
{ |
|||
private DataFormat[]? _formats; |
|||
private IAsyncDataTransferItem[]? _items; |
|||
|
|||
public DataFormat[] Formats |
|||
=> _formats ??= ProvideFormats(); |
|||
|
|||
IReadOnlyList<DataFormat> IAsyncDataTransfer.Formats |
|||
=> Formats; |
|||
|
|||
protected bool AreFormatsInitialized |
|||
=> _formats is not null; |
|||
|
|||
public IAsyncDataTransferItem[] Items |
|||
=> _items ??= ProvideItems(); |
|||
|
|||
IReadOnlyList<IAsyncDataTransferItem> IAsyncDataTransfer.Items |
|||
=> Items; |
|||
|
|||
protected bool AreItemsInitialized |
|||
=> _items is not null; |
|||
|
|||
protected abstract DataFormat[] ProvideFormats(); |
|||
|
|||
protected abstract IAsyncDataTransferItem[] ProvideItems(); |
|||
|
|||
public abstract void Dispose(); |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Avalonia.Input.Platform; |
|||
|
|||
/// <summary>
|
|||
/// Abstract implementation of <see cref="IAsyncDataTransferItem"/> used by platform implementations.
|
|||
/// </summary>
|
|||
/// <remarks>Use this class when the platform can only provide the underlying data asynchronously.</remarks>
|
|||
internal abstract class PlatformAsyncDataTransferItem : IAsyncDataTransferItem |
|||
{ |
|||
private DataFormat[]? _formats; |
|||
|
|||
public DataFormat[] Formats |
|||
=> _formats ??= ProvideFormats(); |
|||
|
|||
IReadOnlyList<DataFormat> IAsyncDataTransferItem.Formats |
|||
=> Formats; |
|||
|
|||
protected abstract DataFormat[] ProvideFormats(); |
|||
|
|||
public bool Contains(DataFormat format) |
|||
=> Array.IndexOf(Formats, format) >= 0; |
|||
|
|||
public Task<object?> TryGetRawAsync(DataFormat format) |
|||
=> Contains(format) ? TryGetRawCoreAsync(format) : Task.FromResult<object?>(null); |
|||
|
|||
protected abstract Task<object?> TryGetRawCoreAsync(DataFormat format); |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.Input.Platform; |
|||
|
|||
/// <summary>
|
|||
/// Abstract implementation of <see cref="IDataTransfer"/> used by platform implementations.
|
|||
/// </summary>
|
|||
/// <remarks>Use this class when the platform can only provide the underlying data asynchronously.</remarks>
|
|||
internal abstract class PlatformDataTransfer : IDataTransfer, IAsyncDataTransfer |
|||
{ |
|||
private DataFormat[]? _formats; |
|||
private PlatformDataTransferItem[]? _items; |
|||
|
|||
public DataFormat[] Formats |
|||
=> _formats ??= ProvideFormats(); |
|||
|
|||
IReadOnlyList<DataFormat> IDataTransfer.Formats |
|||
=> Formats; |
|||
|
|||
IReadOnlyList<DataFormat> IAsyncDataTransfer.Formats |
|||
=> Formats; |
|||
|
|||
protected bool AreFormatsInitialized |
|||
=> _formats is not null; |
|||
|
|||
public PlatformDataTransferItem[] Items |
|||
=> _items ??= ProvideItems(); |
|||
|
|||
IReadOnlyList<IDataTransferItem> IDataTransfer.Items |
|||
=> Items; |
|||
|
|||
IReadOnlyList<IAsyncDataTransferItem> IAsyncDataTransfer.Items |
|||
=> Items; |
|||
|
|||
protected bool AreItemsInitialized |
|||
=> _items is not null; |
|||
|
|||
protected abstract DataFormat[] ProvideFormats(); |
|||
|
|||
protected abstract PlatformDataTransferItem[] ProvideItems(); |
|||
|
|||
public abstract void Dispose(); |
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Avalonia.Input.Platform; |
|||
|
|||
/// <summary>
|
|||
/// Abstract implementation of <see cref="IDataTransferItem"/> used by platform implementations.
|
|||
/// </summary>
|
|||
/// <remarks>Use this class when the platform can only provide the underlying data synchronously.</remarks>
|
|||
internal abstract class PlatformDataTransferItem : IDataTransferItem, IAsyncDataTransferItem |
|||
{ |
|||
private DataFormat[]? _formats; |
|||
|
|||
public DataFormat[] Formats |
|||
=> _formats ??= ProvideFormats(); |
|||
|
|||
IReadOnlyList<DataFormat> IDataTransferItem.Formats |
|||
=> Formats; |
|||
|
|||
IReadOnlyList<DataFormat> IAsyncDataTransferItem.Formats |
|||
=> Formats; |
|||
|
|||
protected abstract DataFormat[] ProvideFormats(); |
|||
|
|||
public bool Contains(DataFormat format) |
|||
=> Array.IndexOf(Formats, format) >= 0; |
|||
|
|||
public object? TryGetRaw(DataFormat format) |
|||
=> Contains(format) ? TryGetRawCore(format) : Task.FromResult<object?>(null); |
|||
|
|||
public Task<object?> TryGetRawAsync(DataFormat format) |
|||
{ |
|||
if (!Contains(format)) |
|||
return Task.FromResult<object?>(null); |
|||
|
|||
try |
|||
{ |
|||
return Task.FromResult(TryGetRawCore(format)); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
return Task.FromException<object?>(ex); |
|||
} |
|||
} |
|||
|
|||
protected abstract object? TryGetRawCore(DataFormat format); |
|||
|
|||
public static PlatformDataTransferItem Create<T>(DataFormat<T> format, T value) where T : class |
|||
=> new SingleFormatItem(format, value); |
|||
|
|||
private sealed class SingleFormatItem(DataFormat format, object value) : PlatformDataTransferItem |
|||
{ |
|||
private readonly DataFormat _format = format; |
|||
private readonly object _value = value; |
|||
|
|||
protected override DataFormat[] ProvideFormats() |
|||
=> [_format]; |
|||
|
|||
protected override object? TryGetRawCore(DataFormat format) |
|||
=> _format.Equals(format) ? _value : null; |
|||
} |
|||
} |
|||
@ -1,15 +0,0 @@ |
|||
namespace Avalonia.Diagnostics; |
|||
|
|||
internal static class Constants |
|||
{ |
|||
/// <summary>
|
|||
/// DevTools Clipboard data format
|
|||
/// </summary>
|
|||
static public class DataFormats |
|||
{ |
|||
/// <summary>
|
|||
/// Clipboard data format for the selector. It is added for quick format recognition in IDEs
|
|||
/// </summary>
|
|||
public const string Avalonia_DevTools_Selector = nameof(Avalonia_DevTools_Selector); |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
using System; |
|||
using Avalonia.Input; |
|||
|
|||
namespace Avalonia.Diagnostics; |
|||
|
|||
/// <summary>
|
|||
/// Contains data formats related to dev tools.
|
|||
/// </summary>
|
|||
public static class DevToolsDataFormats |
|||
{ |
|||
// TODO: this name isn't ideal. For instance, it's not a valid UTI for macOS.
|
|||
// We currently have a converter in place in native code for backwards compatibility with IDataObject,
|
|||
// but this should ideally be removed at some point.
|
|||
// Consider using DataFormat.CreateApplicationFormat() instead (breaking change).
|
|||
|
|||
/// <summary>
|
|||
/// Gets the clipboard data format representing a selector.
|
|||
/// It's used for quick format recognition in IDEs.
|
|||
/// </summary>
|
|||
public static DataFormat<string> Selector { get; } = DataFormat.CreateStringPlatformFormat("Avalonia_DevTools_Selector"); |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
#nullable enable |
|||
|
|||
using System; |
|||
using Avalonia.Input; |
|||
using Avalonia.Native.Interop; |
|||
|
|||
namespace Avalonia.Native; |
|||
|
|||
internal static class ClipboardDataFormatHelper |
|||
{ |
|||
// TODO hide native types behind IAvnClipboard abstraction, so managed side won't depend on macOS.
|
|||
private const string NSPasteboardTypeString = "public.utf8-plain-text"; |
|||
private const string NSPasteboardTypeFileUrl = "public.file-url"; |
|||
private const string AppPrefix = "net.avaloniaui.app.uti."; |
|||
|
|||
public static DataFormat[] ToDataFormats(IAvnStringArray? nativeFormats, Func<string, bool> isTextFormat) |
|||
{ |
|||
if (nativeFormats is null) |
|||
return []; |
|||
|
|||
var count = nativeFormats.Count; |
|||
if (count == 0) |
|||
return []; |
|||
|
|||
var results = new DataFormat[count]; |
|||
|
|||
for (var c = 0u; c < count; c++) |
|||
{ |
|||
using var nativeFormat = nativeFormats.Get(c); |
|||
results[c] = ToDataFormat(nativeFormat.String, isTextFormat); |
|||
} |
|||
|
|||
return results; |
|||
} |
|||
|
|||
public static DataFormat ToDataFormat(string nativeFormat, Func<string, bool> isTextFormat) |
|||
=> nativeFormat switch |
|||
{ |
|||
NSPasteboardTypeString => DataFormat.Text, |
|||
NSPasteboardTypeFileUrl => DataFormat.File, |
|||
_ when isTextFormat(nativeFormat) => DataFormat.FromSystemName<string>(nativeFormat, AppPrefix), |
|||
_ => DataFormat.FromSystemName<byte[]>(nativeFormat, AppPrefix) |
|||
}; |
|||
|
|||
public static string ToNativeFormat(DataFormat format) |
|||
{ |
|||
if (DataFormat.Text.Equals(format)) |
|||
return NSPasteboardTypeString; |
|||
|
|||
if (DataFormat.File.Equals(format)) |
|||
return NSPasteboardTypeFileUrl; |
|||
|
|||
return format.ToSystemName(AppPrefix); |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
#nullable enable |
|||
|
|||
using System.Collections.Generic; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
|
|||
namespace Avalonia.Native; |
|||
|
|||
/// <summary>
|
|||
/// Implementation of <see cref="IDataTransfer"/> for Avalonia.Native.
|
|||
/// </summary>
|
|||
/// <param name="session">
|
|||
/// The clipboard session.
|
|||
/// The <see cref="ClipboardDataTransfer"/> assumes ownership over this instance.
|
|||
/// </param>
|
|||
internal sealed class ClipboardDataTransfer(ClipboardReadSession session) |
|||
: PlatformDataTransfer |
|||
{ |
|||
private readonly ClipboardReadSession _session = session; |
|||
|
|||
protected override DataFormat[] ProvideFormats() |
|||
{ |
|||
using var formats = _session.GetFormats(); |
|||
return ClipboardDataFormatHelper.ToDataFormats(formats, _session.IsTextFormat); |
|||
} |
|||
|
|||
protected override PlatformDataTransferItem[] ProvideItems() |
|||
{ |
|||
var itemCount = _session.GetItemCount(); |
|||
if (itemCount == 0) |
|||
return []; |
|||
|
|||
var items = new PlatformDataTransferItem[itemCount]; |
|||
|
|||
for (var i = 0; i < itemCount; ++i) |
|||
items[i] = new ClipboardDataTransferItem(_session, i); |
|||
|
|||
return items; |
|||
} |
|||
|
|||
public IEnumerable<DataFormat> GetFormats() |
|||
=> Formats; |
|||
|
|||
public override void Dispose() |
|||
=> _session.Dispose(); |
|||
} |
|||
@ -0,0 +1,94 @@ |
|||
#nullable enable |
|||
|
|||
using System; |
|||
using System.Text; |
|||
using Avalonia.Controls.Platform; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
using Avalonia.Platform.Storage; |
|||
|
|||
namespace Avalonia.Native; |
|||
|
|||
/// <summary>
|
|||
/// Represents a single item inside a <see cref="ClipboardDataTransfer"/>.
|
|||
/// </summary>
|
|||
/// <param name="session">The clipboard session. This is NOT owned by the <see cref="ClipboardDataTransferItem"/>.</param>
|
|||
/// <param name="itemIndex">The item index.</param>
|
|||
internal sealed class ClipboardDataTransferItem(ClipboardReadSession session, int itemIndex) |
|||
: PlatformDataTransferItem |
|||
{ |
|||
private readonly ClipboardReadSession _session = session; |
|||
private readonly int _itemIndex = itemIndex; |
|||
|
|||
protected override DataFormat[] ProvideFormats() |
|||
{ |
|||
using var formats = _session.GetItemFormats(_itemIndex); |
|||
return ClipboardDataFormatHelper.ToDataFormats(formats, _session.IsTextFormat); |
|||
} |
|||
|
|||
protected override object? TryGetRawCore(DataFormat format) |
|||
{ |
|||
var nativeFormat = ClipboardDataFormatHelper.ToNativeFormat(format); |
|||
|
|||
if (DataFormat.Text.Equals(format)) |
|||
return TryGetString(nativeFormat); |
|||
|
|||
if (DataFormat.File.Equals(format)) |
|||
return TryGetFile(nativeFormat); |
|||
|
|||
if (format is DataFormat<string>) |
|||
{ |
|||
if (TryGetString(nativeFormat) is { } stringValue) |
|||
return stringValue; |
|||
if (TryGetBytes(nativeFormat) is { } bytes) |
|||
return Encoding.Unicode.GetString(bytes); |
|||
return null; |
|||
} |
|||
|
|||
if (format is DataFormat<byte[]>) |
|||
{ |
|||
if (TryGetBytes(nativeFormat) is { } bytes) |
|||
return bytes; |
|||
if (TryGetString(nativeFormat) is { } stringValue) |
|||
return Encoding.Unicode.GetBytes(stringValue); |
|||
return null; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
private string? TryGetString(string nativeFormat) |
|||
{ |
|||
using var text = _session.GetItemValueAsString(_itemIndex, nativeFormat); |
|||
return text?.String; |
|||
} |
|||
|
|||
private IStorageItem? TryGetFile(string nativeFormat) |
|||
{ |
|||
if (AvaloniaLocator.Current.GetService<IStorageProviderFactory>() is not StorageProviderApi storageApi) |
|||
return null; |
|||
|
|||
using var uriString = _session.GetItemValueAsString(_itemIndex, nativeFormat); |
|||
if (TryGetFilePathUri(uriString?.String, storageApi) is not { } uri) |
|||
return null; |
|||
|
|||
return storageApi.TryGetStorageItem(uri); |
|||
} |
|||
|
|||
private static Uri? TryGetFilePathUri(string? uriString, StorageProviderApi storageApi) |
|||
{ |
|||
if (!Uri.TryCreate(uriString, UriKind.Absolute, out var uri) || !uri.IsFile) |
|||
return null; |
|||
|
|||
// macOS may return a file reference URI (e.g. file:///.file/id=6571367.2773272/), convert it to a path URI.
|
|||
return uri.AbsolutePath.StartsWith("/.file/id=", StringComparison.Ordinal) ? |
|||
storageApi.TryResolveFileReferenceUri(uri) : |
|||
uri; |
|||
} |
|||
|
|||
private byte[]? TryGetBytes(string nativeFormat) |
|||
{ |
|||
using var bytes = _session.GetItemValueAsBytes(_itemIndex, nativeFormat); |
|||
return bytes?.Bytes; |
|||
} |
|||
} |
|||
@ -1,238 +1,94 @@ |
|||
#nullable enable |
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Controls.Platform; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
using Avalonia.Logging; |
|||
using Avalonia.Native.Interop; |
|||
using Avalonia.Platform.Storage; |
|||
using Avalonia.Platform.Storage.FileIO; |
|||
|
|||
namespace Avalonia.Native |
|||
{ |
|||
class ClipboardImpl : IClipboard, IDisposable |
|||
internal sealed class ClipboardImpl(IAvnClipboard native) : IOwnedClipboardImpl, IDisposable |
|||
{ |
|||
private IAvnClipboard? _native; |
|||
private IDataObject? _savedDataObject; |
|||
private long _lastClearChangeCount; |
|||
private IAvnClipboard? _native = native; |
|||
private long _lastClearChangeCount = long.MinValue; |
|||
|
|||
// TODO hide native types behind IAvnClipboard abstraction, so managed side won't depend on macOS.
|
|||
private const string NSPasteboardTypeString = "public.utf8-plain-text"; |
|||
private const string NSFilenamesPboardType = "NSFilenamesPboardType"; |
|||
|
|||
public ClipboardImpl(IAvnClipboard native) |
|||
{ |
|||
_native = native; |
|||
} |
|||
|
|||
private IAvnClipboard Native |
|||
internal IAvnClipboard Native |
|||
=> _native ?? throw new ObjectDisposedException(nameof(ClipboardImpl)); |
|||
|
|||
private void ClearCore() |
|||
{ |
|||
_savedDataObject = null; |
|||
_lastClearChangeCount = Native.Clear(); |
|||
} |
|||
|
|||
public Task ClearAsync() |
|||
{ |
|||
ClearCore(); |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
public Task<string?> GetTextAsync() |
|||
{ |
|||
using (var text = Native.GetText(NSPasteboardTypeString)) |
|||
return Task.FromResult<string?>(text.String); |
|||
} |
|||
|
|||
public Task SetTextAsync(string? text) |
|||
public Task ClearAsync() |
|||
{ |
|||
var native = Native; |
|||
|
|||
ClearCore(); |
|||
|
|||
if (text != null) |
|||
native.SetText(NSPasteboardTypeString, text); |
|||
|
|||
return Task.CompletedTask; |
|||
try |
|||
{ |
|||
ClearCore(); |
|||
return Task.CompletedTask; |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
return Task.FromException(ex); |
|||
} |
|||
} |
|||
|
|||
public IEnumerable<string> GetFormats() |
|||
public Task<IAsyncDataTransfer?> TryGetDataAsync() |
|||
{ |
|||
var rv = new HashSet<string>(); |
|||
using (var formats = Native.ObtainFormats()) |
|||
try |
|||
{ |
|||
var cnt = formats.Count; |
|||
for (uint c = 0; c < cnt; c++) |
|||
{ |
|||
using (var fmt = formats.Get(c)) |
|||
{ |
|||
if(fmt.String == NSPasteboardTypeString) |
|||
rv.Add(DataFormats.Text); |
|||
if (fmt.String == NSFilenamesPboardType) |
|||
{ |
|||
rv.Add(DataFormats.FileNames); |
|||
rv.Add(DataFormats.Files); |
|||
} |
|||
else |
|||
rv.Add(fmt.String); |
|||
} |
|||
} |
|||
return Task.FromResult(TryGetData()); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
return Task.FromException<IAsyncDataTransfer?>(ex); |
|||
} |
|||
|
|||
return rv; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_savedDataObject = null; |
|||
_native?.Dispose(); |
|||
_native = null; |
|||
} |
|||
|
|||
public IEnumerable<string>? GetFileNames() |
|||
private IAsyncDataTransfer? TryGetData() |
|||
{ |
|||
using (var strings = Native.GetStrings(NSFilenamesPboardType)) |
|||
return strings?.ToStringArray(); |
|||
} |
|||
var dataTransfer = new ClipboardDataTransfer( |
|||
new ClipboardReadSession(Native, Native.ChangeCount, ownsNative: false)); |
|||
|
|||
public IEnumerable<IStorageItem>? GetFiles() |
|||
{ |
|||
var storageApi = (StorageProviderApi)AvaloniaLocator.Current.GetRequiredService<IStorageProviderFactory>(); |
|||
|
|||
// TODO: use non-deprecated AppKit API to get NSUri instead of file names.
|
|||
var fileNames = GetFileNames(); |
|||
if (fileNames is null) |
|||
if (dataTransfer.Formats.Length == 0) |
|||
{ |
|||
dataTransfer.Dispose(); |
|||
return null; |
|||
} |
|||
|
|||
return fileNames |
|||
.Select(f => StorageProviderHelpers.TryGetUriFromFilePath(f, false) is { } uri |
|||
? storageApi.TryGetStorageItem(uri) |
|||
: null) |
|||
.Where(f => f is not null)!; |
|||
return dataTransfer; |
|||
} |
|||
|
|||
public unsafe Task SetDataObjectAsync(IDataObject data) |
|||
public Task SetDataAsync(IAsyncDataTransfer dataTransfer) |
|||
{ |
|||
ClearCore(); |
|||
|
|||
// If there is multiple values with the same "to" format, prefer these that were not mapped.
|
|||
var formats = data.GetDataFormats().Select(f => |
|||
{ |
|||
string from, to; |
|||
bool mapped; |
|||
if (f == DataFormats.Text) |
|||
(from, to, mapped) = (f, NSPasteboardTypeString, true); |
|||
else if (f == DataFormats.Files || f == DataFormats.FileNames) |
|||
(from, to, mapped) = (f, NSFilenamesPboardType, true); |
|||
else (from, to, mapped) = (f, f, false); |
|||
return (from, to, mapped); |
|||
}) |
|||
.GroupBy(p => p.to) |
|||
.Select(g => g.OrderBy(f => f.mapped).First()); |
|||
|
|||
|
|||
foreach (var (fromFormat, toFormat, _) in formats) |
|||
try |
|||
{ |
|||
var o = data.Get(fromFormat); |
|||
switch (o) |
|||
{ |
|||
case string s: |
|||
Native.SetText(toFormat, s); |
|||
break; |
|||
case IEnumerable<IStorageItem> storageItems: |
|||
using (var strings = new AvnStringArray(storageItems |
|||
.Select(s => s.TryGetLocalPath()) |
|||
.Where(p => p is not null))) |
|||
{ |
|||
Native.SetStrings(toFormat, strings); |
|||
} |
|||
break; |
|||
case IEnumerable<string> managedStrings: |
|||
using (var strings = new AvnStringArray(managedStrings)) |
|||
{ |
|||
Native.SetStrings(toFormat, strings); |
|||
} |
|||
break; |
|||
case byte[] bytes: |
|||
{ |
|||
fixed (byte* pbytes = bytes) |
|||
Native.SetBytes(toFormat, pbytes, bytes.Length); |
|||
break; |
|||
} |
|||
default: |
|||
Logger.TryGet(LogEventLevel.Warning, LogArea.macOSPlatform)?.Log(this, |
|||
"Unsupported IDataObject value type: {0}", o?.GetType().FullName ?? "(null)"); |
|||
break; |
|||
} |
|||
SetData(dataTransfer); |
|||
return Task.CompletedTask; |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
return Task.FromException(ex); |
|||
} |
|||
|
|||
_savedDataObject = data; |
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
public Task<string[]> GetFormatsAsync() |
|||
{ |
|||
return Task.FromResult(GetFormats().ToArray()); |
|||
} |
|||
|
|||
public async Task<object?> GetDataAsync(string format) |
|||
private void SetData(IAsyncDataTransfer dataTransfer) |
|||
{ |
|||
if (format == DataFormats.Text || format == NSPasteboardTypeString) |
|||
return await GetTextAsync(); |
|||
if (format == DataFormats.FileNames || format == NSFilenamesPboardType) |
|||
return GetFileNames(); |
|||
if (format == DataFormats.Files) |
|||
return GetFiles(); |
|||
using (var n = Native.GetBytes(format)) |
|||
return n.Bytes; |
|||
} |
|||
ClearCore(); |
|||
|
|||
public Task<IDataObject?> TryGetInProcessDataObjectAsync() |
|||
{ |
|||
if (Native.ChangeCount != _lastClearChangeCount) |
|||
_savedDataObject = null; |
|||
return Task.FromResult(_savedDataObject); |
|||
Native.SetData(new DataTransferToAvnClipboardDataSourceWrapper( |
|||
dataTransfer.ToSynchronous(LogArea.macOSPlatform))); |
|||
} |
|||
|
|||
/// <inheritdoc />
|
|||
public Task FlushAsync() => |
|||
Task.CompletedTask; |
|||
|
|||
} |
|||
|
|||
class ClipboardDataObject : IDataObject, IDisposable |
|||
{ |
|||
private ClipboardImpl? _clipboard; |
|||
private List<string>? _formats; |
|||
|
|||
public ClipboardDataObject(IAvnClipboard clipboard) |
|||
{ |
|||
_clipboard = new ClipboardImpl(clipboard); |
|||
} |
|||
public Task<bool> IsCurrentOwnerAsync() |
|||
=> Task.FromResult(Native.ChangeCount == _lastClearChangeCount); |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_clipboard?.Dispose(); |
|||
_clipboard = null; |
|||
_native?.Dispose(); |
|||
_native = null; |
|||
} |
|||
|
|||
private ClipboardImpl Clipboard |
|||
=> _clipboard ?? throw new ObjectDisposedException(nameof(ClipboardDataObject)); |
|||
|
|||
private List<string> Formats => _formats ??= Clipboard.GetFormats().ToList(); |
|||
|
|||
public IEnumerable<string> GetDataFormats() => Formats; |
|||
|
|||
public bool Contains(string dataFormat) => Formats.Contains(dataFormat); |
|||
|
|||
public object? Get(string dataFormat) => Clipboard.GetDataAsync(dataFormat).GetAwaiter().GetResult(); |
|||
|
|||
public Task SetFromDataObjectAsync(IDataObject dataObject) => Clipboard.SetDataObjectAsync(dataObject); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,107 @@ |
|||
#nullable enable |
|||
|
|||
using System; |
|||
using System.Runtime.InteropServices; |
|||
using Avalonia.Native.Interop; |
|||
|
|||
namespace Avalonia.Native; |
|||
|
|||
/// <summary>
|
|||
/// Represents a single "session" inside a clipboard, defined by its current change count.
|
|||
/// When the clipboard changes, this session becomes invalid and throws <see cref="ObjectDisposedException"/>.
|
|||
/// </summary>
|
|||
internal sealed class ClipboardReadSession(IAvnClipboard native, long changeCount, bool ownsNative) : IDisposable |
|||
{ |
|||
private const int COR_E_OBJECTDISPOSED = unchecked((int)0x80131622); |
|||
|
|||
private IAvnClipboard? _native = native; |
|||
private readonly long _changeCount = changeCount; |
|||
private readonly bool _ownsNative = ownsNative; |
|||
|
|||
public IAvnClipboard Native |
|||
=> _native ?? throw new ObjectDisposedException(nameof(ClipboardReadSession)); |
|||
|
|||
public IAvnStringArray? GetFormats() |
|||
{ |
|||
try |
|||
{ |
|||
return Native.GetFormats(_changeCount); |
|||
} |
|||
catch (COMException ex) when (IsComObjectDisposedException(ex)) |
|||
{ |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
public int GetItemCount() |
|||
{ |
|||
try |
|||
{ |
|||
return Native.GetItemCount(_changeCount); |
|||
} |
|||
catch (COMException ex) when (IsComObjectDisposedException(ex)) |
|||
{ |
|||
return 0; |
|||
} |
|||
} |
|||
|
|||
public IAvnStringArray? GetItemFormats(int index) |
|||
{ |
|||
try |
|||
{ |
|||
return Native.GetItemFormats(index, _changeCount); |
|||
} |
|||
catch (COMException ex) when (IsComObjectDisposedException(ex)) |
|||
{ |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
public IAvnString? GetItemValueAsString(int index, string format) |
|||
{ |
|||
try |
|||
{ |
|||
return Native.GetItemValueAsString(index, _changeCount, format); |
|||
} |
|||
catch (COMException ex) when (IsComObjectDisposedException(ex)) |
|||
{ |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
public IAvnString? GetItemValueAsBytes(int index, string format) |
|||
{ |
|||
try |
|||
{ |
|||
return Native.GetItemValueAsBytes(index, _changeCount, format); |
|||
} |
|||
catch (COMException ex) when (IsComObjectDisposedException(ex)) |
|||
{ |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
public bool IsTextFormat(string format) |
|||
{ |
|||
try |
|||
{ |
|||
return Native.IsTextFormat(format) != 0; |
|||
} |
|||
catch (COMException) |
|||
{ |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
public static bool IsComObjectDisposedException(COMException exception) |
|||
// The native side returns COR_E_OBJECTDISPOSED if the clipboard has changed (_changeCount doesn't match).
|
|||
=> exception.HResult == COR_E_OBJECTDISPOSED; |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (_ownsNative) |
|||
_native?.Dispose(); |
|||
|
|||
_native = null; |
|||
} |
|||
} |
|||
@ -0,0 +1,96 @@ |
|||
#nullable enable |
|||
|
|||
using System; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using Avalonia.Input; |
|||
using Avalonia.Logging; |
|||
using Avalonia.Native.Interop; |
|||
using Avalonia.Platform.Storage; |
|||
|
|||
namespace Avalonia.Native; |
|||
|
|||
/// <summary>
|
|||
/// Wraps a <see cref="IDataTransferItem"/> into a <see cref="IAvnClipboardDataItem"/>.
|
|||
/// This class is called by native code.
|
|||
/// </summary>
|
|||
/// <param name="item">The item to wrap.</param>
|
|||
internal sealed class DataTransferItemToAvnClipboardDataItemWrapper(IDataTransferItem item) |
|||
: NativeOwned, IAvnClipboardDataItem |
|||
{ |
|||
private readonly IDataTransferItem _item = item; |
|||
|
|||
IAvnStringArray IAvnClipboardDataItem.ProvideFormats() |
|||
=> new AvnStringArray(_item.Formats.Select(ClipboardDataFormatHelper.ToNativeFormat)); |
|||
|
|||
IAvnClipboardDataValue? IAvnClipboardDataItem.GetValue(string format) |
|||
{ |
|||
if (FindDataFormat(format) is { } dataFormat) |
|||
{ |
|||
if (DataFormat.Text.Equals(dataFormat)) |
|||
return new StringValue(_item.TryGetValue(DataFormat.Text) ?? string.Empty); |
|||
|
|||
if (DataFormat.File.Equals(dataFormat)) |
|||
return _item.TryGetValue(DataFormat.File) is { } file ? new StringValue(file.Path.AbsoluteUri) : null; |
|||
|
|||
if (dataFormat is DataFormat<string> stringFormat) |
|||
return _item.TryGetValue(stringFormat) is { } stringValue ? new StringValue(stringValue) : null; |
|||
|
|||
if (dataFormat is DataFormat<byte[]> bytesFormat) |
|||
return _item.TryGetValue(bytesFormat) is { } bytes ? new BytesValue(bytes) : null; |
|||
} |
|||
|
|||
Logger.TryGet(LogEventLevel.Warning, LogArea.macOSPlatform) |
|||
?.Log(this, "Unsupported data format {Format}", format); |
|||
|
|||
return null; |
|||
} |
|||
|
|||
private DataFormat? FindDataFormat(string nativeFormat) |
|||
{ |
|||
var formats = _item.Formats; |
|||
var count = formats.Count; |
|||
for (var i = 0; i < count; i++) |
|||
{ |
|||
var format = formats[i]; |
|||
if (ClipboardDataFormatHelper.ToNativeFormat(format) == nativeFormat) |
|||
return format; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
private sealed class StringValue(string value) : NativeOwned, IAvnClipboardDataValue |
|||
{ |
|||
private readonly string _value = value; |
|||
|
|||
int IAvnClipboardDataValue.IsString() |
|||
=> true.AsComBool(); |
|||
|
|||
IAvnString IAvnClipboardDataValue.AsString() |
|||
=> new AvnString(_value); |
|||
|
|||
int IAvnClipboardDataValue.ByteLength |
|||
=> throw new InvalidOperationException(); |
|||
|
|||
unsafe void IAvnClipboardDataValue.CopyBytesTo(void* buffer) |
|||
=> throw new InvalidOperationException(); |
|||
} |
|||
|
|||
private sealed class BytesValue(ReadOnlyMemory<byte> value) : NativeOwned, IAvnClipboardDataValue |
|||
{ |
|||
private readonly ReadOnlyMemory<byte> _value = value; |
|||
|
|||
int IAvnClipboardDataValue.IsString() |
|||
=> false.AsComBool(); |
|||
|
|||
IAvnString IAvnClipboardDataValue.AsString() |
|||
=> throw new InvalidOperationException(); |
|||
|
|||
int IAvnClipboardDataValue.ByteLength |
|||
=> _value.Length; |
|||
|
|||
unsafe void IAvnClipboardDataValue.CopyBytesTo(void* buffer) |
|||
=> _value.Span.CopyTo(new Span<byte>(buffer, _value.Length)); |
|||
} |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
#nullable enable |
|||
|
|||
using System; |
|||
using System.Linq; |
|||
using Avalonia.Input; |
|||
using Avalonia.Native.Interop; |
|||
|
|||
namespace Avalonia.Native; |
|||
|
|||
/// <summary>
|
|||
/// Wraps a <see cref="IDataTransfer"/> into a <see cref="IAvnClipboardDataSource"/>.
|
|||
/// This class is called by native code.
|
|||
/// </summary>
|
|||
/// <param name="dataTransfer">The data transfer object to wrap.</param>
|
|||
internal sealed class DataTransferToAvnClipboardDataSourceWrapper(IDataTransfer dataTransfer) |
|||
: NativeOwned, IAvnClipboardDataSource |
|||
{ |
|||
private IDataTransfer? _dataTransfer = dataTransfer; |
|||
private DataTransferItemToAvnClipboardDataItemWrapper[]? _items; |
|||
|
|||
private IDataTransfer DataTransfer |
|||
=> _dataTransfer ?? throw new ObjectDisposedException(nameof(DataTransferToAvnClipboardDataSourceWrapper)); |
|||
|
|||
private DataTransferItemToAvnClipboardDataItemWrapper[] Items |
|||
{ |
|||
get |
|||
{ |
|||
if (_items is null) |
|||
{ |
|||
_items = GetItemsCore(); |
|||
|
|||
if (_items.Length == 0) |
|||
Destroyed(); |
|||
} |
|||
|
|||
return _items; |
|||
|
|||
DataTransferItemToAvnClipboardDataItemWrapper[] GetItemsCore() |
|||
=> DataTransfer.Items |
|||
.Select(static item => new DataTransferItemToAvnClipboardDataItemWrapper(item)) |
|||
.ToArray(); |
|||
} |
|||
} |
|||
|
|||
public int ItemCount |
|||
=> Items.Length; |
|||
|
|||
public IAvnClipboardDataItem GetItem(int index) |
|||
=> Items[index]; |
|||
|
|||
protected override void Destroyed() |
|||
{ |
|||
_dataTransfer?.Dispose(); |
|||
_dataTransfer = null; |
|||
} |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
#nullable enable |
|||
|
|||
using System; |
|||
using System.Runtime.ExceptionServices; |
|||
using Avalonia.Threading; |
|||
using MicroCom.Runtime; |
|||
|
|||
namespace Avalonia.Native; |
|||
|
|||
/// <summary>
|
|||
/// Represents a COM object whose lifetime is completely handled by the native side.
|
|||
/// </summary>
|
|||
internal abstract class NativeOwned : IUnknown, IMicroComShadowContainer, IMicroComExceptionCallback |
|||
{ |
|||
private MicroComShadow? _shadow; |
|||
|
|||
MicroComShadow? IMicroComShadowContainer.Shadow |
|||
{ |
|||
get => _shadow; |
|||
set => _shadow = value; |
|||
} |
|||
|
|||
void IMicroComShadowContainer.OnReferencedFromNative() |
|||
{ |
|||
} |
|||
|
|||
void IMicroComShadowContainer.OnUnreferencedFromNative() |
|||
{ |
|||
_shadow?.Dispose(); |
|||
_shadow = null; |
|||
Destroyed(); |
|||
} |
|||
|
|||
protected virtual void Destroyed() |
|||
{ |
|||
} |
|||
|
|||
void IDisposable.Dispose() |
|||
{ |
|||
} |
|||
|
|||
void IMicroComExceptionCallback.RaiseException(Exception e) |
|||
{ |
|||
if (AvaloniaLocator.Current.GetService<IDispatcherImpl>() is DispatcherImpl dispatcherImpl) |
|||
dispatcherImpl.PropagateCallbackException(ExceptionDispatchInfo.Capture(e)); |
|||
} |
|||
} |
|||
@ -0,0 +1,92 @@ |
|||
using System; |
|||
using System.Text; |
|||
using Avalonia.Input; |
|||
|
|||
namespace Avalonia.X11.Clipboard; |
|||
|
|||
internal static class ClipboardDataFormatHelper |
|||
{ |
|||
private const string MimeTypeTextUriList = "text/uri-list"; |
|||
private const string AppPrefix = "application/avn-fmt."; |
|||
|
|||
public static DataFormat? ToDataFormat(IntPtr formatAtom, X11Atoms atoms) |
|||
{ |
|||
if (formatAtom == IntPtr.Zero) |
|||
return null; |
|||
|
|||
if (formatAtom == atoms.UTF16_STRING || |
|||
formatAtom == atoms.UTF8_STRING || |
|||
formatAtom == atoms.XA_STRING || |
|||
formatAtom == atoms.OEMTEXT) |
|||
{ |
|||
return DataFormat.Text; |
|||
} |
|||
|
|||
if (formatAtom == atoms.MULTIPLE || |
|||
formatAtom == atoms.TARGETS || |
|||
formatAtom == atoms.SAVE_TARGETS) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
if (atoms.GetAtomName(formatAtom) is { } atomName) |
|||
{ |
|||
return atomName == MimeTypeTextUriList ? |
|||
DataFormat.File : |
|||
DataFormat.FromSystemName<byte[]>(atomName, AppPrefix); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
public static IntPtr ToAtom(DataFormat format, IntPtr[] textFormatAtoms, X11Atoms atoms) |
|||
{ |
|||
if (DataFormat.Text.Equals(format)) |
|||
return GetPreferredStringFormatAtom(textFormatAtoms, atoms); |
|||
|
|||
if (DataFormat.File.Equals(format)) |
|||
return atoms.GetAtom(MimeTypeTextUriList); |
|||
|
|||
var systemName = format.ToSystemName(AppPrefix); |
|||
return atoms.GetAtom(systemName); |
|||
} |
|||
|
|||
public static IntPtr[] ToAtoms(DataFormat format, IntPtr[] textFormatAtoms, X11Atoms atoms) |
|||
{ |
|||
if (DataFormat.Text.Equals(format)) |
|||
return textFormatAtoms; |
|||
|
|||
if (DataFormat.File.Equals(format)) |
|||
return [atoms.GetAtom(MimeTypeTextUriList)]; |
|||
|
|||
var systemName = format.ToSystemName(AppPrefix); |
|||
return [atoms.GetAtom(systemName)]; |
|||
} |
|||
|
|||
private static IntPtr GetPreferredStringFormatAtom(IntPtr[] textFormatAtoms, X11Atoms atoms) |
|||
{ |
|||
ReadOnlySpan<IntPtr> preferredFormats = [atoms.UTF16_STRING, atoms.UTF8_STRING, atoms.XA_STRING]; |
|||
|
|||
foreach (var preferredFormat in preferredFormats) |
|||
{ |
|||
if (Array.IndexOf(textFormatAtoms, preferredFormat) >= 0) |
|||
return preferredFormat; |
|||
} |
|||
|
|||
return atoms.UTF8_STRING; |
|||
} |
|||
|
|||
public static Encoding? TryGetStringEncoding(IntPtr formatAtom, X11Atoms atoms) |
|||
{ |
|||
if (formatAtom == atoms.UTF16_STRING) |
|||
return Encoding.Unicode; |
|||
|
|||
if (formatAtom == atoms.UTF8_STRING) |
|||
return Encoding.UTF8; |
|||
|
|||
if (formatAtom == atoms.XA_STRING || formatAtom == atoms.OEMTEXT) |
|||
return Encoding.ASCII; |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Input; |
|||
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) |
|||
: IDisposable |
|||
{ |
|||
private readonly X11Info _x11 = x11; |
|||
private readonly AvaloniaX11Platform _platform = platform; |
|||
private readonly IntPtr[] _textFormatAtoms = textFormatAtoms; |
|||
private IntPtr _owner = owner; |
|||
|
|||
private bool IsOwnerStillValid() |
|||
=> _owner != IntPtr.Zero && XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD) == _owner; |
|||
|
|||
public async Task<object?> TryGetAsync(DataFormat format) |
|||
{ |
|||
if (!IsOwnerStillValid()) |
|||
return null; |
|||
|
|||
var formatAtom = ClipboardDataFormatHelper.ToAtom(format, _textFormatAtoms, _x11.Atoms); |
|||
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.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; |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
|
|||
namespace Avalonia.X11.Clipboard; |
|||
|
|||
/// <summary>
|
|||
/// Implementation of <see cref="IAsyncDataTransfer"/> for the X11 clipboard.
|
|||
/// </summary>
|
|||
/// <param name="reader">The object used to read values.</param>
|
|||
/// <param name="formats">The formats.</param>
|
|||
/// <param name="items">The items.</param>
|
|||
/// <remarks>
|
|||
/// Formats and items are pre-populated because we don't want to do some sync-over-async calls.
|
|||
/// Note that this does not pre-populate values, which are still retrieved asynchronously on demand.
|
|||
/// </remarks>
|
|||
internal sealed class ClipboardDataTransfer( |
|||
ClipboardDataReader reader, |
|||
DataFormat[] formats, |
|||
IAsyncDataTransferItem[] items) |
|||
: PlatformAsyncDataTransfer |
|||
{ |
|||
private readonly ClipboardDataReader _reader = reader; |
|||
private readonly DataFormat[] _formats = formats; |
|||
private readonly IAsyncDataTransferItem[] _items = items; |
|||
|
|||
protected override DataFormat[] ProvideFormats() |
|||
=> _formats; |
|||
|
|||
protected override IAsyncDataTransferItem[] ProvideItems() |
|||
=> _items; |
|||
|
|||
public override void Dispose() |
|||
=> _reader.Dispose(); |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
|
|||
namespace Avalonia.X11.Clipboard; |
|||
|
|||
/// <summary>
|
|||
/// Implementation of <see cref="IAsyncDataTransferItem"/> for the X11 clipboard.
|
|||
/// </summary>
|
|||
/// <param name="reader">The object used to read values.</param>
|
|||
/// <param name="formats">The formats.</param>
|
|||
internal sealed class ClipboardDataTransferItem(ClipboardDataReader reader, DataFormat[] formats) |
|||
: PlatformAsyncDataTransferItem |
|||
{ |
|||
private readonly ClipboardDataReader _reader = reader; |
|||
private readonly DataFormat[] _formats = formats; |
|||
|
|||
protected override DataFormat[] ProvideFormats() |
|||
=> _formats; |
|||
|
|||
protected override Task<object?> TryGetRawCoreAsync(DataFormat format) |
|||
=> _reader.TryGetAsync(format); |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Text; |
|||
using Avalonia.Platform.Storage; |
|||
using Avalonia.Platform.Storage.FileIO; |
|||
|
|||
namespace Avalonia.X11.Clipboard; |
|||
|
|||
internal static class ClipboardUriListHelper |
|||
{ |
|||
private static readonly Encoding s_utf8NoBomEncoding = new UTF8Encoding(false); |
|||
|
|||
public static IStorageItem[] Utf8BytesToFileUriList(byte[] utf8Bytes) |
|||
{ |
|||
try |
|||
{ |
|||
using var stream = new MemoryStream(utf8Bytes); |
|||
using var reader = new StreamReader(stream, s_utf8NoBomEncoding); |
|||
var items = new List<IStorageItem>(); |
|||
|
|||
while (reader.ReadLine() is { } line) |
|||
{ |
|||
if (Uri.TryCreate(line, UriKind.Absolute, out var uri) && |
|||
uri.IsFile && |
|||
StorageProviderHelpers.TryCreateBclStorageItem(uri.LocalPath) is { } storageItem) |
|||
{ |
|||
items.Add(storageItem); |
|||
} |
|||
} |
|||
|
|||
return items.ToArray(); |
|||
} |
|||
catch |
|||
{ |
|||
return []; |
|||
} |
|||
} |
|||
|
|||
public static byte[] FileUriListToUtf8Bytes(IEnumerable<IStorageItem> items) |
|||
{ |
|||
using var stream = new MemoryStream(); |
|||
using var writer = new StreamWriter(stream, s_utf8NoBomEncoding); |
|||
|
|||
writer.NewLine = "\r\n"; // CR+LF is mandatory according to the text/uri-list spec
|
|||
|
|||
foreach (var item in items) |
|||
writer.WriteLine(item.Path.AbsoluteUri); |
|||
|
|||
return stream.ToArray(); |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
using System.Linq; |
|||
using System.Runtime.InteropServices.JavaScript; |
|||
using Avalonia.Browser.Interop; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
|
|||
namespace Avalonia.Browser; |
|||
|
|||
/// <summary>
|
|||
/// Wraps an array of ReadableDataItem (a custom type defined in input.ts) into a <see cref="IAsyncDataTransfer"/>.
|
|||
/// Asynchronous only - used to read the clipboard.
|
|||
/// </summary>
|
|||
/// <param name="jsItems">The array of ReadableDataItem objects.</param>
|
|||
internal sealed class BrowserClipboardDataTransfer(JSObject jsItems) : PlatformAsyncDataTransfer |
|||
{ |
|||
private readonly JSObject _jsItems = jsItems; // JS type: ReadableDataItem[]
|
|||
|
|||
protected override DataFormat[] ProvideFormats() |
|||
=> Items.SelectMany(item => item.Formats).Distinct().ToArray(); |
|||
|
|||
protected override IAsyncDataTransferItem[] ProvideItems() |
|||
{ |
|||
var count = _jsItems.GetPropertyAsInt32("length"); |
|||
var items = new IAsyncDataTransferItem[count]; |
|||
for (var i = 0; i < count; ++i) |
|||
items[i] = new BrowserClipboardDataTransferItem(_jsItems.GetArrayItem(i)); |
|||
return items; |
|||
} |
|||
|
|||
public override void Dispose() |
|||
{ |
|||
_jsItems.Dispose(); |
|||
|
|||
if (AreItemsInitialized) |
|||
{ |
|||
foreach (var item in Items) |
|||
((BrowserClipboardDataTransferItem)item).Dispose(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
using System; |
|||
using System.Runtime.InteropServices.JavaScript; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Browser.Interop; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
|
|||
namespace Avalonia.Browser; |
|||
|
|||
/// <summary>
|
|||
/// Wraps a ReadableDataItem (a custom type defined in input.ts) into a <see cref="IAsyncDataTransferItem"/>.
|
|||
/// Asynchronous only - used to read a clipboard item.
|
|||
/// </summary>
|
|||
/// <param name="readableDataItem">The ReadableDataItem object.</param>
|
|||
internal sealed class BrowserClipboardDataTransferItem(JSObject readableDataItem) |
|||
: PlatformAsyncDataTransferItem, IDisposable |
|||
{ |
|||
private readonly JSObject _readableDataItem = readableDataItem; // JS type: ReadableDataItem
|
|||
|
|||
protected override DataFormat[] ProvideFormats() |
|||
=> BrowserDataTransferHelper.GetReadableItemFormats(_readableDataItem); |
|||
|
|||
protected override async Task<object?> TryGetRawCoreAsync(DataFormat format) |
|||
{ |
|||
var formatString = BrowserDataFormatHelper.ToBrowserFormat(format); |
|||
var value = await InputHelper.TryGetReadableDataItemValueAsync(_readableDataItem, formatString).ConfigureAwait(false); |
|||
return BrowserDataTransferHelper.TryGetValue(value, format); |
|||
} |
|||
|
|||
public void Dispose() |
|||
=> _readableDataItem.Dispose(); |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
using System; |
|||
using Avalonia.Input; |
|||
|
|||
namespace Avalonia.Browser; |
|||
|
|||
internal static class BrowserDataFormatHelper |
|||
{ |
|||
private const string FormatTextPlain = "text/plain"; |
|||
private const string FormatFiles = "Files"; |
|||
private const string AppPrefix = "application/avn-fmt."; |
|||
|
|||
public static DataFormat ToDataFormat(string formatString) |
|||
=> formatString switch |
|||
{ |
|||
FormatTextPlain => DataFormat.Text, |
|||
FormatFiles => DataFormat.File, |
|||
_ when IsTextFormat(formatString) => DataFormat.FromSystemName<string>(formatString, AppPrefix), |
|||
_ => DataFormat.FromSystemName<byte[]>(formatString, AppPrefix) |
|||
}; |
|||
|
|||
private static bool IsTextFormat(string format) |
|||
=> format.StartsWith("text/", StringComparison.OrdinalIgnoreCase); |
|||
|
|||
public static string ToBrowserFormat(DataFormat format) |
|||
{ |
|||
if (DataFormat.Text.Equals(format)) |
|||
return FormatTextPlain; |
|||
|
|||
if (DataFormat.File.Equals(format)) |
|||
return FormatFiles; |
|||
|
|||
return format.ToSystemName(AppPrefix); |
|||
} |
|||
} |
|||
@ -1,91 +0,0 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Runtime.InteropServices.JavaScript; |
|||
using Avalonia.Browser.Interop; |
|||
using Avalonia.Browser.Storage; |
|||
using Avalonia.Input; |
|||
using Avalonia.Platform.Storage; |
|||
|
|||
namespace Avalonia.Browser; |
|||
|
|||
internal class BrowserDataObject : IDataObject |
|||
{ |
|||
private readonly JSObject _dataObject; |
|||
|
|||
public BrowserDataObject(JSObject dataObject) |
|||
{ |
|||
_dataObject = dataObject; |
|||
} |
|||
|
|||
public IEnumerable<string> GetDataFormats() |
|||
{ |
|||
var types = new HashSet<string>(_dataObject.GetPropertyAsStringArray("types")); |
|||
var dataFormats = new HashSet<string>(types.Count); |
|||
|
|||
foreach (var type in types) |
|||
{ |
|||
if (type.StartsWith("text/", StringComparison.Ordinal)) |
|||
{ |
|||
dataFormats.Add(DataFormats.Text); |
|||
} |
|||
else if (type.Equals("Files", StringComparison.Ordinal)) |
|||
{ |
|||
dataFormats.Add(DataFormats.Files); |
|||
} |
|||
dataFormats.Add(type); |
|||
} |
|||
|
|||
// If drag'n'drop an image from the another web page, if won't add "Files" to the supported types, but only a "text/uri-list".
|
|||
// With "text/uri-list" browser can add actual file as well.
|
|||
var filesCount = _dataObject.GetPropertyAsJSObject("files")?.GetPropertyAsInt32("count"); |
|||
if (filesCount > 0) |
|||
{ |
|||
dataFormats.Add(DataFormats.Files); |
|||
} |
|||
|
|||
return dataFormats; |
|||
} |
|||
|
|||
public bool Contains(string dataFormat) |
|||
{ |
|||
return GetDataFormats().Contains(dataFormat); |
|||
} |
|||
|
|||
public object? Get(string dataFormat) |
|||
{ |
|||
if (dataFormat == DataFormats.Files) |
|||
{ |
|||
var files = _dataObject.GetPropertyAsJSObject("files"); |
|||
if (files is not null) |
|||
{ |
|||
return StorageHelper.FilesToItemsArray(files) |
|||
.Select(reference => reference.GetPropertyAsString("kind") switch |
|||
{ |
|||
"directory" => (IStorageItem)new JSStorageFolder(reference), |
|||
"file" => new JSStorageFile(reference), |
|||
_ => null |
|||
}) |
|||
.Where(i => i is not null) |
|||
.ToArray()!; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
if (dataFormat == DataFormats.Text) |
|||
{ |
|||
if (_dataObject.CallMethodString("getData", "text/plain") is { Length :> 0 } textData) |
|||
{ |
|||
return textData; |
|||
} |
|||
} |
|||
|
|||
if (_dataObject.CallMethodString("getData", dataFormat) is { Length: > 0 } data) |
|||
{ |
|||
return data; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
using System.Runtime.InteropServices.JavaScript; |
|||
using System.Text; |
|||
using Avalonia.Browser.Interop; |
|||
using Avalonia.Browser.Storage; |
|||
using Avalonia.Input; |
|||
using Avalonia.Platform.Storage; |
|||
|
|||
namespace Avalonia.Browser; |
|||
|
|||
internal static class BrowserDataTransferHelper |
|||
{ |
|||
public static DataFormat[] GetReadableItemFormats(JSObject readableDataItem /* JS type: ReadableDataItem */) |
|||
{ |
|||
var formatStrings = InputHelper.GetReadableDataItemFormats(readableDataItem); |
|||
var formats = new DataFormat[formatStrings.Length]; |
|||
for (var i = 0; i < formatStrings.Length; ++i) |
|||
formats[i] = BrowserDataFormatHelper.ToDataFormat(formatStrings[i]); |
|||
return formats; |
|||
} |
|||
|
|||
public static object? TryGetValue(JSObject? readableDataValue /* JS type: ReadableDataValue */, DataFormat format) |
|||
{ |
|||
object? data = readableDataValue?.GetPropertyAsString("type") switch |
|||
{ |
|||
"string" => readableDataValue.GetPropertyAsString("value"), |
|||
"bytes" => readableDataValue.GetPropertyAsByteArray("value"), |
|||
"file" => readableDataValue.GetPropertyAsJSObject("value") is { } jsFile ? new JSStorageFile(jsFile) : null, |
|||
_ => null |
|||
}; |
|||
|
|||
if (data is null) |
|||
return null; |
|||
|
|||
if (DataFormat.Text.Equals(format)) |
|||
return data as string; |
|||
|
|||
if (DataFormat.File.Equals(format)) |
|||
return data as IStorageItem; |
|||
|
|||
if (format is DataFormat<string>) |
|||
{ |
|||
return data switch |
|||
{ |
|||
string text => text, |
|||
byte[] bytes => Encoding.UTF8.GetString(bytes), |
|||
_ => null |
|||
}; |
|||
} |
|||
|
|||
if (format is DataFormat<byte[]>) |
|||
{ |
|||
return data switch |
|||
{ |
|||
byte[] bytes => bytes, |
|||
string text => Encoding.UTF8.GetBytes(text), |
|||
_ => null |
|||
}; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
using System.Linq; |
|||
using System.Runtime.InteropServices.JavaScript; |
|||
using Avalonia.Browser.Interop; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
|
|||
namespace Avalonia.Browser; |
|||
|
|||
/// <summary>
|
|||
/// Wraps an array of ReadableDataItem (a custom type defined in input.ts) into a <see cref="IDataTransfer"/>.
|
|||
/// Synchronous only - used to read the drag and drop items.
|
|||
/// </summary>
|
|||
/// <param name="jsItems">The array of ReadableDataItem objects.</param>
|
|||
internal sealed class BrowserDragDataTransfer(JSObject jsItems) : PlatformDataTransfer |
|||
{ |
|||
private readonly JSObject _jsItems = jsItems; // JS type: ReadableDataItem[]
|
|||
|
|||
protected override DataFormat[] ProvideFormats() |
|||
=> Items.SelectMany(item => item.Formats).Distinct().ToArray(); |
|||
|
|||
protected override PlatformDataTransferItem[] ProvideItems() |
|||
{ |
|||
var count = _jsItems.GetPropertyAsInt32("length"); |
|||
var items = new PlatformDataTransferItem[count]; |
|||
for (var i = 0; i < count; ++i) |
|||
items[i] = new BrowserDragDataTransferItem(_jsItems.GetArrayItem(i)); |
|||
return items; |
|||
} |
|||
|
|||
public override void Dispose() |
|||
{ |
|||
_jsItems.Dispose(); |
|||
|
|||
if (AreItemsInitialized) |
|||
{ |
|||
foreach (var item in Items) |
|||
((BrowserDragDataTransferItem)item).Dispose(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
using System; |
|||
using System.Runtime.InteropServices.JavaScript; |
|||
using Avalonia.Browser.Interop; |
|||
using Avalonia.Input; |
|||
using Avalonia.Input.Platform; |
|||
|
|||
namespace Avalonia.Browser; |
|||
|
|||
/// <summary>
|
|||
/// Wraps a ReadableDataItem (a custom type defined in input.ts) into a <see cref="IDataTransferItem"/>.
|
|||
/// Synchronous only - used to read a drag and drop item.
|
|||
/// </summary>
|
|||
/// <param name="readableDataItem">The ReadableDataItem object.</param>
|
|||
internal sealed class BrowserDragDataTransferItem(JSObject readableDataItem) |
|||
: PlatformDataTransferItem, IDisposable |
|||
{ |
|||
private readonly JSObject _readableDataItem = readableDataItem; // JS type: ReadableDataItem
|
|||
|
|||
protected override DataFormat[] ProvideFormats() |
|||
=> BrowserDataTransferHelper.GetReadableItemFormats(_readableDataItem); |
|||
|
|||
protected override object? TryGetRawCore(DataFormat format) |
|||
{ |
|||
var formatString = BrowserDataFormatHelper.ToBrowserFormat(format); |
|||
var value = InputHelper.TryGetReadableDataItemValue(_readableDataItem, formatString); |
|||
return BrowserDataTransferHelper.TryGetValue(value, format); |
|||
} |
|||
|
|||
public void Dispose() |
|||
=> _readableDataItem.Dispose(); |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue