summaryrefslogtreecommitdiffstats
path: root/src/plugins/platforms/ios
diff options
context:
space:
mode:
Diffstat (limited to 'src/plugins/platforms/ios')
-rw-r--r--src/plugins/platforms/ios/qiosinputcontext.h26
-rw-r--r--src/plugins/platforms/ios/qiosinputcontext.mm300
-rw-r--r--src/plugins/platforms/ios/qiosintegration.mm8
-rw-r--r--src/plugins/platforms/ios/qiosscreen.h1
-rw-r--r--src/plugins/platforms/ios/qiosscreen.mm5
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