| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the QtQuick module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL$ |
| ** 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. |
| ** |
| ** GNU Lesser General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU Lesser |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 2.0 or (at your option) the GNU General |
| ** Public license version 3 or any later version approved by the KDE Free |
| ** Qt Foundation. The licenses are as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
| ** https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include "qquicktextnodeengine_p.h" |
| |
| #include <QtCore/qpoint.h> |
| #include <QtGui/qabstracttextdocumentlayout.h> |
| #include <QtGui/qrawfont.h> |
| #include <QtGui/qtextdocument.h> |
| #include <QtGui/qtextlayout.h> |
| #include <QtGui/qtextobject.h> |
| #include <QtGui/qtexttable.h> |
| #include <QtGui/qtextlist.h> |
| |
| #include <private/qquicktext_p.h> |
| #include <private/qquicktextdocument_p.h> |
| #include <private/qtextdocumentlayout_p.h> |
| #include <private/qtextimagehandler_p.h> |
| #include <private/qrawfont_p.h> |
| #include <private/qglyphrun_p.h> |
| |
| QT_BEGIN_NAMESPACE |
| |
| QQuickTextNodeEngine::BinaryTreeNodeKey::BinaryTreeNodeKey(BinaryTreeNode *node) |
| : fontEngine(QRawFontPrivate::get(node->glyphRun.rawFont())->fontEngine) |
| , clipNode(node->clipNode) |
| , color(node->color.rgba()) |
| , selectionState(node->selectionState) |
| { |
| } |
| |
| QQuickTextNodeEngine::BinaryTreeNode::BinaryTreeNode(const QGlyphRun &g, |
| SelectionState selState, |
| const QRectF &brect, |
| const Decorations &decs, |
| const QColor &c, |
| const QColor &bc, |
| const QPointF &pos, qreal a) |
| : glyphRun(g) |
| , boundingRect(brect) |
| , selectionState(selState) |
| , clipNode(nullptr) |
| , decorations(decs) |
| , color(c) |
| , backgroundColor(bc) |
| , position(pos) |
| , ascent(a) |
| , leftChildIndex(-1) |
| , rightChildIndex(-1) |
| { |
| QGlyphRunPrivate *d = QGlyphRunPrivate::get(g); |
| ranges.append(qMakePair(d->textRangeStart, d->textRangeEnd)); |
| } |
| |
| |
| void QQuickTextNodeEngine::BinaryTreeNode::insert(QVarLengthArray<BinaryTreeNode, 16> *binaryTree, const QGlyphRun &glyphRun, SelectionState selectionState, |
| Decorations decorations, const QColor &textColor, |
| const QColor &backgroundColor, const QPointF &position) |
| { |
| QRectF searchRect = glyphRun.boundingRect(); |
| searchRect.translate(position); |
| |
| if (qFuzzyIsNull(searchRect.width()) || qFuzzyIsNull(searchRect.height())) |
| return; |
| |
| decorations |= (glyphRun.underline() ? Decoration::Underline : Decoration::NoDecoration); |
| decorations |= (glyphRun.overline() ? Decoration::Overline : Decoration::NoDecoration); |
| decorations |= (glyphRun.strikeOut() ? Decoration::StrikeOut : Decoration::NoDecoration); |
| decorations |= (backgroundColor.isValid() ? Decoration::Background : Decoration::NoDecoration); |
| |
| qreal ascent = glyphRun.rawFont().ascent(); |
| insert(binaryTree, BinaryTreeNode(glyphRun, |
| selectionState, |
| searchRect, |
| decorations, |
| textColor, |
| backgroundColor, |
| position, |
| ascent)); |
| } |
| |
| void QQuickTextNodeEngine::BinaryTreeNode::insert(QVarLengthArray<BinaryTreeNode, 16> *binaryTree, const BinaryTreeNode &binaryTreeNode) |
| { |
| int newIndex = binaryTree->size(); |
| binaryTree->append(binaryTreeNode); |
| if (newIndex == 0) |
| return; |
| |
| int searchIndex = 0; |
| forever { |
| BinaryTreeNode *node = binaryTree->data() + searchIndex; |
| if (binaryTreeNode.boundingRect.left() < node->boundingRect.left()) { |
| if (node->leftChildIndex < 0) { |
| node->leftChildIndex = newIndex; |
| break; |
| } else { |
| searchIndex = node->leftChildIndex; |
| } |
| } else { |
| if (node->rightChildIndex < 0) { |
| node->rightChildIndex = newIndex; |
| break; |
| } else { |
| searchIndex = node->rightChildIndex; |
| } |
| } |
| } |
| } |
| |
| void QQuickTextNodeEngine::BinaryTreeNode::inOrder(const QVarLengthArray<BinaryTreeNode, 16> &binaryTree, |
| QVarLengthArray<int> *sortedIndexes, int currentIndex) |
| { |
| Q_ASSERT(currentIndex < binaryTree.size()); |
| |
| const BinaryTreeNode *node = binaryTree.data() + currentIndex; |
| if (node->leftChildIndex >= 0) |
| inOrder(binaryTree, sortedIndexes, node->leftChildIndex); |
| |
| sortedIndexes->append(currentIndex); |
| |
| if (node->rightChildIndex >= 0) |
| inOrder(binaryTree, sortedIndexes, node->rightChildIndex); |
| } |
| |
| |
| int QQuickTextNodeEngine::addText(const QTextBlock &block, |
| const QTextCharFormat &charFormat, |
| const QColor &textColor, |
| const QVarLengthArray<QTextLayout::FormatRange> &colorChanges, |
| int textPos, int fragmentEnd, |
| int selectionStart, int selectionEnd) |
| { |
| if (charFormat.foreground().style() != Qt::NoBrush) |
| setTextColor(charFormat.foreground().color()); |
| else |
| setTextColor(textColor); |
| |
| while (textPos < fragmentEnd) { |
| int blockRelativePosition = textPos - block.position(); |
| QTextLine line = block.layout()->lineForTextPosition(blockRelativePosition); |
| if (!currentLine().isValid() |
| || line.lineNumber() != currentLine().lineNumber()) { |
| setCurrentLine(line); |
| } |
| |
| Q_ASSERT(line.textLength() > 0); |
| int lineEnd = line.textStart() + block.position() + line.textLength(); |
| |
| int len = qMin(lineEnd - textPos, fragmentEnd - textPos); |
| Q_ASSERT(len > 0); |
| |
| int currentStepEnd = textPos + len; |
| |
| addGlyphsForRanges(colorChanges, |
| textPos - block.position(), |
| currentStepEnd - block.position(), |
| selectionStart - block.position(), |
| selectionEnd - block.position()); |
| |
| textPos = currentStepEnd; |
| } |
| return textPos; |
| } |
| |
| void QQuickTextNodeEngine::addTextDecorations(const QVarLengthArray<TextDecoration> &textDecorations, |
| qreal offset, qreal thickness) |
| { |
| for (int i=0; i<textDecorations.size(); ++i) { |
| TextDecoration textDecoration = textDecorations.at(i); |
| |
| { |
| QRectF &rect = textDecoration.rect; |
| rect.setY(qRound(rect.y() |
| + m_currentLine.ascent() |
| + (m_currentLine.leadingIncluded() ? m_currentLine.leading() : qreal(0.0f)) |
| + offset)); |
| rect.setHeight(thickness); |
| } |
| |
| m_lines.append(textDecoration); |
| } |
| } |
| |
| void QQuickTextNodeEngine::processCurrentLine() |
| { |
| // No glyphs, do nothing |
| if (m_currentLineTree.isEmpty()) |
| return; |
| |
| // 1. Go through current line and get correct decoration position for each node based on |
| // neighbouring decorations. Add decoration to global list |
| // 2. Create clip nodes for all selected text. Try to merge as many as possible within |
| // the line. |
| // 3. Add QRects to a list of selection rects. |
| // 4. Add all nodes to a global processed list |
| QVarLengthArray<int> sortedIndexes; // Indexes in tree sorted by x position |
| BinaryTreeNode::inOrder(m_currentLineTree, &sortedIndexes); |
| |
| Q_ASSERT(sortedIndexes.size() == m_currentLineTree.size()); |
| |
| SelectionState currentSelectionState = Unselected; |
| QRectF currentRect; |
| |
| Decorations currentDecorations = Decoration::NoDecoration; |
| qreal underlineOffset = 0.0; |
| qreal underlineThickness = 0.0; |
| |
| qreal overlineOffset = 0.0; |
| qreal overlineThickness = 0.0; |
| |
| qreal strikeOutOffset = 0.0; |
| qreal strikeOutThickness = 0.0; |
| |
| QRectF decorationRect = currentRect; |
| |
| QColor lastColor; |
| QColor lastBackgroundColor; |
| |
| QVarLengthArray<TextDecoration> pendingUnderlines; |
| QVarLengthArray<TextDecoration> pendingOverlines; |
| QVarLengthArray<TextDecoration> pendingStrikeOuts; |
| if (!sortedIndexes.isEmpty()) { |
| QQuickDefaultClipNode *currentClipNode = m_hasSelection ? new QQuickDefaultClipNode(QRectF()) : nullptr; |
| bool currentClipNodeUsed = false; |
| for (int i=0; i<=sortedIndexes.size(); ++i) { |
| BinaryTreeNode *node = nullptr; |
| if (i < sortedIndexes.size()) { |
| int sortedIndex = sortedIndexes.at(i); |
| Q_ASSERT(sortedIndex < m_currentLineTree.size()); |
| |
| node = m_currentLineTree.data() + sortedIndex; |
| } |
| |
| if (i == 0) |
| currentSelectionState = node->selectionState; |
| |
| // Update decorations |
| if (currentDecorations != Decoration::NoDecoration) { |
| decorationRect.setY(m_position.y() + m_currentLine.y()); |
| decorationRect.setHeight(m_currentLine.height()); |
| |
| if (node != nullptr) |
| decorationRect.setRight(node->boundingRect.left()); |
| |
| TextDecoration textDecoration(currentSelectionState, decorationRect, lastColor); |
| if (currentDecorations & Decoration::Underline) |
| pendingUnderlines.append(textDecoration); |
| |
| if (currentDecorations & Decoration::Overline) |
| pendingOverlines.append(textDecoration); |
| |
| if (currentDecorations & Decoration::StrikeOut) |
| pendingStrikeOuts.append(textDecoration); |
| |
| if (currentDecorations & Decoration::Background) |
| m_backgrounds.append(qMakePair(decorationRect, lastBackgroundColor)); |
| } |
| |
| // If we've reached an unselected node from a selected node, we add the |
| // selection rect to the graph, and we add decoration every time the |
| // selection state changes, because that means the text color changes |
| if (node == nullptr || node->selectionState != currentSelectionState) { |
| currentRect.setY(m_position.y() + m_currentLine.y()); |
| currentRect.setHeight(m_currentLine.height()); |
| |
| if (currentSelectionState == Selected) |
| m_selectionRects.append(currentRect); |
| |
| if (currentClipNode != nullptr) { |
| if (!currentClipNodeUsed) { |
| delete currentClipNode; |
| } else { |
| currentClipNode->setIsRectangular(true); |
| currentClipNode->setRect(currentRect); |
| currentClipNode->update(); |
| } |
| } |
| |
| if (node != nullptr && m_hasSelection) |
| currentClipNode = new QQuickDefaultClipNode(QRectF()); |
| else |
| currentClipNode = nullptr; |
| currentClipNodeUsed = false; |
| |
| if (node != nullptr) { |
| currentSelectionState = node->selectionState; |
| currentRect = node->boundingRect; |
| |
| // Make sure currentRect is valid, otherwise the unite won't work |
| if (currentRect.isNull()) |
| currentRect.setSize(QSizeF(1, 1)); |
| } |
| } else { |
| if (currentRect.isNull()) |
| currentRect = node->boundingRect; |
| else |
| currentRect = currentRect.united(node->boundingRect); |
| } |
| |
| if (node != nullptr) { |
| if (node->selectionState == Selected) { |
| node->clipNode = currentClipNode; |
| currentClipNodeUsed = true; |
| } |
| |
| decorationRect = node->boundingRect; |
| |
| // If previous item(s) had underline and current does not, then we add the |
| // pending lines to the lists and likewise for overlines and strikeouts |
| if (!pendingUnderlines.isEmpty() |
| && !(node->decorations & Decoration::Underline)) { |
| addTextDecorations(pendingUnderlines, underlineOffset, underlineThickness); |
| |
| pendingUnderlines.clear(); |
| |
| underlineOffset = 0.0; |
| underlineThickness = 0.0; |
| } |
| |
| // ### Add pending when overlineOffset/thickness changes to minimize number of |
| // nodes |
| if (!pendingOverlines.isEmpty()) { |
| addTextDecorations(pendingOverlines, overlineOffset, overlineThickness); |
| |
| pendingOverlines.clear(); |
| |
| overlineOffset = 0.0; |
| overlineThickness = 0.0; |
| } |
| |
| // ### Add pending when overlineOffset/thickness changes to minimize number of |
| // nodes |
| if (!pendingStrikeOuts.isEmpty()) { |
| addTextDecorations(pendingStrikeOuts, strikeOutOffset, strikeOutThickness); |
| |
| pendingStrikeOuts.clear(); |
| |
| strikeOutOffset = 0.0; |
| strikeOutThickness = 0.0; |
| } |
| |
| // Merge current values with previous. Prefer greatest thickness |
| QRawFont rawFont = node->glyphRun.rawFont(); |
| if (node->decorations & Decoration::Underline) { |
| if (rawFont.lineThickness() > underlineThickness) { |
| underlineThickness = rawFont.lineThickness(); |
| underlineOffset = rawFont.underlinePosition(); |
| } |
| } |
| |
| if (node->decorations & Decoration::Overline) { |
| overlineOffset = -rawFont.ascent(); |
| overlineThickness = rawFont.lineThickness(); |
| } |
| |
| if (node->decorations & Decoration::StrikeOut) { |
| strikeOutThickness = rawFont.lineThickness(); |
| strikeOutOffset = rawFont.ascent() / -3.0; |
| } |
| |
| currentDecorations = node->decorations; |
| lastColor = node->color; |
| lastBackgroundColor = node->backgroundColor; |
| m_processedNodes.append(*node); |
| } |
| } |
| |
| if (!pendingUnderlines.isEmpty()) |
| addTextDecorations(pendingUnderlines, underlineOffset, underlineThickness); |
| |
| if (!pendingOverlines.isEmpty()) |
| addTextDecorations(pendingOverlines, overlineOffset, overlineThickness); |
| |
| if (!pendingStrikeOuts.isEmpty()) |
| addTextDecorations(pendingStrikeOuts, strikeOutOffset, strikeOutThickness); |
| } |
| |
| m_currentLineTree.clear(); |
| m_currentLine = QTextLine(); |
| m_hasSelection = false; |
| } |
| |
| void QQuickTextNodeEngine::addImage(const QRectF &rect, const QImage &image, qreal ascent, |
| SelectionState selectionState, |
| QTextFrameFormat::Position layoutPosition) |
| { |
| QRectF searchRect = rect; |
| if (layoutPosition == QTextFrameFormat::InFlow) { |
| if (m_currentLineTree.isEmpty()) { |
| qreal y = m_currentLine.ascent() - ascent; |
| if (m_currentTextDirection == Qt::RightToLeft) |
| searchRect.moveTopRight(m_position + m_currentLine.rect().topRight() + QPointF(0, y)); |
| else |
| searchRect.moveTopLeft(m_position + m_currentLine.position() + QPointF(0, y)); |
| } else { |
| const BinaryTreeNode *lastNode = m_currentLineTree.data() + m_currentLineTree.size() - 1; |
| if (lastNode->glyphRun.isRightToLeft()) { |
| QPointF lastPos = lastNode->boundingRect.topLeft(); |
| searchRect.moveTopRight(lastPos - QPointF(0, ascent - lastNode->ascent)); |
| } else { |
| QPointF lastPos = lastNode->boundingRect.topRight(); |
| searchRect.moveTopLeft(lastPos - QPointF(0, ascent - lastNode->ascent)); |
| } |
| } |
| } |
| |
| BinaryTreeNode::insert(&m_currentLineTree, searchRect, image, ascent, selectionState); |
| m_hasContents = true; |
| } |
| |
| void QQuickTextNodeEngine::addTextObject(const QTextBlock &block, const QPointF &position, const QTextCharFormat &format, |
| SelectionState selectionState, |
| QTextDocument *textDocument, int pos, |
| QTextFrameFormat::Position layoutPosition) |
| { |
| QTextObjectInterface *handler = textDocument->documentLayout()->handlerForObject(format.objectType()); |
| if (handler != nullptr) { |
| QImage image; |
| QSizeF size = handler->intrinsicSize(textDocument, pos, format); |
| |
| if (format.objectType() == QTextFormat::ImageObject) { |
| QTextImageFormat imageFormat = format.toImageFormat(); |
| if (QQuickTextDocumentWithImageResources *imageDoc = qobject_cast<QQuickTextDocumentWithImageResources *>(textDocument)) { |
| image = imageDoc->image(imageFormat); |
| |
| if (image.isNull()) |
| return; |
| } else { |
| QTextImageHandler *imageHandler = static_cast<QTextImageHandler *>(handler); |
| image = imageHandler->image(textDocument, imageFormat); |
| } |
| } |
| |
| if (image.isNull()) { |
| image = QImage(size.toSize(), QImage::Format_ARGB32_Premultiplied); |
| image.fill(Qt::transparent); |
| { |
| QPainter painter(&image); |
| handler->drawObject(&painter, image.rect(), textDocument, pos, format); |
| } |
| } |
| |
| qreal ascent; |
| QTextLine line = block.layout()->lineForTextPosition(pos - block.position()); |
| switch (format.verticalAlignment()) |
| { |
| case QTextCharFormat::AlignTop: |
| ascent = line.ascent(); |
| break; |
| case QTextCharFormat::AlignMiddle: { |
| QFontMetrics m(format.font()); |
| ascent = (size.height() - m.xHeight()) / 2; |
| break; |
| } |
| case QTextCharFormat::AlignBottom: |
| ascent = size.height() - line.descent(); |
| break; |
| case QTextCharFormat::AlignBaseline: |
| default: |
| ascent = size.height(); |
| } |
| |
| addImage(QRectF(position, size), image, ascent, selectionState, layoutPosition); |
| } |
| } |
| |
| void QQuickTextNodeEngine::addUnselectedGlyphs(const QGlyphRun &glyphRun) |
| { |
| BinaryTreeNode::insert(&m_currentLineTree, |
| glyphRun, |
| Unselected, |
| Decoration::NoDecoration, |
| m_textColor, |
| m_backgroundColor, |
| m_position); |
| } |
| |
| void QQuickTextNodeEngine::addSelectedGlyphs(const QGlyphRun &glyphRun) |
| { |
| int currentSize = m_currentLineTree.size(); |
| BinaryTreeNode::insert(&m_currentLineTree, |
| glyphRun, |
| Selected, |
| Decoration::NoDecoration, |
| m_textColor, |
| m_backgroundColor, |
| m_position); |
| m_hasSelection = m_hasSelection || m_currentLineTree.size() > currentSize; |
| } |
| |
| void QQuickTextNodeEngine::addGlyphsForRanges(const QVarLengthArray<QTextLayout::FormatRange> &ranges, |
| int start, int end, |
| int selectionStart, int selectionEnd) |
| { |
| int currentPosition = start; |
| int remainingLength = end - start; |
| for (int j=0; j<ranges.size(); ++j) { |
| const QTextLayout::FormatRange &range = ranges.at(j); |
| if (range.start + range.length >= currentPosition |
| && range.start < currentPosition + remainingLength) { |
| |
| if (range.start > currentPosition) { |
| addGlyphsInRange(currentPosition, range.start - currentPosition, |
| QColor(), QColor(), selectionStart, selectionEnd); |
| } |
| int rangeEnd = qMin(range.start + range.length, currentPosition + remainingLength); |
| QColor rangeColor; |
| if (range.format.hasProperty(QTextFormat::ForegroundBrush)) |
| rangeColor = range.format.foreground().color(); |
| else if (range.format.isAnchor()) |
| rangeColor = m_anchorColor; |
| QColor rangeBackgroundColor = range.format.hasProperty(QTextFormat::BackgroundBrush) |
| ? range.format.background().color() |
| : QColor(); |
| |
| addGlyphsInRange(range.start, rangeEnd - range.start, |
| rangeColor, rangeBackgroundColor, |
| selectionStart, selectionEnd); |
| |
| currentPosition = range.start + range.length; |
| remainingLength = end - currentPosition; |
| |
| } else if (range.start > currentPosition + remainingLength || remainingLength <= 0) { |
| break; |
| } |
| } |
| |
| if (remainingLength > 0) { |
| addGlyphsInRange(currentPosition, remainingLength, QColor(), QColor(), |
| selectionStart, selectionEnd); |
| } |
| |
| } |
| |
| void QQuickTextNodeEngine::addGlyphsInRange(int rangeStart, int rangeLength, |
| const QColor &color, const QColor &backgroundColor, |
| int selectionStart, int selectionEnd) |
| { |
| QColor oldColor; |
| if (color.isValid()) { |
| oldColor = m_textColor; |
| m_textColor = color; |
| } |
| |
| QColor oldBackgroundColor = m_backgroundColor; |
| if (backgroundColor.isValid()) { |
| oldBackgroundColor = m_backgroundColor; |
| m_backgroundColor = backgroundColor; |
| } |
| |
| bool hasSelection = selectionEnd >= 0 |
| && selectionStart <= selectionEnd; |
| |
| QTextLine &line = m_currentLine; |
| int rangeEnd = rangeStart + rangeLength; |
| if (!hasSelection || (selectionStart > rangeEnd || selectionEnd < rangeStart)) { |
| QList<QGlyphRun> glyphRuns = line.glyphRuns(rangeStart, rangeLength); |
| for (int j=0; j<glyphRuns.size(); ++j) { |
| const QGlyphRun &glyphRun = glyphRuns.at(j); |
| addUnselectedGlyphs(glyphRun); |
| } |
| } else { |
| if (rangeStart < selectionStart) { |
| int length = qMin(selectionStart - rangeStart, rangeLength); |
| QList<QGlyphRun> glyphRuns = line.glyphRuns(rangeStart, length); |
| for (int j=0; j<glyphRuns.size(); ++j) { |
| const QGlyphRun &glyphRun = glyphRuns.at(j); |
| addUnselectedGlyphs(glyphRun); |
| } |
| } |
| |
| if (rangeEnd > selectionStart) { |
| int start = qMax(selectionStart, rangeStart); |
| int length = qMin(selectionEnd - start + 1, rangeEnd - start); |
| QList<QGlyphRun> glyphRuns = line.glyphRuns(start, length); |
| |
| for (int j=0; j<glyphRuns.size(); ++j) { |
| const QGlyphRun &glyphRun = glyphRuns.at(j); |
| addSelectedGlyphs(glyphRun); |
| } |
| } |
| |
| if (selectionEnd >= rangeStart && selectionEnd < rangeEnd) { |
| int start = selectionEnd + 1; |
| int length = rangeEnd - selectionEnd - 1; |
| QList<QGlyphRun> glyphRuns = line.glyphRuns(start, length); |
| for (int j=0; j<glyphRuns.size(); ++j) { |
| const QGlyphRun &glyphRun = glyphRuns.at(j); |
| addUnselectedGlyphs(glyphRun); |
| } |
| } |
| } |
| |
| if (backgroundColor.isValid()) |
| m_backgroundColor = oldBackgroundColor; |
| |
| if (oldColor.isValid()) |
| m_textColor = oldColor; |
| } |
| |
| void QQuickTextNodeEngine::addBorder(const QRectF &rect, qreal border, |
| QTextFrameFormat::BorderStyle borderStyle, |
| const QBrush &borderBrush) |
| { |
| const QColor &color = borderBrush.color(); |
| |
| // Currently we don't support other styles than solid |
| Q_UNUSED(borderStyle); |
| |
| m_backgrounds.append(qMakePair(QRectF(rect.left(), rect.top(), border, rect.height() + border), color)); |
| m_backgrounds.append(qMakePair(QRectF(rect.left() + border, rect.top(), rect.width(), border), color)); |
| m_backgrounds.append(qMakePair(QRectF(rect.right(), rect.top() + border, border, rect.height() - border), color)); |
| m_backgrounds.append(qMakePair(QRectF(rect.left() + border, rect.bottom(), rect.width(), border), color)); |
| } |
| |
| void QQuickTextNodeEngine::addFrameDecorations(QTextDocument *document, QTextFrame *frame) |
| { |
| QTextDocumentLayout *documentLayout = qobject_cast<QTextDocumentLayout *>(document->documentLayout()); |
| if (Q_UNLIKELY(!documentLayout)) |
| return; |
| |
| QTextFrameFormat frameFormat = frame->format().toFrameFormat(); |
| QTextTable *table = qobject_cast<QTextTable *>(frame); |
| |
| QRectF boundingRect = table == nullptr |
| ? documentLayout->frameBoundingRect(frame) |
| : documentLayout->tableBoundingRect(table); |
| |
| QBrush bg = frame->frameFormat().background(); |
| if (bg.style() != Qt::NoBrush) |
| m_backgrounds.append(qMakePair(boundingRect, bg.color())); |
| |
| if (!frameFormat.hasProperty(QTextFormat::FrameBorder)) |
| return; |
| |
| qreal borderWidth = frameFormat.border(); |
| if (qFuzzyIsNull(borderWidth)) |
| return; |
| |
| QBrush borderBrush = frameFormat.borderBrush(); |
| QTextFrameFormat::BorderStyle borderStyle = frameFormat.borderStyle(); |
| if (borderStyle == QTextFrameFormat::BorderStyle_None) |
| return; |
| |
| addBorder(boundingRect.adjusted(frameFormat.leftMargin(), frameFormat.topMargin(), |
| -frameFormat.rightMargin(), -frameFormat.bottomMargin()), |
| borderWidth, borderStyle, borderBrush); |
| if (table != nullptr) { |
| int rows = table->rows(); |
| int columns = table->columns(); |
| |
| for (int row=0; row<rows; ++row) { |
| for (int column=0; column<columns; ++column) { |
| QTextTableCell cell = table->cellAt(row, column); |
| |
| QRectF cellRect = documentLayout->tableCellBoundingRect(table, cell); |
| addBorder(cellRect.adjusted(-borderWidth, -borderWidth, 0, 0), borderWidth, |
| borderStyle, borderBrush); |
| } |
| } |
| } |
| } |
| |
| uint qHash(const QQuickTextNodeEngine::BinaryTreeNodeKey &key) |
| { |
| // Just use the default hash for pairs |
| return qHash(qMakePair(key.fontEngine, qMakePair(key.clipNode, |
| qMakePair(key.color, key.selectionState)))); |
| } |
| |
| void QQuickTextNodeEngine::mergeProcessedNodes(QList<BinaryTreeNode *> *regularNodes, |
| QList<BinaryTreeNode *> *imageNodes) |
| { |
| QHash<BinaryTreeNodeKey, QList<BinaryTreeNode *> > map; |
| |
| for (int i = 0; i < m_processedNodes.size(); ++i) { |
| BinaryTreeNode *node = m_processedNodes.data() + i; |
| |
| if (node->image.isNull()) { |
| BinaryTreeNodeKey key(node); |
| |
| QList<BinaryTreeNode *> &nodes = map[key]; |
| if (nodes.isEmpty()) |
| regularNodes->append(node); |
| |
| nodes.append(node); |
| } else { |
| imageNodes->append(node); |
| } |
| } |
| |
| for (int i = 0; i < regularNodes->size(); ++i) { |
| BinaryTreeNode *primaryNode = regularNodes->at(i); |
| BinaryTreeNodeKey key(primaryNode); |
| |
| const QList<BinaryTreeNode *> &nodes = map.value(key); |
| Q_ASSERT(nodes.first() == primaryNode); |
| |
| int count = 0; |
| for (int j = 0; j < nodes.size(); ++j) |
| count += nodes.at(j)->glyphRun.glyphIndexes().size(); |
| |
| if (count != primaryNode->glyphRun.glyphIndexes().size()) { |
| QGlyphRun &glyphRun = primaryNode->glyphRun; |
| QVector<quint32> glyphIndexes = glyphRun.glyphIndexes(); |
| glyphIndexes.reserve(count); |
| |
| QVector<QPointF> glyphPositions = glyphRun.positions(); |
| glyphPositions.reserve(count); |
| |
| QRectF glyphBoundingRect = glyphRun.boundingRect(); |
| |
| for (int j = 1; j < nodes.size(); ++j) { |
| BinaryTreeNode *otherNode = nodes.at(j); |
| glyphIndexes += otherNode->glyphRun.glyphIndexes(); |
| primaryNode->ranges += otherNode->ranges; |
| glyphBoundingRect = glyphBoundingRect.united(otherNode->boundingRect); |
| |
| QVector<QPointF> otherPositions = otherNode->glyphRun.positions(); |
| for (int k = 0; k < otherPositions.size(); ++k) |
| glyphPositions += otherPositions.at(k) + (otherNode->position - primaryNode->position); |
| } |
| |
| Q_ASSERT(glyphPositions.size() == count); |
| Q_ASSERT(glyphIndexes.size() == count); |
| |
| glyphRun.setGlyphIndexes(glyphIndexes); |
| glyphRun.setPositions(glyphPositions); |
| glyphRun.setBoundingRect(glyphBoundingRect); |
| } |
| } |
| } |
| |
| void QQuickTextNodeEngine::addToSceneGraph(QQuickTextNode *parentNode, |
| QQuickText::TextStyle style, |
| const QColor &styleColor) |
| { |
| if (m_currentLine.isValid()) |
| processCurrentLine(); |
| |
| QList<BinaryTreeNode *> nodes; |
| QList<BinaryTreeNode *> imageNodes; |
| mergeProcessedNodes(&nodes, &imageNodes); |
| |
| for (int i = 0; i < m_backgrounds.size(); ++i) { |
| const QRectF &rect = m_backgrounds.at(i).first; |
| const QColor &color = m_backgrounds.at(i).second; |
| if (color.alpha() != 0) |
| parentNode->addRectangleNode(rect, color); |
| } |
| |
| // Add all text with unselected color first |
| for (int i = 0; i < nodes.size(); ++i) { |
| const BinaryTreeNode *node = nodes.at(i); |
| parentNode->addGlyphs(node->position, node->glyphRun, node->color, style, styleColor, nullptr); |
| } |
| |
| for (int i = 0; i < imageNodes.size(); ++i) { |
| const BinaryTreeNode *node = imageNodes.at(i); |
| if (node->selectionState == Unselected) |
| parentNode->addImage(node->boundingRect, node->image); |
| } |
| |
| // Then, prepend all selection rectangles to the tree |
| for (int i = 0; i < m_selectionRects.size(); ++i) { |
| const QRectF &rect = m_selectionRects.at(i); |
| |
| parentNode->addRectangleNode(rect, m_selectionColor); |
| } |
| |
| // Add decorations for each node to the tree. |
| for (int i = 0; i < m_lines.size(); ++i) { |
| const TextDecoration &textDecoration = m_lines.at(i); |
| |
| QColor color = textDecoration.selectionState == Selected |
| ? m_selectedTextColor |
| : textDecoration.color; |
| |
| parentNode->addRectangleNode(textDecoration.rect, color); |
| } |
| |
| // Finally add the selected text on top of everything |
| for (int i = 0; i < nodes.size(); ++i) { |
| const BinaryTreeNode *node = nodes.at(i); |
| QQuickDefaultClipNode *clipNode = node->clipNode; |
| if (clipNode != nullptr && clipNode->parent() == nullptr) |
| parentNode->appendChildNode(clipNode); |
| |
| if (node->selectionState == Selected) { |
| QColor color = m_selectedTextColor; |
| int previousNodeIndex = i - 1; |
| int nextNodeIndex = i + 1; |
| const BinaryTreeNode *previousNode = previousNodeIndex < 0 ? 0 : nodes.at(previousNodeIndex); |
| while (previousNode != nullptr && qFuzzyCompare(previousNode->boundingRect.left(), node->boundingRect.left())) |
| previousNode = --previousNodeIndex < 0 ? 0 : nodes.at(previousNodeIndex); |
| |
| const BinaryTreeNode *nextNode = nextNodeIndex == nodes.size() ? 0 : nodes.at(nextNodeIndex); |
| |
| if (previousNode != nullptr && previousNode->selectionState == Unselected) |
| parentNode->addGlyphs(previousNode->position, previousNode->glyphRun, color, style, styleColor, clipNode); |
| |
| if (nextNode != nullptr && nextNode->selectionState == Unselected) |
| parentNode->addGlyphs(nextNode->position, nextNode->glyphRun, color, style, styleColor, clipNode); |
| |
| // If the previous or next node completely overlaps this one, then we have already drawn the glyphs of |
| // this node |
| bool drawCurrent = false; |
| if (previousNode != nullptr || nextNode != nullptr) { |
| for (int i = 0; i < node->ranges.size(); ++i) { |
| const QPair<int, int> &range = node->ranges.at(i); |
| |
| int rangeLength = range.second - range.first + 1; |
| if (previousNode != nullptr) { |
| for (int j = 0; j < previousNode->ranges.size(); ++j) { |
| const QPair<int, int> &otherRange = previousNode->ranges.at(j); |
| |
| if (range.first < otherRange.second && range.second > otherRange.first) { |
| int start = qMax(range.first, otherRange.first); |
| int end = qMin(range.second, otherRange.second); |
| rangeLength -= end - start + 1; |
| if (rangeLength == 0) |
| break; |
| } |
| } |
| } |
| |
| if (nextNode != nullptr && rangeLength > 0) { |
| for (int j = 0; j < nextNode->ranges.size(); ++j) { |
| const QPair<int, int> &otherRange = nextNode->ranges.at(j); |
| |
| if (range.first < otherRange.second && range.second > otherRange.first) { |
| int start = qMax(range.first, otherRange.first); |
| int end = qMin(range.second, otherRange.second); |
| rangeLength -= end - start + 1; |
| if (rangeLength == 0) |
| break; |
| } |
| } |
| } |
| |
| if (rangeLength > 0) { |
| drawCurrent = true; |
| break; |
| } |
| } |
| } else { |
| drawCurrent = true; |
| } |
| |
| if (drawCurrent) |
| parentNode->addGlyphs(node->position, node->glyphRun, color, style, styleColor, clipNode); |
| } |
| } |
| |
| for (int i = 0; i < imageNodes.size(); ++i) { |
| const BinaryTreeNode *node = imageNodes.at(i); |
| if (node->selectionState == Selected) { |
| parentNode->addImage(node->boundingRect, node->image); |
| if (node->selectionState == Selected) { |
| QColor color = m_selectionColor; |
| color.setAlpha(128); |
| parentNode->addRectangleNode(node->boundingRect, color); |
| } |
| } |
| } |
| } |
| |
| void QQuickTextNodeEngine::mergeFormats(QTextLayout *textLayout, QVarLengthArray<QTextLayout::FormatRange> *mergedFormats) |
| { |
| Q_ASSERT(mergedFormats != nullptr); |
| if (textLayout == nullptr) |
| return; |
| |
| QVector<QTextLayout::FormatRange> additionalFormats = textLayout->formats(); |
| for (int i=0; i<additionalFormats.size(); ++i) { |
| QTextLayout::FormatRange additionalFormat = additionalFormats.at(i); |
| if (additionalFormat.format.hasProperty(QTextFormat::ForegroundBrush) |
| || additionalFormat.format.hasProperty(QTextFormat::BackgroundBrush) |
| || additionalFormat.format.isAnchor()) { |
| // Merge overlapping formats |
| if (!mergedFormats->isEmpty()) { |
| QTextLayout::FormatRange *lastFormat = mergedFormats->data() + mergedFormats->size() - 1; |
| |
| if (additionalFormat.start < lastFormat->start + lastFormat->length) { |
| QTextLayout::FormatRange *mergedRange = nullptr; |
| |
| int length = additionalFormat.length; |
| if (additionalFormat.start > lastFormat->start) { |
| lastFormat->length = additionalFormat.start - lastFormat->start; |
| length -= lastFormat->length; |
| |
| mergedFormats->append(QTextLayout::FormatRange()); |
| mergedRange = mergedFormats->data() + mergedFormats->size() - 1; |
| lastFormat = mergedFormats->data() + mergedFormats->size() - 2; |
| } else { |
| mergedRange = lastFormat; |
| } |
| |
| mergedRange->format = lastFormat->format; |
| mergedRange->format.merge(additionalFormat.format); |
| mergedRange->start = additionalFormat.start; |
| |
| int end = qMin(additionalFormat.start + additionalFormat.length, |
| lastFormat->start + lastFormat->length); |
| |
| mergedRange->length = end - mergedRange->start; |
| length -= mergedRange->length; |
| |
| additionalFormat.start = end; |
| additionalFormat.length = length; |
| } |
| } |
| |
| if (additionalFormat.length > 0) |
| mergedFormats->append(additionalFormat); |
| } |
| } |
| |
| } |
| |
| void QQuickTextNodeEngine::addTextBlock(QTextDocument *textDocument, const QTextBlock &block, const QPointF &position, const QColor &textColor, const QColor &anchorColor, int selectionStart, int selectionEnd) |
| { |
| Q_ASSERT(textDocument); |
| #if QT_CONFIG(im) |
| int preeditLength = block.isValid() ? block.layout()->preeditAreaText().length() : 0; |
| int preeditPosition = block.isValid() ? block.layout()->preeditAreaPosition() : -1; |
| #endif |
| |
| setCurrentTextDirection(block.textDirection()); |
| |
| QVarLengthArray<QTextLayout::FormatRange> colorChanges; |
| mergeFormats(block.layout(), &colorChanges); |
| |
| const QTextCharFormat charFormat = block.charFormat(); |
| const QRectF blockBoundingRect = textDocument->documentLayout()->blockBoundingRect(block).translated(position); |
| |
| if (charFormat.background().style() != Qt::NoBrush) |
| m_backgrounds.append(qMakePair(blockBoundingRect, charFormat.background().color())); |
| |
| if (QTextList *textList = block.textList()) { |
| QPointF pos = blockBoundingRect.topLeft(); |
| QTextLayout *layout = block.layout(); |
| if (layout->lineCount() > 0) { |
| QTextLine firstLine = layout->lineAt(0); |
| Q_ASSERT(firstLine.isValid()); |
| |
| setCurrentLine(firstLine); |
| |
| QRectF textRect = firstLine.naturalTextRect(); |
| pos += textRect.topLeft(); |
| if (block.textDirection() == Qt::RightToLeft) |
| pos.rx() += textRect.width(); |
| |
| QFont font(charFormat.font()); |
| QFontMetricsF fontMetrics(font); |
| QTextListFormat listFormat = textList->format(); |
| |
| QString listItemBullet; |
| switch (listFormat.style()) { |
| case QTextListFormat::ListCircle: |
| listItemBullet = QChar(0x25E6); // White bullet |
| break; |
| case QTextListFormat::ListSquare: |
| listItemBullet = QChar(0x25AA); // Black small square |
| break; |
| case QTextListFormat::ListDecimal: |
| case QTextListFormat::ListLowerAlpha: |
| case QTextListFormat::ListUpperAlpha: |
| case QTextListFormat::ListLowerRoman: |
| case QTextListFormat::ListUpperRoman: |
| listItemBullet = textList->itemText(block); |
| break; |
| default: |
| listItemBullet = QChar(0x2022); // Black bullet |
| break; |
| }; |
| |
| switch (block.blockFormat().marker()) { |
| case QTextBlockFormat::MarkerType::Checked: |
| listItemBullet = QChar(0x2612); // Checked checkbox |
| break; |
| case QTextBlockFormat::MarkerType::Unchecked: |
| listItemBullet = QChar(0x2610); // Unchecked checkbox |
| break; |
| case QTextBlockFormat::MarkerType::NoMarker: |
| break; |
| } |
| |
| QSizeF size(fontMetrics.horizontalAdvance(listItemBullet), fontMetrics.height()); |
| qreal xoff = fontMetrics.horizontalAdvance(QLatin1Char(' ')); |
| if (block.textDirection() == Qt::LeftToRight) |
| xoff = -xoff - size.width(); |
| setPosition(pos + QPointF(xoff, 0)); |
| |
| QTextLayout layout; |
| layout.setFont(font); |
| layout.setText(listItemBullet); // Bullet |
| layout.beginLayout(); |
| QTextLine line = layout.createLine(); |
| line.setPosition(QPointF(0, 0)); |
| layout.endLayout(); |
| |
| QList<QGlyphRun> glyphRuns = layout.glyphRuns(); |
| for (int i=0; i<glyphRuns.size(); ++i) |
| addUnselectedGlyphs(glyphRuns.at(i)); |
| } |
| } |
| |
| int textPos = block.position(); |
| QTextBlock::iterator blockIterator = block.begin(); |
| |
| while (!blockIterator.atEnd()) { |
| QTextFragment fragment = blockIterator.fragment(); |
| QString text = fragment.text(); |
| if (text.isEmpty()) |
| continue; |
| |
| QTextCharFormat charFormat = fragment.charFormat(); |
| QFont font(charFormat.font()); |
| QFontMetricsF fontMetrics(font); |
| |
| int fontHeight = fontMetrics.descent() + fontMetrics.ascent(); |
| int valign = charFormat.verticalAlignment(); |
| if (valign == QTextCharFormat::AlignSuperScript) |
| setPosition(QPointF(blockBoundingRect.x(), blockBoundingRect.y() - fontHeight / 2)); |
| else if (valign == QTextCharFormat::AlignSubScript) |
| setPosition(QPointF(blockBoundingRect.x(), blockBoundingRect.y() + fontHeight / 6)); |
| else |
| setPosition(blockBoundingRect.topLeft()); |
| |
| if (text.contains(QChar::ObjectReplacementCharacter)) { |
| QTextFrame *frame = qobject_cast<QTextFrame *>(textDocument->objectForFormat(charFormat)); |
| if (!frame || frame->frameFormat().position() == QTextFrameFormat::InFlow) { |
| int blockRelativePosition = textPos - block.position(); |
| QTextLine line = block.layout()->lineForTextPosition(blockRelativePosition); |
| if (!currentLine().isValid() |
| || line.lineNumber() != currentLine().lineNumber()) { |
| setCurrentLine(line); |
| } |
| |
| QQuickTextNodeEngine::SelectionState selectionState = |
| (selectionStart < textPos + text.length() |
| && selectionEnd >= textPos) |
| ? QQuickTextNodeEngine::Selected |
| : QQuickTextNodeEngine::Unselected; |
| |
| addTextObject(block, QPointF(), charFormat, selectionState, textDocument, textPos); |
| } |
| textPos += text.length(); |
| } else { |
| if (charFormat.foreground().style() != Qt::NoBrush) |
| setTextColor(charFormat.foreground().color()); |
| else if (charFormat.isAnchor()) |
| setTextColor(anchorColor); |
| else |
| setTextColor(textColor); |
| |
| int fragmentEnd = textPos + fragment.length(); |
| #if QT_CONFIG(im) |
| if (preeditPosition >= 0 |
| && (preeditPosition + block.position()) >= textPos |
| && (preeditPosition + block.position()) <= fragmentEnd) { |
| fragmentEnd += preeditLength; |
| } |
| #endif |
| if (charFormat.background().style() != Qt::NoBrush) { |
| QTextLayout::FormatRange additionalFormat; |
| additionalFormat.start = textPos - block.position(); |
| additionalFormat.length = fragmentEnd - textPos; |
| additionalFormat.format = charFormat; |
| colorChanges << additionalFormat; |
| } |
| |
| textPos = addText(block, charFormat, textColor, colorChanges, textPos, fragmentEnd, |
| selectionStart, selectionEnd); |
| } |
| |
| ++blockIterator; |
| } |
| |
| #if QT_CONFIG(im) |
| if (preeditLength >= 0 && textPos <= block.position() + preeditPosition) { |
| setPosition(blockBoundingRect.topLeft()); |
| textPos = block.position() + preeditPosition; |
| QTextLine line = block.layout()->lineForTextPosition(preeditPosition); |
| if (!currentLine().isValid() |
| || line.lineNumber() != currentLine().lineNumber()) { |
| setCurrentLine(line); |
| } |
| textPos = addText(block, block.charFormat(), textColor, colorChanges, |
| textPos, textPos + preeditLength, |
| selectionStart, selectionEnd); |
| } |
| #endif |
| |
| setCurrentLine(QTextLine()); // Reset current line because the text layout changed |
| m_hasContents = true; |
| } |
| |
| |
| QT_END_NAMESPACE |
| |