From 0b58669e3991b7bf68a0ea26ca76bdd9493cfb20 Mon Sep 17 00:00:00 2001 From: Le Tan Date: Sat, 6 Nov 2021 11:43:10 +0800 Subject: [PATCH] MarkdownEditor: support context-sensitive context menu for images and links --- libs/vtextedit | 2 +- src/core/buffer/filebufferprovider.cpp | 7 +- src/core/buffer/nodebufferprovider.cpp | 7 +- src/core/buffermgr.cpp | 1 + src/data/extra/docs/en/welcome.md | 4 +- src/data/extra/docs/zh_CN/welcome.md | 4 +- src/utils/clipboardutils.cpp | 30 +++- src/utils/clipboardutils.h | 4 + src/utils/contentmediautils.cpp | 32 ++-- src/widgets/editors/markdowneditor.cpp | 215 ++++++++++++++++++++++++- src/widgets/editors/markdowneditor.h | 8 + src/widgets/markdownviewwindow.cpp | 3 +- src/widgets/markdownviewwindow.h | 2 + 13 files changed, 299 insertions(+), 20 deletions(-) diff --git a/libs/vtextedit b/libs/vtextedit index 44e6bcbc..6c2ff0e7 160000 --- a/libs/vtextedit +++ b/libs/vtextedit @@ -1 +1 @@ -Subproject commit 44e6bcbcf4a0be2bfd2333098aa48084ee6fc14c +Subproject commit 6c2ff0e78aedb6d4a107cd4825473d47813596cc diff --git a/src/core/buffer/filebufferprovider.cpp b/src/core/buffer/filebufferprovider.cpp index ec04c56e..765397d0 100644 --- a/src/core/buffer/filebufferprovider.cpp +++ b/src/core/buffer/filebufferprovider.cpp @@ -6,6 +6,7 @@ #include #include #include +#include using namespace vnotex; @@ -160,7 +161,11 @@ void FileBufferProvider::removeImage(const QString &p_imagePath) { auto file = m_file->getImageInterface(); if (file) { - file->removeImage(p_imagePath); + try { + file->removeImage(p_imagePath); + } catch (Exception &e) { + qWarning() << "failed to remove image" << p_imagePath << e.what(); + } } } diff --git a/src/core/buffer/nodebufferprovider.cpp b/src/core/buffer/nodebufferprovider.cpp index 532d14ae..373a5f65 100644 --- a/src/core/buffer/nodebufferprovider.cpp +++ b/src/core/buffer/nodebufferprovider.cpp @@ -6,6 +6,7 @@ #include #include #include +#include using namespace vnotex; @@ -140,7 +141,11 @@ void NodeBufferProvider::removeImage(const QString &p_imagePath) { auto file = m_nodeFile->getImageInterface(); if (file) { - file->removeImage(p_imagePath); + try { + file->removeImage(p_imagePath); + } catch (Exception &e) { + qWarning() << "failed to remove image" << p_imagePath << e.what(); + } } } diff --git a/src/core/buffermgr.cpp b/src/core/buffermgr.cpp index 3c59db33..59ae8749 100644 --- a/src/core/buffermgr.cpp +++ b/src/core/buffermgr.cpp @@ -101,6 +101,7 @@ void BufferMgr::open(const QString &p_filePath, const QSharedPointer #include #include +#include #include "utils.h" +#include "pathutils.h" using namespace vnotex; QString ClipboardUtils::getTextFromClipboard() { QClipboard *clipboard = QApplication::clipboard(); - QString subtype("plain"); - return clipboard->text(subtype); + QString subType(QStringLiteral("plain")); + return clipboard->text(subType, QClipboard::Clipboard); } void ClipboardUtils::setTextToClipboard(const QString &p_text) @@ -25,6 +27,12 @@ void ClipboardUtils::setTextToClipboard(const QString &p_text) clipboard->setText(p_text); } +void ClipboardUtils::setLinkToClipboard(const QString &p_link) +{ + QClipboard *clipboard = QApplication::clipboard(); + setMimeDataToClipboard(clipboard, linkMimeData(p_link).release()); +} + void ClipboardUtils::clearClipboard() { QClipboard *clipboard = QApplication::clipboard(); @@ -194,3 +202,21 @@ void ClipboardUtils::setImageLoop(QClipboard *p_clipboard, Utils::sleepWait(100 /* ms */); } } + +std::unique_ptr ClipboardUtils::linkMimeData(const QString &p_link) +{ + QList urls; + urls.append(PathUtils::pathToUrl(p_link)); + std::unique_ptr data(new QMimeData()); + data->setUrls(urls); + + QString text = urls[0].toEncoded(); +#if defined(Q_OS_WIN) + if (urls[0].isLocalFile()) { + text = urls[0].toString(QUrl::EncodeSpaces); + } +#endif + + data->setText(text); + return data; +} diff --git a/src/utils/clipboardutils.h b/src/utils/clipboardutils.h index 47e83df9..d5e5fdd4 100644 --- a/src/utils/clipboardutils.h +++ b/src/utils/clipboardutils.h @@ -16,6 +16,8 @@ namespace vnotex static void setTextToClipboard(const QString &p_text); + static void setLinkToClipboard(const QString &p_link); + // @p_mimeData will be owned by utils. static void setMimeDataToClipboard(QClipboard *p_clipboard, QMimeData *p_mimeData, @@ -39,6 +41,8 @@ namespace vnotex static void setImageLoop(QClipboard *p_clipboard, const QImage &p_image, QClipboard::Mode p_mode); + + static std::unique_ptr linkMimeData(const QString &p_link); }; } // ns vnotex diff --git a/src/utils/contentmediautils.cpp b/src/utils/contentmediautils.cpp index a72799b3..ffe175ba 100644 --- a/src/utils/contentmediautils.cpp +++ b/src/utils/contentmediautils.cpp @@ -12,10 +12,12 @@ #include #include +#include #include #include #include +#include using namespace vnotex; @@ -78,6 +80,8 @@ void ContentMediaUtils::copyMarkdownMediaFiles(const QString &p_content, Q_ASSERT(link.m_urlInLinkPos < lastPos); lastPos = link.m_urlInLinkPos; + qDebug() << "link" << link.m_path << link.m_urlInLink; + if (handledImages.contains(link.m_path)) { auto it = renamedImages.find(link.m_path); if (it != renamedImages.end()) { @@ -94,7 +98,8 @@ void ContentMediaUtils::copyMarkdownMediaFiles(const QString &p_content, } // Get the relative path of the image and apply it to the dest file path. - const auto oldDestFilePath = destDir.filePath(link.m_urlInLink); + const auto decodedUrlInLink = vte::TextUtils::decodeUrl(link.m_urlInLink); + const auto oldDestFilePath = destDir.filePath(decodedUrlInLink); destDir.mkpath(PathUtils::parentDirPath(oldDestFilePath)); auto destFilePath = p_backend ? p_backend->renameIfExistsCaseInsensitive(oldDestFilePath) : FileUtils::renameIfExistsCaseInsensitive(oldDestFilePath); @@ -102,13 +107,15 @@ void ContentMediaUtils::copyMarkdownMediaFiles(const QString &p_content, // Rename happens. const auto oldFileName = PathUtils::fileName(oldDestFilePath); const auto newFileName = PathUtils::fileName(destFilePath); - qWarning() << QString("image name conflicts when copy. Renamed from (%1) to (%2)").arg(oldFileName, newFileName); + qWarning() << QString("image name conflicts when copy, renamed from (%1) to (%2)").arg(oldFileName, newFileName); // Update the text content. + const auto encodedOldFileName = vte::TextUtils::encodeUrl(oldFileName); + const auto encodedNewFileName = vte::TextUtils::encodeUrl(newFileName); auto newUrlInLink(link.m_urlInLink); - newUrlInLink.replace(newUrlInLink.size() - oldFileName.size(), - oldFileName.size(), - newFileName); + newUrlInLink.replace(newUrlInLink.size() - encodedOldFileName.size(), + encodedOldFileName.size(), + encodedNewFileName); content.replace(link.m_urlInLinkPos, link.m_urlInLink.size(), newUrlInLink); renamedImages.insert(link.m_path, newUrlInLink); @@ -158,7 +165,7 @@ void ContentMediaUtils::removeMarkdownMediaFiles(const File *p_file, INotebookBa handledImages.insert(link.m_path); if (!QFileInfo::exists(link.m_path)) { - qWarning() << "Image of Markdown file does not exist" << link.m_path << link.m_urlInLink; + qWarning() << "image of Markdown file does not exist" << link.m_path << link.m_urlInLink; continue; } p_backend->removeFile(link.m_path); @@ -175,10 +182,15 @@ void ContentMediaUtils::copyAttachment(Node *p_node, // Copy the whole folder. const auto srcAttachmentFolderPath = p_node->fetchAttachmentFolderPath(); - if (p_backend) { - p_backend->copyDir(srcAttachmentFolderPath, p_destAttachmentFolderPath); - } else { - FileUtils::copyDir(srcAttachmentFolderPath, p_destAttachmentFolderPath); + try { + if (p_backend) { + p_backend->copyDir(srcAttachmentFolderPath, p_destAttachmentFolderPath); + } else { + FileUtils::copyDir(srcAttachmentFolderPath, p_destAttachmentFolderPath); + } + } catch (Exception &e) { + qWarning() << "failed to copy attachment folder" << srcAttachmentFolderPath << e.what(); + return; } // Check if we need to modify links in content. diff --git a/src/widgets/editors/markdowneditor.cpp b/src/widgets/editors/markdowneditor.cpp index 962ee147..b82ca238 100644 --- a/src/widgets/editors/markdowneditor.cpp +++ b/src/widgets/editors/markdowneditor.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -24,6 +25,8 @@ #include #include #include +#include +#include #include #include @@ -40,12 +43,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -960,7 +965,9 @@ void MarkdownEditor::handleContextMenuEvent(QContextMenuEvent *p_event, bool *p_ // QAction *copyAct = WidgetUtils::findActionByObjectName(actions, "edit-copy"); QAction *pasteAct = WidgetUtils::findActionByObjectName(actions, "edit-paste"); - if (!m_textEdit->hasSelection()) { + const bool hasSelection = m_textEdit->hasSelection(); + + if (!hasSelection) { auto readAct = new QAction(tr("&Read"), menu); WidgetUtils::addActionShortcutText(readAct, ConfigMgr::getInst().getEditorConfig().getShortcut(EditorConfig::Shortcut::EditRead)); @@ -970,6 +977,8 @@ void MarkdownEditor::handleContextMenuEvent(QContextMenuEvent *p_event, bool *p_ if (firstAct) { menu->insertSeparator(firstAct); } + + prependContextSensitiveMenu(menu, p_event->pos()); } if (pasteAct && pasteAct->isEnabled()) { @@ -1001,7 +1010,9 @@ void MarkdownEditor::handleContextMenuEvent(QContextMenuEvent *p_event, bool *p_ ConfigMgr::getInst().getEditorConfig().getShortcut(EditorConfig::Shortcut::ApplySnippet)); } - appendImageHostMenu(menu); + if (!hasSelection) { + appendImageHostMenu(menu); + } appendSpellCheckMenu(p_event, menu); } @@ -1443,3 +1454,203 @@ void MarkdownEditor::uploadImagesToImageHost() 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 ®ions = getHighlighter()->getImageRegions(); + for (const auto ® : 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::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; +} diff --git a/src/widgets/editors/markdowneditor.h b/src/widgets/editors/markdowneditor.h index b266e748..848cb20a 100644 --- a/src/widgets/editors/markdowneditor.h +++ b/src/widgets/editors/markdowneditor.h @@ -193,6 +193,14 @@ namespace vnotex void uploadImagesToImageHost(); + void prependContextSensitiveMenu(QMenu *p_menu, const QPoint &p_pos); + + bool prependImageMenu(QMenu *p_menu, QAction *p_before, int p_cursorPos, const QTextBlock &p_block); + + bool prependInPlacePreviewMenu(QMenu *p_menu, QAction *p_before, int p_cursorPos, const QTextBlock &p_block); + + bool prependLinkMenu(QMenu *p_menu, QAction *p_before, int p_cursorPos, const QTextBlock &p_block); + static QString generateImageFileNameToInsertAs(const QString &p_title, const QString &p_suffix); const MarkdownEditorConfig &m_config; diff --git a/src/widgets/markdownviewwindow.cpp b/src/widgets/markdownviewwindow.cpp index 1f898699..fca0eefb 100644 --- a/src/widgets/markdownviewwindow.cpp +++ b/src/widgets/markdownviewwindow.cpp @@ -130,7 +130,7 @@ void MarkdownViewWindow::setModeInternal(ViewWindowMode p_mode, bool p_syncBuffe toggleDebug(); } - bool hideViewer = true; + bool hideViewer = m_viewerReady; if (!m_editor) { // We need viewer to preview. if (!m_viewer) { @@ -485,6 +485,7 @@ void MarkdownViewWindow::setupViewer() }); connect(adapter, &MarkdownViewerAdapter::viewerReady, this, [this]() { + m_viewerReady = true; if (m_mode == ViewWindowMode::Edit) { m_viewer->hide(); } diff --git a/src/widgets/markdownviewwindow.h b/src/widgets/markdownviewwindow.h index 29815561..4ca8cd81 100644 --- a/src/widgets/markdownviewwindow.h +++ b/src/widgets/markdownviewwindow.h @@ -208,6 +208,8 @@ namespace vnotex QSharedPointer m_outlineProvider; ImageHost *m_imageHost = nullptr; + + bool m_viewerReady = false; }; }