QIOSTextInputResponder: factor out the "read-only" part to a QIOSTextResponder base class

QIOSTextInputResponder has two responsibilities; It takes care of
handling text input from UIKit, and to implement first responder
actions related to the edit menu, like copy and paste.

Currently the responder offers both writable (paste) and
readable (select, copy) actions. Because of the former, it means
that it can only be used for focus objects that accepts text input.

Since we also want to be able to show an edit menu for selections
done on a read-only input field, this patch will factor out the
read-only actions we want for that case into a QIOSTextResponder
base class. An instance of this class can be used as first responder
for a focus object that has read-only text, but otherwise doesn't
support text input. This part is implemented in a subsequent patch.

The remaining set of writeable actions, together with input method
handling, will continue to be in the QIOSTextInputResponder subclass.

Task-number: QTBUG-91545
Change-Id: I1c215bb509eb7820c6c60f7ad806f61a5de02ded
Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@qt.io>
This commit is contained in:
Richard Moe Gustavsen 2021-10-22 12:58:55 +02:00
parent c7e8133a95
commit 2211092aa5
5 changed files with 224 additions and 137 deletions

View File

@ -54,7 +54,7 @@ const char kImePlatformDataReturnKeyType[] = "returnKeyType";
@class QIOSLocaleListener; @class QIOSLocaleListener;
@class QIOSKeyboardListener; @class QIOSKeyboardListener;
@class QIOSTextInputResponder; @class QIOSTextResponder;
@protocol KeyboardState; @protocol KeyboardState;
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
@ -125,7 +125,7 @@ private:
QIOSLocaleListener *m_localeListener; QIOSLocaleListener *m_localeListener;
QIOSKeyboardListener *m_keyboardHideGesture; QIOSKeyboardListener *m_keyboardHideGesture;
QIOSTextInputResponder *m_textResponder; QIOSTextResponder *m_textResponder;
KeyboardState m_keyboardState; KeyboardState m_keyboardState;
ImeState m_imeState; ImeState m_imeState;
}; };

View File

