Browse Source

refactor image preview logics by adding VImagePreviewer

1. Support previewing non-relative local images;
2. Support previewing network images;
Le Tan 8 years ago
parent
commit
a8614839d9

+ 0 - 31
src/hgmarkdownhighlighter.cpp

@@ -151,41 +151,10 @@ void HGMarkdownHighlighter::initBlockHighlightFromResult(int nrBlocks)
         }
     }
 
-    updateImageBlocks();
-
     pmh_free_elements(result);
     result = NULL;
 }
 
-void HGMarkdownHighlighter::updateImageBlocks()
-{
-    imageBlocks.clear();
-    for (int i = 0; i < highlightingStyles.size(); i++)
-    {
-        const HighlightingStyle &style = highlightingStyles[i];
-        if (style.type != pmh_IMAGE) {
-            continue;
-        }
-        pmh_element *elem_cursor = result[style.type];
-        while (elem_cursor != NULL)
-        {
-            if (elem_cursor->end <= elem_cursor->pos) {
-                elem_cursor = elem_cursor->next;
-                continue;
-            }
-
-            int startBlock = document->findBlock(elem_cursor->pos).blockNumber();
-            int endBlock = document->findBlock(elem_cursor->end).blockNumber();
-            for (int i = startBlock; i <= endBlock; ++i) {
-                imageBlocks.insert(i);
-            }
-
-            elem_cursor = elem_cursor->next;
-        }
-    }
-    emit imageBlocksUpdated(imageBlocks);
-}
-
 void HGMarkdownHighlighter::initBlockHighlihgtOne(unsigned long pos, unsigned long end, int styleIndex)
 {
     int startBlockNum = document->findBlock(pos).blockNumber();

+ 1 - 4
src/hgmarkdownhighlighter.h

@@ -91,7 +91,6 @@ public:
 
 signals:
     void highlightCompleted();
-    void imageBlocksUpdated(QSet<int> p_blocks);
     void codeBlocksUpdated(const QList<VCodeBlock> &p_codeBlocks);
 
 protected:
@@ -124,8 +123,6 @@ private:
 
     int m_numOfCodeBlockHighlightsToRecv;
 
-    // Block numbers containing image link(s).
-    QSet<int> imageBlocks;
     QAtomicInt parsing;
     QTimer *timer;
     int waitInterval;
@@ -144,7 +141,7 @@ private:
     void initBlockHighlightFromResult(int nrBlocks);
     void initBlockHighlihgtOne(unsigned long pos, unsigned long end,
                                int styleIndex);
-    void updateImageBlocks();
+
     // Return true if there are fenced code blocks and it will call rehighlight() later.
     // Return false if there is none.
     bool updateCodeBlocks();

+ 1 - 0
src/resources/vnote.ini

@@ -22,6 +22,7 @@ enable_mathjax=false
 web_zoom_factor=-1
 ; Syntax highlight within code blocks in edit mode
 enable_code_block_highlight=true
+enable_preview_images=true
 
 [session]
 tools_dock_checked=true

+ 4 - 2
src/src.pro

@@ -60,7 +60,8 @@ SOURCES += main.cpp\
     vopenedlistmenu.cpp \
     vorphanfile.cpp \
     vcodeblockhighlighthelper.cpp \
-    vwebview.cpp
+    vwebview.cpp \
+    vimagepreviewer.cpp
 
 HEADERS  += vmainwindow.h \
     vdirectorytree.h \
@@ -107,7 +108,8 @@ HEADERS  += vmainwindow.h \
     vnavigationmode.h \
     vorphanfile.h \
     vcodeblockhighlighthelper.h \
-    vwebview.h
+    vwebview.h \
+    vimagepreviewer.h
 
 RESOURCES += \
     vnote.qrc \

+ 3 - 0
src/vconfigmanager.cpp

@@ -123,6 +123,9 @@ void VConfigManager::initialize()
 
     m_enableCodeBlockHighlight = getConfigFromSettings("global",
                                                        "enable_code_block_highlight").toBool();
+
+    m_enablePreviewImages = getConfigFromSettings("global",
+                                                  "enable_preview_images").toBool();
 }
 
 void VConfigManager::readPredefinedColorsFromSettings()

+ 22 - 0
src/vconfigmanager.h

@@ -157,6 +157,9 @@ public:
     inline bool getEnableCodeBlockHighlight() const;
     inline void setEnableCodeBlockHighlight(bool p_enabled);
 
+    inline bool getEnablePreviewImages() const;
+    inline void setEnablePreviewImages(bool p_enabled);
+
     // Get the folder the ini file exists.
     QString getConfigFolder() const;
 
@@ -264,6 +267,9 @@ private:
     // Enable colde block syntax highlight.
     bool m_enableCodeBlockHighlight;
 
