Browse Source

Clipboard rework (#19347)

* New clipboard with IDataTransfer

* Move legacy format handling to DataObject wrappers

* macOS clipboard rework

* Browser clipboard rework

* Android clipboard rework

* iOS new clipboard

* X11 clipboard rework

* Simplify IDataTransfer API

* Simplify IClipboardImpl API

* Add DataFormat documentation

* Make DataFormat.SystemName platform specific

* Fix clipboard/DnD samples

* Fix native clipboard UTI conversion

* Adjust IDataTransfer namespaces

* Add Obsolete attributes to IDataObject related methods

* Better API for DataTransferItem

* Tizen clipboard rework

* Add missing clipboard extension methods

* Split IDataTransferItem into IAsyncDataTransferItem and ISyncDataTransferItem

* Rename back ISyncDataTransfer to IDataTransfer

* Added IClipboard API suppressions

* Make IPlatformDragSource NotClientImplementable

* Rename DataFormatKinds

* Added DataTransferItem.CreateText/File

* Implemented typed DataFormat<T>

* Fix X11 text/uri-list encoding

* Add API suppressions

* Adjust ClipboardUriListHelper stream ownership

* Fix legacy clipboard BinaryFormatter deserialization

* Fix macOS build

(cherry picked from commit 1f404646fa)
release/11.3.7
Julien Lebosquain 4 months ago
parent
commit
f94d48cd4f
No known key found for this signature in database GPG Key ID: 1833CAD10ACC46FD
  1. 3
      Avalonia.sln
  2. 76
      api/Avalonia.nupkg.xml
  3. 3
      native/Avalonia.Native/inc/com.h
  4. 4
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  5. 2
      native/Avalonia.Native/src/OSX/AvnView.mm
  6. 29
      native/Avalonia.Native/src/OSX/StorageProvider.mm
  7. 9
      native/Avalonia.Native/src/OSX/TopLevelImpl.h
  8. 33
      native/Avalonia.Native/src/OSX/TopLevelImpl.mm
  9. 7
      native/Avalonia.Native/src/OSX/clipboard.h
  10. 373
      native/Avalonia.Native/src/OSX/clipboard.mm
  11. 3
      native/Avalonia.Native/src/OSX/common.h
  12. 14
      native/Avalonia.Native/src/OSX/dnd.mm
  13. 13
      native/Avalonia.Native/src/OSX/main.mm
  14. 10
      samples/ControlCatalog/Pages/ClipboardPage.xaml
  15. 87
      samples/ControlCatalog/Pages/ClipboardPage.xaml.cs
  16. 48
      samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
  17. 36
      src/Android/Avalonia.Android/Platform/AndroidDataFormatHelper.cs
  18. 58
      src/Android/Avalonia.Android/Platform/ClipDataItemToDataTransferItemWrapper.cs
  19. 46
      src/Android/Avalonia.Android/Platform/ClipDataToDataTransferWrapper.cs
  20. 138
      src/Android/Avalonia.Android/Platform/ClipboardImpl.cs
  21. 16
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  22. 6
      src/Avalonia.Base/Avalonia.Base.csproj
  23. 139
      src/Avalonia.Base/Input/AsyncDataTransferExtensions.cs
  24. 58
      src/Avalonia.Base/Input/AsyncDataTransferItemExtensions.cs
  25. 45
      src/Avalonia.Base/Input/AsyncToSyncDataTransfer.cs
  26. 24
      src/Avalonia.Base/Input/AsyncToSyncDataTransferItem.cs
  27. 227
      src/Avalonia.Base/Input/DataFormat.cs
  28. 46
      src/Avalonia.Base/Input/DataFormatKind.cs
  29. 24
      src/Avalonia.Base/Input/DataFormatOfT.cs
  30. 32
      src/Avalonia.Base/Input/DataFormats.cs
  31. 4
      src/Avalonia.Base/Input/DataObject.cs
  32. 3
      src/Avalonia.Base/Input/DataObjectExtensions.cs
  33. 59
      src/Avalonia.Base/Input/DataTransfer.cs
  34. 130
      src/Avalonia.Base/Input/DataTransferExtensions.cs
  35. 222
      src/Avalonia.Base/Input/DataTransferItem.cs
  36. 57
      src/Avalonia.Base/Input/DataTransferItemExtensions.cs
  37. 22
      src/Avalonia.Base/Input/DragDrop.cs
  38. 30
      src/Avalonia.Base/Input/DragDropDevice.cs
  39. 28
      src/Avalonia.Base/Input/DragEventArgs.cs
  40. 47
      src/Avalonia.Base/Input/IAsyncDataTransfer.cs
  41. 35
      src/Avalonia.Base/Input/IAsyncDataTransferItem.cs
  42. 6
      src/Avalonia.Base/Input/IDataObject.cs
  43. 32
      src/Avalonia.Base/Input/IDataTransfer.cs
  44. 34
      src/Avalonia.Base/Input/IDataTransferItem.cs
  45. 72
      src/Avalonia.Base/Input/Platform/BinaryFormatterHelper.cs
  46. 103
      src/Avalonia.Base/Input/Platform/Clipboard.cs
  47. 189
      src/Avalonia.Base/Input/Platform/ClipboardExtensions.cs
  48. 79
      src/Avalonia.Base/Input/Platform/DataObjectToDataTransferItemWrapper.cs
  49. 84
      src/Avalonia.Base/Input/Platform/DataObjectToDataTransferWrapper.cs
  50. 44
      src/Avalonia.Base/Input/Platform/DataTransferToDataObjectWrapper.cs
  51. 79
      src/Avalonia.Base/Input/Platform/IClipboard.cs
  52. 20
      src/Avalonia.Base/Input/Platform/IClipboardImpl.cs
  53. 14
      src/Avalonia.Base/Input/Platform/IFlushableClipboardImpl.cs
  54. 16
      src/Avalonia.Base/Input/Platform/IOwnedClipboardImpl.cs
  55. 16
      src/Avalonia.Base/Input/Platform/IPlatformDragSource.cs
  56. 37
      src/Avalonia.Base/Input/Platform/PlatformAsyncDataTransfer.cs
  57. 30
      src/Avalonia.Base/Input/Platform/PlatformAsyncDataTransferItem.cs
  58. 43
      src/Avalonia.Base/Input/Platform/PlatformDataTransfer.cs
  59. 63
      src/Avalonia.Base/Input/Platform/PlatformDataTransferItem.cs
  60. 39
      src/Avalonia.Base/Input/Raw/RawDragEvent.cs
  61. 2
      src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
  62. 34
      src/Avalonia.Base/Utilities/ThrowHelper.cs
  63. 3
      src/Avalonia.Controls/MaskedTextBox.cs
  64. 19
      src/Avalonia.Controls/Platform/InProcessDragSource.cs
  65. 2
      src/Avalonia.Controls/TextBox.cs
  66. 82
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  67. 15
      src/Avalonia.Diagnostics/Diagnostics/Constants.cs
  68. 21
      src/Avalonia.Diagnostics/Diagnostics/DevToolsDataFormats.cs
  69. 13
      src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs
  70. 25
      src/Avalonia.Native/AvaloniaNativeDragSource.cs
  71. 6
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  72. 55
      src/Avalonia.Native/ClipboardDataFormatHelper.cs
  73. 46
      src/Avalonia.Native/ClipboardDataTransfer.cs
  74. 94
      src/Avalonia.Native/ClipboardDataTransferItem.cs
  75. 234
      src/Avalonia.Native/ClipboardImpl.cs
  76. 107
      src/Avalonia.Native/ClipboardReadSession.cs
  77. 96
      src/Avalonia.Native/DataTransferItemToAvnClipboardDataItemWrapper.cs
  78. 56
      src/Avalonia.Native/DataTransferToAvnClipboardDataSourceWrapper.cs
  79. 47
      src/Avalonia.Native/NativeOwned.cs
  80. 8
      src/Avalonia.Native/StorageProviderApi.cs
  81. 44
      src/Avalonia.Native/TopLevelImpl.cs
  82. 57
      src/Avalonia.Native/avn.idl
  83. 92
      src/Avalonia.X11/Clipboard/ClipboardDataFormatHelper.cs
  84. 73
      src/Avalonia.X11/Clipboard/ClipboardDataReader.cs
  85. 34
      src/Avalonia.X11/Clipboard/ClipboardDataTransfer.cs
  86. 23
      src/Avalonia.X11/Clipboard/ClipboardDataTransferItem.cs
  87. 1
      src/Avalonia.X11/Clipboard/ClipboardReadSession.cs
  88. 52
      src/Avalonia.X11/Clipboard/ClipboardUriListHelper.cs
  89. 2
      src/Avalonia.X11/Clipboard/EventStreamWindow.cs
  90. 337
      src/Avalonia.X11/Clipboard/X11Clipboard.cs
  91. 7
      src/Avalonia.X11/X11Platform.cs
  92. 40
      src/Browser/Avalonia.Browser/BrowserClipboardDataTransfer.cs
  93. 32
      src/Browser/Avalonia.Browser/BrowserClipboardDataTransferItem.cs
  94. 34
      src/Browser/Avalonia.Browser/BrowserDataFormatHelper.cs
  95. 91
      src/Browser/Avalonia.Browser/BrowserDataObject.cs
  96. 62
      src/Browser/Avalonia.Browser/BrowserDataTransferHelper.cs
  97. 40
      src/Browser/Avalonia.Browser/BrowserDragDataTransfer.cs
  98. 31
      src/Browser/Avalonia.Browser/BrowserDragDataTransferItem.cs
  99. 13
      src/Browser/Avalonia.Browser/BrowserInputHandler.cs
  100. 4
      src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs

3
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}"

76
api/Avalonia.nupkg.xml

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/en-us/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>
@ -133,4 +133,76 @@
<Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
<Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
</Suppression>
</Suppressions>
<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>

3
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
{

4
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 = "<group>"; };
F10084832BFF1F9E0024303E /* TopLevelImpl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TopLevelImpl.h; sourceTree = "<group>"; };
F10084852BFF1FB40024303E /* TopLevelImpl.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = TopLevelImpl.mm; sourceTree = "<group>"; };
F931F8672E2D43A4004E081E /* clipboard.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = clipboard.h; sourceTree = "<group>"; };
/* 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 */,

2
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<NSDragOperation>(0);

29
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.

9
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();

33
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<NSDraggingItem*> 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;
}

7
native/Avalonia.Native/src/OSX/clipboard.h

@ -0,0 +1,7 @@
#pragma once
#include "common.h"
@interface WriteableClipboardItem : NSObject <NSPasteboardWriting>
- (nonnull instancetype) initWithItem:(nonnull IAvnClipboardDataItem*)item source:(nonnull IAvnClipboardDataSource*)source;
@end

373
native/Avalonia.Native/src/OSX/clipboard.mm

