diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 2ae456750d..59ec89b7ac 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -24,6 +24,9 @@ struct IAvnGlSurfaceRenderTarget; struct IAvnGlSurfaceRenderingSession; struct IAvnAppMenu; struct IAvnAppMenuItem; +struct IAvnStringArray; +struct IAvnDndResultCallback; +struct IAvnGCHandleDeallocatorCallback; struct AvnSize { @@ -115,6 +118,22 @@ enum AvnInputModifiers MiddleMouseButton = 64 }; +enum class AvnDragDropEffects +{ + None = 0, + Copy = 1, + Move = 2, + Link = 4, +}; + +enum class AvnDragEventType +{ + Enter, + Over, + Leave, + Drop +}; + enum AvnWindowState { Normal, @@ -165,7 +184,7 @@ enum AvnWindowEdge AVNCOM(IAvaloniaNativeFactory, 01) : IUnknown { public: - virtual HRESULT Initialize() = 0; + virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator) = 0; virtual IAvnMacOptions* GetMacOptions() = 0; virtual HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnWindow** ppv) = 0; virtual HRESULT CreatePopup (IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnPopup** ppv) = 0; @@ -173,6 +192,7 @@ public: virtual HRESULT CreateSystemDialogs (IAvnSystemDialogs** ppv) = 0; virtual HRESULT CreateScreens (IAvnScreens** ppv) = 0; virtual HRESULT CreateClipboard(IAvnClipboard** ppv) = 0; + virtual HRESULT CreateDndClipboard(IAvnClipboard** ppv) = 0; virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) = 0; virtual HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv) = 0; virtual HRESULT ObtainAppMenu(IAvnAppMenu** retOut) = 0; @@ -215,6 +235,8 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown virtual HRESULT ObtainNSWindowHandleRetained(void** retOut) = 0; virtual HRESULT ObtainNSViewHandle(void** retOut) = 0; virtual HRESULT ObtainNSViewHandleRetained(void** retOut) = 0; + virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, + IAvnClipboard* clipboard, IAvnDndResultCallback* cb, void* sourceHandle) = 0; }; AVNCOM(IAvnPopup, 03) : virtual IAvnWindowBase @@ -250,6 +272,9 @@ AVNCOM(IAvnWindowBaseEvents, 05) : IUnknown virtual bool RawTextInputEvent (unsigned int timeStamp, const char* text) = 0; virtual void ScalingChanged(double scaling) = 0; virtual void RunRenderPriorityJobs() = 0; + virtual AvnDragDropEffects DragEvent(AvnDragEventType type, AvnPoint position, + AvnInputModifiers modifiers, AvnDragDropEffects effects, + IAvnClipboard* clipboard, void* dataObjectHandle) = 0; }; @@ -333,8 +358,10 @@ AVNCOM(IAvnScreens, 0e) : IUnknown AVNCOM(IAvnClipboard, 0f) : IUnknown { - virtual HRESULT GetText (IAvnString**ppv) = 0; - virtual HRESULT SetText (void* utf8Text) = 0; + virtual HRESULT GetText (char* type, IAvnString**ppv) = 0; + virtual HRESULT SetText (char* type, void* utf8Text) = 0; + virtual HRESULT ObtainFormats(IAvnStringArray**ppv) = 0; + virtual HRESULT GetStrings(char* type, IAvnStringArray**ppv) = 0; virtual HRESULT Clear() = 0; }; @@ -396,4 +423,20 @@ AVNCOM(IAvnAppMenuItem, 19) : IUnknown virtual HRESULT SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) = 0; }; +AVNCOM(IAvnStringArray, 20) : IUnknown +{ + virtual unsigned int GetCount() = 0; + virtual HRESULT Get(unsigned int index, IAvnString**ppv) = 0; +}; + +AVNCOM(IAvnDndResultCallback, 21) : IUnknown +{ + virtual void OnDragAndDropComplete(AvnDragDropEffects effecct) = 0; +}; + +AVNCOM(IAvnGCHandleDeallocatorCallback, 22) : IUnknown +{ + virtual void FreeGCHandle(void* handle) = 0; +}; + extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative(); diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index 50a85bdf9f..ea28780c81 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 1A3E5EAA23E9F26C00EDE661 /* IOSurface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */; }; 1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EAD23E9FB1300EDE661 /* cgl.mm */; }; 1A3E5EB023E9FE8300EDE661 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */; }; + 1A465D10246AB61600C5858B /* dnd.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A465D0F246AB61600C5858B /* dnd.mm */; }; 37155CE4233C00EB0034DCE9 /* menu.h in Headers */ = {isa = PBXBuildFile; fileRef = 37155CE3233C00EB0034DCE9 /* menu.h */; }; 37A517B32159597E00FBA241 /* Screens.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37A517B22159597E00FBA241 /* Screens.mm */; }; 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* SystemDialogs.mm */; }; @@ -33,6 +34,7 @@ 1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOSurface.framework; path = System/Library/Frameworks/IOSurface.framework; sourceTree = SDKROOT; }; 1A3E5EAD23E9FB1300EDE661 /* cgl.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cgl.mm; sourceTree = ""; }; 1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; + 1A465D0F246AB61600C5858B /* dnd.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = dnd.mm; sourceTree = ""; }; 37155CE3233C00EB0034DCE9 /* menu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = menu.h; sourceTree = ""; }; 379860FE214DA0C000CD0246 /* KeyTransform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyTransform.h; sourceTree = ""; }; 37A4E71A2178846A00EACBCD /* headers */ = {isa = PBXFileReference; lastKnownFileType = folder; name = headers; path = ../../inc; sourceTree = ""; }; @@ -92,6 +94,7 @@ 5BF943652167AD1D009CAE35 /* cursor.h */, 5B21A981216530F500CEE36E /* cursor.mm */, 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */, + 1A465D0F246AB61600C5858B /* dnd.mm */, AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */, AB661C212148288600291242 /* common.h */, 379860FE214DA0C000CD0246 /* KeyTransform.h */, @@ -196,6 +199,7 @@ 37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */, 520624B322973F4100C4DCEF /* menu.mm in Sources */, 37A517B32159597E00FBA241 /* Screens.mm in Sources */, + 1A465D10246AB61600C5858B /* dnd.mm in Sources */, AB00E4F72147CA920032A60A /* main.mm in Sources */, 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */, AB661C202148286E00291242 /* window.mm in Sources */, diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme index 1a665d3ea5..1e91b66652 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme @@ -58,7 +58,7 @@ diff --git a/native/Avalonia.Native/src/OSX/AvnString.h b/native/Avalonia.Native/src/OSX/AvnString.h index 9a8f5a1318..88bc4e6963 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.h +++ b/native/Avalonia.Native/src/OSX/AvnString.h @@ -10,5 +10,6 @@ #define AvnString_h extern IAvnString* CreateAvnString(NSString* string); - +extern IAvnStringArray* CreateAvnStringArray(NSArray* array); +extern IAvnStringArray* CreateAvnStringArray(NSString* string); #endif /* AvnString_h */ diff --git a/native/Avalonia.Native/src/OSX/AvnString.mm b/native/Avalonia.Native/src/OSX/AvnString.mm index b62fe8a968..6445a9fef1 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.mm +++ b/native/Avalonia.Native/src/OSX/AvnString.mm @@ -7,6 +7,7 @@ // #include "common.h" +#include class AvnStringImpl : public virtual ComSingleObject { @@ -61,7 +62,55 @@ public: } }; +class AvnStringArrayImpl : public virtual ComSingleObject +{ +private: + std::vector> _list; +public: + FORWARD_IUNKNOWN() + AvnStringArrayImpl(NSArray* array) + { + for(int c = 0; c < [array count]; c++) + { + ComPtr s; + *s.getPPV() = new AvnStringImpl([array objectAtIndex:c]); + _list.push_back(s); + } + } + + AvnStringArrayImpl(NSString* string) + { + ComPtr s; + *s.getPPV() = new AvnStringImpl(string); + _list.push_back(s); + } + + virtual unsigned int GetCount() override + { + return (unsigned int)_list.size(); + } + + virtual HRESULT Get(unsigned int index, IAvnString**ppv) override + { + if(_list.size() <= index) + return E_INVALIDARG; + *ppv = _list[index].getRetainedReference(); + return S_OK; + } +}; + IAvnString* CreateAvnString(NSString* string) { return new AvnStringImpl(string); } + + +IAvnStringArray* CreateAvnStringArray(NSArray * array) +{ + return new AvnStringArrayImpl(array); +} + +IAvnStringArray* CreateAvnStringArray(NSString* string) +{ + return new AvnStringArrayImpl(string); +} diff --git a/native/Avalonia.Native/src/OSX/clipboard.mm b/native/Avalonia.Native/src/OSX/clipboard.mm index 6e4d3ce668..4be45b9c90 100644 --- a/native/Avalonia.Native/src/OSX/clipboard.mm +++ b/native/Avalonia.Native/src/OSX/clipboard.mm @@ -6,16 +6,27 @@ class Clipboard : public ComSingleObject { +private: + NSPasteboard* _pb; + NSPasteboardItem* _item; public: FORWARD_IUNKNOWN() - Clipboard() + Clipboard(NSPasteboard* pasteboard, NSPasteboardItem* item) { - NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; - [pasteBoard stringForType:NSPasteboardTypeString]; + if(pasteboard == nil && item == nil) + pasteboard = [NSPasteboard generalPasteboard]; + + _pb = pasteboard; + _item = item; } - virtual HRESULT GetText (IAvnString**ppv) override + NSPasteboardItem* TryGetItem() + { + return _item; + } + + virtual HRESULT GetText (char* type, IAvnString**ppv) override { @autoreleasepool { @@ -23,20 +34,53 @@ public: { return E_POINTER; } + NSString* typeString = [NSString stringWithUTF8String:(const char*)type]; + NSString* string = _item == nil ? [_pb stringForType:typeString] : [_item stringForType:typeString]; - *ppv = CreateAvnString([[NSPasteboard generalPasteboard] stringForType:NSPasteboardTypeString]); + *ppv = CreateAvnString(string); return S_OK; } } - virtual HRESULT SetText (void* utf8String) override + virtual HRESULT GetStrings(char* type, IAvnStringArray**ppv) override { @autoreleasepool { - NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; - [pasteBoard clearContents]; - [pasteBoard setString:[NSString stringWithUTF8String:(const char*)utf8String] forType:NSPasteboardTypeString]; + *ppv= nil; + NSString* typeString = [NSString stringWithUTF8String:(const char*)type]; + NSObject* data = _item == nil ? [_pb propertyListForType: typeString] : [_item propertyListForType: typeString]; + if(data == nil) + return S_OK; + + if([data isKindOfClass: [NSString class]]) + { + *ppv = CreateAvnStringArray((NSString*) data); + return S_OK; + } + + NSArray* arr = (NSArray*)data; + + for(int c = 0; c < [arr count]; c++) + if(![[arr objectAtIndex:c] isKindOfClass:[NSString class]]) + return E_INVALIDARG; + + *ppv = CreateAvnStringArray(arr); + return S_OK; + } + } + + virtual HRESULT SetText (char* type, void* utf8String) override + { + Clear(); + @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]; } return S_OK; @@ -46,16 +90,34 @@ public: { @autoreleasepool { - NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard]; - [pasteBoard clearContents]; - [pasteBoard setString:@"" forType:NSPasteboardTypeString]; + if(_item != nil) + _item = [NSPasteboardItem new]; + else + { + [_pb clearContents]; + [_pb setString:@"" forType:NSPasteboardTypeString]; + } } return S_OK; } + + virtual HRESULT ObtainFormats(IAvnStringArray** ppv) override + { + *ppv = CreateAvnStringArray(_item == nil ? [_pb types] : [_item types]); + return S_OK; + } }; -extern IAvnClipboard* CreateClipboard() +extern IAvnClipboard* CreateClipboard(NSPasteboard* pb, NSPasteboardItem* item) +{ + return new Clipboard(pb, item); +} + +extern NSPasteboardItem* TryGetPasteboardItem(IAvnClipboard*cb) { - return new Clipboard(); + auto clipboard = dynamic_cast(cb); + if(clipboard == nil) + return nil; + return clipboard->TryGetItem(); } diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index d7eda20f65..428998935e 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -11,11 +11,17 @@ #include extern IAvnPlatformThreadingInterface* CreatePlatformThreading(); +extern void FreeAvnGCHandle(void* handle); extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events, IAvnGlContext* gl); extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events, IAvnGlContext* gl); extern IAvnSystemDialogs* CreateSystemDialogs(); extern IAvnScreens* CreateScreens(); -extern IAvnClipboard* CreateClipboard(); +extern IAvnClipboard* CreateClipboard(NSPasteboard*, NSPasteboardItem*); +extern NSPasteboardItem* TryGetPasteboardItem(IAvnClipboard*); +extern NSObject* CreateDraggingSource(NSDragOperation op, IAvnDndResultCallback* cb, void* handle); +extern void* GetAvnDataObjectHandleFromDraggingInfo(NSObject* info); +extern NSString* GetAvnCustomDataType(); +extern AvnDragDropEffects ConvertDragDropEffects(NSDragOperation nsop); extern IAvnCursorFactory* CreateCursorFactory(); extern IAvnGlDisplay* GetGlDisplay(); extern IAvnAppMenu* CreateAppMenu(); diff --git a/native/Avalonia.Native/src/OSX/dnd.mm b/native/Avalonia.Native/src/OSX/dnd.mm new file mode 100644 index 0000000000..294b8ee8ea --- /dev/null +++ b/native/Avalonia.Native/src/OSX/dnd.mm @@ -0,0 +1,89 @@ +#include "common.h" + +extern AvnDragDropEffects ConvertDragDropEffects(NSDragOperation nsop) +{ + int effects = 0; + if((nsop & NSDragOperationCopy) != 0) + effects |= (int)AvnDragDropEffects::Copy; + if((nsop & NSDragOperationMove) != 0) + effects |= (int)AvnDragDropEffects::Move; + if((nsop & NSDragOperationLink) != 0) + effects |= (int)AvnDragDropEffects::Link; + return (AvnDragDropEffects)effects; +}; + +extern NSString* GetAvnCustomDataType() +{ + char buffer[256]; + sprintf(buffer, "net.avaloniaui.inproc.uti.n%in", getpid()); + return [NSString stringWithUTF8String:buffer]; +} + +@interface AvnDndSource : NSObject + +@end + +@implementation AvnDndSource +{ + NSDragOperation _operation; + ComPtr _cb; + void* _sourceHandle; +}; + +- (NSDragOperation)draggingSession:(nonnull NSDraggingSession *)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context +{ + return NSDragOperationCopy; +} + +- (AvnDndSource*) initWithOperation: (NSDragOperation)operation + andCallback: (IAvnDndResultCallback*) cb + andSourceHandle: (void*) handle +{ + self = [super init]; + _operation = operation; + _cb = cb; + _sourceHandle = handle; + return self; +} + +- (void)draggingSession:(NSDraggingSession *)session + endedAtPoint:(NSPoint)screenPoint + operation:(NSDragOperation)operation +{ + if(_cb != nil) + { + auto cb = _cb; + _cb = nil; + cb->OnDragAndDropComplete(ConvertDragDropEffects(operation)); + } + if(_sourceHandle != nil) + { + FreeAvnGCHandle(_sourceHandle); + _sourceHandle = nil; + } +} + +- (void*) gcHandle +{ + return _sourceHandle; +} + +@end + +extern NSObject* CreateDraggingSource(NSDragOperation op, IAvnDndResultCallback* cb, void* handle) +{ + return [[AvnDndSource alloc] initWithOperation:op andCallback:cb andSourceHandle:handle]; +}; + +extern void* GetAvnDataObjectHandleFromDraggingInfo(NSObject* info) +{ + id obj = [info draggingSource]; + if(obj == nil) + return nil; + if([obj isKindOfClass: [AvnDndSource class]]) + { + auto src = (AvnDndSource*)obj; + return [src gcHandle]; + } + return nil; +} diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 2b6b24cfda..dc9eb0b2bc 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -154,14 +154,15 @@ public: } @end - +static ComPtr _deallocator; class AvaloniaNative : public ComSingleObject { public: FORWARD_IUNKNOWN() - virtual HRESULT Initialize() override + virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator) override { + _deallocator = deallocator; @autoreleasepool{ [[ThreadingInitializer new] do]; } @@ -211,7 +212,13 @@ public: virtual HRESULT CreateClipboard(IAvnClipboard** ppv) override { - *ppv = ::CreateClipboard (); + *ppv = ::CreateClipboard (nil, nil); + return S_OK; + } + + virtual HRESULT CreateDndClipboard(IAvnClipboard** ppv) override + { + *ppv = ::CreateClipboard (nil, [NSPasteboardItem new]); return S_OK; } @@ -273,6 +280,12 @@ extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative() return new AvaloniaNative(); }; +extern void FreeAvnGCHandle(void* handle) +{ + if(_deallocator != nil) + _deallocator->FreeGCHandle(handle); +} + NSSize ToNSSize (AvnSize s) { NSSize result; diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 3e626675d2..1b4a14d17d 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -6,7 +6,7 @@ class WindowBaseImpl; -@interface AvnView : NSView +@interface AvnView : NSView -(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; -(NSEvent* _Nonnull) lastMouseDownEvent; -(AvnPoint) translateLocalPoint:(AvnPoint)pt; diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index cd42ea0437..8efe7ba959 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -393,6 +393,50 @@ public: *ppv = [renderTarget createSurfaceRenderTarget]; return *ppv == nil ? E_FAIL : S_OK; } + + virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point, + IAvnClipboard* clipboard, IAvnDndResultCallback* cb, + void* sourceHandle) override + { + auto item = TryGetPasteboardItem(clipboard); + [item setString:@"" forType:GetAvnCustomDataType()]; + if(item == nil) + return E_INVALIDARG; + if(View == NULL) + return E_FAIL; + + auto nsevent = [NSApp currentEvent]; + auto nseventType = [nsevent type]; + + // If current event isn't a mouse one (probably due to malfunctioning user app) + // attempt to forge a new one + if(!((nseventType >= NSEventTypeLeftMouseDown && nseventType <= NSEventTypeMouseExited) + || (nseventType >= NSEventTypeOtherMouseDown && nseventType <= NSEventTypeOtherMouseDragged))) + { + auto nspoint = [Window convertBaseToScreen: ToNSPoint(point)]; + CGPoint cgpoint = NSPointToCGPoint(nspoint); + auto cgevent = CGEventCreateMouseEvent(NULL, kCGEventLeftMouseDown, cgpoint, kCGMouseButtonLeft); + nsevent = [NSEvent eventWithCGEvent: cgevent]; + CFRelease(cgevent); + } + + auto dragItem = [[NSDraggingItem alloc] initWithPasteboardWriter: item]; + + auto dragItemImage = [NSImage imageNamed:NSImageNameMultipleDocuments]; + NSRect dragItemRect = {(float)point.X, (float)point.Y, [dragItemImage size].width, [dragItemImage size].height}; + [dragItem setDraggingFrame: dragItemRect contents: dragItemImage]; + + int op = 0; int ieffects = (int)effects; + if((ieffects & (int)AvnDragDropEffects::Copy) != 0) + op |= NSDragOperationCopy; + if((ieffects & (int)AvnDragDropEffects::Link) != 0) + op |= NSDragOperationLink; + if((ieffects & (int)AvnDragDropEffects::Move) != 0) + op |= NSDragOperationMove; + [View beginDraggingSessionWithItems: @[dragItem] event: nsevent + source: CreateDraggingSource((NSDragOperation) op, cb, sourceHandle)]; + return S_OK; + } protected: virtual NSWindowStyleMask GetStyle() @@ -716,7 +760,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _area = nullptr; _lastPixelSize.Height = 100; _lastPixelSize.Width = 100; - + [self registerForDraggedTypes: @[@"public.data", GetAvnCustomDataType()]]; return self; } @@ -1076,6 +1120,68 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent return result; } + +- (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id )info +{ + auto localPoint = [self convertPoint:[info draggingLocation] toView:self]; + auto avnPoint = [self toAvnPoint:localPoint]; + auto point = [self translateLocalPoint:avnPoint]; + auto modifiers = [self getModifiers:[[NSApp currentEvent] modifierFlags]]; + NSDragOperation nsop = [info draggingSourceOperationMask]; + + auto effects = ConvertDragDropEffects(nsop); + int reffects = (int)_parent->BaseEvents + ->DragEvent(type, point, modifiers, effects, + CreateClipboard([info draggingPasteboard], nil), + GetAvnDataObjectHandleFromDraggingInfo(info)); + + NSDragOperation ret = 0; + + // Ensure that the managed part didn't add any new effects + reffects = (int)effects & (int)reffects; + + // OSX requires exactly one operation + if((reffects & (int)AvnDragDropEffects::Copy) != 0) + ret = NSDragOperationCopy; + else if((reffects & (int)AvnDragDropEffects::Move) != 0) + ret = NSDragOperationMove; + else if((reffects & (int)AvnDragDropEffects::Link) != 0) + ret = NSDragOperationLink; + if(ret == 0) + ret = NSDragOperationNone; + return ret; +} + +- (NSDragOperation)draggingEntered:(id )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Enter info:sender]; +} + +- (NSDragOperation)draggingUpdated:(id )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender]; +} + +- (void)draggingExited:(id )sender +{ + [self triggerAvnDragEvent: AvnDragEventType::Leave info:sender]; +} + +- (BOOL)prepareForDragOperation:(id )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender] != NSDragOperationNone; +} + +- (BOOL)performDragOperation:(id )sender +{ + return [self triggerAvnDragEvent: AvnDragEventType::Drop info:sender] != NSDragOperationNone; +} + +- (void)concludeDragOperation:(nullable id )sender +{ + +} + @end diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml b/samples/ControlCatalog/Pages/DragAndDropPage.xaml index 9bfcd90149..65a798e53c 100644 --- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml +++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml @@ -9,9 +9,15 @@ Margin="0,16,0,0" HorizontalAlignment="Center" Spacing="16"> - - Drag Me - + + + Drag Me + + + Drag Me (custom) + + + Drop some text or files here diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs index 0bf21c2820..5a52dbe12b 100644 --- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs @@ -3,69 +3,85 @@ using Avalonia.Input; using Avalonia.Markup.Xaml; using System; using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; using System.Text; namespace ControlCatalog.Pages { public class DragAndDropPage : UserControl { - private TextBlock _DropState; - private TextBlock _DragState; - private Border _DragMe; - private int DragCount = 0; - + TextBlock _DropState; + private const string CustomFormat = "application/xxx-avalonia-controlcatalog-custom"; public DragAndDropPage() { this.InitializeComponent(); + _DropState = this.Find("DropState"); - _DragMe.PointerPressed += DoDrag; + int textCount = 0; + SetupDnd("Text", d => d.Set(DataFormats.Text, + $"Text was dragged {++textCount} times")); - AddHandler(DragDrop.DropEvent, Drop); - AddHandler(DragDrop.DragOverEvent, DragOver); + SetupDnd("Custom", d => d.Set(CustomFormat, "Test123")); } - private async void DoDrag(object sender, Avalonia.Input.PointerPressedEventArgs e) + void SetupDnd(string suffix, Action factory, DragDropEffects effects = DragDropEffects.Copy) { - DataObject dragData = new DataObject(); - dragData.Set(DataFormats.Text, $"You have dragged text {++DragCount} times"); + var dragMe = this.Find("DragMe" + suffix); + var dragState = this.Find("DragState"+suffix); - var result = await DragDrop.DoDragDrop(e, dragData, DragDropEffects.Copy); - switch(result) + async void DoDrag(object sender, Avalonia.Input.PointerPressedEventArgs e) { - case DragDropEffects.Copy: - _DragState.Text = "The text was copied"; break; - case DragDropEffects.Link: - _DragState.Text = "The text was linked"; break; - case DragDropEffects.None: - _DragState.Text = "The drag operation was canceled"; break; + var dragData = new DataObject(); + factory(dragData); + + var result = await DragDrop.DoDragDrop(e, dragData, DragDropEffects.Copy); + switch (result) + { + case DragDropEffects.Copy: + dragState.Text = "Data was copied"; + break; + case DragDropEffects.Link: + dragState.Text = "Data was linked"; + break; + case DragDropEffects.None: + dragState.Text = "The drag operation was canceled"; + break; + } } - } - private void DragOver(object sender, DragEventArgs e) - { - // Only allow Copy or Link as Drop Operations. - e.DragEffects = e.DragEffects & (DragDropEffects.Copy | DragDropEffects.Link); + void DragOver(object sender, DragEventArgs e) + { + // Only allow Copy or Link as Drop Operations. + e.DragEffects = e.DragEffects & (DragDropEffects.Copy | DragDropEffects.Link); - // Only allow if the dragged data contains text or filenames. - if (!e.Data.Contains(DataFormats.Text) && !e.Data.Contains(DataFormats.FileNames)) - e.DragEffects = DragDropEffects.None; - } + // Only allow if the dragged data contains text or filenames. + if (!e.Data.Contains(DataFormats.Text) + && !e.Data.Contains(DataFormats.FileNames) + && !e.Data.Contains(CustomFormat)) + e.DragEffects = DragDropEffects.None; + } - private void Drop(object sender, DragEventArgs e) - { - if (e.Data.Contains(DataFormats.Text)) - _DropState.Text = e.Data.GetText(); - else if (e.Data.Contains(DataFormats.FileNames)) - _DropState.Text = string.Join(Environment.NewLine, e.Data.GetFileNames()); + void Drop(object sender, DragEventArgs e) + { + if (e.Data.Contains(DataFormats.Text)) + _DropState.Text = e.Data.GetText(); + else if (e.Data.Contains(DataFormats.FileNames)) + _DropState.Text = string.Join(Environment.NewLine, e.Data.GetFileNames()); + else if (e.Data.Contains(CustomFormat)) + _DropState.Text = "Custom: " + e.Data.Get(CustomFormat); + } + + dragMe.PointerPressed += DoDrag; + + AddHandler(DragDrop.DropEvent, Drop); + AddHandler(DragDrop.DragOverEvent, DragOver); } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); - - _DropState = this.Find("DropState"); - _DragState = this.Find("DragState"); - _DragMe = this.Find("DragMe"); } } } diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 9158ac7038..1ef7686d00 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -219,8 +219,12 @@ namespace Avalonia .Bind().ToTransient() .Bind().ToConstant(_styler) .Bind().ToConstant(AvaloniaScheduler.Instance) - .Bind().ToConstant(DragDropDevice.Instance) - .Bind().ToTransient(); + .Bind().ToConstant(DragDropDevice.Instance); + + // TODO: Fix this, for now we keep this behavior since someone might be relying on it in 0.9.x + if (AvaloniaLocator.Current.GetService() == null) + AvaloniaLocator.CurrentMutable + .Bind().ToTransient(); var clock = new RenderLoopClock(); AvaloniaLocator.CurrentMutable diff --git a/src/Avalonia.Native/AvaloniaNativeDragSource.cs b/src/Avalonia.Native/AvaloniaNativeDragSource.cs new file mode 100644 index 0000000000..80d54d8a10 --- /dev/null +++ b/src/Avalonia.Native/AvaloniaNativeDragSource.cs @@ -0,0 +1,76 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Interactivity; +using Avalonia.Native.Interop; +using Avalonia.Platform; +using Avalonia.VisualTree; + +namespace Avalonia.Native +{ + class AvaloniaNativeDragSource : IPlatformDragSource + { + private readonly IAvaloniaNativeFactory _factory; + + public AvaloniaNativeDragSource(IAvaloniaNativeFactory factory) + { + _factory = factory; + } + + TopLevel FindRoot(IInteractive interactive) + { + while (interactive != null && !(interactive is IVisual)) + interactive = interactive.InteractiveParent; + if (interactive == null) + return null; + var visual = (IVisual)interactive; + return visual.VisualRoot as TopLevel; + } + + class DndCallback : CallbackBase, IAvnDndResultCallback + { + private TaskCompletionSource _tcs; + + public DndCallback(TaskCompletionSource tcs) + { + _tcs = tcs; + } + public void OnDragAndDropComplete(AvnDragDropEffects effect) + { + _tcs?.TrySetResult((DragDropEffects)effect); + _tcs = null; + } + } + + public Task DoDragDrop(PointerEventArgs triggerEvent, IDataObject data, DragDropEffects allowedEffects) + { + // Sanity check + var tl = FindRoot(triggerEvent.Source); + var view = tl?.PlatformImpl as WindowBaseImpl; + if (view == null) + throw new ArgumentException(); + + triggerEvent.Pointer.Capture(null); + + var tcs = new TaskCompletionSource(); + + var clipboardImpl = _factory.CreateDndClipboard(); + using (var clipboard = new ClipboardImpl(clipboardImpl)) + using (var cb = new DndCallback(tcs)) + { + if (data.Contains(DataFormats.Text)) + // API is synchronous, so it's OK + clipboard.SetTextAsync(data.GetText()).Wait(); + + view.BeginDraggingSession((AvnDragDropEffects)allowedEffects, + triggerEvent.GetPosition(tl).ToAvnPoint(), clipboardImpl, cb, + GCHandle.ToIntPtr(GCHandle.Alloc(data))); + } + + return tcs.Task; + } + } +} diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 36167b1b4d..f52a464c51 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -79,17 +79,25 @@ namespace Avalonia.Native _factory = factory; } + class GCHandleDeallocator : CallbackBase, IAvnGCHandleDeallocatorCallback + { + public void FreeGCHandle(IntPtr handle) + { + GCHandle.FromIntPtr(handle).Free(); + } + } + void DoInitialize(AvaloniaNativePlatformOptions options) { _options = options; - _factory.Initialize(); + _factory.Initialize(new GCHandleDeallocator()); if (_factory.MacOptions != null) { var macOpts = AvaloniaLocator.Current.GetService(); _factory.MacOptions.ShowInDock = macOpts?.ShowInDock != false ? 1 : 0; } - + AvaloniaLocator.CurrentMutable .Bind() .ToConstant(new PlatformThreadingInterface(_factory.CreatePlatformThreadingInterface())) @@ -103,7 +111,9 @@ namespace Avalonia.Native .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs())) .Bind().ToConstant(new PlatformHotkeyConfiguration(InputModifiers.Windows)) - .Bind().ToConstant(new MacOSMountedVolumeInfoProvider()); + .Bind().ToConstant(new MacOSMountedVolumeInfoProvider()) + .Bind().ToConstant(new AvaloniaNativeDragSource(_factory)) + ; if (_options.UseGpu) AvaloniaLocator.CurrentMutable.Bind() .ToConstant(_glFeature = new GlPlatformFeature(_factory.ObtainGlDisplay())); diff --git a/src/Avalonia.Native/AvnString.cs b/src/Avalonia.Native/AvnString.cs new file mode 100644 index 0000000000..ba427b6aac --- /dev/null +++ b/src/Avalonia.Native/AvnString.cs @@ -0,0 +1,39 @@ +using System.Runtime.InteropServices; + +namespace Avalonia.Native.Interop +{ + unsafe partial class IAvnString + { + private string _managed; + + public string String + { + get + { + if (_managed == null) + { + var ptr = Pointer(); + if (ptr == null) + return null; + _managed = System.Text.Encoding.UTF8.GetString((byte*)ptr.ToPointer(), Length()); + } + + return _managed; + } + } + + public override string ToString() => String; + } + + partial class IAvnStringArray + { + public string[] ToStringArray() + { + var arr = new string[Count]; + for(uint c = 0; c GetTextAsync() + public Task GetTextAsync() { - using (var text = _native.GetText()) - { - var result = System.Text.Encoding.UTF8.GetString((byte*)text.Pointer(), text.Length()); - - return Task.FromResult(result); - } + using (var text = _native.GetText(NSPasteboardTypeString)) + return Task.FromResult(text.String); } public Task SetTextAsync(string text) @@ -43,11 +46,84 @@ namespace Avalonia.Native { using (var buffer = new Utf8Buffer(text)) { - _native.SetText(buffer.DangerousGetHandle()); + _native.SetText(NSPasteboardTypeString, buffer.DangerousGetHandle()); } } return Task.CompletedTask; } + + public IEnumerable GetFormats() + { + var rv = new List(); + using (var formats = _native.ObtainFormats()) + { + 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); + } + } + } + + return rv; + } + + public void Dispose() + { + _native?.Dispose(); + _native = null; + } + + public IEnumerable GetFileNames() + { + using (var strings = _native.GetStrings(NSFilenamesPboardType)) + return strings.ToStringArray(); + } + } + + class ClipboardDataObject : IDataObject, IDisposable + { + private ClipboardImpl _clipboard; + private List _formats; + + public ClipboardDataObject(IAvnClipboard clipboard) + { + _clipboard = new ClipboardImpl(clipboard); + } + + public void Dispose() + { + _clipboard?.Dispose(); + _clipboard = null; + } + + List Formats => _formats ??= _clipboard.GetFormats().ToList(); + + public IEnumerable GetDataFormats() => Formats; + + public bool Contains(string dataFormat) => Formats.Contains(dataFormat); + + public string GetText() + { + // bad idea in general, but API is synchronous anyway + return _clipboard.GetTextAsync().Result; + } + + public IEnumerable GetFileNames() => _clipboard.GetFileNames(); + + public object Get(string dataFormat) + { + if (dataFormat == DataFormats.Text) + return GetText(); + if (dataFormat == DataFormats.FileNames) + return GetFileNames(); + return null; + } } } diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 331f083e8b..438d619aff 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; using Avalonia.Controls; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Input; @@ -201,6 +202,30 @@ namespace Avalonia.Native { Dispatcher.UIThread.RunJobs(DispatcherPriority.Render); } + + public AvnDragDropEffects DragEvent(AvnDragEventType type, AvnPoint position, + AvnInputModifiers modifiers, + AvnDragDropEffects effects, + IAvnClipboard clipboard, IntPtr dataObjectHandle) + { + var device = AvaloniaLocator.Current.GetService(); + + 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(args); + return (AvnDragDropEffects)args.Effects; + } + } } public void Activate() @@ -359,6 +384,12 @@ namespace Avalonia.Native } + internal void BeginDraggingSession(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard clipboard, + IAvnDndResultCallback callback, IntPtr sourceHandle) + { + _native.BeginDragAndDropOperation(effects, point, clipboard, callback, sourceHandle); + } + public IPlatformHandle Handle { get; private set; } } }