1/**************************************************************************** 2** 3** Copyright (C) 2016 The Qt Company Ltd. 4** Contact: https://www.qt.io/licensing/ 5** 6** This file is part of the plugins of the Qt Toolkit. 7** 8** $QT_BEGIN_LICENSE:LGPL$ 9** Commercial License Usage 10** Licensees holding valid commercial Qt licenses may use this file in 11** accordance with the commercial license agreement provided with the 12** Software or, alternatively, in accordance with the terms contained in 13** a written agreement between you and The Qt Company. For licensing terms 14** and conditions see https://www.qt.io/terms-conditions. For further 15** information use the contact form at https://www.qt.io/contact-us. 16** 17** GNU Lesser General Public License Usage 18** Alternatively, this file may be used under the terms of the GNU Lesser 19** General Public License version 3 as published by the Free Software 20** Foundation and appearing in the file LICENSE.LGPL3 included in the 21** packaging of this file. Please review the following information to 22** ensure the GNU Lesser General Public License version 3 requirements 23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. 24** 25** GNU General Public License Usage 26** Alternatively, this file may be used under the terms of the GNU 27** General Public License version 2.0 or (at your option) the GNU General 28** Public license version 3 or any later version approved by the KDE Free 29** Qt Foundation. The licenses are as published by the Free Software 30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 31** included in the packaging of this file. Please review the following 32** information to ensure the GNU General Public License requirements will 33** be met: https://www.gnu.org/licenses/gpl-2.0.html and 34** https://www.gnu.org/licenses/gpl-3.0.html. 35** 36** $QT_END_LICENSE$ 37** 38****************************************************************************/ 39 40#include "quiview.h" 41 42#include "qiosglobal.h" 43#include "qiosintegration.h" 44#include "qiosviewcontroller.h" 45#include "qiostextresponder.h" 46#include "qiosscreen.h" 47#include "qioswindow.h" 48#ifndef Q_OS_TVOS 49#include "qiosmenu.h" 50#endif 51 52#include <QtGui/private/qguiapplication_p.h> 53#include <QtGui/private/qwindow_p.h> 54#include <qpa/qwindowsysteminterface_p.h> 55 56Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet") 57 58@implementation QUIView { 59 QHash<NSUInteger, QWindowSystemInterface::TouchPoint> m_activeTouches; 60 UITouch *m_activePencilTouch; 61 int m_nextTouchId; 62 NSMutableArray<UIAccessibilityElement *> *m_accessibleElements; 63} 64 65+ (void)load 66{ 67#ifndef Q_OS_TVOS 68 if (QOperatingSystemVersion::current() < QOperatingSystemVersion(QOperatingSystemVersion::IOS, 11)) { 69 // iOS 11 handles this though [UIView safeAreaInsetsDidChange], but there's no signal for 70 // the corresponding top and bottom layout guides that we use on earlier versions. Note 71 // that we use the _will_ change version of the notification, because we want to react 72 // to the change as early was possible. But since the top and bottom layout guides have 73 // not been updated at this point we use asynchronous delivery of the event, so that the 74 // event is processed by QtGui just after iOS has updated the layout margins. 75 [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillChangeStatusBarFrameNotification 76 object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *) { 77 for (QWindow *window : QGuiApplication::allWindows()) 78 QWindowSystemInterface::handleSafeAreaMarginsChanged<QWindowSystemInterface::AsynchronousDelivery>(window); 79 } 80 ]; 81 } 82#endif 83} 84 85+ (Class)layerClass 86{ 87 return [CAEAGLLayer class]; 88} 89 90- (instancetype)initWithQIOSWindow:(QT_PREPEND_NAMESPACE(QIOSWindow) *)window 91{ 92 if (self = [self initWithFrame:window->geometry().toCGRect()]) { 93 self.platformWindow = window; 94 m_accessibleElements = [[NSMutableArray<UIAccessibilityElement *> alloc] init]; 95 } 96 97 return self; 98} 99 100- (instancetype)initWithFrame:(CGRect)frame 101{ 102 if ((self = [super initWithFrame:frame])) { 103 if ([self.layer isKindOfClass:[CAEAGLLayer class]]) { 104 // Set up EAGL layer 105 CAEAGLLayer *eaglLayer = static_cast<CAEAGLLayer *>(self.layer); 106 eaglLayer.opaque = TRUE; 107 eaglLayer.drawableProperties = @{ 108 kEAGLDrawablePropertyRetainedBacking: @(YES), 109 kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8 110 }; 111 } 112 113 if (isQtApplication()) 114 self.hidden = YES; 115 116#ifndef Q_OS_TVOS 117 self.multipleTouchEnabled = YES; 118#endif 119 120 if (qEnvironmentVariableIntValue("QT_IOS_DEBUG_WINDOW_MANAGEMENT")) { 121 static CGFloat hue = 0.0; 122 CGFloat lastHue = hue; 123 for (CGFloat diff = 0; diff < 0.1 || diff > 0.9; diff = fabs(hue - lastHue)) 124 hue = drand48(); 125 126 #define colorWithBrightness(br) \ 127 [UIColor colorWithHue:hue saturation:0.5 brightness:br alpha:1.0].CGColor 128 129 self.layer.borderColor = colorWithBrightness(1.0); 130 self.layer.borderWidth = 1.0; 131 } 132 133 if (qEnvironmentVariableIsSet("QT_IOS_DEBUG_WINDOW_SAFE_AREAS")) { 134 UIView *safeAreaOverlay = [[UIView alloc] initWithFrame:CGRectZero]; 135 [safeAreaOverlay setBackgroundColor:[UIColor colorWithRed:0.3 green:0.7 blue:0.9 alpha:0.3]]; 136 [self addSubview:safeAreaOverlay]; 137 138 safeAreaOverlay.translatesAutoresizingMaskIntoConstraints = NO; 139 [safeAreaOverlay.topAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.topAnchor].active = YES; 140 [safeAreaOverlay.leftAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.leftAnchor].active = YES; 141 [safeAreaOverlay.rightAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.rightAnchor].active = YES; 142 [safeAreaOverlay.bottomAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.bottomAnchor].active = YES; 143 } 144 } 145 146 return self; 147} 148 149- (void)dealloc 150{ 151 [m_accessibleElements release]; 152 153 [super dealloc]; 154} 155 156- (NSString *)description 157{ 158 NSMutableString *description = [NSMutableString stringWithString:[super description]]; 159 160#ifndef QT_NO_DEBUG_STREAM 161 QString platformWindowDescription; 162 QDebug debug(&platformWindowDescription); 163 debug.nospace() << "; " << self.platformWindow << ">"; 164 NSRange lastCharacter = [description rangeOfComposedCharacterSequenceAtIndex:description.length - 1]; 165 [description replaceCharactersInRange:lastCharacter withString:platformWindowDescription.toNSString()]; 166#endif 167 168 return description; 169} 170 171- (void)willMoveToWindow:(UIWindow *)newWindow 172{ 173 // UIKIt will normally set the scale factor of a view to match the corresponding 174 // screen scale factor, but views backed by CAEAGLLayers need to do this manually. 175 self.contentScaleFactor = newWindow && newWindow.screen ? 176 newWindow.screen.scale : [[UIScreen mainScreen] scale]; 177 178 // FIXME: Allow the scale factor to be customized through QSurfaceFormat. 179} 180 181- (void)didAddSubview:(UIView *)subview 182{ 183 if ([subview isKindOfClass:[QUIView class]]) 184 self.clipsToBounds = YES; 185} 186 187- (void)willRemoveSubview:(UIView *)subview 188{ 189 for (UIView *view in self.subviews) { 190 if (view != subview && [view isKindOfClass:[QUIView class]]) 191 return; 192 } 193 194 self.clipsToBounds = NO; 195} 196 197- (void)setNeedsDisplay 198{ 199 [super setNeedsDisplay]; 200 201 // We didn't implement drawRect: so we have to manually 202 // mark the layer as needing display. 203 [self.layer setNeedsDisplay]; 204} 205 206- (void)layoutSubviews 207{ 208 // This method is the de facto way to know that view has been resized, 209 // or otherwise needs invalidation of its buffers. Note though that we 210 // do not get this callback when the view just changes its position, so 211 // the position of our QWindow (and platform window) will only get updated 212 // when the size is also changed. 213 214 if (!CGAffineTransformIsIdentity(self.transform)) 215 qWarning() << self << "has a transform set. This is not supported."; 216 217 QWindow *window = self.platformWindow->window(); 218 QRect lastReportedGeometry = qt_window_private(window)->geometry; 219 QRect currentGeometry = QRectF::fromCGRect(self.frame).toRect(); 220 qCDebug(lcQpaWindow) << self.platformWindow << "new geometry is" << currentGeometry; 221 QWindowSystemInterface::handleGeometryChange(window, currentGeometry); 222 223 if (currentGeometry.size() != lastReportedGeometry.size()) { 224 // Trigger expose event on resize 225 [self setNeedsDisplay]; 226 227 // A new size means we also need to resize the FBO's corresponding buffers, 228 // but we defer that to when the application calls makeCurrent. 229 } 230} 231 232- (void)displayLayer:(CALayer *)layer 233{ 234 Q_UNUSED(layer); 235 Q_ASSERT(layer == self.layer); 236 237 [self sendUpdatedExposeEvent]; 238} 239 240- (void)sendUpdatedExposeEvent 241{ 242 QRegion region; 243 244 if (self.platformWindow->isExposed()) { 245 QSize bounds = QRectF::fromCGRect(self.layer.bounds).toRect().size(); 246 247 Q_ASSERT(self.platformWindow->geometry().size() == bounds); 248 Q_ASSERT(self.hidden == !self.platformWindow->window()->isVisible()); 249 250 region = QRect(QPoint(), bounds); 251 } 252 253 qCDebug(lcQpaWindow) << self.platformWindow << region << "isExposed" << self.platformWindow->isExposed(); 254 QWindowSystemInterface::handleExposeEvent(self.platformWindow->window(), region); 255} 256 257- (void)safeAreaInsetsDidChange 258{ 259 QWindowSystemInterface::handleSafeAreaMarginsChanged(self.platformWindow->window()); 260} 261 262// ------------------------------------------------------------------------- 263 264- (BOOL)canBecomeFirstResponder 265{ 266 return !(self.platformWindow->window()->flags() & Qt::WindowDoesNotAcceptFocus); 267} 268 269- (BOOL)becomeFirstResponder 270{ 271 { 272 // Scope for the duration of becoming first responder only, as the window 273 // activation event may trigger new responders, which we don't want to be 274 // blocked by this guard. 275 FirstResponderCandidate firstResponderCandidate(self); 276 277 qImDebug() << "self:" << self << "first:" << [UIResponder currentFirstResponder]; 278 279 if (![super becomeFirstResponder]) { 280 qImDebug() << self << "was not allowed to become first responder"; 281 return NO; 282 } 283 284 qImDebug() << self << "became first responder"; 285 } 286 287 if (qGuiApp->focusWindow() != self.platformWindow->window()) 288 QWindowSystemInterface::handleWindowActivated(self.platformWindow->window()); 289 else 290 qImDebug() << self.platformWindow->window() << "already active, not sending window activation"; 291 292 return YES; 293} 294 295- (BOOL)responderShouldTriggerWindowDeactivation:(UIResponder *)responder 296{ 297 // We don't want to send window deactivation in case the resign 298 // was a result of another Qt window becoming first responder. 299 if ([responder isKindOfClass:[QUIView class]]) 300 return NO; 301 302 // Nor do we want to deactivate the Qt window if the new responder 303 // is temporarily handling text input on behalf of a Qt window. 304 if ([responder isKindOfClass:[QIOSTextInputResponder class]]) { 305 while ((responder = [responder nextResponder])) { 306 if ([responder isKindOfClass:[QUIView class]]) 307 return NO; 308 } 309 } 310 311 return YES; 312} 313 314- (BOOL)resignFirstResponder 315{ 316 qImDebug() << "self:" << self << "first:" << [UIResponder currentFirstResponder]; 317 318 if (![super resignFirstResponder]) 319 return NO; 320 321 qImDebug() << self << "resigned first responder"; 322 323 UIResponder *newResponder = FirstResponderCandidate::currentCandidate(); 324 if ([self responderShouldTriggerWindowDeactivation:newResponder]) 325 QWindowSystemInterface::handleWindowActivated(0); 326 327 return YES; 328} 329 330- (BOOL)isActiveWindow 331{ 332 // Normally this is determined exclusivly by being firstResponder, but 333 // since we employ a separate first responder for text input we need to 334 // handle both cases as this view being the active Qt window. 335 336 if ([self isFirstResponder]) 337 return YES; 338 339 UIResponder *firstResponder = [UIResponder currentFirstResponder]; 340 if ([firstResponder isKindOfClass:[QIOSTextInputResponder class]] 341 && [firstResponder nextResponder] == self) 342 return YES; 343 344 return NO; 345} 346 347// ------------------------------------------------------------------------- 348 349- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection 350{ 351 [super traitCollectionDidChange: previousTraitCollection]; 352 353 QTouchDevice *touchDevice = QIOSIntegration::instance()->touchDevice(); 354 QTouchDevice::Capabilities touchCapabilities = touchDevice->capabilities(); 355 356 if (self.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable) 357 touchCapabilities |= QTouchDevice::Pressure; 358 else 359 touchCapabilities &= ~QTouchDevice::Pressure; 360 361 touchDevice->setCapabilities(touchCapabilities); 362} 363 364-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event 365{ 366 if (self.platformWindow->window()->flags() & Qt::WindowTransparentForInput) 367 return NO; 368 return [super pointInside:point withEvent:event]; 369} 370 371- (void)handleTouches:(NSSet *)touches withEvent:(UIEvent *)event withState:(Qt::TouchPointState)state withTimestamp:(ulong)timeStamp 372{ 373 QIOSIntegration *iosIntegration = QIOSIntegration::instance(); 374 bool supportsPressure = QIOSIntegration::instance()->touchDevice()->capabilities() & QTouchDevice::Pressure; 375 376#if QT_CONFIG(tabletevent) 377 if (m_activePencilTouch && [touches containsObject:m_activePencilTouch]) { 378 NSArray<UITouch *> *cTouches = [event coalescedTouchesForTouch:m_activePencilTouch]; 379 int i = 0; 380 for (UITouch *cTouch in cTouches) { 381 QPointF localViewPosition = QPointF::fromCGPoint([cTouch preciseLocationInView:self]); 382 QPoint localViewPositionI = localViewPosition.toPoint(); 383 QPointF globalScreenPosition = self.platformWindow->mapToGlobal(localViewPositionI) + 384 (localViewPosition - localViewPositionI); 385 qreal pressure = cTouch.force / cTouch.maximumPossibleForce; 386 // azimuth unit vector: +x to the right, +y going downwards 387 CGVector azimuth = [cTouch azimuthUnitVectorInView: self]; 388 // azimuthAngle given in radians, zero when the stylus points towards +x axis; converted to degrees with 0 pointing straight up 389 qreal azimuthAngle = [cTouch azimuthAngleInView: self] * 180 / M_PI + 90; 390 // altitudeAngle given in radians, pi / 2 is with the stylus perpendicular to the iPad, smaller values mean more tilted, but never negative. 391 // Convert to degrees with zero being perpendicular. 392 qreal altitudeAngle = 90 - cTouch.altitudeAngle * 180 / M_PI; 393 qCDebug(lcQpaTablet) << i << ":" << timeStamp << localViewPosition << pressure << state << "azimuth" << azimuth.dx << azimuth.dy 394 << "angle" << azimuthAngle << "altitude" << cTouch.altitudeAngle 395 << "xTilt" << qBound(-60.0, altitudeAngle * azimuth.dx, 60.0) << "yTilt" << qBound(-60.0, altitudeAngle * azimuth.dy, 60.0); 396 QWindowSystemInterface::handleTabletEvent(self.platformWindow->window(), timeStamp, localViewPosition, globalScreenPosition, 397 // device, pointerType, buttons 398 QTabletEvent::RotationStylus, QTabletEvent::Pen, state == Qt::TouchPointReleased ? Qt::NoButton : Qt::LeftButton, 399 // pressure, xTilt, yTilt 400 pressure, qBound(-60.0, altitudeAngle * azimuth.dx, 60.0), qBound(-60.0, altitudeAngle * azimuth.dy, 60.0), 401 // tangentialPressure, rotation, z, uid, modifiers 402 0, azimuthAngle, 0, 0, Qt::NoModifier); 403 ++i; 404 } 405 } 406#endif 407 408 if (m_activeTouches.isEmpty()) 409 return; 410 for (auto it = m_activeTouches.begin(); it != m_activeTouches.end(); ++it) { 411 auto hash = it.key(); 412 QWindowSystemInterface::TouchPoint &touchPoint = it.value(); 413 UITouch *uiTouch = nil; 414 for (UITouch *touch in touches) { 415 if (touch.hash == hash) { 416 uiTouch = touch; 417 break; 418 } 419 } 420 if (!uiTouch) { 421 touchPoint.state = Qt::TouchPointStationary; 422 } else { 423 touchPoint.state = state; 424 425 // Touch positions are expected to be in QScreen global coordinates, and 426 // as we already have the QWindow positioned at the right place, we can 427 // just map from the local view position to global coordinates. 428 // tvOS: all touches start at the center of the screen and move from there. 429 QPoint localViewPosition = QPointF::fromCGPoint([uiTouch locationInView:self]).toPoint(); 430 QPoint globalScreenPosition = self.platformWindow->mapToGlobal(localViewPosition); 431 432 touchPoint.area = QRectF(globalScreenPosition, QSize(0, 0)); 433 434 // FIXME: Do we really need to support QTouchDevice::NormalizedPosition? 435 QSize screenSize = self.platformWindow->screen()->geometry().size(); 436 touchPoint.normalPosition = QPointF(globalScreenPosition.x() / screenSize.width(), 437 globalScreenPosition.y() / screenSize.height()); 438 439 if (supportsPressure) { 440 // Note: iOS will deliver touchesBegan with a touch force of 0, which 441 // we will reflect/propagate as a 0 pressure, but there is no clear 442 // alternative, as we don't want to wait for a touchedMoved before 443 // sending a touch press event to Qt, just to have a valid pressure. 444 touchPoint.pressure = uiTouch.force / uiTouch.maximumPossibleForce; 445 } else { 446 // We don't claim that our touch device supports QTouchDevice::Pressure, 447 // but fill in a meaningful value in case clients use it anyway. 448 touchPoint.pressure = (state == Qt::TouchPointReleased) ? 0.0 : 1.0; 449 } 450 } 451 } 452 453 if ([self.window isKindOfClass:[QUIWindow class]] && 454 !static_cast<QUIWindow *>(self.window).sendingEvent) { 455 // The event is likely delivered as part of delayed touch delivery, via 456 // _UIGestureEnvironmentSortAndSendDelayedTouches, due to one of the two 457 // _UISystemGestureGateGestureRecognizer instances on the top level window 458 // having its delaysTouchesBegan set to YES. During this delivery, it's not 459 // safe to spin up a recursive event loop, as our calling function is not 460 // reentrant, so any gestures used by the recursive code, e.g. a native 461 // alert dialog, will fail to recognize. To be on the safe side, we deliver 462 // the event asynchronously. 463 QWindowSystemInterface::handleTouchEvent<QWindowSystemInterface::AsynchronousDelivery>( 464 self.platformWindow->window(), timeStamp, iosIntegration->touchDevice(), m_activeTouches.values()); 465 } else { 466 QWindowSystemInterface::handleTouchEvent<QWindowSystemInterface::SynchronousDelivery>( 467 self.platformWindow->window(), timeStamp, iosIntegration->touchDevice(), m_activeTouches.values()); 468 } 469} 470 471- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 472{ 473 // UIKit generates [Began -> Moved -> Ended] event sequences for 474 // each touch point. Internally we keep a hashmap of active UITouch 475 // points to QWindowSystemInterface::TouchPoints, and assigns each TouchPoint 476 // an id for use by Qt. 477 for (UITouch *touch in touches) { 478#if QT_CONFIG(tabletevent) 479 if (touch.type == UITouchTypeStylus) { 480 if (Q_UNLIKELY(m_activePencilTouch)) { 481 qWarning("ignoring additional Pencil while first is still active"); 482 continue; 483 } 484 m_activePencilTouch = touch; 485 } else 486 { 487 Q_ASSERT(!m_activeTouches.contains(touch.hash)); 488#endif 489 m_activeTouches[touch.hash].id = m_nextTouchId++; 490#if QT_CONFIG(tabletevent) 491 } 492#endif 493 } 494 495 if (self.platformWindow->shouldAutoActivateWindow() && m_activeTouches.size() == 1) { 496 QPlatformWindow *topLevel = self.platformWindow; 497 while (QPlatformWindow *p = topLevel->parent()) 498 topLevel = p; 499 if (topLevel->window() != QGuiApplication::focusWindow()) 500 topLevel->requestActivateWindow(); 501 } 502 503 [self handleTouches:touches withEvent:event withState:Qt::TouchPointPressed withTimestamp:ulong(event.timestamp * 1000)]; 504} 505 506- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event 507{ 508 [self handleTouches:touches withEvent:event withState:Qt::TouchPointMoved withTimestamp:ulong(event.timestamp * 1000)]; 509} 510 511- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event 512{ 513 [self handleTouches:touches withEvent:event withState:Qt::TouchPointReleased withTimestamp:ulong(event.timestamp * 1000)]; 514 515 // Remove ended touch points from the active set: 516#ifndef Q_OS_TVOS 517 for (UITouch *touch in touches) { 518#if QT_CONFIG(tabletevent) 519 if (touch.type == UITouchTypeStylus) { 520 m_activePencilTouch = nil; 521 } else 522#endif 523 { 524 m_activeTouches.remove(touch.hash); 525 } 526 } 527#else 528 // tvOS only supports single touch 529 m_activeTouches.clear(); 530#endif 531 532 if (m_activeTouches.isEmpty() && !m_activePencilTouch) 533 m_nextTouchId = 0; 534} 535 536- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event 537{ 538 if (m_activeTouches.isEmpty() && !m_activePencilTouch) 539 return; 540 541 // When four-finger swiping, we get a touchesCancelled callback 542 // which includes all four touch points. The swipe gesture is 543 // then active until all four touches have been released, and 544 // we start getting touchesBegan events again. 545 546 // When five-finger pinching, we also get a touchesCancelled 547 // callback with all five touch points, but the pinch gesture 548 // ends when the second to last finger is released from the 549 // screen. The last finger will not emit any more touch 550 // events, _but_, will contribute to starting another pinch 551 // gesture. That second pinch gesture will _not_ trigger a 552 // touchesCancelled event when starting, but as each finger 553 // is released, and we may get touchesMoved events for the 554 // remaining fingers. [event allTouches] also contains one 555 // less touch point than it should, so this behavior is 556 // likely a bug in the iOS system gesture recognizer, but we 557 // have to take it into account when maintaining the Qt state. 558 // We do this by assuming that there are no cases where a 559 // sub-set of the active touch events are intentionally cancelled. 560 561 NSInteger count = static_cast<NSInteger>([touches count]); 562 if (count != 0 && count != m_activeTouches.count() && !m_activePencilTouch) 563 qWarning("Subset of active touches cancelled by UIKit"); 564 565 m_activeTouches.clear(); 566 m_nextTouchId = 0; 567 m_activePencilTouch = nil; 568 569 NSTimeInterval timestamp = event ? event.timestamp : [[NSProcessInfo processInfo] systemUptime]; 570 571 QIOSIntegration *iosIntegration = static_cast<QIOSIntegration *>(QGuiApplicationPrivate::platformIntegration()); 572 QWindowSystemInterface::handleTouchCancelEvent(self.platformWindow->window(), ulong(timestamp * 1000), iosIntegration->touchDevice()); 573} 574 575- (int)mapPressTypeToKey:(UIPress*)press 576{ 577 switch (press.type) { 578 case UIPressTypeUpArrow: return Qt::Key_Up; 579 case UIPressTypeDownArrow: return Qt::Key_Down; 580 case UIPressTypeLeftArrow: return Qt::Key_Left; 581 case UIPressTypeRightArrow: return Qt::Key_Right; 582 case UIPressTypeSelect: return Qt::Key_Select; 583 case UIPressTypeMenu: return Qt::Key_Menu; 584 case UIPressTypePlayPause: return Qt::Key_MediaTogglePlayPause; 585 } 586 return Qt::Key_unknown; 587} 588 589- (bool)processPresses:(NSSet *)presses withType:(QEvent::Type)type { 590 // Presses on Menu button will generate a Menu key event. By default, not handling 591 // this event will cause the application to return to Headboard (tvOS launcher). 592 // When handling the event (for example, as a back button), both press and 593 // release events must be handled accordingly. 594 595 bool handled = false; 596 for (UIPress* press in presses) { 597 int key = [self mapPressTypeToKey:press]; 598 if (key == Qt::Key_unknown) 599 continue; 600 if (QWindowSystemInterface::handleKeyEvent(self.platformWindow->window(), type, key, Qt::NoModifier)) 601 handled = true; 602 } 603 604 return handled; 605} 606 607- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event 608{ 609 if (![self processPresses:presses withType:QEvent::KeyPress]) 610 [super pressesBegan:presses withEvent:event]; 611} 612 613- (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event 614{ 615 if (![self processPresses:presses withType:QEvent::KeyPress]) 616 [super pressesChanged:presses withEvent:event]; 617} 618 619- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event 620{ 621 if (![self processPresses:presses withType:QEvent::KeyRelease]) 622 [super pressesEnded:presses withEvent:event]; 623} 624 625- (BOOL)canPerformAction:(SEL)action withSender:(id)sender 626{ 627#ifndef Q_OS_TVOS 628 // Check first if QIOSMenu should handle the action before continuing up the responder chain 629 return [QIOSMenu::menuActionTarget() targetForAction:action withSender:sender] != 0; 630#else 631 Q_UNUSED(action) 632 Q_UNUSED(sender) 633 return false; 634#endif 635} 636 637- (id)forwardingTargetForSelector:(SEL)selector 638{ 639 Q_UNUSED(selector) 640#ifndef Q_OS_TVOS 641 return QIOSMenu::menuActionTarget(); 642#else 643 return nil; 644#endif 645} 646 647- (void)addInteraction:(id<UIInteraction>)interaction 648{ 649 if ([NSStringFromClass(interaction.class) isEqualToString:@"UITextInteraction"]) 650 return; 651 652 [super addInteraction:interaction]; 653} 654 655@end 656 657@implementation UIView (QtHelpers) 658 659- (QWindow *)qwindow 660{ 661 if ([self isKindOfClass:[QUIView class]]) { 662 if (QT_PREPEND_NAMESPACE(QIOSWindow) *w = static_cast<QUIView *>(self).platformWindow) 663 return w->window(); 664 } 665 return nil; 666} 667 668- (UIViewController *)viewController 669{ 670 id responder = self; 671 while ((responder = [responder nextResponder])) { 672 if ([responder isKindOfClass:UIViewController.class]) 673 return responder; 674 } 675 return nil; 676} 677 678- (QIOSViewController*)qtViewController 679{ 680 UIViewController *vc = self.viewController; 681 if ([vc isKindOfClass:QIOSViewController.class]) 682 return static_cast<QIOSViewController *>(vc); 683 684 return nil; 685} 686 687- (UIEdgeInsets)qt_safeAreaInsets 688{ 689 return self.safeAreaInsets; 690} 691 692@end 693 694#ifdef Q_OS_IOS 695@implementation QUIMetalView 696 697+ (Class)layerClass 698{ 699#ifdef TARGET_IPHONE_SIMULATOR 700 if (@available(ios 13.0, *)) 701#endif 702 703 return [CAMetalLayer class]; 704 705#ifdef TARGET_IPHONE_SIMULATOR 706 return nil; 707#endif 708} 709 710@end 711#endif 712 713#ifndef QT_NO_ACCESSIBILITY 714// Include category as an alternative to using -ObjC (Apple QA1490) 715#include "quiview_accessibility.mm" 716#endif 717