@ -1,206 +1,295 @@
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#include "common.h"
#include "clipboard.h"
#include "AvnString.h"
class Clipboard : public ComSingleObject<IAvnClipboard, &IID_IAvnClipboard>
{
private:
NSPasteboard* _pb;
NSPasteboardItem* _item;
NSPasteboard* _pasteboard;
public:
FORWARD_IUNKNOWN()
Clipboard(NSPasteboard* pasteboard, NSPasteboardItem* item)
Clipboard(NSPasteboard* pasteboard)
{
if(pasteboard == nil && item == nil)
if (pasteboard == nil)
pasteboard = [NSPasteboard generalPasteboard];
_pb = pasteboard;
_item = item;
_pasteboard = pasteboard;
}
NSPasteboardItem* TryGetItem()
virtual HRESULT GetFormats(int64_t changeCount, IAvnStringArray** ret) override
{
return _item;
START_COM_ARP_CALL;
if (ret == nullptr)
return E_POINTER;
if (changeCount != [_pasteboard changeCount])
return COR_E_OBJECTDISPOSED;
auto types = [_pasteboard types];
*ret = types == nil ? nullptr : CreateAvnStringArray(types);
return S_OK;
}
virtual HRESULT GetText (char* type, IAvnString**ppv) override
virtual HRESULT GetItemCount(int64_t changeCount, int* ret) override
{
START_COM_CALL;
START_COM_ARP_CALL;
@autoreleasepool
{
if(ppv == nullptr)
{
return E_POINTER;
}
NSString* typeString = [NSString stringWithUTF8String:(const char*)type];
NSString* string = _item == nil ? [_pb stringForType:typeString] : [_item stringForType:typeString];
*ppv = CreateAvnString(string);
return S_OK;
}
if (ret == nullptr)
return E_POINTER;
if (changeCount != [_pasteboard changeCount])
return COR_E_OBJECTDISPOSED;
auto items = [_pasteboard pasteboardItems];
*ret = items == nil ? 0 : (int)[items count];
return S_OK;
}
virtual HRESULT SetStrings(char* type, IAvnStringArray*ppv) override
virtual HRESULT GetItemFormats(int index, int64_t changeCount, IAvnStringArray** ret) override
{
START_COM_CALL;
START_COM_ARP_CALL;
@autoreleasepool
{
NSArray<NSString*>* data = GetNSArrayOfStringsAndRelease(ppv);
NSString* typeString = [NSString stringWithUTF8String:(const char*)type];
if(_item == nil)
[_pb setPropertyList: data forType: typeString];
else
[_item setPropertyList: data forType:typeString];
return S_OK;
}
if (ret == nullptr)
return E_POINTER;
if (changeCount != [_pasteboard changeCount])
return COR_E_OBJECTDISPOSED;
auto item = [[_pasteboard pasteboardItems] objectAtIndex:index];
auto types = [item types];
*ret = types == nil ? nullptr : CreateAvnStringArray(types);
return S_OK;
}
virtual HRESULT GetStrings(char* type, IAvnStringArray**ppv) override
virtual HRESULT GetItemValueAsString(int index, int64_t changeCount, const char* format, IAvnString** ret) override
{
START_COM_CALL;
START_COM_ARP_CALL;
@autoreleasepool
{
*ppv= nil;
NSString* typeString = [NSString stringWithUTF8String:(const char*)type];
NSObject* data = _item == nil ? [_pb propertyListForType: typeString] : [_item propertyListForType: typeString];
if(data == nil)
return S_OK;
if([data isKindOfClass: [NSString class]])
{
*ppv = CreateAvnStringArray((NSString*) data);
return S_OK;
}
NSArray<NSString*>* arr = (NSArray*)data;
for(int c = 0; c < [arr count]; c++)
if(![[arr objectAtIndex:c] isKindOfClass:[NSString class]])
return E_INVALIDARG;
*ppv = CreateAvnStringArray(arr);
return S_OK;
}
if (ret == nullptr)
return E_POINTER;
if (changeCount != [_pasteboard changeCount])
return COR_E_OBJECTDISPOSED;
auto item = [[_pasteboard pasteboardItems] objectAtIndex:index];
auto value = [item stringForType:[NSString stringWithUTF8String:format]];
*ret = value == nil ? nullptr : CreateAvnString(value);
return S_OK;
}
virtual HRESULT SetText (char* type, char* utf8String) override
virtual HRESULT GetItemValueAsBytes(int index, int64_t changeCount, const char* format, IAvnString** ret) override
{
START_COM_CALL;
START_COM_ARP_CALL;
@autoreleasepool
{
auto string = [NSString stringWithUTF8String:(const char*)utf8String];
auto typeString = [NSString stringWithUTF8String:(const char*)type];
if(_item == nil)
[_pb setString: string forType: typeString];
else
[_item setString: string forType:typeString];
if (ret == nullptr)
return E_POINTER;
return S_OK;
}
if (changeCount != [_pasteboard changeCount])
return COR_E_OBJECTDISPOSED;
auto item = [[_pasteboard pasteboardItems] objectAtIndex:index];
auto value = [item dataForType:[NSString stringWithUTF8String:format]];
*ret = value == nil || [value length] == 0
? nullptr
: CreateByteArray((void*)[value bytes], (int)[value length]);
return S_OK;
}
virtual HRESULT Clear(int64_t* ret) override
{
START_COM_ARP_CALL;
*ret = [_pasteboard clearContents];
return S_OK;
}
virtual HRESULT SetBytes(char* type, void* bytes, int len) override
virtual HRESULT GetChangeCount(int64_t* ret) override
{
START_COM_CALL;
START_COM_ARP_CALL;
@autoreleasepool
*ret = [_pasteboard changeCount];
return S_OK;
}
virtual HRESULT SetData(IAvnClipboardDataSource* source) override
{
START_COM_ARP_CALL;
auto count = source->GetItemCount();
auto writeableItems = [NSMutableArray<WriteableClipboardItem*> arrayWithCapacity:count];
for (auto i = 0; i < count; ++i)
{
auto typeString = [NSString stringWithUTF8String:(const char*)type];
auto data = [NSData dataWithBytes:bytes length:len];
if(_item == nil)
[_pb setData:data forType:typeString];
else
[_item setData:data forType:typeString];
return S_OK;
auto item = source->GetItem(i);
auto writeableItem = [[WriteableClipboardItem alloc] initWithItem:item source:source];
[writeableItems addObject:writeableItem];
}
[_pasteboard writeObjects:writeableItems];
return S_OK;
}
virtual HRESULT GetBytes(char* type, IAvnString**ppv) override
virtual bool IsTextFormat(const char *format) override
{
START_COM_CALL;
START_COM_ARP_CALL;
auto formatString = [NSString stringWithUTF8String:format];
@autoreleasepool
if (@available(macOS 11.0, *))
{
*ppv = nil;
auto typeString = [NSString stringWithUTF8String:(const char*)type];
NSData*data;
@try
{
if(_item)
data = [_item dataForType:typeString];
else
data = [_pb dataForType:typeString];
if(data == nil)
return E_FAIL;
}
@catch(NSException* e)
{
return E_FAIL;
}
*ppv = CreateByteArray((void*)data.bytes, (int)data.length);
return S_OK;
auto type = [UTType typeWithIdentifier:formatString];
return type != nil && [type conformsToType:UTTypeText];
}
else
{
return UTTypeConformsTo((__bridge CFStringRef)formatString, kUTTypeText);
}
}
};
extern IAvnClipboard* CreateClipboard(NSPasteboard* pb)
{
return new Clipboard(pb);
}
@implementation WriteableClipboardItem
{
IAvnClipboardDataItem* _item;
IAvnClipboardDataSource* _source;
}
- (nonnull WriteableClipboardItem*) initWithItem:(nonnull IAvnClipboardDataItem*)item source:(nonnull IAvnClipboardDataSource*)source
{
self = [super init];
_item = item;
_source = source;
// Each item references its source so it doesn't get disposed too early.
source->AddRef();
return self;
}
virtual HRESULT Clear(int64_t* rv) override
NSString* TryConvertFormatToUti(NSString* format)
{
if (@available(macOS 11.0, *))
{
START_COM_CALL;
@autoreleasepool
auto type = [UTType typeWithIdentifier:format];
if (type == nil)
{
if(_item != nil)
{
_item = [NSPasteboardItem new];
return 0;
}
if ([format containsString:@"/"])
type = [UTType typeWithMIMEType:format];
else
type = [UTType exportedTypeWithIdentifier:format];
if (type == nil)
{
*rv = [_pb clearContents];
[_pb setString:@"" forType:NSPasteboardTypeString];
// For now, we need to use the deprecated UTTypeCreatePreferredIdentifierForTag to create a dynamic UTI for arbitrary strings.
// This is only necessary because the old IDataObject can provide arbitrary types that aren't UTIs nor mime types.
// With the new DataFormat:
// - If the format is an application format, the managed side provides a UTI like net.avaloniaui.app.uti.xxx.
// - If the format is an OS format, the user has been warned that they MUST provide a name which is valid for the OS.
// TODO12: remove!
auto fromPasteboardType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassNSPboardType, (__bridge CFStringRef)format, nil);
if (fromPasteboardType != nil)
return (__bridge_transfer NSString*)fromPasteboardType;
}
return S_OK;
}
return type == nil ? nil : [type identifier];
}
virtual HRESULT GetChangeCount(int64_t* rv) override
else
{
START_COM_CALL;
if(_item == nil)
{
*rv = [_pb changeCount];
return S_OK;
}
return E_NOTIMPL;
auto bridgedFormat = (__bridge CFStringRef)format;
if (UTTypeIsDeclared(bridgedFormat))
return format;
auto fromMimeType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, bridgedFormat, nil);
if (fromMimeType != nil)
return (__bridge_transfer NSString*)fromMimeType;
auto fromPasteboardType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassNSPboardType, bridgedFormat, nil);
if (fromPasteboardType != nil)
return (__bridge_transfer NSString*)fromPasteboardType;
return nil;
}
}
- (nonnull NSArray<NSPasteboardType>*) writableTypesForPasteboard:(nonnull NSPasteboard*)pasteboard
{
auto formats = _item->ProvideFormats();
if (formats == nullptr)
return [NSArray array];
virtual HRESULT ObtainFormats(IAvnStringArray** ppv) override
auto count = formats->GetCount();
if (count == 0)
return [NSArray array];
auto utis = [NSMutableArray arrayWithCapacity:count];
IAvnString* format;
for (auto i = 0; i < count; ++i)
{
START_COM_CALL;
@autoreleasepool
{
*ppv = CreateAvnStringArray(_item == nil ? [_pb types] : [_item types]);
return S_OK;
}
if (formats->Get(i, &format) != S_OK)
continue;
// Only UTIs must be returned from writableTypesForPasteboard or an exception will be thrown
auto formatString = GetNSStringAndRelease(format);
auto uti = TryConvertFormatToUti(formatString);
if (uti != nil)
[utis addObject:uti];
}
};
formats->Release();
[utis addObject:GetAvnCustomDataType()];
return utis;
}
extern IAvnClipboard* CreateClipboard(NSPasteboard* pb, NSPasteboardItem* item)
- (NSPasteboardWritingOptions) writingOptionsForType:(NSPasteboardType)type pasteboard:(NSPasteboard*)pasteboard
{
return new Clipboard(pb, item);
return [type isEqualToString:NSPasteboardTypeString] || [type isEqualToString:GetAvnCustomDataType()]
? 0
: NSPasteboardWritingPromised;
}
extern NSPasteboardItem* TryGetPasteboardItem(IAvnClipboard*cb)
- (nullable id) pasteboardPropertyListForType:(nonnull NSPasteboardType)type
{
auto clipboard = dynamic_cast<Clipboard*>(cb);
if(clipboard == nil)
if ([type isEqualToString:GetAvnCustomDataType()])
return @"";
ComPtr<IAvnClipboardDataValue> value(_item->GetValue([type UTF8String]), true);
if (value.getRaw() == nullptr)
return nil;
return clipboard->TryGetItem();
if (value->IsString())
return GetNSStringAndRelease(value->AsString());
auto length = value->GetByteLength();
auto buffer = malloc(length);
value->CopyBytesTo(buffer);
return [NSData dataWithBytesNoCopy:buffer length:length];
}
- (void) dealloc
{
if (_item != nullptr)
{
_item->Release();
_item = nullptr;
}
if (_source != nullptr)
{
_source->Release();
_source = nullptr;
}
}
@end

3
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<NSDraggingSource>* CreateDraggingSource(NSDragOperation op, IAvnDndResultCallback* cb, void* handle);
extern void* GetAvnDataObjectHandleFromDraggingInfo(NSObject<NSDraggingInfo>* info);
extern NSString* GetAvnCustomDataType();

14
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<NSDraggingSource>

13
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;
}
}

10
samples/ControlCatalog/Pages/ClipboardPage.xaml

@ -6,12 +6,10 @@
<Button Click="CopyText" Content="Copy text to clipboard" />
<Button Click="PasteText" Content="Paste text from clipboard" />
<Button Click="CopyTextDataObject" Content="Copy text to clipboard (data object)" />
<Button Click="PasteTextDataObject" Content="Paste text from clipboard (data object)" />
<Button Click="CopyFilesDataObject" Content="Copy files to clipboard (data object)" />
<Button Click="PasteFilesDataObject" Content="Paste files from clipboard (data object)" />
<Button Click="CopyFiles" Content="Copy files to clipboard" />
<Button Click="PasteFiles" Content="Paste files from clipboard" />
<Button Click="CopyBinaryData" Content="Copy bytes to clipboard" />
<Button Click="PasteBinaryData" Content="Paste bytes from clipboard" />
<Button Click="GetFormats" Content="Get clipboard formats" />
<Button Click="Clear" Content="Clear clipboard" />

87
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<byte[]> _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<Avalonia.Platform.Storage.IStorageItem>;
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 ? "<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<Run>("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;
}
}
}

48
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<string> _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<DataObject> factory, DragDropEffects effects) =>
private void SetupDnd(string suffix, Action<DataTransfer> factory, DragDropEffects effects) =>
SetupDnd(
suffix,
o =>
@ -54,17 +59,17 @@ namespace ControlCatalog.Pages
},
effects);
private void SetupDnd(string suffix, Func<DataObject, Task> factory, DragDropEffects effects)
private void SetupDnd(string suffix, Func<DataTransfer, Task> factory, DragDropEffects effects)
{
var dragMe = this.Get<Border>("DragMe" + suffix);
var dragState = this.Get<TextBlock>("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<IStorageItem>();
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<string>());
}
#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);
}
}

36
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<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);
}
}

58
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;
/// <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;
}
}

46
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;
/// <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()
{
}
}

