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:
parent
c7e8133a95
commit
2211092aa5
@ -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;
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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];
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user