From d563f6142b9f319826ae68dbe630f1d865be29a0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tor=20Arne=20Vestb=C3=B8?= <tor.arne.vestbo@digia.com>
Date: Wed, 22 Oct 2014 13:21:34 +0200
Subject: [PATCH] iOS: Make QIOSTextInputResponder a proper first-responder
 during text input

Instead of faking it, by returning YES for isFirstResponder, which caused
issues when iOS would try to dismiss the keyboard by resigning the true
first-responder.

Change-Id: I816c4cf9c699d72995ce7968e1f1a4aa9c9c167e
Reviewed-by: Richard Moe Gustavsen <richard.gustavsen@digia.com>
---
 src/plugins/platforms/ios/qiosinputcontext.h  |  8 ++-
 src/plugins/platforms/ios/qiosinputcontext.mm | 43 ++++++++++++---
 .../platforms/ios/qiostextresponder.mm        | 54 ++++++++++++++++++-
 src/plugins/platforms/ios/quiview.mm          | 10 ++++
 4 files changed, 105 insertions(+), 10 deletions(-)

diff --git a/src/plugins/platforms/ios/qiosinputcontext.h b/src/plugins/platforms/ios/qiosinputcontext.h
index 8850bbf80e6..46fe35d884d 100644
--- a/src/plugins/platforms/ios/qiosinputcontext.h
+++ b/src/plugins/platforms/ios/qiosinputcontext.h
@@ -65,7 +65,8 @@ public:
 
     void showInputPanel();
     void hideInputPanel();
-    void hideVirtualKeyboard();
+
+    void clearCurrentFocusObject();
 
     bool isInputPanelVisible() const;
     void setFocusObject(QObject *object);
@@ -81,10 +82,15 @@ public:
 
     const ImeState &imeState() { return m_imeState; };
 
+    bool isReloadingInputViewsFromUpdate() const { return m_isReloadingInputViewsFromUpdate; }
+
+    static QIOSInputContext *instance();
+
 private:
     QIOSKeyboardListener *m_keyboardListener;
     QIOSTextInputResponder *m_textResponder;
     ImeState m_imeState;
+    bool m_isReloadingInputViewsFromUpdate;
 };
 
 QT_END_NAMESPACE
diff --git a/src/plugins/platforms/ios/qiosinputcontext.mm b/src/plugins/platforms/ios/qiosinputcontext.mm
index 70307f7f54a..c038628fd9c 100644
--- a/src/plugins/platforms/ios/qiosinputcontext.mm
+++ b/src/plugins/platforms/ios/qiosinputcontext.mm
@@ -44,6 +44,7 @@
 #import <UIKit/UIGestureRecognizerSubclass.h>
 
 #include "qiosglobal.h"
+#include "qiosintegration.h"
 #include "qiostextresponder.h"
 #include "qioswindow.h"
 #include "quiview.h"
@@ -206,7 +207,10 @@ static QUIView *focusView()
     CGPoint p = [[touches anyObject] locationInView:m_viewController.view.window];
     if (CGRectContainsPoint(m_keyboardEndRect, p)) {
         m_keyboardHiddenByGesture = YES;
-        m_context->hideVirtualKeyboard();
+
+        UIResponder *firstResponder = [UIResponder currentFirstResponder];
+        Q_ASSERT([firstResponder isKindOfClass:[QIOSTextInputResponder class]]);
+        [firstResponder resignFirstResponder];
     }
 
     [super touchesMoved:touches withEvent:event];
@@ -279,10 +283,16 @@ Qt::InputMethodQueries ImeState::update(Qt::InputMethodQueries properties)
 
 // -------------------------------------------------------------------------
 
+QIOSInputContext *QIOSInputContext::instance()
+{
+    return static_cast<QIOSInputContext *>(QIOSIntegration::instance()->inputContext());
+}
+
 QIOSInputContext::QIOSInputContext()
     : QPlatformInputContext()
     , m_keyboardListener([[QIOSKeyboardListener alloc] initWithQIOSInputContext:this])
     , m_textResponder(0)
+    , m_isReloadingInputViewsFromUpdate(false)
 {
     if (isQtApplication())
         connect(qGuiApp->inputMethod(), &QInputMethod::cursorRectangleChanged, this, &QIOSInputContext::cursorRectangleChanged);
@@ -310,9 +320,10 @@ void QIOSInputContext::hideInputPanel()
     // No-op, keyboard controlled fully by platform based on focus
 }
 
-void QIOSInputContext::hideVirtualKeyboard()
+void QIOSInputContext::clearCurrentFocusObject()
 {
-    static_cast<QWindowPrivate *>(QObjectPrivate::get(qApp->focusWindow()))->clearFocusObject();
+    if (QWindow *focusWindow = qApp->focusWindow())
+        static_cast<QWindowPrivate *>(QObjectPrivate::get(focusWindow))->clearFocusObject();
 }
 
 bool QIOSInputContext::isInputPanelVisible() const
@@ -452,12 +463,24 @@ void QIOSInputContext::update(Qt::InputMethodQueries updatedProperties)
         updatedProperties |= (Qt::ImHints | Qt::ImPlatformData);
     }
 
