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 QIOSKeyboardListener;
@class QIOSTextInputResponder;
@class QIOSTextResponder;
@protocol KeyboardState;
QT_BEGIN_NAMESPACE
@ -125,7 +125,7 @@ private:
QIOSLocaleListener *m_localeListener;
QIOSKeyboardListener *m_keyboardHideGesture;
QIOSTextInputResponder *m_textResponder;
QIOSTextResponder *m_textResponder;
KeyboardState m_keyboardState;
ImeState m_imeState;
};

View File

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

View File

@ -48,7 +48,18 @@ class QIOSInputContext;
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;
- (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(QInputMethodQueryEvent) *m_configuredImeState;
QString m_markedText;
BOOL m_inSendEventToFocusObject;
BOOL m_inSelectionChange;
}
- (instancetype)initWithInputContext:(QT_PREPEND_NAMESPACE(QIOSInputContext) *)inputContext
@ -174,14 +173,207 @@
if (!(self = [self init]))
return self;
m_inSendEventToFocusObject = NO;
m_inSelectionChange = NO;
m_inputContext = inputContext;
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();
Qt::InputMethodHints hints = Qt::InputMethodHints(m_configuredImeState->value(Qt::ImHints).toUInt());
Qt::EnterKeyType enterKeyType = Qt::EnterKeyType(m_configuredImeState->value(Qt::ImEnterKeyType).toUInt());
switch (enterKeyType) {
@ -264,29 +456,27 @@
{
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());
Qt::InputMethodQueries relevantProperties = updatedProperties;
if ((relevantProperties & Qt::ImEnabled)) {
// 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);
relevantProperties |= (Qt::ImHints | Qt::ImPlatformData);
}
// 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)
return NO;
if (!relevantProperties)
return [super needsKeyboardReconfigure:updatedProperties];
for (uint i = 0; i < (sizeof(Qt::ImQueryAll) * CHAR_BIT); ++i) {
if (Qt::InputMethodQuery property = Qt::InputMethodQuery(int(updatedProperties & (1 << i)))) {
@ -297,100 +487,13 @@
}
}
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;
return [super needsKeyboardReconfigure:updatedProperties];
}
// -------------------------------------------------------------------------
- (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:)
@ -410,7 +513,7 @@
|| action == @selector(redo));
const bool unknownAction = !isEditAction && !isSelectAction;
const bool hasSelection = ![self selectedTextRange].empty;
const bool hasSelection = [self hasSelection];
if (unknownAction)
return [super canPerformAction:action withSender:sender];
@ -434,31 +537,12 @@
[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);
@ -647,11 +731,6 @@
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];

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
// 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])) {
if ([responder isKindOfClass:[QUIView class]])
return NO;