1/****************************************************************************
2**
3** Copyright (C) 2018 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// This file is included from qnsview.mm, and only used to organize the code
41
42@implementation QNSView (Dragging)
43
44-(void)registerDragTypes
45{
46    QMacAutoReleasePool pool;
47
48    NSString * const mimeTypeGeneric = @"com.trolltech.qt.MimeTypeName";
49    NSMutableArray<NSString *> *supportedTypes = [NSMutableArray<NSString *> arrayWithArray:@[
50                   NSColorPboardType,
51                   NSFilenamesPboardType, NSStringPboardType,
52                   NSFilenamesPboardType, NSPostScriptPboardType, NSTIFFPboardType,
53                   NSRTFPboardType, NSTabularTextPboardType, NSFontPboardType,
54                   NSRulerPboardType, NSFileContentsPboardType, NSColorPboardType,
55                   NSRTFDPboardType, NSHTMLPboardType,
56                   NSURLPboardType, NSPDFPboardType, NSVCardPboardType,
57                   NSFilesPromisePboardType, NSInkTextPboardType,
58                   NSMultipleTextSelectionPboardType, mimeTypeGeneric]];
59
60    // Add custom types supported by the application
61    for (const QString &customType : qt_mac_enabledDraggedTypes())
62        [supportedTypes addObject:customType.toNSString()];
63
64    [self registerForDraggedTypes:supportedTypes];
65}
66
67static QWindow *findEventTargetWindow(QWindow *candidate)
68{
69    while (candidate) {
70        if (!(candidate->flags() & Qt::WindowTransparentForInput))
71            return candidate;
72        candidate = candidate->parent();
73    }
74    return candidate;
75}
76
77static QPoint mapWindowCoordinates(QWindow *source, QWindow *target, QPoint point)
78{
79    return target->mapFromGlobal(source->mapToGlobal(point));
80}
81
82- (NSDragOperation)draggingSession:(NSDraggingSession *)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context
83{
84    Q_UNUSED(session);
85    Q_UNUSED(context);
86
87    QCocoaDrag* nativeDrag = QCocoaIntegration::instance()->drag();
88    return qt_mac_mapDropActions(nativeDrag->currentDrag()->supportedActions());
89}
90
91- (BOOL)ignoreModifierKeysForDraggingSession:(NSDraggingSession *)session
92{
93    Q_UNUSED(session);
94    // According to the "Dragging Sources" chapter on Cocoa DnD Programming
95    // (https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/DragandDrop/Concepts/dragsource.html),
96    // if the control, option, or command key is pressed, the source’s
97    // operation mask is filtered to only contain a reduced set of operations.
98    //
99    // Since Qt already takes care of tracking the keyboard modifiers, we
100    // don't need (or want) Cocoa to filter anything. Instead, we'll let
101    // the application do the actual filtering.
102    return YES;
103}
104
105- (BOOL)wantsPeriodicDraggingUpdates
106{
107    // From the documentation:
108    //
109    // "If the destination returns NO, these messages are sent only when the mouse moves
110    //  or a modifier flag changes. Otherwise the destination gets the default behavior,
111    //  where it receives periodic dragging-updated messages even if nothing changes."
112    //
113    // We do not want these constant drag update events while mouse is stationary,
114    // since we do all animations (autoscroll) with timers.
115    return NO;
116}
117
118
119- (BOOL)wantsPeriodicDraggingUpdates:(void *)dummy
120{
121    // This method never gets called. It's a workaround for Apple's
122    // bug: they first respondsToSelector : @selector(wantsPeriodicDraggingUpdates:)
123    // (note ':') and then call -wantsPeriodicDraggingUpdate (without colon).
124    // So, let's make them happy.
125    Q_UNUSED(dummy);
126
127    return NO;
128}
129
130- (void)updateCursorFromDragResponse:(QPlatformDragQtResponse)response drag:(QCocoaDrag *)drag
131{
132    const QPixmap pixmapCursor = drag->currentDrag()->dragCursor(response.acceptedAction());
133    NSCursor *nativeCursor = nil;
134
135    if (pixmapCursor.isNull()) {
136        switch (response.acceptedAction()) {
137        case Qt::CopyAction:
138            nativeCursor = [NSCursor dragCopyCursor];
139            break;
140        case Qt::LinkAction:
141            nativeCursor = [NSCursor dragLinkCursor];
142            break;
143        case Qt::IgnoreAction:
144            // Uncomment the next lines if forbidden cursor is wanted on undroppable targets.
145            /*nativeCursor = [NSCursor operationNotAllowedCursor];
146            break;*/
147        case Qt::MoveAction:
148        default:
149            nativeCursor = [NSCursor arrowCursor];
150            break;
151        }
152    } else {
153        auto *nsimage = [NSImage imageFromQImage:pixmapCursor.toImage()];
154        nativeCursor = [[NSCursor alloc] initWithImage:nsimage hotSpot:NSZeroPoint];
155    }
156
157    // Change the cursor
158    [nativeCursor set];
159
160    // Make sure the cursor is updated correctly if the mouse does not move and window is under cursor
161    // by creating a fake move event, unless on 10.14 and later where doing so will trigger a security
162    // warning dialog. FIXME: Find a way to update the cursor without fake mouse events.
163    if (QOperatingSystemVersion::current() >= QOperatingSystemVersion::MacOSMojave)
164        return;
165
166    if (m_updatingDrag)
167        return;
168
169    QCFType<CGEventRef> moveEvent = CGEventCreateMouseEvent(
170        nullptr, kCGEventMouseMoved, QCursor::pos().toCGPoint(),
171        kCGMouseButtonLeft // ignored
172    );
173    CGEventPost(kCGHIDEventTap, moveEvent);
174}
175
176- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender
177{
178    return [self handleDrag:(QEvent::DragEnter) sender:sender];
179}
180
181- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender
182{
183    QScopedValueRollback<bool> rollback(m_updatingDrag, true);
184    return [self handleDrag:(QEvent::DragMove) sender:sender];
185}
186
187// Sends drag update to Qt, return the action
188- (NSDragOperation)handleDrag:(QEvent::Type)dragType sender:(id<NSDraggingInfo>)sender
189{
190    if (!m_platformWindow)
191        return NSDragOperationNone;
192
193    QPoint windowPoint = QPointF::fromCGPoint([self convertPoint:sender.draggingLocation fromView:nil]).toPoint();
194
195    Qt::DropActions qtAllowed = qt_mac_mapNSDragOperations(sender.draggingSourceOperationMask);
196
197    QWindow *target = findEventTargetWindow(m_platformWindow->window());
198    if (!target)
199        return NSDragOperationNone;
200
201    const auto modifiers = [QNSView convertKeyModifiers:NSApp.currentEvent.modifierFlags];
202    const auto buttons = currentlyPressedMouseButtons();
203    const auto point = mapWindowCoordinates(m_platformWindow->window(), target, windowPoint);
204
205    if (dragType == QEvent::DragEnter)
206        qCDebug(lcQpaMouse) << dragType << self << "at" << windowPoint;
207    else
208        qCDebug(lcQpaMouse) << dragType << "at" << windowPoint << "with" << buttons;
209
210    QPlatformDragQtResponse response(false, Qt::IgnoreAction, QRect());
211    QCocoaDrag* nativeDrag = QCocoaIntegration::instance()->drag();
212    if (nativeDrag->currentDrag()) {
213        // The drag was started from within the application
214        response = QWindowSystemInterface::handleDrag(target, nativeDrag->dragMimeData(),
215                                                      point, qtAllowed, buttons, modifiers);
216        [self updateCursorFromDragResponse:response drag:nativeDrag];
217    } else {
218        QCocoaDropData mimeData(sender.draggingPasteboard);
219        response = QWindowSystemInterface::handleDrag(target, &mimeData,
220                                                      point, qtAllowed, buttons, modifiers);
221    }
222
223    return qt_mac_mapDropAction(response.acceptedAction());
224}
225
226- (void)draggingExited:(id<NSDraggingInfo>)sender
227{
228    if (!m_platformWindow)
229        return;
230
231    QWindow *target = findEventTargetWindow(m_platformWindow->window());
232    if (!target)
233        return;
234
235    auto *nativeDrag = QCocoaIntegration::instance()->drag();
236    Q_ASSERT(nativeDrag);
237    nativeDrag->exitDragLoop();
238
239    QPoint windowPoint = QPointF::fromCGPoint([self convertPoint:sender.draggingLocation fromView:nil]).toPoint();
240
241    qCDebug(lcQpaMouse) << QEvent::DragLeave << self << "at" << windowPoint;
242
243    // Send 0 mime data to indicate drag exit
244    QWindowSystemInterface::handleDrag(target, nullptr,
245                                       mapWindowCoordinates(m_platformWindow->window(), target, windowPoint),
246                                       Qt::IgnoreAction, Qt::NoButton, Qt::NoModifier);
247}
248
249// Called on drop, send the drop to Qt and return if it was accepted
250- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender
251{
252    if (!m_platformWindow)
253        return false;
254
255    QWindow *target = findEventTargetWindow(m_platformWindow->window());
256    if (!target)
257        return false;
258
259    QPoint windowPoint = QPointF::fromCGPoint([self convertPoint:sender.draggingLocation fromView:nil]).toPoint();
260
261    Qt::DropActions qtAllowed = qt_mac_mapNSDragOperations(sender.draggingSourceOperationMask);
262
263    QPlatformDropQtResponse response(false, Qt::IgnoreAction);
264    QCocoaDrag* nativeDrag = QCocoaIntegration::instance()->drag();
265    const auto modifiers = [QNSView convertKeyModifiers:NSApp.currentEvent.modifierFlags];
266    const auto buttons = currentlyPressedMouseButtons();
267    const auto point = mapWindowCoordinates(m_platformWindow->window(), target, windowPoint);
268
269    qCDebug(lcQpaMouse) << QEvent::Drop << "at" << windowPoint << "with" << buttons;
270
271    if (nativeDrag->currentDrag()) {
272        // The drag was started from within the application
273        response = QWindowSystemInterface::handleDrop(target, nativeDrag->dragMimeData(),
274                                                      point, qtAllowed, buttons, modifiers);
275        nativeDrag->setAcceptedAction(response.acceptedAction());
276    } else {
277        QCocoaDropData mimeData(sender.draggingPasteboard);
278        response = QWindowSystemInterface::handleDrop(target, &mimeData,
279                                                      point, qtAllowed, buttons, modifiers);
280    }
281    return response.isAccepted();
282}
283
284- (void)draggingSession:(NSDraggingSession *)session endedAtPoint:(NSPoint)screenPoint operation:(NSDragOperation)operation
285{
286    Q_UNUSED(session);
287    Q_UNUSED(screenPoint);
288    Q_UNUSED(operation);
289
290    if (!m_platformWindow)
291        return;
292
293    QWindow *target = findEventTargetWindow(m_platformWindow->window());
294    if (!target)
295        return;
296
297    QCocoaDrag* nativeDrag = QCocoaIntegration::instance()->drag();
298    Q_ASSERT(nativeDrag);
299    nativeDrag->exitDragLoop();
300    nativeDrag->setAcceptedAction(qt_mac_mapNSDragOperation(operation));
301
302    // Qt starts drag-and-drop on a mouse button press event. Cococa in
303    // this case won't send the matching release event, so we have to
304    // synthesize it here.
305    m_buttons = currentlyPressedMouseButtons();
306    const auto modifiers = [QNSView convertKeyModifiers:NSApp.currentEvent.modifierFlags];
307
308    NSPoint windowPoint = [self.window convertRectFromScreen:NSMakeRect(screenPoint.x, screenPoint.y, 1, 1)].origin;
309    NSPoint nsViewPoint = [self convertPoint: windowPoint fromView: nil];
310
311    QPoint qtWindowPoint(nsViewPoint.x, nsViewPoint.y);
312    QPoint qtScreenPoint = QCocoaScreen::mapFromNative(screenPoint).toPoint();
313
314    QWindowSystemInterface::handleMouseEvent(
315        target,
316        mapWindowCoordinates(m_platformWindow->window(), target, qtWindowPoint),
317        qtScreenPoint,
318        m_buttons,
319        Qt::NoButton,
320        QEvent::MouseButtonRelease,
321        modifiers);
322
323    qCDebug(lcQpaMouse) << "Drag session" << session << "ended, with" << m_buttons;
324}
325
326@end
327