diff --git a/Avalonia.sln b/Avalonia.sln
index a0314b1c33..768fb50452 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -37,11 +37,12 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DEF5-D50F-4975-8B72-124C9EB54066}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
+ src\Shared\CallerArgumentExpressionAttribute.cs = src\Shared\CallerArgumentExpressionAttribute.cs
src\Shared\IsExternalInit.cs = src\Shared\IsExternalInit.cs
src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs
- src\Shared\NullableAttributes.cs = src\Shared\NullableAttributes.cs
src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs
src\Shared\StringCompatibilityExtensions.cs = src\Shared\StringCompatibilityExtensions.cs
+ src\Shared\StreamCompatibilityExtensions.cs = src\Shared\StreamCompatibilityExtensions.cs
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI", "src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj", "{6417B24E-49C2-4985-8DB2-3AB9D898EC91}"
diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml
index 35d8c92717..4df9a900be 100644
--- a/api/Avalonia.nupkg.xml
+++ b/api/Avalonia.nupkg.xml
@@ -1,4 +1,4 @@
-
+
@@ -133,4 +133,76 @@
baseline/netstandard2.0/Avalonia.Controls.dll
target/netstandard2.0/Avalonia.Controls.dll
-
\ No newline at end of file
+
+ CP0006
+ M:Avalonia.Input.Platform.IClipboard.SetDataAsync(Avalonia.Input.IAsyncDataTransfer)
+ baseline/Avalonia/lib/net6.0/Avalonia.Base.dll
+ current/Avalonia/lib/net6.0/Avalonia.Base.dll
+
+
+ CP0006
+ M:Avalonia.Input.Platform.IClipboard.TryGetDataAsync
+ baseline/Avalonia/lib/net6.0/Avalonia.Base.dll
+ current/Avalonia/lib/net6.0/Avalonia.Base.dll
+
+
+ CP0006
+ M:Avalonia.Input.Platform.IClipboard.TryGetInProcessDataAsync
+ baseline/Avalonia/lib/net6.0/Avalonia.Base.dll
+ current/Avalonia/lib/net6.0/Avalonia.Base.dll
+
+
+ CP0006
+ M:Avalonia.Input.Platform.IPlatformDragSource.DoDragDropAsync(Avalonia.Input.PointerEventArgs,Avalonia.Input.IDataTransfer,Avalonia.Input.DragDropEffects)
+ baseline/Avalonia/lib/net6.0/Avalonia.Base.dll
+ current/Avalonia/lib/net6.0/Avalonia.Base.dll
+
+
+ CP0006
+ M:Avalonia.Input.Platform.IClipboard.SetDataAsync(Avalonia.Input.IAsyncDataTransfer)
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+
+ CP0006
+ M:Avalonia.Input.Platform.IClipboard.TryGetDataAsync
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+
+ CP0006
+ M:Avalonia.Input.Platform.IClipboard.TryGetInProcessDataAsync
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+
+ CP0006
+ M:Avalonia.Input.Platform.IPlatformDragSource.DoDragDropAsync(Avalonia.Input.PointerEventArgs,Avalonia.Input.IDataTransfer,Avalonia.Input.DragDropEffects)
+ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll
+ current/Avalonia/lib/net8.0/Avalonia.Base.dll
+
+
+ CP0006
+ M:Avalonia.Input.Platform.IClipboard.SetDataAsync(Avalonia.Input.IAsyncDataTransfer)
+ baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+ current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+
+
+ CP0006
+ M:Avalonia.Input.Platform.IClipboard.TryGetDataAsync
+ baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+ current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+
+
+ CP0006
+ M:Avalonia.Input.Platform.IClipboard.TryGetInProcessDataAsync
+ baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+ current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+
+
+ CP0006
+ M:Avalonia.Input.Platform.IPlatformDragSource.DoDragDropAsync(Avalonia.Input.PointerEventArgs,Avalonia.Input.IDataTransfer,Avalonia.Input.DragDropEffects)
+ baseline/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+ current/Avalonia/lib/netstandard2.0/Avalonia.Base.dll
+
+
diff --git a/native/Avalonia.Native/inc/com.h b/native/Avalonia.Native/inc/com.h
index 42a989e050..9a3f067dce 100644
--- a/native/Avalonia.Native/inc/com.h
+++ b/native/Avalonia.Native/inc/com.h
@@ -28,7 +28,8 @@ typedef DWORD ULONG;
#define E_UNEXPECTED 0x8000FFFFL
#define E_HANDLE 0x80070006L
#define E_INVALIDARG 0x80070057L
-#define COR_E_INVALIDOPERATION 0x80131509L
+#define COR_E_INVALIDOPERATION 0x80131509L
+#define COR_E_OBJECTDISPOSED 0x80131622L
struct IUnknown
{
diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
index 9a67ee0161..868551eac1 100644
--- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
+++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
@@ -63,6 +63,7 @@
EDF8CDCD2964CB01001EE34F /* PlatformSettings.mm in Sources */ = {isa = PBXBuildFile; fileRef = EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */; };
F10084842BFF1F9E0024303E /* TopLevelImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = F10084832BFF1F9E0024303E /* TopLevelImpl.h */; };
F10084862BFF1FB40024303E /* TopLevelImpl.mm in Sources */ = {isa = PBXBuildFile; fileRef = F10084852BFF1FB40024303E /* TopLevelImpl.mm */; };
+ F931F8682E2D43A7004E081E /* clipboard.h in Headers */ = {isa = PBXBuildFile; fileRef = F931F8672E2D43A4004E081E /* clipboard.h */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@@ -130,6 +131,7 @@
EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PlatformSettings.mm; sourceTree = ""; };
F10084832BFF1F9E0024303E /* TopLevelImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TopLevelImpl.h; sourceTree = ""; };
F10084852BFF1FB40024303E /* TopLevelImpl.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = TopLevelImpl.mm; sourceTree = ""; };
+ F931F8672E2D43A4004E081E /* clipboard.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = clipboard.h; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -189,6 +191,7 @@
1AFD334023E03C4F0042899B /* controlhost.mm */,
5BF943652167AD1D009CAE35 /* cursor.h */,
5B21A981216530F500CEE36E /* cursor.mm */,
+ F931F8672E2D43A4004E081E /* clipboard.h */,
5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */,
1A465D0F246AB61600C5858B /* dnd.mm */,
AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */,
@@ -246,6 +249,7 @@
files = (
37155CE4233C00EB0034DCE9 /* menu.h in Headers */,
F10084842BFF1F9E0024303E /* TopLevelImpl.h in Headers */,
+ F931F8682E2D43A7004E081E /* clipboard.h in Headers */,
BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */,
183916173528EC2737DBE5E1 /* WindowBaseImpl.h in Headers */,
1839171DCC651B0638603AC4 /* INSWindowHolder.h in Headers */,
diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm
index 4cc495f321..6fe1c8ceeb 100644
--- a/native/Avalonia.Native/src/OSX/AvnView.mm
+++ b/native/Avalonia.Native/src/OSX/AvnView.mm
@@ -868,7 +868,7 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt)
return NSDragOperationNone;
int reffects = (int)parent->TopLevelEvents
->DragEvent(type, point, modifiers, effects,
- CreateClipboard([info draggingPasteboard], nil),
+ CreateClipboard([info draggingPasteboard]),
GetAvnDataObjectHandleFromDraggingInfo(info));
NSDragOperation ret = static_cast(0);
diff --git a/native/Avalonia.Native/src/OSX/StorageProvider.mm b/native/Avalonia.Native/src/OSX/StorageProvider.mm
index 92278a85e9..4a82eeaea7 100644
--- a/native/Avalonia.Native/src/OSX/StorageProvider.mm
+++ b/native/Avalonia.Native/src/OSX/StorageProvider.mm
@@ -355,6 +355,35 @@ public:
}
}
+ virtual HRESULT TryResolveFileReferenceUri(IAvnString* fileUriStr, IAvnString** ret) override {
+ if (ret == nullptr)
+ return E_POINTER;
+
+ if (fileUriStr == nullptr)
+ {
+ *ret = nullptr;
+ return S_OK;
+ }
+
+ auto fileUri = [NSURL URLWithString:GetNSStringAndRelease(fileUriStr)];
+ if (fileUri == nil)
+ {
+ *ret = nullptr;
+ return S_OK;
+ }
+
+ auto filePathUri = [fileUri filePathURL];
+ if (fileUri == nil)
+ {
+ *ret = nullptr;
+ return S_OK;
+ }
+
+ *ret = CreateAvnString([filePathUri absoluteString]);
+ return S_OK;
+ }
+
+
private:
NSView* CreateAccessoryView() {
// The label. Add attributes per-OS to match the labels that macOS uses.
diff --git a/native/Avalonia.Native/src/OSX/TopLevelImpl.h b/native/Avalonia.Native/src/OSX/TopLevelImpl.h
index b18ad21d30..0be1439ec2 100644
--- a/native/Avalonia.Native/src/OSX/TopLevelImpl.h
+++ b/native/Avalonia.Native/src/OSX/TopLevelImpl.h
@@ -61,8 +61,13 @@ public:
virtual HRESULT GetCurrentDisplayId (CGDirectDisplayID* ret) override;
- virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point,
- IAvnClipboard* clipboard, IAvnDndResultCallback* cb, void* sourceHandle) override;
+ virtual HRESULT BeginDragAndDropOperation(
+ AvnDragDropEffects effects,
+ AvnPoint point,
+ IAvnClipboardDataSource* source,
+ IAvnDndResultCallback* callback,
+ void* sourceHandle) override;
+
protected:
NSCursor *cursor;
virtual void UpdateAppearance();
diff --git a/native/Avalonia.Native/src/OSX/TopLevelImpl.mm b/native/Avalonia.Native/src/OSX/TopLevelImpl.mm
index c247e48fe0..bdfc1be62f 100644
--- a/native/Avalonia.Native/src/OSX/TopLevelImpl.mm
+++ b/native/Avalonia.Native/src/OSX/TopLevelImpl.mm
@@ -7,6 +7,7 @@
#include "AvnTextInputMethod.h"
#include "AvnView.h"
#include "common.h"
+#include "clipboard.h"
TopLevelImpl::~TopLevelImpl() {
View = nullptr;
@@ -272,13 +273,15 @@ void TopLevelImpl::UpdateAppearance() {
}
-HRESULT TopLevelImpl::BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard *clipboard, IAvnDndResultCallback *cb, void *sourceHandle) {
+HRESULT TopLevelImpl::BeginDragAndDropOperation(
+ AvnDragDropEffects effects,
+ AvnPoint point,
+ IAvnClipboardDataSource* source,
+ IAvnDndResultCallback* callback,
+ void* sourceHandle)
+{
START_COM_CALL;
- auto item = TryGetPasteboardItem(clipboard);
- [item setString:@"" forType:GetAvnCustomDataType()];
- if (item == nil)
- return E_INVALIDARG;
if (View == NULL)
return E_FAIL;
@@ -301,11 +304,19 @@ HRESULT TopLevelImpl::BeginDragAndDropOperation(AvnDragDropEffects effects, AvnP
}
}
- auto dragItem = [[NSDraggingItem alloc] initWithPasteboardWriter:item];
-
+ auto itemCount = source->GetItemCount();
+ auto draggingItems = [NSMutableArray arrayWithCapacity:itemCount];
auto dragItemImage = [NSImage imageNamed:NSImageNameMultipleDocuments];
NSRect dragItemRect = {(float) point.X, (float) point.Y, [dragItemImage size].width, [dragItemImage size].height};
- [dragItem setDraggingFrame:dragItemRect contents:dragItemImage];
+
+ for (auto i = 0; i < itemCount; ++i)
+ {
+ auto item = source->GetItem(i);
+ auto writeableItem = [[WriteableClipboardItem alloc] initWithItem:item source:source];
+ auto draggingItem = [[NSDraggingItem alloc] initWithPasteboardWriter:writeableItem];
+ [draggingItem setDraggingFrame:dragItemRect contents:dragItemImage];
+ [draggingItems addObject:draggingItem];
+ }
int op = 0;
int ieffects = (int) effects;
@@ -315,8 +326,10 @@ HRESULT TopLevelImpl::BeginDragAndDropOperation(AvnDragDropEffects effects, AvnP
op |= NSDragOperationLink;
if ((ieffects & (int) AvnDragDropEffects::Move) != 0)
op |= NSDragOperationMove;
- [View beginDraggingSessionWithItems:@[dragItem] event:nsevent
- source:CreateDraggingSource((NSDragOperation) op, cb, sourceHandle)];
+
+ [View beginDraggingSessionWithItems:draggingItems
+ event:nsevent
+ source:CreateDraggingSource((NSDragOperation) op, callback, sourceHandle)];
return S_OK;
}
diff --git a/native/Avalonia.Native/src/OSX/clipboard.h b/native/Avalonia.Native/src/OSX/clipboard.h
new file mode 100644
index 0000000000..fd0ddbf117
--- /dev/null
+++ b/native/Avalonia.Native/src/OSX/clipboard.h
@@ -0,0 +1,7 @@
+#pragma once
+
+#include "common.h"
+
+@interface WriteableClipboardItem : NSObject
+- (nonnull instancetype) initWithItem:(nonnull IAvnClipboardDataItem*)item source:(nonnull IAvnClipboardDataSource*)source;
+@end
diff --git a/native/Avalonia.Native/src/OSX/clipboard.mm b/native/Avalonia.Native/src/OSX/clipboard.mm
index 68f0e7d87a..45770f8c1e 100644
--- a/native/Avalonia.Native/src/OSX/clipboard.mm
+++ b/native/Avalonia.Native/src/OSX/clipboard.mm
@@ -1,206 +1,295 @@
+#import
#include "common.h"
+#include "clipboard.h"
#include "AvnString.h"
class Clipboard : public ComSingleObject
{
private:
- NSPasteboard* _pb;
- NSPasteboardItem* _item;
+ NSPasteboard* _pasteboard;
public:
FORWARD_IUNKNOWN()
- Clipboard(NSPasteboard* pasteboard, NSPasteboardItem* item)
+ Clipboard(NSPasteboard* pasteboard)
{
- if(pasteboard == nil && item == nil)
+ if (pasteboard == nil)
pasteboard = [NSPasteboard generalPasteboard];
- _pb = pasteboard;
- _item = item;
+ _pasteboard = pasteboard;
}
- NSPasteboardItem* TryGetItem()
+ virtual HRESULT GetFormats(int64_t changeCount, IAvnStringArray** ret) override
{
- return _item;
+ START_COM_ARP_CALL;
+
+ if (ret == nullptr)
+ return E_POINTER;
+
+ if (changeCount != [_pasteboard changeCount])
+ return COR_E_OBJECTDISPOSED;
+
+ auto types = [_pasteboard types];
+ *ret = types == nil ? nullptr : CreateAvnStringArray(types);
+ return S_OK;
}
-
- virtual HRESULT GetText (char* type, IAvnString**ppv) override
+
+ virtual HRESULT GetItemCount(int64_t changeCount, int* ret) override
{
- START_COM_CALL;
+ START_COM_ARP_CALL;
- @autoreleasepool
- {
- if(ppv == nullptr)
- {
- return E_POINTER;
- }
- NSString* typeString = [NSString stringWithUTF8String:(const char*)type];
- NSString* string = _item == nil ? [_pb stringForType:typeString] : [_item stringForType:typeString];
-
- *ppv = CreateAvnString(string);
-
- return S_OK;
- }
+ if (ret == nullptr)
+ return E_POINTER;
+
+ if (changeCount != [_pasteboard changeCount])
+ return COR_E_OBJECTDISPOSED;
+
+ auto items = [_pasteboard pasteboardItems];
+ *ret = items == nil ? 0 : (int)[items count];
+ return S_OK;
}
- virtual HRESULT SetStrings(char* type, IAvnStringArray*ppv) override
+ virtual HRESULT GetItemFormats(int index, int64_t changeCount, IAvnStringArray** ret) override
{
- START_COM_CALL;
+ START_COM_ARP_CALL;
- @autoreleasepool
- {
- NSArray* data = GetNSArrayOfStringsAndRelease(ppv);
- NSString* typeString = [NSString stringWithUTF8String:(const char*)type];
- if(_item == nil)
- [_pb setPropertyList: data forType: typeString];
- else
- [_item setPropertyList: data forType:typeString];
- return S_OK;
- }
+ if (ret == nullptr)
+ return E_POINTER;
+
+ if (changeCount != [_pasteboard changeCount])
+ return COR_E_OBJECTDISPOSED;
+
+ auto item = [[_pasteboard pasteboardItems] objectAtIndex:index];
+ auto types = [item types];
+ *ret = types == nil ? nullptr : CreateAvnStringArray(types);
+ return S_OK;
}
- virtual HRESULT GetStrings(char* type, IAvnStringArray**ppv) override
+ virtual HRESULT GetItemValueAsString(int index, int64_t changeCount, const char* format, IAvnString** ret) override
{
- START_COM_CALL;
+ START_COM_ARP_CALL;
- @autoreleasepool
- {
- *ppv= nil;
- NSString* typeString = [NSString stringWithUTF8String:(const char*)type];
- NSObject* data = _item == nil ? [_pb propertyListForType: typeString] : [_item propertyListForType: typeString];
- if(data == nil)
- return S_OK;
-
- if([data isKindOfClass: [NSString class]])
- {
- *ppv = CreateAvnStringArray((NSString*) data);
- return S_OK;
- }
-
- NSArray* arr = (NSArray*)data;
-
- for(int c = 0; c < [arr count]; c++)
- if(![[arr objectAtIndex:c] isKindOfClass:[NSString class]])
- return E_INVALIDARG;
-
- *ppv = CreateAvnStringArray(arr);
- return S_OK;
- }
+ if (ret == nullptr)
+ return E_POINTER;
+
+ if (changeCount != [_pasteboard changeCount])
+ return COR_E_OBJECTDISPOSED;
+
+ auto item = [[_pasteboard pasteboardItems] objectAtIndex:index];
+ auto value = [item stringForType:[NSString stringWithUTF8String:format]];
+ *ret = value == nil ? nullptr : CreateAvnString(value);
+ return S_OK;
}
- virtual HRESULT SetText (char* type, char* utf8String) override
+ virtual HRESULT GetItemValueAsBytes(int index, int64_t changeCount, const char* format, IAvnString** ret) override
{
- START_COM_CALL;
+ START_COM_ARP_CALL;
- @autoreleasepool
- {
- auto string = [NSString stringWithUTF8String:(const char*)utf8String];
- auto typeString = [NSString stringWithUTF8String:(const char*)type];
- if(_item == nil)
- [_pb setString: string forType: typeString];
- else
- [_item setString: string forType:typeString];
+ if (ret == nullptr)
+ return E_POINTER;
- return S_OK;
- }
+ if (changeCount != [_pasteboard changeCount])
+ return COR_E_OBJECTDISPOSED;
+
+ auto item = [[_pasteboard pasteboardItems] objectAtIndex:index];
+ auto value = [item dataForType:[NSString stringWithUTF8String:format]];
+
+ *ret = value == nil || [value length] == 0
+ ? nullptr
+ : CreateByteArray((void*)[value bytes], (int)[value length]);
+ return S_OK;
+ }
+
+ virtual HRESULT Clear(int64_t* ret) override
+ {
+ START_COM_ARP_CALL;
+
+ *ret = [_pasteboard clearContents];
+ return S_OK;
}
- virtual HRESULT SetBytes(char* type, void* bytes, int len) override
+ virtual HRESULT GetChangeCount(int64_t* ret) override
{
- START_COM_CALL;
+ START_COM_ARP_CALL;
- @autoreleasepool
+ *ret = [_pasteboard changeCount];
+ return S_OK;
+ }
+
+ virtual HRESULT SetData(IAvnClipboardDataSource* source) override
+ {
+ START_COM_ARP_CALL;
+
+ auto count = source->GetItemCount();
+ auto writeableItems = [NSMutableArray arrayWithCapacity:count];
+
+ for (auto i = 0; i < count; ++i)
{
- auto typeString = [NSString stringWithUTF8String:(const char*)type];
- auto data = [NSData dataWithBytes:bytes length:len];
- if(_item == nil)
- [_pb setData:data forType:typeString];
- else
- [_item setData:data forType:typeString];
- return S_OK;
+ auto item = source->GetItem(i);
+ auto writeableItem = [[WriteableClipboardItem alloc] initWithItem:item source:source];
+ [writeableItems addObject:writeableItem];
}
+
+ [_pasteboard writeObjects:writeableItems];
+ return S_OK;
}
-
- virtual HRESULT GetBytes(char* type, IAvnString**ppv) override
+
+ virtual bool IsTextFormat(const char *format) override
{
- START_COM_CALL;
+ START_COM_ARP_CALL;
+
+ auto formatString = [NSString stringWithUTF8String:format];
- @autoreleasepool
+ if (@available(macOS 11.0, *))
{
- *ppv = nil;
- auto typeString = [NSString stringWithUTF8String:(const char*)type];
- NSData*data;
- @try
- {
- if(_item)
- data = [_item dataForType:typeString];
- else
- data = [_pb dataForType:typeString];
- if(data == nil)
- return E_FAIL;
- }
- @catch(NSException* e)
- {
- return E_FAIL;
- }
- *ppv = CreateByteArray((void*)data.bytes, (int)data.length);
- return S_OK;
+ auto type = [UTType typeWithIdentifier:formatString];
+ return type != nil && [type conformsToType:UTTypeText];
+ }
+ else
+ {
+ return UTTypeConformsTo((__bridge CFStringRef)formatString, kUTTypeText);
}
}
+};
+
+extern IAvnClipboard* CreateClipboard(NSPasteboard* pb)
+{
+ return new Clipboard(pb);
+}
+
+
+@implementation WriteableClipboardItem
+{
+ IAvnClipboardDataItem* _item;
+ IAvnClipboardDataSource* _source;
+}
+
+- (nonnull WriteableClipboardItem*) initWithItem:(nonnull IAvnClipboardDataItem*)item source:(nonnull IAvnClipboardDataSource*)source
+{
+ self = [super init];
+ _item = item;
+ _source = source;
+
+ // Each item references its source so it doesn't get disposed too early.
+ source->AddRef();
+
+ return self;
+}
- virtual HRESULT Clear(int64_t* rv) override
+NSString* TryConvertFormatToUti(NSString* format)
+{
+ if (@available(macOS 11.0, *))
{
- START_COM_CALL;
-
- @autoreleasepool
+ auto type = [UTType typeWithIdentifier:format];
+ if (type == nil)
{
- if(_item != nil)
- {
- _item = [NSPasteboardItem new];
- return 0;
- }
+ if ([format containsString:@"/"])
+ type = [UTType typeWithMIMEType:format];
else
+ type = [UTType exportedTypeWithIdentifier:format];
+
+ if (type == nil)
{
- *rv = [_pb clearContents];
- [_pb setString:@"" forType:NSPasteboardTypeString];
+ // For now, we need to use the deprecated UTTypeCreatePreferredIdentifierForTag to create a dynamic UTI for arbitrary strings.
+ // This is only necessary because the old IDataObject can provide arbitrary types that aren't UTIs nor mime types.
+ // With the new DataFormat:
+ // - If the format is an application format, the managed side provides a UTI like net.avaloniaui.app.uti.xxx.
+ // - If the format is an OS format, the user has been warned that they MUST provide a name which is valid for the OS.
+ // TODO12: remove!
+ auto fromPasteboardType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassNSPboardType, (__bridge CFStringRef)format, nil);
+ if (fromPasteboardType != nil)
+ return (__bridge_transfer NSString*)fromPasteboardType;
}
-
- return S_OK;
}
+
+ return type == nil ? nil : [type identifier];
}
-
- virtual HRESULT GetChangeCount(int64_t* rv) override
+ else
{
- START_COM_CALL;
- if(_item == nil)
- {
- *rv = [_pb changeCount];
- return S_OK;
- }
- return E_NOTIMPL;
+ auto bridgedFormat = (__bridge CFStringRef)format;
+ if (UTTypeIsDeclared(bridgedFormat))
+ return format;
+
+ auto fromMimeType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, bridgedFormat, nil);
+ if (fromMimeType != nil)
+ return (__bridge_transfer NSString*)fromMimeType;
+
+ auto fromPasteboardType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassNSPboardType, bridgedFormat, nil);
+ if (fromPasteboardType != nil)
+ return (__bridge_transfer NSString*)fromPasteboardType;
+
+ return nil;
}
+}
+
+- (nonnull NSArray*) writableTypesForPasteboard:(nonnull NSPasteboard*)pasteboard
+{
+ auto formats = _item->ProvideFormats();
+ if (formats == nullptr)
+ return [NSArray array];
- virtual HRESULT ObtainFormats(IAvnStringArray** ppv) override
+ auto count = formats->GetCount();
+ if (count == 0)
+ return [NSArray array];
+
+ auto utis = [NSMutableArray arrayWithCapacity:count];
+ IAvnString* format;
+ for (auto i = 0; i < count; ++i)
{
- START_COM_CALL;
-
- @autoreleasepool
- {
- *ppv = CreateAvnStringArray(_item == nil ? [_pb types] : [_item types]);
- return S_OK;
- }
+ if (formats->Get(i, &format) != S_OK)
+ continue;
+
+ // Only UTIs must be returned from writableTypesForPasteboard or an exception will be thrown
+ auto formatString = GetNSStringAndRelease(format);
+ auto uti = TryConvertFormatToUti(formatString);
+ if (uti != nil)
+ [utis addObject:uti];
}
-};
+ formats->Release();
+
+ [utis addObject:GetAvnCustomDataType()];
+
+ return utis;
+}
-extern IAvnClipboard* CreateClipboard(NSPasteboard* pb, NSPasteboardItem* item)
+- (NSPasteboardWritingOptions) writingOptionsForType:(NSPasteboardType)type pasteboard:(NSPasteboard*)pasteboard
{
- return new Clipboard(pb, item);
+ return [type isEqualToString:NSPasteboardTypeString] || [type isEqualToString:GetAvnCustomDataType()]
+ ? 0
+ : NSPasteboardWritingPromised;
}
-extern NSPasteboardItem* TryGetPasteboardItem(IAvnClipboard*cb)
+- (nullable id) pasteboardPropertyListForType:(nonnull NSPasteboardType)type
{
- auto clipboard = dynamic_cast(cb);
- if(clipboard == nil)
+ if ([type isEqualToString:GetAvnCustomDataType()])
+ return @"";
+
+ ComPtr value(_item->GetValue([type UTF8String]), true);
+ if (value.getRaw() == nullptr)
return nil;
- return clipboard->TryGetItem();
+
+ if (value->IsString())
+ return GetNSStringAndRelease(value->AsString());
+
+ auto length = value->GetByteLength();
+ auto buffer = malloc(length);
+ value->CopyBytesTo(buffer);
+ return [NSData dataWithBytesNoCopy:buffer length:length];
+}
+
+- (void) dealloc
+{
+ if (_item != nullptr)
+ {
+ _item->Release();
+ _item = nullptr;
+ }
+
+ if (_source != nullptr)
+ {
+ _source->Release();
+ _source = nullptr;
+ }
}
+
+@end
diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h
index 36c157704d..819254b841 100644
--- a/native/Avalonia.Native/src/OSX/common.h
+++ b/native/Avalonia.Native/src/OSX/common.h
@@ -16,8 +16,7 @@ extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events);
extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events);
extern IAvnStorageProvider* CreateStorageProvider();
extern IAvnScreens* CreateScreens(IAvnScreenEvents* cb);
-extern IAvnClipboard* CreateClipboard(NSPasteboard*, NSPasteboardItem*);
-extern NSPasteboardItem* TryGetPasteboardItem(IAvnClipboard*);
+extern IAvnClipboard* CreateClipboard(NSPasteboard* pb);
extern NSObject* CreateDraggingSource(NSDragOperation op, IAvnDndResultCallback* cb, void* handle);
extern void* GetAvnDataObjectHandleFromDraggingInfo(NSObject* info);
extern NSString* GetAvnCustomDataType();
diff --git a/native/Avalonia.Native/src/OSX/dnd.mm b/native/Avalonia.Native/src/OSX/dnd.mm
index 531bdcccfd..aebe4afb88 100644
--- a/native/Avalonia.Native/src/OSX/dnd.mm
+++ b/native/Avalonia.Native/src/OSX/dnd.mm
@@ -14,9 +14,17 @@ extern AvnDragDropEffects ConvertDragDropEffects(NSDragOperation nsop)
extern NSString* GetAvnCustomDataType()
{
- char buffer[256];
- sprintf(buffer, "net.avaloniaui.inproc.uti.n%in", getpid());
- return [NSString stringWithUTF8String:buffer];
+ static NSString* result = nil;
+
+ if (result == nil)
+ {
+ const size_t bufferSize = 256;
+ char buffer[bufferSize];
+ snprintf(buffer, bufferSize, "net.avaloniaui.inproc.uti.n%in", getpid());
+ result = [NSString stringWithUTF8String:buffer];
+ }
+
+ return result;
}
@interface AvnDndSource : NSObject
diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm
index 0e3621517e..f4e244efdd 100644
--- a/native/Avalonia.Native/src/OSX/main.mm
+++ b/native/Avalonia.Native/src/OSX/main.mm
@@ -312,18 +312,7 @@ public:
@autoreleasepool
{
- *ppv = ::CreateClipboard (nil, nil);
- return S_OK;
- }
- }
-
- virtual HRESULT CreateDndClipboard(IAvnClipboard** ppv) override
- {
- START_COM_CALL;
-
- @autoreleasepool
- {
- *ppv = ::CreateClipboard (nil, [NSPasteboardItem new]);
+ *ppv = ::CreateClipboard(nil);
return S_OK;
}
}
diff --git a/samples/ControlCatalog/Pages/ClipboardPage.xaml b/samples/ControlCatalog/Pages/ClipboardPage.xaml
index 0349936733..2c22348f6f 100644
--- a/samples/ControlCatalog/Pages/ClipboardPage.xaml
+++ b/samples/ControlCatalog/Pages/ClipboardPage.xaml
@@ -6,12 +6,10 @@
-
-
-
-
-
-
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs b/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs
index f471647c15..2fc8cc6616 100644
--- a/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ClipboardPage.xaml.cs
@@ -10,21 +10,22 @@ using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
-using Avalonia.Platform;
using Avalonia.Platform.Storage;
-using Avalonia.Platform.Storage.FileIO;
using Avalonia.Threading;
namespace ControlCatalog.Pages
{
public partial class ClipboardPage : UserControl
{
+ private readonly DataFormat _customBinaryDataFormat =
+ DataFormat.CreateBytesApplicationFormat("controlcatalog-binary-data");
+
private INotificationManager? _notificationManager;
private INotificationManager NotificationManager => _notificationManager
??= new WindowNotificationManager(TopLevel.GetTopLevel(this)!);
private readonly DispatcherTimer _clipboardLastDataObjectChecker;
- private DataObject? _storedDataObject;
+ private DataTransfer? _storedDataTransfer;
public ClipboardPage()
{
_clipboardLastDataObjectChecker =
@@ -40,38 +41,20 @@ namespace ControlCatalog.Pages
}
private async void CopyText(object? sender, RoutedEventArgs args)
- {
- if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard && ClipboardContent is { } clipboardContent)
- await clipboard.SetTextAsync(clipboardContent.Text ?? String.Empty);
- }
-
- private async void PasteText(object? sender, RoutedEventArgs args)
{
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
- {
- ClipboardContent.Text = await clipboard.GetTextAsync();
- }
+ await clipboard.SetTextAsync(ClipboardContent.Text ?? string.Empty);
}
- private async void CopyTextDataObject(object? sender, RoutedEventArgs args)
- {
- if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
- {
- var dataObject = _storedDataObject = new DataObject();
- dataObject.Set(DataFormats.Text, ClipboardContent.Text ?? string.Empty);
- await clipboard.SetDataObjectAsync(dataObject);
- }
- }
-
- private async void PasteTextDataObject(object? sender, RoutedEventArgs args)
+ private async void PasteText(object? sender, RoutedEventArgs args)
{
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
- ClipboardContent.Text = await clipboard.GetDataAsync(DataFormats.Text) as string ?? string.Empty;
+ ClipboardContent.Text = await clipboard.TryGetTextAsync();
}
}
- private async void CopyFilesDataObject(object? sender, RoutedEventArgs args)
+ private async void CopyFiles(object? sender, RoutedEventArgs args)
{
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
@@ -105,10 +88,11 @@ namespace ControlCatalog.Pages
if (files.Count > 0)
{
- var dataObject = _storedDataObject = new DataObject();
- dataObject.Set(DataFormats.Files, files);
- await clipboard.SetDataObjectAsync(dataObject);
- NotificationManager.Show(new Notification("Success", "Copy completated.", NotificationType.Success));
+ var dataTransfer = _storedDataTransfer = new DataTransfer();
+ foreach (var file in files)
+ dataTransfer.Add(DataTransferItem.Create(DataFormat.File, file));
+ await clipboard.SetDataAsync(dataTransfer);
+ NotificationManager.Show(new Notification("Success", "Copy completed.", NotificationType.Success));
}
else
{
@@ -117,11 +101,11 @@ namespace ControlCatalog.Pages
}
}
- private async void PasteFilesDataObject(object? sender, RoutedEventArgs args)
+ private async void PasteFiles(object? sender, RoutedEventArgs args)
{
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
- var files = await clipboard.GetDataAsync(DataFormats.Files) as IEnumerable;
+ var files = await clipboard.TryGetFilesAsync();
ClipboardContent.Text = files != null ? string.Join(Environment.NewLine, files.Select(f => f.TryGetLocalPath() ?? f.Name)) : string.Empty;
}
@@ -131,11 +115,32 @@ namespace ControlCatalog.Pages
{
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
- var formats = await clipboard.GetFormatsAsync();
+ var formats = await clipboard.GetDataFormatsAsync();
ClipboardContent.Text = string.Join(Environment.NewLine, formats);
}
}
+ private async void CopyBinaryData(object? sender, RoutedEventArgs args)
+ {
+ if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
+ {
+ var dataTransfer = _storedDataTransfer = new DataTransfer();
+ var bytes = new byte[10 * 1024 * 1024];
+ new Random().NextBytes(bytes);
+ dataTransfer.Add(DataTransferItem.Create(_customBinaryDataFormat, bytes));
+ await clipboard.SetDataAsync(dataTransfer);
+ }
+ }
+
+ private async void PasteBinaryData(object? sender, RoutedEventArgs args)
+ {
+ if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
+ {
+ var bytes = await clipboard.TryGetValueAsync(_customBinaryDataFormat);
+ ClipboardContent.Text = bytes is null ? "" : $"{bytes.Length} bytes";
+ }
+ }
+
private async void Clear(object sender, RoutedEventArgs args)
{
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
@@ -159,22 +164,28 @@ namespace ControlCatalog.Pages
}
private Run OwnsClipboardDataObject => this.Get("OwnsClipboardDataObject");
- private bool _checkingClipboardDataObject;
+ private bool _checkingClipboardDataTransfer;
private async void CheckLastDataObject(object? sender, EventArgs e)
{
- if(_checkingClipboardDataObject)
+ if(_checkingClipboardDataTransfer)
return;
try
{
- _checkingClipboardDataObject = true;
- var task = TopLevel.GetTopLevel(this)?.Clipboard?.TryGetInProcessDataObjectAsync();
- var owns = task != null && (await task) == _storedDataObject && _storedDataObject != null;
+ _checkingClipboardDataTransfer = true;
+
+ var owns = false;
+ if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
+ {
+ var dataTransfer = await clipboard.TryGetInProcessDataAsync();
+ owns = dataTransfer == _storedDataTransfer && dataTransfer is not null;
+ }
+
OwnsClipboardDataObject.Text = owns ? "Yes" : "No";
OwnsClipboardDataObject.Foreground = owns ? Brushes.Green : Brushes.Red;
}
finally
{
- _checkingClipboardDataObject = false;
+ _checkingClipboardDataTransfer = false;
}
}
}
diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
index b242d020b0..791f075d43 100644
--- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
@@ -1,9 +1,11 @@
using System;
using System.Linq;
using System.Reflection;
+using System.Text;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Input;
+using Avalonia.Input.Platform;
using Avalonia.Markup.Xaml;
using Avalonia.Platform.Storage;
@@ -12,7 +14,10 @@ namespace ControlCatalog.Pages
public class DragAndDropPage : UserControl
{
private readonly TextBlock _dropState;
- private const string CustomFormat = "application/xxx-avalonia-controlcatalog-custom";
+
+ private readonly DataFormat _customFormat =
+ DataFormat.CreateStringApplicationFormat("xxx-avalonia-controlcatalog-custom");
+
public DragAndDropPage()
{
InitializeComponent();
@@ -22,13 +27,13 @@ namespace ControlCatalog.Pages
SetupDnd(
"Text",
- d => d.Set(DataFormats.Text, $"Text was dragged {++textCount} times"),
+ d => d.Add(DataTransferItem.Create(DataFormat.Text, $"Text was dragged {++textCount} times")),
DragDropEffects.Copy | DragDropEffects.Move | DragDropEffects.Link);
SetupDnd(
"Custom",
- d => d.Set(CustomFormat, "Test123"),
- DragDropEffects.Move);
+ d => d.Add(DataTransferItem.Create(_customFormat, "Test123")),
+ DragDropEffects.Copy | DragDropEffects.Move);
SetupDnd(
"Files",
@@ -38,13 +43,13 @@ namespace ControlCatalog.Pages
TopLevel.GetTopLevel(this) is { } topLevel &&
await topLevel.StorageProvider.TryGetFileFromPathAsync(name) is { } storageFile)
{
- d.Set(DataFormats.Files, new[] { storageFile });
+ d.Add(DataTransferItem.Create(DataFormat.File, storageFile));
}
},
DragDropEffects.Copy);
}
- private void SetupDnd(string suffix, Action factory, DragDropEffects effects) =>
+ private void SetupDnd(string suffix, Action factory, DragDropEffects effects) =>
SetupDnd(
suffix,
o =>
@@ -54,17 +59,17 @@ namespace ControlCatalog.Pages
},
effects);
- private void SetupDnd(string suffix, Func factory, DragDropEffects effects)
+ private void SetupDnd(string suffix, Func factory, DragDropEffects effects)
{
var dragMe = this.Get("DragMe" + suffix);
var dragState = this.Get("DragState" + suffix);
async void DoDrag(object? sender, PointerPressedEventArgs e)
{
- var dragData = new DataObject();
+ var dragData = new DataTransfer();
await factory(dragData);
- var result = await DragDrop.DoDragDrop(e, dragData, effects);
+ var result = await DragDrop.DoDragDropAsync(e, dragData, effects);
switch (result)
{
case DragDropEffects.Move:
@@ -97,9 +102,9 @@ namespace ControlCatalog.Pages
}
// Only allow if the dragged data contains text or filenames.
- if (!e.Data.Contains(DataFormats.Text)
- && !e.Data.Contains(DataFormats.Files)
- && !e.Data.Contains(CustomFormat))
+ if (!e.DataTransfer.Contains(DataFormat.Text)
+ && !e.DataTransfer.Contains(DataFormat.File)
+ && !e.DataTransfer.Contains(_customFormat))
e.DragEffects = DragDropEffects.None;
}
@@ -114,13 +119,13 @@ namespace ControlCatalog.Pages
e.DragEffects = e.DragEffects & (DragDropEffects.Copy);
}
- if (e.Data.Contains(DataFormats.Text))
+ if (e.DataTransfer.Contains(DataFormat.Text))
{
- _dropState.Text = e.Data.GetText();
+ _dropState.Text = e.DataTransfer.TryGetText();
}
- else if (e.Data.Contains(DataFormats.Files))
+ else if (e.DataTransfer.Contains(DataFormat.File))
{
- var files = e.Data.GetFiles() ?? Array.Empty();
+ var files = e.DataTransfer.TryGetFiles() ?? [];
var contentStr = "";
foreach (var item in files)
@@ -143,16 +148,9 @@ namespace ControlCatalog.Pages
_dropState.Text = contentStr;
}
-#pragma warning disable CS0618 // Type or member is obsolete
- else if (e.Data.Contains(DataFormats.FileNames))
- {
- var files = e.Data.GetFileNames();
- _dropState.Text = string.Join(Environment.NewLine, files ?? Array.Empty());
- }
-#pragma warning restore CS0618 // Type or member is obsolete
- else if (e.Data.Contains(CustomFormat))
+ else if (e.DataTransfer.Contains(_customFormat))
{
- _dropState.Text = "Custom: " + e.Data.Get(CustomFormat);
+ _dropState.Text = "Custom: " + e.DataTransfer.TryGetValue(_customFormat);
}
}
diff --git a/src/Android/Avalonia.Android/Platform/AndroidDataFormatHelper.cs b/src/Android/Avalonia.Android/Platform/AndroidDataFormatHelper.cs
new file mode 100644
index 0000000000..686234aaef
--- /dev/null
+++ b/src/Android/Avalonia.Android/Platform/AndroidDataFormatHelper.cs
@@ -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(mimeType, AppPrefix);
+
+ return DataFormat.FromSystemName(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);
+ }
+
+}
diff --git a/src/Android/Avalonia.Android/Platform/ClipDataItemToDataTransferItemWrapper.cs b/src/Android/Avalonia.Android/Platform/ClipDataItemToDataTransferItemWrapper.cs
new file mode 100644
index 0000000000..479e5e2001
--- /dev/null
+++ b/src/Android/Avalonia.Android/Platform/ClipDataItemToDataTransferItemWrapper.cs
@@ -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;
+
+///
+/// Wraps a into a .
+///
+/// The clip data item.
+/// The data transfer owning this item.
+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)
+ 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;
+ }
+}
diff --git a/src/Android/Avalonia.Android/Platform/ClipDataToDataTransferWrapper.cs b/src/Android/Avalonia.Android/Platform/ClipDataToDataTransferWrapper.cs
new file mode 100644
index 0000000000..8bd9079142
--- /dev/null
+++ b/src/Android/Avalonia.Android/Platform/ClipDataToDataTransferWrapper.cs
@@ -0,0 +1,46 @@
+using Android.Content;
+using Avalonia.Input;
+using Avalonia.Input.Platform;
+
+namespace Avalonia.Android.Platform;
+
+///
+/// Wraps a into a .
+///
+/// The clip data.
+/// The application context.
+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()
+ {
+ }
+}
diff --git a/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs
index 7012ad1878..4dc7790209 100644
--- a/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs
+++ b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs
@@ -1,65 +1,145 @@
using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Threading.Tasks;
using Android.Content;
using Avalonia.Input;
using Avalonia.Input.Platform;
+using Avalonia.Logging;
+using AndroidUri = Android.Net.Uri;
namespace Avalonia.Android.Platform
{
- internal class ClipboardImpl : IClipboard
+ internal sealed class ClipboardImpl(ClipboardManager? clipboardManager, Context? context)
+ : IClipboardImpl
{
- private readonly ClipboardManager? _clipboardManager;
+ private readonly ClipboardManager? _clipboardManager = clipboardManager;
+ private readonly Context? _context = context;
- internal ClipboardImpl(ClipboardManager? value)
+ public Task TryGetDataAsync()
{
- _clipboardManager = value;
+ try
+ {
+ return Task.FromResult(TryGetData());
+ }
+ catch (Exception ex)
+ {
+ return Task.FromException(ex);
+ }
}
- public Task 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? 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(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 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 stringFormat)
+ {
+ var stringValue = await item.TryGetValueAsync(stringFormat);
+ if (stringValue is null)
+ continue;
+
+ return new ClipData.Item(stringValue);
+ }
}
- var clip = ClipData.NewPlainText("text", text);
- _clipboardManager.PrimaryClip = clip;
+ if (hasFormats)
+ {
+ Logger.TryGet(LogEventLevel.Warning, LogArea.AndroidPlatform)?.Log(
+ this,
+ "No compatible value found for data transfer item with formats {Formats}",
+ string.Join(", ", item.Formats));
+ }
- return Task.CompletedTask;
+ return null;
}
public Task ClearAsync()
{
- if (_clipboardManager == null)
+ try
{
+ Clear();
return Task.CompletedTask;
}
-
- _clipboardManager.PrimaryClip = null;
-
- return Task.CompletedTask;
+ catch (Exception ex)
+ {
+ return Task.FromException(ex);
+ }
}
- public Task SetDataObjectAsync(IDataObject data) => throw new PlatformNotSupportedException();
-
- public Task GetFormatsAsync() => throw new PlatformNotSupportedException();
-
- public Task