Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qnsview_mouse.mm
Go to the documentation of this file.
1// Copyright (C) 2018 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
5// This file is included from qnsview.mm, and only used to organize the code
6
7using namespace Qt::StringLiterals;
8
9static const QPointingDevice *pointingDeviceFor(qint64 deviceID)
10{
11 // macOS will in many cases not report a deviceID (0 value).
12 // We can't pass this on directly, as the QInputDevicePrivate
13 // constructor will treat this as a request to assign a new Id.
14 // Instead we use the default Id of the primary pointing device.
15 static const int kDefaultPrimaryPointingDeviceId = 1;
16 if (!deviceID)
17 deviceID = kDefaultPrimaryPointingDeviceId;
18
19 if (const auto *device = QPointingDevicePrivate::pointingDeviceById(deviceID))
20 return device; // All good, already have the device registered
21
22 const auto *primaryDevice = QPointingDevice::primaryPointingDevice();
23 if (primaryDevice->systemId() == kDefaultPrimaryPointingDeviceId) {
24 // Adopt existing primary device instead of creating a new one
25 QPointingDevicePrivate::get(const_cast<QPointingDevice *>(primaryDevice))->systemId = deviceID;
26 qCDebug(lcQpaInputDevices) << "primaryPointingDevice is now" << primaryDevice;
27 return primaryDevice;
28 } else {
29 // Register a new device. Name and capabilities may need updating later.
30 const auto *device = new QPointingDevice("mouse"_L1, deviceID,
31 QInputDevice::DeviceType::Mouse, QPointingDevice::PointerType::Generic,
32 QInputDevice::Capability::Scroll | QInputDevice::Capability::Position,
33 1, 3, QString(), QPointingDeviceUniqueId(), QCocoaIntegration::instance());
34 QWindowSystemInterface::registerInputDevice(device);
35 return device;
36 }
37}
38
39/*
40 The reason for using this helper is to ensure that QNSView doesn't implement
41 the NSResponder callbacks for mouseEntered, mouseExited, and mouseMoved.
42
43 If it did, we would get mouse events though the responder chain as well,
44 for example if a subview has a tracking area of its own and calls super
45 in the handler, which results in forwarding the event though the responder
46 chain. The same applies if NSWindow.acceptsMouseMovedEvents is YES.
47
48 By having a helper as the target for our tracking areas, we know for sure
49 that the events we are getting stem from our own tracking areas.
50
51 FIXME: Ideally we wouldn't need this workaround, and would correctly
52 interact with the responder chain by e.g. calling super if Qt does not
53 accept the mouse event
54*/
55@implementation QNSViewMouseMoveHelper {
56 QNSView *view;
57}
58
59- (instancetype)initWithView:(QNSView *)theView
60{
61 if ((self = [super init]))
62 view = theView;
63
64 return self;
65}
66
67- (void)mouseMoved:(NSEvent *)theEvent
68{
69 [view mouseMovedImpl:theEvent];
70}
71
72- (void)mouseEntered:(NSEvent *)theEvent
73{
74 [view mouseEnteredImpl:theEvent];
75}
76
77- (void)mouseExited:(NSEvent *)theEvent
78{
79 [view mouseExitedImpl:theEvent];
80}
81
82- (void)cursorUpdate:(NSEvent *)theEvent
83{
84 [view cursorUpdate:theEvent];
85}
86
87@end
88
89@implementation QNSView (MouseAPI)
90
91- (void)resetMouseButtons
92{
93 qCDebug(lcQpaMouse) << "Resetting mouse buttons";
94 m_buttons = Qt::NoButton;
95 m_frameStrutButtons = Qt::NoButton;
96}
97
98- (void)handleMouseEvent:(NSEvent *)theEvent
99{
100 if (!m_platformWindow)
101 return;
102
103#ifndef QT_NO_TABLETEVENT
104 // Tablet events may come in via the mouse event handlers,
105 // check if this is a valid tablet event first.
106 if ([self handleTabletEvent: theEvent])
107 return;
108#endif
109
110 QPointF qtWindowPoint;
111 QPointF qtScreenPoint;
112 QNSView *targetView = self;
113 if (!targetView.platformWindow)
114 return;
115
116
117 [targetView convertFromScreen:[self screenMousePoint:theEvent] toWindowPoint:&qtWindowPoint andScreenPoint:&qtScreenPoint];
118 ulong timestamp = [theEvent timestamp] * 1000;
119
120 QCocoaDrag* nativeDrag = QCocoaIntegration::instance()->drag();
121 nativeDrag->setLastInputEvent(theEvent, self);
122
123 const auto modifiers = QAppleKeyMapper::fromCocoaModifiers(theEvent.modifierFlags);
124 auto button = cocoaButton2QtButton(theEvent);
125 if (button == Qt::LeftButton && m_sendUpAsRightButton)
126 button = Qt::RightButton;
127 const auto eventType = cocoaEvent2QtMouseEvent(theEvent);
128
129 const QPointingDevice *device = pointingDeviceFor(theEvent.deviceID);
130 Q_ASSERT(device);
131
132 if (eventType == QEvent::MouseMove)
133 qCDebug(lcQpaMouse) << eventType << "at" << qtWindowPoint << "with" << m_buttons;
134 else
135 qCInfo(lcQpaMouse) << eventType << "of" << button << "at" << qtWindowPoint << "with" << m_buttons;
136
137 QWindowSystemInterface::handleMouseEvent(targetView->m_platformWindow->window(),
138 timestamp, qtWindowPoint, qtScreenPoint,
139 m_buttons, button, eventType, modifiers);
140}
141
142- (void)handleFrameStrutMouseEvent:(NSEvent *)theEvent
143{
144 if (!m_platformWindow)
145 return;
146
147 switch (theEvent.type) {
148 case NSEventTypeLeftMouseDown:
149 m_frameStrutButtons |= Qt::LeftButton;
150 break;
151 case NSEventTypeLeftMouseUp:
152 m_frameStrutButtons &= ~Qt::LeftButton;
153 break;
154 case NSEventTypeRightMouseDown:
155 m_frameStrutButtons |= Qt::RightButton;
156 break;
157 case NSEventTypeRightMouseUp:
158 m_frameStrutButtons &= ~Qt::RightButton;
159 break;
160 case NSEventTypeOtherMouseDown:
161 m_frameStrutButtons |= cocoaButton2QtButton(theEvent.buttonNumber);
162 break;
163 case NSEventTypeOtherMouseUp:
164 m_frameStrutButtons &= ~cocoaButton2QtButton(theEvent.buttonNumber);
165 default:
166 break;
167 }
168
169 // m_buttons can sometimes get out of sync with the button state in AppKit
170 // E.g if the QNSView where a drag starts is reparented to another window
171 // while the drag is ongoing, it will not get the corresponding mouseUp
172 // call. This will result in m_buttons to be stuck on Qt::LeftButton.
173 // Since we know which buttons was pressed/released directly on the frame
174 // strut, we can rectify m_buttons here so that we at least don't return early
175 // from the drag test underneath because of the faulty m_buttons state.
176 // FIXME: get m_buttons in sync with AppKit/NSEvent all over in QNSView.
177 m_buttons &= ~m_frameStrutButtons;
178
179 if (m_buttons != Qt::NoButton) {
180 // Don't send frame strut events if we are in the middle of
181 // a mouse drag that didn't start on the frame strut.
182 return;
183 }
184
185 NSWindow *window = [self window];
186 NSPoint windowPoint = [theEvent locationInWindow];
187
188 int windowScreenY = [window frame].origin.y + [window frame].size.height;
189 NSPoint windowCoord = [self convertPoint:[self frame].origin toView:nil];
190 int viewScreenY = [window convertRectToScreen:NSMakeRect(windowCoord.x, windowCoord.y, 0, 0)].origin.y;
191 int titleBarHeight = windowScreenY - viewScreenY;
192
193 NSPoint nsViewPoint = [self convertPoint: windowPoint fromView: nil];
194 QPoint qtWindowPoint = QPoint(nsViewPoint.x, titleBarHeight + nsViewPoint.y);
195 NSPoint screenPoint = [window convertRectToScreen:NSMakeRect(windowPoint.x, windowPoint.y, 0, 0)].origin;
196 QPoint qtScreenPoint = QCocoaScreen::mapFromNative(screenPoint).toPoint();
197
198 ulong timestamp = [theEvent timestamp] * 1000;
199
200 const auto button = cocoaButton2QtButton(theEvent);
201 auto eventType = [&]() {
202 switch (theEvent.type) {
203 case NSEventTypeLeftMouseDown:
204 case NSEventTypeRightMouseDown:
205 case NSEventTypeOtherMouseDown:
206 return QEvent::NonClientAreaMouseButtonPress;
207
208 case NSEventTypeLeftMouseUp:
209 case NSEventTypeRightMouseUp:
210 case NSEventTypeOtherMouseUp:
211 return QEvent::NonClientAreaMouseButtonRelease;
212
213 case NSEventTypeMouseMoved:
214 case NSEventTypeLeftMouseDragged:
215 case NSEventTypeRightMouseDragged:
216 case NSEventTypeOtherMouseDragged:
217 return QEvent::NonClientAreaMouseMove;
218
219 default:
220 Q_UNREACHABLE();
221 }
222 }();
223
224 qCInfo(lcQpaMouse) << eventType << "at" << qtWindowPoint << "with" << m_frameStrutButtons << "in" << self.window;
225 QWindowSystemInterface::handleMouseEvent(m_platformWindow->window(),
226 timestamp, qtWindowPoint, qtScreenPoint, m_frameStrutButtons, button, eventType);
227}
228@end
229
230@implementation QNSView (Mouse)
231
232- (void)initMouse
233{
234 m_buttons = Qt::NoButton;
235 m_acceptedMouseDowns = Qt::NoButton;
236 m_frameStrutButtons = Qt::NoButton;
237
238 m_scrolling = false;
239 self.cursor = nil;
240
241 m_sendUpAsRightButton = false;
242 m_dontOverrideCtrlLMB = qt_mac_resolveOption(false, m_platformWindow->window(),
243 "_q_platform_MacDontOverrideCtrlLMB", "QT_MAC_DONT_OVERRIDE_CTRL_LMB");
244
245 m_mouseMoveHelper = [[QNSViewMouseMoveHelper alloc] initWithView:self];
246
247 NSUInteger trackingOptions = NSTrackingCursorUpdate | NSTrackingMouseEnteredAndExited;
248
249 // Ideally we should have used NSTrackingActiveInActiveApp, but that
250 // fails when the application is deactivated from using e.g cmd+tab, and later
251 // reactivated again from a mouse click. So as a work-around we use NSTrackingActiveAlways
252 // instead, and simply ignore any related callbacks while the application is inactive.
253 trackingOptions |= NSTrackingActiveAlways;
254
255 // Ideally, NSTrackingMouseMoved should be turned on only if QWidget::mouseTracking
256 // is enabled, hover is on, or a tool tip is set. Unfortunately, Qt will send "tooltip"
257 // events on mouse moves, so we need to turn it on in ALL case. That means EVERY QWindow
258 // gets to pay the cost of mouse moves delivered to it (Apple recommends keeping it OFF
259 // because there is a performance hit).
260 trackingOptions |= NSTrackingMouseMoved;
261
262 // Using NSTrackingInVisibleRect means AppKit will automatically synchronize the
263 // tracking rect with changes in the view's visible area, so leave it undefined.
264 trackingOptions |= NSTrackingInVisibleRect;
265 static const NSRect trackingRect = NSZeroRect;
266
267 QMacAutoReleasePool pool;
268 [self addTrackingArea:[[[NSTrackingArea alloc] initWithRect:trackingRect
269 options:trackingOptions owner:m_mouseMoveHelper userInfo:nil] autorelease]];
270}
271
272- (BOOL)acceptsFirstMouse:(NSEvent *)theEvent
273{
274 Q_UNUSED(theEvent);
275 if (!m_platformWindow)
276 return NO;
277 if ([self isTransparentForUserInput])
278 return NO;
279 QPointF windowPoint;
280 QPointF screenPoint;
281 [self convertFromScreen:[NSEvent mouseLocation] toWindowPoint: &windowPoint andScreenPoint: &screenPoint];
282 return YES;
283}
284
285- (NSPoint)screenMousePoint:(NSEvent *)theEvent
286{
287 NSPoint screenPoint;
288 if (theEvent) {
289 NSPoint windowPoint = [theEvent locationInWindow];
290 if (qIsNaN(windowPoint.x) || qIsNaN(windowPoint.y)) {
291 screenPoint = [NSEvent mouseLocation];
292 } else {
293 screenPoint = [theEvent.window convertPointToScreen:windowPoint];
294 }
295 } else {
296 screenPoint = [NSEvent mouseLocation];
297 }
298 return screenPoint;
299}
300
301- (bool)handleMouseDownEvent:(NSEvent *)theEvent
302{
303 if ([self isTransparentForUserInput])
304 return false;
305
306 const auto button = cocoaButton2QtButton(theEvent);
307
308 QPointF qtWindowPoint;
309 QPointF qtScreenPoint;
310 [self convertFromScreen:[self screenMousePoint:theEvent] toWindowPoint:&qtWindowPoint andScreenPoint:&qtScreenPoint];
311 Q_UNUSED(qtScreenPoint);
312
313 // Maintain masked state for the button for use by MouseDragged and MouseUp.
314 QRegion mask = QHighDpi::toNativeLocalPosition(m_platformWindow->window()->mask(), m_platformWindow->window());
315 const bool masked = !mask.isEmpty() && !mask.contains(qtWindowPoint.toPoint());
316 if (masked)
317 m_acceptedMouseDowns &= ~button;
318 else
319 m_acceptedMouseDowns |= button;
320
321 // Forward masked out events to the next responder
322 if (masked)
323 return false;
324
325 m_buttons |= button;
326
327 [self handleMouseEvent:theEvent];
328 return true;
329}
330
331- (bool)handleMouseDraggedEvent:(NSEvent *)theEvent
332{
333 if ([self isTransparentForUserInput])
334 return false;
335
336 const auto button = cocoaButton2QtButton(theEvent);
337
338 // Forward the event to the next responder if Qt did not accept the
339 // corresponding mouse down for this button
340 if (!(m_acceptedMouseDowns & button) == button)
341 return false;
342
343 [self handleMouseEvent:theEvent];
344 return true;
345}
346
347- (bool)handleMouseUpEvent:(NSEvent *)theEvent
348{
349 if ([self isTransparentForUserInput])
350 return false;
351
352 auto button = cocoaButton2QtButton(theEvent);
353
354 // Forward the event to the next responder if Qt did not accept the
355 // corresponding mouse down for this button
356 if (!(m_acceptedMouseDowns & button) == button)
357 return false;
358
359 if (m_sendUpAsRightButton && button == Qt::LeftButton)
360 button = Qt::RightButton;
361
362 m_buttons &= ~button;
363
364 [self handleMouseEvent:theEvent];
365
366 if (button == Qt::RightButton)
367 m_sendUpAsRightButton = false;
368
369 return true;
370}
371
372- (void)mouseDown:(NSEvent *)theEvent
373{
374 if ([self isTransparentForUserInput])
375 return [super mouseDown:theEvent];
376 m_sendUpAsRightButton = false;
377
378 QPointF qtWindowPoint;
379 QPointF qtScreenPoint;
380 [self convertFromScreen:[self screenMousePoint:theEvent] toWindowPoint:&qtWindowPoint andScreenPoint:&qtScreenPoint];
381 Q_UNUSED(qtScreenPoint);
382
383 QRegion mask = QHighDpi::toNativeLocalPosition(m_platformWindow->window()->mask(), m_platformWindow->window());
384 const bool masked = !mask.isEmpty() && !mask.contains(qtWindowPoint.toPoint());
385 // Maintain masked state for the button for use by MouseDragged and Up.
386 if (masked)
387 m_acceptedMouseDowns &= ~Qt::LeftButton;
388 else
389 m_acceptedMouseDowns |= Qt::LeftButton;
390
391 // Forward masked out events to the next responder
392 if (masked) {
393 [super mouseDown:theEvent];
394 return;
395 }
396
397 // FIXME: AppKit transfers first responder to the view before calling mouseDown,
398 // whereas we only transfer focus once the mouse press is delivered, which means
399 // on first click the focus item won't be the correct one when transferring focus.
400 auto *focusObject = m_platformWindow->window()->focusObject();
401 if (queryInputMethod(focusObject)) {
402 // Input method is enabled. Pass on to the input context if we
403 // are hitting the input item.
404 if (QPlatformInputContext::inputItemClipRectangle().contains(qtWindowPoint)) {
405 qCDebug(lcQpaInputMethods) << "Asking input context to handle mouse press"
406 << "for focus object" << focusObject;
407 if ([NSTextInputContext.currentInputContext handleEvent:theEvent]) {
408 // NSTextView bails out if the input context handled the event,
409 // which is e.g. the case for 2-Set Korean input. We follow suit,
410 // even if that means having to click twice to move the cursor
411 // for these input methods when they are composing.
412 qCDebug(lcQpaInputMethods) << "Input context handled event; bailing out.";
413 return;
414 }
415 }
416 }
417
418 if (!m_dontOverrideCtrlLMB && (theEvent.modifierFlags & NSEventModifierFlagControl)) {
419 m_buttons |= Qt::RightButton;
420 m_sendUpAsRightButton = true;
421 } else {
422 m_buttons |= Qt::LeftButton;
423 }
424
425 [self handleMouseEvent:theEvent];
426}
427
428- (void)mouseDragged:(NSEvent *)theEvent
429{
430 const bool accepted = [self handleMouseDraggedEvent:theEvent];
431 if (!accepted)
432 [super mouseDragged:theEvent];
433}
434
435- (void)mouseUp:(NSEvent *)theEvent
436{
437 const bool accepted = [self handleMouseUpEvent:theEvent];
438 if (!accepted)
439 [super mouseUp:theEvent];
440}
441
442- (void)rightMouseDown:(NSEvent *)theEvent
443{
444 const bool accepted = [self handleMouseDownEvent:theEvent];
445 if (!accepted)
446 [super rightMouseDown:theEvent];
447}
448
449- (void)rightMouseDragged:(NSEvent *)theEvent
450{
451 const bool accepted = [self handleMouseDraggedEvent:theEvent];
452 if (!accepted)
453 [super rightMouseDragged:theEvent];
454}
455
456- (void)rightMouseUp:(NSEvent *)theEvent
457{
458 const bool accepted = [self handleMouseUpEvent:theEvent];
459 if (!accepted)
460 [super rightMouseUp:theEvent];
461}
462
463- (void)otherMouseDown:(NSEvent *)theEvent
464{
465 const bool accepted = [self handleMouseDownEvent:theEvent];
466 if (!accepted)
467 [super otherMouseDown:theEvent];
468}
469
470- (void)otherMouseDragged:(NSEvent *)theEvent
471{
472 const bool accepted = [self handleMouseDraggedEvent:theEvent];
473 if (!accepted)
474 [super otherMouseDragged:theEvent];
475}
476
477- (void)otherMouseUp:(NSEvent *)theEvent
478{
479 const bool accepted = [self handleMouseUpEvent:theEvent];
480 if (!accepted)
481 [super otherMouseUp:theEvent];
482}
483
484- (void)cursorUpdate:(NSEvent *)theEvent
485{
486 if (!NSApp.active)
487 return;
488
489 auto previousCursor = NSCursor.currentCursor;
490
491 if (self.cursor)
492 [self.cursor set];
493 else
494 [super cursorUpdate:theEvent];
495
496 if (NSCursor.currentCursor != previousCursor)
497 qCInfo(lcQpaMouse) << "Cursor update for" << self << "resulted in new cursor" << NSCursor.currentCursor;
498}
499
500- (void)mouseMovedImpl:(NSEvent *)theEvent
501{
502 if (!m_platformWindow)
503 return;
504
505 // Top-level windows generate enter-leave events for sub-windows.
506 // Qt wants to know which window (if any) will be entered at the
507 // the time of the leave. This is dificult to accomplish by
508 // handling mouseEnter and mouseLeave envents, since they are sent
509 // individually to different views.
510 QPointF windowPoint;
511 QPointF screenPoint;
512 QCocoaWindow *windowToLeave = nullptr;
513
514 if (m_platformWindow->isContentView()) {
515 [self convertFromScreen:[self screenMousePoint:theEvent] toWindowPoint:&windowPoint andScreenPoint:&screenPoint];
516 QWindow *childUnderMouse = m_platformWindow->childWindowAt(windowPoint.toPoint());
517 QCocoaWindow *childWindow = static_cast<QCocoaWindow *>(childUnderMouse->handle());
518 if (childWindow != QCocoaWindow::s_windowUnderMouse) {
519 if (QCocoaWindow::s_windowUnderMouse)
520 windowToLeave = QCocoaWindow::s_windowUnderMouse;
521 QCocoaWindow::s_windowUnderMouse = childWindow;
522 }
523 }
524
525 if (!NSApp.active)
526 return;
527
528 if ([self isTransparentForUserInput])
529 return;
530
531 if (windowToLeave) {
532 qCInfo(lcQpaMouse) << "Detected new window under mouse at" << windowPoint << "; sending"
533 << QEvent::Enter << QCocoaWindow::s_windowUnderMouse->window()
534 << QEvent::Leave << windowToLeave->window();
535 QWindowSystemInterface::handleEnterLeaveEvent(QCocoaWindow::s_windowUnderMouse->window(), windowToLeave->window(), windowPoint, screenPoint);
536 }
537
538 // Cocoa keeps firing mouse move events for obscured parent views. Qt should not
539 // send those events so filter them out here.
540 if (m_platformWindow != QCocoaWindow::s_windowUnderMouse)
541 return;
542
543 [self handleMouseEvent: theEvent];
544}
545
546- (BOOL)shouldPropagateMouseEnterExit
547{
548 Q_ASSERT(m_platformWindow);
549
550 // We send out enter and leave events mainly from mouse move events (mouseMovedImpl),
551 // but in some case (see mouseEnteredImpl:) we also want to propagate enter/leave
552 // events from the platform. We only do this for windows that themselves are not
553 // handled by another parent QWindow.
554
555 if (m_platformWindow->isContentView())
556 return true;
557
558 // Windows manually embedded into a native view does not have a QWindow parent
559 if (m_platformWindow->isEmbedded())
560 return true;
561
562 // Windows embedded via fromWinId do, but the parent isn't a QNSView
563 QPlatformWindow *parentWindow = m_platformWindow->QPlatformWindow::parent();
564 if (parentWindow && parentWindow->isForeignWindow())
565 return true;
566
567 return false;
568}
569
570- (void)mouseEnteredImpl:(NSEvent *)theEvent
571{
572 Q_UNUSED(theEvent);
573 if (!m_platformWindow)
574 return;
575
576 // We send out enter and leave events mainly from mouse move events (mouseMovedImpl).
577 // Therefore, in most cases, we should not send out enter/leave events from here, as
578 // this results in duplicated enter/leave events being delivered.
579 // This is especially important when working with NSTrackingArea, since AppKit documents that
580 // the order of enter/exit events when several NSTrackingAreas are in use is not guaranteed.
581 // So if we just forwarded enter/leave events from NSTrackingArea directly, it would not only
582 // result in duplicated events, but also sometimes events that would be out of sync.
583 // But not all enter events can be resolved from mouse move events. E.g if a window is raised
584 // in front of the mouse, or if the application is activated while the mouse is on top of a
585 // window, we need to send out enter events for those cases as well. And we do so from this
586 // function to support the former case. But only when we receive an enter event for the
587 // top-level window, when no child QWindows are being hovered from before.
588 // Since QWSI expects us to send both the window entered, and the window left, in the same
589 // callback, we manually keep track of which child QWindow is under the mouse at any point
590 // in time (s_windowUnderMouse). The latter is also used to also send out enter/leave
591 // events when the application is activated/deactivated.
592
593 if (![self shouldPropagateMouseEnterExit])
594 return;
595
596 QPointF windowPoint;
597 QPointF screenPoint;
598 [self convertFromScreen:[self screenMousePoint:theEvent] toWindowPoint:&windowPoint andScreenPoint:&screenPoint];
599 QWindow *childUnderMouse = m_platformWindow->childWindowAt(windowPoint.toPoint());
600 QCocoaWindow::s_windowUnderMouse = static_cast<QCocoaWindow *>(childUnderMouse->handle());
601
602 if ([self isTransparentForUserInput])
603 return;
604
605 if (!NSApp.active)
606 return;
607
608 qCInfo(lcQpaMouse) << "Mouse entered" << self << "at" << windowPoint << "with" << currentlyPressedMouseButtons()
609 << "; sending" << QEvent::Enter << "to" << QCocoaWindow::s_windowUnderMouse->window();
610 QWindowSystemInterface::handleEnterEvent(QCocoaWindow::s_windowUnderMouse->window(), windowPoint, screenPoint);
611}
612
613- (void)mouseExitedImpl:(NSEvent *)theEvent
614{
615 Q_UNUSED(theEvent);
616 if (!m_platformWindow)
617 return;
618
619 if (![self shouldPropagateMouseEnterExit])
620 return;
621
622 QCocoaWindow *windowToLeave = QCocoaWindow::s_windowUnderMouse;
623 QCocoaWindow::s_windowUnderMouse = nullptr;
624
625 if ([self isTransparentForUserInput])
626 return;
627
628 if (!NSApp.active)
629 return;
630
631 if (!windowToLeave)
632 return;
633
634 qCInfo(lcQpaMouse) << "Mouse left" << self << "; sending" << QEvent::Leave << "to" << windowToLeave->window();
635 QWindowSystemInterface::handleLeaveEvent(windowToLeave->window());
636}
637
638#if QT_CONFIG(wheelevent)
639- (void)scrollWheel:(NSEvent *)theEvent
640{
641 if (!m_platformWindow)
642 return;
643
644 if ([self isTransparentForUserInput])
645 return [super scrollWheel:theEvent];
646
647 QPoint angleDelta;
648 Qt::MouseEventSource source = Qt::MouseEventNotSynthesized;
649 if ([theEvent hasPreciseScrollingDeltas]) {
650 // The mouse device contains pixel scroll wheel support (Mighty Mouse, Trackpad).
651 // Since deviceDelta is delivered as pixels rather than degrees, we need to
652 // convert from pixels to degrees in a sensible manner.
653 // It looks like 1/4 degrees per pixel behaves most native.
654 // (NB: Qt expects the unit for delta to be 8 per degree):
655 const int pixelsToDegrees = 2; // 8 * 1/4
656 angleDelta.setX([theEvent scrollingDeltaX] * pixelsToDegrees);
657 angleDelta.setY([theEvent scrollingDeltaY] * pixelsToDegrees);
658 source = Qt::MouseEventSynthesizedBySystem;
659 } else {
660 // Remove acceleration, and use either -120 or 120 as delta:
661 angleDelta.setX(qBound(-120, int([theEvent deltaX] * 10000), 120));
662 angleDelta.setY(qBound(-120, int([theEvent deltaY] * 10000), 120));
663 }
664
665 QPoint pixelDelta;
666 if ([theEvent hasPreciseScrollingDeltas]) {
667 pixelDelta.setX([theEvent scrollingDeltaX]);
668 pixelDelta.setY([theEvent scrollingDeltaY]);
669 } else {
670 // docs: "In the case of !hasPreciseScrollingDeltas, multiply the delta with the line width."
671 // scrollingDeltaX seems to return a minimum value of 0.1 in this case, map that to two pixels.
672 const CGFloat lineWithEstimate = 20.0;
673 pixelDelta.setX([theEvent scrollingDeltaX] * lineWithEstimate);
674 pixelDelta.setY([theEvent scrollingDeltaY] * lineWithEstimate);
675 }
676
677 QPointF qt_windowPoint;
678 QPointF qt_screenPoint;
679 [self convertFromScreen:[self screenMousePoint:theEvent] toWindowPoint:&qt_windowPoint andScreenPoint:&qt_screenPoint];
680 NSTimeInterval timestamp = [theEvent timestamp];
681 ulong qt_timestamp = timestamp * 1000;
682
683 Qt::ScrollPhase phase = Qt::NoScrollPhase;
684 if (theEvent.phase == NSEventPhaseMayBegin || theEvent.phase == NSEventPhaseBegan) {
685 // MayBegin is likely to happen. We treat it the same as an actual begin,
686 // and follow it with an update when the actual begin is delivered.
687 phase = m_scrolling ? Qt::ScrollUpdate : Qt::ScrollBegin;
688 m_scrolling = true;
689 } else if (theEvent.phase == NSEventPhaseStationary || theEvent.phase == NSEventPhaseChanged) {
690 phase = Qt::ScrollUpdate;
691 } else if (theEvent.phase == NSEventPhaseEnded) {
692 // A scroll event phase may be followed by a momentum phase after the user releases
693 // the finger, and in that case we don't want to send a Qt::ScrollEnd until after
694 // the momentum phase has ended. Unfortunately there isn't any guaranteed way of
695 // knowing whether or not a NSEventPhaseEnded will be followed by a momentum phase.
696 // The best we can do is to look at the event queue and hope that the system has
697 // had time to emit a momentum phase event.
698 if ([NSApp nextEventMatchingMask:NSEventMaskScrollWheel untilDate:[NSDate distantPast]
699 inMode:@"QtMomementumEventSearchMode" dequeue:NO].momentumPhase == NSEventPhaseBegan) {
700 return; // Ignore, even if it has delta
701 } else {
702 phase = Qt::ScrollEnd;
703 m_scrolling = false;
704 }
705 } else if (theEvent.momentumPhase == NSEventPhaseBegan) {
706 // If we missed finding a momentum NSEventPhaseBegan when the non-momentum
707 // phase ended we need to treat this as a scroll begin, to not confuse client
708 // code. Otherwise we treat it as a continuation of the existing scroll.
709 phase = m_scrolling ? Qt::ScrollUpdate : Qt::ScrollBegin;
710 m_scrolling = true;
711 } else if (theEvent.momentumPhase == NSEventPhaseChanged) {
712 phase = Qt::ScrollMomentum;
713 } else if (theEvent.phase == NSEventPhaseCancelled
714 || theEvent.momentumPhase == NSEventPhaseEnded
715 || theEvent.momentumPhase == NSEventPhaseCancelled) {
716 phase = Qt::ScrollEnd;
717 m_scrolling = false;
718 } else {
719 Q_ASSERT(theEvent.momentumPhase != NSEventPhaseStationary);
720 }
721
722 // Sanitize deltas for events that should not result in scrolling.
723 // On macOS 12.1 this phase has been observed to report deltas.
724 if (theEvent.phase == NSEventPhaseCancelled) {
725 if (!pixelDelta.isNull() || !angleDelta.isNull()) {
726 qCInfo(lcQpaMouse) << "Ignoring unexpected delta for" << theEvent;
727 pixelDelta = QPoint();
728 angleDelta = QPoint();
729 }
730 }
731
732 // Prevent keyboard modifier state from changing during scroll event streams.
733 // A two-finger trackpad flick generates a stream of scroll events. We want
734 // the keyboard modifier state to be the state at the beginning of the
735 // flick in order to avoid changing the interpretation of the events
736 // mid-stream. One example of this happening would be when pressing cmd
737 // after scrolling in Qt Creator: not taking the phase into account causes
738 // the end of the event stream to be interpreted as font size changes.
739 if (theEvent.momentumPhase == NSEventPhaseNone)
740 m_currentWheelModifiers = QAppleKeyMapper::fromCocoaModifiers(theEvent.modifierFlags);
741
742 // "isInverted": natural OS X scrolling, inverted from the Qt/other platform/Jens perspective.
743 bool isInverted = [theEvent isDirectionInvertedFromDevice];
744
745 qCInfo(lcQpaMouse).nospace() << phase << " at " << qt_windowPoint
746 << " pixelDelta=" << pixelDelta << " angleDelta=" << angleDelta
747 << (isInverted ? " inverted=true" : "");
748
749 const QPointingDevice *device = pointingDeviceFor(theEvent.deviceID);
750 Q_ASSERT(device);
751
752 if (theEvent.hasPreciseScrollingDeltas) {
753 auto *devicePriv = QPointingDevicePrivate::get(const_cast<QPointingDevice *>(device));
754 if (!devicePriv->capabilities.testFlag(QInputDevice::Capability::PixelScroll)) {
755 devicePriv->name = "trackpad or magic mouse"_L1;
756 devicePriv->deviceType = QInputDevice::DeviceType::TouchPad;
757 devicePriv->setCapabilities(devicePriv->capabilities | QInputDevice::Capability::PixelScroll);
758 qCDebug(lcQpaInputDevices) << "mouse scrolling: updated capabilities" << device;
759 }
760 }
761
762 QWindowSystemInterface::handleWheelEvent(m_platformWindow->window(), qt_timestamp,
763 device, qt_windowPoint, qt_screenPoint, pixelDelta, angleDelta,
764 m_currentWheelModifiers, phase, source, isInverted);
765}
766#endif // QT_CONFIG(wheelevent)
767
768@end
static const QPointingDevice * pointingDeviceFor(qint64 deviceID)