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