From d2ee3e66d63159f99d69eeab29959a5aec3fb512 Mon Sep 17 00:00:00 2001 From: Le Tan Date: Wed, 29 Nov 2017 21:44:01 +0800 Subject: [PATCH] VTextEdit: support previewing inline images --- src/vimageresourcemanager2.cpp | 42 +++- src/vimageresourcemanager2.h | 6 +- src/vpreviewmanager.cpp | 9 +- src/vtextdocumentlayout.cpp | 349 +++++++++++++++++++++++++++------ src/vtextdocumentlayout.h | 90 ++++++++- src/vtextedit.h | 13 ++ 6 files changed, 432 insertions(+), 77 deletions(-) diff --git a/src/vimageresourcemanager2.cpp b/src/vimageresourcemanager2.cpp index 71078c9b..53b12c01 100644 --- a/src/vimageresourcemanager2.cpp +++ b/src/vimageresourcemanager2.cpp @@ -23,20 +23,44 @@ bool VImageResourceManager2::contains(const QString &p_name) const QSet VImageResourceManager2::updateBlockInfos(const QVector &p_blocksInfo) { QSet usedImages; - QHash newBlocksInfo; + QHash> newBlocksInfo; for (auto const & info : p_blocksInfo) { - auto it = newBlocksInfo.insert(info.m_blockNumber, info); - VBlockImageInfo2 &newInfo = it.value(); - if (newInfo.m_padding < 0) { - newInfo.m_padding = 0; + VBlockImageInfo2 *newInfo = NULL; + auto blockIt = newBlocksInfo.find(info.m_blockNumber); + if (blockIt == newBlocksInfo.end()) { + // New block. + QVector vec(1, info); + auto it = newBlocksInfo.insert(info.m_blockNumber, vec); + newInfo = &it.value().last(); + } else { + // Multiple images for a block. + QVector &vec = blockIt.value(); + int i; + for (i = 0; i < vec.size(); ++i) { + Q_ASSERT(vec[i].m_blockNumber == info.m_blockNumber); + if (info < vec[i]) { + vec.insert(i, info); + newInfo = &vec[i]; + break; + } + } + + if (i == vec.size()) { + vec.append(info); + newInfo = &vec.last(); + } } - auto imageIt = m_images.find(newInfo.m_imageName); + if (newInfo->m_padding < 0) { + newInfo->m_padding = 0; + } + + auto imageIt = m_images.find(newInfo->m_imageName); if (imageIt != m_images.end()) { // Fill the width and height. - newInfo.m_imageSize = imageIt.value().size(); - usedImages.insert(newInfo.m_imageName); + newInfo->m_imageSize = imageIt.value().size(); + usedImages.insert(newInfo->m_imageName); } } @@ -61,7 +85,7 @@ QSet VImageResourceManager2::updateBlockInfos(const QVector *VImageResourceManager2::findImageInfoByBlock(int p_blockNumber) const { auto it = m_blocksInfo.find(p_blockNumber); if (it != m_blocksInfo.end()) { diff --git a/src/vimageresourcemanager2.h b/src/vimageresourcemanager2.h index ebf420a8..22383bf6 100644 --- a/src/vimageresourcemanager2.h +++ b/src/vimageresourcemanager2.h @@ -28,7 +28,7 @@ public: // Return changed blocks' block number. QSet updateBlockInfos(const QVector &p_blocksInfo); - const VBlockImageInfo2 *findImageInfoByBlock(int p_blockNumber) const; + const QVector *findImageInfoByBlock(int p_blockNumber) const; const QPixmap *findImage(const QString &p_name) const; @@ -39,7 +39,9 @@ private: QHash m_images; // Image info of all the blocks with image. - QHash m_blocksInfo; + // One block may contain multiple inline images or only one block image. + // If there are multiple inline images, they are sorted by the start position. + QHash> m_blocksInfo; }; #endif // VIMAGERESOURCEMANAGER2_H diff --git a/src/vpreviewmanager.cpp b/src/vpreviewmanager.cpp index ff063c17..1be41db5 100644 --- a/src/vpreviewmanager.cpp +++ b/src/vpreviewmanager.cpp @@ -226,11 +226,6 @@ void VPreviewManager::updateBlockImageInfo(const QVector &p_image for (int i = 0; i < p_imageLinks.size(); ++i) { const ImageLinkInfo &link = p_imageLinks[i]; - // Skip inline images. - if (!link.m_isBlock) { - continue; - } - QString name = imageResourceName(link); if (name.isEmpty()) { continue; @@ -240,7 +235,9 @@ void VPreviewManager::updateBlockImageInfo(const QVector &p_image name, link.m_startPos - link.m_blockPos, link.m_endPos - link.m_blockPos, - link.m_padding); + link.m_padding, + !link.m_isBlock); + blockInfos.push_back(info); } } diff --git a/src/vtextdocumentlayout.cpp b/src/vtextdocumentlayout.cpp index 00bc6487..af1a38e8 100644 --- a/src/vtextdocumentlayout.cpp +++ b/src/vtextdocumentlayout.cpp @@ -13,6 +13,8 @@ #include "vimageresourcemanager2.h" #include "vtextedit.h" +#define MARKER_THICKNESS 2 +#define MAX_INLINE_IMAGE_HEIGHT 400 VTextDocumentLayout::VTextDocumentLayout(QTextDocument *p_doc, VImageResourceManager2 *p_imageMgr) @@ -223,7 +225,9 @@ void VTextDocumentLayout::draw(QPainter *p_painter, const PaintContext &p_contex selections, p_context.clip.isValid() ? p_context.clip : QRectF()); - drawBlockImage(p_painter, block, offset); + drawImages(p_painter, block, offset); + + drawMarkers(p_painter, block, offset); // Draw the cursor. int blpos = block.position(); @@ -502,8 +506,6 @@ void VTextDocumentLayout::layoutBlock(const QTextBlock &p_block) QTextDocument *doc = document(); Q_ASSERT(m_margin == doc->documentMargin()); - // The height (y) of the next line. - qreal height = 0; QTextLayout *tl = p_block.layout(); QTextOption option = doc->defaultTextOption(); tl->setTextOption(option); @@ -521,40 +523,126 @@ void VTextDocumentLayout::layoutBlock(const QTextBlock &p_block) availableWidth -= (2 * m_margin + extraMargin + m_cursorMargin + m_cursorWidth); - tl->beginLayout(); + QVector markers; + QVector images; - while (true) { - QTextLine line = tl->createLine(); - if (!line.isValid()) { - break; - } - - line.setLeadingIncluded(true); - line.setLineWidth(availableWidth); - height += m_lineLeading; - line.setPosition(QPointF(m_margin, height)); - height += line.height(); - } - - tl->endLayout(); + layoutLines(p_block, tl, markers, images, availableWidth, 0); // Set this block's line count to its layout's line count. // That is one block may occupy multiple visual lines. const_cast(p_block).setLineCount(p_block.isVisible() ? tl->lineCount() : 0); // Update the info about this block. - finishBlockLayout(p_block); + finishBlockLayout(p_block, markers, images); } -void VTextDocumentLayout::finishBlockLayout(const QTextBlock &p_block) +qreal VTextDocumentLayout::layoutLines(const QTextBlock &p_block, + QTextLayout *p_tl, + QVector &p_markers, + QVector &p_images, + qreal p_availableWidth, + qreal p_height) +{ + // Handle block inline image. + bool hasInlineImages = false; + const QVector *info = NULL; + if (m_blockImageEnabled) { + info = m_imageMgr->findImageInfoByBlock(p_block.blockNumber()); + + if (info + && !info->isEmpty() + && info->first().m_inlineImage) { + hasInlineImages = true; + } + } + + p_tl->beginLayout(); + + int imgIdx = 0; + + while (true) { + QTextLine line = p_tl->createLine(); + if (!line.isValid()) { + break; + } + + line.setLeadingIncluded(true); + line.setLineWidth(p_availableWidth); + p_height += m_lineLeading; + + if (hasInlineImages) { + QVector images; + QVector> imageRange; + qreal imgHeight = fetchInlineImagesForOneLine(*info, + &line, + m_margin, + imgIdx, + images, + imageRange); + + for (int i = 0; i < images.size(); ++i) { + layoutInlineImage(images[i], + p_height, + imgHeight, + imageRange[i].first, + imageRange[i].second, + p_markers, + p_images); + } + + if (!images.isEmpty()) { + p_height += imgHeight + MARKER_THICKNESS + MARKER_THICKNESS; + } + } + + line.setPosition(QPointF(m_margin, p_height)); + p_height += line.height(); + } + + p_tl->endLayout(); + + return p_height; +} + +void VTextDocumentLayout::layoutInlineImage(const VBlockImageInfo2 *p_info, + qreal p_heightInBlock, + qreal p_imageSpaceHeight, + qreal p_xStart, + qreal p_xEnd, + QVector &p_markers, + QVector &p_images) +{ + Marker mk; + qreal mky = p_imageSpaceHeight + p_heightInBlock + MARKER_THICKNESS; + mk.m_start = QPointF(p_xStart, mky); + mk.m_end = QPointF(p_xEnd, mky); + p_markers.append(mk); + + if (p_info) { + QSize size = p_info->m_imageSize; + scaleSize(size, p_xEnd - p_xStart, p_imageSpaceHeight); + + ImagePaintInfo ipi; + ipi.m_name = p_info->m_imageName; + ipi.m_rect = QRectF(QPointF(p_xStart, + p_heightInBlock + p_imageSpaceHeight - size.height()), + size); + p_images.append(ipi); + } +} + +void VTextDocumentLayout::finishBlockLayout(const QTextBlock &p_block, + const QVector &p_markers, + const QVector &p_images) { // Update rect and offset. Q_ASSERT(p_block.isValid()); int num = p_block.blockNumber(); Q_ASSERT(m_blocks.size() > num); + ImagePaintInfo ipi; BlockInfo &info = m_blocks[num]; info.reset(); - info.m_rect = blockRectFromTextLayout(p_block); + info.m_rect = blockRectFromTextLayout(p_block, &ipi); Q_ASSERT(!info.m_rect.isNull()); int pre = previousValidBlockNumber(num); if (pre == -1) { @@ -563,6 +651,30 @@ void VTextDocumentLayout::finishBlockLayout(const QTextBlock &p_block) info.m_offset = m_blocks[pre].bottom(); } + bool hasImage = false; + if (ipi.isValid()) { + Q_ASSERT(p_markers.isEmpty()); + Q_ASSERT(p_images.isEmpty()); + info.m_images.append(ipi); + hasImage = true; + } else if (!p_markers.isEmpty()) { + // Q_ASSERT(!p_images.isEmpty()); + info.m_markers = p_markers; + info.m_images = p_images; + hasImage = true; + } + + // Add vertical marker. + if (hasImage) { + // Fill the marker. + // Will be adjusted using offset. + Marker mk; + mk.m_start = QPointF(-1, 0); + mk.m_end = QPointF(-1, info.m_rect.height()); + + info.m_markers.append(mk); + } + if (info.hasOffset()) { fillOffsetFrom(num); } @@ -622,8 +734,13 @@ int VTextDocumentLayout::cursorWidth() const return m_cursorWidth; } -QRectF VTextDocumentLayout::blockRectFromTextLayout(const QTextBlock &p_block) +QRectF VTextDocumentLayout::blockRectFromTextLayout(const QTextBlock &p_block, + ImagePaintInfo *p_image) { + if (p_image) { + *p_image = ImagePaintInfo(); + } + QTextLayout *tl = p_block.layout(); if (tl->lineCount() < 1) { return QRectF(); @@ -637,17 +754,29 @@ QRectF VTextDocumentLayout::blockRectFromTextLayout(const QTextBlock &p_block) br.setWidth(qMax(br.width(), tl->lineAt(0).naturalTextWidth())); } - // Handle block image. + // Handle block non-inline image. if (m_blockImageEnabled) { - const VBlockImageInfo2 *info = m_imageMgr->findImageInfoByBlock(p_block.blockNumber()); - if (info && !info->m_imageSize.isNull()) { - int maximumWidth = tlRect.width(); - int padding; - QSize size; - adjustImagePaddingAndSize(info, maximumWidth, padding, size); - int dw = padding + size.width() + m_margin - br.width(); - int dh = size.height() + m_lineLeading; - br.adjust(0, 0, dw > 0 ? dw : 0, dh); + const QVector *info = m_imageMgr->findImageInfoByBlock(p_block.blockNumber()); + if (info && info->size() == 1) { + const VBlockImageInfo2& img = info->first(); + if (!img.m_inlineImage && !img.m_imageSize.isNull()) { + int maximumWidth = tlRect.width(); + int padding; + QSize size; + adjustImagePaddingAndSize(&img, maximumWidth, padding, size); + + if (p_image) { + p_image->m_name = img.m_imageName; + p_image->m_rect = QRectF(padding + m_margin, + br.height() + m_lineLeading, + size.width(), + size.height()); + } + + int dw = padding + size.width() + m_margin - br.width(); + int dh = size.height() + m_lineLeading; + br.adjust(0, 0, dw > 0 ? dw : 0, dh); + } } } @@ -729,41 +858,53 @@ void VTextDocumentLayout::adjustImagePaddingAndSize(const VBlockImageInfo2 *p_in } } -void VTextDocumentLayout::drawBlockImage(QPainter *p_painter, - const QTextBlock &p_block, - const QPointF &p_offset) +void VTextDocumentLayout::drawImages(QPainter *p_painter, + const QTextBlock &p_block, + const QPointF &p_offset) { - if (!m_blockImageEnabled) { + if (m_blocks.size() <= p_block.blockNumber()) { return; } - const VBlockImageInfo2 *info = m_imageMgr->findImageInfoByBlock(p_block.blockNumber()); - if (!info || info->m_imageSize.isNull()) { + const QVector &images = m_blocks[p_block.blockNumber()].m_images; + if (images.isEmpty()) { return; } - const QPixmap *image = m_imageMgr->findImage(info->m_imageName); - Q_ASSERT(image); + for (auto const & img : images) { + const QPixmap *image = m_imageMgr->findImage(img.m_name); + Q_ASSERT(image); + QRect targetRect = img.m_rect.adjusted(p_offset.x(), + p_offset.y(), + p_offset.x(), + p_offset.y()).toRect(); - // Draw block image. - QTextLayout *tl = p_block.layout(); - QRectF tlRect = tl->boundingRect(); - int maximumWidth = tlRect.width(); - int padding; - QSize size; - adjustImagePaddingAndSize(info, maximumWidth, padding, size); - QRect targetRect(p_offset.x() + padding, - p_offset.y() + tlRect.height() + m_lineLeading, - size.width(), - size.height()); + p_painter->drawPixmap(targetRect, *image); + } +} - p_painter->drawPixmap(targetRect, *image); - // Draw a thin line to link them. +void VTextDocumentLayout::drawMarkers(QPainter *p_painter, + const QTextBlock &p_block, + const QPointF &p_offset) +{ + if (m_blocks.size() <= p_block.blockNumber()) { + return; + } + + const QVector &markers = m_blocks[p_block.blockNumber()].m_markers; + if (markers.isEmpty()) { + return; + } + QPen oldPen = p_painter->pen(); - QPen newPen(m_imageLineColor, 2, Qt::DashLine); + QPen newPen(m_imageLineColor, MARKER_THICKNESS, Qt::DashLine); p_painter->setPen(newPen); - p_painter->drawLine(QPointF(2, p_offset.y()), QPointF(2, targetRect.bottom())); + + for (auto const & mk : markers) { + p_painter->drawLine(mk.m_start + p_offset, mk.m_end + p_offset); + } + p_painter->setPen(oldPen); } @@ -810,3 +951,101 @@ void VTextDocumentLayout::relayout(const QSet &p_blocks) updateDocumentSize(); } + +qreal VTextDocumentLayout::fetchInlineImagesForOneLine(const QVector &p_info, + const QTextLine *p_line, + qreal p_margin, + int &p_index, + QVector &p_images, + QVector> &p_imageRange) +{ + qreal maxHeight = 0; + int start = p_line->textStart(); + int end = p_line->textLength() + start; + + for (int i = 0; i < p_info.size(); ++i) { + const VBlockImageInfo2 &img = p_info[i]; + Q_ASSERT(img.m_inlineImage); + + if (img.m_imageSize.isNull()) { + p_index = i + 1; + continue; + } + + if (img.m_startPos >= start && img.m_startPos < end) { + // Start of a new image. + qreal startX = p_line->cursorToX(img.m_startPos) + p_margin; + qreal endX; + if (img.m_endPos <= end) { + // End an image. + endX = p_line->cursorToX(img.m_endPos) + p_margin; + p_images.append(&img); + p_imageRange.append(QPair(startX, endX)); + + QSize size = img.m_imageSize; + scaleSize(size, endX - startX, MAX_INLINE_IMAGE_HEIGHT); + if (size.height() > maxHeight) { + maxHeight = size.height(); + } + + // Image i has been drawn. + p_index = i + 1; + } else { + // This image cross the line. + endX = p_line->x() + p_line->width() + p_margin; + if (end - img.m_startPos >= ((img.m_endPos - img.m_startPos) >> 1)) { + // Put image at this side. + p_images.append(&img); + p_imageRange.append(QPair(startX, endX)); + + QSize size = img.m_imageSize; + scaleSize(size, endX - startX, MAX_INLINE_IMAGE_HEIGHT); + if (size.height() > maxHeight) { + maxHeight = size.height(); + } + + // Image i has been drawn. + p_index = i + 1; + } else { + // Just put a marker here. + p_images.append(NULL); + p_imageRange.append(QPair(startX, endX)); + } + + break; + } + } else if (img.m_endPos > start && img.m_startPos < start) { + qreal startX = p_line->x() + p_margin; + qreal endX = img.m_endPos > end ? p_line->x() + p_line->width() + : p_line->cursorToX(img.m_endPos); + if (p_index <= i) { + // Image i has not been drawn. Draw it here. + p_images.append(&img); + p_imageRange.append(QPair(startX, endX)); + + QSize size = img.m_imageSize; + scaleSize(size, endX - startX, MAX_INLINE_IMAGE_HEIGHT); + if (size.height() > maxHeight) { + maxHeight = size.height(); + } + + // Image i has been drawn. + p_index = i + 1; + } else { + // Image i has been drawn. Just put a marker here. + p_images.append(NULL); + p_imageRange.append(QPair(startX, endX)); + } + + if (img.m_endPos >= end) { + break; + } + } else if (img.m_endPos <= start) { + continue; + } else { + break; + } + } + + return maxHeight; +} diff --git a/src/vtextdocumentlayout.h b/src/vtextdocumentlayout.h index 8b38ca23..120484e2 100644 --- a/src/vtextdocumentlayout.h +++ b/src/vtextdocumentlayout.h @@ -57,6 +57,27 @@ protected: void documentChanged(int p_from, int p_charsRemoved, int p_charsAdded) Q_DECL_OVERRIDE; private: + // Denote the start and end position of a marker line. + struct Marker + { + QPointF m_start; + QPointF m_end; + }; + + struct ImagePaintInfo + { + // The rect to draw the image. + QRectF m_rect; + + // Name of the image. + QString m_name; + + bool isValid() + { + return !m_name.isEmpty(); + } + }; + struct BlockInfo { BlockInfo() @@ -68,6 +89,8 @@ private: { m_offset = -1; m_rect = QRectF(); + m_markers.clear(); + m_images.clear(); } bool hasOffset() const @@ -94,10 +117,50 @@ private: // The bounding rect of this block, including the margins. // Null for invalid. QRectF m_rect; + + // Markers to draw for this block. + // Y is the offset within this block. + QVector m_markers; + + // Images to draw for this block. + // Y is the offset within this block. + QVector m_images; }; void layoutBlock(const QTextBlock &p_block); + // Returns the total height of this block after layouting lines and inline + // images. + qreal layoutLines(const QTextBlock &p_block, + QTextLayout *p_tl, + QVector &p_markers, + QVector &p_images, + qreal p_availableWidth, + qreal p_height); + + // Layout inline image in a line. + // @p_info: if NULL, means just layout a marker. + // Returns the image height. + void layoutInlineImage(const VBlockImageInfo2 *p_info, + qreal p_heightInBlock, + qreal p_imageSpaceHeight, + qreal p_xStart, + qreal p_xEnd, + QVector &p_markers, + QVector &p_images); + + // Get inline images belonging to @p_line from @p_info. + // @p_index: image [0, p_index) has been drawn. + // @p_images: contains all images and markers (NULL element indicates it + // is just a placeholder for the marker. + // Returns the maximum height of the images. + qreal fetchInlineImagesForOneLine(const QVector &p_info, + const QTextLine *p_line, + qreal p_margin, + int &p_index, + QVector &p_images, + QVector> &p_imageRange); + // Clear the layout of @p_block. // Also clear all the offset behind this block. void clearBlockLayout(QTextBlock &p_block); @@ -115,7 +178,9 @@ private: bool validateBlocks() const; - void finishBlockLayout(const QTextBlock &p_block); + void finishBlockLayout(const QTextBlock &p_block, + const QVector &p_markers, + const QVector &p_images); int previousValidBlockNumber(int p_number) const; @@ -136,8 +201,11 @@ private: void blockRangeFromRectBS(const QRectF &p_rect, int &p_first, int &p_last) const; // Return a rect from the layout. + // If @p_imageRect is not NULL and there is block image for this block, it will + // be set to the rect of that image. // Return a null rect if @p_block has not been layouted. - QRectF blockRectFromTextLayout(const QTextBlock &p_block); + QRectF blockRectFromTextLayout(const QTextBlock &p_block, + ImagePaintInfo *p_image = NULL); // Update document size when only block @p_blockNumber is changed and the height // remain the same. @@ -150,9 +218,15 @@ private: // Draw images of block @p_block. // @p_offset: the offset for the drawing of the block. - void drawBlockImage(QPainter *p_painter, - const QTextBlock &p_block, - const QPointF &p_offset); + void drawImages(QPainter *p_painter, + const QTextBlock &p_block, + const QPointF &p_offset); + + void drawMarkers(QPainter *p_painter, + const QTextBlock &p_block, + const QPointF &p_offset); + + void scaleSize(QSize &p_size, int p_width, int p_height); // Document margin on left/right/bottom. qreal m_margin; @@ -201,4 +275,10 @@ inline void VTextDocumentLayout::setImageLineColor(const QColor &p_color) m_imageLineColor = p_color; } +inline void VTextDocumentLayout::scaleSize(QSize &p_size, int p_width, int p_height) +{ + if (p_size.width() > p_width || p_size.height() > p_height) { + p_size.scale(p_width, p_height, Qt::KeepAspectRatio); + } +} #endif // VTEXTDOCUMENTLAYOUT_H diff --git a/src/vtextedit.h b/src/vtextedit.h index dfe739d1..eb8b7e7e 100644 --- a/src/vtextedit.h +++ b/src/vtextedit.h @@ -50,6 +50,19 @@ public: && m_imageSize == p_other.m_imageSize; } + bool operator<(const VBlockImageInfo2 &p_other) const + { + if (m_blockNumber < p_other.m_blockNumber) { + return true; + } else if (m_blockNumber > p_other.m_blockNumber) { + return false; + } else if (m_startPos < p_other.m_startPos) { + return true; + } else { + return false; + } + } + QString toString() const { return QString("VBlockImageInfo2 block %1 start %2 end %3 padding %4 "