138
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<IAsyncDataTransfer?> TryGetDataAsync()
{
_clipboardManager = value;
try
{
return Task.FromResult<IAsyncDataTransfer?>(TryGetData());
}
catch (Exception ex)
{
return Task.FromException<IAsyncDataTransfer?>(ex);
}
}
public Task<string?> GetTextAsync()
private ClipDataToDataTransferWrapper? TryGetData()
=> _clipboardManager?.PrimaryClip is { } clipData ?
new ClipDataToDataTransferWrapper(clipData, _context) :
null;
public async Task SetDataAsync(IAsyncDataTransfer dataTransfer)
{
if (_clipboardManager?.HasPrimaryClip == true)
if (_clipboardManager is null)
return;
var mimeTypes = dataTransfer.Formats
.Select(AndroidDataFormatHelper.DataFormatToMimeType)
.ToArray();
ClipData.Item? firstItem = null;
List<ClipData.Item>? additionalItems = null;
foreach (var dataTransferItem in dataTransfer.Items)
{
return Task.FromResult(_clipboardManager.PrimaryClip?.GetItemAt(0)?.Text);
if (await TryCreateDataItemAsync(dataTransferItem) is not { } clipDataItem)
continue;
if (firstItem is null)
firstItem = clipDataItem;
else
(additionalItems ??= new()).Add(clipDataItem);
}
return Task.FromResult<string?>(null);
if (firstItem is null)
{
Clear();
return;
}
var clipData = new ClipData((string?)null, mimeTypes, firstItem);
if (additionalItems is not null)
{
foreach (var additionalItem in additionalItems)
clipData.AddItem(additionalItem);
}
_clipboardManager.PrimaryClip = clipData;
}
public Task SetTextAsync(string? text)
private async Task<ClipData.Item?> TryCreateDataItemAsync(IAsyncDataTransferItem item)
{
if(_clipboardManager == null)
var hasFormats = false;
// Create the item from the first format returning a supported value.
foreach (var dataFormat in item.Formats)
{
return Task.CompletedTask;
hasFormats = true;
if (DataFormat.Text.Equals(dataFormat))
{
var text = await item.TryGetValueAsync(DataFormat.Text);
return new ClipData.Item(text, string.Empty);
}
if (DataFormat.File.Equals(dataFormat))
{
var storageItem = await item.TryGetValueAsync(DataFormat.File);
if (storageItem is null)
continue;
return new ClipData.Item(AndroidUri.Parse(storageItem.Path.OriginalString));
}
if (dataFormat is DataFormat<string> stringFormat)
{
var stringValue = await item.TryGetValueAsync(stringFormat);
if (stringValue is null)
continue;
return new ClipData.Item(stringValue);
}
}
var clip = ClipData.NewPlainText("text", text);
_clipboardManager.PrimaryClip = clip;
if (hasFormats)
{
Logger.TryGet(LogEventLevel.Warning, LogArea.AndroidPlatform)?.Log(
this,
"No compatible value found for data transfer item with formats {Formats}",
string.Join(", ", item.Formats));
}
return Task.CompletedTask;
return null;
}
public Task ClearAsync()
{
if (_clipboardManager == null)
try
{
Clear();
return Task.CompletedTask;
}
_clipboardManager.PrimaryClip = null;
return Task.CompletedTask;
catch (Exception ex)
{
return Task.FromException(ex);
}
}
public Task SetDataObjectAsync(IDataObject data) => throw new PlatformNotSupportedException();
public Task<string[]> GetFormatsAsync() => throw new PlatformNotSupportedException();
public Task<object?> GetDataAsync(string format) => throw new PlatformNotSupportedException();
public Task<IDataObject?> TryGetInProcessDataObjectAsync() => Task.FromResult<IDataObject?>(null);
private void Clear()
{
if (_clipboardManager is null)
return;
/// <inheritdoc />
public Task FlushAsync() =>
Task.CompletedTask;
if (OperatingSystem.IsAndroidVersionAtLeast(28))
_clipboardManager.ClearPrimaryClip();
else
_clipboardManager.PrimaryClip = ClipData.NewPlainText(null, string.Empty);
}
}
}

16
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@ -40,7 +40,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
private readonly IStorageProvider? _storageProvider;
private readonly AndroidSystemNavigationManagerImpl _systemNavigationManager;
private readonly AndroidInsetsManager? _insetsManager;
private readonly ClipboardImpl _clipboard;
private readonly Clipboard _clipboard;
private readonly AndroidLauncher? _launcher;
private readonly AndroidScreens? _screens;
private ViewImpl _view;
@ -48,23 +48,25 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false)
{
if (avaloniaView.Context is null)
if (avaloniaView.Context is not { } context)
{
throw new ArgumentException("AvaloniaView.Context must not be null");
}
_view = new ViewImpl(avaloniaView.Context, this, placeOnTop);
_view = new ViewImpl(context, this, placeOnTop);
_textInputMethod = new AndroidInputMethod<AvaloniaView>(avaloniaView);
_keyboardHelper = new AndroidKeyboardEventsHelper<TopLevelImpl>(this);
_pointerHelper = new AndroidMotionEventsHelper(this);
_gl = new EglGlPlatformSurface(this);
_framebuffer = new FramebufferManager(this);
_clipboard = new ClipboardImpl(avaloniaView.Context.GetSystemService(Context.ClipboardService).JavaCast<ClipboardManager>());
_screens = new AndroidScreens(avaloniaView.Context);
_clipboard = new Clipboard(new ClipboardImpl(
context.GetSystemService(Context.ClipboardService).JavaCast<ClipboardManager>(),
context));
_screens = new AndroidScreens(context);
RenderScaling = _view.Scaling;
if (avaloniaView.Context is Activity mainActivity)
if (context is Activity mainActivity)
{
_insetsManager = new AndroidInsetsManager(mainActivity, this);
_storageProvider = new AndroidStorageProvider(mainActivity);
@ -74,7 +76,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_nativeControlHost = new AndroidNativeControlHostImpl(avaloniaView);
_transparencyLevel = WindowTransparencyLevel.None;
_systemNavigationManager = new AndroidSystemNavigationManagerImpl(avaloniaView.Context as IActivityNavigationService);
_systemNavigationManager = new AndroidSystemNavigationManagerImpl(context as IActivityNavigationService);
Surfaces = new object[] { _gl, _framebuffer, _view };
Handle = new AndroidViewControlHandle(_view);

6
src/Avalonia.Base/Avalonia.Base.csproj

@ -17,9 +17,11 @@
<Import Project="..\..\build\DevAnalyzers.props" />
<Import Project="..\..\build\SourceGenerators.props" />
<ItemGroup>
<Compile Include="..\Shared\IsExternalInit.cs" Link="IsExternalInit.cs" />
<Compile Include="..\Shared\ModuleInitializer.cs" Link="ModuleInitializer.cs" />
<Compile Include="..\Shared\CallerArgumentExpressionAttribute.cs" Link="Compatibility\CallerArgumentExpressionAttribute.cs" />
<Compile Include="..\Shared\IsExternalInit.cs" Link="Compatibility\IsExternalInit.cs" />
<Compile Include="..\Shared\ModuleInitializer.cs" Link="Compatibility\ModuleInitializer.cs" />
<Compile Include="..\Shared\StringCompatibilityExtensions.cs" Link="Compatibility\StringCompatibilityExtensions.cs" />
<Compile Include="..\Shared\StreamCompatibilityExtensions.cs" Link="Compatibility\StreamCompatibilityExtensions.cs" />
</ItemGroup>
<ItemGroup Label="InternalsVisibleTo">

139
src/Avalonia.Base/Input/AsyncDataTransferExtensions.cs

@ -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);
}

58
src/Avalonia.Base/Input/AsyncDataTransferItemExtensions.cs

@ -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);
}

45
src/Avalonia.Base/Input/AsyncToSyncDataTransfer.cs

@ -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();
}

24
src/Avalonia.Base/Input/AsyncToSyncDataTransferItem.cs

@ -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);
}

227
src/Avalonia.Base/Input/DataFormat.cs

@ -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}";
}

46
src/Avalonia.Base/Input/DataFormatKind.cs

@ -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
}

24
src/Avalonia.Base/Input/DataFormatOfT.cs

@ -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)
{
}
}

32
src/Avalonia.Base/Input/DataFormats.cs

@ -1,6 +1,6 @@
using System;
using System.ComponentModel;
using Avalonia.Metadata;
using Avalonia.Input.Platform;
namespace Avalonia.Input
{
@ -9,11 +9,13 @@ namespace Avalonia.Input
/// <summary>
/// Dataformat for plaintext
/// </summary>
[Obsolete($"Use {nameof(DataFormat)}.{nameof(DataFormat.Text)} instead.")]
public static readonly string Text = nameof(Text);
/// <summary>
/// Dataformat for one or more files.
/// </summary>
[Obsolete($"Use {nameof(DataFormat)}.{nameof(DataFormat.File)} instead.")]
public static readonly string Files = nameof(Files);
/// <summary>
@ -22,7 +24,33 @@ namespace Avalonia.Input
/// <remarks>
/// This data format is supported only on desktop platforms.
/// </remarks>
[Unstable("Use DataFormats.Files, this format is supported only on desktop platforms. And it will be removed in 12.0."), EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete($"Use {nameof(DataFormat)}.{nameof(DataFormat.File)} instead."), EditorBrowsable(EditorBrowsableState.Never)]
public static readonly string FileNames = nameof(FileNames);
#pragma warning disable CS0618 // Type or member is obsolete
internal static DataFormat ToDataFormat(string format)
{
if (format == Text)
return DataFormat.Text;
if (format == Files || format == FileNames)
return DataFormat.File;
return DataFormat.CreateBytesPlatformFormat(format);
}
internal static string ToString(DataFormat format)
{
if (DataFormat.Text.Equals(format))
return Text;
if (DataFormat.File.Equals(format))
return Files;
return format.Identifier;
}
#pragma warning restore CS0618 // Type or member is obsolete
}
}

4
src/Avalonia.Base/Input/DataObject.cs

@ -1,10 +1,12 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
namespace Avalonia.Input
{
/// <summary>
/// Specific and mutable implementation of the IDataObject interface.
/// </summary>
[Obsolete($"Use {nameof(DataTransfer)} instead")]
public class DataObject : IDataObject
{
private readonly Dictionary<string, object> _items = new();

3
src/Avalonia.Base/Input/DataObjectExtensions.cs

@ -3,8 +3,11 @@ using System.ComponentModel;
using System.Linq;
using Avalonia.Platform.Storage;
#pragma warning disable CS0618 // Type or member is obsolete
namespace Avalonia.Input
{
// TODO12: remove
public static class DataObjectExtensions
{
/// <summary>

59
src/Avalonia.Base/Input/DataTransfer.cs

@ -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()
{
}
}

130
src/Avalonia.Base/Input/DataTransferExtensions.cs

@ -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);
}

222
src/Avalonia.Base/Input/DataTransferItem.cs

@ -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);
}
}

57
src/Avalonia.Base/Input/DataTransferItemExtensions.cs

@ -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);
}

22
src/Avalonia.Base/Input/DragDrop.cs

@ -126,10 +126,28 @@ namespace Avalonia.Input
/// Starts a dragging operation with the given <see cref="IDataObject"/> and returns the applied drop effect from the target.
/// <seealso cref="DataObject"/>
/// </summary>
[Obsolete($"Use {nameof(DoDragDropAsync)} instead.")]
public static Task<DragDropEffects> DoDragDrop(PointerEventArgs triggerEvent, IDataObject data, DragDropEffects allowedEffects)
{
var src = AvaloniaLocator.Current.GetService<IPlatformDragSource>();
return src?.DoDragDrop(triggerEvent, data, allowedEffects) ?? Task.FromResult(DragDropEffects.None);
return DoDragDropAsync(triggerEvent, new DataObjectToDataTransferWrapper(data), allowedEffects);
}
/// <summary>
/// Starts a dragging operation with the given <see cref="IDataTransfer"/> and returns the applied drop effect from the target.
/// <seealso cref="DataTransfer"/>
/// </summary>
public static Task<DragDropEffects> DoDragDropAsync(
PointerEventArgs triggerEvent,
IDataTransfer dataTransfer,
DragDropEffects allowedEffects)
{
if (AvaloniaLocator.Current.GetService<IPlatformDragSource>() is not { } dragSource)
{
dataTransfer.Dispose();
return Task.FromResult(DragDropEffects.None);
}
return dragSource.DoDragDropAsync(triggerEvent, dataTransfer, allowedEffects);
}
}
}

30
src/Avalonia.Base/Input/DragDropDevice.cs

