diff options
Diffstat (limited to 'src/plugins/platforms/ios')
18 files changed, 699 insertions, 229 deletions
diff --git a/src/plugins/platforms/ios/qiosapplicationdelegate.h b/src/plugins/platforms/ios/qiosapplicationdelegate.h index bfe31af198..617b740d6e 100644 --- a/src/plugins/platforms/ios/qiosapplicationdelegate.h +++ b/src/plugins/platforms/ios/qiosapplicationdelegate.h @@ -47,6 +47,5 @@ @interface QIOSApplicationDelegate : UIResponder <UIApplicationDelegate> @property (strong, nonatomic) UIWindow *window; -@property (strong, nonatomic) QIOSViewController *qiosViewController; @end diff --git a/src/plugins/platforms/ios/qiosapplicationdelegate.mm b/src/plugins/platforms/ios/qiosapplicationdelegate.mm index e06d2b8840..cf702c82af 100644 --- a/src/plugins/platforms/ios/qiosapplicationdelegate.mm +++ b/src/plugins/platforms/ios/qiosapplicationdelegate.mm @@ -49,7 +49,6 @@ @implementation QIOSApplicationDelegate @synthesize window; -@synthesize qiosViewController; - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { @@ -57,21 +56,34 @@ Q_UNUSED(launchOptions); self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease]; - self.qiosViewController = [[[QIOSViewController alloc] init] autorelease]; - self.window.rootViewController = self.qiosViewController; + self.window.rootViewController = [[[QIOSViewController alloc] init] autorelease]; -#ifdef QT_DEBUG - self.window.backgroundColor = [UIColor cyanColor]; +#if QT_IOS_DEPLOYMENT_TARGET_BELOW(__IPHONE_7_0) + QSysInfo::MacVersion iosVersion = QSysInfo::MacintoshVersion; + + // We prefer to keep the root viewcontroller in fullscreen layout, so that + // we don't have to compensate for the viewcontroller position. This also + // gives us the same behavior on iOS 5/6 as on iOS 7, where full screen layout + // is the only way. + if (iosVersion < QSysInfo::MV_IOS_7_0) + self.window.rootViewController.wantsFullScreenLayout = YES; + + // Use translucent statusbar by default on iOS6 iPhones (unless the user changed + // the default in the Info.plist), so that windows placed under the stausbar are + // still visible, just like on iOS7. + if (iosVersion >= QSysInfo::MV_IOS_6_0 && iosVersion < QSysInfo::MV_IOS_7_0 + && [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone + && [UIApplication sharedApplication].statusBarStyle == UIStatusBarStyleDefault) + [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleBlackTranslucent]; #endif - [self.window makeKeyAndVisible]; + self.window.hidden = NO; return YES; } - (void)dealloc { - [qiosViewController release]; [window release]; [super dealloc]; } diff --git a/src/plugins/platforms/ios/qiosbackingstore.mm b/src/plugins/platforms/ios/qiosbackingstore.mm index 2dadc5672b..5ea5fbd8d1 100644 --- a/src/plugins/platforms/ios/qiosbackingstore.mm +++ b/src/plugins/platforms/ios/qiosbackingstore.mm @@ -56,6 +56,9 @@ QIOSBackingStore::QIOSBackingStore(QWindow *window) fmt.setDepthBufferSize(16); fmt.setStencilBufferSize(8); + // Needed to prevent QOpenGLContext::makeCurrent() from failing + window->setSurfaceType(QSurface::OpenGLSurface); + m_context->setFormat(fmt); m_context->setScreen(window->screen()); m_context->create(); @@ -69,9 +72,6 @@ QIOSBackingStore::~QIOSBackingStore() void QIOSBackingStore::beginPaint(const QRegion &) { - // Needed to prevent QOpenGLContext::makeCurrent() from failing - window()->setSurfaceType(QSurface::OpenGLSurface); - m_context->makeCurrent(window()); QIOSWindow *iosWindow = static_cast<QIOSWindow *>(window()->handle()); @@ -102,6 +102,8 @@ void QIOSBackingStore::flush(QWindow *window, const QRegion ®ion, const QPoin // the child window overlaps a sibling window that's draws using a separate QOpenGLContext. return; } + + m_context->makeCurrent(window); m_context->swapBuffers(window); } @@ -115,7 +117,7 @@ void QIOSBackingStore::resize(const QSize &size, const QRegion &staticContents) // backing store and always keep the paint device's size in sync with the // window size in beginPaint(). - if (size != window()->size()) + if (size != window()->size() && !window()->inherits("QWidgetWindow")) qWarning() << "QIOSBackingStore needs to have the same size as its window"; } diff --git a/src/plugins/platforms/ios/qioscontext.h b/src/plugins/platforms/ios/qioscontext.h index 082ec4794c..c48a0251a9 100644 --- a/src/plugins/platforms/ios/qioscontext.h +++ b/src/plugins/platforms/ios/qioscontext.h @@ -48,6 +48,8 @@ QT_BEGIN_NAMESPACE +class QIOSWindow; + class QIOSContext : public QObject, public QPlatformOpenGLContext { Q_OBJECT @@ -66,10 +68,14 @@ public: GLuint defaultFramebufferObject(QPlatformSurface *) const; QFunctionPointer getProcAddress(const QByteArray &procName); + bool isSharing() const Q_DECL_OVERRIDE; + bool isValid() const Q_DECL_OVERRIDE; + private Q_SLOTS: void windowDestroyed(QObject *object); private: + QIOSContext *m_sharedContext; EAGLContext *m_eaglContext; QSurfaceFormat m_format; @@ -83,7 +89,7 @@ private: static void deleteBuffers(const FramebufferObject &framebufferObject); - mutable QHash<QWindow *, FramebufferObject> m_framebufferObjects; + mutable QHash<QIOSWindow *, FramebufferObject> m_framebufferObjects; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/ios/qioscontext.mm b/src/plugins/platforms/ios/qioscontext.mm index 0c4bee1ef0..7310d2904f 100644 --- a/src/plugins/platforms/ios/qioscontext.mm +++ b/src/plugins/platforms/ios/qioscontext.mm @@ -51,7 +51,10 @@ QIOSContext::QIOSContext(QOpenGLContext *context) : QPlatformOpenGLContext() - , m_eaglContext([[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]) + , m_sharedContext(static_cast<QIOSContext *>(context->shareHandle())) + , m_eaglContext([[EAGLContext alloc] + initWithAPI:kEAGLRenderingAPIOpenGLES2 + sharegroup:m_sharedContext ? [m_sharedContext->m_eaglContext sharegroup] : nil]) , m_format(context->format()) { m_format.setRenderableType(QSurfaceFormat::OpenGLES); @@ -110,7 +113,7 @@ void QIOSContext::swapBuffers(QPlatformSurface *surface) { Q_ASSERT(surface && surface->surface()->surfaceType() == QSurface::OpenGLSurface); Q_ASSERT(surface->surface()->surfaceClass() == QSurface::Window); - QWindow *window = static_cast<QWindow *>(surface->surface()); + QIOSWindow *window = static_cast<QIOSWindow *>(surface); Q_ASSERT(m_framebufferObjects.contains(window)); [EAGLContext setCurrentContext:m_eaglContext]; @@ -121,7 +124,7 @@ void QIOSContext::swapBuffers(QPlatformSurface *surface) GLuint QIOSContext::defaultFramebufferObject(QPlatformSurface *surface) const { Q_ASSERT(surface && surface->surface()->surfaceClass() == QSurface::Window); - QWindow *window = static_cast<QWindow *>(surface->surface()); + QIOSWindow *window = static_cast<QIOSWindow *>(surface); FramebufferObject &framebufferObject = m_framebufferObjects[window]; @@ -152,8 +155,7 @@ GLuint QIOSContext::defaultFramebufferObject(QPlatformSurface *surface) const } // Ensure that the FBO's buffers match the size of the layer - QIOSWindow *platformWindow = static_cast<QIOSWindow *>(surface); - UIView *view = reinterpret_cast<UIView *>(platformWindow->winId()); + UIView *view = reinterpret_cast<UIView *>(window->winId()); CAEAGLLayer *layer = static_cast<CAEAGLLayer *>(view.layer); if (framebufferObject.renderbufferWidth != (layer.frame.size.width * layer.contentsScale) || framebufferObject.renderbufferHeight != (layer.frame.size.height * layer.contentsScale)) { @@ -188,7 +190,7 @@ GLuint QIOSContext::defaultFramebufferObject(QPlatformSurface *surface) const void QIOSContext::windowDestroyed(QObject *object) { - QWindow *window = static_cast<QWindow *>(object); + QIOSWindow *window = static_cast<QIOSWindow *>(object); if (m_framebufferObjects.contains(window)) { EAGLContext *originalContext = [EAGLContext currentContext]; [EAGLContext setCurrentContext:m_eaglContext]; @@ -203,5 +205,15 @@ QFunctionPointer QIOSContext::getProcAddress(const QByteArray& functionName) return QFunctionPointer(dlsym(RTLD_DEFAULT, functionName.constData())); } +bool QIOSContext::isValid() const +{ + return m_eaglContext; +} + +bool QIOSContext::isSharing() const +{ + return m_sharedContext; +} + #include "moc_qioscontext.cpp" diff --git a/src/plugins/platforms/ios/qioseventdispatcher.h b/src/plugins/platforms/ios/qioseventdispatcher.h index f2272ecd68..5caa7f5d2d 100644 --- a/src/plugins/platforms/ios/qioseventdispatcher.h +++ b/src/plugins/platforms/ios/qioseventdispatcher.h @@ -54,11 +54,9 @@ public: explicit QIOSEventDispatcher(QObject *parent = 0); bool processEvents(QEventLoop::ProcessEventsFlags flags) Q_DECL_OVERRIDE; - void interrupt() Q_DECL_OVERRIDE; void handleRunLoopExit(CFRunLoopActivity activity); - void checkIfEventLoopShouldExit(); void interruptEventLoopExec(); private: diff --git a/src/plugins/platforms/ios/qioseventdispatcher.mm b/src/plugins/platforms/ios/qioseventdispatcher.mm index 3dd9c7ad9f..51eb10d385 100644 --- a/src/plugins/platforms/ios/qioseventdispatcher.mm +++ b/src/plugins/platforms/ios/qioseventdispatcher.mm @@ -446,6 +446,8 @@ bool __attribute__((returns_twice)) QIOSEventDispatcher::processEvents(QEventLoo if (!m_processEventCallsAfterExec && (flags & QEventLoop::EventLoopExec)) { ++m_processEventCallsAfterExec; + m_runLoopExitObserver.addToMode(kCFRunLoopCommonModes); + // We set a new jump point here that we can return to when the event loop // is asked to exit, so that we can return from QEventLoop::exec(). switch (setjmp(processEventExitJumpPoint)) { @@ -475,44 +477,18 @@ bool __attribute__((returns_twice)) QIOSEventDispatcher::processEvents(QEventLoo if (m_processEventCallsAfterExec) --m_processEventCallsAfterExec; - // If we're running with nested event loops and the application is quit, - // then the forwarded interrupt call will happen while our processEvent - // counter is still 2, and we won't detect that we're about to fall down - // to the root iOS run-loop. We do an extra check here to catch that case. - checkIfEventLoopShouldExit(); - return processedEvents; } -void QIOSEventDispatcher::interrupt() -{ - QEventDispatcherCoreFoundation::interrupt(); - - if (!rootLevelRunLoopIntegration()) - return; - - // If an interrupt happens as part of a non-nested event loop, that is, - // by processing an event or timer in the root iOS run-loop, we'll be - // able to detect it here. - checkIfEventLoopShouldExit(); -} - -void QIOSEventDispatcher::checkIfEventLoopShouldExit() -{ - if (m_processEventCallsAfterExec == 1) { - qEventDispatcherDebug() << "Hit root runloop level, watching for runloop exit"; - m_runLoopExitObserver.addToMode(kCFRunLoopCommonModes); - } -} - void QIOSEventDispatcher::handleRunLoopExit(CFRunLoopActivity activity) { Q_UNUSED(activity); Q_ASSERT(activity == kCFRunLoopExit); - m_runLoopExitObserver.removeFromMode(kCFRunLoopCommonModes); - - interruptEventLoopExec(); + if (m_processEventCallsAfterExec == 1 && !QThreadData::current()->eventLoops.top()->isRunning()) { + qEventDispatcherDebug() << "Root runloop level exited"; + interruptEventLoopExec(); + } } void QIOSEventDispatcher::interruptEventLoopExec() @@ -521,6 +497,8 @@ void QIOSEventDispatcher::interruptEventLoopExec() --m_processEventCallsAfterExec; + m_runLoopExitObserver.removeFromMode(kCFRunLoopCommonModes); + // We re-set applicationProcessEventsReturnPoint here so that future // calls to QEventLoop::exec() will end up back here after entering // processEvents, instead of back in didFinishLaunchingWithOptions. diff --git a/src/plugins/platforms/ios/qiosglobal.h b/src/plugins/platforms/ios/qiosglobal.h index fd328c9171..1c76d29389 100644 --- a/src/plugins/platforms/ios/qiosglobal.h +++ b/src/plugins/platforms/ios/qiosglobal.h @@ -52,12 +52,12 @@ QT_BEGIN_NAMESPACE class QPlatformScreen; bool isQtApplication(); -QIOSViewController *qiosViewController(); -CGRect toCGRect(const QRect &rect); -QRect fromCGRect(const CGRect &rect); -CGPoint toCGPoint(const QPoint &point); -QPoint fromCGPoint(const CGPoint &point); +CGRect toCGRect(const QRectF &rect); +QRectF fromCGRect(const CGRect &rect); +CGPoint toCGPoint(const QPointF &point); +QPointF fromCGPoint(const CGPoint &point); + Qt::ScreenOrientation toQtScreenOrientation(UIDeviceOrientation uiDeviceOrientation); UIDeviceOrientation fromQtScreenOrientation(Qt::ScreenOrientation qtOrientation); QRect fromPortraitToPrimary(const QRect &rect, QPlatformScreen *screen); diff --git a/src/plugins/platforms/ios/qiosglobal.mm b/src/plugins/platforms/ios/qiosglobal.mm index 9b8462a6cc..d749b8f514 100644 --- a/src/plugins/platforms/ios/qiosglobal.mm +++ b/src/plugins/platforms/ios/qiosglobal.mm @@ -58,35 +58,24 @@ bool isQtApplication() return isQt; } -QIOSViewController *qiosViewController() -{ - // If Qt controls the application, we have created a root view controller were we place top-level - // QWindows. Note that in a mixed native application, our view controller might later be removed or - // added as a child of another controller. To protect against that, we keep an explicit pointer to the - // view controller in cases where this is the controller we need to access. - static QIOSViewController *c = isQtApplication() ? - static_cast<QIOSApplicationDelegate *>([UIApplication sharedApplication].delegate).qiosViewController : nil; - return c; -} - -CGRect toCGRect(const QRect &rect) +CGRect toCGRect(const QRectF &rect) { return CGRectMake(rect.x(), rect.y(), rect.width(), rect.height()); } -QRect fromCGRect(const CGRect &rect) +QRectF fromCGRect(const CGRect &rect) { - return QRect(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height); + return QRectF(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height); } -CGPoint toCGPoint(const QPoint &point) +CGPoint toCGPoint(const QPointF &point) { return CGPointMake(point.x(), point.y()); } -QPoint fromCGPoint(const CGPoint &point) +QPointF fromCGPoint(const CGPoint &point) { - return QPoint(point.x, point.y); + return QPointF(point.x, point.y); } Qt::ScreenOrientation toQtScreenOrientation(UIDeviceOrientation uiDeviceOrientation) diff --git a/src/plugins/platforms/ios/qiosinputcontext.h b/src/plugins/platforms/ios/qiosinputcontext.h index 78c1b260e6..533ba686e1 100644 --- a/src/plugins/platforms/ios/qiosinputcontext.h +++ b/src/plugins/platforms/ios/qiosinputcontext.h @@ -44,6 +44,7 @@ #include <UIKit/UIKit.h> +#include <QtGui/qtransform.h> #include <qpa/qplatforminputcontext.h> QT_BEGIN_NAMESPACE @@ -60,13 +61,17 @@ public: void showInputPanel(); void hideInputPanel(); bool isInputPanelVisible() const; + void setFocusObject(QObject *object); - void focusViewChanged(UIView *view); + void focusWindowChanged(QWindow *focusWindow); + void scrollRootView(); private: QIOSKeyboardListener *m_keyboardListener; - UIView *m_focusView; + UIView<UIKeyInput> *m_focusView; + QTransform m_inputItemTransform; bool m_hasPendingHideRequest; + bool m_inSetFocusObject; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/ios/qiosinputcontext.mm b/src/plugins/platforms/ios/qiosinputcontext.mm index d430589037..0e43429015 100644 --- a/src/plugins/platforms/ios/qiosinputcontext.mm +++ b/src/plugins/platforms/ios/qiosinputcontext.mm @@ -48,7 +48,12 @@ @public QIOSInputContext *m_context; BOOL m_keyboardVisible; + BOOL m_keyboardVisibleAndDocked; QRectF m_keyboardRect; + QRectF m_keyboardEndRect; + NSTimeInterval m_duration; + UIViewAnimationCurve m_curve; + UIViewController *m_viewController; } @end @@ -60,8 +65,30 @@ if (self) { m_context = context; m_keyboardVisible = NO; - // After the keyboard became undockable (iOS5), UIKeyboardWillShow/UIKeyboardWillHide - // no longer works for all cases. So listen to keyboard frame changes instead: + m_keyboardVisibleAndDocked = NO; + m_duration = 0; + m_curve = UIViewAnimationCurveEaseOut; + m_viewController = 0; + + if (isQtApplication()) { + // Get the root view controller that is on the same screen as the keyboard: + for (UIWindow *uiWindow in [[UIApplication sharedApplication] windows]) { + if (uiWindow.screen == [UIScreen mainScreen]) { + m_viewController = [uiWindow.rootViewController retain]; + break; + } + } + Q_ASSERT(m_viewController); + } + + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(keyboardWillShow:) + name:@"UIKeyboardWillShowNotification" object:nil]; + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(keyboardWillHide:) + name:@"UIKeyboardWillHideNotification" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidChangeFrame:) @@ -72,25 +99,68 @@ - (void) dealloc { + [m_viewController release]; + [[NSNotificationCenter defaultCenter] + removeObserver:self + name:@"UIKeyboardWillShowNotification" object:nil]; + [[NSNotificationCenter defaultCenter] + removeObserver:self + name:@"UIKeyboardWillHideNotification" object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:@"UIKeyboardDidChangeFrameNotification" object:nil]; [super dealloc]; } -- (void) keyboardDidChangeFrame:(NSNotification *)notification +- (QRectF) getKeyboardRect:(NSNotification *)notification { - CGRect frame; - [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] getValue:&frame]; + // For Qt applications we rotate the keyboard rect to align with the screen + // orientation (which is the interface orientation of the root view controller). + // For hybrid apps we follow native behavior, and return the rect unmodified: + CGRect keyboardFrame = [[notification userInfo][UIKeyboardFrameEndUserInfoKey] CGRectValue]; + if (isQtApplication()) { + UIView *view = m_viewController.view; + return fromCGRect(CGRectOffset([view convertRect:keyboardFrame fromView:view.window], 0, -view.bounds.origin.y)); + } else { + return fromCGRect(keyboardFrame); + } +} - m_keyboardRect = fromPortraitToPrimary(fromCGRect(frame), QGuiApplication::primaryScreen()->handle()); +- (void) keyboardDidChangeFrame:(NSNotification *)notification +{ + m_keyboardRect = [self getKeyboardRect:notification]; m_context->emitKeyboardRectChanged(); - BOOL visible = CGRectIntersectsRect(frame, [UIScreen mainScreen].bounds); + BOOL visible = m_keyboardRect.intersects(fromCGRect([UIScreen mainScreen].bounds)); if (m_keyboardVisible != visible) { m_keyboardVisible = visible; m_context->emitInputPanelVisibleChanged(); } + + // If the keyboard was visible and docked from before, this is just a geometry + // change (normally caused by an orientation change). In that case, update scroll: + if (m_keyboardVisibleAndDocked) + m_context->scrollRootView(); +} + +- (void) keyboardWillShow:(NSNotification *)notification +{ + // Note that UIKeyboardWillShowNotification is only sendt when the keyboard is docked. + m_keyboardVisibleAndDocked = YES; + m_keyboardEndRect = [self getKeyboardRect:notification]; + if (!m_duration) { + m_duration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + m_curve = [notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue] << 16; + } + m_context->scrollRootView(); +} + +- (void) keyboardWillHide:(NSNotification *)notification +{ + // Note that UIKeyboardWillHideNotification is also sendt when the keyboard is undocked. + m_keyboardVisibleAndDocked = NO; + m_keyboardEndRect = [self getKeyboardRect:notification]; + m_context->scrollRootView(); } @end @@ -100,7 +170,11 @@ QIOSInputContext::QIOSInputContext() , m_keyboardListener([[QIOSKeyboardListener alloc] initWithQIOSInputContext:this]) , m_focusView(0) , m_hasPendingHideRequest(false) + , m_inSetFocusObject(false) { + if (isQtApplication()) + connect(qGuiApp->inputMethod(), &QInputMethod::cursorRectangleChanged, this, &QIOSInputContext::scrollRootView); + connect(qGuiApp, &QGuiApplication::focusWindowChanged, this, &QIOSInputContext::focusWindowChanged); } QIOSInputContext::~QIOSInputContext() @@ -142,10 +216,76 @@ bool QIOSInputContext::isInputPanelVisible() const return m_keyboardListener->m_keyboardVisible; } -void QIOSInputContext::focusViewChanged(UIView *view) +void QIOSInputContext::setFocusObject(QObject *) +{ + m_inputItemTransform = qApp->inputMethod()->inputItemTransform(); + + if (!m_focusView || !m_focusView.isFirstResponder) + return; + + // Since m_focusView is the first responder, it means that the keyboard is open and we + // should update keyboard layout. But there seem to be no way to tell it to reread the + // UITextInputTraits from m_focusView. To work around that, we quickly resign first + // responder status just to reassign it again. To not remove the focusObject in the same + // go, we need to call the super implementation of resignFirstResponder. Since the call + // will cause a 'keyboardWillHide' notification to be sendt, we also block scrollRootView + // to avoid artifacts: + m_inSetFocusObject = true; + SEL sel = @selector(resignFirstResponder); + [[m_focusView superclass] instanceMethodForSelector:sel](m_focusView, sel); + m_inSetFocusObject = false; + [m_focusView becomeFirstResponder]; +} + +void QIOSInputContext::focusWindowChanged(QWindow *focusWindow) { + UIView<UIKeyInput> *view = reinterpret_cast<UIView<UIKeyInput> *>(focusWindow->handle()->winId()); if ([m_focusView isFirstResponder]) [view becomeFirstResponder]; [m_focusView release]; m_focusView = [view retain]; } + +void QIOSInputContext::scrollRootView() +{ + // Scroll the root view (screen) if: + // - our backend controls the root view controller on the main screen (no hybrid app) + // - the focus object is on the same screen as the keyboard. + // - the first responder is a QUIView, and not some other foreign UIView. + // - the keyboard is docked. Otherwise the user can move the keyboard instead. + // - the inputItem has not been moved/scrolled + if (!isQtApplication() || !m_focusView || m_inSetFocusObject) + return; + + if (m_inputItemTransform != qApp->inputMethod()->inputItemTransform()) { + // The inputItem has moved since the last scroll update. To avoid competing + // with the application where the cursor/inputItem should be, we bail: + return; + } + + UIView *view = m_keyboardListener->m_viewController.view; + qreal scrollTo = 0; + + if (m_focusView.isFirstResponder + && m_keyboardListener->m_keyboardVisibleAndDocked + && m_focusView.window == view.window) { + QRectF cursorRect = qGuiApp->inputMethod()->cursorRectangle(); + cursorRect.translate(qGuiApp->focusWindow()->geometry().topLeft()); + qreal keyboardY = m_keyboardListener->m_keyboardEndRect.y(); + int statusBarY = qGuiApp->primaryScreen()->availableGeometry().y(); + const int margin = 20; + + if (cursorRect.bottomLeft().y() > keyboardY - margin) + scrollTo = qMin(view.bounds.size.height - keyboardY, cursorRect.y() - statusBarY - margin); + } + + if (scrollTo != view.bounds.origin.y) { + // Scroll the view the same way a UIScrollView works: by changing bounds.origin: + CGRect newBounds = view.bounds; + newBounds.origin.y = scrollTo; + [UIView animateWithDuration:m_keyboardListener->m_duration delay:0 + options:m_keyboardListener->m_curve + animations:^{ view.bounds = newBounds; } + completion:0]; + } +} diff --git a/src/plugins/platforms/ios/qiosintegration.mm b/src/plugins/platforms/ios/qiosintegration.mm index dcad6121be..5c8f67bda2 100644 --- a/src/plugins/platforms/ios/qiosintegration.mm +++ b/src/plugins/platforms/ios/qiosintegration.mm @@ -101,7 +101,12 @@ QIOSIntegration::~QIOSIntegration() bool QIOSIntegration::hasCapability(Capability cap) const { switch (cap) { + case BufferQueueingOpenGL: + return true; case OpenGL: + case ThreadedOpenGL: + return true; + case ThreadedPixmaps: return true; case MultipleWindows: return true; @@ -155,7 +160,7 @@ QPlatformServices *QIOSIntegration::services() const QVariant QIOSIntegration::styleHint(StyleHint hint) const { switch (hint) { - case ShowIsFullScreen: + case ShowIsMaximized: return true; case SetFocusOnTouchRelease: return true; diff --git a/src/plugins/platforms/ios/qiosscreen.h b/src/plugins/platforms/ios/qiosscreen.h index 40c7a3ccf7..173bd11719 100644 --- a/src/plugins/platforms/ios/qiosscreen.h +++ b/src/plugins/platforms/ios/qiosscreen.h @@ -50,8 +50,10 @@ QT_BEGIN_NAMESPACE -class QIOSScreen : public QPlatformScreen +class QIOSScreen : public QObject, public QPlatformScreen { + Q_OBJECT + public: QIOSScreen(unsigned int screenIndex); ~QIOSScreen(); @@ -72,13 +74,18 @@ public: UIScreen *uiScreen() const; - void setPrimaryOrientation(Qt::ScreenOrientation orientation); + void updateProperties(); + void layoutWindows(); + +public slots: + void updateStatusBarVisibility(); private: UIScreen *m_uiScreen; QRect m_geometry; QRect m_availableGeometry; int m_depth; + uint m_unscaledDpi; QSizeF m_physicalSize; QIOSOrientationListener *m_orientationListener; }; diff --git a/src/plugins/platforms/ios/qiosscreen.mm b/src/plugins/platforms/ios/qiosscreen.mm index c28d8881bf..57522cb1a3 100644 --- a/src/plugins/platforms/ios/qiosscreen.mm +++ b/src/plugins/platforms/ios/qiosscreen.mm @@ -123,8 +123,6 @@ QIOSScreen::QIOSScreen(unsigned int screenIndex) , m_uiScreen([[UIScreen screens] objectAtIndex:qMin(NSUInteger(screenIndex), [[UIScreen screens] count] - 1)]) , m_orientationListener(0) { - NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; - QString deviceIdentifier = deviceModelIdentifier(); if (deviceIdentifier == QStringLiteral("iPhone2,1") /* iPhone 3GS */ @@ -134,32 +132,118 @@ QIOSScreen::QIOSScreen(unsigned int screenIndex) m_depth = 24; } - int unscaledDpi = 163; // Regular iPhone DPI if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad && !deviceIdentifier.contains(QRegularExpression("^iPad2,[567]$")) /* excluding iPad Mini */) { - unscaledDpi = 132; - }; + m_unscaledDpi = 132; + } else { + m_unscaledDpi = 163; // Regular iPhone DPI + } - CGRect bounds = [m_uiScreen bounds]; - m_geometry = QRect(bounds.origin.x, bounds.origin.y, bounds.size.width, bounds.size.height); + connect(qGuiApp, &QGuiApplication::focusWindowChanged, this, &QIOSScreen::updateStatusBarVisibility); - CGRect frame = m_uiScreen.applicationFrame; - m_availableGeometry = QRect(frame.origin.x, frame.origin.y, frame.size.width, frame.size.height); + updateProperties(); +} - const qreal millimetersPerInch = 25.4; - m_physicalSize = QSizeF(m_geometry.size()) / unscaledDpi * millimetersPerInch; +QIOSScreen::~QIOSScreen() +{ + [m_orientationListener release]; +} - if (isQtApplication()) { - // When in a non-mixed environment, let QScreen follow the current interface orientation: - setPrimaryOrientation(toQtScreenOrientation(UIDeviceOrientation(qiosViewController().interfaceOrientation))); +void QIOSScreen::updateProperties() +{ + UIWindow *uiWindow = 0; + for (uiWindow in [[UIApplication sharedApplication] windows]) { + if (uiWindow.screen == m_uiScreen) + break; } - [pool release]; + bool inPortrait = UIInterfaceOrientationIsPortrait(uiWindow.rootViewController.interfaceOrientation); + QRect geometry = inPortrait ? fromCGRect(m_uiScreen.bounds).toRect() + : QRect(m_uiScreen.bounds.origin.x, m_uiScreen.bounds.origin.y, + m_uiScreen.bounds.size.height, m_uiScreen.bounds.size.width); + + if (geometry != m_geometry) { + m_geometry = geometry; + + const qreal millimetersPerInch = 25.4; + m_physicalSize = QSizeF(m_geometry.size()) / m_unscaledDpi * millimetersPerInch; + + QWindowSystemInterface::handleScreenGeometryChange(screen(), m_geometry); + } + + QRect availableGeometry = geometry; + + CGSize applicationFrameSize = m_uiScreen.applicationFrame.size; + int statusBarHeight = geometry.height() - (inPortrait ? applicationFrameSize.height : applicationFrameSize.width); + + availableGeometry.adjust(0, statusBarHeight, 0, 0); + + if (availableGeometry != m_availableGeometry) { + m_availableGeometry = availableGeometry; + QWindowSystemInterface::handleScreenAvailableGeometryChange(screen(), m_availableGeometry); + } + + if (screen()) + layoutWindows(); } -QIOSScreen::~QIOSScreen() +void QIOSScreen::updateStatusBarVisibility() { - [m_orientationListener release]; + QWindow *focusWindow = QGuiApplication::focusWindow(); + + // If we don't have a focus window we leave the status + // bar as is, so that the user can activate a new window + // with the same window state without the status bar jumping + // back and forth. + if (!focusWindow) + return; + + UIView *view = reinterpret_cast<UIView *>(focusWindow->handle()->winId()); +#if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_7_0) + if (QSysInfo::MacintoshVersion >= QSysInfo::MV_IOS_7_0) { + [view.viewController setNeedsStatusBarAppearanceUpdate]; + } else +#endif + { + bool wasHidden = [UIApplication sharedApplication].statusBarHidden; + QIOSViewController *viewController = static_cast<QIOSViewController *>(view.viewController); + [[UIApplication sharedApplication] + setStatusBarHidden:[viewController prefersStatusBarHidden] + withAnimation:UIStatusBarAnimationNone]; + + if ([UIApplication sharedApplication].statusBarHidden != wasHidden) + updateProperties(); + } +} + +void QIOSScreen::layoutWindows() +{ + QList<QWindow*> windows = QGuiApplication::topLevelWindows(); + + const QRect oldGeometry = screen()->geometry(); + const QRect oldAvailableGeometry = screen()->availableGeometry(); + const QRect newGeometry = geometry(); + const QRect newAvailableGeometry = availableGeometry(); + + for (int i = 0; i < windows.size(); ++i) { + QWindow *window = windows.at(i); + + if (platformScreenForWindow(window) != this) + continue; + + QIOSWindow *platformWindow = static_cast<QIOSWindow *>(window->handle()); + if (!platformWindow) + continue; + + // FIXME: Handle more complex cases of no-state and/or child windows when rotating + + if (window->windowState() & Qt::WindowFullScreen + || (window->windowState() & Qt::WindowNoState && window->geometry() == oldGeometry)) + platformWindow->applyGeometry(newGeometry); + else if (window->windowState() & Qt::WindowMaximized + || (window->windowState() & Qt::WindowNoState && window->geometry() == oldAvailableGeometry)) + platformWindow->applyGeometry(newAvailableGeometry); + } } QRect QIOSScreen::geometry() const @@ -199,7 +283,9 @@ qreal QIOSScreen::devicePixelRatio() const Qt::ScreenOrientation QIOSScreen::nativeOrientation() const { - return Qt::PortraitOrientation; + // A UIScreen stays in the native orientation, regardless of rotation + return m_uiScreen.bounds.size.width >= m_uiScreen.bounds.size.height ? + Qt::LandscapeOrientation : Qt::PortraitOrientation; } Qt::ScreenOrientation QIOSScreen::orientation() const @@ -217,31 +303,11 @@ void QIOSScreen::setOrientationUpdateMask(Qt::ScreenOrientations mask) } } -void QIOSScreen::setPrimaryOrientation(Qt::ScreenOrientation orientation) -{ - // Note that UIScreen never changes orientation, but QScreen should. To work around - // this, we let QIOSViewController call us whenever interface orientation changes, and - // use that as primary orientation. After all, the viewcontrollers geometry is what we - // place QWindows on top of. A problem with this approach is that QIOSViewController is - // not in use in a mixed environment, which results in no change to primary orientation. - // We see that as acceptable since Qt should most likely not interfere with orientation - // for that case anyway. - bool portrait = screen()->isPortrait(orientation); - if (portrait && m_geometry.width() < m_geometry.height()) - return; - - // Switching portrait/landscape means swapping width/height (and adjusting x/y): - m_geometry = QRect(0, 0, m_geometry.height(), m_geometry.width()); - m_physicalSize = QSizeF(m_physicalSize.height(), m_physicalSize.width()); - m_availableGeometry = fromPortraitToPrimary(fromCGRect(m_uiScreen.applicationFrame), this); - - QWindowSystemInterface::handleScreenGeometryChange(screen(), m_geometry); - QWindowSystemInterface::handleScreenAvailableGeometryChange(screen(), m_availableGeometry); -} - UIScreen *QIOSScreen::uiScreen() const { return m_uiScreen; } +#include "moc_qiosscreen.cpp" + QT_END_NAMESPACE diff --git a/src/plugins/platforms/ios/qiosviewcontroller.h b/src/plugins/platforms/ios/qiosviewcontroller.h index d5a61cb3f4..a0017808d3 100644 --- a/src/plugins/platforms/ios/qiosviewcontroller.h +++ b/src/plugins/platforms/ios/qiosviewcontroller.h @@ -42,5 +42,6 @@ #import <UIKit/UIKit.h> @interface QIOSViewController : UIViewController +- (BOOL)prefersStatusBarHidden; @end diff --git a/src/plugins/platforms/ios/qiosviewcontroller.mm b/src/plugins/platforms/ios/qiosviewcontroller.mm index d315b49776..2e7e44d32c 100644 --- a/src/plugins/platforms/ios/qiosviewcontroller.mm +++ b/src/plugins/platforms/ios/qiosviewcontroller.mm @@ -42,20 +42,14 @@ #import "qiosviewcontroller.h" #include <QtGui/QGuiApplication> +#include <QtGui/QWindow> #include <QtGui/QScreen> #include "qiosscreen.h" #include "qiosglobal.h" +#include "qioswindow.h" @implementation QIOSViewController -- (void)viewDidLoad -{ -#ifdef QT_DEBUG - if (!self.nibName) - self.view.backgroundColor = [UIColor magentaColor]; -#endif -} - -(BOOL)shouldAutorotate { // Until a proper orientation and rotation API is in place, we always auto rotate. @@ -63,26 +57,56 @@ return YES; } +#if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_6_0) -(NSUInteger)supportedInterfaceOrientations { // We need to tell iOS that we support all orientations in order to set // status bar orientation when application content orientation changes. return UIInterfaceOrientationMaskAll; } +#endif + +#if QT_IOS_DEPLOYMENT_TARGET_BELOW(__IPHONE_6_0) +-(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation +{ + Q_UNUSED(interfaceOrientation); + return YES; +} +#endif -- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration +- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation duration:(NSTimeInterval)duration { Q_UNUSED(duration); + Q_UNUSED(interfaceOrientation); if (!QCoreApplication::instance()) return; // FIXME: Store orientation for later (?) - Qt::ScreenOrientation orientation = toQtScreenOrientation(UIDeviceOrientation(toInterfaceOrientation)); - if (orientation == -1) - return; - QIOSScreen *qiosScreen = static_cast<QIOSScreen *>(QGuiApplication::primaryScreen()->handle()); - qiosScreen->setPrimaryOrientation(orientation); + qiosScreen->updateProperties(); +} + +#if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_7_0) +- (UIStatusBarStyle)preferredStatusBarStyle +{ + // Since we don't place anything behind the status bare by default, we + // end up with a black area, so we have to enable the white text mode + // of the iOS7 statusbar. + return UIStatusBarStyleLightContent; + + // FIXME: Try to detect the content underneath the statusbar and choose + // an appropriate style, and/or expose Qt APIs to control the style. +} +#endif + +- (BOOL)prefersStatusBarHidden +{ + QWindow *focusWindow = QGuiApplication::focusWindow(); + if (!focusWindow) + return [UIApplication sharedApplication].statusBarHidden; + + QIOSWindow *topLevel = static_cast<QIOSWindow *>(focusWindow->handle())->topLevelWindow(); + return topLevel->window()->windowState() == Qt::WindowFullScreen; } @end diff --git a/src/plugins/platforms/ios/qioswindow.h b/src/plugins/platforms/ios/qioswindow.h index 20f0aa59b6..a5e122bda1 100644 --- a/src/plugins/platforms/ios/qioswindow.h +++ b/src/plugins/platforms/ios/qioswindow.h @@ -52,12 +52,17 @@ class QIOSWindow; @interface UIView (QIOS) @property(readonly) QWindow *qwindow; +@property(readonly) UIViewController *viewController; @end QT_BEGIN_NAMESPACE -class QIOSWindow : public QPlatformWindow +@class QUIView; + +class QIOSWindow : public QObject, public QPlatformWindow { + Q_OBJECT + public: explicit QIOSWindow(QWindow *window); ~QIOSWindow(); @@ -74,20 +79,21 @@ public: void requestActivateWindow(); qreal devicePixelRatio() const; - int effectiveWidth() const; - int effectiveHeight() const; bool setMouseGrabEnabled(bool grab) { return grab; } bool setKeyboardGrabEnabled(bool grab) { return grab; } WId winId() const { return WId(m_view); }; + QIOSWindow *topLevelWindow() const; + private: - UIView *m_view; + void applyGeometry(const QRect &rect); - QRect m_requestedGeometry; + QUIView *m_view; + + QRect m_normalGeometry; int m_windowLevel; - qreal m_devicePixelRatio; void raiseOrLower(bool raise); void updateWindowLevel(); @@ -95,6 +101,8 @@ private: inline Qt::WindowType windowType() { return static_cast<Qt::WindowType>(int(window()->flags() & Qt::WindowType_Mask)); } inline bool windowIsPopup() { return windowType() & Qt::Popup & ~Qt::Window; } + + friend class QIOSScreen; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/ios/qioswindow.mm b/src/plugins/platforms/ios/qioswindow.mm index dbeec5f5f2..0dd810bdf6 100644 --- a/src/plugins/platforms/ios/qioswindow.mm +++ b/src/plugins/platforms/ios/qioswindow.mm @@ -48,6 +48,7 @@ #include "qiosviewcontroller.h" #include "qiosintegration.h" #include <QtGui/private/qguiapplication_p.h> +#include <QtGui/private/qwindow_p.h> #include <qpa/qplatformintegration.h> #import <QuartzCore/CAEAGLLayer.h> @@ -57,7 +58,7 @@ #include <QtDebug> -@interface EAGLView : UIView <UIKeyInput> +@interface QUIView : UIView <UIKeyInput> { @public UITextAutocapitalizationType autocapitalizationType; @@ -82,7 +83,7 @@ @end -@implementation EAGLView +@implementation QUIView + (Class)layerClass { @@ -107,15 +108,7 @@ [NSNumber numberWithBool:YES], kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat, nil]; - // Set up text input - autocapitalizationType = UITextAutocapitalizationTypeNone; - autocorrectionType = UITextAutocorrectionTypeNo; - enablesReturnKeyAutomatically = NO; - keyboardAppearance = UIKeyboardAppearanceDefault; - keyboardType = UIKeyboardTypeDefault; - returnKeyType = UIReturnKeyDone; - secureTextEntry = NO; - m_nextTouchId = 0; + [self updateTextInputTraits]; if (isQtApplication()) self.hidden = YES; @@ -126,6 +119,41 @@ return self; } +- (void)willMoveToWindow:(UIWindow *)newWindow +{ + // UIKIt will normally set the scale factor of a view to match the corresponding + // screen scale factor, but views backed by CAEAGLLayers need to do this manually. + self.contentScaleFactor = newWindow && newWindow.screen ? + newWindow.screen.scale : [[UIScreen mainScreen] scale]; + + // FIXME: Allow the scale factor to be customized through QSurfaceFormat. +} + +- (void)didAddSubview:(UIView *)subview +{ + if ([subview isKindOfClass:[QUIView class]]) + self.clipsToBounds = YES; +} + +- (void)willRemoveSubview:(UIView *)subview +{ + for (UIView *view in self.subviews) { + if (view != subview && [view isKindOfClass:[QUIView class]]) + return; + } + + self.clipsToBounds = NO; +} + +- (void)setNeedsDisplay +{ + [super setNeedsDisplay]; + + // We didn't implement drawRect: so we have to manually + // mark the layer as needing display. + [self.layer setNeedsDisplay]; +} + - (void)layoutSubviews { // This method is the de facto way to know that view has been resized, @@ -138,21 +166,67 @@ qWarning() << m_qioswindow->window() << "is backed by a UIView that has a transform set. This is not supported."; - QRect geometry = fromCGRect(self.frame); - m_qioswindow->QPlatformWindow::setGeometry(geometry); - QWindowSystemInterface::handleGeometryChange(m_qioswindow->window(), geometry); + // The original geometry requested by setGeometry() might be different + // from what we end up with after applying window constraints. + QRect requestedGeometry = m_qioswindow->geometry(); + + QRect actualGeometry; + if (m_qioswindow->window()->isTopLevel()) { + UIWindow *uiWindow = self.window; + UIView *rootView = uiWindow.rootViewController.view; + CGRect rootViewPositionInRelationToRootViewController = + [rootView convertRect:uiWindow.bounds fromView:uiWindow]; + + actualGeometry = fromCGRect(CGRectOffset([self.superview convertRect:self.frame toView:rootView], + -rootViewPositionInRelationToRootViewController.origin.x, + -rootViewPositionInRelationToRootViewController.origin.y + + rootView.bounds.origin.y)).toRect(); + } else { + actualGeometry = fromCGRect(self.frame).toRect(); + } + + // Persist the actual/new geometry so that QWindow::geometry() can + // be queried on the resize event. + m_qioswindow->QPlatformWindow::setGeometry(actualGeometry); + + QRect previousGeometry = requestedGeometry != actualGeometry ? + requestedGeometry : qt_window_private(m_qioswindow->window())->geometry; - // If we have a new size here we need to resize the FBO's corresponding buffers, - // but we defer that to when the application calls makeCurrent. + QWindowSystemInterface::handleGeometryChange(m_qioswindow->window(), actualGeometry, previousGeometry); + QWindowSystemInterface::flushWindowSystemEvents(); + + if (actualGeometry.size() != previousGeometry.size()) { + // Trigger expose event on resize + [self setNeedsDisplay]; - [super layoutSubviews]; + // A new size means we also need to resize the FBO's corresponding buffers, + // but we defer that to when the application calls makeCurrent. + } +} + +- (void)displayLayer:(CALayer *)layer +{ + QRect geometry = fromCGRect(layer.frame).toRect(); + Q_ASSERT(m_qioswindow->geometry() == geometry); + Q_ASSERT(self.hidden == !m_qioswindow->window()->isVisible()); + + QRegion region = self.hidden ? QRegion() : QRect(QPoint(), geometry.size()); + QWindowSystemInterface::handleExposeEvent(m_qioswindow->window(), region); + QWindowSystemInterface::flushWindowSystemEvents(); } - (void)updateTouchList:(NSSet *)touches withState:(Qt::TouchPointState)state { - // We deliver touch events with global coordinates. But global in this respect means - // the coordinate system where this QWindow lives. And that is our superview. - CGSize parentSize = self.superview.frame.size; + // We deliver touch events in global coordinates. But global in this respect + // means the same coordinate system that we use for describing the geometry + // of the top level QWindow we're inside. And that would be the coordinate + // system of the superview of the UIView that backs that window: + QPlatformWindow *topLevel = m_qioswindow; + while (QPlatformWindow *topLevelParent = topLevel->parent()) + topLevel = topLevelParent; + UIView *rootView = reinterpret_cast<UIView *>(topLevel->winId()).superview; + CGSize rootViewSize = rootView.frame.size; + foreach (UITouch *uiTouch, m_activeTouches.keys()) { QWindowSystemInterface::TouchPoint &touchPoint = m_activeTouches[uiTouch]; if (![touches containsObject:uiTouch]) { @@ -160,9 +234,9 @@ } else { touchPoint.state = state; touchPoint.pressure = (state == Qt::TouchPointReleased) ? 0.0 : 1.0; - QPoint touchPos = fromCGPoint([uiTouch locationInView:self.superview]); + QPoint touchPos = fromCGPoint([uiTouch locationInView:rootView]).toPoint(); touchPoint.area = QRectF(touchPos, QSize(0, 0)); - touchPoint.normalPosition = QPointF(touchPos.x() / parentSize.width, touchPos.y() / parentSize.height); + touchPoint.normalPosition = QPointF(touchPos.x() / rootViewSize.width, touchPos.y() / rootViewSize.height); } } } @@ -218,15 +292,26 @@ - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { - Q_UNUSED(touches) // ### can a subset of the active touches be cancelled? + if (!touches && m_activeTouches.isEmpty()) + return; + + if (!touches) { + m_activeTouches.clear(); + } else { + for (UITouch *touch in touches) + m_activeTouches.remove(touch); + + Q_ASSERT_X(m_activeTouches.isEmpty(), Q_FUNC_INFO, + "Subset of active touches cancelled by UIKit"); + } - // Clear current touch points - m_activeTouches.clear(); m_nextTouchId = 0; + NSTimeInterval timestamp = event ? event.timestamp : [[NSProcessInfo processInfo] systemUptime]; + // Send cancel touch event synchronously QIOSIntegration *iosIntegration = static_cast<QIOSIntegration *>(QGuiApplicationPrivate::platformIntegration()); - QWindowSystemInterface::handleTouchCancelEvent(m_qioswindow->window(), ulong(event.timestamp * 1000), iosIntegration->touchDevice()); + QWindowSystemInterface::handleTouchCancelEvent(m_qioswindow->window(), ulong(timestamp * 1000), iosIntegration->touchDevice()); QWindowSystemInterface::flushWindowSystemEvents(); } @@ -250,13 +335,17 @@ // user cannot type. And since the keyboard will open when a view becomes // the first responder, it's now a good time to inform QPA that the QWindow // this view backs became active: + [self updateTextInputTraits]; QWindowSystemInterface::handleWindowActivated(m_qioswindow->window()); return [super becomeFirstResponder]; } - (BOOL)resignFirstResponder { - QWindowSystemInterface::handleWindowActivated(0); + // Resigning first responed status means that the virtual keyboard was closed, or + // some other view became first responder. In either case we clear the focus object to + // avoid blinking cursors in line edits etc: + static_cast<QWindowPrivate *>(QObjectPrivate::get(m_qioswindow->window()))->clearFocusObject(); return [super resignFirstResponder]; } @@ -269,8 +358,11 @@ { QString string = QString::fromUtf8([text UTF8String]); int key = 0; - if ([text isEqualToString:@"\n"]) + if ([text isEqualToString:@"\n"]) { key = (int)Qt::Key_Return; + if (self.returnKeyType == UIReturnKeyDone) + [self resignFirstResponder]; + } // Send key event to window system interface QWindowSystemInterface::handleKeyEvent( @@ -288,14 +380,65 @@ 0, QEvent::KeyRelease, (int)Qt::Key_Backspace, Qt::NoModifier); } +- (void)updateTextInputTraits +{ + // Ask the current focus object what kind of input it + // expects, and configure the keyboard appropriately: + QObject *focusObject = QGuiApplication::focusObject(); + if (!focusObject) + return; + QInputMethodQueryEvent queryEvent(Qt::ImEnabled | Qt::ImHints); + if (!QCoreApplication::sendEvent(focusObject, &queryEvent)) + return; + if (!queryEvent.value(Qt::ImEnabled).toBool()) + return; + + Qt::InputMethodHints hints = static_cast<Qt::InputMethodHints>(queryEvent.value(Qt::ImHints).toUInt()); + + self.returnKeyType = (hints & Qt::ImhMultiLine) ? UIReturnKeyDefault : UIReturnKeyDone; + self.secureTextEntry = BOOL(hints & Qt::ImhHiddenText); + self.autocorrectionType = (hints & Qt::ImhNoPredictiveText) ? + UITextAutocorrectionTypeNo : UITextAutocorrectionTypeDefault; + + if (hints & Qt::ImhUppercaseOnly) + self.autocapitalizationType = UITextAutocapitalizationTypeAllCharacters; + else if (hints & Qt::ImhNoAutoUppercase) + self.autocapitalizationType = UITextAutocapitalizationTypeNone; + else + self.autocapitalizationType = UITextAutocapitalizationTypeSentences; + + if (hints & Qt::ImhUrlCharactersOnly) + self.keyboardType = UIKeyboardTypeURL; + else if (hints & Qt::ImhEmailCharactersOnly) + self.keyboardType = UIKeyboardTypeEmailAddress; + else if (hints & Qt::ImhDigitsOnly) + self.keyboardType = UIKeyboardTypeNumberPad; + else if (hints & Qt::ImhFormattedNumbersOnly) + self.keyboardType = UIKeyboardTypeDecimalPad; + else if (hints & Qt::ImhDialableCharactersOnly) + self.keyboardType = UIKeyboardTypeNumberPad; + else + self.keyboardType = UIKeyboardTypeDefault; +} + @end @implementation UIView (QIOS) - (QWindow *)qwindow { - if ([self isKindOfClass:[EAGLView class]]) - return static_cast<EAGLView *>(self)->m_qioswindow->window(); + if ([self isKindOfClass:[QUIView class]]) + return static_cast<QUIView *>(self)->m_qioswindow->window(); + return nil; +} + +- (UIViewController *)viewController +{ + id responder = self; + while ((responder = [responder nextResponder])) { + if ([responder isKindOfClass:UIViewController.class]) + return responder; + } return nil; } @@ -305,26 +448,23 @@ QT_BEGIN_NAMESPACE QIOSWindow::QIOSWindow(QWindow *window) : QPlatformWindow(window) - , m_view([[EAGLView alloc] initWithQIOSWindow:this]) - , m_requestedGeometry(QPlatformWindow::geometry()) + , m_view([[QUIView alloc] initWithQIOSWindow:this]) + , m_normalGeometry(QPlatformWindow::geometry()) , m_windowLevel(0) - , m_devicePixelRatio(1.0) { - setParent(parent()); + setParent(QPlatformWindow::parent()); setWindowState(window->windowState()); - - // Retina support: get screen scale factor and set it in the content view. - // This will make framebufferObject() create a 2x frame buffer on retina - // displays. Also set m_devicePixelRatio which is used for scaling the - // paint device. - if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)] == YES) { - m_devicePixelRatio = [[UIScreen mainScreen] scale]; - [m_view setContentScaleFactor: m_devicePixelRatio]; - } } QIOSWindow::~QIOSWindow() { + // According to the UIResponder documentation, Cocoa Touch should react to system interruptions + // that "might cause the view to be removed from the window" by sending touchesCancelled, but in + // practice this doesn't seem to happen when removing the view from its superview. To ensure that + // Qt's internal state for touch and mouse handling is kept consistent, we therefor have to force + // cancellation of all touch events. + [m_view touchesCancelled:0 withEvent:0]; + [m_view removeFromSuperview]; [m_view release]; } @@ -337,8 +477,8 @@ bool QIOSWindow::blockedByModal() void QIOSWindow::setVisible(bool visible) { - QPlatformWindow::setVisible(visible); m_view.hidden = !visible; + [m_view setNeedsDisplay]; if (!isQtApplication()) return; @@ -356,9 +496,13 @@ void QIOSWindow::setVisible(bool visible) if (visible) { requestActivateWindow(); + + if (window()->isTopLevel()) + static_cast<QIOSScreen *>(screen())->updateStatusBarVisibility(); + } else { // Activate top-most visible QWindow: - NSArray *subviews = qiosViewController().view.subviews; + NSArray *subviews = m_view.viewController.view.subviews; for (int i = int(subviews.count) - 1; i >= 0; --i) { UIView *view = [subviews objectAtIndex:i]; if (!view.hidden) { @@ -373,38 +517,95 @@ void QIOSWindow::setVisible(bool visible) void QIOSWindow::setGeometry(const QRect &rect) { - // If the window is in fullscreen, just bookkeep the requested - // geometry in case the window goes into Qt::WindowNoState later: - m_requestedGeometry = rect; - if (window()->windowState() & (Qt::WindowMaximized | Qt::WindowFullScreen)) + m_normalGeometry = rect; + + if (window()->windowState() != Qt::WindowNoState) { + QPlatformWindow::setGeometry(rect); + + // The layout will realize the requested geometry was not applied, and + // send geometry-change events that match the actual geometry. + [m_view setNeedsLayout]; + + if (window()->inherits("QWidgetWindow")) { + // QWidget wrongly assumes that setGeometry resets the window + // state back to Qt::NoWindowState, so we need to inform it that + // that his is not the case by re-issuing the current window state. + QWindowSystemInterface::handleWindowStateChanged(window(), window()->windowState()); + + // It also needs to be told immediately that the geometry it requested + // did not apply, otherwise it will continue on as if it did, instead + // of waiting for a resize event. + [m_view layoutIfNeeded]; + } + return; + } + + applyGeometry(rect); +} + +void QIOSWindow::applyGeometry(const QRect &rect) +{ + // Geometry changes are asynchronous, but QWindow::geometry() is + // expected to report back the 'requested geometry' until we get + // a callback with the updated geometry from the window system. + // The baseclass takes care of persisting this for us. + QPlatformWindow::setGeometry(rect); + + if (window()->isTopLevel()) { + // The QWindow is in QScreen coordinates, which maps to a possibly rotated root-view-controller. + // Since the root-view-controller might be translated in relation to the UIWindow, we need to + // check specifically for that and compensate. Also check if the root view has been scrolled + // as a result of the keyboard being open. + UIWindow *uiWindow = m_view.window; + UIView *rootView = uiWindow.rootViewController.view; + CGRect rootViewPositionInRelationToRootViewController = + [rootView convertRect:uiWindow.bounds fromView:uiWindow]; + + m_view.frame = CGRectOffset([m_view.superview convertRect:toCGRect(rect) fromView:rootView], + rootViewPositionInRelationToRootViewController.origin.x, + rootViewPositionInRelationToRootViewController.origin.y + + rootView.bounds.origin.y); + } else { + // Easy, in parent's coordinates + m_view.frame = toCGRect(rect); + } + + // iOS will automatically trigger -[layoutSubviews:] for resize, + // but not for move, so we force it just in case. + [m_view setNeedsLayout]; - // Since we don't support transformations on the UIView, we can set the frame - // directly and let UIKit deal with translating that into bounds and center. - // Changing the size of the view will end up in a call to -[EAGLView layoutSubviews] - // which will update QWindowSystemInterface with the new size. - m_view.frame = toCGRect(rect); + if (window()->inherits("QWidgetWindow")) + [m_view layoutIfNeeded]; } void QIOSWindow::setWindowState(Qt::WindowState state) { - // FIXME: Figure out where or how we should disable/enable the statusbar. - // Perhaps setting QWindow to maximized should also mean that we'll show - // the statusbar, and vice versa for fullscreen? + // Update the QWindow representation straight away, so that + // we can update the statusbar visibility based on the new + // state before applying geometry changes. + qt_window_private(window())->windowState = state; + + if (window()->isTopLevel() && window()->isVisible() && window()->isActive()) + static_cast<QIOSScreen *>(screen())->updateStatusBarVisibility(); switch (state) { + case Qt::WindowNoState: + applyGeometry(m_normalGeometry); + break; case Qt::WindowMaximized: - case Qt::WindowFullScreen: { - // Since UIScreen does not take orientation into account when - // reporting geometry, we need to look at the top view instead: - CGSize fullscreenSize = m_view.window.rootViewController.view.bounds.size; - m_view.frame = CGRectMake(0, 0, fullscreenSize.width, fullscreenSize.height); - m_view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; - break; } - default: - m_view.frame = toCGRect(m_requestedGeometry); - m_view.autoresizingMask = UIViewAutoresizingNone; + applyGeometry(screen()->availableGeometry()); + break; + case Qt::WindowFullScreen: + applyGeometry(screen()->geometry()); break; + case Qt::WindowMinimized: + applyGeometry(QRect()); + break; + case Qt::WindowActive: + Q_UNREACHABLE(); + default: + Q_UNREACHABLE(); } } @@ -414,8 +615,30 @@ void QIOSWindow::setParent(const QPlatformWindow *parentWindow) UIView *parentView = reinterpret_cast<UIView *>(parentWindow->winId()); [parentView addSubview:m_view]; } else if (isQtApplication()) { - [qiosViewController().view addSubview:m_view]; + for (UIWindow *uiWindow in [[UIApplication sharedApplication] windows]) { + if (uiWindow.screen == static_cast<QIOSScreen *>(screen())->uiScreen()) { + [uiWindow.rootViewController.view addSubview:m_view]; + break; + } + } + } +} + +QIOSWindow *QIOSWindow::topLevelWindow() const +{ + QWindow *window = this->window(); + while (window) { + QWindow *parent = window->parent(); + if (!parent) + parent = window->transientParent(); + + if (!parent) + break; + + window = parent; } + + return static_cast<QIOSWindow *>(window->handle()); } void QIOSWindow::requestActivateWindow() @@ -423,12 +646,15 @@ void QIOSWindow::requestActivateWindow() // Note that several windows can be active at the same time if they exist in the same // hierarchy (transient children). But only one window can be QGuiApplication::focusWindow(). // Dispite the name, 'requestActivateWindow' means raise and transfer focus to the window: - if (!window()->isTopLevel() || blockedByModal()) + if (blockedByModal()) return; - raise(); - QPlatformInputContext *context = QGuiApplicationPrivate::platformIntegration()->inputContext(); - static_cast<QIOSInputContext *>(context)->focusViewChanged(m_view); + [m_view.window makeKeyWindow]; + + if (window()->isTopLevel()) + raise(); + + QWindowSystemInterface::handleWindowActivated(window()); } void QIOSWindow::raiseOrLower(bool raise) @@ -444,7 +670,7 @@ void QIOSWindow::raiseOrLower(bool raise) for (int i = int(subviews.count) - 1; i >= 0; --i) { UIView *view = static_cast<UIView *>([subviews objectAtIndex:i]); - if (view.hidden || view == m_view) + if (view.hidden || view == m_view || !view.qwindow) continue; int level = static_cast<QIOSWindow *>(view.qwindow->handle())->m_windowLevel; if (m_windowLevel > level || (raise && m_windowLevel == level)) { @@ -491,17 +717,9 @@ void QIOSWindow::handleContentOrientationChange(Qt::ScreenOrientation orientatio qreal QIOSWindow::devicePixelRatio() const { - return m_devicePixelRatio; -} - -int QIOSWindow::effectiveWidth() const -{ - return geometry().width() * m_devicePixelRatio; + return m_view.contentScaleFactor; } -int QIOSWindow::effectiveHeight() const -{ - return geometry().height() * m_devicePixelRatio; -} +#include "moc_qioswindow.cpp" QT_END_NAMESPACE |