From 6910b8a552de6f0cd98fdfa50620825d59d63363 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tor=20Arne=20Vestb=C3=B8?= Date: Wed, 3 Sep 2014 15:29:16 +0200 Subject: iOS: Refactor text input handling to standalone responder Instead of coupling the visibility of the virtual keyboard to the first-responder status of the currently active QUIView, we now treat first-responder as a separate state, tied directly to QWindow activation. This fits better with the concept of first-responders in iOS, as a UIView can become first-responder without dealing with text input, eg when dealing with touch events or menu actions. The decision point on whether or not to show the virtual keyboard is then handled by implementing the conformsToProtocol method and selectively returning YES for the UIKeyInput protocol. iOS internally calls _requiresKeyboardWhenFirstResponder on the UIResponder to determine this, but since we can't override a private method (like WKContentView in WebKit does) we have to rely on the fact that the implementation of the method uses the protocol conformance to make its decision. Once the virtual keyboard is up, we then need to react to changes to its configuration, such as keyboard type or the type of return key. Normally this would be a simple call to [view reloadInputViews], but iOS will not reload the built-in keyboards unless the UIResponder returns YES for _requiresKeyboardResetOnReload. Since we again can't override this private method (like WebKit does), we work around it by taking advantage of the fact that iOS will treat any change to the first-responder as a reason to do a keyboard reset. By using a stand-alone UIResponder for text input we can init and destroy these responders as needed, so that every call to reloadInputViews will trigger a reset, as the responder has not been seen before. We keep track of changes to the input-method-query, and detect whether or not we need to bring up a new UIResponder for text handling. As part of this refactoring we now tie the visibility of the virtual keyboard to the presence of a focus object that has input-methods enabled. This means that we automatically will track changes to input-elements through the focus changes, and reconfigure or hide the keyboard as appropriate. As a result the hide() method of QInputMethod becomes a no-op on iOS. Change-Id: I4c4834df490bc8b0bac32aeedbd819780bd5aaba Reviewed-by: Richard Moe Gustavsen --- src/plugins/platforms/ios/ios.pro | 2 + src/plugins/platforms/ios/qiosinputcontext.h | 19 +- src/plugins/platforms/ios/qiosinputcontext.mm | 177 +++++--- src/plugins/platforms/ios/qiostextresponder.h | 74 +++ src/plugins/platforms/ios/qiostextresponder.mm | 544 ++++++++++++++++++++++ src/plugins/platforms/ios/qioswindow.mm | 4 +- src/plugins/platforms/ios/quiview.h | 28 -- src/plugins/platforms/ios/quiview.mm | 50 +- src/plugins/platforms/ios/quiview_textinput.mm | 603 ------------------------- 9 files changed, 795 insertions(+), 706 deletions(-) create mode 100644 src/plugins/platforms/ios/qiostextresponder.h create mode 100644 src/plugins/platforms/ios/qiostextresponder.mm delete mode 100644 src/plugins/platforms/ios/quiview_textinput.mm (limited to 'src/plugins/platforms/ios') diff --git a/src/plugins/platforms/ios/ios.pro b/src/plugins/platforms/ios/ios.pro index ad9912a8d0..82f0bd91c4 100644 --- a/src/plugins/platforms/ios/ios.pro +++ b/src/plugins/platforms/ios/ios.pro @@ -27,6 +27,7 @@ OBJECTIVE_SOURCES = \ qiosclipboard.mm \ quiaccessibilityelement.mm \ qiosplatformaccessibility.mm \ + qiostextresponder.mm \ HEADERS = \ qiosintegration.h \ @@ -46,6 +47,7 @@ HEADERS = \ qiosclipboard.h \ quiaccessibilityelement.h \ qiosplatformaccessibility.h \ + qiostextresponder.h \ OTHER_FILES = \ quiview_textinput.mm \ diff --git a/src/plugins/platforms/ios/qiosinputcontext.h b/src/plugins/platforms/ios/qiosinputcontext.h index 13255ada56..7f94a9836a 100644 --- a/src/plugins/platforms/ios/qiosinputcontext.h +++ b/src/plugins/platforms/ios/qiosinputcontext.h @@ -44,13 +44,21 @@ #include +#include #include #include QT_BEGIN_NAMESPACE @class QIOSKeyboardListener; -@class QUIView; +@class QIOSTextInputResponder; + +struct ImeState +{ + ImeState() : currentState(0) {} + Qt::InputMethodQueries update(Qt::InputMethodQueries properties); + QInputMethodQueryEvent currentState; +}; class QIOSInputContext : public QPlatformInputContext { @@ -59,8 +67,11 @@ public: ~QIOSInputContext(); QRectF keyboardRect() const; + void showInputPanel(); void hideInputPanel(); + void hideVirtualKeyboard(); + bool isInputPanelVisible() const; void setFocusObject(QObject *object); @@ -73,10 +84,12 @@ public: void reset(); void commit(); + const ImeState &imeState() { return m_imeState; }; + private: QIOSKeyboardListener *m_keyboardListener; - QUIView *m_focusView; - bool m_hasPendingHideRequest; + QIOSTextInputResponder *m_textResponder; + ImeState m_imeState; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/ios/qiosinputcontext.mm b/src/plugins/platforms/ios/qiosinputcontext.mm index 064098157d..cbf3fb4ff2 100644 --- a/src/plugins/platforms/ios/qiosinputcontext.mm +++ b/src/plugins/platforms/ios/qiosinputcontext.mm @@ -44,8 +44,10 @@ #import #include "qiosglobal.h" +#include "qiostextresponder.h" #include "qioswindow.h" #include "quiview.h" + #include #include @@ -158,8 +160,6 @@ - (void) keyboardWillShow:(NSNotification *)notification { - if ([QUIView inUpdateKeyboardLayout]) - return; // Note that UIKeyboardWillShowNotification is only sendt when the keyboard is docked. m_keyboardVisibleAndDocked = YES; m_keyboardEndRect = [self getKeyboardRect:notification]; @@ -173,8 +173,6 @@ - (void) keyboardWillHide:(NSNotification *)notification { - if ([QUIView inUpdateKeyboardLayout]) - return; // Note that UIKeyboardWillHideNotification is also sendt when the keyboard is undocked. m_keyboardVisibleAndDocked = NO; m_keyboardEndRect = [self getKeyboardRect:notification]; @@ -207,7 +205,7 @@ QPointF p = fromCGPoint([[touches anyObject] locationInView:m_viewController.view]); if (m_keyboardRect.contains(p)) { m_keyboardHiddenByGesture = YES; - m_context->hideInputPanel(); + m_context->hideVirtualKeyboard(); } [super touchesMoved:touches withEvent:event]; @@ -253,11 +251,43 @@ @end +// ------------------------------------------------------------------------- + +Qt::InputMethodQueries ImeState::update(Qt::InputMethodQueries properties) +{ + if (!properties) + return 0; + + QInputMethodQueryEvent newState(properties); + + if (qApp && qApp->focusObject()) + QCoreApplication::sendEvent(qApp->focusObject(), &newState); + + Qt::InputMethodQueries updatedProperties; + for (uint i = 0; i < (sizeof(Qt::ImQueryAll) * CHAR_BIT); ++i) { + if (Qt::InputMethodQuery property = Qt::InputMethodQuery(int(properties & (1 << i)))) { + if (newState.value(property) != currentState.value(property)) { + updatedProperties |= property; + currentState.setValue(property, newState.value(property)); + } + } + } + + return updatedProperties; +} + +// ------------------------------------------------------------------------- + +static QUIView *focusView() +{ + return qApp->focusWindow() ? + reinterpret_cast(qApp->focusWindow()->handle()->winId()) : 0; +} + QIOSInputContext::QIOSInputContext() : QPlatformInputContext() , m_keyboardListener([[QIOSKeyboardListener alloc] initWithQIOSInputContext:this]) - , m_focusView(0) - , m_hasPendingHideRequest(false) + , m_textResponder(0) { if (isQtApplication()) connect(qGuiApp->inputMethod(), &QInputMethod::cursorRectangleChanged, this, &QIOSInputContext::cursorRectangleChanged); @@ -267,7 +297,7 @@ QIOSInputContext::QIOSInputContext() QIOSInputContext::~QIOSInputContext() { [m_keyboardListener release]; - [m_focusView release]; + [m_textResponder release]; } QRectF QIOSInputContext::keyboardRect() const @@ -277,61 +307,22 @@ QRectF QIOSInputContext::keyboardRect() const void QIOSInputContext::showInputPanel() { - if (m_keyboardListener->m_keyboardHiddenByGesture) { - // We refuse to re-show the keyboard until the touch - // sequence that triggered the gesture has ended. - return; - } - - // Documentation tells that one should call (and recall, if necessary) becomeFirstResponder/resignFirstResponder - // to show/hide the keyboard. This is slightly inconvenient, since there exist no API to get the current first - // responder. Rather than searching for it from the top, we let the active QIOSWindow tell us which view to use. - // Note that Qt will forward keyevents to whichever QObject that needs it, regardless of which UIView the input - // actually came from. So in this respect, we're undermining iOS' responder chain. - m_hasPendingHideRequest = false; - [m_focusView becomeFirstResponder]; + // No-op, keyboard controlled fully by platform based on focus } void QIOSInputContext::hideInputPanel() { - // Delay hiding the keyboard for cases where the user is transferring focus between - // 'line edits'. In that case the 'line edit' that lost focus will close the input - // panel, just to see that the new 'line edit' will open it again: - m_hasPendingHideRequest = true; - dispatch_async(dispatch_get_main_queue(), ^{ - if (m_hasPendingHideRequest) - [m_focusView resignFirstResponder]; - }); + // No-op, keyboard controlled fully by platform based on focus } -bool QIOSInputContext::isInputPanelVisible() const +void QIOSInputContext::hideVirtualKeyboard() { - return m_keyboardListener->m_keyboardVisible; + static_cast(QObjectPrivate::get(qApp->focusWindow()))->clearFocusObject(); } -void QIOSInputContext::setFocusObject(QObject *focusObject) -{ - if (!focusObject || !m_focusView || !m_focusView.isFirstResponder) { - scroll(0); - return; - } - - reset(); - - if (m_keyboardListener->m_keyboardVisibleAndDocked) - scrollToCursor(); -} - -void QIOSInputContext::focusWindowChanged(QWindow *focusWindow) +bool QIOSInputContext::isInputPanelVisible() const { - QUIView *view = focusWindow ? reinterpret_cast(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); + return m_keyboardListener->m_keyboardVisible; } void QIOSInputContext::cursorRectangleChanged() @@ -353,7 +344,7 @@ void QIOSInputContext::cursorRectangleChanged() void QIOSInputContext::scrollToCursor() { - if (!isQtApplication() || !m_focusView) + if (!isQtApplication()) return; if (m_keyboardListener->m_touchPressWhileKeyboardVisible) { @@ -364,12 +355,12 @@ void QIOSInputContext::scrollToCursor() } UIView *view = m_keyboardListener->m_viewController.view; - if (view.window != m_focusView.window) + if (view.window != focusView().window) return; const int margin = 20; QRectF translatedCursorPos = qApp->inputMethod()->cursorRectangle(); - translatedCursorPos.translate(m_focusView.qwindow->geometry().topLeft()); + translatedCursorPos.translate(focusView().qwindow->geometry().topLeft()); qreal keyboardY = m_keyboardListener->m_keyboardEndRect.y(); int statusBarY = qGuiApp->primaryScreen()->availableGeometry().y(); @@ -398,18 +389,84 @@ void QIOSInputContext::scroll(int y) ]; } -void QIOSInputContext::update(Qt::InputMethodQueries query) +// ------------------------------------------------------------------------- + +void QIOSInputContext::setFocusObject(QObject *focusObject) +{ + Q_UNUSED(focusObject); + + reset(); + + if (m_keyboardListener->m_keyboardVisibleAndDocked) + scrollToCursor(); +} + +void QIOSInputContext::focusWindowChanged(QWindow *focusWindow) +{ + Q_UNUSED(focusWindow); + + reset(); + + if (m_keyboardListener->m_keyboardVisibleAndDocked) + scrollToCursor(); +} + +/*! + Called by the input item to inform the platform input methods when there has been + state changes in editor's input method query attributes. When calling the function + \a queries parameter has to be used to tell what has changes, which input method + can use to make queries for attributes it's interested with QInputMethodQueryEvent. +*/ +void QIOSInputContext::update(Qt::InputMethodQueries updatedProperties) { - [m_focusView updateInputMethodWithQuery:query]; + // Mask for properties that we are interested in and see if any of them changed + updatedProperties &= (Qt::ImEnabled | Qt::ImHints | Qt::ImQueryInput); + + Qt::InputMethodQueries changedProperties = m_imeState.update(updatedProperties); + if (changedProperties & (Qt::ImEnabled | Qt::ImHints)) { + // Changes to enablement or hints require virtual keyboard reconfigure + [m_textResponder release]; + m_textResponder = [[QIOSTextInputResponder alloc] initWithInputContext:this]; + [m_textResponder reloadInputViews]; + } else { + [m_textResponder notifyInputDelegate:changedProperties]; + } } +/*! + Called by the input item to reset the input method state. +*/ void QIOSInputContext::reset() { - [m_focusView reset]; + update(Qt::ImQueryAll); + + [m_textResponder setMarkedText:@"" selectedRange:NSMakeRange(0, 0)]; + [m_textResponder notifyInputDelegate:Qt::ImQueryInput]; } +/*! + Commits the word user is currently composing to the editor. The function is + mostly needed by the input methods with text prediction features and by the + methods where the script used for typing characters is different from the + script that actually gets appended to the editor. Any kind of action that + interrupts the text composing needs to flush the composing state by calling the + commit() function, for example when the cursor is moved elsewhere. +*/ void QIOSInputContext::commit() { - [m_focusView commit]; + [m_textResponder unmarkText]; + [m_textResponder notifyInputDelegate:Qt::ImSurroundingText]; } +// ------------------------------------------------------------------------- + +@interface QUIView (InputMethods) +- (void)reloadInputViews; +@end + +@implementation QUIView (InputMethods) +- (void)reloadInputViews +{ + qApp->inputMethod()->reset(); +} +@end diff --git a/src/plugins/platforms/ios/qiostextresponder.h b/src/plugins/platforms/ios/qiostextresponder.h new file mode 100644 index 0000000000..7290d9e454 --- /dev/null +++ b/src/plugins/platforms/ios/qiostextresponder.h @@ -0,0 +1,74 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#import + +#include + +class QIOSInputContext; + +@interface QIOSTextInputResponder : UIResponder +{ + @public + QString m_markedText; + BOOL m_inSendEventToFocusObject; + + @private + QIOSInputContext *m_inputContext; +} + +- (id)initWithInputContext:(QIOSInputContext *)context; +- (void)notifyInputDelegate:(Qt::InputMethodQueries)updatedProperties; + +// UITextInputTraits +@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; + +// UITextInput +@property(nonatomic, assign) id inputDelegate; + +@end diff --git a/src/plugins/platforms/ios/qiostextresponder.mm b/src/plugins/platforms/ios/qiostextresponder.mm new file mode 100644 index 0000000000..ea6b5ffb94 --- /dev/null +++ b/src/plugins/platforms/ios/qiostextresponder.mm @@ -0,0 +1,544 @@ +/**************************************************************************** +** +** 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 "qiostextresponder.h" + +#include "qiosglobal.h" +#include "qiosinputcontext.h" +#include "quiview.h" + +#include + +#include +#include +#include +#include + +// ------------------------------------------------------------------------- + +@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 QIOSTextInputResponder + +- (id)initWithInputContext:(QIOSInputContext *)inputContext +{ + if (!(self = [self init])) + return self; + + m_inSendEventToFocusObject = NO; + m_inputContext = inputContext; + + Qt::InputMethodHints hints = Qt::InputMethodHints([self imValue: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; + + return self; +} + +- (void)dealloc +{ + [super dealloc]; +} + +- (BOOL)isFirstResponder +{ + return YES; +} + +- (UIResponder*)nextResponder +{ + return qApp->focusWindow() ? + reinterpret_cast(qApp->focusWindow()->handle()->winId()) : 0; +} + +/*! + iOS uses [UIResponder(Internal) _requiresKeyboardWhenFirstResponder] to check if the + current responder should bring up the keyboard, which in turn checks if the responder + supports the UIKeyInput protocol. By dynamically reporting our protocol conformance + we can control the keyboard visibility depending on whether or not we have a focus + object with IME enabled. +*/ +- (BOOL)conformsToProtocol:(Protocol *)protocol +{ + if (protocol == @protocol(UIKeyInput)) + return m_inputContext->inputMethodAccepted(); + + return [super conformsToProtocol:protocol]; +} + +// ------------------------------------------------------------------------- + +- (void)notifyInputDelegate:(Qt::InputMethodQueries)updatedProperties +{ + // As documented, we should not report textWillChange/textDidChange unless the text + // was changed externally. That will cause spell checking etc to fail. But we don't + // really know if the text/selection was changed by UITextInput or Qt/app when getting + // update calls from Qt. We therefore use a less ideal approach where we always assume + // that UITextView caused the change if we're currently processing an event sendt from it. + if (m_inSendEventToFocusObject) + return; + + if (updatedProperties & (Qt::ImCursorPosition | Qt::ImAnchorPosition)) { + [self.inputDelegate selectionWillChange:self]; + [self.inputDelegate selectionDidChange:self]; + } + + if (updatedProperties & Qt::ImSurroundingText) { + [self.inputDelegate textWillChange:self]; + [self.inputDelegate textDidChange:self]; + } +} + +- (void)sendEventToFocusObject:(QEvent &)e +{ + QObject *focusObject = QGuiApplication::focusObject(); + if (!focusObject) + return; + + // While sending the event, we will receive back updateInputMethodWithQuery calls. + // Note that it would be more correct to post the event instead, but UITextInput expects + // callbacks to take effect immediately (it will query us for information after a callback). + QScopedValueRollback rollback(m_inSendEventToFocusObject); + m_inSendEventToFocusObject = YES; + QCoreApplication::sendEvent(focusObject, &e); +} + +- (QVariant)imValue:(Qt::InputMethodQuery)query +{ + return m_inputContext->imeState().currentState.value(query); +} + +-(id)tokenizer +{ + return [[[UITextInputStringTokenizer alloc] initWithTextInput:id(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(range); + QList 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(qMin(cursorPos, anchorPos), qAbs(anchorPos - cursorPos))]; +} + +- (NSString *)textInRange:(UITextRange *)range +{ + int s = static_cast([range start]).index; + int e = static_cast([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(); + + static QTextCharFormat markedTextFormat; + if (markedTextFormat.isEmpty()) { + // 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)); + } + + QList attrs; + attrs << QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat, 0, markedText.length, 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(position).index; + int o = static_cast(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(fromPosition).index; + int t = static_cast(toPosition).index; + return [QUITextRange rangeWithNSRange:NSMakeRange(f, t - f)]; +} + +- (UITextPosition *)positionFromPosition:(UITextPosition *)position offset:(NSInteger)offset +{ + int p = static_cast(position).index; + return [QUITextPosition positionWithIndex:p + offset]; +} + +- (UITextPosition *)positionFromPosition:(UITextPosition *)position inDirection:(UITextLayoutDirection)direction offset:(NSInteger)offset +{ + int p = static_cast(position).index; + return [QUITextPosition positionWithIndex:(direction == UITextLayoutDirectionRight ? p + offset : p - offset)]; +} + +- (UITextPosition *)positionWithinRange:(UITextRange *)range farthestInDirection:(UITextLayoutDirection)direction +{ + NSRange r = static_cast(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(fromPosition).index; + int t = static_cast(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. + Q_ASSERT(qApp->focusWindow()->handle()); + QPlatformWindow *topLevel = qApp->focusWindow()->handle(); + while (QPlatformWindow *p = topLevel->parent()) + topLevel = p; + return reinterpret_cast(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(range).range; + QList 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(); + 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(); + 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(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(e.value(Qt::ImFont)); + UIFont *uifont = [UIFont fontWithName:qfont.family().toNSString() size:qfont.pointSize()]; + if (!uifont) + return [NSDictionary dictionary]; + 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"]) { + QKeyEvent press(QEvent::KeyPress, Qt::Key_Return, Qt::NoModifier); + QKeyEvent release(QEvent::KeyRelease, Qt::Key_Return, Qt::NoModifier); + [self sendEventToFocusObject:press]; + [self sendEventToFocusObject:release]; + + Qt::InputMethodHints imeHints = static_cast([self imValue:Qt::ImHints].toUInt()); + if (!(imeHints & Qt::ImhMultiLine)) + m_inputContext->hideVirtualKeyboard(); + + return; + } + + 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]; +} + +@end diff --git a/src/plugins/platforms/ios/qioswindow.mm b/src/plugins/platforms/ios/qioswindow.mm index aa33a9b21d..2874d272fe 100644 --- a/src/plugins/platforms/ios/qioswindow.mm +++ b/src/plugins/platforms/ios/qioswindow.mm @@ -264,12 +264,12 @@ void QIOSWindow::requestActivateWindow() if (blockedByModal()) return; + Q_ASSERT(m_view.window); [m_view.window makeKeyWindow]; + [m_view becomeFirstResponder]; if (window()->isTopLevel()) raise(); - - QWindowSystemInterface::handleWindowActivated(window()); } void QIOSWindow::raiseOrLower(bool raise) diff --git a/src/plugins/platforms/ios/quiview.h b/src/plugins/platforms/ios/quiview.h index 99c710ffee..c5bf3b6cbe 100644 --- a/src/plugins/platforms/ios/quiview.h +++ b/src/plugins/platforms/ios/quiview.h @@ -56,42 +56,14 @@ class QIOSWindow; QHash m_activeTouches; int m_nextTouchId; - @public - UITextAutocapitalizationType autocapitalizationType; - UITextAutocorrectionType autocorrectionType; - BOOL enablesReturnKeyAutomatically; - UIKeyboardAppearance keyboardAppearance; - UIKeyboardType keyboardType; - UIReturnKeyType returnKeyType; - BOOL secureTextEntry; - QString m_markedText; - BOOL m_inSendEventToFocusObject; - @private NSMutableArray *m_accessibleElements; } -@property(nonatomic, assign) id 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; - - (id)initWithQIOSWindow:(QIOSWindow *)window; - (void)sendUpdatedExposeEvent; @end -@interface QUIView (TextInput) -- (void)updateInputMethodWithQuery:(Qt::InputMethodQueries)query; -- (void)reset; -- (void)commit; -+ (bool)inUpdateKeyboardLayout; -@end - @interface QUIView (Accessibility) - (void)clearAccessibleCache; @end diff --git a/src/plugins/platforms/ios/quiview.mm b/src/plugins/platforms/ios/quiview.mm index e534bdcd69..5687c078ea 100644 --- a/src/plugins/platforms/ios/quiview.mm +++ b/src/plugins/platforms/ios/quiview.mm @@ -50,14 +50,6 @@ @implementation QUIView -@synthesize autocapitalizationType; -@synthesize autocorrectionType; -@synthesize enablesReturnKeyAutomatically; -@synthesize keyboardAppearance; -@synthesize keyboardType; -@synthesize returnKeyType; -@synthesize secureTextEntry; - + (Class)layerClass { return [CAEAGLLayer class]; @@ -86,7 +78,6 @@ self.hidden = YES; self.multipleTouchEnabled = YES; - m_inSendEventToFocusObject = NO; } return self; @@ -202,6 +193,46 @@ QWindowSystemInterface::flushWindowSystemEvents(); } +// ------------------------------------------------------------------------- + +- (BOOL)canBecomeFirstResponder +{ + return YES; +} + +- (BOOL)becomeFirstResponder +{ + if ([super becomeFirstResponder]) { + QWindowSystemInterface::handleWindowActivated(m_qioswindow->window()); + QWindowSystemInterface::flushWindowSystemEvents(); + + return YES; + } + + return NO; +} + +- (BOOL)resignFirstResponder +{ + if ([super resignFirstResponder]) { + // We don't want to send window deactivation in case we're in the process + // of activating another window. The handleWindowActivated of the activation + // will take care of both. + dispatch_async(dispatch_get_main_queue (), ^{ + if (![[UIResponder currentFirstResponder] isKindOfClass:[QUIView class]]) + QWindowSystemInterface::handleWindowActivated(0); + QWindowSystemInterface::flushWindowSystemEvents(); + }); + + return YES; + } + + return NO; +} + +// ------------------------------------------------------------------------- + + - (void)updateTouchList:(NSSet *)touches withState:(Qt::TouchPointState)state { // We deliver touch events in global coordinates. But global in this respect @@ -342,5 +373,4 @@ @end // Include category as an alternative to using -ObjC (Apple QA1490) -#include "quiview_textinput.mm" #include "quiview_accessibility.mm" diff --git a/src/plugins/platforms/ios/quiview_textinput.mm b/src/plugins/platforms/ios/quiview_textinput.mm deleted file mode 100644 index 6b2707e706..0000000000 --- a/src/plugins/platforms/ios/quiview_textinput.mm +++ /dev/null @@ -1,603 +0,0 @@ -/**************************************************************************** -** -** 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 "qiosinputcontext.h" - -#include -#include - -class StaticVariables -{ -public: - QInputMethodQueryEvent inputMethodQueryEvent; - bool inUpdateKeyboardLayout; - - StaticVariables() - : inputMethodQueryEvent(Qt::ImQueryInput) - , inUpdateKeyboardLayout(false) - { - } -}; - -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(QObjectPrivate::get(m_qioswindow->window()))->clearFocusObject(); - return [super resignFirstResponder]; -} - -+ (bool)inUpdateKeyboardLayout -{ - return staticVariables()->inUpdateKeyboardLayout; -} - -- (void)updateKeyboardLayout -{ - if (![self isFirstResponder]) - return; - - // There seems to be no API to inform that the keyboard layout needs to update. - // As a work-around, we quickly resign first responder just to reassign it again. - QScopedValueRollback rollback(staticVariables()->inUpdateKeyboardLayout); - staticVariables()->inUpdateKeyboardLayout = true; - [super resignFirstResponder]; - [self updateTextInputTraits]; - [super becomeFirstResponder]; -} - -- (void)updateUITextInputDelegate:(Qt::InputMethodQueries)query -{ - // As documented, we should not report textWillChange/textDidChange unless the text - // was changed externally. That will cause spell checking etc to fail. But we don't - // really know if the text/selection was changed by UITextInput or Qt/app when getting - // update calls from Qt. We therefore use a less ideal approach where we always assume - // that UITextView caused the change if we're currently processing an event sendt from it. - if (m_inSendEventToFocusObject) - return; - - if (query & (Qt::ImCursorPosition | Qt::ImAnchorPosition)) { - [self.inputDelegate selectionWillChange:id(self)]; - [self.inputDelegate selectionDidChange:id(self)]; - } - - if (query & Qt::ImSurroundingText) { - [self.inputDelegate textWillChange:id(self)]; - [self.inputDelegate textDidChange:id(self)]; - } -} - -- (void)updateInputMethodWithQuery:(Qt::InputMethodQueries)query -{ - Q_UNUSED(query); - - QObject *focusObject = QGuiApplication::focusObject(); - if (!focusObject) - return; - - // 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); - [self updateUITextInputDelegate:query]; -} - -- (void)sendEventToFocusObject:(QEvent &)e -{ - QObject *focusObject = QGuiApplication::focusObject(); - if (!focusObject) - return; - - // While sending the event, we will receive back updateInputMethodWithQuery calls. - // Note that it would be more correct to post the event instead, but UITextInput expects - // callbacks to take effect immediately (it will query us for information after a callback). - QScopedValueRollback rollback(m_inSendEventToFocusObject); - m_inSendEventToFocusObject = YES; - QCoreApplication::sendEvent(focusObject, &e); -} - -- (void)reset -{ - [self setMarkedText:@"" selectedRange:NSMakeRange(0, 0)]; - [self updateInputMethodWithQuery:Qt::ImQueryInput]; - - // Guard agains recursive callbacks by posting calls to UITextInput - dispatch_async(dispatch_get_main_queue(), ^{ - [self updateKeyboardLayout]; - [self updateUITextInputDelegate:Qt::ImQueryInput]; - }); -} - -- (void)commit -{ - [self unmarkText]; - - // Guard agains recursive callbacks by posting calls to UITextInput - dispatch_async(dispatch_get_main_queue(), ^{ - [self updateKeyboardLayout]; - [self updateUITextInputDelegate:Qt::ImSurroundingText]; - }); -} - -- (QVariant)imValue:(Qt::InputMethodQuery)query -{ - return staticVariables()->inputMethodQueryEvent.value(query); -} - --(id)tokenizer -{ - return [[[UITextInputStringTokenizer alloc] initWithTextInput:id(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(range); - QList 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(qMin(cursorPos, anchorPos), qAbs(anchorPos - cursorPos))]; -} - -- (NSString *)textInRange:(UITextRange *)range -{ - int s = static_cast([range start]).index; - int e = static_cast([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(); - - static QTextCharFormat markedTextFormat; - if (markedTextFormat.isEmpty()) { - // 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)); - } - - QList attrs; - attrs << QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat, 0, markedText.length, 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(position).index; - int o = static_cast(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(fromPosition).index; - int t = static_cast(toPosition).index; - return [QUITextRange rangeWithNSRange:NSMakeRange(f, t - f)]; -} - -- (UITextPosition *)positionFromPosition:(UITextPosition *)position offset:(NSInteger)offset -{ - int p = static_cast(position).index; - return [QUITextPosition positionWithIndex:p + offset]; -} - -- (UITextPosition *)positionFromPosition:(UITextPosition *)position inDirection:(UITextLayoutDirection)direction offset:(NSInteger)offset -{ - int p = static_cast(position).index; - return [QUITextPosition positionWithIndex:(direction == UITextLayoutDirectionRight ? p + offset : p - offset)]; -} - -- (UITextPosition *)positionWithinRange:(UITextRange *)range farthestInDirection:(UITextLayoutDirection)direction -{ - NSRange r = static_cast(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(fromPosition).index; - int t = static_cast(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(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(range).range; - QList 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(); - 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(); - 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(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(e.value(Qt::ImFont)); - UIFont *uifont = [UIFont fontWithName:qfont.family().toNSString() size:qfont.pointSize()]; - if (!uifont) - return [NSDictionary dictionary]; - 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"]) { - if (self.returnKeyType == UIReturnKeyDone) - qApp->inputMethod()->hide(); - - QKeyEvent press(QEvent::KeyPress, Qt::Key_Return, Qt::NoModifier); - QKeyEvent release(QEvent::KeyRelease, Qt::Key_Return, Qt::NoModifier); - [self sendEventToFocusObject:press]; - [self sendEventToFocusObject:release]; - - return; - } - - 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(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 -- cgit v1.2.3