Browse Source

refactor VImagePreviewer

Le Tan 8 years ago
parent
commit
c3408769b0

+ 73 - 8
src/hgmarkdownhighlighter.cpp

@@ -5,6 +5,7 @@
 #include "hgmarkdownhighlighter.h"
 #include "vconfigmanager.h"
 #include "utils/vutils.h"
+#include "vtextblockdata.h"
 
 extern VConfigManager *g_config;
 
@@ -79,6 +80,17 @@ HGMarkdownHighlighter::~HGMarkdownHighlighter()
     }
 }
 
+void HGMarkdownHighlighter::updateBlockUserData(const QString &p_text)
+{
+    VTextBlockData *blockData = dynamic_cast<VTextBlockData *>(currentBlockUserData());
+    if (!blockData) {
+        blockData = new VTextBlockData();
+        setCurrentBlockUserData(blockData);
+    }
+
+    blockData->setContainsPreviewImage(p_text.contains(QChar::ObjectReplacementCharacter));
+}
+
 void HGMarkdownHighlighter::highlightBlock(const QString &text)
 {
     int blockNum = currentBlock().blockNumber();
@@ -94,6 +106,9 @@ void HGMarkdownHighlighter::highlightBlock(const QString &text)
     // We use PEG Markdown Highlight as the main highlighter.
     // We can use other highlighting methods to complement it.
 
+    // Set current block's user data.
+    updateBlockUserData(text);
+
     // If it is a block inside HTML comment, just skip it.
     if (isBlockInsideCommentRegion(currentBlock())) {
         setCurrentBlockState(HighlightBlockState::Comment);
@@ -105,7 +120,9 @@ void HGMarkdownHighlighter::highlightBlock(const QString &text)
     highlightCodeBlock(text);
 
     // PEG Markdown Highlight does not handle links with spaces in the URL.
-    highlightLinkWithSpacesInURL(text);
+    // Links in the URL should be encoded to %20. We just let it be here and won't
+    // fix this.
+    // highlightLinkWithSpacesInURL(text);
 
     // Highlight CodeBlock using VCodeBlockHighlightHelper.
     if (m_codeBlockHighlights.size() > blockNum) {
@@ -176,6 +193,7 @@ void HGMarkdownHighlighter::initBlockHighlightFromResult(int nrBlocks)
 
 void HGMarkdownHighlighter::initHtmlCommentRegionsFromResult()
 {
+    // From Qt5.7, the capacity is preserved.
     m_commentRegions.clear();
 
     if (!result) {
@@ -189,12 +207,54 @@ void HGMarkdownHighlighter::initHtmlCommentRegionsFromResult()
             continue;
         }
 
-        m_commentRegions.push_back(VCommentRegion(elem->pos, elem->end));
+        m_commentRegions.push_back(VElementRegion(elem->pos, elem->end));
+
+        elem = elem->next;
+    }
+
+    qDebug() << "highlighter: parse" << m_commentRegions.size() << "HTML comment regions";
+}
+
+void HGMarkdownHighlighter::initImageRegionsFromResult()
+{
+    if (!result) {
+        // From Qt5.7, the capacity is preserved.
+        m_imageRegions.clear();
+        emit imageLinksUpdated(m_imageRegions);
+        return;
+    }
+
+    int idx = 0;
+    int oriSize = m_imageRegions.size();
+    pmh_element *elem = result[pmh_IMAGE];
+    while (elem != NULL) {
+        if (elem->end <= elem->pos) {
+            elem = elem->next;
+            continue;
+        }
+
+        if (idx < oriSize) {
+            // Try to reuse the original element.
+            VElementRegion &reg = m_imageRegions[idx];
+            if ((int)elem->pos != reg.m_startPos || (int)elem->end != reg.m_endPos) {
+                reg.m_startPos = (int)elem->pos;
+                reg.m_endPos = (int)elem->end;
+            }
+        } else {
+            m_imageRegions.push_back(VElementRegion(elem->pos, elem->end));
+        }
 
+        ++idx;
         elem = elem->next;
     }
 
-    qDebug() << "highlighter:" << m_commentRegions.size() << "HTML comment regions";
+    if (idx < oriSize) {
+        m_imageRegions.resize(idx);
+    }
+
+    emit imageLinksUpdated(m_imageRegions);
+
+    qDebug() << "highlighter: parse" << m_imageRegions.size() << "image regions";
 }
 
 void HGMarkdownHighlighter::initBlockHighlihgtOne(unsigned long pos, unsigned long end, int styleIndex)
@@ -274,6 +334,7 @@ void HGMarkdownHighlighter::highlightLinkWithSpacesInURL(const QString &p_text)
     if (currentBlockState() == HighlightBlockState::CodeBlock) {
         return;
     }
+
     // TODO: should select links with spaces in URL.
     QRegExp regExp("[\\!]?\\[[^\\]]*\\]\\(([^\\n\\)]+)\\)");
     int index = regExp.indexIn(p_text);
@@ -298,23 +359,27 @@ void HGMarkdownHighlighter::parse()
         return;
     }
 
-    int nrBlocks = document->blockCount();
-    parseInternal();
-
     if (highlightingStyles.isEmpty()) {
-        qWarning() << "HighlightingStyles is not set";
-        return;
+        goto exit;
     }
 
+    {
+    int nrBlocks = document->blockCount();
+    parseInternal();
+
     initBlockHighlightFromResult(nrBlocks);
 
     initHtmlCommentRegionsFromResult();
 
+    initImageRegionsFromResult();
+
     if (result) {
         pmh_free_elements(result);
         result = NULL;
     }
+    }
 
+exit:
     parsing.store(0);
 }
 

+ 30 - 5
src/hgmarkdownhighlighter.h

@@ -83,12 +83,12 @@ struct HLUnitPos
     QString m_style;
 };
 
-// HTML comment.
-struct VCommentRegion
+// Denote the region of a certain Markdown element.
+struct VElementRegion
 {
-    VCommentRegion() : m_startPos(0), m_endPos(0) {}
+    VElementRegion() : m_startPos(0), m_endPos(0) {}
 
-    VCommentRegion(int p_start, int p_end) : m_startPos(p_start), m_endPos(p_end) {}
+    VElementRegion(int p_start, int p_end) : m_startPos(p_start), m_endPos(p_end) {}
 
     // The start position of the region in document.
     int m_startPos;
@@ -101,6 +101,12 @@ struct VCommentRegion
     {
         return m_startPos <= p_pos && m_endPos >= p_pos;
     }
+
+    bool operator==(const VElementRegion &p_other) const
+    {
+        return (m_startPos == p_other.m_startPos
+                && m_endPos == p_other.m_endPos);
+    }
 };
 
 class HGMarkdownHighlighter : public QSyntaxHighlighter
@@ -118,8 +124,13 @@ public:
 
 signals:
     void highlightCompleted();
+
+    // QList is implicitly shared.
     void codeBlocksUpdated(const QList<VCodeBlock> &p_codeBlocks);
 
+    // Emitted when image regions have been fetched from a new parsing result.
+    void imageLinksUpdated(const QVector<VElementRegion> &p_imageRegions);
+
 protected:
     void highlightBlock(const QString &text) Q_DECL_OVERRIDE;
 
@@ -151,7 +162,10 @@ private:
     int m_numOfCodeBlockHighlightsToRecv;
 
     // All HTML comment regions.
-    QVector<VCommentRegion> m_commentRegions;
+    QVector<VElementRegion> m_commentRegions;
+
+    // All image link regions.
+    QVector<VElementRegion> m_imageRegions;
 
     // Timer to signal highlightCompleted().
     QTimer *m_completeTimer;
@@ -168,7 +182,12 @@ private:
 
     void resizeBuffer(int newCap);
     void highlightCodeBlock(const QString &text);
+
+    // Highlight links using regular expression.
+    // PEG Markdown Highlight treat URLs with spaces illegal. This function is
+    // intended to complement this.
     void highlightLinkWithSpacesInURL(const QString &p_text);
+
     void parse();
     void parseInternal();
     void initBlockHighlightFromResult(int nrBlocks);
@@ -182,11 +201,17 @@ private:
     // Fetch all the HTML comment regions from parsing result.
     void initHtmlCommentRegionsFromResult();
 
+    // Fetch all the image link regions from parsing result.
+    void initImageRegionsFromResult();
+
     // Whether @p_block is totally inside a HTML comment.
     bool isBlockInsideCommentRegion(const QTextBlock &p_block) const;
 
     // Highlights have been changed. Try to signal highlightCompleted().
     void highlightChanged();
+
+    // Set the user data of currentBlock().
+    void updateBlockUserData(const QString &p_text);
 };
 
 #endif

+ 6 - 2
src/src.pro

@@ -71,7 +71,9 @@ SOURCES += main.cpp\
     vbuttonwithwidget.cpp \
     vtabindicator.cpp \
     dialog/vupdater.cpp \
-    dialog/vorphanfileinfodialog.cpp
+    dialog/vorphanfileinfodialog.cpp \
+    vtextblockdata.cpp \
+    utils/vpreviewutils.cpp
 
 HEADERS  += vmainwindow.h \
     vdirectorytree.h \
@@ -130,7 +132,9 @@ HEADERS  += vmainwindow.h \
     vedittabinfo.h \
     vtabindicator.h \
     dialog/vupdater.h \
-    dialog/vorphanfileinfodialog.h
+    dialog/vorphanfileinfodialog.h \
+    vtextblockdata.h \
+    utils/vpreviewutils.h
 
 RESOURCES += \
     vnote.qrc \

