1/**************************************************************************** 2** 3** Copyright (C) 2016 The Qt Company Ltd. 4** Contact: https://www.qt.io/licensing/ 5** 6** This file is part of the plugins of the Qt Toolkit. 7** 8** $QT_BEGIN_LICENSE:LGPL$ 9** Commercial License Usage 10** Licensees holding valid commercial Qt licenses may use this file in 11** accordance with the commercial license agreement provided with the 12** Software or, alternatively, in accordance with the terms contained in 13** a written agreement between you and The Qt Company. For licensing terms 14** and conditions see https://www.qt.io/terms-conditions. For further 15** information use the contact form at https://www.qt.io/contact-us. 16** 17** GNU Lesser General Public License Usage 18** Alternatively, this file may be used under the terms of the GNU Lesser 19** General Public License version 3 as published by the Free Software 20** Foundation and appearing in the file LICENSE.LGPL3 included in the 21** packaging of this file. Please review the following information to 22** ensure the GNU Lesser General Public License version 3 requirements 23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. 24** 25** GNU General Public License Usage 26** Alternatively, this file may be used under the terms of the GNU 27** General Public License version 2.0 or (at your option) the GNU General 28** Public license version 3 or any later version approved by the KDE Free 29** Qt Foundation. The licenses are as published by the Free Software 30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 31** included in the packaging of this file. Please review the following 32** information to ensure the GNU General Public License requirements will 33** be met: https://www.gnu.org/licenses/gpl-2.0.html and 34** https://www.gnu.org/licenses/gpl-3.0.html. 35** 36** $QT_END_LICENSE$ 37** 38****************************************************************************/ 39 40#include "qcocoadrag.h" 41#include "qmacclipboard.h" 42#include "qcocoahelpers.h" 43#ifndef QT_NO_WIDGETS 44#include <QtWidgets/qwidget.h> 45#endif 46#include <QtGui/private/qcoregraphics_p.h> 47#include <QtCore/qsysinfo.h> 48 49#include <vector> 50 51QT_BEGIN_NAMESPACE 52 53static const int dragImageMaxChars = 26; 54 55QCocoaDrag::QCocoaDrag() : 56 m_drag(nullptr) 57{ 58 m_lastEvent = nil; 59 m_lastView = nil; 60} 61 62QCocoaDrag::~QCocoaDrag() 63{ 64 [m_lastEvent release]; 65} 66 67void QCocoaDrag::setLastMouseEvent(NSEvent *event, NSView *view) 68{ 69 [m_lastEvent release]; 70 m_lastEvent = [event copy]; 71 m_lastView = view; 72} 73 74QMimeData *QCocoaDrag::dragMimeData() 75{ 76 if (m_drag) 77 return m_drag->mimeData(); 78 79 return nullptr; 80} 81 82Qt::DropAction QCocoaDrag::defaultAction(Qt::DropActions possibleActions, 83 Qt::KeyboardModifiers modifiers) const 84{ 85 Qt::DropAction default_action = Qt::IgnoreAction; 86 87 if (currentDrag()) { 88 default_action = currentDrag()->defaultAction(); 89 possibleActions = currentDrag()->supportedActions(); 90 } 91 92 if (default_action == Qt::IgnoreAction) { 93 //This means that the drag was initiated by QDrag::start and we need to 94 //preserve the old behavior 95 default_action = Qt::CopyAction; 96 } 97 98 if (modifiers & Qt::ControlModifier && modifiers & Qt::AltModifier) 99 default_action = Qt::LinkAction; 100 else if (modifiers & Qt::AltModifier) 101 default_action = Qt::CopyAction; 102 else if (modifiers & Qt::ControlModifier) 103 default_action = Qt::MoveAction; 104 105#ifdef QDND_DEBUG 106 qDebug("possible actions : %s", dragActionsToString(possibleActions).latin1()); 107#endif 108 109 // Check if the action determined is allowed 110 if (!(possibleActions & default_action)) { 111 if (possibleActions & Qt::CopyAction) 112 default_action = Qt::CopyAction; 113 else if (possibleActions & Qt::MoveAction) 114 default_action = Qt::MoveAction; 115 else if (possibleActions & Qt::LinkAction) 116 default_action = Qt::LinkAction; 117 else 118 default_action = Qt::IgnoreAction; 119 } 120 121#ifdef QDND_DEBUG 122 qDebug("default action : %s", dragActionsToString(default_action).latin1()); 123#endif 124 125 return default_action; 126} 127 128 129Qt::DropAction QCocoaDrag::drag(QDrag *o) 130{ 131 m_drag = o; 132 m_executed_drop_action = Qt::IgnoreAction; 133 134 QMacPasteboard dragBoard(CFStringRef(NSPasteboardNameDrag), QMacInternalPasteboardMime::MIME_DND); 135 m_drag->mimeData()->setData(QLatin1String("application/x-qt-mime-type-name"), QByteArray("dummy")); 136 dragBoard.setMimeData(m_drag->mimeData(), QMacPasteboard::LazyRequest); 137 138 if (maybeDragMultipleItems()) 139 return m_executed_drop_action; 140 141 QPoint hotSpot = m_drag->hotSpot(); 142 QPixmap pm = dragPixmap(m_drag, hotSpot); 143 NSImage *dragImage = [NSImage imageFromQImage:pm.toImage()]; 144 Q_ASSERT(dragImage); 145 146 NSPoint event_location = [m_lastEvent locationInWindow]; 147 NSWindow *theWindow = [m_lastEvent window]; 148 Q_ASSERT(theWindow); 149 event_location.x -= hotSpot.x(); 150 CGFloat flippedY = dragImage.size.height - hotSpot.y(); 151 event_location.y -= flippedY; 152 NSSize mouseOffset_unused = NSMakeSize(0.0, 0.0); 153 NSPasteboard *pboard = [NSPasteboard pasteboardWithName:NSPasteboardNameDrag]; 154 155 [theWindow dragImage:dragImage 156 at:event_location 157 offset:mouseOffset_unused 158 event:m_lastEvent 159 pasteboard:pboard 160 source:m_lastView 161 slideBack:YES]; 162 163 m_drag = nullptr; 164 return m_executed_drop_action; 165} 166 167bool QCocoaDrag::maybeDragMultipleItems() 168{ 169 Q_ASSERT(m_drag && m_drag->mimeData()); 170 Q_ASSERT(m_executed_drop_action == Qt::IgnoreAction); 171 172 if (QOperatingSystemVersion::current() < QOperatingSystemVersion::MacOSMojave) { 173 // -dragImage: stopped working in 10.14 first. 174 return false; 175 } 176 177 const QMacAutoReleasePool pool; 178 179 NSWindow *theWindow = [m_lastEvent window]; 180 Q_ASSERT(theWindow); 181 182 if (![theWindow.contentView respondsToSelector:@selector(draggingSession:sourceOperationMaskForDraggingContext:)]) 183 return false; 184 185 auto *sourceView = static_cast<NSView<NSDraggingSource>*>(theWindow.contentView); 186 187 const auto &qtUrls = m_drag->mimeData()->urls(); 188 NSPasteboard *dragBoard = [NSPasteboard pasteboardWithName:NSPasteboardNameDrag]; 189 190 if (qtUrls.size() <= 1) { 191 // Good old -dragImage: works perfectly for this ... 192 return false; 193 } 194 195 std::vector<NSPasteboardItem *> nonUrls; 196 for (NSPasteboardItem *item in dragBoard.pasteboardItems) { 197 bool isUrl = false; 198 for (NSPasteboardType type in item.types) { 199 using NSStringRef = NSString *; 200 if ([type isEqualToString:NSStringRef(kUTTypeFileURL)]) { 201 isUrl = true; 202 break; 203 } 204 } 205 206 if (!isUrl) 207 nonUrls.push_back(item); 208 } 209 210 QPoint hotSpot = m_drag->hotSpot(); 211 const auto pixmap = dragPixmap(m_drag, hotSpot); 212 NSImage *dragImage = [NSImage imageFromQImage:pixmap.toImage()]; 213 Q_ASSERT(dragImage); 214 215 NSMutableArray<NSDraggingItem *> *dragItems = [[[NSMutableArray alloc] init] autorelease]; 216 const NSPoint itemLocation = m_drag->hotSpot().toCGPoint(); 217 // 0. We start from URLs, which can be actually in a list (thus technically 218 // only ONE item in the pasteboard. The fact it's only one does not help, we are 219 // still getting an exception because of the number of items/images mismatch ... 220 // We only set the image for the first item and nil for the rest, the image already 221 // contains a combined picture for all urls we drag. 222 auto imageOrNil = dragImage; 223 for (const auto &qtUrl : qtUrls) { 224 NSURL *nsUrl = qtUrl.toNSURL(); 225 auto *newItem = [[[NSDraggingItem alloc] initWithPasteboardWriter:nsUrl] autorelease]; 226 const NSRect itemFrame = NSMakeRect(itemLocation.x, itemLocation.y, 227 dragImage.size.width, 228 dragImage.size.height); 229 230 [newItem setDraggingFrame:itemFrame contents:imageOrNil]; 231 imageOrNil = nil; 232 [dragItems addObject:newItem]; 233 } 234 // 1. Repeat for non-url items, if any: 235 for (auto *pbItem : nonUrls) { 236 auto *newItem = [[[NSDraggingItem alloc] initWithPasteboardWriter:pbItem] autorelease]; 237 const NSRect itemFrame = NSMakeRect(itemLocation.x, itemLocation.y, 238 dragImage.size.width, 239 dragImage.size.height); 240 [newItem setDraggingFrame:itemFrame contents:imageOrNil]; 241 [dragItems addObject:newItem]; 242 } 243 244 [sourceView beginDraggingSessionWithItems:dragItems event:m_lastEvent source:sourceView]; 245 internalDragLoop.exec(); 246 return true; 247} 248 249void QCocoaDrag::setAcceptedAction(Qt::DropAction act) 250{ 251 m_executed_drop_action = act; 252} 253 254void QCocoaDrag::exitDragLoop() 255{ 256 if (internalDragLoop.isRunning()) 257 internalDragLoop.exit(); 258} 259 260 261QPixmap QCocoaDrag::dragPixmap(QDrag *drag, QPoint &hotSpot) const 262{ 263 const QMimeData* data = drag->mimeData(); 264 QPixmap pm = drag->pixmap(); 265 266 if (pm.isNull()) { 267 QFont f(qApp->font()); 268 f.setPointSize(12); 269 QFontMetrics fm(f); 270 271 if (data->hasImage()) { 272 const QImage img = data->imageData().value<QImage>(); 273 if (!img.isNull()) { 274 pm = QPixmap::fromImage(img).scaledToWidth(dragImageMaxChars *fm.averageCharWidth()); 275 } 276 } 277 278 if (pm.isNull() && (data->hasText() || data->hasUrls()) ) { 279 QString s = data->hasText() ? data->text() : data->urls().first().toString(); 280 if (s.length() > dragImageMaxChars) 281 s = s.left(dragImageMaxChars -3) + QChar(0x2026); 282 if (!s.isEmpty()) { 283 const int width = fm.horizontalAdvance(s); 284 const int height = fm.height(); 285 if (width > 0 && height > 0) { 286 qreal dpr = 1.0; 287 if (const QWindow *sourceWindow = qobject_cast<QWindow *>(drag->source())) { 288 dpr = sourceWindow->devicePixelRatio(); 289 } 290#ifndef QT_NO_WIDGETS 291 else if (const QWidget *sourceWidget = qobject_cast<QWidget *>(drag->source())) { 292 if (const QWindow *sourceWindow = sourceWidget->window()->windowHandle()) 293 dpr = sourceWindow->devicePixelRatio(); 294 } 295#endif 296 else { 297 if (const QWindow *focusWindow = qApp->focusWindow()) 298 dpr = focusWindow->devicePixelRatio(); 299 } 300 pm = QPixmap(width * dpr, height * dpr); 301 pm.setDevicePixelRatio(dpr); 302 QPainter p(&pm); 303 p.fillRect(0, 0, pm.width(), pm.height(), Qt::color0); 304 p.setPen(Qt::color1); 305 p.setFont(f); 306 p.drawText(0, fm.ascent(), s); 307 p.end(); 308 hotSpot = QPoint(pm.width() / 2, pm.height() / 2); 309 } 310 } 311 } 312 } 313 314 if (pm.isNull()) 315 pm = defaultPixmap(); 316 317 return pm; 318} 319 320QCocoaDropData::QCocoaDropData(NSPasteboard *pasteboard) 321{ 322 dropPasteboard = reinterpret_cast<CFStringRef>(const_cast<const NSString *>([pasteboard name])); 323 CFRetain(dropPasteboard); 324} 325 326QCocoaDropData::~QCocoaDropData() 327{ 328 CFRelease(dropPasteboard); 329} 330 331QStringList QCocoaDropData::formats_sys() const 332{ 333 QStringList formats; 334 PasteboardRef board; 335 if (PasteboardCreate(dropPasteboard, &board) != noErr) { 336 qDebug("DnD: Cannot get PasteBoard!"); 337 return formats; 338 } 339 formats = QMacPasteboard(board, QMacInternalPasteboardMime::MIME_DND).formats(); 340 return formats; 341} 342 343QVariant QCocoaDropData::retrieveData_sys(const QString &mimeType, QVariant::Type type) const 344{ 345 QVariant data; 346 PasteboardRef board; 347 if (PasteboardCreate(dropPasteboard, &board) != noErr) { 348 qDebug("DnD: Cannot get PasteBoard!"); 349 return data; 350 } 351 data = QMacPasteboard(board, QMacInternalPasteboardMime::MIME_DND).retrieveData(mimeType, type); 352 CFRelease(board); 353 return data; 354} 355 356bool QCocoaDropData::hasFormat_sys(const QString &mimeType) const 357{ 358 bool has = false; 359 PasteboardRef board; 360 if (PasteboardCreate(dropPasteboard, &board) != noErr) { 361 qDebug("DnD: Cannot get PasteBoard!"); 362 return has; 363 } 364 has = QMacPasteboard(board, QMacInternalPasteboardMime::MIME_DND).hasFormat(mimeType); 365 CFRelease(board); 366 return has; 367} 368 369 370QT_END_NAMESPACE 371 372