mirror of
https://gitee.com/vnotex/vnote.git
synced 2025-07-05 13:59:52 +08:00
refactor VImagePreviewer
This commit is contained in:
parent
8b1d7e9841
commit
c3408769b0
@ -5,6 +5,7 @@
|
||||
#include "hgmarkdownhighlighter.h"
|
||||
#include "vconfigmanager.h"
|
||||
#include "utils/vutils.h"
|
||||
#include "vtextblockdata.h"
|
||||
|
||||
extern VConfigManager *g_config;
|
||||
|
||||
@ -79,6 +80,17 @@ HGMarkdownHighlighter::~HGMarkdownHighlighter()
|
||||
}
|
||||
}
|
||||
|
||||
void HGMarkdownHighlighter::updateBlockUserData(const QString &p_text)
|
||||
{
|
||||
VTextBlockData *blockData = dynamic_cast<VTextBlockData *>(currentBlockUserData());
|
||||
if (!blockData) {
|
||||
blockData = new VTextBlockData();
|
||||
setCurrentBlockUserData(blockData);
|
||||
}
|
||||
|
||||
blockData->setContainsPreviewImage(p_text.contains(QChar::ObjectReplacementCharacter));
|
||||
}
|
||||
|
||||
void HGMarkdownHighlighter::highlightBlock(const QString &text)
|
||||
{
|
||||
int blockNum = currentBlock().blockNumber();
|
||||
@ -94,6 +106,9 @@ void HGMarkdownHighlighter::highlightBlock(const QString &text)
|
||||
// We use PEG Markdown Highlight as the main highlighter.
|
||||
// We can use other highlighting methods to complement it.
|
||||
|
||||
// Set current block's user data.
|
||||
updateBlockUserData(text);
|
||||
|
||||
// If it is a block inside HTML comment, just skip it.
|
||||
if (isBlockInsideCommentRegion(currentBlock())) {
|
||||
setCurrentBlockState(HighlightBlockState::Comment);
|
||||
@ -105,7 +120,9 @@ void HGMarkdownHighlighter::highlightBlock(const QString &text)
|
||||
highlightCodeBlock(text);
|
||||
|
||||
// PEG Markdown Highlight does not handle links with spaces in the URL.
|
||||
highlightLinkWithSpacesInURL(text);
|
||||
// Links in the URL should be encoded to %20. We just let it be here and won't
|
||||
// fix this.
|
||||
// highlightLinkWithSpacesInURL(text);
|
||||
|
||||
// Highlight CodeBlock using VCodeBlockHighlightHelper.
|
||||
if (m_codeBlockHighlights.size() > blockNum) {
|
||||
@ -176,6 +193,7 @@ void HGMarkdownHighlighter::initBlockHighlightFromResult(int nrBlocks)
|
||||
|
||||
void HGMarkdownHighlighter::initHtmlCommentRegionsFromResult()
|
||||
{
|
||||
// From Qt5.7, the capacity is preserved.
|
||||
m_commentRegions.clear();
|
||||
|
||||
if (!result) {
|
||||
@ -189,12 +207,54 @@ void HGMarkdownHighlighter::initHtmlCommentRegionsFromResult()
|
||||
continue;
|
||||
}
|
||||
|
||||
m_commentRegions.push_back(VCommentRegion(elem->pos, elem->end));
|
||||
m_commentRegions.push_back(VElementRegion(elem->pos, elem->end));
|
||||
|
||||
elem = elem->next;
|
||||
}
|
||||
|
||||
qDebug() << "highlighter:" << 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);
|
||||
}
|
||||
|
||||
|
@ -83,12 +83,12 @@ struct HLUnitPos
|
||||
QString m_style;
|
||||
};
|
||||
|
||||
// HTML comment.
|
||||
struct VCommentRegion
|
||||
// Denote the region of a certain Markdown element.
|
||||
struct VElementRegion
|
||||
{
|
||||
VCommentRegion() : m_startPos(0), m_endPos(0) {}
|
||||
VElementRegion() : m_startPos(0), m_endPos(0) {}
|
||||
|
||||
VCommentRegion(int p_start, int p_end) : m_startPos(p_start), m_endPos(p_end) {}
|
||||
VElementRegion(int p_start, int p_end) : m_startPos(p_start), m_endPos(p_end) {}
|
||||
|
||||
// The start position of the region in document.
|
||||
int m_startPos;
|
||||
@ -101,6 +101,12 @@ struct VCommentRegion
|
||||
{
|
||||
return m_startPos <= p_pos && m_endPos >= p_pos;
|
||||
}
|
||||
|
||||
bool operator==(const VElementRegion &p_other) const
|
||||
{
|
||||
return (m_startPos == p_other.m_startPos
|
||||
&& m_endPos == p_other.m_endPos);
|
||||
}
|
||||
};
|
||||
|
||||
class HGMarkdownHighlighter : public QSyntaxHighlighter
|
||||
@ -118,8 +124,13 @@ public:
|
||||
|
||||
signals:
|
||||
void highlightCompleted();
|
||||
|
||||
// QList is implicitly shared.
|
||||
void codeBlocksUpdated(const QList<VCodeBlock> &p_codeBlocks);
|
||||
|
||||
// Emitted when image regions have been fetched from a new parsing result.
|
||||
void imageLinksUpdated(const QVector<VElementRegion> &p_imageRegions);
|
||||
|
||||
protected:
|
||||
void highlightBlock(const QString &text) Q_DECL_OVERRIDE;
|
||||
|
||||
@ -151,7 +162,10 @@ private:
|
||||
int m_numOfCodeBlockHighlightsToRecv;
|
||||
|
||||
// All HTML comment regions.
|
||||
QVector<VCommentRegion> m_commentRegions;
|
||||
QVector<VElementRegion> m_commentRegions;
|
||||
|
||||
// All image link regions.
|
||||
QVector<VElementRegion> m_imageRegions;
|
||||
|
||||
// Timer to signal highlightCompleted().
|
||||
QTimer *m_completeTimer;
|
||||
@ -168,7 +182,12 @@ private:
|
||||
|
||||
void resizeBuffer(int newCap);
|
||||
void highlightCodeBlock(const QString &text);
|
||||
|
||||
// Highlight links using regular expression.
|
||||
// PEG Markdown Highlight treat URLs with spaces illegal. This function is
|
||||
// intended to complement this.
|
||||
void highlightLinkWithSpacesInURL(const QString &p_text);
|
||||
|
||||
void parse();
|
||||
void parseInternal();
|
||||
void initBlockHighlightFromResult(int nrBlocks);
|
||||
@ -182,11 +201,17 @@ private:
|
||||
// Fetch all the HTML comment regions from parsing result.
|
||||
void initHtmlCommentRegionsFromResult();
|
||||
|
||||
// Fetch all the image link regions from parsing result.
|
||||
void initImageRegionsFromResult();
|
||||
|
||||
// Whether @p_block is totally inside a HTML comment.
|
||||
bool isBlockInsideCommentRegion(const QTextBlock &p_block) const;
|
||||
|
||||
// Highlights have been changed. Try to signal highlightCompleted().
|
||||
void highlightChanged();
|
||||
|
||||
// Set the user data of currentBlock().
|
||||
void updateBlockUserData(const QString &p_text);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
@ -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 \
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -29,6 +29,9 @@ public:
|
||||
// @p_cursor will be placed at the position after inserting leading spaces.
|
||||
static bool indentBlockAsPreviousBlock(QTextCursor &p_cursor);
|
||||
|
||||
// Returns true if two blocks has the same indent.
|
||||
static bool hasSameIndent(const QTextBlock &p_blocka, const QTextBlock &p_blockb);
|
||||
|
||||
// Insert a new block at current position with the same indentation as
|
||||
// current block. Should clear the selection before calling this.
|
||||
// Returns true if non-empty indentation has been inserted.
|
||||
|
61
src/utils/vpreviewutils.cpp
Normal file
61
src/utils/vpreviewutils.cpp
Normal 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
28
src/utils/vpreviewutils.h
Normal 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
|
@ -36,6 +36,8 @@ const QString VUtils::c_fencedCodeBlockStartRegExp = QString("^(\\s*)```([^`\\s]
|
||||
|
||||
const QString VUtils::c_fencedCodeBlockEndRegExp = QString("^(\\s*)```$");
|
||||
|
||||
const QString VUtils::c_previewImageBlockRegExp = QString("[\\n|^][ |\\t]*\\xfffc[ |\\t]*(?=\\n)");
|
||||
|
||||
VUtils::VUtils()
|
||||
{
|
||||
}
|
||||
@ -684,3 +686,13 @@ bool VUtils::splitPathInBasePath(const QString &p_base,
|
||||
qDebug() << QString("split path %1 based on %2 to %3 parts").arg(p_path).arg(p_base).arg(p_parts.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
void VUtils::decodeUrl(QString &p_url)
|
||||
{
|
||||
QHash<QString, QString> maps;
|
||||
maps.insert("%20", " ");
|
||||
|
||||
for (auto it = maps.begin(); it != maps.end(); ++it) {
|
||||
p_url.replace(it.key(), it.value());
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ public:
|
||||
static void processStyle(QString &style, const QVector<QPair<QString, QString> > &varMap);
|
||||
|
||||
// Return the last directory name of @p_path.
|
||||
static inline QString directoryNameFromPath(const QString& p_path);
|
||||
static QString directoryNameFromPath(const QString& p_path);
|
||||
|
||||
// Return the file name of @p_path.
|
||||
// /home/tamlok/abc, /home/tamlok/abc/ will both return abc.
|
||||
@ -118,6 +118,9 @@ public:
|
||||
const QString &p_path,
|
||||
QStringList &p_parts);
|
||||
|
||||
// Decode URL by simply replacing meta-characters.
|
||||
static void decodeUrl(QString &p_url);
|
||||
|
||||
// Regular expression for image link.
|
||||
// 
|
||||
// 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();
|
||||
|
||||
|
@ -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
|
||||
|
@ -785,6 +785,8 @@ void VEdit::contextMenuEvent(QContextMenuEvent *p_event)
|
||||
}
|
||||
}
|
||||
|
||||
alterContextMenu(menu, actions);
|
||||
|
||||
menu->exec(p_event->globalPos());
|
||||
delete menu;
|
||||
}
|
||||
@ -1256,3 +1258,9 @@ bool VEdit::isBlockVisible(const QTextBlock &p_block)
|
||||
|
||||
return (y >= 0 && y < height) || (y < 0 && y + rectHeight > 0);
|
||||
}
|
||||
|
||||
void VEdit::alterContextMenu(QMenu *p_menu, const QList<QAction *> &p_actions)
|
||||
{
|
||||
Q_UNUSED(p_menu);
|
||||
Q_UNUSED(p_actions);
|
||||
}
|
||||
|
@ -203,6 +203,9 @@ protected:
|
||||
// Update m_config according to VConfigManager.
|
||||
void updateConfig();
|
||||
|
||||
// Called in contextMenuEvent() to modify the context menu.
|
||||
virtual void alterContextMenu(QMenu *p_menu, const QList<QAction *> &p_actions);
|
||||
|
||||
private:
|
||||
QLabel *m_wrapLabel;
|
||||
QTimer *m_labelTimer;
|
||||
|
@ -5,131 +5,347 @@
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
#include <QUrl>
|
||||
#include <QVector>
|
||||
#include "vmdedit.h"
|
||||
#include "vconfigmanager.h"
|
||||
#include "utils/vutils.h"
|
||||
#include "utils/veditutils.h"
|
||||
#include "utils/vpreviewutils.h"
|
||||
#include "vfile.h"
|
||||
#include "vdownloader.h"
|
||||
#include "hgmarkdownhighlighter.h"
|
||||
#include "vtextblockdata.h"
|
||||
|
||||
extern VConfigManager *g_config;
|
||||
|
||||
enum ImageProperty { ImagePath = 1 };
|
||||
|
||||
const int VImagePreviewer::c_minImageWidth = 100;
|
||||
|
||||
VImagePreviewer::VImagePreviewer(VMdEdit *p_edit, int p_timeToPreview)
|
||||
VImagePreviewer::VImagePreviewer(VMdEdit *p_edit)
|
||||
: QObject(p_edit), m_edit(p_edit), m_document(p_edit->document()),
|
||||
m_file(p_edit->getFile()), m_enablePreview(true), m_isPreviewing(false),
|
||||
m_requestCearBlocks(false), m_requestRefreshBlocks(false),
|
||||
m_updatePending(false), m_imageWidth(c_minImageWidth)
|
||||
m_file(p_edit->getFile()), m_imageWidth(c_minImageWidth),
|
||||
m_timeStamp(0), m_previewIndex(0),
|
||||
m_previewEnabled(g_config->getEnablePreviewImages()), m_isPreviewing(false)
|
||||
{
|
||||
m_timer = new QTimer(this);
|
||||
m_timer->setSingleShot(true);
|
||||
Q_ASSERT(p_timeToPreview > 0);
|
||||
m_timer->setInterval(p_timeToPreview);
|
||||
|
||||
connect(m_timer, &QTimer::timeout,
|
||||
this, &VImagePreviewer::timerTimeout);
|
||||
m_updateTimer = new QTimer(this);
|
||||
m_updateTimer->setSingleShot(true);
|
||||
m_updateTimer->setInterval(400);
|
||||
connect(m_updateTimer, &QTimer::timeout,
|
||||
this, &VImagePreviewer::doUpdatePreviewImageWidth);
|
||||
|
||||
m_downloader = new VDownloader(this);
|
||||
connect(m_downloader, &VDownloader::downloadFinished,
|
||||
this, &VImagePreviewer::imageDownloaded);
|
||||
|
||||
connect(m_edit->document(), &QTextDocument::contentsChange,
|
||||
this, &VImagePreviewer::handleContentChange);
|
||||
}
|
||||
|
||||
void VImagePreviewer::timerTimeout()
|
||||
void VImagePreviewer::imageLinksChanged(const QVector<VElementRegion> &p_imageRegions)
|
||||
{
|
||||
if (!g_config->getEnablePreviewImages()) {
|
||||
if (m_enablePreview) {
|
||||
disableImagePreview();
|
||||
}
|
||||
kickOffPreview(p_imageRegions);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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<ImageLinkInfo> 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<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;
|
||||
|
||||
if (m_requestCearBlocks) {
|
||||
m_requestCearBlocks = false;
|
||||
clearAllImagePreviewBlocks();
|
||||
QString imageName = imageCacheResourceName(link.m_linkUrl);
|
||||
if (imageName.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (m_requestRefreshBlocks) {
|
||||
m_requestRefreshBlocks = false;
|
||||
refresh();
|
||||
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);
|
||||
}
|
||||
|
||||
if (m_updatePending) {
|
||||
m_updatePending = false;
|
||||
m_timer->stop();
|
||||
m_timer->start();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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 ® = 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.
|
||||
@ -200,221 +392,38 @@ QString VImagePreviewer::fetchImagePathToPreview(const QString &p_text)
|
||||
} else {
|
||||
imagePath = imageUrl;
|
||||
}
|
||||
} else {
|
||||
QString decodedUrl(imageUrl);
|
||||
VUtils::decodeUrl(decodedUrl);
|
||||
QFileInfo dinfo(m_file->retriveBasePath(), decodedUrl);
|
||||
if (dinfo.exists()) {
|
||||
if (dinfo.isNativePath()) {
|
||||
// Local file.
|
||||
imagePath = QDir::cleanPath(dinfo.absoluteFilePath());
|
||||
} else {
|
||||
imagePath = imageUrl;
|
||||
}
|
||||
} else {
|
||||
QUrl url(imageUrl);
|
||||
imagePath = url.toString();
|
||||
}
|
||||
}
|
||||
|
||||
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<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)
|
||||
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<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,9 +5,10 @@
|
||||
#include <QString>
|
||||
#include <QTextBlock>
|
||||
#include <QHash>
|
||||
#include "hgmarkdownhighlighter.h"
|
||||
|
||||
class VMdEdit;
|
||||
class QTimer;
|
||||
class VMdEdit;
|
||||
class QTextDocument;
|
||||
class VFile;
|
||||
class VDownloader;
|
||||
@ -16,27 +17,37 @@ class VImagePreviewer : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit VImagePreviewer(VMdEdit *p_edit, int p_timeToPreview);
|
||||
|
||||
void disableImagePreview();
|
||||
void enableImagePreview();
|
||||
bool isPreviewEnabled();
|
||||
explicit VImagePreviewer(VMdEdit *p_edit);
|
||||
|
||||
// Whether @p_block is an image previewed block.
|
||||
// The image previewed block is a block containing only the special character
|
||||
// and whitespaces.
|
||||
bool isImagePreviewBlock(const QTextBlock &p_block);
|
||||
|
||||
QImage fetchCachedImageFromPreviewBlock(QTextBlock &p_block);
|
||||
QImage fetchCachedImageByID(long long p_id);
|
||||
|
||||
// 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<VElementRegion> &p_imageRegions);
|
||||
|
||||
private slots:
|
||||
void timerTimeout();
|
||||
void handleContentChange(int p_position, int p_charsRemoved, int p_charsAdded);
|
||||
// Non-local image downloaded for preview.
|
||||
void imageDownloaded(const QByteArray &p_data, const QString &p_url);
|
||||
|
||||
// Update preview image width right now.
|
||||
void doUpdatePreviewImageWidth();
|
||||
|
||||
signals:
|
||||
// Request highlighter to update image links.
|
||||
void requestUpdateImageLinks();
|
||||
|
||||
private:
|
||||
struct ImageInfo
|
||||
{
|
||||
@ -49,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<VElementRegion> &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<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.
|
||||
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<QString, ImageInfo> m_imageCache;;
|
||||
QHash<QString, ImageInfo> m_imageCache;
|
||||
|
||||
VDownloader *m_downloader;
|
||||
|
||||
// The preview width.
|
||||
int m_imageWidth;
|
||||
|
||||
// Used to denote the obsolete previewed images.
|
||||
// Increased when a new preview is kicked off.
|
||||
long long m_timeStamp;
|
||||
|
||||
// Incremental ID for previewed images.
|
||||
long long m_previewIndex;
|
||||
|
||||
// Map from previewImageID to PreviewImageInfo.
|
||||
QHash<long long, PreviewImageInfo> m_previewImages;
|
||||
|
||||
// Regions of all the image links.
|
||||
QVector<VElementRegion> m_imageRegions;
|
||||
|
||||
// Timer for updatePreviewImageWidth().
|
||||
QTimer *m_updateTimer;
|
||||
|
||||
// Whether preview is enabled.
|
||||
bool m_previewEnabled;
|
||||
|
||||
// Whether preview is ongoing.
|
||||
bool m_isPreviewing;
|
||||
|
||||
static const int c_minImageWidth;
|
||||
};
|
||||
|
||||
inline bool VImagePreviewer::isPreviewing() const
|
||||
{
|
||||
return m_isPreviewing;
|
||||
}
|
||||
|
||||
inline bool VImagePreviewer::isEnabled() const
|
||||
{
|
||||
return m_previewEnabled;
|
||||
}
|
||||
|
||||
#endif // VIMAGEPREVIEWER_H
|
||||
|
@ -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);
|
||||
|
174
src/vmdedit.cpp
174
src/vmdedit.cpp
@ -7,8 +7,11 @@
|
||||
#include "vconfigmanager.h"
|
||||
#include "vtoc.h"
|
||||
#include "utils/vutils.h"
|
||||
#include "utils/veditutils.h"
|
||||
#include "utils/vpreviewutils.h"
|
||||
#include "dialog/vselectdialog.h"
|
||||
#include "vimagepreviewer.h"
|
||||
#include "vtextblockdata.h"
|
||||
|
||||
extern VConfigManager *g_config;
|
||||
extern VNote *g_vnote;
|
||||
@ -36,7 +39,11 @@ VMdEdit::VMdEdit(VFile *p_file, VDocument *p_vdoc, MarkdownConverterType p_type,
|
||||
m_cbHighlighter = new VCodeBlockHighlightHelper(m_mdHighlighter, p_vdoc,
|
||||
p_type);
|
||||
|
||||
m_imagePreviewer = new VImagePreviewer(this, 500);
|
||||
m_imagePreviewer = new VImagePreviewer(this);
|
||||
connect(m_mdHighlighter, &HGMarkdownHighlighter::imageLinksUpdated,
|
||||
m_imagePreviewer, &VImagePreviewer::imageLinksChanged);
|
||||
connect(m_imagePreviewer, &VImagePreviewer::requestUpdateImageLinks,
|
||||
m_mdHighlighter, &HGMarkdownHighlighter::updateHighlight);
|
||||
|
||||
m_editOps = new VMdEditOperations(this, m_file);
|
||||
|
||||
@ -48,8 +55,6 @@ VMdEdit::VMdEdit(VFile *p_file, VDocument *p_vdoc, MarkdownConverterType p_type,
|
||||
connect(this, &VMdEdit::cursorPositionChanged,
|
||||
this, &VMdEdit::updateCurHeader);
|
||||
|
||||
connect(this, &VMdEdit::selectionChanged,
|
||||
this, &VMdEdit::handleSelectionChanged);
|
||||
connect(QApplication::clipboard(), &QClipboard::changed,
|
||||
this, &VMdEdit::handleClipboardChanged);
|
||||
|
||||
@ -74,8 +79,6 @@ void VMdEdit::beginEdit()
|
||||
|
||||
initInitImages();
|
||||
|
||||
m_imagePreviewer->refresh();
|
||||
|
||||
setReadOnly(false);
|
||||
setModified(false);
|
||||
|
||||
@ -94,6 +97,7 @@ void VMdEdit::saveFile()
|
||||
if (!document()->isModified()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_file->setContent(toPlainTextWithoutImg());
|
||||
document()->setModified(false);
|
||||
}
|
||||
@ -364,50 +368,107 @@ void VMdEdit::scrollToHeader(const VAnchor &p_anchor)
|
||||
scrollToLine(p_anchor.lineNumber);
|
||||
}
|
||||
|
||||
QString VMdEdit::toPlainTextWithoutImg() const
|
||||
QString VMdEdit::toPlainTextWithoutImg()
|
||||
{
|
||||
QString text = toPlainText();
|
||||
int start = 0;
|
||||
do {
|
||||
int index = text.indexOf(QChar::ObjectReplacementCharacter, start);
|
||||
if (index == -1) {
|
||||
break;
|
||||
}
|
||||
start = removeObjectReplacementLine(text, index);
|
||||
} while (start > -1 && start < text.size());
|
||||
QString text;
|
||||
bool readOnly = isReadOnly();
|
||||
setReadOnly(true);
|
||||
text = getPlainTextWithoutPreviewImage();
|
||||
setReadOnly(readOnly);
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
int VMdEdit::removeObjectReplacementLine(QString &p_text, int p_index) const
|
||||
QString VMdEdit::getPlainTextWithoutPreviewImage() const
|
||||
{
|
||||
Q_ASSERT(p_text.size() > p_index && p_text.at(p_index) == QChar::ObjectReplacementCharacter);
|
||||
int prevLineIdx = p_text.lastIndexOf('\n', p_index);
|
||||
if (prevLineIdx == -1) {
|
||||
prevLineIdx = 0;
|
||||
QVector<Region> deletions;
|
||||
|
||||
while (true) {
|
||||
deletions.clear();
|
||||
|
||||
while (m_imagePreviewer->isPreviewing()) {
|
||||
VUtils::sleepWait(100);
|
||||
}
|
||||
|
||||
// Iterate all the block to get positions for deletion.
|
||||
QTextBlock block = document()->begin();
|
||||
bool tryAgain = false;
|
||||
while (block.isValid()) {
|
||||
if (VTextBlockData::containsPreviewImage(block)) {
|
||||
if (!getPreviewImageRegionOfBlock(block, deletions)) {
|
||||
tryAgain = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
block = block.next();
|
||||
}
|
||||
|
||||
if (tryAgain) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString text = toPlainText();
|
||||
// deletions is sorted by m_startPos.
|
||||
// From back to front.
|
||||
for (int i = deletions.size() - 1; i >= 0; --i) {
|
||||
const Region ® = deletions[i];
|
||||
qDebug() << "img region to delete" << reg.m_startPos << reg.m_endPos;
|
||||
text.remove(reg.m_startPos, reg.m_endPos - reg.m_startPos);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
// Remove [\n....?]
|
||||
p_text.remove(prevLineIdx, p_index - prevLineIdx + 1);
|
||||
return prevLineIdx - 1;
|
||||
}
|
||||
|
||||
void VMdEdit::handleSelectionChanged()
|
||||
bool VMdEdit::getPreviewImageRegionOfBlock(const QTextBlock &p_block,
|
||||
QVector<Region> &p_regions) const
|
||||
{
|
||||
if (!g_config->getEnablePreviewImages()) {
|
||||
return;
|
||||
QTextDocument *doc = document();
|
||||
QVector<Region> regs;
|
||||
QString text = p_block.text();
|
||||
int nrOtherChar = 0;
|
||||
int nrImage = 0;
|
||||
bool hasBlock = false;
|
||||
|
||||
// From back to front.
|
||||
for (int i = text.size() - 1; i >= 0; --i) {
|
||||
if (text[i].isSpace()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString text = textCursor().selectedText();
|
||||
if (text.isEmpty() && !m_imagePreviewer->isPreviewEnabled()) {
|
||||
m_imagePreviewer->enableImagePreview();
|
||||
} else if (m_imagePreviewer->isPreviewEnabled()) {
|
||||
if (text.trimmed() == QString(QChar::ObjectReplacementCharacter)) {
|
||||
// Select the image and some whitespaces.
|
||||
// We can let the user copy the image.
|
||||
return;
|
||||
} else if (text.contains(QChar::ObjectReplacementCharacter)) {
|
||||
m_imagePreviewer->disableImagePreview();
|
||||
if (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();
|
||||
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);
|
||||
}
|
||||
|
@ -34,8 +34,8 @@ public:
|
||||
|
||||
void scrollToHeader(const VAnchor &p_anchor);
|
||||
|
||||
// Like toPlainText(), but remove special blocks containing images.
|
||||
QString toPlainTextWithoutImg() const;
|
||||
// Like toPlainText(), but remove image preview characters.
|
||||
QString toPlainTextWithoutImg();
|
||||
|
||||
const QVector<VHeader> &getHeaders() const;
|
||||
|
||||
@ -58,7 +58,6 @@ private slots:
|
||||
// When there is no header in current cursor, will signal an invalid header.
|
||||
void updateCurHeader();
|
||||
|
||||
void handleSelectionChanged();
|
||||
void handleClipboardChanged(QClipboard::Mode p_mode);
|
||||
|
||||
protected:
|
||||
@ -69,20 +68,38 @@ protected:
|
||||
void resizeEvent(QResizeEvent *p_event) Q_DECL_OVERRIDE;
|
||||
|
||||
private:
|
||||
struct Region
|
||||
{
|
||||
Region() : m_startPos(-1), m_endPos(-1)
|
||||
{
|
||||
}
|
||||
|
||||
Region(int p_start, int p_end)
|
||||
: m_startPos(p_start), m_endPos(p_end)
|
||||
{
|
||||
}
|
||||
|
||||
int m_startPos;
|
||||
int m_endPos;
|
||||
};
|
||||
|
||||
void initInitImages();
|
||||
void clearUnusedImages();
|
||||
|
||||
// p_text[p_index] is QChar::ObjectReplacementCharacter. Remove the line containing it.
|
||||
// Returns the index of previous line's '\n'.
|
||||
int removeObjectReplacementLine(QString &p_text, int p_index) const;
|
||||
|
||||
// There is a QChar::ObjectReplacementCharacter in the selection.
|
||||
// Get the QImage.
|
||||
QImage selectedImage();
|
||||
// There is a QChar::ObjectReplacementCharacter (and maybe some spaces)
|
||||
// in the selection. Get the QImage.
|
||||
QImage tryGetSelectedImage();
|
||||
|
||||
// Return the header index in m_headers where current cursor locates.
|
||||
int currentCursorHeader() const;
|
||||
|
||||
QString getPlainTextWithoutPreviewImage() const;
|
||||
|
||||
// Try to get all the regions of preview image within @p_block.
|
||||
// Returns false if preview image is not ready yet.
|
||||
bool getPreviewImageRegionOfBlock(const QTextBlock &p_block,
|
||||
QVector<Region> &p_regions) const;
|
||||
|
||||
HGMarkdownHighlighter *m_mdHighlighter;
|
||||
VCodeBlockHighlightHelper *m_cbHighlighter;
|
||||
VImagePreviewer *m_imagePreviewer;
|
||||
|
10
src/vtextblockdata.cpp
Normal file
10
src/vtextblockdata.cpp
Normal file
@ -0,0 +1,10 @@
|
||||
#include "vtextblockdata.h"
|
||||
|
||||
VTextBlockData::VTextBlockData()
|
||||
: QTextBlockUserData(), m_containsPreviewImage(false)
|
||||
{
|
||||
}
|
||||
|
||||
VTextBlockData::~VTextBlockData()
|
||||
{
|
||||
}
|
44
src/vtextblockdata.h
Normal file
44
src/vtextblockdata.h
Normal 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
|
Loading…
x
Reference in New Issue
Block a user