Skip to content

Commit

Permalink
Add API to get more information for each line in a QML Text element
Browse files Browse the repository at this point in the history
Previously there was no way to know what area is occupied by each line
in a QML Text element.

This commit adds new API to expose implicitWidth and isLast
on QQuickTextLine for use in the lineLaidOut signal.
It also adds improved documentation to the lineLaidOut signal and
an example usage of the new API to the text layout example.

An example use case of the new API is eg. to allow embedding
timestamps and indicators within a text paragraph, to enable
creating more efficient layouts.

[ChangeLog][QtQuick][Text] Added new API that exposes implicitWidth,
and isLast on the QQuickTextLine for use in the lineLaidOut signal.
This allows the user to layout other items relative to the lines
of text.

Fixes: QTBUG-78277
Change-Id: Ibc754db17c78efb01468106aba32e30d70d2f4df
  • Loading branch information
Timur Kristóf committed Oct 28, 2019
1 parent 9129188 commit 68c4e09
Show file tree
Hide file tree
Showing 7 changed files with 239 additions and 12 deletions.
19 changes: 19 additions & 0 deletions examples/quick/text/styledtext-layout.qml
Expand Up @@ -68,8 +68,27 @@ Rectangle {
line.y -= height - margin
line.x = width / 2 + margin
}

if (line.isLast) {
lastLineMarker.x = line.x + line.implicitWidth
lastLineMarker.y = line.y + (line.height - lastLineMarker.height) / 2
}
}
//! [layout]

Rectangle {
id: lastLineMarker
color: "#44cccccc"
width: theEndText.width + margin
height: theEndText.height + margin

Text {
id: theEndText
text: "THE\nEND"
anchors.centerIn: parent
font.pixelSize: myText.font.pixelSize / 2
}
}
}

}
79 changes: 68 additions & 11 deletions src/quick/items/qquicktext.cpp
Expand Up @@ -475,13 +475,36 @@ void QQuickTextLine::setLineOffset(int offset)
m_lineOffset = offset;
}

void QQuickTextLine::setFullLayoutTextLength(int length)
{
m_fullLayoutTextLength = length;
}

int QQuickTextLine::number() const
{
if (m_line)
return m_line->lineNumber() + m_lineOffset;
return 0;
}

qreal QQuickTextLine::implicitWidth() const
{
if (m_line)
return m_line->naturalTextWidth();
return 0;
}

bool QQuickTextLine::isLast() const
{
if (m_line && (m_line->textStart() + m_line->textLength()) == m_fullLayoutTextLength) {
// Ensure that isLast will change if the user reduced the width of the line
// so that the text no longer fits.
return m_line->width() >= m_line->naturalTextWidth();
}

return false;
}

qreal QQuickTextLine::width() const
{
if (m_line)
Expand Down Expand Up @@ -543,12 +566,13 @@ bool QQuickTextPrivate::isLineLaidOutConnected()
IS_SIGNAL_CONNECTED(q, QQuickText, lineLaidOut, (QQuickTextLine *));
}

