From a8614839d9667a684679faeca83e5fde2f7eaf34 Mon Sep 17 00:00:00 2001 From: Le Tan Date: Thu, 4 May 2017 19:45:30 +0800 Subject: [PATCH] refactor image preview logics by adding VImagePreviewer 1. Support previewing non-relative local images; 2. Support previewing network images; --- src/hgmarkdownhighlighter.cpp | 31 --- src/hgmarkdownhighlighter.h | 5 +- src/resources/vnote.ini | 1 + src/src.pro | 6 +- src/vconfigmanager.cpp | 3 + src/vconfigmanager.h | 22 ++ src/vdownloader.cpp | 11 +- src/vdownloader.h | 4 +- src/vedit.cpp | 6 + src/vedit.h | 1 + src/vimagepreviewer.cpp | 491 ++++++++++++++++++++++++++++++++++ src/vimagepreviewer.h | 95 +++++++ src/vmdedit.cpp | 297 ++------------------ src/vmdedit.h | 28 +- src/vmdeditoperations.cpp | 4 +- 15 files changed, 665 insertions(+), 340 deletions(-) create mode 100644 src/vimagepreviewer.cpp create mode 100644 src/vimagepreviewer.h diff --git a/src/hgmarkdownhighlighter.cpp b/src/hgmarkdownhighlighter.cpp index 06f5121e..fb8b44ac 100644 --- a/src/hgmarkdownhighlighter.cpp +++ b/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(); diff --git a/src/hgmarkdownhighlighter.h b/src/hgmarkdownhighlighter.h index 67f9dccb..927b7c1f 100644 --- a/src/hgmarkdownhighlighter.h +++ b/src/hgmarkdownhighlighter.h @@ -91,7 +91,6 @@ public: signals: void highlightCompleted(); - void imageBlocksUpdated(QSet p_blocks); void codeBlocksUpdated(const QList &p_codeBlocks); protected: @@ -124,8 +123,6 @@ private: int m_numOfCodeBlockHighlightsToRecv; - // Block numbers containing image link(s). - QSet 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(); diff --git a/src/resources/vnote.ini b/src/resources/vnote.ini index 64145b87..93e574d8 100644 --- a/src/resources/vnote.ini +++ b/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 diff --git a/src/src.pro b/src/src.pro index 44f625f5..323e2bab 100644 --- a/src/src.pro +++ b/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 \ diff --git a/src/vconfigmanager.cpp b/src/vconfigmanager.cpp index 119feecf..ba219966 100644 --- a/src/vconfigmanager.cpp +++ b/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() diff --git a/src/vconfigmanager.h b/src/vconfigmanager.h index 4255836a..35e70edc 100644 --- a/src/vconfigmanager.h +++ b/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 diff --git a/src/vdownloader.cpp b/src/vdownloader.cpp index 5c2f144d..74d9c4d9 100644 --- a/src/vdownloader.cpp +++ b/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(); } diff --git a/src/vdownloader.h b/src/vdownloader.h index 287daf8e..31e20877 100644 --- a/src/vdownloader.h +++ b/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); diff --git a/src/vedit.cpp b/src/vedit.cpp index 13cc6442..d4487f72 100644 --- a/src/vedit.cpp +++ b/src/vedit.cpp @@ -563,3 +563,9 @@ void VEdit::handleEditAct() { emit editNote(); } + +VFile *VEdit::getFile() const +{ + return m_file; +} + diff --git a/src/vedit.h b/src/vedit.h index 4b0dff77..c347eb12 100644 --- a/src/vedit.h +++ b/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(); diff --git a/src/vimagepreviewer.cpp b/src/vimagepreviewer.cpp new file mode 100644 index 00000000..4510199d --- /dev/null +++ b/src/vimagepreviewer.cpp @@ -0,0 +1,491 @@ +#include "vimagepreviewer.h" + +#include +#include +#include +#include +#include +#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 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(); +} diff --git a/src/vimagepreviewer.h b/src/vimagepreviewer.h new file mode 100644 index 00000000..7620ee93 --- /dev/null +++ b/src/vimagepreviewer.h @@ -0,0 +1,95 @@ +#ifndef VIMAGEPREVIEWER_H +#define VIMAGEPREVIEWER_H + +#include +#include +#include +#include + +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 m_imageCache;; + + VDownloader *m_downloader; +}; + +#endif // VIMAGEPREVIEWER_H diff --git a/src/vmdedit.cpp b/src/vmdedit.cpp index 5b0e9045..8470ff54 100644 --- a/src/vmdedit.cpp +++ b/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 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 blockList = p_imageBlocks.toList(); - std::sort(blockList.begin(), blockList.end(), std::greater()); - 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 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; } diff --git a/src/vmdedit.h b/src/vmdedit.h index b37afec9..b1e0ddec 100644 --- a/src/vmdedit.h +++ b/src/vmdedit.h @@ -6,6 +6,7 @@ #include #include #include +#include #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 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 m_insertedImages; QVector m_initImages; QVector m_headers; - bool m_previewImage; }; #endif // VMDEDIT_H diff --git a/src/vmdeditoperations.cpp b/src/vmdeditoperations.cpp index aa204624..6b94596f 100644 --- a/src/vmdeditoperations.cpp +++ b/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) {