refactor VImagePreviewer

This commit is contained in:
Le Tan 2017-08-29 19:47:01 +08:00
parent 8b1d7e9841
commit c3408769b0
19 changed files with 1050 additions and 484 deletions

View File

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

View File

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

View File

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

View File

@ -116,6 +116,32 @@ bool VEditUtils::indentBlockAsPreviousBlock(QTextCursor &p_cursor)
return changed; 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, void VEditUtils::moveCursorFirstNonSpaceCharacter(QTextCursor &p_cursor,
QTextCursor::MoveMode p_mode) QTextCursor::MoveMode p_mode)
{ {
@ -135,7 +161,7 @@ void VEditUtils::moveCursorFirstNonSpaceCharacter(QTextCursor &p_cursor,
void VEditUtils::removeObjectReplacementCharacter(QString &p_text) 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(orcBlockExp);
p_text.remove(QChar::ObjectReplacementCharacter); p_text.remove(QChar::ObjectReplacementCharacter);
} }

View File

@ -29,6 +29,9 @@ public:
// @p_cursor will be placed at the position after inserting leading spaces. // @p_cursor will be placed at the position after inserting leading spaces.
static bool indentBlockAsPreviousBlock(QTextCursor &p_cursor); 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 // Insert a new block at current position with the same indentation as
// current block. Should clear the selection before calling this. // current block. Should clear the selection before calling this.
// Returns true if non-empty indentation has been inserted. // Returns true if non-empty indentation has been inserted.

View File

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

28
src/utils/vpreviewutils.h Normal file
View File

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

View File

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

View File

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

View File

@ -50,4 +50,15 @@ enum FindOption
IncrementalSearch = 0x8U 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 #endif

View File

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

View File

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

View File

@ -5,131 +5,347 @@
#include <QDebug> #include <QDebug>
#include <QDir> #include <QDir>
#include <QUrl> #include <QUrl>
#include <QVector>
#include "vmdedit.h" #include "vmdedit.h"
#include "vconfigmanager.h" #include "vconfigmanager.h"
#include "utils/vutils.h" #include "utils/vutils.h"
#include "utils/veditutils.h" #include "utils/veditutils.h"
#include "utils/vpreviewutils.h"
#include "vfile.h" #include "vfile.h"
#include "vdownloader.h" #include "vdownloader.h"
#include "hgmarkdownhighlighter.h" #include "hgmarkdownhighlighter.h"
#include "vtextblockdata.h"
extern VConfigManager *g_config; extern VConfigManager *g_config;
enum ImageProperty { ImagePath = 1 };
const int VImagePreviewer::c_minImageWidth = 100; 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()), : QObject(p_edit), m_edit(p_edit), m_document(p_edit->document()),
m_file(p_edit->getFile()), m_enablePreview(true), m_isPreviewing(false), m_file(p_edit->getFile()), m_imageWidth(c_minImageWidth),
m_requestCearBlocks(false), m_requestRefreshBlocks(false), m_timeStamp(0), m_previewIndex(0),
m_updatePending(false), m_imageWidth(c_minImageWidth) m_previewEnabled(g_config->getEnablePreviewImages()), m_isPreviewing(false)
{ {
m_timer = new QTimer(this); m_updateTimer = new QTimer(this);
m_timer->setSingleShot(true); m_updateTimer->setSingleShot(true);
Q_ASSERT(p_timeToPreview > 0); m_updateTimer->setInterval(400);
m_timer->setInterval(p_timeToPreview); connect(m_updateTimer, &QTimer::timeout,
this, &VImagePreviewer::doUpdatePreviewImageWidth);
connect(m_timer, &QTimer::timeout,
this, &VImagePreviewer::timerTimeout);
m_downloader = new VDownloader(this); m_downloader = new VDownloader(this);
connect(m_downloader, &VDownloader::downloadFinished, connect(m_downloader, &VDownloader::downloadFinished,
this, &VImagePreviewer::imageDownloaded); this, &VImagePreviewer::imageDownloaded);
connect(m_edit->document(), &QTextDocument::contentsChange,
this, &VImagePreviewer::handleContentChange);
} }
void VImagePreviewer::timerTimeout() void VImagePreviewer::imageLinksChanged(const QVector<VElementRegion> &p_imageRegions)
{ {
if (!g_config->getEnablePreviewImages()) { kickOffPreview(p_imageRegions);
if (m_enablePreview) { }
disableImagePreview();
} void VImagePreviewer::kickOffPreview(const QVector<VElementRegion> &p_imageRegions)
{
if (!m_previewEnabled) {
Q_ASSERT(m_imageRegions.isEmpty());
Q_ASSERT(m_previewImages.isEmpty());
Q_ASSERT(m_imageCache.isEmpty());
return; return;
} }
if (!m_enablePreview) { m_isPreviewing = true;
return;
}
if (m_isPreviewing) { m_imageRegions = p_imageRegions;
m_updatePending = true; ++m_timeStamp;
return;
}
previewImages(); previewImages();
}
void VImagePreviewer::handleContentChange(int /* p_position */, shrinkImageCache();
int p_charsRemoved, m_isPreviewing = false;
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;
} }
void VImagePreviewer::previewImages() void VImagePreviewer::previewImages()
{ {
if (m_isPreviewing) {
return;
}
// Get the width of the m_edit. // Get the width of the m_edit.
m_imageWidth = qMax(m_edit->size().width() - 50, c_minImageWidth); m_imageWidth = qMax(m_edit->size().width() - 50, c_minImageWidth);
m_isPreviewing = true; QVector<ImageLinkInfo> imageLinks;
QTextBlock block = m_document->begin(); fetchImageLinksFromRegions(imageLinks);
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);
if (isNormalBlock(block)) { QTextCursor cursor(m_document);
block = previewImageOfOneBlock(block); previewImageLinks(imageLinks, cursor);
} else { clearObsoletePreviewImages(cursor);
block = block.next(); }
}
} 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<ImageLinkInfo> &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;
} }
m_isPreviewing = false; QString imageName = imageCacheResourceName(link.m_linkUrl);
if (imageName.isEmpty()) {
if (m_requestCearBlocks) { continue;
m_requestCearBlocks = false;
clearAllImagePreviewBlocks();
} }
if (m_requestRefreshBlocks) { PreviewImageInfo info(m_previewIndex++, m_timeStamp,
m_requestRefreshBlocks = false; link.m_linkUrl, link.m_isBlock);
refresh(); 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);
} }
if (m_updatePending) { p_cursor.insertImage(imgFormat);
m_updatePending = false; p_cursor.endEditBlock();
m_timer->stop();
m_timer->start(); 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(); 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;
}
}
bool hasObsolete = false;
QTextBlock block = m_document->begin();
// Clean block data and delete obsolete preview.
while (block.isValid()) {
if (!VTextBlockData::containsPreviewImage(block)) {
block = block.next();
continue;
} else {
QTextBlock nextBlock = block.next();
// Notice the short circuit.
hasObsolete = clearObsoletePreviewImagesOfBlock(block, p_cursor) || hasObsolete;
block = nextBlock;
}
}
if (hasObsolete) {
emit m_edit->statusChanged();
}
}
bool VImagePreviewer::isImageSourcePreviewImage(const QTextImageFormat &p_format) const
{
if (!p_format.isValid()) {
return false;
}
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;
}
}
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<ImageLinkInfo> &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 &reg = 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) bool VImagePreviewer::isImagePreviewBlock(const QTextBlock &p_block)
@ -142,31 +358,6 @@ bool VImagePreviewer::isImagePreviewBlock(const QTextBlock &p_block)
return text == QString(QChar::ObjectReplacementCharacter); 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) QString VImagePreviewer::fetchImageUrlToPreview(const QString &p_text)
{ {
QRegExp regExp(VUtils::c_imageLinkRegExp); QRegExp regExp(VUtils::c_imageLinkRegExp);
@ -193,6 +384,7 @@ QString VImagePreviewer::fetchImagePathToPreview(const QString &p_text)
QString imagePath; QString imagePath;
QFileInfo info(m_file->retriveBasePath(), imageUrl); QFileInfo info(m_file->retriveBasePath(), imageUrl);
if (info.exists()) { if (info.exists()) {
if (info.isNativePath()) { if (info.isNativePath()) {
// Local file. // Local file.
@ -200,221 +392,38 @@ QString VImagePreviewer::fetchImagePathToPreview(const QString &p_text)
} else { } else {
imagePath = imageUrl; imagePath = imageUrl;
} }
} else {
QString decodedUrl(imageUrl);
VUtils::decodeUrl(decodedUrl);
QFileInfo dinfo(m_file->retriveBasePath(), decodedUrl);
if (dinfo.exists()) {
if (dinfo.isNativePath()) {
// Local file.
imagePath = QDir::cleanPath(dinfo.absoluteFilePath());
} else {
imagePath = imageUrl;
}
} else { } else {
QUrl url(imageUrl); QUrl url(imageUrl);
imagePath = url.toString(); imagePath = url.toString();
} }
}
return imagePath; return imagePath;
} }
QTextBlock VImagePreviewer::previewImageOfOneBlock(QTextBlock &p_block) void VImagePreviewer::clearAllPreviewImages()
{ {
if (!p_block.isValid()) { m_imageRegions.clear();
return p_block; ++m_timeStamp;
}
QTextBlock nblock = p_block.next(); QTextCursor cursor(m_document);
clearObsoletePreviewImages(cursor);
QString imagePath = fetchImagePathToPreview(p_block.text()); m_imageCache.clear();
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, QTextImageFormat VImagePreviewer::fetchFormatFromPreviewBlock(const QTextBlock &p_block) const
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<int> replacementChars;
bool onlySpaces = true;
for (int i = 0; i < text.size(); ++i) {
if (text[i] == QChar::ObjectReplacementCharacter) {
replacementChars.append(i);
} else if (!text[i].isSpace()) {
onlySpaces = false;
}
}
if (!onlySpaces && !replacementChars.isEmpty()) {
// ObjectReplacementCharacter mixed with other non-space texts.
// Users corrupt the image preview block. Just remove the char.
bool modified = m_edit->isModified();
QTextCursor cursor(p_block);
cursor.beginEditBlock();
int blockPos = p_block.position();
for (int i = replacementChars.size() - 1; i >= 0; --i) {
int pos = replacementChars[i];
cursor.setPosition(blockPos + pos);
cursor.deleteChar();
}
cursor.endEditBlock();
m_edit->setModified(modified);
V_ASSERT(text.remove(QChar::ObjectReplacementCharacter) == p_block.text());
}
}
bool VImagePreviewer::isPreviewEnabled()
{
return m_enablePreview;
}
void VImagePreviewer::enableImagePreview()
{
m_enablePreview = true;
if (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)
{ {
QTextCursor cursor(p_block); QTextCursor cursor(p_block);
int shift = p_block.text().indexOf(QChar::ObjectReplacementCharacter); int shift = p_block.text().indexOf(QChar::ObjectReplacementCharacter);
@ -427,29 +436,6 @@ QTextImageFormat VImagePreviewer::fetchFormatFromPreviewBlock(QTextBlock &p_bloc
return cursor.charFormat().toImageFormat(); 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) QString VImagePreviewer::imageCacheResourceName(const QString &p_imagePath)
{ {
V_ASSERT(!p_imagePath.isEmpty()); V_ASSERT(!p_imagePath.isEmpty());
@ -496,33 +482,23 @@ void VImagePreviewer::imageDownloaded(const QByteArray &p_data, const QString &p
return; return;
} }
m_timer->stop();
QString name(imagePathToCacheResourceName(p_url)); QString name(imagePathToCacheResourceName(p_url));
m_document->addResource(QTextDocument::ImageResource, name, image); m_document->addResource(QTextDocument::ImageResource, name, image);
m_imageCache.insert(p_url, ImageInfo(name, image.width())); m_imageCache.insert(p_url, ImageInfo(name, image.width()));
qDebug() << "downloaded image cache insert" << p_url << name; qDebug() << "downloaded image cache insert" << p_url << name;
emit requestUpdateImageLinks();
m_timer->start();
} }
} }
void VImagePreviewer::refresh() QImage VImagePreviewer::fetchCachedImageByID(long long p_id)
{ {
if (m_isPreviewing) { auto imgIt = m_previewImages.find(p_id);
m_requestRefreshBlocks = true; if (imgIt == m_previewImages.end()) {
return; return QImage();
} }
m_timer->stop(); QString path = imgIt->m_path;
m_imageCache.clear();
clearAllImagePreviewBlocks();
m_timer->start();
}
QImage VImagePreviewer::fetchCachedImageFromPreviewBlock(QTextBlock &p_block)
{
QString path = fetchImagePathFromPreviewBlock(p_block);
if (path.isEmpty()) { if (path.isEmpty()) {
return QImage(); return QImage();
} }
@ -537,13 +513,17 @@ QImage VImagePreviewer::fetchCachedImageFromPreviewBlock(QTextBlock &p_block)
bool VImagePreviewer::updateImageWidth(QTextImageFormat &p_format) bool VImagePreviewer::updateImageWidth(QTextImageFormat &p_format)
{ {
QString path = p_format.property(ImagePath).toString(); long long imageID = VPreviewUtils::getPreviewImageID(p_format);
auto it = m_imageCache.find(path); 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()) { if (it != m_imageCache.end()) {
int newWidth = it.value().m_width; int newWidth = it.value().m_width;
if (g_config->getEnablePreviewImageConstraint()) { if (g_config->getEnablePreviewImageConstraint()) {
newWidth = qMin(m_imageWidth, it.value().m_width); newWidth = qMin(m_imageWidth, newWidth);
} }
if (newWidth != p_format.width()) { if (newWidth != p_format.width()) {
@ -555,8 +535,90 @@ bool VImagePreviewer::updateImageWidth(QTextImageFormat &p_format)
return false; return false;
} }
void VImagePreviewer::update() void VImagePreviewer::updatePreviewImageWidth()
{ {
m_timer->stop(); if (!m_previewEnabled) {
m_timer->start(); 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<QString, bool> usedImagePath;
for (auto it = m_previewImages.begin(); it != m_previewImages.end(); ++it) {
usedImagePath.insert(it->m_path, true);
}
for (auto it = m_imageCache.begin(); it != m_imageCache.end();) {
if (!usedImagePath.contains(it.key())) {
qDebug() << "shrink one image" << it.key();
it = m_imageCache.erase(it);
} else {
++it;
}
}
}
} }

View File

@ -5,9 +5,10 @@
#include <QString> #include <QString>
#include <QTextBlock> #include <QTextBlock>
#include <QHash> #include <QHash>
#include "hgmarkdownhighlighter.h"
class VMdEdit;
class QTimer; class QTimer;
class VMdEdit;
class QTextDocument; class QTextDocument;
class VFile; class VFile;
class VDownloader; class VDownloader;
@ -16,27 +17,37 @@ class VImagePreviewer : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit VImagePreviewer(VMdEdit *p_edit, int p_timeToPreview); explicit VImagePreviewer(VMdEdit *p_edit);
void disableImagePreview();
void enableImagePreview();
bool isPreviewEnabled();
// 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); 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. // Update preview image width.
// Then re-preview all the blocks. void updatePreviewImageWidth();
void refresh();
void update(); bool isPreviewing() const;
bool isEnabled() const;
public slots:
// Image links have changed.
void imageLinksChanged(const QVector<VElementRegion> &p_imageRegions);
private slots: private slots:
void timerTimeout(); // Non-local image downloaded for preview.
void handleContentChange(int p_position, int p_charsRemoved, int p_charsAdded);
void imageDownloaded(const QByteArray &p_data, const QString &p_url); 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: private:
struct ImageInfo struct ImageInfo
{ {
@ -49,8 +60,92 @@ private:
int m_width; int m_width;
}; };
struct ImageLinkInfo
{
ImageLinkInfo()
: m_startPos(-1), m_endPos(-1),
m_isBlock(false), m_previewImageID(-1)
{
}
ImageLinkInfo(int p_startPos, int p_endPos)
: m_startPos(p_startPos), m_endPos(p_endPos),
m_isBlock(false), m_previewImageID(-1)
{
}
int m_startPos;
int m_endPos;
QString m_linkUrl;
// Whether it is a image block.
bool m_isBlock;
// The previewed image ID if this link has been previewed.
// -1 if this link has not yet been previewed.
long long m_previewImageID;
};
// Info about a previewed image.
struct PreviewImageInfo
{
PreviewImageInfo() : m_id(-1), m_timeStamp(-1)
{
}
PreviewImageInfo(long long p_id, long long p_timeStamp,
const QString p_path, bool p_isBlock)
: m_id(p_id), m_timeStamp(p_timeStamp),
m_path(p_path), m_isBlock(p_isBlock)
{
}
QString toString()
{
return QString("PreviewImageInfo(ID %0 path %1 stamp %2 isBlock %3")
.arg(m_id).arg(m_path).arg(m_timeStamp).arg(m_isBlock);
}
long long m_id;
long long m_timeStamp;
QString m_path;
bool m_isBlock;
};
// Kick off new preview of m_imageRegions.
void kickOffPreview(const QVector<VElementRegion> &p_imageRegions);
// Preview images according to m_timeStamp and m_imageRegions.
void previewImages(); 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<ImageLinkInfo> &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<ImageLinkInfo> &p_imageLinks, QTextCursor &p_cursor);
// Clear obsolete preview images whose timeStamp does not match current one
// or does not exist in the cache.
void clearObsoletePreviewImages(QTextCursor &p_cursor);
// 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. // Fetch the image link's URL if there is only one link.
QString fetchImageUrlToPreview(const QString &p_text); QString fetchImageUrlToPreview(const QString &p_text);
@ -58,31 +153,18 @@ private:
// Fetch teh image's full path if there is only one image link. // Fetch teh image's full path if there is only one image link.
QString fetchImagePathToPreview(const QString &p_text); QString fetchImagePathToPreview(const QString &p_text);
// Try to preview the image of @p_block. // Clear all the previewed images.
// Return the next block to process. void clearAllPreviewImages();
QTextBlock previewImageOfOneBlock(QTextBlock &p_block);
// Insert a new block to preview image. // Fetch the text image format from an image preview block.
QTextBlock insertImagePreviewBlock(QTextBlock &p_block, const QString &p_imagePath); QTextImageFormat fetchFormatFromPreviewBlock(const QTextBlock &p_block) const;
// @p_block is the image block. Update it to preview @p_imagePath. // Whether the preview image is Image source.
void updateImagePreviewBlock(QTextBlock &p_block, const QString &p_imagePath); bool isImageSourcePreviewImage(const QTextImageFormat &p_format) const;
void removeBlock(QTextBlock &p_block); void initImageFormat(QTextImageFormat &p_imgFormat,
const QString &p_imageName,
// Corrupted image preview block: ObjectReplacementCharacter mixed with other const PreviewImageInfo &p_info) const;
// 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. // Look up m_imageCache to get the resource name in QTextDocument's cache.
// If there is none, insert it. // If there is none, insert it.
@ -93,28 +175,54 @@ private:
// Return true if and only if there is update. // Return true if and only if there is update.
bool updateImageWidth(QTextImageFormat &p_format); bool updateImageWidth(QTextImageFormat &p_format);
// Whether it is a normal block or not. // Clean up image cache.
bool isNormalBlock(const QTextBlock &p_block); void shrinkImageCache();
VMdEdit *m_edit; VMdEdit *m_edit;
QTextDocument *m_document; QTextDocument *m_document;
VFile *m_file; 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. // Map from image full path to QUrl identifier in the QTextDocument's cache.
QHash<QString, ImageInfo> m_imageCache;; QHash<QString, ImageInfo> m_imageCache;
VDownloader *m_downloader; VDownloader *m_downloader;
// The preview width. // The preview width.
int m_imageWidth; int m_imageWidth;
// Used to denote the obsolete previewed images.
// Increased when a new preview is kicked off.
long long m_timeStamp;
// Incremental ID for previewed images.
long long m_previewIndex;
// Map from previewImageID to PreviewImageInfo.
QHash<long long, PreviewImageInfo> m_previewImages;
// Regions of all the image links.
QVector<VElementRegion> m_imageRegions;
// Timer for updatePreviewImageWidth().
QTimer *m_updateTimer;
// Whether preview is enabled.
bool m_previewEnabled;
// Whether preview is ongoing.
bool m_isPreviewing;
static const int c_minImageWidth; static const int c_minImageWidth;
}; };
inline bool VImagePreviewer::isPreviewing() const
{
return m_isPreviewing;
}
inline bool VImagePreviewer::isEnabled() const
{
return m_previewEnabled;
}
#endif // VIMAGEPREVIEWER_H #endif // VIMAGEPREVIEWER_H

View File

@ -638,12 +638,11 @@ void VMainWindow::initMarkdownMenu()
codeBlockAct->setChecked(g_config->getEnableCodeBlockHighlight()); codeBlockAct->setChecked(g_config->getEnableCodeBlockHighlight());
QAction *previewImageAct = new QAction(tr("Preview Images In Edit Mode"), this); 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); previewImageAct->setCheckable(true);
connect(previewImageAct, &QAction::triggered, connect(previewImageAct, &QAction::triggered,
this, &VMainWindow::enableImagePreview); 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()); previewImageAct->setChecked(g_config->getEnablePreviewImages());
QAction *previewWidthAct = new QAction(tr("Constrain The Width Of Previewed Images"), this); QAction *previewWidthAct = new QAction(tr("Constrain The Width Of Previewed Images"), this);

View File

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

View File

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

10
src/vtextblockdata.cpp Normal file
View File

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

44
src/vtextblockdata.h Normal file
View File

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