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