diff --git a/src/widgets/widgets/qwidgettextcontrol.cpp b/src/widgets/widgets/qwidgettextcontrol.cpp index 76029714d5d..8c849ddbae3 100644 --- a/src/widgets/widgets/qwidgettextcontrol.cpp +++ b/src/widgets/widgets/qwidgettextcontrol.cpp @@ -1299,7 +1299,7 @@ void QWidgetTextControlPrivate::keyPressEvent(QKeyEvent *e) } #ifndef QT_NO_SHORTCUT else if (e == QKeySequence::InsertParagraphSeparator) { - cursor.insertBlock(); + insertParagraphSeparator(); e->accept(); goto accept; } else if (e == QKeySequence::InsertLineSeparator) { @@ -3198,6 +3198,49 @@ QString QWidgetTextControl::toMarkdown(QTextDocument::MarkdownFeatures features) } #endif +void QWidgetTextControlPrivate::insertParagraphSeparator() +{ + // clear blockFormat properties that the user is unlikely to want duplicated: + // - don't insert
automatically + // - the next paragraph after a heading should be a normal paragraph + // - remove the bottom margin from the last list item before appending + // - the next checklist item after a checked item should be unchecked + auto blockFmt = cursor.blockFormat(); + auto charFmt = cursor.charFormat(); + blockFmt.clearProperty(QTextFormat::BlockTrailingHorizontalRulerWidth); + if (blockFmt.hasProperty(QTextFormat::HeadingLevel)) { + blockFmt.clearProperty(QTextFormat::HeadingLevel); + charFmt = QTextCharFormat(); + } + if (cursor.currentList()) { + auto existingFmt = cursor.blockFormat(); + existingFmt.clearProperty(QTextBlockFormat::BlockBottomMargin); + cursor.setBlockFormat(existingFmt); + if (blockFmt.marker() == QTextBlockFormat::MarkerType::Checked) + blockFmt.setMarker(QTextBlockFormat::MarkerType::Unchecked); + } + + // After a blank line, reset block and char formats. I.e. you can end a list, + // block quote, etc. by hitting enter twice, and get back to normal paragraph style. + if (cursor.block().text().isEmpty() && + !cursor.blockFormat().hasProperty(QTextFormat::BlockTrailingHorizontalRulerWidth) && + !cursor.blockFormat().hasProperty(QTextFormat::BlockCodeLanguage)) { + blockFmt = QTextBlockFormat(); + const bool blockFmtChanged = (cursor.blockFormat() != blockFmt); + charFmt = QTextCharFormat(); + cursor.setBlockFormat(blockFmt); + cursor.setCharFormat(charFmt); + // If the user hit enter twice just to get back to default format, + // don't actually insert a new block. But if the user then hits enter + // yet again, the block format will not change, so we will insert a block. + // This is what many word processors do. + if (blockFmtChanged) + return; + } + + cursor.insertBlock(blockFmt, charFmt); +} + void QWidgetTextControlPrivate::append(const QString &text, Qt::TextFormat format) { QTextCursor tmp(doc); diff --git a/src/widgets/widgets/qwidgettextcontrol_p_p.h b/src/widgets/widgets/qwidgettextcontrol_p_p.h index a7a19e748d0..c94f859e6f6 100644 --- a/src/widgets/widgets/qwidgettextcontrol_p_p.h +++ b/src/widgets/widgets/qwidgettextcontrol_p_p.h @@ -179,6 +179,7 @@ public: bool isPreediting() const; void commitPreedit(); + void insertParagraphSeparator(); void append(const QString &text, Qt::TextFormat format = Qt::AutoText); QTextDocument *doc; diff --git a/tests/auto/widgets/widgets/qtextedit/tst_qtextedit.cpp b/tests/auto/widgets/widgets/qtextedit/tst_qtextedit.cpp index 8d5716c1292..9126b874724 100644 --- a/tests/auto/widgets/widgets/qtextedit/tst_qtextedit.cpp +++ b/tests/auto/widgets/widgets/qtextedit/tst_qtextedit.cpp @@ -58,6 +58,8 @@ #include "../../../shared/platforminputcontext.h" #include +Q_LOGGING_CATEGORY(lcTests, "qt.widgets.tests") + //Used in copyAvailable typedef QPair keyPairType; typedef QList pairListType; @@ -209,6 +211,9 @@ private slots: void preeditCharFormat_data(); void preeditCharFormat(); + void nextFormatAfterEnterPressed_data(); + void nextFormatAfterEnterPressed(); + private: void createSelection(); int blockCount() const; @@ -2895,5 +2900,94 @@ void tst_QTextEdit::preeditCharFormat() delete w; } +void tst_QTextEdit::nextFormatAfterEnterPressed_data() +{ + typedef QMap pmap; + QTest::addColumn("html"); + QTest::addColumn("enterKeyCount"); + QTest::addColumn("expectedPrevBlockProps"); + QTest::addColumn("expectedPrevCharProps"); + QTest::addColumn("expectedNewBlockProps"); + QTest::addColumn("expectedNewCharProps"); + + // the BlockBottomMargin on "two" will be removed: property() returns invalid QVariant + QTest::newRow("bottom margin after ordered list") << "
  1. one
  2. two