@ -1,6 +1,7 @@
using Avalonia.Interactivity;
using Avalonia.VisualTree;
using System.Linq;
using Avalonia.Input.Platform;
using Avalonia.Input.Raw;
using Avalonia.Metadata;
@ -21,8 +22,15 @@ namespace Avalonia.Input
return target;
return null;
}
private static DragDropEffects RaiseDragEvent(Interactive? target, IInputRoot inputRoot, Point point, RoutedEvent<DragEventArgs> routedEvent, DragDropEffects operation, IDataObject data, KeyModifiers modifiers)
private static DragDropEffects RaiseDragEvent(
Interactive? target,
IInputRoot inputRoot,
Point point,
RoutedEvent<DragEventArgs> routedEvent,
DragDropEffects operation,
IDataTransfer dataTransfer,
KeyModifiers modifiers)
{
if (target == null)
return DragDropEffects.None;
@ -32,7 +40,7 @@ namespace Avalonia.Input
if (!p.HasValue)
return DragDropEffects.None;
var args = new DragEventArgs(routedEvent, data, target, p.Value, modifiers)
var args = new DragEventArgs(routedEvent, dataTransfer, target, p.Value, modifiers)
{
RoutedEvent = routedEvent,
DragEffects = operation
@ -41,13 +49,13 @@ namespace Avalonia.Input
return args.DragEffects;
}
private DragDropEffects DragEnter(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, KeyModifiers modifiers)
private DragDropEffects DragEnter(IInputRoot inputRoot, Point point, IDataTransfer data, DragDropEffects effects, KeyModifiers modifiers)
{
_lastTarget = GetTarget(inputRoot, point);
return RaiseDragEvent(_lastTarget, inputRoot, point, DragDrop.DragEnterEvent, effects, data, modifiers);
}
private DragDropEffects DragOver(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, KeyModifiers modifiers)
private DragDropEffects DragOver(IInputRoot inputRoot, Point point, IDataTransfer data, DragDropEffects effects, KeyModifiers modifiers)
{
var target = GetTarget(inputRoot, point);
@ -66,7 +74,7 @@ namespace Avalonia.Input
}
}
private void DragLeave(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, KeyModifiers modifiers)
private void DragLeave(IInputRoot inputRoot, Point point, IDataTransfer data, DragDropEffects effects, KeyModifiers modifiers)
{
if (_lastTarget == null)
return;
@ -80,7 +88,7 @@ namespace Avalonia.Input
}
}
private DragDropEffects Drop(IInputRoot inputRoot, Point point, IDataObject data, DragDropEffects effects, KeyModifiers modifiers)
private DragDropEffects Drop(IInputRoot inputRoot, Point point, IDataTransfer data, DragDropEffects effects, KeyModifiers modifiers)
{
try
{
@ -103,16 +111,16 @@ namespace Avalonia.Input
switch (e.Type)
{
case RawDragEventType.DragEnter:
e.Effects = DragEnter(e.Root, e.Location, e.Data, e.Effects, e.KeyModifiers);
e.Effects = DragEnter(e.Root, e.Location, e.DataTransfer, e.Effects, e.KeyModifiers);
break;
case RawDragEventType.DragOver:
e.Effects = DragOver(e.Root, e.Location, e.Data, e.Effects, e.KeyModifiers);
e.Effects = DragOver(e.Root, e.Location, e.DataTransfer, e.Effects, e.KeyModifiers);
break;
case RawDragEventType.DragLeave:
DragLeave(e.Root, e.Location, e.Data, e.Effects, e.KeyModifiers);
DragLeave(e.Root, e.Location, e.DataTransfer, e.Effects, e.KeyModifiers);
break;
case RawDragEventType.Drop:
e.Effects = Drop(e.Root, e.Location, e.Data, e.Effects, e.KeyModifiers);
e.Effects = Drop(e.Root, e.Location, e.DataTransfer, e.Effects, e.KeyModifiers);
break;
}
}

28
src/Avalonia.Base/Input/DragEventArgs.cs

@ -1,4 +1,5 @@
using System;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Metadata;
@ -8,10 +9,15 @@ namespace Avalonia.Input
{
private readonly Interactive _target;
private readonly Point _targetLocation;
[Obsolete] private IDataObject? _legacyDataObject;
public DragDropEffects DragEffects { get; set; }
public IDataObject Data { get; }
public IDataTransfer DataTransfer { get; }
[Obsolete($"Use {nameof(DataTransfer)} instead.")]
public IDataObject Data
=> _legacyDataObject ??= DataTransfer.ToLegacyDataObject();
public KeyModifiers KeyModifiers { get; }
@ -25,11 +31,27 @@ namespace Avalonia.Input
return _target.TranslatePoint(_targetLocation, relativeTo) ?? new Point(0, 0);
}
[Obsolete($"Use the constructor accepting a {nameof(IDataTransfer)} instance instead.")]
public DragEventArgs(
RoutedEvent<DragEventArgs> routedEvent,
IDataObject data,
Interactive target,
Point targetLocation,
KeyModifiers keyModifiers)
: this(routedEvent, new DataObjectToDataTransferWrapper(data), target, targetLocation, keyModifiers)
{
}
[Unstable("This constructor might be removed in 12.0. For unit testing, consider using DragDrop.DoDragDrop or IHeadlessWindow.DragDrop.")]
public DragEventArgs(RoutedEvent<DragEventArgs> routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers)
public DragEventArgs(
RoutedEvent<DragEventArgs> routedEvent,
IDataTransfer dataTransfer,
Interactive target,
Point targetLocation,
KeyModifiers keyModifiers)
: base(routedEvent)
{
Data = data;
DataTransfer = dataTransfer;
_target = target;
_targetLocation = targetLocation;
KeyModifiers = keyModifiers;

47
src/Avalonia.Base/Input/IAsyncDataTransfer.cs

@ -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; }
}

35
src/Avalonia.Base/Input/IAsyncDataTransferItem.cs

@ -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);
}

6
src/Avalonia.Base/Input/IDataObject.cs

@ -1,12 +1,12 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Platform.Storage;
using System;
using System.Collections.Generic;
namespace Avalonia.Input
{
/// <summary>
/// Interface to access information about the data of a drag-and-drop operation.
/// </summary>
[Obsolete($"Use {nameof(IDataTransfer)} or {nameof(IAsyncDataTransfer)} instead")]
public interface IDataObject
{
/// <summary>

32
src/Avalonia.Base/Input/IDataTransfer.cs

@ -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; }
}

34
src/Avalonia.Base/Input/IDataTransferItem.cs

@ -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);
}

72
src/Avalonia.Base/Input/Platform/BinaryFormatterHelper.cs

@ -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;
}
}

103
src/Avalonia.Base/Input/Platform/Clipboard.cs

@ -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;
}
}

189
src/Avalonia.Base/Input/Platform/ClipboardExtensions.cs

@ -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);
}

79
src/Avalonia.Base/Input/Platform/DataObjectToDataTransferItemWrapper.cs

@ -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);
}
}
}

84
src/Avalonia.Base/Input/Platform/DataObjectToDataTransferWrapper.cs

@ -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();
}

44
src/Avalonia.Base/Input/Platform/DataTransferToDataObjectWrapper.cs

@ -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;
}
}

79
src/Avalonia.Base/Input/Platform/IClipboard.cs

@ -1,26 +1,39 @@
using System;
using System.Threading.Tasks;
using Avalonia.Metadata;
namespace Avalonia.Input.Platform
{
/// <summary>
/// Represents the system clipboard.
/// </summary>
[NotClientImplementable]
public interface IClipboard
{
// TODO12: remove, ClipboardExtensions.TryGetTextAsync exists
/// <summary>
/// Returns a string containing the text data on the Clipboard.
/// Returns a string containing the text data on the clipboard.
/// </summary>
/// <returns>A string containing text data in the specified data format, or an empty string if no corresponding text data is available.</returns>
/// <returns>A string containing text data, or null if no corresponding text data is available.</returns>
[Obsolete($"Use {nameof(ClipboardExtensions)}.{nameof(ClipboardExtensions.TryGetTextAsync)} instead")]
Task<string?> GetTextAsync();
// TODO12: remove, ClipboardExtensions.SetTextAsync exists
/// <summary>
/// Stores text data on the Clipboard. The text data to store is specified as a string.
/// Places a text on the clipboard.
/// </summary>
/// <param name="text">A string that contains the UnicodeText data to store on the Clipboard.</param>
/// <exception cref="System.ArgumentNullException"><paramref name="text"/> is null.</exception>
/// <param name="text">The text value to set.</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 or empty, nothing will get placed on the clipboard and this method
/// will be equivalent to <see cref="ClearAsync"/>.
/// </para>
/// </remarks>
Task SetTextAsync(string? text);
/// <summary>
/// Clears any data from the system Clipboard.
/// Clears any data from the system clipboard.
/// </summary>
Task ClearAsync();
@ -29,18 +42,37 @@ namespace Avalonia.Input.Platform
/// </summary>
/// <param name="data">A data object (an object that implements <see cref="IDataObject"/>) to place on the system Clipboard.</param>
/// <exception cref="System.ArgumentNullException"><paramref name="data"/> is null.</exception>
[Obsolete($"Use {nameof(SetDataAsync)} instead.")]
Task SetDataObjectAsync(IDataObject data);
/// <summary>
/// Places a data object on the clipboard.
/// The data object is responsible for providing supported formats and data upon request.
/// </summary>
/// <param name="dataTransfer">The data object to set on the clipboard.</param>
/// <remarks>
/// <para>
/// If <paramref name="dataTransfer"/> is null, nothing will get placed on the clipboard and this method
/// will be equivalent to <see cref="ClearAsync"/>.
/// </para>
/// <para>
/// The <see cref="IAsyncDataTransfer"/> must NOT be disposed by the caller after this call.
/// The clipboard will dispose of it automatically when it becomes unused.
/// </para>
/// </remarks>
Task SetDataAsync(IAsyncDataTransfer? dataTransfer);
/// <summary>
/// Permanently adds the data that is on the Clipboard so that it is available after the data's original application closes.
/// </summary>
/// <returns></returns>
/// <remarks>This method works only on Windows platform, on other platforms it does nothing.</remarks>
/// <remarks>This method is only supported on the Windows platform. This method will do nothing on other platforms.</remarks>
Task FlushAsync();
/// <summary>
/// Get list of available Clipboard format.
/// </summary>
[Obsolete($"Use {nameof(ClipboardExtensions.GetDataFormatsAsync)} instead.")]
Task<string[]> GetFormatsAsync();
/// <summary>
@ -48,15 +80,44 @@ namespace Avalonia.Input.Platform
/// </summary>
/// <param name="format">A string that specifies the format of the data to retrieve. For a set of predefined data formats, see the <see cref="DataFormats"/> class.</param>
/// <returns></returns>
[Obsolete($"Use {nameof(TryGetDataAsync)} instead.")]
Task<object?> GetDataAsync(string format);
/// <summary>
/// Retrieves data from the clipboard.
/// </summary>
/// <remarks>
/// <para>The returned <see cref="IAsyncDataTransfer"/> MUST be disposed by the caller.</para>
/// <para>
/// Avoid storing the returned <see cref="IAsyncDataTransfer"/> instance for a long time:
/// use it, then dispose it as soon as possible.
/// </para>
/// </remarks>
Task<IAsyncDataTransfer?> TryGetDataAsync();
/// <summary>
/// If clipboard contains the IDataObject that was set by a previous call to <see cref="SetDataObjectAsync"/>,
/// If clipboard contains the IDataObject that was set by a previous call to <see cref="SetDataObjectAsync(Avalonia.Input.IDataObject)"/>,
/// return said IDataObject instance. Otherwise, return null.
/// Note that not every platform supports that method, on unsupported platforms this method will always return
/// null
/// </summary>
/// <returns></returns>
[Obsolete($"Use {nameof(TryGetInProcessDataAsync)} instead.")]
Task<IDataObject?> TryGetInProcessDataObjectAsync();
/// <summary>
/// Retrieves the exact instance of a <see cref="IAsyncDataTransfer"/> previously placed on the clipboard
/// by <see cref="SetDataAsync"/>, if any.
/// </summary>
/// <returns>The data transfer object if present, null otherwise.</returns>
/// <remarks>
/// <para>This method cannot be used to retrieve a <see cref="IAsyncDataTransfer"/> set by another process.</para>
/// <para>This method is only supported on Windows, macOS and X11 platforms. Other platforms will always return null.</para>
/// <para>
/// Contrary to <see cref="TryGetDataAsync"/>, the returned <see cref="IAsyncDataTransfer"/> must NOT be disposed
/// by the caller since it's still owned by the clipboard.
/// </para>
/// </remarks>
Task<IAsyncDataTransfer?> TryGetInProcessDataAsync();
}
}

20
src/Avalonia.Base/Input/Platform/IClipboardImpl.cs

@ -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();
}

14
src/Avalonia.Base/Input/Platform/IFlushableClipboardImpl.cs

@ -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();
}

16
src/Avalonia.Base/Input/Platform/IOwnedClipboardImpl.cs

@ -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();
}

16
src/Avalonia.Base/Input/Platform/IPlatformDragSource.cs

@ -1,11 +1,21 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
using Avalonia.Metadata;
namespace Avalonia.Input.Platform
{
[Unstable]
[NotClientImplementable]
public interface IPlatformDragSource
{
Task<DragDropEffects> DoDragDrop(PointerEventArgs triggerEvent, IDataObject data, DragDropEffects allowedEffects);
[Obsolete($"Use {nameof(DoDragDropAsync)} instead.")]
Task<DragDropEffects> DoDragDrop(
PointerEventArgs triggerEvent,
IDataObject data,
DragDropEffects allowedEffects);
Task<DragDropEffects> DoDragDropAsync(
PointerEventArgs triggerEvent,
IDataTransfer dataTransfer,
DragDropEffects allowedEffects);
}
}

37
src/Avalonia.Base/Input/Platform/PlatformAsyncDataTransfer.cs

@ -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();
}

30
src/Avalonia.Base/Input/Platform/PlatformAsyncDataTransferItem.cs

@ -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);
}

43
src/Avalonia.Base/Input/Platform/PlatformDataTransfer.cs

@ -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();
}

63
src/Avalonia.Base/Input/Platform/PlatformDataTransferItem.cs

@ -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;
}
}

39
src/Avalonia.Base/Input/Raw/RawDragEvent.cs

