diff --git a/src/hgmarkdownhighlighter.cpp b/src/hgmarkdownhighlighter.cpp index d1f12d51..2a8972e4 100644 --- a/src/hgmarkdownhighlighter.cpp +++ b/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(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:" << m_commentRegions.size() << "HTML comment regions"; + 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 ® = 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; + } + + 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; } + if (highlightingStyles.isEmpty()) { + goto exit; + } + + { int nrBlocks = document->blockCount(); parseInternal(); - if (highlightingStyles.isEmpty()) { - qWarning() << "HighlightingStyles is not set"; - return; - } - initBlockHighlightFromResult(nrBlocks); initHtmlCommentRegionsFromResult(); + initImageRegionsFromResult(); + if (result) { pmh_free_elements(result); result = NULL; } + } +exit: parsing.store(0); } diff --git a/src/hgmarkdownhighlighter.h b/src/hgmarkdownhighlighter.h index 33eac9f9..98314c9f 100644 --- a/src/hgmarkdownhighlighter.h +++ b/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 &p_codeBlocks); + // Emitted when image regions have been fetched from a new parsing result. + void imageLinksUpdated(const QVector &p_imageRegions); + protected: void highlightBlock(const QString &text) Q_DECL_OVERRIDE; @@ -151,7 +162,10 @@ private: int m_numOfCodeBlockHighlightsToRecv; // All HTML comment regions. - QVector m_commentRegions; + QVector m_commentRegions; + + // All image link regions. + QVector 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 diff --git a/src/src.pro b/src/src.pro index 2dd1a518..c356cdd9 100644 --- a/src/src.pro +++ b/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 \ diff --git a/src/utils/veditutils.cpp b/src/utils/veditutils.cpp index 5df5a30a..fee0f7d1 100644 --- a/src/utils/veditutils.cpp +++ b/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); } diff --git a/src/utils/veditutils.h b/src/utils/veditutils.h index 025a45c8..407a5dcb 100644 --- a/src/utils/veditutils.h +++ b/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. diff --git a/src/utils/vpreviewutils.cpp b/src/utils/vpreviewutils.cpp new file mode 100644 index 00000000..94445516 --- /dev/null +++ b/src/utils/vpreviewutils.cpp @@ -0,0 +1,61 @@ +#include "vpreviewutils.h" + +#include +#include + +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; + } +} diff --git a/src/utils/vpreviewutils.h b/src/utils/vpreviewutils.h new file mode 100644 index 00000000..3cb2a0e6 --- /dev/null +++ b/src/utils/vpreviewutils.h @@ -0,0 +1,28 @@ +#ifndef VPREVIEWUTILS_H +#define VPREVIEWUTILS_H + +#include +#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 diff --git a/src/utils/vutils.cpp b/src/utils/vutils.cpp index 3e126482..ea44cfc9 100644 --- a/src/utils/vutils.cpp +++ b/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 maps; + maps.insert("%20", " "); + + for (auto it = maps.begin(); it != maps.end(); ++it) { + p_url.replace(it.key(), it.value()); + } +} diff --git a/src/utils/vutils.h b/src/utils/vutils.h index d1d9d6c8..6c863744 100644 --- a/src/utils/vutils.h +++ b/src/utils/vutils.h @@ -53,7 +53,7 @@ public: static void processStyle(QString &style, const QVector > &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(); diff --git a/src/vconstants.h b/src/vconstants.h index efdba3b2..4f03a60e 100644 --- a/src/vconstants.h +++ b/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 diff --git a/src/vedit.cpp b/src/vedit.cpp index 797fe90f..a44c04ab 100644 --- a/src/vedit.cpp +++ b/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 &p_actions) +{ + Q_UNUSED(p_menu); + Q_UNUSED(p_actions); +} diff --git a/src/vedit.h b/src/vedit.h index 83c6692d..b3639719 100644 --- a/src/vedit.h +++ b/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 &p_actions); + private: QLabel *m_wrapLabel; QTimer *m_labelTimer; diff --git a/src/vimagepreviewer.cpp b/src/vimagepreviewer.cpp index af623e7d..45f0d778 100644 --- a/src/vimagepreviewer.cpp +++ b/src/vimagepreviewer.cpp @@ -5,131 +5,347 @@ #include #include #include +#include #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::timerTimeout() +void VImagePreviewer::imageLinksChanged(const QVector &p_imageRegions) { - if (!g_config->getEnablePreviewImages()) { - if (m_enablePreview) { - disableImagePreview(); - } + kickOffPreview(p_imageRegions); +} + +void VImagePreviewer::kickOffPreview(const QVector &p_imageRegions) +{ + 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(); -} -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(); -} - -bool VImagePreviewer::isNormalBlock(const QTextBlock &p_block) -{ - return p_block.userState() == HighlightBlockState::Normal; + shrinkImageCache(); + m_isPreviewing = false; } void VImagePreviewer::previewImages() { - if (m_isPreviewing) { - return; - } - // Get the width of the m_edit. m_imageWidth = qMax(m_edit->size().width() - 50, c_minImageWidth); - 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); + QVector imageLinks; + fetchImageLinksFromRegions(imageLinks); - if (isNormalBlock(block)) { - block = previewImageOfOneBlock(block); - } else { - block = block.next(); - } + QTextCursor cursor(m_document); + previewImageLinks(imageLinks, cursor); + clearObsoletePreviewImages(cursor); +} + +void VImagePreviewer::initImageFormat(QTextImageFormat &p_imgFormat, + const QString &p_imageName, + const PreviewImageInfo &p_info) const +{ + 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::previewImageLinks(QVector &p_imageLinks, + QTextCursor &p_cursor) +{ + bool hasNewPreview = false; + for (int i = 0; i < p_imageLinks.size(); ++i) { + ImageLinkInfo &link = p_imageLinks[i]; + if (link.m_previewImageID > -1) { + continue; + } + + QString imageName = imageCacheResourceName(link.m_linkUrl); + if (imageName.isEmpty()) { + continue; + } + + PreviewImageInfo info(m_previewIndex++, m_timeStamp, + link.m_linkUrl, link.m_isBlock); + QTextImageFormat imgFormat; + initImageFormat(imgFormat, imageName, info); + + 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); + } + + p_cursor.insertImage(imgFormat); + p_cursor.endEditBlock(); + + m_edit->setModified(isModified); + + Q_ASSERT(!m_previewImages.contains(info.m_id)); + m_previewImages.insert(info.m_id, info); + link.m_previewImageID = info.m_id; + + hasNewPreview = true; + qDebug() << "preview new image" << info.toString(); + } + + if (hasNewPreview) { + emit m_edit->statusChanged(); + } +} + +void VImagePreviewer::clearObsoletePreviewImages(QTextCursor &p_cursor) +{ + // 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; } } - m_isPreviewing = false; - - if (m_requestCearBlocks) { - m_requestCearBlocks = false; - clearAllImagePreviewBlocks(); + 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 (m_requestRefreshBlocks) { - m_requestRefreshBlocks = false; - refresh(); + if (hasObsolete) { + emit m_edit->statusChanged(); + } +} + +bool VImagePreviewer::isImageSourcePreviewImage(const QTextImageFormat &p_format) const +{ + if (!p_format.isValid()) { + return false; } - if (m_updatePending) { - m_updatePending = false; - m_timer->stop(); - m_timer->start(); + bool ok = true; + int src = p_format.property((int)ImageProperty::ImageSource).toInt(&ok); + if (ok) { + return src == (int)PreviewImageSource::Image; + } else { + return false; + } +} + +bool VImagePreviewer::clearObsoletePreviewImagesOfBlock(QTextBlock &p_block, + QTextCursor &p_cursor) +{ + 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; + } + + 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 (!isImageSourcePreviewImage(imageFormat)) { + hasValidPreview = true; + continue; + } + + 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 { + hasOtherChars = true; + } } - emit m_edit->statusChanged(); + 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); + } + + return hasObsolete; +} + +// 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) +{ + for (int i = p_start; i < p_end && i < p_text.size(); ++i) { + if (!p_text[i].isSpace()) { + return false; + } + } + + return true; +} + +void VImagePreviewer::fetchImageLinksFromRegions(QVector &p_imageLinks) +{ + p_imageLinks.clear(); + + if (m_imageRegions.isEmpty()) { + return; + } + + p_imageLinks.reserve(m_imageRegions.size()); + + for (int i = 0; i < m_imageRegions.size(); ++i) { + VElementRegion ® = m_imageRegions[i]; + QTextBlock block = m_document->findBlock(reg.m_startPos); + if (!block.isValid()) { + continue; + } + + 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)); + } + + // Check if this image link has been previewed previously. + info.m_previewImageID = isImageLinkPreviewed(info); + + // Sorted in descending order of m_startPos. + p_imageLinks.append(info); + + qDebug() << "image region" << i << info.m_startPos << info.m_endPos + << info.m_linkUrl << info.m_isBlock << info.m_previewImageID; + } +} + +long long VImagePreviewer::isImageLinkPreviewed(const ImageLinkInfo &p_info) +{ + 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 (!isImagePreviewBlock(nextBlock)) { + return imageID; + } + + // 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); + } + } + + 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::isImagePreviewBlock(const QTextBlock &p_block) @@ -142,31 +358,6 @@ bool VImagePreviewer::isImagePreviewBlock(const QTextBlock &p_block) 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(VUtils::c_imageLinkRegExp); @@ -193,6 +384,7 @@ QString VImagePreviewer::fetchImagePathToPreview(const QString &p_text) QString imagePath; QFileInfo info(m_file->retriveBasePath(), imageUrl); + if (info.exists()) { if (info.isNativePath()) { // Local file. @@ -201,220 +393,37 @@ QString VImagePreviewer::fetchImagePathToPreview(const QString &p_text) imagePath = imageUrl; } } else { - QUrl url(imageUrl); - imagePath = url.toString(); + 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(); + } } return imagePath; } -QTextBlock VImagePreviewer::previewImageOfOneBlock(QTextBlock &p_block) +void VImagePreviewer::clearAllPreviewImages() { - if (!p_block.isValid()) { - return p_block; - } + m_imageRegions.clear(); + ++m_timeStamp; - QTextBlock nblock = p_block.next(); + QTextCursor cursor(m_document); + clearObsoletePreviewImages(cursor); - 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(); - } + m_imageCache.clear(); } -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); - - updateImageWidth(imgFormat); - - 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(); - QString imageName; - - if (curPath == p_imagePath) { - if (updateImageWidth(format)) { - goto update; - } - - 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); - - updateImageWidth(format); - -update: - updateFormatInPreviewBlock(p_block, format); -} - -void VImagePreviewer::removeBlock(QTextBlock &p_block) -{ - bool modified = m_edit->isModified(); - - VEditUtils::removeBlock(p_block); - - 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 (g_config->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) +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() { - m_timer->stop(); - m_timer->start(); + 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) +{ + 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 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; + } + } + } } diff --git a/src/vimagepreviewer.h b/src/vimagepreviewer.h index 50ffe864..9d6a25a4 100644 --- a/src/vimagepreviewer.h +++ b/src/vimagepreviewer.h @@ -5,9 +5,10 @@ #include #include #include +#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); - // Clear the m_imageCache and all the preview blocks. - // Then re-preview all the blocks. - void refresh(); + // Update preview image width. + void updatePreviewImageWidth(); - void update(); + bool isPreviewing() const; + + bool isEnabled() const; + +public slots: + // Image links have changed. + void imageLinksChanged(const QVector &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,8 +60,92 @@ 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 &p_imageRegions); + + // Preview images according to m_timeStamp and m_imageRegions. void previewImages(); - bool isValidImagePreviewBlock(QTextBlock &p_block); + + // 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 &p_imageLinks); + + // 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 &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); + + // 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); + + // Update the width of preview image in @p_block. + bool updatePreviewImageWidthOfBlock(const QTextBlock &p_block, QTextCursor &p_cursor); + + // 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); // Fetch the image link's URL if there is only one link. QString fetchImageUrlToPreview(const QString &p_text); @@ -58,31 +153,18 @@ private: // 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); + // Clear all the previewed images. + void clearAllPreviewImages(); - // Insert a new block to preview image. - QTextBlock insertImagePreviewBlock(QTextBlock &p_block, const QString &p_imagePath); + // Fetch the text image format from an image preview block. + QTextImageFormat fetchFormatFromPreviewBlock(const QTextBlock &p_block) const; - // @p_block is the image block. Update it to preview @p_imagePath. - void updateImagePreviewBlock(QTextBlock &p_block, const QString &p_imagePath); + // Whether the preview image is Image source. + bool isImageSourcePreviewImage(const QTextImageFormat &p_format) const; - 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); + 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 m_imageCache;; + QHash 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 m_previewImages; + + // Regions of all the image links. + QVector 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 diff --git a/src/vmainwindow.cpp b/src/vmainwindow.cpp index f0579310..c9c010d8 100644 --- a/src/vmainwindow.cpp +++ b/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); diff --git a/src/vmdedit.cpp b/src/vmdedit.cpp index 9d08ac32..5c017c8e 100644 --- a/src/vmdedit.cpp +++ b/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 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 ® = 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 &p_regions) const { - if (!g_config->getEnablePreviewImages()) { - return; - } + QTextDocument *doc = document(); + QVector regs; + QString text = p_block.text(); + int nrOtherChar = 0; + int nrImage = 0; + bool hasBlock = false; - 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(); + // 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; } } + + 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; - } - block = block.next(); } + + 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); + } + } + return image; } void VMdEdit::resizeEvent(QResizeEvent *p_event) { - m_imagePreviewer->update(); + m_imagePreviewer->updatePreviewImageWidth(); VEdit::resizeEvent(p_event); } diff --git a/src/vmdedit.h b/src/vmdedit.h index ecb0930f..7fc4920b 100644 --- a/src/vmdedit.h +++ b/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 &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 &p_regions) const; + HGMarkdownHighlighter *m_mdHighlighter; VCodeBlockHighlightHelper *m_cbHighlighter; VImagePreviewer *m_imagePreviewer; diff --git a/src/vtextblockdata.cpp b/src/vtextblockdata.cpp new file mode 100644 index 00000000..21fcfed7 --- /dev/null +++ b/src/vtextblockdata.cpp @@ -0,0 +1,10 @@ +#include "vtextblockdata.h" + +VTextBlockData::VTextBlockData() + : QTextBlockUserData(), m_containsPreviewImage(false) +{ +} + +VTextBlockData::~VTextBlockData() +{ +} diff --git a/src/vtextblockdata.h b/src/vtextblockdata.h new file mode 100644 index 00000000..1be35d8e --- /dev/null +++ b/src/vtextblockdata.h @@ -0,0 +1,44 @@ +#ifndef VTEXTBLOCKDATA_H +#define VTEXTBLOCKDATA_H + +#include + +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(p_block.userData()); + if (!blockData) { + return false; + } + + return blockData->containsPreviewImage(); +} + +#endif // VTEXTBLOCKDATA_H