+    // Preview images in edit mode.
+    bool m_enablePreviewImages;
+
     // The name of the config file in each directory, obsolete.
     // Use c_dirConfigFile instead.
     static const QString c_obsoleteDirConfigFile;
@@ -689,4 +695,20 @@ inline void VConfigManager::setEnableCodeBlockHighlight(bool p_enabled)
                         m_enableCodeBlockHighlight);
 }
 
+inline bool VConfigManager::getEnablePreviewImages() const
+{
+    return m_enablePreviewImages;
+}
+
+inline void VConfigManager::setEnablePreviewImages(bool p_enabled)
+{
+    if (m_enablePreviewImages == p_enabled) {
+        return;
+    }
+
+    m_enablePreviewImages = p_enabled;
+    setConfigToSettings("global", "enable_preview_images",
+                        m_enablePreviewImages);
+}
+
 #endif // VCONFIGMANAGER_H

+ 6 - 5
src/vdownloader.cpp

@@ -11,13 +11,14 @@ void VDownloader::handleDownloadFinished(QNetworkReply *reply)
 {
     data = reply->readAll();
     reply->deleteLater();
-    emit downloadFinished(data);
+    qDebug() << "VDownloader receive" << reply->url().toString();
+    emit downloadFinished(data, reply->url().toString());
 }
 
-void VDownloader::download(QUrl url)
+void VDownloader::download(const QUrl &p_url)
 {
-    Q_ASSERT(url.isValid());
-    QNetworkRequest request(url);
+    Q_ASSERT(p_url.isValid());
+    QNetworkRequest request(p_url);
     webCtrl.get(request);
-    qDebug() << "VDownloader get" << url.toString();
+    qDebug() << "VDownloader get" << p_url.toString();
 }

+ 2 - 2
src/vdownloader.h

@@ -13,10 +13,10 @@ class VDownloader : public QObject
     Q_OBJECT
 public:
     explicit VDownloader(QObject *parent = 0);
-    void download(QUrl url);
+    void download(const QUrl &p_url);
 
 signals:
-    void downloadFinished(const QByteArray &data);
+    void downloadFinished(const QByteArray &data, const QString &url);
 
 private slots:
     void handleDownloadFinished(QNetworkReply *reply);

+ 6 - 0
src/vedit.cpp

@@ -563,3 +563,9 @@ void VEdit::handleEditAct()
 {
     emit editNote();
 }
+
+VFile *VEdit::getFile() const
+{
+    return m_file;
+}
+

+ 1 - 0
src/vedit.h

@@ -48,6 +48,7 @@ public:
                         const QString &p_replaceText);
     void setReadOnly(bool p_ro);
     void clearSearchedWordHighlight();
+    VFile *getFile() const;
 
 signals:
     void saveAndRead();

+ 491 - 0
src/vimagepreviewer.cpp

