diff options
author | Shawn Rutledge <shawn.rutledge@qt.io> | 2022-08-17 21:57:52 +0200 |
---|---|---|
committer | Qt Cherry-pick Bot <cherrypick_bot@qt-project.org> | 2022-08-25 20:48:01 +0000 |
commit | d199a957577375063daf135535600483101c8d95 (patch) | |
tree | 40f5433183c7450f214b63fab8f830e6551a2372 | |
parent | 67ae7ec261f6543bb7059db5f1940b955d4553b1 (diff) |
Fix bad rendering after text selection
In 9db23e0e04906cf9ea33e23fa41f34955e5e6fe0 we added a new call to
createTextNode(), but it turns out that it could be called again for the
same block, a few lines below (in code that was originally added in
dfdc4ce825e814b30449cfa1c85d3d2f47f8a845), if we have not reached the
"last node that needed replacing or last block of the frame". This
resulted in a memory leak: TransformNodes were created without parents,
which b616028dae933e7ff6142d41bdafee8ad445a8ec tried to fix by calling
addCurrentTextNodeToRoot(). That change made the code in "Update the
position of the subsequent text blocks" re-add offsets that had already
been added, which had the effect of moving down the blocks below the
selected ones by the height of one line; but the new call to
addCurrentTextNodeToRoot() was anyway just adding empty TransformNode
instances to the scene graph. Afterwards we can see that any
TransformNode that does not have a GeometryNode as a child is not
actually rendering any text, it's just wasting memory. Having a debug
operator for QQuickTextEditPrivate::Node, and using a QQuickTextEdit
subclass in the autotest that checks the nodes corresponding to blocks
rendered at the end of updatePaintNode(), makes this easier to see
and verify.
So now, if createTextNode() has already been called, we avoid calling
it again a few lines below; but we keep the checks in place to make
sure that every node which does not have a parent will get added to
the scene graph (regardless of which line of code it was created on),
so that there's no leak.
It remains to be seen if this could be cleaned up further.
Amends 9db23e0e04906cf9ea33e23fa41f34955e5e6fe0 and
b616028dae933e7ff6142d41bdafee8ad445a8ec
Task-number: QTBUG-103819
Fixes: QTBUG-105208
Fixes: QTBUG-105555
Fixes: QTBUG-105728
Change-Id: I321980c705887bfdb37825f7e6ed8da3d200654e
Reviewed-by: Eskil Abrahamsen Blomfeldt <eskil.abrahamsen-blomfeldt@qt.io>
(cherry picked from commit b271755e6c9a5a939c3ad6dfca8da573779e06ee)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
-rw-r--r-- | src/quick/items/qquicktextedit.cpp | 16 | ||||
-rw-r--r-- | src/quick/items/qquicktextedit_p_p.h | 8 | ||||
-rw-r--r-- | tests/auto/quick/qquicktextedit/data/threeLines.qml | 7 | ||||
-rw-r--r-- | tests/auto/quick/qquicktextedit/tst_qquicktextedit.cpp | 87 |
4 files changed, 116 insertions, 2 deletions
diff --git a/src/quick/items/qquicktextedit.cpp b/src/quick/items/qquicktextedit.cpp index 8ce622d176..ba8d8a1fc4 100644 --- a/src/quick/items/qquicktextedit.cpp +++ b/src/quick/items/qquicktextedit.cpp @@ -2271,11 +2271,13 @@ QSGNode *QQuickTextEdit::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData * } } + bool createdNodeInView = false; if (inView) { if (!engine.hasContents()) { if (node && !node->parent()) d->addCurrentTextNodeToRoot(&engine, rootNode, node, nodeIterator, nodeStart); node = d->createTextNode(); + createdNodeInView = true; updateNodeTransform(node, nodeOffset); nodeStart = block.position(); } @@ -2285,13 +2287,13 @@ QSGNode *QQuickTextEdit::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData * if ((it.atEnd()) || block.next().position() >= firstCleanNode.startPos()) break; // last node that needed replacing or last block of the frame - QList<int>::const_iterator lowerBound = std::lower_bound(frameBoundaries.constBegin(), frameBoundaries.constEnd(), block.next().position()); if (node && (currentNodeSize > nodeBreakingSize || lowerBound == frameBoundaries.constEnd() || *lowerBound > nodeStart)) { currentNodeSize = 0; if (!node->parent()) d->addCurrentTextNodeToRoot(&engine, rootNode, node, nodeIterator, nodeStart); - node = d->createTextNode(); + if (!createdNodeInView) + node = d->createTextNode(); resetEngine(&engine, d->color, d->selectedTextColor, d->selectionColor); nodeStart = block.next().position(); } @@ -3357,6 +3359,16 @@ void QQuickTextEdit::clear() d->control->clear(); } +#ifndef QT_NO_DEBUG_STREAM +QDebug operator<<(QDebug debug, const QQuickTextEditPrivate::Node &n) +{ + QDebugStateSaver saver(debug); + debug.space(); + debug << "Node(startPos:" << n.m_startPos << "dirty:" << n.m_dirty << n.m_node << ')'; + return debug; +} +#endif + #if QT_VERSION < QT_VERSION_CHECK(7, 0, 0) void QQuickTextEdit::setOldSelectionDefault() { diff --git a/src/quick/items/qquicktextedit_p_p.h b/src/quick/items/qquicktextedit_p_p.h index b2cbd80626..db07462a5a 100644 --- a/src/quick/items/qquicktextedit_p_p.h +++ b/src/quick/items/qquicktextedit_p_p.h @@ -53,6 +53,10 @@ public: int m_startPos; QQuickTextNode* m_node; bool m_dirty; + +#ifndef QT_NO_DEBUG_STREAM + friend QDebug Q_QUICK_PRIVATE_EXPORT operator<<(QDebug, const Node &); +#endif }; typedef QList<Node>::iterator TextNodeIterator; @@ -202,6 +206,10 @@ public: static const int largeTextSizeThreshold; }; +#ifndef QT_NO_DEBUG_STREAM +QDebug Q_QUICK_PRIVATE_EXPORT operator<<(QDebug debug, const QQuickTextEditPrivate::Node &); +#endif + QT_END_NAMESPACE #endif // QQUICKTEXTEDIT_P_P_H diff --git a/tests/auto/quick/qquicktextedit/data/threeLines.qml b/tests/auto/quick/qquicktextedit/data/threeLines.qml new file mode 100644 index 0000000000..cee03bfa15 --- /dev/null +++ b/tests/auto/quick/qquicktextedit/data/threeLines.qml @@ -0,0 +1,7 @@ +import QtQuick +import Qt.test 1.0 + +NodeCheckerTextEdit { + width: 200; height: 100 + text: "Line 1\nLine 2\nLine 3\n" +} diff --git a/tests/auto/quick/qquicktextedit/tst_qquicktextedit.cpp b/tests/auto/quick/qquicktextedit/tst_qquicktextedit.cpp index b8861f101d..7445f72783 100644 --- a/tests/auto/quick/qquicktextedit/tst_qquicktextedit.cpp +++ b/tests/auto/quick/qquicktextedit/tst_qquicktextedit.cpp @@ -57,6 +57,8 @@ public: tst_qquicktextedit(); private slots: + void initTestCase() override; + void cleanup(); void text(); void width(); @@ -138,6 +140,7 @@ private slots: void implicitSizeBinding(); void largeTextObservesViewport_data(); void largeTextObservesViewport(); + void renderingAroundSelection(); void signal_editingfinished(); @@ -336,6 +339,56 @@ tst_qquicktextedit::tst_qquicktextedit() // } +class NodeCheckerTextEdit : public QQuickTextEdit +{ +public: + NodeCheckerTextEdit(QQuickItem *parent = nullptr) : QQuickTextEdit(parent) {} + + void populateLinePositions(QSGNode *node) + { + linePositions.clear(); + lastLinePosition = 0; + QSGNode *ch = node->firstChild(); + while (ch != node->lastChild()) { + QCOMPARE(ch->type(), QSGNode::TransformNodeType); + QSGTransformNode *tn = static_cast<QSGTransformNode *>(ch); + int y = 0; + if (!tn->matrix().isIdentity()) + y = tn->matrix().column(3).y(); + if (tn->childCount() == 0) { + // A TransformNode with no children is a waste of memory. + // So far, QQuickTextEdit still creates a couple of extras. + qCDebug(lcTests) << "ignoring leaf TransformNode" << tn << "@ y" << y; + } else { + qCDebug(lcTests) << "child" << tn << "@ y" << y << "has children" << tn->childCount(); + if (!linePositions.contains(y)) { + linePositions.append(y); + lastLinePosition = qMax(lastLinePosition, y); + } + } + ch = ch->nextSibling(); + } + std::sort(linePositions.begin(), linePositions.end()); + } + + QSGNode *updatePaintNode(QSGNode *node, UpdatePaintNodeData *data) override + { + QSGNode *ret = QQuickTextEdit::updatePaintNode(node, data); + qCDebug(lcTests) << "updated root node" << ret; + populateLinePositions(ret); + return ret; + } + + QList<int> linePositions; + int lastLinePosition; +}; + +void tst_qquicktextedit::initTestCase() +{ + QQmlDataTest::initTestCase(); + qmlRegisterType<NodeCheckerTextEdit>("Qt.test", 1, 0, "NodeCheckerTextEdit"); +} + void tst_qquicktextedit::cleanup() { // ensure not even skipped tests with custom input context leave it dangling @@ -3823,6 +3876,40 @@ void tst_qquicktextedit::largeTextObservesViewport() QCOMPARE(textPriv->cursorItem->isVisible(), textPriv->renderedRegion.intersects(textItem->cursorRectangle())); } +void tst_qquicktextedit::renderingAroundSelection() +{ + QQuickView window; + QVERIFY(QQuickTest::showView(window, testFileUrl("threeLines.qml"))); + NodeCheckerTextEdit *textItem = qmlobject_cast<NodeCheckerTextEdit*>(window.rootObject()); + QVERIFY(textItem); + QTRY_VERIFY(textItem->linePositions.count() > 0); + const auto linePositions = textItem->linePositions; + const int lastLinePosition = textItem->lastLinePosition; + QQuickTextEditPrivate *textPriv = QQuickTextEditPrivate::get(textItem); + QSignalSpy renderSpy(&window, &QQuickWindow::afterRendering); + + if (lcTests().isDebugEnabled()) + QTest::qWait(500); // for visual check; not needed in CI + + const int renderCount = renderSpy.count(); + QPoint p1 = textItem->mapToScene(textItem->positionToRectangle(8).center()).toPoint(); + QPoint p2 = textItem->mapToScene(textItem->positionToRectangle(10).center()).toPoint(); + qCDebug(lcTests) << "drag from" << p1 << "to" << p2; + QTest::mousePress(&window, Qt::LeftButton, Qt::NoModifier, p1); + QTest::mouseMove(&window, p2); + QTest::mouseRelease(&window, Qt::LeftButton, Qt::NoModifier, p2); + // ensure that QQuickTextEdit::updatePaintNode() has a chance to run + QTRY_COMPARE_GT(renderSpy.count(), renderCount); + + if (lcTests().isDebugEnabled()) + QTest::qWait(500); // for visual check; not needed in CI + + qCDebug(lcTests) << "TextEdit's nodes" << textPriv->textNodeMap; + qCDebug(lcTests) << "font" << textItem->font() << "line positions" << textItem->linePositions << "should be" << linePositions; + QCOMPARE(textItem->lastLinePosition, lastLinePosition); + QTRY_COMPARE(textItem->linePositions, linePositions); +} + void tst_qquicktextedit::signal_editingfinished() { QQuickView *window = new QQuickView(nullptr); |