iOS: implement support for input methods

This change will add support for input methods, word
completion, spell checking and related functionality.

Change-Id: I41d4de1cab521c679d414cfc7c1a2d0f9c1fcaaf
Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@digia.com>
bb10
Richard Moe Gustavsen 2014-01-15 10:40:13 +01:00 committed by The Qt Project
parent e60357fa9e
commit 7eb8b67c8b
5 changed files with 420 additions and 17 deletions

View File

@ -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<UIKeyInput> *m_focusView;
QUIView *m_focusView;
bool m_hasPendingHideRequest;
QObject *m_focusObject;
};

View File

@ -42,6 +42,7 @@
#include "qiosglobal.h"
#include "qiosinputcontext.h"
#include "qioswindow.h"
#include "quiview.h"
#include <QGuiApplication>
@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<UIKeyInput> *view = focusWindow ?
reinterpret_cast<UIView<UIKeyInput> *>(focusWindow->handle()->winId()) : 0;
QUIView *view = focusWindow ? reinterpret_cast<QUIView *>(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];
}

View File

@ -99,6 +99,7 @@
self.hidden = YES;
self.multipleTouchEnabled = YES;
m_inSendEventToFocusObject = NO;
}
return self;

View File

@ -55,8 +55,11 @@
QIOSWindow *m_qioswindow;
QHash<UITouch *, QWindowSystemInterface::TouchPoint> m_activeTouches;
int m_nextTouchId;
QString m_markedText;
BOOL m_inSendEventToFocusObject;
}
@property(nonatomic, assign) id<UITextInputDelegate> inputDelegate;
@property(nonatomic) UITextAutocapitalizationType autocapitalizationType;
@property(nonatomic) UITextAutocorrectionType autocorrectionType;
@property(nonatomic) BOOL enablesReturnKeyAutomatically;
@ -67,5 +70,8 @@
@end
@interface QUIView (TextInput) <UIKeyInput>
@interface QUIView (TextInput) <UITextInput>
- (void)updateInputMethodWithQuery:(Qt::InputMethodQueries)query;
- (void)reset;
- (void)commit;
@end

View File

@ -39,6 +39,94 @@
**
****************************************************************************/
#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
@ -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<UITextInput>(self)];
staticVariables()->inputMethodQueryEvent = QInputMethodQueryEvent(Qt::ImQueryInput);
QCoreApplication::sendEvent(focusObject, &staticVariables()->inputMethodQueryEvent);
if (!m_inSendEventToFocusObject)
[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;
}
- (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 *)markedTextStyle
{
return [NSDictionary dictionary];
}
- (BOOL)hasText
{
return YES;