diff --git a/src/dialog/vexportdialog.cpp b/src/dialog/vexportdialog.cpp index da4a581c..4961cff8 100644 --- a/src/dialog/vexportdialog.cpp +++ b/src/dialog/vexportdialog.cpp @@ -892,8 +892,8 @@ int VExportDialog::doExport(VCart *p_cart, QString *p_errMsg, QList *p_outputFiles) { + Q_UNUSED(p_cart); Q_ASSERT(p_cart); - int ret = 0; QVector files = m_cart->getFiles(); diff --git a/src/hgmarkdownhighlighter.h b/src/hgmarkdownhighlighter.h index dd3c09df..6555539e 100644 --- a/src/hgmarkdownhighlighter.h +++ b/src/hgmarkdownhighlighter.h @@ -46,12 +46,27 @@ struct HLUnitStyle // Fenced code block only. struct VCodeBlock { + // Global position of the start. int m_startPos; + int m_startBlock; int m_endBlock; + QString m_lang; QString m_text; + + bool equalContent(const VCodeBlock &p_block) const + { + return p_block.m_lang == m_lang && p_block.m_text == m_text; + } + + void updateNonContent(const VCodeBlock &p_block) + { + m_startPos = p_block.m_startPos; + m_startBlock = p_block.m_startBlock; + m_endBlock = p_block.m_endBlock; + } }; // Highlight unit with global position and string style name. diff --git a/src/resources/hoedown.js b/src/resources/hoedown.js index cbaa9a0b..b72180cc 100644 --- a/src/resources/hoedown.js +++ b/src/resources/hoedown.js @@ -1,5 +1,3 @@ -var placeholder = document.getElementById('placeholder'); - // Use Marked to highlight code blocks in edit mode. marked.setOptions({ highlight: function(code, lang) { @@ -18,7 +16,7 @@ marked.setOptions({ var updateHtml = function(html) { asyncJobsCount = 0; - placeholder.innerHTML = html; + contentDiv.innerHTML = html; insertImageCaption(); @@ -83,7 +81,7 @@ var updateHtml = function(html) { // MathJax may be not loaded for now. if (VEnableMathjax && (typeof MathJax != "undefined")) { try { - MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder, postProcessMathJax]); + MathJax.Hub.Queue(["Typeset", MathJax.Hub, contentDiv, postProcessMathJax]); } catch (err) { content.setLog("err: " + err); finishLogics(); @@ -100,7 +98,7 @@ var highlightText = function(text, id, timeStamp) { var textToHtml = function(text) { var html = marked(text); - var container = document.getElementById('text-to-html-div'); + var container = textHtmlDiv; container.innerHTML = html; html = getHtmlWithInlineStyles(container); diff --git a/src/resources/markdown-it.js b/src/resources/markdown-it.js index 049779a4..dab748f0 100644 --- a/src/resources/markdown-it.js +++ b/src/resources/markdown-it.js @@ -1,4 +1,3 @@ -var placeholder = document.getElementById('placeholder'); var nameCounter = 0; var toc = []; // Table of Content as a list @@ -110,7 +109,7 @@ var updateText = function(text) { var needToc = mdHasTocSection(text); var html = markdownToHtml(text, needToc); - placeholder.innerHTML = html; + contentDiv.innerHTML = html; handleToc(needToc); insertImageCaption(); renderMermaid('lang-mermaid'); @@ -124,7 +123,7 @@ var updateText = function(text) { // finishLoading logic. if (VEnableMathjax) { try { - MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder, postProcessMathJax]); + MathJax.Hub.Queue(["Typeset", MathJax.Hub, contentDiv, postProcessMathJax]); } catch (err) { content.setLog("err: " + err); finishLogics(); @@ -141,7 +140,7 @@ var highlightText = function(text, id, timeStamp) { var textToHtml = function(text) { var html = mdit.render(text); - var container = document.getElementById('text-to-html-div'); + var container = textHtmlDiv; container.innerHTML = html; html = getHtmlWithInlineStyles(container); diff --git a/src/resources/markdown_template.html b/src/resources/markdown_template.html index 2467a218..3b0c6c17 100644 --- a/src/resources/markdown_template.html +++ b/src/resources/markdown_template.html @@ -30,8 +30,10 @@ -
+
- + + + diff --git a/src/resources/markdown_template.js b/src/resources/markdown_template.js index 023d4df3..71d4f1e5 100644 --- a/src/resources/markdown_template.js +++ b/src/resources/markdown_template.js @@ -1,5 +1,11 @@ var channelInitialized = false; +var contentDiv = document.getElementById('content-div'); + +var previewDiv = document.getElementById('preview-div'); + +var textHtmlDiv = document.getElementById('text-html-div'); + var content; // Current header index in all headers. @@ -131,7 +137,7 @@ var styleContent = function() { }; var htmlContent = function() { - content.htmlContentCB("", styleContent(), placeholder.innerHTML); + content.htmlContentCB("", styleContent(), contentDiv.innerHTML); }; new QWebChannel(qt.webChannelTransport, @@ -157,6 +163,11 @@ new QWebChannel(qt.webChannelTransport, content.plantUMLResultReady.connect(handlePlantUMLResult); content.graphvizResultReady.connect(handleGraphvizResult); + content.requestPreviewEnabled.connect(setPreviewEnabled); + + content.requestPreviewCodeBlock.connect(previewCodeBlock); + content.requestSetPreviewContent.connect(setPreviewContent); + if (typeof updateHtml == "function") { updateHtml(content.html); content.htmlChanged.connect(updateHtml); @@ -980,7 +991,7 @@ window.onmousedown = function(e) { // Left button and Ctrl key. if (e.buttons == 1 && e.ctrlKey - && window.getSelection().rangeCount == 0) { + && window.getSelection().type != 'Range') { vds_oriMouseClientX = e.clientX; vds_oriMouseClientY = e.clientY; vds_readyToScroll = true; @@ -1267,7 +1278,7 @@ function getNodeText(el) { } var calculateWordCount = function() { - var words = getNodeText(placeholder); + var words = getNodeText(contentDiv); // Char without spaces. var cns = 0; @@ -1349,3 +1360,49 @@ var handleGraphvizResult = function(id, format, result) { finishOneAsyncJob(); }; + +var setPreviewEnabled = function(enabled) { + if (enabled) { + contentDiv.style.display = 'none'; + previewDiv.style.display = 'block'; + } else { + contentDiv.style.display = 'block'; + previewDiv.style.display = 'none'; + previewDiv.innerHTML = ''; + } +}; + +var previewCodeBlock = function(id, lang, text, isLivePreview) { + var div = previewDiv; + div.innerHTML = ''; + div.className = ''; + + if (text.length == 0 + || (lang != 'flow' + && lang != 'flowchart' + && lang != 'mermaid' + && (lang != 'puml' || VPlantUMLMode != 1))) { + return; + } + + var pre = document.createElement('pre'); + var code = document.createElement('code'); + code.textContent = text; + + pre.appendChild(code); + div.appendChild(pre); + + if (lang == 'flow' || lang == 'flowchart') { + renderFlowchartOne(code); + } else if (lang == 'mermaid') { + renderMermaidOne(code); + } else if (lang == 'puml') { + renderPlantUMLOneOnline(code); + } +}; + +var setPreviewContent = function(lang, html) { + previewDiv.innerHTML = html; + // Treat plantUML and graphviz the same. + previewDiv.classList = VPlantUMLDivClass; +}; diff --git a/src/resources/marked.js b/src/resources/marked.js index 67e0a9b3..4330d93c 100644 --- a/src/resources/marked.js +++ b/src/resources/marked.js @@ -1,4 +1,3 @@ -var placeholder = document.getElementById('placeholder'); var renderer = new marked.Renderer(); var toc = []; // Table of contents as a list var nameCounter = 0; @@ -54,7 +53,7 @@ var updateText = function(text) { var needToc = mdHasTocSection(text); var html = markdownToHtml(text, needToc); - placeholder.innerHTML = html; + contentDiv.innerHTML = html; handleToc(needToc); insertImageCaption(); renderMermaid('lang-mermaid'); @@ -68,7 +67,7 @@ var updateText = function(text) { // finishLoading logic. if (VEnableMathjax) { try { - MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder, postProcessMathJax]); + MathJax.Hub.Queue(["Typeset", MathJax.Hub, contentDiv, postProcessMathJax]); } catch (err) { content.setLog("err: " + err); finishLogics(); @@ -85,7 +84,7 @@ var highlightText = function(text, id, timeStamp) { var textToHtml = function(text) { var html = marked(text); - var container = document.getElementById('text-to-html-div'); + var container = textHtmlDiv; container.innerHTML = html; html = getHtmlWithInlineStyles(container); diff --git a/src/resources/showdown.js b/src/resources/showdown.js index 17ce9069..49ddb8f3 100644 --- a/src/resources/showdown.js +++ b/src/resources/showdown.js @@ -1,4 +1,3 @@ -var placeholder = document.getElementById('placeholder'); var renderer = new showdown.Converter({simplifiedAutoLink: 'true', excludeTrailingPunctuationFromURLs: 'true', strikethrough: 'true', @@ -94,7 +93,7 @@ var updateText = function(text) { var needToc = mdHasTocSection(text); var html = markdownToHtml(text, needToc); - placeholder.innerHTML = html; + contentDiv.innerHTML = html; handleToc(needToc); insertImageCaption(); highlightCodeBlocks(document, @@ -114,7 +113,7 @@ var updateText = function(text) { // finishLoading logic. if (VEnableMathjax) { try { - MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder, postProcessMathJax]); + MathJax.Hub.Queue(["Typeset", MathJax.Hub, contentDiv, postProcessMathJax]); } catch (err) { content.setLog("err: " + err); finishLogics(); @@ -149,7 +148,7 @@ var textToHtml = function(text) { delete parser; - var container = document.getElementById('text-to-html-div'); + var container = textHtmlDiv; container.innerHTML = html; html = getHtmlWithInlineStyles(container); diff --git a/src/resources/themes/v_moonlight/v_moonlight.css b/src/resources/themes/v_moonlight/v_moonlight.css index e4ebafdb..fbe2dc2b 100644 --- a/src/resources/themes/v_moonlight/v_moonlight.css +++ b/src/resources/themes/v_moonlight/v_moonlight.css @@ -307,3 +307,7 @@ table.hljs-ln tr td.hljs-ln-code { background-repeat: no-repeat; background-size: contain; } + +::selection { + background: #64B5F6; +} diff --git a/src/resources/themes/v_native/v_native.css b/src/resources/themes/v_native/v_native.css index da870c8b..e6384320 100644 --- a/src/resources/themes/v_native/v_native.css +++ b/src/resources/themes/v_native/v_native.css @@ -237,3 +237,7 @@ table.hljs-ln tr td.hljs-ln-numbers { table.hljs-ln tr td.hljs-ln-code { padding-left: 10px; } + +::selection { + background: #64B5F6; +} diff --git a/src/resources/themes/v_pure/v_pure.css b/src/resources/themes/v_pure/v_pure.css index 91b23f3e..b414fb90 100644 --- a/src/resources/themes/v_pure/v_pure.css +++ b/src/resources/themes/v_pure/v_pure.css @@ -307,3 +307,7 @@ table.hljs-ln tr td.hljs-ln-code { background-repeat: no-repeat; background-size: contain; } + +::selection { + background: #64B5F6; +} diff --git a/src/resources/vnote.ini b/src/resources/vnote.ini index f6834830..60e3567f 100644 --- a/src/resources/vnote.ini +++ b/src/resources/vnote.ini @@ -387,6 +387,8 @@ MagicWord=M ApplySnippet=S ; Open export dialog Export=O +; Toggle live preview +LivePreview=I [external_editors] ; Define external editors which could be called to edit notes diff --git a/src/src.pro b/src/src.pro index b6efef2a..5e648f18 100644 --- a/src/src.pro +++ b/src/src.pro @@ -127,7 +127,8 @@ SOURCES += main.cpp\ vlistfolderue.cpp \ dialog/vfixnotebookdialog.cpp \ vplantumlhelper.cpp \ - vgraphvizhelper.cpp + vgraphvizhelper.cpp \ + vlivepreviewhelper.cpp HEADERS += vmainwindow.h \ vdirectorytree.h \ @@ -245,7 +246,8 @@ HEADERS += vmainwindow.h \ vlistfolderue.h \ dialog/vfixnotebookdialog.h \ vplantumlhelper.h \ - vgraphvizhelper.h + vgraphvizhelper.h \ + vlivepreviewhelper.h RESOURCES += \ vnote.qrc \ diff --git a/src/vdocument.cpp b/src/vdocument.cpp index 62a1e171..50dc50ad 100644 --- a/src/vdocument.cpp +++ b/src/vdocument.cpp @@ -160,3 +160,21 @@ void VDocument::processGraphviz(int p_id, const QString &p_format, const QString m_graphvizHelper->processAsync(p_id, p_format, p_text); } + +void VDocument::setPreviewEnabled(bool p_enabled) +{ + emit requestPreviewEnabled(p_enabled); +} + +void VDocument::previewCodeBlock(int p_id, + const QString &p_lang, + const QString &p_text, + bool p_livePreview) +{ + emit requestPreviewCodeBlock(p_id, p_lang, p_text, p_livePreview); +} + +void VDocument::setPreviewContent(const QString &p_lang, const QString &p_html) +{ + emit requestSetPreviewContent(p_lang, p_html); +} diff --git a/src/vdocument.h b/src/vdocument.h index ebc72737..a4fbddd7 100644 --- a/src/vdocument.h +++ b/src/vdocument.h @@ -47,6 +47,20 @@ public: const VWordCountInfo &getWordCountInfo() const; + // Whether change to preview mode. + void setPreviewEnabled(bool p_enabled); + + // @p_livePreview: if true, display the result in the preview-div; otherwise, + // call previewCodeBlockCB() to pass back the result. + // Only for online parser. + void previewCodeBlock(int p_id, + const QString &p_lang, + const QString &p_text, + bool p_livePreview); + + // Set the content of the preview. + void setPreviewContent(const QString &p_lang, const QString &p_html); + public slots: // Will be called in the HTML side @@ -130,6 +144,15 @@ signals: void graphvizResultReady(int p_id, const QString &p_format, const QString &p_result); + void requestPreviewEnabled(bool p_enabled); + + void requestPreviewCodeBlock(int p_id, + const QString &p_lang, + const QString &p_text, + bool p_livePreview); + + void requestSetPreviewContent(const QString &p_lang, const QString &p_html); + private: QString m_toc; QString m_header; diff --git a/src/veditarea.cpp b/src/veditarea.cpp index a0d37f1c..44d840d6 100644 --- a/src/veditarea.cpp +++ b/src/veditarea.cpp @@ -923,6 +923,10 @@ void VEditArea::registerCaptainTargets() g_config->getCaptainShortcutKeySequence("ApplySnippet"), this, applySnippetByCaptain); + captain->registerCaptainTarget(tr("LivePreview"), + g_config->getCaptainShortcutKeySequence("LivePreview"), + this, + toggleLivePreviewByCaptain); } bool VEditArea::activateTabByCaptain(void *p_target, void *p_data, int p_idx) @@ -1085,6 +1089,19 @@ bool VEditArea::applySnippetByCaptain(void *p_target, void *p_data) return true; } +bool VEditArea::toggleLivePreviewByCaptain(void *p_target, void *p_data) +{ + Q_UNUSED(p_data); + VEditArea *obj = static_cast(p_target); + + VEditTab *tab = obj->getCurrentTab(); + if (tab) { + tab->toggleLivePreview(); + } + + return true; +} + void VEditArea::recordClosedFile(const VFileSessionInfo &p_file) { for (auto it = m_lastClosedFiles.begin(); it != m_lastClosedFiles.end(); ++it) { diff --git a/src/veditarea.h b/src/veditarea.h index 165dab70..4fd9bea0 100644 --- a/src/veditarea.h +++ b/src/veditarea.h @@ -208,6 +208,9 @@ private: // Prompt for user to apply a snippet. static bool applySnippetByCaptain(void *p_target, void *p_data); + // Toggle live preview. + static bool toggleLivePreviewByCaptain(void *p_target, void *p_data); + // End Captain mode functions. int curWindowIndex; diff --git a/src/veditor.h b/src/veditor.h index 4721100a..5d866380 100644 --- a/src/veditor.h +++ b/src/veditor.h @@ -366,6 +366,8 @@ signals: void mouseReleased(QMouseEvent *p_event); + void cursorPositionChanged(); + private slots: // Timer for find-wrap label. void labelTimerTimeout() diff --git a/src/vedittab.h b/src/vedittab.h index 6e591a89..5c1fdb84 100644 --- a/src/vedittab.h +++ b/src/vedittab.h @@ -118,6 +118,10 @@ public: // Fetch tab stat info. virtual VWordCountInfo fetchWordCountInfo(bool p_editMode) const; + virtual void toggleLivePreview() + { + } + public slots: // Enter edit mode virtual void editFile() = 0; diff --git a/src/vlivepreviewhelper.cpp b/src/vlivepreviewhelper.cpp new file mode 100644 index 00000000..fa75c27e --- /dev/null +++ b/src/vlivepreviewhelper.cpp @@ -0,0 +1,235 @@ +#include "vlivepreviewhelper.h" + +#include + +#include "veditor.h" +#include "vdocument.h" +#include "vconfigmanager.h" +#include "vgraphvizhelper.h" +#include "vplantumlhelper.h" + +extern VConfigManager *g_config; + +// Use the highest 4 bits (31-28) to indicate the lang. +#define LANG_PREFIX_GRAPHVIZ 0x10000000UL +#define LANG_PREFIX_PLANTUML 0x20000000UL +#define LANG_PREFIX_MASK 0xf0000000UL + +// Use th 27th bit to indicate the preview type. +#define TYPE_LIVE_PREVIEW 0x0UL +#define TYPE_INPLACE_PREVIEW 0x08000000UL +#define TYPE_MASK 0x08000000UL + +#define INDEX_MASK 0x00ffffffUL + +VLivePreviewHelper::VLivePreviewHelper(VEditor *p_editor, + VDocument *p_document, + QObject *p_parent) + : QObject(p_parent), + m_editor(p_editor), + m_document(p_document), + m_cbIndex(-1), + m_livePreviewEnabled(false), + m_graphvizHelper(NULL), + m_plantUMLHelper(NULL) +{ + connect(m_editor->object(), &VEditorObject::cursorPositionChanged, + this, &VLivePreviewHelper::handleCursorPositionChanged); + + m_flowchartEnabled = g_config->getEnableFlowchart(); + m_mermaidEnabled = g_config->getEnableMermaid(); + m_plantUMLMode = g_config->getPlantUMLMode(); + m_graphvizEnabled = g_config->getEnableGraphviz(); +} + +bool VLivePreviewHelper::isPreviewLang(const QString &p_lang) const +{ + return (m_flowchartEnabled && (p_lang == "flow" || p_lang == "flowchart")) + || (m_mermaidEnabled && p_lang == "mermaid") + || (m_plantUMLMode != PlantUMLMode::DisablePlantUML && p_lang == "puml") + || (m_graphvizEnabled && p_lang == "dot"); +} + +void VLivePreviewHelper::updateCodeBlocks(const QVector &p_codeBlocks) +{ + if (!m_livePreviewEnabled) { + return; + } + + int lastIndex = m_cbIndex; + m_cbIndex = -1; + int cursorBlock = m_editor->textCursorW().block().blockNumber(); + int idx = 0; + bool needUpdate = true; + int nrCached = 0; + for (auto const & cb : p_codeBlocks) { + if (!isPreviewLang(cb.m_lang)) { + continue; + } + + bool cached = false; + if (idx < m_codeBlocks.size()) { + CodeBlock &vcb = m_codeBlocks[idx]; + if (vcb.m_codeBlock.equalContent(cb)) { + vcb.m_codeBlock.updateNonContent(cb); + cached = true; + ++nrCached; + } else { + vcb.m_codeBlock = cb; + vcb.m_cachedResult.clear(); + } + } else { + m_codeBlocks.append(CodeBlock()); + m_codeBlocks[idx].m_codeBlock = cb; + } + + if (cb.m_startBlock <= cursorBlock && cb.m_endBlock >= cursorBlock) { + if (lastIndex == idx && cached) { + needUpdate = false; + } + + m_cbIndex = idx; + } + + ++idx; + } + + m_codeBlocks.resize(idx); + + qDebug() << "VLivePreviewHelper cache" << nrCached << "code blocks of" << m_codeBlocks.size(); + + if (needUpdate) { + updateLivePreview(); + } +} + +void VLivePreviewHelper::handleCursorPositionChanged() +{ + if (!m_livePreviewEnabled || m_codeBlocks.isEmpty()) { + return; + } + + int cursorBlock = m_editor->textCursorW().block().blockNumber(); + + int left = 0, right = m_codeBlocks.size() - 1; + int mid = left; + while (left <= right) { + mid = (left + right) / 2; + const CodeBlock &cb = m_codeBlocks[mid]; + + if (cb.m_codeBlock.m_startBlock <= cursorBlock && cb.m_codeBlock.m_endBlock >= cursorBlock) { + break; + } else if (cb.m_codeBlock.m_startBlock > cursorBlock) { + right = mid - 1; + } else { + left = mid + 1; + } + } + + if (left <= right) { + if (m_cbIndex != mid) { + m_cbIndex = mid; + updateLivePreview(); + } + } +} + +static QString removeFence(const QString &p_text) +{ + Q_ASSERT(p_text.startsWith("```") && p_text.endsWith("```")); + int idx = p_text.indexOf('\n') + 1; + return p_text.mid(idx, p_text.size() - idx - 3); +} + +void VLivePreviewHelper::updateLivePreview() +{ + if (m_cbIndex < 0) { + return; + } + + Q_ASSERT(!(m_cbIndex & ~INDEX_MASK)); + + const CodeBlock &cb = m_codeBlocks[m_cbIndex]; + QString text = removeFence(cb.m_codeBlock.m_text); + qDebug() << "updateLivePreview" << m_cbIndex << cb.m_codeBlock.m_lang; + + if (cb.m_codeBlock.m_lang == "dot") { + if (!m_graphvizHelper) { + m_graphvizHelper = new VGraphvizHelper(this); + connect(m_graphvizHelper, &VGraphvizHelper::resultReady, + this, &VLivePreviewHelper::localAsyncResultReady); + } + + if (cb.m_cachedResult.isEmpty()) { + m_graphvizHelper->processAsync(m_cbIndex | LANG_PREFIX_GRAPHVIZ | TYPE_LIVE_PREVIEW, + "svg", + text); + } else { + qDebug() << "use cached preview result of code block" << m_cbIndex; + m_document->setPreviewContent(cb.m_codeBlock.m_lang, cb.m_cachedResult); + } + } else if (cb.m_codeBlock.m_lang == "puml" && m_plantUMLMode == PlantUMLMode::LocalPlantUML) { + if (!m_plantUMLHelper) { + m_plantUMLHelper = new VPlantUMLHelper(this); + connect(m_plantUMLHelper, &VPlantUMLHelper::resultReady, + this, &VLivePreviewHelper::localAsyncResultReady); + } + + if (cb.m_cachedResult.isEmpty()) { + m_plantUMLHelper->processAsync(m_cbIndex | LANG_PREFIX_PLANTUML | TYPE_LIVE_PREVIEW, + "svg", + text); + } else { + qDebug() << "use cached preview result of code block" << m_cbIndex; + m_document->setPreviewContent(cb.m_codeBlock.m_lang, cb.m_cachedResult); + } + } else { + m_document->previewCodeBlock(m_cbIndex, cb.m_codeBlock.m_lang, text, true); + } +} + +void VLivePreviewHelper::setLivePreviewEnabled(bool p_enabled) +{ + if (m_livePreviewEnabled == p_enabled) { + return; + } + + m_livePreviewEnabled = p_enabled; + if (!m_livePreviewEnabled) { + m_cbIndex = -1; + m_codeBlocks.clear(); + m_document->previewCodeBlock(-1, "", "", true); + } +} + +void VLivePreviewHelper::localAsyncResultReady(int p_id, + const QString &p_format, + const QString &p_result) +{ + Q_UNUSED(p_format); + Q_ASSERT(p_format == "svg"); + int idx = p_id & INDEX_MASK; + bool livePreview = (p_id & TYPE_MASK) == TYPE_LIVE_PREVIEW; + QString lang; + switch (p_id & LANG_PREFIX_MASK) { + case LANG_PREFIX_PLANTUML: + lang = "puml"; + break; + + case LANG_PREFIX_GRAPHVIZ: + lang = "dot"; + break; + + default: + return; + } + + if (livePreview) { + if (idx != m_cbIndex) { + return; + } + + m_codeBlocks[idx].m_cachedResult = p_result; + m_document->setPreviewContent(lang, p_result); + } +} diff --git a/src/vlivepreviewhelper.h b/src/vlivepreviewhelper.h new file mode 100644 index 00000000..365eb138 --- /dev/null +++ b/src/vlivepreviewhelper.h @@ -0,0 +1,63 @@ +#ifndef VLIVEPREVIEWHELPER_H +#define VLIVEPREVIEWHELPER_H + +#include + +#include "hgmarkdownhighlighter.h" + +class VEditor; +class VDocument; +class VGraphvizHelper; +class VPlantUMLHelper; + +class VLivePreviewHelper : public QObject +{ + Q_OBJECT +public: + VLivePreviewHelper(VEditor *p_editor, + VDocument *p_document, + QObject *p_parent = nullptr); + + void updateLivePreview(); + + void setLivePreviewEnabled(bool p_enabled); + +public slots: + void updateCodeBlocks(const QVector &p_codeBlocks); + +private slots: + void handleCursorPositionChanged(); + + void localAsyncResultReady(int p_id, const QString &p_format, const QString &p_result); + +private: + bool isPreviewLang(const QString &p_lang) const; + + struct CodeBlock + { + VCodeBlock m_codeBlock; + QString m_cachedResult; + }; + + // Sorted by m_startBlock in ascending order. + QVector m_codeBlocks; + + VEditor *m_editor; + + VDocument *m_document; + + // Current previewed code block index in m_codeBlocks. + int m_cbIndex; + + bool m_flowchartEnabled; + bool m_mermaidEnabled; + int m_plantUMLMode; + bool m_graphvizEnabled; + + bool m_livePreviewEnabled; + + VGraphvizHelper *m_graphvizHelper; + VPlantUMLHelper *m_plantUMLHelper; +}; + +#endif // VLIVEPREVIEWHELPER_H diff --git a/src/vmdeditor.cpp b/src/vmdeditor.cpp index 8f42d23f..bedc34d9 100644 --- a/src/vmdeditor.cpp +++ b/src/vmdeditor.cpp @@ -38,7 +38,8 @@ VMdEditor::VMdEditor(VFile *p_file, m_mdHighlighter(NULL), m_freshEdit(true), m_textToHtmlDialog(NULL), - m_zoomDelta(0) + m_zoomDelta(0), + m_editTab(NULL) { Q_ASSERT(p_file->getDocType() == DocType::Markdown); @@ -97,6 +98,9 @@ VMdEditor::VMdEditor(VFile *p_file, connect(this, &VTextEdit::cursorPositionChanged, this, &VMdEditor::updateCurrentHeader); + connect(this, &VTextEdit::cursorPositionChanged, + m_object, &VEditorObject::cursorPositionChanged); + setDisplayScaleFactor(VUtils::calculateScaleFactor()); updateFontAndPalette(); @@ -276,10 +280,7 @@ void VMdEditor::contextMenuEvent(QContextMenuEvent *p_event) { QScopedPointer menu(createStandardContextMenu()); menu->setToolTipsVisible(true); - - VEditTab *editTab = dynamic_cast(parent()); - Q_ASSERT(editTab); - if (editTab->isEditMode()) { + if (m_editTab && m_editTab->isEditMode()) { const QList actions = menu->actions(); if (textCursor().hasSelection()) { @@ -303,9 +304,19 @@ void VMdEditor::contextMenuEvent(QContextMenuEvent *p_event) emit m_object->discardAndRead(); }); - menu->insertAction(actions.isEmpty() ? NULL : actions[0], discardExitAct); + QAction *toggleLivePreviewAct = new QAction(tr("Toggle Live Preview"), menu.data()); + toggleLivePreviewAct->setToolTip(tr("Toggle live preview of diagrams")); + connect(toggleLivePreviewAct, &QAction::triggered, + this, [this]() { + m_editTab->toggleLivePreview(); + }); + + menu->insertAction(actions.isEmpty() ? NULL : actions[0], toggleLivePreviewAct); + menu->insertAction(toggleLivePreviewAct, discardExitAct); menu->insertAction(discardExitAct, saveExitAct); + menu->insertSeparator(toggleLivePreviewAct); + if (!actions.isEmpty()) { menu->insertSeparator(actions[0]); } @@ -1322,3 +1333,8 @@ VWordCountInfo VMdEditor::fetchWordCountInfo() const info.m_charWithSpacesCount = cc; return info; } + +void VMdEditor::setEditTab(VEditTab *p_editTab) +{ + m_editTab = p_editTab; +} diff --git a/src/vmdeditor.h b/src/vmdeditor.h index 0aeb33d8..00c92b96 100644 --- a/src/vmdeditor.h +++ b/src/vmdeditor.h @@ -19,6 +19,7 @@ class VCodeBlockHighlightHelper; class VDocument; class VPreviewManager; class VCopyTextAsHtmlDialog; +class VEditTab; class VMdEditor : public VTextEdit, public VEditor { @@ -69,6 +70,10 @@ public: VWordCountInfo fetchWordCountInfo() const Q_DECL_OVERRIDE; + void setEditTab(VEditTab *p_editTab); + + HGMarkdownHighlighter *getMarkdownHighlighter() const; + public slots: bool jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) Q_DECL_OVERRIDE; @@ -260,5 +265,12 @@ private: VCopyTextAsHtmlDialog *m_textToHtmlDialog; int m_zoomDelta; + + VEditTab *m_editTab; }; + +inline HGMarkdownHighlighter *VMdEditor::getMarkdownHighlighter() const +{ + return m_mdHighlighter; +} #endif // VMDEDITOR_H diff --git a/src/vmdtab.cpp b/src/vmdtab.cpp index e7c1abcd..5bafe626 100644 --- a/src/vmdtab.cpp +++ b/src/vmdtab.cpp @@ -22,11 +22,13 @@ #include "vsnippet.h" #include "vinsertselector.h" #include "vsnippetlist.h" +#include "vlivepreviewhelper.h" extern VMainWindow *g_mainWin; extern VConfigManager *g_config; + VMdTab::VMdTab(VFile *p_file, VEditArea *p_editArea, OpenFileMode p_mode, QWidget *p_parent) : VEditTab(p_file, p_editArea, p_parent), @@ -35,7 +37,9 @@ VMdTab::VMdTab(VFile *p_file, VEditArea *p_editArea, m_document(NULL), m_mdConType(g_config->getMdConverterType()), m_enableHeadingSequence(false), - m_backupFileChecked(false) + m_backupFileChecked(false), + m_mode(Mode::InvalidMode), + m_livePreviewHelper(NULL) { V_ASSERT(m_file->getDocType() == DocType::Markdown); @@ -459,6 +463,7 @@ void VMdTab::setupMarkdownEditor() m_editor = new VMdEditor(m_file, m_document, m_mdConType, this); m_editor->setProperty("MainEditor", true); + m_editor->setEditTab(this); connect(m_editor, &VMdEditor::headersChanged, this, &VMdTab::updateOutlineFromHeaders); connect(m_editor, SIGNAL(currentHeaderChanged(int)), @@ -467,10 +472,10 @@ void VMdTab::setupMarkdownEditor() this, &VMdTab::updateStatus); connect(m_editor, &VMdEditor::textChanged, this, &VMdTab::updateStatus); - connect(m_editor, &VMdEditor::cursorPositionChanged, - this, &VMdTab::updateCursorStatus); connect(g_mainWin, &VMainWindow::editorConfigUpdated, m_editor, &VMdEditor::updateConfig); + connect(m_editor->object(), &VEditorObject::cursorPositionChanged, + this, &VMdTab::updateCursorStatus); connect(m_editor->object(), &VEditorObject::saveAndRead, this, &VMdTab::saveAndRead); connect(m_editor->object(), &VEditorObject::discardAndRead, @@ -753,9 +758,7 @@ void VMdTab::handleWebKeyPressed(int p_key, bool p_ctrl, bool p_shift) void VMdTab::zoom(bool p_zoomIn, qreal p_step) { - // Editor will handle it itself. - Q_ASSERT(!m_isEditMode); - if (!m_isEditMode) { + if (!m_isEditMode || m_mode == Mode::EditPreview) { zoomWebPage(p_zoomIn, p_step); } } @@ -1316,6 +1319,18 @@ VWordCountInfo VMdTab::fetchWordCountInfo(bool p_editMode) const void VMdTab::setCurrentMode(Mode p_mode) { + if (m_mode == p_mode) { + return; + } + + qreal factor = m_webViewer->zoomFactor(); + if (m_mode == Mode::Read) { + m_readWebViewState->m_zoomFactor = factor; + } else if (m_mode == Mode::EditPreview) { + m_previewWebViewState->m_zoomFactor = factor; + m_livePreviewHelper->setLivePreviewEnabled(false); + } + switch (p_mode) { case Mode::Read: m_webViewer->show(); @@ -1323,13 +1338,55 @@ void VMdTab::setCurrentMode(Mode p_mode) m_editor->hide(); } + if (m_readWebViewState.isNull()) { + m_readWebViewState.reset(new WebViewState()); + m_readWebViewState->m_zoomFactor = factor; + } else if (factor != m_readWebViewState->m_zoomFactor) { + m_webViewer->setZoomFactor(m_readWebViewState->m_zoomFactor); + } + + m_document->setPreviewEnabled(false); + break; + + case Mode::Edit: + m_editor->show(); + m_webViewer->hide(); break; case Mode::EditPreview: - case Mode::Edit: Q_ASSERT(m_editor); m_editor->show(); - m_webViewer->hide(); + m_webViewer->show(); + if (m_previewWebViewState.isNull()) { + m_previewWebViewState.reset(new WebViewState()); + m_previewWebViewState->m_zoomFactor = factor; + + // Init the size of two splits. + QList sizes = m_splitter->sizes(); + Q_ASSERT(sizes.size() == 2); + int a = (sizes[0] + sizes[1]) / 2; + if (a <= 0) { + a = 1; + } + + int b = (sizes[0] + sizes[1]) - a; + + QList newSizes; + newSizes.append(a); + newSizes.append(b); + m_splitter->setSizes(newSizes); + + Q_ASSERT(!m_livePreviewHelper); + m_livePreviewHelper = new VLivePreviewHelper(m_editor, m_document, this); + connect(m_editor->getMarkdownHighlighter(), &HGMarkdownHighlighter::codeBlocksUpdated, + m_livePreviewHelper, &VLivePreviewHelper::updateCodeBlocks); + } else if (factor != m_previewWebViewState->m_zoomFactor) { + m_webViewer->setZoomFactor(m_previewWebViewState->m_zoomFactor); + } + + m_document->setPreviewEnabled(true); + m_livePreviewHelper->setLivePreviewEnabled(true); + m_editor->getMarkdownHighlighter()->updateHighlight(); break; default: @@ -1340,3 +1397,19 @@ void VMdTab::setCurrentMode(Mode p_mode) focusChild(); } + +void VMdTab::toggleLivePreview() +{ + switch (m_mode) { + case Mode::EditPreview: + setCurrentMode(Mode::Edit); + break; + + case Mode::Edit: + setCurrentMode(Mode::EditPreview); + break; + + default: + break; + } +} diff --git a/src/vmdtab.h b/src/vmdtab.h index 55a948e3..cb59f70a 100644 --- a/src/vmdtab.h +++ b/src/vmdtab.h @@ -3,6 +3,7 @@ #include #include +#include #include "vedittab.h" #include "vconstants.h" #include "vmarkdownconverter.h" @@ -15,6 +16,7 @@ class VInsertSelector; class QTimer; class QWebEngineDownloadItem; class QSplitter; +class VLivePreviewHelper; class VMdTab : public VEditTab { @@ -91,6 +93,9 @@ public: // Fetch tab stat info. VWordCountInfo fetchWordCountInfo(bool p_editMode) const Q_DECL_OVERRIDE; + // Toggle live preview in edit mode. + void toggleLivePreview() Q_DECL_OVERRIDE; + public slots: // Enter edit mode. void editFile() Q_DECL_OVERRIDE; @@ -145,7 +150,12 @@ private slots: private: enum TabReady { None = 0, ReadMode = 0x1, EditMode = 0x2 }; - enum Mode { Read = 0, Edit, EditPreview }; + enum Mode { InvalidMode = 0, Read, Edit, EditPreview }; + + struct WebViewState + { + qreal m_zoomFactor; + }; // Setup UI. void setupUI(); @@ -238,6 +248,11 @@ private: VVim::SearchItem m_lastSearchItem; Mode m_mode; + + QSharedPointer m_readWebViewState; + QSharedPointer m_previewWebViewState; + + VLivePreviewHelper *m_livePreviewHelper; }; inline VMdEditor *VMdTab::getEditor()