@@ -0,0 +1,491 @@
+#include "vimagepreviewer.h"
+
+#include <QTimer>
+#include <QTextDocument>
+#include <QDebug>
+#include <QDir>
+#include <QUrl>
+#include "vmdedit.h"
+#include "vconfigmanager.h"
+#include "utils/vutils.h"
+#include "vfile.h"
+#include "vdownloader.h"
+
+extern VConfigManager vconfig;
+
+enum ImageProperty { ImagePath = 1 };
+
+VImagePreviewer::VImagePreviewer(VMdEdit *p_edit, int p_timeToPreview)
+    : 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_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_downloader = new VDownloader(this);
+    connect(m_downloader, &VDownloader::downloadFinished,
+            this, &VImagePreviewer::imageDownloaded);
+
+    connect(m_edit->document(), &QTextDocument::contentsChange,
+            this, &VImagePreviewer::handleContentChange);
+}
+
+void VImagePreviewer::timerTimeout()
+{
+    if (!vconfig.getEnablePreviewImages()) {
+        if (m_enablePreview) {
+            disableImagePreview();
+        }
+        return;
+    }
+
+    if (!m_enablePreview) {
+        return;
+    }
+
+    previewImages();
+}
+
+void VImagePreviewer::handleContentChange(int /* p_position */,
+                                          int p_charsRemoved,
+                                          int p_charsAdded)
+{
+    if (p_charsRemoved == 0 && p_charsAdded == 0) {
+        return;
+    }
+
+    m_timer->stop();
+    m_timer->start();
+}
+
+void VImagePreviewer::previewImages()
+{
+    if (m_isPreviewing) {
+        return;
+    }
+
+    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)) {
+                QTextBlock nblock = block.next();
+                removeBlock(block);
+                block = nblock;
+            } else {
+                block = block.next();
+            }
+        } else {
+            clearCorruptedImagePreviewBlock(block);
+
+            block = previewImageOfOneBlock(block);
+        }
+    }
+
+    m_isPreviewing = false;
+
+    if (m_requestCearBlocks) {
+        m_requestCearBlocks = false;
+        clearAllImagePreviewBlocks();
+    }
+
+    if (m_requestRefreshBlocks) {
+        m_requestRefreshBlocks = false;
+        refresh();
+    }
+
+    emit m_edit->statusChanged();
+}
+
+bool VImagePreviewer::isImagePreviewBlock(const QTextBlock &p_block)
+{
+    if (!p_block.isValid()) {
+        return false;
+    }
+
+    QString text = p_block.text().trimmed();
+    return text == QString(QChar::ObjectReplacementCharacter);
+}
+
+bool VImagePreviewer::isValidImagePreviewBlock(QTextBlock &p_block)
+{
+    if (!isImagePreviewBlock(p_block)) {
+        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;
+    } else {
+        return false;
+    }
+}
+
+QString VImagePreviewer::fetchImageUrlToPreview(const QString &p_text)
+{
+    QRegExp regExp("\\!\\[[^\\]]*\\]\\(([^\\)]+)\\)");
+    int index = regExp.indexIn(p_text);
+    if (index == -1) {
+        return QString();
+    }
+
+    int lastIndex = regExp.lastIndexIn(p_text);
+    if (lastIndex != index) {
+        return QString();
+    }
+
+    return regExp.capturedTexts()[1];
+}
+
+QString VImagePreviewer::fetchImagePathToPreview(const QString &p_text)
+{
+    QString imageUrl = fetchImageUrlToPreview(p_text);
+    if (imageUrl.isEmpty()) {
+        return imageUrl;
+    }
+
+    QString imagePath;
+    QFileInfo info(m_file->retriveBasePath(), imageUrl);
+    if (info.exists()) {
+        if (info.isNativePath()) {
+            // Local file.
+            imagePath = info.absoluteFilePath();
+        } else {
+            imagePath = imageUrl;
+        }
+    } 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;
+    }
+
+    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();
+    }
+}
+
+QTextBlock VImagePreviewer::insertImagePreviewBlock(QTextBlock &p_block,
+                                                    const QString &p_imagePath)
+{
+    QString imageName = imageCacheResourceName(p_imagePath);
+    if (imageName.isEmpty()) {
+        return p_block;
+    }
+
+    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);
+    cursor.insertImage(imgFormat);
+    cursor.endEditBlock();
+
+    V_ASSERT(cursor.block().text().at(0) == QChar::ObjectReplacementCharacter);
+
+    m_edit->setModified(modified);
+
+    return cursor.block();
+}
+
+void VImagePreviewer::updateImagePreviewBlock(QTextBlock &p_block,
+                                              const QString &p_imagePath)
+{
+    QTextImageFormat format = fetchFormatFromPreviewBlock(p_block);
+    V_ASSERT(format.isValid());
+    QString curPath = format.property(ImagePath).toString();
+
+    if (curPath == p_imagePath) {
+        return;
+    }
+
+    // Update it with the new image.
+    QString imageName = imageCacheResourceName(p_imagePath);
+    if (imageName.isEmpty()) {
+        // Delete current preview block.
+        removeBlock(p_block);
+        return;
+    }
+
+    format.setName(imageName);
+    format.setProperty(ImagePath, p_imagePath);
+    updateFormatInPreviewBlock(p_block, format);
+}
+
+void VImagePreviewer::removeBlock(QTextBlock &p_block)
+{
+    bool modified = m_edit->isModified();
+
+    QTextCursor cursor(p_block);
+    cursor.select(QTextCursor::BlockUnderCursor);
+    cursor.removeSelectedText();
+
+    m_edit->setModified(modified);
+}
+
+void VImagePreviewer::clearCorruptedImagePreviewBlock(QTextBlock &p_block)
+{
+    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;
+        }
+    }
+
+    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();
+        }
+        cursor.endEditBlock();
+
+        m_edit->setModified(modified);
+
+        V_ASSERT(text.remove(QChar::ObjectReplacementCharacter) == p_block.text());
+    }
+}
+
+bool VImagePreviewer::isPreviewEnabled()
+{
+    return m_enablePreview;
+}
+
+void VImagePreviewer::enableImagePreview()
+{
+    m_enablePreview = true;
+
+    if (vconfig.getEnablePreviewImages()) {
+        m_timer->stop();
+        m_timer->start();
+    }
+}
+
+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;
+    }
+
+    clearAllImagePreviewBlocks();
+}
+
+void VImagePreviewer::clearAllImagePreviewBlocks()
+{
+    V_ASSERT(!m_isPreviewing);
+
+    QTextBlock block = m_document->begin();
+    QTextCursor cursor = m_edit->textCursor();
+    bool modified = m_edit->isModified();
+
+    cursor.beginEditBlock();
+    while (block.isValid()) {
+        if (isImagePreviewBlock(block)) {
+            QTextBlock nextBlock = block.next();
+            removeBlock(block);
+            block = nextBlock;
+        } else {
+            clearCorruptedImagePreviewBlock(block);
+
+            block = block.next();
+        }
+    }
+    cursor.endEditBlock();
+
+    m_edit->setModified(modified);
+
+    emit m_edit->statusChanged();
+}
+
+QString VImagePreviewer::fetchImagePathFromPreviewBlock(QTextBlock &p_block)
+{
+    QTextImageFormat format = fetchFormatFromPreviewBlock(p_block);
+    if (!format.isValid()) {
+        return QString();
+    }
+
+    return format.property(ImagePath).toString();
+}
+
+QTextImageFormat VImagePreviewer::fetchFormatFromPreviewBlock(QTextBlock &p_block)
+{
+    QTextCursor cursor(p_block);
+    int shift = p_block.text().indexOf(QChar::ObjectReplacementCharacter);
+    if (shift >= 0) {
+        cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, shift + 1);
+    } else {
+        return QTextImageFormat();
+    }
+
+    return cursor.charFormat().toImageFormat();
+}
+
+void VImagePreviewer::updateFormatInPreviewBlock(QTextBlock &p_block,
+                                                 const QTextImageFormat &p_format)
+{
+    QTextCursor cursor(p_block);
+    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);
+}
+
+QString VImagePreviewer::imageCacheResourceName(const QString &p_imagePath)
+{
+    V_ASSERT(!p_imagePath.isEmpty());
+
+    auto it = m_imageCache.find(p_imagePath);
+    if (it != m_imageCache.end()) {
+        return it.value();
+    }
+
+    // Add it to the resource cache even if it may exist there.
+    QFileInfo info(p_imagePath);
+    QImage image;
+    if (info.exists()) {
+        // Local file.
+        image = QImage(p_imagePath);
+    } else {
+        // URL. Try to download it.
+        m_downloader->download(p_imagePath);
+    }
+
+    if (image.isNull()) {
+        return QString();
+    }
+
+    QString name(imagePathToCacheResourceName(p_imagePath));
+    m_document->addResource(QTextDocument::ImageResource, name, image);
+    m_imageCache.insert(p_imagePath, name);
+
+    return name;
+}
+
+QString VImagePreviewer::imagePathToCacheResourceName(const QString &p_imagePath)
+{
+    return p_imagePath;
+}
+
+void VImagePreviewer::imageDownloaded(const QByteArray &p_data, const QString &p_url)
+{
+    QImage image(QImage::fromData(p_data));
+
+    if (!image.isNull()) {
+        auto it = m_imageCache.find(p_url);
+        if (it != m_imageCache.end()) {
+            return;
+        }
+
+        m_timer->stop();
+        QString name(imagePathToCacheResourceName(p_url));
+        m_document->addResource(QTextDocument::ImageResource, name, image);
+        m_imageCache.insert(p_url, name);
+
+        qDebug() << "downloaded image cache insert" << p_url << name;
+
+        m_timer->start();
+    }
+}
+
+void VImagePreviewer::refresh()
+{
+    if (m_isPreviewing) {
+        m_requestRefreshBlocks = true;
+        return;
+    }
+
+    m_timer->stop();
+    m_imageCache.clear();
+    clearAllImagePreviewBlocks();
+    m_timer->start();
+}
+
+QImage VImagePreviewer::fetchCachedImageFromPreviewBlock(QTextBlock &p_block)
+{
+    QString path = fetchImagePathFromPreviewBlock(p_block);
+    if (path.isEmpty()) {
+        return QImage();
+    }
+
+    auto it = m_imageCache.find(path);
+    if (it == m_imageCache.end()) {
+        return QImage();
+    }
+
+    return m_document->resource(QTextDocument::ImageResource, it.value()).value<QImage>();
+}

