From f513e88403b66c4a5efe4c62c160dfce151efb33 Mon Sep 17 00:00:00 2001 From: Pierre Rossi Date: Mon, 18 Mar 2013 16:25:59 +0100 Subject: [PATCH] Optimize QQuickTextEdit for larger documents. The rationale is to not end up re-processing the whole document on each update Since we know where the editing takes place, we can break down the text edit's contents in several text nodes and only re-create the affected text nodes upon editing. This requires ripping out the SelectionEngine helper class from QQuickTextNode so that QQuickTextEdit can tap into its functionality directly. A positive side-effect of this exercise is that it should be much harder to to come across GlyphNodes packing more than 16300 glyphs or so. Task-number: QTBUG-29596 Change-Id: Id29b0709baa43f5b29c44ab02398ba996be3e28a Reviewed-by: Yoann Lopes --- src/quick/items/items.pri | 2 + src/quick/items/qquicktextcontrol.cpp | 1 + src/quick/items/qquicktextcontrol_p.h | 1 + src/quick/items/qquicktextcontrol_p_p.h | 4 +- src/quick/items/qquicktextedit.cpp | 349 ++++++--- src/quick/items/qquicktextedit_p.h | 6 +- src/quick/items/qquicktextedit_p_p.h | 29 +- src/quick/items/qquicktextnode.cpp | 1129 +----------------------------- src/quick/items/qquicktextnode_p.h | 9 +- src/quick/items/qquicktextnodeengine.cpp | 935 +++++++++++++++++++++++++ src/quick/items/qquicktextnodeengine_p.h | 234 +++++++ 11 files changed, 1506 insertions(+), 1193 deletions(-) create mode 100644 src/quick/items/qquicktextnodeengine.cpp create mode 100644 src/quick/items/qquicktextnodeengine_p.h diff --git a/src/quick/items/items.pri b/src/quick/items/items.pri index fe406ec..5aaf7d3 100644 --- a/src/quick/items/items.pri +++ b/src/quick/items/items.pri @@ -17,6 +17,7 @@ HEADERS += \ $$PWD/qquicktext_p.h \ $$PWD/qquicktext_p_p.h \ $$PWD/qquicktextnode_p.h \ + $$PWD/qquicktextnodeengine_p.h \ $$PWD/qquicktextinput_p.h \ $$PWD/qquicktextinput_p_p.h \ $$PWD/qquicktextcontrol_p.h \ @@ -84,6 +85,7 @@ SOURCES += \ $$PWD/qquickpainteditem.cpp \ $$PWD/qquicktext.cpp \ $$PWD/qquicktextnode.cpp \ + $$PWD/qquicktextnodeengine.cpp \ $$PWD/qquicktextinput.cpp \ $$PWD/qquicktextcontrol.cpp \ $$PWD/qquicktextdocument.cpp \ diff --git a/src/quick/items/qquicktextcontrol.cpp b/src/quick/items/qquicktextcontrol.cpp index 6cce3cc..9625aa5 100644 --- a/src/quick/items/qquicktextcontrol.cpp +++ b/src/quick/items/qquicktextcontrol.cpp @@ -586,6 +586,7 @@ QQuickTextControl::QQuickTextControl(QTextDocument *doc, QObject *parent) qmlobject_connect(doc, QTextDocument, SIGNAL(contentsChanged()), this, QQuickTextControl, SIGNAL(textChanged())); qmlobject_connect(doc, QTextDocument, SIGNAL(contentsChanged()), this, QQuickTextControl, SLOT(_q_updateCurrentCharFormatAndSelection())); qmlobject_connect(doc, QTextDocument, SIGNAL(cursorPositionChanged(QTextCursor)), this, QQuickTextControl, SLOT(_q_emitCursorPosChanged(QTextCursor))); + connect(doc, &QTextDocument::contentsChange, this, &QQuickTextControl::contentsChange); layout->setProperty("cursorWidth", textCursorWidth); diff --git a/src/quick/items/qquicktextcontrol_p.h b/src/quick/items/qquicktextcontrol_p.h index 94eae81..7ec8a68 100644 --- a/src/quick/items/qquicktextcontrol_p.h +++ b/src/quick/items/qquicktextcontrol_p.h @@ -138,6 +138,7 @@ public Q_SLOTS: Q_SIGNALS: void textChanged(); + void contentsChange(int from, int charsRemoved, int charsAdded); void undoAvailable(bool b); void redoAvailable(bool b); void currentCharFormatChanged(const QTextCharFormat &format); diff --git a/src/quick/items/qquicktextcontrol_p_p.h b/src/quick/items/qquicktextcontrol_p_p.h index a0727c7..fbb88bd 100644 --- a/src/quick/items/qquicktextcontrol_p_p.h +++ b/src/quick/items/qquicktextcontrol_p_p.h @@ -39,8 +39,8 @@ ** ****************************************************************************/ -#ifndef QTEXTCONTROL_P_P_H -#define QTEXTCONTROL_P_P_H +#ifndef QQUICKTEXTCONTROL_P_P_H +#define QQUICKTEXTCONTROL_P_P_H // // W A R N I N G diff --git a/src/quick/items/qquicktextedit.cpp b/src/quick/items/qquicktextedit.cpp index feabbba..cf20b1d 100644 --- a/src/quick/items/qquicktextedit.cpp +++ b/src/quick/items/qquicktextedit.cpp @@ -46,6 +46,7 @@ #include "qquickevents_p_p.h" #include "qquickwindow.h" #include "qquicktextnode_p.h" +#include "qquicktextnodeengine_p.h" #include "qquicktextutil_p.h" #include @@ -55,6 +56,7 @@ #include #include #include +#include #include #include @@ -118,6 +120,11 @@ TextEdit { The link must be in rich text or HTML format and the \a link string provides access to the particular link. */ + +// This is a pretty arbitrary figure. The idea is that we don't want to break down the document +// into text nodes corresponding to a text block each so that the glyph node grouping doesn't become pointless. +static const int nodeBreakingSize = 300; + QQuickTextEdit::QQuickTextEdit(QQuickItem *parent) : QQuickImplicitSizeItem(*(new QQuickTextEditPrivate), parent) { @@ -410,7 +417,7 @@ void QQuickTextEdit::setFont(const QFont &font) moveCursorDelegate(); } updateSize(); - updateDocument(); + updateWholeDocument(); #ifndef QT_NO_IM updateInputMethod(Qt::ImCursorRectangle | Qt::ImFont); #endif @@ -446,7 +453,7 @@ void QQuickTextEdit::setColor(const QColor &color) return; d->color = color; - updateDocument(); + updateWholeDocument(); emit colorChanged(d->color); } @@ -468,7 +475,7 @@ void QQuickTextEdit::setSelectionColor(const QColor &color) return; d->selectionColor = color; - updateDocument(); + updateWholeDocument(); emit selectionColorChanged(d->selectionColor); } @@ -490,7 +497,7 @@ void QQuickTextEdit::setSelectedTextColor(const QColor &color) return; d->selectedTextColor = color; - updateDocument(); + updateWholeDocument(); emit selectedTextColorChanged(d->selectedTextColor); } @@ -1205,6 +1212,7 @@ void QQuickTextEdit::geometryChanged(const QRectF &newGeometry, Q_D(QQuickTextEdit); if (newGeometry.width() != oldGeometry.width() && widthValid() && !d->inLayout) { updateSize(); + updateWholeDocument(); moveCursorDelegate(); } QQuickImplicitSizeItem::geometryChanged(newGeometry, oldGeometry); @@ -1492,7 +1500,7 @@ void QQuickTextEdit::select(int start, int end) d->control->setTextCursor(cursor); // QTBUG-11100 - updateSelectionMarkers(); + updateSelection(); } /*! @@ -1679,6 +1687,15 @@ void QQuickTextEdit::triggerPreprocess() update(); } +typedef QQuickTextEditPrivate::Node TextNode; +typedef QList::iterator TextNodeIterator; + + +static bool comesBefore(TextNode* n1, TextNode* n2) +{ + return n1->startPos() < n2->startPos(); +} + QSGNode *QQuickTextEdit::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *updatePaintNodeData) { Q_UNUSED(updatePaintNodeData); @@ -1692,45 +1709,144 @@ QSGNode *QQuickTextEdit::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData * d->updateType = QQuickTextEditPrivate::UpdateNone; - QSGNode *currentNode = oldNode; - if (oldNode == 0 || d->documentDirty) { - d->documentDirty = false; + QSGTransformNode *rootNode = static_cast(oldNode); + TextNodeIterator nodeIterator = d->textNodeMap.begin(); + while (nodeIterator != d->textNodeMap.end() && !(*nodeIterator)->dirty()) + ++nodeIterator; - QQuickTextNode *node = 0; - if (oldNode == 0) { - node = new QQuickTextNode(QQuickItemPrivate::get(this)->sceneGraphContext(), this); - currentNode = node; - } else { - node = static_cast(oldNode); + + if (!oldNode || nodeIterator < d->textNodeMap.end()) { + + if (!oldNode) + rootNode = new QSGTransformNode; + + int firstDirtyPos = 0; + if (nodeIterator != d->textNodeMap.end()) { + firstDirtyPos = (*nodeIterator)->startPos(); + do { + rootNode->removeChildNode((*nodeIterator)->textNode()); + delete (*nodeIterator)->textNode(); + delete *nodeIterator; + nodeIterator = d->textNodeMap.erase(nodeIterator); + } while (nodeIterator != d->textNodeMap.end() && (*nodeIterator)->dirty()); } + // FIXME: the text decorations could probably be handled separately (only updated for affected textFrames) + if (d->frameDecorationsNode) { + rootNode->removeChildNode(d->frameDecorationsNode); + delete d->frameDecorationsNode; + } + d->frameDecorationsNode = new QQuickTextNode(QQuickItemPrivate::get(this)->sceneGraphContext(), this); + d->frameDecorationsNode->initEngine(QColor(), QColor(), QColor()); + + + QQuickTextNode *node = new QQuickTextNode(QQuickItemPrivate::get(this)->sceneGraphContext(), this); node->setUseNativeRenderer(d->renderType == NativeRendering); - node->deleteContent(); - node->setMatrix(QMatrix4x4()); + node->initEngine(d->color, d->selectedTextColor, d->selectionColor); + + + int sizeCounter = 0; + int prevBlockStart = firstDirtyPos; + QPointF basePosition(d->xoff, d->yoff); + QPointF nodeOffset; + TextNode *firstCleanNode = (nodeIterator != d->textNodeMap.end()) ? *nodeIterator : 0; + + QList frames; + frames.append(d->document->rootFrame()); + + while (!frames.isEmpty()) { + QTextFrame *textFrame = frames.takeFirst(); + frames.append(textFrame->childFrames()); + d->frameDecorationsNode->m_engine->addFrameDecorations(d->document, textFrame); + + if (textFrame->firstPosition() > textFrame->lastPosition() + && textFrame->frameFormat().position() != QTextFrameFormat::InFlow) { + QRectF rect = d->document->documentLayout()->frameBoundingRect(textFrame); + + if (!node->m_engine->hasContents()) { + nodeOffset = rect.topLeft(); + QMatrix4x4 transformMatrix; + transformMatrix.translate(nodeOffset.x(), nodeOffset.y()); + node->setMatrix(transformMatrix); + } + const int pos = textFrame->firstPosition() - 1; + QTextBlock block = textFrame->firstCursorPosition().block(); + node->m_engine->setCurrentLine(block.layout()->lineForTextPosition(pos - block.position())); + node->m_engine->addTextObject(QPointF(0, 0), block.charFormat(), QQuickTextNodeEngine::Unselected, d->document, + pos, textFrame->frameFormat().position()); + } else { + + QTextFrame::iterator it = textFrame->begin(); + + while (!it.atEnd()) { + QTextBlock block = it.currentBlock(); + ++it; + if (block.position() < firstDirtyPos) + continue; + + if (!node->m_engine->hasContents()) { + nodeOffset = d->document->documentLayout()->blockBoundingRect(block).topLeft(); + QMatrix4x4 transformMatrix; + transformMatrix.translate(nodeOffset.x(), nodeOffset.y()); + node->setMatrix(transformMatrix); + } + + node->m_engine->addTextBlock(d->document, block, basePosition - nodeOffset, d->color, QColor(), selectionStart(), selectionEnd() - 1); + sizeCounter += block.length(); + + if ((it.atEnd() && frames.isEmpty()) || (firstCleanNode && block.next().position() >= firstCleanNode->startPos())) // last node that needed replacing or last block of the last frame + break; + + if (sizeCounter > nodeBreakingSize) { + sizeCounter = 0; + node->m_engine->addToSceneGraph(node, QQuickText::Normal, QColor()); + nodeIterator = d->textNodeMap.insert(nodeIterator, new TextNode(prevBlockStart, node)); + ++nodeIterator; + rootNode->appendChildNode(node); + prevBlockStart = block.next().position(); + node = new QQuickTextNode(QQuickItemPrivate::get(this)->sceneGraphContext(), this); + node->setUseNativeRenderer(d->renderType == NativeRendering); + node->initEngine(d->color, d->selectedTextColor, d->selectionColor); + } + } + } + } + node->m_engine->addToSceneGraph(node, QQuickText::Normal, QColor()); + nodeIterator = d->textNodeMap.insert(nodeIterator, new TextNode(prevBlockStart, node)); + ++nodeIterator; + rootNode->appendChildNode(node); + d->frameDecorationsNode->m_engine->addToSceneGraph(d->frameDecorationsNode, QQuickText::Normal, QColor()); + // Now prepend the frame decorations since we want them rendered first, with the text nodes and cursor in front. + rootNode->prependChildNode(d->frameDecorationsNode); + + Q_ASSERT(nodeIterator == d->textNodeMap.end() || (*nodeIterator) == firstCleanNode); + // Update the position of the subsequent text blocks. + if (firstCleanNode) { + QPointF oldOffset = firstCleanNode->textNode()->matrix().map(QPointF(0,0)); + QPointF currentOffset = d->document->documentLayout()->blockBoundingRect(d->document->findBlock(firstCleanNode->startPos())).topLeft(); + QPointF delta = currentOffset - oldOffset; + while (nodeIterator != d->textNodeMap.end()) { + QMatrix4x4 transformMatrix = (*nodeIterator)->textNode()->matrix(); + transformMatrix.translate(delta.x(), delta.y()); + (*nodeIterator)->textNode()->setMatrix(transformMatrix); + ++nodeIterator; + } - node->addTextDocument(QPointF(d->xoff, d->yoff), d->document, d->color, QQuickText::Normal, QColor(), - QColor(), d->selectionColor, d->selectedTextColor, selectionStart(), - selectionEnd() - 1); // selectionEnd() returns first char after - // selection + } } if (d->cursorComponent == 0 && !isReadOnly()) { - QQuickTextNode *node = static_cast(currentNode); - QColor color = (!d->cursorVisible || !d->control->cursorOn()) ? QColor(0, 0, 0, 0) : d->color; - - if (node->cursorNode() == 0) { - node->setCursor(cursorRectangle(), color); - } else { - node->cursorNode()->setRect(cursorRectangle()); - node->cursorNode()->setColor(color); - } - + if (d->cursorNode) + rootNode->removeChildNode(d->cursorNode); + delete d->cursorNode; + d->cursorNode = new QSGSimpleRectNode(cursorRectangle(), color); + rootNode->appendChildNode(d->cursorNode); } - return currentNode; + return rootNode; } /*! @@ -1818,27 +1934,28 @@ void QQuickTextEditPrivate::init() control->setAcceptRichText(false); control->setCursorIsFocusIndicator(true); - qmlobject_connect(control, QQuickTextControl, SIGNAL(updateRequest()), q, QQuickTextEdit, SLOT(updateDocument())); qmlobject_connect(control, QQuickTextControl, SIGNAL(updateCursorRequest()), q, QQuickTextEdit, SLOT(updateCursor())); - qmlobject_connect(control, QQuickTextControl, SIGNAL(textChanged()), q, QQuickTextEdit, SLOT(q_textChanged())); qmlobject_connect(control, QQuickTextControl, SIGNAL(selectionChanged()), q, QQuickTextEdit, SIGNAL(selectedTextChanged())); - qmlobject_connect(control, QQuickTextControl, SIGNAL(selectionChanged()), q, QQuickTextEdit, SLOT(updateSelectionMarkers())); - qmlobject_connect(control, QQuickTextControl, SIGNAL(cursorPositionChanged()), q, QQuickTextEdit, SLOT(updateSelectionMarkers())); + qmlobject_connect(control, QQuickTextControl, SIGNAL(selectionChanged()), q, QQuickTextEdit, SLOT(updateSelection())); + qmlobject_connect(control, QQuickTextControl, SIGNAL(cursorPositionChanged()), q, QQuickTextEdit, SLOT(updateSelection())); qmlobject_connect(control, QQuickTextControl, SIGNAL(cursorPositionChanged()), q, QQuickTextEdit, SIGNAL(cursorPositionChanged())); qmlobject_connect(control, QQuickTextControl, SIGNAL(cursorRectangleChanged()), q, QQuickTextEdit, SLOT(moveCursorDelegate())); qmlobject_connect(control, QQuickTextControl, SIGNAL(linkActivated(QString)), q, QQuickTextEdit, SIGNAL(linkActivated(QString))); + qmlobject_connect(control, QQuickTextControl, SIGNAL(textChanged()), q, QQuickTextEdit, SLOT(q_textChanged())); #ifndef QT_NO_CLIPBOARD qmlobject_connect(QGuiApplication::clipboard(), QClipboard, SIGNAL(dataChanged()), q, QQuickTextEdit, SLOT(q_canPasteChanged())); #endif qmlobject_connect(document, QQuickTextDocumentWithImageResources, SIGNAL(undoAvailable(bool)), q, QQuickTextEdit, SIGNAL(canUndoChanged())); qmlobject_connect(document, QQuickTextDocumentWithImageResources, SIGNAL(redoAvailable(bool)), q, QQuickTextEdit, SIGNAL(canRedoChanged())); qmlobject_connect(document, QQuickTextDocumentWithImageResources, SIGNAL(imagesLoaded()), q, QQuickTextEdit, SLOT(updateSize())); + QObject::connect(document, &QQuickTextDocumentWithImageResources::contentsChange, q, &QQuickTextEdit::q_contentsChange); document->setDefaultFont(font); document->setDocumentMargin(textMargin); document->setUndoRedoEnabled(false); // flush undo buffer. document->setUndoRedoEnabled(true); updateDefaultTextOption(); + q->updateSize(); } void QQuickTextEdit::q_textChanged() @@ -1857,6 +1974,44 @@ void QQuickTextEdit::q_textChanged() emit textChanged(); } +void QQuickTextEdit::markDirtyNodesForRange(int start, int end, int charDelta) +{ + Q_D(QQuickTextEdit); + if (start == end) + return; + TextNode dummyNode(start, 0); + TextNodeIterator it = qLowerBound(d->textNodeMap.begin(), d->textNodeMap.end(), &dummyNode, &comesBefore); + // qLowerBound gives us the first node past the start of the affected portion, rewind by one if we can. + if (it != d->textNodeMap.begin()) + --it; + + // mark the affected nodes as dirty + while (it != d->textNodeMap.constEnd()) { + if ((*it)->startPos() <= end) + (*it)->setDirty(); + else if (charDelta) + (*it)->moveStartPos(charDelta); + else + return; + ++it; + } +} + +void QQuickTextEdit::q_contentsChange(int pos, int charsRemoved, int charsAdded) +{ + Q_D(QQuickTextEdit); + + const int editRange = pos + qMax(charsAdded, charsRemoved); + const int delta = charsAdded - charsRemoved; + + markDirtyNodesForRange(pos, editRange, delta); + + if (isComponentComplete()) { + d->updateType = QQuickTextEditPrivate::UpdatePaintNode; + update(); + } +} + void QQuickTextEdit::moveCursorDelegate() { Q_D(QQuickTextEdit); @@ -1871,9 +2026,21 @@ void QQuickTextEdit::moveCursorDelegate() d->cursorItem->setY(cursorRect.y()); } -void QQuickTextEdit::updateSelectionMarkers() +void QQuickTextEdit::updateSelection() { Q_D(QQuickTextEdit); + + // No need for node updates when we go from an empty selection to another empty selection + if (d->control->textCursor().hasSelection() || d->hadSelection) { + markDirtyNodesForRange(qMin(d->lastSelectionStart, d->control->textCursor().selectionStart()), qMax(d->control->textCursor().selectionEnd(), d->lastSelectionEnd), 0); + if (isComponentComplete()) { + d->updateType = QQuickTextEditPrivate::UpdatePaintNode; + update(); + } + } + + d->hadSelection = d->control->textCursor().hasSelection(); + if (d->lastSelectionStart != d->control->textCursor().selectionStart()) { d->lastSelectionStart = d->control->textCursor().selectionStart(); emit selectionStartChanged(); @@ -1938,70 +2105,70 @@ qreal QQuickTextEditPrivate::getImplicitWidth() const void QQuickTextEdit::updateSize() { Q_D(QQuickTextEdit); - if (isComponentComplete()) { - qreal naturalWidth = d->implicitWidth; - // ### assumes that if the width is set, the text will fill to edges - // ### (unless wrap is false, then clipping will occur) - if (widthValid()) { - if (!d->requireImplicitWidth) { - emit implicitWidthChanged(); - // if the implicitWidth is used, then updateSize() has already been called (recursively) - if (d->requireImplicitWidth) - return; - } - if (d->requireImplicitWidth) { - d->document->setTextWidth(-1); - naturalWidth = d->document->idealWidth(); - - const bool wasInLayout = d->inLayout; - d->inLayout = true; - setImplicitWidth(naturalWidth); - d->inLayout = wasInLayout; - if (d->inLayout) // probably the result of a binding loop, but by letting it - return; // get this far we'll get a warning to that effect. - } - if (d->document->textWidth() != width()) - d->document->setTextWidth(width()); - } else { + if (!isComponentComplete()) { + d->dirty = true; + return; + } + + qreal naturalWidth = d->implicitWidth; + + qreal newWidth = d->document->idealWidth(); + // ### assumes that if the width is set, the text will fill to edges + // ### (unless wrap is false, then clipping will occur) + if (widthValid()) { + if (!d->requireImplicitWidth) { + emit implicitWidthChanged(); + // if the implicitWidth is used, then updateSize() has already been called (recursively) + if (d->requireImplicitWidth) + return; + } + if (d->requireImplicitWidth) { d->document->setTextWidth(-1); + naturalWidth = d->document->idealWidth(); + + const bool wasInLayout = d->inLayout; + d->inLayout = true; + setImplicitWidth(naturalWidth); + d->inLayout = wasInLayout; + if (d->inLayout) // probably the result of a binding loop, but by letting it + return; // get this far we'll get a warning to that effect. } - + if (d->document->textWidth() != width()) + d->document->setTextWidth(width()); //### need to confirm cost of always setting these - qreal newWidth = d->document->idealWidth(); - if ((!widthValid() || d->wrapMode == NoWrap) && d->document->textWidth() != newWidth) - d->document->setTextWidth(newWidth); // ### Text does not align if width is not set or the idealWidth exceeds the textWidth (QTextDoc bug) - // ### Setting the implicitWidth triggers another updateSize(), and unless there are bindings nothing has changed. - qreal iWidth = -1; - if (!widthValid() && !d->requireImplicitWidth) - iWidth = newWidth; - - QFontMetricsF fm(d->font); - qreal newHeight = d->document->isEmpty() ? qCeil(fm.height()) : d->document->size().height(); - - if (iWidth > -1) - setImplicitSize(iWidth, newHeight); - else - setImplicitHeight(newHeight); + } else if (d->wrapMode == NoWrap && d->document->textWidth() != newWidth) { + d->document->setTextWidth(newWidth); // ### Text does not align if width is not set or the idealWidth exceeds the textWidth (QTextDoc bug) + } else { + d->document->setTextWidth(-1); + } - d->xoff = QQuickTextUtil::alignedX(d->document->size().width(), width(), effectiveHAlign()); - d->yoff = QQuickTextUtil::alignedY(d->document->size().height(), height(), d->vAlign); - setBaselineOffset(fm.ascent() + d->yoff + d->textMargin); + QFontMetricsF fm(d->font); + qreal newHeight = d->document->isEmpty() ? qCeil(fm.height()) : d->document->size().height(); - QSizeF size(newWidth, newHeight); - if (d->contentSize != size) { - d->contentSize = size; - emit contentSizeChanged(); - } - } else { - d->dirty = true; + // ### Setting the implicitWidth triggers another updateSize(), and unless there are bindings nothing has changed. + if (!widthValid() && !d->requireImplicitWidth) + setImplicitSize(newWidth, newHeight); + else + setImplicitHeight(newHeight); + + d->xoff = qMax(qreal(0), QQuickTextUtil::alignedX(d->document->size().width(), width(), effectiveHAlign())); + d->yoff = QQuickTextUtil::alignedY(d->document->size().height(), height(), d->vAlign); + setBaselineOffset(fm.ascent() + d->yoff + d->textMargin); + + QSizeF size(newWidth, newHeight); + if (d->contentSize != size) { + d->contentSize = size; + emit contentSizeChanged(); } - updateDocument(); } -void QQuickTextEdit::updateDocument() +void QQuickTextEdit::updateWholeDocument() { Q_D(QQuickTextEdit); - d->documentDirty = true; + if (!d->textNodeMap.isEmpty()) { + Q_FOREACH (TextNode* node, d->textNodeMap) + node->setDirty(); + } if (isComponentComplete()) { d->updateType = QQuickTextEditPrivate::UpdatePaintNode; @@ -2023,7 +2190,7 @@ void QQuickTextEdit::q_updateAlignment() Q_D(QQuickTextEdit); if (d->determineHorizontalAlignment()) { d->updateDefaultTextOption(); - d->xoff = QQuickTextUtil::alignedX(d->document->size().width(), width(), effectiveHAlign()); + d->xoff = qMax(qreal(0), QQuickTextUtil::alignedX(d->document->size().width(), width(), effectiveHAlign())); moveCursorDelegate(); } } diff --git a/src/quick/items/qquicktextedit_p.h b/src/quick/items/qquicktextedit_p.h index 8a2d9b1..4e09eaf 100644 --- a/src/quick/items/qquicktextedit_p.h +++ b/src/quick/items/qquicktextedit_p.h @@ -313,17 +313,19 @@ public Q_SLOTS: private Q_SLOTS: void q_textChanged(); - void updateSelectionMarkers(); + void q_contentsChange(int, int, int); + void updateSelection(); void moveCursorDelegate(); void createCursor(); void q_canPasteChanged(); - void updateDocument(); + void updateWholeDocument(); void updateCursor(); void q_updateAlignment(); void updateSize(); void triggerPreprocess(); private: + void markDirtyNodesForRange(int start, int end, int charDelta); void updateTotalLines(); protected: diff --git a/src/quick/items/qquicktextedit_p_p.h b/src/quick/items/qquicktextedit_p_p.h index dd0f76f..feb7e98 100644 --- a/src/quick/items/qquicktextedit_p_p.h +++ b/src/quick/items/qquicktextedit_p_p.h @@ -58,11 +58,14 @@ #include "qquicktextcontrol_p.h" #include +#include QT_BEGIN_NAMESPACE class QTextLayout; class QQuickTextDocumentWithImageResources; class QQuickTextControl; +class QQuickTextNode; +class QSGSimpleRectNode; class QQuickTextEditPrivate : public QQuickImplicitSizeItemPrivate { public: @@ -70,10 +73,26 @@ public: typedef QQuickTextEdit Public; + struct Node { + explicit Node(int startPos, QQuickTextNode* node) + : m_startPos(startPos), m_node(node), m_dirty(false) { } + QQuickTextNode* textNode() const { return m_node; } + void moveStartPos(int delta) { Q_ASSERT(m_startPos + delta > 0); m_startPos += delta; } + int startPos() const { return m_startPos; } + void setDirty() { m_dirty = true; } + bool dirty() const { return m_dirty; } + + private: + int m_startPos; + QQuickTextNode* m_node; + bool m_dirty; + }; + + QQuickTextEditPrivate() : color(QRgb(0xFF000000)), selectionColor(QRgb(0xFF000080)), selectedTextColor(QRgb(0xFFFFFFFF)) , textMargin(0.0), xoff(0), yoff(0), font(sourceFont), cursorComponent(0), cursorItem(0), document(0), control(0) - , quickDocument(0) + , quickDocument(0), frameDecorationsNode(0), cursorNode(0) , lastSelectionStart(0), lastSelectionEnd(0), lineCount(0) , hAlign(QQuickTextEdit::AlignLeft), vAlign(QQuickTextEdit::AlignTop) , format(QQuickTextEdit::PlainText), wrapMode(QQuickTextEdit::NoWrap) @@ -84,10 +103,11 @@ public: , inputMethodHints(Qt::ImhNone) #endif , updateType(UpdatePaintNode) - , documentDirty(true), dirty(false), richText(false), cursorVisible(false), cursorPending(false) + , dirty(false), richText(false), cursorVisible(false), cursorPending(false) , focusOnPress(true), persistentSelection(false), requireImplicitWidth(false) , selectByMouse(false), canPaste(false), canPasteValid(false), hAlignImplicit(true) , textCached(true), inLayout(false), selectByKeyboard(false), selectByKeyboardSet(false) + , hadSelection(false) { } @@ -131,6 +151,9 @@ public: QQuickTextDocumentWithImageResources *document; QQuickTextControl *control; QQuickTextDocument *quickDocument; + QList textNodeMap; + QQuickTextNode *frameDecorationsNode; + QSGSimpleRectNode *cursorNode; int lastSelectionStart; int lastSelectionEnd; @@ -154,7 +177,6 @@ public: #endif UpdateType updateType; - bool documentDirty : 1; bool dirty : 1; bool richText : 1; bool cursorVisible : 1; @@ -170,6 +192,7 @@ public: bool inLayout:1; bool selectByKeyboard:1; bool selectByKeyboardSet:1; + bool hadSelection : 1; }; QT_END_NAMESPACE diff --git a/src/quick/items/qquicktextnode.cpp b/src/quick/items/qquicktextnode.cpp index 5e1b20c..d469594 100644 --- a/src/quick/items/qquicktextnode.cpp +++ b/src/quick/items/qquicktextnode.cpp @@ -41,6 +41,8 @@ #include "qquicktextnode_p.h" +#include "qquicktextnodeengine_p.h" + #include #include #include @@ -53,20 +55,28 @@ #include #include #include -#include -#include -#include #include -#include #include #include -#include -#include + #include #include QT_BEGIN_NAMESPACE +namespace { + + class ProtectedLayoutAccessor: public QAbstractTextDocumentLayout + { + public: + inline QTextCharFormat formatAccessor(int pos) + { + return format(pos); + } + }; + +} + /*! Creates an empty QQuickTextNode */ @@ -166,937 +176,14 @@ void QQuickTextNode::setCursor(const QRectF &rect, const QColor &color) appendChildNode(m_cursorNode); } -namespace { - - struct BinaryTreeNode { - enum SelectionState { - Unselected, - Selected - }; - - BinaryTreeNode() - : selectionState(Unselected) - , clipNode(0) - , decorations(QQuickTextNode::NoDecoration) - , ascent(0.0) - , leftChildIndex(-1) - , rightChildIndex(-1) - { - - } - - BinaryTreeNode(const QRectF &brect, const QImage &i, SelectionState selState, qreal a) - : boundingRect(brect) - , selectionState(selState) - , clipNode(0) - , decorations(QQuickTextNode::NoDecoration) - , image(i) - , ascent(a) - , leftChildIndex(-1) - , rightChildIndex(-1) - { - } - - BinaryTreeNode(const QGlyphRun &g, SelectionState selState, const QRectF &brect, - const QQuickTextNode::Decorations &decs, const QColor &c, const QColor &bc, - const QPointF &pos, qreal a) - : glyphRun(g) - , boundingRect(brect) - , selectionState(selState) - , clipNode(0) - , decorations(decs) - , color(c) - , backgroundColor(bc) - , position(pos) - , ascent(a) - , leftChildIndex(-1) - , rightChildIndex(-1) - { - } - - QGlyphRun glyphRun; - QRectF boundingRect; - SelectionState selectionState; - QQuickDefaultClipNode *clipNode; - QQuickTextNode::Decorations decorations; - QColor color; - QColor backgroundColor; - QPointF position; - QImage image; - qreal ascent; - - int leftChildIndex; - int rightChildIndex; - - static void insert(QVarLengthArray *binaryTree, - const QRectF &rect, - const QImage &image, - qreal ascent, - SelectionState selectionState) - { - insert(binaryTree, BinaryTreeNode(rect, image, selectionState, ascent)); - } - - static void insert(QVarLengthArray *binaryTree, - const QGlyphRun &glyphRun, - SelectionState selectionState, - QQuickTextNode::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() ? QQuickTextNode::Underline : QQuickTextNode::NoDecoration); - decorations |= (glyphRun.overline() ? QQuickTextNode::Overline : QQuickTextNode::NoDecoration); - decorations |= (glyphRun.strikeOut() ? QQuickTextNode::StrikeOut : QQuickTextNode::NoDecoration); - decorations |= (backgroundColor.isValid() ? QQuickTextNode::Background : QQuickTextNode::NoDecoration); - - qreal ascent = glyphRun.rawFont().ascent(); - insert(binaryTree, BinaryTreeNode(glyphRun, selectionState, searchRect, decorations, - textColor, backgroundColor, position, ascent)); - } - - static void insert(QVarLengthArray *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; - } - } - } - } - - static void inOrder(const QVarLengthArray &binaryTree, - QVarLengthArray *sortedIndexes, - int currentIndex = 0) - { - 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); - } - }; - - // Engine that takes glyph runs as input, and produces a set of glyph nodes, clip nodes, - // and rectangle nodes to represent the text, decorations and selection. Will try to minimize - // number of nodes, and join decorations in neighbouring items - class SelectionEngine - { - public: - SelectionEngine() : m_hasSelection(false) {} - - QTextLine currentLine() const { return m_currentLine; } - - void setCurrentLine(const QTextLine ¤tLine) - { - if (m_currentLine.isValid()) - processCurrentLine(); - - m_currentLine = currentLine; - } - - void addBorder(const QRectF &rect, qreal border, QTextFrameFormat::BorderStyle borderStyle, - const QBrush &borderBrush); - void addFrameDecorations(QTextDocument *document, QTextFrame *frame); - void addImage(const QRectF &rect, const QImage &image, qreal ascent, - BinaryTreeNode::SelectionState selectionState, - QTextFrameFormat::Position layoutPosition); - int addText(const QTextBlock &block, - const QTextCharFormat &charFormat, - const QColor &textColor, - const QVarLengthArray &colorChanges, - int textPos, int fragmentEnd, - int selectionStart, int selectionEnd); - void addTextObject(const QPointF &position, const QTextCharFormat &format, - BinaryTreeNode::SelectionState selectionState, - QTextDocument *textDocument, int pos, - QTextFrameFormat::Position layoutPosition = QTextFrameFormat::InFlow); - void addSelectedGlyphs(const QGlyphRun &glyphRun); - void addUnselectedGlyphs(const QGlyphRun &glyphRun); - void addGlyphsInRange(int rangeStart, int rangeEnd, - const QColor &color, const QColor &backgroundColor, - int selectionStart, int selectionEnd); - void addGlyphsForRanges(const QVarLengthArray &ranges, - int start, int end, - int selectionStart, int selectionEnd); - - void addToSceneGraph(QQuickTextNode *parent, - QQuickText::TextStyle style = QQuickText::Normal, - const QColor &styleColor = QColor()); - - void setSelectionColor(const QColor &selectionColor) - { - m_selectionColor = selectionColor; - } - - void setSelectedTextColor(const QColor &selectedTextColor) - { - m_selectedTextColor = selectedTextColor; - } - - void setTextColor(const QColor &textColor) - { - m_textColor = textColor; - } - - void setAnchorColor(const QColor &anchorColor) - { - m_anchorColor = anchorColor; - } - - void setPosition(const QPointF &position) - { - m_position = position; - } - - private: - struct TextDecoration - { - TextDecoration() : selectionState(BinaryTreeNode::Unselected) {} - TextDecoration(const BinaryTreeNode::SelectionState &s, - const QRectF &r, - const QColor &c) - : selectionState(s) - , rect(r) - , color(c) - { - } - - BinaryTreeNode::SelectionState selectionState; - QRectF rect; - QColor color; - }; - - void processCurrentLine(); - void addTextDecorations(const QVarLengthArray &textDecorations, - qreal offset, qreal thickness); - - QColor m_selectionColor; - QColor m_textColor; - QColor m_backgroundColor; - QColor m_selectedTextColor; - QColor m_anchorColor; - QPointF m_position; - - QTextLine m_currentLine; - bool m_hasSelection; - - QList > m_backgrounds; - QList m_selectionRects; - QVarLengthArray m_currentLineTree; - - QList m_lines; - QVector m_processedNodes; - - QList > m_images; - }; - - int SelectionEngine::addText(const QTextBlock &block, - const QTextCharFormat &charFormat, - const QColor &textColor, - const QVarLengthArray &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 SelectionEngine::addTextDecorations(const QVarLengthArray &textDecorations, - qreal offset, qreal thickness) - { - for (int i=0; i sortedIndexes; // Indexes in tree sorted by x position - BinaryTreeNode::inOrder(m_currentLineTree, &sortedIndexes); - - Q_ASSERT(sortedIndexes.size() == m_currentLineTree.size()); - - BinaryTreeNode::SelectionState currentSelectionState = BinaryTreeNode::Unselected; - QRectF currentRect; - - QQuickTextNode::Decorations currentDecorations = QQuickTextNode::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 pendingUnderlines; - QVarLengthArray pendingOverlines; - QVarLengthArray pendingStrikeOuts; - if (!sortedIndexes.isEmpty()) { - QQuickDefaultClipNode *currentClipNode = m_hasSelection ? new QQuickDefaultClipNode(QRectF()) : 0; - bool currentClipNodeUsed = false; - for (int i=0; i<=sortedIndexes.size(); ++i) { - BinaryTreeNode *node = 0; - 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 != QQuickTextNode::NoDecoration) { - decorationRect.setY(m_position.y() + m_currentLine.y()); - decorationRect.setHeight(m_currentLine.height()); - - if (node != 0) - decorationRect.setRight(node->boundingRect.left()); - - TextDecoration textDecoration(currentSelectionState, decorationRect, lastColor); - if (currentDecorations & QQuickTextNode::Underline) - pendingUnderlines.append(textDecoration); - - if (currentDecorations & QQuickTextNode::Overline) - pendingOverlines.append(textDecoration); - - if (currentDecorations & QQuickTextNode::StrikeOut) - pendingStrikeOuts.append(textDecoration); - - if (currentDecorations & QQuickTextNode::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 == 0 || node->selectionState != currentSelectionState) { - if (node != 0) - currentRect.setRight(node->boundingRect.left()); - currentRect.setY(m_position.y() + m_currentLine.y()); - currentRect.setHeight(m_currentLine.height()); - - // Draw selection all the way up to the left edge of the unselected item - if (currentSelectionState == BinaryTreeNode::Selected) - m_selectionRects.append(currentRect); - - if (currentClipNode != 0) { - if (!currentClipNodeUsed) { - delete currentClipNode; - } else { - currentClipNode->setIsRectangular(true); - currentClipNode->setRect(currentRect); - currentClipNode->update(); - } - } - - if (node != 0 && m_hasSelection) - currentClipNode = new QQuickDefaultClipNode(QRectF()); - else - currentClipNode = 0; - currentClipNodeUsed = false; - - if (node != 0) { - 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 != 0) { - 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 & QQuickTextNode::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 & QQuickTextNode::Underline) { - if (rawFont.lineThickness() > underlineThickness) { - underlineThickness = rawFont.lineThickness(); - underlineOffset = rawFont.underlinePosition(); - } - } - - if (node->decorations & QQuickTextNode::Overline) { - overlineOffset = -rawFont.ascent(); - overlineThickness = rawFont.lineThickness(); - } - - if (node->decorations & QQuickTextNode::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 SelectionEngine::addImage(const QRectF &rect, const QImage &image, qreal ascent, - BinaryTreeNode::SelectionState selectionState, - QTextFrameFormat::Position layoutPosition) - { - QRectF searchRect = rect; - if (layoutPosition == QTextFrameFormat::InFlow) { - if (m_currentLineTree.isEmpty()) { - searchRect.moveTopLeft(m_position + m_currentLine.position()); - } 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); - } - - void SelectionEngine::addTextObject(const QPointF &position, const QTextCharFormat &format, - BinaryTreeNode::SelectionState selectionState, - QTextDocument *textDocument, int pos, - QTextFrameFormat::Position layoutPosition) - { - QTextObjectInterface *handler = textDocument->documentLayout()->handlerForObject(format.objectType()); - if (handler != 0) { - QImage image; - QSizeF size = handler->intrinsicSize(textDocument, pos, format); - - if (format.objectType() == QTextFormat::ImageObject) { - QTextImageFormat imageFormat = format.toImageFormat(); - if (QQuickTextDocumentWithImageResources *imageDoc = qobject_cast(textDocument)) { - image = imageDoc->image(imageFormat); - - if (image.isNull()) - return; - } else { - QTextImageHandler *imageHandler = static_cast(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; - QFontMetrics m(format.font()); - switch (format.verticalAlignment()) - { - case QTextCharFormat::AlignMiddle: - ascent = size.height() / 2 - 1; - break; - case QTextCharFormat::AlignBaseline: - ascent = size.height() - m.descent() - 1; - break; - default: - ascent = size.height() - 1; - } - - addImage(QRectF(position, size), image, ascent, selectionState, layoutPosition); - } - } - - void SelectionEngine::addUnselectedGlyphs(const QGlyphRun &glyphRun) - { - BinaryTreeNode::insert(&m_currentLineTree, glyphRun, BinaryTreeNode::Unselected, - QQuickTextNode::NoDecoration, m_textColor, m_backgroundColor, m_position); - } - - void SelectionEngine::addSelectedGlyphs(const QGlyphRun &glyphRun) - { - int currentSize = m_currentLineTree.size(); - BinaryTreeNode::insert(&m_currentLineTree, glyphRun, BinaryTreeNode::Selected, - QQuickTextNode::NoDecoration, m_textColor, m_backgroundColor, m_position); - m_hasSelection = m_hasSelection || m_currentLineTree.size() > currentSize; - } - - void SelectionEngine::addGlyphsForRanges(const QVarLengthArray &ranges, - int start, int end, - int selectionStart, int selectionEnd) - { - int currentPosition = start; - int remainingLength = end - start; - for (int j=0; j= 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 SelectionEngine::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 glyphRuns = line.glyphRuns(rangeStart, rangeLength); - for (int j=0; j glyphRuns = line.glyphRuns(rangeStart, - qMin(selectionStart - rangeStart, - rangeLength)); - - for (int j=0; j selectionStart) { - int start = qMax(selectionStart, rangeStart); - int length = qMin(selectionEnd - start + 1, rangeEnd - start); - QList glyphRuns = line.glyphRuns(start, length); - - for (int j=0; j= rangeStart && selectionEnd < rangeEnd) { - QList glyphRuns = line.glyphRuns(selectionEnd + 1, rangeEnd - selectionEnd - 1); - for (int j=0; j(document->documentLayout()); - QTextFrameFormat frameFormat = frame->format().toFrameFormat(); - - QTextTable *table = qobject_cast(frame); - QRectF boundingRect = table == 0 - ? 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 != 0) { - int rows = table->rows(); - int columns = table->columns(); - - for (int row=0; rowcellAt(row, column); - - QRectF cellRect = documentLayout->tableCellBoundingRect(table, cell); - addBorder(cellRect.adjusted(-borderWidth, -borderWidth, 0, 0), borderWidth, - borderStyle, borderBrush); - } - } - } - } - - void SelectionEngine::addToSceneGraph(QQuickTextNode *parentNode, - QQuickText::TextStyle style, - const QColor &styleColor) - { - if (m_currentLine.isValid()) - processCurrentLine(); - - - for (int i=0; iappendChildNode(new QSGSimpleRectNode(rect, color)); - } - - // First, prepend all selection rectangles to the tree - for (int i=0; iappendChildNode(new QSGSimpleRectNode(rect, m_selectionColor)); - } - - // Finally, add decorations for each node to the tree. - for (int i=0; iappendChildNode(new QSGSimpleRectNode(textDecoration.rect, color)); - } - - // Then, go through all the nodes for all lines and combine all QGlyphRuns with a common - // font, selection state and clip node. - typedef QPair > > KeyType; - QHash map; - QList nodes; - for (int i=0; iimage.isNull()) { - QGlyphRun glyphRun = node->glyphRun; - QRawFont rawFont = glyphRun.rawFont(); - QRawFontPrivate *rawFontD = QRawFontPrivate::get(rawFont); - - QFontEngine *fontEngine = rawFontD->fontEngine; - - KeyType key(qMakePair(fontEngine, - qMakePair(node->clipNode, - qMakePair(node->color.rgba(), int(node->selectionState))))); - - BinaryTreeNode *otherNode = map.value(key, 0); - if (otherNode != 0) { - QGlyphRun &otherGlyphRun = otherNode->glyphRun; - - QVector otherGlyphIndexes = otherGlyphRun.glyphIndexes(); - QVector otherGlyphPositions = otherGlyphRun.positions(); - - otherGlyphIndexes += glyphRun.glyphIndexes(); - - QVector glyphPositions = glyphRun.positions(); - for (int j=0; jposition - otherNode->position); - } - - otherGlyphRun.setGlyphIndexes(otherGlyphIndexes); - otherGlyphRun.setPositions(otherGlyphPositions); - - } else { - map.insert(key, node); - nodes.append(node); - } - } else { - parentNode->addImage(node->boundingRect, node->image); - if (node->selectionState == BinaryTreeNode::Selected) { - QColor color = m_selectionColor; - color.setAlpha(128); - parentNode->appendChildNode(new QSGSimpleRectNode(node->boundingRect, color)); - } - } - } - - // ...and add clip nodes and glyphs to tree. - foreach (const BinaryTreeNode *node, nodes) { - - QQuickDefaultClipNode *clipNode = node->clipNode; - if (clipNode != 0 && clipNode->parent() == 0 ) - parentNode->appendChildNode(clipNode); - - QColor color = node->selectionState == BinaryTreeNode::Selected - ? m_selectedTextColor - : node->color; - - parentNode->addGlyphs(node->position, node->glyphRun, color, style, styleColor, clipNode); - } - } -} - -void QQuickTextNode::mergeFormats(QTextLayout *textLayout, - QVarLengthArray *mergedFormats) +void QQuickTextNode::initEngine(const QColor& textColor, const QColor& selectedTextColor, const QColor& selectionColor, const QColor& anchorColor) { - Q_ASSERT(mergedFormats != 0); - if (textLayout == 0) - return; - - QList additionalFormats = textLayout->additionalFormats(); - for (int i=0; iisEmpty()) { - QTextLayout::FormatRange *lastFormat = mergedFormats->data() + mergedFormats->size() - 1; - - if (additionalFormat.start < lastFormat->start + lastFormat->length) { - QTextLayout::FormatRange *mergedRange = 0; - - 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); - } - } - -} - -namespace { - - class ProtectedLayoutAccessor: public QAbstractTextDocumentLayout - { - public: - inline QTextCharFormat formatAccessor(int pos) - { - return format(pos); - } - }; - + m_engine.reset(new QQuickTextNodeEngine); + m_engine->m_hasContents = false; + m_engine->setTextColor(textColor); + m_engine->setSelectedTextColor(selectedTextColor); + m_engine->setSelectionColor(selectionColor); + m_engine->setAnchorColor(anchorColor); } void QQuickTextNode::addImage(const QRectF &rect, const QImage &image) @@ -1118,11 +205,7 @@ void QQuickTextNode::addTextDocument(const QPointF &position, QTextDocument *tex const QColor &selectionColor, const QColor &selectedTextColor, int selectionStart, int selectionEnd) { - SelectionEngine engine; - engine.setTextColor(textColor); - engine.setSelectedTextColor(selectedTextColor); - engine.setSelectionColor(selectionColor); - engine.setAnchorColor(anchorColor); + initEngine(textColor, selectedTextColor, selectionColor, anchorColor); QList frames; frames.append(textDocument->rootFrame()); @@ -1130,7 +213,7 @@ void QQuickTextNode::addTextDocument(const QPointF &position, QTextDocument *tex QTextFrame *textFrame = frames.takeFirst(); frames.append(textFrame->childFrames()); - engine.addFrameDecorations(textDocument, textFrame); + m_engine->addFrameDecorations(textDocument, textFrame); if (textFrame->firstPosition() > textFrame->lastPosition() && textFrame->frameFormat().position() != QTextFrameFormat::InFlow) { @@ -1140,160 +223,23 @@ void QQuickTextNode::addTextDocument(const QPointF &position, QTextDocument *tex QRectF rect = a->frameBoundingRect(textFrame); QTextBlock block = textFrame->firstCursorPosition().block(); - engine.setCurrentLine(block.layout()->lineForTextPosition(pos - block.position())); - engine.addTextObject(rect.topLeft(), format, BinaryTreeNode::Unselected, textDocument, + m_engine->setCurrentLine(block.layout()->lineForTextPosition(pos - block.position())); + m_engine->addTextObject(rect.topLeft(), format, QQuickTextNodeEngine::Unselected, textDocument, pos, textFrame->frameFormat().position()); } else { QTextFrame::iterator it = textFrame->begin(); while (!it.atEnd()) { - Q_ASSERT(!engine.currentLine().isValid()); + Q_ASSERT(!m_engine->currentLine().isValid()); QTextBlock block = it.currentBlock(); -#ifndef QT_NO_IM - int preeditLength = block.isValid() ? block.layout()->preeditAreaText().length() : 0; - int preeditPosition = block.isValid() ? block.layout()->preeditAreaPosition() : -1; -#endif - - QVarLengthArray colorChanges; - mergeFormats(block.layout(), &colorChanges); - - QPointF blockPosition = textDocument->documentLayout()->blockBoundingRect(block).topLeft() + position; - if (QTextList *textList = block.textList()) { - QPointF pos = blockPosition; - QTextLayout *layout = block.layout(); - if (layout->lineCount() > 0) { - QTextLine firstLine = layout->lineAt(0); - Q_ASSERT(firstLine.isValid()); - - engine.setCurrentLine(firstLine); - - QRectF textRect = firstLine.naturalTextRect(); - pos += textRect.topLeft(); - if (block.textDirection() == Qt::RightToLeft) - pos.rx() += textRect.width(); - - const QTextCharFormat charFormat = block.charFormat(); - 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; - }; - - QSizeF size(fontMetrics.width(listItemBullet), fontMetrics.height()); - qreal xoff = fontMetrics.width(QLatin1Char(' ')); - if (block.textDirection() == Qt::LeftToRight) - xoff = -xoff - size.width(); - engine.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 glyphRuns = layout.glyphRuns(); - for (int i=0; i(textDocument->objectForFormat(charFormat)); - if (frame && frame->frameFormat().position() == QTextFrameFormat::InFlow) { - int blockRelativePosition = textPos - block.position(); - QTextLine line = block.layout()->lineForTextPosition(blockRelativePosition); - if (!engine.currentLine().isValid() - || line.lineNumber() != engine.currentLine().lineNumber()) { - engine.setCurrentLine(line); - } - - BinaryTreeNode::SelectionState selectionState = - (selectionStart < textPos + text.length() - && selectionEnd >= textPos) - ? BinaryTreeNode::Selected - : BinaryTreeNode::Unselected; - - engine.addTextObject(QPointF(), charFormat, selectionState, textDocument, textPos); - } - textPos += text.length(); - } else { - if (charFormat.foreground().style() != Qt::NoBrush) - engine.setTextColor(charFormat.foreground().color()); - else if (charFormat.isAnchor()) - engine.setTextColor(anchorColor); - else - engine.setTextColor(textColor); - - int fragmentEnd = textPos + fragment.length(); -#ifndef QT_NO_IM - if (preeditPosition >= 0 - && preeditPosition >= textPos - && preeditPosition <= fragmentEnd) { - fragmentEnd += preeditLength; - } -#endif - - textPos = engine.addText(block, charFormat, textColor, colorChanges, textPos, fragmentEnd, - selectionStart, selectionEnd); - } - - ++blockIterator; - } - -#ifndef QT_NO_IM - if (preeditLength >= 0 && textPos <= block.position() + preeditPosition) { - engine.setPosition(blockPosition); - textPos = block.position() + preeditPosition; - QTextLine line = block.layout()->lineForTextPosition(preeditPosition); - if (!engine.currentLine().isValid() - || line.lineNumber() != engine.currentLine().lineNumber()) { - engine.setCurrentLine(line); - } - textPos = engine.addText(block, block.charFormat(), textColor, colorChanges, - textPos, textPos + preeditLength, - selectionStart, selectionEnd); - } -#endif - - engine.setCurrentLine(QTextLine()); // Reset current line because the text layout changed + m_engine->addTextBlock(textDocument, block, position, textColor, anchorColor, selectionStart, selectionEnd); ++it; } } } - engine.addToSceneGraph(this, style, styleColor); + m_engine->addToSceneGraph(this, style, styleColor); } void QQuickTextNode::addTextLayout(const QPointF &position, QTextLayout *textLayout, const QColor &color, @@ -1303,12 +249,7 @@ void QQuickTextNode::addTextLayout(const QPointF &position, QTextLayout *textLay int selectionStart, int selectionEnd, int lineStart, int lineCount) { - SelectionEngine engine; - engine.setTextColor(color); - engine.setSelectedTextColor(selectedTextColor); - engine.setSelectionColor(selectionColor); - engine.setAnchorColor(anchorColor); - engine.setPosition(position); + initEngine(color, selectedTextColor, selectionColor, anchorColor); #ifndef QT_NO_IM int preeditLength = textLayout->preeditAreaText().length(); @@ -1316,7 +257,7 @@ void QQuickTextNode::addTextLayout(const QPointF &position, QTextLayout *textLay #endif QVarLengthArray colorChanges; - mergeFormats(textLayout, &colorChanges); + m_engine->mergeFormats(textLayout, &colorChanges); lineCount = lineCount >= 0 ? qMin(lineStart + lineCount, textLayout->lineCount()) @@ -1337,11 +278,11 @@ void QQuickTextNode::addTextLayout(const QPointF &position, QTextLayout *textLay } #endif - engine.setCurrentLine(line); - engine.addGlyphsForRanges(colorChanges, start, end, selectionStart, selectionEnd); + m_engine->setCurrentLine(line); + m_engine->addGlyphsForRanges(colorChanges, start, end, selectionStart, selectionEnd); } - engine.addToSceneGraph(this, style, styleColor); + m_engine->addToSceneGraph(this, style, styleColor); } void QQuickTextNode::deleteContent() diff --git a/src/quick/items/qquicktextnode_p.h b/src/quick/items/qquicktextnode_p.h index dcc4ebe..16da3ce 100644 --- a/src/quick/items/qquicktextnode_p.h +++ b/src/quick/items/qquicktextnode_p.h @@ -49,6 +49,7 @@ #include #include #include +#include QT_BEGIN_NAMESPACE @@ -62,6 +63,8 @@ class QSGSimpleRectNode; class QSGClipNode; class QSGTexture; +class QQuickTextNodeEngine; + class QQuickTextNode : public QSGTransformNode { public: @@ -104,13 +107,17 @@ public: void setUseNativeRenderer(bool on) { m_useNativeRenderer = on; } private: - void mergeFormats(QTextLayout *textLayout, QVarLengthArray *mergedFormats); + void initEngine(const QColor &textColor, const QColor &selectedTextColor, const QColor &selectionColor, const QColor& anchorColor = QColor()); + QSGContext *m_context; QSGSimpleRectNode *m_cursorNode; QList m_textures; QQuickItem *m_ownerElement; bool m_useNativeRenderer; + QScopedPointer m_engine; + + friend class QQuickTextEdit; }; QT_END_NAMESPACE diff --git a/src/quick/items/qquicktextnodeengine.cpp b/src/quick/items/qquicktextnodeengine.cpp new file mode 100644 index 0000000..7bbfd1b --- /dev/null +++ b/src/quick/items/qquicktextnodeengine.cpp @@ -0,0 +1,935 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtQml 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qquicktextnodeengine_p.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +void QQuickTextNodeEngine::BinaryTreeNode::insert(QVarLengthArray *binaryTree, const QGlyphRun &glyphRun, SelectionState selectionState, + QQuickTextNode::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() ? QQuickTextNode::Underline : QQuickTextNode::NoDecoration); + decorations |= (glyphRun.overline() ? QQuickTextNode::Overline : QQuickTextNode::NoDecoration); + decorations |= (glyphRun.strikeOut() ? QQuickTextNode::StrikeOut : QQuickTextNode::NoDecoration); + decorations |= (backgroundColor.isValid() ? QQuickTextNode::Background : QQuickTextNode::NoDecoration); + + qreal ascent = glyphRun.rawFont().ascent(); + insert(binaryTree, BinaryTreeNode(glyphRun, selectionState, searchRect, decorations, + textColor, backgroundColor, position, ascent)); +} + +void QQuickTextNodeEngine::BinaryTreeNode::insert(QVarLengthArray *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 &binaryTree, + QVarLengthArray *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 &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 &textDecorations, + qreal offset, qreal thickness) +{ + for (int i=0; i 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; + + QQuickTextNode::Decorations currentDecorations = QQuickTextNode::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 pendingUnderlines; + QVarLengthArray pendingOverlines; + QVarLengthArray pendingStrikeOuts; + if (!sortedIndexes.isEmpty()) { + QQuickDefaultClipNode *currentClipNode = m_hasSelection ? new QQuickDefaultClipNode(QRectF()) : 0; + bool currentClipNodeUsed = false; + for (int i=0; i<=sortedIndexes.size(); ++i) { + BinaryTreeNode *node = 0; + 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 != QQuickTextNode::NoDecoration) { + decorationRect.setY(m_position.y() + m_currentLine.y()); + decorationRect.setHeight(m_currentLine.height()); + + if (node != 0) + decorationRect.setRight(node->boundingRect.left()); + + TextDecoration textDecoration(currentSelectionState, decorationRect, lastColor); + if (currentDecorations & QQuickTextNode::Underline) + pendingUnderlines.append(textDecoration); + + if (currentDecorations & QQuickTextNode::Overline) + pendingOverlines.append(textDecoration); + + if (currentDecorations & QQuickTextNode::StrikeOut) + pendingStrikeOuts.append(textDecoration); + + if (currentDecorations & QQuickTextNode::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 == 0 || node->selectionState != currentSelectionState) { + if (node != 0) + currentRect.setRight(node->boundingRect.left()); + currentRect.setY(m_position.y() + m_currentLine.y()); + currentRect.setHeight(m_currentLine.height()); + + // Draw selection all the way up to the left edge of the unselected item + if (currentSelectionState == Selected) + m_selectionRects.append(currentRect); + + if (currentClipNode != 0) { + if (!currentClipNodeUsed) { + delete currentClipNode; + } else { + currentClipNode->setIsRectangular(true); + currentClipNode->setRect(currentRect); + currentClipNode->update(); + } + } + + if (node != 0 && m_hasSelection) + currentClipNode = new QQuickDefaultClipNode(QRectF()); + else + currentClipNode = 0; + currentClipNodeUsed = false; + + if (node != 0) { + 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 != 0) { + 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 & QQuickTextNode::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 & QQuickTextNode::Underline) { + if (rawFont.lineThickness() > underlineThickness) { + underlineThickness = rawFont.lineThickness(); + underlineOffset = rawFont.underlinePosition(); + } + } + + if (node->decorations & QQuickTextNode::Overline) { + overlineOffset = -rawFont.ascent(); + overlineThickness = rawFont.lineThickness(); + } + + if (node->decorations & QQuickTextNode::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()) { + searchRect.moveTopLeft(m_position + m_currentLine.position()); + } 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 QPointF &position, const QTextCharFormat &format, + SelectionState selectionState, + QTextDocument *textDocument, int pos, + QTextFrameFormat::Position layoutPosition) +{ + QTextObjectInterface *handler = textDocument->documentLayout()->handlerForObject(format.objectType()); + if (handler != 0) { + QImage image; + QSizeF size = handler->intrinsicSize(textDocument, pos, format); + + if (format.objectType() == QTextFormat::ImageObject) { + QTextImageFormat imageFormat = format.toImageFormat(); + if (QQuickTextDocumentWithImageResources *imageDoc = qobject_cast(textDocument)) { + image = imageDoc->image(imageFormat); + + if (image.isNull()) + return; + } else { + QTextImageHandler *imageHandler = static_cast(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; + QFontMetrics m(format.font()); + switch (format.verticalAlignment()) + { + case QTextCharFormat::AlignMiddle: + ascent = size.height() / 2 - 1; + break; + case QTextCharFormat::AlignBaseline: + ascent = size.height() - m.descent() - 1; + break; + default: + ascent = size.height() - 1; + } + + addImage(QRectF(position, size), image, ascent, selectionState, layoutPosition); + } +} + +void QQuickTextNodeEngine::addUnselectedGlyphs(const QGlyphRun &glyphRun) +{ + BinaryTreeNode::insert(&m_currentLineTree, glyphRun, Unselected, + QQuickTextNode::NoDecoration, m_textColor, m_backgroundColor, m_position); +} + +void QQuickTextNodeEngine::addSelectedGlyphs(const QGlyphRun &glyphRun) +{ + int currentSize = m_currentLineTree.size(); + BinaryTreeNode::insert(&m_currentLineTree, glyphRun, Selected, + QQuickTextNode::NoDecoration, m_textColor, m_backgroundColor, m_position); + m_hasSelection = m_hasSelection || m_currentLineTree.size() > currentSize; +} + +void QQuickTextNodeEngine::addGlyphsForRanges(const QVarLengthArray &ranges, + int start, int end, + int selectionStart, int selectionEnd) +{ + int currentPosition = start; + int remainingLength = end - start; + for (int j=0; j= 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 glyphRuns = line.glyphRuns(rangeStart, rangeLength); + for (int j=0; j glyphRuns = line.glyphRuns(rangeStart, + qMin(selectionStart - rangeStart, + rangeLength)); + + for (int j=0; j selectionStart) { + int start = qMax(selectionStart, rangeStart); + int length = qMin(selectionEnd - start + 1, rangeEnd - start); + QList glyphRuns = line.glyphRuns(start, length); + + for (int j=0; j= rangeStart && selectionEnd < rangeEnd) { + QList glyphRuns = line.glyphRuns(selectionEnd + 1, rangeEnd - selectionEnd - 1); + for (int j=0; j(document->documentLayout()); + QTextFrameFormat frameFormat = frame->format().toFrameFormat(); + + QTextTable *table = qobject_cast(frame); + QRectF boundingRect = table == 0 + ? 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 != 0) { + int rows = table->rows(); + int columns = table->columns(); + + for (int row=0; rowcellAt(row, column); + + QRectF cellRect = documentLayout->tableCellBoundingRect(table, cell); + addBorder(cellRect.adjusted(-borderWidth, -borderWidth, 0, 0), borderWidth, + borderStyle, borderBrush); + } + } + } +} + +void QQuickTextNodeEngine::addToSceneGraph(QQuickTextNode *parentNode, + QQuickText::TextStyle style, + const QColor &styleColor) +{ + if (m_currentLine.isValid()) + processCurrentLine(); + + + for (int i=0; iappendChildNode(new QSGSimpleRectNode(rect, color)); + } + + // First, prepend all selection rectangles to the tree + for (int i=0; iappendChildNode(new QSGSimpleRectNode(rect, m_selectionColor)); + } + + // Finally, add decorations for each node to the tree. + for (int i=0; iappendChildNode(new QSGSimpleRectNode(textDecoration.rect, color)); + } + + // Then, go through all the nodes for all lines and combine all QGlyphRuns with a common + // font, selection state and clip node. + typedef QPair > > KeyType; + QHash map; + QList nodes; + for (int i = 0; i < m_processedNodes.size(); ++i) { + BinaryTreeNode *node = m_processedNodes.data() + i; + + if (node->image.isNull()) { + QGlyphRun glyphRun = node->glyphRun; + QRawFont rawFont = glyphRun.rawFont(); + QRawFontPrivate *rawFontD = QRawFontPrivate::get(rawFont); + + QFontEngine *fontEngine = rawFontD->fontEngine; + + KeyType key(qMakePair(fontEngine, + qMakePair(node->clipNode, + qMakePair(node->color.rgba(), int(node->selectionState))))); + + BinaryTreeNode *otherNode = map.value(key, 0); + if (otherNode != 0) { + QGlyphRun &otherGlyphRun = otherNode->glyphRun; + + QVector otherGlyphIndexes = otherGlyphRun.glyphIndexes(); + QVector otherGlyphPositions = otherGlyphRun.positions(); + + otherGlyphIndexes += glyphRun.glyphIndexes(); + + QVector glyphPositions = glyphRun.positions(); + otherGlyphPositions.reserve(otherGlyphPositions.size() + glyphPositions.size()); + for (int j = 0; j < glyphPositions.size(); ++j) { + otherGlyphPositions += glyphPositions.at(j) + (node->position - otherNode->position); + } + + otherGlyphRun.setGlyphIndexes(otherGlyphIndexes); + otherGlyphRun.setPositions(otherGlyphPositions); + + } else { + map.insert(key, node); + nodes.append(node); + } + } else { + parentNode->addImage(node->boundingRect, node->image); + if (node->selectionState == Selected) { + QColor color = m_selectionColor; + color.setAlpha(128); + parentNode->appendChildNode(new QSGSimpleRectNode(node->boundingRect, color)); + } + } + } + + foreach (const BinaryTreeNode *node, nodes) { + + QQuickDefaultClipNode *clipNode = node->clipNode; + if (clipNode != 0 && clipNode->parent() == 0 ) + parentNode->appendChildNode(clipNode); + + QColor color = node->selectionState == Selected + ? m_selectedTextColor + : node->color; + + parentNode->addGlyphs(node->position, node->glyphRun, color, style, styleColor, clipNode); + } +} + +void QQuickTextNodeEngine::mergeFormats(QTextLayout *textLayout, QVarLengthArray *mergedFormats) +{ + Q_ASSERT(mergedFormats != 0); + if (textLayout == 0) + return; + + QList additionalFormats = textLayout->additionalFormats(); + for (int i=0; iisEmpty()) { + QTextLayout::FormatRange *lastFormat = mergedFormats->data() + mergedFormats->size() - 1; + + if (additionalFormat.start < lastFormat->start + lastFormat->length) { + QTextLayout::FormatRange *mergedRange = 0; + + 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); +#ifndef QT_NO_IM + int preeditLength = block.isValid() ? block.layout()->preeditAreaText().length() : 0; + int preeditPosition = block.isValid() ? block.layout()->preeditAreaPosition() : -1; +#endif + + QVarLengthArray colorChanges; + mergeFormats(block.layout(), &colorChanges); + + QPointF blockPosition = textDocument->documentLayout()->blockBoundingRect(block).topLeft() + position; + if (QTextList *textList = block.textList()) { + QPointF pos = blockPosition; + 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(); + + const QTextCharFormat charFormat = block.charFormat(); + 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; + }; + + QSizeF size(fontMetrics.width(listItemBullet), fontMetrics.height()); + qreal xoff = fontMetrics.width(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 glyphRuns = layout.glyphRuns(); + for (int i=0; i(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(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(); +#ifndef QT_NO_IM + if (preeditPosition >= 0 + && preeditPosition >= textPos + && preeditPosition <= fragmentEnd) { + fragmentEnd += preeditLength; + } +#endif + + textPos = addText(block, charFormat, textColor, colorChanges, textPos, fragmentEnd, + selectionStart, selectionEnd); + } + + ++blockIterator; + } + +#ifndef QT_NO_IM + if (preeditLength >= 0 && textPos <= block.position() + preeditPosition) { + setPosition(blockPosition); + 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 + diff --git a/src/quick/items/qquicktextnodeengine_p.h b/src/quick/items/qquicktextnodeengine_p.h new file mode 100644 index 0000000..6a98d6b --- /dev/null +++ b/src/quick/items/qquicktextnodeengine_p.h @@ -0,0 +1,234 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtQml 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include "qquickclipnode_p.h" +#include "qquicktextnode_p.h" + +#ifndef QQUICKTEXTNODEENGINE_P_H +#define QQUICKTEXTNODEENGINE_P_H + +QT_BEGIN_NAMESPACE + +// Engine that takes glyph runs as input, and produces a set of glyph nodes, clip nodes, +// and rectangle nodes to represent the text, decorations and selection. Will try to minimize +// number of nodes, and join decorations in neighbouring items + +class QQuickTextNodeEngine { + +public: + + enum SelectionState { + Unselected, + Selected + }; + + struct BinaryTreeNode { + + BinaryTreeNode() + : selectionState(Unselected), clipNode(0), decorations(QQuickTextNode::NoDecoration) + , ascent(0.0), leftChildIndex(-1), rightChildIndex(-1) + { + } + + BinaryTreeNode(const QRectF &brect, const QImage &i, SelectionState selState, qreal a) + : boundingRect(brect), selectionState(selState), clipNode(0), decorations(QQuickTextNode::NoDecoration) + , image(i), ascent(a), leftChildIndex(-1), rightChildIndex(-1) + { + } + + BinaryTreeNode(const QGlyphRun &g, SelectionState selState, const QRectF &brect, + const QQuickTextNode::Decorations &decs, const QColor &c, const QColor &bc, + const QPointF &pos, qreal a) + : glyphRun(g), boundingRect(brect), selectionState(selState), clipNode(0), decorations(decs) + , color(c), backgroundColor(bc), position(pos), ascent(a), leftChildIndex(-1), rightChildIndex(-1) + { + } + + QGlyphRun glyphRun; + QRectF boundingRect; + SelectionState selectionState; + QQuickDefaultClipNode *clipNode; + QQuickTextNode::Decorations decorations; + QColor color; + QColor backgroundColor; + QPointF position; + QImage image; + qreal ascent; + + int leftChildIndex; + int rightChildIndex; + + static void insert(QVarLengthArray *binaryTree, const QRectF &rect, const QImage &image, qreal ascent, SelectionState selectionState) + { insert(binaryTree, BinaryTreeNode(rect, image, selectionState, ascent)); } + + static void insert(QVarLengthArray *binaryTree, const QGlyphRun &glyphRun, SelectionState selectionState, + QQuickTextNode::Decorations decorations, const QColor &textColor, const QColor &backgroundColor, const QPointF &position); + static void insert(QVarLengthArray *binaryTree, const BinaryTreeNode &binaryTreeNode); + static void inOrder(const QVarLengthArray &binaryTree, QVarLengthArray *sortedIndexes, int currentIndex = 0); + }; + + QQuickTextNodeEngine() : m_hasSelection(false), m_hasContents(false) {} + + bool hasContents() const { return m_hasContents; } + void addTextBlock(QTextDocument *, const QTextBlock &, const QPointF &position, const QColor &textColor, const QColor& anchorColor, int selectionStart, int selectionEnd); + QTextLine currentLine() const { return m_currentLine; } + + void setCurrentLine(const QTextLine ¤tLine) + { + if (m_currentLine.isValid()) + processCurrentLine(); + + m_currentLine = currentLine; + } + + void addBorder(const QRectF &rect, qreal border, QTextFrameFormat::BorderStyle borderStyle, + const QBrush &borderBrush); + void addFrameDecorations(QTextDocument *document, QTextFrame *frame); + void addImage(const QRectF &rect, const QImage &image, qreal ascent, + SelectionState selectionState, + QTextFrameFormat::Position layoutPosition); + int addText(const QTextBlock &block, + const QTextCharFormat &charFormat, + const QColor &textColor, + const QVarLengthArray &colorChanges, + int textPos, int fragmentEnd, + int selectionStart, int selectionEnd); + void addTextObject(const QPointF &position, const QTextCharFormat &format, + SelectionState selectionState, + QTextDocument *textDocument, int pos, + QTextFrameFormat::Position layoutPosition = QTextFrameFormat::InFlow); + void addSelectedGlyphs(const QGlyphRun &glyphRun); + void addUnselectedGlyphs(const QGlyphRun &glyphRun); + void addGlyphsInRange(int rangeStart, int rangeEnd, + const QColor &color, const QColor &backgroundColor, + int selectionStart, int selectionEnd); + void addGlyphsForRanges(const QVarLengthArray &ranges, + int start, int end, + int selectionStart, int selectionEnd); + + void addToSceneGraph(QQuickTextNode *parent, + QQuickText::TextStyle style = QQuickText::Normal, + const QColor &styleColor = QColor()); + + void setSelectionColor(const QColor &selectionColor) + { + m_selectionColor = selectionColor; + } + + void setSelectedTextColor(const QColor &selectedTextColor) + { + m_selectedTextColor = selectedTextColor; + } + + void setTextColor(const QColor &textColor) + { + m_textColor = textColor; + } + + void setAnchorColor(const QColor &anchorColor) + { + m_anchorColor = anchorColor; + } + + void setPosition(const QPointF &position) + { + m_position = position; + } + + + + +private: + struct TextDecoration + { + TextDecoration() : selectionState(Unselected) {} + TextDecoration(const SelectionState &s, + const QRectF &r, + const QColor &c) + : selectionState(s) + , rect(r) + , color(c) + { + } + + SelectionState selectionState; + QRectF rect; + QColor color; + }; + + void processCurrentLine(); + void addTextDecorations(const QVarLengthArray &textDecorations, qreal offset, qreal thickness); + void mergeFormats(QTextLayout *textLayout, QVarLengthArray *mergedFormats); + + QColor m_selectionColor; + QColor m_textColor; + QColor m_backgroundColor; + QColor m_selectedTextColor; + QColor m_anchorColor; + QPointF m_position; + + QTextLine m_currentLine; + + QList > m_backgrounds; + QList m_selectionRects; + QVarLengthArray m_currentLineTree; + + QList m_lines; + QVector m_processedNodes; + + QList > m_images; + + bool m_hasSelection : 1; + bool m_hasContents : 1; + friend class QQuickTextNode; + +}; + +QT_END_NAMESPACE + +#endif // QQUICKTEXTNODEENGINE_P_H -- 2.7.4