+    qImDebug() << "fw =" << qApp->focusWindow() << "fo =" << qApp->focusObject();
+
     Qt::InputMethodQueries changedProperties = m_imeState.update(updatedProperties);
     if (changedProperties & (Qt::ImEnabled | Qt::ImHints | Qt::ImPlatformData)) {
         // Changes to enablement or hints require virtual keyboard reconfigure
-        [m_textResponder release];
-        m_textResponder = [[QIOSTextInputResponder alloc] initWithInputContext:this];
-        [m_textResponder reloadInputViews];
+
+        qImDebug() << "changed IM properties" << changedProperties << "require keyboard reconfigure";
+
+        if (inputMethodAccepted()) {
+            qImDebug() << "replacing text responder with new text responder";
+            [m_textResponder autorelease];
+            m_textResponder = [[QIOSTextInputResponder alloc] initWithInputContext:this];
+            [m_textResponder becomeFirstResponder];
+        } else {
+            qImDebug() << "IM not enabled, reloading input views";
+            QScopedValueRollback<bool> recursionGuard(m_isReloadingInputViewsFromUpdate, true);
+            [[UIResponder currentFirstResponder] reloadInputViews];
+        }
     } else {
         [m_textResponder notifyInputDelegate:changedProperties];
     }
@@ -497,6 +520,12 @@ void QIOSInputContext::commit()
 @implementation QUIView (InputMethods)
 - (void)reloadInputViews
 {
-    qApp->inputMethod()->reset();
+    if (QIOSInputContext::instance()->isReloadingInputViewsFromUpdate()) {
+        qImDebug() << "preventing recursion by reloading super";
+        [super reloadInputViews];
+    } else {
+        qImDebug() << "reseting input methods";
+        qApp->inputMethod()->reset();
+    }
 }
 @end
diff --git a/src/plugins/platforms/ios/qiostextresponder.mm b/src/plugins/platforms/ios/qiostextresponder.mm
index 54362cde7a3..b809fc4b512 100644
--- a/src/plugins/platforms/ios/qiostextresponder.mm
+++ b/src/plugins/platforms/ios/qiostextresponder.mm
@@ -218,11 +218,61 @@
     [super dealloc];
 }
 
-- (BOOL)isFirstResponder
+- (BOOL)canBecomeFirstResponder
 {
     return YES;
 }
 
+- (BOOL)becomeFirstResponder
+{
+    FirstResponderCandidate firstResponderCandidate(self);
+
+    qImDebug() << "self:" << self << "first:" << [UIResponder currentFirstResponder];
+
+    if (![super becomeFirstResponder]) {
+        qImDebug() << self << "was not allowed to become first responder";
+        return NO;
+    }
+
+    qImDebug() << self << "became first responder";
+
+    return YES;
+}
+
+- (BOOL)resignFirstResponder
+{
+    qImDebug() << "self:" << self << "first:" << [UIResponder currentFirstResponder];
+
+    // Don't allow activation events of the window that we're doing text on behalf on
+    // to steal responder.
+    if (FirstResponderCandidate::currentCandidate() == [self nextResponder]) {
+        qImDebug() << "not allowing parent window to steal responder";
+        return NO;
+    }
+
+    if (![super resignFirstResponder])
+        return NO;
+
+    qImDebug() << self << "resigned first responder";
+
+    // Dismissing the keyboard will trigger resignFirstResponder, but so will
+    // a regular responder transfer to another window. In the former case, iOS
+    // will set the new first-responder to our next-responder, and in the latter
+    // case we'll have an active responder candidate.
+    if ([UIResponder currentFirstResponder] == [self nextResponder]) {
+        // We have resigned the keyboard, and transferred back to the parent view, so unset focus object
+        Q_ASSERT(!FirstResponderCandidate::currentCandidate());
+        qImDebug() << "keyboard was closed, clearing focus object";
+        m_inputContext->clearCurrentFocusObject();
+    } else {
+        // We've lost responder status because another window was made active
+        Q_ASSERT(FirstResponderCandidate::currentCandidate());
+    }
+
+    return YES;
+}
+
+
 - (UIResponder*)nextResponder
 {
     return qApp->focusWindow() ?
@@ -577,7 +627,7 @@
 
         Qt::InputMethodHints imeHints = static_cast<Qt::InputMethodHints>([self imValue:Qt::ImHints].toUInt());
         if (!(imeHints & Qt::ImhMultiLine))
-            m_inputContext->hideVirtualKeyboard();
+            [self resignFirstResponder];
 
         return;
     }
diff --git a/src/plugins/platforms/ios/quiview.mm b/src/plugins/platforms/ios/quiview.mm
index 33e5b955e3a..c4b92618b1e 100644
--- a/src/plugins/platforms/ios/quiview.mm
+++ b/src/plugins/platforms/ios/quiview.mm
@@ -44,6 +44,7 @@
 #include "qiosglobal.h"
 #include "qiosintegration.h"
 #include "qiosviewcontroller.h"
+#include "qiostextresponder.h"
 #include "qioswindow.h"
 #include "qiosmenu.h"
 
@@ -222,6 +223,15 @@
     if ([responder isKindOfClass:[QUIView class]])
         return NO;
 
+    // Nor do we want to deactivate the Qt window if the new responder
+    // is temporarily handling text input on behalf of a Qt window.
+    if ([responder isKindOfClass:[QIOSTextInputResponder class]]) {
+        while ((responder = [responder nextResponder])) {
+            if ([responder isKindOfClass:[QUIView class]])
+                return NO;
+        }
+    }
+
     return YES;
 }
 
-- 
GitLab