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
quiview.mm
Go to the documentation of this file.
1// Copyright (C) 2021 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#include "quiview.h"
6
7#include "qiosglobal.h"
11#include "qiosscreen.h"
12#include "qioswindow.h"
14#include "quiwindow.h"
15#ifndef Q_OS_TVOS
16#include "qiosmenu.h"
17#endif
18
19#include <QtCore/qmath.h>
20#include <QtGui/qpointingdevice.h>
21#include <QtGui/private/qguiapplication_p.h>
22#include <QtGui/private/qwindow_p.h>
23#include <QtGui/private/qapplekeymapper_p.h>
24#include <QtGui/private/qpointingdevice_p.h>
25#include <qpa/qwindowsysteminterface_p.h>
26
27Q_LOGGING_CATEGORY(lcQpaMouse, "qt.qpa.input.mouse")
28Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet")
29Q_LOGGING_CATEGORY(lcQpaInputEvents, "qt.qpa.input.events")
30
31namespace {
32inline ulong getTimeStamp(UIEvent *event)
33{
34 bool useEvent = event != nil;
35
36#if TARGET_OS_SIMULATOR == 1
37 // We currently build Qt for simulator using X86_64, even on ARM based macs.
38 // This results in the simulator running on ARM, while the app is running
39 // inside it using Rosetta. And with this combination, the event.timestamp, which is
40 // documented to be in seconds, looks to be something else, and is not progressing
41 // in sync with a normal clock.
42 // Sending out mouse events with a timestamp that doesn't follow normal clock time
43 // will cause problems for mouse-, and pointer handlers that uses them to e.g calculate
44 // the time between a press and release, and to decide if the user is performing a tap
45 // or a drag.
46 // For that reason, we choose to ignore UIEvent.timestamp under the mentioned condition, and
47 // instead rely on NSProcessInfo. Note that if we force the whole simulator to use Rosetta
48 // (and not only the Qt app), the timestamps will progress normally.
49#if defined(Q_PROCESSOR_ARM)
50 #warning The timestamp work-around for x86_64 can (probably) be removed when building for ARM
51#endif
52 useEvent = false;
53#endif
54
55 return ulong((useEvent ? event.timestamp : NSProcessInfo.processInfo.systemUptime) * 1000);
56}
57}
58
59#if QT_CONFIG(tabletevent)
60@protocol TabletEventSender
61- (CGPoint)locationInView:(UIView *)view;
62- (CGVector)azimuthUnitVectorInView:(UIView *)view;
63@property (nonatomic, readonly) CGFloat altitudeAngle;
64@property (nonatomic, readonly) CGFloat rollAngle;
65@end
66@interface UIGestureRecognizer (TabletEventSender) <TabletEventSender> @end
67@interface UITouch (TabletEventSender) <TabletEventSender> @end
68#endif
69
70@implementation QUIView {
71 QHash<NSUInteger, QWindowSystemInterface::TouchPoint> m_activeTouches;
72 UITouch *m_activePencilTouch;
73 NSMutableArray<id> *m_accessibleElements;
74 CGPoint m_lastScrollCursorPos;
75 CGPoint m_lastScrollDelta;
76}
77
78+ (Class)layerClass
79{
80#if QT_CONFIG(opengl)
81 return [CAEAGLLayer class];
82#endif
83 return [super layerClass];
84}
85
86- (instancetype)initWithQIOSWindow:(QT_PREPEND_NAMESPACE(QIOSWindow) *)window
87{
88 if (self = [self initWithFrame:window->geometry().toCGRect()]) {
89 self.platformWindow = window;
90
91 if (isQtApplication())
92 self.hidden = YES;
93
94 m_accessibleElements = [NSMutableArray<id> new];
95
96#ifndef Q_OS_TVOS
97 self.multipleTouchEnabled = YES;
98#endif
99
100 auto scrollGestureRecognizer = [[UIPanGestureRecognizer alloc]
101 initWithTarget:self action:@selector(handleScroll:)];
102 // The gesture recognizer should only care about scroll gestures (for now)
103 // Set allowedTouchTypes to empty array here to not interfere with touch events
104 // handled by the UIView. Scroll gestures, even those coming from touch devices,
105 // such as trackpads will still be received as they are not touch events
106 scrollGestureRecognizer.allowedTouchTypes = @[];
107 scrollGestureRecognizer.allowedScrollTypesMask = UIScrollTypeMaskAll;
108 scrollGestureRecognizer.maximumNumberOfTouches = 0;
109 m_lastScrollDelta = CGPointZero;
110 m_lastScrollCursorPos = CGPointZero;
111 [self addGestureRecognizer:[scrollGestureRecognizer autorelease]];
112
113 auto mouseHoverGestureRecognizer = [[UIHoverGestureRecognizer alloc]
114 initWithTarget:self action:@selector(handleMouseHover:)];
115 mouseHoverGestureRecognizer.allowedTouchTypes = @[ @(UITouchTypeIndirectPointer) ];
116 [self addGestureRecognizer:[mouseHoverGestureRecognizer autorelease]];
117
118#if QT_CONFIG(tabletevent)
119 auto pencilHoverGestureRecognizer = [[UIHoverGestureRecognizer alloc]
120 initWithTarget:self action:@selector(handlePencilHover:)];
121 pencilHoverGestureRecognizer.allowedTouchTypes = @[ @(UITouchTypePencil) ];
122 [self addGestureRecognizer:[pencilHoverGestureRecognizer autorelease]];
123#endif
124
125 // Set up layer
126 if ([self.layer isKindOfClass:CAMetalLayer.class]) {
127 QWindow *window = self.platformWindow->window();
128 if (QColorSpace colorSpace = window->format().colorSpace(); colorSpace.isValid()) {
129 QCFType<CFDataRef> iccData = colorSpace.iccProfile().toCFData();
130 QCFType<CGColorSpaceRef> cgColorSpace = CGColorSpaceCreateWithICCData(iccData);
131 CAMetalLayer *metalLayer = static_cast<CAMetalLayer *>(self.layer);
132 metalLayer.colorspace = cgColorSpace;
133 qCDebug(lcQpaWindow) << "Set" << self << "color space to" << metalLayer.colorspace;
134 }
135 }
136#if QT_CONFIG(opengl)
137 else if ([self.layer isKindOfClass:[CAEAGLLayer class]]) {
138 CAEAGLLayer *eaglLayer = static_cast<CAEAGLLayer *>(self.layer);
139 eaglLayer.opaque = TRUE;
140 eaglLayer.drawableProperties = @{
141 kEAGLDrawablePropertyRetainedBacking: @(YES),
142 kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8
143 };
144 }
145#endif
146
147#if defined(Q_OS_VISIONOS)
148 // Although the "Drawing sharp layer-based content in visionOS" docs
149 // claim that by default a CALayer rasterizes at a 2x scale this does
150 // not seem to be the case in practice. So we explicitly set the view's
151 // scale factor based on the screen, where we hard-code it to 2.0.
152 self.contentScaleFactor = self.platformWindow->screen()->devicePixelRatio();
153#endif
154 }
155
156 return self;
157}
158
159- (void)dealloc
160{
161 [m_accessibleElements release];
162
163 [super dealloc];
164}
165
166- (NSString *)description
167{
168 NSMutableString *description = [NSMutableString stringWithString:[super description]];
169
170#ifndef QT_NO_DEBUG_STREAM
171 QString platformWindowDescription;
172 QDebug debug(&platformWindowDescription);
173 debug.nospace() << "; " << self.platformWindow << ">";
174 NSRange lastCharacter = [description rangeOfComposedCharacterSequenceAtIndex:description.length - 1];
175 [description replaceCharactersInRange:lastCharacter withString:platformWindowDescription.toNSString()];
176#endif
177
178 return description;
179}
180
181#if !defined(Q_OS_VISIONOS)
182- (void)willMoveToWindow:(UIWindow *)newWindow
183{
184 // UIKIt will normally set the scale factor of a view to match the corresponding
185 // screen scale factor, but views backed by CAEAGLLayers need to do this manually.
186 self.contentScaleFactor = newWindow && newWindow.screen ?
187 newWindow.screen.scale : [[UIScreen mainScreen] scale];
188
189 // FIXME: Allow the scale factor to be customized through QSurfaceFormat.
190}
191#endif
192
193- (void)didAddSubview:(UIView *)subview
194{
195 if ([subview isKindOfClass:[QUIView class]])
196 self.clipsToBounds = YES;
197}
198
199- (void)willRemoveSubview:(UIView *)subview
200{
201 for (UIView *view in self.subviews) {
202 if (view != subview && [view isKindOfClass:[QUIView class]])
203 return;
204 }
205
206 self.clipsToBounds = NO;
207}
208
209- (void)setNeedsDisplay
210{
211 [super setNeedsDisplay];
212
213 // We didn't implement drawRect: so we have to manually
214 // mark the layer as needing display.
215 [self.layer setNeedsDisplay];
216}
217
218- (void)layoutSubviews
219{
220 // This method is the de facto way to know that view has been resized,
221 // or otherwise needs invalidation of its buffers. Note though that we
222 // do not get this callback when the view just changes its position, so
223 // the position of our QWindow (and platform window) will only get updated
224 // when the size is also changed.
225
226 if (!CGAffineTransformIsIdentity(self.transform))
227 qWarning() << self << "has a transform set. This is not supported.";
228
229 QWindow *window = self.platformWindow->window();
230 QRect lastReportedGeometry = qt_window_private(window)->geometry;
231 QRect currentGeometry = QRectF::fromCGRect(self.frame).toRect();
232 qCDebug(lcQpaWindow) << self.platformWindow << "new geometry is" << currentGeometry;
233 QWindowSystemInterface::handleGeometryChange(window, currentGeometry);
234
235 if (currentGeometry.size() != lastReportedGeometry.size()) {
236 // Trigger expose event on resize
237 [self setNeedsDisplay];
238
239 // A new size means we also need to resize the FBO's corresponding buffers,
240 // but we defer that to when the application calls makeCurrent.
241 }
242}
243
244- (void)displayLayer:(CALayer *)layer
245{
246 Q_UNUSED(layer);
247 Q_ASSERT(layer == self.layer);
248
249 if (!self.platformWindow)
250 return;
251
252 [self sendUpdatedExposeEvent];
253}
254
255- (void)sendUpdatedExposeEvent
256{
257 QRegion region;
258
259 if (self.platformWindow->isExposed()) {
260 QSize bounds = QRectF::fromCGRect(self.layer.bounds).toRect().size();
261
262 Q_ASSERT(self.platformWindow->geometry().size() == bounds);
263 Q_ASSERT(self.hidden == !self.platformWindow->window()->isVisible());
264
265 region = QRect(QPoint(), bounds);
266 }
267
268 qCDebug(lcQpaWindow) << self.platformWindow << region << "isExposed" << self.platformWindow->isExposed();
269 QWindowSystemInterface::handleExposeEvent(self.platformWindow->window(), region);
270}
271
272- (void)safeAreaInsetsDidChange
273{
274 QWindowSystemInterface::handleSafeAreaMarginsChanged(self.platformWindow->window());
275}
276
277// -------------------------------------------------------------------------
278
279- (BOOL)canBecomeFirstResponder
280{
281 return !(self.platformWindow->window()->flags() & (Qt::WindowDoesNotAcceptFocus
282 | Qt::WindowTransparentForInput));
283}
284
285- (BOOL)becomeFirstResponder
286{
287 {
288 // Scope for the duration of becoming first responder only, as the window
289 // activation event may trigger new responders, which we don't want to be
290 // blocked by this guard.
291 FirstResponderCandidate firstResponderCandidate(self);
292
293 qImDebug() << "self:" << self << "first:" << [UIResponder qt_currentFirstResponder];
294
295 if (![super becomeFirstResponder]) {
296 qImDebug() << self << "was not allowed to become first responder";
297 return NO;
298 }
299
300 qImDebug() << self << "became first responder";
301 }
302
303 if (qGuiApp->focusWindow() != self.platformWindow->window())
304 QWindowSystemInterface::handleFocusWindowChanged(self.platformWindow->window(), Qt::ActiveWindowFocusReason);
305 else
306 qImDebug() << self.platformWindow->window() << "already active, not sending window activation";
307
308 return YES;
309}
310
311- (BOOL)responderShouldTriggerWindowDeactivation:(UIResponder *)responder
312{
313 // We don't want to send window deactivation in case the resign
314 // was a result of another Qt window becoming first responder.
315 if ([responder isKindOfClass:[QUIView class]])
316 return NO;
317
318 // Nor do we want to deactivate the Qt window if the new responder
319 // is temporarily handling text input on behalf of a Qt window.
320 if ([responder isKindOfClass:[QIOSTextResponder class]]) {
321 while ((responder = [responder nextResponder])) {
322 if ([responder isKindOfClass:[QUIView class]])
323 return NO;
324 }
325 }
326
327 return YES;
328}
329
330- (BOOL)resignFirstResponder
331{
332 qImDebug() << "self:" << self << "first:" << [UIResponder qt_currentFirstResponder];
333
334 if (![super resignFirstResponder])
335 return NO;
336
337 qImDebug() << self << "resigned first responder";
338
339 if (qGuiApp) {
340 UIResponder *newResponder = FirstResponderCandidate::currentCandidate();
341 if ([self responderShouldTriggerWindowDeactivation:newResponder])
342 QWindowSystemInterface::handleFocusWindowChanged(nullptr, Qt::ActiveWindowFocusReason);
343 }
344
345 return YES;
346}
347
348- (BOOL)isActiveWindow
349{
350 // Normally this is determined exclusivly by being firstResponder, but
351 // since we employ a separate first responder for text input we need to
352 // handle both cases as this view being the active Qt window.
353
354 if ([self isFirstResponder])
355 return YES;
356
357 UIResponder *firstResponder = [UIResponder qt_currentFirstResponder];
358 if ([firstResponder isKindOfClass:[QIOSTextInputResponder class]]
359 && [firstResponder nextResponder] == self)
360 return YES;
361
362 return NO;
363}
364
365// -------------------------------------------------------------------------
366
367- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
368{
369 [super traitCollectionDidChange: previousTraitCollection];
370
371 QPointingDevice *touchDevice = QIOSIntegration::instance()->touchDevice();
372 auto *devicePriv = QPointingDevicePrivate::get(touchDevice);
373
374 auto capabilities = touchDevice->capabilities();
375 capabilities.setFlag(QPointingDevice::Capability::Pressure,
376 (self.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable));
377 devicePriv->setCapabilities(capabilities);
378}
379
380-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
381{
382 if (self.platformWindow->window()->flags() & Qt::WindowTransparentForInput)
383 return NO;
384 return [super pointInside:point withEvent:event];
385}
386
387- (void)handleTouches:(NSSet *)touches withEvent:(UIEvent *)event withState:(QEventPoint::State)state
388{
389 QIOSIntegration *iosIntegration = QIOSIntegration::instance();
390 const ulong timeStamp = getTimeStamp(event);
391
392#if QT_CONFIG(tabletevent)
393 if (m_activePencilTouch && [touches containsObject:m_activePencilTouch]) {
394 NSArray<UITouch *> *cTouches = [event coalescedTouchesForTouch:m_activePencilTouch];
395 for (UITouch *cTouch in cTouches) {
396 QEvent::Type eventType = [&]{
397 switch (cTouch.phase) {
398 case UITouchPhaseBegan:
399 return QEvent::TabletPress;
400 case UITouchPhaseEnded:
401 return QEvent::TabletRelease;
402 case UITouchPhaseMoved:
403 case UITouchPhaseStationary:
404 return QEvent::TabletMove;
405 default:
406 return QEvent::None;
407 }
408 }();
409 [self handleTabletEvent:eventType withSender:cTouch andTimestamp:timeStamp];
410 }
411 }
412#endif
413
414 if (m_activeTouches.isEmpty())
415 return;
416 for (auto it = m_activeTouches.begin(); it != m_activeTouches.end(); ++it) {
417 auto hash = it.key();
418 QWindowSystemInterface::TouchPoint &touchPoint = it.value();
419 UITouch *uiTouch = nil;
420 for (UITouch *touch in touches) {
421 if (touch.hash == hash) {
422 uiTouch = touch;
423 break;
424 }
425 }
426 if (!uiTouch) {
427 touchPoint.state = QEventPoint::State::Stationary;
428 } else {
429 touchPoint.state = state;
430
431 // Touch positions are expected to be in QScreen global coordinates, and
432 // as we already have the QWindow positioned at the right place, we can
433 // just map from the local view position to global coordinates.
434 // tvOS: all touches start at the center of the screen and move from there.
435 QPoint localViewPosition = QPointF::fromCGPoint([uiTouch locationInView:self]).toPoint();
436 QPoint globalScreenPosition = self.platformWindow->mapToGlobal(localViewPosition);
437
438 touchPoint.area = QRectF(globalScreenPosition, QSize(0, 0));
439 touchPoint.pressure = [self pressureForTouch:uiTouch];
440 }
441 }
442
443 if ([self.window isKindOfClass:[QUIWindow class]] &&
444 !static_cast<QUIWindow *>(self.window).sendingEvent) {
445 // The event is likely delivered as part of delayed touch delivery, via
446 // _UIGestureEnvironmentSortAndSendDelayedTouches, due to one of the two
447 // _UISystemGestureGateGestureRecognizer instances on the top level window
448 // having its delaysTouchesBegan set to YES. During this delivery, it's not
449 // safe to spin up a recursive event loop, as our calling function is not
450 // reentrant, so any gestures used by the recursive code, e.g. a native
451 // alert dialog, will fail to recognize. To be on the safe side, we deliver
452 // the event asynchronously.
453 QWindowSystemInterface::handleTouchEvent<QWindowSystemInterface::AsynchronousDelivery>(
454 self.platformWindow->window(), timeStamp, iosIntegration->touchDevice(), m_activeTouches.values());
455 } else {
456 // Send the touch event asynchronously, as the application might spin a recursive
457 // event loop in response to the touch event (a dialog e.g.), which will deadlock
458 // the UIKit event delivery system (QTBUG-98651).
459 QWindowSystemInterface::handleTouchEvent<QWindowSystemInterface::AsynchronousDelivery>(
460 self.platformWindow->window(), timeStamp, iosIntegration->touchDevice(), m_activeTouches.values());
461 }
462}
463
464- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
465{
466 // UIKit generates [Began -> Moved -> Ended] event sequences for
467 // each touch point. Internally we keep a hashmap of active UITouch
468 // points to QWindowSystemInterface::TouchPoints, and assigns each TouchPoint
469 // an id for use by Qt.
470 for (UITouch *touch in touches) {
471#if QT_CONFIG(tabletevent)
472 if (touch.type == UITouchTypePencil) {
473 if (Q_UNLIKELY(m_activePencilTouch)) {
474 qWarning("ignoring additional Pencil while first is still active");
475 continue;
476 }
477 m_activePencilTouch = touch;
478 } else
479 {
480 Q_ASSERT(!m_activeTouches.contains(touch.hash));
481#endif
482 // Use window-independent touch identifiers, so that
483 // multi-touch works across windows.
484 static quint16 nextTouchId = 0;
485 m_activeTouches[touch.hash].id = nextTouchId++;
486#if QT_CONFIG(tabletevent)
487 }
488#endif
489 }
490
491 if (self.platformWindow->shouldAutoActivateWindow() && m_activeTouches.size() == 1) {
492 QPlatformWindow *topLevel = self.platformWindow;
493 while (QPlatformWindow *p = topLevel->parent())
494 topLevel = p;
495 if (topLevel->window() != QGuiApplication::focusWindow())
496 topLevel->requestActivateWindow();
497 }
498
499 [self handleTouches:touches withEvent:event withState:QEventPoint::State::Pressed];
500}
501
502- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
503{
504 [self handleTouches:touches withEvent:event withState:QEventPoint::State::Updated];
505}
506
507- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
508{
509 [self handleTouches:touches withEvent:event withState:QEventPoint::State::Released];
510
511 // Remove ended touch points from the active set:
512#ifndef Q_OS_TVOS
513 for (UITouch *touch in touches) {
514#if QT_CONFIG(tabletevent)
515 if (touch.type == UITouchTypePencil) {
516 m_activePencilTouch = nil;
517 } else
518#endif
519 {
520 m_activeTouches.remove(touch.hash);
521 }
522 }
523#else
524 // tvOS only supports single touch
525 m_activeTouches.clear();
526#endif
527}
528
529- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
530{
531 if (m_activeTouches.isEmpty() && !m_activePencilTouch)
532 return;
533
534 // When four-finger swiping, we get a touchesCancelled callback
535 // which includes all four touch points. The swipe gesture is
536 // then active until all four touches have been released, and
537 // we start getting touchesBegan events again.
538
539 // When five-finger pinching, we also get a touchesCancelled
540 // callback with all five touch points, but the pinch gesture
541 // ends when the second to last finger is released from the
542 // screen. The last finger will not emit any more touch
543 // events, _but_, will contribute to starting another pinch
544 // gesture. That second pinch gesture will _not_ trigger a
545 // touchesCancelled event when starting, but as each finger
546 // is released, and we may get touchesMoved events for the
547 // remaining fingers. [event allTouches] also contains one
548 // less touch point than it should, so this behavior is
549 // likely a bug in the iOS system gesture recognizer, but we
550 // have to take it into account when maintaining the Qt state.
551 // We do this by assuming that there are no cases where a
552 // sub-set of the active touch events are intentionally cancelled.
553
554 NSInteger count = static_cast<NSInteger>([touches count]);
555 if (count != 0 && count != m_activeTouches.count() && !m_activePencilTouch)
556 qWarning("Subset of active touches cancelled by UIKit");
557
558 m_activeTouches.clear();
559 m_activePencilTouch = nil;
560
561 QIOSIntegration *iosIntegration = static_cast<QIOSIntegration *>(QGuiApplicationPrivate::platformIntegration());
562
563 // Send the touch event asynchronously, as the application might spin a recursive
564 // event loop in response to the touch event (a dialog e.g.), which will deadlock
565 // the UIKit event delivery system (QTBUG-98651).
566 QWindowSystemInterface::handleTouchCancelEvent<QWindowSystemInterface::AsynchronousDelivery>(
567 self.platformWindow->window(), getTimeStamp(event), iosIntegration->touchDevice());
568}
569
570- (void)handleMouseHover:(UIHoverGestureRecognizer *)recognizer
571{
572 if (!self.platformWindow)
573 return;
574
575 auto *window = self.platformWindow->window();
576 auto localPosition = QPointF::fromCGPoint([recognizer locationInView:self]);
577 auto globalPosition = self.platformWindow->mapToGlobalF(localPosition);
578
579 switch (recognizer.state) {
580 case UIGestureRecognizerStateBegan:
581 qCDebug(lcQpaMouse) << "🖱️ enter" << window << "local =" << localPosition;
582 QWindowSystemInterface::handleEnterEvent(window, localPosition, globalPosition);
583 break;
584 case UIGestureRecognizerStateEnded:
585 qCDebug(lcQpaMouse) << "🖱️ leave" << window;
586 QWindowSystemInterface::handleLeaveEvent(window);
587 break;
588 case UIGestureRecognizerStateChanged:
589 qCDebug(lcQpaMouse) << "🖱️ move" << "local =" << localPosition << "global =" << globalPosition;
590 QWindowSystemInterface::handleMouseEvent(window, localPosition, globalPosition,
591 Qt::NoButton, Qt::NoButton, QEvent::MouseMove);
592 break;
593 default:
594 qCWarning(lcQpaMouse) << "Unknown hover state for" << recognizer;
595 }
596}
597
598/*
599 Computes the normalized axial pressure for a touch point.
600
601 Note that this is not the same as the perpendicular force,
602 which would incorporate the altitudeAngle. Qt does not have
603 an API for this pressure.
604*/
605- (qreal)pressureForTouch:(UITouch*)touch
606{
607 if (!touch)
608 return 0;
609
610 if (touch.maximumPossibleForce) {
611 // Note: iOS will deliver touchesBegan with a touch force of 0, which
612 // we will reflect/propagate as a 0 pressure, but there is no clear
613 // alternative, as we don't want to wait for a touchedMoved before
614 // sending a touch press event to Qt, just to have a valid pressure.
615 return touch.force / touch.maximumPossibleForce;
616 } else {
617 // We don't claim that our touch device supports QPointingDevice::Capability::Pressure,
618 // but fill in a meaningful value in case clients use it anyway. We match the behavior
619 // from above, not sending pressure for touchesBegan.
620 return (touch.phase == UITouchPhaseMoved || touch.phase == UITouchPhaseStationary) ? 1.0 : 0.0;
621 }
622}
623
624// -------------------------------------------------------------------------
625
626#if QT_CONFIG(tabletevent)
627- (void)handlePencilHover:(UIHoverGestureRecognizer *)recognizer
628{
629 if (!self.platformWindow)
630 return;
631
632 // Just before lifting the pencil we might receive a hover event,
633 // but the event's position wrongly reflects the position where the
634 // pencil was first pressed instead of the current position, and
635 // semantically it doesn't make sense to send hover before release.
636 if (m_activePencilTouch)
637 return;
638
639 QEvent::Type eventType = [&]{
640 switch (recognizer.state) {
641 case UIGestureRecognizerStateBegan: return QEvent::TabletEnterProximity;
642 case UIGestureRecognizerStateEnded: return QEvent::TabletLeaveProximity;
643 case UIGestureRecognizerStateChanged: return QEvent::TabletMove;
644 default: return QEvent::None;
645 }
646 }();
647
648 if (!eventType) {
649 qCWarning(lcQpaTablet) << "Unknown hover state for" << recognizer;
650 return;
651 }
652
653 [self handleTabletEvent:eventType withSender:recognizer andTimestamp:getTimeStamp(nil)];
654}
655
656- (void)handleTabletEvent:(QEvent::Type)eventType withSender:(id<TabletEventSender>)sender andTimestamp:(ulong)timeStamp
657{
658 QWindow *window = self.platformWindow->window();
659
660 auto localViewPosition = QPointF::fromCGPoint([sender locationInView:self]);
661 auto globalScreenPosition = self.platformWindow->mapToGlobalF(localViewPosition);
662
663 // Azimuth unit vector: +x to the right, +y going downwards
664 CGVector azimuth = [sender azimuthUnitVectorInView:self];
665 // Altitude angle given in radians. π/2 is with the pen perpendicular
666 // to the iPad. Smaller values mean more tilted, but never negative.
667 CGFloat altitudeAngleRadians = sender.altitudeAngle;
668 // Convert to degrees with zero being perpendicular
669 qreal altitudeAngleDegrees = 90 - qRadiansToDegrees(altitudeAngleRadians);
670 qreal xTilt = qBound(-60.0, altitudeAngleDegrees * azimuth.dx, 60.0);
671 qreal yTilt = qBound(-60.0, altitudeAngleDegrees * azimuth.dy, 60.0);
672
673 auto zOffset = qt_objc_cast<UIHoverGestureRecognizer*>(sender).zOffset;
674 auto pressure = [self pressureForTouch:qt_objc_cast<UITouch*>(sender)];
675
676 // Apple Pencil models that don’t support barrel-roll angle data will return 0
677 auto rotation = -qRadiansToDegrees(sender.rollAngle);
678
679 // Apple Pencils do not have the concept of tangential pressure, which
680 // is commonly found on Wacom tablet pens with a dedicated finger wheel.
681 static const int tangentialPressure = 0;
682
683 // The semantics of buttons in a QTabletEvent is that the left button is
684 // the tip, so we report it when the pencil is touching the screen.
685 Qt::MouseButtons buttons = qt_objc_cast<UITouch*>(sender) &&
686 eventType != QEvent::TabletRelease ? Qt::LeftButton : Qt::NoButton;
687 static const auto modifiers = Qt::NoModifier;
688
689 QPointingDevice *pencilDevice = QIOSIntegration::instance()->pencilDevice();
690
691 qCDebug(lcQpaTablet) << "✏️" << eventType
692 << "local =" << localViewPosition << "global =" << globalScreenPosition
693 << "z =" << zOffset << "pressure =" << pressure
694 << "rotation =" << rotation << "tilt =" << QPointF(xTilt, yTilt);
695
696 switch (eventType) {
697 case QEvent::TabletEnterProximity:
698 case QEvent::TabletLeaveProximity: {
699 const bool inProximity = eventType == QEvent::TabletEnterProximity;
700 QWindowSystemInterface::handleTabletEnterLeaveProximityEvent(window, timeStamp,
701 pencilDevice, inProximity, localViewPosition, globalScreenPosition,
702 buttons, xTilt, yTilt, tangentialPressure, rotation, zOffset, modifiers);
703 break;
704 }
705 case QEvent::TabletPress:
706 case QEvent::TabletMove:
707 case QEvent::TabletRelease: {
708 QWindowSystemInterface::handleTabletEvent(window, timeStamp,
709 pencilDevice, localViewPosition, globalScreenPosition,
710 buttons, pressure, xTilt, yTilt, tangentialPressure, rotation, zOffset, modifiers);
711 break;
712 }
713 default:
714 qCWarning(lcQpaTablet) << "Unknown tablet event type" << eventType << "for" << sender;
715 }
716}
717#endif // CONFIG(tabletevent)
718
719// -------------------------------------------------------------------------
720
721- (int)mapPressTypeToKey:(UIPress*)press withModifiers:(Qt::KeyboardModifiers)qtModifiers text:(QString &)text
722{
723 switch (press.type) {
724 case UIPressTypeUpArrow: return Qt::Key_Up;
725 case UIPressTypeDownArrow: return Qt::Key_Down;
726 case UIPressTypeLeftArrow: return Qt::Key_Left;
727 case UIPressTypeRightArrow: return Qt::Key_Right;
728 case UIPressTypeSelect: return Qt::Key_Select;
729 case UIPressTypeMenu: return Qt::Key_Menu;
730 case UIPressTypePlayPause: return Qt::Key_MediaTogglePlayPause;
731 }
732 Qt::Key key = QAppleKeyMapper::fromUIKitKey(press.key.keyCode);
733 if (key != Qt::Key_unknown)
734 return key;
735 NSString *charactersIgnoringModifiers = press.key.charactersIgnoringModifiers;
736 key = QAppleKeyMapper::fromUIKitKey(charactersIgnoringModifiers);
737 if (key != Qt::Key_unknown)
738 return key;
739 key = QAppleKeyMapper::fromNSString(qtModifiers, press.key.characters,
740 charactersIgnoringModifiers, text);
741 if (key != Qt::Key_unknown)
742 return key;
743 return Qt::Key_unknown;
744}
745
746- (bool)isControlKey:(Qt::Key)key
747{
748 switch (key) {
749 case Qt::Key_Up:
750 case Qt::Key_Down:
751 case Qt::Key_Left:
752 case Qt::Key_Right:
753 return true;
754 default:
755 break;
756 }
757
758 return false;
759}
760
761- (bool)handlePresses:(NSSet<UIPress *> *)presses eventType:(QEvent::Type)type
762{
763 // Presses on Menu button will generate a Menu key event. By default, not handling
764 // this event will cause the application to return to Headboard (tvOS launcher).
765 // When handling the event (for example, as a back button), both press and
766 // release events must be handled accordingly.
767 if (!qApp->focusWindow())
768 return false;
769
770 bool eventHandled = false;
771 const bool imEnabled = QIOSInputContext::instance()->inputMethodAccepted();
772
773 for (UIPress* press in presses) {
774 Qt::KeyboardModifiers qtModifiers = QAppleKeyMapper::fromUIKitModifiers(press.key.modifierFlags);
775 QString text;
776 int key = [self mapPressTypeToKey:press withModifiers:qtModifiers text:text];
777 if (key == Qt::Key_unknown)
778 continue;
779 if (imEnabled && ![self isControlKey:Qt::Key(key)])
780 continue;
781
782 bool keyHandled = QWindowSystemInterface::handleKeyEvent(
783 self.platformWindow->window(), type, key, qtModifiers, text);
784 eventHandled = eventHandled || keyHandled;
785 }
786
787 return eventHandled;
788}
789
790- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event
791{
792 if (![self handlePresses:presses eventType:QEvent::KeyPress])
793 [super pressesBegan:presses withEvent:event];
794}
795
796- (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event
797{
798 if (![self handlePresses:presses eventType:QEvent::KeyPress])
799 [super pressesChanged:presses withEvent:event];
800 [super pressesChanged:presses withEvent:event];
801}
802
803- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event
804{
805 if (![self handlePresses:presses eventType:QEvent::KeyRelease])
806 [super pressesEnded:presses withEvent:event];
807 [super pressesEnded:presses withEvent:event];
808}
809
810- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
811{
812#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS)
813 // Check first if QIOSMenu should handle the action before continuing up the responder chain
814 return [QIOSMenu::menuActionTarget() targetForAction:action withSender:sender] != 0;
815#else
816 Q_UNUSED(action);
817 Q_UNUSED(sender);
818 return false;
819#endif
820}
821
822- (id)forwardingTargetForSelector:(SEL)selector
823{
824 Q_UNUSED(selector);
825#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS)
826 return QIOSMenu::menuActionTarget();
827#else
828 return nil;
829#endif
830}
831
832- (void)addInteraction:(id<UIInteraction>)interaction
833{
834 if ([NSStringFromClass(interaction.class) isEqualToString:@"UITextInteraction"])
835 return;
836
837 [super addInteraction:interaction];
838}
839
840- (UIEditingInteractionConfiguration)editingInteractionConfiguration
841{
842 // We only want the three-finger-tap edit menu to be available when there's
843 // actually something to edit. Otherwise the OS will cause a slight delay
844 // before delivering the release of three finger touch input. Note that we
845 // do not do any hit testing here to check that the focus object is the one
846 // being tapped, as the behavior of native iOS apps is to trigger the menu
847 // regardless of where the gesture is being made.
848 return QIOSInputContext::instance()->inputMethodAccepted() ?
849 UIEditingInteractionConfigurationDefault : UIEditingInteractionConfigurationNone;
850}
851
852#if QT_CONFIG(wheelevent)
853- (void)handleScroll:(UIPanGestureRecognizer *)recognizer
854{
855 if (!self.platformWindow->window())
856 return;
857
858 if (!self.canBecomeFirstResponder)
859 return;
860
861 CGPoint translation = [recognizer translationInView:self];
862 CGFloat deltaX = translation.x - m_lastScrollDelta.x;
863 CGFloat deltaY = translation.y - m_lastScrollDelta.y;
864
865 QPoint angleDelta;
866 // From QNSView implementation:
867 // "Since deviceDelta is delivered as pixels rather than degrees, we need to
868 // convert from pixels to degrees in a sensible manner.
869 // It looks like 1/4 degrees per pixel behaves most native.
870 // (NB: Qt expects the unit for delta to be 8 per degree):"
871 const int pixelsToDegrees = 2; // 8 * 1/4
872 angleDelta.setX(deltaX * pixelsToDegrees);
873 angleDelta.setY(deltaY * pixelsToDegrees);
874
875 QPoint pixelDelta;
876 pixelDelta.setX(deltaX);
877 pixelDelta.setY(deltaY);
878
879 Qt::KeyboardModifiers qt_modifierFlags =
880 QAppleKeyMapper::fromUIKitModifiers(recognizer.modifierFlags);
881
882 if (recognizer.state == UIGestureRecognizerStateBegan)
883 // locationInView: doesn't return the cursor position at the time of the wheel event,
884 // but rather gives us the position with the deltas applied, so we need to save the
885 // cursor position at the beginning of the gesture
886 m_lastScrollCursorPos = [recognizer locationInView:self];
887
888 if (recognizer.state != UIGestureRecognizerStateEnded) {
889 m_lastScrollDelta.x = translation.x;
890 m_lastScrollDelta.y = translation.y;
891 } else {
892 m_lastScrollDelta = CGPointZero;
893 }
894
895 QPoint qt_local = QPointF::fromCGPoint(m_lastScrollCursorPos).toPoint();
896 QPoint qt_global = self.platformWindow->mapToGlobal(qt_local);
897
898 qCInfo(lcQpaInputEvents).nospace() << "wheel event" << " at " << qt_local
899 << " pixelDelta=" << pixelDelta << " angleDelta=" << angleDelta;
900
901 QWindowSystemInterface::handleWheelEvent(self.platformWindow->window(),
902 getTimeStamp(nil), qt_local, qt_global, pixelDelta, angleDelta, qt_modifierFlags);
903}
904
905#endif // QT_CONFIG(wheelevent)
906
907@end
908
909@implementation UIView (QtHelpers)
910
911- (QWindow *)qwindow
912{
913 if ([self isKindOfClass:[QUIView class]]) {
914 if (QT_PREPEND_NAMESPACE(QIOSWindow) *w = static_cast<QUIView *>(self).platformWindow)
915 return w->window();
916 }
917 return nil;
918}
919
920- (UIViewController *)viewController
921{
922 id responder = self;
923 while ((responder = [responder nextResponder])) {
924 if ([responder isKindOfClass:UIViewController.class])
925 return responder;
926 }
927 return nil;
928}
929
930- (QIOSViewController*)qtViewController
931{
932 UIViewController *vc = self.viewController;
933 if ([vc isKindOfClass:QIOSViewController.class])
934 return static_cast<QIOSViewController *>(vc);
935
936 return nil;
937}
938
939@end
940
941#if QT_CONFIG(metal)
942@implementation QUIMetalView
943
944+ (Class)layerClass
945{
946 return [CAMetalLayer class];
947}
948
949@end
950#endif
951
952#if QT_CONFIG(accessibility)
953// Include category as an alternative to using -ObjC (Apple QA1490)
954#include "quiview_accessibility.mm"
955#endif
Q_FORWARD_DECLARE_OBJC_CLASS(NSString)
Q_LOGGING_CATEGORY(lcEventDispatcher, "qt.eventdispatcher")
Q_FORWARD_DECLARE_OBJC_CLASS(UIViewController)
#define qImDebug
Definition qiosglobal.h:21