+ 95 - 0
src/vimagepreviewer.h

@@ -0,0 +1,95 @@
+#ifndef VIMAGEPREVIEWER_H
+#define VIMAGEPREVIEWER_H
+
+#include <QObject>
+#include <QString>
+#include <QTextBlock>
+#include <QHash>
+
+class VMdEdit;
+class QTimer;
+class QTextDocument;
+class VFile;
+class VDownloader;
+
+class VImagePreviewer : public QObject
+{
+    Q_OBJECT
+public:
+    explicit VImagePreviewer(VMdEdit *p_edit, int p_timeToPreview);
+
+    void disableImagePreview();
+    void enableImagePreview();
+    bool isPreviewEnabled();
+
+    bool isImagePreviewBlock(const QTextBlock &p_block);
+
+    QImage fetchCachedImageFromPreviewBlock(QTextBlock &p_block);
+
+    // Clear the m_imageCache and all the preview blocks.
+    // Then re-preview all the blocks.
+    void refresh();
+
+private slots:
+    void timerTimeout();
+    void handleContentChange(int p_position, int p_charsRemoved, int p_charsAdded);
+    void imageDownloaded(const QByteArray &p_data, const QString &p_url);
+
+private:
+    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);
+
+    // Fetch teh image's full path if there is only one image link.
+    QString fetchImagePathToPreview(const QString &p_text);
+
+    // Try to preview the image of @p_block.
+    // Return the next block to process.
+    QTextBlock previewImageOfOneBlock(QTextBlock &p_block);
+
+    // Insert a new block to preview image.
+    QTextBlock insertImagePreviewBlock(QTextBlock &p_block, const QString &p_imagePath);
+
+    // @p_block is the image block. Update it to preview @p_imagePath.
+    void updateImagePreviewBlock(QTextBlock &p_block, const QString &p_imagePath);
+
+    void removeBlock(QTextBlock &p_block);
+
+    // Corrupted image preview block: ObjectReplacementCharacter mixed with other
+    // non-space characters.
+    // Remove the ObjectReplacementCharacter chars.
+    void clearCorruptedImagePreviewBlock(QTextBlock &p_block);
+
+    void clearAllImagePreviewBlocks();
+
+    QTextImageFormat fetchFormatFromPreviewBlock(QTextBlock &p_block);
+
+    QString fetchImagePathFromPreviewBlock(QTextBlock &p_block);
+
+    void updateFormatInPreviewBlock(QTextBlock &p_block,
+                                    const QTextImageFormat &p_format);
+
+    // Look up m_imageCache to get the resource name in QTextDocument's cache.
+    // If there is none, insert it.
+    QString imageCacheResourceName(const QString &p_imagePath);
+
+    QString imagePathToCacheResourceName(const QString &p_imagePath);
+
+    VMdEdit *m_edit;
+    QTextDocument *m_document;
+    VFile *m_file;
+    QTimer *m_timer;
+    bool m_enablePreview;
+    bool m_isPreviewing;
+    bool m_requestCearBlocks;
+    bool m_requestRefreshBlocks;
+
+    // Map from image full path to QUrl identifier in the QTextDocument's cache.
+    QHash<QString, QString> m_imageCache;;
+
+    VDownloader *m_downloader;
+};
+
+#endif // VIMAGEPREVIEWER_H

