From 79abddd802e527dd7a56ef943f78c8073c42e6b2 Mon Sep 17 00:00:00 2001 From: Le Tan Date: Fri, 28 May 2021 18:24:55 -0700 Subject: [PATCH] support local PlantUml and Graphviz (#1776) --- libs/vtextedit | 2 +- src/core/configmgr.cpp | 2 +- src/core/configmgr.h | 4 +- src/core/core.pri | 1 + src/core/fileopenparameters.h | 3 + src/core/markdowneditorconfig.cpp | 45 ++++ src/core/markdowneditorconfig.h | 20 ++ src/core/noncopyable.h | 18 ++ src/core/vnotex.h | 6 +- src/data/core/vnotex.json | 7 + .../extra/themes/moonlight/text-editor.theme | 3 + src/data/extra/web/js/graphrenderer.js | 5 +- src/data/extra/web/js/graphviz.js | 76 ++++++- src/data/extra/web/js/markdownviewer.js | 4 + src/data/extra/web/js/plantuml.js | 56 +++-- src/data/extra/web/js/vnotex.js | 16 ++ src/data/extra/web/js/vxworker.js | 5 + .../dialogs/settings/markdowneditorpage.cpp | 151 ++++++++++++- .../dialogs/settings/markdowneditorpage.h | 10 + src/widgets/editors/graphhelper.cpp | 201 ++++++++++++++++++ src/widgets/editors/graphhelper.h | 91 ++++++++ src/widgets/editors/graphvizhelper.cpp | 74 +++++++ src/widgets/editors/graphvizhelper.h | 36 ++++ src/widgets/editors/markdowneditor.cpp | 8 + src/widgets/editors/markdowneditor.h | 2 + src/widgets/editors/markdownvieweradapter.cpp | 34 +++ src/widgets/editors/markdownvieweradapter.h | 12 ++ src/widgets/editors/plantumlhelper.cpp | 104 +++++++++ src/widgets/editors/plantumlhelper.h | 41 ++++ src/widgets/editors/previewhelper.cpp | 105 ++++++++- src/widgets/editors/previewhelper.h | 16 ++ src/widgets/locationinputwithbrowsebutton.cpp | 1 + src/widgets/markdownviewwindow.cpp | 35 ++- src/widgets/markdownviewwindow.h | 4 +- src/widgets/viewarea.cpp | 10 + src/widgets/widgets.pri | 6 + 36 files changed, 1168 insertions(+), 46 deletions(-) create mode 100644 src/core/noncopyable.h create mode 100644 src/widgets/editors/graphhelper.cpp create mode 100644 src/widgets/editors/graphhelper.h create mode 100644 src/widgets/editors/graphvizhelper.cpp create mode 100644 src/widgets/editors/graphvizhelper.h create mode 100644 src/widgets/editors/plantumlhelper.cpp create mode 100644 src/widgets/editors/plantumlhelper.h diff --git a/libs/vtextedit b/libs/vtextedit index 9b9aa9dd..5e02a011 160000 --- a/libs/vtextedit +++ b/libs/vtextedit @@ -1 +1 @@ -Subproject commit 9b9aa9dd1d8ebec02daee23cb26d0b12ae00cf69 +Subproject commit 5e02a011cb7e4979e897c0670fe8ac8f1060feb4 diff --git a/src/core/configmgr.cpp b/src/core/configmgr.cpp index 7ea813b3..a1d29019 100644 --- a/src/core/configmgr.cpp +++ b/src/core/configmgr.cpp @@ -24,7 +24,7 @@ using namespace vnotex; #ifndef QT_NO_DEBUG -// #define VX_DEBUG_WEB +// #define VX_DEBUG_WEB #endif const QString ConfigMgr::c_orgName = "VNote"; diff --git a/src/core/configmgr.h b/src/core/configmgr.h index 49fee6b9..27a7d2e4 100644 --- a/src/core/configmgr.h +++ b/src/core/configmgr.h @@ -6,6 +6,8 @@ #include #include +#include "noncopyable.h" + namespace vnotex { class MainConfig; @@ -14,7 +16,7 @@ namespace vnotex class EditorConfig; class WidgetConfig; - class ConfigMgr : public QObject + class ConfigMgr : public QObject, private Noncopyable { Q_OBJECT public: diff --git a/src/core/core.pri b/src/core/core.pri index c66493a3..3590aea2 100644 --- a/src/core/core.pri +++ b/src/core/core.pri @@ -47,6 +47,7 @@ HEADERS += \ $$PWD/logger.h \ $$PWD/mainconfig.h \ $$PWD/markdowneditorconfig.h \ + $$PWD/noncopyable.h \ $$PWD/quickaccesshelper.h \ $$PWD/singleinstanceguard.h \ $$PWD/iconfig.h \ diff --git a/src/core/fileopenparameters.h b/src/core/fileopenparameters.h index f993b0d4..939d78b4 100644 --- a/src/core/fileopenparameters.h +++ b/src/core/fileopenparameters.h @@ -11,6 +11,9 @@ namespace vnotex { ViewWindowMode m_mode = ViewWindowMode::Read; + // Force to enter m_mode. + bool m_forceMode = false; + // Whether focus to the opened window. bool m_focus = true; diff --git a/src/core/markdowneditorconfig.cpp b/src/core/markdowneditorconfig.cpp index 8df20c3c..809a85c4 100644 --- a/src/core/markdowneditorconfig.cpp +++ b/src/core/markdowneditorconfig.cpp @@ -30,8 +30,15 @@ void MarkdownEditorConfig::init(const QJsonObject &p_app, const QJsonObject &p_u loadExportResource(appObj, userObj); m_webPlantUml = READBOOL(QStringLiteral("web_plantuml")); + + m_plantUmlJar = READSTR(QStringLiteral("plantuml_jar")); + + m_plantUmlCommand = READSTR(QStringLiteral("plantuml_command")); + m_webGraphviz = READBOOL(QStringLiteral("web_graphviz")); + m_graphvizExe = READSTR(QStringLiteral("graphviz_exe")); + m_prependDotInRelativeLink = READBOOL(QStringLiteral("prepend_dot_in_relative_link")); m_confirmBeforeClearObsoleteImages = READBOOL(QStringLiteral("confirm_before_clear_obsolete_images")); m_insertFileNameAsTitle = READBOOL(QStringLiteral("insert_file_name_as_title")); @@ -63,7 +70,10 @@ QJsonObject MarkdownEditorConfig::toJson() const obj[QStringLiteral("viewer_resource")] = saveViewerResource(); obj[QStringLiteral("export_resource")] = saveExportResource(); obj[QStringLiteral("web_plantuml")] = m_webPlantUml; + obj[QStringLiteral("plantuml_jar")] = m_plantUmlJar; + obj[QStringLiteral("plantuml_command")] = m_plantUmlCommand; obj[QStringLiteral("web_graphviz")] = m_webGraphviz; + obj[QStringLiteral("graphviz_exe")] = m_graphvizExe; obj[QStringLiteral("prepend_dot_in_relative_link")] = m_prependDotInRelativeLink; obj[QStringLiteral("confirm_before_clear_obsolete_images")] = m_confirmBeforeClearObsoleteImages; obj[QStringLiteral("insert_file_name_as_title")] = m_insertFileNameAsTitle; @@ -167,11 +177,46 @@ bool MarkdownEditorConfig::getWebPlantUml() const return m_webPlantUml; } +void MarkdownEditorConfig::setWebPlantUml(bool p_enabled) +{ + updateConfig(m_webPlantUml, p_enabled, this); +} + +const QString &MarkdownEditorConfig::getPlantUmlJar() const +{ + return m_plantUmlJar; +} + +void MarkdownEditorConfig::setPlantUmlJar(const QString &p_jar) +{ + updateConfig(m_plantUmlJar, p_jar, this); +} + +const QString &MarkdownEditorConfig::getPlantUmlCommand() const +{ + return m_plantUmlCommand; +} + bool MarkdownEditorConfig::getWebGraphviz() const { return m_webGraphviz; } +void MarkdownEditorConfig::setWebGraphviz(bool p_enabled) +{ + updateConfig(m_webGraphviz, p_enabled, this); +} + +const QString &MarkdownEditorConfig::getGraphvizExe() const +{ + return m_graphvizExe; +} + +void MarkdownEditorConfig::setGraphvizExe(const QString &p_exe) +{ + updateConfig(m_graphvizExe, p_exe, this); +} + bool MarkdownEditorConfig::getPrependDotInRelativeLink() const { return m_prependDotInRelativeLink; diff --git a/src/core/markdowneditorconfig.h b/src/core/markdowneditorconfig.h index 98ab75dc..2859ee4a 100644 --- a/src/core/markdowneditorconfig.h +++ b/src/core/markdowneditorconfig.h @@ -48,8 +48,18 @@ namespace vnotex const WebResource &getExportResource() const; bool getWebPlantUml() const; + void setWebPlantUml(bool p_enabled); + + const QString &getPlantUmlJar() const; + void setPlantUmlJar(const QString &p_jar); + + const QString &getPlantUmlCommand() const; bool getWebGraphviz() const; + void setWebGraphviz(bool p_enabled); + + const QString &getGraphvizExe() const; + void setGraphvizExe(const QString &p_exe); bool getPrependDotInRelativeLink() const; @@ -124,8 +134,18 @@ namespace vnotex // Whether use javascript or external program to render PlantUML. bool m_webPlantUml = true; + // File path of the JAR to render PlantUmL. + QString m_plantUmlJar; + + // Command to render PlantUml. If set, will ignore m_plantUmlJar. + // %1: the format to render in. + QString m_plantUmlCommand; + bool m_webGraphviz = true; + // Graphviz executable file. + QString m_graphvizExe; + // Whether prepend a dot in front of the relative link, like images. bool m_prependDotInRelativeLink = false; diff --git a/src/core/noncopyable.h b/src/core/noncopyable.h new file mode 100644 index 00000000..92cbfdf1 --- /dev/null +++ b/src/core/noncopyable.h @@ -0,0 +1,18 @@ +#ifndef NONCOPYABLE_H +#define NONCOPYABLE_H + +namespace vnotex +{ + class Noncopyable + { + protected: + Noncopyable() = default; + + virtual ~Noncopyable() = default; + + Noncopyable(const Noncopyable&) = delete; + Noncopyable &operator=(const Noncopyable&) = delete; + }; +} + +#endif // NONCOPYABLE_H diff --git a/src/core/vnotex.h b/src/core/vnotex.h index 7c0cfdd3..7f782be8 100644 --- a/src/core/vnotex.h +++ b/src/core/vnotex.h @@ -4,6 +4,7 @@ #include #include +#include "noncopyable.h" #include "thememgr.h" #include "global.h" @@ -18,7 +19,7 @@ namespace vnotex class Notebook; struct ComplexLocation; - class VNoteX : public QObject + class VNoteX : public QObject, private Noncopyable { Q_OBJECT public: @@ -28,9 +29,6 @@ namespace vnotex return inst; } - VNoteX(const VNoteX &) = delete; - void operator=(const VNoteX &) = delete; - // MUST be called to load some heavy data. // It is good to call it after MainWindow is shown. void initLoad(); diff --git a/src/data/core/vnotex.json b/src/data/core/vnotex.json index e3f7e9bb..7fbf05fd 100644 --- a/src/data/core/vnotex.json +++ b/src/data/core/vnotex.json @@ -261,8 +261,15 @@ }, "//comment" : "Whether use javascript or external program to render PlantUML", "web_plantuml" : true, + "//commnet" : "Local PlantUML JAR file to render PlantUML", + "plantuml_jar" : "", + "//commnet" : "Command to render PlantUML via stdin and stdout (overrides plantuml_jar)", + "//commnet" : "- %1: the format to render in", + "plantuml_command" : "", "//comment" : "Whether use javascript or external program to render Graphviz", "web_graphviz" : true, + "//commnet" : "Local Graphviz executable file to render Graphviz", + "graphviz_exe" : "", "//comment" : "Whether prepend a dot at front in relative link like images", "prepend_dot_in_relative_link" : false, "//comment" : "Whether ask for user confirmation before clearing obsolete images", diff --git a/src/data/extra/themes/moonlight/text-editor.theme b/src/data/extra/themes/moonlight/text-editor.theme index 086d81ce..8b35631a 100644 --- a/src/data/extra/themes/moonlight/text-editor.theme +++ b/src/data/extra/themes/moonlight/text-editor.theme @@ -73,6 +73,9 @@ "background-color" : "#333842", "selected-text-color" : "#e3e5e9", "selected-background-color" : "#0c7bff" + }, + "Preview" : { + "background-color" : "#b0bec5" } }, "markdown-syntax-styles" : { diff --git a/src/data/extra/web/js/graphrenderer.js b/src/data/extra/web/js/graphrenderer.js index 9712192a..a33022f6 100644 --- a/src/data/extra/web/js/graphrenderer.js +++ b/src/data/extra/web/js/graphrenderer.js @@ -30,7 +30,7 @@ class GraphRenderer extends VxWorker { registerInternal() { this.vnotex.on('basicMarkdownRendered', () => { this.reset(); - this.renderCodeNodes(this.vnotex.contentContainer); + this.renderCodeNodes(); }); this.vnotex.getWorker('markdownit').addLangsToSkipHighlight(this.langs); @@ -78,8 +78,9 @@ class GraphRenderer extends VxWorker { // Interface 2. // Get code nodes from markdownIt directly. - renderCodeNodes(p_node) { + renderCodeNodes() { this.nodesToRender = this.vnotex.getWorker('markdownit').getCodeNodes(this.langs); + this.numOfRenderedNodes = 0; this.doRender(); } diff --git a/src/data/extra/web/js/graphviz.js b/src/data/extra/web/js/graphviz.js index eab94f98..6728afe0 100644 --- a/src/data/extra/web/js/graphviz.js +++ b/src/data/extra/web/js/graphviz.js @@ -14,21 +14,30 @@ class Graphviz extends GraphRenderer { this.format = 'svg'; this.langs = ['dot']; + + this.useWeb = true; + + this.nextLocalGraphIndex = 1; } registerInternal() { this.vnotex.on('basicMarkdownRendered', () => { this.reset(); - this.renderCodeNodes(this.vnotex.contentContainer, - window.vxOptions.transformSvgToPngEnabled ? 'png' : 'svg'); + this.renderCodeNodes(window.vxOptions.transformSvgToPngEnabled ? 'png' : 'svg'); }); this.vnotex.getWorker('markdownit').addLangsToSkipHighlight(this.langs); + this.useWeb = window.vxOptions.webGraphviz; + if (!this.useWeb) { + this.extraScripts = []; + } } initialize(p_callback) { return super.initialize(() => { - this.viz = new Viz(); + if (this.useWeb) { + this.viz = new Viz(); + } p_callback(); }); } @@ -41,13 +50,22 @@ class Graphviz extends GraphRenderer { } // Interface 2. - renderCodeNodes(p_node, p_format) { + renderCodeNodes(p_format) { this.format = p_format; - super.renderCodeNodes(p_node); + super.renderCodeNodes(); } renderOne(p_node, p_idx) { + if (this.useWeb) { + this.renderOnline(p_node, p_idx); + } else { + this.renderLocal(p_node); + } + } + + renderOnline(p_node, p_idx) { + console.assert(this.viz); let func = function(p_graphviz, p_renderNode) { let graphviz = p_graphviz; let node = p_renderNode; @@ -85,13 +103,61 @@ class Graphviz extends GraphRenderer { }); } + return true; } + renderLocal(p_node) { + let func = function(p_graphviz, p_renderNode) { + let graphviz = p_graphviz; + let node = p_renderNode; + return function(format, data) { + if (node && data.length > 0) { + let obj = null; + if (format == 'svg') { + obj = document.createElement('div'); + obj.classList.add(graphviz.graphDivClass); + obj.innerHTML = data; + window.vxImageViewer.setupSVGToView(obj.children[0], false); + } else { + obj = document.createElement('div'); + obj.classList.add(graphviz.graphDivClass); + + let imgObj = document.createElement('img'); + obj.appendChild(imgObj); + imgObj.src = "data:image/" + format + ";base64, " + data; + window.vxImageViewer.setupIMGToView(imgObj); + } + + Utils.checkSourceLine(p_node, obj); + + Utils.replaceNodeWithPreCheck(p_node, obj); + } + graphviz.finishRenderingOne(); + }; + }; + + let callback = func(this, p_node); + this.vnotex.renderGraph(this.id, + this.nextLocalGraphIndex++, + this.format, + 'dot', + p_node.textContent, + function(id, index, format, data) { + callback(format, data); + }); + } + // Render a graph from @p_text in SVG format. // p_callback(svgNode). renderText(p_text, p_callback) { + console.assert(this.useWeb, "renderText() should be called only when web Graphviz is enabled"); + let func = () => { + if (!this.viz) { + console.log("viz is not ready yet"); + return; + } this.viz.renderSVGElement(p_text) .then(p_callback) .catch(function(err) { diff --git a/src/data/extra/web/js/markdownviewer.js b/src/data/extra/web/js/markdownviewer.js index e7f88d3a..10b534fb 100644 --- a/src/data/extra/web/js/markdownviewer.js +++ b/src/data/extra/web/js/markdownviewer.js @@ -47,6 +47,10 @@ new QWebChannel(qt.webChannelTransport, window.vnotex.saveContent(); }); + adapter.graphRenderDataReady.connect(function(p_id, p_index, p_format, p_data) { + window.vnotex.graphRenderDataReady(p_id, p_index, p_format, p_data); + }); + console.log('QWebChannel has been set up'); if (window.vnotex.initialized) { window.vnotex.kickOffMarkdown(); diff --git a/src/data/extra/web/js/plantuml.js b/src/data/extra/web/js/plantuml.js index 4d516338..fb981958 100644 --- a/src/data/extra/web/js/plantuml.js +++ b/src/data/extra/web/js/plantuml.js @@ -14,16 +14,24 @@ class PlantUml extends GraphRenderer { this.format = 'svg'; this.langs = ['plantuml', 'puml']; + + this.useWeb = true; + + this.nextLocalGraphIndex = 1; } registerInternal() { this.vnotex.on('basicMarkdownRendered', () => { this.reset(); - this.renderCodeNodes(this.vnotex.contentContainer, - window.vxOptions.transformSvgToPngEnabled ? 'png' : 'svg'); + this.renderCodeNodes(window.vxOptions.transformSvgToPngEnabled ? 'png' : 'svg'); }); this.vnotex.getWorker('markdownit').addLangsToSkipHighlight(this.langs); + + this.useWeb = window.vxOptions.webPlantUml; + if (!this.useWeb) { + this.extraScripts = []; + } } @@ -35,10 +43,10 @@ class PlantUml extends GraphRenderer { } // Interface 2. - renderCodeNodes(p_node, p_format) { + renderCodeNodes(p_format) { this.format = p_format; - super.renderCodeNodes(p_node); + super.renderCodeNodes(); } renderOne(p_node, p_idx) { @@ -46,20 +54,26 @@ class PlantUml extends GraphRenderer { let plantUml = p_plantUml; let node = p_node; return function(p_format, p_data) { - plantUml.handlePlantUmlResult(node, 0, p_format, p_data); + plantUml.handlePlantUmlResult(node, p_format, p_data); }; }; - this.renderOnline(this.serverUrl, - this.format, - p_node.textContent, - func(this, p_node)); + if (this.useWeb) { + this.renderOnline(this.serverUrl, + this.format, + p_node.textContent, + func(this, p_node)); + } else { + this.renderLocal(this.format, p_node.textContent, func(this, p_node)); + } return true; } // Render a graph from @p_text in SVG format. // p_callback(format, data). renderText(p_text, p_callback) { + console.assert(this.useWeb, "renderText() should be called only when web PlantUml is enabled"); + let func = () => { this.renderOnline(this.serverUrl, 'svg', @@ -111,7 +125,19 @@ class PlantUml extends GraphRenderer { return url; } - handlePlantUmlResult(p_node, p_timeStamp, p_format, p_result) { + // A helper function to render PlantUml via local JAR. + renderLocal(p_format, p_text, p_callback) { + this.vnotex.renderGraph(this.id, + this.nextLocalGraphIndex++, + p_format, + 'puml', + p_text, + function(id, index, format, data) { + p_callback(format, data); + }); + } + + handlePlantUmlResult(p_node, p_format, p_result) { if (p_node && p_result.length > 0) { let obj = null; if (p_format == 'svg') { @@ -120,9 +146,13 @@ class PlantUml extends GraphRenderer { obj.innerHTML = p_result; window.vxImageViewer.setupSVGToView(obj.children[0], false); } else { - obj = document.createElement('img'); - obj.src = "data:image/" + p_format + ";base64, " + p_result; - window.vxImageViewer.setupIMGToView(obj); + obj = document.createElement('div'); + obj.classList.add(this.graphDivClass); + + let imgObj = document.createElement('img'); + obj.appendChild(imgObj); + imgObj.src = "data:image/" + p_format + ";base64, " + p_result; + window.vxImageViewer.setupIMGToView(imgObj); } Utils.checkSourceLine(p_node, obj); diff --git a/src/data/extra/web/js/vnotex.js b/src/data/extra/web/js/vnotex.js index 325cb856..be6f391a 100644 --- a/src/data/extra/web/js/vnotex.js +++ b/src/data/extra/web/js/vnotex.js @@ -39,6 +39,9 @@ class VNoteX extends EventEmitter { this.sectionNumberBaseLevel = 2; + // Dict mapping from {id, index} to callback for renderGraph(). + this.renderGraphCallbacks = {} + window.addEventListener('load', () => { console.log('window load finished'); @@ -307,6 +310,19 @@ class VNoteX extends EventEmitter { } } + renderGraph(p_id, p_index, p_format, p_lang, p_text, p_callback) { + this.renderGraphCallbacks[p_id + '_' + p_index] = p_callback; + window.vxMarkdownAdapter.renderGraph(p_id, p_index, p_format, p_lang, p_text); + } + + graphRenderDataReady(p_id, p_index, p_format, p_data) { + let key = p_id + '_' + p_index; + if (key in this.renderGraphCallbacks) { + this.renderGraphCallbacks[key](p_id, p_index, p_format, p_data); + delete this.renderGraphCallbacks[key]; + } + } + static detectOS() { let osName="Unknown OS"; if (navigator.appVersion.indexOf("Win")!=-1) { diff --git a/src/data/extra/web/js/vxworker.js b/src/data/extra/web/js/vxworker.js index e8b84872..4beb78a7 100644 --- a/src/data/extra/web/js/vxworker.js +++ b/src/data/extra/web/js/vxworker.js @@ -3,6 +3,11 @@ class VxWorker { constructor() { this.name = ''; this.vnotex = null; + + if (!window.vxWorkerId) { + window.vxWorkerId = 1; + } + this.id = window.vxWorkerId++; } // Called when registering this worker. diff --git a/src/widgets/dialogs/settings/markdowneditorpage.cpp b/src/widgets/dialogs/settings/markdowneditorpage.cpp index 00926ddc..4c8bfb2f 100644 --- a/src/widgets/dialogs/settings/markdowneditorpage.cpp +++ b/src/widgets/dialogs/settings/markdowneditorpage.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include #include @@ -16,6 +18,10 @@ #include #include "editorpage.h" +#include +#include +#include +#include using namespace vnotex; @@ -76,6 +82,20 @@ void MarkdownEditorPage::loadInternal() m_smartTableCheckBox->setChecked(markdownConfig.getSmartTableEnabled()); m_spellCheckCheckBox->setChecked(markdownConfig.isSpellCheckEnabled()); + + { + int idx = m_plantUmlModeComboBox->findData(markdownConfig.getWebPlantUml() ? 0 : 1); + m_plantUmlModeComboBox->setCurrentIndex(idx); + } + + m_plantUmlJarFileInput->setText(markdownConfig.getPlantUmlJar()); + + { + int idx = m_graphvizModeComboBox->findData(markdownConfig.getWebGraphviz() ? 0 : 1); + m_graphvizModeComboBox->setCurrentIndex(idx); + } + + m_graphvizFileInput->setText(markdownConfig.getGraphvizExe()); } void MarkdownEditorPage::saveInternal() @@ -118,6 +138,14 @@ void MarkdownEditorPage::saveInternal() markdownConfig.setSpellCheckEnabled(m_spellCheckCheckBox->isChecked()); + markdownConfig.setWebPlantUml(m_plantUmlModeComboBox->currentData().toInt() == 0); + + markdownConfig.setPlantUmlJar(m_plantUmlJarFileInput->text()); + + markdownConfig.setWebGraphviz(m_graphvizModeComboBox->currentData().toInt() == 0); + + markdownConfig.setGraphvizExe(m_graphvizFileInput->text()); + EditorPage::notifyEditorConfigChange(); } @@ -264,7 +292,7 @@ QGroupBox *MarkdownEditorPage::setupGeneralGroup() { auto sectionLayout = new QHBoxLayout(); - m_sectionNumberComboBox = WidgetsFactory::createComboBox(this); + m_sectionNumberComboBox = WidgetsFactory::createComboBox(box); m_sectionNumberComboBox->setToolTip(tr("Section number mode")); m_sectionNumberComboBox->addItem(tr("None"), (int)MarkdownEditorConfig::SectionNumberMode::None); m_sectionNumberComboBox->addItem(tr("Read"), (int)MarkdownEditorConfig::SectionNumberMode::Read); @@ -273,7 +301,7 @@ QGroupBox *MarkdownEditorPage::setupGeneralGroup() connect(m_sectionNumberComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &MarkdownEditorPage::pageIsChanged); - m_sectionNumberBaseLevelSpinBox = WidgetsFactory::createSpinBox(this); + m_sectionNumberBaseLevelSpinBox = WidgetsFactory::createSpinBox(box); m_sectionNumberBaseLevelSpinBox->setToolTip(tr("Base level to start section numbering in edit mode")); m_sectionNumberBaseLevelSpinBox->setRange(1, 6); m_sectionNumberBaseLevelSpinBox->setSingleStep(1); @@ -281,7 +309,7 @@ QGroupBox *MarkdownEditorPage::setupGeneralGroup() connect(m_sectionNumberBaseLevelSpinBox, QOverload::of(&QSpinBox::valueChanged), this, &MarkdownEditorPage::pageIsChanged); - m_sectionNumberStyleComboBox = WidgetsFactory::createComboBox(this); + m_sectionNumberStyleComboBox = WidgetsFactory::createComboBox(box); m_sectionNumberStyleComboBox->setToolTip(tr("Section number style")); m_sectionNumberStyleComboBox->addItem(tr("1.1."), (int)MarkdownEditorConfig::SectionNumberStyle::DigDotDigDot); m_sectionNumberStyleComboBox->addItem(tr("1.1"), (int)MarkdownEditorConfig::SectionNumberStyle::DigDotDig); @@ -300,5 +328,122 @@ QGroupBox *MarkdownEditorPage::setupGeneralGroup() addSearchItem(label, m_sectionNumberComboBox->toolTip(), m_sectionNumberComboBox); } + { + m_plantUmlModeComboBox = WidgetsFactory::createComboBox(box); + m_plantUmlModeComboBox->setToolTip(tr("Use online service or local JAR file to render PlantUml graphs")); + + m_plantUmlModeComboBox->addItem(tr("Online Service"), 0); + m_plantUmlModeComboBox->addItem(tr("Local JAR"), 1); + + const QString label(tr("PlantUml:")); + layout->addRow(label, m_plantUmlModeComboBox); + addSearchItem(label, m_plantUmlModeComboBox->toolTip(), m_plantUmlModeComboBox); + connect(m_plantUmlModeComboBox, QOverload::of(&QComboBox::currentIndexChanged), + this, &MarkdownEditorPage::pageIsChanged); + } + + { + auto jarLayout = new QHBoxLayout(); + + m_plantUmlJarFileInput = new LocationInputWithBrowseButton(box); + m_plantUmlJarFileInput->setToolTip(tr("Local JAR file to render PlantUML graphs")); + connect(m_plantUmlJarFileInput, &LocationInputWithBrowseButton::clicked, + this, [this]() { + auto filePath = QFileDialog::getOpenFileName(this, + tr("Select PlantUml JAR File"), + QDir::homePath(), + "PlantUml JAR (*.jar)"); + if (!filePath.isEmpty()) { + m_plantUmlJarFileInput->setText(filePath); + } + }); + jarLayout->addWidget(m_plantUmlJarFileInput, 1); + + auto testBtn = new QPushButton(tr("Test"), box); + testBtn->setToolTip(tr("Test PlantUml JAR and Java Runtime Environment")); + connect(testBtn, &QPushButton::clicked, + this, [this]() { + const auto jar = m_plantUmlJarFileInput->text(); + if (jar.isEmpty() || !QFileInfo::exists(jar)) { + MessageBoxHelper::notify(MessageBoxHelper::Warning, + tr("The JAR file (%1) specified does not exist.").arg(jar), + this); + return; + } + + auto testRet = PlantUmlHelper::testPlantUml(jar); + MessageBoxHelper::notify(MessageBoxHelper::Information, + tr("Test %1.").arg(testRet.first ? tr("succeeded") : tr("failed")), + QString(), + testRet.second, + this); + }); + jarLayout->addWidget(testBtn); + + const QString label(tr("PlantUml JAR file:")); + layout->addRow(label, jarLayout); + addSearchItem(label, m_plantUmlJarFileInput->toolTip(), m_plantUmlJarFileInput); + connect(m_plantUmlJarFileInput, &LocationInputWithBrowseButton::textChanged, + this, &MarkdownEditorPage::pageIsChanged); + } + + { + m_graphvizModeComboBox = WidgetsFactory::createComboBox(box); + m_graphvizModeComboBox->setToolTip(tr("Use online service or local executable file to render Graphviz graphs")); + + m_graphvizModeComboBox->addItem(tr("Online Service"), 0); + m_graphvizModeComboBox->addItem(tr("Local Executable"), 1); + + const QString label(tr("Graphviz:")); + layout->addRow(label, m_graphvizModeComboBox); + addSearchItem(label, m_graphvizModeComboBox->toolTip(), m_graphvizModeComboBox); + connect(m_graphvizModeComboBox, QOverload::of(&QComboBox::currentIndexChanged), + this, &MarkdownEditorPage::pageIsChanged); + } + + { + auto fileLayout = new QHBoxLayout(); + + m_graphvizFileInput = new LocationInputWithBrowseButton(box); + m_graphvizFileInput->setToolTip(tr("Local executable file to render Graphviz graphs")); + connect(m_graphvizFileInput, &LocationInputWithBrowseButton::clicked, + this, [this]() { + auto filePath = QFileDialog::getOpenFileName(this, + tr("Select Graphviz Executable File"), + QDir::homePath()); + if (!filePath.isEmpty()) { + m_graphvizFileInput->setText(filePath); + } + }); + fileLayout->addWidget(m_graphvizFileInput, 1); + + auto testBtn = new QPushButton(tr("Test"), box); + testBtn->setToolTip(tr("Test Graphviz executable file")); + connect(testBtn, &QPushButton::clicked, + this, [this]() { + const auto exe = m_graphvizFileInput->text(); + if (exe.isEmpty() || !QFileInfo::exists(exe)) { + MessageBoxHelper::notify(MessageBoxHelper::Warning, + tr("The executable file (%1) specified does not exist.").arg(exe), + this); + return; + } + + auto testRet = GraphvizHelper::testGraphviz(exe); + MessageBoxHelper::notify(MessageBoxHelper::Information, + tr("Test %1.").arg(testRet.first ? tr("succeeded") : tr("failed")), + QString(), + testRet.second, + this); + }); + fileLayout->addWidget(testBtn); + + const QString label(tr("Graphviz executable file:")); + layout->addRow(label, fileLayout); + addSearchItem(label, m_graphvizFileInput->toolTip(), m_graphvizFileInput); + connect(m_graphvizFileInput, &LocationInputWithBrowseButton::textChanged, + this, &MarkdownEditorPage::pageIsChanged); + } + return box; } diff --git a/src/widgets/dialogs/settings/markdowneditorpage.h b/src/widgets/dialogs/settings/markdowneditorpage.h index 5dc737f2..9e1f93cb 100644 --- a/src/widgets/dialogs/settings/markdowneditorpage.h +++ b/src/widgets/dialogs/settings/markdowneditorpage.h @@ -11,6 +11,8 @@ class QComboBox; namespace vnotex { + class LocationInputWithBrowseButton; + class MarkdownEditorPage : public SettingsPage { Q_OBJECT @@ -60,6 +62,14 @@ namespace vnotex QCheckBox *m_smartTableCheckBox = nullptr; QCheckBox *m_spellCheckCheckBox = nullptr; + + QComboBox *m_plantUmlModeComboBox = nullptr; + + LocationInputWithBrowseButton *m_plantUmlJarFileInput = nullptr; + + QComboBox *m_graphvizModeComboBox = nullptr; + + LocationInputWithBrowseButton *m_graphvizFileInput = nullptr; }; } diff --git a/src/widgets/editors/graphhelper.cpp b/src/widgets/editors/graphhelper.cpp new file mode 100644 index 00000000..9a68870b --- /dev/null +++ b/src/widgets/editors/graphhelper.cpp @@ -0,0 +1,201 @@ +#include "graphhelper.h" + +#include +#include + +#include + +using namespace vnotex; + +#define TaskIdProperty "GraphTaskId" +#define TaskTimeStampProperty "GraphTaskTimeStamp" + +GraphHelper::GraphHelper() + : m_cache(100, CacheItem()) +{ +} + +QStringList GraphHelper::getArgsToUse(const QStringList &p_args) +{ + if (p_args.isEmpty()) { + return QStringList(); + } + + if (p_args[0] == "-c") { + // Combine all the arguments except the first one. + QStringList args; + args << p_args[0]; + + QString subCmd; + for (int i = 1; i < p_args.size(); ++i) { + subCmd += " " + p_args[i]; + } + args << subCmd; + + return args; + } else { + return p_args; + } +} + +void GraphHelper::process(quint64 p_id, + TimeStamp p_timeStamp, + const QString &p_format, + const QString &p_text, + const ResultCallback &p_callback) +{ + Task task; + task.m_id = p_id; + task.m_timeStamp = p_timeStamp; + task.m_format = p_format; + task.m_text = p_text; + task.m_callback = p_callback; + + m_tasks.enqueue(task); + + processOneTask(); +} + +void GraphHelper::processOneTask() +{ + if (m_taskOngoing || m_tasks.isEmpty()) { + return; + } + + m_taskOngoing = true; + + const auto &task = m_tasks.head(); + + const auto &cachedData = m_cache.get(task.m_text); + if (!cachedData.isNull() && cachedData.m_format == task.m_format) { + finishOneTask(cachedData.m_data); + return; + } + + if (!m_programValid) { + qWarning() << "program to execute for rendering is not valid" << m_program; + finishOneTask(QString()); + return; + } + + // Will be released in finishOneTask. + QProcess *process = new QProcess(); + process->setProperty(TaskIdProperty, task.m_id); + process->setProperty(TaskTimeStampProperty, task.m_timeStamp); + QObject::connect(process, QOverload::of(&QProcess::finished), + [this, process](int exitCode, QProcess::ExitStatus exitStatus) { + finishOneTask(process, exitCode, exitStatus); + }); + + if (m_overriddenCommand.isEmpty()) { + Q_ASSERT(!m_program.isEmpty()); + QStringList args(m_args); + args << getFormatArgs(task.m_format); + process->start(m_program, getArgsToUse(args)); + } else { + auto cmd = getCommandToUse(m_overriddenCommand, task.m_format); + process->start(cmd); + } + + if (process->write(task.m_text.toUtf8()) == -1) { + qWarning() << "Graph task" << task.m_id << "failed to write to process stdin:" << process->errorString(); + } + + process->closeWriteChannel(); +} + +void GraphHelper::finishOneTask(QProcess *p_process, int p_exitCode, QProcess::ExitStatus p_exitStatus) +{ + Q_ASSERT(m_taskOngoing && !m_tasks.isEmpty()); + + const auto task = m_tasks.dequeue(); + + const quint64 id = p_process->property(TaskIdProperty).toULongLong(); + const quint64 timeStamp = p_process->property(TaskTimeStampProperty).toULongLong(); + Q_ASSERT(task.m_id == id && task.m_timeStamp == timeStamp); + + qDebug() << "Graph task" << id << timeStamp << "finished"; + + bool failed = true; + if (p_exitStatus == QProcess::NormalExit) { + if (p_exitCode < 0) { + qWarning() << "Graph task" << id << "failed:" << p_exitCode; + } else { + failed = false; + const auto outBa = p_process->readAllStandardOutput(); + QString data; + if (task.m_format == QStringLiteral("svg")) { + data = QString::fromLocal8Bit(outBa); + task.m_callback(id, timeStamp, task.m_format, data); + } else { + data = QString::fromLocal8Bit(outBa.toBase64()); + task.m_callback(id, timeStamp, task.m_format, data); + } + + CacheItem item; + item.m_format = task.m_format; + item.m_data = data; + m_cache.set(task.m_text, item); + } + } else { + qWarning() << "Graph task" << id << "failed to start" << p_exitCode << p_exitStatus; + } + + const QByteArray errBa = p_process->readAllStandardError(); + if (!errBa.isEmpty()) { + QString errStr(QString::fromLocal8Bit(errBa)); + if (failed) { + qWarning() << "Graph task" << id << "stderr:" << errStr; + } else { + qDebug() << "Graph task" << id << "stderr:" << errStr; + } + } + + if (failed) { + task.m_callback(id, task.m_timeStamp, task.m_format, QString()); + } + + p_process->deleteLater(); + + m_taskOngoing = false; + processOneTask(); +} + +void GraphHelper::finishOneTask(const QString &p_data) +{ + Q_ASSERT(m_taskOngoing && !m_tasks.isEmpty()); + + const auto task = m_tasks.dequeue(); + + qDebug() << "Graph task" << task.m_id << task.m_timeStamp << "finished by cache" << p_data.size(); + + task.m_callback(task.m_id, task.m_timeStamp, task.m_format, p_data); + + m_taskOngoing = false; + processOneTask(); +} + +QString GraphHelper::getCommandToUse(const QString &p_command, const QString &p_format) +{ + auto cmd(p_command); + cmd.replace("%1", p_format); + return cmd; +} + +void GraphHelper::clearCache() +{ + m_cache.clear(); +} + +void GraphHelper::checkValidProgram() +{ + m_programValid = true; + if (m_overriddenCommand.isEmpty()) { + if (m_program.isEmpty()) { + m_programValid = false; + } else { + QFileInfo finfo(m_program); + m_programValid = !finfo.isAbsolute() || finfo.isExecutable(); + } + } +} diff --git a/src/widgets/editors/graphhelper.h b/src/widgets/editors/graphhelper.h new file mode 100644 index 00000000..6ebf7b06 --- /dev/null +++ b/src/widgets/editors/graphhelper.h @@ -0,0 +1,91 @@ +#ifndef GRAPHHELPER_H +#define GRAPHHELPER_H + +#include +#include +#include +#include + +#include +#include +#include + +namespace vnotex +{ + class GraphHelper : private Noncopyable + { + public: + typedef std::function ResultCallback; + + GraphHelper(); + + void process(quint64 p_id, + TimeStamp p_timeStamp, + const QString &p_format, + const QString &p_text, + const ResultCallback &p_callback); + + protected: + virtual QStringList getFormatArgs(const QString &p_format) = 0; + + void clearCache(); + + void checkValidProgram(); + + static QStringList getArgsToUse(const QStringList &p_args); + + static QString getCommandToUse(const QString &p_command, + const QString &p_format); + + QString m_program; + + QStringList m_args; + + // If this is not empty, @m_program and @m_args will be ignored. + QString m_overriddenCommand; + + private: + struct Task + { + quint64 m_id = 0; + + TimeStamp m_timeStamp = 0; + + QString m_format; + + QString m_text; + + ResultCallback m_callback; + }; + + struct CacheItem + { + bool isNull() const + { + return m_data.isNull(); + } + + QString m_format; + + QString m_data; + }; + + void processOneTask(); + + void finishOneTask(QProcess *p_process, int p_exitCode, QProcess::ExitStatus p_exitStatus); + + void finishOneTask(const QString &p_data); + + QQueue m_tasks; + + bool m_taskOngoing = false; + + // {text} -> CacheItem. + vte::LruCache m_cache; + + // Whether @m_program is valid. + bool m_programValid = true; + }; +} + +#endif // GRAPHHELPER_H diff --git a/src/widgets/editors/graphvizhelper.cpp b/src/widgets/editors/graphvizhelper.cpp new file mode 100644 index 00000000..28149997 --- /dev/null +++ b/src/widgets/editors/graphvizhelper.cpp @@ -0,0 +1,74 @@ +#include "graphvizhelper.h" + +#include + +#include + +using namespace vnotex; + +void GraphvizHelper::init(const QString &p_graphvizFile) +{ + if (m_initialized) { + return; + } + + m_initialized = true; + + update(p_graphvizFile); +} + +void GraphvizHelper::update(const QString &p_graphvizFile) +{ + if (!m_initialized) { + return; + } + + prepareProgramAndArgs(p_graphvizFile, m_program, m_args); + + checkValidProgram(); + + clearCache(); +} + +void GraphvizHelper::prepareProgramAndArgs(const QString &p_graphvizFile, + QString &p_program, + QStringList &p_args) +{ + p_program = p_graphvizFile.isEmpty() ? QStringLiteral("dot") : p_graphvizFile; + p_args.clear(); +} + +QPair GraphvizHelper::testGraphviz(const QString &p_graphvizFile) +{ + auto ret = qMakePair(false, QString()); + + QString program; + QStringList args; + prepareProgramAndArgs(p_graphvizFile, program, args); + args << "-Tsvg"; + + const QString testGraph("digraph G {VNote->Markdown}"); + + int exitCode = -1; + QByteArray outData; + QByteArray errData; + auto state = ProcessUtils::start(program, + args, + testGraph.toUtf8(), + exitCode, + outData, + errData); + ret.first = (state == ProcessUtils::Succeeded) && (exitCode == 0); + + ret.second = QString("%1 %2\n\nExitcode: %3\n\nOutput: %4\n\nError: %5") + .arg(program, args.join(' '), QString::number(exitCode), QString::fromLocal8Bit(outData), QString::fromLocal8Bit(errData)); + + return ret; +} + +QStringList GraphvizHelper::getFormatArgs(const QString &p_format) +{ + QStringList args; + args << ("-T" + p_format); + return args; +} diff --git a/src/widgets/editors/graphvizhelper.h b/src/widgets/editors/graphvizhelper.h new file mode 100644 index 00000000..2a081218 --- /dev/null +++ b/src/widgets/editors/graphvizhelper.h @@ -0,0 +1,36 @@ +#ifndef GRAPHVIZHELPER_H +#define GRAPHVIZHELPER_H + +#include "graphhelper.h" + +namespace vnotex +{ + class GraphvizHelper : public GraphHelper + { + public: + void init(const QString &p_graphvizFile); + + void update(const QString &p_graphvizFile); + + static GraphvizHelper &getInst() + { + static GraphvizHelper inst; + return inst; + } + + static QPair testGraphviz(const QString &p_graphvizFile); + + private: + GraphvizHelper() = default; + + QStringList getFormatArgs(const QString &p_format) Q_DECL_OVERRIDE; + + static void prepareProgramAndArgs(const QString &p_graphvizFile, + QString &p_program, + QStringList &p_args); + + bool m_initialized = false; + }; +} + +#endif // GRAPHVIZHELPER_H diff --git a/src/widgets/editors/markdowneditor.cpp b/src/widgets/editors/markdowneditor.cpp index 98011325..b001343c 100644 --- a/src/widgets/editors/markdowneditor.cpp +++ b/src/widgets/editors/markdowneditor.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -1274,3 +1275,10 @@ void MarkdownEditor::setupTableHelper() 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; +} diff --git a/src/widgets/editors/markdowneditor.h b/src/widgets/editors/markdowneditor.h index a5f315d7..65c94c48 100644 --- a/src/widgets/editors/markdowneditor.h +++ b/src/widgets/editors/markdowneditor.h @@ -100,6 +100,8 @@ namespace vnotex void updateFromConfig(bool p_initialized = true); + QRgb getPreviewBackground() const; + public slots: void handleHtmlToMarkdownData(quint64 p_id, TimeStamp p_timeStamp, const QString &p_text); diff --git a/src/widgets/editors/markdownvieweradapter.cpp b/src/widgets/editors/markdownvieweradapter.cpp index c37d8a4d..44a75b0f 100644 --- a/src/widgets/editors/markdownvieweradapter.cpp +++ b/src/widgets/editors/markdownvieweradapter.cpp @@ -4,6 +4,8 @@ #include #include "../outlineprovider.h" +#include "plantumlhelper.h" +#include "graphvizhelper.h" using namespace vnotex; @@ -371,3 +373,35 @@ void MarkdownViewerAdapter::reset() m_currentHeadingIndex = -1; m_crossCopyTargets.clear(); } + +void MarkdownViewerAdapter::renderGraph(quint64 p_id, + quint64 p_index, + const QString &p_format, + const QString &p_lang, + const QString &p_text) +{ + if (p_text.isEmpty()) { + emit graphRenderDataReady(p_id, p_index, p_format, QString()); + return; + } + + if (p_lang == QStringLiteral("puml")) { + PlantUmlHelper::getInst().process(p_id, + p_index, + p_format, + p_text, + [this](quint64 id, TimeStamp timeStamp, const QString &format, const QString &data) { + emit graphRenderDataReady(id, timeStamp, format, data); + }); + } else if (p_lang == QStringLiteral("dot")) { + GraphvizHelper::getInst().process(p_id, + p_index, + p_format, + p_text, + [this](quint64 id, TimeStamp timeStamp, const QString &format, const QString &data) { + emit graphRenderDataReady(id, timeStamp, format, data); + }); + } else { + Q_ASSERT(false); + } +} diff --git a/src/widgets/editors/markdownvieweradapter.h b/src/widgets/editors/markdownvieweradapter.h index 5c7d27bc..7007f66e 100644 --- a/src/widgets/editors/markdownvieweradapter.h +++ b/src/widgets/editors/markdownvieweradapter.h @@ -172,6 +172,13 @@ namespace vnotex void setSavedContent(const QString &p_headContent, const QString &p_styleContent, const QString &p_content, const QString &p_bodyClassList); + // Call local CPP code to render graph. + void renderGraph(quint64 p_id, + quint64 p_index, + const QString &p_format, + const QString &p_lang, + const QString &p_text); + // Signals to be connected at web side. signals: // Current Markdown text is updated. @@ -208,6 +215,11 @@ namespace vnotex // Request to get the whole HTML content. void contentRequested(); + void graphRenderDataReady(quint64 p_id, + quint64 p_index, + const QString &p_format, + const QString &p_data); + // Signals to be connected at cpp side. signals: void graphPreviewDataReady(const PreviewData &p_data); diff --git a/src/widgets/editors/plantumlhelper.cpp b/src/widgets/editors/plantumlhelper.cpp new file mode 100644 index 00000000..eec580c4 --- /dev/null +++ b/src/widgets/editors/plantumlhelper.cpp @@ -0,0 +1,104 @@ +#include "plantumlhelper.h" + +#include + +#include + +using namespace vnotex; + +void PlantUmlHelper::init(const QString &p_plantUmlJarFile, + const QString &p_graphvizFile, + const QString &p_overriddenCommand) +{ + if (m_initialized) { + return; + } + + m_initialized = true; + + update(p_plantUmlJarFile, p_graphvizFile, p_overriddenCommand); +} + +void PlantUmlHelper::update(const QString &p_plantUmlJarFile, + const QString &p_graphvizFile, + const QString &p_overriddenCommand) +{ + if (!m_initialized) { + return; + } + + m_overriddenCommand = p_overriddenCommand; + if (m_overriddenCommand.isEmpty()) { + prepareProgramAndArgs(p_plantUmlJarFile, p_graphvizFile, m_program, m_args); + } else { + m_program.clear(); + m_args.clear(); + } + + checkValidProgram(); + + clearCache(); +} + +void PlantUmlHelper::prepareProgramAndArgs(const QString &p_plantUmlJarFile, + const QString &p_graphvizFile, + QString &p_program, + QStringList &p_args) +{ +#if defined(Q_OS_WIN) + p_program = "java"; +#else + p_program = "/bin/sh"; + p_args << "-c"; + p_args << "java"; +#endif + + p_args << "-Djava.awt.headless=true"; + + p_args << "-jar" << p_plantUmlJarFile; + + p_args << "-charset" << "UTF-8"; + + if (!p_graphvizFile.isEmpty()) { + p_args << "-graphvizdot" << p_graphvizFile; + } + + p_args << "-pipe"; +} + +QPair PlantUmlHelper::testPlantUml(const QString &p_plantUmlJarFile) +{ + auto ret = qMakePair(false, QString()); + + QString program; + QStringList args; + prepareProgramAndArgs(p_plantUmlJarFile, QString(), program, args); + + args << "-tsvg"; + args = getArgsToUse(args); + + const QString testGraph("VNote->Markdown : Hello"); + + int exitCode = -1; + QByteArray outData; + QByteArray errData; + auto state = ProcessUtils::start(program, + args, + testGraph.toUtf8(), + exitCode, + outData, + errData); + ret.first = (state == ProcessUtils::Succeeded) && (exitCode == 0); + + ret.second = QString("%1 %2\n\nExitcode: %3\n\nOutput: %4\n\nError: %5") + .arg(program, args.join(' '), QString::number(exitCode), QString::fromLocal8Bit(outData), QString::fromLocal8Bit(errData)); + + return ret; +} + +QStringList PlantUmlHelper::getFormatArgs(const QString &p_format) +{ + QStringList args; + args << ("-t" + p_format); + return args; +} diff --git a/src/widgets/editors/plantumlhelper.h b/src/widgets/editors/plantumlhelper.h new file mode 100644 index 00000000..67aebafe --- /dev/null +++ b/src/widgets/editors/plantumlhelper.h @@ -0,0 +1,41 @@ +#ifndef PLANTUMLHELPER_H +#define PLANTUMLHELPER_H + +#include "graphhelper.h" + +namespace vnotex +{ + class PlantUmlHelper : public GraphHelper + { + public: + void init(const QString &p_plantUmlJarFile, + const QString &p_graphvizFile, + const QString &p_overriddenCommand); + + void update(const QString &p_plantUmlJarFile, + const QString &p_graphvizFile, + const QString &p_overriddenCommand); + + static PlantUmlHelper &getInst() + { + static PlantUmlHelper inst; + return inst; + } + + static QPair testPlantUml(const QString &p_plantUmlJarFile); + + private: + PlantUmlHelper() = default; + + QStringList getFormatArgs(const QString &p_format) Q_DECL_OVERRIDE; + + static void prepareProgramAndArgs(const QString &p_plantUmlJarFile, + const QString &p_graphvizFile, + QString &p_program, + QStringList &p_args); + + bool m_initialized = false; + }; +} + +#endif // PLANTUMLHELPER_H diff --git a/src/widgets/editors/previewhelper.cpp b/src/widgets/editors/previewhelper.cpp index b0671391..3e7116b0 100644 --- a/src/widgets/editors/previewhelper.cpp +++ b/src/widgets/editors/previewhelper.cpp @@ -12,6 +12,8 @@ #include #include "markdowneditor.h" +#include "plantumlhelper.h" +#include "graphvizhelper.h" using namespace vnotex; @@ -134,9 +136,11 @@ void PreviewHelper::codeBlocksUpdated(vte::TimeStamp p_timeStamp, ++m_codeBlockTimeStamp; m_codeBlocksData.clear(); - bool needUpdateEditorInplacePreview = true; + QVector needPreviewBlocks; + + for (int i = 0; i < p_codeBlocks.size(); ++i) { + const auto &cb = p_codeBlocks[i]; - for (const auto &cb : p_codeBlocks) { const auto needPreview = isLangNeedPreview(cb.m_lang); if (!needPreview.first && !needPreview.second) { continue; @@ -158,16 +162,16 @@ void PreviewHelper::codeBlocksUpdated(vte::TimeStamp p_timeStamp, } if (m_inplacePreviewEnabled && needPreview.first && !cacheHit) { - // No need to update in-place preview for now. - needUpdateEditorInplacePreview = false; m_codeBlocksData[blockPreviewIdx].m_text = cb.m_text; - inplacePreviewCodeBlock(blockPreviewIdx); + needPreviewBlocks.push_back(blockPreviewIdx); } } - if (needUpdateEditorInplacePreview) { - updateEditorInplacePreviewCodeBlock(); + for (auto idx : needPreviewBlocks) { + inplacePreviewCodeBlock(idx); } + + updateEditorInplacePreviewCodeBlock(); } bool PreviewHelper::checkPreviewSourceLang(SourceFlag p_flag, const QString &p_lang) const @@ -221,13 +225,38 @@ void PreviewHelper::inplacePreviewCodeBlock(int p_blockPreviewIdx) if (checkPreviewSourceLang(SourceFlag::FlowChart, blockData.m_lang) || checkPreviewSourceLang(SourceFlag::WaveDrom, blockData.m_lang) || checkPreviewSourceLang(SourceFlag::Mermaid, blockData.m_lang) - || checkPreviewSourceLang(SourceFlag::PlantUml, blockData.m_lang) - || checkPreviewSourceLang(SourceFlag::Graphviz, blockData.m_lang) + || (checkPreviewSourceLang(SourceFlag::PlantUml, blockData.m_lang) && m_webPlantUmlEnabled) + || (checkPreviewSourceLang(SourceFlag::Graphviz, blockData.m_lang) && m_webGraphvizEnabled) || checkPreviewSourceLang(SourceFlag::Math, blockData.m_lang)) { emit graphPreviewRequested(p_blockPreviewIdx, m_codeBlockTimeStamp, blockData.m_lang, TextUtils::removeCodeBlockFence(blockData.m_text)); + return; + } + + if (!m_webPlantUmlEnabled && checkPreviewSourceLang(SourceFlag::PlantUml, blockData.m_lang)) { + // Local PlantUml. + PlantUmlHelper::getInst().process(static_cast(p_blockPreviewIdx), + m_codeBlockTimeStamp, + QStringLiteral("svg"), + TextUtils::removeCodeBlockFence(blockData.m_text), + [this](quint64 id, TimeStamp timeStamp, const QString &format, const QString &data) { + handleLocalData(id, timeStamp, format, data, true); + }); + return; + } + + if (!m_webGraphvizEnabled && checkPreviewSourceLang(SourceFlag::Graphviz, blockData.m_lang)) { + // Local PlantUml. + GraphvizHelper::getInst().process(static_cast(p_blockPreviewIdx), + m_codeBlockTimeStamp, + QStringLiteral("svg"), + TextUtils::removeCodeBlockFence(blockData.m_text), + [this](quint64 id, TimeStamp timeStamp, const QString &format, const QString &data) { + handleLocalData(id, timeStamp, format, data, false); + }); + return; } } @@ -242,10 +271,11 @@ void PreviewHelper::handleGraphPreviewData(const MarkdownViewerAdapter::PreviewD } auto &blockData = m_codeBlocksData[p_data.m_id]; + const bool forcedBackground = needForcedBackground(blockData.m_lang); auto previewData = QSharedPointer::create(p_data.m_timeStamp, p_data.m_format, p_data.m_data, - 0, + forcedBackground ? m_editor->getPreviewBackground() : 0, p_data.m_needScale ? getEditorScaleFactor() : 1); m_codeBlockCache.set(blockData.m_text, previewData); blockData.m_text.clear(); @@ -414,3 +444,58 @@ qreal PreviewHelper::getEditorScaleFactor() const return 1; } + +void PreviewHelper::setWebPlantUmlEnabled(bool p_enabled) +{ + m_webPlantUmlEnabled = p_enabled; +} + +void PreviewHelper::setWebGraphvizEnabled(bool p_enabled) +{ + m_webGraphvizEnabled = p_enabled; +} + +void PreviewHelper::handleLocalData(quint64 p_id, + TimeStamp p_timeStamp, + const QString &p_format, + const QString &p_data, + bool p_forcedBackground) +{ + if (p_timeStamp != m_codeBlockTimeStamp) { + return; + } + + Q_UNUSED(p_format); + Q_ASSERT(p_format == QStringLiteral("svg")); + + if (p_id >= static_cast(m_codeBlocksData.size()) || p_data.isEmpty()) { + updateEditorInplacePreviewCodeBlock(); + return; + } + + auto &blockData = m_codeBlocksData[p_id]; + auto previewData = QSharedPointer::create(p_timeStamp, + p_format, + p_data.toUtf8(), + p_forcedBackground ? m_editor->getPreviewBackground() : 0, + getEditorScaleFactor()); + m_codeBlockCache.set(blockData.m_text, previewData); + blockData.m_text.clear(); + + blockData.updateInplacePreview(m_document, + previewData->m_image, + previewData->m_name, + previewData->m_background, + m_tabStopWidth); + + updateEditorInplacePreviewCodeBlock(); +} + +bool PreviewHelper::needForcedBackground(const QString &p_lang) const +{ + if (checkPreviewSourceLang(SourceFlag::PlantUml, p_lang)) { + return true; + } + + return false; +} diff --git a/src/widgets/editors/previewhelper.h b/src/widgets/editors/previewhelper.h index 7f6b693f..d539c751 100644 --- a/src/widgets/editors/previewhelper.h +++ b/src/widgets/editors/previewhelper.h @@ -47,6 +47,10 @@ namespace vnotex void setMarkdownEditor(MarkdownEditor *p_editor); + void setWebPlantUmlEnabled(bool p_enabled); + + void setWebGraphvizEnabled(bool p_enabled); + public slots: void codeBlocksUpdated(vte::TimeStamp p_timeStamp, const QVector &p_codeBlocks); @@ -180,8 +184,16 @@ namespace vnotex void updateEditorInplacePreviewMathBlock(); + void handleLocalData(quint64 p_id, + TimeStamp p_timeStamp, + const QString &p_format, + const QString &p_data, + bool p_forcedBackground); + qreal getEditorScaleFactor() const; + bool needForcedBackground(const QString &p_lang) const; + MarkdownEditor *m_editor = nullptr; QTextDocument *m_document = nullptr; @@ -213,6 +225,10 @@ namespace vnotex vte::LruCache> m_codeBlockCache; vte::LruCache> m_mathBlockCache; + + bool m_webPlantUmlEnabled = true; + + bool m_webGraphvizEnabled = true; }; } diff --git a/src/widgets/locationinputwithbrowsebutton.cpp b/src/widgets/locationinputwithbrowsebutton.cpp index 256fd04a..b466b679 100644 --- a/src/widgets/locationinputwithbrowsebutton.cpp +++ b/src/widgets/locationinputwithbrowsebutton.cpp @@ -12,6 +12,7 @@ LocationInputWithBrowseButton::LocationInputWithBrowseButton(QWidget *p_parent) : QWidget(p_parent) { auto layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); m_lineEdit = WidgetsFactory::createLineEdit(this); layout->addWidget(m_lineEdit, 1); diff --git a/src/widgets/markdownviewwindow.cpp b/src/widgets/markdownviewwindow.cpp index fc69be25..44c87642 100644 --- a/src/widgets/markdownviewwindow.cpp +++ b/src/widgets/markdownviewwindow.cpp @@ -29,6 +29,8 @@ #include "toolbarhelper.h" #include "findandreplacewidget.h" #include "editors/statuswidget.h" +#include "editors/plantumlhelper.h" +#include "editors/graphvizhelper.h" using namespace vnotex; @@ -40,7 +42,7 @@ MarkdownViewWindow::MarkdownViewWindow(QWidget *p_parent) setupUI(); - m_previewHelper = new PreviewHelper(nullptr, this); + setupPreviewHelper(); } MarkdownViewWindow::~MarkdownViewWindow() @@ -176,7 +178,12 @@ void MarkdownViewWindow::handleEditorConfigChange() if (markdownEditorConfig.revision() != m_markdownEditorConfigRevision) { m_markdownEditorConfigRevision = markdownEditorConfig.revision(); + + m_previewHelper->setWebPlantUmlEnabled(markdownEditorConfig.getWebPlantUml()); + m_previewHelper->setWebGraphvizEnabled(markdownEditorConfig.getWebGraphviz()); + HtmlTemplateHelper::updateMarkdownViewerTemplate(markdownEditorConfig); + if (m_editor) { auto config = createMarkdownEditorConfig(markdownEditorConfig); m_editor->setConfig(config); @@ -236,7 +243,7 @@ void MarkdownViewWindow::handleBufferChangedInternal(const QSharedPointersetReplaceEnabled(!isReadMode()); } -void MarkdownViewWindow::handleFileOpenParameters(const QSharedPointer &p_paras) +void MarkdownViewWindow::handleFileOpenParameters(const QSharedPointer &p_paras, bool p_twice) { if (!p_paras) { return; @@ -905,7 +912,9 @@ void MarkdownViewWindow::handleFileOpenParameters(const QSharedPointerinsertText(title); } } else { - setMode(p_paras->m_mode); + if (!p_twice || p_paras->m_forceMode) { + setMode(p_paras->m_mode); + } scrollToLine(p_paras->m_lineNumber); } @@ -934,7 +943,7 @@ bool MarkdownViewWindow::isReadMode() const void MarkdownViewWindow::openTwice(const QSharedPointer &p_paras) { Q_ASSERT(!p_paras || !p_paras->m_newFile); - handleFileOpenParameters(p_paras); + handleFileOpenParameters(p_paras, true); } ViewWindowSession MarkdownViewWindow::saveSession() const @@ -946,3 +955,19 @@ ViewWindowSession MarkdownViewWindow::saveSession() const } return session; } + +void MarkdownViewWindow::setupPreviewHelper() +{ + Q_ASSERT(!m_previewHelper); + + m_previewHelper = new PreviewHelper(nullptr, this); + + const auto &markdownEditorConfig = ConfigMgr::getInst().getEditorConfig().getMarkdownEditorConfig(); + m_previewHelper->setWebPlantUmlEnabled(markdownEditorConfig.getWebPlantUml()); + m_previewHelper->setWebGraphvizEnabled(markdownEditorConfig.getWebGraphviz()); + + PlantUmlHelper::getInst().init(markdownEditorConfig.getPlantUmlJar(), + markdownEditorConfig.getGraphvizExe(), + markdownEditorConfig.getPlantUmlCommand()); + GraphvizHelper::getInst().init(markdownEditorConfig.getGraphvizExe()); +} diff --git a/src/widgets/markdownviewwindow.h b/src/widgets/markdownviewwindow.h index 9f4efd0e..28b7b7ed 100644 --- a/src/widgets/markdownviewwindow.h +++ b/src/widgets/markdownviewwindow.h @@ -97,6 +97,8 @@ namespace vnotex void setupViewer(); + void setupPreviewHelper(); + void syncTextEditorFromBuffer(bool p_syncPositionFromReadMode); void syncViewerFromBuffer(bool p_syncPositionFromEditMode); @@ -131,7 +133,7 @@ namespace vnotex void setModeInternal(ViewWindowMode p_mode, bool p_syncBuffer); - void handleFileOpenParameters(const QSharedPointer &p_paras); + void handleFileOpenParameters(const QSharedPointer &p_paras, bool p_twice); void scrollToLine(int p_lineNumber); diff --git a/src/widgets/viewarea.cpp b/src/widgets/viewarea.cpp index eb0e113f..a795677f 100644 --- a/src/widgets/viewarea.cpp +++ b/src/widgets/viewarea.cpp @@ -22,10 +22,14 @@ #include #include #include +#include +#include #include #include #include #include +#include "editors/plantumlhelper.h" +#include "editors/graphvizhelper.h" using namespace vnotex; @@ -87,6 +91,12 @@ ViewArea::ViewArea(QWidget *p_parent) p_win->handleEditorConfigChange(); return true; }); + + const auto &markdownEditorConfig = ConfigMgr::getInst().getEditorConfig().getMarkdownEditorConfig(); + PlantUmlHelper::getInst().update(markdownEditorConfig.getPlantUmlJar(), + markdownEditorConfig.getGraphvizExe(), + markdownEditorConfig.getPlantUmlCommand()); + GraphvizHelper::getInst().update(markdownEditorConfig.getGraphvizExe()); }); m_fileCheckTimer = new QTimer(this); diff --git a/src/widgets/widgets.pri b/src/widgets/widgets.pri index 76c857a2..7fbd6626 100644 --- a/src/widgets/widgets.pri +++ b/src/widgets/widgets.pri @@ -28,11 +28,14 @@ SOURCES += \ $$PWD/dialogs/tableinsertdialog.cpp \ $$PWD/dragdropareaindicator.cpp \ $$PWD/editors/editormarkdownvieweradapter.cpp \ + $$PWD/editors/graphhelper.cpp \ + $$PWD/editors/graphvizhelper.cpp \ $$PWD/editors/markdowneditor.cpp \ $$PWD/editors/markdowntable.cpp \ $$PWD/editors/markdowntablehelper.cpp \ $$PWD/editors/markdownviewer.cpp \ $$PWD/editors/markdownvieweradapter.cpp \ + $$PWD/editors/plantumlhelper.cpp \ $$PWD/editors/previewhelper.cpp \ $$PWD/editors/statuswidget.cpp \ $$PWD/editors/texteditor.cpp \ @@ -123,11 +126,14 @@ HEADERS += \ $$PWD/dialogs/tableinsertdialog.h \ $$PWD/dragdropareaindicator.h \ $$PWD/editors/editormarkdownvieweradapter.h \ + $$PWD/editors/graphhelper.h \ + $$PWD/editors/graphvizhelper.h \ $$PWD/editors/markdowneditor.h \ $$PWD/editors/markdowntable.h \ $$PWD/editors/markdowntablehelper.h \ $$PWD/editors/markdownviewer.h \ $$PWD/editors/markdownvieweradapter.h \ + $$PWD/editors/plantumlhelper.h \ $$PWD/editors/previewhelper.h \ $$PWD/editors/statuswidget.h \ $$PWD/editors/texteditor.h \