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 buildpull/19722/head
committed by
GitHub
137 changed files with 5920 additions and 1881 deletions
@ -0,0 +1,76 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids --> |
||||
|
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> |
||||
|
<Suppression> |
||||
|
<DiagnosticId>CP0006</DiagnosticId> |
||||
|
<Target>M:Avalonia.Input.Platform.IClipboard.SetDataAsync(Avalonia.Input.IAsyncDataTransfer)</Target> |
||||
|
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left> |
||||
|
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right> |
||||
|
</Suppression> |
||||
|
<Suppression> |
||||
|
<DiagnosticId>CP0006</DiagnosticId> |
||||
|
<Target>M:Avalonia.Input.Platform.IClipboard.TryGetDataAsync</Target> |
||||
|
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left> |
||||
|
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right> |
||||
|
</Suppression> |
||||
|
<Suppression> |
||||
|
<DiagnosticId>CP0006</DiagnosticId> |
||||
|
<Target>M:Avalonia.Input.Platform.IClipboard.TryGetInProcessDataAsync</Target> |
||||
|
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left> |
||||
|
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right> |
||||
|
</Suppression> |
||||
|
<Suppression> |
||||
|
<DiagnosticId>CP0006</DiagnosticId> |
||||
|
<Target>M:Avalonia.Input.Platform.IPlatformDragSource.DoDragDropAsync(Avalonia.Input.PointerEventArgs,Avalonia.Input.IDataTransfer,Avalonia.Input.DragDropEffects)</Target> |
||||
|
<Left>baseline/Avalonia/lib/net6.0/Avalonia.Base.dll</Left> |
||||
|
<Right>current/Avalonia/lib/net6.0/Avalonia.Base.dll</Right> |
||||
|
</Suppression> |
||||
|
<Suppression> |
||||
|
<DiagnosticId>CP0006</DiagnosticId> |
||||
|
<Target>M:Avalonia.Input.Platform.IClipboard.SetDataAsync(Avalonia.Input.IAsyncDataTransfer)</Target> |
||||
|
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left> |
||||
|
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right> |
||||
|
</Suppression> |
||||
|
<Suppression> |
||||
|
<DiagnosticId>CP0006</DiagnosticId> |
||||
|
<Target>M:Avalonia.Input.Platform.IClipboard.TryGetDataAsync</Target> |
||||
|
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left> |
||||
|
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right> |
||||
|
</Suppression> |
||||
|
<Suppression> |
||||
|
<DiagnosticId>CP0006</DiagnosticId> |
||||
|
<Target>M:Avalonia.Input.Platform.IClipboard.TryGetInProcessDataAsync</Target> |
||||
|
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left> |
||||
|
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right> |
||||
|
</Suppression> |
||||
|
<Suppression> |
||||
|
<DiagnosticId>CP0006</DiagnosticId> |
||||
|
<Target>M:Avalonia.Input.Platform.IPlatformDragSource.DoDragDropAsync(Avalonia.Input.PointerEventArgs,Avalonia.Input.IDataTransfer,Avalonia.Input.DragDropEffects)</Target> |
||||
|
<Left>baseline/Avalonia/lib/net8.0/Avalonia.Base.dll</Left> |
||||
|
<Right>current/Avalonia/lib/net8.0/Avalonia.Base.dll</Right> |
||||
|
</Suppression> |
||||
|
<Suppression> |
||||
|
<DiagnosticId>CP0006</DiagnosticId> |
||||
|
<Target>M:Avalonia.Input.Platform.IClipboard.SetDataAsync(Avalonia.Input.IAsyncDataTransfer)</Target> |
||||
|
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left> |
||||
|
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right> |
||||
|
</Suppression> |
||||
|
<Suppression> |
||||
|
<DiagnosticId>CP0006</DiagnosticId> |
||||
|
<Target>M:Avalonia.Input.Platform.IClipboard.TryGetDataAsync</Target> |
||||
|
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left> |
||||
|
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right> |
||||
|
</Suppression> |
||||
|
<Suppression> |
||||
|
<DiagnosticId>CP0006</DiagnosticId> |
||||
|
<Target>M:Avalonia.Input.Platform.IClipboard.TryGetInProcessDataAsync</Target> |
||||
|
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left> |
||||
|
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right> |
||||
|
</Suppression> |
||||
|
<Suppression> |
||||
|
<DiagnosticId>CP0006</DiagnosticId> |
||||
|
<Target>M:Avalonia.Input.Platform.IPlatformDragSource.DoDragDropAsync(Avalonia.Input.PointerEventArgs,Avalonia.Input.IDataTransfer,Avalonia.Input.DragDropEffects)</Target> |
||||
|
<Left>baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Left> |
||||
|
<Right>current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll</Right> |
||||
|
</Suppression> |
||||
|
</Suppressions> |
||||
@ -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 "common.h" |
||||
|
#include "clipboard.h" |
||||
#include "AvnString.h" |
#include "AvnString.h" |
||||
|
|
||||
class Clipboard : public ComSingleObject<IAvnClipboard, &IID_IAvnClipboard> |
class Clipboard : public ComSingleObject<IAvnClipboard, &IID_IAvnClipboard> |
||||
{ |
{ |
||||
private: |
private: |
||||
NSPasteboard* _pb; |
NSPasteboard* _pasteboard; |
||||
NSPasteboardItem* _item; |
|
||||
public: |
public: |
||||
FORWARD_IUNKNOWN() |
FORWARD_IUNKNOWN() |
||||
|
|
||||
Clipboard(NSPasteboard* pasteboard, NSPasteboardItem* item) |
Clipboard(NSPasteboard* pasteboard) |
||||
{ |
{ |
||||
if(pasteboard == nil && item == nil) |
if (pasteboard == nil) |
||||
pasteboard = [NSPasteboard generalPasteboard]; |
pasteboard = [NSPasteboard generalPasteboard]; |
||||
|
|
||||
_pb = pasteboard; |
_pasteboard = pasteboard; |
||||
_item = item; |
|
||||
} |
} |
||||
|
|
||||
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 (ret == nullptr) |
||||
{ |
return E_POINTER; |
||||
if(ppv == nullptr) |
|
||||
{ |
if (changeCount != [_pasteboard changeCount]) |
||||
return E_POINTER; |
return COR_E_OBJECTDISPOSED; |
||||
} |
|
||||
NSString* typeString = [NSString stringWithUTF8String:(const char*)type]; |
auto items = [_pasteboard pasteboardItems]; |
||||
NSString* string = _item == nil ? [_pb stringForType:typeString] : [_item stringForType:typeString]; |
*ret = items == nil ? 0 : (int)[items count]; |
||||
|
return S_OK; |
||||
*ppv = CreateAvnString(string); |
|
||||
|
|
||||
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 |
if (ret == nullptr) |
||||
{ |
return E_POINTER; |
||||
NSArray<NSString*>* data = GetNSArrayOfStringsAndRelease(ppv); |
|
||||
NSString* typeString = [NSString stringWithUTF8String:(const char*)type]; |
if (changeCount != [_pasteboard changeCount]) |
||||
if(_item == nil) |
return COR_E_OBJECTDISPOSED; |
||||
[_pb setPropertyList: data forType: typeString]; |
|
||||
else |
auto item = [[_pasteboard pasteboardItems] objectAtIndex:index]; |
||||
[_item setPropertyList: data forType:typeString]; |
auto types = [item types]; |
||||
return S_OK; |
*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 |
if (ret == nullptr) |
||||
{ |
return E_POINTER; |
||||
*ppv= nil; |
|
||||
NSString* typeString = [NSString stringWithUTF8String:(const char*)type]; |
if (changeCount != [_pasteboard changeCount]) |
||||
NSObject* data = _item == nil ? [_pb propertyListForType: typeString] : [_item propertyListForType: typeString]; |
return COR_E_OBJECTDISPOSED; |
||||
if(data == nil) |
|
||||
return S_OK; |
auto item = [[_pasteboard pasteboardItems] objectAtIndex:index]; |
||||
|
auto value = [item stringForType:[NSString stringWithUTF8String:format]]; |
||||
if([data isKindOfClass: [NSString class]]) |
*ret = value == nil ? nullptr : CreateAvnString(value); |
||||
{ |
return S_OK; |
||||
*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; |
|
||||
} |
|
||||
} |
} |
||||
|
|
||||
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 |
if (ret == nullptr) |
||||
{ |
return E_POINTER; |
||||
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]; |
|
||||
|
|
||||
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 item = source->GetItem(i); |
||||
auto data = [NSData dataWithBytes:bytes length:len]; |
auto writeableItem = [[WriteableClipboardItem alloc] initWithItem:item source:source]; |
||||
if(_item == nil) |
[writeableItems addObject:writeableItem]; |
||||
[_pb setData:data forType:typeString]; |
|
||||
else |
|
||||
[_item setData:data forType:typeString]; |
|
||||
return S_OK; |
|
||||
} |
} |
||||
|
|
||||
|
[_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 type = [UTType typeWithIdentifier:formatString]; |
||||
auto typeString = [NSString stringWithUTF8String:(const char*)type]; |
return type != nil && [type conformsToType:UTTypeText]; |
||||
NSData*data; |
} |
||||
@try |
else |
||||
{ |
{ |
||||
if(_item) |
return UTTypeConformsTo((__bridge CFStringRef)formatString, kUTTypeText); |
||||
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; |
|
||||
} |
} |
||||
} |
} |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
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; |
auto type = [UTType typeWithIdentifier:format]; |
||||
|
if (type == nil) |
||||
@autoreleasepool |
|
||||
{ |
{ |
||||
if(_item != nil) |
if ([format containsString:@"/"]) |
||||
{ |
type = [UTType typeWithMIMEType:format]; |
||||
_item = [NSPasteboardItem new]; |
|
||||
return 0; |
|
||||
} |
|
||||
else |
else |
||||
|
type = [UTType exportedTypeWithIdentifier:format]; |
||||
|
|
||||
|
if (type == nil) |
||||
{ |
{ |
||||
*rv = [_pb clearContents]; |
// For now, we need to use the deprecated UTTypeCreatePreferredIdentifierForTag to create a dynamic UTI for arbitrary strings. |
||||
[_pb setString:@"" forType:NSPasteboardTypeString]; |
// 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]; |
||||
} |
} |
||||
|
else |
||||
virtual HRESULT GetChangeCount(int64_t* rv) override |
|
||||
{ |
{ |
||||
START_COM_CALL; |
auto bridgedFormat = (__bridge CFStringRef)format; |
||||
if(_item == nil) |
if (UTTypeIsDeclared(bridgedFormat)) |
||||
{ |
return format; |
||||
*rv = [_pb changeCount]; |
|
||||
return S_OK; |
auto fromMimeType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, bridgedFormat, nil); |
||||
} |
if (fromMimeType != nil) |
||||
return E_NOTIMPL; |
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; |
if (formats->Get(i, &format) != S_OK) |
||||
|
continue; |
||||
@autoreleasepool |
|
||||
{ |
// Only UTIs must be returned from writableTypesForPasteboard or an exception will be thrown |
||||
*ppv = CreateAvnStringArray(_item == nil ? [_pb types] : [_item types]); |
auto formatString = GetNSStringAndRelease(format); |
||||
return S_OK; |
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 ([type isEqualToString:GetAvnCustomDataType()]) |
||||
if(clipboard == nil) |
return @""; |
||||
|
|
||||
|
ComPtr<IAvnClipboardDataValue> value(_item->GetValue([type UTF8String]), true); |
||||
|
if (value.getRaw() == nullptr) |
||||
return nil; |
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; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
using System.Threading.Tasks; |
using System.Threading.Tasks; |
||||
using Android.Content; |
using Android.Content; |
||||
using Avalonia.Input; |
using Avalonia.Input; |
||||
using Avalonia.Input.Platform; |
using Avalonia.Input.Platform; |
||||
|
using Avalonia.Logging; |
||||
|
using AndroidUri = Android.Net.Uri; |
||||
|
|
||||
namespace Avalonia.Android.Platform |
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); |
if (hasFormats) |
||||
_clipboardManager.PrimaryClip = clip; |
{ |
||||
|
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() |
public Task ClearAsync() |
||||
{ |
{ |
||||
if (_clipboardManager == null) |
try |
||||
{ |
{ |
||||
|
Clear(); |
||||
return Task.CompletedTask; |
return Task.CompletedTask; |
||||
} |
} |
||||
|
catch (Exception ex) |
||||
_clipboardManager.PrimaryClip = null; |
{ |
||||
|
return Task.FromException(ex); |
||||
return Task.CompletedTask; |
} |
||||
} |
} |
||||
|
|
||||
public Task SetDataObjectAsync(IDataObject data) => throw new PlatformNotSupportedException(); |
private void Clear() |
||||
|
{ |
||||
public Task<string[]> GetFormatsAsync() => throw new PlatformNotSupportedException(); |
if (_clipboardManager is null) |
||||
|
return; |
||||
public Task<object?> GetDataAsync(string format) => throw new PlatformNotSupportedException(); |
|
||||
|
|
||||
public Task<IDataObject?> TryGetInProcessDataObjectAsync() => Task.FromResult<IDataObject?>(null); |
|
||||
|
|
||||
/// <inheritdoc />
|
if (OperatingSystem.IsAndroidVersionAtLeast(28)) |
||||
public Task FlushAsync() => |
_clipboardManager.ClearPrimaryClip(); |
||||
Task.CompletedTask; |
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; |
using Avalonia.Metadata; |
||||
|
|
||||
namespace Avalonia.Input.Platform |
namespace Avalonia.Input.Platform |
||||
{ |
{ |
||||
[Unstable] |
[NotClientImplementable] |
||||
public interface IPlatformDragSource |
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 |
#nullable enable |
||||
|
|
||||
using System; |
using System; |
||||
using System.Collections.Generic; |
|
||||
using System.Linq; |
|
||||
using System.Threading.Tasks; |
using System.Threading.Tasks; |
||||
using Avalonia.Controls.Platform; |
|
||||
using Avalonia.Input; |
using Avalonia.Input; |
||||
using Avalonia.Input.Platform; |
using Avalonia.Input.Platform; |
||||
using Avalonia.Logging; |
using Avalonia.Logging; |
||||
using Avalonia.Native.Interop; |
using Avalonia.Native.Interop; |
||||
using Avalonia.Platform.Storage; |
|
||||
using Avalonia.Platform.Storage.FileIO; |
|
||||
|
|
||||
namespace Avalonia.Native |
namespace Avalonia.Native |
||||
{ |
{ |
||||
class ClipboardImpl : IClipboard, IDisposable |
internal sealed class ClipboardImpl(IAvnClipboard native) : IOwnedClipboardImpl, IDisposable |
||||
{ |
{ |
||||
private IAvnClipboard? _native; |
private IAvnClipboard? _native = native; |
||||
private IDataObject? _savedDataObject; |
private long _lastClearChangeCount = long.MinValue; |
||||
private long _lastClearChangeCount; |
|
||||
|
|
||||
// TODO hide native types behind IAvnClipboard abstraction, so managed side won't depend on macOS.
|
internal IAvnClipboard Native |
||||
private const string NSPasteboardTypeString = "public.utf8-plain-text"; |
|
||||
private const string NSFilenamesPboardType = "NSFilenamesPboardType"; |
|
||||
|
|
||||
public ClipboardImpl(IAvnClipboard native) |
|
||||
{ |
|
||||
_native = native; |
|
||||
} |
|
||||
|
|
||||
private IAvnClipboard Native |
|
||||
=> _native ?? throw new ObjectDisposedException(nameof(ClipboardImpl)); |
=> _native ?? throw new ObjectDisposedException(nameof(ClipboardImpl)); |
||||
|
|
||||
private void ClearCore() |
private void ClearCore() |
||||
{ |
{ |
||||
_savedDataObject = null; |
|
||||
_lastClearChangeCount = Native.Clear(); |
_lastClearChangeCount = Native.Clear(); |
||||
} |
} |
||||
|
|
||||
public Task ClearAsync() |
|
||||
{ |
|
||||
ClearCore(); |
|
||||
return Task.CompletedTask; |
|
||||
} |
|
||||
|
|
||||
public Task<string?> GetTextAsync() |
public Task ClearAsync() |
||||
{ |
|
||||
using (var text = Native.GetText(NSPasteboardTypeString)) |
|
||||
return Task.FromResult<string?>(text.String); |
|
||||
} |
|
||||
|
|
||||
public Task SetTextAsync(string? text) |
|
||||
{ |
{ |
||||
var native = Native; |
try |
||||
|
{ |
||||
ClearCore(); |
ClearCore(); |
||||
|
return Task.CompletedTask; |
||||
if (text != null) |
} |
||||
native.SetText(NSPasteboardTypeString, text); |
catch (Exception ex) |
||||
|
{ |
||||
return Task.CompletedTask; |
return Task.FromException(ex); |
||||
|
} |
||||
} |
} |
||||
|
|
||||
public IEnumerable<string> GetFormats() |
public Task<IAsyncDataTransfer?> TryGetDataAsync() |
||||
{ |
{ |
||||
var rv = new HashSet<string>(); |
try |
||||
using (var formats = Native.ObtainFormats()) |
|
||||
{ |
{ |
||||
var cnt = formats.Count; |
return Task.FromResult(TryGetData()); |
||||
for (uint c = 0; c < cnt; c++) |
} |
||||
{ |
catch (Exception ex) |
||||
using (var fmt = formats.Get(c)) |
{ |
||||
{ |
return Task.FromException<IAsyncDataTransfer?>(ex); |
||||
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 rv; |
|
||||
} |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
_savedDataObject = null; |
|
||||
_native?.Dispose(); |
|
||||
_native = null; |
|
||||
} |
} |
||||
|
|
||||
public IEnumerable<string>? GetFileNames() |
private IAsyncDataTransfer? TryGetData() |
||||
{ |
{ |
||||
using (var strings = Native.GetStrings(NSFilenamesPboardType)) |
var dataTransfer = new ClipboardDataTransfer( |
||||
return strings?.ToStringArray(); |
new ClipboardReadSession(Native, Native.ChangeCount, ownsNative: false)); |
||||
} |
|
||||
|
|
||||
public IEnumerable<IStorageItem>? GetFiles() |
if (dataTransfer.Formats.Length == 0) |
||||
{ |
{ |
||||
var storageApi = (StorageProviderApi)AvaloniaLocator.Current.GetRequiredService<IStorageProviderFactory>(); |
dataTransfer.Dispose(); |
||||
|
|
||||
// TODO: use non-deprecated AppKit API to get NSUri instead of file names.
|
|
||||
var fileNames = GetFileNames(); |
|
||||
if (fileNames is null) |
|
||||
return null; |
return null; |
||||
|
} |
||||
|
|
||||
return fileNames |
return dataTransfer; |
||||
.Select(f => StorageProviderHelpers.TryGetUriFromFilePath(f, false) is { } uri |
|
||||
? storageApi.TryGetStorageItem(uri) |
|
||||
: null) |
|
||||
.Where(f => f is not null)!; |
|
||||
} |
} |
||||
|
|
||||
public unsafe Task SetDataObjectAsync(IDataObject data) |
public Task SetDataAsync(IAsyncDataTransfer dataTransfer) |
||||
{ |
{ |
||||
ClearCore(); |
try |
||||
|
|
||||
// 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) |
|
||||
{ |
{ |
||||
var o = data.Get(fromFormat); |
SetData(dataTransfer); |
||||
switch (o) |
return Task.CompletedTask; |
||||
{ |
} |
||||
case string s: |
catch (Exception ex) |
||||
Native.SetText(toFormat, s); |
{ |
||||
break; |
return Task.FromException(ex); |
||||
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; |
|
||||
} |
|
||||
} |
} |
||||
|
|
||||
_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) |
ClearCore(); |
||||
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; |
|
||||
} |
|
||||
|
|
||||
public Task<IDataObject?> TryGetInProcessDataObjectAsync() |
Native.SetData(new DataTransferToAvnClipboardDataSourceWrapper( |
||||
{ |
dataTransfer.ToSynchronous(LogArea.macOSPlatform))); |
||||
if (Native.ChangeCount != _lastClearChangeCount) |
|
||||
_savedDataObject = null; |
|
||||
return Task.FromResult(_savedDataObject); |
|
||||
} |
} |
||||
|
|
||||
/// <inheritdoc />
|
public Task<bool> IsCurrentOwnerAsync() |
||||
public Task FlushAsync() => |
=> Task.FromResult(Native.ChangeCount == _lastClearChangeCount); |
||||
Task.CompletedTask; |
|
||||
|
|
||||
} |
|
||||
|
|
||||
class ClipboardDataObject : IDataObject, IDisposable |
|
||||
{ |
|
||||
private ClipboardImpl? _clipboard; |
|
||||
private List<string>? _formats; |
|
||||
|
|
||||
public ClipboardDataObject(IAvnClipboard clipboard) |
|
||||
{ |
|
||||
_clipboard = new ClipboardImpl(clipboard); |
|
||||
} |
|
||||
|
|
||||
public void Dispose() |
public void Dispose() |
||||
{ |
{ |
||||
_clipboard?.Dispose(); |
_native?.Dispose(); |
||||
_clipboard = null; |
_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