+ 25 - 272
src/vmdedit.cpp

@@ -8,30 +8,29 @@
 #include "vtoc.h"
 #include "utils/vutils.h"
 #include "dialog/vselectdialog.h"
+#include "vimagepreviewer.h"
 
 extern VConfigManager vconfig;
 extern VNote *g_vnote;
 
-enum ImageProperty { ImagePath = 1 };
-
 VMdEdit::VMdEdit(VFile *p_file, VDocument *p_vdoc, MarkdownConverterType p_type,
                  QWidget *p_parent)
-    : VEdit(p_file, p_parent), m_mdHighlighter(NULL), m_previewImage(true)
+    : VEdit(p_file, p_parent), m_mdHighlighter(NULL)
 {
     Q_ASSERT(p_file->getDocType() == DocType::Markdown);
 
     setAcceptRichText(false);
     m_mdHighlighter = new HGMarkdownHighlighter(vconfig.getMdHighlightingStyles(),
                                                 vconfig.getCodeBlockStyles(),
-                                                500, document());
+                                                700, document());
     connect(m_mdHighlighter, &HGMarkdownHighlighter::highlightCompleted,
             this, &VMdEdit::generateEditOutline);
-    connect(m_mdHighlighter, &HGMarkdownHighlighter::imageBlocksUpdated,
-            this, &VMdEdit::updateImageBlocks);
 
     m_cbHighlighter = new VCodeBlockHighlightHelper(m_mdHighlighter, p_vdoc,
                                                     p_type);
 
+    m_imagePreviewer = new VImagePreviewer(this, 500);
+
     m_editOps = new VMdEditOperations(this, m_file);
     connect(m_editOps, &VEditOperations::keyStateChanged,
             this, &VMdEdit::handleEditStateChanged);
@@ -64,6 +63,8 @@ void VMdEdit::beginEdit()
 
     initInitImages();
 
+    m_imagePreviewer->refresh();
+
     setReadOnly(false);
     setModified(false);
 
