浏览代码

MarkdownEditor: support context-sensitive context menu for images and links

Le Tan 4 年之前
父节点
当前提交
0b58669e39

+ 1 - 1
libs/vtextedit

@@ -1 +1 @@
-Subproject commit 44e6bcbcf4a0be2bfd2333098aa48084ee6fc14c
+Subproject commit 6c2ff0e78aedb6d4a107cd4825473d47813596cc

+ 6 - 1
src/core/buffer/filebufferprovider.cpp

@@ -6,6 +6,7 @@
 #include <utils/fileutils.h>
 #include <notebook/node.h>
 #include <core/file.h>
+#include <core/exception.h>
 
 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();
+        }
     }
 }
 

+ 6 - 1
src/core/buffer/nodebufferprovider.cpp

@@ -6,6 +6,7 @@
 #include <notebook/notebook.h>
 #include <utils/pathutils.h>
 #include <core/file.h>
+#include <core/exception.h>
 
 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();
+        }
     }
 }
 

+ 1 - 0
src/core/buffermgr.cpp

@@ -101,6 +101,7 @@ void BufferMgr::open(const QString &p_filePath, const QSharedPointer<FileOpenPar
         auto msg = QString("Failed to open file that does not exist (%1)").arg(p_filePath);
         qWarning() << msg;
         VNoteX::getInst().showStatusMessageShort(msg);
+        WidgetUtils::openUrlByDesktop(QUrl::fromUserInput(p_filePath));
         return;
     }
 

+ 3 - 1
src/data/extra/docs/en/welcome.md

@@ -5,8 +5,10 @@ For more information, please visit [**VNote's Home Page**](https://vnotex.github
 
 ## FAQs
 * If VNote crashes after update, please delete the `vnotex.json` file under user configuration folder.
-* For **Windows** users, if VNote hangs frequently or behaves unexpectedly in interface, please check the **OpenGL** option. [Details here](https://github.com/vnotex/vnote/issues/853).
 * VNote has a series of powerful shortcuts. Please view the user configuration file `vnotex.json` for a complete list of shortcuts.
     * The key sequence `Ctrl+G, I` means that first press both `Ctrl` and `G` simultaneously, release them, then press `I` and release.
 * Feedbacks are appreciated! Please [post an issue](https://github.com/vnotex/vnote/issues) on GitHub if there is any.
 
+### Windows Users
+* if VNote hangs frequently or behaves unexpectedly in interface, please check the **OpenGL** option. [Details here](https://github.com/vnotex/vnote/issues/853).
+* Please close *Youdao Dict* or disable its fetching-word feature.

+ 3 - 1
src/data/extra/docs/zh_CN/welcome.md

@@ -5,8 +5,10 @@
 
 ## 常见问题
 * 如果更新后 VNote 崩溃,请删除用户配置文件夹中的 `vnotex.json` 文件。
-* 对于 **Windows** 用户,如果 VNote 经常卡顿或无响应,或者界面异常,请检查 **OpenGL** 选项。[详情](https://github.com/vnotex/vnote/issues/853) 。
 * VNote 有着一系列强大的快捷键。请查看用户配置文件 `vnotex.json` 以获取一个完整的快捷键列表。
     * 按键序列 `Ctrl+G, I` 表示先同时按下 `Ctrl` 和 `G`,释放,然后按下 `I` 并释放。
 * 使用中有任何问题,欢迎[反馈](https://github.com/vnotex/vnote/issues) 。
 
+## Windows 用户
+* 如果 VNote 经常卡顿或无响应,或者界面异常,请检查 **OpenGL** 选项。[详情](https://github.com/vnotex/vnote/issues/853) 。
+* 请关闭 *有道词典* 或者禁用其取词翻译功能。

+ 28 - 2
src/utils/clipboardutils.cpp

@@ -7,16 +7,18 @@
 #include <QDebug>
 #include <QUrl>
 #include <QAction>
+#include <QList>
 
 #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<QMimeData> ClipboardUtils::linkMimeData(const QString &p_link)
+{
+    QList<QUrl> urls;
+    urls.append(PathUtils::pathToUrl(p_link));
+    std::unique_ptr<QMimeData> 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;
+}

+ 4 - 0
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<QMimeData> linkMimeData(const QString &p_link);
     };
 } // ns vnotex
 

+ 22 - 10
src/utils/contentmediautils.cpp

@@ -12,10 +12,12 @@
 #include <buffer/filetypehelper.h>
 
 #include <vtextedit/markdownutils.h>
+#include <vtextedit/textutils.h>
 
 #include <utils/pathutils.h>
 #include <utils/fileutils.h>
 #include <core/file.h>
+#include <core/exception.h>
 
 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.

+ 213 - 2
src/widgets/editors/markdowneditor.cpp

@@ -15,6 +15,7 @@
 #include <QTemporaryFile>
 #include <QTimer>
 #include <QBuffer>
+#include <QPainter>
 
 #include <vtextedit/markdowneditorconfig.h>
 #include <vtextedit/previewmgr.h>
@@ -24,6 +25,8 @@
 #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>
@@ -40,12 +43,14 @@
 #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>
@@ -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 &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;
+}

+ 8 - 0
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;

+ 2 - 1
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();
                 }

+ 2 - 0
src/widgets/markdownviewwindow.h

@@ -208,6 +208,8 @@ namespace vnotex
         QSharedPointer<OutlineProvider> m_outlineProvider;
 
         ImageHost *m_imageHost = nullptr;
+
+        bool m_viewerReady = false;
     };
 }