@ -1,4 +1,5 @@
using System;
using Avalonia.Input.Platform;
using Avalonia.Metadata;
namespace Avalonia.Input.Raw
@ -6,19 +7,47 @@ namespace Avalonia.Input.Raw
[PrivateApi]
public class RawDragEvent : RawInputEventArgs
{
[Obsolete] private IDataObject? _legacyDataObject;
public Point Location { get; set; }
public IDataObject Data { get; }
public IDataTransfer DataTransfer { get; }
[Obsolete($"Use {nameof(DataTransfer)} instead.")]
public IDataObject Data
=> _legacyDataObject ??= DataTransfer.ToLegacyDataObject();
public DragDropEffects Effects { get; set; }
public RawDragEventType Type { get; }
public KeyModifiers KeyModifiers { get; }
public RawDragEvent(IDragDropDevice inputDevice, RawDragEventType type,
IInputRoot root, Point location, IDataObject data, DragDropEffects effects, RawInputModifiers modifiers)
:base(inputDevice, 0, root)
[Obsolete($"Use the constructor accepting a {nameof(IDataTransfer)} instance instead.")]
public RawDragEvent(IDragDropDevice inputDevice,
RawDragEventType type,
IInputRoot root,
Point location,
IDataObject data,
DragDropEffects effects,
RawInputModifiers modifiers)
: this(inputDevice, type, root, location, new DataObjectToDataTransferWrapper(data), effects, modifiers)
{
}
public RawDragEvent(
IDragDropDevice inputDevice,
RawDragEventType type,
IInputRoot root,
Point location,
IDataTransfer dataTransfer,
DragDropEffects effects,
RawInputModifiers modifiers)
: base(inputDevice, 0, root)
{
Type = type;
Location = location;
Data = data;
DataTransfer = dataTransfer;
Effects = effects;
KeyModifiers = modifiers.ToKeyModifiers();
}

2
src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs

@ -8,7 +8,7 @@ namespace Avalonia.Platform.Storage.FileIO;
internal static class StorageProviderHelpers
{
public static BclStorageItem? TryCreateBclStorageItem(string path)
public static BclStorageItem? TryCreateBclStorageItem(string? path)
{
if (!string.IsNullOrWhiteSpace(path))
{

34
src/Avalonia.Base/Utilities/ThrowHelper.cs

@ -5,20 +5,42 @@ using System.Runtime.CompilerServices;
namespace Avalonia.Utilities;
/// <summary>
/// Helper method to help inlining methods that do a throw check.
/// Equivalent of .NET6+ ArgumentNullException.ThrowIfNull() for netstandard2.0+
/// Helper methods to help inlining methods that do a throw check.
/// </summary>
internal class ThrowHelper
internal static class ThrowHelper
{
/// <summary>
/// Equivalent of .NET6+ ArgumentNullException.ThrowIfNull().
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ThrowIfNull([NotNull] object? argument, string paramName)
public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null)
{
if (argument is null)
{
ThrowArgumentNullException(paramName);
}
[DoesNotReturn]
static void ThrowArgumentNullException(string? paramName) => throw new ArgumentNullException(paramName);
}
/// <summary>
/// Equivalent of .NET8+ ArgumentException.ThrowIfNullOrEmpty().
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ThrowIfNullOrEmpty([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null)
{
if (string.IsNullOrEmpty(argument))
{
ThrowNullOrEmptyException(argument, paramName);
}
[DoesNotReturn]
static void ThrowNullOrEmptyException(string? argument, string? paramName)
{
ThrowIfNull(argument, paramName);
throw new ArgumentException("Empty string", paramName);
}
}
[DoesNotReturn]
private static void ThrowArgumentNullException(string paramName) => throw new ArgumentNullException(paramName);
}

3
src/Avalonia.Controls/MaskedTextBox.cs

@ -4,6 +4,7 @@ using System.ComponentModel;
using System.Globalization;
using System.Linq;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
namespace Avalonia.Controls
@ -216,7 +217,7 @@ namespace Avalonia.Controls
string? text = null;
try
{
text = await clipboard.GetTextAsync();
text = await clipboard.TryGetTextAsync();
}
catch (TimeoutException)
{

19
src/Avalonia.Controls/Platform/InProcessDragSource.cs

@ -1,4 +1,5 @@
using System.Linq;
using System;
using System.Linq;
using Avalonia.Reactive;
using System.Threading.Tasks;
using Avalonia.Controls;
@ -18,7 +19,7 @@ namespace Avalonia.Platform
private readonly LightweightSubject<DragDropEffects> _result = new();
private DragDropEffects _allowedEffects;
private IDataObject? _draggedData;
private IDataTransfer? _draggedData;
private TopLevel? _lastRoot;
private Point _lastPosition;
private StandardCursorType? _lastCursorType;
@ -30,13 +31,23 @@ namespace Avalonia.Platform
_dragDrop = AvaloniaLocator.Current.GetRequiredService<IDragDropDevice>();
}
public async Task<DragDropEffects> DoDragDrop(PointerEventArgs triggerEvent, IDataObject data, DragDropEffects allowedEffects)
[Obsolete($"Use {nameof(DoDragDropAsync)} instead.")]
Task<DragDropEffects> IPlatformDragSource.DoDragDrop(
PointerEventArgs triggerEvent,
IDataObject data,
DragDropEffects allowedEffects)
=> DoDragDropAsync(triggerEvent, new DataObjectToDataTransferWrapper(data), allowedEffects);
public async Task<DragDropEffects> DoDragDropAsync(
PointerEventArgs triggerEvent,
IDataTransfer dataTransfer,
DragDropEffects allowedEffects)
{
Dispatcher.UIThread.VerifyAccess();
triggerEvent.Pointer.Capture(null);
if (_draggedData == null)
{
_draggedData = data;
_draggedData = dataTransfer;
_lastRoot = null;
_lastPosition = default;
_allowedEffects = allowedEffects;

2
src/Avalonia.Controls/TextBox.cs

@ -1259,7 +1259,7 @@ namespace Avalonia.Controls
{
try
{
text = await clipboard.GetTextAsync();
text = await clipboard.TryGetTextAsync();
}
catch (TimeoutException)
{

82
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@ -1,11 +1,10 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using Avalonia.Reactive;
using System.Threading.Tasks;
using Avalonia.Automation.Peers;
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
using Avalonia.Input.Platform;
@ -15,49 +14,48 @@ using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
namespace Avalonia.DesignerSupport.Remote
{
class WindowStub : IWindowImpl, IPopupImpl
{
public Action Deactivated { get; set; }
public Action Activated { get; set; }
public IPlatformHandle Handle { get; }
public Action? Deactivated { get; set; }
public Action? Activated { get; set; }
public IPlatformHandle? Handle { get; }
public Size MaxAutoSizeHint { get; }
public Size ClientSize { get; }
public Size? FrameSize => null;
public double RenderScaling { get; } = 1.0;
public double DesktopScaling => 1.0;
public IEnumerable<object> Surfaces { get; }
public Action<RawInputEventArgs> Input { get; set; }
public Action<Rect> Paint { get; set; }
public Action<Size, WindowResizeReason> Resized { get; set; }
public Action<double> ScalingChanged { get; set; }
public Func<WindowCloseReason, bool> Closing { get; set; }
public Action Closed { get; set; }
public Action LostFocus { get; set; }
public IEnumerable<object> Surfaces => [];
public Action<RawInputEventArgs>? Input { get; set; }
public Action<Rect>? Paint { get; set; }
public Action<Size, WindowResizeReason>? Resized { get; set; }
public Action<double>? ScalingChanged { get; set; }
public Func<WindowCloseReason, bool>? Closing { get; set; }
public Action? Closed { get; set; }
public Action? LostFocus { get; set; }
public IMouseDevice MouseDevice { get; } = new MouseDevice();
public IPopupImpl CreatePopup() => new WindowStub(this);
public PixelPoint Position { get; set; }
public Action<PixelPoint> PositionChanged { get; set; }
public Action<PixelPoint>? PositionChanged { get; set; }
public WindowState WindowState { get; set; }
public Action<WindowState> WindowStateChanged { get; set; }
public Action<WindowState>? WindowStateChanged { get; set; }
public Action<WindowTransparencyLevel> TransparencyLevelChanged { get; set; }
public Action<WindowTransparencyLevel>? TransparencyLevelChanged { get; set; }
public Action<bool> ExtendClientAreaToDecorationsChanged { get; set; }
public Action<bool>? ExtendClientAreaToDecorationsChanged { get; set; }
public Thickness ExtendedMargins { get; } = new Thickness();
public Thickness OffScreenMargin { get; } = new Thickness();
public WindowStub(IWindowImpl parent = null)
public WindowStub(IWindowImpl? parent = null)
{
if (parent != null)
PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent,
(_, size, __) =>
(_, size, _) =>
{
Resize(size, WindowResizeReason.Unspecified);
}));
@ -91,7 +89,7 @@ namespace Avalonia.DesignerSupport.Remote
public PixelPoint PointToScreen(Point p) => PixelPoint.FromPoint(p, 1);
public void SetCursor(ICursorImpl cursor)
public void SetCursor(ICursorImpl? cursor)
{
}
@ -128,7 +126,7 @@ namespace Avalonia.DesignerSupport.Remote
{
}
public void SetTitle(string title)
public void SetTitle(string? title)
{
}
@ -140,7 +138,7 @@ namespace Avalonia.DesignerSupport.Remote
{
}
public void SetIcon(IWindowIconImpl icon)
public void SetIcon(IWindowIconImpl? icon)
{
}
@ -164,7 +162,7 @@ namespace Avalonia.DesignerSupport.Remote
{
}
public void SetParent(IWindowImpl parent)
public void SetParent(IWindowImpl? parent)
{
}
@ -186,9 +184,9 @@ namespace Avalonia.DesignerSupport.Remote
public void GetWindowsZOrder(Span<Window> windows, Span<long> zOrder) => throw new NotSupportedException();
public IPopupPositioner PopupPositioner { get; }
public IPopupPositioner? PopupPositioner { get; }
public Action GotInputWhenDisabled { get; set; }
public Action? GotInputWhenDisabled { get; set; }
public void SetTransparencyLevelHint(IReadOnlyList<WindowTransparencyLevel> transparencyLevel) { }
@ -205,7 +203,8 @@ namespace Avalonia.DesignerSupport.Remote
public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { }
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1);
public object TryGetFeature(Type featureType)
public object? TryGetFeature(Type featureType)
{
if (featureType == typeof(IStorageProvider))
{
@ -224,20 +223,29 @@ namespace Avalonia.DesignerSupport.Remote
class ClipboardStub : IClipboard
{
public Task<string> GetTextAsync() => Task.FromResult("");
public Task<string?> GetTextAsync() => Task.FromResult<string?>(null);
public Task SetTextAsync(string text) => Task.CompletedTask;
public Task SetTextAsync(string? text) => Task.CompletedTask;
public Task ClearAsync() => Task.CompletedTask;
[Obsolete($"Use {nameof(SetDataAsync)} instead.")]
public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask;
public Task<string[]> GetFormatsAsync() => Task.FromResult(Array.Empty<string>());
public Task<object> GetDataAsync(string format) => Task.FromResult((object)null);
public Task SetDataAsync(IAsyncDataTransfer? dataTransfer) => Task.CompletedTask;
public Task<string[]> GetFormatsAsync() => Task.FromResult<string[]>([]);
public Task<object?> GetDataAsync(string format) => Task.FromResult<object?>(null);
public Task<IAsyncDataTransfer?> TryGetDataAsync() => Task.FromResult<IAsyncDataTransfer?>(null);
[Obsolete($"Use {nameof(TryGetInProcessDataAsync)} instead.")]
public Task<IDataObject?> TryGetInProcessDataObjectAsync() => Task.FromResult<IDataObject?>(null);
public Task<IDataObject> TryGetInProcessDataObjectAsync() => Task.FromResult<IDataObject>(null);
public Task FlushAsync() => Task.CompletedTask;
public Task FlushAsync() =>
Task.CompletedTask;
public Task<IAsyncDataTransfer?> TryGetInProcessDataAsync() => Task.FromResult<IAsyncDataTransfer?>(null);
}
class CursorFactoryStub : ICursorFactory
@ -294,9 +302,9 @@ namespace Avalonia.DesignerSupport.Remote
}
public override bool CanSave => false;
public override Task<IStorageFile> SaveFilePickerAsync(FilePickerSaveOptions options)
public override Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options)
{
return Task.FromResult<IStorageFile>(null);
return Task.FromResult<IStorageFile?>(null);
}
public override bool CanPickFolder => false;

15
src/Avalonia.Diagnostics/Diagnostics/Constants.cs

@ -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);
}
}

21
src/Avalonia.Diagnostics/Diagnostics/DevToolsDataFormats.cs

@ -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");
}

13
src/Avalonia.Diagnostics/Diagnostics/Views/TreePageView.xaml.cs

@ -1,3 +1,4 @@
using System.Text;
using Avalonia.Controls;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Input;
@ -79,11 +80,13 @@ namespace Avalonia.Diagnostics.Views
{
if (TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard)
{
var @do = new DataObject();
var text = ToText(selector);
@do.Set(DataFormats.Text, text);
@do.Set(Constants.DataFormats.Avalonia_DevTools_Selector, selector);
clipboard.SetDataObjectAsync(@do);
var dataTransferItem = new DataTransferItem();
dataTransferItem.SetText(ToText(selector));
dataTransferItem.Set(DevToolsDataFormats.Selector, selector);
var dataTransfer = new DataTransfer();
dataTransfer.Add(dataTransferItem);
clipboard.SetDataAsync(dataTransfer);
}
}

25
src/Avalonia.Native/AvaloniaNativeDragSource.cs

@ -31,8 +31,18 @@ namespace Avalonia.Native
_tcs = null;
}
}
public Task<DragDropEffects> DoDragDrop(PointerEventArgs triggerEvent, IDataObject data, DragDropEffects allowedEffects)
[Obsolete($"Use {nameof(DoDragDropAsync)} instead.")]
Task<DragDropEffects> IPlatformDragSource.DoDragDrop(
PointerEventArgs triggerEvent,
IDataObject data,
DragDropEffects allowedEffects)
=> DoDragDropAsync(triggerEvent, new DataObjectToDataTransferWrapper(data), allowedEffects);
public Task<DragDropEffects> DoDragDropAsync(
PointerEventArgs triggerEvent,
IDataTransfer dataTransfer,
DragDropEffects allowedEffects)
{
// Sanity check
var tl = TopLevel.GetTopLevel(triggerEvent.Source as Visual);
@ -43,17 +53,14 @@ namespace Avalonia.Native
triggerEvent.Pointer.Capture(null);
var tcs = new TaskCompletionSource<DragDropEffects>();
var clipboardImpl = _factory.CreateDndClipboard();
using (var clipboard = new ClipboardImpl(clipboardImpl))
using (var cb = new DndCallback(tcs))
{
// Native API is synchronous, so it's OK. For now.
clipboard.SetDataObjectAsync(data).GetAwaiter().GetResult();
var dataSource = new DataTransferToAvnClipboardDataSourceWrapper(dataTransfer);
view.BeginDraggingSession((AvnDragDropEffects)allowedEffects,
triggerEvent.GetPosition(tl).ToAvnPoint(), clipboardImpl, cb,
GCHandle.ToIntPtr(GCHandle.Alloc(data)));
triggerEvent.GetPosition(tl).ToAvnPoint(), dataSource, cb,
GCHandle.ToIntPtr(GCHandle.Alloc(dataTransfer)));
}
return tcs.Task;

6
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -106,6 +106,9 @@ namespace Avalonia.Native
_factory.MacOptions.SetDisableSetProcessName(macOpts.DisableSetProcessName ? 1 : 0);
}
var clipboardImpl = new ClipboardImpl(_factory.CreateClipboard());
var clipboard = new Clipboard(clipboardImpl);
AvaloniaLocator.CurrentMutable
.Bind<IDispatcherImpl>().ToConstant(new DispatcherImpl(_factory.CreatePlatformThreadingInterface()))
.Bind<ICursorFactory>().ToConstant(new CursorFactory(_factory.CreateCursorFactory()))
@ -114,7 +117,8 @@ namespace Avalonia.Native
.Bind<IKeyboardDevice>().ToConstant(KeyboardDevice)
.Bind<IPlatformSettings>().ToConstant(new NativePlatformSettings(_factory.CreatePlatformSettings()))
.Bind<IWindowingPlatform>().ToConstant(this)
.Bind<IClipboard>().ToConstant(new ClipboardImpl(_factory.CreateClipboard()))
.Bind<IClipboardImpl>().ToConstant(clipboardImpl)
.Bind<IClipboard>().ToConstant(clipboard)
.Bind<IRenderTimer>().ToConstant(new ThreadProxyRenderTimer(new AvaloniaNativeRenderTimer(_factory.CreatePlatformRenderTimer())))
.Bind<IMountedVolumeInfoProvider>().ToConstant(new MacOSMountedVolumeInfoProvider())
.Bind<IPlatformDragSource>().ToConstant(new AvaloniaNativeDragSource(_factory))

55
src/Avalonia.Native/ClipboardDataFormatHelper.cs

@ -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);
}
}

