19#include <QtCore/qmath.h>
20#include <QtGui/qpointingdevice.h>
21#include <QtGui/private/qguiapplication_p.h>
22#include <QtGui/private/qwindow_p.h>
23#include <QtGui/private/qapplekeymapper_p.h>
24#include <QtGui/private/qpointingdevice_p.h>
25#include <qpa/qwindowsysteminterface_p.h>
28Q_LOGGING_CATEGORY(lcQpaInputEvents,
"qt.qpa.input.events")
31inline ulong getTimeStamp(UIEvent *event)
33#if TARGET_OS_SIMULATOR == 1
46#if defined(Q_PROCESSOR_ARM)
47 #warning The timestamp work-around for x86_64 can (probably) be removed when building for ARM
49 return ulong(NSProcessInfo.processInfo.systemUptime * 1000);
52 return ulong(event.timestamp * 1000);
56@implementation QUIView {
57 QHash<NSUInteger, QWindowSystemInterface::TouchPoint> m_activeTouches;
58 UITouch *m_activePencilTouch;
59 NSMutableArray<UIAccessibilityElement *> *m_accessibleElements;
60 UIPanGestureRecognizer *m_scrollGestureRecognizer;
61 CGPoint m_lastScrollCursorPos;
62 CGPoint m_lastScrollDelta;
63#if QT_CONFIG(tabletevent)
64 UIHoverGestureRecognizer *m_hoverGestureRecognizer;
71 return [CAEAGLLayer
class];
73 return [super layerClass];
76- (instancetype)initWithQIOSWindow:(QT_PREPEND_NAMESPACE(QIOSWindow) *)window
78 if (self = [self initWithFrame:window->geometry().toCGRect()]) {
79 self.platformWindow = window;
81 if (isQtApplication())
84 m_accessibleElements = [[NSMutableArray<UIAccessibilityElement *> alloc] init];
87 self.multipleTouchEnabled = YES;
90 m_scrollGestureRecognizer = [[UIPanGestureRecognizer alloc]
92 action:@selector(handleScroll:)];
97 m_scrollGestureRecognizer.allowedTouchTypes = [NSArray array];
98 if (@available(ios 13.4, *)) {
99 m_scrollGestureRecognizer.allowedScrollTypesMask = UIScrollTypeMaskAll;
101 m_scrollGestureRecognizer.maximumNumberOfTouches = 0;
102 m_lastScrollDelta = CGPointZero;
103 m_lastScrollCursorPos = CGPointZero;
104 [self addGestureRecognizer:m_scrollGestureRecognizer];
106#if QT_CONFIG(tabletevent)
107 m_hoverGestureRecognizer = [[UIHoverGestureRecognizer alloc]
109 action:@selector(handleHover:)];
110 [self addGestureRecognizer:m_hoverGestureRecognizer];
114 if ([self.layer isKindOfClass:CAMetalLayer.
class]) {
115 QWindow *window = self.platformWindow->window();
116 if (QColorSpace colorSpace = window->format().colorSpace(); colorSpace.isValid()) {
117 QCFType<CFDataRef> iccData = colorSpace.iccProfile().toCFData();
118 QCFType<CGColorSpaceRef> cgColorSpace = CGColorSpaceCreateWithICCData(iccData);
119 CAMetalLayer *metalLayer =
static_cast<CAMetalLayer *>(self.layer);
120 metalLayer.colorspace = cgColorSpace;
121 qCDebug(lcQpaWindow) <<
"Set" << self <<
"color space to" << metalLayer.colorspace;
125 else if ([self.layer isKindOfClass:[CAEAGLLayer
class]]) {
126 CAEAGLLayer *eaglLayer =
static_cast<CAEAGLLayer *>(self.layer);
127 eaglLayer.opaque = TRUE;
128 eaglLayer.drawableProperties = @{
129 kEAGLDrawablePropertyRetainedBacking: @(YES),
130 kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8
135#if defined(Q_OS_VISIONOS)
140 self.contentScaleFactor = self.platformWindow->screen()->devicePixelRatio();
149 [m_accessibleElements release];
150 [m_scrollGestureRecognizer release];
157 NSMutableString *description = [NSMutableString stringWithString:[super description]];
159#ifndef QT_NO_DEBUG_STREAM
160 QString platformWindowDescription;
161 QDebug debug(&platformWindowDescription);
162 debug.nospace() <<
"; " << self.platformWindow <<
">";
163 NSRange lastCharacter = [description rangeOfComposedCharacterSequenceAtIndex:description.length - 1];
164 [description replaceCharactersInRange:lastCharacter withString:platformWindowDescription.toNSString()];
170#if !defined(Q_OS_VISIONOS)
171- (
void)willMoveToWindow:(UIWindow *)newWindow
175 self.contentScaleFactor = newWindow && newWindow.screen ?
176 newWindow.screen.scale : [[UIScreen mainScreen] scale];
182- (
void)didAddSubview:(UIView *)subview
184 if ([subview isKindOfClass:[QUIView
class]])
185 self.clipsToBounds = YES;
188- (
void)willRemoveSubview:(UIView *)subview
190 for (UIView *view in self.subviews) {
191 if (view != subview && [view isKindOfClass:[QUIView
class]])
195 self.clipsToBounds = NO;
198- (
void)setNeedsDisplay
200 [super setNeedsDisplay];
204 [self.layer setNeedsDisplay];
207- (
void)layoutSubviews
215 if (!CGAffineTransformIsIdentity(self.transform))
216 qWarning() << self <<
"has a transform set. This is not supported.";
218 QWindow *window = self.platformWindow->window();
219 QRect lastReportedGeometry = qt_window_private(window)->geometry;
220 QRect currentGeometry = QRectF::fromCGRect(self.frame).toRect();
221 qCDebug(lcQpaWindow) << self.platformWindow <<
"new geometry is" << currentGeometry;
222 QWindowSystemInterface::handleGeometryChange(window, currentGeometry);
224 if (currentGeometry.size() != lastReportedGeometry.size()) {
226 [self setNeedsDisplay];
233- (
void)displayLayer:(CALayer *)layer
236 Q_ASSERT(layer == self.layer);
238 if (!self.platformWindow)
241 [self sendUpdatedExposeEvent];
244- (
void)sendUpdatedExposeEvent
248 if (self.platformWindow->isExposed()) {
249 QSize bounds = QRectF::fromCGRect(self.layer.bounds).toRect().size();
251 Q_ASSERT(self.platformWindow->geometry().size() == bounds);
252 Q_ASSERT(self.hidden == !self.platformWindow->window()->isVisible());
254 region = QRect(QPoint(), bounds);
257 qCDebug(lcQpaWindow) << self.platformWindow << region <<
"isExposed" << self.platformWindow->isExposed();
258 QWindowSystemInterface::handleExposeEvent(self.platformWindow->window(), region);
261- (
void)safeAreaInsetsDidChange
263 QWindowSystemInterface::handleSafeAreaMarginsChanged(self.platformWindow->window());
268- (BOOL)canBecomeFirstResponder
270 return !(self.platformWindow->window()->flags() & (Qt::WindowDoesNotAcceptFocus
271 | Qt::WindowTransparentForInput));
274- (BOOL)becomeFirstResponder
280 FirstResponderCandidate firstResponderCandidate(self);
282 qImDebug() <<
"self:" << self <<
"first:" << [UIResponder qt_currentFirstResponder];
284 if (![super becomeFirstResponder]) {
285 qImDebug() << self <<
"was not allowed to become first responder";
289 qImDebug() << self <<
"became first responder";
292 if (qGuiApp->focusWindow() != self.platformWindow->window())
293 QWindowSystemInterface::handleFocusWindowChanged(self.platformWindow->window(), Qt::ActiveWindowFocusReason);
295 qImDebug() << self.platformWindow->window() <<
"already active, not sending window activation";
300- (BOOL)responderShouldTriggerWindowDeactivation:(UIResponder *)responder
304 if ([responder isKindOfClass:[QUIView
class]])
309 if ([responder isKindOfClass:[QIOSTextResponder
class]]) {
310 while ((responder = [responder nextResponder])) {
311 if ([responder isKindOfClass:[QUIView
class]])
319- (BOOL)resignFirstResponder
321 qImDebug() <<
"self:" << self <<
"first:" << [UIResponder qt_currentFirstResponder];
323 if (![super resignFirstResponder])
326 qImDebug() << self <<
"resigned first responder";
329 UIResponder *newResponder = FirstResponderCandidate::currentCandidate();
330 if ([self responderShouldTriggerWindowDeactivation:newResponder])
331 QWindowSystemInterface::handleFocusWindowChanged(
nullptr, Qt::ActiveWindowFocusReason);
337- (BOOL)isActiveWindow
343 if ([self isFirstResponder])
346 UIResponder *firstResponder = [UIResponder qt_currentFirstResponder];
347 if ([firstResponder isKindOfClass:[QIOSTextInputResponder
class]]
348 && [firstResponder nextResponder] == self)
356- (
void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
358 [super traitCollectionDidChange: previousTraitCollection];
360 QPointingDevice *touchDevice = QIOSIntegration::instance()->touchDevice();
361 auto *devicePriv = QPointingDevicePrivate::get(touchDevice);
363 auto capabilities = touchDevice->capabilities();
364 capabilities.setFlag(QPointingDevice::Capability::Pressure,
365 (self.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable));
366 devicePriv->setCapabilities(capabilities);
369-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
371 if (self.platformWindow->window()->flags() & Qt::WindowTransparentForInput)
373 return [super pointInside:point withEvent:event];
376#if QT_CONFIG(tabletevent)
377- (
void)handlePencilEventForLocationInView:(CGPoint)locationInView withState:(QEventPoint::State)state withTimestamp:(ulong)timeStamp
378 withForce:(CGFloat)force withMaximumPossibleForce:(CGFloat)maximumPossibleForce withZOffset:(CGFloat)zOffset
379 withAzimuthUnitVector:(CGVector)azimuth withAltitudeAngleRadian:(CGFloat)altitudeAngleRadian
381 QIOSIntegration *iosIntegration = QIOSIntegration::instance();
383 QPointF localViewPosition = QPointF::fromCGPoint(locationInView);
384 QPoint localViewPositionI = localViewPosition.toPoint();
385 QPointF globalScreenPosition = self.platformWindow->mapToGlobal(localViewPositionI) +
386 (localViewPosition - localViewPositionI);
388 if (force != 0 && maximumPossibleForce != 0)
389 pressure = force / maximumPossibleForce;
393 qreal altitudeAngle = 90 - qRadiansToDegrees(altitudeAngleRadian);
394 qreal xTilt = qBound(-60.0, altitudeAngle * azimuth.dx, 60.0);
395 qreal yTilt = qBound(-60.0, altitudeAngle * azimuth.dy, 60.0);
397 qCDebug(lcQpaTablet) <<
":" << timeStamp << localViewPosition << pressure << state <<
"azimuth" << azimuth.dx << azimuth.dy
398 <<
"altitude" << altitudeAngleRadian <<
"xTilt" << xTilt <<
"yTilt" << yTilt;
399 QWindowSystemInterface::handleTabletEvent(self.platformWindow->window(), timeStamp,
401 iosIntegration->pencilDevice(), localViewPosition, globalScreenPosition,
403 state == QEventPoint::State::Released ? Qt::NoButton : Qt::LeftButton,
405 pressure, xTilt, yTilt, 0, 0, zOffset, Qt::NoModifier);
409- (
void)handleTouches:(NSSet *)touches withEvent:(UIEvent *)event withState:(QEventPoint::State)state withTimestamp:(ulong)timeStamp
411 QIOSIntegration *iosIntegration = QIOSIntegration::instance();
412 bool supportsPressure = QIOSIntegration::instance()->touchDevice()->capabilities() & QPointingDevice::Capability::Pressure;
414#if QT_CONFIG(tabletevent)
415 if (m_activePencilTouch && [touches containsObject:m_activePencilTouch]) {
416 NSArray<UITouch *> *cTouches = [event coalescedTouchesForTouch:m_activePencilTouch];
417 for (UITouch *cTouch in cTouches) {
418 [self handlePencilEventForLocationInView:[cTouch preciseLocationInView:self] withState:state withTimestamp:timeStamp
419 withForce:cTouch.force withMaximumPossibleForce:cTouch.maximumPossibleForce withZOffset:0
420 withAzimuthUnitVector:[cTouch azimuthUnitVectorInView:self]
421 withAltitudeAngleRadian:cTouch.altitudeAngle];
426 if (m_activeTouches.isEmpty())
428 for (
auto it = m_activeTouches.begin(); it != m_activeTouches.end(); ++it) {
429 auto hash = it.key();
430 QWindowSystemInterface::TouchPoint &touchPoint = it.value();
431 UITouch *uiTouch = nil;
432 for (UITouch *touch in touches) {
433 if (touch.hash == hash) {
439 touchPoint.state = QEventPoint::State::Stationary;
441 touchPoint.state = state;
447 QPoint localViewPosition = QPointF::fromCGPoint([uiTouch locationInView:self]).toPoint();
448 QPoint globalScreenPosition = self.platformWindow->mapToGlobal(localViewPosition);
450 touchPoint.area = QRectF(globalScreenPosition, QSize(0, 0));
453 QSize screenSize = self.platformWindow->screen()->geometry().size();
454 touchPoint.normalPosition = QPointF(globalScreenPosition.x() / screenSize.width(),
455 globalScreenPosition.y() / screenSize.height());
457 if (supportsPressure) {
462 touchPoint.pressure = uiTouch.force / uiTouch.maximumPossibleForce;
466 touchPoint.pressure = (state == QEventPoint::State::Released) ? 0.0 : 1.0;
471 if ([self.window isKindOfClass:[QUIWindow
class]] &&
472 !
static_cast<QUIWindow *>(self.window).sendingEvent) {
481 QWindowSystemInterface::handleTouchEvent<QWindowSystemInterface::AsynchronousDelivery>(
482 self.platformWindow->window(), timeStamp, iosIntegration->touchDevice(), m_activeTouches.values());
487 QWindowSystemInterface::handleTouchEvent<QWindowSystemInterface::AsynchronousDelivery>(
488 self.platformWindow->window(), timeStamp, iosIntegration->touchDevice(), m_activeTouches.values());
492- (
void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
498 for (UITouch *touch in touches) {
499#if QT_CONFIG(tabletevent)
500 if (touch.type == UITouchTypeStylus) {
501 if (Q_UNLIKELY(m_activePencilTouch)) {
502 qWarning(
"ignoring additional Pencil while first is still active");
505 m_activePencilTouch = touch;
508 Q_ASSERT(!m_activeTouches.contains(touch.hash));
512 static quint16 nextTouchId = 0;
513 m_activeTouches[touch.hash].id = nextTouchId++;
514#if QT_CONFIG(tabletevent)
519 if (self.platformWindow->shouldAutoActivateWindow() && m_activeTouches.size() == 1) {
520 QPlatformWindow *topLevel = self.platformWindow;
521 while (QPlatformWindow *p = topLevel->parent())
523 if (topLevel->window() != QGuiApplication::focusWindow())
524 topLevel->requestActivateWindow();
527 [self handleTouches:touches withEvent:event withState:QEventPoint::State::Pressed withTimestamp:getTimeStamp(event)];
530- (
void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
532 [self handleTouches:touches withEvent:event withState:QEventPoint::State::Updated withTimestamp:getTimeStamp(event)];
535- (
void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
537 [self handleTouches:touches withEvent:event withState:QEventPoint::State::Released withTimestamp:getTimeStamp(event)];
541 for (UITouch *touch in touches) {
542#if QT_CONFIG(tabletevent)
543 if (touch.type == UITouchTypeStylus) {
544 m_activePencilTouch = nil;
548 m_activeTouches.remove(touch.hash);
553 m_activeTouches.clear();
557- (
void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
559 if (m_activeTouches.isEmpty() && !m_activePencilTouch)
582 NSInteger count =
static_cast<NSInteger>([touches count]);
583 if (count != 0 && count != m_activeTouches.count() && !m_activePencilTouch)
584 qWarning(
"Subset of active touches cancelled by UIKit");
586 m_activeTouches.clear();
587 m_activePencilTouch = nil;
589 ulong timestamp = event ? getTimeStamp(event) : ([[NSProcessInfo processInfo] systemUptime] * 1000);
591 QIOSIntegration *iosIntegration =
static_cast<QIOSIntegration *>(QGuiApplicationPrivate::platformIntegration());
596 QWindowSystemInterface::handleTouchCancelEvent<QWindowSystemInterface::AsynchronousDelivery>(
597 self.platformWindow->window(), timestamp, iosIntegration->touchDevice());
600- (
int)mapPressTypeToKey:(UIPress*)press withModifiers:(Qt::KeyboardModifiers)qtModifiers text:(QString &)text
602 switch (press.type) {
603 case UIPressTypeUpArrow:
return Qt::Key_Up;
604 case UIPressTypeDownArrow:
return Qt::Key_Down;
605 case UIPressTypeLeftArrow:
return Qt::Key_Left;
606 case UIPressTypeRightArrow:
return Qt::Key_Right;
607 case UIPressTypeSelect:
return Qt::Key_Select;
608 case UIPressTypeMenu:
return Qt::Key_Menu;
609 case UIPressTypePlayPause:
return Qt::Key_MediaTogglePlayPause;
611 if (@available(ios 13.4, *)) {
612 NSString *charactersIgnoringModifiers = press.key.charactersIgnoringModifiers;
613 Qt::Key key = QAppleKeyMapper::fromUIKitKey(charactersIgnoringModifiers);
614 if (key != Qt::Key_unknown)
616 return QAppleKeyMapper::fromNSString(qtModifiers, press.key.characters,
617 charactersIgnoringModifiers, text);
619 return Qt::Key_unknown;
622- (
bool)isControlKey:(Qt::Key)key
637- (
bool)handlePresses:(NSSet<UIPress *> *)presses eventType:(QEvent::Type)type
643 if (!qApp->focusWindow())
646 bool eventHandled =
false;
647 const bool imEnabled = QIOSInputContext::instance()->inputMethodAccepted();
649 for (UIPress* press in presses) {
650 Qt::KeyboardModifiers qtModifiers = Qt::NoModifier;
651 if (@available(ios 13.4, *))
652 qtModifiers = QAppleKeyMapper::fromUIKitModifiers(press.key.modifierFlags);
654 int key = [self mapPressTypeToKey:press withModifiers:qtModifiers text:text];
655 if (key == Qt::Key_unknown)
657 if (imEnabled && ![self isControlKey:Qt::Key(key)])
660 bool keyHandled = QWindowSystemInterface::handleKeyEvent(
661 self.platformWindow->window(), type, key, qtModifiers, text);
662 eventHandled = eventHandled || keyHandled;
668- (
void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event
670 if (![self handlePresses:presses eventType:QEvent::KeyPress])
671 [super pressesBegan:presses withEvent:event];
674- (
void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event
676 if (![self handlePresses:presses eventType:QEvent::KeyPress])
677 [super pressesChanged:presses withEvent:event];
678 [super pressesChanged:presses withEvent:event];
681- (
void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event
683 if (![self handlePresses:presses eventType:QEvent::KeyRelease])
684 [super pressesEnded:presses withEvent:event];
685 [super pressesEnded:presses withEvent:event];
688- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
690#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS)
692 return [QIOSMenu::menuActionTarget() targetForAction:action withSender:sender] != 0;
700- (
id)forwardingTargetForSelector:(SEL)selector
703#if !defined(Q_OS_TVOS) && !defined(Q_OS_VISIONOS)
704 return QIOSMenu::menuActionTarget();
710- (
void)addInteraction:(id<UIInteraction>)interaction
712 if ([NSStringFromClass(interaction.
class) isEqualToString:@
"UITextInteraction"])
715 [super addInteraction:interaction];
718- (UIEditingInteractionConfiguration)editingInteractionConfiguration
726 return QIOSInputContext::instance()->inputMethodAccepted() ?
727 UIEditingInteractionConfigurationDefault : UIEditingInteractionConfigurationNone;
730#if QT_CONFIG(wheelevent)
731- (
void)handleScroll:(UIPanGestureRecognizer *)recognizer
733 if (!self.platformWindow->window())
736 if (!self.canBecomeFirstResponder)
739 CGPoint translation = [recognizer translationInView:self];
740 CGFloat deltaX = translation.x - m_lastScrollDelta.x;
741 CGFloat deltaY = translation.y - m_lastScrollDelta.y;
749 const int pixelsToDegrees = 2;
750 angleDelta.setX(deltaX * pixelsToDegrees);
751 angleDelta.setY(deltaY * pixelsToDegrees);
754 pixelDelta.setX(deltaX);
755 pixelDelta.setY(deltaY);
757 NSTimeInterval time_stamp = [[NSProcessInfo processInfo] systemUptime];
758 ulong qt_timestamp = time_stamp * 1000;
760 Qt::KeyboardModifiers qt_modifierFlags = Qt::NoModifier;
761 if (@available(ios 13.4, *))
762 qt_modifierFlags = QAppleKeyMapper::fromUIKitModifiers(recognizer.modifierFlags);
764 if (recognizer.state == UIGestureRecognizerStateBegan)
768 m_lastScrollCursorPos = [recognizer locationInView:self];
770 if (recognizer.state != UIGestureRecognizerStateEnded) {
771 m_lastScrollDelta.x = translation.x;
772 m_lastScrollDelta.y = translation.y;
774 m_lastScrollDelta = CGPointZero;
777 QPoint qt_local = QPointF::fromCGPoint(m_lastScrollCursorPos).toPoint();
778 QPoint qt_global = self.platformWindow->mapToGlobal(qt_local);
780 qCInfo(lcQpaInputEvents).nospace() <<
"wheel event" <<
" at " << qt_local
781 <<
" pixelDelta=" << pixelDelta <<
" angleDelta=" << angleDelta;
783 QWindowSystemInterface::handleWheelEvent(self.platformWindow->window(), qt_timestamp, qt_local, qt_global, pixelDelta, angleDelta, qt_modifierFlags);
787#if QT_CONFIG(tabletevent)
788- (
void)handleHover:(UIHoverGestureRecognizer *)recognizer
790 if (!self.platformWindow)
793 ulong timeStamp = [[NSProcessInfo processInfo] systemUptime] * 1000;
796 if (@available(ios 16.1, *))
797 zOffset = [recognizer zOffset];
800 CGFloat altitudeAngleRadian = 0;
801 if (@available(ios 16.4, *)) {
802 azimuth = [recognizer azimuthUnitVectorInView:self];
803 altitudeAngleRadian = recognizer.altitudeAngle;
806 [self handlePencilEventForLocationInView:[recognizer locationInView:self] withState:QEventPoint::State::Released
807 withTimestamp:timeStamp withForce:0 withMaximumPossibleForce:0 withZOffset:zOffset
808 withAzimuthUnitVector:azimuth withAltitudeAngleRadian:altitudeAngleRadian];
814@implementation UIView (QtHelpers)
818 if ([self isKindOfClass:[QUIView
class]]) {
819 if (QT_PREPEND_NAMESPACE(QIOSWindow) *w =
static_cast<QUIView *>(self).platformWindow)
828 while ((responder = [responder nextResponder])) {
829 if ([responder isKindOfClass:UIViewController.
class])
835- (QIOSViewController*)qtViewController
837 UIViewController *vc = self.viewController;
838 if ([vc isKindOfClass:QIOSViewController.
class])
839 return static_cast<QIOSViewController *>(vc);
847@implementation QUIMetalView
851 return [CAMetalLayer
class];
857#if QT_CONFIG(accessibility)
859#include "quiview_accessibility.mm"
Q_FORWARD_DECLARE_OBJC_CLASS(NSString)
Q_LOGGING_CATEGORY(lcEventDispatcher, "qt.eventdispatcher")
Q_FORWARD_DECLARE_OBJC_CLASS(UIViewController)