summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorShawn Rutledge <shawn.rutledge@qt.io>2021-10-19 11:13:11 +0200
committerShawn Rutledge <shawn.rutledge@qt.io>2021-12-02 22:22:59 +0100
commitdbb9579566f3accd8aa5fe61db9692991117afd3 (patch)
treef74836f3ec51860b8caca8424964bf2ff1cdc9e2
parentebac49dd45ca75b1592122df5935017d4592c89d (diff)
Text editing: smart block and char format after newline
When you are editing in a QTextEdit and press enter to start a new line, calling insertBlock() with no arguments tries to preserve the current charFormat and blockFormat. That is often OK: - if you hit enter at the end of a list item, you probably want another item in the same list - if you are writing code inside a code block, you're probably just writing the next statement on the next line: stay in the same block - margins, indents, tab positions should stay the same (but hopefully your editor has UI to manually reset the block format to default in case you are not continuing in the same style) But there are some exceptions we can apply to be helpful: - nobody ever wants to follow an <hr/> with another one (but hopefully the application has an action to insert one manually) - a heading is more likely to be followed by a paragraph, or perhaps a smaller heading; another heading at the same level is unlikely. We need to reset the char format, not only the block format, because the large font and heavy font weight are stored there. - when adding to a todo list, hitting enter at the end of the last task, let's assume the next task is not yet done, so it will be unchecked by default (else, why are you writing a todo list at all) To achieve that, we need to customize the formats and call the insertBlock() overload that takes them. The no-argument insertBlock() will continue to preserve the formats, because it's an old API that is used for much more than interactive editing. Additionally, word processors tend to let you end a list (for example) by hitting enter twice. In that case, you stay in the same paragraph that you created the first time you hit enter, but now the formats are reset to default, so that you can go on typing an ordinary paragraph, rather than having to mouse up to the toolbar to select the paragraph style in a combobox, or something like that. So we now do that: reset both block and char formats after you hit enter on a blank line; but if you then hit enter again, after the block format has been reset, then you will get the actual blank line (empty block) inserted. [ChangeLog][QtWidgets][QTextEdit] Hitting enter at the end of a line with a special block format (horizontal rule, heading, checklist item) now makes some "smart" adjustments to avoid retaining properties that are unlikely to be continued on the next line. Hitting enter twice now resets block and char formats to default. Fixes: QTBUG-48815 Task-number: QTBUG-80473 Fixes: QTBUG-97459 Change-Id: I3dfdd5b4c0d9ffb4673acc861cb7b5c22291df25 Reviewed-by: Eskil Abrahamsen Blomfeldt <eskil.abrahamsen-blomfeldt@qt.io>
-rw-r--r--src/widgets/widgets/qwidgettextcontrol.cpp45
-rw-r--r--src/widgets/widgets/qwidgettextcontrol_p_p.h1
-rw-r--r--tests/auto/widgets/widgets/qtextedit/tst_qtextedit.cpp94
3 files changed, 139 insertions, 1 deletions
diff --git a/src/widgets/widgets/qwidgettextcontrol.cpp b/src/widgets/widgets/qwidgettextcontrol.cpp
index 76029714d5..8c849ddbae 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 <hr/> 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 a7a19e748d..c94f859e6f 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 8d5716c129..9126b87472 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 <private/qinputmethod_p.h>
+Q_LOGGING_CATEGORY(lcTests, "qt.widgets.tests")
+
//Used in copyAvailable
typedef QPair<Qt::Key, Qt::KeyboardModifier> keyPairType;
typedef QList<keyPairType> 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<int, QVariant> pmap;
+ QTest::addColumn<QString>("html");
+ QTest::addColumn<int>("enterKeyCount");
+ QTest::addColumn<pmap>("expectedPrevBlockProps");
+ QTest::addColumn<pmap>("expectedPrevCharProps");
+ QTest::addColumn<pmap>("expectedNewBlockProps");
+ QTest::addColumn<pmap>("expectedNewCharProps");
+
+ // the BlockBottomMargin on "two" will be removed: property() returns invalid QVariant
+ QTest::newRow("bottom margin after ordered list") << "<ol><li>one</li><li>two</li></ol>" << 1
+ << pmap{{QTextFormat::BlockBottomMargin, {}}} << pmap{}
+ << pmap{{QTextFormat::BlockBottomMargin, 12}} << pmap{};
+ QTest::newRow("double enter after list: default format") << "<ol><li>one</li><li>two</li></ol>" << 2
+ << pmap{{QTextFormat::BlockBottomMargin, {}}} << pmap{}
+ << pmap{} << pmap{};
+ QTest::newRow("continue block quote") << "<blockquote>I'll be back</blockquote>" << 1
+ << pmap{{QTextFormat::BlockLeftMargin, 40}} << pmap{}
+ << pmap{{QTextFormat::BlockLeftMargin, 40}} << pmap{};
+ QTest::newRow("double enter after block quote") << "<blockquote>I'll be back</blockquote>" << 2
+ << pmap{{QTextFormat::BlockLeftMargin, 40}} << pmap{}
+ << pmap{{QTextFormat::BlockLeftMargin, {}}} << pmap{};
+ QTest::newRow("bottom margin after bullet list") << "<ul><li>one</li><li>two</li></ul>" << 1
+ << pmap{{QTextFormat::BlockBottomMargin, {}}} << pmap{}
+ << pmap{{QTextFormat::BlockBottomMargin, 12}} << pmap{};
+ QTest::newRow("paragraph after heading") << "<h1>so big!</h1>" << 1
+ << pmap{{QTextFormat::HeadingLevel, 1}} << pmap{}
+ << pmap{{QTextFormat::HeadingLevel, {}}} << pmap{};
+ QTest::newRow("paragraph after hrule") << "<p style='font-size:18px;'>blah blah<hr/></p>" << 1
+ << pmap{} << pmap{}
+ << pmap{{QTextFormat::BlockTrailingHorizontalRulerWidth, {}}} << pmap{};
+}
+
+void tst_QTextEdit::nextFormatAfterEnterPressed()
+{
+ typedef QMap<int, QVariant> 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"