refactor image preview logics by adding VImagePreviewer

1. Support previewing non-relative local images;
2. Support previewing network images;
This commit is contained in:
Le Tan 2017-05-04 19:45:30 +08:00
parent 27b0d99965
commit a8614839d9
15 changed files with 665 additions and 340 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

491
src/vimagepreviewer.cpp Normal file
View File

@ -0,0 +1,491 @@
#include "vimagepreviewer.h"
#include <QTimer>
#include <QTextDocument>
#include <QDebug>
#include <QDir>
#include <QUrl>
#include "vmdedit.h"
#include "vconfigmanager.h"
#include "utils/vutils.h"
#include "vfile.h"
#include "vdownloader.h"
extern VConfigManager vconfig;
enum ImageProperty { ImagePath = 1 };
VImagePreviewer::VImagePreviewer(VMdEdit *p_edit, int p_timeToPreview)
: QObject(p_edit), m_edit(p_edit), m_document(p_edit->document()),
m_file(p_edit->getFile()), m_enablePreview(true), m_isPreviewing(false),
m_requestCearBlocks(false), m_requestRefreshBlocks(false)
{
m_timer = new QTimer(this);
m_timer->setSingleShot(true);
Q_ASSERT(p_timeToPreview > 0);
m_timer->setInterval(p_timeToPreview);
connect(m_timer, &QTimer::timeout,
this, &VImagePreviewer::timerTimeout);
m_downloader = new VDownloader(this);
connect(m_downloader, &VDownloader::downloadFinished,
this, &VImagePreviewer::imageDownloaded);
connect(m_edit->document(), &QTextDocument::contentsChange,
this, &VImagePreviewer::handleContentChange);
}
void VImagePreviewer::timerTimeout()
{
if (!vconfig.getEnablePreviewImages()) {
if (m_enablePreview) {
disableImagePreview();
}
return;
}
if (!m_enablePreview) {
return;
}
previewImages();
}
void VImagePreviewer::handleContentChange(int /* p_position */,
int p_charsRemoved,
int p_charsAdded)
{
if (p_charsRemoved == 0 && p_charsAdded == 0) {
return;
}
m_timer->stop();
m_timer->start();
}
void VImagePreviewer::previewImages()
{
if (m_isPreviewing) {
return;
}
m_isPreviewing = true;
QTextBlock block = m_document->begin();
while (block.isValid() && m_enablePreview) {
if (isImagePreviewBlock(block)) {
// Image preview block. Check if it is parentless.
if (!isValidImagePreviewBlock(block)) {
QTextBlock nblock = block.next();
removeBlock(block);
block = nblock;
} else {
block = block.next();
}
} else {
clearCorruptedImagePreviewBlock(block);
block = previewImageOfOneBlock(block);
}
}
m_isPreviewing = false;
if (m_requestCearBlocks) {
m_requestCearBlocks = false;
clearAllImagePreviewBlocks();
}
if (m_requestRefreshBlocks) {
m_requestRefreshBlocks = false;
refresh();
}
emit m_edit->statusChanged();
}
bool VImagePreviewer::isImagePreviewBlock(const QTextBlock &p_block)
{
if (!p_block.isValid()) {
return false;
}
QString text = p_block.text().trimmed();
return text == QString(QChar::ObjectReplacementCharacter);
}
bool VImagePreviewer::isValidImagePreviewBlock(QTextBlock &p_block)
{
if (!isImagePreviewBlock(p_block)) {
return false;
}
// It is a valid image preview block only if the previous block is a block
// need to preview (containing exactly one image) and the image paths are
// identical.
QTextBlock prevBlock = p_block.previous();
if (prevBlock.isValid()) {
QString imagePath = fetchImagePathToPreview(prevBlock.text());
if (imagePath.isEmpty()) {
return false;
}
// Get image preview block's image path.
QString curPath = fetchImagePathFromPreviewBlock(p_block);
return curPath == imagePath;
} else {
return false;
}
}
QString VImagePreviewer::fetchImageUrlToPreview(const QString &p_text)
{
QRegExp regExp("\\!\\[[^\\]]*\\]\\(([^\\)]+)\\)");
int index = regExp.indexIn(p_text);
if (index == -1) {
return QString();
}
int lastIndex = regExp.lastIndexIn(p_text);
if (lastIndex != index) {
return QString();
}
return regExp.capturedTexts()[1];
}
QString VImagePreviewer::fetchImagePathToPreview(const QString &p_text)
{
QString imageUrl = fetchImageUrlToPreview(p_text);
if (imageUrl.isEmpty()) {
return imageUrl;
}
QString imagePath;
QFileInfo info(m_file->retriveBasePath(), imageUrl);
if (info.exists()) {
if (info.isNativePath()) {
// Local file.
imagePath = info.absoluteFilePath();
} else {
imagePath = imageUrl;
}
} else {
QUrl url(imageUrl);
imagePath = url.toString();
}
return imagePath;
}
QTextBlock VImagePreviewer::previewImageOfOneBlock(QTextBlock &p_block)
{
if (!p_block.isValid()) {
return p_block;
}
QTextBlock nblock = p_block.next();
QString imagePath = fetchImagePathToPreview(p_block.text());
if (imagePath.isEmpty()) {
return nblock;
}
qDebug() << "block" << p_block.blockNumber() << imagePath;
if (isImagePreviewBlock(nblock)) {
QTextBlock nextBlock = nblock.next();
updateImagePreviewBlock(nblock, imagePath);
return nextBlock;
} else {
QTextBlock imgBlock = insertImagePreviewBlock(p_block, imagePath);
return imgBlock.next();
}
}
QTextBlock VImagePreviewer::insertImagePreviewBlock(QTextBlock &p_block,
const QString &p_imagePath)
{
QString imageName = imageCacheResourceName(p_imagePath);
if (imageName.isEmpty()) {
return p_block;
}
bool modified = m_edit->isModified();
QTextCursor cursor(p_block);
cursor.beginEditBlock();
cursor.movePosition(QTextCursor::EndOfBlock);
cursor.insertBlock();
QTextImageFormat imgFormat;
imgFormat.setName(imageName);
imgFormat.setProperty(ImagePath, p_imagePath);
cursor.insertImage(imgFormat);
cursor.endEditBlock();
V_ASSERT(cursor.block().text().at(0) == QChar::ObjectReplacementCharacter);
m_edit->setModified(modified);
return cursor.block();
}
void VImagePreviewer::updateImagePreviewBlock(QTextBlock &p_block,
const QString &p_imagePath)
{
QTextImageFormat format = fetchFormatFromPreviewBlock(p_block);
V_ASSERT(format.isValid());
QString curPath = format.property(ImagePath).toString();
if (curPath == p_imagePath) {
return;
}
// Update it with the new image.
QString imageName = imageCacheResourceName(p_imagePath);
if (imageName.isEmpty()) {
// Delete current preview block.
removeBlock(p_block);
return;
}
format.setName(imageName);
format.setProperty(ImagePath, p_imagePath);
updateFormatInPreviewBlock(p_block, format);
}
void VImagePreviewer::removeBlock(QTextBlock &p_block)
{
bool modified = m_edit->isModified();
QTextCursor cursor(p_block);
cursor.select(QTextCursor::BlockUnderCursor);
cursor.removeSelectedText();
m_edit->setModified(modified);
}
void VImagePreviewer::clearCorruptedImagePreviewBlock(QTextBlock &p_block)
{
if (!p_block.isValid()) {
return;
}
QString text = p_block.text();
QVector<int> replacementChars;
bool onlySpaces = true;
for (int i = 0; i < text.size(); ++i) {
if (text[i] == QChar::ObjectReplacementCharacter) {
replacementChars.append(i);
} else if (!text[i].isSpace()) {
onlySpaces = false;
}
}
if (!onlySpaces && !replacementChars.isEmpty()) {
// ObjectReplacementCharacter mixed with other non-space texts.
// Users corrupt the image preview block. Just remove the char.
bool modified = m_edit->isModified();
QTextCursor cursor(p_block);
cursor.beginEditBlock();
int blockPos = p_block.position();
for (int i = replacementChars.size() - 1; i >= 0; --i) {
int pos = replacementChars[i];
cursor.setPosition(blockPos + pos);
cursor.deleteChar();
}
cursor.endEditBlock();
m_edit->setModified(modified);
V_ASSERT(text.remove(QChar::ObjectReplacementCharacter) == p_block.text());
}
}
bool VImagePreviewer::isPreviewEnabled()
{
return m_enablePreview;
}
void VImagePreviewer::enableImagePreview()
{
m_enablePreview = true;
if (vconfig.getEnablePreviewImages()) {
m_timer->stop();
m_timer->start();
}
}
void VImagePreviewer::disableImagePreview()
{
m_enablePreview = false;
if (m_isPreviewing) {
// It is previewing, append the request and clear preview blocks after
// finished previewing.
// It is weird that when selection changed, it will interrupt the process
// of previewing.
m_requestCearBlocks = true;
return;
}
clearAllImagePreviewBlocks();
}
void VImagePreviewer::clearAllImagePreviewBlocks()
{
V_ASSERT(!m_isPreviewing);
QTextBlock block = m_document->begin();
QTextCursor cursor = m_edit->textCursor();
bool modified = m_edit->isModified();
cursor.beginEditBlock();
while (block.isValid()) {
if (isImagePreviewBlock(block)) {
QTextBlock nextBlock = block.next();
removeBlock(block);
block = nextBlock;
} else {
clearCorruptedImagePreviewBlock(block);
block = block.next();
}
}
cursor.endEditBlock();
m_edit->setModified(modified);
emit m_edit->statusChanged();
}
QString VImagePreviewer::fetchImagePathFromPreviewBlock(QTextBlock &p_block)
{
QTextImageFormat format = fetchFormatFromPreviewBlock(p_block);
if (!format.isValid()) {
return QString();
}
return format.property(ImagePath).toString();
}
QTextImageFormat VImagePreviewer::fetchFormatFromPreviewBlock(QTextBlock &p_block)
{
QTextCursor cursor(p_block);
int shift = p_block.text().indexOf(QChar::ObjectReplacementCharacter);
if (shift >= 0) {
cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, shift + 1);
} else {
return QTextImageFormat();
}
return cursor.charFormat().toImageFormat();
}
void VImagePreviewer::updateFormatInPreviewBlock(QTextBlock &p_block,
const QTextImageFormat &p_format)
{
QTextCursor cursor(p_block);
int shift = p_block.text().indexOf(QChar::ObjectReplacementCharacter);
if (shift > 0) {
cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, shift);
}
V_ASSERT(shift >= 0);
cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 1);
V_ASSERT(cursor.charFormat().toImageFormat().isValid());
cursor.setCharFormat(p_format);
}
QString VImagePreviewer::imageCacheResourceName(const QString &p_imagePath)
{
V_ASSERT(!p_imagePath.isEmpty());
auto it = m_imageCache.find(p_imagePath);
if (it != m_imageCache.end()) {
return it.value();
}
// Add it to the resource cache even if it may exist there.
QFileInfo info(p_imagePath);
QImage image;
if (info.exists()) {
// Local file.
image = QImage(p_imagePath);
} else {
// URL. Try to download it.
m_downloader->download(p_imagePath);
}
if (image.isNull()) {
return QString();
}
QString name(imagePathToCacheResourceName(p_imagePath));
m_document->addResource(QTextDocument::ImageResource, name, image);
m_imageCache.insert(p_imagePath, name);
return name;
}
QString VImagePreviewer::imagePathToCacheResourceName(const QString &p_imagePath)
{
return p_imagePath;
}
void VImagePreviewer::imageDownloaded(const QByteArray &p_data, const QString &p_url)
{
QImage image(QImage::fromData(p_data));
if (!image.isNull()) {
auto it = m_imageCache.find(p_url);
if (it != m_imageCache.end()) {
return;
}
m_timer->stop();
QString name(imagePathToCacheResourceName(p_url));
m_document->addResource(QTextDocument::ImageResource, name, image);
m_imageCache.insert(p_url, name);
qDebug() << "downloaded image cache insert" << p_url << name;
m_timer->start();
}
}
void VImagePreviewer::refresh()
{
if (m_isPreviewing) {
m_requestRefreshBlocks = true;
return;
}
m_timer->stop();
m_imageCache.clear();
clearAllImagePreviewBlocks();
m_timer->start();
}
QImage VImagePreviewer::fetchCachedImageFromPreviewBlock(QTextBlock &p_block)
{
QString path = fetchImagePathFromPreviewBlock(p_block);
if (path.isEmpty()) {
return QImage();
}
auto it = m_imageCache.find(path);
if (it == m_imageCache.end()) {
return QImage();
}
return m_document->resource(QTextDocument::ImageResource, it.value()).value<QImage>();
}

95
src/vimagepreviewer.h Normal file
View File

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

View File

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

View File

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

View File

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