@ -732,8 +732,7 @@ void QIOSInputContext::reset()
update(Qt::ImQueryAll); update(Qt::ImQueryAll);
[m_textResponder setMarkedText:@"" selectedRange:NSMakeRange(0, 0)]; [m_textResponder reset];
[m_textResponder notifyInputDelegate:Qt::ImQueryInput];
} }
/*! /*!
@ -747,9 +746,7 @@ void QIOSInputContext::reset()
void QIOSInputContext::commit() void QIOSInputContext::commit()
{ {
qImDebug("unmarking text"); qImDebug("unmarking text");
[m_textResponder commit];
[m_textResponder unmarkText];
[m_textResponder notifyInputDelegate:Qt::ImSurroundingText];
} }
QLocale QIOSInputContext::locale() const QLocale QIOSInputContext::locale() const

View File

@ -48,7 +48,18 @@ class QIOSInputContext;
QT_END_NAMESPACE QT_END_NAMESPACE
@interface QIOSTextInputResponder : UIResponder <UITextInputTraits, UIKeyInput, UITextInput> @interface QIOSTextResponder : UIResponder
- (instancetype)initWithInputContext:(QT_PREPEND_NAMESPACE(QIOSInputContext) *)context;
- (void)notifyInputDelegate:(Qt::InputMethodQueries)updatedProperties;
- (BOOL)needsKeyboardReconfigure:(Qt::InputMethodQueries)updatedProperties;
- (void)reset;
- (void)commit;
@end
@interface QIOSTextInputResponder : QIOSTextResponder <UITextInputTraits, UIKeyInput, UITextInput>
- (instancetype)initWithInputContext:(QT_PREPEND_NAMESPACE(QIOSInputContext) *)context; - (instancetype)initWithInputContext:(QT_PREPEND_NAMESPACE(QIOSInputContext) *)context;
- (BOOL)needsKeyboardReconfigure:(Qt::InputMethodQueries)updatedProperties; - (BOOL)needsKeyboardReconfigure:(Qt::InputMethodQueries)updatedProperties;

View File

@ -161,12 +161,11 @@
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@implementation QIOSTextInputResponder { @implementation QIOSTextResponder {
@public
QT_PREPEND_NAMESPACE(QIOSInputContext) *m_inputContext; QT_PREPEND_NAMESPACE(QIOSInputContext) *m_inputContext;
QT_PREPEND_NAMESPACE(QInputMethodQueryEvent) *m_configuredImeState; QT_PREPEND_NAMESPACE(QInputMethodQueryEvent) *m_configuredImeState;
QString m_markedText;
BOOL m_inSendEventToFocusObject; BOOL m_inSendEventToFocusObject;
BOOL m_inSelectionChange;
} }
- (instancetype)initWithInputContext:(QT_PREPEND_NAMESPACE(QIOSInputContext) *)inputContext - (instancetype)initWithInputContext:(QT_PREPEND_NAMESPACE(QIOSInputContext) *)inputContext
@ -174,14 +173,207 @@
if (!(self = [self init])) if (!(self = [self init]))
return self; return self;
m_inSendEventToFocusObject = NO;
m_inSelectionChange = NO;
m_inputContext = inputContext; m_inputContext = inputContext;
m_configuredImeState = static_cast<QInputMethodQueryEvent*>(m_inputContext->imeState().currentState.clone()); m_configuredImeState = static_cast<QInputMethodQueryEvent*>(m_inputContext->imeState().currentState.clone());
m_inSendEventToFocusObject = NO;
return self;
}
- (void)dealloc
{
delete m_configuredImeState;
[super dealloc];
}
- (QVariant)currentImeState:(Qt::InputMethodQuery)query
{
return m_inputContext->imeState().currentState.value(query);
}
- (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)notifyInputDelegate:(Qt::InputMethodQueries)updatedProperties
{
Q_UNUSED(updatedProperties);
}
- (BOOL)needsKeyboardReconfigure:(Qt::InputMethodQueries)updatedProperties
{
if (updatedProperties & Qt::ImEnabled) {
qImDebug() << "Qt::ImEnabled has changed since text responder was configured, need reconfigure";
return YES;
}
if (updatedProperties & Qt::ImReadOnly) {
qImDebug() << "Qt::ImReadOnly has changed since text responder was configured, need reconfigure";
return YES;
}
return NO;
}
- (void)reset
{
// Nothing to reset for read-only text fields
}
- (void)commit
{
// Nothing to commit for read-only text fields
}
// -------------------------------------------------------------------------
#ifndef QT_NO_SHORTCUT
- (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);
}
- (void)sendShortcut:(QKeySequence::StandardKey)standardKey
{
const QKeyCombination combination = QKeySequence(standardKey)[0];
[self sendKeyPressRelease:combination.key() modifiers:combination.keyboardModifiers()];
}
- (BOOL)hasSelection
{
QInputMethodQueryEvent query(Qt::ImAnchorPosition | Qt::ImCursorPosition);
QGuiApplication::sendEvent(QGuiApplication::focusObject(), &query);
int anchorPos = query.value(Qt::ImAnchorPosition).toInt();
int cursorPos = query.value(Qt::ImCursorPosition).toInt();
return anchorPos != cursorPos;
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
const bool isSelectAction =
action == @selector(select:) ||
action == @selector(selectAll:);
const bool isReadAction = action == @selector(copy:);
if (!isSelectAction && !isReadAction)
return [super canPerformAction:action withSender:sender];
const bool hasSelection = [self hasSelection];
return (!hasSelection && isSelectAction) || (hasSelection && isReadAction);
}
- (void)copy:(id)sender
{
Q_UNUSED(sender);
[self sendShortcut:QKeySequence::Copy];
}
- (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];
}
#endif // QT_NO_SHORTCUT
@end
// -------------------------------------------------------------------------
@implementation QIOSTextInputResponder {
QString m_markedText;
BOOL m_inSelectionChange;
}
- (instancetype)initWithInputContext:(QT_PREPEND_NAMESPACE(QIOSInputContext) *)inputContext
{
if (!(self = [super initWithInputContext:inputContext]))
return self;
m_inSelectionChange = NO;
QVariantMap platformData = m_configuredImeState->value(Qt::ImPlatformData).toMap(); QVariantMap platformData = m_configuredImeState->value(Qt::ImPlatformData).toMap();
Qt::InputMethodHints hints = Qt::InputMethodHints(m_configuredImeState->value(Qt::ImHints).toUInt()); Qt::InputMethodHints hints = Qt::InputMethodHints(m_configuredImeState->value(Qt::ImHints).toUInt());
Qt::EnterKeyType enterKeyType = Qt::EnterKeyType(m_configuredImeState->value(Qt::ImEnterKeyType).toUInt()); Qt::EnterKeyType enterKeyType = Qt::EnterKeyType(m_configuredImeState->value(Qt::ImEnterKeyType).toUInt());
switch (enterKeyType) { switch (enterKeyType) {
@ -264,29 +456,27 @@
{ {
self.inputView = 0; self.inputView = 0;
self.inputAccessoryView = 0; self.inputAccessoryView = 0;
delete m_configuredImeState;
[super dealloc]; [super dealloc];
} }
- (BOOL)needsKeyboardReconfigure:(Qt::InputMethodQueries)updatedProperties - (BOOL)needsKeyboardReconfigure:(Qt::InputMethodQueries)updatedProperties
{ {
if ((updatedProperties & Qt::ImEnabled)) { Qt::InputMethodQueries relevantProperties = updatedProperties;
Q_ASSERT([self currentImeState:Qt::ImEnabled].toBool()); if ((relevantProperties & Qt::ImEnabled)) {
// When switching on input-methods we need to consider hints and platform data // 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 // as well, as the IM state that we were based on may have been invalidated when
// IM was switched off. // IM was switched off.
qImDebug("IM was turned on, we need to check hints and platform data as well"); qImDebug("IM was turned on, we need to check hints and platform data as well");
updatedProperties |= (Qt::ImHints | Qt::ImPlatformData); relevantProperties |= (Qt::ImHints | Qt::ImPlatformData);
} }
// Based on what we set up in initWithInputContext above // Based on what we set up in initWithInputContext above
updatedProperties &= (Qt::ImHints | Qt::ImEnterKeyType | Qt::ImPlatformData); relevantProperties &= (Qt::ImHints | Qt::ImEnterKeyType | Qt::ImPlatformData);
if (!updatedProperties) if (!relevantProperties)
return NO; return [super needsKeyboardReconfigure:updatedProperties];
for (uint i = 0; i < (sizeof(Qt::ImQueryAll) * CHAR_BIT); ++i) { for (uint i = 0; i < (sizeof(Qt::ImQueryAll) * CHAR_BIT); ++i) {
if (Qt::InputMethodQuery property = Qt::InputMethodQuery(int(updatedProperties & (1 << i)))) { if (Qt::InputMethodQuery property = Qt::InputMethodQuery(int(updatedProperties & (1 << i)))) {
@ -297,100 +487,13 @@
} }
} }
return NO; return [super needsKeyboardReconfigure:updatedProperties];
}
- (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 #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)canPerformAction:(SEL)action withSender:(id)sender
{ {
bool isEditAction = (action == @selector(cut:) bool isEditAction = (action == @selector(cut:)
@ -410,7 +513,7 @@
|| action == @selector(redo)); || action == @selector(redo));
const bool unknownAction = !isEditAction && !isSelectAction; const bool unknownAction = !isEditAction && !isSelectAction;
const bool hasSelection = ![self selectedTextRange].empty; const bool hasSelection = [self hasSelection];
if (unknownAction) if (unknownAction)
return [super canPerformAction:action withSender:sender]; return [super canPerformAction:action withSender:sender];
@ -434,31 +537,12 @@
[self sendShortcut:QKeySequence::Cut]; [self sendShortcut:QKeySequence::Cut];
} }
- (void)copy:(id)sender
{
Q_UNUSED(sender);
[self sendShortcut:QKeySequence::Copy];
}
- (void)paste:(id)sender - (void)paste:(id)sender
{ {
Q_UNUSED(sender); Q_UNUSED(sender);
[self sendShortcut:QKeySequence::Paste]; [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 - (void)delete:(id)sender
{ {
Q_UNUSED(sender); Q_UNUSED(sender);
@ -647,11 +731,6 @@
QCoreApplication::sendEvent(focusObject, &e); QCoreApplication::sendEvent(focusObject, &e);
} }
- (QVariant)currentImeState:(Qt::InputMethodQuery)query
{
return m_inputContext->imeState().currentState.value(query);
}
- (id<UITextInputTokenizer>)tokenizer - (id<UITextInputTokenizer>)tokenizer
{ {
return [[[UITextInputStringTokenizer alloc] initWithTextInput:self] autorelease]; return [[[UITextInputStringTokenizer alloc] initWithTextInput:self] autorelease];

View File

@ -306,7 +306,7 @@ Q_LOGGING_CATEGORY(lcQpaTablet, "qt.qpa.input.tablet")
// Nor do we want to deactivate the Qt window if the new responder // Nor do we want to deactivate the Qt window if the new responder
// is temporarily handling text input on behalf of a Qt window. // is temporarily handling text input on behalf of a Qt window.
if ([responder isKindOfClass:[QIOSTextInputResponder class]]) { if ([responder isKindOfClass:[QIOSTextResponder class]]) {
while ((responder = [responder nextResponder])) { while ((responder = [responder nextResponder])) {
if ([responder isKindOfClass:[QUIView class]]) if ([responder isKindOfClass:[QUIView class]])
return NO; return NO;