Browse Source

add VPlainTextEdit with custom layout

- Support block images;
- Support line number;
- Do NOT support line distance height due to constraint of QPlainTextEdit.
Le Tan 8 years ago
parent
commit
5abcb1a8d9

+ 8 - 2
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 \

+ 5 - 0
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);
+}

+ 1 - 1
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);

+ 132 - 0
src/vimageresourcemanager.cpp

@@ -0,0 +1,132 @@
+#include "vimageresourcemanager.h"
+
+#include <QDebug>
+
+#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<VBlockImageInfo> &p_blocksInfo,
+                                             int p_maximumWidth)
+{
+    QSet<QString> 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;
+    }
+}

+ 63 - 0
src/vimageresourcemanager.h

@@ -0,0 +1,63 @@
+#ifndef VIMAGERESOURCEMANAGER_H
+#define VIMAGERESOURCEMANAGER_H
+
+#include <QHash>
+#include <QString>
+#include <QPixmap>
+#include <QTextBlock>
+#include <QVector>
+
+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<VBlockImageInfo> &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<QString, QPixmap> m_images;
+
+    // Image info of all the blocks with image.
+    QHash<int, VBlockImageInfo> m_blocksInfo;
+
+    // Maximum width of all images from m_blocksInfo.
+    int m_maximumImageWidth;
+};
+
+inline int VImageResourceManager::getMaximumImageWidth() const
+{
+    return m_maximumImageWidth;
+}
+
+#endif // VIMAGERESOURCEMANAGER_H

+ 42 - 0
src/vlinenumberarea.cpp

@@ -0,0 +1,42 @@
+#include "vlinenumberarea.h"
+
+#include <QPaintEvent>
+#include <QTextDocument>
+
+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<VLineNumberArea *>(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<VLineNumberArea *>(this)->m_width = width;
+
+    return m_width;
+}

+ 95 - 0
src/vlinenumberarea.h

@@ -0,0 +1,95 @@
+#ifndef VLINENUMBERAREA_H
+#define VLINENUMBERAREA_H
+
+#include <QWidget>
+#include <QColor>
+
+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

+ 581 - 0
src/vplaintextedit.cpp

@@ -0,0 +1,581 @@
+#include "vplaintextedit.h"
+
+#include <QTextDocument>
+#include <QPainter>
+#include <QPaintEvent>
+#include <QResizeEvent>
+#include <QDebug>
+#include <QScrollBar>
+
+#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<VBlockImageInfo> &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<QGradient *>(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<QTextLayout::FormatRange> 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<VPlainTextDocumentLayout *>(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;
+}

+ 179 - 0
src/vplaintextedit.h

@@ -0,0 +1,179 @@
+#ifndef VPLAINTEXTEDIT_H
+#define VPLAINTEXTEDIT_H
+
+#include <QPlainTextEdit>
+#include <QPlainTextDocumentLayout>
+#include <QTextBlock>
+
+#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<VBlockImageInfo> &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