From d4daf32f20788b4fd4603c547a6475d32aed602e Mon Sep 17 00:00:00 2001 From: Le Tan Date: Wed, 22 Aug 2018 20:01:31 +0800 Subject: [PATCH] MdEditor: support copying diagram in puml and graphviz --- src/pegmarkdownhighlighter.h | 7 ++ src/utils/veditutils.cpp | 9 +++ src/utils/veditutils.h | 3 + src/utils/vprocessutils.cpp | 53 +++++++++++---- src/utils/vprocessutils.h | 13 ++++ src/veditor.cpp | 11 +++ src/veditor.h | 11 +++ src/vgraphvizhelper.cpp | 23 +++++++ src/vgraphvizhelper.h | 6 +- src/vlivepreviewhelper.cpp | 24 +++---- src/vmdeditor.cpp | 125 +++++++++++++++++++++++++++++++++++ src/vmdeditor.h | 14 ++++ src/vplantumlhelper.cpp | 83 ++++++++++++++++------- src/vplantumlhelper.h | 13 +++- 14 files changed, 337 insertions(+), 58 deletions(-) diff --git a/src/pegmarkdownhighlighter.h b/src/pegmarkdownhighlighter.h index 7c0ff2c8..fcf35e54 100644 --- a/src/pegmarkdownhighlighter.h +++ b/src/pegmarkdownhighlighter.h @@ -44,6 +44,8 @@ public: const QVector &getImageRegions() const; + const QVector &getCodeBlocks() const; + public slots: // Parse and rehighlight immediately. void updateHighlight(); @@ -360,4 +362,9 @@ inline bool PegMarkdownHighlighter::isFastParseBlock(int p_blockNum) const { return p_blockNum >= m_fastParseBlocks.first && p_blockNum <= m_fastParseBlocks.second; } + +inline const QVector &PegMarkdownHighlighter::getCodeBlocks() const +{ + return m_result->m_codeBlocks; +} #endif // PEGMARKDOWNHIGHLIGHTER_H diff --git a/src/utils/veditutils.cpp b/src/utils/veditutils.cpp index db7cbe7b..808769c6 100644 --- a/src/utils/veditutils.cpp +++ b/src/utils/veditutils.cpp @@ -7,6 +7,7 @@ #include #include "vutils.h" +#include "vcodeblockhighlighthelper.h" void VEditUtils::removeBlock(QTextBlock &p_block, QString *p_text) { @@ -1083,3 +1084,11 @@ bool VEditUtils::isWordSeparator(QChar p_char) return false; } + +QString VEditUtils::removeCodeBlockFence(const QString &p_text) +{ + QString text = VCodeBlockHighlightHelper::unindentCodeBlock(p_text); + Q_ASSERT(text.startsWith("```") && text.endsWith("```")); + int idx = text.indexOf('\n') + 1; + return text.mid(idx, text.size() - idx - 3); +} diff --git a/src/utils/veditutils.h b/src/utils/veditutils.h index ba861d12..b164e831 100644 --- a/src/utils/veditutils.h +++ b/src/utils/veditutils.h @@ -211,6 +211,9 @@ public: static bool isWordSeparator(QChar p_char); + // Remove the fence of fenced code block. + static QString removeCodeBlockFence(const QString &p_text); + private: VEditUtils() {} }; diff --git a/src/utils/vprocessutils.cpp b/src/utils/vprocessutils.cpp index da3547f3..9b358857 100644 --- a/src/utils/vprocessutils.cpp +++ b/src/utils/vprocessutils.cpp @@ -11,24 +11,36 @@ int VProcessUtils::startProcess(const QString &p_program, QByteArray &p_out, QByteArray &p_err) { - int ret = 0; QScopedPointer process(new QProcess()); process->start(p_program, p_args); + return startProcess(process.data(), + p_in, + p_exitCode, + p_out, + p_err); +} +int VProcessUtils::startProcess(QProcess *p_process, + const QByteArray &p_in, + int &p_exitCode, + QByteArray &p_out, + QByteArray &p_err) +{ + int ret = 0; if (!p_in.isEmpty()) { - if (process->write(p_in) == -1) { - process->closeWriteChannel(); - qWarning() << "fail to write to QProcess:" << process->errorString(); + if (p_process->write(p_in) == -1) { + p_process->closeWriteChannel(); + qWarning() << "fail to write to QProcess:" << p_process->errorString(); return -1; } else { - process->closeWriteChannel(); + p_process->closeWriteChannel(); } } bool finished = false; bool started = false; while (true) { - QProcess::ProcessError err = process->error(); + QProcess::ProcessError err = p_process->error(); if (err == QProcess::FailedToStart || err == QProcess::Crashed) { if (err == QProcess::FailedToStart) { @@ -41,34 +53,34 @@ int VProcessUtils::startProcess(const QString &p_program, } if (started) { - if (process->state() == QProcess::NotRunning) { + if (p_process->state() == QProcess::NotRunning) { finished = true; } } else { - if (process->state() != QProcess::NotRunning) { + if (p_process->state() != QProcess::NotRunning) { started = true; } } - if (process->waitForFinished(500)) { + if (p_process->waitForFinished(500)) { // Finished. finished = true; } if (finished) { - QProcess::ExitStatus sta = process->exitStatus(); + QProcess::ExitStatus sta = p_process->exitStatus(); if (sta == QProcess::CrashExit) { ret = -1; break; } - p_exitCode = process->exitCode(); + p_exitCode = p_process->exitCode(); break; } } - p_out = process->readAllStandardOutput(); - p_err = process->readAllStandardError(); + p_out = p_process->readAllStandardOutput(); + p_err = p_process->readAllStandardError(); return ret; } @@ -86,3 +98,18 @@ int VProcessUtils::startProcess(const QString &p_program, p_out, p_err); } + +int VProcessUtils::startProcess(const QString &p_cmd, + const QByteArray &p_in, + int &p_exitCode, + QByteArray &p_out, + QByteArray &p_err) +{ + QScopedPointer process(new QProcess()); + process->start(p_cmd); + return startProcess(process.data(), + p_in, + p_exitCode, + p_out, + p_err); +} diff --git a/src/utils/vprocessutils.h b/src/utils/vprocessutils.h index afa6ac7b..cb7924f4 100644 --- a/src/utils/vprocessutils.h +++ b/src/utils/vprocessutils.h @@ -4,6 +4,7 @@ #include #include +class QProcess; class VProcessUtils { @@ -25,8 +26,20 @@ public: QByteArray &p_out, QByteArray &p_err); + static int startProcess(const QString &p_cmd, + const QByteArray &p_in, + int &p_exitCode, + QByteArray &p_out, + QByteArray &p_err); + private: VProcessUtils() {} + + static int startProcess(QProcess *p_process, + const QByteArray &p_in, + int &p_exitCode, + QByteArray &p_out, + QByteArray &p_err); }; #endif // VPROCESSUTILS_H diff --git a/src/veditor.cpp b/src/veditor.cpp index 91b3a812..2503e0cc 100644 --- a/src/veditor.cpp +++ b/src/veditor.cpp @@ -35,6 +35,17 @@ VEditor::~VEditor() if (m_completer->widget() == m_editor) { m_completer->setWidget(NULL); } + + cleanUp(); +} + +void VEditor::cleanUp() +{ + for (auto const & file : m_tempFiles) { + VUtils::deleteFile(file); + } + + m_tempFiles.clear(); } void VEditor::init() diff --git a/src/veditor.h b/src/veditor.h index fb929b4f..2a2968d7 100644 --- a/src/veditor.h +++ b/src/veditor.h @@ -254,6 +254,8 @@ protected: virtual int lineNumberAreaWidth() const = 0; + void addTempFile(const QString &p_file); + QWidget *m_editor; VEditorObject *m_object; @@ -309,6 +311,8 @@ private: QStringList generateCompletionCandidates() const; + void cleanUp(); + QLabel *m_wrapLabel; QTimer *m_labelTimer; @@ -352,6 +356,9 @@ private: QSharedPointer m_completer; + // Temp files needed to be delete. + QStringList m_tempFiles; + // Functions for private slots. private: void labelTimerTimeout(); @@ -456,4 +463,8 @@ inline QWidget *VEditor::getEditor() const return m_editor; } +inline void VEditor::addTempFile(const QString &p_file) +{ + m_tempFiles.append(p_file); +} #endif // VEDITOR_H diff --git a/src/vgraphvizhelper.cpp b/src/vgraphvizhelper.cpp index d89c5721..fa3ed04c 100644 --- a/src/vgraphvizhelper.cpp +++ b/src/vgraphvizhelper.cpp @@ -119,3 +119,26 @@ bool VGraphvizHelper::testGraphviz(const QString &p_dot, QString &p_msg) return ret == 0 && exitCode == 0; } + +QByteArray VGraphvizHelper::process(const QString &p_format, const QString &p_text) +{ + VGraphvizHelper inst; + + int exitCode = -1; + QByteArray out, err; + + QStringList args(inst.m_args); + args << ("-T" + p_format); + int ret = VProcessUtils::startProcess(inst.m_program, + args, + p_text.toUtf8(), + exitCode, + out, + err); + + if (ret != 0 || exitCode < 0) { + qWarning() << "Graphviz fail" << ret << exitCode << QString::fromLocal8Bit(err); + } + + return out; +} diff --git a/src/vgraphvizhelper.h b/src/vgraphvizhelper.h index 6a84327d..bf56e3a0 100644 --- a/src/vgraphvizhelper.h +++ b/src/vgraphvizhelper.h @@ -16,10 +16,10 @@ public: void processAsync(int p_id, TimeStamp p_timeStamp, const QString &p_format, const QString &p_text); - void prepareCommand(QString &p_cmd, QStringList &p_args) const; - static bool testGraphviz(const QString &p_dot, QString &p_msg); + static QByteArray process(const QString &p_format, const QString &p_text); + signals: void resultReady(int p_id, TimeStamp p_timeStamp, const QString &p_format, const QString &p_result); @@ -27,6 +27,8 @@ private slots: void handleProcessFinished(int p_exitCode, QProcess::ExitStatus p_exitStatus); private: + void prepareCommand(QString &p_cmd, QStringList &p_args) const; + QString m_program; QStringList m_args; }; diff --git a/src/vlivepreviewhelper.cpp b/src/vlivepreviewhelper.cpp index 20710a52..bdf4908a 100644 --- a/src/vlivepreviewhelper.cpp +++ b/src/vlivepreviewhelper.cpp @@ -8,10 +8,10 @@ #include "vconfigmanager.h" #include "vgraphvizhelper.h" #include "vplantumlhelper.h" -#include "vcodeblockhighlighthelper.h" #include "vmainwindow.h" #include "veditarea.h" #include "vmathjaxpreviewhelper.h" +#include "utils/veditutils.h" extern VConfigManager *g_config; @@ -249,14 +249,6 @@ void VLivePreviewHelper::handleCursorPositionChanged() } } -static QString removeFence(const QString &p_text) -{ - QString text = VCodeBlockHighlightHelper::unindentCodeBlock(p_text); - Q_ASSERT(text.startsWith("```") && text.endsWith("```")); - int idx = text.indexOf('\n') + 1; - return text.mid(idx, text.size() - idx - 3); -} - void VLivePreviewHelper::updateLivePreview() { if (m_cbIndex < 0) { @@ -277,7 +269,7 @@ void VLivePreviewHelper::updateLivePreview() m_graphvizHelper->processAsync(m_cbIndex | LANG_PREFIX_GRAPHVIZ | TYPE_LIVE_PREVIEW, m_timeStamp, "svg", - removeFence(vcb.m_text)); + VEditUtils::removeCodeBlockFence(vcb.m_text)); } else { m_document->setPreviewContent(vcb.m_lang, cb.imageData()); } @@ -292,7 +284,7 @@ void VLivePreviewHelper::updateLivePreview() m_plantUMLHelper->processAsync(m_cbIndex | LANG_PREFIX_PLANTUML | TYPE_LIVE_PREVIEW, m_timeStamp, "svg", - removeFence(vcb.m_text)); + VEditUtils::removeCodeBlockFence(vcb.m_text)); } else { m_document->setPreviewContent(vcb.m_lang, cb.imageData()); } @@ -300,7 +292,7 @@ void VLivePreviewHelper::updateLivePreview() // No need to live preview MathJax. m_document->previewCodeBlock(m_cbIndex, vcb.m_lang, - removeFence(vcb.m_text), + VEditUtils::removeCodeBlockFence(vcb.m_text), true); } } @@ -418,7 +410,7 @@ void VLivePreviewHelper::processForInplacePreview(int p_idx) m_graphvizHelper->processAsync(p_idx | LANG_PREFIX_GRAPHVIZ | TYPE_INPLACE_PREVIEW, m_timeStamp, "svg", - removeFence(vcb.m_text)); + VEditUtils::removeCodeBlockFence(vcb.m_text)); } else if (vcb.m_lang == "puml" && m_plantUMLMode == PlantUMLMode::LocalPlantUML) { if (!m_plantUMLHelper) { m_plantUMLHelper = new VPlantUMLHelper(this); @@ -429,19 +421,19 @@ void VLivePreviewHelper::processForInplacePreview(int p_idx) m_plantUMLHelper->processAsync(p_idx | LANG_PREFIX_PLANTUML | TYPE_INPLACE_PREVIEW, m_timeStamp, "svg", - removeFence(vcb.m_text)); + VEditUtils::removeCodeBlockFence(vcb.m_text)); } else if (vcb.m_lang == "flow" || vcb.m_lang == "flowchart") { m_mathJaxHelper->previewDiagram(m_mathJaxID, p_idx, m_timeStamp, vcb.m_lang, - removeFence(vcb.m_text)); + VEditUtils::removeCodeBlockFence(vcb.m_text)); } else if (vcb.m_lang == "mathjax") { m_mathJaxHelper->previewMathJax(m_mathJaxID, p_idx, m_timeStamp, - removeFence(vcb.m_text)); + VEditUtils::removeCodeBlockFence(vcb.m_text)); } } diff --git a/src/vmdeditor.cpp b/src/vmdeditor.cpp index f23132a4..d04582ed 100644 --- a/src/vmdeditor.cpp +++ b/src/vmdeditor.cpp @@ -25,6 +25,8 @@ #include "utils/vwebutils.h" #include "dialog/vinsertlinkdialog.h" #include "utils/vclipboardutils.h" +#include "vplantumlhelper.h" +#include "vgraphvizhelper.h" extern VWebUtils *g_webUtils; @@ -1604,7 +1606,16 @@ void VMdEditor::initLinkAndPreviewMenu(QAction *p_before, QMenu *p_menu, const Q return; } + bool needSeparator = false; if (initInPlacePreviewMenu(p_before, p_menu, block, pos)) { + needSeparator = true; + } + + if (initExportAndCopyMenu(p_before, p_menu, block, pos)) { + needSeparator = true; + } + + if (needSeparator) { p_menu->insertSeparator(p_before ? p_before : NULL); } } @@ -1665,3 +1676,117 @@ bool VMdEditor::initInPlacePreviewMenu(QAction *p_before, p_menu->insertAction(p_before, copyImageAct); return true; } + +bool VMdEditor::initExportAndCopyMenu(QAction *p_before, + QMenu *p_menu, + const QTextBlock &p_block, + int p_pos) +{ + Q_UNUSED(p_pos); + int state = p_block.userState(); + if (state != HighlightBlockState::CodeBlockStart + && state != HighlightBlockState::CodeBlock + && state != HighlightBlockState::CodeBlockEnd) { + return false; + } + + int blockNum = p_block.blockNumber(); + const QVector &cbs = m_pegHighlighter->getCodeBlocks(); + int idx = 0; + for (idx = 0; idx < cbs.size(); ++idx) { + if (cbs[idx].m_startBlock <= blockNum + && cbs[idx].m_endBlock >= blockNum) { + break; + } + } + + if (idx >= cbs.size()) { + return false; + } + + const VCodeBlock &cb = cbs[idx]; + if (cb.m_lang != "puml" && cb.m_lang != "dot") { + return false; + } + + QMenu *subMenu = new QMenu(tr("Copy Diagram"), p_menu); + subMenu->setToolTipsVisible(true); + + QAction *pngAct = new QAction(tr("PNG"), subMenu); + pngAct->setToolTip(tr("Export diagram as PNG to a temporary file and copy")); + connect(pngAct, &QAction::triggered, + this, [this, lang = cb.m_lang, text = cb.m_text]() { + exportDiagramAndCopy(lang, text, "png"); + }); + subMenu->addAction(pngAct); + + QAction *svgAct = new QAction(tr("SVG"), subMenu); + svgAct->setToolTip(tr("Export diagram as SVG to a temporary file and copy")); + connect(svgAct, &QAction::triggered, + this, [this, lang = cb.m_lang, text = cb.m_text]() { + exportDiagramAndCopy(lang, text, "svg"); + }); + subMenu->addAction(svgAct); + + p_menu->insertMenu(p_before, subMenu); + return true; +} + +void VMdEditor::exportDiagramAndCopy(const QString &p_lang, + const QString &p_text, + const QString &p_format) +{ + m_exportTempFile.reset(new QTemporaryFile(QDir::tempPath() + + QDir::separator() + + "XXXXXX." + p_format)); + if (!m_exportTempFile->open()) { + VUtils::showMessage(QMessageBox::Warning, + tr("Warning"), + tr("Fail to open a temporary file for export."), + "", + QMessageBox::Ok, + QMessageBox::Ok, + this); + m_exportTempFile.clear(); + return; + } + + emit m_object->statusMessage(tr("Exporting diagram")); + + QString filePath(m_exportTempFile->fileName()); + QByteArray out; + if (p_lang == "puml") { + out = VPlantUMLHelper::process(p_format, + VEditUtils::removeCodeBlockFence(p_text)); + } else if (p_lang == "dot") { + out = VGraphvizHelper::process(p_format, + VEditUtils::removeCodeBlockFence(p_text)); + } + + if (out.isEmpty() || m_exportTempFile->write(out) == -1) { + VUtils::showMessage(QMessageBox::Warning, + tr("Warning"), + tr("Fail to export diagram."), + "", + QMessageBox::Ok, + QMessageBox::Ok, + this); + } else { + QClipboard *clipboard = QApplication::clipboard(); + clipboard->clear(); + QImage img; + img.loadFromData(out, p_format.toLocal8Bit().data()); + if (!img.isNull()) { + VClipboardUtils::setImageAndLinkToClipboard(clipboard, + img, + filePath, + QClipboard::Clipboard); + emit m_object->statusMessage(tr("Diagram exported and copied")); + } else { + emit m_object->statusMessage(tr("Fail to read exported image: %1").arg(filePath)); + } + } + + m_exportTempFile->close(); + +} diff --git a/src/vmdeditor.h b/src/vmdeditor.h index 02e4f08f..9235d19f 100644 --- a/src/vmdeditor.h +++ b/src/vmdeditor.h @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include "vtextedit.h" #include "veditor.h" @@ -284,12 +286,21 @@ private: const QTextBlock &p_block, int p_pos); + bool initExportAndCopyMenu(QAction *p_before, + QMenu *p_menu, + const QTextBlock &p_block, + int p_pos); + void insertImageLink(const QString &p_text, const QString &p_url); void setFontPointSizeByStyleSheet(int p_ptSize); void setFontAndPaletteByStyleSheet(const QFont &p_font, const QPalette &p_palette); + void exportDiagramAndCopy(const QString &p_lang, + const QString &p_text, + const QString &p_format); + PegMarkdownHighlighter *m_pegHighlighter; VCodeBlockHighlightHelper *m_cbHighlighter; @@ -314,6 +325,9 @@ private: VEditTab *m_editTab; int m_copyTimeStamp; + + // Temp file used for ExportAndCopy. + QSharedPointer m_exportTempFile; }; inline PegMarkdownHighlighter *VMdEditor::getMarkdownHighlighter() const diff --git a/src/vplantumlhelper.cpp b/src/vplantumlhelper.cpp index 1d005391..f0362230 100644 --- a/src/vplantumlhelper.cpp +++ b/src/vplantumlhelper.cpp @@ -15,7 +15,19 @@ extern VConfigManager *g_config; VPlantUMLHelper::VPlantUMLHelper(QObject *p_parent) : QObject(p_parent) { - prepareCommand(m_customCmd, m_program, m_args); + m_customCmd = g_config->getPlantUMLCmd(); + if (m_customCmd.isEmpty()) { + prepareCommand(m_program, m_args); + } +} + +VPlantUMLHelper::VPlantUMLHelper(const QString &p_jar, QObject *p_parent) + : QObject(p_parent) +{ + m_customCmd = g_config->getPlantUMLCmd(); + if (m_customCmd.isEmpty()) { + prepareCommand(m_program, m_args, p_jar); + } } void VPlantUMLHelper::processAsync(int p_id, @@ -49,18 +61,13 @@ void VPlantUMLHelper::processAsync(int p_id, process->closeWriteChannel(); } -void VPlantUMLHelper::prepareCommand(QString &p_customCmd, - QString &p_program, - QStringList &p_args) const +void VPlantUMLHelper::prepareCommand(QString &p_program, + QStringList &p_args, + const QString &p_jar) const { - p_customCmd = g_config->getPlantUMLCmd(); - if (!p_customCmd.isEmpty()) { - return; - } - p_program = "java"; - p_args << "-jar" << g_config->getPlantUMLJar(); + p_args << "-jar" << (p_jar.isEmpty() ? g_config->getPlantUMLJar() : p_jar); p_args << "-charset" << "UTF-8"; int nbthread = QThread::idealThreadCount(); @@ -124,28 +131,23 @@ void VPlantUMLHelper::handleProcessFinished(int p_exitCode, QProcess::ExitStatus bool VPlantUMLHelper::testPlantUMLJar(const QString &p_jar, QString &p_msg) { - QString program("java"); - QStringList args; - args << "-jar" << p_jar; - args << "-charset" << "UTF-8"; - - const QString &dot = g_config->getGraphvizDot(); - if (!dot.isEmpty()) { - args << "-graphvizdot"; - args << dot; - } - - args << "-pipe"; + VPlantUMLHelper inst(p_jar); + QStringList args(inst.m_args); args << "-tsvg"; QString testGraph("VNote->Markdown : hello"); int exitCode = -1; QByteArray out, err; - int ret = VProcessUtils::startProcess(program, args, testGraph.toUtf8(), exitCode, out, err); + int ret = VProcessUtils::startProcess(inst.m_program, + args, + testGraph.toUtf8(), + exitCode, + out, + err); p_msg = QString("Command: %1 %2\nExitCode: %3\nOutput: %4\nError: %5") - .arg(program) + .arg(inst.m_program) .arg(args.join(' ')) .arg(exitCode) .arg(QString::fromLocal8Bit(out)) @@ -153,3 +155,36 @@ bool VPlantUMLHelper::testPlantUMLJar(const QString &p_jar, QString &p_msg) return ret == 0 && exitCode == 0; } + +QByteArray VPlantUMLHelper::process(const QString &p_format, const QString &p_text) +{ + VPlantUMLHelper inst; + + int exitCode = -1; + QByteArray out, err; + int ret = -1; + if (inst.m_customCmd.isEmpty()) { + QStringList args(inst.m_args); + args << ("-t" + p_format); + ret = VProcessUtils::startProcess(inst.m_program, + args, + p_text.toUtf8(), + exitCode, + out, + err); + } else { + QString cmd(inst.m_customCmd); + cmd.replace("%0", p_format); + ret = VProcessUtils::startProcess(cmd, + p_text.toUtf8(), + exitCode, + out, + err); + } + + if (ret != 0 || exitCode < 0) { + qWarning() << "PlantUML fail" << ret << exitCode << QString::fromLocal8Bit(err); + } + + return out; +} diff --git a/src/vplantumlhelper.h b/src/vplantumlhelper.h index 58277400..31b909ab 100644 --- a/src/vplantumlhelper.h +++ b/src/vplantumlhelper.h @@ -19,17 +19,24 @@ public: const QString &p_format, const QString &p_text); - void prepareCommand(QString &p_customCmd, QString &p_cmd, QStringList &p_args) const; - static bool testPlantUMLJar(const QString &p_jar, QString &p_msg); + static QByteArray process(const QString &p_format, const QString &p_text); + signals: - void resultReady(int p_id, TimeStamp p_timeStamp, const QString &p_format, const QString &p_result); + void resultReady(int p_id, + TimeStamp p_timeStamp, + const QString &p_format, + const QString &p_result); private slots: void handleProcessFinished(int p_exitCode, QProcess::ExitStatus p_exitStatus); private: + VPlantUMLHelper(const QString &p_jar, QObject *p_parent = nullptr); + + void prepareCommand(QString &p_cmd, QStringList &p_args, const QString &p_jar= QString()) const; + QString m_program; QStringList m_args;