blob: a3350bda87bf39dd17fefa9b1562ba638a9578c1 [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** 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 The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/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 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "qiostextresponder.h"
#include "qiosglobal.h"
#include "qiosinputcontext.h"
#include "quiview.h"
#include <QtCore/qscopedvaluerollback.h>
#include <QtGui/qevent.h>
#include <QtGui/qtextformat.h>
#include <QtGui/private/qguiapplication_p.h>
#include <QtGui/qpa/qplatformwindow.h>
// -------------------------------------------------------------------------
@interface QUITextPosition : UITextPosition
@property (nonatomic) NSUInteger index;
+ (instancetype)positionWithIndex:(NSUInteger)index;
@end
@implementation QUITextPosition
+ (instancetype)positionWithIndex:(NSUInteger)index
{
QUITextPosition *pos = [[QUITextPosition alloc] init];
pos.index = index;
return [pos autorelease];
}
@end
// -------------------------------------------------------------------------
@interface QUITextRange : UITextRange
@property (nonatomic) NSRange range;
+ (instancetype)rangeWithNSRange:(NSRange)range;
@end
@implementation QUITextRange
+ (instancetype)rangeWithNSRange:(NSRange)nsrange
{
QUITextRange *range = [[self 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
// -------------------------------------------------------------------------
@interface WrapperView : UIView
@end
@implementation WrapperView
- (instancetype)initWithView:(UIView *)view
{
if (self = [self init]) {
[self addSubview:view];
self.autoresizingMask = view.autoresizingMask;
[self sizeToFit];
}
return self;
}
- (void)layoutSubviews
{
UIView *view = [self.subviews firstObject];
view.frame = self.bounds;
// FIXME: During orientation changes the size and position
// of the view is not respected by the host view, even if
// we call sizeToFit or setNeedsLayout on the superview.
}
- (CGSize)sizeThatFits:(CGSize)size
{
return [[self.subviews firstObject] sizeThatFits:size];
}
// By keeping the responder (QIOSTextInputResponder in this case)
// retained, we ensure that all messages sent to the view during
// its lifetime in a window hierarcy will be able to traverse the
// responder chain.
- (void)willMoveToWindow:(UIWindow *)window
{
if (window)
[[self nextResponder] retain];
else
[[self nextResponder] autorelease];
}
@end
// -------------------------------------------------------------------------
@implementation QIOSTextInputResponder {
QT_PREPEND_NAMESPACE(QIOSInputContext) *m_inputContext;
QT_PREPEND_NAMESPACE(QInputMethodQueryEvent) *m_configuredImeState;
QString m_markedText;
BOOL m_inSendEventToFocusObject;
BOOL m_inSelectionChange;
}
- (instancetype)initWithInputContext:(QT_PREPEND_NAMESPACE(QIOSInputContext) *)inputContext
{
if (!(self = [self init]))
return self;
m_inSendEventToFocusObject = NO;
m_inSelectionChange = NO;
m_inputContext = inputContext;
m_configuredImeState = new QInputMethodQueryEvent(m_inputContext->imeState().currentState);
QVariantMap platformData = m_configuredImeState->value(Qt::ImPlatformData).toMap();
Qt::InputMethodHints hints = Qt::InputMethodHints(m_configuredImeState->value(Qt::ImHints).toUInt());
Qt::EnterKeyType enterKeyType = Qt::EnterKeyType(m_configuredImeState->value(Qt::ImEnterKeyType).toUInt());
switch (enterKeyType) {
case Qt::EnterKeyReturn:
self.returnKeyType = UIReturnKeyDefault;
break;
case Qt::EnterKeyDone:
self.returnKeyType = UIReturnKeyDone;
break;
case Qt::EnterKeyGo:
self.returnKeyType = UIReturnKeyGo;
break;
case Qt::EnterKeySend:
self.returnKeyType = UIReturnKeySend;
break;
case Qt::EnterKeySearch:
self.returnKeyType = UIReturnKeySearch;
break;
case Qt::EnterKeyNext:
self.returnKeyType = UIReturnKeyNext;
break;
default:
self.returnKeyType = (hints & Qt::ImhMultiLine) ? UIReturnKeyDefault : UIReturnKeyDone;
break;
}
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 = UIKeyboardTypePhonePad;
else if (hints & Qt::ImhLatinOnly)
self.keyboardType = UIKeyboardTypeASCIICapable;
else if (hints & Qt::ImhPreferNumbers)
self.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
else
self.keyboardType = UIKeyboardTypeDefault;
if (UIView *inputView = static_cast<UIView *>(platformData.value(kImePlatformDataInputView).value<void *>()))
self.inputView = [[[WrapperView alloc] initWithView:inputView] autorelease];
if (UIView *accessoryView = static_cast<UIView *>(platformData.value(kImePlatformDataInputAccessoryView).value<void *>()))
self.inputAccessoryView = [[[WrapperView alloc] initWithView:accessoryView] autorelease];
#ifndef Q_OS_TVOS
if (platformData.value(kImePlatformDataHideShortcutsBar).toBool()) {
// According to the docs, leadingBarButtonGroups/trailingBarButtonGroups should be set to nil to hide the shortcuts bar.
// However, starting with iOS 10, the API has been surrounded with NS_ASSUME_NONNULL, which contradicts this and causes
// compiler warnings. Still it is the way to go to really hide the space reserved for that.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wnonnull"
self.inputAssistantItem.leadingBarButtonGroups = nil;
self.inputAssistantItem.trailingBarButtonGroups = nil;
#pragma clang diagnostic pop
}
#endif
self.undoManager.groupsByEvent = NO;
[self rebuildUndoStack];
return self;
}
- (void)dealloc
{
self.inputView = 0;
self.inputAccessoryView = 0;
delete m_configuredImeState;
[super dealloc];
}
- (BOOL)needsKeyboardReconfigure:(Qt::InputMethodQueries)updatedProperties
{
if ((updatedProperties & Qt::ImEnabled)) {
Q_ASSERT([self currentImeState:Qt::ImEnabled].toBool());
// When switching on input-methods we need to consider hints and platform data
// as well, as the IM state that we were based on may have been invalidated when
// IM was switched off.
qImDebug("IM was turned on, we need to check hints and platform data as well");
updatedProperties |= (Qt::ImHints | Qt::ImPlatformData);
}
// Based on what we set up in initWithInputContext above
updatedProperties &= (Qt::ImHints | Qt::ImEnterKeyType | Qt::ImPlatformData);
if (!updatedProperties)
return NO;
for (uint i = 0; i < (sizeof(Qt::ImQueryAll) * CHAR_BIT); ++i) {
if (Qt::InputMethodQuery property = Qt::InputMethodQuery(int(updatedProperties & (1 << i)))) {
if ([self currentImeState:property] != m_configuredImeState->value(property)) {
qImDebug() << property << "has changed since text responder was configured, need reconfigure";
return YES;
}
}
}
return NO;
}
- (BOOL)canBecomeFirstResponder
{
return YES;
}
- (BOOL)becomeFirstResponder
{
FirstResponderCandidate firstResponderCandidate(self);
qImDebug() << "self:" << self << "first:" << [UIResponder currentFirstResponder];
if (![super becomeFirstResponder]) {
qImDebug() << self << "was not allowed to become first responder";
return NO;
}
qImDebug() << self << "became first responder";
return YES;
}
- (BOOL)resignFirstResponder
{
qImDebug() << "self:" << self << "first:" << [UIResponder currentFirstResponder];
// Don't allow activation events of the window that we're doing text on behalf on
// to steal responder.
if (FirstResponderCandidate::currentCandidate() == [self nextResponder]) {
qImDebug("not allowing parent window to steal responder");
return NO;
}
if (![super resignFirstResponder])
return NO;
qImDebug() << self << "resigned first responder";
// Dismissing the keyboard will trigger resignFirstResponder, but so will
// a regular responder transfer to another window. In the former case, iOS
// will set the new first-responder to our next-responder, and in the latter
// case we'll have an active responder candidate.
if (![UIResponder currentFirstResponder] && !FirstResponderCandidate::currentCandidate()) {
// No first responder set anymore, sync this with Qt by clearing the
// focus object.
m_inputContext->clearCurrentFocusObject();
} else if ([UIResponder currentFirstResponder] == [self nextResponder]) {
// We have resigned the keyboard, and transferred first responder back to the parent view
Q_ASSERT(!FirstResponderCandidate::currentCandidate());
if ([self currentImeState:Qt::ImEnabled].toBool()) {
// The current focus object expects text input, but there
// is no keyboard to get input from. So we clear focus.
qImDebug("no keyboard available, clearing focus object");
m_inputContext->clearCurrentFocusObject();
}
} else {
// We've lost responder status because another Qt window was made active,
// another QIOSTextResponder was made first-responder, another UIView was
// made first-responder, or the first-responder was cleared globally. In
// either of these cases we don't have to do anything.
qImDebug("lost first responder, but not clearing focus object");
}
return YES;
}
- (UIResponder*)nextResponder
{
return qApp->focusWindow() ?
reinterpret_cast<QUIView *>(qApp->focusWindow()->handle()->winId()) : 0;
}
// -------------------------------------------------------------------------
- (void)sendKeyPressRelease:(Qt::Key)key modifiers:(Qt::KeyboardModifiers)modifiers
{
QScopedValueRollback<BOOL> rollback(m_inSendEventToFocusObject, true);
QWindowSystemInterface::handleKeyEvent(qApp->focusWindow(), QEvent::KeyPress, key, modifiers);
QWindowSystemInterface::handleKeyEvent(qApp->focusWindow(), QEvent::KeyRelease, key, modifiers);
}
#ifndef QT_NO_SHORTCUT
- (void)sendShortcut:(QKeySequence::StandardKey)standardKey
{
const int keys = QKeySequence(standardKey)[0];
Qt::Key key = Qt::Key(keys & 0x0000FFFF);
Qt::KeyboardModifiers modifiers = Qt::KeyboardModifiers(keys & 0xFFFF0000);
[self sendKeyPressRelease:key modifiers:modifiers];
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
bool isEditAction = (action == @selector(cut:)
|| action == @selector(copy:)
|| action == @selector(paste:)
|| action == @selector(delete:)
|| action == @selector(toggleBoldface:)
|| action == @selector(toggleItalics:)
|| action == @selector(toggleUnderline:)
|| action == @selector(undo)
|| action == @selector(redo));
bool isSelectAction = (action == @selector(select:)
|| action == @selector(selectAll:)
|| action == @selector(paste:)
|| action == @selector(undo)
|| action == @selector(redo));
const bool unknownAction = !isEditAction && !isSelectAction;
const bool hasSelection = ![self selectedTextRange].empty;
if (unknownAction)
return [super canPerformAction:action withSender:sender];
return (hasSelection && isEditAction) || (!hasSelection && isSelectAction);
}
- (void)cut:(id)sender
{
Q_UNUSED(sender);
[self sendShortcut:QKeySequence::Cut];
}
- (void)copy:(id)sender
{
Q_UNUSED(sender);
[self sendShortcut:QKeySequence::Copy];
}
- (void)paste:(id)sender
{
Q_UNUSED(sender);
[self sendShortcut:QKeySequence::Paste];
}
- (void)select:(id)sender
{
Q_UNUSED(sender);
[self sendShortcut:QKeySequence::MoveToPreviousWord];
[self sendShortcut:QKeySequence::SelectNextWord];
}
- (void)selectAll:(id)sender
{
Q_UNUSED(sender);
[self sendShortcut:QKeySequence::SelectAll];
}
- (void)delete:(id)sender
{
Q_UNUSED(sender);
[self sendShortcut:QKeySequence::Delete];
}
- (void)toggleBoldface:(id)sender
{
Q_UNUSED(sender);
[self sendShortcut:QKeySequence::Bold];
}
- (void)toggleItalics:(id)sender
{
Q_UNUSED(sender);
[self sendShortcut:QKeySequence::Italic];
}
- (void)toggleUnderline:(id)sender
{
Q_UNUSED(sender);
[self sendShortcut:QKeySequence::Underline];
}
// -------------------------------------------------------------------------
- (void)undo
{
[self sendShortcut:QKeySequence::Undo];
[self rebuildUndoStack];
}
- (void)redo
{
[self sendShortcut:QKeySequence::Redo];
[self rebuildUndoStack];
}
- (void)registerRedo
{
NSUndoManager *undoMgr = self.undoManager;
[undoMgr beginUndoGrouping];
[undoMgr registerUndoWithTarget:self selector:@selector(redo) object:nil];
[undoMgr endUndoGrouping];
}
- (void)rebuildUndoStack
{
dispatch_async(dispatch_get_main_queue (), ^{
// Register dummy undo/redo operations to enable Cmd-Z and Cmd-Shift-Z
// Ensure we do this outside any undo/redo callback since NSUndoManager
// will treat registerUndoWithTarget as registering a redo when called
// from within a undo callback.
NSUndoManager *undoMgr = self.undoManager;
[undoMgr removeAllActions];
[undoMgr beginUndoGrouping];
[undoMgr registerUndoWithTarget:self selector:@selector(undo) object:nil];
[undoMgr endUndoGrouping];
// Schedule an operation that we immediately pop off to be able to schedule a redo
[undoMgr beginUndoGrouping];
[undoMgr registerUndoWithTarget:self selector:@selector(registerRedo) object:nil];
[undoMgr endUndoGrouping];
[undoMgr undo];
// Note that, perhaps because of a bug in UIKit, the buttons on the shortcuts bar ends up
// disabled if a undo/redo callback doesn't lead to a [UITextInputDelegate textDidChange].
// And we only call that method if Qt made changes to the text. The effect is that the buttons
// become disabled when there is nothing more to undo (Qt didn't change anything upon receiving
// an undo request). This seems to be OK behavior, so we let it stay like that unless it shows
// to cause problems.
});
}
// -------------------------------------------------------------------------
- (void)keyCommandTriggered:(UIKeyCommand *)keyCommand
{
Qt::Key key = Qt::Key_unknown;
Qt::KeyboardModifiers modifiers = Qt::NoModifier;
if (keyCommand.input == UIKeyInputLeftArrow)
key = Qt::Key_Left;
else if (keyCommand.input == UIKeyInputRightArrow)
key = Qt::Key_Right;
else if (keyCommand.input == UIKeyInputUpArrow)
key = Qt::Key_Up;
else if (keyCommand.input == UIKeyInputDownArrow)
key = Qt::Key_Down;
else
Q_UNREACHABLE();
if (keyCommand.modifierFlags & UIKeyModifierAlternate)
modifiers |= Qt::AltModifier;
if (keyCommand.modifierFlags & UIKeyModifierShift)
modifiers |= Qt::ShiftModifier;
if (keyCommand.modifierFlags & UIKeyModifierCommand)
modifiers |= Qt::ControlModifier;
[self sendKeyPressRelease:key modifiers:modifiers];
}
- (void)addKeyCommandsToArray:(NSMutableArray<UIKeyCommand *> *)array key:(NSString *)key
{
SEL s = @selector(keyCommandTriggered:);
[array addObject:[UIKeyCommand keyCommandWithInput:key modifierFlags:0 action:s]];
[array addObject:[UIKeyCommand keyCommandWithInput:key modifierFlags:UIKeyModifierShift action:s]];
[array addObject:[UIKeyCommand keyCommandWithInput:key modifierFlags:UIKeyModifierAlternate action:s]];
[array addObject:[UIKeyCommand keyCommandWithInput:key modifierFlags:UIKeyModifierAlternate|UIKeyModifierShift action:s]];
[array addObject:[UIKeyCommand keyCommandWithInput:key modifierFlags:UIKeyModifierCommand action:s]];
[array addObject:[UIKeyCommand keyCommandWithInput:key modifierFlags:UIKeyModifierCommand|UIKeyModifierShift action:s]];
}
- (NSArray<UIKeyCommand *> *)keyCommands
{
// Since keyCommands is called for every key
// press/release, we cache the result
static dispatch_once_t once;
static NSMutableArray<UIKeyCommand *> *array;
dispatch_once(&once, ^{
// We let Qt move the cursor around when the arrow keys are being used. This
// is normally implemented through UITextInput, but since IM in Qt have poor
// support for moving the cursor vertically, and even less support for selecting
// text across multiple paragraphs, we do this through key events.
array = [NSMutableArray<UIKeyCommand *> new];
[self addKeyCommandsToArray:array key:UIKeyInputUpArrow];
[self addKeyCommandsToArray:array key:UIKeyInputDownArrow];
[self addKeyCommandsToArray:array key:UIKeyInputLeftArrow];
[self addKeyCommandsToArray:array key:UIKeyInputRightArrow];
});
return array;
}
#endif // QT_NO_SHORTCUT
// -------------------------------------------------------------------------
- (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)) {
QScopedValueRollback<BOOL> rollback(m_inSelectionChange, true);
[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<BOOL> rollback(m_inSendEventToFocusObject);
m_inSendEventToFocusObject = YES;
QCoreApplication::sendEvent(focusObject, &e);
}
- (QVariant)currentImeState:(Qt::InputMethodQuery)query
{
return m_inputContext->imeState().currentState.value(query);
}
- (id<UITextInputTokenizer>)tokenizer
{
return [[[UITextInputStringTokenizer alloc] initWithTextInput:self] autorelease];
}
- (UITextPosition *)beginningOfDocument
{
return [QUITextPosition positionWithIndex:0];
}
- (UITextPosition *)endOfDocument
{
QString surroundingText = [self currentImeState:Qt::ImSurroundingText].toString();
int endPosition = surroundingText.length() + m_markedText.length();
return [QUITextPosition positionWithIndex:endPosition];
}
- (void)setSelectedTextRange:(UITextRange *)range
{
if (m_inSelectionChange) {
// After [UITextInputDelegate selectionWillChange], UIKit will cancel
// any ongoing auto correction (if enabled) and ask us to set an empty selection.
// This is contradictory to our current attempt to set a selection, so we ignore
// the callback. UIKit will be re-notified of the new selection after
// [UITextInputDelegate selectionDidChange].
return;
}
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 currentImeState:Qt::ImCursorPosition].toInt();
int anchorPos = [self currentImeState:Qt::ImAnchorPosition].toInt();
return [QUITextRange rangeWithNSRange:NSMakeRange(qMin(cursorPos, anchorPos), qAbs(anchorPos - cursorPos))];
}
- (NSString *)textInRange:(UITextRange *)range
{
QString text = [self currentImeState:Qt::ImSurroundingText].toString();
if (!m_markedText.isEmpty()) {
// [UITextInput textInRange] is sparsely documented, but it turns out that unconfirmed
// marked text should be seen as a part of the text document. This is different from
// ImSurroundingText, which excludes it.
int cursorPos = [self currentImeState:Qt::ImCursorPosition].toInt();
text = text.left(cursorPos) + m_markedText + text.mid(cursorPos);
}
int s = static_cast<QUITextPosition *>([range start]).index;
int e = static_cast<QUITextPosition *>([range end]).index;
return text.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.
markedTextFormat.setBackground(QColor(206, 221, 238));
}
QList<QInputMethodEvent::Attribute> 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<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;
const int posWithIndex = p + offset;
const int textLength = [self currentImeState:Qt::ImSurroundingText].toString().length();
if (posWithIndex < 0 || posWithIndex > textLength)
return nil;
return [QUITextPosition positionWithIndex:posWithIndex];
}
- (UITextPosition *)positionFromPosition:(UITextPosition *)position inDirection:(UITextLayoutDirection)direction offset:(NSInteger)offset
{
int p = static_cast<QUITextPosition *>(position).index;
switch (direction) {
case UITextLayoutDirectionLeft:
return [QUITextPosition positionWithIndex:p - offset];
case UITextLayoutDirectionRight:
return [QUITextPosition positionWithIndex:p + offset];
default:
// Qt doesn't support getting the position above or below the current position, so
// for those cases we just return the current position, making it a no-op.
return position;
}
}
- (UITextPosition *)positionWithinRange:(UITextRange *)range farthestInDirection:(UITextLayoutDirection)direction
{
NSRange r = static_cast<QUITextRange *>(range).range;
if (direction == UITextLayoutDirectionRight)
return [QUITextPosition positionWithIndex:r.location + r.length];
return [QUITextPosition positionWithIndex:r.location];
}
- (NSInteger)offsetFromPosition:(UITextPosition *)fromPosition toPosition:(UITextPosition *)toPosition
{
int f = static_cast<QUITextPosition *>(fromPosition).index;
int t = static_cast<QUITextPosition *>(toPosition).index;
return t - f;
}
- (UIView *)textInputView
{
auto *focusWindow = QGuiApplication::focusWindow();
if (!focusWindow)
return nil;
// 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(focusWindow->handle());
QPlatformWindow *topLevel = focusWindow->handle();
while (QPlatformWindow *p = topLevel->parent())
topLevel = p;
return reinterpret_cast<UIView *>(topLevel->winId());
}
- (CGRect)firstRectForRange:(UITextRange *)range
{
QObject *focusObject = QGuiApplication::focusObject();
if (!focusObject)
return CGRectZero;
// Using a work-around to get the current rect until
// a better API is in place:
if (!m_markedText.isEmpty())
return CGRectZero;
int cursorPos = [self currentImeState:Qt::ImCursorPosition].toInt();
int anchorPos = [self currentImeState: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, qMin(cursorPos, anchorPos), qAbs(cursorPos - anchorPos), 0);
e = QInputMethodEvent(m_markedText, attrs);
[self sendEventToFocusObject:e];
}
return startRect.united(endRect).toCGRect();
}
- (NSArray<UITextSelectionRect *> *)selectionRectsForRange:(UITextRange *)range
{
Q_UNUSED(range);
// This method is supposed to return a rectangle for each line with selection. Since we don't
// expose an API in Qt/IM for getting this information, and since we never seems to be getting
// a call from UIKit for this, we return an empty array until a need arise.
return [[NSArray<UITextSelectionRect *> new] autorelease];
}
- (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 cursorRect.toCGRect();
}
- (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 currentImeState:Qt::ImSurroundingText].toString().length();
return [QUITextRange rangeWithNSRange:NSMakeRange(p, l - p)];
}
- (UITextPosition *)closestPositionToPoint:(CGPoint)point
{
QPointF p = QPointF::fromCGPoint(point);
const QTransform mapToLocal = QGuiApplication::inputMethod()->inputItemTransform().inverted();
int textPos = QInputMethod::queryFocusObject(Qt::ImCursorPosition, p * mapToLocal).toInt();
return [QUITextPosition positionWithIndex:textPos];
}
- (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 currentImeState: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 currentImeState: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...
}
#ifndef Q_OS_TVOS
- (NSDictionary *)textStylingAtPosition:(UITextPosition *)position inDirection:(UITextStorageDirection)direction
{
Q_UNUSED(position);
Q_UNUSED(direction);
QObject *focusObject = QGuiApplication::focusObject();
if (!focusObject)
return @{};
// Assume position is the same as the cursor for now. QInputMethodQueryEvent with Qt::ImFont
// needs to be extended to take an extra position argument before this can be fully correct.
QInputMethodQueryEvent e(Qt::ImFont);
QCoreApplication::sendEvent(focusObject, &e);
QFont qfont = qvariant_cast<QFont>(e.value(Qt::ImFont));
UIFont *uifont = [UIFont fontWithName:qfont.family().toNSString() size:qfont.pointSize()];
if (!uifont)
return @{};
return @{NSFontAttributeName: uifont};
}
#endif
- (NSDictionary *)markedTextStyle
{
return [NSDictionary dictionary];
}
- (BOOL)hasText
{
return YES;
}
- (void)insertText:(NSString *)text
{
QObject *focusObject = QGuiApplication::focusObject();
if (!focusObject)
return;
if ([text isEqualToString:@"\n"]) {
[self sendKeyPressRelease:Qt::Key_Return modifiers:Qt::NoModifier];
// An onEnter handler of a TextInput might move to the next input by calling
// nextInput.forceActiveFocus() which changes the focusObject.
// In that case we don't want to hide the VKB.
if (focusObject != QGuiApplication::focusObject()) {
qImDebug() << "focusObject already changed, not resigning first responder.";
return;
}
if (self.returnKeyType == UIReturnKeyDone || self.returnKeyType == UIReturnKeyGo
|| self.returnKeyType == UIReturnKeySend || self.returnKeyType == UIReturnKeySearch)
[self resignFirstResponder];
return;
}
QInputMethodEvent e;
e.setCommitString(QString::fromNSString(text));
[self sendEventToFocusObject:e];
}
- (void)deleteBackward
{
// UITextInput selects the text to be deleted before calling this method. To avoid
// drawing the selection, we flush after posting the key press/release.
[self sendKeyPressRelease:Qt::Key_Backspace modifiers:Qt::NoModifier];
}
@end