From: Andrew den Exter Date: Tue, 19 Jun 2012 01:30:59 +0000 (+1000) Subject: Fix clicking on links in aligned or elided Text. X-Git-Tag: upstream/5.2.1~1591 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=3ae9c8c36097abffd73e29cea1b897ca7d704916;p=platform%2Fupstream%2Fqtdeclarative.git Fix clicking on links in aligned or elided Text. Adjust the mouse position to compensate for any alignment offsets and test the elided text layout for anchors if none is found in the normal layout. Change-Id: Idfda3f7e372d0f2d6c1b7bb5f22d7015d52e8239 Reviewed-by: Yann Bodson --- diff --git a/src/quick/items/qquicktext.cpp b/src/quick/items/qquicktext.cpp index f03afd6..b19b13f 100644 --- a/src/quick/items/qquicktext.cpp +++ b/src/quick/items/qquicktext.cpp @@ -47,6 +47,8 @@ #include #include "qquicktextnode_p.h" #include "qquickimage_p_p.h" +#include "qquicktextutil_p.h" + #include #include @@ -2011,36 +2013,13 @@ QRectF QQuickText::boundingRect() const Q_D(const QQuickText); QRectF rect = d->layedOutTextRect; + rect.moveLeft(QQuickTextUtil::alignedX(rect, width(), d->hAlign)); + rect.moveTop(QQuickTextUtil::alignedY(rect, height(), d->vAlign)); + if (d->style != Normal) rect.adjust(-1, 0, 1, 2); - // Could include font max left/right bearings to either side of rectangle. - qreal w = width(); - switch (d->hAlign) { - case AlignLeft: - case AlignJustify: - break; - case AlignRight: - rect.moveLeft(w - rect.width()); - break; - case AlignHCenter: - rect.moveLeft((w - rect.width()) / 2); - break; - } - - qreal h = height(); - switch (d->vAlign) { - case AlignTop: - break; - case AlignBottom: - rect.moveTop(h - rect.height()); - break; - case AlignVCenter: - rect.moveTop((h - rect.height()) / 2); - break; - } - return rect; } @@ -2131,7 +2110,7 @@ QSGNode *QQuickText::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *data d->updateType = QQuickTextPrivate::UpdateNone; - QRectF bounds = boundingRect(); + const qreal dy = QQuickTextUtil::alignedY(d->layedOutTextRect, height(), d->vAlign); // We need to make sure the layout is done in the current thread #if defined(Q_OS_MAC) @@ -2156,27 +2135,28 @@ QSGNode *QQuickText::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *data if (d->richText) { d->ensureDoc(); - node->addTextDocument(bounds.topLeft(), d->extra->doc, color, d->style, styleColor, linkColor); - } else if (d->elideMode == QQuickText::ElideNone || bounds.width() > 0.) { + const qreal dx = QQuickTextUtil::alignedX(d->layedOutTextRect, width(), d->hAlign); + node->addTextDocument(QPointF(dx, dy), d->extra->doc, color, d->style, styleColor, linkColor); + } else if (d->elideMode == QQuickText::ElideNone || d->layedOutTextRect.width() > 0.) { int unelidedLineCount = d->lineCount; if (d->elideLayout) unelidedLineCount -= 1; if (unelidedLineCount > 0) { node->addTextLayout( - QPoint(0, bounds.y()), + QPoint(0, dy), &d->layout, color, d->style, styleColor, linkColor, QColor(), QColor(), -1, -1, 0, unelidedLineCount); } if (d->elideLayout) - node->addTextLayout(QPoint(0, bounds.y()), d->elideLayout, color, d->style, styleColor, linkColor); + node->addTextLayout(QPoint(0, dy), d->elideLayout, color, d->style, styleColor, linkColor); } foreach (QQuickStyledTextImgTag *img, d->visibleImgTags) { QQuickPixmap *pix = img->pix; if (pix && pix->isReady()) - node->addImage(QRectF(img->pos.x(), img->pos.y() + bounds.y(), pix->width(), pix->height()), pix->image()); + node->addImage(QRectF(img->pos.x(), img->pos.y() + dy, pix->width(), pix->height()), pix->image()); } return node; } @@ -2397,28 +2377,42 @@ void QQuickText::componentComplete() d->updateLayout(); } - -QString QQuickTextPrivate::anchorAt(const QPointF &mousePos) +QString QQuickTextPrivate::anchorAt(const QTextLayout *layout, const QPointF &mousePos) { - if (styledText) { - for (int i = 0; i < layout.lineCount(); ++i) { - QTextLine line = layout.lineAt(i); - if (line.naturalTextRect().contains(mousePos)) { - int charPos = line.xToCursor(mousePos.x()); - foreach (const QTextLayout::FormatRange &formatRange, layout.additionalFormats()) { - if (formatRange.format.isAnchor() - && charPos >= formatRange.start - && charPos <= formatRange.start + formatRange.length) { - return formatRange.format.anchorHref(); - } + for (int i = 0; i < layout->lineCount(); ++i) { + QTextLine line = layout->lineAt(i); + if (line.naturalTextRect().contains(mousePos)) { + int charPos = line.xToCursor(mousePos.x(), QTextLine::CursorOnCharacter); + foreach (const QTextLayout::FormatRange &formatRange, layout->additionalFormats()) { + if (formatRange.format.isAnchor() + && charPos >= formatRange.start + && charPos < formatRange.start + formatRange.length) { + return formatRange.format.anchorHref(); } - break; } + break; } } return QString(); } +QString QQuickTextPrivate::anchorAt(const QPointF &mousePos) const +{ + Q_Q(const QQuickText); + QPointF translatedMousePos = mousePos; + translatedMousePos.ry() -= QQuickTextUtil::alignedY(layedOutTextRect, q->height(), vAlign); + if (styledText) { + QString link = anchorAt(&layout, translatedMousePos); + if (link.isEmpty() && elideLayout) + link = anchorAt(elideLayout, translatedMousePos); + return link; + } else if (richText && extra.isAllocated() && extra->doc) { + translatedMousePos.rx() -= QQuickTextUtil::alignedX(layedOutTextRect, q->width(), hAlign); + return extra->doc->documentLayout()->anchorAt(translatedMousePos); + } + return QString(); +} + bool QQuickTextPrivate::isLinkActivatedConnected() { Q_Q(QQuickText); @@ -2431,14 +2425,8 @@ void QQuickText::mousePressEvent(QMouseEvent *event) Q_D(QQuickText); QString link; - if (d->isLinkActivatedConnected()) { - if (d->styledText) - link = d->anchorAt(event->localPos()); - else if (d->richText) { - d->ensureDoc(); - link = d->extra->doc->documentLayout()->anchorAt(event->localPos()); - } - } + if (d->isLinkActivatedConnected()) + link = d->anchorAt(event->localPos()); if (link.isEmpty()) { event->setAccepted(false); @@ -2450,9 +2438,9 @@ void QQuickText::mousePressEvent(QMouseEvent *event) if (!event->isAccepted()) QQuickItem::mousePressEvent(event); - } + /*! \internal */ void QQuickText::mouseReleaseEvent(QMouseEvent *event) { @@ -2461,14 +2449,8 @@ void QQuickText::mouseReleaseEvent(QMouseEvent *event) // ### confirm the link, and send a signal out QString link; - if (d->isLinkActivatedConnected()) { - if (d->styledText) - link = d->anchorAt(event->localPos()); - else if (d->richText) { - d->ensureDoc(); - link = d->extra->doc->documentLayout()->anchorAt(event->localPos()); - } - } + if (d->isLinkActivatedConnected()) + link = d->anchorAt(event->localPos()); if (!link.isEmpty() && d->extra.isAllocated() && d->extra->activeLink == link) emit linkActivated(d->extra->activeLink); diff --git a/src/quick/items/qquicktext_p_p.h b/src/quick/items/qquicktext_p_p.h index 2afcd8f..985b1e1 100644 --- a/src/quick/items/qquicktext_p_p.h +++ b/src/quick/items/qquicktext_p_p.h @@ -166,7 +166,8 @@ public: QRectF setupTextLayout(qreal *const naturalWidth, qreal * const baseline); void setupCustomLineGeometry(QTextLine &line, qreal &height, int lineOffset = 0); bool isLinkActivatedConnected(); - QString anchorAt(const QPointF &pos); + static QString anchorAt(const QTextLayout *layout, const QPointF &mousePos); + QString anchorAt(const QPointF &pos) const; inline qreal lineHeight() const { return extra.isAllocated() ? extra->lineHeight : 1.0; } inline int maximumLineCount() const { return extra.isAllocated() ? extra->maximumLineCount : INT_MAX; } diff --git a/src/quick/items/qquicktextutil.cpp b/src/quick/items/qquicktextutil.cpp index c2c11b3..176301d 100644 --- a/src/quick/items/qquicktextutil.cpp +++ b/src/quick/items/qquicktextutil.cpp @@ -78,4 +78,37 @@ QQuickItem *QQuickTextUtil::createCursor( return item; } +qreal QQuickTextUtil::alignedX(const QRectF &rect, qreal width, int alignment) +{ + qreal x = 0; + switch (alignment) { + case Qt::AlignLeft: + case Qt::AlignJustify: + break; + case Qt::AlignRight: + x = width - rect.width(); + break; + case Qt::AlignHCenter: + x = (width - rect.width()) / 2; + break; + } + return x; +} + +qreal QQuickTextUtil::alignedY(const QRectF &rect, const qreal height, int alignment) +{ + qreal y = 0; + switch (alignment) { + case Qt::AlignTop: + break; + case Qt::AlignBottom: + y = height - rect.height(); + break; + case Qt::AlignVCenter: + y = (height - rect.height()) / 2; + break; + } + return y; +} + QT_END_NAMESPACE diff --git a/src/quick/items/qquicktextutil_p.h b/src/quick/items/qquicktextutil_p.h index 91ef40b..d6c05aa 100644 --- a/src/quick/items/qquicktextutil_p.h +++ b/src/quick/items/qquicktextutil_p.h @@ -66,12 +66,16 @@ public: template static void setCursorDelegate(Private *d, QQmlComponent *delegate); template static void createCursor(Private *d); + static qreal alignedX(const QRectF &rect, qreal width, int alignment); + static qreal alignedY(const QRectF &rect, qreal height, int alignment); + private: static QQuickItem *createCursor( QQmlComponent *component, QQuickItem *parent, const QRectF &cursorRectangle, const char *className); + }; template diff --git a/tests/auto/quick/qquicktext/tst_qquicktext.cpp b/tests/auto/quick/qquicktext/tst_qquicktext.cpp index 7d24de6..2bdf4a9 100644 --- a/tests/auto/quick/qquicktext/tst_qquicktext.cpp +++ b/tests/auto/quick/qquicktext/tst_qquicktext.cpp @@ -105,6 +105,7 @@ private slots: void letterSpacing(); void wordSpacing(); + void clickLink_data(); void clickLink(); void implicitSize_data(); @@ -1438,6 +1439,8 @@ public: mousePressEvent(event); else if (event->type() == QEvent::MouseButtonRelease) mouseReleaseEvent(event); + else if (event->type() == QEvent::MouseMove) + mouseMoveEvent(event); else qWarning() << "Trying to send unsupported event type"; } @@ -1455,36 +1458,339 @@ public slots: void linkClicked(QString l) { link = l; } }; -void tst_qquicktext::clickLink() +class TextMetrics { +public: + TextMetrics(const QString &text, Qt::TextElideMode elideMode = Qt::ElideNone) { - QString componentStr = "import QtQuick 2.0\nText { text: \"Hello world!\" }"; - QQmlComponent textComponent(&engine); - textComponent.setData(componentStr.toLatin1(), QUrl::fromLocalFile("")); - QQuickText *textObject = qobject_cast(textComponent.create()); - - QVERIFY(textObject != 0); + QString adjustedText = text; + adjustedText.replace(QLatin1Char('\n'), QChar(QChar::LineSeparator)); + if (elideMode == Qt::ElideLeft) + adjustedText = QChar(0x2026) + adjustedText; + else if (elideMode == Qt::ElideRight) + adjustedText = adjustedText + QChar(0x2026); + + layout.setText(adjustedText); + QTextOption option; + option.setUseDesignMetrics(true); + layout.setTextOption(option); - LinkTest test; - QObject::connect(textObject, SIGNAL(linkActivated(QString)), &test, SLOT(linkClicked(QString))); + layout.beginLayout(); + qreal height = 0; + QTextLine line = layout.createLine(); + while (line.isValid()) { + line.setLineWidth(FLT_MAX); + line.setPosition(QPointF(0, height)); + height += line.height(); + line = layout.createLine(); + } + layout.endLayout(); + } - { - QMouseEvent me(QEvent::MouseButtonPress,QPointF(textObject->x()/2, textObject->y()/2), Qt::LeftButton, Qt::NoButton, Qt::NoModifier); - static_cast(static_cast(textObject))->sendEvent(&me); + qreal width() const { return layout.maximumWidth(); } + QRectF characterRectangle( + int position, + int hAlign = Qt::AlignLeft, + int vAlign = Qt::AlignTop, + const QSizeF &bounds = QSizeF(240, 320)) const + { + qreal dy = 0; + switch (vAlign) { + case Qt::AlignBottom: + dy = bounds.height() - layout.boundingRect().height(); + break; + case Qt::AlignVCenter: + dy = (bounds.height() - layout.boundingRect().height()) / 2; + break; + default: + break; } - { - QMouseEvent me(QEvent::MouseButtonRelease,QPointF(textObject->x()/2, textObject->y()/2), Qt::LeftButton, Qt::NoButton, Qt::NoModifier); - static_cast(static_cast(textObject))->sendEvent(&me); + for (int i = 0; i < layout.lineCount(); ++i) { + QTextLine line = layout.lineAt(i); + if (position >= line.textStart() + line.textLength()) + continue; + qreal dx = 0; + switch (hAlign) { + case Qt::AlignRight: + dx = bounds.width() - line.naturalTextWidth(); + break; + case Qt::AlignHCenter: + dx = (bounds.width() - line.naturalTextWidth()) / 2; + break; + default: + break; + } + QRectF rect; + rect.setLeft(dx + line.cursorToX(position, QTextLine::Leading)); + rect.setRight(dx + line.cursorToX(position, QTextLine::Trailing)); + rect.setTop(dy + line.y()); + rect.setBottom(dy + line.y() + line.height()); + + return rect; } + return QRectF(); + } + QTextLayout layout; +}; - QCOMPARE(test.link, QLatin1String("http://qt.nokia.com")); - delete textObject; +typedef QVector PointVector; +Q_DECLARE_METATYPE(PointVector); + +void tst_qquicktext::clickLink_data() +{ + QTest::addColumn("text"); + QTest::addColumn("width"); + QTest::addColumn("bindings"); + QTest::addColumn("mousePositions"); + QTest::addColumn("link"); + + const QString singleLineText = "this text has a link in it"; + const QString singleLineLink = "http://qt-project.org/single"; + const QString multipleLineText = "this text
has multiple
lines
in it"; + const QString multipleLineLink = "http://qt-project.org/multiple"; + const QString nestedText = "this text has a nested link in it"; + const QString outerLink = "http://qt-project.org/outer"; + const QString innerLink = "http://qt-project.org/inner"; + + { + const TextMetrics metrics("this text has a link in it"); + + QTest::newRow("click on link") + << singleLineText << 240. + << "" + << (PointVector() << metrics.characterRectangle(18).center()) + << singleLineLink; + QTest::newRow("click on text") + << singleLineText << 240. + << "" + << (PointVector() << metrics.characterRectangle(13).center()) + << QString(); + QTest::newRow("drag within link") + << singleLineText << 240. + << "" + << (PointVector() + << metrics.characterRectangle(17).center() + << metrics.characterRectangle(19).center()) + << singleLineLink; + QTest::newRow("drag away from link") + << singleLineText << 240. + << "" + << (PointVector() + << metrics.characterRectangle(18).center() + << metrics.characterRectangle(13).center()) + << QString(); + QTest::newRow("drag on to link") + << singleLineText << 240. + << "" + << (PointVector() + << metrics.characterRectangle(13).center() + << metrics.characterRectangle(18).center()) + << QString(); + QTest::newRow("click on bottom right aligned link") + << singleLineText << 240. + << "horizontalAlignment: Text.AlignRight; verticalAlignment: Text.AlignBottom" + << (PointVector() << metrics.characterRectangle(18, Qt::AlignRight, Qt::AlignBottom).center()) + << singleLineLink; + QTest::newRow("click on center aligned link") + << singleLineText << 240. + << "horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter" + << (PointVector() << metrics.characterRectangle(18, Qt::AlignHCenter, Qt::AlignVCenter).center()) + << singleLineLink; + QTest::newRow("click on rich text link") + << singleLineText << 240. + << "textFormat: Text.RichText" + << (PointVector() << metrics.characterRectangle(18).center()) + << singleLineLink; + QTest::newRow("click on rich text") + << singleLineText << 240. + << "textFormat: Text.RichText" + << (PointVector() << metrics.characterRectangle(13).center()) + << QString(); + QTest::newRow("click on bottom right aligned rich text link") + << singleLineText << 240. + << "textFormat: Text.RichText; horizontalAlignment: Text.AlignRight; verticalAlignment: Text.AlignBottom" + << (PointVector() << metrics.characterRectangle(18, Qt::AlignRight, Qt::AlignBottom).center()) + << singleLineLink; + QTest::newRow("click on center aligned rich text link") + << singleLineText << 240. + << "textFormat: Text.RichText; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter" + << (PointVector() << metrics.characterRectangle(18, Qt::AlignHCenter, Qt::AlignVCenter).center()) + << singleLineLink; + } { + const TextMetrics metrics("this text has a li", Qt::ElideRight); + QTest::newRow("click on right elided link") + << singleLineText << metrics.width() + 2 + << "elide: Text.ElideRight" + << (PointVector() << metrics.characterRectangle(17).center()) + << singleLineLink; + } { + const TextMetrics metrics("ink in it", Qt::ElideLeft); + QTest::newRow("click on left elided link") + << singleLineText << metrics.width() + 2 + << "elide: Text.ElideLeft" + << (PointVector() << metrics.characterRectangle(2).center()) + << singleLineLink; + } { + const TextMetrics metrics("this text\nhas multiple\nlines in it"); + QTest::newRow("click on second line") + << multipleLineText << 240. + << "" + << (PointVector() << metrics.characterRectangle(18).center()) + << multipleLineLink; + QTest::newRow("click on third line") + << multipleLineText << 240. + << "" + << (PointVector() << metrics.characterRectangle(25).center()) + << multipleLineLink; + QTest::newRow("drag from second line to third") + << multipleLineText << 240. + << "" + << (PointVector() + << metrics.characterRectangle(18).center() + << metrics.characterRectangle(25).center()) + << multipleLineLink; + QTest::newRow("click on rich text second line") + << multipleLineText << 240. + << "textFormat: Text.RichText" + << (PointVector() << metrics.characterRectangle(18).center()) + << multipleLineLink; + QTest::newRow("click on rich text third line") + << multipleLineText << 240. + << "textFormat: Text.RichText" + << (PointVector() << metrics.characterRectangle(25).center()) + << multipleLineLink; + QTest::newRow("drag rich text from second line to third") + << multipleLineText << 240. + << "textFormat: Text.RichText" + << (PointVector() + << metrics.characterRectangle(18).center() + << metrics.characterRectangle(25).center()) + << multipleLineLink; + } { + const TextMetrics metrics("this text has a nested link in it"); + QTest::newRow("click on left outer link") + << nestedText << 240. + << "" + << (PointVector() << metrics.characterRectangle(22).center()) + << outerLink; + QTest::newRow("click on right outer link") + << nestedText << 240. + << "" + << (PointVector() << metrics.characterRectangle(27).center()) + << outerLink; + QTest::newRow("click on inner link left") + << nestedText << 240. + << "" + << (PointVector() << metrics.characterRectangle(23).center()) + << innerLink; + QTest::newRow("click on inner link right") + << nestedText << 240. + << "" + << (PointVector() << metrics.characterRectangle(26).center()) + << innerLink; + QTest::newRow("drag from inner to outer link") + << nestedText << 240. + << "" + << (PointVector() + << metrics.characterRectangle(25).center() + << metrics.characterRectangle(30).center()) + << QString(); + QTest::newRow("drag from outer to inner link") + << nestedText << 240. + << "" + << (PointVector() + << metrics.characterRectangle(30).center() + << metrics.characterRectangle(25).center()) + << QString(); + QTest::newRow("click on left outer rich text link") + << nestedText << 240. + << "textFormat: Text.RichText" + << (PointVector() << metrics.characterRectangle(22).center()) + << outerLink; + QTest::newRow("click on right outer rich text link") + << nestedText << 240. + << "textFormat: Text.RichText" + << (PointVector() << metrics.characterRectangle(27).center()) + << outerLink; + QTest::newRow("click on inner rich text link left") + << nestedText << 240. + << "textFormat: Text.RichText" + << (PointVector() << metrics.characterRectangle(23).center()) + << innerLink; + QTest::newRow("click on inner rich text link right") + << nestedText << 240. + << "textFormat: Text.RichText" + << (PointVector() << metrics.characterRectangle(26).center()) + << innerLink; + QTest::newRow("drag from inner to outer rich text link") + << nestedText << 240. + << "textFormat: Text.RichText" + << (PointVector() + << metrics.characterRectangle(25).center() + << metrics.characterRectangle(30).center()) + << QString(); + QTest::newRow("drag from outer to inner rich text link") + << nestedText << 240. + << "textFormat: Text.RichText" + << (PointVector() + << metrics.characterRectangle(30).center() + << metrics.characterRectangle(25).center()) + << QString(); + } +} + +void tst_qquicktext::clickLink() +{ + QFETCH(QString, text); + QFETCH(qreal, width); + QFETCH(QString, bindings); + QFETCH(PointVector, mousePositions); + QFETCH(QString, link); + + QString componentStr = + "import QtQuick 2.0\nText {\n" + "width: " + QString::number(width) + "\n" + "height: 320\n" + "text: \"" + text + "\"\n" + "" + bindings + "\n" + "}"; + QQmlComponent textComponent(&engine); + textComponent.setData(componentStr.toLatin1(), QUrl::fromLocalFile("")); + QQuickText *textObject = qobject_cast(textComponent.create()); + + QVERIFY(textObject != 0); + + LinkTest test; + QObject::connect(textObject, SIGNAL(linkActivated(QString)), &test, SLOT(linkClicked(QString))); + + QVERIFY(mousePositions.count() > 0); + + QPointF mousePosition = mousePositions.first(); + { + QMouseEvent me(QEvent::MouseButtonPress, mousePosition, Qt::LeftButton, Qt::NoButton, Qt::NoModifier); + static_cast(static_cast(textObject))->sendEvent(&me); } + + for (int i = 1; i < mousePositions.count(); ++i) { + mousePosition = mousePositions.at(i); + + QMouseEvent me(QEvent::MouseMove, mousePosition, Qt::LeftButton, Qt::NoButton, Qt::NoModifier); + static_cast(static_cast(textObject))->sendEvent(&me); + } + + { + QMouseEvent me(QEvent::MouseButtonRelease, mousePosition, Qt::LeftButton, Qt::NoButton, Qt::NoModifier); + static_cast(static_cast(textObject))->sendEvent(&me); + } + + QCOMPARE(test.link, link); + + delete textObject; } void tst_qquicktext::baseUrl()