+ 27 - 1
src/utils/veditutils.cpp

@@ -116,6 +116,32 @@ bool VEditUtils::indentBlockAsPreviousBlock(QTextCursor &p_cursor)
     return changed;
 }
 
+bool VEditUtils::hasSameIndent(const QTextBlock &p_blocka, const QTextBlock &p_blockb)
+{
+    int nonSpaceIdxa = 0;
+    int nonSpaceIdxb = 0;
+
+    QString texta = p_blocka.text();
+    for (int i = 0; i < texta.size(); ++i) {
+        if (!texta[i].isSpace()) {
+            nonSpaceIdxa = i;
+            break;
+        }
+    }
+
+    QString textb = p_blockb.text();
+    for (int i = 0; i < textb.size(); ++i) {
+        if (!textb[i].isSpace()) {
+            nonSpaceIdxb = i;
+            break;
+        } else if (i >= nonSpaceIdxa || texta[i] != textb[i]) {
+            return false;
+        }
+    }
+
+    return nonSpaceIdxa == nonSpaceIdxb;
+}
+
 void VEditUtils::moveCursorFirstNonSpaceCharacter(QTextCursor &p_cursor,
                                                   QTextCursor::MoveMode p_mode)
 {
@@ -135,7 +161,7 @@ void VEditUtils::moveCursorFirstNonSpaceCharacter(QTextCursor &p_cursor,
 
 void VEditUtils::removeObjectReplacementCharacter(QString &p_text)
 {
-    QRegExp orcBlockExp(QString("[\\n|^][ |\\t]*\\xfffc[ |\\t]*(?=\\n)"));
+    QRegExp orcBlockExp(VUtils::c_previewImageBlockRegExp);
     p_text.remove(orcBlockExp);
     p_text.remove(QChar::ObjectReplacementCharacter);
 }

+ 3 - 0
src/utils/veditutils.h

@@ -29,6 +29,9 @@ public:
     // @p_cursor will be placed at the position after inserting leading spaces.
     static bool indentBlockAsPreviousBlock(QTextCursor &p_cursor);
 
+    // Returns true if two blocks has the same indent.
+    static bool hasSameIndent(const QTextBlock &p_blocka, const QTextBlock &p_blockb);
+
     // Insert a new block at current position with the same indentation as
     // current block. Should clear the selection before calling this.
     // Returns true if non-empty indentation has been inserted.

+ 61 - 0
src/utils/vpreviewutils.cpp

@@ -0,0 +1,61 @@
+#include "vpreviewutils.h"
+
+#include <QTextDocument>
+#include <QTextCursor>
+
+QTextImageFormat VPreviewUtils::fetchFormatFromPosition(QTextDocument *p_doc,
+                                                        int p_position)
+{
+    if (p_doc->characterAt(p_position) != QChar::ObjectReplacementCharacter) {
+        return QTextImageFormat();
+    }
+
+    QTextCursor cursor(p_doc);
+    cursor.setPosition(p_position);
+    if (cursor.atBlockEnd()) {
+        return QTextImageFormat();
+    }
+
+    cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, 1);
+
+    return cursor.charFormat().toImageFormat();
+}
+
+PreviewImageType VPreviewUtils::getPreviewImageType(const QTextImageFormat &p_format)
+{
+    Q_ASSERT(p_format.isValid());
+    bool ok = true;
+    int type = p_format.property((int)ImageProperty::ImageType).toInt(&ok);
+    if (ok) {
+        return (PreviewImageType)type;
+    } else {
+        return PreviewImageType::Invalid;
+    }
+}
+
+PreviewImageSource VPreviewUtils::getPreviewImageSource(const QTextImageFormat &p_format)
+{
+    Q_ASSERT(p_format.isValid());
+    bool ok = true;
+    int src = p_format.property((int)ImageProperty::ImageSource).toInt(&ok);
+    if (ok) {
+        return (PreviewImageSource)src;
+    } else {
+        return PreviewImageSource::Invalid;
+    }
+}
+
+long long VPreviewUtils::getPreviewImageID(const QTextImageFormat &p_format)
+{
+    if (!p_format.isValid()) {
+        return -1;
+    }
+
+    bool ok = true;
+    long long id = p_format.property((int)ImageProperty::ImageID).toLongLong(&ok);
+    if (ok) {
+        return id;
+    } else {
+        return -1;
+    }
+}

+ 28 - 0
src/utils/vpreviewutils.h

@@ -0,0 +1,28 @@
+#ifndef VPREVIEWUTILS_H
+#define VPREVIEWUTILS_H
+
+#include <QTextImageFormat>
+#include "vconstants.h"
+
+class QTextDocument;
+
+class VPreviewUtils
+{
+public:
+    // Fetch the text image format from an image preview position.
+    static QTextImageFormat fetchFormatFromPosition(QTextDocument *p_doc,
+                                                    int p_position);
+
+    static PreviewImageType getPreviewImageType(const QTextImageFormat &p_format);
+
+    static PreviewImageSource getPreviewImageSource(const QTextImageFormat &p_format);
+
+    // Fetch the ImageID from an image format.
+    // Returns -1 if not valid.
+    static long long getPreviewImageID(const QTextImageFormat &p_format);
+
+private:
+    VPreviewUtils() {}
+};
+
+#endif // VPREVIEWUTILS_H

+ 12 - 0
src/utils/vutils.cpp

@@ -36,6 +36,8 @@ const QString VUtils::c_fencedCodeBlockStartRegExp = QString("^(\\s*)```([^`\\s]
 
 const QString VUtils::c_fencedCodeBlockEndRegExp = QString("^(\\s*)```$");
 
+const QString VUtils::c_previewImageBlockRegExp = QString("[\\n|^][ |\\t]*\\xfffc[ |\\t]*(?=\\n)");
+
 VUtils::VUtils()
 {
 }
@@ -684,3 +686,13 @@ bool VUtils::splitPathInBasePath(const QString &p_base,
     qDebug() << QString("split path %1 based on %2 to %3 parts").arg(p_path).arg(p_base).arg(p_parts.size());
     return true;
 }
+
+void VUtils::decodeUrl(QString &p_url)
+{
+    QHash<QString, QString> maps;
+    maps.insert("%20", " ");
+
+    for (auto it = maps.begin(); it != maps.end(); ++it) {
+        p_url.replace(it.key(), it.value());
+    }
+}

+ 7 - 1
src/utils/vutils.h

@@ -53,7 +53,7 @@ public:
     static void processStyle(QString &style, const QVector<QPair<QString, QString> > &varMap);
 
     // Return the last directory name of @p_path.
-    static inline QString directoryNameFromPath(const QString& p_path);
+    static QString directoryNameFromPath(const QString& p_path);
 
     // Return the file name of @p_path.
     // /home/tamlok/abc, /home/tamlok/abc/ will both return abc.
@@ -118,6 +118,9 @@ public:
                                     const QString &p_path,
                                     QStringList &p_parts);
 
+    // Decode URL by simply replacing meta-characters.
+    static void decodeUrl(QString &p_url);
+
     // Regular expression for image link.
     // ![image title]( http://github.com/tamlok/vnote.jpg "alt \" text" )
     // Captured texts (need to be trimmed):
@@ -135,6 +138,9 @@ public:
     static const QString c_fencedCodeBlockStartRegExp;
     static const QString c_fencedCodeBlockEndRegExp;
 
+    // Regular expression for preview image block.
+    static const QString c_previewImageBlockRegExp;
+
 private:
     VUtils();
 

+ 11 - 0
src/vconstants.h

@@ -50,4 +50,15 @@ enum FindOption
     IncrementalSearch = 0x8U
 };
 
+enum class ImageProperty {/* ID of the image preview (long long). Unique for each source. */
+                          ImageID = 1,
+                          /* Source type of the preview, such as image, codeblock. */
+                          ImageSource,
+                          /* Type of the preview, block or inline. */
+                          ImageType };
+
+enum class PreviewImageType { Block, Inline, Invalid };
+
+enum class PreviewImageSource { Image, CodeBlock, Invalid };
+
 #endif

+ 8 - 0
src/vedit.cpp

@@ -785,6 +785,8 @@ void VEdit::contextMenuEvent(QContextMenuEvent *p_event)
         }
     }
 
+    alterContextMenu(menu, actions);
+
     menu->exec(p_event->globalPos());
     delete menu;
 }
@@ -1256,3 +1258,9 @@ bool VEdit::isBlockVisible(const QTextBlock &p_block)
 
     return (y >= 0 && y < height) || (y < 0 && y + rectHeight > 0);
 }
+
+void VEdit::alterContextMenu(QMenu *p_menu, const QList<QAction *> &p_actions)
+{
+    Q_UNUSED(p_menu);
+    Q_UNUSED(p_actions);
+}

+ 3 - 0
src/vedit.h

@@ -203,6 +203,9 @@ protected:
     // Update m_config according to VConfigManager.
     void updateConfig();
 
+    // Called in contextMenuEvent() to modify the context menu.
+    virtual void alterContextMenu(QMenu *p_menu, const QList<QAction *> &p_actions);
+
 private:
     QLabel *m_wrapLabel;
     QTimer *m_labelTimer;

+ 391 - 329
src/vimagepreviewer.cpp

@@ -5,416 +5,425 @@
 #include <QDebug>
 #include <QDir>
 #include <QUrl>