void QQuickTextPrivate::setupCustomLineGeometry(QTextLine &line, qreal &height, int lineOffset)
void QQuickTextPrivate::setupCustomLineGeometry(QTextLine &line, qreal &height, int fullLayoutTextLength, int lineOffset)
{
Q_Q(QQuickText);

if (!textLine)
textLine = new QQuickTextLine;
textLine->setFullLayoutTextLength(fullLayoutTextLength);
textLine->setLine(&line);
textLine->setY(height);
textLine->setHeight(0);
Expand Down Expand Up @@ -733,7 +757,7 @@ QRectF QQuickTextPrivate::setupTextLayout(qreal *const baseline)
QTextLine line = layout.createLine();
for (visibleCount = 1; ; ++visibleCount) {
if (customLayout) {
setupCustomLineGeometry(line, naturalHeight);
setupCustomLineGeometry(line, naturalHeight, layoutText.length());
} else {
setLineGeometry(line, lineWidth, naturalHeight);
}
Expand Down Expand Up @@ -1045,7 +1069,7 @@ QRectF QQuickTextPrivate::setupTextLayout(qreal *const baseline)
QTextLine elidedLine = elideLayout->createLine();
elidedLine.setPosition(QPointF(0, height));
if (customLayout) {
setupCustomLineGeometry(elidedLine, height, visibleCount - 1);
setupCustomLineGeometry(elidedLine, height, elideText.length(), visibleCount - 1);
} else {
setLineGeometry(elidedLine, lineWidth, height);
}
Expand Down Expand Up @@ -1252,20 +1276,43 @@ QQuickText::~QQuickText()
\qmlsignal QtQuick::Text::lineLaidOut(object line)
This signal is emitted for each line of text that is laid out during the layout
process. The specified \a line object provides more details about the line that
process in plain text or styled text mode. It is not emitted in rich text mode.
The specified \a line object provides more details about the line that
is currently being laid out.
This gives the opportunity to position and resize a line as it is being laid out.
It can for example be used to create columns or lay out text around objects.
The properties of the specified \a line object are:
\list
\li number (read-only)
\li x
\li y
\li width
\li height
\endlist
\table
\header
\li Property name
\li Description
\row
\li number (read-only)
\li Line number, starts with zero.
\row
\li x
\li Specifies the line's x position inside the \c Text element.
\row
\li y
\li Specifies the line's y position inside the \c Text element.
\row
\li width
\li Specifies the width of the line.
\row
\li height
\li Specifies the height of the line.
\row
\li implicitWidth (read-only)
\li The width that the line would naturally occupy based on its contents,
not taking into account any modifications made to \a width.
\row
\li isLast (read-only)
\li Whether the line is the last. This property can change if you
set the \a width property to a different value.
\endtable
For example, this will move the first 5 lines of a Text item by 100 pixels to the right:
\code
Expand All @@ -1277,6 +1324,16 @@ QQuickText::~QQuickText()
}
\endcode
The following example will allow you to position an item at the end of the last line:
\code
onLineLaidOut: {
if (line.isLast) {
lastLineMarker.x = line.x + line.implicitWidth
lastLineMarker.y = line.y + (line.height - lastLineMarker.height) / 2
}
}
\endcode
The corresponding handler is \c onLineLaidOut.
*/

Expand Down
6 changes: 6 additions & 0 deletions src/quick/items/qquicktext_p.h
Expand Up @@ -310,13 +310,18 @@ class QQuickTextLine : public QObject
Q_PROPERTY(qreal height READ height WRITE setHeight)
Q_PROPERTY(qreal x READ x WRITE setX)
Q_PROPERTY(qreal y READ y WRITE setY)
Q_PROPERTY(qreal implicitWidth READ implicitWidth)
Q_PROPERTY(bool isLast READ isLast)

public:
QQuickTextLine();

void setLine(QTextLine* line);
void setLineOffset(int offset);
void setFullLayoutTextLength(int length);
int number() const;
qreal implicitWidth() const;
bool isLast() const;

qreal width() const;
void setWidth(qreal width);
Expand All @@ -334,6 +339,7 @@ class QQuickTextLine : public QObject
QTextLine *m_line;
qreal m_height;
int m_lineOffset;
int m_fullLayoutTextLength;
};

QT_END_NAMESPACE
Expand Down
2 changes: 1 addition & 1 deletion src/quick/items/qquicktext_p_p.h
Expand Up @@ -181,7 +181,7 @@ class Q_QUICK_PRIVATE_EXPORT QQuickTextPrivate : public QQuickImplicitSizeItemPr
void ensureDoc();

