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 NSRect screenRect = [[theEvent window] convertRectToScreen:NSMakeRect(windowPoint.x, windowPoint.y, 1, 1)];
294 screenPoint = screenRect.origin;
295 }
296 } else {
297 screenPoint = [NSEvent mouseLocation];
298 }
299 return screenPoint;
300}
301
302- (bool)handleMouseDownEvent:(NSEvent *)theEvent
303{
304 if ([self isTransparentForUserInput])
305 return false;
306
307 const auto button = cocoaButton2QtButton(theEvent);
308
309 QPointF qtWindowPoint;
310 QPointF qtScreenPoint;
311 [self convertFromScreen:[self screenMousePoint:theEvent] toWindowPoint:&qtWindowPoint andScreenPoint:&qtScreenPoint];
312 Q_UNUSED(qtScreenPoint);
313
314 // Maintain masked state for the button for use by MouseDragged and MouseUp.
315 QRegion mask = QHighDpi::toNativeLocalPosition(m_platformWindow->window()->mask(), m_platformWindow->window());
316 const bool masked = !mask.isEmpty() && !mask.contains(qtWindowPoint.toPoint());
317 if (masked)
318 m_acceptedMouseDowns &= ~button;
319 else
320 m_acceptedMouseDowns |= button;
321
322 // Forward masked out events to the next responder
323 if (masked)
324 return false;
325
326 m_buttons |= button;
327
328 [self handleMouseEvent:theEvent];
329 return true;
330}
331
332- (bool)handleMouseDraggedEvent:(NSEvent *)theEvent
333{
334 if ([self isTransparentForUserInput])
335 return false;
336
337 const auto button = cocoaButton2QtButton(theEvent);
338
339 // Forward the event to the next responder if Qt did not accept the
340 // corresponding mouse down for this button
341 if (!(m_acceptedMouseDowns & button) == button)
342 return false;
343
344 [self handleMouseEvent:theEvent];
345 return true;
346}
347
348- (bool)handleMouseUpEvent:(NSEvent *)theEvent
349{
350 if ([self isTransparentForUserInput])
351 return false;
352
353 auto button = cocoaButton2QtButton(theEvent);
354
355 // Forward the event to the next responder if Qt did not accept the
356 // corresponding mouse down for this button
357 if (!(m_acceptedMouseDowns & button) == button)
358 return false;
359
360 if (m_sendUpAsRightButton && button == Qt::LeftButton)
361 button = Qt::RightButton;
362
363 m_buttons &= ~button;
364
365 [self handleMouseEvent:theEvent];
366
367 if (button == Qt::RightButton)
368 m_sendUpAsRightButton = false;
369
370 return true;
371}
372
373- (void)mouseDown:(NSEvent *)theEvent
374{
375 if ([self isTransparentForUserInput])
376 return [super mouseDown:theEvent];
377 m_sendUpAsRightButton = false;
378
379 QPointF qtWindowPoint;
380 QPointF qtScreenPoint;
381 [self convertFromScreen:[self screenMousePoint:theEvent] toWindowPoint:&qtWindowPoint andScreenPoint:&qtScreenPoint];
382 Q_UNUSED(qtScreenPoint);
383
384 QRegion mask = QHighDpi::toNativeLocalPosition(m_platformWindow->window()->mask(), m_platformWindow->window());
385 const bool masked = !mask.isEmpty() && !mask.contains(qtWindowPoint.toPoint());
386 // Maintain masked state for the button for use by MouseDragged and Up.
387 if (masked)
388 m_acceptedMouseDowns &= ~Qt::LeftButton;
389 else
390 m_acceptedMouseDowns |= Qt::LeftButton;
391
392 // Forward masked out events to the next responder
393 if (masked) {
394 [super mouseDown:theEvent];
395 return;
396 }
397
398 // FIXME: AppKit transfers first responder to the view before calling mouseDown,
399 // whereas we only transfer focus once the mouse press is delivered, which means
400 // on first click the focus item won't be the correct one when transferring focus.
401 auto *focusObject = m_platformWindow->window()->focusObject();
402 if (queryInputMethod(focusObject)) {
403 // Input method is enabled. Pass on to the input context if we
404 // are hitting the input item.
405 if (QPlatformInputContext::inputItemClipRectangle().contains(qtWindowPoint)) {
406 qCDebug(lcQpaInputMethods) << "Asking input context to handle mouse press"
407 << "for focus object" << focusObject;
408 if ([NSTextInputContext.currentInputContext handleEvent:theEvent]) {
409 // NSTextView bails out if the input context handled the event,
410 // which is e.g. the case for 2-Set Korean input. We follow suit,
411 // even if that means having to click twice to move the cursor
412 // for these input methods when they are composing.
413 qCDebug(lcQpaInputMethods) << "Input context handled event; bailing out.";
414 return;
415 }
416 }
417 }
418
419 if (!m_dontOverrideCtrlLMB && (theEvent.modifierFlags & NSEventModifierFlagControl)) {
420 m_buttons |= Qt::RightButton;
421 m_sendUpAsRightButton = true;
422 } else {
423 m_buttons |= Qt::LeftButton;
424 }
425
426 [self handleMouseEvent:theEvent];
427}
428
429- (void)mouseDragged:(NSEvent *)theEvent
430{
431 const bool accepted = [self handleMouseDraggedEvent:theEvent];
432 if (!accepted)
433 [super mouseDragged:theEvent];
434}
435
436- (void)mouseUp:(NSEvent *)theEvent
437{
438 const bool accepted = [self handleMouseUpEvent:theEvent];
439 if (!accepted)
440 [super mouseUp:theEvent];
441}
442
443- (void)rightMouseDown:(NSEvent *)theEvent
444{
445 const bool accepted = [self handleMouseDownEvent:theEvent];
446 if (!accepted)
447 [super rightMouseDown:theEvent];
448}
449
450- (void)rightMouseDragged:(NSEvent *)theEvent
451{
452 const bool accepted = [self handleMouseDraggedEvent:theEvent];
453 if (!accepted)
454 [super rightMouseDragged:theEvent];
455}
456
457- (void)rightMouseUp:(NSEvent *)theEvent
458{
459 const bool accepted = [self handleMouseUpEvent:theEvent];
460 if (!accepted)
461 [super rightMouseUp:theEvent];
462}
463
464- (void)otherMouseDown:(NSEvent *)theEvent
465{
466 const bool accepted = [self handleMouseDownEvent:theEvent];
467 if (!accepted)
468 [super otherMouseDown:theEvent];
469}
470
471- (void)otherMouseDragged:(NSEvent *)theEvent
472{
473 const bool accepted = [self handleMouseDraggedEvent:theEvent];
474 if (!accepted)
475 [super otherMouseDragged:theEvent];
476}
477
478- (void)otherMouseUp:(NSEvent *)theEvent
479{
480 const bool accepted = [self handleMouseUpEvent:theEvent];
481 if (!accepted)
482 [super otherMouseUp:theEvent];
483}
484
485- (void)cursorUpdate:(NSEvent *)theEvent
486{
487 if (!NSApp.active)
488 return;
489
490 auto previousCursor = NSCursor.currentCursor;
491
492 if (self.cursor)
493 [self.cursor set];
494 else
495 [super cursorUpdate:theEvent];
496
497 if (NSCursor.currentCursor != previousCursor)
498 qCInfo(lcQpaMouse) << "Cursor update for" << self << "resulted in new cursor" << NSCursor.currentCursor;
499}
500
501- (void)mouseMovedImpl:(NSEvent *)theEvent
502{
503 if (!m_platformWindow)
504 return;
505
506 // Top-level windows generate enter-leave events for sub-windows.
507 // Qt wants to know which window (if any) will be entered at the
508 // the time of the leave. This is dificult to accomplish by
509 // handling mouseEnter and mouseLeave envents, since they are sent
510 // individually to different views.
511 QPointF windowPoint;
512 QPointF screenPoint;
513 QCocoaWindow *windowToLeave = nullptr;
514
515 if (m_platformWindow->isContentView()) {
516 [self convertFromScreen:[self screenMousePoint:theEvent] toWindowPoint:&windowPoint andScreenPoint:&screenPoint];
517 QWindow *childUnderMouse = m_platformWindow->childWindowAt(windowPoint.toPoint());
518 QCocoaWindow *childWindow = static_cast<QCocoaWindow *>(childUnderMouse->handle());
519 if (childWindow != QCocoaWindow::s_windowUnderMouse) {
520 if (QCocoaWindow::s_windowUnderMouse)
521 windowToLeave = QCocoaWindow::s_windowUnderMouse;
522 QCocoaWindow::s_windowUnderMouse = childWindow;
523 }
524 }
525
526 if (!NSApp.active)
527 return;
528
529 if ([self isTransparentForUserInput])
530 return;
531
532 if (windowToLeave) {
533 qCInfo(lcQpaMouse) << "Detected new window under mouse at" << windowPoint << "; sending"
534 << QEvent::Enter << QCocoaWindow::s_windowUnderMouse->window()
535 << QEvent::Leave << windowToLeave->window();
536 QWindowSystemInterface::handleEnterLeaveEvent(QCocoaWindow::s_windowUnderMouse->window(), windowToLeave->window(), windowPoint, screenPoint);
537 }
538
539 // Cocoa keeps firing mouse move events for obscured parent views. Qt should not
540 // send those events so filter them out here.
541 if (m_platformWindow != QCocoaWindow::s_windowUnderMouse)
542 return;
543
544 [self handleMouseEvent: theEvent];
545}
546
547- (BOOL)shouldPropagateMouseEnterExit
548{
549 Q_ASSERT(m_platformWindow);
550
551 // We send out enter and leave events mainly from mouse move events (mouseMovedImpl),
552 // but in some case (see mouseEnteredImpl:) we also want to propagate enter/leave
553 // events from the platform. We only do this for windows that themselves are not
554 // handled by another parent QWindow.
555
556 if (m_platformWindow->isContentView())
557 return true;
558
559 // Windows manually embedded into a native view does not have a QWindow parent
560 if (m_platformWindow->isEmbedded())
561 return true;
562
563 // Windows embedded via fromWinId do, but the parent isn't a QNSView
564 QPlatformWindow *parentWindow = m_platformWindow->QPlatformWindow::parent();
565 if (parentWindow && parentWindow->isForeignWindow())
566 return true;
567
568 return false;
569}
570
571- (void)mouseEnteredImpl:(NSEvent *)theEvent
572{
573 Q_UNUSED(theEvent);
574 if (!m_platformWindow)
575 return;
576
577 // We send out enter and leave events mainly from mouse move events (mouseMovedImpl).
578 // Therefore, in most cases, we should not send out enter/leave events from here, as
579 // this results in duplicated enter/leave events being delivered.
580 // This is especially important when working with NSTrackingArea, since AppKit documents that
581 // the order of enter/exit events when several NSTrackingAreas are in use is not guaranteed.
582 // So if we just forwarded enter/leave events from NSTrackingArea directly, it would not only
583 // result in duplicated events, but also sometimes events that would be out of sync.
584 // But not all enter events can be resolved from mouse move events. E.g if a window is raised
585 // in front of the mouse, or if the application is activated while the mouse is on top of a
586 // window, we need to send out enter events for those cases as well. And we do so from this
587 // function to support the former case. But only when we receive an enter event for the
588 // top-level window, when no child QWindows are being hovered from before.
589 // Since QWSI expects us to send both the window entered, and the window left, in the same
590 // callback, we manually keep track of which child QWindow is under the mouse at any point
591 // in time (s_windowUnderMouse). The latter is also used to also send out enter/leave
592 // events when the application is activated/deactivated.
593
594 if (![self shouldPropagateMouseEnterExit])
595 return;
596
597 QPointF windowPoint;
598 QPointF screenPoint;
599 [self convertFromScreen:[self screenMousePoint:theEvent] toWindowPoint:&windowPoint andScreenPoint:&screenPoint];
600 QWindow *childUnderMouse = m_platformWindow->childWindowAt(windowPoint.toPoint());
601 QCocoaWindow::s_windowUnderMouse = static_cast<QCocoaWindow *>(childUnderMouse->handle());
602
603 if ([self isTransparentForUserInput])
604 return;
605
606 if (!NSApp.active)
607 return;
608
609 qCInfo(lcQpaMouse) << "Mouse entered" << self << "at" << windowPoint << "with" << currentlyPressedMouseButtons()
610 << "; sending" << QEvent::Enter << "to" << QCocoaWindow::s_windowUnderMouse->window();
611 QWindowSystemInterface::handleEnterEvent(QCocoaWindow::s_windowUnderMouse->window(), windowPoint, screenPoint);
612}
613
614- (void)mouseExitedImpl:(NSEvent *)theEvent
615{
616 Q_UNUSED(theEvent);
617 if (!m_platformWindow)
618 return;
619
620 if (![self shouldPropagateMouseEnterExit])
621 return;
622
623 QCocoaWindow *windowToLeave = QCocoaWindow::s_windowUnderMouse;
624 QCocoaWindow::s_windowUnderMouse = nullptr;
625
626 if ([self isTransparentForUserInput])
627 return;
628
629 if (!NSApp.active)
630 return;
631
632 if (!windowToLeave)
633 return;
634
635 qCInfo(lcQpaMouse) << "Mouse left" << self << "; sending" << QEvent::Leave << "to" << windowToLeave->window();
636 QWindowSystemInterface::handleLeaveEvent(windowToLeave->window());
637}
638
639#if QT_CONFIG(wheelevent)
640- (void)scrollWheel:(NSEvent *)theEvent
641{
642 if (!m_platformWindow)
643 return;
644
645 if ([self isTransparentForUserInput])
646 return [super scrollWheel:theEvent];
647
648 QPoint angleDelta;
649 Qt::MouseEventSource source = Qt::MouseEventNotSynthesized;
650 if ([theEvent hasPreciseScrollingDeltas]) {
651 // The mouse device contains pixel scroll wheel support (Mighty Mouse, Trackpad).
652 // Since deviceDelta is delivered as pixels rather than degrees, we need to
653 // convert from pixels to degrees in a sensible manner.
654 // It looks like 1/4 degrees per pixel behaves most native.
655 // (NB: Qt expects the unit for delta to be 8 per degree):
656 const int pixelsToDegrees = 2; // 8 * 1/4
657 angleDelta.setX([theEvent scrollingDeltaX] * pixelsToDegrees);
658 angleDelta.setY([theEvent scrollingDeltaY] * pixelsToDegrees);
659 source = Qt::MouseEventSynthesizedBySystem;
660 } else {
661 // Remove acceleration, and use either -120 or 120 as delta:
662 angleDelta.setX(qBound(-120, int([theEvent deltaX] * 10000), 120));
663 angleDelta.setY(qBound(-120, int([theEvent deltaY] * 10000), 120));
664 }
665
666 QPoint pixelDelta;
667 if ([theEvent hasPreciseScrollingDeltas]) {
668 pixelDelta.setX([theEvent scrollingDeltaX]);
669 pixelDelta.setY([theEvent scrollingDeltaY]);
670 } else {
671 // docs: "In the case of !hasPreciseScrollingDeltas, multiply the delta with the line width."
672 // scrollingDeltaX seems to return a minimum value of 0.1 in this case, map that to two pixels.
673 const CGFloat lineWithEstimate = 20.0;
674 pixelDelta.setX([theEvent scrollingDeltaX] * lineWithEstimate);
675 pixelDelta.setY([theEvent scrollingDeltaY] * lineWithEstimate);
676 }
677
678 QPointF qt_windowPoint;
679 QPointF qt_screenPoint;
680 [self convertFromScreen:[self screenMousePoint:theEvent] toWindowPoint:&qt_windowPoint andScreenPoint:&qt_screenPoint];
681 NSTimeInterval timestamp = [theEvent timestamp];
682 ulong qt_timestamp = timestamp * 1000;
683
684 Qt::ScrollPhase phase = Qt::NoScrollPhase;
685 if (theEvent.phase == NSEventPhaseMayBegin || theEvent.phase == NSEventPhaseBegan) {
686 // MayBegin is likely to happen. We treat it the same as an actual begin,
687 // and follow it with an update when the actual begin is delivered.
688 phase = m_scrolling ? Qt::ScrollUpdate : Qt::ScrollBegin;
689 m_scrolling = true;
690 } else if (theEvent.phase == NSEventPhaseStationary || theEvent.phase == NSEventPhaseChanged) {
691 phase = Qt::ScrollUpdate;
692 } else if (theEvent.phase == NSEventPhaseEnded) {
693 // A scroll event phase may be followed by a momentum phase after the user releases
694 // the finger, and in that case we don't want to send a Qt::ScrollEnd until after
695 // the momentum phase has ended. Unfortunately there isn't any guaranteed way of
696 // knowing whether or not a NSEventPhaseEnded will be followed by a momentum phase.
697 // The best we can do is to look at the event queue and hope that the system has
698 // had time to emit a momentum phase event.
699 if ([NSApp nextEventMatchingMask:NSEventMaskScrollWheel untilDate:[NSDate distantPast]
700 inMode:@"QtMomementumEventSearchMode" dequeue:NO].momentumPhase == NSEventPhaseBegan) {
701 return; // Ignore, even if it has delta
702 } else {
703 phase = Qt::ScrollEnd;
704 m_scrolling = false;
705 }
706 } else if (theEvent.momentumPhase == NSEventPhaseBegan) {
707 // If we missed finding a momentum NSEventPhaseBegan when the non-momentum
708 // phase ended we need to treat this as a scroll begin, to not confuse client
709 // code. Otherwise we treat it as a continuation of the existing scroll.
710 phase = m_scrolling ? Qt::ScrollUpdate : Qt::ScrollBegin;
711 m_scrolling = true;
712 } else if (theEvent.momentumPhase == NSEventPhaseChanged) {
713 phase = Qt::ScrollMomentum;
714 } else if (theEvent.phase == NSEventPhaseCancelled
715 || theEvent.momentumPhase == NSEventPhaseEnded
716 || theEvent.momentumPhase == NSEventPhaseCancelled) {
717 phase = Qt::ScrollEnd;
718 m_scrolling = false;
719 } else {
720 Q_ASSERT(theEvent.momentumPhase != NSEventPhaseStationary);
721 }
722
723 // Sanitize deltas for events that should not result in scrolling.
724 // On macOS 12.1 this phase has been observed to report deltas.
725 if (theEvent.phase == NSEventPhaseCancelled) {
726 if (!pixelDelta.isNull() || !angleDelta.isNull()) {
727 qCInfo(lcQpaMouse) << "Ignoring unexpected delta for" << theEvent;
728 pixelDelta = QPoint();
729 angleDelta = QPoint();
730 }
731 }
732
733 // Prevent keyboard modifier state from changing during scroll event streams.
734 // A two-finger trackpad flick generates a stream of scroll events. We want
735 // the keyboard modifier state to be the state at the beginning of the
736 // flick in order to avoid changing the interpretation of the events
737 // mid-stream. One example of this happening would be when pressing cmd
738 // after scrolling in Qt Creator: not taking the phase into account causes
739 // the end of the event stream to be interpreted as font size changes.
740 if (theEvent.momentumPhase == NSEventPhaseNone)
741 m_currentWheelModifiers = QAppleKeyMapper::fromCocoaModifiers(theEvent.modifierFlags);
742
743 // "isInverted": natural OS X scrolling, inverted from the Qt/other platform/Jens perspective.
744 bool isInverted = [theEvent isDirectionInvertedFromDevice];
745
746 qCInfo(lcQpaMouse).nospace() << phase << " at " << qt_windowPoint
747 << " pixelDelta=" << pixelDelta << " angleDelta=" << angleDelta
748 << (isInverted ? " inverted=true" : "");
749
750 const QPointingDevice *device = pointingDeviceFor(theEvent.deviceID);
751 Q_ASSERT(device);
752
753 if (theEvent.hasPreciseScrollingDeltas) {
754 auto *devicePriv = QPointingDevicePrivate::get(const_cast<QPointingDevice *>(device));
755 if (!devicePriv->capabilities.testFlag(QInputDevice::Capability::PixelScroll)) {
756 devicePriv->name = "trackpad or magic mouse"_L1;
757 devicePriv->deviceType = QInputDevice::DeviceType::TouchPad;
758 devicePriv->setCapabilities(devicePriv->capabilities | QInputDevice::Capability::PixelScroll);
759 qCDebug(lcQpaInputDevices) << "mouse scrolling: updated capabilities" << device;
760 }
761 }
762
763 QWindowSystemInterface::handleWheelEvent(m_platformWindow->window(), qt_timestamp,
764 device, qt_windowPoint, qt_screenPoint, pixelDelta, angleDelta,
765 m_currentWheelModifiers, phase, source, isInverted);
766}
767#endif // QT_CONFIG(wheelevent)
768
769@end
static const QPointingDevice * pointingDeviceFor(qint64 deviceID)