From 7eb8b67c8bc8e69b3dd6fb39dde09ac36f56e538 Mon Sep 17 00:00:00 2001 From: Richard Moe Gustavsen Date: Wed, 15 Jan 2014 10:40:13 +0100 Subject: iOS: implement support for input methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change will add support for input methods, word completion, spell checking and related functionality. Change-Id: I41d4de1cab521c679d414cfc7c1a2d0f9c1fcaaf Reviewed-by: Tor Arne Vestbø --- src/plugins/platforms/ios/qiosinputcontext.h | 8 +- src/plugins/platforms/ios/qiosinputcontext.mm | 38 ++- src/plugins/platforms/ios/qioswindow.mm | 1 + src/plugins/platforms/ios/quiview.h | 8 +- src/plugins/platforms/ios/quiview_textinput.mm | 382 +++++++++++++++++++++++++ 5 files changed, 420 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/src/plugins/platforms/ios/qiosinputcontext.h b/src/plugins/platforms/ios/qiosinputcontext.h index 91a6939ad4..aec1686b93 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 { @@ -67,9 +68,14 @@ public: void cursorRectangleChanged(); void scrollToCursor(); void scroll(int y); + + void update(Qt::InputMethodQueries); + void reset(); + void commit(); + private: QIOSKeyboardListener *m_keyboardListener; - UIView *m_focusView; + QUIView *m_focusView; bool m_hasPendingHideRequest; QObject *m_focusObject; }; diff --git a/src/plugins/platforms/ios/qiosinputcontext.mm b/src/plugins/platforms/ios/qiosinputcontext.mm index c25d76518f..15f5082f62 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 @interface QIOSKeyboardListener : NSObject { @@ -228,23 +229,12 @@ void QIOSInputContext::setFocusObject(QObject *focusObject) { m_focusObject = focusObject; - 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_keyboardListener->m_ignoreKeyboardChanges = true; - SEL sel = @selector(resignFirstResponder); - [[m_focusView superclass] instanceMethodForSelector:sel](m_focusView, sel); - [m_focusView becomeFirstResponder]; - m_keyboardListener->m_ignoreKeyboardChanges = false; + reset(); if (m_keyboardListener->m_keyboardVisibleAndDocked) scrollToCursor(); @@ -252,8 +242,7 @@ void QIOSInputContext::setFocusObject(QObject *focusObject) void QIOSInputContext::focusWindowChanged(QWindow *focusWindow) { - UIView *view = focusWindow ? - reinterpret_cast *>(focusWindow->handle()->winId()) : 0; + QUIView *view = focusWindow ? reinterpret_cast(focusWindow->handle()->winId()) : 0; if ([m_focusView isFirstResponder]) [view becomeFirstResponder]; [m_focusView release]; @@ -315,3 +304,22 @@ void QIOSInputContext::scroll(int y) 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/qioswindow.mm b/src/plugins/platforms/ios/qioswindow.mm index c25d6ba303..41a253e605 100644 --- a/src/plugins/platforms/ios/qioswindow.mm +++ b/src/plugins/platforms/ios/qioswindow.mm @@ -99,6 +99,7 @@ self.hidden = YES; self.multipleTouchEnabled = YES; + m_inSendEventToFocusObject = NO; } return self; diff --git a/src/plugins/platforms/ios/quiview.h b/src/plugins/platforms/ios/quiview.h index 7a54da1321..8984561dd8 100644 --- a/src/plugins/platforms/ios/quiview.h +++ b/src/plugins/platforms/ios/quiview.h @@ -55,8 +55,11 @@ QIOSWindow *m_qioswindow; QHash m_activeTouches; int m_nextTouchId; + QString m_markedText; + BOOL m_inSendEventToFocusObject; } +@property(nonatomic, assign) id inputDelegate; @property(nonatomic) UITextAutocapitalizationType autocapitalizationType; @property(nonatomic) UITextAutocorrectionType autocorrectionType; @property(nonatomic) BOOL enablesReturnKeyAutomatically; @@ -67,5 +70,8 @@ @end -@interface QUIView (TextInput) +@interface QUIView (TextInput) +- (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 index 76d697dc19..06d6fb4078 100644 --- a/src/plugins/platforms/ios/quiview_textinput.mm +++ b/src/plugins/platforms/ios/quiview_textinput.mm @@ -39,6 +39,94 @@ ** ****************************************************************************/ +#include + +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 @@ -64,6 +152,300 @@ return [super resignFirstResponder]; } +- (void)updateInputMethodWithQuery:(Qt::InputMethodQueries)query +{ + // TODO: check what changed, and perhaps update delegate if the text was + // changed from somewhere other than this plugin.... + + // Note: This function is called both when as a result of the application changing the + // input, but also (and most commonly) as a response to us sending QInputMethodQueryEvents. + // Because of the latter, we cannot call textWill/DidChange here, as that will confuse + // iOS IM handling, and e.g stop spellchecking from working. + Q_UNUSED(query); + + QObject *focusObject = QGuiApplication::focusObject(); + if (!focusObject) + return; + + if (!m_inSendEventToFocusObject) + [self.inputDelegate textWillChange:id(self)]; + + staticVariables()->inputMethodQueryEvent = QInputMethodQueryEvent(Qt::ImQueryInput); + QCoreApplication::sendEvent(focusObject, &staticVariables()->inputMethodQueryEvent); + + if (!m_inSendEventToFocusObject) + [self.inputDelegate textDidChange:id(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(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(self)]; +} + +- (void)commit +{ + [self.inputDelegate textWillChange:id(self)]; + [self unmarkText]; + [self.inputDelegate textDidChange:id(self)]; +} + +- (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(cursorPos, (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(); + + QList 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(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; +} + +- (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 *)markedTextStyle +{ + return [NSDictionary dictionary]; +} + - (BOOL)hasText { return YES; -- cgit v1.2.3