diff --git a/src/src.pro b/src/src.pro index 4dcc8d70..72a2e355 100644 --- a/src/src.pro +++ b/src/src.pro @@ -81,7 +81,10 @@ SOURCES += main.cpp\ vtableofcontent.cpp \ utils/vmetawordmanager.cpp \ vlineedit.cpp \ - dialog/vinsertlinkdialog.cpp + dialog/vinsertlinkdialog.cpp \ + vplaintextedit.cpp \ + vimageresourcemanager.cpp \ + vlinenumberarea.cpp HEADERS += vmainwindow.h \ vdirectorytree.h \ @@ -150,7 +153,10 @@ HEADERS += vmainwindow.h \ vtableofcontent.h \ utils/vmetawordmanager.h \ vlineedit.h \ - dialog/vinsertlinkdialog.h + dialog/vinsertlinkdialog.h \ + vplaintextedit.h \ + vimageresourcemanager.h \ + vlinenumberarea.h RESOURCES += \ vnote.qrc \ diff --git a/src/veditoperations.cpp b/src/veditoperations.cpp index 74e3183d..9a666a69 100644 --- a/src/veditoperations.cpp +++ b/src/veditoperations.cpp @@ -97,3 +97,8 @@ void VEditOperations::setVimMode(VimMode p_mode) m_vim->setMode(p_mode); } } + +void VEditOperations::decorateText(TextDecoration p_decoration) +{ + Q_UNUSED(p_decoration); +} diff --git a/src/veditoperations.h b/src/veditoperations.h index 8085e2f1..3c0e4ac5 100644 --- a/src/veditoperations.h +++ b/src/veditoperations.h @@ -38,7 +38,7 @@ public: void requestUpdateVimStatus(); // Insert decoration markers or decorate selected text. - virtual void decorateText(TextDecoration p_decoration) {Q_UNUSED(p_decoration);}; + virtual void decorateText(TextDecoration p_decoration); // Set Vim mode if not NULL. void setVimMode(VimMode p_mode); diff --git a/src/vimageresourcemanager.cpp b/src/vimageresourcemanager.cpp new file mode 100644 index 00000000..1f3673fc --- /dev/null +++ b/src/vimageresourcemanager.cpp @@ -0,0 +1,132 @@ +#include "vimageresourcemanager.h" + +#include + +#include "vplaintextedit.h" + + +VImageResourceManager::VImageResourceManager() + : m_maximumImageWidth(0) +{ +} + +void VImageResourceManager::addImage(const QString &p_name, + const QPixmap &p_image) +{ + m_images.insert(p_name, p_image); +} + +bool VImageResourceManager::contains(const QString &p_name) const +{ + return m_images.contains(p_name); +} + +void VImageResourceManager::updateBlockInfos(const QVector &p_blocksInfo, + int p_maximumWidth) +{ + QSet usedImages; + m_blocksInfo.clear(); + m_maximumImageWidth = 0; + + for (auto const & info : p_blocksInfo) { + auto it = m_blocksInfo.insert(info.m_blockNumber, info); + VBlockImageInfo &newInfo = it.value(); + auto imageIt = m_images.find(newInfo.m_imageName); + if (imageIt != m_images.end()) { + // Fill the width and height. + newInfo.m_imageWidth = imageIt.value().width(); + newInfo.m_imageHeight = imageIt.value().height(); + adjustWidthAndHeight(newInfo, p_maximumWidth); + updateMaximumImageWidth(newInfo, p_maximumWidth); + usedImages.insert(newInfo.m_imageName); + } + } + + // Clear unused images. + for (auto it = m_images.begin(); it != m_images.end();) { + if (!m_images.contains(it.key())) { + // Remove the image. + it = m_images.erase(it); + } else { + ++it; + } + } + + qDebug() << "updateBlockInfos() blocks" << m_blocksInfo.size() + << "images" << m_images.size(); +} + +const VBlockImageInfo *VImageResourceManager::findImageInfoByBlock(int p_blockNumber) const +{ + auto it = m_blocksInfo.find(p_blockNumber); + if (it != m_blocksInfo.end()) { + return &it.value(); + } + + return NULL; +} + +const QPixmap *VImageResourceManager::findImage(const QString &p_name) const +{ + auto it = m_images.find(p_name); + if (it != m_images.end()) { + return &it.value(); + } + + return NULL; +} + +void VImageResourceManager::clear() +{ + m_blocksInfo.clear(); + m_images.clear(); +} + +void VImageResourceManager::updateImageWidth(int p_maximumWidth) +{ + qDebug() << "updateImageWidth()" << p_maximumWidth; + m_maximumImageWidth = 0; + for (auto it = m_blocksInfo.begin(); it != m_blocksInfo.end(); ++it) { + VBlockImageInfo &info = it.value(); + auto imageIt = m_images.find(info.m_imageName); + if (imageIt != m_images.end()) { + info.m_imageWidth = imageIt.value().width(); + info.m_imageHeight = imageIt.value().height(); + adjustWidthAndHeight(info, p_maximumWidth); + updateMaximumImageWidth(info, p_maximumWidth); + } + } +} + +void VImageResourceManager::adjustWidthAndHeight(VBlockImageInfo &p_info, + int p_maximumWidth) +{ + int oriWidth = p_info.m_imageWidth; + int availableWidth = p_maximumWidth - p_info.m_margin; + if (availableWidth < p_info.m_imageWidth) { + if (availableWidth >= VPlainTextEdit::c_minimumImageWidth) { + p_info.m_imageWidth = availableWidth; + } else { + // Omit the margin when displaying this image. + p_info.m_imageWidth = p_maximumWidth; + } + } + + if (oriWidth != p_info.m_imageWidth) { + // Update the height respecting the ratio. + p_info.m_imageHeight = (1.0 * p_info.m_imageWidth / oriWidth) * p_info.m_imageHeight; + } +} + +void VImageResourceManager::updateMaximumImageWidth(const VBlockImageInfo &p_info, + int p_maximumWidth) +{ + int width = p_info.m_imageWidth + p_info.m_margin; + if (width > p_maximumWidth) { + width = p_info.m_imageWidth; + } + + if (width > m_maximumImageWidth) { + m_maximumImageWidth = width; + } +} diff --git a/src/vimageresourcemanager.h b/src/vimageresourcemanager.h new file mode 100644 index 00000000..517e7717 --- /dev/null +++ b/src/vimageresourcemanager.h @@ -0,0 +1,63 @@ +#ifndef VIMAGERESOURCEMANAGER_H +#define VIMAGERESOURCEMANAGER_H + +#include +#include +#include +#include +#include + +struct VBlockImageInfo; + + +class VImageResourceManager +{ +public: + VImageResourceManager(); + + // Add an image to the resource with @p_name as the key. + // If @p_name already exists in the resources, it will update it. + void addImage(const QString &p_name, const QPixmap &p_image); + + // Whether the resources contains image with name @p_name. + bool contains(const QString &p_name) const; + + // Update the block-image info for all blocks. + // @p_maximumWidth: maximum width of the images plus the margin. + void updateBlockInfos(const QVector &p_blocksInfo, + int p_maximumWidth = INT_MAX); + + const VBlockImageInfo *findImageInfoByBlock(int p_blockNumber) const; + + const QPixmap *findImage(const QString &p_name) const; + + void clear(); + + // Update the width of all the block info. + void updateImageWidth(int p_maximumWidth); + + // Get the maximum width of all block images. + int getMaximumImageWidth() const; + +private: + // Adjust the width and height according to @p_maximumWidth and margin. + void adjustWidthAndHeight(VBlockImageInfo &p_info, int p_maximumWidth); + + void updateMaximumImageWidth(const VBlockImageInfo &p_info, int p_maximumWidth); + + // All the images resources. + QHash m_images; + + // Image info of all the blocks with image. + QHash m_blocksInfo; + + // Maximum width of all images from m_blocksInfo. + int m_maximumImageWidth; +}; + +inline int VImageResourceManager::getMaximumImageWidth() const +{ + return m_maximumImageWidth; +} + +#endif // VIMAGERESOURCEMANAGER_H diff --git a/src/vlinenumberarea.cpp b/src/vlinenumberarea.cpp new file mode 100644 index 00000000..5a91b445 --- /dev/null +++ b/src/vlinenumberarea.cpp @@ -0,0 +1,42 @@ +#include "vlinenumberarea.h" + +#include +#include + +VLineNumberArea::VLineNumberArea(VTextEditWithLineNumber *p_editor, + const QTextDocument *p_document, + int p_digitWidth, + int p_digitHeight, + QWidget *p_parent) + : QWidget(p_parent), + m_editor(p_editor), + m_document(p_document), + m_width(0), + m_blockCount(-1), + m_digitWidth(p_digitWidth), + m_digitHeight(p_digitHeight), + m_foregroundColor("black"), + m_backgroundColor("grey") +{ +} + +int VLineNumberArea::calculateWidth() const +{ + int bc = m_document->blockCount(); + if (m_blockCount == bc) { + return m_width; + } + + const_cast(this)->m_blockCount = bc; + int digits = 1; + int max = qMax(1, m_blockCount); + while (max >= 10) { + max /= 10; + ++digits; + } + + int width = m_digitWidth * (digits + 1); + const_cast(this)->m_width = width; + + return m_width; +} diff --git a/src/vlinenumberarea.h b/src/vlinenumberarea.h new file mode 100644 index 00000000..8d0bf18c --- /dev/null +++ b/src/vlinenumberarea.h @@ -0,0 +1,95 @@ +#ifndef VLINENUMBERAREA_H +#define VLINENUMBERAREA_H + +#include +#include + +class QPaintEvent; +class QTextDocument; + + +enum class LineNumberType +{ + None = 0, + Absolute, + Relative, + CodeBlock +}; + + +class VTextEditWithLineNumber +{ +public: + virtual ~VTextEditWithLineNumber() {} + + virtual void paintLineNumberArea(QPaintEvent *p_event) = 0; +}; + + +// To use VLineNumberArea, the editor should implement VTextEditWithLineNumber. +class VLineNumberArea : public QWidget +{ + Q_OBJECT +public: + VLineNumberArea(VTextEditWithLineNumber *p_editor, + const QTextDocument *p_document, + int p_digitWidth, + int p_digitHeight, + QWidget *p_parent = nullptr); + + QSize sizeHint() const Q_DECL_OVERRIDE + { + return QSize(calculateWidth(), 0); + } + + int calculateWidth() const; + + int getDigitHeight() const + { + return m_digitHeight; + } + + const QColor &getBackgroundColor() const; + void setBackgroundColor(const QColor &p_color); + + const QColor &getForegroundColor() const; + void setForegroundColor(const QColor &p_color); + +protected: + void paintEvent(QPaintEvent *p_event) Q_DECL_OVERRIDE + { + m_editor->paintLineNumberArea(p_event); + } + +private: + VTextEditWithLineNumber *m_editor; + const QTextDocument *m_document; + int m_width; + int m_blockCount; + int m_digitWidth; + int m_digitHeight; + QColor m_foregroundColor; + QColor m_backgroundColor; +}; + +inline const QColor &VLineNumberArea::getBackgroundColor() const +{ + return m_backgroundColor; +} + +inline void VLineNumberArea::setBackgroundColor(const QColor &p_color) +{ + m_backgroundColor = p_color; +} + +inline const QColor &VLineNumberArea::getForegroundColor() const +{ + return m_foregroundColor; +} + +inline void VLineNumberArea::setForegroundColor(const QColor &p_color) +{ + m_foregroundColor = p_color; +} + +#endif // VLINENUMBERAREA_H diff --git a/src/vplaintextedit.cpp b/src/vplaintextedit.cpp new file mode 100644 index 00000000..134dc871 --- /dev/null +++ b/src/vplaintextedit.cpp @@ -0,0 +1,581 @@ +#include "vplaintextedit.h" + +#include +#include +#include +#include +#include +#include + +#include "vimageresourcemanager.h" + + +const int VPlainTextEdit::c_minimumImageWidth = 100; + +enum class BlockState +{ + Normal = 1, + CodeBlockStart, + CodeBlock, + CodeBlockEnd +}; + + +VPlainTextEdit::VPlainTextEdit(QWidget *p_parent) + : QPlainTextEdit(p_parent), + m_imageMgr(NULL), + m_blockImageEnabled(false), + m_imageWidthConstrainted(false), + m_maximumImageWidth(INT_MAX) +{ + init(); +} + +VPlainTextEdit::VPlainTextEdit(const QString &p_text, QWidget *p_parent) + : QPlainTextEdit(p_text, p_parent), + m_imageMgr(NULL), + m_blockImageEnabled(false), + m_imageWidthConstrainted(false), + m_maximumImageWidth(INT_MAX) +{ + init(); +} + +VPlainTextEdit::~VPlainTextEdit() +{ + if (m_imageMgr) { + delete m_imageMgr; + } +} + +void VPlainTextEdit::init() +{ + m_lineNumberType = LineNumberType::None; + + m_imageMgr = new VImageResourceManager(); + + QTextDocument *doc = document(); + QPlainTextDocumentLayout *layout = new VPlainTextDocumentLayout(doc, + m_imageMgr, + m_blockImageEnabled); + doc->setDocumentLayout(layout); + + m_lineNumberArea = new VLineNumberArea(this, + document(), + fontMetrics().width(QLatin1Char('8')), + fontMetrics().height(), + this); + connect(document(), &QTextDocument::blockCountChanged, + this, &VPlainTextEdit::updateLineNumberAreaMargin); + connect(this, &QPlainTextEdit::textChanged, + this, &VPlainTextEdit::updateLineNumberArea); + connect(verticalScrollBar(), &QScrollBar::valueChanged, + this, &VPlainTextEdit::updateLineNumberArea); + connect(this, &QPlainTextEdit::cursorPositionChanged, + this, &VPlainTextEdit::updateLineNumberArea); +} + +void VPlainTextEdit::updateBlockImages(const QVector &p_blocksInfo) +{ + if (m_blockImageEnabled) { + m_imageMgr->updateBlockInfos(p_blocksInfo, m_maximumImageWidth); + } +} + +void VPlainTextEdit::clearBlockImages() +{ + m_imageMgr->clear(); +} + +bool VPlainTextEdit::containsImage(const QString &p_imageName) const +{ + return m_imageMgr->contains(p_imageName); +} + +void VPlainTextEdit::addImage(const QString &p_imageName, const QPixmap &p_image) +{ + if (m_blockImageEnabled) { + m_imageMgr->addImage(p_imageName, p_image); + } +} + +static void fillBackground(QPainter *p, + const QRectF &rect, + QBrush brush, + const QRectF &gradientRect = QRectF()) +{ + p->save(); + if (brush.style() >= Qt::LinearGradientPattern + && brush.style() <= Qt::ConicalGradientPattern) { + if (!gradientRect.isNull()) { + QTransform m = QTransform::fromTranslate(gradientRect.left(), + gradientRect.top()); + m.scale(gradientRect.width(), + gradientRect.height()); + brush.setTransform(m); + const_cast(brush.gradient())->setCoordinateMode(QGradient::LogicalMode); + } + } else { + p->setBrushOrigin(rect.topLeft()); + } + + p->fillRect(rect, brush); + p->restore(); +} + +void VPlainTextEdit::paintEvent(QPaintEvent *p_event) +{ + QPainter painter(viewport()); + QPointF offset(contentOffset()); + + QRect er = p_event->rect(); + QRect viewportRect = viewport()->rect(); + + bool editable = !isReadOnly(); + + QTextBlock block = firstVisibleBlock(); + qreal maximumWidth = document()->documentLayout()->documentSize().width(); + + // Set a brush origin so that the WaveUnderline knows where the wave started. + painter.setBrushOrigin(offset); + + // Keep right margin clean from full-width selection. + int maxX = offset.x() + qMax((qreal)viewportRect.width(), maximumWidth) + - document()->documentMargin(); + er.setRight(qMin(er.right(), maxX)); + painter.setClipRect(er); + + QAbstractTextDocumentLayout::PaintContext context = getPaintContext(); + + while (block.isValid()) { + QRectF r = blockBoundingRect(block).translated(offset); + QTextLayout *layout = block.layout(); + + if (!block.isVisible()) { + offset.ry() += r.height(); + block = block.next(); + continue; + } + + if (r.bottom() >= er.top() && r.top() <= er.bottom()) { + QTextBlockFormat blockFormat = block.blockFormat(); + QBrush bg = blockFormat.background(); + if (bg != Qt::NoBrush) { + QRectF contentsRect = r; + contentsRect.setWidth(qMax(r.width(), maximumWidth)); + fillBackground(&painter, contentsRect, bg); + } + + QVector selections; + int blpos = block.position(); + int bllen = block.length(); + for (int i = 0; i < context.selections.size(); ++i) { + const QAbstractTextDocumentLayout::Selection &range = context.selections.at(i); + const int selStart = range.cursor.selectionStart() - blpos; + const int selEnd = range.cursor.selectionEnd() - blpos; + if (selStart < bllen + && selEnd > 0 + && selEnd > selStart) { + QTextLayout::FormatRange o; + o.start = selStart; + o.length = selEnd - selStart; + o.format = range.format; + selections.append(o); + } else if (!range.cursor.hasSelection() + && range.format.hasProperty(QTextFormat::FullWidthSelection) + && block.contains(range.cursor.position())) { + // For full width selections we don't require an actual selection, just + // a position to specify the line. That's more convenience in usage. + QTextLayout::FormatRange o; + QTextLine l = layout->lineForTextPosition(range.cursor.position() - blpos); + o.start = l.textStart(); + o.length = l.textLength(); + if (o.start + o.length == bllen - 1) { + ++o.length; // include newline + } + + o.format = range.format; + selections.append(o); + } + } + + bool drawCursor = (editable + || (textInteractionFlags() & Qt::TextSelectableByKeyboard)) + && context.cursorPosition >= blpos + && context.cursorPosition < blpos + bllen; + + bool drawCursorAsBlock = drawCursor && overwriteMode() ; + if (drawCursorAsBlock) { + if (context.cursorPosition == blpos + bllen - 1) { + drawCursorAsBlock = false; + } else { + QTextLayout::FormatRange o; + o.start = context.cursorPosition - blpos; + o.length = 1; + o.format.setForeground(palette().base()); + o.format.setBackground(palette().text()); + selections.append(o); + } + } + + if (!placeholderText().isEmpty() + && document()->isEmpty() + && layout->preeditAreaText().isEmpty()) { + QColor col = palette().text().color(); + col.setAlpha(128); + painter.setPen(col); + const int margin = int(document()->documentMargin()); + painter.drawText(r.adjusted(margin, 0, 0, 0), + Qt::AlignTop | Qt::TextWordWrap, + placeholderText()); + } else { + layout->draw(&painter, offset, selections, er); + } + + if ((drawCursor && !drawCursorAsBlock) + || (editable + && context.cursorPosition < -1 + && !layout->preeditAreaText().isEmpty())) { + int cpos = context.cursorPosition; + if (cpos < -1) { + cpos = layout->preeditAreaPosition() - (cpos + 2); + } else { + cpos -= blpos; + } + + layout->drawCursor(&painter, offset, cpos, cursorWidth()); + } + + // Draw preview image of this block if there is one. + drawImageOfBlock(block, &painter, r); + } + + offset.ry() += r.height(); + if (offset.y() > viewportRect.height()) { + break; + } + + block = block.next(); + } + + if (backgroundVisible() + && !block.isValid() + && offset.y() <= er.bottom() + && (centerOnScroll() + || verticalScrollBar()->maximum() == verticalScrollBar()->minimum())) { + painter.fillRect(QRect(QPoint((int)er.left(), + (int)offset.y()), + er.bottomRight()), + palette().background()); + } +} + +void VPlainTextEdit::drawImageOfBlock(const QTextBlock &p_block, + QPainter *p_painter, + const QRectF &p_blockRect) +{ + if (!m_blockImageEnabled) { + return; + } + + const VBlockImageInfo *info = m_imageMgr->findImageInfoByBlock(p_block.blockNumber()); + if (!info) { + return; + } + + const QPixmap *image = m_imageMgr->findImage(info->m_imageName); + if (!image) { + return; + } + + int oriHeight = originalBlockBoundingRect(p_block).height(); + bool noMargin = (info->m_margin + info->m_imageWidth > m_maximumImageWidth); + int margin = noMargin ? 0 : info->m_margin; + QRect tmpRect(p_blockRect.toRect()); + QRect targetRect(tmpRect.x() + margin, + tmpRect.y() + oriHeight, + info->m_imageWidth, + qMax(info->m_imageHeight, tmpRect.height() - oriHeight)); + + p_painter->drawPixmap(targetRect, *image); +} + +QRectF VPlainTextEdit::originalBlockBoundingRect(const QTextBlock &p_block) const +{ + return getLayout()->QPlainTextDocumentLayout::blockBoundingRect(p_block); +} + +void VPlainTextEdit::setBlockImageEnabled(bool p_enabled) +{ + if (m_blockImageEnabled == p_enabled) { + return; + } + + m_blockImageEnabled = p_enabled; + if (!p_enabled) { + clearBlockImages(); + } + + getLayout()->setBlockImageEnabled(m_blockImageEnabled); +} + +void VPlainTextEdit::setImageWidthConstrainted(bool p_enabled) +{ + m_imageWidthConstrainted = p_enabled; +} + +void VPlainTextEdit::resizeEvent(QResizeEvent *p_event) +{ + bool needUpdate = false; + if (m_imageWidthConstrainted) { + const QSize &si = p_event->size(); + m_maximumImageWidth = si.width(); + needUpdate = true; + } else if (m_maximumImageWidth != INT_MAX) { + needUpdate = true; + m_maximumImageWidth = INT_MAX; + } + + if (needUpdate) { + m_imageMgr->updateImageWidth(m_maximumImageWidth); + } + + QPlainTextEdit::resizeEvent(p_event); + + if (m_lineNumberType != LineNumberType::None) { + QRect rect = contentsRect(); + m_lineNumberArea->setGeometry(QRect(rect.left(), + rect.top(), + m_lineNumberArea->calculateWidth(), + rect.height())); + } +} + +void VPlainTextEdit::paintLineNumberArea(QPaintEvent *p_event) +{ + if (m_lineNumberType == LineNumberType::None) { + updateLineNumberAreaMargin(); + m_lineNumberArea->hide(); + return; + } + + QPainter painter(m_lineNumberArea); + painter.fillRect(p_event->rect(), m_lineNumberArea->getBackgroundColor()); + + QTextBlock block = firstVisibleBlock(); + if (!block.isValid()) { + return; + } + + int blockNumber = block.blockNumber(); + int offsetY = (int)contentOffset().y(); + QRectF rect = blockBoundingRect(block); + int top = offsetY + (int)rect.y(); + int bottom = top + (int)rect.height(); + int eventTop = p_event->rect().top(); + int eventBtm = p_event->rect().bottom(); + const int digitHeight = m_lineNumberArea->getDigitHeight(); + const int curBlockNumber = textCursor().block().blockNumber(); + painter.setPen(m_lineNumberArea->getForegroundColor()); + + // Display line number only in code block. + if (m_lineNumberType == LineNumberType::CodeBlock) { + int number = 0; + while (block.isValid() && top <= eventBtm) { + int blockState = block.userState(); + switch (blockState) { + case (int)BlockState::CodeBlockStart: + Q_ASSERT(number == 0); + number = 1; + break; + + case (int)BlockState::CodeBlockEnd: + number = 0; + break; + + case (int)BlockState::CodeBlock: + if (number == 0) { + // Need to find current line number in code block. + QTextBlock startBlock = block.previous(); + while (startBlock.isValid()) { + if (startBlock.userState() == (int)BlockState::CodeBlockStart) { + number = block.blockNumber() - startBlock.blockNumber(); + break; + } + + startBlock = startBlock.previous(); + } + } + + break; + + default: + break; + } + + if (blockState == (int)BlockState::CodeBlock) { + if (block.isVisible() && bottom >= eventTop) { + QString numberStr = QString::number(number); + painter.drawText(0, + top, + m_lineNumberArea->width(), + digitHeight, + Qt::AlignRight, + numberStr); + } + + ++number; + } + + block = block.next(); + top = bottom; + bottom = top + (int)blockBoundingRect(block).height(); + } + + return; + } + + // Handle m_lineNumberType 1 and 2. + Q_ASSERT(m_lineNumberType == LineNumberType::Absolute + || m_lineNumberType == LineNumberType::Relative); + while (block.isValid() && top <= eventBtm) { + if (block.isVisible() && bottom >= eventTop) { + bool currentLine = false; + int number = blockNumber + 1; + if (m_lineNumberType == LineNumberType::Relative) { + number = blockNumber - curBlockNumber; + if (number == 0) { + currentLine = true; + number = blockNumber + 1; + } else if (number < 0) { + number = -number; + } + } else if (blockNumber == curBlockNumber) { + currentLine = true; + } + + QString numberStr = QString::number(number); + + if (currentLine) { + QFont font = painter.font(); + font.setBold(true); + painter.setFont(font); + } + + painter.drawText(0, + top, + m_lineNumberArea->width(), + digitHeight, + Qt::AlignRight, + numberStr); + + if (currentLine) { + QFont font = painter.font(); + font.setBold(false); + painter.setFont(font); + } + } + + block = block.next(); + top = bottom; + bottom = top + (int)blockBoundingRect(block).height(); + ++blockNumber; + } + +} + +VPlainTextDocumentLayout *VPlainTextEdit::getLayout() const +{ + return qobject_cast(document()->documentLayout()); +} + +void VPlainTextEdit::updateLineNumberAreaMargin() +{ + int width = 0; + if (m_lineNumberType != LineNumberType::None) { + width = m_lineNumberArea->calculateWidth(); + } + + setViewportMargins(width, 0, 0, 0); +} + +void VPlainTextEdit::updateLineNumberArea() +{ + if (m_lineNumberType != LineNumberType::None) { + if (!m_lineNumberArea->isVisible()) { + updateLineNumberAreaMargin(); + m_lineNumberArea->show(); + } + + m_lineNumberArea->update(); + } else if (m_lineNumberArea->isVisible()) { + updateLineNumberAreaMargin(); + m_lineNumberArea->hide(); + } +} + +VPlainTextDocumentLayout::VPlainTextDocumentLayout(QTextDocument *p_document, + VImageResourceManager *p_imageMgr, + bool p_blockImageEnabled) + : QPlainTextDocumentLayout(p_document), + m_imageMgr(p_imageMgr), + m_blockImageEnabled(p_blockImageEnabled), + m_maximumImageWidth(INT_MAX) +{ +} + +QRectF VPlainTextDocumentLayout::blockBoundingRect(const QTextBlock &p_block) const +{ + QRectF br = QPlainTextDocumentLayout::blockBoundingRect(p_block); + if (!m_blockImageEnabled) { + return br; + } + + const VBlockImageInfo *info = m_imageMgr->findImageInfoByBlock(p_block.blockNumber()); + if (info) { + int tmp = info->m_margin + info->m_imageWidth; + if (tmp > m_maximumImageWidth) { + Q_ASSERT(info->m_imageWidth <= m_maximumImageWidth); + tmp = info->m_imageWidth; + } + + qreal width = (qreal)(tmp); + qreal dw = width > br.width() ? width - br.width() : 0; + qreal dh = (qreal)info->m_imageHeight; + + br.adjust(0, 0, dw, dh); + } + + return br; +} + +QRectF VPlainTextDocumentLayout::frameBoundingRect(QTextFrame *p_frame) const +{ + QRectF fr = QPlainTextDocumentLayout::frameBoundingRect(p_frame); + if (!m_blockImageEnabled) { + return fr; + } + + qreal imageWidth = (qreal)m_imageMgr->getMaximumImageWidth(); + qreal dw = imageWidth - fr.width(); + if (dw > 0) { + fr.adjust(0, 0, dw, 0); + } + + return fr; +} + +QSizeF VPlainTextDocumentLayout::documentSize() const +{ + QSizeF si = QPlainTextDocumentLayout::documentSize(); + if (!m_blockImageEnabled) { + return si; + } + + qreal imageWidth = (qreal)m_imageMgr->getMaximumImageWidth(); + if (imageWidth > si.width()) { + si.setWidth(imageWidth); + } + + return si; +} diff --git a/src/vplaintextedit.h b/src/vplaintextedit.h new file mode 100644 index 00000000..031777c9 --- /dev/null +++ b/src/vplaintextedit.h @@ -0,0 +1,179 @@ +#ifndef VPLAINTEXTEDIT_H +#define VPLAINTEXTEDIT_H + +#include +#include +#include + +#include "vlinenumberarea.h" + +class QTextDocument; +class VImageResourceManager; +class QPaintEvent; +class QPainter; +class QResizeEvent; + + +struct VBlockImageInfo +{ +public: + VBlockImageInfo() + : m_blockNumber(-1), m_margin(0), m_imageWidth(0), m_imageHeight(0) + { + } + + VBlockImageInfo(int p_blockNumber, + const QString &p_imageName, + int p_margin = 0) + : m_blockNumber(p_blockNumber), + m_imageName(p_imageName), + m_margin(p_margin), + m_imageWidth(0), + m_imageHeight(0) + { + } + + // Block number. + int m_blockNumber; + + // The name of the image corresponding to this block. + QString m_imageName; + + // Left margin of the image. + int m_margin; + +private: + // Width and height of the image display. + int m_imageWidth; + int m_imageHeight; + + friend class VImageResourceManager; + friend class VPlainTextEdit; + friend class VPlainTextDocumentLayout; +}; + + +class VPlainTextEdit : public QPlainTextEdit, public VTextEditWithLineNumber +{ + Q_OBJECT +public: + explicit VPlainTextEdit(QWidget *p_parent = nullptr); + + explicit VPlainTextEdit(const QString &p_text, QWidget *p_parent = nullptr); + + virtual ~VPlainTextEdit(); + + // Update images of these given blocks. + // Images of blocks not given here will be clear. + void updateBlockImages(const QVector &p_blocksInfo); + + void clearBlockImages(); + + // Whether the resoruce manager contains image of name @p_imageName. + bool containsImage(const QString &p_imageName) const; + + // Add an image to the resources. + void addImage(const QString &p_imageName, const QPixmap &p_image); + + void setBlockImageEnabled(bool p_enabled); + + void setImageWidthConstrainted(bool p_enabled); + + void paintLineNumberArea(QPaintEvent *p_event) Q_DECL_OVERRIDE; + + void setLineNumberType(LineNumberType p_type); + + // The minimum width of an image in pixels. + static const int c_minimumImageWidth; + +protected: + // Most logics are copied from QPlainTextEdit. + // Differences: draw images for blocks with preview image. + void paintEvent(QPaintEvent *p_event) Q_DECL_OVERRIDE; + + void resizeEvent(QResizeEvent *p_event) Q_DECL_OVERRIDE; + +private slots: + // Update viewport margin to hold the line number area. + void updateLineNumberAreaMargin(); + + void updateLineNumberArea(); + +private: + void init(); + + // @p_blockRect: the content rect of @p_block. + void drawImageOfBlock(const QTextBlock &p_block, + QPainter *p_painter, + const QRectF &p_blockRect); + + QRectF originalBlockBoundingRect(const QTextBlock &p_block) const; + + VPlainTextDocumentLayout *getLayout() const; + + // Widget to display line number area. + VLineNumberArea *m_lineNumberArea; + + VImageResourceManager *m_imageMgr; + + bool m_blockImageEnabled; + + // Whether constraint the width of image to the width of the viewport. + bool m_imageWidthConstrainted; + + // Maximum width of the images. + int m_maximumImageWidth; + + LineNumberType m_lineNumberType; +}; + + +class VPlainTextDocumentLayout : public QPlainTextDocumentLayout +{ + Q_OBJECT +public: + explicit VPlainTextDocumentLayout(QTextDocument *p_document, + VImageResourceManager *p_imageMgr, + bool p_blockImageEnabled = false); + + // Will adjust the rect if there is an image for this block. + QRectF blockBoundingRect(const QTextBlock &p_block) const Q_DECL_OVERRIDE; + + QRectF frameBoundingRect(QTextFrame *p_frame) const Q_DECL_OVERRIDE; + + QSizeF documentSize() const Q_DECL_OVERRIDE; + + void setBlockImageEnabled(bool p_enabled); + + void setMaximumImageWidth(int p_width); + +private: + VImageResourceManager *m_imageMgr; + + bool m_blockImageEnabled; + + int m_maximumImageWidth; +}; + +inline void VPlainTextDocumentLayout::setBlockImageEnabled(bool p_enabled) +{ + m_blockImageEnabled = p_enabled; +} + +inline void VPlainTextDocumentLayout::setMaximumImageWidth(int p_width) +{ + m_maximumImageWidth = p_width; +} + +inline void VPlainTextEdit::setLineNumberType(LineNumberType p_type) +{ + if (p_type == m_lineNumberType) { + return; + } + + m_lineNumberType = p_type; + + updateLineNumberArea(); +} + +#endif // VPLAINTEXTEDIT_H