summaryrefslogtreecommitdiffstats
path: root/widget/cocoa/nsDragService.mm
diff options
context:
space:
mode:
Diffstat (limited to 'widget/cocoa/nsDragService.mm')
-rw-r--r--widget/cocoa/nsDragService.mm477
1 files changed, 477 insertions, 0 deletions
diff --git a/widget/cocoa/nsDragService.mm b/widget/cocoa/nsDragService.mm
new file mode 100644
index 0000000000..4ac5c3cbc0
--- /dev/null
+++ b/widget/cocoa/nsDragService.mm
@@ -0,0 +1,477 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/Logging.h"
+
+#include "gfxContext.h"
+#include "nsArrayUtils.h"
+#include "nsDragService.h"
+#include "nsArrayUtils.h"
+#include "nsObjCExceptions.h"
+#include "nsITransferable.h"
+#include "nsString.h"
+#include "nsClipboard.h"
+#include "nsXPCOM.h"
+#include "nsCOMPtr.h"
+#include "nsPrimitiveHelpers.h"
+#include "nsLinebreakConverter.h"
+#include "nsINode.h"
+#include "nsRect.h"
+#include "nsPoint.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/DocumentInlines.h"
+#include "nsIContent.h"
+#include "nsView.h"
+#include "nsCocoaUtils.h"
+#include "mozilla/gfx/2D.h"
+#include "gfxPlatform.h"
+#include "nsDeviceContext.h"
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+
+extern mozilla::LazyLogModule sCocoaLog;
+
+extern NSPasteboard* globalDragPboard;
+extern ChildView* gLastDragView;
+extern NSEvent* gLastDragMouseDownEvent;
+extern bool gUserCancelledDrag;
+
+// This global makes the transferable array available to Cocoa's promised
+// file destination callback.
+mozilla::StaticRefPtr<nsIArray> gDraggedTransferables;
+
+nsDragService::nsDragService()
+ : mNativeDragView(nil), mNativeDragEvent(nil), mDragImageChanged(false) {}
+
+nsDragService::~nsDragService() {}
+
+NSImage* nsDragService::ConstructDragImage(nsINode* aDOMNode, const Maybe<CSSIntRegion>& aRegion,
+ NSPoint* aDragPoint) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mNativeDragView);
+
+ LayoutDeviceIntRect dragRect(0, 0, 20, 20);
+ NSImage* image = ConstructDragImage(mSourceNode, aRegion, mScreenPosition, &dragRect);
+ if (!image) {
+ // if no image was returned, just draw a rectangle
+ NSSize size;
+ size.width = nsCocoaUtils::DevPixelsToCocoaPoints(dragRect.width, scaleFactor);
+ size.height = nsCocoaUtils::DevPixelsToCocoaPoints(dragRect.height, scaleFactor);
+ image = [NSImage imageWithSize:size
+ flipped:YES
+ drawingHandler:^BOOL(NSRect dstRect) {
+ [[NSColor grayColor] set];
+ NSBezierPath* path = [NSBezierPath bezierPathWithRect:dstRect];
+ [path setLineWidth:2.0];
+ [path stroke];
+ return YES;
+ }];
+ }
+
+ LayoutDeviceIntPoint pt(dragRect.x, dragRect.YMost());
+ NSPoint point = nsCocoaUtils::DevPixelsToCocoaPoints(pt, scaleFactor);
+ point.y = nsCocoaUtils::FlippedScreenY(point.y);
+
+ point = nsCocoaUtils::ConvertPointFromScreen([mNativeDragView window], point);
+ *aDragPoint = [mNativeDragView convertPoint:point fromView:nil];
+
+ return image;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+NSImage* nsDragService::ConstructDragImage(nsINode* aDOMNode, const Maybe<CSSIntRegion>& aRegion,
+ CSSIntPoint aPoint, LayoutDeviceIntRect* aDragRect) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mNativeDragView);
+
+ RefPtr<SourceSurface> surface;
+ nsPresContext* pc;
+ nsresult rv = DrawDrag(aDOMNode, aRegion, aPoint, aDragRect, &surface, &pc);
+ if (pc && (!aDragRect->width || !aDragRect->height)) {
+ // just use some suitable defaults
+ int32_t size = nsCocoaUtils::CocoaPointsToDevPixels(20, scaleFactor);
+ aDragRect->SetRect(pc->CSSPixelsToDevPixels(aPoint.x), pc->CSSPixelsToDevPixels(aPoint.y), size,
+ size);
+ }
+
+ if (NS_FAILED(rv) || !surface) return nil;
+
+ uint32_t width = aDragRect->width;
+ uint32_t height = aDragRect->height;
+
+ RefPtr<DataSourceSurface> dataSurface =
+ Factory::CreateDataSourceSurface(IntSize(width, height), SurfaceFormat::B8G8R8A8);
+ DataSourceSurface::MappedSurface map;
+ if (!dataSurface->Map(DataSourceSurface::MapType::READ_WRITE, &map)) {
+ return nil;
+ }
+
+ RefPtr<DrawTarget> dt = Factory::CreateDrawTargetForData(
+ BackendType::CAIRO, map.mData, dataSurface->GetSize(), map.mStride, dataSurface->GetFormat());
+ if (!dt) {
+ dataSurface->Unmap();
+ return nil;
+ }
+
+ dt->FillRect(gfx::Rect(0, 0, width, height), SurfacePattern(surface, ExtendMode::CLAMP),
+ DrawOptions(1.0f, CompositionOp::OP_SOURCE));
+
+ NSBitmapImageRep* imageRep =
+ [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL
+ pixelsWide:width
+ pixelsHigh:height
+ bitsPerSample:8
+ samplesPerPixel:4
+ hasAlpha:YES
+ isPlanar:NO
+ colorSpaceName:NSDeviceRGBColorSpace
+ bytesPerRow:width * 4
+ bitsPerPixel:32];
+
+ uint8_t* dest = [imageRep bitmapData];
+ for (uint32_t i = 0; i < height; ++i) {
+ uint8_t* src = map.mData + i * map.mStride;
+ for (uint32_t j = 0; j < width; ++j) {
+ // Reduce transparency overall by multipying by a factor. Remember, Alpha
+ // is premultipled here. Also, Quartz likes RGBA, so do that translation as well.
+#ifdef IS_BIG_ENDIAN
+ dest[0] = uint8_t(src[1] * DRAG_TRANSLUCENCY);
+ dest[1] = uint8_t(src[2] * DRAG_TRANSLUCENCY);
+ dest[2] = uint8_t(src[3] * DRAG_TRANSLUCENCY);
+ dest[3] = uint8_t(src[0] * DRAG_TRANSLUCENCY);
+#else
+ dest[0] = uint8_t(src[2] * DRAG_TRANSLUCENCY);
+ dest[1] = uint8_t(src[1] * DRAG_TRANSLUCENCY);
+ dest[2] = uint8_t(src[0] * DRAG_TRANSLUCENCY);
+ dest[3] = uint8_t(src[3] * DRAG_TRANSLUCENCY);
+#endif
+ src += 4;
+ dest += 4;
+ }
+ }
+ dataSurface->Unmap();
+
+ NSImage* image =
+ [[NSImage alloc] initWithSize:NSMakeSize(width / scaleFactor, height / scaleFactor)];
+ [image addRepresentation:imageRep];
+ [imageRep release];
+
+ return [image autorelease];
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(nil);
+}
+
+nsresult nsDragService::InvokeDragSessionImpl(nsIArray* aTransferableArray,
+ const Maybe<CSSIntRegion>& aRegion,
+ uint32_t aActionType) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+#ifdef NIGHTLY_BUILD
+ MOZ_RELEASE_ASSERT(NS_IsMainThread());
+#endif
+
+ if (!gLastDragView) {
+ // gLastDragView is non-null between -[ChildView mouseDown:] and -[ChildView mouseUp:].
+ // If we get here with gLastDragView being null, that means that the mouse button has already
+ // been released. In that case we need to abort the drag because the OS won't know where to drop
+ // whatever's being dragged, and we might end up with a stuck drag & drop session.
+ return NS_ERROR_FAILURE;
+ }
+
+ mDataItems = aTransferableArray;
+
+ // Save the transferables away in case a promised file callback is invoked.
+ gDraggedTransferables = aTransferableArray;
+
+ // We need to retain the view and the event during the drag in case either
+ // gets destroyed.
+ mNativeDragView = [gLastDragView retain];
+ mNativeDragEvent = [gLastDragMouseDownEvent retain];
+
+ gUserCancelledDrag = false;
+
+ NSPasteboardItem* pbItem = [NSPasteboardItem new];
+ NSMutableArray* types = [NSMutableArray arrayWithCapacity:5];
+
+ if (gDraggedTransferables) {
+ uint32_t count = 0;
+ gDraggedTransferables->GetLength(&count);
+
+ for (uint32_t j = 0; j < count; j++) {
+ nsCOMPtr<nsITransferable> currentTransferable = do_QueryElementAt(aTransferableArray, j);
+ if (!currentTransferable) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Transform the transferable to an NSDictionary
+ NSDictionary* pasteboardOutputDict =
+ nsClipboard::PasteboardDictFromTransferable(currentTransferable);
+ if (!pasteboardOutputDict) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // write everything out to the general pasteboard
+ [types addObjectsFromArray:[pasteboardOutputDict allKeys]];
+ // Gecko is initiating this drag so we always want its own views to
+ // consider it. Add our wildcard type to the pasteboard to accomplish
+ // this.
+ [types addObject:[UTIHelper stringFromPboardType:kMozWildcardPboardType]];
+ }
+ }
+ [pbItem setDataProvider:mNativeDragView forTypes:types];
+
+ NSPoint draggingPoint;
+ NSImage* image = ConstructDragImage(mSourceNode, aRegion, &draggingPoint);
+
+ NSRect localDragRect = image.alignmentRect;
+ localDragRect.origin.x = draggingPoint.x;
+ localDragRect.origin.y = draggingPoint.y - localDragRect.size.height;
+
+ NSDraggingItem* dragItem = [[NSDraggingItem alloc] initWithPasteboardWriter:pbItem];
+ [pbItem release];
+ [dragItem setDraggingFrame:localDragRect contents:image];
+
+ nsBaseDragService::StartDragSession();
+ nsBaseDragService::OpenDragPopup();
+
+ NSDraggingSession* draggingSession = [mNativeDragView
+ beginDraggingSessionWithItems:[NSArray arrayWithObject:[dragItem autorelease]]
+ event:mNativeDragEvent
+ source:mNativeDragView];
+ draggingSession.animatesToStartingPositionsOnCancelOrFail =
+ !mDataTransfer || mDataTransfer->MozShowFailAnimation();
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsDragService::GetData(nsITransferable* aTransferable, uint32_t aItemIndex) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (!aTransferable) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // get flavor list that includes all acceptable flavors (including ones obtained through
+ // conversion)
+ nsTArray<nsCString> flavors;
+ nsresult rv = aTransferable->FlavorsTransferableCanImport(flavors);
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // if this drag originated within Mozilla we should just use the cached data from
+ // when the drag started if possible
+ if (mDataItems) {
+ nsCOMPtr<nsITransferable> currentTransferable = do_QueryElementAt(mDataItems, aItemIndex);
+ if (currentTransferable) {
+ for (uint32_t i = 0; i < flavors.Length(); i++) {
+ nsCString& flavorStr = flavors[i];
+
+ nsCOMPtr<nsISupports> dataSupports;
+ rv = currentTransferable->GetTransferData(flavorStr.get(), getter_AddRefs(dataSupports));
+ if (NS_SUCCEEDED(rv)) {
+ aTransferable->SetTransferData(flavorStr.get(), dataSupports);
+ return NS_OK; // maybe try to fill in more types? Is there a point?
+ }
+ }
+ }
+ }
+
+ NSArray* droppedItems = [globalDragPboard pasteboardItems];
+ if (!droppedItems) {
+ return NS_ERROR_FAILURE;
+ }
+
+ uint32_t itemCount = [droppedItems count];
+ if (aItemIndex >= itemCount) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NSPasteboardItem* item = [droppedItems objectAtIndex:aItemIndex];
+ if (!item) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // now check the actual clipboard for data
+ for (uint32_t i = 0; i < flavors.Length(); i++) {
+ nsCocoaUtils::SetTransferDataForTypeFromPasteboardItem(aTransferable, flavors[i], item);
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsDragService::IsDataFlavorSupported(const char* aDataFlavor, bool* _retval) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ *_retval = false;
+
+ if (!globalDragPboard) return NS_ERROR_FAILURE;
+
+ nsDependentCString dataFlavor(aDataFlavor);
+
+ // first see if we have data for this in our cached transferable
+ if (mDataItems) {
+ uint32_t dataItemsCount;
+ mDataItems->GetLength(&dataItemsCount);
+ for (unsigned int i = 0; i < dataItemsCount; i++) {
+ nsCOMPtr<nsITransferable> currentTransferable = do_QueryElementAt(mDataItems, i);
+ if (!currentTransferable) continue;
+
+ nsTArray<nsCString> flavors;
+ nsresult rv = currentTransferable->FlavorsTransferableCanImport(flavors);
+ if (NS_FAILED(rv)) continue;
+
+ for (uint32_t j = 0; j < flavors.Length(); j++) {
+ if (dataFlavor.Equals(flavors[j])) {
+ *_retval = true;
+ return NS_OK;
+ }
+ }
+ }
+ }
+
+ const NSString* type = nil;
+ bool allowFileURL = false;
+ if (dataFlavor.EqualsLiteral(kFileMime)) {
+ type = [UTIHelper stringFromPboardType:(NSString*)kUTTypeFileURL];
+ allowFileURL = true;
+ } else if (dataFlavor.EqualsLiteral(kTextMime)) {
+ type = [UTIHelper stringFromPboardType:NSPasteboardTypeString];
+ } else if (dataFlavor.EqualsLiteral(kHTMLMime)) {
+ type = [UTIHelper stringFromPboardType:NSPasteboardTypeHTML];
+ } else if (dataFlavor.EqualsLiteral(kURLMime) || dataFlavor.EqualsLiteral(kURLDataMime)) {
+ type = [UTIHelper stringFromPboardType:kPublicUrlPboardType];
+ } else if (dataFlavor.EqualsLiteral(kURLDescriptionMime)) {
+ type = [UTIHelper stringFromPboardType:kPublicUrlNamePboardType];
+ } else if (dataFlavor.EqualsLiteral(kRTFMime)) {
+ type = [UTIHelper stringFromPboardType:NSPasteboardTypeRTF];
+ } else if (dataFlavor.EqualsLiteral(kCustomTypesMime)) {
+ type = [UTIHelper stringFromPboardType:kMozCustomTypesPboardType];
+ }
+
+ NSString* availableType =
+ [globalDragPboard availableTypeFromArray:[NSArray arrayWithObjects:(id)type, nil]];
+ if (availableType && nsCocoaUtils::IsValidPasteboardType(availableType, allowFileURL)) {
+ *_retval = true;
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsDragService::GetNumDropItems(uint32_t* aNumItems) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ *aNumItems = 0;
+
+ // first check to see if we have a number of items cached
+ if (mDataItems) {
+ mDataItems->GetLength(aNumItems);
+ return NS_OK;
+ }
+
+ NSArray* droppedItems = [globalDragPboard pasteboardItems];
+ if (droppedItems) {
+ *aNumItems = [droppedItems count];
+ }
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsDragService::UpdateDragImage(nsINode* aImage, int32_t aImageX, int32_t aImageY) {
+ nsBaseDragService::UpdateDragImage(aImage, aImageX, aImageY);
+ mDragImageChanged = true;
+ return NS_OK;
+}
+
+void nsDragService::DragMovedWithView(NSDraggingSession* aSession, NSPoint aPoint) {
+ aPoint.y = nsCocoaUtils::FlippedScreenY(aPoint.y);
+
+ // XXX It feels like we should be using the backing scale factor at aPoint
+ // rather than the initial drag view, but I've seen no ill effects of this.
+ CGFloat scaleFactor = nsCocoaUtils::GetBackingScaleFactor(mNativeDragView);
+ LayoutDeviceIntPoint devPoint = nsCocoaUtils::CocoaPointsToDevPixels(aPoint, scaleFactor);
+
+ // If the image has changed, call enumerateDraggingItemsWithOptions to get
+ // the item being dragged and update its image.
+ if (mDragImageChanged && mNativeDragView) {
+ mDragImageChanged = false;
+
+ nsPresContext* pc = nullptr;
+ nsCOMPtr<nsIContent> content = do_QueryInterface(mImage);
+ if (content) {
+ pc = content->OwnerDoc()->GetPresContext();
+ }
+
+ if (pc) {
+ void (^changeImageBlock)(NSDraggingItem*, NSInteger, BOOL*) =
+ ^(NSDraggingItem* draggingItem, NSInteger idx, BOOL* stop) {
+ // We never add more than one item right now, but check just in case.
+ if (idx > 0) {
+ return;
+ }
+
+ nsPoint pt =
+ LayoutDevicePixel::ToAppUnits(devPoint, pc->DeviceContext()->AppUnitsPerDevPixel());
+ CSSIntPoint screenPoint = CSSIntPoint(nsPresContext::AppUnitsToIntCSSPixels(pt.x),
+ nsPresContext::AppUnitsToIntCSSPixels(pt.y));
+
+ // Create a new image; if one isn't returned don't change the current one.
+ LayoutDeviceIntRect newRect;
+ NSImage* image = ConstructDragImage(mSourceNode, Nothing(), screenPoint, &newRect);
+ if (image) {
+ NSRect draggingRect = nsCocoaUtils::GeckoRectToCocoaRectDevPix(newRect, scaleFactor);
+ [draggingItem setDraggingFrame:draggingRect contents:image];
+ }
+ };
+
+ [aSession enumerateDraggingItemsWithOptions:NSDraggingItemEnumerationConcurrent
+ forView:nil
+ classes:[NSArray arrayWithObject:[NSPasteboardItem class]]
+ searchOptions:@{}
+ usingBlock:changeImageBlock];
+ }
+ }
+
+ DragMoved(devPoint.x, devPoint.y);
+}
+
+NS_IMETHODIMP
+nsDragService::EndDragSession(bool aDoneDrag, uint32_t aKeyModifiers) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (mNativeDragView) {
+ [mNativeDragView release];
+ mNativeDragView = nil;
+ }
+ if (mNativeDragEvent) {
+ [mNativeDragEvent release];
+ mNativeDragEvent = nil;
+ }
+
+ mUserCancelled = gUserCancelledDrag;
+
+ nsresult rv = nsBaseDragService::EndDragSession(aDoneDrag, aKeyModifiers);
+ mDataItems = nullptr;
+ return rv;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}