@@ -282,253 +283,6 @@ void VMdEdit::scrollToHeader(int p_headerIndex)
     }
 }
 
-void VMdEdit::updateImageBlocks(QSet<int> p_imageBlocks)
-{
-    if (!m_previewImage) {
-        return;
-    }
-    // We need to handle blocks backward to avoid shifting all the following blocks.
-    // Inserting the preview image block may cause highlighter to emit signal again.
-    QList<int> blockList = p_imageBlocks.toList();
-    std::sort(blockList.begin(), blockList.end(), std::greater<int>());
-    auto it = blockList.begin();
-    while (it != blockList.end()) {
-        previewImageOfBlock(*it);
-        ++it;
-    }
-
-    // Clean up un-referenced QChar::ObjectReplacementCharacter.
-    clearOrphanImagePreviewBlock();
-
-    emit statusChanged();
-}
-
-void VMdEdit::clearOrphanImagePreviewBlock()
-{
-    QTextDocument *doc = document();
-    QTextBlock block = doc->begin();
-    while (block.isValid()) {
-        if (isOrphanImagePreviewBlock(block)) {
-            qDebug() << "remove orphan image preview block" << block.blockNumber();
-            QTextBlock nextBlock = block.next();
-            removeBlock(block);
-            block = nextBlock;
-        } else {
-            clearCorruptedImagePreviewBlock(block);
-            block = block.next();
-        }
-    }
-}
-
-bool VMdEdit::isOrphanImagePreviewBlock(QTextBlock p_block)
-{
-    if (isImagePreviewBlock(p_block)) {
-        // It is an orphan image preview block if previous block is not
-        // a block need to preview (containing exactly one image) or the image
-        // paths are not equal to each other.
-        QTextBlock prevBlock = p_block.previous();
-        if (prevBlock.isValid()) {
-            QString imageLink = fetchImageToPreview(prevBlock.text());
-            if (imageLink.isEmpty()) {
-                return true;
-            }
-            QString imagePath = QDir(m_file->retriveBasePath()).filePath(imageLink);
-
-            // Get image preview block's image path.
-            QTextCursor cursor(p_block);
-            int shift = p_block.text().indexOf(QChar::ObjectReplacementCharacter);
-            if (shift > 0) {
-                cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor,
-                                    shift + 1);
-            }
-            QTextImageFormat format = cursor.charFormat().toImageFormat();
-            Q_ASSERT(format.isValid());
-            QString curPath = format.property(ImagePath).toString();
-
-            return curPath != imagePath;
-        } else {
-            return true;
-        }
-    }
-    return false;
-}
-
-void VMdEdit::clearCorruptedImagePreviewBlock(QTextBlock p_block)
-{
-    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;
-        }
-    }
-    if (!onlySpaces && !replacementChars.isEmpty()) {
-        // ObjectReplacementCharacter mixed with other non-space texts.
-        // Users corrupt the image preview block. Just remove the char.
-        QTextCursor cursor(p_block);
-        int blockPos = p_block.position();
-        for (int i = replacementChars.size() - 1; i >= 0; --i) {
-            int pos = replacementChars[i];
-            cursor.setPosition(blockPos + pos);
-            cursor.deleteChar();
-        }
-        Q_ASSERT(text.remove(QChar::ObjectReplacementCharacter) == p_block.text());
-    }
-}
-
-void VMdEdit::clearAllImagePreviewBlocks()
-{
-    QTextDocument *doc = document();
-    QTextBlock block = doc->begin();
-    bool modified = isModified();
-    while (block.isValid()) {
-        if (isImagePreviewBlock(block)) {
-            QTextBlock nextBlock = block.next();
-            removeBlock(block);
-            block = nextBlock;
-        } else {
-            clearCorruptedImagePreviewBlock(block);
-            block = block.next();
-        }
-    }
-    setModified(modified);
-    emit statusChanged();
-}
-
-QString VMdEdit::fetchImageToPreview(const QString &p_text)
-{
-    QRegExp regExp("\\!\\[[^\\]]*\\]\\((images/[^/\\)]+)\\)");
-    int index = regExp.indexIn(p_text);
-    if (index == -1) {
-        return QString();
-    }
-    int lastIndex = regExp.lastIndexIn(p_text);
-    if (lastIndex != index) {
-        return QString();
-    }
-    return regExp.capturedTexts()[1];
-}
-
-void VMdEdit::previewImageOfBlock(int p_block)
-{
-    QTextDocument *doc = document();
-    QTextBlock block = doc->findBlockByNumber(p_block);
-    if (!block.isValid()) {
-        return;
-    }
-
-    QString text = block.text();
-    QString imageLink = fetchImageToPreview(text);
-    if (imageLink.isEmpty()) {
-        return;
-    }
-    QString imagePath = QDir(m_file->retriveBasePath()).filePath(imageLink);
-    qDebug() << "block" << p_block << "image" << imagePath;
-
-    if (isImagePreviewBlock(p_block + 1)) {
-        updateImagePreviewBlock(p_block + 1, imagePath);
-        return;
-    }
-    insertImagePreviewBlock(p_block, imagePath);
-}
-
-bool VMdEdit::isImagePreviewBlock(int p_block)
-{
-    QTextDocument *doc = document();
-    QTextBlock block = doc->findBlockByNumber(p_block);
-    if (!block.isValid()) {
-        return false;
-    }
-    QString text = block.text().trimmed();
-    return text == QString(QChar::ObjectReplacementCharacter);
-}
-
-bool VMdEdit::isImagePreviewBlock(QTextBlock p_block)
-{
-    if (!p_block.isValid()) {
-        return false;
-    }
-    QString text = p_block.text().trimmed();
-    return text == QString(QChar::ObjectReplacementCharacter);
-}
-
-void VMdEdit::insertImagePreviewBlock(int p_block, const QString &p_image)
-{
-    QTextDocument *doc = document();
-
-    QImage image(p_image);
-    if (image.isNull()) {
-        return;
-    }
-
-    // Store current status.
-    bool modified = isModified();
-    int pos = textCursor().position();
-
-    QTextCursor cursor(doc->findBlockByNumber(p_block));
-    cursor.beginEditBlock();
-    cursor.movePosition(QTextCursor::EndOfBlock);
-    cursor.insertBlock();
-
-    QTextImageFormat imgFormat;
-    imgFormat.setName(p_image);
-    imgFormat.setProperty(ImagePath, p_image);
-    cursor.insertImage(imgFormat);
-    Q_ASSERT(cursor.block().text().at(0) == QChar::ObjectReplacementCharacter);
-    cursor.endEditBlock();
-
-    QTextCursor tmp = textCursor();
-    tmp.setPosition(pos);
-    setTextCursor(tmp);
-    setModified(modified);
-    emit statusChanged();
-}
-
-void VMdEdit::updateImagePreviewBlock(int p_block, const QString &p_image)
-{
-    Q_ASSERT(isImagePreviewBlock(p_block));
-    QTextDocument *doc = document();
-    QTextBlock block = doc->findBlockByNumber(p_block);
-    if (!block.isValid()) {
-        return;
-    }
-    QTextCursor cursor(block);
-    int shift = block.text().indexOf(QChar::ObjectReplacementCharacter);
-    if (shift > 0) {
-        cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, shift + 1);
-    }
-    QTextImageFormat format = cursor.charFormat().toImageFormat();
-    Q_ASSERT(format.isValid());
-    QString curPath = format.property(ImagePath).toString();
-
-    if (curPath == p_image) {
-        return;
-    }
-    // Update it with the new image.
-    QImage image(p_image);
-    if (image.isNull()) {
-        // Delete current preview block.
-        removeBlock(block);
-        qDebug() << "remove invalid image in block" << p_block;
-        return;
-    }
-    format.setName(p_image);
-    qDebug() << "update block" << p_block << "to image" << p_image;
-}
-
-void VMdEdit::removeBlock(QTextBlock p_block)
-{
-    QTextCursor cursor(p_block);
-    cursor.select(QTextCursor::BlockUnderCursor);
-    cursor.removeSelectedText();
-}
-
 QString VMdEdit::toPlainTextWithoutImg() const
 {
     QString text = toPlainText();
@@ -568,18 +322,20 @@ void VMdEdit::handleEditStateChanged(KeyState p_state)
 
 void VMdEdit::handleSelectionChanged()
 {
+    if (!vconfig.getEnablePreviewImages()) {
+        return;
+    }
+
     QString text = textCursor().selectedText();
-    if (text.isEmpty() && !m_previewImage) {
-        m_previewImage = true;
-        m_mdHighlighter->updateHighlight();
-    } else if (m_previewImage) {
+    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_previewImage = false;
-            clearAllImagePreviewBlocks();
+            m_imagePreviewer->disableImagePreview();
         }
     }
 }