+#include <QVector>
 #include "vmdedit.h"
 #include "vconfigmanager.h"
 #include "utils/vutils.h"
 #include "utils/veditutils.h"
+#include "utils/vpreviewutils.h"
 #include "vfile.h"
 #include "vdownloader.h"
 #include "hgmarkdownhighlighter.h"
+#include "vtextblockdata.h"
 
 extern VConfigManager *g_config;
 
-enum ImageProperty { ImagePath = 1 };
-
 const int VImagePreviewer::c_minImageWidth = 100;
 
-VImagePreviewer::VImagePreviewer(VMdEdit *p_edit, int p_timeToPreview)
+VImagePreviewer::VImagePreviewer(VMdEdit *p_edit)
     : QObject(p_edit), m_edit(p_edit), m_document(p_edit->document()),
-      m_file(p_edit->getFile()), m_enablePreview(true), m_isPreviewing(false),
-      m_requestCearBlocks(false), m_requestRefreshBlocks(false),
-      m_updatePending(false), m_imageWidth(c_minImageWidth)
+      m_file(p_edit->getFile()), m_imageWidth(c_minImageWidth),
+      m_timeStamp(0), m_previewIndex(0),
+      m_previewEnabled(g_config->getEnablePreviewImages()), m_isPreviewing(false)
 {
-    m_timer = new QTimer(this);
-    m_timer->setSingleShot(true);
-    Q_ASSERT(p_timeToPreview > 0);
-    m_timer->setInterval(p_timeToPreview);
-
-    connect(m_timer, &QTimer::timeout,
-            this, &VImagePreviewer::timerTimeout);
+    m_updateTimer = new QTimer(this);
+    m_updateTimer->setSingleShot(true);
+    m_updateTimer->setInterval(400);
+    connect(m_updateTimer, &QTimer::timeout,
+            this, &VImagePreviewer::doUpdatePreviewImageWidth);
 
     m_downloader = new VDownloader(this);
     connect(m_downloader, &VDownloader::downloadFinished,
             this, &VImagePreviewer::imageDownloaded);
+}
 
-    connect(m_edit->document(), &QTextDocument::contentsChange,
-            this, &VImagePreviewer::handleContentChange);
+void VImagePreviewer::imageLinksChanged(const QVector<VElementRegion> &p_imageRegions)
+{
+    kickOffPreview(p_imageRegions);
 }
 
