diff --git a/src/quick/items/qquicktext.cpp b/src/quick/items/qquicktext.cpp index b19b13fec213ec84d0f7f338c07c1023168ec7da..fa5652141c108173a8703546549fb235d5c1d83e 100644 --- a/src/quick/items/qquicktext.cpp +++ b/src/quick/items/qquicktext.cpp @@ -78,6 +78,7 @@ QQuickTextPrivate::QQuickTextPrivate() #if defined(Q_OS_MAC) , layoutThread(0), paintingThread(0) #endif + , lineWidth(0) , color(0xFF000000), linkColor(0xFF0000FF), styleColor(0xFF000000) , lineCount(1), multilengthEos(-1) , elideMode(QQuickText::ElideNone), hAlign(QQuickText::AlignLeft), vAlign(QQuickText::AlignTop) @@ -85,7 +86,8 @@ QQuickTextPrivate::QQuickTextPrivate() , style(QQuickText::Normal) , updateType(UpdatePaintNode) , maximumLineCountValid(false), updateOnComponentComplete(true), richText(false) - , styledText(false), singleline(false), internalWidthUpdate(false), requireImplicitWidth(false) + , styledText(false), widthExceeded(false), heightExceeded(false), internalWidthUpdate(false) + , requireImplicitSize(false), implicitWidthValid(false), implicitHeightValid(false) , truncated(false), hAlignImplicit(true), rightToLeftText(false) , layoutTextElided(false), textHasChanged(true), needToUpdateLayout(false), formatModifiesFontSize(false) { @@ -278,16 +280,26 @@ QQuickTextPrivate::~QQuickTextPrivate() qreal QQuickTextPrivate::getImplicitWidth() const { - if (!requireImplicitWidth) { + if (!requireImplicitSize) { // We don't calculate implicitWidth unless it is required. // We need to force a size update now to ensure implicitWidth is calculated QQuickTextPrivate *me = const_cast<QQuickTextPrivate*>(this); - me->requireImplicitWidth = true; + me->requireImplicitSize = true; me->updateSize(); } return implicitWidth; } +qreal QQuickTextPrivate::getImplicitHeight() const +{ + if (!requireImplicitSize) { + QQuickTextPrivate *me = const_cast<QQuickTextPrivate*>(this); + me->requireImplicitSize = true; + me->updateSize(); + } + return implicitHeight; +} + void QQuickText::q_imagesLoaded() { Q_D(QQuickText); @@ -409,10 +421,11 @@ void QQuickTextPrivate::updateSize() return; } - if (!requireImplicitWidth) { + if (!requireImplicitSize) { emit q->implicitWidthChanged(); + emit q->implicitHeightChanged(); // if the implicitWidth is used, then updateSize() has already been called (recursively) - if (requireImplicitWidth) + if (requireImplicitSize) return; } @@ -437,8 +450,6 @@ void QQuickTextPrivate::updateSize() return; } - qreal naturalWidth = 0; - QSizeF size(0, 0); QSizeF previousSize = layedOutTextRect.size(); #if defined(Q_OS_MAC) @@ -448,7 +459,7 @@ void QQuickTextPrivate::updateSize() //setup instance of QTextLayout for all cases other than richtext if (!richText) { qreal baseline = 0; - QRectF textRect = setupTextLayout(&naturalWidth, &baseline); + QRectF textRect = setupTextLayout(&baseline); if (internalWidthUpdate) // probably the result of a binding loop, but by letting it return; // get this far we'll get a warning to that effect if it is. @@ -457,7 +468,8 @@ void QQuickTextPrivate::updateSize() size = textRect.size(); updateBaseline(baseline, q->height() - size.height()); } else { - singleline = false; // richtext can't elide or be optimized for single-line case + widthExceeded = true; // always relayout rich text on width changes.. + heightExceeded = false; // rich text layout isn't affected by height changes. ensureDoc(); extra->doc->setDefaultFont(font); QQuickText::HAlignment horizontalAlignment = q->effectiveHAlign(); @@ -472,7 +484,8 @@ void QQuickTextPrivate::updateSize() option.setWrapMode(QTextOption::WrapMode(wrapMode)); option.setUseDesignMetrics(true); extra->doc->setDefaultTextOption(option); - if (requireImplicitWidth && q->widthValid()) { + qreal naturalWidth = 0; + if (requireImplicitSize && q->widthValid()) { extra->doc->setTextWidth(-1); naturalWidth = extra->doc->idealWidth(); const bool wasInLayout = internalWidthUpdate; @@ -486,25 +499,27 @@ void QQuickTextPrivate::updateSize() extra->doc->setTextWidth(q->width()); else extra->doc->setTextWidth(extra->doc->idealWidth()); // ### Text does not align if width is not set (QTextDoc bug) + widthExceeded = extra->doc->textWidth() < extra->doc->idealWidth(); QSizeF dsize = extra->doc->size(); layedOutTextRect = QRectF(QPointF(0,0), dsize); size = QSizeF(extra->doc->idealWidth(),dsize.height()); QFontMetricsF fm(font); updateBaseline(fm.ascent(), q->height() - size.height()); - } - //### need to comfirm cost of always setting these for richText - internalWidthUpdate = true; - qreal iWidth = -1; - if (!q->widthValid()) - iWidth = size.width(); - if (iWidth > -1) - q->setImplicitSize(iWidth, size.height()); - internalWidthUpdate = false; + //### need to confirm cost of always setting these for richText + internalWidthUpdate = true; + qreal iWidth = -1; + if (!q->widthValid()) + iWidth = size.width(); + if (iWidth > -1) + q->setImplicitSize(iWidth, size.height()); + internalWidthUpdate = false; + + if (iWidth == -1) + q->setImplicitHeight(size.height()); + } - if (iWidth == -1) - q->setImplicitHeight(size.height()); if (layedOutTextRect.size() != previousSize) emit q->contentSizeChanged(); updateType = UpdatePaintNode; @@ -661,7 +676,6 @@ void QQuickTextPrivate::elideFormats( QString QQuickTextPrivate::elidedText(qreal lineWidth, const QTextLine &line, QTextLine *nextLine) const { if (nextLine) { - nextLine->setLineWidth(INT_MAX); return layout.engine()->elidedText( Qt::TextElideMode(elideMode), QFixed::fromReal(lineWidth), @@ -690,22 +704,21 @@ QString QQuickTextPrivate::elidedText(qreal lineWidth, const QTextLine &line, QT already absolutely positioned horizontally). */ -QRectF QQuickTextPrivate::setupTextLayout(qreal *const naturalWidth, qreal *const baseline) +QRectF QQuickTextPrivate::setupTextLayout(qreal *const baseline) { Q_Q(QQuickText); - layout.setCacheEnabled(true); - QTextOption textOption = layout.textOption(); - textOption.setAlignment(Qt::Alignment(q->effectiveHAlign())); - textOption.setWrapMode(QTextOption::WrapMode(wrapMode)); - textOption.setUseDesignMetrics(true); - layout.setTextOption(textOption); - layout.setFont(font); - - if (!requireImplicitWidth - && ((q->widthValid() && q->width() <= 0. && elideMode != QQuickText::ElideNone) - || (q->heightValid() && q->height() <= 0. && wrapMode != QQuickText::NoWrap && elideMode == QQuickText::ElideRight))) { + bool singlelineElide = elideMode != QQuickText::ElideNone && q->widthValid(); + bool multilineElide = elideMode == QQuickText::ElideRight + && q->widthValid() + && (q->heightValid() || maximumLineCountValid); + + if ((!requireImplicitSize || (implicitWidthValid && implicitHeightValid)) + && ((singlelineElide && q->width() <= 0.) || (multilineElide && q->heightValid() && q->height() <= 0.))) { // we are elided and we have a zero width or height + widthExceeded = q->widthValid() && q->width() <= 0.; + heightExceeded = q->heightValid() && q->height() <= 0.; + if (!truncated) { truncated = true; emit q->truncatedChanged(); @@ -716,21 +729,32 @@ QRectF QQuickTextPrivate::setupTextLayout(qreal *const naturalWidth, qreal *cons } QFontMetricsF fm(font); - qreal height = (lineHeightMode() == QQuickText::FixedHeight) ? lineHeight() : fm.height() * lineHeight(); + qreal height = (lineHeightMode() == QQuickText::FixedHeight) ? lineHeight() : qCeil(fm.height()) * lineHeight(); *baseline = fm.ascent(); return QRectF(0, 0, 0, height); } - qreal lineWidth = q->widthValid() && q->width() > 0 ? q->width() : FLT_MAX; - const qreal maxHeight = q->heightValid() ? q->height() : FLT_MAX; + layout.setCacheEnabled(true); + QTextOption textOption = layout.textOption(); + if (textOption.alignment() != q->effectiveHAlign() + || textOption.wrapMode() != QTextOption::WrapMode(wrapMode) + || !textOption.useDesignMetrics()) { + textOption.setAlignment(Qt::Alignment(q->effectiveHAlign())); + textOption.setWrapMode(QTextOption::WrapMode(wrapMode)); + textOption.setUseDesignMetrics(true); + layout.setTextOption(textOption); + } + if (layout.font() != font) + layout.setFont(font); + + lineWidth = (q->widthValid() || implicitWidthValid) && q->width() > 0 + ? q->width() + : FLT_MAX; + qreal maxHeight = q->heightValid() ? q->height() : FLT_MAX; const bool customLayout = isLineLaidOutConnected(); const bool wasTruncated = truncated; - bool singlelineElide = elideMode != QQuickText::ElideNone && q->widthValid(); - bool multilineElide = elideMode == QQuickText::ElideRight - && q->widthValid() - && (q->heightValid() || maximumLineCountValid); bool canWrap = wrapMode != QQuickText::NoWrap && q->widthValid(); bool horizontalFit = fontSizeMode() & QQuickText::HorizontalFit && q->widthValid(); @@ -746,11 +770,13 @@ QRectF QQuickTextPrivate::setupTextLayout(qreal *const naturalWidth, qreal *cons : largeFont; int scaledFontSize = largeFont; + widthExceeded = q->width() <= 0 && (singlelineElide || canWrap || horizontalFit); + heightExceeded = q->height() <= 0 && (multilineElide || verticalFit); + QRectF br; QFont scaledFont = font; - QTextLine line; int visibleCount = 0; bool elide; qreal height = 0; @@ -759,19 +785,19 @@ QRectF QQuickTextPrivate::setupTextLayout(qreal *const naturalWidth, qreal *cons int elideStart = 0; int elideEnd = 0; - *naturalWidth = 0; - int eos = multilengthEos; // Repeated layouts with reduced font sizes or abbreviated strings may be required if the text - // doesn't fit within the element dimensions. + // doesn't fit within the item dimensions, or a binding to implicitWidth/Height changes + // the item dimensions. for (;;) { if (!once) { if (pixelSize) scaledFont.setPixelSize(scaledFontSize); else scaledFont.setPointSize(scaledFontSize); - layout.setFont(scaledFont); + if (layout.font() != scaledFont) + layout.setFont(scaledFont); } layout.beginLayout(); @@ -784,27 +810,31 @@ QRectF QQuickTextPrivate::setupTextLayout(qreal *const naturalWidth, qreal *cons int unwrappedLineCount = 1; int maxLineCount = maximumLineCount(); height = 0; + qreal naturalHeight = 0; + qreal previousHeight = 0; br = QRectF(); - line = layout.createLine(); - for (visibleCount = 1; ; ++visibleCount) { - qreal preLayoutHeight = height; + QRectF unelidedRect; + QTextLine line = layout.createLine(); + for (visibleCount = 1; ; ++visibleCount) { if (customLayout) { - setupCustomLineGeometry(line, height); + setupCustomLineGeometry(line, naturalHeight); } else { - setLineGeometry(line, lineWidth, height); + setLineGeometry(line, lineWidth, naturalHeight); } + unelidedRect = br.united(line.naturalTextRect()); + // Elide the previous line if the accumulated height of the text exceeds the height // of the element. - if (multilineElide && height > maxHeight && visibleCount > 1) { + if (multilineElide && naturalHeight > maxHeight && visibleCount > 1) { elide = true; + heightExceeded = true; if (eos != -1) // There's an abbreviated string available, skip the rest as it's break; // all going to be discarded. truncated = true; truncateHeight = true; - height = preLayoutHeight; characterCount = line.textStart() + line.textLength(); visibleCount -= 1; @@ -816,36 +846,37 @@ QRectF QQuickTextPrivate::setupTextLayout(qreal *const naturalWidth, qreal *cons elideStart = previousLine.textStart(); // elideEnd isn't required for right eliding. - line = previousLine; - height -= (lineHeightMode() == QQuickText::FixedHeight) ? lineHeight() : previousLine.height() * lineHeight(); + height = previousHeight; break; } - QTextLine nextLine = layout.createLine(); - if (!nextLine.isValid()) { - characterCount = line.textStart() + line.textLength(); - if (singlelineElide && visibleCount == 1 && line.naturalTextWidth() > lineWidth) { - // Elide a single line of text if its width exceeds the element width. + const QTextLine previousLine = line; + line = layout.createLine(); + if (!line.isValid()) { + characterCount = previousLine.textStart() + previousLine.textLength(); + if (singlelineElide && visibleCount == 1 && previousLine.naturalTextWidth() > lineWidth) { + // Elide a single previousLine of text if its width exceeds the element width. elide = true; + widthExceeded = true; if (eos != -1) // There's an abbreviated string available. break; truncated = true; - height = preLayoutHeight; elideText = layout.engine()->elidedText( Qt::TextElideMode(elideMode), QFixed::fromReal(lineWidth), 0, - line.textStart(), - line.textLength()); - elideStart = line.textStart(); - elideEnd = elideStart + line.textLength(); + previousLine.textStart(), + previousLine.textLength()); + elideStart = previousLine.textStart(); + elideEnd = elideStart + previousLine.textLength(); } else { - br = br.united(line.naturalTextRect()); + br = unelidedRect; + height = naturalHeight; } break; } else { - const bool wrappedLine = layoutText.at(nextLine.textStart() - 1) != QChar::LineSeparator; + const bool wrappedLine = layoutText.at(line.textStart() - 1) != QChar::LineSeparator; wrapped |= wrappedLine; if (!wrappedLine) @@ -855,58 +886,66 @@ QRectF QQuickTextPrivate::setupTextLayout(qreal *const naturalWidth, qreal *cons // if enabled. if (visibleCount == maxLineCount) { truncated = true; - characterCount = nextLine.textStart() + nextLine.textLength(); + heightExceeded |= wrapped; + characterCount = line.textStart() + line.textLength(); if (multilineElide) { elide = true; if (eos != -1) // There's an abbreviated string available break; - height = preLayoutHeight; elideText = wrappedLine - ? elidedText(lineWidth, line, &nextLine) - : elidedText(lineWidth, line); - elideStart = line.textStart(); + ? elidedText(lineWidth, previousLine, &line) + : elidedText(lineWidth, previousLine); + elideStart = previousLine.textStart(); // elideEnd isn't required for right eliding. } else { - br = br.united(line.naturalTextRect()); + br = unelidedRect; + height = naturalHeight; } break; } } - br = br.united(line.naturalTextRect()); - line = nextLine; + br = unelidedRect; + previousHeight = height; + height = naturalHeight; } - layout.endLayout(); - br.moveTop(0); + widthExceeded |= wrapped; - // Save the implicitWidth of the text on the first layout only. + // Save the implicit size of the text on the first layout only. if (once) { - *naturalWidth = layout.maximumWidth(); once = false; - if (requireImplicitWidth - && characterCount < layoutText.length() - && unwrappedLineCount < maxLineCount) { - // Use a new layout to get the maximum width for the remaining text. Using a - // different layout excludes the truncated text from rendering. - QTextLayout widthLayout(layoutText.mid(characterCount), scaledFont); - widthLayout.setTextOption(layout.textOption()); - - widthLayout.beginLayout(); - for (; unwrappedLineCount <= maxLineCount; ++unwrappedLineCount) { - QTextLine line = widthLayout.createLine(); + // If implicit sizes are required layout any additional lines up to the maximum line + // count. + if ((requireImplicitSize) && line.isValid() && unwrappedLineCount < maxLineCount) { + // Layout the remainder of the wrapped lines up to maxLineCount to get the implicit + // height. + for (int lineCount = layout.lineCount(); lineCount < maxLineCount; ++lineCount) { + line = layout.createLine(); if (!line.isValid()) break; + if (layoutText.at(line.textStart() - 1) == QChar::LineSeparator) + ++unwrappedLineCount; + setLineGeometry(line, lineWidth, naturalHeight); } - widthLayout.endLayout(); - *naturalWidth = qMax(*naturalWidth, widthLayout.maximumWidth()); + + // Create the remainder of the unwrapped lines up to maxLineCount to get the + // implicit width. + if (line.isValid() && layoutText.at(line.textStart() + line.textLength()) != QChar::LineSeparator) + line = layout.createLine(); + for (; line.isValid() && unwrappedLineCount <= maxLineCount; ++unwrappedLineCount) + line = layout.createLine(); } + layout.endLayout(); + + const qreal naturalWidth = layout.maximumWidth(); bool wasInLayout = internalWidthUpdate; internalWidthUpdate = true; - q->setImplicitWidth(*naturalWidth); + q->setImplicitSize(naturalWidth, naturalHeight); internalWidthUpdate = wasInLayout; + // Update any variables that are dependent on the validity of the width or height. singlelineElide = elideMode != QQuickText::ElideNone && q->widthValid(); multilineElide = elideMode == QQuickText::ElideRight && q->widthValid() @@ -918,13 +957,38 @@ QRectF QQuickTextPrivate::setupTextLayout(qreal *const naturalWidth, qreal *cons && (q->heightValid() || (maximumLineCountValid && canWrap)); const qreal oldWidth = lineWidth; - lineWidth = q->widthValid() && q->width() > 0 ? q->width() : *naturalWidth; - if (lineWidth != oldWidth && (singlelineElide || multilineElide || canWrap || horizontalFit)) + const qreal oldHeight = maxHeight; + + lineWidth = q->widthValid() && q->width() > 0 ? q->width() : naturalWidth; + maxHeight = q->heightValid() ? q->height() : FLT_MAX; + + // If the width of the item has changed and it's possible the result of wrapping, + // eliding, or scaling has changed do another layout. + if ((lineWidth < qMin(oldWidth, naturalWidth) || (widthExceeded && lineWidth > oldWidth)) + && (singlelineElide || multilineElide || canWrap || horizontalFit)) { + widthExceeded = false; + heightExceeded = false; + continue; + } + + // If the height of the item has changed and it's possible the result of eliding, + // line count truncation or scaling has changed, do another layout. + if ((maxHeight < qMin(oldHeight, naturalHeight) || (heightExceeded && maxHeight > oldHeight)) + && (multilineElide || (canWrap && maximumLineCountValid))) { + widthExceeded = false; + heightExceeded = false; continue; + } + // If the horizontal alignment is not left and the width was not valid we need to relayout // now that we know the maximum line width. - if (!q->widthValid() && maxLineCount > 1 && q->effectiveHAlign() != QQuickText::AlignLeft) + if (!implicitWidthValid && lineCount > 1 && q->effectiveHAlign() != QQuickText::AlignLeft) { + widthExceeded = false; + heightExceeded = false; continue; + } + } else { + layout.endLayout(); } // If the next needs to be elided and there's an abbreviated string available @@ -939,14 +1003,15 @@ QRectF QQuickTextPrivate::setupTextLayout(qreal *const naturalWidth, qreal *cons continue; } + br.moveTop(0); + if (!horizontalFit && !verticalFit) break; // Try and find a font size that better fits the dimensions of the element. - QRectF unelidedRect = br.united(line.naturalTextRect()); - if (horizontalFit) { if (unelidedRect.width() > lineWidth || (!verticalFit && wrapped)) { + widthExceeded = true; largeFont = scaledFontSize - 1; if (smallFont > largeFont) break; @@ -966,6 +1031,7 @@ QRectF QQuickTextPrivate::setupTextLayout(qreal *const naturalWidth, qreal *cons if (verticalFit) { if (truncateHeight || unelidedRect.height() > maxHeight) { + heightExceeded = true; largeFont = scaledFontSize - 1; if (smallFont > largeFont) break; @@ -980,6 +1046,9 @@ QRectF QQuickTextPrivate::setupTextLayout(qreal *const naturalWidth, qreal *cons } } + implicitWidthValid = true; + implicitHeightValid = true; + if (eos != multilengthEos) truncated = true; @@ -1023,7 +1092,7 @@ QRectF QQuickTextPrivate::setupTextLayout(qreal *const naturalWidth, qreal *cons QTextLine elidedLine = elideLayout->createLine(); elidedLine.setPosition(QPointF(0, height)); if (customLayout) { - setupCustomLineGeometry(elidedLine, height, line.lineNumber()); + setupCustomLineGeometry(elidedLine, height, visibleCount - 1); } else { setLineGeometry(elidedLine, lineWidth, height); } @@ -1389,6 +1458,8 @@ void QQuickText::setFont(const QFont &font) // with headings or <font> tag, we need to re-parse if (d->formatModifiesFontSize) d->textHasChanged = true; + d->implicitWidthValid = false; + d->implicitHeightValid = false; d->updateLayout(); } @@ -1429,6 +1500,8 @@ void QQuickText::setText(const QString &n) d->determineHorizontalAlignment(); } d->textHasChanged = true; + d->implicitWidthValid = false; + d->implicitHeightValid = false; qDeleteAll(d->imgTags); d->imgTags.clear(); d->updateLayout(); @@ -1788,6 +1861,7 @@ void QQuickText::setMaximumLineCount(int lines) d->maximumLineCountValid = lines==INT_MAX ? false : true; if (d->maximumLineCount() != lines) { d->extra.value().maximumLineCount = lines; + d->implicitHeightValid = false; d->updateLayout(); emit maximumLineCountChanged(); } @@ -2013,8 +2087,8 @@ 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)); + rect.moveLeft(QQuickTextUtil::alignedX(rect.width(), width(), d->hAlign)); + rect.moveTop(QQuickTextUtil::alignedY(rect.height(), height(), d->vAlign)); if (d->style != Normal) rect.adjust(-1, 0, 1, 2); @@ -2044,32 +2118,54 @@ void QQuickText::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeo bool widthChanged = newGeometry.width() != oldGeometry.width(); bool heightChanged = newGeometry.height() != oldGeometry.height(); - bool leftAligned = effectiveHAlign() == QQuickText::AlignLeft; bool wrapped = d->wrapMode != QQuickText::NoWrap; bool elide = d->elideMode != QQuickText::ElideNone; bool scaleFont = d->fontSizeMode() != QQuickText::FixedSize && (widthValid() || heightValid()); + bool verticalScale = (d->fontSizeMode() & QQuickText::VerticalFit) && heightValid(); + + bool widthMaximum = newGeometry.width() >= oldGeometry.width() && !d->widthExceeded; + bool heightMaximum = newGeometry.height() >= oldGeometry.height() && !d->heightExceeded; if ((!widthChanged && !heightChanged) || d->internalWidthUpdate) goto geomChangeDone; - if (leftAligned && !wrapped && !elide && !scaleFont) - goto geomChangeDone; // left aligned unwrapped text without eliding never needs relayout + if (effectiveHAlign() != QQuickText::AlignLeft && widthChanged) { + // If the width has changed and we're not left aligned do an update so the text is + // repositioned even if a full layout isn't required. + d->updateType = QQuickTextPrivate::UpdatePaintNode; + update(); + } - if (!widthChanged && !wrapped && d->singleline && !scaleFont) - goto geomChangeDone; // only height has changed which doesn't affect single line unwrapped text + if (!wrapped && !elide && !scaleFont) + goto geomChangeDone; // left aligned unwrapped text without eliding never needs relayout - if (!widthChanged && wrapped && d->elideMode != QQuickText::ElideRight && !scaleFont && !d->isLineLaidOutConnected()) - goto geomChangeDone; // only height changed and no multiline eliding. + if (elide // eliding and dimensions were and remain invalid; + && ((widthValid() && oldGeometry.width() <= 0 && newGeometry.width() <= 0) + || (heightValid() && oldGeometry.height() <= 0 && newGeometry.height() <= 0))) { + goto geomChangeDone; + } - if (leftAligned && d->elideMode == QQuickText::ElideRight && !d->truncated && d->singleline - && !wrapped && newGeometry.width() > oldGeometry.width() && !scaleFont) - goto geomChangeDone; // Eliding not affected if we're not currently truncated and we get wider. + if (widthMaximum && heightMaximum && !d->isLineLaidOutConnected()) // Size is sufficient and growing. + goto geomChangeDone; - if (d->elideMode == QQuickText::ElideRight && wrapped && newGeometry.height() > oldGeometry.height() && !scaleFont) { - if (!d->truncated) - goto geomChangeDone; // Multiline eliding not affected if we're not currently truncated and we get higher. - if (d->maximumLineCountValid && d->lineCount == d->maximumLineCount()) - goto geomChangeDone; // Multiline eliding not affected if we're already at max line count and we get higher. + if (!(widthChanged || widthMaximum) && !d->isLineLaidOutConnected()) { // only height has changed + if (newGeometry.height() > oldGeometry.height()) { + if (!d->heightExceeded) // Height is adequate and growing. + goto geomChangeDone; + if (d->lineCount == d->maximumLineCount()) // Reached maximum line and height is growing. + goto geomChangeDone; + } else if (newGeometry.height() < oldGeometry.height()) { + if (d->lineCount < 2 && !verticalScale && newGeometry.height() > 0) // A single line won't be truncated until the text is 0 height. + goto geomChangeDone; + + if (!verticalScale // no scaling, no eliding, and either unwrapped, or no maximum line count. + && d->elideMode != QQuickText::ElideRight + && !(d->maximumLineCountValid && d->widthExceeded)) { + goto geomChangeDone; + } + } + } else if (!heightChanged && widthMaximum) { + goto geomChangeDone; } if (d->updateOnComponentComplete || d->textHasChanged) { @@ -2110,7 +2206,7 @@ QSGNode *QQuickText::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *data d->updateType = QQuickTextPrivate::UpdateNone; - const qreal dy = QQuickTextUtil::alignedY(d->layedOutTextRect, height(), d->vAlign); + const qreal dy = QQuickTextUtil::alignedY(d->layedOutTextRect.height(), height(), d->vAlign); // We need to make sure the layout is done in the current thread #if defined(Q_OS_MAC) @@ -2134,29 +2230,30 @@ QSGNode *QQuickText::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *data const QColor linkColor = QColor::fromRgba(d->linkColor); if (d->richText) { + const qreal dx = QQuickTextUtil::alignedX(d->layedOutTextRect.width(), width(), d->hAlign); d->ensureDoc(); - 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.) { + } else if (d->layedOutTextRect.width() > 0) { + const qreal dx = QQuickTextUtil::alignedX(d->lineWidth, width(), d->hAlign); int unelidedLineCount = d->lineCount; if (d->elideLayout) unelidedLineCount -= 1; if (unelidedLineCount > 0) { node->addTextLayout( - QPoint(0, dy), + QPointF(dx, dy), &d->layout, color, d->style, styleColor, linkColor, QColor(), QColor(), -1, -1, 0, unelidedLineCount); } if (d->elideLayout) - node->addTextLayout(QPoint(0, dy), d->elideLayout, color, d->style, styleColor, linkColor); - } + node->addTextLayout(QPointF(dx, 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() + dy, pix->width(), pix->height()), pix->image()); + foreach (QQuickStyledTextImgTag *img, d->visibleImgTags) { + QQuickPixmap *pix = img->pix; + if (pix && pix->isReady()) + node->addImage(QRectF(img->pos.x() + dx, img->pos.y() + dy, pix->width(), pix->height()), pix->image()); + } } return node; } @@ -2214,6 +2311,7 @@ void QQuickText::setLineHeight(qreal lineHeight) return; d->extra.value().lineHeight = lineHeight; + d->implicitHeightValid = false; d->updateLayout(); emit lineHeightChanged(lineHeight); } @@ -2242,6 +2340,7 @@ void QQuickText::setLineHeightMode(LineHeightMode mode) if (mode == d->lineHeightMode()) return; + d->implicitHeightValid = false; d->extra.value().lineHeightMode = mode; d->updateLayout(); @@ -2400,14 +2499,14 @@ QString QQuickTextPrivate::anchorAt(const QPointF &mousePos) const { Q_Q(const QQuickText); QPointF translatedMousePos = mousePos; - translatedMousePos.ry() -= QQuickTextUtil::alignedY(layedOutTextRect, q->height(), vAlign); + translatedMousePos.ry() -= QQuickTextUtil::alignedY(layedOutTextRect.height(), 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); + translatedMousePos.rx() -= QQuickTextUtil::alignedX(layedOutTextRect.width(), q->width(), hAlign); return extra->doc->documentLayout()->anchorAt(translatedMousePos); } return QString(); diff --git a/src/quick/items/qquicktext_p_p.h b/src/quick/items/qquicktext_p_p.h index 985b1e1c80fb9831d83300969409f079763ab90e..49d3580578e5187337902986a91b2b44a6cc3a1d 100644 --- a/src/quick/items/qquicktext_p_p.h +++ b/src/quick/items/qquicktext_p_p.h @@ -121,6 +121,8 @@ public: QThread *paintingThread; #endif + qreal lineWidth; + QRgb color; QRgb linkColor; QRgb styleColor; @@ -146,9 +148,12 @@ public: bool updateOnComponentComplete:1; bool richText:1; bool styledText:1; - bool singleline:1; + bool widthExceeded:1; + bool heightExceeded:1; bool internalWidthUpdate:1; - bool requireImplicitWidth:1; + bool requireImplicitSize:1; + bool implicitWidthValid:1; + bool implicitHeightValid:1; bool truncated:1; bool hAlignImplicit:1; bool rightToLeftText:1; @@ -160,10 +165,11 @@ public: static const QChar elideChar; virtual qreal getImplicitWidth() const; + virtual qreal getImplicitHeight() const; void ensureDoc(); - QRectF setupTextLayout(qreal *const naturalWidth, qreal * const baseline); + QRectF setupTextLayout(qreal * const baseline); void setupCustomLineGeometry(QTextLine &line, qreal &height, int lineOffset = 0); bool isLinkActivatedConnected(); static QString anchorAt(const QTextLayout *layout, const QPointF &mousePos); diff --git a/src/quick/items/qquicktextutil.cpp b/src/quick/items/qquicktextutil.cpp index 176301d450b4d1de521fe5bd4f820146147fc466..151d72c6ee233e95382dd4cf283321651f168cdf 100644 --- a/src/quick/items/qquicktextutil.cpp +++ b/src/quick/items/qquicktextutil.cpp @@ -78,7 +78,7 @@ QQuickItem *QQuickTextUtil::createCursor( return item; } -qreal QQuickTextUtil::alignedX(const QRectF &rect, qreal width, int alignment) +qreal QQuickTextUtil::alignedX(const qreal textWidth, const qreal itemWidth, int alignment) { qreal x = 0; switch (alignment) { @@ -86,26 +86,26 @@ qreal QQuickTextUtil::alignedX(const QRectF &rect, qreal width, int alignment) case Qt::AlignJustify: break; case Qt::AlignRight: - x = width - rect.width(); + x = itemWidth - textWidth; break; case Qt::AlignHCenter: - x = (width - rect.width()) / 2; + x = (itemWidth - textWidth) / 2; break; } return x; } -qreal QQuickTextUtil::alignedY(const QRectF &rect, const qreal height, int alignment) +qreal QQuickTextUtil::alignedY(const qreal textHeight, const qreal itemHeight, int alignment) { qreal y = 0; switch (alignment) { case Qt::AlignTop: break; case Qt::AlignBottom: - y = height - rect.height(); + y = itemHeight - textHeight; break; case Qt::AlignVCenter: - y = (height - rect.height()) / 2; + y = (itemHeight - textHeight) / 2; break; } return y; diff --git a/src/quick/items/qquicktextutil_p.h b/src/quick/items/qquicktextutil_p.h index d6c05aac3b88cf1a9790b11bff94631723e041d5..21435c31f80b92bbb881a0aad7ce51d844812ade 100644 --- a/src/quick/items/qquicktextutil_p.h +++ b/src/quick/items/qquicktextutil_p.h @@ -66,8 +66,9 @@ public: template <typename Private> static void setCursorDelegate(Private *d, QQmlComponent *delegate); template <typename Private> 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); + + static qreal alignedX(qreal textWidth, qreal itemWidth, int alignment); + static qreal alignedY(qreal textHeight, qreal itemHeight, int alignment); private: static QQuickItem *createCursor( diff --git a/tests/auto/quick/qquicktext/tst_qquicktext.cpp b/tests/auto/quick/qquicktext/tst_qquicktext.cpp index 0adddb9c85f98f36811577e2e1301ae16b3209d6..7a7f5cc053c327489ff3360e9e9e957b433b6de5 100644 --- a/tests/auto/quick/qquicktext/tst_qquicktext.cpp +++ b/tests/auto/quick/qquicktext/tst_qquicktext.cpp @@ -62,6 +62,10 @@ DEFINE_BOOL_CONFIG_OPTION(qmlDisableDistanceField, QML_DISABLE_DISTANCEFIELD) Q_DECLARE_METATYPE(QQuickText::TextFormat) +QT_BEGIN_NAMESPACE +extern void qt_setQtEnableTestFont(bool value); +QT_END_NAMESPACE + class tst_qquicktext : public QQmlDataTest { Q_OBJECT @@ -116,6 +120,7 @@ private slots: void contentSize(); void implicitSizeBinding_data(); void implicitSizeBinding(); + void geometryChanged(); void boundingRect_data(); void boundingRect(); @@ -218,6 +223,7 @@ tst_qquicktext::tst_qquicktext() // << "#AA0011DD" // << "#00F16B11"; // + qt_setQtEnableTestFont(true); } QQuickView *tst_qquicktext::createView(const QString &filename) @@ -2031,6 +2037,260 @@ void tst_qquicktext::contentSize() QCOMPARE(spy.count(), ++spyCount); } +void tst_qquicktext::geometryChanged() +{ + // Test that text is re-laid out when the geometry of the item by verifying changes in content + // size. Implicit width is also tested as that in combination with item geometry provides a + // reference for expected content sizes. + + QString componentStr = "import QtQuick 2.0\nText { font.family: \"__Qt__Box__Engine__\"; font.pixelSize: 10 }"; + QQmlComponent textComponent(&engine); + textComponent.setData(componentStr.toLatin1(), QUrl::fromLocalFile("")); + QScopedPointer<QObject> object(textComponent.create()); + QQuickText *textObject = qobject_cast<QQuickText *>(object.data()); + + const qreal implicitHeight = textObject->implicitHeight(); + + const qreal widths[] = { 100, 2000, 3000, -100, 100 }; + const qreal heights[] = { implicitHeight, 2000, 3000, -implicitHeight, implicitHeight }; + + QCOMPARE(textObject->implicitWidth(), 0.); + QVERIFY(implicitHeight > 0.); + QCOMPARE(textObject->width(), textObject->implicitWidth()); + QCOMPARE(textObject->height(), implicitHeight); + QCOMPARE(textObject->contentWidth(), textObject->implicitWidth()); + QCOMPARE(textObject->contentHeight(), implicitHeight); + + textObject->setText("The quick red fox jumped over the lazy brown dog"); + + const qreal implicitWidth = textObject->implicitWidth(); + + QVERIFY(implicitWidth > 0.); + QCOMPARE(textObject->implicitHeight(), implicitHeight); + QCOMPARE(textObject->width(), textObject->implicitWidth()); + QCOMPARE(textObject->height(), textObject->implicitHeight()); + QCOMPARE(textObject->contentWidth(), textObject->implicitWidth()); + QCOMPARE(textObject->contentHeight(), textObject->implicitHeight()); + + // Changing the geometry with no eliding, or wrapping doesn't change the content size. + for (int i = 0; i < 5; ++i) { + textObject->setWidth(widths[i]); + QCOMPARE(textObject->implicitWidth(), implicitWidth); + QCOMPARE(textObject->implicitHeight(), implicitHeight); + QCOMPARE(textObject->width(), widths[i]); + QCOMPARE(textObject->height(), implicitHeight); + QCOMPARE(textObject->contentWidth(), implicitWidth); + QCOMPARE(textObject->contentHeight(), implicitHeight); + } + + // With eliding enabled the content width is bounded to the item width, but is never + // larger than the implicit width. + textObject->setElideMode(QQuickText::ElideRight); + QCOMPARE(textObject->implicitWidth(), implicitWidth); + QCOMPARE(textObject->implicitHeight(), implicitHeight); + QCOMPARE(textObject->width(), 100.); + QCOMPARE(textObject->height(), implicitHeight); + QVERIFY(textObject->contentWidth() <= 100.); + QCOMPARE(textObject->contentHeight(), implicitHeight); + + textObject->setWidth(2000.); + QCOMPARE(textObject->implicitWidth(), implicitWidth); + QCOMPARE(textObject->implicitHeight(), implicitHeight); + QCOMPARE(textObject->width(), 2000.); + QCOMPARE(textObject->height(), implicitHeight); + QCOMPARE(textObject->contentWidth(), implicitWidth); + QCOMPARE(textObject->contentHeight(), implicitHeight); + + textObject->setWidth(3000.); + QCOMPARE(textObject->implicitWidth(), implicitWidth); + QCOMPARE(textObject->implicitHeight(), implicitHeight); + QCOMPARE(textObject->width(), 3000.); + QCOMPARE(textObject->height(), implicitHeight); + QCOMPARE(textObject->contentWidth(), implicitWidth); + QCOMPARE(textObject->contentHeight(), implicitHeight); + + textObject->setWidth(-100); + QCOMPARE(textObject->implicitWidth(), implicitWidth); + QCOMPARE(textObject->implicitHeight(), implicitHeight); + QCOMPARE(textObject->width(), -100.); + QCOMPARE(textObject->height(), implicitHeight); + QCOMPARE(textObject->contentWidth(), 0.); + QCOMPARE(textObject->contentHeight(), implicitHeight); + + textObject->setWidth(100.); + QCOMPARE(textObject->implicitWidth(), implicitWidth); + QCOMPARE(textObject->implicitHeight(), implicitHeight); + QCOMPARE(textObject->width(), 100.); + QCOMPARE(textObject->height(), implicitHeight); + QVERIFY(textObject->contentWidth() <= 100.); + QCOMPARE(textObject->contentHeight(), implicitHeight); + + // With wrapping enabled the implicit height changes with the width. + textObject->setElideMode(QQuickText::ElideNone); + textObject->setWrapMode(QQuickText::Wrap); + const qreal wrappedImplicitHeight = textObject->implicitHeight(); + + QVERIFY(wrappedImplicitHeight > implicitHeight); + + QCOMPARE(textObject->implicitWidth(), implicitWidth); + QCOMPARE(textObject->width(), 100.); + QCOMPARE(textObject->height(), wrappedImplicitHeight); + QVERIFY(textObject->contentWidth() <= 100.); + QCOMPARE(textObject->contentHeight(), wrappedImplicitHeight); + + textObject->setWidth(2000.); + QCOMPARE(textObject->implicitWidth(), implicitWidth); + QCOMPARE(textObject->implicitHeight(), implicitHeight); + QCOMPARE(textObject->width(), 2000.); + QCOMPARE(textObject->height(), implicitHeight); + QCOMPARE(textObject->contentWidth(), implicitWidth); + QCOMPARE(textObject->contentHeight(), implicitHeight); + + textObject->setWidth(3000.); + QCOMPARE(textObject->implicitWidth(), implicitWidth); + QCOMPARE(textObject->implicitHeight(), implicitHeight); + QCOMPARE(textObject->width(), 3000.); + QCOMPARE(textObject->height(), implicitHeight); + QCOMPARE(textObject->contentWidth(), implicitWidth); + QCOMPARE(textObject->contentHeight(), implicitHeight); + + textObject->setWidth(-100); + QCOMPARE(textObject->implicitWidth(), implicitWidth); + QCOMPARE(textObject->implicitHeight(), implicitHeight); + QCOMPARE(textObject->width(), -100.); + QCOMPARE(textObject->height(), implicitHeight); + QCOMPARE(textObject->contentWidth(), implicitWidth); // 0 or negative width item won't wrap. + QCOMPARE(textObject->contentHeight(), implicitHeight); + + textObject->setWidth(100.); + QCOMPARE(textObject->implicitWidth(), implicitWidth); + QCOMPARE(textObject->implicitHeight(), wrappedImplicitHeight); + QCOMPARE(textObject->width(), 100.); + QCOMPARE(textObject->height(), wrappedImplicitHeight); + QVERIFY(textObject->contentWidth() <= 100.); + QCOMPARE(textObject->contentHeight(), wrappedImplicitHeight); + + // With no eliding or maximum line count the content height is the same as the implicit height. + for (int i = 0; i < 5; ++i) { + textObject->setHeight(heights[i]); + QCOMPARE(textObject->implicitWidth(), implicitWidth); + QCOMPARE(textObject->implicitHeight(), wrappedImplicitHeight); + QCOMPARE(textObject->width(), 100.); + QCOMPARE(textObject->height(), heights[i]); + QVERIFY(textObject->contentWidth() <= 100.); + QCOMPARE(textObject->contentHeight(), wrappedImplicitHeight); + } + + // The implicit height is unaffected by eliding but the content height will change. + textObject->setElideMode(QQuickText::ElideRight); + + QCOMPARE(textObject->implicitWidth(), implicitWidth); + QCOMPARE(textObject->implicitHeight(), wrappedImplicitHeight); + QCOMPARE(textObject->width(), 100.); + QCOMPARE(textObject->height(), implicitHeight); + QVERIFY(textObject->contentWidth() <= 100.); + QCOMPARE(textObject->contentHeight(), implicitHeight); + + textObject->setHeight(2000); + QCOMPARE(textObject->implicitWidth(), implicitWidth); + QCOMPARE(textObject->implicitHeight(), wrappedImplicitHeight); + QCOMPARE(textObject->width(), 100.); + QCOMPARE(textObject->height(), 2000.); + QVERIFY(textObject->contentWidth() <= 100.); + QCOMPARE(textObject->contentHeight(), wrappedImplicitHeight); + + textObject->setHeight(3000); + QCOMPARE(textObject->implicitWidth(), implicitWidth); + QCOMPARE(textObject->implicitHeight(), wrappedImplicitHeight); + QCOMPARE(textObject->width(), 100.); + QCOMPARE(textObject->height(), 3000.); + QVERIFY(textObject->contentWidth() <= 100.); + QCOMPARE(textObject->contentHeight(), wrappedImplicitHeight); + + textObject->setHeight(-implicitHeight); + QCOMPARE(textObject->implicitWidth(), implicitWidth); + QCOMPARE(textObject->implicitHeight(), wrappedImplicitHeight); + QCOMPARE(textObject->width(), 100.); + QCOMPARE(textObject->height(), -implicitHeight); + QVERIFY(textObject->contentWidth() <= 0.); + QCOMPARE(textObject->contentHeight(), implicitHeight); // content height is never less than font height. seems a little odd in this instance. + + textObject->setHeight(implicitHeight); + QCOMPARE(textObject->implicitWidth(), implicitWidth); + QCOMPARE(textObject->implicitHeight(), wrappedImplicitHeight); + QCOMPARE(textObject->width(), 100.); + QCOMPARE(textObject->height(), implicitHeight); + QVERIFY(textObject->contentWidth() <= 100.); + QCOMPARE(textObject->contentHeight(), implicitHeight); + + // Varying the height with a maximum line count but no eliding won't affect the content height. + textObject->setElideMode(QQuickText::ElideNone); + textObject->setMaximumLineCount(2); + textObject->resetHeight(); + + const qreal maxLineCountImplicitHeight = textObject->implicitHeight(); + QVERIFY(maxLineCountImplicitHeight > implicitHeight); + QVERIFY(maxLineCountImplicitHeight < wrappedImplicitHeight); + + QCOMPARE(textObject->implicitWidth(), implicitWidth); + QCOMPARE(textObject->width(), 100.); + QCOMPARE(textObject->height(), maxLineCountImplicitHeight); + QVERIFY(textObject->contentWidth() <= 100.); + QCOMPARE(textObject->contentHeight(), maxLineCountImplicitHeight); + + for (int i = 0; i < 5; ++i) { + textObject->setHeight(heights[i]); + QCOMPARE(textObject->implicitWidth(), implicitWidth); + QCOMPARE(textObject->implicitHeight(), maxLineCountImplicitHeight); + QCOMPARE(textObject->width(), 100.); + QCOMPARE(textObject->height(), heights[i]); + QVERIFY(textObject->contentWidth() <= 100.); + QCOMPARE(textObject->contentHeight(), maxLineCountImplicitHeight); + } + + // Varying the width with a maximum line count won't increase the implicit height beyond the + // height of the maximum number of lines. + textObject->setWidth(2000.); + QCOMPARE(textObject->implicitWidth(), implicitWidth); + QCOMPARE(textObject->implicitHeight(), implicitHeight); + QCOMPARE(textObject->width(), 2000.); + QCOMPARE(textObject->height(), implicitHeight); + QCOMPARE(textObject->contentWidth(), implicitWidth); + QCOMPARE(textObject->contentHeight(), implicitHeight); + + textObject->setWidth(3000.); + QCOMPARE(textObject->implicitWidth(), implicitWidth); + QCOMPARE(textObject->implicitHeight(), implicitHeight); + QCOMPARE(textObject->width(), 3000.); + QCOMPARE(textObject->height(), implicitHeight); + QCOMPARE(textObject->contentWidth(), implicitWidth); + QCOMPARE(textObject->contentHeight(), implicitHeight); + + textObject->setWidth(-100); + QCOMPARE(textObject->implicitWidth(), implicitWidth); + QCOMPARE(textObject->implicitHeight(), implicitHeight); + QCOMPARE(textObject->width(), -100.); + QCOMPARE(textObject->height(), implicitHeight); + QCOMPARE(textObject->contentWidth(), implicitWidth); // 0 or negative width item won't wrap. + QCOMPARE(textObject->contentHeight(), implicitHeight); + + textObject->setWidth(50.); + QCOMPARE(textObject->implicitWidth(), implicitWidth); + QCOMPARE(textObject->implicitHeight(), maxLineCountImplicitHeight); + QCOMPARE(textObject->width(), 50.); + QCOMPARE(textObject->height(), implicitHeight); + QVERIFY(textObject->contentWidth() <= 50.); + QCOMPARE(textObject->contentHeight(), maxLineCountImplicitHeight); + + textObject->setWidth(100.); + QCOMPARE(textObject->implicitWidth(), implicitWidth); + QCOMPARE(textObject->implicitHeight(), maxLineCountImplicitHeight); + QCOMPARE(textObject->width(), 100.); + QCOMPARE(textObject->height(), implicitHeight); + QVERIFY(textObject->contentWidth() <= 100.); + QCOMPARE(textObject->contentHeight(), maxLineCountImplicitHeight); +} + void tst_qquicktext::implicitSizeBinding_data() { implicitSize_data();