@@ -596,24 +352,22 @@ void VMdEdit::handleClipboardChanged(QClipboard::Mode p_mode)
             QString text = mimeData->text();
             if (clipboard->ownsClipboard() &&
                 (text.trimmed() == QString(QChar::ObjectReplacementCharacter))) {
-                QString imagePath = selectedImage();
-                qDebug() <<  "clipboard" << imagePath;
-                Q_ASSERT(!imagePath.isEmpty());
-                QImage image(imagePath);
-                Q_ASSERT(!image.isNull());
+                QImage image = selectedImage();
                 clipboard->clear(QClipboard::Clipboard);
-                clipboard->setImage(image, QClipboard::Clipboard);
+                if (!image.isNull()) {
+                    clipboard->setImage(image, QClipboard::Clipboard);
+                }
             }
         }
     }
 }
 
-QString VMdEdit::selectedImage()
+QImage VMdEdit::selectedImage()
 {
-    QString imagePath;
+    QImage image;
     QTextCursor cursor = textCursor();
     if (!cursor.hasSelection()) {
-        return imagePath;
+        return image;
     }
     int start = cursor.selectionStart();
     int end = cursor.selectionEnd();
@@ -622,9 +376,8 @@ QString VMdEdit::selectedImage()
     QTextBlock endBlock = doc->findBlock(end);
     QTextBlock block = startBlock;
     while (block.isValid()) {
-        if (isImagePreviewBlock(block)) {
-            QString image = fetchImageToPreview(block.previous().text());
-            imagePath = QDir(m_file->retriveBasePath()).filePath(image);
+        if (m_imagePreviewer->isImagePreviewBlock(block)) {
+            image = m_imagePreviewer->fetchCachedImageFromPreviewBlock(block);
             break;
         }
         if (block == endBlock) {
@@ -632,5 +385,5 @@ QString VMdEdit::selectedImage()
         }
         block = block.next();
     }
-    return imagePath;
+    return image;
 }

+ 6 - 22
src/vmdedit.h

@@ -6,6 +6,7 @@
 #include <QString>
 #include <QColor>
 #include <QClipboard>
+#include <QImage>
 #include "vtoc.h"
 #include "veditoperations.h"
 #include "vconfigmanager.h"
@@ -13,6 +14,7 @@
 class HGMarkdownHighlighter;
 class VCodeBlockHighlightHelper;
 class VDocument;
+class VImagePreviewer;
 
 class VMdEdit : public VEdit
 {
@@ -41,8 +43,6 @@ signals:
 private slots:
     void generateEditOutline();
     void updateCurHeader();
-    // Update block list containing image links.
-    void updateImageBlocks(QSet<int> p_imageBlocks);
     void handleEditStateChanged(KeyState p_state);
     void handleSelectionChanged();
     void handleClipboardChanged(QClipboard::Mode p_mode);
@@ -59,32 +59,16 @@ private:
     // 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;
-    void previewImageOfBlock(int p_block);
-    bool isImagePreviewBlock(int p_block);
-    bool isImagePreviewBlock(QTextBlock p_block);
-    // p_block is a image preview block. We need to update it with image.
-    void updateImagePreviewBlock(int p_block, const QString &p_image);
-    // Insert a block after @p_block to preview image @p_image.
-    void insertImagePreviewBlock(int p_block, const QString &p_image);
-    // Clean up un-referenced image preview block.
-    void clearOrphanImagePreviewBlock();
-    void removeBlock(QTextBlock p_block);
-    bool isOrphanImagePreviewBlock(QTextBlock p_block);
-    // Block that has the QChar::ObjectReplacementCharacter as well as some non-space characters.
-    void clearCorruptedImagePreviewBlock(QTextBlock p_block);
-    // Returns the image relative path (image/xxx.png) only when
-    // there is one and only one image link.
-    QString fetchImageToPreview(const QString &p_text);
-    void clearAllImagePreviewBlocks();
-    // There is a QChar::ObjectReplacementCharacter in the selection. Find out the image path.
-    QString selectedImage();
+    // There is a QChar::ObjectReplacementCharacter in the selection.
+    // Get the QImage.
+    QImage selectedImage();
 
     HGMarkdownHighlighter *m_mdHighlighter;
     VCodeBlockHighlightHelper *m_cbHighlighter;
+    VImagePreviewer *m_imagePreviewer;
     QVector<QString> m_insertedImages;
     QVector<QString> m_initImages;
     QVector<VHeader> m_headers;
-    bool m_previewImage;
 };
 
 #endif // VMDEDIT_H

+ 2 - 2
src/vmdeditoperations.cpp

@@ -131,8 +131,8 @@ bool VMdEditOperations::insertImageFromURL(const QUrl &imageUrl)
     } else {
         // Download it to a QImage
         VDownloader *downloader = new VDownloader(&dialog);
-        QObject::connect(downloader, &VDownloader::downloadFinished,
-                         &dialog, &VInsertImageDialog::imageDownloaded);
+        connect(downloader, &VDownloader::downloadFinished,
+                &dialog, &VInsertImageDialog::imageDownloaded);
         downloader->download(imageUrl.toString());
     }
     if (dialog.exec() == QDialog::Accepted) {