diff --git a/src/widgets/itemviews/qabstractitemview.cpp b/src/widgets/itemviews/qabstractitemview.cpp index 6ee9c2c7d1d..67f24f071d6 100644 --- a/src/widgets/itemviews/qabstractitemview.cpp +++ b/src/widgets/itemviews/qabstractitemview.cpp @@ -87,6 +87,7 @@ QAbstractItemViewPrivate::QAbstractItemViewPrivate() selectionBehavior(QAbstractItemView::SelectItems), currentlyCommittingEditor(nullptr), pressClosedEditor(false), + waitForIMCommit(false), pressedModifiers(Qt::NoModifier), pressedPosition(QPoint(-1, -1)), pressedAlreadySelected(false), @@ -885,10 +886,26 @@ QAbstractItemDelegate *QAbstractItemView::itemDelegate() const */ QVariant QAbstractItemView::inputMethodQuery(Qt::InputMethodQuery query) const { + Q_D(const QAbstractItemView); const QModelIndex current = currentIndex(); - if (!current.isValid() || query != Qt::ImCursorRectangle) - return QAbstractScrollArea::inputMethodQuery(query); - return visualRect(current); + QVariant result; + if (current.isValid()) { + if (QWidget *currentEditor; + d->waitForIMCommit && (currentEditor = d->editorForIndex(current).widget)) { + // An editor is open but the initial preedit is still ongoing. Delegate + // queries to the editor and map coordinates from editor to this view. + result = currentEditor->inputMethodQuery(query); + if (result.typeId() == QMetaType::QRect) { + const QRect editorRect = result.value(); + result = QRect(currentEditor->mapTo(this, editorRect.topLeft()), editorRect.size()); + } + } else if (query == Qt::ImCursorRectangle) { + result = visualRect(current); + } + } + if (!result.isValid()) + result = QAbstractScrollArea::inputMethodQuery(query); + return result; } /*! @@ -2599,14 +2616,53 @@ void QAbstractItemView::timerEvent(QTimerEvent *event) */ void QAbstractItemView::inputMethodEvent(QInputMethodEvent *event) { - if (event->commitString().isEmpty() && event->preeditString().isEmpty()) { + Q_D(QAbstractItemView); + // When QAbstractItemView::AnyKeyPressed is used, a new IM composition might + // start before the editor widget acquires focus. Changing focus would interrupt + // the composition, so we keep focus on the view until that first composition + // is complete, and pass QInputMethoEvents on to the editor widget so that the + // user gets the expected feedback. See also inputMethodQuery, which redirects + // calls to the editor widget during that period. + bool forwardEventToEditor = false; + const bool commit = !event->commitString().isEmpty(); + const bool preediting = !event->preeditString().isEmpty(); + if (QWidget *currentEditor = d->editorForIndex(currentIndex()).widget) { + if (d->waitForIMCommit) { + if (commit || !preediting) { + // commit or cancel + d->waitForIMCommit = false; + QApplication::sendEvent(currentEditor, event); + if (!commit) { + QAbstractItemDelegate *delegate = itemDelegateForIndex(currentIndex()); + if (delegate) + delegate->setEditorData(currentEditor, currentIndex()); + d->selectAllInEditor(currentEditor); + } + if (currentEditor->focusPolicy() != Qt::NoFocus) + currentEditor->setFocus(); + } else { + // more pre-editing + QApplication::sendEvent(currentEditor, event); + } + return; + } + } else if (preediting) { + // don't set focus when the editor opens + d->waitForIMCommit = true; + // but pass preedit on to editor + forwardEventToEditor = true; + } else if (!commit) { event->ignore(); return; } if (!edit(currentIndex(), AnyKeyPressed, event)) { - if (!event->commitString().isEmpty()) + d->waitForIMCommit = false; + if (commit) keyboardSearch(event->commitString()); event->ignore(); + } else if (QWidget *currentEditor; forwardEventToEditor + && (currentEditor = d->editorForIndex(currentIndex()).widget)) { + QApplication::sendEvent(currentEditor, event); } } @@ -2685,7 +2741,10 @@ bool QAbstractItemView::edit(const QModelIndex &index, EditTrigger trigger, QEve if (QWidget *w = (d->persistent.isEmpty() ? static_cast(nullptr) : d->editorForIndex(index).widget.data())) { if (w->focusPolicy() == Qt::NoFocus) return false; - w->setFocus(); + if (!d->waitForIMCommit) + w->setFocus(); + else + updateMicroFocus(); return true; } @@ -4241,6 +4300,28 @@ void QAbstractItemViewPrivate::updateGeometry() q->updateGeometry(); } +/* + Handles selection of content for some editors containing QLineEdit. + + ### Qt 7 This should be done by a virtual method in QAbstractItemDelegate. +*/ +void QAbstractItemViewPrivate::selectAllInEditor(QWidget *editor) +{ + while (QWidget *fp = editor->focusProxy()) + editor = fp; + +#if QT_CONFIG(lineedit) + if (QLineEdit *le = qobject_cast(editor)) + le->selectAll(); +#endif +#if QT_CONFIG(spinbox) + if (QSpinBox *sb = qobject_cast(editor)) + sb->selectAll(); + else if (QDoubleSpinBox *dsb = qobject_cast(editor)) + dsb->selectAll(); +#endif +} + QWidget *QAbstractItemViewPrivate::editor(const QModelIndex &index, const QStyleOptionViewItem &options) { @@ -4260,20 +4341,7 @@ QWidget *QAbstractItemViewPrivate::editor(const QModelIndex &index, if (w->parent() == viewport) QWidget::setTabOrder(q, w); - // Special cases for some editors containing QLineEdit - QWidget *focusWidget = w; - while (QWidget *fp = focusWidget->focusProxy()) - focusWidget = fp; -#if QT_CONFIG(lineedit) - if (QLineEdit *le = qobject_cast(focusWidget)) - le->selectAll(); -#endif -#if QT_CONFIG(spinbox) - if (QSpinBox *sb = qobject_cast(focusWidget)) - sb->selectAll(); - else if (QDoubleSpinBox *dsb = qobject_cast(focusWidget)) - dsb->selectAll(); -#endif + selectAllInEditor(w); } } @@ -4444,7 +4512,10 @@ bool QAbstractItemViewPrivate::openEditor(const QModelIndex &index, QEvent *even q->setState(QAbstractItemView::EditingState); w->show(); - w->setFocus(); + if (!waitForIMCommit) + w->setFocus(); + else + q->updateMicroFocus(); if (event) QCoreApplication::sendEvent(w->focusProxy() ? w->focusProxy() : w, event); diff --git a/src/widgets/itemviews/qabstractitemview_p.h b/src/widgets/itemviews/qabstractitemview_p.h index f45d642dcfc..74850de9927 100644 --- a/src/widgets/itemviews/qabstractitemview_p.h +++ b/src/widgets/itemviews/qabstractitemview_p.h @@ -140,6 +140,7 @@ public: bool sendDelegateEvent(const QModelIndex &index, QEvent *event) const; bool openEditor(const QModelIndex &index, QEvent *event); void updateEditorData(const QModelIndex &topLeft, const QModelIndex &bottomRight); + void selectAllInEditor(QWidget *w); QItemSelectionModel::SelectionFlags multiSelectionCommand(const QModelIndex &index, const QEvent *event) const; @@ -367,6 +368,7 @@ public: QBasicTimer pressClosedEditorWatcher; QPersistentModelIndex lastEditedIndex; bool pressClosedEditor; + bool waitForIMCommit; QPersistentModelIndex enteredIndex; QPersistentModelIndex pressedIndex; diff --git a/tests/auto/widgets/itemviews/qabstractitemview/tst_qabstractitemview.cpp b/tests/auto/widgets/itemviews/qabstractitemview/tst_qabstractitemview.cpp index 2ac1e927d9b..d904f97d821 100644 --- a/tests/auto/widgets/itemviews/qabstractitemview/tst_qabstractitemview.cpp +++ b/tests/auto/widgets/itemviews/qabstractitemview/tst_qabstractitemview.cpp @@ -164,6 +164,8 @@ private slots: void mouseSelection_data(); void mouseSelection(); void scrollerSmoothScroll(); + void inputMethodOpensEditor_data(); + void inputMethodOpensEditor(); private: static QAbstractItemView *viewFromString(const QByteArray &viewType, QWidget *parent = nullptr) @@ -3102,5 +3104,78 @@ void tst_QAbstractItemView::scrollerSmoothScroll() QTest::mouseRelease(view.viewport(), Qt::LeftButton, Qt::NoModifier, dragPosition); } +/*! + Verify that starting the editing of an item with a key press while a composing + input method is active doesn't break the input method. See QTBUG-54848. +*/ +void tst_QAbstractItemView::inputMethodOpensEditor_data() +{ + QTest::addColumn("editItem"); + QTest::addColumn("preedit"); + QTest::addColumn("commit"); + + QTest::addRow("IM accepted") << QPoint(1, 1) << "chang" << QString("长"); + QTest::addRow("IM cancelled") << QPoint(25, 25) << "chang" << QString(); +} + +void tst_QAbstractItemView::inputMethodOpensEditor() +{ + QTableWidget tableWidget(50, 50); + tableWidget.setEditTriggers(QAbstractItemView::AnyKeyPressed); + for (int r = 0; r < 50; ++r) { + for (int c = 0; c < 50; ++c ) + tableWidget.setItem(r, c, new QTableWidgetItem(QString("Item %1:%2").arg(r).arg(c))); + } + + tableWidget.show(); + QVERIFY(QTest::qWaitForWindowActive(&tableWidget)); + + const auto sendInputMethodEvent = [](const QString &preeditText, const QString &commitString = {}){ + QInputMethodEvent imEvent(preeditText, {}); + imEvent.setCommitString(commitString); + QApplication::sendEvent(QApplication::focusWidget(), &imEvent); + }; + + QCOMPARE(QApplication::focusWidget(), &tableWidget); + + QFETCH(QPoint, editItem); + QFETCH(QString, preedit); + QFETCH(QString, commit); + + tableWidget.setCurrentCell(editItem.y(), editItem.x()); + const QString orgText = tableWidget.currentItem()->text(); + const QModelIndex currentIndex = tableWidget.currentIndex(); + QCOMPARE(tableWidget.inputMethodQuery(Qt::ImCursorRectangle), tableWidget.visualRect(currentIndex)); + + // simulate the start of input via a composing input method + sendInputMethodEvent(preedit.left(1)); + QCOMPARE(tableWidget.state(), QAbstractItemView::EditingState); + QLineEdit *editor = tableWidget.findChild(); + QVERIFY(editor); + QCOMPARE(editor->text(), QString()); + // the focus must remain with the tableWidget, as otherwise the compositing is interrupted + QCOMPARE(QApplication::focusWidget(), &tableWidget); + // the item view delegates input method queries to the editor + const QRect cursorRect = tableWidget.inputMethodQuery(Qt::ImCursorRectangle).toRect(); + QVERIFY(cursorRect.isValid()); + QVERIFY(tableWidget.visualRect(currentIndex).intersects(cursorRect)); + + // finish preediting, then commit or cancel the input + sendInputMethodEvent(preedit); + sendInputMethodEvent(QString(), commit); + // editing continues, the editor now has focus + QCOMPARE(tableWidget.state(), QAbstractItemView::EditingState); + QVERIFY(editor->hasFocus()); + // finish editing + QTest::keyClick(editor, Qt::Key_Return); + if (commit.isEmpty()) { + // if composition was cancelled, then the item's value is unchanged + QCOMPARE(tableWidget.currentItem()->text(), orgText); + } else { + // otherwise, the item's value is now the commit string + QTRY_COMPARE(tableWidget.currentItem()->text(), commit); + } +} + QTEST_MAIN(tst_QAbstractItemView) #include "tst_qabstractitemview.moc"