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

This commit is contained in:
Le Tan 2021-11-06 11:43:10 +08:00
parent 9fca1cad12
commit 0b58669e39
13 changed files with 299 additions and 20 deletions

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

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}

View File

@ -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.

View File

@ -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) 。
* 请关闭 *有道词典* 或者禁用其取词翻译功能。

View File

@ -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;
}

View File

@ -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

View File

@ -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.

View File

@ -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;
}

View File

@ -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;

View File

@ -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();
}

View File

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