46
src/Avalonia.Native/ClipboardDataTransfer.cs

@ -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();
}

94
src/Avalonia.Native/ClipboardDataTransferItem.cs

@ -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;
}
}

234
src/Avalonia.Native/ClipboardImpl.cs

@ -1,238 +1,94 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Logging;
using Avalonia.Native.Interop;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
namespace Avalonia.Native
{
class ClipboardImpl : IClipboard, IDisposable
internal sealed class ClipboardImpl(IAvnClipboard native) : IOwnedClipboardImpl, IDisposable
{
private IAvnClipboard? _native;
private IDataObject? _savedDataObject;
private long _lastClearChangeCount;
private IAvnClipboard? _native = native;
private long _lastClearChangeCount = long.MinValue;
// TODO hide native types behind IAvnClipboard abstraction, so managed side won't depend on macOS.
private const string NSPasteboardTypeString = "public.utf8-plain-text";
private const string NSFilenamesPboardType = "NSFilenamesPboardType";
public ClipboardImpl(IAvnClipboard native)
{
_native = native;
}
private IAvnClipboard Native
internal IAvnClipboard Native
=> _native ?? throw new ObjectDisposedException(nameof(ClipboardImpl));
private void ClearCore()
{
_savedDataObject = null;
_lastClearChangeCount = Native.Clear();
}
public Task ClearAsync()
{
ClearCore();
return Task.CompletedTask;
}
public Task<string?> GetTextAsync()
{
using (var text = Native.GetText(NSPasteboardTypeString))
return Task.FromResult<string?>(text.String);
}
public Task SetTextAsync(string? text)
public Task ClearAsync()
{
var native = Native;
ClearCore();
if (text != null)
native.SetText(NSPasteboardTypeString, text);
return Task.CompletedTask;
try
{
ClearCore();
return Task.CompletedTask;
}
catch (Exception ex)
{
return Task.FromException(ex);
}
}
public IEnumerable<string> GetFormats()
public Task<IAsyncDataTransfer?> TryGetDataAsync()
{
var rv = new HashSet<string>();
using (var formats = Native.ObtainFormats())
try
{
var cnt = formats.Count;
for (uint c = 0; c < cnt; c++)
{
using (var fmt = formats.Get(c))
{
if(fmt.String == NSPasteboardTypeString)
rv.Add(DataFormats.Text);
if (fmt.String == NSFilenamesPboardType)
{
rv.Add(DataFormats.FileNames);
rv.Add(DataFormats.Files);
}
else
rv.Add(fmt.String);
}
}
return Task.FromResult(TryGetData());
}
catch (Exception ex)
{
return Task.FromException<IAsyncDataTransfer?>(ex);
}
return rv;
}
public void Dispose()
{
_savedDataObject = null;
_native?.Dispose();
_native = null;
}
public IEnumerable<string>? GetFileNames()
private IAsyncDataTransfer? TryGetData()
{
using (var strings = Native.GetStrings(NSFilenamesPboardType))
return strings?.ToStringArray();
}
var dataTransfer = new ClipboardDataTransfer(
new ClipboardReadSession(Native, Native.ChangeCount, ownsNative: false));
public IEnumerable<IStorageItem>? GetFiles()
{
var storageApi = (StorageProviderApi)AvaloniaLocator.Current.GetRequiredService<IStorageProviderFactory>();
// TODO: use non-deprecated AppKit API to get NSUri instead of file names.
var fileNames = GetFileNames();
if (fileNames is null)
if (dataTransfer.Formats.Length == 0)
{
dataTransfer.Dispose();
return null;
}
return fileNames
.Select(f => StorageProviderHelpers.TryGetUriFromFilePath(f, false) is { } uri
? storageApi.TryGetStorageItem(uri)
: null)
.Where(f => f is not null)!;
return dataTransfer;
}
public unsafe Task SetDataObjectAsync(IDataObject data)
public Task SetDataAsync(IAsyncDataTransfer dataTransfer)
{
ClearCore();
// If there is multiple values with the same "to" format, prefer these that were not mapped.
var formats = data.GetDataFormats().Select(f =>
{
string from, to;
bool mapped;
if (f == DataFormats.Text)
(from, to, mapped) = (f, NSPasteboardTypeString, true);
else if (f == DataFormats.Files || f == DataFormats.FileNames)
(from, to, mapped) = (f, NSFilenamesPboardType, true);
else (from, to, mapped) = (f, f, false);
return (from, to, mapped);
})
.GroupBy(p => p.to)
.Select(g => g.OrderBy(f => f.mapped).First());
foreach (var (fromFormat, toFormat, _) in formats)
try
{
var o = data.Get(fromFormat);
switch (o)
{
case string s:
Native.SetText(toFormat, s);
break;
case IEnumerable<IStorageItem> storageItems:
using (var strings = new AvnStringArray(storageItems
.Select(s => s.TryGetLocalPath())
.Where(p => p is not null)))
{
Native.SetStrings(toFormat, strings);
}
break;
case IEnumerable<string> managedStrings:
using (var strings = new AvnStringArray(managedStrings))
{
Native.SetStrings(toFormat, strings);
}
break;
case byte[] bytes:
{
fixed (byte* pbytes = bytes)
Native.SetBytes(toFormat, pbytes, bytes.Length);
break;
}
default:
Logger.TryGet(LogEventLevel.Warning, LogArea.macOSPlatform)?.Log(this,
"Unsupported IDataObject value type: {0}", o?.GetType().FullName ?? "(null)");
break;
}
SetData(dataTransfer);
return Task.CompletedTask;
}
catch (Exception ex)
{
return Task.FromException(ex);
}
_savedDataObject = data;
return Task.CompletedTask;
}
public Task<string[]> GetFormatsAsync()
{
return Task.FromResult(GetFormats().ToArray());
}
public async Task<object?> GetDataAsync(string format)
private void SetData(IAsyncDataTransfer dataTransfer)
{
if (format == DataFormats.Text || format == NSPasteboardTypeString)
return await GetTextAsync();
if (format == DataFormats.FileNames || format == NSFilenamesPboardType)
return GetFileNames();
if (format == DataFormats.Files)
return GetFiles();
using (var n = Native.GetBytes(format))
return n.Bytes;
}
ClearCore();
public Task<IDataObject?> TryGetInProcessDataObjectAsync()
{
if (Native.ChangeCount != _lastClearChangeCount)
_savedDataObject = null;
return Task.FromResult(_savedDataObject);
Native.SetData(new DataTransferToAvnClipboardDataSourceWrapper(
dataTransfer.ToSynchronous(LogArea.macOSPlatform)));
}
/// <inheritdoc />
public Task FlushAsync() =>
Task.CompletedTask;
}
class ClipboardDataObject : IDataObject, IDisposable
{
private ClipboardImpl? _clipboard;
private List<string>? _formats;
public ClipboardDataObject(IAvnClipboard clipboard)
{
_clipboard = new ClipboardImpl(clipboard);
}
public Task<bool> IsCurrentOwnerAsync()
=> Task.FromResult(Native.ChangeCount == _lastClearChangeCount);
public void Dispose()
{
_clipboard?.Dispose();
_clipboard = null;
_native?.Dispose();
_native = null;
}
private ClipboardImpl Clipboard
=> _clipboard ?? throw new ObjectDisposedException(nameof(ClipboardDataObject));
private List<string> Formats => _formats ??= Clipboard.GetFormats().ToList();
public IEnumerable<string> GetDataFormats() => Formats;
public bool Contains(string dataFormat) => Formats.Contains(dataFormat);
public object? Get(string dataFormat) => Clipboard.GetDataAsync(dataFormat).GetAwaiter().GetResult();
public Task SetFromDataObjectAsync(IDataObject dataObject) => Clipboard.SetDataObjectAsync(dataObject);
}
}

107
src/Avalonia.Native/ClipboardReadSession.cs

@ -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;
}
}

96
src/Avalonia.Native/DataTransferItemToAvnClipboardDataItemWrapper.cs

@ -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));
}
}

56
src/Avalonia.Native/DataTransferToAvnClipboardDataSourceWrapper.cs

@ -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;
}
}

47
src/Avalonia.Native/NativeOwned.cs

@ -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));
}
}

8
src/Avalonia.Native/StorageProviderApi.cs

@ -216,6 +216,14 @@ internal class StorageProviderApi(IAvnStorageProvider native, bool sandboxEnable
.Where(f => f is not null) ?? [])!;
}
public Uri? TryResolveFileReferenceUri(Uri uri)
{
using var uriString = new AvnString(uri.AbsoluteUri);
using var resultString = _native.TryResolveFileReferenceUri(uriString);
return Uri.TryCreate(resultString?.String, UriKind.Absolute, out var resultUri) ? resultUri : null;
}
internal class FilePickerFileTypesWrapper(
IReadOnlyList<FilePickerFileType>? types,
string? defaultExtension)

44
src/Avalonia.Native/TopLevelImpl.cs

@ -105,9 +105,14 @@ internal class TopLevelImpl : ITopLevelImpl, IFramebufferPlatformSurface
InputMethod = new AvaloniaNativeTextInputMethod(Native);
}
internal void BeginDraggingSession(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard clipboard, IAvnDndResultCallback callback, IntPtr sourceHandle)
internal void BeginDraggingSession(
AvnDragDropEffects effects,
AvnPoint point,
IAvnClipboardDataSource source,
IAvnDndResultCallback callback,
IntPtr sourceHandle)
{
Native?.BeginDragAndDropOperation(effects, point, clipboard, callback, sourceHandle);
Native?.BeginDragAndDropOperation(effects, point, source, callback, sourceHandle);
}
public double DesktopScaling => 1;
@ -484,7 +489,7 @@ internal class TopLevelImpl : ITopLevelImpl, IFramebufferPlatformSurface
AvnDragDropEffects IAvnTopLevelEvents.DragEvent(AvnDragEventType type, AvnPoint position,
AvnInputModifiers modifiers,
AvnDragDropEffects effects,
IAvnClipboard clipboard, IntPtr dataObjectHandle)
IAvnClipboard clipboard, IntPtr dataTransferHandle)
{
var device = AvaloniaLocator.Current.GetService<IDragDropDevice>();
@ -498,21 +503,24 @@ internal class TopLevelImpl : ITopLevelImpl, IFramebufferPlatformSurface
return AvnDragDropEffects.None;
}
IDataObject? dataObject = null;
if (dataObjectHandle != IntPtr.Zero)
dataObject = GCHandle.FromIntPtr(dataObjectHandle).Target as IDataObject;
using (var clipboardDataObject = new ClipboardDataObject(clipboard))
{
if (dataObject == null)
dataObject = clipboardDataObject;
var args = new RawDragEvent(device, (RawDragEventType)type,
_parent._inputRoot, position.ToAvaloniaPoint(), dataObject, (DragDropEffects)effects,
(RawInputModifiers)modifiers);
_parent.Input?.Invoke(args);
return (AvnDragDropEffects)args.Effects;
}
IDataTransfer? dataTransfer = null;
if (dataTransferHandle != IntPtr.Zero)
dataTransfer = GCHandle.FromIntPtr(dataTransferHandle).Target as IDataTransfer;
using var clipboardDataTransfer = new ClipboardDataTransfer(
new ClipboardReadSession(clipboard, clipboard.ChangeCount, ownsNative: true));
dataTransfer ??= clipboardDataTransfer;
var args = new RawDragEvent(
device,
(RawDragEventType)type,
_parent._inputRoot,
position.ToAvaloniaPoint(),
dataTransfer,
(DragDropEffects)effects,
(RawInputModifiers)modifiers);
_parent.Input?.Invoke(args);
return (AvnDragDropEffects)args.Effects;
}
IAvnAutomationPeer? IAvnTopLevelEvents.AutomationPeer

