Qt
Internal/Contributor docs for the Qt SDK. <b>Note:</b> These are NOT official API docs; those are found <a href='https://doc.qt.io/'>here</a>.
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
4#include "quiview.h"
5
6#include "qiosglobal.h"
7#include "qiosintegration.h"
9#include "qiostextresponder.h"
10#include "qiosscreen.h"
11#include "qioswindow.h"
12#include "qiosinputcontext.h"
13#include "quiwindow.h"
14#ifndef Q_OS_TVOS
15#include "qiosmenu.h"
16#endif
17
18#include <QtCore/qmath.h>
19#include <QtGui/qpointingdevice.h>
20#include <QtGui/private/qguiapplication_p.h>
21#include <QtGui/private/qwindow_p.h>
22#include <QtGui/private/qapplekeymapper_p.h>
23#include <qpa/qwindowsysteminterface_p.h>
24
25Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet")
26Q_LOGGING_CATEGORY(lcQpaInputEvents, "qt.qpa.input.events")
27
28namespace {
29inline ulong getTimeStamp(UIEvent *event)
30{
31#if TARGET_OS_SIMULATOR == 1
32 // We currently build Qt for simulator using X86_64, even on ARM based macs.
33 // This results in the simulator running on ARM, while the app is running
34 // inside it using Rosetta. And with this combination, the event.timestamp, which is
35 // documented to be in seconds, looks to be something else, and is not progressing
36 // in sync with a normal clock.
37 // Sending out mouse events with a timestamp that doesn't follow normal clock time
38 // will cause problems for mouse-, and pointer handlers that uses them to e.g calculate
39 // the time between a press and release, and to decide if the user is performing a tap
40 // or a drag.
41 // For that reason, we choose to ignore UIEvent.timestamp under the mentioned condition, and
42 // instead rely on NSProcessInfo. Note that if we force the whole simulator to use Rosetta
43 // (and not only the Qt app), the timestamps will progress normally.
44#if defined(Q_PROCESSOR_ARM)
45 #warning The timestamp work-around for x86_64 can (probably) be removed when building for ARM
46#endif
47 return ulong(NSProcessInfo.processInfo.systemUptime * 1000);
48#endif
49
50 return ulong(event.timestamp * 1000);
51}
52}
53
54@implementation QUIView {
55 QHash<NSUInteger, QWindowSystemInterface::TouchPoint> m_activeTouches;
57 NSMutableArray<UIAccessibilityElement *> *m_accessibleElements;
58 UIPanGestureRecognizer *m_scrollGestureRecognizer;
61}
62
63+ (void)load
64{
65#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS)
67 // iOS 11 handles this though [UIView safeAreaInsetsDidChange], but there's no signal for
68 // the corresponding top and bottom layout guides that we use on earlier versions. Note
69 // that we use the _will_ change version of the notification, because we want to react
70 // to the change as early was possible. But since the top and bottom layout guides have
71 // not been updated at this point we use asynchronous delivery of the event, so that the
72 // event is processed by QtGui just after iOS has updated the layout margins.
73 [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillChangeStatusBarFrameNotification
74 object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *) {
75 for (QWindow *window : QGuiApplication::allWindows())
76 QWindowSystemInterface::handleSafeAreaMarginsChanged<QWindowSystemInterface::AsynchronousDelivery>(window);
77 }
78 ];
79 }
80#endif
81}
82
83+ (Class)layerClass
84{
85#if QT_CONFIG(opengl)
86 return [CAEAGLLayer class];
87#endif
88 return [super layerClass];
89}
90
91- (instancetype)initWithQIOSWindow:(QT_PREPEND_NAMESPACE(QIOSWindow) *)window
92{
93 if (self = [self initWithFrame:window->geometry().toCGRect()]) {
94 self.platformWindow = window;
95 m_accessibleElements = [[NSMutableArray<UIAccessibilityElement *> alloc] init];
96 m_scrollGestureRecognizer = [[UIPanGestureRecognizer alloc]
97 initWithTarget:self
98 action:@selector(handleScroll:)];
99 // The gesture recognizer should only care about scroll gestures (for now)
100 // Set allowedTouchTypes to empty array here to not interfere with touch events
101 // handled by the UIView. Scroll gestures, even those coming from touch devices,
102 // such as trackpads will still be received as they are not touch events
103 m_scrollGestureRecognizer.allowedTouchTypes = [NSArray array];
104 if (@available(ios 13.4, *)) {
105 m_scrollGestureRecognizer.allowedScrollTypesMask = UIScrollTypeMaskAll;
106 }
107 m_scrollGestureRecognizer.maximumNumberOfTouches = 0;
108 m_lastScrollDelta = CGPointZero;
109 m_lastScrollCursorPos = CGPointZero;
110 [self addGestureRecognizer:m_scrollGestureRecognizer];
111
112 if ([self.layer isKindOfClass:CAMetalLayer.class]) {
113 QWindow *window = self.platformWindow->window();
114 if (QColorSpace colorSpace = window->format().colorSpace(); colorSpace.isValid()) {
115 QCFType<CFDataRef> iccData = colorSpace.iccProfile().toCFData();
116 QCFType<CGColorSpaceRef> cgColorSpace = CGColorSpaceCreateWithICCData(iccData);
117 CAMetalLayer *metalLayer = static_cast<CAMetalLayer *>(self.layer);
118 metalLayer.colorspace = cgColorSpace;
119 qCDebug(lcQpaWindow) << "Set" << self << "color space to" << metalLayer.colorspace;
120 }
121 }
122 }
123
124 return self;
125}
126
127- (instancetype)initWithFrame:(CGRect)frame
128{
129 if ((self = [super initWithFrame:frame])) {
130#if QT_CONFIG(opengl)
131 if ([self.layer isKindOfClass:[CAEAGLLayer class]]) {
132 // Set up EAGL layer
133 CAEAGLLayer *eaglLayer = static_cast<CAEAGLLayer *>(self.layer);
134 eaglLayer.opaque = TRUE;
135 eaglLayer.drawableProperties = @{
136 kEAGLDrawablePropertyRetainedBacking: @(YES),
137 kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8
138 };
139 }
140#endif
141
142 if (isQtApplication())
143 self.hidden = YES;
144
145#ifndef Q_OS_TVOS
146 self.multipleTouchEnabled = YES;
147#endif
148
149 if (qEnvironmentVariableIntValue("QT_IOS_DEBUG_WINDOW_MANAGEMENT")) {
150 static CGFloat hue = 0.0;
151 CGFloat lastHue = hue;
152 for (CGFloat diff = 0; diff < 0.1 || diff > 0.9; diff = fabs(hue - lastHue))
153 hue = drand48();
154
155 #define colorWithBrightness(br) \
156 [UIColor colorWithHue:hue saturation:0.5 brightness:br alpha:1.0].CGColor
157
158 self.layer.borderColor = colorWithBrightness(1.0);
159 self.layer.borderWidth = 1.0;
160 }
161
162 if (qEnvironmentVariableIsSet("QT_IOS_DEBUG_WINDOW_SAFE_AREAS")) {
163 UIView *safeAreaOverlay = [[UIView alloc] initWithFrame:CGRectZero];
164 [safeAreaOverlay setBackgroundColor:[UIColor colorWithRed:0.3 green:0.7 blue:0.9 alpha:0.3]];
165 [self addSubview:safeAreaOverlay];
166
167 safeAreaOverlay.translatesAutoresizingMaskIntoConstraints = NO;
168 [safeAreaOverlay.topAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.topAnchor].active = YES;
169 [safeAreaOverlay.leftAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.leftAnchor].active = YES;
170 [safeAreaOverlay.rightAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.rightAnchor].active = YES;
171 [safeAreaOverlay.bottomAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.bottomAnchor].active = YES;
172 }
173 }
174
175 return self;
176}
177
178- (void)dealloc
179{
180 [m_accessibleElements release];
181 [m_scrollGestureRecognizer release];
182
183 [super dealloc];
184}
185
186- (NSString *)description
187{
188 NSMutableString *description = [NSMutableString stringWithString:[super description]];
189
190#ifndef QT_NO_DEBUG_STREAM
191 QString platformWindowDescription;
192 QDebug debug(&platformWindowDescription);
193 debug.nospace() << "; " << self.platformWindow << ">";
194 NSRange lastCharacter = [description rangeOfComposedCharacterSequenceAtIndex:description.length - 1];
195 [description replaceCharactersInRange:lastCharacter withString:platformWindowDescription.toNSString()];
196#endif
197
198 return description;
199}
200
201#if !defined(Q_OS_VISIONOS)
202- (void)willMoveToWindow:(UIWindow *)newWindow
203{
204 // UIKIt will normally set the scale factor of a view to match the corresponding
205 // screen scale factor, but views backed by CAEAGLLayers need to do this manually.
206 self.contentScaleFactor = newWindow && newWindow.screen ?
207 newWindow.screen.scale : [[UIScreen mainScreen] scale];
208
209 // FIXME: Allow the scale factor to be customized through QSurfaceFormat.
210}
211#endif
212
213- (void)didAddSubview:(UIView *)subview
214{
215 if ([subview isKindOfClass:[QUIView class]])
216 self.clipsToBounds = YES;
217}
218
219- (void)willRemoveSubview:(UIView *)subview
220{
221 for (UIView *view in self.subviews) {
222 if (view != subview && [view isKindOfClass:[QUIView class]])
223 return;
224 }
225
226 self.clipsToBounds = NO;
227}
228
229- (void)setNeedsDisplay
230{
231 [super setNeedsDisplay];
232
233 // We didn't implement drawRect: so we have to manually
234 // mark the layer as needing display.
235 [self.layer setNeedsDisplay];
236}
237
238- (void)layoutSubviews
239{
240 // This method is the de facto way to know that view has been resized,
241 // or otherwise needs invalidation of its buffers. Note though that we
242 // do not get this callback when the view just changes its position, so
243 // the position of our QWindow (and platform window) will only get updated
244 // when the size is also changed.
245
246 if (!CGAffineTransformIsIdentity(self.transform))
247 qWarning() << self << "has a transform set. This is not supported.";
248
249 QWindow *window = self.platformWindow->window();
250 QRect lastReportedGeometry = qt_window_private(window)->geometry;
251 QRect currentGeometry = QRectF::fromCGRect(self.frame).toRect();
252 qCDebug(lcQpaWindow) << self.platformWindow << "new geometry is" << currentGeometry;
254
255 if (currentGeometry.size() != lastReportedGeometry.size()) {
256 // Trigger expose event on resize
257 [self setNeedsDisplay];
258
259 // A new size means we also need to resize the FBO's corresponding buffers,
260 // but we defer that to when the application calls makeCurrent.
261 }
262}
263
264- (void)displayLayer:(CALayer *)layer
265{
267 Q_ASSERT(layer == self.layer);
268
269 if (!self.platformWindow)
270 return;
271
272 [self sendUpdatedExposeEvent];
273}
274
275- (void)sendUpdatedExposeEvent
276{
277 QRegion region;
278
279 if (self.platformWindow->isExposed()) {
280 QSize bounds = QRectF::fromCGRect(self.layer.bounds).toRect().size();
281
282 Q_ASSERT(self.platformWindow->geometry().size() == bounds);
283 Q_ASSERT(self.hidden == !self.platformWindow->window()->isVisible());
284
285 region = QRect(QPoint(), bounds);
286 }
287
288 qCDebug(lcQpaWindow) << self.platformWindow << region << "isExposed" << self.platformWindow->isExposed();
289 QWindowSystemInterface::handleExposeEvent(self.platformWindow->window(), region);
290}
291
292- (void)safeAreaInsetsDidChange
293{
295}
296
297// -------------------------------------------------------------------------
298
299- (BOOL)canBecomeFirstResponder
300{
301 return !(self.platformWindow->window()->flags() & (Qt::WindowDoesNotAcceptFocus
303}
304
305- (BOOL)becomeFirstResponder
306{
307 {
308 // Scope for the duration of becoming first responder only, as the window
309 // activation event may trigger new responders, which we don't want to be
310 // blocked by this guard.
311 FirstResponderCandidate firstResponderCandidate(self);
312
313 qImDebug() << "self:" << self << "first:" << [UIResponder qt_currentFirstResponder];
314
315 if (![super becomeFirstResponder]) {
316 qImDebug() << self << "was not allowed to become first responder";
317 return NO;
318 }
319
320 qImDebug() << self << "became first responder";
321 }
322
323 if (qGuiApp->focusWindow() != self.platformWindow->window())
325 else
326 qImDebug() << self.platformWindow->window() << "already active, not sending window activation";
327
328 return YES;
329}
330
331- (BOOL)responderShouldTriggerWindowDeactivation:(UIResponder *)responder
332{
333 // We don't want to send window deactivation in case the resign
334 // was a result of another Qt window becoming first responder.
335 if ([responder isKindOfClass:[QUIView class]])
336 return NO;
337
338 // Nor do we want to deactivate the Qt window if the new responder
339 // is temporarily handling text input on behalf of a Qt window.
340 if ([responder isKindOfClass:[QIOSTextResponder class]]) {
341 while ((responder = [responder nextResponder])) {
342 if ([responder isKindOfClass:[QUIView class]])
343 return NO;
344 }
345 }
346
347 return YES;
348}
349
350- (BOOL)resignFirstResponder
351{
352 qImDebug() << "self:" << self << "first:" << [UIResponder qt_currentFirstResponder];
353
354 if (![super resignFirstResponder])
355 return NO;
356
357 qImDebug() << self << "resigned first responder";
358
359 UIResponder *newResponder = FirstResponderCandidate::currentCandidate();
360 if ([self responderShouldTriggerWindowDeactivation:newResponder])
361 QWindowSystemInterface::handleFocusWindowChanged(nullptr, Qt::ActiveWindowFocusReason);
362
363 return YES;
364}
365
366- (BOOL)isActiveWindow
367{
368 // Normally this is determined exclusivly by being firstResponder, but
369 // since we employ a separate first responder for text input we need to
370 // handle both cases as this view being the active Qt window.
371
372 if ([self isFirstResponder])
373 return YES;
374
375 UIResponder *firstResponder = [UIResponder qt_currentFirstResponder];
376 if ([firstResponder isKindOfClass:[QIOSTextInputResponder class]]
377 && [firstResponder nextResponder] == self)
378 return YES;
379
380 return NO;
381}
382
383// -------------------------------------------------------------------------
384
385- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
386{
387 [super traitCollectionDidChange: previousTraitCollection];
388
389 QPointingDevice *touchDevice = QIOSIntegration::instance()->touchDevice();
390 QPointingDevice::Capabilities touchCapabilities = touchDevice->capabilities();
391
392 touchCapabilities.setFlag(QPointingDevice::Capability::Pressure,
393 (self.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable));
394
395 touchDevice->setCapabilities(touchCapabilities);
396}
397
398-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
399{
400 if (self.platformWindow->window()->flags() & Qt::WindowTransparentForInput)
401 return NO;
402 return [super pointInside:point withEvent:event];
403}
404
405- (void)handleTouches:(NSSet *)touches withEvent:(UIEvent *)event withState:(QEventPoint::State)state withTimestamp:(ulong)timeStamp
406{
407 QIOSIntegration *iosIntegration = QIOSIntegration::instance();
408 bool supportsPressure = QIOSIntegration::instance()->touchDevice()->capabilities() & QPointingDevice::Capability::Pressure;
409
410#if QT_CONFIG(tabletevent)
411 if (m_activePencilTouch && [touches containsObject:m_activePencilTouch]) {
412 NSArray<UITouch *> *cTouches = [event coalescedTouchesForTouch:m_activePencilTouch];
413 int i = 0;
414 for (UITouch *cTouch in cTouches) {
415 QPointF localViewPosition = QPointF::fromCGPoint([cTouch preciseLocationInView:self]);
416 QPoint localViewPositionI = localViewPosition.toPoint();
417 QPointF globalScreenPosition = self.platformWindow->mapToGlobal(localViewPositionI) +
418 (localViewPosition - localViewPositionI);
419 qreal pressure = cTouch.force / cTouch.maximumPossibleForce;
420 // azimuth unit vector: +x to the right, +y going downwards
421 CGVector azimuth = [cTouch azimuthUnitVectorInView: self];
422 // azimuthAngle given in radians, zero when the stylus points towards +x axis; converted to degrees with 0 pointing straight up
423 qreal azimuthAngle = qRadiansToDegrees([cTouch azimuthAngleInView: self]) + 90;
424 // altitudeAngle given in radians, pi / 2 is with the stylus perpendicular to the iPad, smaller values mean more tilted, but never negative.
425 // Convert to degrees with zero being perpendicular.
426 qreal altitudeAngle = 90 - qRadiansToDegrees(cTouch.altitudeAngle);
427 qCDebug(lcQpaTablet) << i << ":" << timeStamp << localViewPosition << pressure << state << "azimuth" << azimuth.dx << azimuth.dy
428 << "angle" << azimuthAngle << "altitude" << cTouch.altitudeAngle
429 << "xTilt" << qBound(-60.0, altitudeAngle * azimuth.dx, 60.0) << "yTilt" << qBound(-60.0, altitudeAngle * azimuth.dy, 60.0);
430 QWindowSystemInterface::handleTabletEvent(self.platformWindow->window(), timeStamp, localViewPosition, globalScreenPosition,
431 // device, pointerType, buttons
433 // pressure, xTilt, yTilt
434 pressure, qBound(-60.0, altitudeAngle * azimuth.dx, 60.0), qBound(-60.0, altitudeAngle * azimuth.dy, 60.0),
435 // tangentialPressure, rotation, z, uid, modifiers
436 0, azimuthAngle, 0, 0, Qt::NoModifier);
437 ++i;
438 }
439 }
440#endif
441
442 if (m_activeTouches.isEmpty())
443 return;
444 for (auto it = m_activeTouches.begin(); it != m_activeTouches.end(); ++it) {
445 auto hash = it.key();
446 QWindowSystemInterface::TouchPoint &touchPoint = it.value();
447 UITouch *uiTouch = nil;
448 for (UITouch *touch in touches) {
449 if (touch.hash == hash) {
450 uiTouch = touch;
451 break;
452 }
453 }
454 if (!uiTouch) {
456 } else {
457 touchPoint.state = state;
458
459 // Touch positions are expected to be in QScreen global coordinates, and
460 // as we already have the QWindow positioned at the right place, we can
461 // just map from the local view position to global coordinates.
462 // tvOS: all touches start at the center of the screen and move from there.
463 QPoint localViewPosition = QPointF::fromCGPoint([uiTouch locationInView:self]).toPoint();
464 QPoint globalScreenPosition = self.platformWindow->mapToGlobal(localViewPosition);
465
466 touchPoint.area = QRectF(globalScreenPosition, QSize(0, 0));
467
468 // FIXME: Do we really need to support QPointingDevice::Capability::NormalizedPosition?
469 QSize screenSize = self.platformWindow->screen()->geometry().size();
470 touchPoint.normalPosition = QPointF(globalScreenPosition.x() / screenSize.width(),
471 globalScreenPosition.y() / screenSize.height());
472
473 if (supportsPressure) {
474 // Note: iOS will deliver touchesBegan with a touch force of 0, which
475 // we will reflect/propagate as a 0 pressure, but there is no clear
476 // alternative, as we don't want to wait for a touchedMoved before
477 // sending a touch press event to Qt, just to have a valid pressure.
478 touchPoint.pressure = uiTouch.force / uiTouch.maximumPossibleForce;
479 } else {
480 // We don't claim that our touch device supports QPointingDevice::Capability::Pressure,
481 // but fill in a meaningful value in case clients use it anyway.
482 touchPoint.pressure = (state == QEventPoint::State::Released) ? 0.0 : 1.0;
483 }
484 }
485 }
486
487 if ([self.window isKindOfClass:[QUIWindow class]] &&
488 !static_cast<QUIWindow *>(self.window).sendingEvent) {
489 // The event is likely delivered as part of delayed touch delivery, via
490 // _UIGestureEnvironmentSortAndSendDelayedTouches, due to one of the two
491 // _UISystemGestureGateGestureRecognizer instances on the top level window
492 // having its delaysTouchesBegan set to YES. During this delivery, it's not
493 // safe to spin up a recursive event loop, as our calling function is not
494 // reentrant, so any gestures used by the recursive code, e.g. a native
495 // alert dialog, will fail to recognize. To be on the safe side, we deliver
496 // the event asynchronously.
497 QWindowSystemInterface::handleTouchEvent<QWindowSystemInterface::AsynchronousDelivery>(
498 self.platformWindow->window(), timeStamp, iosIntegration->touchDevice(), m_activeTouches.values());
499 } else {
500 // Send the touch event asynchronously, as the application might spin a recursive
501 // event loop in response to the touch event (a dialog e.g.), which will deadlock
502 // the UIKit event delivery system (QTBUG-98651).
503 QWindowSystemInterface::handleTouchEvent<QWindowSystemInterface::AsynchronousDelivery>(
504 self.platformWindow->window(), timeStamp, iosIntegration->touchDevice(), m_activeTouches.values());
505 }
506}
507
508- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
509{
510 // UIKit generates [Began -> Moved -> Ended] event sequences for
511 // each touch point. Internally we keep a hashmap of active UITouch
512 // points to QWindowSystemInterface::TouchPoints, and assigns each TouchPoint
513 // an id for use by Qt.
514 for (UITouch *touch in touches) {
515#if QT_CONFIG(tabletevent)
516 if (touch.type == UITouchTypeStylus) {
518 qWarning("ignoring additional Pencil while first is still active");
519 continue;
520 }
521 m_activePencilTouch = touch;
522 } else
523 {
524 Q_ASSERT(!m_activeTouches.contains(touch.hash));
525#endif
526 // Use window-independent touch identifiers, so that
527 // multi-touch works across windows.
528 static quint16 nextTouchId = 0;
529 m_activeTouches[touch.hash].id = nextTouchId++;
530#if QT_CONFIG(tabletevent)
531 }
532#endif
533 }
534
535 if (self.platformWindow->shouldAutoActivateWindow() && m_activeTouches.size() == 1) {
536 QPlatformWindow *topLevel = self.platformWindow;
537 while (QPlatformWindow *p = topLevel->parent())
538 topLevel = p;
539 if (topLevel->window() != QGuiApplication::focusWindow())
540 topLevel->requestActivateWindow();
541 }
542
543 [self handleTouches:touches withEvent:event withState:QEventPoint::State::Pressed withTimestamp:getTimeStamp(event)];
544}
545
546- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
547{
548 [self handleTouches:touches withEvent:event withState:QEventPoint::State::Updated withTimestamp:getTimeStamp(event)];
549}
550
551- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
552{
553 [self handleTouches:touches withEvent:event withState:QEventPoint::State::Released withTimestamp:getTimeStamp(event)];
554
555 // Remove ended touch points from the active set:
556#ifndef Q_OS_TVOS
557 for (UITouch *touch in touches) {
558#if QT_CONFIG(tabletevent)
559 if (touch.type == UITouchTypeStylus) {
561 } else
562#endif
563 {
564 m_activeTouches.remove(touch.hash);
565 }
566 }
567#else
568 // tvOS only supports single touch
569 m_activeTouches.clear();
570#endif
571}
572
573- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
574{
575 if (m_activeTouches.isEmpty() && !m_activePencilTouch)
576 return;
577
578 // When four-finger swiping, we get a touchesCancelled callback
579 // which includes all four touch points. The swipe gesture is
580 // then active until all four touches have been released, and
581 // we start getting touchesBegan events again.
582
583 // When five-finger pinching, we also get a touchesCancelled
584 // callback with all five touch points, but the pinch gesture
585 // ends when the second to last finger is released from the
586 // screen. The last finger will not emit any more touch
587 // events, _but_, will contribute to starting another pinch
588 // gesture. That second pinch gesture will _not_ trigger a
589 // touchesCancelled event when starting, but as each finger
590 // is released, and we may get touchesMoved events for the
591 // remaining fingers. [event allTouches] also contains one
592 // less touch point than it should, so this behavior is
593 // likely a bug in the iOS system gesture recognizer, but we
594 // have to take it into account when maintaining the Qt state.
595 // We do this by assuming that there are no cases where a
596 // sub-set of the active touch events are intentionally cancelled.
597
598 NSInteger count = static_cast<NSInteger>([touches count]);
599 if (count != 0 && count != m_activeTouches.count() && !m_activePencilTouch)
600 qWarning("Subset of active touches cancelled by UIKit");
601
602 m_activeTouches.clear();
604
605 ulong timestamp = event ? getTimeStamp(event) : ([[NSProcessInfo processInfo] systemUptime] * 1000);
606
608
609 // Send the touch event asynchronously, as the application might spin a recursive
610 // event loop in response to the touch event (a dialog e.g.), which will deadlock
611 // the UIKit event delivery system (QTBUG-98651).
612 QWindowSystemInterface::handleTouchCancelEvent<QWindowSystemInterface::AsynchronousDelivery>(
613 self.platformWindow->window(), timestamp, iosIntegration->touchDevice());
614}
615
616- (int)mapPressTypeToKey:(UIPress*)press withModifiers:(Qt::KeyboardModifiers)qtModifiers text:(QString &)text
617{
618 switch (press.type) {
619 case UIPressTypeUpArrow: return Qt::Key_Up;
620 case UIPressTypeDownArrow: return Qt::Key_Down;
621 case UIPressTypeLeftArrow: return Qt::Key_Left;
622 case UIPressTypeRightArrow: return Qt::Key_Right;
623 case UIPressTypeSelect: return Qt::Key_Select;
624 case UIPressTypeMenu: return Qt::Key_Menu;
625 case UIPressTypePlayPause: return Qt::Key_MediaTogglePlayPause;
626 }
627 if (@available(ios 13.4, *)) {
628 NSString *charactersIgnoringModifiers = press.key.charactersIgnoringModifiers;
629 Qt::Key key = QAppleKeyMapper::fromUIKitKey(charactersIgnoringModifiers);
630 if (key != Qt::Key_unknown)
631 return key;
632 return QAppleKeyMapper::fromNSString(qtModifiers, press.key.characters,
633 charactersIgnoringModifiers, text);
634 }
635 return Qt::Key_unknown;
636}
637
638- (bool)isControlKey:(Qt::Key)key
639{
640 switch (key) {
641 case Qt::Key_Up:
642 case Qt::Key_Down:
643 case Qt::Key_Left:
644 case Qt::Key_Right:
645 return true;
646 default:
647 break;
648 }
649
650 return false;
651}
652
653- (bool)handlePresses:(NSSet<UIPress *> *)presses eventType:(QEvent::Type)type
654{
655 // Presses on Menu button will generate a Menu key event. By default, not handling
656 // this event will cause the application to return to Headboard (tvOS launcher).
657 // When handling the event (for example, as a back button), both press and
658 // release events must be handled accordingly.
659 if (!qApp->focusWindow())
660 return false;
661
662 bool eventHandled = false;
663 const bool imEnabled = QIOSInputContext::instance()->inputMethodAccepted();
664
665 for (UIPress* press in presses) {
666 Qt::KeyboardModifiers qtModifiers = Qt::NoModifier;
667 if (@available(ios 13.4, *))
668 qtModifiers = QAppleKeyMapper::fromUIKitModifiers(press.key.modifierFlags);
670 int key = [self mapPressTypeToKey:press withModifiers:qtModifiers text:text];
671 if (key == Qt::Key_unknown)
672 continue;
673 if (imEnabled && ![self isControlKey:Qt::Key(key)])
674 continue;
675
677 self.platformWindow->window(), type, key, qtModifiers, text);
678 eventHandled = eventHandled || keyHandled;
679 }
680
681 return eventHandled;
682}
683
684- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event
685{
686 if (![self handlePresses:presses eventType:QEvent::KeyPress])
687 [super pressesBegan:presses withEvent:event];
688}
689
690- (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event
691{
692 if (![self handlePresses:presses eventType:QEvent::KeyPress])
693 [super pressesChanged:presses withEvent:event];
694 [super pressesChanged:presses withEvent:event];
695}
696
697- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event
698{
699 if (![self handlePresses:presses eventType:QEvent::KeyRelease])
700 [super pressesEnded:presses withEvent:event];
701 [super pressesEnded:presses withEvent:event];
702}
703
704- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
705{
706#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS)
707 // Check first if QIOSMenu should handle the action before continuing up the responder chain
708 return [QIOSMenu::menuActionTarget() targetForAction:action withSender:sender] != 0;
709#else
710 Q_UNUSED(action);
711 Q_UNUSED(sender);
712 return false;
713#endif
714}
715
716- (id)forwardingTargetForSelector:(SEL)selector
717{
719#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS)
721#else
722 return nil;
723#endif
724}
725
726- (void)addInteraction:(id<UIInteraction>)interaction
727{
728 if ([NSStringFromClass(interaction.class) isEqualToString:@"UITextInteraction"])
729 return;
730
731 [super addInteraction:interaction];
732}
733
734- (UIEditingInteractionConfiguration)editingInteractionConfiguration
735{
736 // We only want the three-finger-tap edit menu to be available when there's
737 // actually something to edit. Otherwise the OS will cause a slight delay
738 // before delivering the release of three finger touch input. Note that we
739 // do not do any hit testing here to check that the focus object is the one
740 // being tapped, as the behavior of native iOS apps is to trigger the menu
741 // regardless of where the gesture is being made.
742 return QIOSInputContext::instance()->inputMethodAccepted() ?
743 UIEditingInteractionConfigurationDefault : UIEditingInteractionConfigurationNone;
744}
745
746#if QT_CONFIG(wheelevent)
747- (void)handleScroll:(UIPanGestureRecognizer *)recognizer
748{
749 if (!self.platformWindow->window())
750 return;
751
752 if (!self.canBecomeFirstResponder)
753 return;
754
755 CGPoint translation = [recognizer translationInView:self];
756 CGFloat deltaX = translation.x - m_lastScrollDelta.x;
757 CGFloat deltaY = translation.y - m_lastScrollDelta.y;
758
759 QPoint angleDelta;
760 // From QNSView implementation:
761 // "Since deviceDelta is delivered as pixels rather than degrees, we need to
762 // convert from pixels to degrees in a sensible manner.
763 // It looks like 1/4 degrees per pixel behaves most native.
764 // (NB: Qt expects the unit for delta to be 8 per degree):"
765 const int pixelsToDegrees = 2; // 8 * 1/4
766 angleDelta.setX(deltaX * pixelsToDegrees);
767 angleDelta.setY(deltaY * pixelsToDegrees);
768
769 QPoint pixelDelta;
770 pixelDelta.setX(deltaX);
771 pixelDelta.setY(deltaY);
772
773 NSTimeInterval time_stamp = [[NSProcessInfo processInfo] systemUptime];
774 ulong qt_timestamp = time_stamp * 1000;
775
776 Qt::KeyboardModifiers qt_modifierFlags = Qt::NoModifier;
777 if (@available(ios 13.4, *))
778 qt_modifierFlags = QAppleKeyMapper::fromUIKitModifiers(recognizer.modifierFlags);
779
780 if (recognizer.state == UIGestureRecognizerStateBegan)
781 // locationInView: doesn't return the cursor position at the time of the wheel event,
782 // but rather gives us the position with the deltas applied, so we need to save the
783 // cursor position at the beginning of the gesture
784 m_lastScrollCursorPos = [recognizer locationInView:self];
785
786 if (recognizer.state != UIGestureRecognizerStateEnded) {
787 m_lastScrollDelta.x = translation.x;
788 m_lastScrollDelta.y = translation.y;
789 } else {
790 m_lastScrollDelta = CGPointZero;
791 }
792
793 QPoint qt_local = QPointF::fromCGPoint(m_lastScrollCursorPos).toPoint();
794 QPoint qt_global = self.platformWindow->mapToGlobal(qt_local);
795
796 qCInfo(lcQpaInputEvents).nospace() << "wheel event" << " at " << qt_local
797 << " pixelDelta=" << pixelDelta << " angleDelta=" << angleDelta;
798
799 QWindowSystemInterface::handleWheelEvent(self.platformWindow->window(), qt_timestamp, qt_local, qt_global, pixelDelta, angleDelta, qt_modifierFlags);
800}
801#endif // QT_CONFIG(wheelevent)
802
803@end
804
805@implementation UIView (QtHelpers)
806
807- (QWindow *)qwindow
808{
809 if ([self isKindOfClass:[QUIView class]]) {
810 if (QT_PREPEND_NAMESPACE(QIOSWindow) *w = static_cast<QUIView *>(self).platformWindow)
811 return w->window();
812 }
813 return nil;
814}
815
816- (UIViewController *)viewController
817{
818 id responder = self;
819 while ((responder = [responder nextResponder])) {
820 if ([responder isKindOfClass:UIViewController.class])
821 return responder;
822 }
823 return nil;
824}
825
827{
828 UIViewController *vc = self.viewController;
829 if ([vc isKindOfClass:QIOSViewController.class])
830 return static_cast<QIOSViewController *>(vc);
831
832 return nil;
833}
834
835@end
836
837#if QT_CONFIG(metal)
838@implementation QUIMetalView
839
840+ (Class)layerClass
841{
842 return [CAMetalLayer class];
843}
844
845@end
846#endif
847
848#if QT_CONFIG(accessibility)
849// Include category as an alternative to using -ObjC (Apple QA1490)
851#endif
QIOSViewController * qtViewController()
Definition quiview.mm:826
UIViewController * viewController()
Definition quiview.mm:816
QWindow * qwindow()
Definition quiview.mm:807
static UIResponder * currentCandidate()
Definition qiosglobal.h:56
static Qt::Key fromNSString(Qt::KeyboardModifiers qtMods, NSString *characters, NSString *charactersIgnoringModifiers, QString &text)
static Qt::Key fromUIKitKey(NSString *keyCode)
static Qt::KeyboardModifiers fromUIKitModifiers(ulong uikitModifiers)
The QColorSpace class provides a color space abstraction.
Definition qcolorspace.h:21
\inmodule QtCore
The QEventPoint class provides information about a point in a QPointerEvent.
Definition qeventpoint.h:20
\inmodule QtCore
Definition qcoreevent.h:45
static QPlatformIntegration * platformIntegration()
static QWindow * focusWindow()
Returns the QWindow that receives events tied to focus, such as key events.
static QIOSInputContext * instance()
static QIOSIntegration * instance()
static id menuActionTarget()
Definition qiosmenu.h:77
Capabilities capabilities
static Q_CORE_EXPORT QOperatingSystemVersionBase current()
The QPlatformWindow class provides an abstraction for top-level windows.
QWindow * window() const
Returns the window which belongs to the QPlatformWindow.
QPlatformWindow * parent() const
Returns the parent platform window (or \nullptr if orphan).
virtual void requestActivateWindow()
Reimplement to let Qt be able to request activation/focus for a window.
\inmodule QtCore\reentrant
Definition qpoint.h:217
\inmodule QtCore\reentrant
Definition qpoint.h:25
constexpr void setY(int y) noexcept
Sets the y coordinate of this point to the given y coordinate.
Definition qpoint.h:145
constexpr void setX(int x) noexcept
Sets the x coordinate of this point to the given x coordinate.
Definition qpoint.h:140
The QPointingDevice class describes a device from which mouse, touch or tablet events originate.
\inmodule QtCore\reentrant
Definition qrect.h:484
\inmodule QtCore\reentrant
Definition qrect.h:30
The QRegion class specifies a clip region for a painter.
Definition qregion.h:27
iterator begin()
Definition qset.h:136
iterator end()
Definition qset.h:140
\inmodule QtCore
Definition qsize.h:25
constexpr int height() const noexcept
Returns the height.
Definition qsize.h:133
constexpr int width() const noexcept
Returns the width.
Definition qsize.h:130
\macro QT_RESTRICTED_CAST_FROM_ASCII
Definition qstring.h:129
qsizetype size() const noexcept
Returns the number of characters in this string.
Definition qstring.h:186
The QWindowSystemInterface provides an event queue for the QPA platform.
static void handleSafeAreaMarginsChanged(QWindow *window)
static bool handleTabletEvent(QWindow *window, ulong timestamp, const QPointingDevice *device, const QPointF &local, const QPointF &global, Qt::MouseButtons buttons, qreal pressure, int xTilt, int yTilt, qreal tangentialPressure, qreal rotation, int z, Qt::KeyboardModifiers modifiers=Qt::NoModifier)
static void handleFocusWindowChanged(QWindow *window, Qt::FocusReason r=Qt::OtherFocusReason)
static void handleGeometryChange(QWindow *window, const QRect &newRect)
static bool handleKeyEvent(QWindow *window, QEvent::Type t, int k, Qt::KeyboardModifiers mods, const QString &text=QString(), bool autorep=false, ushort count=1)
static bool handleExposeEvent(QWindow *window, const QRegion &region)
static bool handleWheelEvent(QWindow *window, const QPointF &local, const QPointF &global, QPoint pixelDelta, QPoint angleDelta, Qt::KeyboardModifiers mods=Qt::NoModifier, Qt::ScrollPhase phase=Qt::NoScrollPhase, Qt::MouseEventSource source=Qt::MouseEventNotSynthesized)
\inmodule QtGui
Definition qwindow.h:63
QHash< int, QWidget * > hash
[35multi]
p1 load("image.bmp")
QString text
QSet< QString >::iterator it
else opt state
[0]
Definition qcompare.h:63
@ LeftButton
Definition qnamespace.h:58
@ NoButton
Definition qnamespace.h:57
@ Key_Select
@ Key_Right
Definition qnamespace.h:679
@ Key_MediaTogglePlayPause
Definition qnamespace.h:864
@ Key_Left
Definition qnamespace.h:677
@ Key_Up
Definition qnamespace.h:678
@ Key_Down
Definition qnamespace.h:680
@ Key_Menu
Definition qnamespace.h:727
@ Key_unknown
@ NoModifier
@ WindowDoesNotAcceptFocus
Definition qnamespace.h:236
@ WindowTransparentForInput
Definition qnamespace.h:234
@ ActiveWindowFocusReason
QString self
Definition language.cpp:58
float CGFloat
long NSInteger
#define Q_UNLIKELY(x)
#define qApp
DBusConnection const char DBusError DBusBusType DBusError return DBusConnection DBusHandleMessageFunction void DBusFreeFunction return DBusConnection return DBusConnection return const char DBusError return DBusConnection DBusMessage dbus_uint32_t return DBusConnection dbus_bool_t DBusConnection DBusAddWatchFunction DBusRemoveWatchFunction DBusWatchToggledFunction void DBusFreeFunction return DBusConnection DBusDispatchStatusFunction void DBusFreeFunction DBusTimeout return DBusTimeout return DBusWatch return DBusWatch unsigned int return DBusError const DBusError return const DBusMessage return DBusMessage return DBusMessage return DBusMessage return DBusMessage return DBusMessage return DBusMessageIter int const void return DBusMessageIter DBusMessageIter return DBusMessageIter void DBusMessageIter void int return DBusMessage DBusMessageIter return DBusMessageIter return DBusMessageIter DBusMessageIter const char const char const char const char return DBusMessage return DBusMessage const char return DBusMessage dbus_bool_t return DBusMessage dbus_uint32_t return DBusMessage void
EGLOutputLayerEXT layer
#define qGuiApp
bool isQtApplication()
Definition qiosglobal.mm:20
#define qImDebug
Definition qiosglobal.h:20
#define qWarning
Definition qlogging.h:166
#define Q_LOGGING_CATEGORY(name,...)
#define qCInfo(category,...)
#define qCDebug(category,...)
constexpr float qRadiansToDegrees(float radians)
Definition qmath.h:281
constexpr const T & qBound(const T &min, const T &val, const T &max)
Definition qminmax.h:44
GLint GLint GLint GLint GLint x
[0]
GLuint64 key
GLfloat GLfloat GLfloat w
[0]
GLenum GLuint id
[7]
GLenum GLenum GLsizei count
GLenum type
GLint y
struct _cl_event * event
GLuint in
GLfloat GLfloat p
[1]
GLenum GLenum GLenum input
#define Q_ASSERT(cond)
Definition qrandom.cpp:47
Q_CORE_EXPORT bool qEnvironmentVariableIsSet(const char *varName) noexcept
Q_CORE_EXPORT int qEnvironmentVariableIntValue(const char *varName, bool *ok=nullptr) noexcept
#define Q_UNUSED(x)
unsigned short quint16
Definition qtypes.h:48
unsigned long ulong
Definition qtypes.h:35
double qreal
Definition qtypes.h:187
CGPoint m_lastScrollDelta
Definition quiview.mm:60
UITouch * m_activePencilTouch
Definition quiview.mm:56
UIPanGestureRecognizer * m_scrollGestureRecognizer
Definition quiview.mm:58
NSMutableArray< UIAccessibilityElement * > * m_accessibleElements
Definition quiview.mm:57
#define colorWithBrightness(br)
CGPoint m_lastScrollCursorPos
Definition quiview.mm:59
Q_GUI_EXPORT QWindowPrivate * qt_window_private(QWindow *window)
Definition qwindow.cpp:2950
QFileSelector selector
[1]
QObject::connect nullptr
aWidget window() -> setWindowTitle("New Window Title")
[2]
QFrame frame
[0]
QQuickView * view
[0]
Definition moc.h:23