diff options
Diffstat (limited to 'src/plugins/platforms/ios')
-rw-r--r-- | src/plugins/platforms/ios/qiosinputcontext.h | 26 | ||||
-rw-r--r-- | src/plugins/platforms/ios/qiosinputcontext.mm | 300 | ||||
-rw-r--r-- | src/plugins/platforms/ios/qiosintegration.mm | 8 | ||||
-rw-r--r-- | src/plugins/platforms/ios/qiosscreen.h | 1 | ||||
-rw-r--r-- | src/plugins/platforms/ios/qiosscreen.mm | 5 |
5 files changed, 213 insertions, 127 deletions
diff --git a/src/plugins/platforms/ios/qiosinputcontext.h b/src/plugins/platforms/ios/qiosinputcontext.h index b4ff695f1a..863e503c3b 100644 --- a/src/plugins/platforms/ios/qiosinputcontext.h +++ b/src/plugins/platforms/ios/qiosinputcontext.h @@ -48,6 +48,22 @@ QT_BEGIN_NAMESPACE @class QIOSKeyboardListener; @class QIOSTextInputResponder; +@protocol KeyboardState; + +struct KeyboardState +{ + KeyboardState() : + keyboardVisible(false), keyboardAnimating(false), + animationDuration(0), animationCurve(UIViewAnimationCurve(-1)) + {} + + bool keyboardVisible; + bool keyboardAnimating; + QRectF keyboardRect; + CGRect keyboardEndRect; + NSTimeInterval animationDuration; + UIViewAnimationCurve animationCurve; +}; struct ImeState { @@ -69,6 +85,7 @@ public: void hideInputPanel() Q_DECL_OVERRIDE; bool isInputPanelVisible() const Q_DECL_OVERRIDE; + bool isAnimating() const Q_DECL_OVERRIDE; QRectF keyboardRect() const Q_DECL_OVERRIDE; void update(Qt::InputMethodQueries) Q_DECL_OVERRIDE; @@ -84,14 +101,21 @@ public: void scrollToCursor(); void scroll(int y); + void updateKeyboardState(NSNotification *notification = 0); + const ImeState &imeState() { return m_imeState; }; + const KeyboardState &keyboardState() { return m_keyboardState; }; + bool inputMethodAccepted() const; static QIOSInputContext *instance(); private: - QIOSKeyboardListener *m_keyboardListener; + UIView* scrollableRootView(); + + QIOSKeyboardListener *m_keyboardHideGesture; QIOSTextInputResponder *m_textResponder; + KeyboardState m_keyboardState; ImeState m_imeState; }; diff --git a/src/plugins/platforms/ios/qiosinputcontext.mm b/src/plugins/platforms/ios/qiosinputcontext.mm index e417e9a1fb..fe9ee18155 100644 --- a/src/plugins/platforms/ios/qiosinputcontext.mm +++ b/src/plugins/platforms/ios/qiosinputcontext.mm @@ -45,6 +45,7 @@ #include "qiosglobal.h" #include "qiosintegration.h" +#include "qiosscreen.h" #include "qiostextresponder.h" #include "qiosviewcontroller.h" #include "qioswindow.h" @@ -64,15 +65,8 @@ static QUIView *focusView() // ------------------------------------------------------------------------- @interface QIOSKeyboardListener : UIGestureRecognizer <UIGestureRecognizerDelegate> { -@public + @private QIOSInputContext *m_context; - BOOL m_keyboardVisible; - BOOL m_keyboardVisibleAndDocked; - QRectF m_keyboardRect; - CGRect m_keyboardEndRect; - NSTimeInterval m_duration; - UIViewAnimationCurve m_curve; - UIViewController *m_viewController; } @end @@ -80,87 +74,51 @@ static QUIView *focusView() - (id)initWithQIOSInputContext:(QIOSInputContext *)context { - self = [super initWithTarget:self action:@selector(gestureStateChanged:)]; - if (self) { + id originalSelf = self; + if (self = [super initWithTarget:self action:@selector(gestureStateChanged:)]) { + Q_ASSERT(self == originalSelf); + m_context = context; - m_keyboardVisible = NO; - 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); - - // Attach 'hide keyboard' gesture to the window, but keep it disabled when the - // keyboard is not visible. - self.enabled = NO; - self.cancelsTouchesInView = NO; - self.delaysTouchesEnded = NO; - [m_viewController.view.window addGestureRecognizer:self]; - } - [[NSNotificationCenter defaultCenter] - addObserver:self + // UIGestureRecognizer + self.enabled = NO; + self.cancelsTouchesInView = NO; + self.delaysTouchesEnded = NO; + + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + + [notificationCenter addObserver:self selector:@selector(keyboardWillShow:) - name:@"UIKeyboardWillShowNotification" object:nil]; - [[NSNotificationCenter defaultCenter] - addObserver:self + name:UIKeyboardWillShowNotification object:nil]; + [notificationCenter addObserver:self + selector:@selector(keyboardWillOrDidChange:) + name:UIKeyboardDidShowNotification object:nil]; + [notificationCenter addObserver:self selector:@selector(keyboardWillHide:) - name:@"UIKeyboardWillHideNotification" object:nil]; - [[NSNotificationCenter defaultCenter] - addObserver:self + name:UIKeyboardWillHideNotification object:nil]; + [notificationCenter addObserver:self + selector:@selector(keyboardWillOrDidChange:) + name:UIKeyboardDidHideNotification object:nil]; + [notificationCenter addObserver:self selector:@selector(keyboardDidChangeFrame:) - name:@"UIKeyboardDidChangeFrameNotification" object:nil]; + name:UIKeyboardDidChangeFrameNotification object:nil]; } + return self; } -- (void) dealloc +- (void)dealloc { - [m_viewController.view.window removeGestureRecognizer:self]; - [m_viewController release]; + [[NSNotificationCenter defaultCenter] removeObserver:self]; - [[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 -{ - Q_UNUSED(notification); - [self handleKeyboardRectChanged]; - - // 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->scrollToCursor(); -} +// ------------------------------------------------------------------------- -- (void) keyboardWillShow:(NSNotification *)notification +- (void)keyboardWillShow:(NSNotification *)notification { - // Note that UIKeyboardWillShowNotification is only sendt when the keyboard is docked. - m_keyboardVisibleAndDocked = YES; - m_keyboardEndRect = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; - - if (!m_duration) { - m_duration = [[notification.userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]; - m_curve = UIViewAnimationCurve([[notification.userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] integerValue]); - } + [self keyboardWillOrDidChange:notification]; UIResponder *firstResponder = [UIResponder currentFirstResponder]; if (![firstResponder isKindOfClass:[QIOSTextInputResponder class]]) @@ -172,11 +130,10 @@ static QUIView *focusView() m_context->scrollToCursor(); } -- (void) keyboardWillHide:(NSNotification *)notification +- (void)keyboardWillHide:(NSNotification *)notification { - // Note that UIKeyboardWillHideNotification is also sendt when the keyboard is undocked. - m_keyboardVisibleAndDocked = NO; - m_keyboardEndRect = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; + [self keyboardWillOrDidChange:notification]; + if (self.state != UIGestureRecognizerStateBegan) { // Only disable the gesture if the hiding of the keyboard was not caused by it. // Otherwise we need to await the final touchEnd callback for doing some clean-up. @@ -185,29 +142,19 @@ static QUIView *focusView() m_context->scroll(0); } -- (void) handleKeyboardRectChanged +- (void)keyboardDidChangeFrame:(NSNotification *)notification { - // QInputmethod::keyboardRectangle() is documented to be in window coordinates. - // If there is no focus window, we return an empty rectangle - UIView *view = focusView(); - QRectF convertedRect = fromCGRect([view convertRect:m_keyboardEndRect fromView:nil]); - - // Set height to zero if keyboard is hidden. Otherwise the rect will not change - // when the keyboard hides on a scrolled screen (since the keyboard will already - // be at the bottom of the 'screen' in that case) - if (!m_keyboardVisibleAndDocked) - convertedRect.setHeight(0); + [self keyboardWillOrDidChange:notification]; - if (convertedRect != m_keyboardRect) { - m_keyboardRect = convertedRect; - m_context->emitKeyboardRectChanged(); - } + // 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_context->isInputPanelVisible()) + m_context->scrollToCursor(); +} - BOOL visible = CGRectIntersectsRect(m_keyboardEndRect, [UIScreen mainScreen].bounds); - if (m_keyboardVisible != visible) { - m_keyboardVisible = visible; - m_context->emitInputPanelVisibleChanged(); - } +- (void)keyboardWillOrDidChange:(NSNotification *)notification +{ + m_context->updateKeyboardState(notification); } // ------------------------------------------------------------------------- @@ -216,7 +163,7 @@ static QUIView *focusView() { [super touchesBegan:touches withEvent:event]; - Q_ASSERT(m_keyboardVisibleAndDocked); + Q_ASSERT(m_context->isInputPanelVisible()); if ([touches count] != 1) self.state = UIGestureRecognizerStateFailed; @@ -229,8 +176,8 @@ static QUIView *focusView() if (self.state != UIGestureRecognizerStatePossible) return; - CGPoint touchPoint = [[touches anyObject] locationInView:m_viewController.view.window]; - if (CGRectContainsPoint(m_keyboardEndRect, touchPoint)) + CGPoint touchPoint = [[touches anyObject] locationInView:self.view]; + if (CGRectContainsPoint(m_context->keyboardState().keyboardEndRect, touchPoint)) self.state = UIGestureRecognizerStateBegan; } @@ -280,7 +227,7 @@ static QUIView *focusView() { [super reset]; - if (!m_keyboardVisibleAndDocked) { + if (!m_context->isInputPanelVisible()) { qImDebug() << "keyboard was hidden, disabling hide-keyboard gesture"; self.enabled = NO; } else { @@ -328,23 +275,25 @@ QIOSInputContext *QIOSInputContext::instance() QIOSInputContext::QIOSInputContext() : QPlatformInputContext() - , m_keyboardListener([[QIOSKeyboardListener alloc] initWithQIOSInputContext:this]) + , m_keyboardHideGesture([[QIOSKeyboardListener alloc] initWithQIOSInputContext:this]) , m_textResponder(0) { - if (isQtApplication()) + if (isQtApplication()) { + QIOSScreen *iosScreen = static_cast<QIOSScreen*>(QGuiApplication::primaryScreen()->handle()); + [iosScreen->uiWindow() addGestureRecognizer:m_keyboardHideGesture]; + connect(qGuiApp->inputMethod(), &QInputMethod::cursorRectangleChanged, this, &QIOSInputContext::cursorRectangleChanged); + } + connect(qGuiApp, &QGuiApplication::focusWindowChanged, this, &QIOSInputContext::focusWindowChanged); } QIOSInputContext::~QIOSInputContext() { - [m_keyboardListener release]; - [m_textResponder release]; -} + [m_keyboardHideGesture.view removeGestureRecognizer:m_keyboardHideGesture]; + [m_keyboardHideGesture release]; -QRectF QIOSInputContext::keyboardRect() const -{ - return m_keyboardListener->m_keyboardRect; + [m_textResponder release]; } void QIOSInputContext::showInputPanel() @@ -375,14 +324,85 @@ void QIOSInputContext::clearCurrentFocusObject() static_cast<QWindowPrivate *>(QObjectPrivate::get(focusWindow))->clearFocusObject(); } +// ------------------------------------------------------------------------- + +void QIOSInputContext::updateKeyboardState(NSNotification *notification) +{ + static CGRect currentKeyboardRect = CGRectZero; + + KeyboardState previousState = m_keyboardState; + + if (notification) { + NSDictionary *userInfo = [notification userInfo]; + + CGRect frameBegin = [[userInfo objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue]; + CGRect frameEnd = [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; + + bool atEndOfKeyboardTransition = [notification.name rangeOfString:@"Did"].location != NSNotFound; + + currentKeyboardRect = atEndOfKeyboardTransition ? frameEnd : frameBegin; + + // The isInputPanelVisible() property is based on whether or not the virtual keyboard + // is visible on screen, and does not follow the logic of the iOS WillShow and WillHide + // notifications which are not emitted for undocked keyboards, and are buggy when dealing + // with input-accesosory-views. The reason for using frameEnd here (the future state), + // instead of the current state reflected in frameBegin, is that QInputMethod::isVisible() + // is documented to reflect the future state in the case of animated transitions. + m_keyboardState.keyboardVisible = CGRectIntersectsRect(frameEnd, [UIScreen mainScreen].bounds); + + // Used for auto-scroller, and will be used for animation-signal in the future + m_keyboardState.keyboardEndRect = frameEnd; + + if (m_keyboardState.animationCurve < 0) { + // We only set the animation curve the first time it has a valid value, since iOS will sometimes report + // an invalid animation curve even if the keyboard is animating, and we don't want to overwrite the + // curve in that case. + m_keyboardState.animationCurve = UIViewAnimationCurve([[userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] integerValue]); + } + + m_keyboardState.animationDuration = [[userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + m_keyboardState.keyboardAnimating = m_keyboardState.animationDuration > 0 && !atEndOfKeyboardTransition; + + qImDebug() << qPrintable(QString::fromNSString(notification.name)) << "from" << fromCGRect(frameBegin) << "to" << fromCGRect(frameEnd) + << "(curve =" << m_keyboardState.animationCurve << "duration =" << m_keyboardState.animationDuration << "s)"; + } else { + qImDebug() << "No notification to update keyboard state based on, just updating keyboard rect"; + } + + if (!focusView() || CGRectIsEmpty(currentKeyboardRect)) + m_keyboardState.keyboardRect = QRectF(); + else // QInputmethod::keyboardRectangle() is documented to be in window coordinates. + m_keyboardState.keyboardRect = fromCGRect([focusView() convertRect:currentKeyboardRect fromView:nil]); + + // Emit for all changed properties + if (m_keyboardState.keyboardVisible != previousState.keyboardVisible) + emitInputPanelVisibleChanged(); + if (m_keyboardState.keyboardAnimating != previousState.keyboardAnimating) + emitAnimatingChanged(); + if (m_keyboardState.keyboardRect != previousState.keyboardRect) + emitKeyboardRectChanged(); +} + bool QIOSInputContext::isInputPanelVisible() const { - return m_keyboardListener->m_keyboardVisible; + return m_keyboardState.keyboardVisible; +} + +bool QIOSInputContext::isAnimating() const +{ + return m_keyboardState.keyboardAnimating; +} + +QRectF QIOSInputContext::keyboardRect() const +{ + return m_keyboardState.keyboardRect; } +// ------------------------------------------------------------------------- + void QIOSInputContext::cursorRectangleChanged() { - if (!m_keyboardListener->m_keyboardVisibleAndDocked || !qApp->focusObject()) + if (!isInputPanelVisible() || !qApp->focusObject()) return; // Check if the cursor has changed position inside the input item. Since @@ -397,44 +417,67 @@ void QIOSInputContext::cursorRectangleChanged() prevCursor = cursor; } +UIView *QIOSInputContext::scrollableRootView() +{ + if (!m_keyboardHideGesture.view) + return 0; + + UIWindow *window = static_cast<UIWindow*>(m_keyboardHideGesture.view); + if (![window.rootViewController isKindOfClass:[QIOSViewController class]]) + return 0; + + return window.rootViewController.view; +} + void QIOSInputContext::scrollToCursor() { if (!isQtApplication()) return; - if (m_keyboardListener.state == UIGestureRecognizerStatePossible && m_keyboardListener.numberOfTouches == 1) { + if (m_keyboardHideGesture.state == UIGestureRecognizerStatePossible && m_keyboardHideGesture.numberOfTouches == 1) { // Don't scroll to the cursor if the user is touching the screen and possibly // trying to trigger the hide-keyboard gesture. qImDebug() << "preventing scrolling to cursor as we're still waiting for a possible gesture"; return; } - UIView *view = m_keyboardListener->m_viewController.view; - if (view.window != focusView().window) + UIView *rootView = scrollableRootView(); + if (!rootView) + return; + + if (rootView.window != focusView().window) return; + // We only support auto-scroll for docked keyboards for now, so make sure that's the case + if (CGRectGetMaxY(m_keyboardState.keyboardEndRect) != CGRectGetMaxY([UIScreen mainScreen].bounds)) { + qImDebug() << "Keyboard not docked, ignoring request to scroll to reveal cursor"; + return; + } + const int margin = 20; QRectF translatedCursorPos = qApp->inputMethod()->cursorRectangle(); translatedCursorPos.translate(focusView().qwindow->geometry().topLeft()); - qreal keyboardY = [view convertRect:m_keyboardListener->m_keyboardEndRect fromView:nil].origin.y; + qreal keyboardY = [rootView convertRect:m_keyboardState.keyboardEndRect fromView:nil].origin.y; int statusBarY = qGuiApp->primaryScreen()->availableGeometry().y(); scroll((translatedCursorPos.bottomLeft().y() < keyboardY - margin) ? 0 - : qMin(view.bounds.size.height - keyboardY, translatedCursorPos.y() - statusBarY - margin)); + : qMin(rootView.bounds.size.height - keyboardY, translatedCursorPos.y() - statusBarY - margin)); } void QIOSInputContext::scroll(int y) { - UIView *rootView = m_keyboardListener->m_viewController.view; + UIView *rootView = scrollableRootView(); + if (!rootView) + return; CATransform3D translationTransform = CATransform3DMakeTranslation(0.0, -y, 0.0); if (CATransform3DEqualToTransform(translationTransform, rootView.layer.sublayerTransform)) return; QPointer<QIOSInputContext> self = this; - [UIView animateWithDuration:m_keyboardListener->m_duration delay:0 - options:(m_keyboardListener->m_curve << 16) | UIViewAnimationOptionBeginFromCurrentState + [UIView animateWithDuration:m_keyboardState.animationDuration delay:0 + options:(m_keyboardState.animationCurve << 16) | UIViewAnimationOptionBeginFromCurrentState animations:^{ // The sublayerTransform property of CALayer is not implicitly animated for a // layer-backed view, even inside a UIView animation block, so we need to set up @@ -463,8 +506,12 @@ void QIOSInputContext::scroll(int y) [rootView.qtViewController updateProperties]; } completion:^(BOOL){ - if (self) - [m_keyboardListener handleKeyboardRectChanged]; + if (self) { + // Scrolling the root view results in the keyboard being moved + // relative to the focus window, so we need to re-evaluate the + // keyboard rectangle. + updateKeyboardState(); + } } ]; } @@ -477,7 +524,7 @@ void QIOSInputContext::setFocusObject(QObject *focusObject) qImDebug() << "new focus object =" << focusObject; - if (m_keyboardListener.state == UIGestureRecognizerStateChanged) { + if (m_keyboardHideGesture.state == UIGestureRecognizerStateChanged) { // A new focus object may be set as part of delivering touch events to // application during the hide-keyboard gesture, but we don't want that // to result in a new object getting focus and bringing the keyboard up @@ -489,7 +536,7 @@ void QIOSInputContext::setFocusObject(QObject *focusObject) reset(); - if (m_keyboardListener->m_keyboardVisibleAndDocked) + if (isInputPanelVisible()) scrollToCursor(); } @@ -501,8 +548,11 @@ void QIOSInputContext::focusWindowChanged(QWindow *focusWindow) reset(); - [m_keyboardListener handleKeyboardRectChanged]; - if (m_keyboardListener->m_keyboardVisibleAndDocked) + // The keyboard rectangle depend on the focus window, so + // we need to re-evaluate the keyboard state. + updateKeyboardState(); + + if (isInputPanelVisible()) scrollToCursor(); } diff --git a/src/plugins/platforms/ios/qiosintegration.mm b/src/plugins/platforms/ios/qiosintegration.mm index 9a722ead37..461f160892 100644 --- a/src/plugins/platforms/ios/qiosintegration.mm +++ b/src/plugins/platforms/ios/qiosintegration.mm @@ -88,7 +88,13 @@ QIOSIntegration::QIOSIntegration() // Set current directory to app bundle folder QDir::setCurrent(QString::fromUtf8([[[NSBundle mainBundle] bundlePath] UTF8String])); - for (UIScreen *screen in [UIScreen screens]) + NSMutableArray *screens = [[[UIScreen screens] mutableCopy] autorelease]; + if (![screens containsObject:[UIScreen mainScreen]]) { + // Fallback for iOS 7.1 (QTBUG-42345) + [screens insertObject:[UIScreen mainScreen] atIndex:0]; + } + + for (UIScreen *screen in screens) addScreen(new QIOSScreen(screen)); // Depends on a primary screen being present diff --git a/src/plugins/platforms/ios/qiosscreen.h b/src/plugins/platforms/ios/qiosscreen.h index 7987ef82d5..7aa62b9190 100644 --- a/src/plugins/platforms/ios/qiosscreen.h +++ b/src/plugins/platforms/ios/qiosscreen.h @@ -63,6 +63,7 @@ public: void setOrientationUpdateMask(Qt::ScreenOrientations mask); UIScreen *uiScreen() const; + UIWindow *uiWindow() const; void updateProperties(); diff --git a/src/plugins/platforms/ios/qiosscreen.mm b/src/plugins/platforms/ios/qiosscreen.mm index e70b369b79..4af2a4965f 100644 --- a/src/plugins/platforms/ios/qiosscreen.mm +++ b/src/plugins/platforms/ios/qiosscreen.mm @@ -321,6 +321,11 @@ UIScreen *QIOSScreen::uiScreen() const return m_uiScreen; } +UIWindow *QIOSScreen::uiWindow() const +{ + return m_uiWindow; +} + #include "moc_qiosscreen.cpp" QT_END_NAMESPACE |