57
src/Avalonia.Native/avn.idl

@ -687,7 +687,6 @@ interface IAvaloniaNativeFactory : IUnknown
HRESULT CreateStorageProvider(IAvnStorageProvider** ppv);
HRESULT CreateScreens(IAvnScreenEvents* cb, IAvnScreens** ppv);
HRESULT CreateClipboard(IAvnClipboard** ppv);
HRESULT CreateDndClipboard(IAvnClipboard** ppv);
HRESULT CreateCursorFactory(IAvnCursorFactory** ppv);
HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv);
HRESULT ObtainMetalDisplay(IAvnMetalDisplay** ppv);
@ -736,8 +735,12 @@ interface IAvnTopLevel : IUnknown
HRESULT GetCurrentDisplayId(uint* ret);
HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point,
IAvnClipboard* clipboard, IAvnDndResultCallback* cb, [intptr]void* sourceHandle);
HRESULT BeginDragAndDropOperation(
AvnDragDropEffects effects,
AvnPoint point,
IAvnClipboardDataSource* source,
IAvnDndResultCallback* callback,
[intptr]void* sourceHandle);
}
[uuid(e5aca675-02b7-4129-aa79-d6e417210bda), cpp-virtual-inherits]
@ -817,7 +820,7 @@ interface IAvnTopLevelEvents : IUnknown
AvnDragDropEffects DragEvent(AvnDragEventType type, AvnPoint position,
AvnInputModifiers modifiers, AvnDragDropEffects effects,
IAvnClipboard* clipboard, [intptr]void* dataObjectHandle);
IAvnClipboard* clipboard, [intptr]void* dataTransferHandle);
}
[uuid(939b6599-40a8-4710-a4c8-5d72d8f174fb)]
@ -936,7 +939,9 @@ interface IAvnStorageProvider : IUnknown
void ReleaseBookmark(IAvnString*fileUri);
bool OpenSecurityScope(IAvnString*fileUri);
void CloseSecurityScope(IAvnString*fileUri);
void CloseSecurityScope(IAvnString*fileUri);
HRESULT TryResolveFileReferenceUri(IAvnString* fileUri, IAvnString** ret);
}
[uuid(4d7ab7db-a111-406f-abeb-11cb6aa033d5)]
@ -968,16 +973,38 @@ interface IAvnScreens : IUnknown
[uuid(792b1bd4-76cc-46ea-bfd0-9d642154b1b3)]
interface IAvnClipboard : IUnknown
{
HRESULT GetText(char* type, IAvnString**ppv);
HRESULT SetText(char* type, char* utf8Text);
HRESULT ObtainFormats(IAvnStringArray**ppv);
HRESULT GetStrings(char* type, IAvnStringArray**ppv);
HRESULT SetStrings(char* type, IAvnStringArray*ppv);
HRESULT SetBytes(char* type, void* utf8Text, int len);
HRESULT GetBytes(char* type, IAvnString**ppv);
HRESULT Clear(int64_t* ret);
HRESULT GetChangeCount(int64_t* ret);
HRESULT GetFormats(int64_t changeCount, IAvnStringArray** ret);
HRESULT GetItemCount(int64_t changeCount, int* ret);
HRESULT GetItemFormats(int index, int64_t changeCount, IAvnStringArray** ret);
HRESULT GetItemValueAsString(int index, int64_t changeCount, [const] char* format, IAvnString** ret);
HRESULT GetItemValueAsBytes(int index, int64_t changeCount, [const] char* format, IAvnString** ret);
HRESULT Clear(int64_t* ret);
HRESULT GetChangeCount(int64_t* ret);
HRESULT SetData(IAvnClipboardDataSource* dataSource);
bool IsTextFormat([const] char* format);
}
[uuid(10b39f02-efcb-428b-bee5-a0b012c1fb7d)]
interface IAvnClipboardDataSource : IUnknown
{
int GetItemCount();
IAvnClipboardDataItem* GetItem(int index);
}
[uuid(e40f36d9-69f4-45fd-9ca2-6e64e80feb6d)]
interface IAvnClipboardDataItem : IUnknown
{
IAvnStringArray* ProvideFormats();
IAvnClipboardDataValue* GetValue([const] char* format);
}
[uuid(e97f24f6-1c84-4d95-8ffe-5b2c72e016ed)]
interface IAvnClipboardDataValue : IUnknown
{
bool IsString();
IAvnString* AsString();
int GetByteLength();
void CopyBytesTo(void* buffer);
}
[uuid(3f998545-f027-4d4d-bd2a-1a80926d984e)]

92
src/Avalonia.X11/Clipboard/ClipboardDataFormatHelper.cs

@ -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;
}
}

73
src/Avalonia.X11/Clipboard/ClipboardDataReader.cs

@ -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;
}

34
src/Avalonia.X11/Clipboard/ClipboardDataTransfer.cs

@ -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();
}

23
src/Avalonia.X11/Clipboard/ClipboardDataTransferItem.cs

@ -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);
}

1
src/Avalonia.X11/Clipboard/ClipboardReadSession.cs

@ -4,6 +4,7 @@ using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using static Avalonia.X11.XLib;
namespace Avalonia.X11.Clipboard;
class ClipboardReadSession : IDisposable

52
src/Avalonia.X11/Clipboard/ClipboardUriListHelper.cs

@ -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();
}
}

2
src/Avalonia.X11/Clipboard/EventStreamWindow.cs

@ -5,7 +5,7 @@ using System.Linq;
using System.Threading.Tasks;
using Avalonia.Threading;
namespace Avalonia.X11;
namespace Avalonia.X11.Clipboard;
internal class EventStreamWindow : IDisposable
{

337
src/Avalonia.X11/Clipboard/X11Clipboard.cs

@ -3,27 +3,28 @@ using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.X11.Clipboard;
using Avalonia.Logging;
using Avalonia.Platform.Storage;
using static Avalonia.X11.XLib;
namespace Avalonia.X11
namespace Avalonia.X11.Clipboard
{
internal class X11Clipboard : IClipboard
internal sealed class X11ClipboardImpl : IOwnedClipboardImpl
{
private readonly AvaloniaX11Platform _platform;
private readonly X11Info _x11;
private IDataObject? _storedDataObject;
private IntPtr _handle;
private IAsyncDataTransfer? _storedDataTransfer;
private readonly IntPtr _handle;
private TaskCompletionSource<bool>? _storeAtomTcs;
private readonly IntPtr[] _textAtoms;
private readonly IntPtr _avaloniaSaveTargetsAtom;
private int _maximumPropertySize;
private readonly int _maximumPropertySize;
public X11Clipboard(AvaloniaX11Platform platform)
public X11ClipboardImpl(AvaloniaX11Platform platform)
{
_platform = platform;
_x11 = platform.Info;
@ -45,22 +46,11 @@ namespace Avalonia.X11
: extendedMaxRequestSize).ToInt64() - 0x100);
}
private Encoding? GetStringEncoding(IntPtr atom)
{
return (atom == _x11.Atoms.XA_STRING
|| atom == _x11.Atoms.OEMTEXT)
? Encoding.ASCII
: atom == _x11.Atoms.UTF8_STRING
? Encoding.UTF8
: atom == _x11.Atoms.UTF16_STRING
? Encoding.Unicode
: null;
}
private unsafe void OnEvent(ref XEvent ev)
{
if (ev.type == XEventName.SelectionClear)
{
_storedDataTransfer = null;
_storeAtomTcs?.TrySetResult(true);
return;
}
@ -92,10 +82,9 @@ namespace Avalonia.X11
IntPtr WriteTargetToProperty(IntPtr target, IntPtr window, IntPtr property)
{
Encoding? textEnc;
if (target == _x11.Atoms.TARGETS)
{
var atoms = ConvertDataObject(_storedDataObject);
var atoms = ConvertDataTransfer(_storedDataTransfer);
XChangeProperty(_x11.Display, window, property,
_x11.Atoms.XA_ATOM, 32, PropertyMode.Replace, atoms, atoms.Length);
return property;
@ -104,15 +93,15 @@ namespace Avalonia.X11
{
return property;
}
else if ((textEnc = GetStringEncoding(target)) != null
&& _storedDataObject?.Contains(DataFormats.Text) == true)
else if (ClipboardDataFormatHelper.ToDataFormat(target, _x11.Atoms) is { } dataFormat)
{
var text = _storedDataObject.GetText();
if (text == null)
if (_storedDataTransfer is null || !_storedDataTransfer.Contains(dataFormat))
return IntPtr.Zero;
if (TryGetDataAsBytes(_storedDataTransfer, dataFormat, target) is not { } bytes)
return IntPtr.Zero;
var data = textEnc.GetBytes(text);
SendDataToClient(window, property, target, data);
_ = SendDataToClientAsync(window, property, target, bytes);
return property;
}
else if (target == _x11.Atoms.MULTIPLE && _x11.Atoms.MULTIPLE != IntPtr.Zero)
@ -140,29 +129,48 @@ namespace Avalonia.X11
return property;
}
else if (_x11.Atoms.GetAtomName(target) is { } atomName &&
_storedDataObject?.Contains(atomName) == true)
{
var objValue = _storedDataObject.Get(atomName);
if (!(objValue is byte[] bytes))
{
if (objValue is string s)
bytes = Encoding.UTF8.GetBytes(s);
else
return IntPtr.Zero;
}
SendDataToClient(window, property, target, bytes);
return property;
}
else
return IntPtr.Zero;
}
}
async void SendIncrDataToClient(IntPtr window, IntPtr property, IntPtr target, Stream data)
private byte[]? TryGetDataAsBytes(IAsyncDataTransfer dataTransfer, DataFormat format, IntPtr targetFormatAtom)
{
if (DataFormat.Text.Equals(format))
{
var text = dataTransfer.TryGetValueAsync(DataFormat.Text).GetAwaiter().GetResult();
return ClipboardDataFormatHelper.TryGetStringEncoding(targetFormatAtom, _x11.Atoms) is { } encoding ?
encoding.GetBytes(text ?? string.Empty) :
null;
}
if (DataFormat.File.Equals(format))
{
if (dataTransfer.TryGetValuesAsync(DataFormat.File).GetAwaiter().GetResult() is not { } files)
return null;
return ClipboardUriListHelper.FileUriListToUtf8Bytes(files);
}
if (format is DataFormat<string> stringFormat)
{
return dataTransfer.TryGetValueAsync(stringFormat).GetAwaiter().GetResult() is { } stringValue ?
Encoding.UTF8.GetBytes(stringValue) :
null;
}
if (format is DataFormat<byte[]> bytesFormat)
return dataTransfer.TryGetValueAsync(bytesFormat).GetAwaiter().GetResult();
Logger.TryGet(LogEventLevel.Warning, LogArea.X11Platform)
?.Log(this, "Unsupported data format {Format}", format);
return null;
}
private async Task SendIncrDataToClientAsync(IntPtr window, IntPtr property, IntPtr target, Stream data)
{
data.Position = 0;
using var events = new EventStreamWindow(_platform, window);
@ -188,182 +196,169 @@ namespace Avalonia.X11
XChangeProperty(_x11.Display, window, property, target, 8, PropertyMode.Replace, IntPtr.Zero, 0);
}
void SendDataToClient(IntPtr window, IntPtr property, IntPtr target, byte[] bytes)
private Task SendDataToClientAsync(IntPtr window, IntPtr property, IntPtr target, byte[] bytes)
{
if (bytes.Length < _maximumPropertySize)
{
XChangeProperty(_x11.Display, window, property, target, 8,
PropertyMode.Replace,
bytes, bytes.Length);
return Task.CompletedTask;
}
else
SendIncrDataToClient(window, property, target, new MemoryStream(bytes));
return SendIncrDataToClientAsync(window, property, target, new MemoryStream(bytes));
}
private bool HasOwner => XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD) != IntPtr.Zero;
private IntPtr GetOwner()
=> XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD);
private ClipboardReadSession OpenReadSession() => new(_platform);
public async Task<string?> GetTextAsync()
{
if (!HasOwner)
return null;
if (TryGetInProcessDataObject() is { } inProc)
return inProc.GetText();
using var session = OpenReadSession();
var res = await session.SendFormatRequest();
var target = _x11.Atoms.UTF8_STRING;
if (res != null)
{
var preferredFormats = new[] {_x11.Atoms.UTF16_STRING, _x11.Atoms.UTF8_STRING, _x11.Atoms.XA_STRING};
foreach (var pf in preferredFormats)
if (res.Contains(pf))
{
target = pf;
break;
}
}
return ConvertData(await session.SendDataRequest(target)) as string;
}
private object? ConvertData(ClipboardReadSession.GetDataResult? result)
{
if (result == null)
return null;
if (GetStringEncoding(result.TypeAtom) is { } textEncoding)
return textEncoding.GetString(result.AsBytes());
// TODO: image encoding
return result.AsBytes();
}
private IntPtr[] ConvertDataObject(IDataObject? data)
private IntPtr[] ConvertDataTransfer(IAsyncDataTransfer? dataTransfer)
{
var atoms = new HashSet<IntPtr> { _x11.Atoms.TARGETS, _x11.Atoms.MULTIPLE };
var atoms = new List<IntPtr> { _x11.Atoms.TARGETS, _x11.Atoms.MULTIPLE };
if (data is not null)
if (dataTransfer is not null)
{
foreach (var fmt in data.GetDataFormats())
foreach (var format in dataTransfer.Formats)
{
if (fmt == DataFormats.Text)
foreach (var ta in _textAtoms)
atoms.Add(ta);
else
atoms.Add(_x11.Atoms.GetAtom(fmt));
foreach (var atom in ClipboardDataFormatHelper.ToAtoms(format, _textAtoms, _x11.Atoms))
atoms.Add(atom);
}
}
return atoms.ToArray();
}
private Task StoreAtomsInClipboardManager(IDataObject data)
private Task StoreAtomsInClipboardManager(IAsyncDataTransfer dataTransfer)
{
// Skip storing atoms if the data object contains any non-trivial formats or trivial formats are too big
if (data.GetDataFormats().Any(f => f != DataFormats.Text)
|| data.GetText()?.Length * 2 > 64 * 1024
)
if (_x11.Atoms.CLIPBOARD_MANAGER == IntPtr.Zero || _x11.Atoms.SAVE_TARGETS == IntPtr.Zero)
return Task.CompletedTask;
if (_x11.Atoms.CLIPBOARD_MANAGER != IntPtr.Zero && _x11.Atoms.SAVE_TARGETS != IntPtr.Zero)
{
var clipboardManager = XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD_MANAGER);
if (clipboardManager != IntPtr.Zero)
{
if (_storeAtomTcs == null || _storeAtomTcs.Task.IsCompleted)
_storeAtomTcs = new TaskCompletionSource<bool>();
var atoms = ConvertDataObject(data);
XChangeProperty(_x11.Display, _handle, _avaloniaSaveTargetsAtom, _x11.Atoms.XA_ATOM, 32,
PropertyMode.Replace,
atoms, atoms.Length);
XConvertSelection(_x11.Display, _x11.Atoms.CLIPBOARD_MANAGER, _x11.Atoms.SAVE_TARGETS,
_avaloniaSaveTargetsAtom, _handle, IntPtr.Zero);
return _storeAtomTcs.Task;
}
}
return Task.CompletedTask;
}
public Task SetTextAsync(string? text)
{
var data = new DataObject();
var clipboardManager = XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD_MANAGER);
if (clipboardManager == IntPtr.Zero)
return Task.CompletedTask;
// Skip storing atoms if the data object contains any non-trivial formats
if (dataTransfer.Formats.Any(f => !DataFormat.Text.Equals(f)))
return Task.CompletedTask;
if (text is not null)
data.Set(DataFormats.Text, text);
return StoreTextCoreAsync();
return SetDataObjectAsync(data);
async Task StoreTextCoreAsync()
{
// Skip storing atoms if the trivial formats are too big
var text = await dataTransfer.TryGetTextAsync();
if (text is null || text.Length * 2 > 64 * 1024)
return;
if (_storeAtomTcs is null || _storeAtomTcs.Task.IsCompleted)
_storeAtomTcs = new TaskCompletionSource<bool>();
var atoms = ConvertDataTransfer(dataTransfer);
XChangeProperty(_x11.Display, _handle, _avaloniaSaveTargetsAtom, _x11.Atoms.XA_ATOM, 32,
PropertyMode.Replace, atoms, atoms.Length);
XConvertSelection(_x11.Display, _x11.Atoms.CLIPBOARD_MANAGER, _x11.Atoms.SAVE_TARGETS,
_avaloniaSaveTargetsAtom, _handle, IntPtr.Zero);
await _storeAtomTcs.Task;
}
}
public Task ClearAsync()
{
return SetTextAsync(string.Empty);
_storedDataTransfer = null;
XSetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD, IntPtr.Zero, IntPtr.Zero);
return Task.CompletedTask;
}
public Task SetDataObjectAsync(IDataObject data)
public async Task<IAsyncDataTransfer?> TryGetDataAsync()
{
_storedDataObject = data;
XSetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD, _handle, IntPtr.Zero);
return StoreAtomsInClipboardManager(data);
}
var owner = GetOwner();
if (owner == IntPtr.Zero)
return null;
private IDataObject? TryGetInProcessDataObject()
{
if (XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD) == _handle)
return _storedDataObject;
return null;
}
if (owner == _handle && _storedDataTransfer is { } storedDataTransfer)
return storedDataTransfer;
// Get the formats while we're in an async method, since IAsyncDataTransfer.GetFormats() is synchronous.
var (dataFormats, textFormatAtoms) = await GetDataFormatsCoreAsync().ConfigureAwait(false);
if (dataFormats.Length == 0)
return null;
public Task<IDataObject?> TryGetInProcessDataObjectAsync() => Task.FromResult(TryGetInProcessDataObject());
// Get the items while we're in an async method. This does not get values, except for DataFormat.File.
var reader = new ClipboardDataReader(_x11, _platform, textFormatAtoms, owner);
var items = await CreateItemsAsync(reader, dataFormats);
return new ClipboardDataTransfer(reader, dataFormats, items);
}
public async Task<string[]> GetFormatsAsync()
private async Task<(DataFormat[] DataFormats, IntPtr[] TextFormatAtoms)> GetDataFormatsCoreAsync()
{
if (!HasOwner)
return [];
if (TryGetInProcessDataObject() is { } inProc)
return inProc.GetDataFormats().ToArray();
using var session = OpenReadSession();
var res = await session.SendFormatRequest();
if (res == null)
return [];
var rv = new List<string>();
if (_textAtoms.Any(res.Contains))
rv.Add(DataFormats.Text);
var formatAtoms = await session.SendFormatRequest();
if (formatAtoms is null)
return ([], []);
var formats = new List<DataFormat>(formatAtoms.Length);
List<IntPtr>? textFormatAtoms = null;
foreach (var t in res)
foreach (var formatAtom in formatAtoms)
{
if (_x11.Atoms.GetAtomName(t) is { } atomName)
rv.Add(atomName);
if (ClipboardDataFormatHelper.ToDataFormat(formatAtom, _x11.Atoms) is not { } format)
continue;
if (DataFormat.Text.Equals(format))
{
if (textFormatAtoms is null)
{
formats.Add(format);
textFormatAtoms = [];
}
textFormatAtoms.Add(formatAtom);
}
else
formats.Add(format);
}
return rv.ToArray();
return (formats.ToArray(), textFormatAtoms?.ToArray() ?? []);
}
public async Task<object?> GetDataAsync(string format)
private static async Task<IAsyncDataTransferItem[]> CreateItemsAsync(ClipboardDataReader reader, DataFormat[] formats)
{
if (!HasOwner)
return null;
if(TryGetInProcessDataObject() is {} inProc)
return inProc.Get(format);
if (format == DataFormats.Text)
return await GetTextAsync();
List<DataFormat>? nonFileFormats = null;
var items = new List<IAsyncDataTransferItem>();
var formatAtom = _x11.Atoms.GetAtom(format);
using var session = OpenReadSession();
var res = await session.SendFormatRequest();
if (res is null || !res.Contains(formatAtom))
return null;
foreach (var format in formats)
{
if (DataFormat.File.Equals(format))
{
// We're reading the filenames ahead of time to generate the appropriate items.
// This is async, so it should be fine.
if (await reader.TryGetAsync(format) is IEnumerable<IStorageItem> storageItems)
{
foreach (var storageItem in storageItems)
items.Add(PlatformDataTransferItem.Create(DataFormat.File, storageItem));
}
}
else
(nonFileFormats ??= new()).Add(format);
}
return ConvertData(await session.SendDataRequest(formatAtom));
// Single item containing all formats except for DataFormat.File.
if (nonFileFormats is not null)
items.Add(new ClipboardDataTransferItem(reader, formats));
return items.ToArray();
}
public Task SetDataAsync(IAsyncDataTransfer dataTransfer)
{
_storedDataTransfer = dataTransfer;
XSetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD, _handle, IntPtr.Zero);
return StoreAtomsInClipboardManager(dataTransfer);
}
/// <inheritdoc />
public Task FlushAsync() =>
Task.CompletedTask;
public Task<bool> IsCurrentOwnerAsync()
=> Task.FromResult(GetOwner() == _handle);
}
}