" << 1 + << pmap{{QTextFormat::BlockBottomMargin, {}}} << pmap{} + << pmap{{QTextFormat::BlockBottomMargin, 12}} << pmap{}; + QTest::newRow("double enter after list: default format") << "
  1. one
  2. two
" << 2 + << pmap{{QTextFormat::BlockBottomMargin, {}}} << pmap{} + << pmap{} << pmap{}; + QTest::newRow("continue block quote") << "
I'll be back
" << 1 + << pmap{{QTextFormat::BlockLeftMargin, 40}} << pmap{} + << pmap{{QTextFormat::BlockLeftMargin, 40}} << pmap{}; + QTest::newRow("double enter after block quote") << "
I'll be back
" << 2 + << pmap{{QTextFormat::BlockLeftMargin, 40}} << pmap{} + << pmap{{QTextFormat::BlockLeftMargin, {}}} << pmap{}; + QTest::newRow("bottom margin after bullet list") << "
  • one
  • two
" << 1 + << pmap{{QTextFormat::BlockBottomMargin, {}}} << pmap{} + << pmap{{QTextFormat::BlockBottomMargin, 12}} << pmap{}; + QTest::newRow("paragraph after heading") << "

so big!

" << 1 + << pmap{{QTextFormat::HeadingLevel, 1}} << pmap{} + << pmap{{QTextFormat::HeadingLevel, {}}} << pmap{}; + QTest::newRow("paragraph after hrule") << "

blah blah


" << 1 + << pmap{} << pmap{} + << pmap{{QTextFormat::BlockTrailingHorizontalRulerWidth, {}}} << pmap{}; +} + +void tst_QTextEdit::nextFormatAfterEnterPressed() +{ + typedef QMap pmap; + QFETCH(QString, html); + QFETCH(int, enterKeyCount); + QFETCH(pmap, expectedPrevBlockProps); + QFETCH(pmap, expectedPrevCharProps); + QFETCH(pmap, expectedNewBlockProps); + QFETCH(pmap, expectedNewCharProps); + + ed->setHtml(html); + QTextCursor cursor = ed->textCursor(); + cursor.movePosition(QTextCursor::End); + ed->setTextCursor(cursor); + + if (lcTests().isDebugEnabled()) { + ed->show(); + QTest::qWait(500); + } + + for (int i = 0; i < enterKeyCount; ++i) + QTest::keyClick(ed, Qt::Key_Enter); + QTest::keyClicks(ed, "foo"); + + if (lcTests().isDebugEnabled()) { + // visually see what happened when debug is enabled + QTest::qWait(500); + qCDebug(lcTests) << "new block" << Qt::hex << ed->textCursor().blockFormat().properties(); + qCDebug(lcTests) << "new char" << Qt::hex << ed->textCursor().charFormat().properties(); + } + + // if expectedNewBlockProps is empty, we expect the current block format to be the default format + if (expectedNewBlockProps.isEmpty()) + QCOMPARE(ed->textCursor().blockFormat(), QTextBlockFormat()); + // otherwise we expect to find certain property values in the current block format + else for (auto it = expectedNewBlockProps.constBegin(); it != expectedNewBlockProps.constEnd(); ++it) + QCOMPARE(ed->textCursor().blockFormat().property(it.key()), it.value()); + + // if expectedNewCharProps is empty, we expect the current char format to be the default format + if (expectedNewCharProps.isEmpty()) + QCOMPARE(ed->textCursor().charFormat(), QTextCharFormat()); + // otherwise we expect to find certain property values in the current char format + else for (auto it = expectedNewCharProps.constBegin(); it != expectedNewCharProps.constEnd(); ++it) + QCOMPARE(ed->textCursor().charFormat().property(it.key()), it.value()); + + // check the cases where QWidgetTextControlPrivate::insertParagraphSeparator() should modify + // the previous block's block format and/or char format + auto prevBlockCursor = ed->textCursor(); + prevBlockCursor.movePosition(QTextCursor::PreviousBlock); + for (auto it = expectedPrevBlockProps.constBegin(); it != expectedPrevBlockProps.constEnd(); ++it) + QCOMPARE(prevBlockCursor.blockFormat().property(it.key()), it.value()); + for (auto it = expectedPrevCharProps.constBegin(); it != expectedPrevCharProps.constEnd(); ++it) + QCOMPARE(prevBlockCursor.charFormat().property(it.key()), it.value()); +} + QTEST_MAIN(tst_QTextEdit) #include "tst_qtextedit.moc"