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/*
43    The reason for using this helper is to ensure that QNSView doesn't implement
44    the NSResponder callbacks for mouseEntered, mouseExited, and mouseMoved.
45
46    If it did, we would get mouse events though the responder chain as well,
47    for example if a subview has a tracking area of its own and calls super
48    in the handler, which results in forwarding the event though the responder
49    chain. The same applies if NSWindow.acceptsMouseMovedEvents is YES.
50
51    By having a helper as the target for our tracking areas, we know for sure
52    that the events we are getting stem from our own tracking areas.
53
54    FIXME: Ideally we wouldn't need this workaround, and would correctly
55    interact with the responder chain by e.g. calling super if Qt does not
56    accept the mouse event
57*/
58@implementation QNSViewMouseMoveHelper {
59    QNSView *view;
60}
61
62- (instancetype)initWithView:(QNSView *)theView
63{
64    if ((self = [super init]))
65        view = theView;
66
67    return self;
68}
69
70- (void)mouseMoved:(NSEvent *)theEvent
71{
72    [view mouseMovedImpl:theEvent];
73}
74
75- (void)mouseEntered:(NSEvent *)theEvent
76{
77    [view mouseEnteredImpl:theEvent];
78}
79
80- (void)mouseExited:(NSEvent *)theEvent
81{
82    [view mouseExitedImpl:theEvent];
83}
84
85- (void)cursorUpdate:(NSEvent *)theEvent
86{
87    [view cursorUpdate:theEvent];
88}
89
90@end
91
92@implementation QNSView (MouseAPI)
93
94- (void)resetMouseButtons
95{
96    qCDebug(lcQpaMouse) << "Reseting mouse buttons";
97    m_buttons = Qt::NoButton;
98    m_frameStrutButtons = Qt::NoButton;
99}
100
101- (void)handleFrameStrutMouseEvent:(NSEvent *)theEvent
102{
103    if (!m_platformWindow)
104        return;
105
106    // get m_buttons in sync
107    // Don't send frme strut events if we are in the middle of a mouse drag.
108    if (m_buttons != Qt::NoButton)
109        return;
110
111    switch (theEvent.type) {
112    case NSEventTypeLeftMouseDown:
113    case NSEventTypeLeftMouseDragged:
114        m_frameStrutButtons |= Qt::LeftButton;
115        break;
116    case NSEventTypeLeftMouseUp:
117         m_frameStrutButtons &= ~Qt::LeftButton;
118         break;
119    case NSEventTypeRightMouseDown:
120    case NSEventTypeRightMouseDragged:
121        m_frameStrutButtons |= Qt::RightButton;
122        break;
123    case NSEventTypeRightMouseUp:
124        m_frameStrutButtons &= ~Qt::RightButton;
125        break;
126    case NSEventTypeOtherMouseDown:
127        m_frameStrutButtons |= cocoaButton2QtButton(theEvent.buttonNumber);
128        break;
129    case NSEventTypeOtherMouseUp:
130        m_frameStrutButtons &= ~cocoaButton2QtButton(theEvent.buttonNumber);
131    default:
132        break;
133    }
134
135    NSWindow *window = [self window];
136    NSPoint windowPoint = [theEvent locationInWindow];
137
138    int windowScreenY = [window frame].origin.y + [window frame].size.height;
139    NSPoint windowCoord = [self convertPoint:[self frame].origin toView:nil];
140    int viewScreenY = [window convertRectToScreen:NSMakeRect(windowCoord.x, windowCoord.y, 0, 0)].origin.y;
141    int titleBarHeight = windowScreenY - viewScreenY;
142
143    NSPoint nsViewPoint = [self convertPoint: windowPoint fromView: nil];
144    QPoint qtWindowPoint = QPoint(nsViewPoint.x, titleBarHeight + nsViewPoint.y);
145    NSPoint screenPoint = [window convertRectToScreen:NSMakeRect(windowPoint.x, windowPoint.y, 0, 0)].origin;
146    QPoint qtScreenPoint = QCocoaScreen::mapFromNative(screenPoint).toPoint();
147
148    ulong timestamp = [theEvent timestamp] * 1000;
149
150    const auto button = cocoaButton2QtButton(theEvent);
151    auto eventType = [&]() {
152        switch (theEvent.type) {
153        case NSEventTypeLeftMouseDown:
154        case NSEventTypeRightMouseDown:
155        case NSEventTypeOtherMouseDown:
156            return QEvent::NonClientAreaMouseButtonPress;
157
158        case NSEventTypeLeftMouseUp:
159        case NSEventTypeRightMouseUp:
160        case NSEventTypeOtherMouseUp:
161            return QEvent::NonClientAreaMouseButtonRelease;
162
163        case NSEventTypeLeftMouseDragged:
164        case NSEventTypeRightMouseDragged:
165        case NSEventTypeOtherMouseDragged:
166            return QEvent::NonClientAreaMouseMove;
167
168        default:
169            break;
170        }
171
172        return QEvent::None;
173    }();
174
175    qCInfo(lcQpaMouse) << eventType << "at" << qtWindowPoint << "with" << m_frameStrutButtons << "in" << self.window;
176    QWindowSystemInterface::handleFrameStrutMouseEvent(m_platformWindow->window(),
177        timestamp, qtWindowPoint, qtScreenPoint, m_frameStrutButtons, button, eventType);
178}
179@end
180
181@implementation QNSView (Mouse)
182
183- (void)initMouse
184{
185    m_buttons = Qt::NoButton;
186    m_acceptedMouseDowns = Qt::NoButton;
187    m_frameStrutButtons = Qt::NoButton;
188
189    m_scrolling = false;
190    self.cursor = nil;
191
192    m_sendUpAsRightButton = false;
193    m_dontOverrideCtrlLMB = qt_mac_resolveOption(false, m_platformWindow->window(),
194            "_q_platform_MacDontOverrideCtrlLMB", "QT_MAC_DONT_OVERRIDE_CTRL_LMB");
195
196    m_mouseMoveHelper = [[QNSViewMouseMoveHelper alloc] initWithView:self];
197
198    NSUInteger trackingOptions = NSTrackingActiveInActiveApp
199        | NSTrackingMouseEnteredAndExited | NSTrackingCursorUpdate;
200
201    // Ideally, NSTrackingMouseMoved should be turned on only if QWidget::mouseTracking
202    // is enabled, hover is on, or a tool tip is set. Unfortunately, Qt will send "tooltip"
203    // events on mouse moves, so we need to turn it on in ALL case. That means EVERY QWindow
204    // gets to pay the cost of mouse moves delivered to it (Apple recommends keeping it OFF
205    // because there is a performance hit).
206    trackingOptions |= NSTrackingMouseMoved;
207
208    // Using NSTrackingInVisibleRect means AppKit will automatically synchronize the
209    // tracking rect with changes in the view's visible area, so leave it undefined.
210    trackingOptions |= NSTrackingInVisibleRect;
211    static const NSRect trackingRect = NSZeroRect;
212
213    QMacAutoReleasePool pool;
214    [self addTrackingArea:[[[NSTrackingArea alloc] initWithRect:trackingRect
215        options:trackingOptions owner:m_mouseMoveHelper userInfo:nil] autorelease]];
216}
217
218- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent
219{
220    Q_UNUSED(theEvent)
221    if (!m_platformWindow)
222        return NO;
223    if ([self isTransparentForUserInput])
224        return NO;
225    QPointF windowPoint;
226    QPointF screenPoint;
227    [self convertFromScreen:[NSEvent mouseLocation] toWindowPoint: &windowPoint andScreenPoint: &screenPoint];
228    if (!qt_window_private(m_platformWindow->window())->allowClickThrough(screenPoint.toPoint()))
229        return NO;
230    return YES;
231}
232
233- (NSPoint)screenMousePoint:(NSEvent *)theEvent
234{
235    NSPoint screenPoint;
236    if (theEvent) {
237        NSPoint windowPoint = [theEvent locationInWindow];
238        if (qIsNaN(windowPoint.x) || qIsNaN(windowPoint.y)) {
239            screenPoint = [NSEvent mouseLocation];
240        } else {
241            NSRect screenRect = [[theEvent window] convertRectToScreen:NSMakeRect(windowPoint.x, windowPoint.y, 1, 1)];
242            screenPoint = screenRect.origin;
243        }
244    } else {
245        screenPoint = [NSEvent mouseLocation];
246    }
247    return screenPoint;
248}
249
250- (void)handleMouseEvent:(NSEvent *)theEvent
251{
252    if (!m_platformWindow)
253        return;
254
255#ifndef QT_NO_TABLETEVENT
256    // Tablet events may come in via the mouse event handlers,
257    // check if this is a valid tablet event first.
258    if ([self handleTabletEvent: theEvent])
259        return;
260#endif
261
262    QPointF qtWindowPoint;
263    QPointF qtScreenPoint;
264    QNSView *targetView = self;
265    if (!targetView.platformWindow)
266        return;
267
268    // Popups implicitly grap mouse events; forward to the active popup if there is one
269    if (QCocoaWindow *popup = QCocoaIntegration::instance()->activePopupWindow()) {
270        // Tooltips must be transparent for mouse events
271        // The bug reference is QTBUG-46379
272        if (!popup->window()->flags().testFlag(Qt::ToolTip)) {
273            if (QNSView *popupView = qnsview_cast(popup->view()))
274                targetView = popupView;
275        }
276    }
277
278    [targetView convertFromScreen:[self screenMousePoint:theEvent] toWindowPoint:&qtWindowPoint andScreenPoint:&qtScreenPoint];
279    ulong timestamp = [theEvent timestamp] * 1000;
280
281    QCocoaDrag* nativeDrag = QCocoaIntegration::instance()->drag();
282    nativeDrag->setLastMouseEvent(theEvent, self);
283
284    const auto modifiers = [QNSView convertKeyModifiers:theEvent.modifierFlags];
285    auto button = cocoaButton2QtButton(theEvent);
286    if (button == Qt::LeftButton && m_sendUpAsRightButton)
287        button = Qt::RightButton;
288    const auto eventType = cocoaEvent2QtMouseEvent(theEvent);
289
290    if (eventType == QEvent::MouseMove)
291        qCDebug(lcQpaMouse) << eventType << "at" << qtWindowPoint << "with" << m_buttons;
292    else
293        qCInfo(lcQpaMouse) << eventType << "of" << button << "at" << qtWindowPoint << "with" << m_buttons;
294
295    QWindowSystemInterface::handleMouseEvent(targetView->m_platformWindow->window(),
296                                             timestamp, qtWindowPoint, qtScreenPoint,
297                                             m_buttons, button, eventType, modifiers);
298}
299
300- (bool)handleMouseDownEvent:(NSEvent *)theEvent
301{
302    if ([self isTransparentForUserInput])
303        return false;
304
305    const auto button = cocoaButton2QtButton(theEvent);
306
307    QPointF qtWindowPoint;
308    QPointF qtScreenPoint;
309    [self convertFromScreen:[self screenMousePoint:theEvent] toWindowPoint:&qtWindowPoint andScreenPoint:&qtScreenPoint];
310    Q_UNUSED(qtScreenPoint);
311
312    // Maintain masked state for the button for use by MouseDragged and MouseUp.
313    QRegion mask = m_platformWindow->window()->mask();
314    const bool masked = !mask.isEmpty() && !mask.contains(qtWindowPoint.toPoint());
315    if (masked)
316        m_acceptedMouseDowns &= ~button;
317    else
318        m_acceptedMouseDowns |= button;
319
320    // Forward masked out events to the next responder
321    if (masked)
322        return false;
323
324    m_buttons |= button;
325
326    [self handleMouseEvent:theEvent];
327    return true;
328}
329
330- (bool)handleMouseDraggedEvent:(NSEvent *)theEvent
331{
332    if ([self isTransparentForUserInput])
333        return false;
334
335    const auto button = cocoaButton2QtButton(theEvent);
336
337    // Forward the event to the next responder if Qt did not accept the
338    // corresponding mouse down for this button
339    if (!(m_acceptedMouseDowns & button) == button)
340        return false;
341
342    [self handleMouseEvent:theEvent];
343    return true;
344}
345
346- (bool)handleMouseUpEvent:(NSEvent *)theEvent
347{
348    if ([self isTransparentForUserInput])
349        return false;
350
351    auto button = cocoaButton2QtButton(theEvent);
352
353    // Forward the event to the next responder if Qt did not accept the
354    // corresponding mouse down for this button
355    if (!(m_acceptedMouseDowns & button) == button)
356        return false;
357
358    if (m_sendUpAsRightButton && button == Qt::LeftButton)
359        button = Qt::RightButton;
360
361    m_buttons &= ~button;
362
363    [self handleMouseEvent:theEvent];
364
365    if (button == Qt::RightButton)
366        m_sendUpAsRightButton = false;
367
368    return true;
369}
370
371- (void)mouseDown:(NSEvent *)theEvent
372{
373    if ([self isTransparentForUserInput])
374        return [super mouseDown:theEvent];
375    m_sendUpAsRightButton = false;
376
377    // Handle any active poup windows; clicking outisde them should close them
378    // all. Don't do anything or clicks inside one of the menus, let Cocoa
379    // handle that case. Note that in practice many windows of the Qt::Popup type
380    // will actually close themselves in this case using logic implemented in
381    // that particular poup type (for example context menus). However, Qt expects
382    // that plain popup QWindows will also be closed, so we implement the logic
383    // here as well.
384    QList<QCocoaWindow *> *popups = QCocoaIntegration::instance()->popupWindowStack();
385    if (!popups->isEmpty()) {
386        // Check if the click is outside all popups.
387        bool inside = false;
388        QPointF qtScreenPoint = QCocoaScreen::mapFromNative([self screenMousePoint:theEvent]);
389        for (QList<QCocoaWindow *>::const_iterator it = popups->begin(); it != popups->end(); ++it) {
390            if ((*it)->geometry().contains(qtScreenPoint.toPoint())) {
391                inside = true;
392                break;
393            }
394        }
395        // Close the popups if the click was outside.
396        if (!inside) {
397            bool selfClosed = false;
398            Qt::WindowType type = QCocoaIntegration::instance()->activePopupWindow()->window()->type();
399            while (QCocoaWindow *popup = QCocoaIntegration::instance()->popPopupWindow()) {
400                selfClosed = self == popup->view();
401                QWindowSystemInterface::handleCloseEvent(popup->window());
402                QWindowSystemInterface::flushWindowSystemEvents();
403                if (!m_platformWindow)
404                    return; // Bail out if window was destroyed
405            }
406            // Consume the mouse event when closing the popup, except for tool tips
407            // were it's expected that the event is processed normally.
408            if (type != Qt::ToolTip || selfClosed)
409                 return;
410        }
411    }
412
413    QPointF qtWindowPoint;
414    QPointF qtScreenPoint;
415    [self convertFromScreen:[self screenMousePoint:theEvent] toWindowPoint:&qtWindowPoint andScreenPoint:&qtScreenPoint];
416    Q_UNUSED(qtScreenPoint);
417
418    QRegion mask = m_platformWindow->window()->mask();
419    const bool masked = !mask.isEmpty() && !mask.contains(qtWindowPoint.toPoint());
420    // Maintain masked state for the button for use by MouseDragged and Up.
421    if (masked)
422        m_acceptedMouseDowns &= ~Qt::LeftButton;
423    else
424        m_acceptedMouseDowns |= Qt::LeftButton;
425
426    // Forward masked out events to the next responder
427    if (masked) {
428        [super mouseDown:theEvent];
429        return;
430    }
431
432    if ([self hasMarkedText]) {
433        [[NSTextInputContext currentInputContext] handleEvent:theEvent];
434    } else {
435        auto ctrlOrMetaModifier = qApp->testAttribute(Qt::AA_MacDontSwapCtrlAndMeta) ? Qt::ControlModifier : Qt::MetaModifier;
436        if (!m_dontOverrideCtrlLMB && [QNSView convertKeyModifiers:[theEvent modifierFlags]] & ctrlOrMetaModifier) {
437            m_buttons |= Qt::RightButton;
438            m_sendUpAsRightButton = true;
439        } else {
440            m_buttons |= Qt::LeftButton;
441        }
442        [self handleMouseEvent:theEvent];
443    }
444}
445
446- (void)mouseDragged:(NSEvent *)theEvent
447{
448    const bool accepted = [self handleMouseDraggedEvent:theEvent];
449    if (!accepted)
450        [super mouseDragged:theEvent];
451}
452
453- (void)mouseUp:(NSEvent *)theEvent
454{
455    const bool accepted = [self handleMouseUpEvent:theEvent];
456    if (!accepted)
457        [super mouseUp:theEvent];
458}
459
460- (void)rightMouseDown:(NSEvent *)theEvent
461{
462    const bool accepted = [self handleMouseDownEvent:theEvent];
463    if (!accepted)
464        [super rightMouseDown:theEvent];
465}
466
467- (void)rightMouseDragged:(NSEvent *)theEvent
468{
469    const bool accepted = [self handleMouseDraggedEvent:theEvent];
470    if (!accepted)
471        [super rightMouseDragged:theEvent];
472}
473
474- (void)rightMouseUp:(NSEvent *)theEvent
475{
476    const bool accepted = [self handleMouseUpEvent:theEvent];
477    if (!accepted)
478        [super rightMouseUp:theEvent];
479}
480
481- (void)otherMouseDown:(NSEvent *)theEvent
482{
483    const bool accepted = [self handleMouseDownEvent:theEvent];
484    if (!accepted)
485        [super otherMouseDown:theEvent];
486}
487
488- (void)otherMouseDragged:(NSEvent *)theEvent
489{
490    const bool accepted = [self handleMouseDraggedEvent:theEvent];
491    if (!accepted)
492        [super otherMouseDragged:theEvent];
493}
494
495- (void)otherMouseUp:(NSEvent *)theEvent
496{
497    const bool accepted = [self handleMouseUpEvent:theEvent];
498    if (!accepted)
499        [super otherMouseUp:theEvent];
500}
501
502- (void)cursorUpdate:(NSEvent *)theEvent
503{
504    // Note: We do not get this callback when moving from a subview that
505    // uses the legacy cursorRect API, so the cursor is reset to the arrow
506    // cursor. See rdar://34183708
507
508    auto previousCursor = NSCursor.currentCursor;
509
510    if (self.cursor)
511        [self.cursor set];
512    else
513        [super cursorUpdate:theEvent];
514
515    if (NSCursor.currentCursor != previousCursor)
516        qCInfo(lcQpaMouse) << "Cursor update for" << self << "resulted in new cursor" << NSCursor.currentCursor;
517}
518
519- (void)mouseMovedImpl:(NSEvent *)theEvent
520{
521    if (!m_platformWindow)
522        return;
523
524    if ([self isTransparentForUserInput])
525        return;
526
527    QPointF windowPoint;
528    QPointF screenPoint;
529    [self convertFromScreen:[self screenMousePoint:theEvent] toWindowPoint:&windowPoint andScreenPoint:&screenPoint];
530    QWindow *childWindow = m_platformWindow->childWindowAt(windowPoint.toPoint());
531
532    // Top-level windows generate enter-leave events for sub-windows.
533    // Qt wants to know which window (if any) will be entered at the
534    // the time of the leave. This is dificult to accomplish by
535    // handling mouseEnter and mouseLeave envents, since they are sent
536    // individually to different views.
537    if (m_platformWindow->isContentView() && childWindow) {
538        if (childWindow != m_platformWindow->m_enterLeaveTargetWindow) {
539            QWindowSystemInterface::handleEnterLeaveEvent(childWindow, m_platformWindow->m_enterLeaveTargetWindow, windowPoint, screenPoint);
540            m_platformWindow->m_enterLeaveTargetWindow = childWindow;
541        }
542    }
543
544    // Cocoa keeps firing mouse move events for obscured parent views. Qt should not
545    // send those events so filter them out here.
546    if (childWindow != m_platformWindow->window())
547        return;
548
549    [self handleMouseEvent: theEvent];
550}
551
552- (void)mouseEnteredImpl:(NSEvent *)theEvent
553{
554    Q_UNUSED(theEvent)
555    if (!m_platformWindow)
556        return;
557
558    m_platformWindow->m_windowUnderMouse = true;
559
560    if ([self isTransparentForUserInput])
561        return;
562
563    // Top-level windows generate enter events for sub-windows.
564    if (!m_platformWindow->isContentView())
565        return;
566
567    QPointF windowPoint;
568    QPointF screenPoint;
569    [self convertFromScreen:[self screenMousePoint:theEvent] toWindowPoint:&windowPoint andScreenPoint:&screenPoint];
570    m_platformWindow->m_enterLeaveTargetWindow = m_platformWindow->childWindowAt(windowPoint.toPoint());
571
572    qCInfo(lcQpaMouse) << QEvent::Enter << self << "at" << windowPoint << "with" << currentlyPressedMouseButtons();
573    QWindowSystemInterface::handleEnterEvent(m_platformWindow->m_enterLeaveTargetWindow, windowPoint, screenPoint);
574}
575
576- (void)mouseExitedImpl:(NSEvent *)theEvent
577{
578    Q_UNUSED(theEvent);
579    if (!m_platformWindow)
580        return;
581
582    m_platformWindow->m_windowUnderMouse = false;
583
584    if ([self isTransparentForUserInput])
585        return;
586
587    // Top-level windows generate leave events for sub-windows.
588    if (!m_platformWindow->isContentView())
589        return;
590
591    qCInfo(lcQpaMouse) << QEvent::Leave << self;
592    QWindowSystemInterface::handleLeaveEvent(m_platformWindow->m_enterLeaveTargetWindow);
593    m_platformWindow->m_enterLeaveTargetWindow = 0;
594}
595
596#if QT_CONFIG(wheelevent)
597- (void)scrollWheel:(NSEvent *)theEvent
598{
599    if (!m_platformWindow)
600        return;
601
602    if ([self isTransparentForUserInput])
603        return [super scrollWheel:theEvent];
604
605    QPoint angleDelta;
606    Qt::MouseEventSource source = Qt::MouseEventNotSynthesized;
607    if ([theEvent hasPreciseScrollingDeltas]) {
608        // The mouse device contains pixel scroll wheel support (Mighty Mouse, Trackpad).
609        // Since deviceDelta is delivered as pixels rather than degrees, we need to
610        // convert from pixels to degrees in a sensible manner.
611        // It looks like 1/4 degrees per pixel behaves most native.
612        // (NB: Qt expects the unit for delta to be 8 per degree):
613        const int pixelsToDegrees = 2; // 8 * 1/4
614        angleDelta.setX([theEvent scrollingDeltaX] * pixelsToDegrees);
615        angleDelta.setY([theEvent scrollingDeltaY] * pixelsToDegrees);
616        source = Qt::MouseEventSynthesizedBySystem;
617    } else {
618        // Remove acceleration, and use either -120 or 120 as delta:
619        angleDelta.setX(qBound(-120, int([theEvent deltaX] * 10000), 120));
620        angleDelta.setY(qBound(-120, int([theEvent deltaY] * 10000), 120));
621    }
622
623    QPoint pixelDelta;
624    if ([theEvent hasPreciseScrollingDeltas]) {
625        pixelDelta.setX([theEvent scrollingDeltaX]);
626        pixelDelta.setY([theEvent scrollingDeltaY]);
627    } else {
628        // docs: "In the case of !hasPreciseScrollingDeltas, multiply the delta with the line width."
629        // scrollingDeltaX seems to return a minimum value of 0.1 in this case, map that to two pixels.
630        const CGFloat lineWithEstimate = 20.0;
631        pixelDelta.setX([theEvent scrollingDeltaX] * lineWithEstimate);
632        pixelDelta.setY([theEvent scrollingDeltaY] * lineWithEstimate);
633    }
634
635    QPointF qt_windowPoint;
636    QPointF qt_screenPoint;
637    [self convertFromScreen:[self screenMousePoint:theEvent] toWindowPoint:&qt_windowPoint andScreenPoint:&qt_screenPoint];
638    NSTimeInterval timestamp = [theEvent timestamp];
639    ulong qt_timestamp = timestamp * 1000;
640
641    Qt::ScrollPhase phase = Qt::NoScrollPhase;
642    if (theEvent.phase == NSEventPhaseMayBegin || theEvent.phase == NSEventPhaseBegan) {
643        // MayBegin is likely to happen. We treat it the same as an actual begin,
644        // and follow it with an update when the actual begin is delivered.
645        phase = m_scrolling ? Qt::ScrollUpdate : Qt::ScrollBegin;
646        m_scrolling = true;
647    } else if (theEvent.phase == NSEventPhaseStationary || theEvent.phase == NSEventPhaseChanged) {
648        phase = Qt::ScrollUpdate;
649    } else if (theEvent.phase == NSEventPhaseEnded) {
650        // A scroll event phase may be followed by a momentum phase after the user releases
651        // the finger, and in that case we don't want to send a Qt::ScrollEnd until after
652        // the momentum phase has ended. Unfortunately there isn't any guaranteed way of
653        // knowing whether or not a NSEventPhaseEnded will be followed by a momentum phase.
654        // The best we can do is to look at the event queue and hope that the system has
655        // had time to emit a momentum phase event.
656        if ([NSApp nextEventMatchingMask:NSEventMaskScrollWheel untilDate:[NSDate distantPast]
657                inMode:@"QtMomementumEventSearchMode" dequeue:NO].momentumPhase == NSEventPhaseBegan) {
658            Q_ASSERT(pixelDelta.isNull() && angleDelta.isNull());
659            return; // Ignore this event, as it has a delta of 0,0
660        }
661        phase = Qt::ScrollEnd;
662        m_scrolling = false;
663    } else if (theEvent.momentumPhase == NSEventPhaseBegan) {
664        Q_ASSERT(!pixelDelta.isNull() && !angleDelta.isNull());
665        phase = Qt::ScrollUpdate; // Send as update, it has a delta
666    } else if (theEvent.momentumPhase == NSEventPhaseChanged) {
667        phase = Qt::ScrollMomentum;
668    } else if (theEvent.phase == NSEventPhaseCancelled
669            || theEvent.momentumPhase == NSEventPhaseEnded
670            || theEvent.momentumPhase == NSEventPhaseCancelled) {
671        phase = Qt::ScrollEnd;
672        m_scrolling = false;
673    } else {
674        Q_ASSERT(theEvent.momentumPhase != NSEventPhaseStationary);
675    }
676
677    // Prevent keyboard modifier state from changing during scroll event streams.
678    // A two-finger trackpad flick generates a stream of scroll events. We want
679    // the keyboard modifier state to be the state at the beginning of the
680    // flick in order to avoid changing the interpretation of the events
681    // mid-stream. One example of this happening would be when pressing cmd
682    // after scrolling in Qt Creator: not taking the phase into account causes
683    // the end of the event stream to be interpreted as font size changes.
684    if (theEvent.momentumPhase == NSEventPhaseNone)
685        m_currentWheelModifiers = [QNSView convertKeyModifiers:[theEvent modifierFlags]];
686
687    // "isInverted": natural OS X scrolling, inverted from the Qt/other platform/Jens perspective.
688    bool isInverted  = [theEvent isDirectionInvertedFromDevice];
689
690    qCInfo(lcQpaMouse).nospace() << phase << " at " << qt_windowPoint
691        << " pixelDelta=" << pixelDelta << " angleDelta=" << angleDelta
692        << (isInverted ? " inverted=true" : "");
693
694    QWindowSystemInterface::handleWheelEvent(m_platformWindow->window(), qt_timestamp, qt_windowPoint,
695            qt_screenPoint, pixelDelta, angleDelta, m_currentWheelModifiers, phase, source, isInverted);
696}
697#endif // QT_CONFIG(wheelevent)
698
699@end
700