QRectF setupTextLayout(qreal * const baseline);
void setupCustomLineGeometry(QTextLine &line, qreal &height, int lineOffset = 0);
void setupCustomLineGeometry(QTextLine &line, qreal &height, int fullLayoutTextLength, int lineOffset = 0);
bool isLinkActivatedConnected();
bool isLinkHoveredConnected();
static QString anchorAt(const QTextLayout *layout, const QPointF &mousePos);
Expand Down
9 changes: 9 additions & 0 deletions tests/auto/quick/qquicktext/data/lineLayout.qml
Expand Up @@ -14,6 +14,9 @@ Rectangle {
textFormat: Text.StyledText
focus: true

property int lastLineNumber: -1
property bool receivedMultipleLastLines

text: "<b>Lorem ipsum</b> dolor sit amet, consectetur adipiscing elit. Integer at ante dui. Sed eu egestas est.
<br/><p><i>Maecenas nec libero leo. Sed ac leo eget ipsum ultricies viverra sit amet eu orci. Praesent et tortor risus, viverra accumsan sapien. Sed faucibus eleifend lectus, sed euismod urna porta eu. Aenean ultricies lectus ut orci dictum quis convallis nisi ultrices. Nunc elit mi, iaculis a porttitor rutrum, venenatis malesuada nisi. Suspendisse turpis quam, euismod non imperdiet et, rutrum nec ligula. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam semper tristique metus eu sodales. Integer eget risus ipsum. Quisque ut risus ut nulla tristique volutpat at sit amet nisl. Aliquam pulvinar auctor diam nec bibendum.</i><br/><p>Quisque luctus sapien id arcu volutpat pharetra. Praesent pretium imperdiet euismod. Integer fringilla rhoncus condimentum. Quisque sit amet ornare nulla. Cras sapien augue, sagittis a dictum id, suscipit et nunc. Cras vitae augue in enim elementum venenatis sed nec risus. Sed nisi quam, mollis quis auctor ac, vestibulum in neque. Vivamus eu justo risus. Suspendisse vel mollis est. Vestibulum gravida interdum mi, in molestie neque gravida in. Donec nibh odio, mattis facilisis vulputate et, scelerisque ut felis. Sed ornare eros nec odio aliquam eu varius augue adipiscing. Vivamus sit amet massa dapibus sapien pulvinar consectetur a sit amet felis. Cras non mi id libero dictum iaculis id dignissim eros. Praesent eget enim dui, sed bibendum neque. Ut interdum nisl id leo malesuada ornare. Pellentesque id nisl eu odio volutpat posuere et at massa. Pellentesque nec lorem justo. Integer sem urna, pharetra sed sagittis vitae, condimentum ac felis. Ut vitae sapien ac tortor adipiscing pharetra. Cras tristique urna tempus ante volutpat eleifend non eu ligula. Mauris sodales nisl et lorem tristique sodales. Mauris arcu orci, vehicula semper cursus ac, dapibus ut mi."
Expand All @@ -30,6 +33,12 @@ Rectangle {
line.x = line.width * 2 + 60
line.height = 20
}
// Save last line number, it is important to do this after setting the width
if (line.isLast) {
receivedMultipleLastLines = (lastLineNumber !== -1) && (lastLineNumber !== line.number)
lastLineNumber = line.number
}
}
}
}
80 changes: 80 additions & 0 deletions tests/auto/quick/qquicktext/data/lineLayoutImplicitWidth.qml
@@ -0,0 +1,80 @@
/****************************************************************************
**
** Copyright (C) 2019 Jolla Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:BSD$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** BSD License Usage
** Alternatively, you may use this file under the terms of the BSD license
** as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
** * Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** * Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in
** the documentation and/or other materials provided with the
** distribution.
** * Neither the name of The Qt Company Ltd nor the names of its
** contributors may be used to endorse or promote products derived
** from this software without specific prior written permission.
**
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
**
** $QT_END_LICENSE$
**
****************************************************************************/

import QtQuick 2.0