7
src/Avalonia.X11/X11Platform.cs

@ -15,6 +15,7 @@ using Avalonia.Rendering.Composition;
using Avalonia.Threading;
using Avalonia.Vulkan;
using Avalonia.X11;
using Avalonia.X11.Clipboard;
using Avalonia.X11.Dispatching;
using Avalonia.X11.Glx;
using Avalonia.X11.Vulkan;
@ -71,6 +72,9 @@ namespace Avalonia.X11
? new UiThreadRenderTimer(60)
: new SleepLoopRenderTimer(60);
var clipboardImpl = new X11ClipboardImpl(this);
var clipboard = new Input.Platform.Clipboard(clipboardImpl);
AvaloniaLocator.CurrentMutable.BindToSelf(this)
.Bind<IWindowingPlatform>().ToConstant(this)
.Bind<IDispatcherImpl>().ToConstant<IDispatcherImpl>(options.UseGLibMainLoop
@ -81,7 +85,8 @@ namespace Avalonia.X11
.Bind<KeyGestureFormatInfo>().ToConstant(new KeyGestureFormatInfo(new Dictionary<Key, string>() { }, meta: "Super"))
.Bind<IKeyboardDevice>().ToFunc(() => KeyboardDevice)
.Bind<ICursorFactory>().ToConstant(new X11CursorFactory(Display))
.Bind<IClipboard>().ToLazy(() => new X11Clipboard(this))
.Bind<IClipboardImpl>().ToConstant(clipboardImpl)
.Bind<IClipboard>().ToConstant(clipboard)
.Bind<IPlatformSettings>().ToSingleton<DBusPlatformSettings>()
.Bind<IPlatformIconLoader>().ToConstant(new X11IconLoader())
.Bind<IMountedVolumeInfoProvider>().ToConstant(new LinuxMountedVolumeInfoProvider())

40
src/Browser/Avalonia.Browser/BrowserClipboardDataTransfer.cs

@ -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();
}
}
}

32
src/Browser/Avalonia.Browser/BrowserClipboardDataTransferItem.cs

@ -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();
}

34
src/Browser/Avalonia.Browser/BrowserDataFormatHelper.cs

@ -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);
}
}

91
src/Browser/Avalonia.Browser/BrowserDataObject.cs

@ -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;
}
}

62
src/Browser/Avalonia.Browser/BrowserDataTransferHelper.cs

@ -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;
}
}

40
src/Browser/Avalonia.Browser/BrowserDragDataTransfer.cs

@ -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();
}
}
}

31
src/Browser/Avalonia.Browser/BrowserDragDataTransferItem.cs

@ -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();
}

13
src/Browser/Avalonia.Browser/BrowserInputHandler.cs

@ -169,7 +169,7 @@ internal class BrowserInputHandler
(RawInputModifiers)modifier);
}
public bool OnDragEvent(string type, double offsetX, double offsetY, int modifiers, string? effectAllowedStr, JSObject? dataTransfer)
public bool OnDragEvent(string type, double offsetX, double offsetY, int modifiers, JSObject dataTransfer, JSObject items)
{
var eventType = type switch
{
@ -179,7 +179,7 @@ internal class BrowserInputHandler
"drop" => RawDragEventType.Drop,
_ => (RawDragEventType)(int)-1
};
if (eventType < 0 || dataTransfer is null)
if (eventType < 0)
{
return false;
}
@ -190,8 +190,9 @@ internal class BrowserInputHandler
var position = new Point(offsetX, offsetY);
effectAllowedStr ??= "none";
var effectAllowedStr = dataTransfer.GetPropertyAsString("effectAllowed") ?? "none";
var effectAllowed = DragDropEffects.None;
if (effectAllowedStr.Contains("copy", StringComparison.OrdinalIgnoreCase))
{
effectAllowed |= DragDropEffects.Copy;
@ -217,7 +218,7 @@ internal class BrowserInputHandler
return false;
}
var dropEffect = RawDragEvent(eventType, position, (RawInputModifiers)modifiers, new BrowserDataObject(dataTransfer), effectAllowed);
var dropEffect = RawDragEvent(eventType, position, (RawInputModifiers)modifiers, new BrowserDragDataTransfer(items), effectAllowed);
dataTransfer.SetProperty("dropEffect", dropEffect.ToString().ToLowerInvariant());
// Note, due to complications of JS interop, we ignore this return value.
@ -341,10 +342,10 @@ internal class BrowserInputHandler
}
private DragDropEffects RawDragEvent(RawDragEventType eventType, Point position, RawInputModifiers modifiers,
BrowserDataObject dataObject, DragDropEffects dropEffect)
BrowserDragDataTransfer dataTransfer, DragDropEffects dropEffect)
{
var device = AvaloniaLocator.Current.GetRequiredService<IDragDropDevice>();
var eventArgs = new RawDragEvent(device, eventType, _inputRoot!, position, dataObject, dropEffect, modifiers);
var eventArgs = new RawDragEvent(device, eventType, _inputRoot!, position, dataTransfer, dropEffect, modifiers);
ScheduleInput(eventArgs);
return eventArgs.Effects;
}

4
src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs

@ -28,7 +28,7 @@ namespace Avalonia.Browser
private readonly INativeControlHostImpl _nativeControlHost;
private readonly IStorageProvider _storageProvider;
private readonly ClipboardImpl _clipboard;
private readonly Clipboard _clipboard;
private readonly IInsetsManager _insetsManager;
private readonly JSObject _container;
private readonly BrowserInputHandler _inputHandler;
@ -60,7 +60,7 @@ namespace Avalonia.Browser
_insetsManager = new BrowserInsetsManager();
_nativeControlHost = new BrowserNativeControlHost(nativeControlHost);
_storageProvider = new BrowserStorageProvider();
_clipboard = new ClipboardImpl();
_clipboard = new Clipboard(new ClipboardImpl());
_container = container;

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save