| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the plugins of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU Lesser General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU Lesser |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 2.0 or (at your option) the GNU General |
| ** Public license version 3 or any later version approved by the KDE Free |
| ** Qt Foundation. The licenses are as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
| ** https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include "qcocoadrag.h" |
| #include "qmacclipboard.h" |
| #include "qcocoahelpers.h" |
| #ifndef QT_NO_WIDGETS |
| #include <QtWidgets/qwidget.h> |
| #endif |
| #include <QtGui/private/qcoregraphics_p.h> |
| #include <QtCore/qsysinfo.h> |
| |
| #include <vector> |
| |
| QT_BEGIN_NAMESPACE |
| |
| static const int dragImageMaxChars = 26; |
| |
| QCocoaDrag::QCocoaDrag() : |
| m_drag(nullptr) |
| { |
| m_lastEvent = nil; |
| m_lastView = nil; |
| } |
| |
| QCocoaDrag::~QCocoaDrag() |
| { |
| [m_lastEvent release]; |
| } |
| |
| void QCocoaDrag::setLastMouseEvent(NSEvent *event, NSView *view) |
| { |
| [m_lastEvent release]; |
| m_lastEvent = [event copy]; |
| m_lastView = view; |
| } |
| |
| QMimeData *QCocoaDrag::dragMimeData() |
| { |
| if (m_drag) |
| return m_drag->mimeData(); |
| |
| return nullptr; |
| } |
| |
| Qt::DropAction QCocoaDrag::defaultAction(Qt::DropActions possibleActions, |
| Qt::KeyboardModifiers modifiers) const |
| { |
| Qt::DropAction default_action = Qt::IgnoreAction; |
| |
| if (currentDrag()) { |
| default_action = currentDrag()->defaultAction(); |
| possibleActions = currentDrag()->supportedActions(); |
| } |
| |
| if (default_action == Qt::IgnoreAction) { |
| //This means that the drag was initiated by QDrag::start and we need to |
| //preserve the old behavior |
| default_action = Qt::CopyAction; |
| } |
| |
| if (modifiers & Qt::ControlModifier && modifiers & Qt::AltModifier) |
| default_action = Qt::LinkAction; |
| else if (modifiers & Qt::AltModifier) |
| default_action = Qt::CopyAction; |
| else if (modifiers & Qt::ControlModifier) |
| default_action = Qt::MoveAction; |
| |
| #ifdef QDND_DEBUG |
| qDebug("possible actions : %s", dragActionsToString(possibleActions).latin1()); |
| #endif |
| |
| // Check if the action determined is allowed |
| if (!(possibleActions & default_action)) { |
| if (possibleActions & Qt::CopyAction) |
| default_action = Qt::CopyAction; |
| else if (possibleActions & Qt::MoveAction) |
| default_action = Qt::MoveAction; |
| else if (possibleActions & Qt::LinkAction) |
| default_action = Qt::LinkAction; |
| else |
| default_action = Qt::IgnoreAction; |
| } |
| |
| #ifdef QDND_DEBUG |
| qDebug("default action : %s", dragActionsToString(default_action).latin1()); |
| #endif |
| |
| return default_action; |
| } |
| |
| |
| Qt::DropAction QCocoaDrag::drag(QDrag *o) |
| { |
| m_drag = o; |
| m_executed_drop_action = Qt::IgnoreAction; |
| |
| if (maybeDragMultipleItems()) |
| return m_executed_drop_action; |
| |
| QPoint hotSpot = m_drag->hotSpot(); |
| QPixmap pm = dragPixmap(m_drag, hotSpot); |
| NSImage *dragImage = [NSImage imageFromQImage:pm.toImage()]; |
| Q_ASSERT(dragImage); |
| |
| QMacPasteboard dragBoard(CFStringRef(NSPasteboardNameDrag), QMacInternalPasteboardMime::MIME_DND); |
| m_drag->mimeData()->setData(QLatin1String("application/x-qt-mime-type-name"), QByteArray("dummy")); |
| dragBoard.setMimeData(m_drag->mimeData(), QMacPasteboard::LazyRequest); |
| |
| NSPoint event_location = [m_lastEvent locationInWindow]; |
| NSWindow *theWindow = [m_lastEvent window]; |
| Q_ASSERT(theWindow); |
| event_location.x -= hotSpot.x(); |
| CGFloat flippedY = dragImage.size.height - hotSpot.y(); |
| event_location.y -= flippedY; |
| NSSize mouseOffset_unused = NSMakeSize(0.0, 0.0); |
| NSPasteboard *pboard = [NSPasteboard pasteboardWithName:NSPasteboardNameDrag]; |
| |
| [theWindow dragImage:dragImage |
| at:event_location |
| offset:mouseOffset_unused |
| event:m_lastEvent |
| pasteboard:pboard |
| source:m_lastView |
| slideBack:YES]; |
| |
| m_drag = nullptr; |
| return m_executed_drop_action; |
| } |
| |
| bool QCocoaDrag::maybeDragMultipleItems() |
| { |
| Q_ASSERT(m_drag && m_drag->mimeData()); |
| Q_ASSERT(m_executed_drop_action == Qt::IgnoreAction); |
| |
| if (QOperatingSystemVersion::current() < QOperatingSystemVersion::MacOSMojave) { |
| // -dragImage: stopped working in 10.14 first. |
| return false; |
| } |
| |
| const QMacAutoReleasePool pool; |
| |
| NSWindow *theWindow = [m_lastEvent window]; |
| Q_ASSERT(theWindow); |
| |
| if (![theWindow.contentView respondsToSelector:@selector(draggingSession:sourceOperationMaskForDraggingContext:)]) |
| return false; |
| |
| auto *sourceView = static_cast<NSView<NSDraggingSource>*>(theWindow.contentView); |
| |
| const auto &qtUrls = m_drag->mimeData()->urls(); |
| NSPasteboard *dragBoard = [NSPasteboard pasteboardWithName:NSPasteboardNameDrag]; |
| |
| if (int(dragBoard.pasteboardItems.count) == 1 && qtUrls.size() <= 1) { |
| // Good old -dragImage: works perfectly for this ... |
| return false; |
| } |
| |
| std::vector<NSPasteboardItem *> nonUrls; |
| for (NSPasteboardItem *item in dragBoard.pasteboardItems) { |
| bool isUrl = false; |
| for (NSPasteboardType type in item.types) { |
| using NSStringRef = NSString *; |
| if ([type isEqualToString:NSStringRef(kUTTypeFileURL)]) { |
| isUrl = true; |
| break; |
| } |
| } |
| |
| if (!isUrl) |
| nonUrls.push_back(item); |
| } |
| |
| QPoint hotSpot = m_drag->hotSpot(); |
| const auto pixmap = dragPixmap(m_drag, hotSpot); |
| NSImage *dragImage = [NSImage imageFromQImage:pixmap.toImage()]; |
| Q_ASSERT(dragImage); |
| |
| NSMutableArray<NSDraggingItem *> *dragItems = [[[NSMutableArray alloc] init] autorelease]; |
| const NSPoint itemLocation = m_drag->hotSpot().toCGPoint(); |
| // 0. We start from URLs, which can be actually in a list (thus technically |
| // only ONE item in the pasteboard. The fact it's only one does not help, we are |
| // still getting an exception because of the number of items/images mismatch ... |
| for (const auto &qtUrl : qtUrls) { |
| NSURL *nsUrl = qtUrl.toNSURL(); |
| auto *newItem = [[[NSDraggingItem alloc] initWithPasteboardWriter:nsUrl] autorelease]; |
| const NSRect itemFrame = NSMakeRect(itemLocation.x, itemLocation.y, |
| dragImage.size.width, |
| dragImage.size.height); |
| [newItem setDraggingFrame:itemFrame contents:dragImage]; |
| [dragItems addObject:newItem]; |
| } |
| // 1. Repeat for non-url items, if any: |
| for (auto *pbItem : nonUrls) { |
| auto *newItem = [[[NSDraggingItem alloc] initWithPasteboardWriter:pbItem] autorelease]; |
| const NSRect itemFrame = NSMakeRect(itemLocation.x, itemLocation.y, |
| dragImage.size.width, |
| dragImage.size.height); |
| [newItem setDraggingFrame:itemFrame contents:dragImage]; |
| [dragItems addObject:newItem]; |
| } |
| |
| [sourceView beginDraggingSessionWithItems:dragItems event:m_lastEvent source:sourceView]; |
| internalDragLoop.exec(); |
| return true; |
| } |
| |
| void QCocoaDrag::setAcceptedAction(Qt::DropAction act) |
| { |
| m_executed_drop_action = act; |
| } |
| |
| void QCocoaDrag::exitDragLoop() |
| { |
| if (internalDragLoop.isRunning()) |
| internalDragLoop.exit(); |
| } |
| |
| |
| QPixmap QCocoaDrag::dragPixmap(QDrag *drag, QPoint &hotSpot) const |
| { |
| const QMimeData* data = drag->mimeData(); |
| QPixmap pm = drag->pixmap(); |
| |
| if (pm.isNull()) { |
| QFont f(qApp->font()); |
| f.setPointSize(12); |
| QFontMetrics fm(f); |
| |
| if (data->hasImage()) { |
| const QImage img = data->imageData().value<QImage>(); |
| if (!img.isNull()) { |
| pm = QPixmap::fromImage(img).scaledToWidth(dragImageMaxChars *fm.averageCharWidth()); |
| } |
| } |
| |
| if (pm.isNull() && (data->hasText() || data->hasUrls()) ) { |
| QString s = data->hasText() ? data->text() : data->urls().first().toString(); |
| if (s.length() > dragImageMaxChars) |
| s = s.left(dragImageMaxChars -3) + QChar(0x2026); |
| if (!s.isEmpty()) { |
| const int width = fm.horizontalAdvance(s); |
| const int height = fm.height(); |
| if (width > 0 && height > 0) { |
| qreal dpr = 1.0; |
| if (const QWindow *sourceWindow = qobject_cast<QWindow *>(drag->source())) { |
| dpr = sourceWindow->devicePixelRatio(); |
| } |
| #ifndef QT_NO_WIDGETS |
| else if (const QWidget *sourceWidget = qobject_cast<QWidget *>(drag->source())) { |
| if (const QWindow *sourceWindow = sourceWidget->window()->windowHandle()) |
| dpr = sourceWindow->devicePixelRatio(); |
| } |
| #endif |
| else { |
| if (const QWindow *focusWindow = qApp->focusWindow()) |
| dpr = focusWindow->devicePixelRatio(); |
| } |
| pm = QPixmap(width * dpr, height * dpr); |
| pm.setDevicePixelRatio(dpr); |
| QPainter p(&pm); |
| p.fillRect(0, 0, pm.width(), pm.height(), Qt::color0); |
| p.setPen(Qt::color1); |
| p.setFont(f); |
| p.drawText(0, fm.ascent(), s); |
| p.end(); |
| hotSpot = QPoint(pm.width() / 2, pm.height() / 2); |
| } |
| } |
| } |
| } |
| |
| if (pm.isNull()) |
| pm = defaultPixmap(); |
| |
| return pm; |
| } |
| |
| QCocoaDropData::QCocoaDropData(NSPasteboard *pasteboard) |
| { |
| dropPasteboard = reinterpret_cast<CFStringRef>(const_cast<const NSString *>([pasteboard name])); |
| CFRetain(dropPasteboard); |
| } |
| |
| QCocoaDropData::~QCocoaDropData() |
| { |
| CFRelease(dropPasteboard); |
| } |
| |
| QStringList QCocoaDropData::formats_sys() const |
| { |
| QStringList formats; |
| PasteboardRef board; |
| if (PasteboardCreate(dropPasteboard, &board) != noErr) { |
| qDebug("DnD: Cannot get PasteBoard!"); |
| return formats; |
| } |
| formats = QMacPasteboard(board, QMacInternalPasteboardMime::MIME_DND).formats(); |
| return formats; |
| } |
| |
| QVariant QCocoaDropData::retrieveData_sys(const QString &mimeType, QVariant::Type type) const |
| { |
| QVariant data; |
| PasteboardRef board; |
| if (PasteboardCreate(dropPasteboard, &board) != noErr) { |
| qDebug("DnD: Cannot get PasteBoard!"); |
| return data; |
| } |
| data = QMacPasteboard(board, QMacInternalPasteboardMime::MIME_DND).retrieveData(mimeType, type); |
| CFRelease(board); |
| return data; |
| } |
| |
| bool QCocoaDropData::hasFormat_sys(const QString &mimeType) const |
| { |
| bool has = false; |
| PasteboardRef board; |
| if (PasteboardCreate(dropPasteboard, &board) != noErr) { |
| qDebug("DnD: Cannot get PasteBoard!"); |
| return has; |
| } |
| has = QMacPasteboard(board, QMacInternalPasteboardMime::MIME_DND).hasFormat(mimeType); |
| CFRelease(board); |
| return has; |
| } |
| |
| |
| QT_END_NAMESPACE |
| |