-void VImagePreviewer::timerTimeout()
+void VImagePreviewer::kickOffPreview(const QVector<VElementRegion> &p_imageRegions)
 {
-    if (!g_config->getEnablePreviewImages()) {
-        if (m_enablePreview) {
-            disableImagePreview();
-        }
+    if (!m_previewEnabled) {
+        Q_ASSERT(m_imageRegions.isEmpty());
+        Q_ASSERT(m_previewImages.isEmpty());
+        Q_ASSERT(m_imageCache.isEmpty());
         return;
     }
 
-    if (!m_enablePreview) {
-        return;
-    }
+    m_isPreviewing = true;
 
-    if (m_isPreviewing) {
-        m_updatePending = true;
-        return;
-    }
+    m_imageRegions = p_imageRegions;
+    ++m_timeStamp;
 
     previewImages();
+
+    shrinkImageCache();
+    m_isPreviewing = false;
 }
 
-void VImagePreviewer::handleContentChange(int /* p_position */,
-                                          int p_charsRemoved,
-                                          int p_charsAdded)
+void VImagePreviewer::previewImages()
 {
-    if (p_charsRemoved == 0 && p_charsAdded == 0) {
-        return;
-    }
+    // Get the width of the m_edit.
+    m_imageWidth = qMax(m_edit->size().width() - 50, c_minImageWidth);
+
+    QVector<ImageLinkInfo> imageLinks;
+    fetchImageLinksFromRegions(imageLinks);
 
-    m_timer->stop();
-    m_timer->start();
+    QTextCursor cursor(m_document);
+    previewImageLinks(imageLinks, cursor);
+    clearObsoletePreviewImages(cursor);
 }
 
-bool VImagePreviewer::isNormalBlock(const QTextBlock &p_block)
+void VImagePreviewer::initImageFormat(QTextImageFormat &p_imgFormat,
+                                      const QString &p_imageName,
+                                      const PreviewImageInfo &p_info) const
 {
-    return p_block.userState() == HighlightBlockState::Normal;
+    p_imgFormat.setName(p_imageName);
+    p_imgFormat.setProperty((int)ImageProperty::ImageID, p_info.m_id);
+    p_imgFormat.setProperty((int)ImageProperty::ImageSource, (int)PreviewImageSource::Image);
+    p_imgFormat.setProperty((int)ImageProperty::ImageType,
+                            p_info.m_isBlock ? (int)PreviewImageType::Block
+                                             : (int)PreviewImageType::Inline);
 }
 
-void VImagePreviewer::previewImages()
+void VImagePreviewer::previewImageLinks(QVector<ImageLinkInfo> &p_imageLinks,
+                                        QTextCursor &p_cursor)
 {
-    if (m_isPreviewing) {
-        return;
-    }
+    bool hasNewPreview = false;
+    for (int i = 0; i < p_imageLinks.size(); ++i) {
+        ImageLinkInfo &link = p_imageLinks[i];
+        if (link.m_previewImageID > -1) {
+            continue;
+        }
 
-    // Get the width of the m_edit.
-    m_imageWidth = qMax(m_edit->size().width() - 50, c_minImageWidth);
+        QString imageName = imageCacheResourceName(link.m_linkUrl);
+        if (imageName.isEmpty()) {
+            continue;
+        }
 
-    m_isPreviewing = true;
-    QTextBlock block = m_document->begin();
-    while (block.isValid() && m_enablePreview) {
-        if (isImagePreviewBlock(block)) {
-            // Image preview block. Check if it is parentless.
-            if (!isValidImagePreviewBlock(block) || !isNormalBlock(block)) {
-                QTextBlock nblock = block.next();
-                removeBlock(block);
-                block = nblock;
-            } else {
-                block = block.next();
-            }
-        } else {
-            clearCorruptedImagePreviewBlock(block);
+        PreviewImageInfo info(m_previewIndex++, m_timeStamp,
+                              link.m_linkUrl, link.m_isBlock);
+        QTextImageFormat imgFormat;
+        initImageFormat(imgFormat, imageName, info);
 
-            if (isNormalBlock(block)) {
-                block = previewImageOfOneBlock(block);
-            } else {
-                block = block.next();
-            }
+        updateImageWidth(imgFormat);
+
+        bool isModified = m_edit->isModified();
+        p_cursor.joinPreviousEditBlock();
+        p_cursor.setPosition(link.m_endPos);
+        if (link.m_isBlock) {
+            p_cursor.movePosition(QTextCursor::EndOfBlock);
+            VEditUtils::insertBlockWithIndent(p_cursor);
         }
-    }
 
-    m_isPreviewing = false;
+        p_cursor.insertImage(imgFormat);
+        p_cursor.endEditBlock();
 
-    if (m_requestCearBlocks) {
-        m_requestCearBlocks = false;
-        clearAllImagePreviewBlocks();
-    }
+        m_edit->setModified(isModified);
 
-    if (m_requestRefreshBlocks) {
-        m_requestRefreshBlocks = false;
-        refresh();
-    }
+        Q_ASSERT(!m_previewImages.contains(info.m_id));
+        m_previewImages.insert(info.m_id, info);
+        link.m_previewImageID = info.m_id;
 
-    if (m_updatePending) {
-        m_updatePending = false;
-        m_timer->stop();
-        m_timer->start();
+        hasNewPreview = true;
+        qDebug() << "preview new image" << info.toString();
     }
 
-    emit m_edit->statusChanged();
+    if (hasNewPreview) {
+        emit m_edit->statusChanged();
+    }
 }
 
-bool VImagePreviewer::isImagePreviewBlock(const QTextBlock &p_block)
+void VImagePreviewer::clearObsoletePreviewImages(QTextCursor &p_cursor)
 {
-    if (!p_block.isValid()) {
-        return false;
+    // Clean up the hash.
+    for (auto it = m_previewImages.begin(); it != m_previewImages.end();) {
+        PreviewImageInfo &info = it.value();
+        if (info.m_timeStamp != m_timeStamp) {
+            qDebug() << "obsolete preview image" << info.toString();
+            it = m_previewImages.erase(it);
+        } else {
+            ++it;
+        }
     }
 
-    QString text = p_block.text().trimmed();
-    return text == QString(QChar::ObjectReplacementCharacter);
+    bool hasObsolete = false;
+    QTextBlock block = m_document->begin();
+    // Clean block data and delete obsolete preview.
+    while (block.isValid()) {
+        if (!VTextBlockData::containsPreviewImage(block)) {
+            block = block.next();
+            continue;
+        } else {
+            QTextBlock nextBlock = block.next();
+            // Notice the short circuit.
+            hasObsolete = clearObsoletePreviewImagesOfBlock(block, p_cursor) || hasObsolete;
+            block = nextBlock;
+        }
+    }
+
+    if (hasObsolete) {
+        emit m_edit->statusChanged();
+    }
 }
 
-bool VImagePreviewer::isValidImagePreviewBlock(QTextBlock &p_block)
+bool VImagePreviewer::isImageSourcePreviewImage(const QTextImageFormat &p_format) const
 {
-    if (!isImagePreviewBlock(p_block)) {
+    if (!p_format.isValid()) {
         return false;
     }
 
-    // It is a valid image preview block only if the previous block is a block
-    // need to preview (containing exactly one image) and the image paths are
-    // identical.
-    QTextBlock prevBlock = p_block.previous();
-    if (prevBlock.isValid()) {
-        QString imagePath = fetchImagePathToPreview(prevBlock.text());
-        if (imagePath.isEmpty()) {
-            return false;
-        }
-
-        // Get image preview block's image path.
-        QString curPath = fetchImagePathFromPreviewBlock(p_block);
-
-        return curPath == imagePath;
+    bool ok = true;
+    int src = p_format.property((int)ImageProperty::ImageSource).toInt(&ok);
+    if (ok) {
+        return src == (int)PreviewImageSource::Image;
     } else {
         return false;
     }
 }
 
-QString VImagePreviewer::fetchImageUrlToPreview(const QString &p_text)
+bool VImagePreviewer::clearObsoletePreviewImagesOfBlock(QTextBlock &p_block,
+                                                        QTextCursor &p_cursor)
 {
-    QRegExp regExp(VUtils::c_imageLinkRegExp);
-
-    int index = regExp.indexIn(p_text);
-    if (index == -1) {
-        return QString();
-    }
-
-    int lastIndex = regExp.lastIndexIn(p_text);
-    if (lastIndex != index) {
-        return QString();
-    }
+    QString text = p_block.text();
+    bool hasObsolete = false;
+    bool hasOtherChars = false;
+    bool hasValidPreview = false;
+    // From back to front.
+    for (int i = text.size() - 1; i >= 0; --i) {
+        if (text[i].isSpace()) {
+            continue;
+        }
 
-    return regExp.capturedTexts()[2].trimmed();
-}
+        if (text[i] == QChar::ObjectReplacementCharacter) {
+            int pos = p_block.position() + i;
+            Q_ASSERT(m_document->characterAt(pos) == QChar::ObjectReplacementCharacter);
 
-QString VImagePreviewer::fetchImagePathToPreview(const QString &p_text)
-{
-    QString imageUrl = fetchImageUrlToPreview(p_text);
-    if (imageUrl.isEmpty()) {
-        return imageUrl;
-    }
+            QTextImageFormat imageFormat = VPreviewUtils::fetchFormatFromPosition(m_document, pos);
+            if (!isImageSourcePreviewImage(imageFormat)) {
+                hasValidPreview = true;
+                continue;
+            }
 
-    QString imagePath;
-    QFileInfo info(m_file->retriveBasePath(), imageUrl);
-    if (info.exists()) {
-        if (info.isNativePath()) {
-            // Local file.
-            imagePath = QDir::cleanPath(info.absoluteFilePath());
+            long long imageID = VPreviewUtils::getPreviewImageID(imageFormat);
+            auto it = m_previewImages.find(imageID);
+            if (it == m_previewImages.end()) {
+                // It is obsolete since we can't find it in the cache.
+                qDebug() << "remove obsolete preview image" << imageID;
+                bool isModified = m_edit->isModified();
+                p_cursor.joinPreviousEditBlock();
+                p_cursor.setPosition(pos);
+                p_cursor.deleteChar();
+                p_cursor.endEditBlock();
+                m_edit->setModified(isModified);
+                hasObsolete = true;
+            } else {
+                hasValidPreview = true;
+            }
         } else {
-            imagePath = imageUrl;
+            hasOtherChars = true;
         }
-    } else {
-        QUrl url(imageUrl);
-        imagePath = url.toString();
-    }
-
-    return imagePath;
-}
-
-QTextBlock VImagePreviewer::previewImageOfOneBlock(QTextBlock &p_block)
-{
-    if (!p_block.isValid()) {
-        return p_block;
     }
 
-    QTextBlock nblock = p_block.next();
-
-    QString imagePath = fetchImagePathToPreview(p_block.text());
-    if (imagePath.isEmpty()) {
-        return nblock;
+    if (hasObsolete && !hasOtherChars && !hasValidPreview) {
+        // Delete the whole block.
+        qDebug() << "delete a preview block" << p_block.blockNumber();
+        bool isModified = m_edit->isModified();
+        p_cursor.joinPreviousEditBlock();
+        p_cursor.setPosition(p_block.position());
+        VEditUtils::removeBlock(p_cursor);
+        p_cursor.endEditBlock();
+        m_edit->setModified(isModified);
     }
 
-    qDebug() << "block" << p_block.blockNumber() << imagePath;
-
-    if (isImagePreviewBlock(nblock)) {
-        QTextBlock nextBlock = nblock.next();
-        updateImagePreviewBlock(nblock, imagePath);
-
-        return nextBlock;
-    } else {
-        QTextBlock imgBlock = insertImagePreviewBlock(p_block, imagePath);
-
-        return imgBlock.next();
-    }
+    return hasObsolete;
 }
 
-QTextBlock VImagePreviewer::insertImagePreviewBlock(QTextBlock &p_block,
-                                                    const QString &p_imagePath)
+// Returns true if p_text[p_start, p_end) is all spaces.
+static bool isAllSpaces(const QString &p_text, int p_start, int p_end)
 {
-    QString imageName = imageCacheResourceName(p_imagePath);
-    if (imageName.isEmpty()) {
-        return p_block;
+    for (int i = p_start; i < p_end && i < p_text.size(); ++i) {
+        if (!p_text[i].isSpace()) {
+            return false;
+        }
     }
 
-    bool modified = m_edit->isModified();
-
-    QTextCursor cursor(p_block);
-    cursor.beginEditBlock();
-    cursor.movePosition(QTextCursor::EndOfBlock);
-    cursor.insertBlock();
-
-    QTextImageFormat imgFormat;
-    imgFormat.setName(imageName);
-    imgFormat.setProperty(ImagePath, p_imagePath);
-
-    updateImageWidth(imgFormat);
-
-    cursor.insertImage(imgFormat);
-    cursor.endEditBlock();
-
-    V_ASSERT(cursor.block().text().at(0) == QChar::ObjectReplacementCharacter);
-
-    m_edit->setModified(modified);
-
-    return cursor.block();
+    return true;
 }
 
-void VImagePreviewer::updateImagePreviewBlock(QTextBlock &p_block,
-                                              const QString &p_imagePath)
+void VImagePreviewer::fetchImageLinksFromRegions(QVector<ImageLinkInfo> &p_imageLinks)
 {
-    QTextImageFormat format = fetchFormatFromPreviewBlock(p_block);
-    V_ASSERT(format.isValid());
-    QString curPath = format.property(ImagePath).toString();
-    QString imageName;
-
-    if (curPath == p_imagePath) {
-        if (updateImageWidth(format)) {
-            goto update;
-        }
+    p_imageLinks.clear();
 
+    if (m_imageRegions.isEmpty()) {
         return;
     }
 
-    // Update it with the new image.
-    imageName = imageCacheResourceName(p_imagePath);
-    if (imageName.isEmpty()) {
-        // Delete current preview block.
-        removeBlock(p_block);
-        return;
-    }
-
-    format.setName(imageName);
-    format.setProperty(ImagePath, p_imagePath);
+    p_imageLinks.reserve(m_imageRegions.size());
 
-    updateImageWidth(format);
+    for (int i = 0; i < m_imageRegions.size(); ++i) {
+        VElementRegion &reg = m_imageRegions[i];
+        QTextBlock block = m_document->findBlock(reg.m_startPos);
+        if (!block.isValid()) {
+            continue;
+        }
 
-update:
-    updateFormatInPreviewBlock(p_block, format);
-}
+        int blockStart = block.position();
+        int blockEnd = blockStart + block.length() - 1;
+        QString text = block.text();
+        Q_ASSERT(reg.m_endPos <= blockEnd);
+        ImageLinkInfo info(reg.m_startPos, reg.m_endPos);
+        if ((reg.m_startPos == blockStart
+             || isAllSpaces(text, 0, reg.m_startPos - blockStart))
+            && (reg.m_endPos == blockEnd
+                || isAllSpaces(text, reg.m_endPos - blockStart, blockEnd - blockStart))) {
+            // Image block.
+            info.m_isBlock = true;
+            info.m_linkUrl = fetchImagePathToPreview(text);
+        } else {
+            // Inline image.
+            info.m_isBlock = false;
+            info.m_linkUrl = fetchImagePathToPreview(text.mid(reg.m_startPos - blockStart,
+                                                              reg.m_endPos - reg.m_startPos));
+        }
 
-void VImagePreviewer::removeBlock(QTextBlock &p_block)
-{
-    bool modified = m_edit->isModified();
+        // Check if this image link has been previewed previously.
+        info.m_previewImageID = isImageLinkPreviewed(info);
 
-    VEditUtils::removeBlock(p_block);
+        // Sorted in descending order of m_startPos.
+        p_imageLinks.append(info);
 
-    m_edit->setModified(modified);
+        qDebug() << "image region" << i << info.m_startPos << info.m_endPos
+                 << info.m_linkUrl << info.m_isBlock << info.m_previewImageID;
+    }
 }
 
-void VImagePreviewer::clearCorruptedImagePreviewBlock(QTextBlock &p_block)
+long long VImagePreviewer::isImageLinkPreviewed(const ImageLinkInfo &p_info)
 {
-    if (!p_block.isValid()) {
-        return;
-    }
-
-    QString text = p_block.text();
-    QVector<int> replacementChars;
-    bool onlySpaces = true;
-    for (int i = 0; i < text.size(); ++i) {
-        if (text[i] == QChar::ObjectReplacementCharacter) {
-            replacementChars.append(i);
-        } else if (!text[i].isSpace()) {
-            onlySpaces = false;
+    long long imageID = -1;
+    if (p_info.m_isBlock) {
+        QTextBlock block = m_document->findBlock(p_info.m_startPos);
+        QTextBlock nextBlock = block.next();
+        if (!nextBlock.isValid()) {
+            return imageID;
         }
-    }
 
-    if (!onlySpaces && !replacementChars.isEmpty()) {
-        // ObjectReplacementCharacter mixed with other non-space texts.
-        // Users corrupt the image preview block. Just remove the char.
-        bool modified = m_edit->isModified();
-
-        QTextCursor cursor(p_block);
-        cursor.beginEditBlock();
-        int blockPos = p_block.position();
-        for (int i = replacementChars.size() - 1; i >= 0; --i) {
-            int pos = replacementChars[i];
-            cursor.setPosition(blockPos + pos);
-            cursor.deleteChar();
+        if (!isImagePreviewBlock(nextBlock)) {
+            return imageID;
         }
-        cursor.endEditBlock();
 
-        m_edit->setModified(modified);
+        // Make sure the indentation is the same as @block.
+        if (VEditUtils::hasSameIndent(block, nextBlock)) {
+            QTextImageFormat format = fetchFormatFromPreviewBlock(nextBlock);
+            if (isImageSourcePreviewImage(format)) {
+                imageID = VPreviewUtils::getPreviewImageID(format);
+            }
+        }
+    } else {
+        QTextImageFormat format = VPreviewUtils::fetchFormatFromPosition(m_document, p_info.m_endPos);
+        if (isImageSourcePreviewImage(format)) {
+            imageID = VPreviewUtils::getPreviewImageID(format);
+        }
+    }
 
-        V_ASSERT(text.remove(QChar::ObjectReplacementCharacter) == p_block.text());
+    if (imageID != -1) {
+        auto it = m_previewImages.find(imageID);
+        if (it != m_previewImages.end()) {
+            PreviewImageInfo &img = it.value();
+            if (img.m_path == p_info.m_linkUrl
+                && img.m_isBlock == p_info.m_isBlock) {
+                img.m_timeStamp = m_timeStamp;
+            } else {
+                imageID = -1;
+            }
+        } else {
+            // This preview image does not exist in the cache, which means it may
+            // be deleted before but added back by user's undo action.
+            // We treat it an obsolete preview image.
+            imageID = -1;
+        }
     }
+
+    return imageID;
 }
 
-bool VImagePreviewer::isPreviewEnabled()
+bool VImagePreviewer::isImagePreviewBlock(const QTextBlock &p_block)
 {
-    return m_enablePreview;
+    if (!p_block.isValid()) {
+        return false;
+    }
+
+    QString text = p_block.text().trimmed();
+    return text == QString(QChar::ObjectReplacementCharacter);
 }
 
-void VImagePreviewer::enableImagePreview()
+QString VImagePreviewer::fetchImageUrlToPreview(const QString &p_text)
 {
-    m_enablePreview = true;
+    QRegExp regExp(VUtils::c_imageLinkRegExp);
 
-    if (g_config->getEnablePreviewImages()) {
-        m_timer->stop();
-        m_timer->start();
+    int index = regExp.indexIn(p_text);
+    if (index == -1) {
+        return QString();
     }
-}
 
-void VImagePreviewer::disableImagePreview()
-{
-    m_enablePreview = false;
-
-    if (m_isPreviewing) {
-        // It is previewing, append the request and clear preview blocks after
-        // finished previewing.
-        // It is weird that when selection changed, it will interrupt the process
-        // of previewing.
-        m_requestCearBlocks = true;
-        return;
+    int lastIndex = regExp.lastIndexIn(p_text);
+    if (lastIndex != index) {
+        return QString();
     }
 
-    clearAllImagePreviewBlocks();
+    return regExp.capturedTexts()[2].trimmed();
 }
 
-void VImagePreviewer::clearAllImagePreviewBlocks()
+QString VImagePreviewer::fetchImagePathToPreview(const QString &p_text)
 {
-    V_ASSERT(!m_isPreviewing);
+    QString imageUrl = fetchImageUrlToPreview(p_text);
+    if (imageUrl.isEmpty()) {
+        return imageUrl;
+    }
 
-    QTextBlock block = m_document->begin();
-    QTextCursor cursor = m_edit->textCursor();
-    bool modified = m_edit->isModified();
+    QString imagePath;
+    QFileInfo info(m_file->retriveBasePath(), imageUrl);
 
-    cursor.beginEditBlock();
-    while (block.isValid()) {
-        if (isImagePreviewBlock(block)) {
-            QTextBlock nextBlock = block.next();
-            removeBlock(block);
-            block = nextBlock;
+    if (info.exists()) {
+        if (info.isNativePath()) {
+            // Local file.
+            imagePath = QDir::cleanPath(info.absoluteFilePath());
         } else {
-            clearCorruptedImagePreviewBlock(block);
-
-            block = block.next();
+            imagePath = imageUrl;
+        }
+    } else {
+        QString decodedUrl(imageUrl);
+        VUtils::decodeUrl(decodedUrl);
+        QFileInfo dinfo(m_file->retriveBasePath(), decodedUrl);
+        if (dinfo.exists()) {
+            if (dinfo.isNativePath()) {
+                // Local file.
+                imagePath = QDir::cleanPath(dinfo.absoluteFilePath());
+            } else {
+                imagePath = imageUrl;
+            }
+        } else {
+            QUrl url(imageUrl);
+            imagePath = url.toString();
         }
     }
-    cursor.endEditBlock();
-
-    m_edit->setModified(modified);
 
-    emit m_edit->statusChanged();
+    return imagePath;
 }
 
-QString VImagePreviewer::fetchImagePathFromPreviewBlock(QTextBlock &p_block)
+void VImagePreviewer::clearAllPreviewImages()
 {
-    QTextImageFormat format = fetchFormatFromPreviewBlock(p_block);
-    if (!format.isValid()) {
-        return QString();
-    }
+    m_imageRegions.clear();
+    ++m_timeStamp;
+
+    QTextCursor cursor(m_document);
+    clearObsoletePreviewImages(cursor);
 
-    return format.property(ImagePath).toString();
+    m_imageCache.clear();
 }
 
-QTextImageFormat VImagePreviewer::fetchFormatFromPreviewBlock(QTextBlock &p_block)
+QTextImageFormat VImagePreviewer::fetchFormatFromPreviewBlock(const QTextBlock &p_block) const
 {
     QTextCursor cursor(p_block);
     int shift = p_block.text().indexOf(QChar::ObjectReplacementCharacter);
@@ -427,29 +436,6 @@ QTextImageFormat VImagePreviewer::fetchFormatFromPreviewBlock(QTextBlock &p_bloc
     return cursor.charFormat().toImageFormat();
 }
 
-void VImagePreviewer::updateFormatInPreviewBlock(QTextBlock &p_block,
-                                                 const QTextImageFormat &p_format)
-{
-    bool modified = m_edit->isModified();
-
-    QTextCursor cursor(p_block);
-    cursor.beginEditBlock();
-    int shift = p_block.text().indexOf(QChar::ObjectReplacementCharacter);
-    if (shift > 0) {
-        cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, shift);
-    }
-
-    V_ASSERT(shift >= 0);
-
-    cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 1);
-    V_ASSERT(cursor.charFormat().toImageFormat().isValid());
-
-    cursor.setCharFormat(p_format);
-    cursor.endEditBlock();
-
-    m_edit->setModified(modified);
-}
-
 QString VImagePreviewer::imageCacheResourceName(const QString &p_imagePath)
 {
     V_ASSERT(!p_imagePath.isEmpty());
@@ -496,33 +482,23 @@ void VImagePreviewer::imageDownloaded(const QByteArray &p_data, const QString &p
             return;
         }
 
-        m_timer->stop();
         QString name(imagePathToCacheResourceName(p_url));
         m_document->addResource(QTextDocument::ImageResource, name, image);
         m_imageCache.insert(p_url, ImageInfo(name, image.width()));
 
         qDebug() << "downloaded image cache insert" << p_url << name;
-
-        m_timer->start();
+        emit requestUpdateImageLinks();
     }
 }
 
-void VImagePreviewer::refresh()
+QImage VImagePreviewer::fetchCachedImageByID(long long p_id)
 {
-    if (m_isPreviewing) {
-        m_requestRefreshBlocks = true;
-        return;
+    auto imgIt = m_previewImages.find(p_id);
+    if (imgIt == m_previewImages.end()) {
+        return QImage();
     }
 
-    m_timer->stop();
-    m_imageCache.clear();
-    clearAllImagePreviewBlocks();
-    m_timer->start();
-}
-
-QImage VImagePreviewer::fetchCachedImageFromPreviewBlock(QTextBlock &p_block)
-{
-    QString path = fetchImagePathFromPreviewBlock(p_block);
+    QString path = imgIt->m_path;
     if (path.isEmpty()) {
         return QImage();
     }
@@ -537,13 +513,17 @@ QImage VImagePreviewer::fetchCachedImageFromPreviewBlock(QTextBlock &p_block)
 
 bool VImagePreviewer::updateImageWidth(QTextImageFormat &p_format)
 {
-    QString path = p_format.property(ImagePath).toString();
-    auto it = m_imageCache.find(path);
+    long long imageID = VPreviewUtils::getPreviewImageID(p_format);
+    auto imgIt = m_previewImages.find(imageID);
+    if (imgIt == m_previewImages.end()) {
+        return false;
+    }
 
+    auto it = m_imageCache.find(imgIt->m_path);
     if (it != m_imageCache.end()) {
         int newWidth = it.value().m_width;
         if (g_config->getEnablePreviewImageConstraint()) {
-            newWidth = qMin(m_imageWidth, it.value().m_width);
+            newWidth = qMin(m_imageWidth, newWidth);
         }
 
         if (newWidth != p_format.width()) {
@@ -555,8 +535,90 @@ bool VImagePreviewer::updateImageWidth(QTextImageFormat &p_format)
     return false;
 }
 
-void VImagePreviewer::update()
+void VImagePreviewer::updatePreviewImageWidth()
+{
+    if (!m_previewEnabled) {
+        return;
+    }
+
+    m_updateTimer->stop();
+    m_updateTimer->start();
+}
+
+void VImagePreviewer::doUpdatePreviewImageWidth()
+{
+    // Get the width of the m_edit.
+    m_imageWidth = qMax(m_edit->size().width() - 50, c_minImageWidth);
+
+    bool updated = false;
+    QTextBlock block = m_document->begin();
+    QTextCursor cursor(block);
+    while (block.isValid()) {
+        if (VTextBlockData::containsPreviewImage(block)) {
+            // Notice the short circuit.
+            updated = updatePreviewImageWidthOfBlock(block, cursor) || updated;
+        }
+
+        block = block.next();
+    }
+
+    if (updated) {
+        emit m_edit->statusChanged();
+    }
+}
+
+bool VImagePreviewer::updatePreviewImageWidthOfBlock(const QTextBlock &p_block,
+                                                     QTextCursor &p_cursor)
 {
-    m_timer->stop();
-    m_timer->start();
+    QString text = p_block.text();
+    bool updated = false;
+    // From back to front.
+    for (int i = text.size() - 1; i >= 0; --i) {
+        if (text[i].isSpace()) {
+            continue;
+        }
+
+        if (text[i] == QChar::ObjectReplacementCharacter) {
+            int pos = p_block.position() + i;
+            Q_ASSERT(m_document->characterAt(pos) == QChar::ObjectReplacementCharacter);
+
+            QTextImageFormat imageFormat = VPreviewUtils::fetchFormatFromPosition(m_document, pos);
+            if (imageFormat.isValid()
+                && isImageSourcePreviewImage(imageFormat)
+                && updateImageWidth(imageFormat)) {
+                bool isModified = m_edit->isModified();
+                p_cursor.joinPreviousEditBlock();
+                p_cursor.setPosition(pos);
+                p_cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 1);
+                Q_ASSERT(p_cursor.charFormat().toImageFormat().isValid());
+                p_cursor.setCharFormat(imageFormat);
+                p_cursor.endEditBlock();
+                m_edit->setModified(isModified);
+                updated = true;
+            }
+        }
+    }
+
+    return updated;
+}
+
+void VImagePreviewer::shrinkImageCache()
+{
+    const int MaxSize = 20;
+    if (m_imageCache.size() > m_previewImages.size()
+        && m_imageCache.size() > MaxSize) {
+        QHash<QString, bool> usedImagePath;
+        for (auto it = m_previewImages.begin(); it != m_previewImages.end(); ++it) {
+            usedImagePath.insert(it->m_path, true);
+        }
+
+        for (auto it = m_imageCache.begin(); it != m_imageCache.end();) {
+            if (!usedImagePath.contains(it.key())) {
+                qDebug() << "shrink one image" << it.key();
+                it = m_imageCache.erase(it);
+            } else {
+                ++it;
+            }
+        }
+    }
 }

+ 152 - 44
src/vimagepreviewer.h

@@ -5,9 +5,10 @@
 #include <QString>
 #include <QTextBlock>
 #include <QHash>
+#include "hgmarkdownhighlighter.h"
 
-class VMdEdit;
 class QTimer;
+class VMdEdit;
 class QTextDocument;
 class VFile;
 class VDownloader;
@@ -16,27 +17,37 @@ class VImagePreviewer : public QObject
 {
     Q_OBJECT
 public:
-    explicit VImagePreviewer(VMdEdit *p_edit, int p_timeToPreview);
-
-    void disableImagePreview();
-    void enableImagePreview();
-    bool isPreviewEnabled();
+    explicit VImagePreviewer(VMdEdit *p_edit);
 
+    // Whether @p_block is an image previewed block.
+    // The image previewed block is a block containing only the special character
+    // and whitespaces.
     bool isImagePreviewBlock(const QTextBlock &p_block);
 
-    QImage fetchCachedImageFromPreviewBlock(QTextBlock &p_block);
+    QImage fetchCachedImageByID(long long p_id);
+
+    // Update preview image width.
+    void updatePreviewImageWidth();
 
-    // Clear the m_imageCache and all the preview blocks.
-    // Then re-preview all the blocks.
-    void refresh();
+    bool isPreviewing() const;
 
-    void update();
+    bool isEnabled() const;
+
+public slots:
+    // Image links have changed.
+    void imageLinksChanged(const QVector<VElementRegion> &p_imageRegions);
 
 private slots:
-    void timerTimeout();
-    void handleContentChange(int p_position, int p_charsRemoved, int p_charsAdded);
+    // Non-local image downloaded for preview.
     void imageDownloaded(const QByteArray &p_data, const QString &p_url);
 
+    // Update preview image width right now.
+    void doUpdatePreviewImageWidth();
+
+signals:
+    // Request highlighter to update image links.
+    void requestUpdateImageLinks();
+
 private:
     struct ImageInfo
     {
@@ -49,40 +60,111 @@ private:
         int m_width;
     };
 
+    struct ImageLinkInfo
+    {
+        ImageLinkInfo()
+            : m_startPos(-1), m_endPos(-1),
+              m_isBlock(false), m_previewImageID(-1)
+        {
+        }
+
+        ImageLinkInfo(int p_startPos, int p_endPos)
+            : m_startPos(p_startPos), m_endPos(p_endPos),
+              m_isBlock(false), m_previewImageID(-1)
+        {
+        }
+
+        int m_startPos;
+        int m_endPos;
+        QString m_linkUrl;
+
+        // Whether it is a image block.
+        bool m_isBlock;
+
+        // The previewed image ID if this link has been previewed.
+        // -1 if this link has not yet been previewed.
+        long long m_previewImageID;
+    };
+
+    // Info about a previewed image.
+    struct PreviewImageInfo
+    {
+        PreviewImageInfo() : m_id(-1), m_timeStamp(-1)
+        {
+        }
+
+        PreviewImageInfo(long long p_id, long long p_timeStamp,
+                         const QString p_path, bool p_isBlock)
+            : m_id(p_id), m_timeStamp(p_timeStamp),
+              m_path(p_path), m_isBlock(p_isBlock)
+        {
+        }
+
+        QString toString()
+        {
+            return QString("PreviewImageInfo(ID %0 path %1 stamp %2 isBlock %3")
+                          .arg(m_id).arg(m_path).arg(m_timeStamp).arg(m_isBlock);
+        }
+
+        long long m_id;
+        long long m_timeStamp;
+        QString m_path;
+        bool m_isBlock;
+    };
+
+    // Kick off new preview of m_imageRegions.
+    void kickOffPreview(const QVector<VElementRegion> &p_imageRegions);
+
+    // Preview images according to m_timeStamp and m_imageRegions.
     void previewImages();
-    bool isValidImagePreviewBlock(QTextBlock &p_block);
 
-    // Fetch the image link's URL if there is only one link.
-    QString fetchImageUrlToPreview(const QString &p_text);
+    // According to m_imageRegions, fetch the image link Url.
+    // Will check if this link has been previewed correctly and mark the previewed
+    // image with the newest timestamp.
+    // @p_imageLinks should be sorted in descending order of m_startPos.
+    void fetchImageLinksFromRegions(QVector<ImageLinkInfo> &p_imageLinks);
 
-    // Fetch teh image's full path if there is only one image link.
-    QString fetchImagePathToPreview(const QString &p_text);
+    // Preview not previewed image links in @p_imageLinks.
+    // Insert the preview block with same indentation with the link block.
+    // @p_imageLinks should be sorted in descending order of m_startPos.
+    void previewImageLinks(QVector<ImageLinkInfo> &p_imageLinks, QTextCursor &p_cursor);
+
+    // Clear obsolete preview images whose timeStamp does not match current one
+    // or does not exist in the cache.
+    void clearObsoletePreviewImages(QTextCursor &p_cursor);
 
-    // Try to preview the image of @p_block.
-    // Return the next block to process.
-    QTextBlock previewImageOfOneBlock(QTextBlock &p_block);
+    // Clear obsolete preview image in @p_block.
+    // A preview image is obsolete if it is not in the cache.
+    // If it is a preview block, delete the whole block.
+    // @p_block: a block may contain multiple preview images;
+    // @p_cursor: cursor used to manipulate the text;
+    bool clearObsoletePreviewImagesOfBlock(QTextBlock &p_block, QTextCursor &p_cursor);
 
-    // Insert a new block to preview image.
-    QTextBlock insertImagePreviewBlock(QTextBlock &p_block, const QString &p_imagePath);
+    // Update the width of preview image in @p_block.
+    bool updatePreviewImageWidthOfBlock(const QTextBlock &p_block, QTextCursor &p_cursor);
 
-    // @p_block is the image block. Update it to preview @p_imagePath.
-    void updateImagePreviewBlock(QTextBlock &p_block, const QString &p_imagePath);
+    // Check if there is a correct previewed image following the @p_info link.
+    // Returns the previewImageID if yes. Otherwise, returns -1.
+    long long isImageLinkPreviewed(const ImageLinkInfo &p_info);
 
-    void removeBlock(QTextBlock &p_block);
+    // Fetch the image link's URL if there is only one link.
+    QString fetchImageUrlToPreview(const QString &p_text);
 
-    // Corrupted image preview block: ObjectReplacementCharacter mixed with other
-    // non-space characters.
-    // Remove the ObjectReplacementCharacter chars.
-    void clearCorruptedImagePreviewBlock(QTextBlock &p_block);
+    // Fetch teh image's full path if there is only one image link.
+    QString fetchImagePathToPreview(const QString &p_text);
 
-    void clearAllImagePreviewBlocks();
+    // Clear all the previewed images.
+    void clearAllPreviewImages();
 
-    QTextImageFormat fetchFormatFromPreviewBlock(QTextBlock &p_block);
+    // Fetch the text image format from an image preview block.
+    QTextImageFormat fetchFormatFromPreviewBlock(const QTextBlock &p_block) const;
 
-    QString fetchImagePathFromPreviewBlock(QTextBlock &p_block);
+    // Whether the preview image is Image source.
+    bool isImageSourcePreviewImage(const QTextImageFormat &p_format) const;
 
-    void updateFormatInPreviewBlock(QTextBlock &p_block,
-                                    const QTextImageFormat &p_format);
+    void initImageFormat(QTextImageFormat &p_imgFormat,
+                         const QString &p_imageName,
+                         const PreviewImageInfo &p_info) const;
 
     // Look up m_imageCache to get the resource name in QTextDocument's cache.
     // If there is none, insert it.
@@ -93,28 +175,54 @@ private:
     // Return true if and only if there is update.
     bool updateImageWidth(QTextImageFormat &p_format);
 
-    // Whether it is a normal block or not.
-    bool isNormalBlock(const QTextBlock &p_block);
+    // Clean up image cache.
+    void shrinkImageCache();
 
     VMdEdit *m_edit;
     QTextDocument *m_document;
     VFile *m_file;
-    QTimer *m_timer;
-    bool m_enablePreview;
-    bool m_isPreviewing;
-    bool m_requestCearBlocks;
-    bool m_requestRefreshBlocks;
-    bool m_updatePending;
 
     // Map from image full path to QUrl identifier in the QTextDocument's cache.
-    QHash<QString, ImageInfo> m_imageCache;;
+    QHash<QString, ImageInfo> m_imageCache;
 
     VDownloader *m_downloader;
 
     // The preview width.
     int m_imageWidth;
 
+    // Used to denote the obsolete previewed images.
+    // Increased when a new preview is kicked off.
+    long long m_timeStamp;
+
+    // Incremental ID for previewed images.
+    long long m_previewIndex;
+
+    // Map from previewImageID to PreviewImageInfo.
+    QHash<long long, PreviewImageInfo> m_previewImages;
+
+    // Regions of all the image links.
+    QVector<VElementRegion> m_imageRegions;
+
+    // Timer for updatePreviewImageWidth().
+    QTimer *m_updateTimer;
+
+    // Whether preview is enabled.
+    bool m_previewEnabled;
+
+    // Whether preview is ongoing.
+    bool m_isPreviewing;
+
     static const int c_minImageWidth;
 };
 
+inline bool VImagePreviewer::isPreviewing() const
+{
+    return m_isPreviewing;
+}
+
+inline bool VImagePreviewer::isEnabled() const
+{
+    return m_previewEnabled;
+}
+
 #endif // VIMAGEPREVIEWER_H

+ 2 - 3
src/vmainwindow.cpp

@@ -638,12 +638,11 @@ void VMainWindow::initMarkdownMenu()
     codeBlockAct->setChecked(g_config->getEnableCodeBlockHighlight());
 
     QAction *previewImageAct = new QAction(tr("Preview Images In Edit Mode"), this);
-    previewImageAct->setToolTip(tr("Enable image preview in edit mode"));
+    previewImageAct->setToolTip(tr("Enable image preview in edit mode (re-open current tabs to make it work)"));
     previewImageAct->setCheckable(true);
     connect(previewImageAct, &QAction::triggered,
             this, &VMainWindow::enableImagePreview);
-    // TODO: add the action to the menu after handling the UNDO history well.
-    // markdownMenu->addAction(previewImageAct);
+    markdownMenu->addAction(previewImageAct);
     previewImageAct->setChecked(g_config->getEnablePreviewImages());
 
     QAction *previewWidthAct = new QAction(tr("Constrain The Width Of Previewed Images"), this);

+ 127 - 53
src/vmdedit.cpp

@@ -7,8 +7,11 @@
 #include "vconfigmanager.h"
 #include "vtoc.h"
 #include "utils/vutils.h"
+#include "utils/veditutils.h"
+#include "utils/vpreviewutils.h"
 #include "dialog/vselectdialog.h"
 #include "vimagepreviewer.h"
+#include "vtextblockdata.h"
 
 extern VConfigManager *g_config;
 extern VNote *g_vnote;
@@ -36,7 +39,11 @@ VMdEdit::VMdEdit(VFile *p_file, VDocument *p_vdoc, MarkdownConverterType p_type,
     m_cbHighlighter = new VCodeBlockHighlightHelper(m_mdHighlighter, p_vdoc,
                                                     p_type);
 
-    m_imagePreviewer = new VImagePreviewer(this, 500);
+    m_imagePreviewer = new VImagePreviewer(this);
+    connect(m_mdHighlighter, &HGMarkdownHighlighter::imageLinksUpdated,
+            m_imagePreviewer, &VImagePreviewer::imageLinksChanged);
+    connect(m_imagePreviewer, &VImagePreviewer::requestUpdateImageLinks,
+            m_mdHighlighter, &HGMarkdownHighlighter::updateHighlight);
 
     m_editOps = new VMdEditOperations(this, m_file);
 
@@ -48,8 +55,6 @@ VMdEdit::VMdEdit(VFile *p_file, VDocument *p_vdoc, MarkdownConverterType p_type,
     connect(this, &VMdEdit::cursorPositionChanged,
             this, &VMdEdit::updateCurHeader);
 
-    connect(this, &VMdEdit::selectionChanged,
-            this, &VMdEdit::handleSelectionChanged);
     connect(QApplication::clipboard(), &QClipboard::changed,
             this, &VMdEdit::handleClipboardChanged);
 
@@ -74,8 +79,6 @@ void VMdEdit::beginEdit()
 
     initInitImages();
 
-    m_imagePreviewer->refresh();
-
     setReadOnly(false);
     setModified(false);
 
@@ -94,6 +97,7 @@ void VMdEdit::saveFile()
     if (!document()->isModified()) {
         return;
     }
+
     m_file->setContent(toPlainTextWithoutImg());
     document()->setModified(false);
 }
@@ -364,50 +368,107 @@ void VMdEdit::scrollToHeader(const VAnchor &p_anchor)
     scrollToLine(p_anchor.lineNumber);
 }
 
-QString VMdEdit::toPlainTextWithoutImg() const
+QString VMdEdit::toPlainTextWithoutImg()
 {
-    QString text = toPlainText();
-    int start = 0;
-    do {
-        int index = text.indexOf(QChar::ObjectReplacementCharacter, start);
-        if (index == -1) {
-            break;
-        }
-        start = removeObjectReplacementLine(text, index);
-    } while (start > -1 && start < text.size());
+    QString text;
+    bool readOnly = isReadOnly();
+    setReadOnly(true);
+    text = getPlainTextWithoutPreviewImage();
+    setReadOnly(readOnly);
+
     return text;
 }
 
-int VMdEdit::removeObjectReplacementLine(QString &p_text, int p_index) const
+QString VMdEdit::getPlainTextWithoutPreviewImage() const
 {
-    Q_ASSERT(p_text.size() > p_index && p_text.at(p_index) == QChar::ObjectReplacementCharacter);
-    int prevLineIdx = p_text.lastIndexOf('\n', p_index);
-    if (prevLineIdx == -1) {
-        prevLineIdx = 0;
+    QVector<Region> deletions;
+
+    while (true) {
+        deletions.clear();
+
+        while (m_imagePreviewer->isPreviewing()) {
+            VUtils::sleepWait(100);
+        }
+
+        // Iterate all the block to get positions for deletion.
+        QTextBlock block = document()->begin();
+        bool tryAgain = false;
+        while (block.isValid()) {
+            if (VTextBlockData::containsPreviewImage(block)) {
+                if (!getPreviewImageRegionOfBlock(block, deletions)) {
+                    tryAgain = true;
+                    break;
+                }
+            }
+
+            block = block.next();
+        }
+
+        if (tryAgain) {
+            continue;
+        }
+
+        QString text = toPlainText();
+        // deletions is sorted by m_startPos.
+        // From back to front.
+        for (int i = deletions.size() - 1; i >= 0; --i) {
+            const Region &reg = deletions[i];
+            qDebug() << "img region to delete" << reg.m_startPos << reg.m_endPos;
+            text.remove(reg.m_startPos, reg.m_endPos - reg.m_startPos);
+        }
+
+        return text;
     }
-    // Remove [\n....?]
-    p_text.remove(prevLineIdx, p_index - prevLineIdx + 1);
-    return prevLineIdx - 1;
 }
 
-void VMdEdit::handleSelectionChanged()
+bool VMdEdit::getPreviewImageRegionOfBlock(const QTextBlock &p_block,
+                                           QVector<Region> &p_regions) const
 {
-    if (!g_config->getEnablePreviewImages()) {
-        return;
+    QTextDocument *doc = document();
+    QVector<Region> regs;
+    QString text = p_block.text();
+    int nrOtherChar = 0;
+    int nrImage = 0;
+    bool hasBlock = false;
+
+    // From back to front.
+    for (int i = text.size() - 1; i >= 0; --i) {
+        if (text[i].isSpace()) {
+            continue;
+        }
+
+        if (text[i] == QChar::ObjectReplacementCharacter) {
+            int pos = p_block.position() + i;
+            Q_ASSERT(doc->characterAt(pos) == QChar::ObjectReplacementCharacter);
+
+            QTextImageFormat imageFormat = VPreviewUtils::fetchFormatFromPosition(doc, pos);
+            if (imageFormat.isValid()) {
+                ++nrImage;
+                bool isBlock = VPreviewUtils::getPreviewImageType(imageFormat) == PreviewImageType::Block;
+                if (isBlock) {
+                    hasBlock = true;
+                } else {
+                    regs.push_back(Region(pos, pos + 1));
+                }
+            } else {
+                return false;
+            }
+        } else {
+            ++nrOtherChar;
+        }
     }
 
-    QString text = textCursor().selectedText();
-    if (text.isEmpty() && !m_imagePreviewer->isPreviewEnabled()) {
-        m_imagePreviewer->enableImagePreview();
-    } else if (m_imagePreviewer->isPreviewEnabled()) {
-        if (text.trimmed() == QString(QChar::ObjectReplacementCharacter)) {
-            // Select the image and some whitespaces.
-            // We can let the user copy the image.
-            return;
-        } else if (text.contains(QChar::ObjectReplacementCharacter)) {
-            m_imagePreviewer->disableImagePreview();
+    if (hasBlock) {
+        if (nrOtherChar > 0 || nrImage > 1) {
+            // Inconsistent state.
+            return false;
         }
+
+        regs.push_back(Region(p_block.position(), p_block.position() + p_block.length()));
     }
+
+    p_regions.append(regs);
+    return true;
 }
 
 void VMdEdit::handleClipboardChanged(QClipboard::Mode p_mode)
@@ -415,24 +476,33 @@ void VMdEdit::handleClipboardChanged(QClipboard::Mode p_mode)
     if (!hasFocus()) {
         return;
     }
+
     if (p_mode == QClipboard::Clipboard) {
         QClipboard *clipboard = QApplication::clipboard();
         const QMimeData *mimeData = clipboard->mimeData();
         if (mimeData->hasText()) {
             QString text = mimeData->text();
-            if (clipboard->ownsClipboard() &&
-                (text.trimmed() == QString(QChar::ObjectReplacementCharacter))) {
-                QImage image = selectedImage();
-                clipboard->clear(QClipboard::Clipboard);
-                if (!image.isNull()) {
-                    clipboard->setImage(image, QClipboard::Clipboard);
+            if (clipboard->ownsClipboard()) {
+                if (text.trimmed() == QString(QChar::ObjectReplacementCharacter)) {
+                    QImage image = tryGetSelectedImage();
+                    clipboard->clear(QClipboard::Clipboard);
+                    if (!image.isNull()) {
+                        clipboard->setImage(image, QClipboard::Clipboard);
+                    }
+                } else {
+                    // Try to remove all the preview image in text.
+                    VEditUtils::removeObjectReplacementCharacter(text);
+                    if (text.size() != mimeData->text().size()) {
+                        clipboard->clear(QClipboard::Clipboard);
+                        clipboard->setText(text);
+                    }
                 }
             }
         }
     }
 }
 
-QImage VMdEdit::selectedImage()
+QImage VMdEdit::tryGetSelectedImage()
 {
     QImage image;
     QTextCursor cursor = textCursor();
@@ -442,25 +512,29 @@ QImage VMdEdit::selectedImage()
     int start = cursor.selectionStart();
     int end = cursor.selectionEnd();
     QTextDocument *doc = document();
-    QTextBlock startBlock = doc->findBlock(start);
-    QTextBlock endBlock = doc->findBlock(end);
-    QTextBlock block = startBlock;
-    while (block.isValid()) {
-        if (m_imagePreviewer->isImagePreviewBlock(block)) {
-            image = m_imagePreviewer->fetchCachedImageFromPreviewBlock(block);
+    QTextImageFormat format;
+    for (int i = start; i < end; ++i) {
+        if (doc->characterAt(i) == QChar::ObjectReplacementCharacter) {
+            format = VPreviewUtils::fetchFormatFromPosition(doc, i);
             break;
         }
-        if (block == endBlock) {
-            break;
+    }
+
+    if (format.isValid()) {
+        PreviewImageSource src = VPreviewUtils::getPreviewImageSource(format);
+        long long id = VPreviewUtils::getPreviewImageID(format);
+        if (src == PreviewImageSource::Image) {
+            Q_ASSERT(m_imagePreviewer->isEnabled());
+            image = m_imagePreviewer->fetchCachedImageByID(id);
         }
-        block = block.next();
     }
+
     return image;
 }
 
 void VMdEdit::resizeEvent(QResizeEvent *p_event)
 {
-    m_imagePreviewer->update();
+    m_imagePreviewer->updatePreviewImageWidth();
 
     VEdit::resizeEvent(p_event);
 }

+ 27 - 10
src/vmdedit.h

@@ -34,8 +34,8 @@ public:
 
     void scrollToHeader(const VAnchor &p_anchor);
 
-    // Like toPlainText(), but remove special blocks containing images.
-    QString toPlainTextWithoutImg() const;
+    // Like toPlainText(), but remove image preview characters.
+    QString toPlainTextWithoutImg();
 
     const QVector<VHeader> &getHeaders() const;
 
@@ -58,7 +58,6 @@ private slots:
     // When there is no header in current cursor, will signal an invalid header.
     void updateCurHeader();
 
-    void handleSelectionChanged();
     void handleClipboardChanged(QClipboard::Mode p_mode);
 
 protected:
@@ -69,20 +68,38 @@ protected:
     void resizeEvent(QResizeEvent *p_event) Q_DECL_OVERRIDE;
 
 private:
+    struct Region
+    {
+        Region() : m_startPos(-1), m_endPos(-1)
+        {
+        }
+
+        Region(int p_start, int p_end)
+            : m_startPos(p_start), m_endPos(p_end)
+        {
+        }
+
+        int m_startPos;
+        int m_endPos;
+    };
+
     void initInitImages();
     void clearUnusedImages();
 
-    // p_text[p_index] is QChar::ObjectReplacementCharacter. Remove the line containing it.
-    // Returns the index of previous line's '\n'.
-    int removeObjectReplacementLine(QString &p_text, int p_index) const;
-
-    // There is a QChar::ObjectReplacementCharacter in the selection.
-    // Get the QImage.
-    QImage selectedImage();
+    // There is a QChar::ObjectReplacementCharacter (and maybe some spaces)
+    // in the selection. Get the QImage.
+    QImage tryGetSelectedImage();
 
     // Return the header index in m_headers where current cursor locates.
     int currentCursorHeader() const;
 
+    QString getPlainTextWithoutPreviewImage() const;
+
+    // Try to get all the regions of preview image within @p_block.
+    // Returns false if preview image is not ready yet.
+    bool getPreviewImageRegionOfBlock(const QTextBlock &p_block,
+                                      QVector<Region> &p_regions) const;
+
     HGMarkdownHighlighter *m_mdHighlighter;
     VCodeBlockHighlightHelper *m_cbHighlighter;
     VImagePreviewer *m_imagePreviewer;

+ 10 - 0
src/vtextblockdata.cpp

@@ -0,0 +1,10 @@
+#include "vtextblockdata.h"
+
+VTextBlockData::VTextBlockData()
+    : QTextBlockUserData(), m_containsPreviewImage(false)
+{
+}
+
+VTextBlockData::~VTextBlockData()
+{
+}

+ 44 - 0
src/vtextblockdata.h

@@ -0,0 +1,44 @@
+#ifndef VTEXTBLOCKDATA_H
+#define VTEXTBLOCKDATA_H
+
+#include <QTextBlockUserData>
+
+class VTextBlockData : public QTextBlockUserData
+{
+public:
+    VTextBlockData();
+
+    ~VTextBlockData();
+
+    bool containsPreviewImage() const;
+
+    static bool containsPreviewImage(const QTextBlock &p_block);
+
+    void setContainsPreviewImage(bool p_contains);
+
+private:
+    // Whether this block maybe contains one or more preview images.
+    bool m_containsPreviewImage;
+};
+
+inline bool VTextBlockData::containsPreviewImage() const
+{
+    return m_containsPreviewImage;
+}
+
+inline void VTextBlockData::setContainsPreviewImage(bool p_contains)
+{
+    m_containsPreviewImage = p_contains;
+}
+
+inline bool VTextBlockData::containsPreviewImage(const QTextBlock &p_block)
+{
+    VTextBlockData *blockData = dynamic_cast<VTextBlockData *>(p_block.userData());
+    if (!blockData) {
+        return false;
+    }
+
+    return blockData->containsPreviewImage();
+}
+
+#endif // VTEXTBLOCKDATA_H