diff options
Diffstat (limited to 'src/plugins/platforms/ios')
-rw-r--r-- | src/plugins/platforms/ios/ios.pro | 6 | ||||
-rw-r--r-- | src/plugins/platforms/ios/qiosglobal.mm | 8 | ||||
-rw-r--r-- | src/plugins/platforms/ios/qiosinputcontext.h | 13 | ||||
-rw-r--r-- | src/plugins/platforms/ios/qiosinputcontext.mm | 145 | ||||
-rw-r--r-- | src/plugins/platforms/ios/qiosviewcontroller.mm | 8 | ||||
-rw-r--r-- | src/plugins/platforms/ios/qioswindow.h | 2 | ||||
-rw-r--r-- | src/plugins/platforms/ios/qioswindow.mm | 184 | ||||
-rw-r--r-- | src/plugins/platforms/ios/quiview.h (renamed from src/plugins/platforms/ios/qiossoftwareinputhandler.h) | 59 | ||||
-rw-r--r-- | src/plugins/platforms/ios/quiview_textinput.mm | 557 |
9 files changed, 725 insertions, 257 deletions
diff --git a/src/plugins/platforms/ios/ios.pro b/src/plugins/platforms/ios/ios.pro index 72716e6a4c..175cc3f8bd 100644 --- a/src/plugins/platforms/ios/ios.pro +++ b/src/plugins/platforms/ios/ios.pro @@ -36,6 +36,8 @@ HEADERS = \ qiosinputcontext.h \ qiostheme.h \ qiosglobal.h \ - qiosservices.h + qiosservices.h \ + quiview.h -#HEADERS = qiossoftwareinputhandler.h +OTHER_FILES = \ + quiview_textinput.mm diff --git a/src/plugins/platforms/ios/qiosglobal.mm b/src/plugins/platforms/ios/qiosglobal.mm index 8dd690f301..2ce064582e 100644 --- a/src/plugins/platforms/ios/qiosglobal.mm +++ b/src/plugins/platforms/ios/qiosglobal.mm @@ -86,10 +86,10 @@ Qt::ScreenOrientation toQtScreenOrientation(UIDeviceOrientation uiDeviceOrientat qtOrientation = Qt::InvertedPortraitOrientation; break; case UIDeviceOrientationLandscapeLeft: - qtOrientation = Qt::InvertedLandscapeOrientation; + qtOrientation = Qt::LandscapeOrientation; break; case UIDeviceOrientationLandscapeRight: - qtOrientation = Qt::LandscapeOrientation; + qtOrientation = Qt::InvertedLandscapeOrientation; break; case UIDeviceOrientationFaceUp: case UIDeviceOrientationFaceDown: @@ -108,10 +108,10 @@ UIDeviceOrientation fromQtScreenOrientation(Qt::ScreenOrientation qtOrientation) UIDeviceOrientation uiOrientation; switch (qtOrientation) { case Qt::LandscapeOrientation: - uiOrientation = UIDeviceOrientationLandscapeRight; + uiOrientation = UIDeviceOrientationLandscapeLeft; break; case Qt::InvertedLandscapeOrientation: - uiOrientation = UIDeviceOrientationLandscapeLeft; + uiOrientation = UIDeviceOrientationLandscapeRight; break; case Qt::InvertedPortraitOrientation: uiOrientation = UIDeviceOrientationPortraitUpsideDown; diff --git a/src/plugins/platforms/ios/qiosinputcontext.h b/src/plugins/platforms/ios/qiosinputcontext.h index 533ba686e1..13255ada56 100644 --- a/src/plugins/platforms/ios/qiosinputcontext.h +++ b/src/plugins/platforms/ios/qiosinputcontext.h @@ -50,6 +50,7 @@ QT_BEGIN_NAMESPACE @class QIOSKeyboardListener; +@class QUIView; class QIOSInputContext : public QPlatformInputContext { @@ -64,14 +65,18 @@ public: void setFocusObject(QObject *object); void focusWindowChanged(QWindow *focusWindow); - void scrollRootView(); + void cursorRectangleChanged(); + void scrollToCursor(); + void scroll(int y); + + void update(Qt::InputMethodQueries); + void reset(); + void commit(); private: QIOSKeyboardListener *m_keyboardListener; - UIView<UIKeyInput> *m_focusView; - QTransform m_inputItemTransform; + QUIView *m_focusView; 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 39a22f367e..d426028c46 100644 --- a/src/plugins/platforms/ios/qiosinputcontext.mm +++ b/src/plugins/platforms/ios/qiosinputcontext.mm @@ -42,6 +42,7 @@ #include "qiosglobal.h" #include "qiosinputcontext.h" #include "qioswindow.h" +#include "quiview.h" #include <QGuiApplication> @interface QIOSKeyboardListener : NSObject { @@ -49,6 +50,7 @@ QIOSInputContext *m_context; BOOL m_keyboardVisible; BOOL m_keyboardVisibleAndDocked; + BOOL m_ignoreKeyboardChanges; QRectF m_keyboardRect; QRectF m_keyboardEndRect; NSTimeInterval m_duration; @@ -66,6 +68,7 @@ m_context = context; m_keyboardVisible = NO; m_keyboardVisibleAndDocked = NO; + m_ignoreKeyboardChanges = NO; m_duration = 0; m_curve = UIViewAnimationCurveEaseOut; m_viewController = 0; @@ -128,6 +131,8 @@ - (void) keyboardDidChangeFrame:(NSNotification *)notification { + if (m_ignoreKeyboardChanges) + return; m_keyboardRect = [self getKeyboardRect:notification]; m_context->emitKeyboardRectChanged(); @@ -140,11 +145,13 @@ // 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(); + m_context->scrollToCursor(); } - (void) keyboardWillShow:(NSNotification *)notification { + if (m_ignoreKeyboardChanges) + return; // Note that UIKeyboardWillShowNotification is only sendt when the keyboard is docked. m_keyboardVisibleAndDocked = YES; m_keyboardEndRect = [self getKeyboardRect:notification]; @@ -152,15 +159,17 @@ m_duration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; m_curve = UIViewAnimationCurve([notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue] << 16); } - m_context->scrollRootView(); + m_context->scrollToCursor(); } - (void) keyboardWillHide:(NSNotification *)notification { + if (m_ignoreKeyboardChanges) + return; // Note that UIKeyboardWillHideNotification is also sendt when the keyboard is undocked. m_keyboardVisibleAndDocked = NO; m_keyboardEndRect = [self getKeyboardRect:notification]; - m_context->scrollRootView(); + m_context->scroll(0); } @end @@ -170,10 +179,9 @@ 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->inputMethod(), &QInputMethod::cursorRectangleChanged, this, &QIOSInputContext::cursorRectangleChanged); connect(qGuiApp, &QGuiApplication::focusWindowChanged, this, &QIOSInputContext::focusWindowChanged); } @@ -216,76 +224,99 @@ bool QIOSInputContext::isInputPanelVisible() const return m_keyboardListener->m_keyboardVisible; } -void QIOSInputContext::setFocusObject(QObject *) +void QIOSInputContext::setFocusObject(QObject *focusObject) { - m_inputItemTransform = qApp->inputMethod()->inputItemTransform(); - - if (!m_focusView || !m_focusView.isFirstResponder) + if (!focusObject || !m_focusView || !m_focusView.isFirstResponder) { + scroll(0); 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]; + reset(); + + if (m_keyboardListener->m_keyboardVisibleAndDocked) + scrollToCursor(); } void QIOSInputContext::focusWindowChanged(QWindow *focusWindow) { - UIView<UIKeyInput> *view = reinterpret_cast<UIView<UIKeyInput> *>(focusWindow->handle()->winId()); + QUIView *view = focusWindow ? reinterpret_cast<QUIView *>(focusWindow->handle()->winId()) : 0; if ([m_focusView isFirstResponder]) [view becomeFirstResponder]; [m_focusView release]; m_focusView = [view retain]; + + if (view.window != m_keyboardListener->m_viewController.view) + scroll(0); } -void QIOSInputContext::scrollRootView() +void QIOSInputContext::cursorRectangleChanged() { - // 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) + if (!m_keyboardListener->m_keyboardVisibleAndDocked) 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: + // Check if the cursor has changed position inside the input item. Since + // qApp->inputMethod()->cursorRectangle() will also change when the input item + // itself moves, we need to ask the focus object for ImCursorRectangle: + static QPoint prevCursor; + QInputMethodQueryEvent queryEvent(Qt::ImCursorRectangle); + QCoreApplication::sendEvent(qApp->focusObject(), &queryEvent); + QPoint cursor = queryEvent.value(Qt::ImCursorRectangle).toRect().topLeft(); + if (cursor != prevCursor) + scrollToCursor(); + prevCursor = cursor; +} + +void QIOSInputContext::scrollToCursor() +{ + if (!isQtApplication() || !m_focusView) 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(m_focusView.qwindow->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 (view.window != m_focusView.window) + return; - 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]; - } + const int margin = 20; + QRectF translatedCursorPos = qApp->inputMethod()->cursorRectangle(); + translatedCursorPos.translate(m_focusView.qwindow->geometry().topLeft()); + qreal keyboardY = m_keyboardListener->m_keyboardEndRect.y(); + int statusBarY = qGuiApp->primaryScreen()->availableGeometry().y(); + + scroll((translatedCursorPos.bottomLeft().y() < keyboardY - margin) ? 0 + : qMin(view.bounds.size.height - keyboardY, translatedCursorPos.y() - statusBarY - margin)); } + +void QIOSInputContext::scroll(int y) +{ + // Scroll the view the same way a UIScrollView + // works: by changing bounds.origin: + UIView *view = m_keyboardListener->m_viewController.view; + if (y == view.bounds.origin.y) + return; + + CGRect newBounds = view.bounds; + newBounds.origin.y = y; + [UIView animateWithDuration:m_keyboardListener->m_duration delay:0 + options:m_keyboardListener->m_curve + animations:^{ view.bounds = newBounds; } + completion:0]; +} + +void QIOSInputContext::update(Qt::InputMethodQueries query) +{ + [m_focusView updateInputMethodWithQuery:query]; +} + +void QIOSInputContext::reset() +{ + // Since the call to reset will cause a 'keyboardWillHide' + // notification to be sendt, we block keyboard nofifications to avoid artifacts: + m_keyboardListener->m_ignoreKeyboardChanges = true; + [m_focusView reset]; + m_keyboardListener->m_ignoreKeyboardChanges = false; +} + +void QIOSInputContext::commit() +{ + [m_focusView commit]; +} + diff --git a/src/plugins/platforms/ios/qiosviewcontroller.mm b/src/plugins/platforms/ios/qiosviewcontroller.mm index 0a6a00b753..2fe679fc20 100644 --- a/src/plugins/platforms/ios/qiosviewcontroller.mm +++ b/src/plugins/platforms/ios/qiosviewcontroller.mm @@ -44,6 +44,9 @@ #include <QtGui/QGuiApplication> #include <QtGui/QWindow> #include <QtGui/QScreen> + +#include <QtGui/private/qwindow_p.h> + #include "qiosscreen.h" #include "qiosglobal.h" #include "qioswindow.h" @@ -105,11 +108,10 @@ if (hiddenFromPlist) return YES; QWindow *focusWindow = QGuiApplication::focusWindow(); - if (!focusWindow || !focusWindow->handle()) + if (!focusWindow) return [UIApplication sharedApplication].statusBarHidden; - QWindow *topLevel = static_cast<QIOSWindow *>(focusWindow->handle())->topLevelWindow(); - return topLevel->windowState() == Qt::WindowFullScreen; + return qt_window_private(focusWindow)->topLevelWindow()->windowState() == Qt::WindowFullScreen; } @end diff --git a/src/plugins/platforms/ios/qioswindow.h b/src/plugins/platforms/ios/qioswindow.h index 8a5eb589d2..fc99543aa6 100644 --- a/src/plugins/platforms/ios/qioswindow.h +++ b/src/plugins/platforms/ios/qioswindow.h @@ -87,8 +87,6 @@ public: WId winId() const { return WId(m_view); }; - QWindow *topLevelWindow() const; - private: void applyGeometry(const QRect &rect); diff --git a/src/plugins/platforms/ios/qioswindow.mm b/src/plugins/platforms/ios/qioswindow.mm index 7a0ff055ec..7d5c507972 100644 --- a/src/plugins/platforms/ios/qioswindow.mm +++ b/src/plugins/platforms/ios/qioswindow.mm @@ -41,6 +41,7 @@ #include "qiosglobal.h" #include "qioswindow.h" +#include "quiview.h" #include "qioscontext.h" #include "qiosinputcontext.h" #include "qiosscreen.h" @@ -58,33 +59,19 @@ #include <QtDebug> -@interface QUIView : UIView <UIKeyInput> -{ -@public - UITextAutocapitalizationType autocapitalizationType; - UITextAutocorrectionType autocorrectionType; - BOOL enablesReturnKeyAutomatically; - UIKeyboardAppearance keyboardAppearance; - UIKeyboardType keyboardType; - UIReturnKeyType returnKeyType; - BOOL secureTextEntry; - QIOSWindow *m_qioswindow; - QHash<UITouch *, QWindowSystemInterface::TouchPoint> m_activeTouches; - int m_nextTouchId; -} - -@property(nonatomic) UITextAutocapitalizationType autocapitalizationType; -@property(nonatomic) UITextAutocorrectionType autocorrectionType; -@property(nonatomic) BOOL enablesReturnKeyAutomatically; -@property(nonatomic) UIKeyboardAppearance keyboardAppearance; -@property(nonatomic) UIKeyboardType keyboardType; -@property(nonatomic) UIReturnKeyType returnKeyType; -@property(nonatomic, getter=isSecureTextEntry) BOOL secureTextEntry; - -@end +// Include category as an alternative to using -ObjC (Apple QA1490) +#include "quiview_textinput.mm" @implementation QUIView +@synthesize autocapitalizationType; +@synthesize autocorrectionType; +@synthesize enablesReturnKeyAutomatically; +@synthesize keyboardAppearance; +@synthesize keyboardType; +@synthesize returnKeyType; +@synthesize secureTextEntry; + + (Class)layerClass { return [CAEAGLLayer class]; @@ -112,6 +99,7 @@ self.hidden = YES; self.multipleTouchEnabled = YES; + m_inSendEventToFocusObject = NO; } return self; @@ -259,6 +247,14 @@ m_activeTouches[touch].id = m_nextTouchId++; } + if (m_activeTouches.size() == 1) { + QPlatformWindow *topLevel = m_qioswindow; + while (QPlatformWindow *p = topLevel->parent()) + topLevel = p; + if (topLevel->window() != QGuiApplication::focusWindow()) + topLevel->requestActivateWindow(); + } + [self updateTouchList:touches withState:Qt::TouchPointPressed]; [self sendTouchEventWithTimestamp:ulong(event.timestamp * 1000)]; } @@ -271,14 +267,6 @@ - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { - QWindow *window = m_qioswindow->window(); - if (window != QGuiApplication::focusWindow() && m_activeTouches.size() == 1) { - // Activate the touched window if the last touch was released inside it: - UITouch *touch = static_cast<UITouch *>([[touches allObjects] lastObject]); - if (CGRectContainsPoint([self bounds], [touch locationInView:self])) - m_qioswindow->requestActivateWindow(); - } - [self updateTouchList:touches withState:Qt::TouchPointReleased]; [self sendTouchEventWithTimestamp:ulong(event.timestamp * 1000)]; @@ -314,109 +302,6 @@ QWindowSystemInterface::flushWindowSystemEvents(); } -@synthesize autocapitalizationType; -@synthesize autocorrectionType; -@synthesize enablesReturnKeyAutomatically; -@synthesize keyboardAppearance; -@synthesize keyboardType; -@synthesize returnKeyType; -@synthesize secureTextEntry; - -- (BOOL)canBecomeFirstResponder -{ - return YES; -} - -- (BOOL)becomeFirstResponder -{ - // Note: QIOSInputContext controls our first responder status based on - // whether or not the keyboard should be open or closed. - [self updateTextInputTraits]; - return [super becomeFirstResponder]; -} - -- (BOOL)resignFirstResponder -{ - // 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: - if (m_qioswindow) - static_cast<QWindowPrivate *>(QObjectPrivate::get(m_qioswindow->window()))->clearFocusObject(); - return [super resignFirstResponder]; -} - -- (BOOL)hasText -{ - return YES; -} - -- (void)insertText:(NSString *)text -{ - QString string = QString::fromUtf8([text UTF8String]); - int key = 0; - if ([text isEqualToString:@"\n"]) { - key = (int)Qt::Key_Return; - if (self.returnKeyType == UIReturnKeyDone) - [self resignFirstResponder]; - } - - // Send key event to window system interface - QWindowSystemInterface::handleKeyEvent( - 0, QEvent::KeyPress, key, Qt::NoModifier, string, false, int(string.length())); - QWindowSystemInterface::handleKeyEvent( - 0, QEvent::KeyRelease, key, Qt::NoModifier, string, false, int(string.length())); -} - -- (void)deleteBackward -{ - // Send key event to window system interface - QWindowSystemInterface::handleKeyEvent( - 0, QEvent::KeyPress, (int)Qt::Key_Backspace, Qt::NoModifier); - QWindowSystemInterface::handleKeyEvent( - 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) @@ -487,7 +372,7 @@ void QIOSWindow::setVisible(bool visible) m_view.hidden = !visible; [m_view setNeedsDisplay]; - if (!isQtApplication()) + if (!isQtApplication() || !window()->isTopLevel()) return; // Since iOS doesn't do window management the way a Qt application @@ -503,18 +388,16 @@ void QIOSWindow::setVisible(bool visible) if (visible) { requestActivateWindow(); - - if (window()->isTopLevel()) - static_cast<QIOSScreen *>(screen())->updateStatusBarVisibility(); - + static_cast<QIOSScreen *>(screen())->updateStatusBarVisibility(); } else { // Activate top-most visible QWindow: 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) { - if (QWindow *window = view.qwindow) { - static_cast<QIOSWindow *>(window->handle())->requestActivateWindow(); + QWindow *w = view.qwindow; + if (w && w->isTopLevel()) { + static_cast<QIOSWindow *>(w->handle())->requestActivateWindow(); break; } } @@ -636,23 +519,6 @@ void QIOSWindow::setParent(const QPlatformWindow *parentWindow) } } -QWindow *QIOSWindow::topLevelWindow() const -{ - QWindow *window = this->window(); - while (window) { - QWindow *parent = window->parent(); - if (!parent) - parent = window->transientParent(); - - if (!parent) - break; - - window = parent; - } - - return window; -} - void QIOSWindow::requestActivateWindow() { // Note that several windows can be active at the same time if they exist in the same diff --git a/src/plugins/platforms/ios/qiossoftwareinputhandler.h b/src/plugins/platforms/ios/quiview.h index 5dad6b8d86..575dedab89 100644 --- a/src/plugins/platforms/ios/qiossoftwareinputhandler.h +++ b/src/plugins/platforms/ios/quiview.h @@ -1,6 +1,6 @@ /**************************************************************************** ** -** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). ** Contact: http://www.qt-project.org/legal ** ** This file is part of the plugins of the Qt Toolkit. @@ -39,33 +39,40 @@ ** ****************************************************************************/ -#ifndef QIOSSOFTWAREINPUTHANDLER_H -#define QIOSSOFTWAREINPUTHANDLER_H +#import <UIKit/UIKit.h> +#include "qioswindow.h" -#include <QtCore/QObject> -#include <QtCore/QPointer> -#include <QtWidgets/QWidget> - -QT_BEGIN_NAMESPACE - -class QIOSSoftwareInputHandler : public QObject +@interface QUIView : UIView { - Q_OBJECT - -public: - QIOSSoftwareInputHandler() : m_CurrentFocusWidget(0), m_CurrentFocusObject(0) {} - bool eventFilter(QObject *obj, QEvent *event); - -private slots: - void activeFocusChanged(bool focus); - -private: - bool closeSoftwareInputPanel(QWidget *widget); +@public + UITextAutocapitalizationType autocapitalizationType; + UITextAutocorrectionType autocorrectionType; + BOOL enablesReturnKeyAutomatically; + UIKeyboardAppearance keyboardAppearance; + UIKeyboardType keyboardType; + UIReturnKeyType returnKeyType; + BOOL secureTextEntry; + QIOSWindow *m_qioswindow; + QHash<UITouch *, QWindowSystemInterface::TouchPoint> m_activeTouches; + int m_nextTouchId; + QString m_markedText; + BOOL m_inSendEventToFocusObject; +} - QPointer<QWidget> m_currentFocusWidget; - QPointer<QObject> m_currentFocusObject; -}; +@property(nonatomic, assign) id<UITextInputDelegate> inputDelegate; +@property(nonatomic) UITextAutocapitalizationType autocapitalizationType; +@property(nonatomic) UITextAutocorrectionType autocorrectionType; +@property(nonatomic) UITextSpellCheckingType spellCheckingType; +@property(nonatomic) BOOL enablesReturnKeyAutomatically; +@property(nonatomic) UIKeyboardAppearance keyboardAppearance; +@property(nonatomic) UIKeyboardType keyboardType; +@property(nonatomic) UIReturnKeyType returnKeyType; +@property(nonatomic, getter=isSecureTextEntry) BOOL secureTextEntry; -QT_END_NAMESPACE +@end -#endif +@interface QUIView (TextInput) <UITextInput> +- (void)updateInputMethodWithQuery:(Qt::InputMethodQueries)query; +- (void)reset; +- (void)commit; +@end diff --git a/src/plugins/platforms/ios/quiview_textinput.mm b/src/plugins/platforms/ios/quiview_textinput.mm new file mode 100644 index 0000000000..d0088d415a --- /dev/null +++ b/src/plugins/platforms/ios/quiview_textinput.mm @@ -0,0 +1,557 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the plugins of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QtGui/qtextformat.h> + +class StaticVariables +{ +public: + QInputMethodQueryEvent inputMethodQueryEvent; + QTextCharFormat markedTextFormat; + + StaticVariables() : inputMethodQueryEvent(Qt::ImQueryInput) + { + // There seems to be no way to query how the preedit text + // should be drawn. So we need to hard-code the color. + QSysInfo::MacVersion iosVersion = QSysInfo::MacintoshVersion; + if (iosVersion < QSysInfo::MV_IOS_7_0) + markedTextFormat.setBackground(QColor(235, 239, 247)); + else + markedTextFormat.setBackground(QColor(206, 221, 238)); + } +}; + +Q_GLOBAL_STATIC(StaticVariables, staticVariables); + +// ------------------------------------------------------------------------- + +@interface QUITextPosition : UITextPosition +{ +} + +@property (nonatomic) NSUInteger index; ++ (QUITextPosition *)positionWithIndex:(NSUInteger)index; + +@end + +@implementation QUITextPosition + ++ (QUITextPosition *)positionWithIndex:(NSUInteger)index +{ + QUITextPosition *pos = [[QUITextPosition alloc] init]; + pos.index = index; + return [pos autorelease]; +} + +@end + +// ------------------------------------------------------------------------- + +@interface QUITextRange : UITextRange +{ +} + +@property (nonatomic) NSRange range; ++ (QUITextRange *)rangeWithNSRange:(NSRange)range; + +@end + +@implementation QUITextRange + ++ (QUITextRange *)rangeWithNSRange:(NSRange)nsrange +{ + QUITextRange *range = [[QUITextRange alloc] init]; + range.range = nsrange; + return [range autorelease]; +} + +- (UITextPosition *)start +{ + return [QUITextPosition positionWithIndex:self.range.location]; +} + +- (UITextPosition *)end +{ + return [QUITextPosition positionWithIndex:(self.range.location + self.range.length)]; +} + +- (NSRange) range +{ + return _range; +} + +-(BOOL)isEmpty +{ + return (self.range.length == 0); +} + +@end + +// ------------------------------------------------------------------------- + +@implementation QUIView (TextInput) + +- (BOOL)canBecomeFirstResponder +{ + return YES; +} + +- (BOOL)becomeFirstResponder +{ + // Note: QIOSInputContext controls our first responder status based on + // whether or not the keyboard should be open or closed. + [self updateTextInputTraits]; + return [super becomeFirstResponder]; +} + +- (BOOL)resignFirstResponder +{ + // 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: + if (m_qioswindow) + static_cast<QWindowPrivate *>(QObjectPrivate::get(m_qioswindow->window()))->clearFocusObject(); + return [super resignFirstResponder]; +} + +- (void)updateInputMethodWithQuery:(Qt::InputMethodQueries)query +{ + Q_UNUSED(query); + + QObject *focusObject = QGuiApplication::focusObject(); + if (!focusObject) + return; + + if (!m_inSendEventToFocusObject) { + if (query & (Qt::ImCursorPosition | Qt::ImAnchorPosition)) + [self.inputDelegate selectionWillChange:id<UITextInput>(self)]; + if (query & Qt::ImSurroundingText) + [self.inputDelegate textWillChange:id<UITextInput>(self)]; + } + + // Note that we ignore \a query, and instead update using Qt::ImQueryInput. This enables us to just + // store the event without copying out the result from the event each time. Besides, we seem to be + // called with Qt::ImQueryInput when only changing selection, and always if typing text. So there would + // not be any performance gain by only updating \a query. + staticVariables()->inputMethodQueryEvent = QInputMethodQueryEvent(Qt::ImQueryInput); + QCoreApplication::sendEvent(focusObject, &staticVariables()->inputMethodQueryEvent); + + if (!m_inSendEventToFocusObject) { + if (query & (Qt::ImCursorPosition | Qt::ImAnchorPosition)) + [self.inputDelegate selectionDidChange:id<UITextInput>(self)]; + if (query & Qt::ImSurroundingText) + [self.inputDelegate textDidChange:id<UITextInput>(self)]; + } +} + +- (void)sendEventToFocusObject:(QEvent &)e +{ + QObject *focusObject = QGuiApplication::focusObject(); + if (!focusObject) + return; + + // While sending the event, we will receive back updateInputMethodWithQuery calls. + // To not confuse iOS, we cannot not call textWillChange/textDidChange at that + // point since it will cause spell checking etc to fail. So we use a guard. + m_inSendEventToFocusObject = YES; + QCoreApplication::sendEvent(focusObject, &e); + m_inSendEventToFocusObject = NO; +} + +- (void)reset +{ + [self.inputDelegate textWillChange:id<UITextInput>(self)]; + [self setMarkedText:@"" selectedRange:NSMakeRange(0, 0)]; + [self updateInputMethodWithQuery:Qt::ImQueryInput]; + + if ([self isFirstResponder]) { + // There seem to be no way to inform that the keyboard needs to update (since + // text input traits might have changed). As a work-around, we quickly resign + // first responder status just to reassign it again: + [super resignFirstResponder]; + [self updateTextInputTraits]; + [super becomeFirstResponder]; + } + [self.inputDelegate textDidChange:id<UITextInput>(self)]; +} + +- (void)commit +{ + [self.inputDelegate textWillChange:id<UITextInput>(self)]; + [self unmarkText]; + [self.inputDelegate textDidChange:id<UITextInput>(self)]; +} + +- (QVariant)imValue:(Qt::InputMethodQuery)query +{ + return staticVariables()->inputMethodQueryEvent.value(query); +} + +-(id<UITextInputTokenizer>)tokenizer +{ + return [[[UITextInputStringTokenizer alloc] initWithTextInput:id<UITextInput>(self)] autorelease]; +} + +-(UITextPosition *)beginningOfDocument +{ + return [QUITextPosition positionWithIndex:0]; +} + +-(UITextPosition *)endOfDocument +{ + int endPosition = [self imValue:Qt::ImSurroundingText].toString().length(); + return [QUITextPosition positionWithIndex:endPosition]; +} + +- (void)setSelectedTextRange:(UITextRange *)range +{ + QUITextRange *r = static_cast<QUITextRange *>(range); + QList<QInputMethodEvent::Attribute> attrs; + attrs << QInputMethodEvent::Attribute(QInputMethodEvent::Selection, r.range.location, r.range.length, 0); + QInputMethodEvent e(m_markedText, attrs); + [self sendEventToFocusObject:e]; +} + +- (UITextRange *)selectedTextRange { + int cursorPos = [self imValue:Qt::ImCursorPosition].toInt(); + int anchorPos = [self imValue:Qt::ImAnchorPosition].toInt(); + return [QUITextRange rangeWithNSRange:NSMakeRange(cursorPos, (anchorPos - cursorPos))]; +} + +- (NSString *)textInRange:(UITextRange *)range +{ + int s = static_cast<QUITextPosition *>([range start]).index; + int e = static_cast<QUITextPosition *>([range end]).index; + return [self imValue:Qt::ImSurroundingText].toString().mid(s, e - s).toNSString(); +} + +- (void)setMarkedText:(NSString *)markedText selectedRange:(NSRange)selectedRange +{ + Q_UNUSED(selectedRange); + + m_markedText = markedText ? QString::fromNSString(markedText) : QString(); + + QList<QInputMethodEvent::Attribute> attrs; + attrs << QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat, 0, markedText.length, staticVariables()->markedTextFormat); + QInputMethodEvent e(m_markedText, attrs); + [self sendEventToFocusObject:e]; +} + +- (void)unmarkText +{ + if (m_markedText.isEmpty()) + return; + + QInputMethodEvent e; + e.setCommitString(m_markedText); + [self sendEventToFocusObject:e]; + + m_markedText.clear(); +} + +- (NSComparisonResult)comparePosition:(UITextPosition *)position toPosition:(UITextPosition *)other +{ + int p = static_cast<QUITextPosition *>(position).index; + int o = static_cast<QUITextPosition *>(other).index; + if (p > o) + return NSOrderedAscending; + else if (p < o) + return NSOrderedDescending; + return NSOrderedSame; +} + +- (UITextRange *)markedTextRange { + return m_markedText.isEmpty() ? nil : [QUITextRange rangeWithNSRange:NSMakeRange(0, m_markedText.length())]; +} + +- (UITextRange *)textRangeFromPosition:(UITextPosition *)fromPosition toPosition:(UITextPosition *)toPosition +{ + int f = static_cast<QUITextPosition *>(fromPosition).index; + int t = static_cast<QUITextPosition *>(toPosition).index; + return [QUITextRange rangeWithNSRange:NSMakeRange(f, t - f)]; +} + +- (UITextPosition *)positionFromPosition:(UITextPosition *)position offset:(NSInteger)offset +{ + int p = static_cast<QUITextPosition *>(position).index; + return [QUITextPosition positionWithIndex:p + offset]; +} + +- (UITextPosition *)positionFromPosition:(UITextPosition *)position inDirection:(UITextLayoutDirection)direction offset:(NSInteger)offset +{ + int p = static_cast<QUITextPosition *>(position).index; + return [QUITextPosition positionWithIndex:(direction == UITextLayoutDirectionRight ? p + offset : p - offset)]; +} + +- (UITextPosition *)positionWithinRange:(UITextRange *)range farthestInDirection:(UITextLayoutDirection)direction +{ + NSRange r = static_cast<QUITextRange *>(range).range; + if (direction == UITextLayoutDirectionRight) + return [QUITextPosition positionWithIndex:r.location + r.length]; + return [QUITextPosition positionWithIndex:r.location]; +} + +- (NSInteger)offsetFromPosition:(UITextPosition *)fromPosition toPosition:(UITextPosition *)toPosition +{ + int f = static_cast<QUITextPosition *>(fromPosition).index; + int t = static_cast<QUITextPosition *>(toPosition).index; + return t - f; +} + +- (UIView *)textInputView +{ + // iOS expects rects we return from other UITextInput methods + // to be relative to the view this method returns. + // Since QInputMethod returns rects relative to the top level + // QWindow, that is also the view we need to return. + QPlatformWindow *topLevel = m_qioswindow; + while (QPlatformWindow *p = topLevel->parent()) + topLevel = p; + return reinterpret_cast<UIView *>(topLevel->winId()); +} + +- (CGRect)firstRectForRange:(UITextRange *)range +{ + QObject *focusObject = QGuiApplication::focusObject(); + if (!focusObject) + return CGRectZero; + + // Using a work-around to get the current rect until + // a better API is in place: + if (!m_markedText.isEmpty()) + return CGRectZero; + + int cursorPos = [self imValue:Qt::ImCursorPosition].toInt(); + int anchorPos = [self imValue:Qt::ImAnchorPosition].toInt(); + + NSRange r = static_cast<QUITextRange*>(range).range; + QList<QInputMethodEvent::Attribute> attrs; + attrs << QInputMethodEvent::Attribute(QInputMethodEvent::Selection, r.location, 0, 0); + QInputMethodEvent e(m_markedText, attrs); + [self sendEventToFocusObject:e]; + QRectF startRect = qApp->inputMethod()->cursorRectangle(); + + attrs = QList<QInputMethodEvent::Attribute>(); + attrs << QInputMethodEvent::Attribute(QInputMethodEvent::Selection, r.location + r.length, 0, 0); + e = QInputMethodEvent(m_markedText, attrs); + [self sendEventToFocusObject:e]; + QRectF endRect = qApp->inputMethod()->cursorRectangle(); + + if (cursorPos != int(r.location + r.length) || cursorPos != anchorPos) { + attrs = QList<QInputMethodEvent::Attribute>(); + attrs << QInputMethodEvent::Attribute(QInputMethodEvent::Selection, cursorPos, (cursorPos - anchorPos), 0); + e = QInputMethodEvent(m_markedText, attrs); + [self sendEventToFocusObject:e]; + } + + return toCGRect(startRect.united(endRect)); +} + +- (CGRect)caretRectForPosition:(UITextPosition *)position +{ + Q_UNUSED(position); + // Assume for now that position is always the same as + // cursor index until a better API is in place: + QRectF cursorRect = qApp->inputMethod()->cursorRectangle(); + return toCGRect(cursorRect); +} + +- (void)replaceRange:(UITextRange *)range withText:(NSString *)text +{ + [self setSelectedTextRange:range]; + + QInputMethodEvent e; + e.setCommitString(QString::fromNSString(text)); + [self sendEventToFocusObject:e]; +} + +- (void)setBaseWritingDirection:(UITextWritingDirection)writingDirection forRange:(UITextRange *)range +{ + Q_UNUSED(writingDirection); + Q_UNUSED(range); + // Writing direction is handled by QLocale +} + +- (UITextWritingDirection)baseWritingDirectionForPosition:(UITextPosition *)position inDirection:(UITextStorageDirection)direction +{ + Q_UNUSED(position); + Q_UNUSED(direction); + if (QLocale::system().textDirection() == Qt::RightToLeft) + return UITextWritingDirectionRightToLeft; + return UITextWritingDirectionLeftToRight; +} + +- (UITextRange *)characterRangeByExtendingPosition:(UITextPosition *)position inDirection:(UITextLayoutDirection)direction +{ + int p = static_cast<QUITextPosition *>(position).index; + if (direction == UITextLayoutDirectionLeft) + return [QUITextRange rangeWithNSRange:NSMakeRange(0, p)]; + int l = [self imValue:Qt::ImSurroundingText].toString().length(); + return [QUITextRange rangeWithNSRange:NSMakeRange(p, l - p)]; +} + +- (UITextPosition *)closestPositionToPoint:(CGPoint)point +{ + // No API in Qt for determining this. Use sensible default instead: + Q_UNUSED(point); + return [QUITextPosition positionWithIndex:[self imValue:Qt::ImCursorPosition].toInt()]; +} + +- (UITextPosition *)closestPositionToPoint:(CGPoint)point withinRange:(UITextRange *)range +{ + // No API in Qt for determining this. Use sensible default instead: + Q_UNUSED(point); + Q_UNUSED(range); + return [QUITextPosition positionWithIndex:[self imValue:Qt::ImCursorPosition].toInt()]; +} + +- (UITextRange *)characterRangeAtPoint:(CGPoint)point +{ + // No API in Qt for determining this. Use sensible default instead: + Q_UNUSED(point); + return [QUITextRange rangeWithNSRange:NSMakeRange([self imValue:Qt::ImCursorPosition].toInt(), 0)]; +} + +- (void)setMarkedTextStyle:(NSDictionary *)style +{ + Q_UNUSED(style); + // No-one is going to change our style. If UIKit itself did that + // it would be very welcome, since then we knew how to style marked + // text instead of just guessing... +} + +- (NSDictionary *)textStylingAtPosition:(UITextPosition *)position inDirection:(UITextStorageDirection)direction +{ + Q_UNUSED(position); + Q_UNUSED(direction); + + QObject *focusObject = QGuiApplication::focusObject(); + if (!focusObject) + return [NSDictionary dictionary]; + + // Assume position is the same as the cursor for now. QInputMethodQueryEvent with Qt::ImFont + // needs to be extended to take an extra position argument before this can be fully correct. + QInputMethodQueryEvent e(Qt::ImFont); + QCoreApplication::sendEvent(focusObject, &e); + QFont qfont = qvariant_cast<QFont>(e.value(Qt::ImFont)); + UIFont *uifont = [UIFont fontWithName:qfont.family().toNSString() size:qfont.pointSize()]; + return [NSDictionary dictionaryWithObject:uifont forKey:UITextInputTextFontKey]; +} + +-(NSDictionary *)markedTextStyle +{ + return [NSDictionary dictionary]; +} + +- (BOOL)hasText +{ + return YES; +} + +- (void)insertText:(NSString *)text +{ + QObject *focusObject = QGuiApplication::focusObject(); + if (!focusObject) + return; + + if ([text isEqualToString:@"\n"] && self.returnKeyType == UIReturnKeyDone) + [self resignFirstResponder]; + + QInputMethodEvent e; + e.setCommitString(QString::fromNSString(text)); + [self sendEventToFocusObject:e]; +} + +- (void)deleteBackward +{ + // Since we're posting im events directly to the focus object, we should do the + // same for key events. Otherwise they might end up in a different place or out + // of sync with im events. + QKeyEvent press(QEvent::KeyPress, (int)Qt::Key_Backspace, Qt::NoModifier); + QKeyEvent release(QEvent::KeyRelease, (int)Qt::Key_Backspace, Qt::NoModifier); + [self sendEventToFocusObject:press]; + [self sendEventToFocusObject:release]; +} + +- (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; + self.spellCheckingType = (hints & Qt::ImhNoPredictiveText) ? + UITextSpellCheckingTypeNo : UITextSpellCheckingTypeDefault; + + 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 |