Rectangle {
id: main
width: 800; height: 600

property real off: 0

Text {
id: myText
objectName: "myText"
wrapMode: Text.WordWrap
font.pixelSize: 14
textFormat: Text.PlainText
focus: true
anchors.fill: parent

// The autotest will retrieve these so that it can verify them
property var lineImplicitWidths: []

text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam quis ante tristique, fermentum magna at, varius lacus. Donec elementum orci sit amet ligula efficitur, eget sodales orci porttitor. Etiam laoreet tellus quis nisi mollis lacinia. Cras vitae nisl sed nunc semper blandit. Duis egestas commodo lacus non congue. Fusce quis rhoncus urna. And magna arcu, sodales vitae nunc vel, rutrum hendrerit magna. Nullam imperdiet porttitor sem at euismod. Morbi faucibus libero sit amet vestibulum aliquam. Duis consectetur lacinia malesuada. Sed quis ante dui. Name dignissim faucibus felis. Quisque dapibus aliquam ante, eu cursus elit dictum in. Mauris placerat efficitur rutrum."

onLineLaidOut: {
var n = line.number

// Save information about the line so the autotest can retrieve it
lineImplicitWidths[n] = line.implicitWidth
}
}
}
56 changes: 56 additions & 0 deletions tests/auto/quick/qquicktext/tst_qquicktext.cpp
Expand Up @@ -35,6 +35,7 @@
#include <QTextDocument>
#include <QtQml/qqmlengine.h>
#include <QtQml/qqmlcomponent.h>
#include <QtQml/qjsvalue.h>
#include <QtQuick/private/qquicktext_p.h>
#include <QtQuick/private/qquickmousearea_p.h>
#include <private/qquicktext_p_p.h>
Expand Down Expand Up @@ -119,6 +120,7 @@ private slots:
void lineLaidOut();
void lineLaidOutRelayout();
void lineLaidOutHAlign();
void lineLaidOutImplicitWidth();

void imgTagsBaseUrl_data();
void imgTagsBaseUrl();
Expand Down Expand Up @@ -2756,6 +2758,13 @@ void tst_qquicktext::lineLaidOut()
}
}

// Ensure that isLast was correctly emitted
int lastLineNumber = myText->property("lastLineNumber").toInt();
QCOMPARE(lastLineNumber, myText->lineCount() - 1);
// Ensure that only one line was considered last (after changing its width)
bool receivedMultipleLastLines = myText->property("receivedMultipleLastLines").toBool();
QVERIFY(!receivedMultipleLastLines);

delete window;
}

Expand Down Expand Up @@ -2865,6 +2874,53 @@ void tst_qquicktext::imgTagsBaseUrl_data()
<< 181.;
}

void tst_qquicktext::lineLaidOutImplicitWidth()
{
QScopedPointer<QQuickView> window(createView(testFile("lineLayoutImplicitWidth.qml")));

QQuickText *myText = window->rootObject()->findChild<QQuickText*>("myText");
QVERIFY(myText != nullptr);

QQuickTextPrivate *textPrivate = QQuickTextPrivate::get(myText);
QVERIFY(textPrivate != nullptr);

// Retrieve the saved implicitWidth values of each rendered line
QVariant widthsProperty = myText->property("lineImplicitWidths");
QVERIFY(!widthsProperty.isNull());
QVERIFY(widthsProperty.isValid());
QVERIFY(widthsProperty.canConvert<QJSValue>());
QJSValue widthsValue = widthsProperty.value<QJSValue>();
QVERIFY(widthsValue.isArray());
int lineCount = widthsValue.property("length").toInt();
QVERIFY(lineCount > 0);

// Create the same text layout by hand
// Note that this approach needs additional processing for styled text,
// so we only use it for plain text here.
QTextLayout layout;
layout.setCacheEnabled(true);
layout.setText(myText->text());
layout.setTextOption(textPrivate->layout.textOption());
layout.setFont(myText->font());
layout.beginLayout();
for (QTextLine line = layout.createLine(); line.isValid(); line = layout.createLine()) {
line.setLineWidth(myText->width());
}
layout.endLayout();

// Line count of the just created layout should match the rendered text
QCOMPARE(lineCount, layout.lineCount());

// Go through each line and verify that the values emitted by lineLaidOut are correct
for (int i = 0; i < layout.lineCount(); ++i) {
qreal implicitWidth = widthsValue.property(i).toNumber();
QVERIFY(implicitWidth > 0);

QTextLine line = layout.lineAt(i);
QCOMPARE(implicitWidth, line.naturalTextWidth());
}
}

static QUrl substituteTestServerUrl(const QUrl &serverUrl, const QUrl &testUrl)
{
QUrl result = testUrl;
Expand Down

0 comments on commit 68c4e09

Please sign in to comment.