vnote/src/widgets/editors/markdowneditor.cpp

1797 lines
57 KiB
C++

#include "markdowneditor.h"
#include <QRegularExpression>
#include <QApplication>
#include <QClipboard>
#include <QMimeData>
#include <QFileInfo>
#include <QDir>
#include <QMimeDatabase>
#include <QClipboard>
#include <QMenu>
#include <QAction>
#include <QShortcut>
#include <QProgressDialog>
#include <QTemporaryFile>
#include <QTimer>
#include <QBuffer>
#include <QPainter>
#include <QHash>
#include <vtextedit/markdowneditorconfig.h>
#include <vtextedit/previewmgr.h>
#include <vtextedit/markdownutils.h>
#include <vtextedit/vtextedit.h>
#include <vtextedit/texteditutils.h>
#include <vtextedit/markdownutils.h>
#include <vtextedit/networkutils.h>
#include <vtextedit/theme.h>
#include <vtextedit/previewdata.h>
#include <vtextedit/textblockdata.h>
#include <widgets/dialogs/linkinsertdialog.h>
#include <widgets/dialogs/imageinsertdialog.h>
#include <widgets/dialogs/tableinsertdialog.h>
#include <widgets/messageboxhelper.h>
#include <widgets/dialogs/selectdialog.h>
#include <buffer/buffer.h>
#include <buffer/markdownbuffer.h>
#include <utils/fileutils.h>
#include <utils/pathutils.h>
#include <utils/htmlutils.h>
#include <utils/widgetutils.h>
#include <utils/webutils.h>
#include <utils/imageutils.h>
#include <utils/clipboardutils.h>
#include <core/exception.h>
#include <core/markdowneditorconfig.h>
#include <core/texteditorconfig.h>
#include <core/configmgr.h>
#include <core/editorconfig.h>
#include <core/vnotex.h>
#include <core/fileopenparameters.h>
#include <imagehost/imagehostutils.h>
#include <imagehost/imagehost.h>
#include <imagehost/imagehostmgr.h>
#include "previewhelper.h"
#include "../outlineprovider.h"
#include "markdowntablehelper.h"
using namespace vnotex;
MarkdownEditor::Heading::Heading(const QString &p_name,
int p_level,
const QString &p_sectionNumber,
int p_blockNumber)
: m_name(p_name),
m_level(p_level),
m_sectionNumber(p_sectionNumber),
m_blockNumber(p_blockNumber)
{
}
MarkdownEditor::MarkdownEditor(const MarkdownEditorConfig &p_config,
const QSharedPointer<vte::MarkdownEditorConfig> &p_editorConfig,
const QSharedPointer<vte::TextEditorParameters> &p_editorParas,
QWidget *p_parent)
: vte::VMarkdownEditor(p_editorConfig, p_editorParas, p_parent),
m_config(p_config)
{
setupShortcuts();
connect(m_textEdit, &vte::VTextEdit::canInsertFromMimeDataRequested,
this, &MarkdownEditor::handleCanInsertFromMimeData);
connect(m_textEdit, &vte::VTextEdit::insertFromMimeDataRequested,
this, &MarkdownEditor::handleInsertFromMimeData);
connect(m_textEdit, &vte::VTextEdit::contextMenuEventRequested,
this, &MarkdownEditor::handleContextMenuEvent);
connect(getHighlighter(), &vte::PegMarkdownHighlighter::headersUpdated,
this, &MarkdownEditor::updateHeadings);
setupTableHelper();
m_headingTimer = new QTimer(this);
m_headingTimer->setInterval(500);
m_headingTimer->setSingleShot(true);
connect(m_headingTimer, &QTimer::timeout,
this, &MarkdownEditor::currentHeadingChanged);
connect(m_textEdit, &vte::VTextEdit::cursorLineChanged,
m_headingTimer, QOverload<>::of(&QTimer::start));
m_sectionNumberTimer = new QTimer(this);
m_sectionNumberTimer->setInterval(1000);
m_sectionNumberTimer->setSingleShot(true);
connect(m_sectionNumberTimer, &QTimer::timeout,
this, [this]() {
updateSectionNumber(m_headings);
});
updateFromConfig(false);
}
MarkdownEditor::~MarkdownEditor()
{
}
void MarkdownEditor::setPreviewHelper(PreviewHelper *p_helper)
{
auto highlighter = getHighlighter();
connect(highlighter, &vte::PegMarkdownHighlighter::codeBlocksUpdated,
p_helper, &PreviewHelper::codeBlocksUpdated);
connect(highlighter, &vte::PegMarkdownHighlighter::mathBlocksUpdated,
p_helper, &PreviewHelper::mathBlocksUpdated);
auto previewMgr = getPreviewMgr();
connect(p_helper, &PreviewHelper::inplacePreviewCodeBlockUpdated,
previewMgr, &vte::PreviewMgr::updateCodeBlocks);
connect(p_helper, &PreviewHelper::inplacePreviewMathBlockUpdated,
previewMgr, &vte::PreviewMgr::updateMathBlocks);
connect(p_helper, &PreviewHelper::potentialObsoletePreviewBlocksUpdated,
previewMgr, &vte::PreviewMgr::checkBlocksForObsoletePreview);
}
void MarkdownEditor::typeHeading(int p_level)
{
enterInsertModeIfApplicable();
vte::MarkdownUtils::typeHeading(m_textEdit, p_level);
}
void MarkdownEditor::typeBold()
{
enterInsertModeIfApplicable();
vte::MarkdownUtils::typeBold(m_textEdit);
}
void MarkdownEditor::typeItalic()
{
enterInsertModeIfApplicable();
vte::MarkdownUtils::typeItalic(m_textEdit);
}
void MarkdownEditor::typeStrikethrough()
{
enterInsertModeIfApplicable();
vte::MarkdownUtils::typeStrikethrough(m_textEdit);
}
void MarkdownEditor::typeMark()
{
enterInsertModeIfApplicable();
vte::MarkdownUtils::typeMark(m_textEdit);
}
void MarkdownEditor::typeUnorderedList()
{
enterInsertModeIfApplicable();
vte::MarkdownUtils::typeUnorderedList(m_textEdit);
}
void MarkdownEditor::typeOrderedList()
{
enterInsertModeIfApplicable();
vte::MarkdownUtils::typeOrderedList(m_textEdit);
}
void MarkdownEditor::typeTodoList(bool p_checked)
{
enterInsertModeIfApplicable();
vte::MarkdownUtils::typeTodoList(m_textEdit, p_checked);
}
void MarkdownEditor::typeCode()
{
enterInsertModeIfApplicable();
vte::MarkdownUtils::typeCode(m_textEdit);
}
void MarkdownEditor::typeCodeBlock()
{
enterInsertModeIfApplicable();
vte::MarkdownUtils::typeCodeBlock(m_textEdit);
}
void MarkdownEditor::typeMath()
{
enterInsertModeIfApplicable();
vte::MarkdownUtils::typeMath(m_textEdit);
}
void MarkdownEditor::typeMathBlock()
{
enterInsertModeIfApplicable();
vte::MarkdownUtils::typeMathBlock(m_textEdit);
}
void MarkdownEditor::typeQuote()
{
enterInsertModeIfApplicable();
vte::MarkdownUtils::typeQuote(m_textEdit);
}
void MarkdownEditor::typeLink()
{
QString linkText;
QString linkUrl;
// Try get Url or text from selection.
auto cursor = m_textEdit->textCursor();
QRegularExpression urlReg("[\\.\\\\/]");
if (cursor.hasSelection()) {
auto text = vte::TextEditUtils::getSelectedText(cursor).trimmed();
if (!text.isEmpty() && !text.contains(QLatin1Char('\n'))) {
if (text.contains(urlReg) && QUrl::fromUserInput(text).isValid()) {
linkUrl = text;
} else {
linkText = text;
}
}
}
// Fetch link from clipboard.
if (linkUrl.isEmpty() && linkText.isEmpty()) {
const auto clipboard = QApplication::clipboard();
const auto mimeData = clipboard->mimeData();
const QString text = mimeData->text().trimmed();
// No multi-line.
if (!text.isEmpty() && !text.contains(QLatin1Char('\n'))) {
if (text.contains(urlReg) && QUrl::fromUserInput(text).isValid()) {
linkUrl = text;
} else {
linkText = text;
}
}
}
LinkInsertDialog dialog(tr("Insert Link"), linkText, linkUrl, false, this);
if (dialog.exec() == QDialog::Accepted) {
linkText = dialog.getLinkText();
linkUrl = dialog.getLinkUrl();
enterInsertModeIfApplicable();
vte::MarkdownUtils::typeLink(m_textEdit, linkText, linkUrl);
}
}
void MarkdownEditor::typeImage()
{
Q_ASSERT(m_buffer);
ImageInsertDialog dialog(tr("Insert Image"), "", "", "", true, this);
// Try fetch image from clipboard.
{
QClipboard *clipboard = QApplication::clipboard();
const QMimeData *mimeData = clipboard->mimeData();
QUrl url;
if (mimeData->hasImage()) {
QImage im = qvariant_cast<QImage>(mimeData->imageData());
if (im.isNull()) {
return;
}
dialog.setImage(im);
dialog.setImageSource(ImageInsertDialog::Source::ImageData);
} else if (mimeData->hasUrls()) {
QList<QUrl> urls = mimeData->urls();
if (urls.size() == 1) {
url = urls[0];
}
} else if (mimeData->hasText()) {
url = QUrl::fromUserInput(mimeData->text());
}
if (url.isValid()) {
if (url.isLocalFile()) {
dialog.setImagePath(url.toLocalFile());
} else {
dialog.setImagePath(url.toString());
}
}
}
if (dialog.exec() != QDialog::Accepted) {
return;
}
enterInsertModeIfApplicable();
if (dialog.getImageSource() == ImageInsertDialog::Source::LocalFile) {
insertImageToBufferFromLocalFile(dialog.getImageTitle(),
dialog.getImageAltText(),
dialog.getImagePath(),
dialog.getScaledWidth());
} else {
auto image = dialog.getImage();
if (!image.isNull()) {
insertImageToBufferFromData(dialog.getImageTitle(),
dialog.getImageAltText(),
image,
dialog.getScaledWidth());
}
}
}
void MarkdownEditor::typeTable()
{
TableInsertDialog dialog(tr("Insert Table"), this);
if (dialog.exec() != QDialog::Accepted) {
return;
}
auto cursor = m_textEdit->textCursor();
cursor.beginEditBlock();
if (cursor.hasSelection()) {
cursor.setPosition(qMax(cursor.selectionStart(), cursor.selectionEnd()));
}
bool newBlock = !cursor.atBlockEnd();
if (!newBlock && !cursor.atBlockStart()) {
QString text = cursor.block().text().trimmed();
if (!text.isEmpty() && text != QStringLiteral(">")) {
// Insert a new block before inserting table.
newBlock = true;
}
}
if (newBlock) {
auto indentationStr = vte::TextEditUtils::fetchIndentationSpaces(cursor.block());
vte::TextEditUtils::insertBlock(cursor, false);
cursor.insertText(indentationStr);
}
cursor.endEditBlock();
m_textEdit->setTextCursor(cursor);
// Insert table.
m_tableHelper->insertTable(dialog.getRowCount(), dialog.getColumnCount(), dialog.getAlignment());
}
void MarkdownEditor::setBuffer(Buffer *p_buffer)
{
m_buffer = p_buffer;
}
bool MarkdownEditor::insertImageToBufferFromLocalFile(const QString &p_title,
const QString &p_altText,
const QString &p_srcImagePath,
int p_scaledWidth,
int p_scaledHeight,
bool p_insertText,
QString *p_urlInLink)
{
auto destFileName = generateImageFileNameToInsertAs(p_title, QFileInfo(p_srcImagePath).suffix());
QString destFilePath;
if (m_imageHost) {
// Save to image host.
QByteArray ba;
try {
ba = FileUtils::readFile(p_srcImagePath);
} catch (Exception &e) {
MessageBoxHelper::notify(MessageBoxHelper::Warning,
tr("Failed to read local image file (%1) (%2).").arg(p_srcImagePath, e.what()),
this);
return false;
}
destFilePath = saveToImageHost(ba, destFileName);
if (destFilePath.isEmpty()) {
return false;
}
} else {
try {
destFilePath = m_buffer->insertImage(p_srcImagePath, destFileName);
} catch (Exception &e) {
MessageBoxHelper::notify(MessageBoxHelper::Warning,
tr("Failed to insert image from local file (%1) (%2).").arg(p_srcImagePath, e.what()),
this);
return false;
}
}
insertImageLink(p_title, p_altText, destFilePath, p_scaledWidth, p_scaledHeight, p_insertText, p_urlInLink);
return true;
}
QString MarkdownEditor::generateImageFileNameToInsertAs(const QString &p_title, const QString &p_suffix)
{
return FileUtils::generateRandomFileName(p_title, p_suffix);
}
bool MarkdownEditor::insertImageToBufferFromData(const QString &p_title,
const QString &p_altText,
const QImage &p_image,
int p_scaledWidth,
int p_scaledHeight)
{
// Save as PNG by default.
const QString format("png");
const auto destFileName = generateImageFileNameToInsertAs(p_title, format);
QString destFilePath;
if (m_imageHost) {
// Save to image host.
QByteArray ba;
QBuffer buffer(&ba);
buffer.open(QIODevice::WriteOnly);
p_image.save(&buffer, format.toStdString().c_str());
destFilePath = saveToImageHost(ba, destFileName);
if (destFilePath.isEmpty()) {
return false;
}
} else {
try {
destFilePath = m_buffer->insertImage(p_image, destFileName);
} catch (Exception &e) {
MessageBoxHelper::notify(MessageBoxHelper::Warning,
tr("Failed to insert image from data (%1).").arg(e.what()),
this);
return false;
}
}
insertImageLink(p_title, p_altText, destFilePath, p_scaledWidth, p_scaledHeight);
return true;
}
void MarkdownEditor::insertImageLink(const QString &p_title,
const QString &p_altText,
const QString &p_destImagePath,
int p_scaledWidth,
int p_scaledHeight,
bool p_insertText,
QString *p_urlInLink)
{
const auto urlInLink = getRelativeLink(p_destImagePath);
if (p_urlInLink) {
*p_urlInLink = urlInLink;
}
static_cast<MarkdownBuffer *>(m_buffer)->addInsertedImage(p_destImagePath, urlInLink);
if (p_insertText) {
const auto imageLink = vte::MarkdownUtils::generateImageLink(p_title,
urlInLink,
p_altText,
p_scaledWidth,
p_scaledHeight);
m_textEdit->insertPlainText(imageLink);
}
}
void MarkdownEditor::handleCanInsertFromMimeData(const QMimeData *p_source, bool *p_handled, bool *p_allowed)
{
m_shouldTriggerRichPaste = ConfigMgr::getInst().getEditorConfig().getMarkdownEditorConfig().getRichPasteByDefaultEnabled();
if (m_plainTextPasteAsked) {
m_shouldTriggerRichPaste = false;
return;
}
if (m_richPasteAsked) {
m_shouldTriggerRichPaste = true;
*p_handled = true;
*p_allowed = true;
return;
}
if (QGuiApplication::keyboardModifiers() == Qt::ShiftModifier) {
m_shouldTriggerRichPaste = !m_shouldTriggerRichPaste;
}
if (m_shouldTriggerRichPaste) {
*p_handled = true;
*p_allowed = true;
return;
}
if (p_source->hasImage()) {
m_shouldTriggerRichPaste = true;
*p_handled = true;
*p_allowed = true;
return;
}
if (p_source->hasUrls()) {
*p_handled = true;
*p_allowed = true;
return;
}
}
void MarkdownEditor::handleInsertFromMimeData(const QMimeData *p_source, bool *p_handled)
{
if (!m_shouldTriggerRichPaste) {
// Default paste.
// Give tips about the Rich Paste and Parse to Markdown And Paste features.
VNoteX::getInst().showStatusMessageShort(
tr("For advanced paste, try the \"Rich Paste\" and \"Parse to Markdown and Paste\" on the editor's context menu"));
return;
}
m_shouldTriggerRichPaste = false;
if (processHtmlFromMimeData(p_source)) {
*p_handled = true;
return;
}
if (processImageFromMimeData(p_source)) {
*p_handled = true;
return;
}
if (processUrlFromMimeData(p_source)) {
*p_handled = true;
return;
}
if (processMultipleUrlsFromMimeData(p_source)) {
*p_handled = true;
return;
}
}
bool MarkdownEditor::processHtmlFromMimeData(const QMimeData *p_source)
{
if (!p_source->hasHtml()) {
return false;
}
const QString html(p_source->html());
// Process <img>.
QRegularExpression reg("<img ([^>]*)src=\"([^\"]+)\"([^>]*)>");
QRegularExpressionMatch match;
if (html.indexOf(reg, 0, &match) != -1 && HtmlUtils::hasOnlyImgTag(html)) {
if (p_source->hasImage()) {
// Both image data and URL are embedded.
SelectDialog dialog(tr("Insert From Clipboard"), this);
dialog.addSelection(tr("Insert From URL"), 0);
dialog.addSelection(tr("Insert From Image Data"), 1);
dialog.addSelection(tr("Insert As Image Link"), 2);
if (dialog.exec() == QDialog::Accepted) {
int selection = dialog.getSelection();
if (selection == 1) {
// Insert from image data.
insertImageFromMimeData(p_source);
return true;
} else if (selection == 2) {
// Insert as link.
auto imageLink = vte::MarkdownUtils::generateImageLink("", match.captured(2), "");
m_textEdit->insertPlainText(imageLink);
return true;
}
} else {
return true;
}
}
insertImageFromUrl(match.captured(2));
return true;
}
return false;
}
bool MarkdownEditor::processImageFromMimeData(const QMimeData *p_source)
{
if (!p_source->hasImage()) {
return false;
}
// Image url in the clipboard.
if (p_source->hasText()) {
SelectDialog dialog(tr("Insert From Clipboard"), this);
dialog.addSelection(tr("Insert As Image"), 0);
dialog.addSelection(tr("Insert As Text"), 1);
dialog.addSelection(tr("Insert As Image Link"), 2);
if (dialog.exec() == QDialog::Accepted) {
int selection = dialog.getSelection();
if (selection == 1) {
// Insert as text.
Q_ASSERT(p_source->hasText() && p_source->hasImage());
m_textEdit->insertFromMimeDataOfBase(p_source);
return true;
} else if (selection == 2) {
// Insert as link.
auto imageLink = vte::MarkdownUtils::generateImageLink("", p_source->text(), "");
m_textEdit->insertPlainText(imageLink);
return true;
}
} else {
return true;
}
}
insertImageFromMimeData(p_source);
return true;
}
bool MarkdownEditor::processUrlFromMimeData(const QMimeData *p_source)
{
const auto urls = p_source->urls();
if (urls.size() > 1) {
return false;
}
QUrl url;
if (p_source->hasUrls()) {
if (urls.size() == 1) {
url = urls[0];
}
} else if (p_source->hasText()) {
// Try to get URL from text.
const QString text = p_source->text();
if (QFileInfo::exists(text)) {
url = QUrl::fromLocalFile(text);
} else {
url.setUrl(text);
if (url.scheme() != QStringLiteral("https") && url.scheme() != QStringLiteral("http")) {
url.clear();
}
}
}
if (!url.isValid()) {
return false;
}
const bool isImage = PathUtils::isImageUrl(PathUtils::urlToPath(url));
QString localFile = url.toLocalFile();
if (!url.isLocalFile() || !QFileInfo::exists(localFile)) {
localFile.clear();
}
bool isTextFile = false;
if (!isImage && !localFile.isEmpty()) {
const auto mimeType = QMimeDatabase().mimeTypeForFile(localFile);
if (mimeType.isValid() && mimeType.inherits(QStringLiteral("text/plain"))) {
isTextFile = true;
}
}
SelectDialog dialog(tr("Insert From Clipboard"), this);
if (isImage) {
dialog.addSelection(tr("Insert As Image"), 0);
dialog.addSelection(tr("Insert As Image Link"), 1);
if (!localFile.isEmpty()) {
dialog.addSelection(tr("Insert As Relative Image Link"), 7);
}
}
dialog.addSelection(tr("Insert As Link"), 2);
if (!localFile.isEmpty()) {
dialog.addSelection(tr("Insert As Relative Link"), 3);
if (m_buffer->isAttachmentSupported() && !m_buffer->isAttachment(localFile) && !PathUtils::isDir(localFile)) {
dialog.addSelection(tr("Attach And Insert Link"), 6);
}
}
dialog.addSelection(tr("Insert As Text"), 4);
if (!localFile.isEmpty() && isTextFile) {
dialog.addSelection(tr("Insert File Content"), 5);
}
// FIXME: After calling dialog.exec(), p_source->hasUrl() returns false.
if (dialog.exec() == QDialog::Accepted) {
bool relativeLink = false;
switch (dialog.getSelection()) {
case 0:
{
// Insert As Image.
insertImageFromUrl(PathUtils::urlToPath(url));
return true;
}
case 7:
// Insert As Relative Image Link.
relativeLink = true;
Q_FALLTHROUGH();
case 1:
{
// Insert As Image Link.
QString urlInLink;
if (relativeLink) {
urlInLink = getRelativeLink(localFile);
} else {
urlInLink = url.toString(QUrl::EncodeSpaces);
}
enterInsertModeIfApplicable();
const auto imageLink = vte::MarkdownUtils::generateImageLink("", urlInLink, "");
m_textEdit->insertPlainText(imageLink);
return true;
}
case 6:
{
// Attach And Insert Link.
QStringList fileList;
fileList << localFile;
fileList = m_buffer->addAttachment(QString(), fileList);
// Update localFile to point to the attachment file.
localFile = fileList[0];
Q_FALLTHROUGH();
}
case 3:
// Insert As Relative link.
relativeLink = true;
Q_FALLTHROUGH();
case 2:
{
// Insert As Link.
QString linkText;
if (!localFile.isEmpty()) {
linkText = QFileInfo(localFile).fileName();
}
QString linkUrl;
if (relativeLink) {
Q_ASSERT(!localFile.isEmpty());
linkUrl = getRelativeLink(localFile);
} else {
linkUrl = url.toString(QUrl::EncodeSpaces);
}
LinkInsertDialog linkDialog(tr("Insert Link"), linkText, linkUrl, false, this);
if (linkDialog.exec() == QDialog::Accepted) {
linkText = linkDialog.getLinkText();
linkUrl = linkDialog.getLinkUrl();
enterInsertModeIfApplicable();
vte::MarkdownUtils::typeLink(m_textEdit, linkText, linkUrl);
}
return true;
}
case 4:
{
// Insert As Text.
enterInsertModeIfApplicable();
if (p_source->hasText()) {
m_textEdit->insertPlainText(p_source->text());
} else {
m_textEdit->insertPlainText(url.toString());
}
return true;
}
case 5:
{
// Insert File Content.
Q_ASSERT(!localFile.isEmpty() && isTextFile);
enterInsertModeIfApplicable();
m_textEdit->insertPlainText(FileUtils::readTextFile(localFile));
return true;
}
default:
Q_ASSERT(false);
break;
}
} else {
// Nothing happens.
return true;
}
return false;
}
bool MarkdownEditor::processMultipleUrlsFromMimeData(const QMimeData *p_source) {
const auto urls = p_source->urls();
if (urls.size() <= 1) {
return false;
}
bool isProcessed = false;
// Judgment if all QMimeData are images.
bool isAllImage = true;
for (const QUrl &url : urls) {
if (!PathUtils::isImageUrl(PathUtils::urlToPath(url))) {
isAllImage = false;
break;
}
}
SelectDialog dialog(tr("Insert From Clipboard (%n items)", "", urls.size()), this);
if (isAllImage) {
dialog.addSelection(tr("Insert As Image"), 0);
}
if (m_buffer->isAttachmentSupported()) {
dialog.addSelection(tr("Attach And Insert Link"), 1);
}
dialog.setMinimumWidth(400);
if (dialog.exec() == QDialog::Accepted) {
switch (dialog.getSelection()) {
case 0:
{
// Insert As Image.
for (const QUrl &url : urls) {
insertImageFromUrl(PathUtils::urlToPath(url), true);
m_textEdit->insertPlainText("\n\n");
}
isProcessed = true;
break;
}
case 1:
{
// Attach And Insert Link.
QStringList fileList;
for (const QUrl &url : urls) {
fileList << url.toLocalFile();
}
fileList = m_buffer->addAttachment(QString(), fileList);
enterInsertModeIfApplicable();
for (int i = 0; i < fileList.length(); ++i) {
vte::MarkdownUtils::typeLink(
m_textEdit, QFileInfo(fileList[i]).fileName(),
getRelativeLink(fileList[i]));
m_textEdit->insertPlainText("\n\n");
}
isProcessed = true;
break;
}
}
}
return isProcessed;
}
void MarkdownEditor::insertImageFromMimeData(const QMimeData *p_source)
{
QImage image = qvariant_cast<QImage>(p_source->imageData());
if (image.isNull()) {
return;
}
ImageInsertDialog dialog(tr("Insert Image From Clipboard"), "", "", "", false, this);
dialog.setImage(image);
if (dialog.exec() == QDialog::Accepted) {
enterInsertModeIfApplicable();
insertImageToBufferFromData(dialog.getImageTitle(),
dialog.getImageAltText(),
image,
dialog.getScaledWidth());
}
}
void MarkdownEditor::insertImageFromUrl(const QString &p_url, bool p_quiet)
{
if (p_quiet) {
insertImageToBufferFromLocalFile("", "", p_url, 0);
} else {
ImageInsertDialog dialog(tr("Insert Image From URL"), "", "", "", false, this);
dialog.setImagePath(p_url);
if (dialog.exec() == QDialog::Accepted) {
enterInsertModeIfApplicable();
if (dialog.getImageSource() == ImageInsertDialog::Source::LocalFile) {
insertImageToBufferFromLocalFile(dialog.getImageTitle(),
dialog.getImageAltText(),
dialog.getImagePath(),
dialog.getScaledWidth());
} else {
auto image = dialog.getImage();
if (!image.isNull()) {
insertImageToBufferFromData(dialog.getImageTitle(),
dialog.getImageAltText(),
image,
dialog.getScaledWidth());
}
}
}
}
}
QString MarkdownEditor::getRelativeLink(const QString &p_path)
{
if (PathUtils::isLocalFile(p_path)) {
auto relativePath = PathUtils::relativePath(PathUtils::parentDirPath(m_buffer->getContentPath()), p_path);
auto link = PathUtils::encodeSpacesInPath(QDir::fromNativeSeparators(relativePath));
if (m_config.getPrependDotInRelativeLink()) {
PathUtils::prependDotIfRelative(link);
}
return link;
} else {
return p_path;
}
}
const QVector<MarkdownEditor::Heading> &MarkdownEditor::getHeadings() const
{
return m_headings;
}
int MarkdownEditor::getCurrentHeadingIndex() const
{
int blockNumber = m_textEdit->textCursor().blockNumber();
return getHeadingIndexByBlockNumber(blockNumber);
}
void MarkdownEditor::updateHeadings(const QVector<vte::peg::ElementRegion> &p_headerRegions)
{
bool needUpdateSectionNumber = false;
if (isReadOnly()) {
m_sectionNumberEnabled = false;
} else {
needUpdateSectionNumber = m_config.getSectionNumberMode() == MarkdownEditorConfig::SectionNumberMode::Edit;
if (m_overriddenSectionNumber != OverrideState::NoOverride) {
needUpdateSectionNumber = m_overriddenSectionNumber == OverrideState::ForceEnable;
}
if (needUpdateSectionNumber) {
m_sectionNumberEnabled = true;
} else if (m_sectionNumberEnabled) {
// On -> Off. We still need to do the clean up.
needUpdateSectionNumber = true;
m_sectionNumberEnabled = false;
}
}
QVector<Heading> headings;
headings.reserve(p_headerRegions.size());
// Assume that each block contains only one line.
// Only support # syntax for now.
auto doc = document();
for (auto const &reg : p_headerRegions) {
auto block = doc->findBlock(reg.m_startPos);
if (!block.isValid()) {
continue;
}
if (!block.contains(reg.m_endPos - 1)) {
qWarning() << "header accross multiple blocks, starting from block" << block.blockNumber() << block.text();
}
auto match = vte::MarkdownUtils::matchHeader(block.text());
if (match.m_matched) {
Heading heading(match.m_header,
match.m_level,
match.m_sequence,
block.blockNumber());
headings.append(heading);
}
}
OutlineProvider::makePerfectHeadings(headings, m_headings);
if (needUpdateSectionNumber) {
// Use a timer to kick off the update to let user have time to undo.
m_sectionNumberTimer->start();
}
emit headingsChanged();
emit currentHeadingChanged();
}
int MarkdownEditor::getHeadingIndexByBlockNumber(int p_blockNumber) const
{
if (m_headings.isEmpty()) {
return -1;
}
int left = 0, right = m_headings.size() - 1;
while (left < right) {
int mid = left + (right - left + 1) / 2;
int val = m_headings[mid].m_blockNumber;
if (val == -1) {
// Search to right.
for (int i = mid + 1; i <= right; ++i) {
if (m_headings[i].m_blockNumber != -1) {
mid = i;
val = m_headings[i].m_blockNumber;
break;
}
}
if (val == -1) {
// Search to left.
for (int i = mid - 1; i >= left; --i) {
if (m_headings[i].m_blockNumber != -1) {
mid = i;
val = m_headings[i].m_blockNumber;
break;
}
}
}
}
if (val == -1) {
// No more valid values.
break;
}
if (val == p_blockNumber) {
return mid;
} else if (val > p_blockNumber) {
// Skip the -1 headings.
// Bad case: [0, 2, 3, 43, 44, -1, 46, 60].
// If not skipped, [left, right] will be stuck at [4, 5].
right = mid - 1;
while (right >= left && m_headings[right].m_blockNumber == -1) {
--right;
}
} else {
left = mid;
}
}
if (m_headings[left].m_blockNumber <= p_blockNumber && m_headings[left].m_blockNumber != -1) {
return left;
}
return -1;
}
void MarkdownEditor::scrollToHeading(int p_idx)
{
if (p_idx < 0 || p_idx >= m_headings.size()) {
return;
}
if (m_headings[p_idx].m_blockNumber == -1) {
return;
}
scrollToLine(m_headings[p_idx].m_blockNumber, true);
}
void MarkdownEditor::handleContextMenuEvent(QContextMenuEvent *p_event, bool *p_handled, QScopedPointer<QMenu> *p_menu)
{
const auto &editorConfig = ConfigMgr::getInst().getEditorConfig();
*p_handled = true;
p_menu->reset(m_textEdit->createStandardContextMenu(p_event->pos()));
auto menu = p_menu->data();
const auto actions = menu->actions();
QAction *firstAct = actions.isEmpty() ? nullptr : actions.first();
// QAction *copyAct = WidgetUtils::findActionByObjectName(actions, "edit-copy");
QAction *pasteAct = WidgetUtils::findActionByObjectName(actions, "edit-paste");
const bool hasSelection = m_textEdit->hasSelection();
if (!hasSelection) {
auto readAct = new QAction(tr("&Read"), menu);
WidgetUtils::addActionShortcutText(readAct, editorConfig.getShortcut(EditorConfig::Shortcut::EditRead));
connect(readAct, &QAction::triggered,
this, &MarkdownEditor::readRequested);
menu->insertAction(firstAct, readAct);
if (firstAct) {
menu->insertSeparator(firstAct);
}
prependContextSensitiveMenu(menu, p_event->pos());
}
if (pasteAct && pasteAct->isEnabled()) {
QClipboard *clipboard = QApplication::clipboard();
const QMimeData *mimeData = clipboard->mimeData();
// Rich Paste or Plain Text Paste.
const bool richPasteByDefault = editorConfig.getMarkdownEditorConfig().getRichPasteByDefaultEnabled();
auto altPasteAct = new QAction(richPasteByDefault ? tr("Paste as Plain Text") : tr("Rich Paste"), menu);
WidgetUtils::addActionShortcutText(altPasteAct,
editorConfig.getShortcut(EditorConfig::Shortcut::AltPaste));
connect(altPasteAct, &QAction::triggered,
this, &MarkdownEditor::altPaste);
WidgetUtils::insertActionAfter(menu, pasteAct, altPasteAct);
if (mimeData->hasHtml()) {
// Parse to Markdown and Paste.
auto parsePasteAct = new QAction(tr("Parse to Markdown and Paste"), menu);
WidgetUtils::addActionShortcutText(parsePasteAct,
editorConfig.getShortcut(EditorConfig::Shortcut::ParseToMarkdownAndPaste));
connect(parsePasteAct, &QAction::triggered,
this, &MarkdownEditor::parseToMarkdownAndPaste);
WidgetUtils::insertActionAfter(menu, altPasteAct, parsePasteAct);
}
}
{
menu->addSeparator();
auto snippetAct = menu->addAction(tr("Insert Snippet"), this, &MarkdownEditor::applySnippetRequested);
WidgetUtils::addActionShortcutText(snippetAct,
editorConfig.getShortcut(EditorConfig::Shortcut::ApplySnippet));
}
if (!hasSelection) {
appendImageHostMenu(menu);
}
appendSpellCheckMenu(p_event, menu);
}
void MarkdownEditor::altPaste()
{
const bool richPasteByDefault = ConfigMgr::getInst().getEditorConfig().getMarkdownEditorConfig().getRichPasteByDefaultEnabled();
if (richPasteByDefault) {
// Paste as plain text.
m_plainTextPasteAsked = true;
m_richPasteAsked = false;
} else {
// Rich paste.
m_plainTextPasteAsked = false;
m_richPasteAsked = true;
}
// handleCanInsertFromMimeData() is called before this function. Call it manually.
if (m_textEdit->canPaste()) {
m_textEdit->paste();
}
m_plainTextPasteAsked = false;
m_richPasteAsked = false;
}
void MarkdownEditor::setupShortcuts()
{
const auto &editorConfig = ConfigMgr::getInst().getEditorConfig();
// Alt paste.
{
auto shortcut = WidgetUtils::createShortcut(editorConfig.getShortcut(EditorConfig::Shortcut::AltPaste),
this);
if (shortcut) {
connect(shortcut, &QShortcut::activated,
this, &MarkdownEditor::altPaste);
}
}
// Parse to Markdown and Paste.
{
auto shortcut = WidgetUtils::createShortcut(editorConfig.getShortcut(EditorConfig::Shortcut::ParseToMarkdownAndPaste),
this);
if (shortcut) {
connect(shortcut, &QShortcut::activated,
this, &MarkdownEditor::parseToMarkdownAndPaste);
}
}
}
void MarkdownEditor::parseToMarkdownAndPaste()
{
if (isReadOnly()) {
return;
}
QClipboard *clipboard = QApplication::clipboard();
const QMimeData *mimeData = clipboard->mimeData();
QString html(mimeData->html());
if (!html.isEmpty()) {
emit htmlToMarkdownRequested(0, ++m_timeStamp, html);
}
}
void MarkdownEditor::handleHtmlToMarkdownData(quint64 p_id, TimeStamp p_timeStamp, const QString &p_text)
{
Q_UNUSED(p_id);
if (m_timeStamp == p_timeStamp && !p_text.isEmpty()) {
QString text(p_text);
const auto &editorConfig = ConfigMgr::getInst().getEditorConfig().getMarkdownEditorConfig();
if (editorConfig.getFetchImagesInParseAndPaste()) {
fetchImagesToLocalAndReplace(text);
}
insertText(text);
}
}
static QString purifyImageTitle(QString p_title)
{
return p_title.remove(QRegularExpression("[\\r\\n\\[\\]]"));
}
void MarkdownEditor::fetchImagesToLocalAndReplace(QString &p_text)
{
auto regs = vte::MarkdownUtils::fetchImageRegionsViaParser(p_text);
if (regs.isEmpty()) {
return;
}
// Sort it in ascending order.
std::sort(regs.begin(), regs.end());
QProgressDialog proDlg(tr("Fetching images to local..."),
tr("Abort"),
0,
regs.size(),
this);
proDlg.setWindowModality(Qt::WindowModal);
proDlg.setWindowTitle(tr("Fetch Images To Local"));
QRegularExpression zhihuRegExp("^https?://www\\.zhihu\\.com/equation\\?tex=(.+)$");
QRegularExpression regExp(vte::MarkdownUtils::c_imageLinkRegExp);
for (int i = regs.size() - 1; i >= 0; --i) {
proDlg.setValue(regs.size() - 1 - i);
if (proDlg.wasCanceled()) {
break;
}
const auto &reg = regs[i];
QString linkText = p_text.mid(reg.m_startPos, reg.m_endPos - reg.m_startPos);
QRegularExpressionMatch match;
if (linkText.indexOf(regExp, 0, &match) == -1) {
continue;
}
qDebug() << "fetching image link" << linkText;
const QString imageTitle = purifyImageTitle(match.captured(1).trimmed());
QString imageUrl = match.captured(2).trimmed();
const int maxUrlLength = 100;
QString urlToDisplay(imageUrl);
if (urlToDisplay.size() > maxUrlLength) {
urlToDisplay = urlToDisplay.left(maxUrlLength) + "...";
}
proDlg.setLabelText(tr("Fetching image (%1)").arg(urlToDisplay));
// Handle equation from zhihu.com like http://www.zhihu.com/equation?tex=P.
QRegularExpressionMatch zhihuMatch;
if (imageUrl.indexOf(zhihuRegExp, 0, &zhihuMatch) != -1) {
QString tex = zhihuMatch.captured(1).trimmed();
// Remove the +.
tex.replace(QChar('+'), " ");
tex = QUrl::fromPercentEncoding(tex.toUtf8());
if (tex.isEmpty()) {
continue;
}
tex = "$" + tex + "$";
p_text.replace(reg.m_startPos,
reg.m_endPos - reg.m_startPos,
tex);
continue;
}
// Only handle absolute file path or network path.
QString srcImagePath;
QFileInfo info(WebUtils::purifyUrl(imageUrl));
// For network image.
QScopedPointer<QTemporaryFile> tmpFile;
if (info.exists()) {
if (info.isAbsolute()) {
// Absolute local path.
srcImagePath = info.absoluteFilePath();
}
} else {
// Network path.
// Prepend the protocol if missing.
if (imageUrl.startsWith(QStringLiteral("//"))) {
imageUrl.prepend(QStringLiteral("https:"));
}
QByteArray data = vte::NetworkAccess::request(QUrl(imageUrl)).m_data;
if (!data.isEmpty()) {
// Prefer the suffix from the real data.
auto suffix = ImageUtils::guessImageSuffix(data);
if (suffix.isEmpty()) {
suffix = info.suffix();
} else if (info.suffix() != suffix) {
qWarning() << "guess a different suffix from image data" << info.suffix() << suffix;
}
tmpFile.reset(FileUtils::createTemporaryFile(suffix));
if (tmpFile->open() && tmpFile->write(data) > -1) {
srcImagePath = tmpFile->fileName();
}
// Need to close it explicitly to flush cache of small file.
tmpFile->close();
}
}
if (srcImagePath.isEmpty()) {
continue;
}
// Insert image without inserting text.
QString urlInLink;
bool ret = insertImageToBufferFromLocalFile(imageTitle,
QString(),
srcImagePath,
0,
0,
false,
&urlInLink);
if (!ret || urlInLink.isEmpty()) {
continue;
}
// Replace URL in link.
QString newLink = QStringLiteral("![%1](%2%3%4)")
.arg(imageTitle, urlInLink, match.captured(3), match.captured(6));
p_text.replace(reg.m_startPos,
reg.m_endPos - reg.m_startPos,
newLink);
}
proDlg.setValue(regs.size());
}
static bool updateHeadingSectionNumber(QTextCursor &p_cursor,
const QTextBlock &p_block,
const QString &p_sectionNumber,
bool p_endingDot)
{
if (!p_block.isValid()) {
return false;
}
QString text = p_block.text();
auto match = vte::MarkdownUtils::matchHeader(text);
Q_ASSERT(match.m_matched);
bool isSequence = false;
if (!match.m_sequence.isEmpty()) {
// Check if this sequence is the real sequence matching current style.
if (match.m_sequence.endsWith('.')) {
isSequence = p_endingDot;
} else {
isSequence = !p_endingDot;
}
}
int start = match.m_level + 1;
int end = match.m_level + match.m_spacesAfterMarker;
if (isSequence) {
end += match.m_sequence.size() + match.m_spacesAfterSequence;
}
Q_ASSERT(start <= end);
p_cursor.setPosition(p_block.position() + start);
if (start != end) {
p_cursor.setPosition(p_block.position() + end, QTextCursor::KeepAnchor);
}
if (p_sectionNumber.isEmpty()) {
p_cursor.removeSelectedText();
} else {
p_cursor.insertText(p_sectionNumber + ' ');
}
return true;
}
bool MarkdownEditor::updateSectionNumber(const QVector<Heading> &p_headings)
{
SectionNumber sectionNumber(7, 0);
int baseLevel = m_config.getSectionNumberBaseLevel();
if (baseLevel < 1 || baseLevel > 6) {
baseLevel = 1;
}
bool changed = false;
bool endingDot = m_config.getSectionNumberStyle() == MarkdownEditorConfig::SectionNumberStyle::DigDotDigDot;
auto doc = document();
QTextCursor cursor(doc);
cursor.beginEditBlock();
for (const auto &heading : p_headings) {
OutlineProvider::increaseSectionNumber(sectionNumber, heading.m_level, baseLevel);
auto sectionStr = m_sectionNumberEnabled ? OutlineProvider::joinSectionNumber(sectionNumber, endingDot) : QString();
if (heading.m_blockNumber > -1 && sectionStr != heading.m_sectionNumber) {
if (updateHeadingSectionNumber(cursor,
doc->findBlockByNumber(heading.m_blockNumber),
sectionStr,
endingDot)) {
changed = true;
}
}
}
cursor.endEditBlock();
return changed;
}
void MarkdownEditor::overrideSectionNumber(OverrideState p_state)
{
if (m_overriddenSectionNumber == p_state) {
return;
}
m_overriddenSectionNumber = p_state;
getHighlighter()->updateHighlight();
}
void MarkdownEditor::updateFromConfig(bool p_initialized)
{
if (m_config.getTextEditorConfig().getZoomDelta() != 0) {
zoom(m_config.getTextEditorConfig().getZoomDelta());
}
if (p_initialized) {
getHighlighter()->updateHighlight();
}
}
void MarkdownEditor::setupTableHelper()
{
m_tableHelper = new MarkdownTableHelper(this, this);
connect(getHighlighter(), &vte::PegMarkdownHighlighter::tableBlocksUpdated,
m_tableHelper, &MarkdownTableHelper::updateTableBlocks);
}
QRgb MarkdownEditor::getPreviewBackground() const
{
auto th = theme();
const auto &fmt = th->editorStyle(vte::Theme::EditorStyle::Preview);
return fmt.m_backgroundColor;
}
void MarkdownEditor::setImageHost(ImageHost *p_host)
{
// It may be different than the global default image host.
m_imageHost = p_host;
}
static QString generateImageHostFileName(const Buffer *p_buffer, const QString &p_destFileName)
{
auto destPath = ImageHostUtils::generateRelativePath(p_buffer);
if (destPath.isEmpty()) {
destPath = p_destFileName;
} else {
destPath += "/" + p_destFileName;
}
return destPath;
}
QString MarkdownEditor::saveToImageHost(const QByteArray &p_imageData, const QString &p_destFileName)
{
Q_ASSERT(m_imageHost);
const auto destPath = generateImageHostFileName(m_buffer, p_destFileName);
QString errMsg;
QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
auto targetUrl = m_imageHost->create(p_imageData, destPath, errMsg);
QApplication::restoreOverrideCursor();
if (targetUrl.isEmpty()) {
MessageBoxHelper::notify(MessageBoxHelper::Warning,
tr("Failed to upload image to image host (%1) as (%2).").arg(m_imageHost->getName(), destPath),
QString(),
errMsg,
this);
}
return targetUrl;
}
void MarkdownEditor::appendImageHostMenu(QMenu *p_menu)
{
p_menu->addSeparator();
auto subMenu = p_menu->addMenu(tr("Upload Images To Image Host"));
const auto &hosts = ImageHostMgr::getInst().getImageHosts();
if (hosts.isEmpty()) {
auto act = subMenu->addAction(tr("None"));
act->setEnabled(false);
return;
}
for (const auto &host : hosts) {
auto act = subMenu->addAction(host->getName(),
this,
&MarkdownEditor::uploadImagesToImageHost);
act->setData(host->getName());
}
}
void MarkdownEditor::uploadImagesToImageHost()
{
auto act = static_cast<QAction *>(sender());
auto host = ImageHostMgr::getInst().find(act->data().toString());
Q_ASSERT(host);
// Only LocalRelativeInternal images.
// Descending order of the link position.
auto images = vte::MarkdownUtils::fetchImagesFromMarkdownText(m_buffer->getContent(),
m_buffer->getResourcePath(),
vte::MarkdownLink::TypeFlag::LocalRelativeInternal);
if (images.isEmpty()) {
return;
}
QProgressDialog proDlg(tr("Uploading local images..."),
tr("Abort"),
0,
images.size(),
this);
proDlg.setWindowModality(Qt::WindowModal);
proDlg.setWindowTitle(tr("Upload Images To Image Host"));
QHash<QString, QString> uploadedImages;
int cnt = 0;
auto cursor = m_textEdit->textCursor();
cursor.beginEditBlock();
for (int i = 0; i < images.size(); ++i) {
const auto &link = images[i];
auto it = uploadedImages.find(link.m_path);
if (it != uploadedImages.end()) {
cursor.setPosition(link.m_urlInLinkPos);
cursor.setPosition(link.m_urlInLinkPos + link.m_urlInLink.size(), QTextCursor::KeepAnchor);
cursor.insertText(it.value());
continue;
}
proDlg.setValue(i + 1);
if (proDlg.wasCanceled()) {
break;
}
proDlg.setLabelText(tr("Upload image (%1)").arg(link.m_path));
Q_ASSERT(i == 0 || link.m_urlInLinkPos < images[i - 1].m_urlInLinkPos);
QByteArray ba;
try {
ba = FileUtils::readFile(link.m_path);
} catch (Exception &e) {
MessageBoxHelper::notify(MessageBoxHelper::Warning,
tr("Failed to read local image file (%1) (%2).").arg(link.m_path, e.what()),
this);
continue;
}
if (ba.isEmpty()) {
qWarning() << "Skipped uploading empty image" << link.m_path;
continue;
}
const auto destPath = generateImageHostFileName(m_buffer, PathUtils::fileName(link.m_path));
QString errMsg;
QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
const auto targetUrl = host->create(ba, destPath, errMsg);
QApplication::restoreOverrideCursor();
if (targetUrl.isEmpty()) {
MessageBoxHelper::notify(MessageBoxHelper::Warning,
tr("Failed to upload image to image host (%1) as (%2).").arg(host->getName(), destPath),
QString(),
errMsg,
this);
continue;
}
// Update the link URL.
cursor.setPosition(link.m_urlInLinkPos);
cursor.setPosition(link.m_urlInLinkPos + link.m_urlInLink.size(), QTextCursor::KeepAnchor);
cursor.insertText(targetUrl);
uploadedImages.insert(link.m_path, targetUrl);
++cnt;
}
cursor.endEditBlock();
proDlg.setValue(images.size());
if (cnt > 0) {
m_textEdit->setTextCursor(cursor);
}
}
void MarkdownEditor::prependContextSensitiveMenu(QMenu *p_menu, const QPoint &p_pos)
{
auto cursor = m_textEdit->cursorForPosition(p_pos);
const int pos = cursor.position();
const auto block = cursor.block();
Q_ASSERT(!p_menu->isEmpty());
auto firstAct = p_menu->actions().at(0);
bool ret = prependImageMenu(p_menu, firstAct, pos, block);
if (ret) {
return;
}
ret = prependLinkMenu(p_menu, firstAct, pos, block);
if (ret) {
return;
}
if (prependInPlacePreviewMenu(p_menu, firstAct, pos, block)) {
p_menu->insertSeparator(firstAct);
}
}
bool MarkdownEditor::prependImageMenu(QMenu *p_menu, QAction *p_before, int p_cursorPos, const QTextBlock &p_block)
{
const auto text = p_block.text();
if (!vte::MarkdownUtils::hasImageLink(text)) {
return false;
}
QString imgPath;
const auto &regions = getHighlighter()->getImageRegions();
for (const auto &reg : regions) {
if (!reg.contains(p_cursorPos) && (!reg.contains(p_cursorPos - 1) || p_cursorPos != p_block.position() + text.size())) {
continue;
}
if (reg.m_endPos > p_block.position() + text.size()) {
return true;
}
const auto linkText = text.mid(reg.m_startPos - p_block.position(), reg.m_endPos - reg.m_startPos);
int linkWidth = 0;
int linkHeight = 0;
const auto shortUrl = vte::MarkdownUtils::fetchImageLinkUrl(linkText, linkWidth, linkHeight);
if (shortUrl.isEmpty()) {
return true;
}
imgPath = vte::MarkdownUtils::linkUrlToPath(getBasePath(), shortUrl);
break;
}
{
auto act = new QAction(tr("View Image"), p_menu);
connect(act, &QAction::triggered,
p_menu, [imgPath]() {
WidgetUtils::openUrlByDesktop(PathUtils::pathToUrl(imgPath));
});
p_menu->insertAction(p_before, act);
}
{
auto act = new QAction(tr("Copy Image URL"), p_menu);
connect(act, &QAction::triggered,
p_menu, [imgPath]() {
ClipboardUtils::setLinkToClipboard(imgPath);
});
p_menu->insertAction(p_before, act);
}
if (QFileInfo::exists(imgPath)) {
// Local image.
auto act = new QAction(tr("Copy Image"), p_menu);
connect(act, &QAction::triggered,
p_menu, [imgPath]() {
auto clipboard = QApplication::clipboard();
clipboard->clear();
auto img = FileUtils::imageFromFile(imgPath);
if (!img.isNull()) {
ClipboardUtils::setImageToClipboard(clipboard, img);
}
});
p_menu->insertAction(p_before, act);
} else {
// Online image.
prependInPlacePreviewMenu(p_menu, p_before, p_cursorPos, p_block);
}
p_menu->insertSeparator(p_before);
return true;
}
bool MarkdownEditor::prependInPlacePreviewMenu(QMenu *p_menu, QAction *p_before, int p_cursorPos, const QTextBlock &p_block)
{
auto data = vte::TextBlockData::get(p_block);
if (!data) {
return false;
}
auto previewData = data->getBlockPreviewData();
if (!previewData) {
return false;
}
QPixmap image;
QRgb background = 0;
const int pib = p_cursorPos - p_block.position();
for (const auto &info : previewData->getPreviewData()) {
const auto *imageData = info->getImageData();
if (!imageData) {
continue;
}
if (imageData->contains(pib) || (imageData->contains(pib - 1) && pib == p_block.length() - 1)) {
const auto *img = findImageFromDocumentResourceMgr(imageData->m_imageName);
if (img) {
image = *img;
background = imageData->m_backgroundColor;
}
break;
}
}
if (image.isNull()) {
return false;
}
auto act = new QAction(tr("Copy In-Place Preview"), p_menu);
connect(act, &QAction::triggered,
p_menu, [this, image, background]() {
QColor color(background);
if (background == 0) {
color = m_textEdit->palette().color(QPalette::Base);
}
QImage img(image.size(), QImage::Format_ARGB32);
img.fill(color);
QPainter painter(&img);
painter.drawPixmap(img.rect(), image);
auto clipboard = QApplication::clipboard();
clipboard->clear();
ClipboardUtils::setImageToClipboard(clipboard, img);
});
p_menu->insertAction(p_before, act);
return true;
}
bool MarkdownEditor::prependLinkMenu(QMenu *p_menu, QAction *p_before, int p_cursorPos, const QTextBlock &p_block)
{
const auto text = p_block.text();
QRegularExpression regExp(vte::MarkdownUtils::c_linkRegExp);
QString linkText;
const int pib = p_cursorPos - p_block.position();
auto matchIter = regExp.globalMatch(text);
while (matchIter.hasNext()) {
auto match = matchIter.next();
if (pib >= match.capturedStart() && pib < match.capturedEnd()) {
linkText = match.captured(2);
break;
}
}
if (linkText.isEmpty()) {
return false;
}
const auto linkUrl = vte::MarkdownUtils::linkUrlToPath(getBasePath(), linkText);
{
auto act = new QAction(tr("Open Link"), p_menu);
connect(act, &QAction::triggered,
p_menu, [linkUrl]() {
emit VNoteX::getInst().openFileRequested(linkUrl, QSharedPointer<FileOpenParameters>::create());
});
p_menu->insertAction(p_before, act);
}
{
auto act = new QAction(tr("Copy Link"), p_menu);
connect(act, &QAction::triggered,
p_menu, [linkUrl]() {
ClipboardUtils::setLinkToClipboard(linkUrl);
});
p_menu->insertAction(p_before, act);
}
p_menu->insertSeparator(p_before);
return true;
}