From a55a852e2eb183f45fa7bca2ce08a477aab27995 Mon Sep 17 00:00:00 2001 From: Hannu Koivisto Date: Mon, 15 Nov 2010 16:33:15 +0200 Subject: New: MTextEdit: double click to select a word RevBy: Pekka Vuorela --- src/corelib/widgets/mtextedit.cpp | 102 +++++++++++++++++++++++++----------- src/corelib/widgets/mtextedit_p.h | 9 ++++ tests/ut_mtextedit/ut_mtextedit.cpp | 92 +++++++++++++++++++++++++++++++- tests/ut_mtextedit/ut_mtextedit.h | 2 + 4 files changed, 172 insertions(+), 33 deletions(-) diff --git a/src/corelib/widgets/mtextedit.cpp b/src/corelib/widgets/mtextedit.cpp index 64007aff..d5351d52 100755 --- a/src/corelib/widgets/mtextedit.cpp +++ b/src/corelib/widgets/mtextedit.cpp @@ -61,6 +61,7 @@ M_REGISTER_WIDGET(MTextEdit) namespace { const char * const UrlContentToolbarFile("UrlContentToolbar.xml"); const char * const EmailContentToolbarFile("EmailContentToolbar.xml"); + const int IgnorePreeditChangeAfterDoubleClickInterval(500); // in ms } @@ -312,7 +313,10 @@ MTextEditPrivate::MTextEditPrivate() editActive(false), omitInputMethodEvents(false), updateMicroFocusDisabled(0), - pendingMicroFocusUpdate(false) + pendingMicroFocusUpdate(false), + doubleClick(false), + previousReleaseWordStart(0), + previousReleaseWordEnd(0) { } @@ -1900,10 +1904,19 @@ void MTextEdit::handleMousePress(int cursorPosition, QGraphicsSceneMouseEvent *e { Q_D(MTextEdit); - if (textInteractionFlags() != Qt::NoTextInteraction && location) { - QString text = document()->toPlainText(); - MBreakIterator breakIterator(text); + MBreakIterator breakIterator(document()->toPlainText()); + d->doubleClick = d->lastMousePressTime.isValid() + && (d->lastMousePressTime.elapsed() < QApplication::doubleClickInterval()) + && (breakIterator.previousInclusive(cursorPosition) == d->previousReleaseWordStart) + && (breakIterator.next(cursorPosition) == d->previousReleaseWordEnd); + + if (d->doubleClick) { + d->lastMousePressTime = QTime(); + } else { + d->lastMousePressTime.start(); + } + if (textInteractionFlags() != Qt::NoTextInteraction && location) { if (breakIterator.isBoundary(cursorPosition) == true) { *location = MTextEdit::WordBoundary; } else { @@ -1932,14 +1945,17 @@ void MTextEdit::handleMouseRelease(int eventCursorPosition, QGraphicsSceneMouseE deselect(); - if (d->isPositionOnPreedit(eventCursorPosition) == false) { + d->previousReleaseWordStart = 0; + d->previousReleaseWordEnd = 0; + + if (d->isPositionOnPreedit(eventCursorPosition) == false + || (d->doubleClick && (textInteractionFlags() & Qt::TextSelectableByMouse))) { // input context takes care of releases happening on top of preedit, the rest // is handled here d->commitPreedit(); QString text = document()->toPlainText(); MBreakIterator breakIterator(text); - QInputContext *ic = qApp->inputContext(); // clicks on word boundaries move the cursor if (breakIterator.isBoundary(eventCursorPosition) == true) { @@ -1952,37 +1968,47 @@ void MTextEdit::handleMouseRelease(int eventCursorPosition, QGraphicsSceneMouseE if (location) { *location = MTextEdit::Word; } - if (inputMethodCorrectionEnabled()) { + d->previousReleaseWordStart = breakIterator.previousInclusive(eventCursorPosition); + d->previousReleaseWordEnd = breakIterator.next(eventCursorPosition); + if (inputMethodCorrectionEnabled() || d->doubleClick) { // clicks on words remove them from the normal contents and makes them preedit. - int start = breakIterator.previousInclusive(eventCursorPosition); - int end = breakIterator.next(eventCursorPosition); + const int start(d->previousReleaseWordStart); + const int end(d->previousReleaseWordEnd); QString preedit = text.mid(start, end - start); - d->storePreeditTextStyling(start, end); d->cursor()->setPosition(start); d->cursor()->setPosition(end, QTextCursor::KeepAnchor); - QTextDocumentFragment preeditFragment = d->cursor()->selection(); - d->cursor()->removeSelectedText(); - - // offer the word to input context as preedit. if the input context accepts it and - // plays nicely, it should offer the preedit right back, changing the mode to - // active. - bool injectionAccepted = false; - - if (ic) { - MPreeditInjectionEvent event(preedit, eventCursorPosition - start); - QCoreApplication::sendEvent(ic, &event); - - injectionAccepted = event.isAccepted(); - } + if (d->doubleClick) { // select the word + d->setMode(MTextEditModel::EditModeSelect); + model()->updateCursor(); + emit selectionChanged(); + d->sendCopyAvailable(true); + d->doubleClickSelectionTime = QTime::currentTime(); + } else { // activate pre-edit + d->storePreeditTextStyling(start, end); + QTextDocumentFragment preeditFragment = d->cursor()->selection(); + d->cursor()->removeSelectedText(); + + // offer the word to input context as preedit. if the input context accepts it and + // plays nicely, it should offer the preedit right back, changing the mode to + // active. + bool injectionAccepted = false; + + QInputContext *ic = qApp->inputContext(); + if (ic) { + MPreeditInjectionEvent event(preedit, eventCursorPosition - start); + QCoreApplication::sendEvent(ic, &event); + + injectionAccepted = event.isAccepted(); + } - // if injection wasn't supported, put the text back and fall back to cursor changing - if (injectionAccepted == false) { - d->cursor()->insertFragment(preeditFragment); - d->setCursorPosition(eventCursorPosition); - d->preeditStyling.clear(); + // if injection wasn't supported, put the text back and fall back to cursor changing + if (injectionAccepted == false) { + d->cursor()->insertFragment(preeditFragment); + d->setCursorPosition(eventCursorPosition); + d->preeditStyling.clear(); + } } - } else { d->setCursorPosition(eventCursorPosition); } @@ -2148,11 +2174,23 @@ void MTextEdit::inputMethodEvent(QInputMethodEvent *event) // FIXME: replacement info not honored. Q_D(MTextEdit); + QString preedit = event->preeditString(); + + // The first click of a double click sequence causes pre-edit injection, which implies + // that the IC sends pre-edit to the IM server and the input method plugin may send it + // back with correct formatting. Due to asynchronous operation the pre-edit sent by + // the plugin may arrive after the second click of the double click sequence, at which + // point it will remove double click selection unless we ignore it here. The logic to + // recognize exactly that particular pre-edit sending by time and content is not 100% + // precise but should be pretty safe. + if ((d->doubleClickSelectionTime.addMSecs(IgnorePreeditChangeAfterDoubleClickInterval) > QTime::currentTime()) + && !preedit.isEmpty() && preedit == selectedText()) { + d->doubleClickSelectionTime = QTime(); + return; + } if (d->omitInputMethodEvents) { return; } - - QString preedit = event->preeditString(); QString commitString = event->commitString(); bool emitReturnPressed = false; diff --git a/src/corelib/widgets/mtextedit_p.h b/src/corelib/widgets/mtextedit_p.h index 4d13856b..cb40cd00 100755 --- a/src/corelib/widgets/mtextedit_p.h +++ b/src/corelib/widgets/mtextedit_p.h @@ -24,6 +24,7 @@ #include #include #include +#include class QGraphicsSceneMouseEvent; class QValidator; @@ -153,6 +154,14 @@ private: int updateMicroFocusDisabled; bool pendingMicroFocusUpdate; + QTime lastMousePressTime; + // was the last mouse press event a double click event? + bool doubleClick; + // start and end indices of the word over which the mouse was released the last time + int previousReleaseWordStart; + int previousReleaseWordEnd; + // the last time when double click selection was done + QTime doubleClickSelectionTime; }; #endif diff --git a/tests/ut_mtextedit/ut_mtextedit.cpp b/tests/ut_mtextedit/ut_mtextedit.cpp index b532da3e..96a154df 100644 --- a/tests/ut_mtextedit/ut_mtextedit.cpp +++ b/tests/ut_mtextedit/ut_mtextedit.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include @@ -62,6 +63,7 @@ Q_DECLARE_METATYPE(Ut_MTextEdit::KeyList); Q_DECLARE_METATYPE(Qt::KeyboardModifiers); Q_DECLARE_METATYPE(Qt::FocusReason); Q_DECLARE_METATYPE(Qt::Key) +Q_DECLARE_METATYPE(Qt::TextInteractionFlag); const QString Ut_MTextEdit::testString = QString("jallajalla"); @@ -153,6 +155,15 @@ public: return false; } + bool event(QEvent *event) + { + if (event->type() == MPreeditInjectionEvent::eventNumber()) { + event->accept(); + return true; + } + return QInputContext::event(event); + } + void update() { ++updateCallCount; @@ -1167,6 +1178,85 @@ void Ut_MTextEdit::testSelection() } +void Ut_MTextEdit::testDoubleClick_data() +{ + QTest::addColumn("interactionFlag"); + + QTest::newRow("selectionEnabled") << Qt::TextSelectableByMouse; + QTest::newRow("selectionDisabled") << Qt::NoTextInteraction; +} + + +void Ut_MTextEdit::testDoubleClick() +{ + QFETCH(Qt::TextInteractionFlag, interactionFlag); + const bool enabled(interactionFlag & Qt::TextSelectableByMouse); + + m_subject->setTextInteractionFlags(interactionFlag); + + QSignalSpy copyAvailableSpy(m_subject.get(), SIGNAL(copyAvailable(bool))); + m_subject->setText("xyzzy quux"); + + QGraphicsSceneMouseEvent dummyEvent; + + // Double click the first word + m_subject->handleMousePress(2, &dummyEvent, NULL); + m_subject->handleMouseRelease(2, &dummyEvent, NULL); + { + QInputMethodEvent event("xyzzy", QList()); + m_subject->inputMethodEvent(&event); + } + m_subject->handleMousePress(3, &dummyEvent, NULL); + m_subject->handleMouseRelease(3, &dummyEvent, NULL); + + QCOMPARE(copyAvailableSpy.count(), enabled ? 1 : 0); + if (enabled) { + QCOMPARE(copyAvailableSpy.first().count(), 1); + QVERIFY(copyAvailableSpy.first().first().toBool()); + } + copyAvailableSpy.clear(); + + // Click the 2nd. word to loose the selection + m_subject->handleMousePress(8, &dummyEvent, NULL); + m_subject->handleMouseRelease(8, &dummyEvent, NULL); + { + QInputMethodEvent event("quux", QList()); + m_subject->inputMethodEvent(&event); + } + QCOMPARE(copyAvailableSpy.count(), enabled ? 1 : 0); + if (enabled) { + QCOMPARE(copyAvailableSpy.first().count(), 1); + QVERIFY(!copyAvailableSpy.first().first().toBool()); + } + copyAvailableSpy.clear(); + + // Click too slowly -> no selection + m_subject->handleMousePress(2, &dummyEvent, NULL); + m_subject->handleMouseRelease(2, &dummyEvent, NULL); + { + QInputMethodEvent event("xyzzy", QList()); + m_subject->inputMethodEvent(&event); + } + QTest::qWait(QApplication::doubleClickInterval()*2); + m_subject->handleMousePress(3, &dummyEvent, NULL); + m_subject->handleMouseRelease(3, &dummyEvent, NULL); + + QCOMPARE(copyAvailableSpy.count(), 0); + + // Click fast, but different words -> no selection + m_subject->handleMousePress(8, &dummyEvent, NULL); + m_subject->handleMouseRelease(8, &dummyEvent, NULL); + { + QInputMethodEvent event("quux", QList()); + m_subject->inputMethodEvent(&event); + } + m_subject->handleMousePress(2, &dummyEvent, NULL); + m_subject->handleMouseRelease(2, &dummyEvent, NULL); + + QCOMPARE(copyAvailableSpy.count(), 0); +} + + void Ut_MTextEdit::testAutoSelection() { Qt::TextInteractionFlag testSelection[] = { @@ -1184,7 +1274,7 @@ void Ut_MTextEdit::testAutoSelection() for (unsigned n = 0; n < sizeof(testSelection) / sizeof(testSelection[0]); ++n) { qDebug() << n << testSelection[n]; m_subject->setTextInteractionFlags(testSelection[n]); - + m_subject->setAutoSelectionEnabled(false); QVERIFY(m_subject->isAutoSelectionEnabled() == false); diff --git a/tests/ut_mtextedit/ut_mtextedit.h b/tests/ut_mtextedit/ut_mtextedit.h index 716361a3..b16c5272 100644 --- a/tests/ut_mtextedit/ut_mtextedit.h +++ b/tests/ut_mtextedit/ut_mtextedit.h @@ -95,6 +95,8 @@ private slots: //void testFeedback(); void testBadData(); void testSelection(); + void testDoubleClick_data(); + void testDoubleClick(); void testAutoSelection(); void testPrompt(); void testValidator(); -- cgit v1.2.3