VMdTab: live preview

This commit is contained in:
Le Tan 2018-04-07 15:23:11 +08:00
parent 051088be31
commit c6b7561864
25 changed files with 606 additions and 40 deletions

View File

@ -892,8 +892,8 @@ int VExportDialog::doExport(VCart *p_cart,
QString *p_errMsg, QString *p_errMsg,
QList<QString> *p_outputFiles) QList<QString> *p_outputFiles)
{ {
Q_UNUSED(p_cart);
Q_ASSERT(p_cart); Q_ASSERT(p_cart);
int ret = 0; int ret = 0;
QVector<QString> files = m_cart->getFiles(); QVector<QString> files = m_cart->getFiles();

View File

@ -46,12 +46,27 @@ struct HLUnitStyle
// Fenced code block only. // Fenced code block only.
struct VCodeBlock struct VCodeBlock
{ {
// Global position of the start.
int m_startPos; int m_startPos;
int m_startBlock; int m_startBlock;
int m_endBlock; int m_endBlock;
QString m_lang; QString m_lang;
QString m_text; 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. // Highlight unit with global position and string style name.

View File

@ -1,5 +1,3 @@
var placeholder = document.getElementById('placeholder');
// Use Marked to highlight code blocks in edit mode. // Use Marked to highlight code blocks in edit mode.
marked.setOptions({ marked.setOptions({
highlight: function(code, lang) { highlight: function(code, lang) {
@ -18,7 +16,7 @@ marked.setOptions({
var updateHtml = function(html) { var updateHtml = function(html) {
asyncJobsCount = 0; asyncJobsCount = 0;
placeholder.innerHTML = html; contentDiv.innerHTML = html;
insertImageCaption(); insertImageCaption();
@ -83,7 +81,7 @@ var updateHtml = function(html) {
// MathJax may be not loaded for now. // MathJax may be not loaded for now.
if (VEnableMathjax && (typeof MathJax != "undefined")) { if (VEnableMathjax && (typeof MathJax != "undefined")) {
try { try {
MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder, postProcessMathJax]); MathJax.Hub.Queue(["Typeset", MathJax.Hub, contentDiv, postProcessMathJax]);
} catch (err) { } catch (err) {
content.setLog("err: " + err); content.setLog("err: " + err);
finishLogics(); finishLogics();
@ -100,7 +98,7 @@ var highlightText = function(text, id, timeStamp) {
var textToHtml = function(text) { var textToHtml = function(text) {
var html = marked(text); var html = marked(text);
var container = document.getElementById('text-to-html-div'); var container = textHtmlDiv;
container.innerHTML = html; container.innerHTML = html;
html = getHtmlWithInlineStyles(container); html = getHtmlWithInlineStyles(container);

View File

@ -1,4 +1,3 @@
var placeholder = document.getElementById('placeholder');
var nameCounter = 0; var nameCounter = 0;
var toc = []; // Table of Content as a list var toc = []; // Table of Content as a list
@ -110,7 +109,7 @@ var updateText = function(text) {
var needToc = mdHasTocSection(text); var needToc = mdHasTocSection(text);
var html = markdownToHtml(text, needToc); var html = markdownToHtml(text, needToc);
placeholder.innerHTML = html; contentDiv.innerHTML = html;
handleToc(needToc); handleToc(needToc);
insertImageCaption(); insertImageCaption();
renderMermaid('lang-mermaid'); renderMermaid('lang-mermaid');
@ -124,7 +123,7 @@ var updateText = function(text) {
// finishLoading logic. // finishLoading logic.
if (VEnableMathjax) { if (VEnableMathjax) {
try { try {
MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder, postProcessMathJax]); MathJax.Hub.Queue(["Typeset", MathJax.Hub, contentDiv, postProcessMathJax]);
} catch (err) { } catch (err) {
content.setLog("err: " + err); content.setLog("err: " + err);
finishLogics(); finishLogics();
@ -141,7 +140,7 @@ var highlightText = function(text, id, timeStamp) {
var textToHtml = function(text) { var textToHtml = function(text) {
var html = mdit.render(text); var html = mdit.render(text);
var container = document.getElementById('text-to-html-div'); var container = textHtmlDiv;
container.innerHTML = html; container.innerHTML = html;
html = getHtmlWithInlineStyles(container); html = getHtmlWithInlineStyles(container);

View File

@ -30,8 +30,10 @@
<script src="qrc:/resources/markdown_template.js" defer></script> <script src="qrc:/resources/markdown_template.js" defer></script>
</head> </head>
<body> <body>
<div id="placeholder"></div> <div id="content-div"></div>
<div id="text-to-html-div" style="display:none;"></div> <div id="preview-div" style="display:none;"></div>
<div id="text-html-div" style="display:none;"></div>
</body> </body>
</html> </html>

View File

@ -1,5 +1,11 @@
var channelInitialized = false; var channelInitialized = false;
var contentDiv = document.getElementById('content-div');
var previewDiv = document.getElementById('preview-div');
var textHtmlDiv = document.getElementById('text-html-div');
var content; var content;
// Current header index in all headers. // Current header index in all headers.
@ -131,7 +137,7 @@ var styleContent = function() {
}; };
var htmlContent = function() { var htmlContent = function() {
content.htmlContentCB("", styleContent(), placeholder.innerHTML); content.htmlContentCB("", styleContent(), contentDiv.innerHTML);
}; };
new QWebChannel(qt.webChannelTransport, new QWebChannel(qt.webChannelTransport,
@ -157,6 +163,11 @@ new QWebChannel(qt.webChannelTransport,
content.plantUMLResultReady.connect(handlePlantUMLResult); content.plantUMLResultReady.connect(handlePlantUMLResult);
content.graphvizResultReady.connect(handleGraphvizResult); content.graphvizResultReady.connect(handleGraphvizResult);
content.requestPreviewEnabled.connect(setPreviewEnabled);
content.requestPreviewCodeBlock.connect(previewCodeBlock);
content.requestSetPreviewContent.connect(setPreviewContent);
if (typeof updateHtml == "function") { if (typeof updateHtml == "function") {
updateHtml(content.html); updateHtml(content.html);
content.htmlChanged.connect(updateHtml); content.htmlChanged.connect(updateHtml);
@ -980,7 +991,7 @@ window.onmousedown = function(e) {
// Left button and Ctrl key. // Left button and Ctrl key.
if (e.buttons == 1 if (e.buttons == 1
&& e.ctrlKey && e.ctrlKey
&& window.getSelection().rangeCount == 0) { && window.getSelection().type != 'Range') {
vds_oriMouseClientX = e.clientX; vds_oriMouseClientX = e.clientX;
vds_oriMouseClientY = e.clientY; vds_oriMouseClientY = e.clientY;
vds_readyToScroll = true; vds_readyToScroll = true;
@ -1267,7 +1278,7 @@ function getNodeText(el) {
} }
var calculateWordCount = function() { var calculateWordCount = function() {
var words = getNodeText(placeholder); var words = getNodeText(contentDiv);
// Char without spaces. // Char without spaces.
var cns = 0; var cns = 0;
@ -1349,3 +1360,49 @@ var handleGraphvizResult = function(id, format, result) {
finishOneAsyncJob(); 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;
};

View File

@ -1,4 +1,3 @@
var placeholder = document.getElementById('placeholder');
var renderer = new marked.Renderer(); var renderer = new marked.Renderer();
var toc = []; // Table of contents as a list var toc = []; // Table of contents as a list
var nameCounter = 0; var nameCounter = 0;
@ -54,7 +53,7 @@ var updateText = function(text) {
var needToc = mdHasTocSection(text); var needToc = mdHasTocSection(text);
var html = markdownToHtml(text, needToc); var html = markdownToHtml(text, needToc);
placeholder.innerHTML = html; contentDiv.innerHTML = html;
handleToc(needToc); handleToc(needToc);
insertImageCaption(); insertImageCaption();
renderMermaid('lang-mermaid'); renderMermaid('lang-mermaid');
@ -68,7 +67,7 @@ var updateText = function(text) {
// finishLoading logic. // finishLoading logic.
if (VEnableMathjax) { if (VEnableMathjax) {
try { try {
MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder, postProcessMathJax]); MathJax.Hub.Queue(["Typeset", MathJax.Hub, contentDiv, postProcessMathJax]);
} catch (err) { } catch (err) {
content.setLog("err: " + err); content.setLog("err: " + err);
finishLogics(); finishLogics();
@ -85,7 +84,7 @@ var highlightText = function(text, id, timeStamp) {
var textToHtml = function(text) { var textToHtml = function(text) {
var html = marked(text); var html = marked(text);
var container = document.getElementById('text-to-html-div'); var container = textHtmlDiv;
container.innerHTML = html; container.innerHTML = html;
html = getHtmlWithInlineStyles(container); html = getHtmlWithInlineStyles(container);

View File

@ -1,4 +1,3 @@
var placeholder = document.getElementById('placeholder');
var renderer = new showdown.Converter({simplifiedAutoLink: 'true', var renderer = new showdown.Converter({simplifiedAutoLink: 'true',
excludeTrailingPunctuationFromURLs: 'true', excludeTrailingPunctuationFromURLs: 'true',
strikethrough: 'true', strikethrough: 'true',
@ -94,7 +93,7 @@ var updateText = function(text) {
var needToc = mdHasTocSection(text); var needToc = mdHasTocSection(text);
var html = markdownToHtml(text, needToc); var html = markdownToHtml(text, needToc);
placeholder.innerHTML = html; contentDiv.innerHTML = html;
handleToc(needToc); handleToc(needToc);
insertImageCaption(); insertImageCaption();
highlightCodeBlocks(document, highlightCodeBlocks(document,
@ -114,7 +113,7 @@ var updateText = function(text) {
// finishLoading logic. // finishLoading logic.
if (VEnableMathjax) { if (VEnableMathjax) {
try { try {
MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder, postProcessMathJax]); MathJax.Hub.Queue(["Typeset", MathJax.Hub, contentDiv, postProcessMathJax]);
} catch (err) { } catch (err) {
content.setLog("err: " + err); content.setLog("err: " + err);
finishLogics(); finishLogics();
@ -149,7 +148,7 @@ var textToHtml = function(text) {
delete parser; delete parser;
var container = document.getElementById('text-to-html-div'); var container = textHtmlDiv;
container.innerHTML = html; container.innerHTML = html;
html = getHtmlWithInlineStyles(container); html = getHtmlWithInlineStyles(container);

View File

@ -307,3 +307,7 @@ table.hljs-ln tr td.hljs-ln-code {
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: contain; background-size: contain;
} }
::selection {
background: #64B5F6;
}

View File

@ -237,3 +237,7 @@ table.hljs-ln tr td.hljs-ln-numbers {
table.hljs-ln tr td.hljs-ln-code { table.hljs-ln tr td.hljs-ln-code {
padding-left: 10px; padding-left: 10px;
} }
::selection {
background: #64B5F6;
}

View File

@ -307,3 +307,7 @@ table.hljs-ln tr td.hljs-ln-code {
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: contain; background-size: contain;
} }
::selection {
background: #64B5F6;
}

View File

@ -387,6 +387,8 @@ MagicWord=M
ApplySnippet=S ApplySnippet=S
; Open export dialog ; Open export dialog
Export=O Export=O
; Toggle live preview
LivePreview=I
[external_editors] [external_editors]
; Define external editors which could be called to edit notes ; Define external editors which could be called to edit notes

View File

@ -127,7 +127,8 @@ SOURCES += main.cpp\
vlistfolderue.cpp \ vlistfolderue.cpp \
dialog/vfixnotebookdialog.cpp \ dialog/vfixnotebookdialog.cpp \
vplantumlhelper.cpp \ vplantumlhelper.cpp \
vgraphvizhelper.cpp vgraphvizhelper.cpp \
vlivepreviewhelper.cpp
HEADERS += vmainwindow.h \ HEADERS += vmainwindow.h \
vdirectorytree.h \ vdirectorytree.h \
@ -245,7 +246,8 @@ HEADERS += vmainwindow.h \
vlistfolderue.h \ vlistfolderue.h \
dialog/vfixnotebookdialog.h \ dialog/vfixnotebookdialog.h \
vplantumlhelper.h \ vplantumlhelper.h \
vgraphvizhelper.h vgraphvizhelper.h \
vlivepreviewhelper.h
RESOURCES += \ RESOURCES += \
vnote.qrc \ vnote.qrc \

View File

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

View File

@ -47,6 +47,20 @@ public:
const VWordCountInfo &getWordCountInfo() const; 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: public slots:
// Will be called in the HTML side // 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 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: private:
QString m_toc; QString m_toc;
QString m_header; QString m_header;

View File

@ -923,6 +923,10 @@ void VEditArea::registerCaptainTargets()
g_config->getCaptainShortcutKeySequence("ApplySnippet"), g_config->getCaptainShortcutKeySequence("ApplySnippet"),
this, this,
applySnippetByCaptain); applySnippetByCaptain);
captain->registerCaptainTarget(tr("LivePreview"),
g_config->getCaptainShortcutKeySequence("LivePreview"),
this,
toggleLivePreviewByCaptain);
} }
bool VEditArea::activateTabByCaptain(void *p_target, void *p_data, int p_idx) 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; return true;
} }
bool VEditArea::toggleLivePreviewByCaptain(void *p_target, void *p_data)
{
Q_UNUSED(p_data);
VEditArea *obj = static_cast<VEditArea *>(p_target);
VEditTab *tab = obj->getCurrentTab();
if (tab) {
tab->toggleLivePreview();
}
return true;
}
void VEditArea::recordClosedFile(const VFileSessionInfo &p_file) void VEditArea::recordClosedFile(const VFileSessionInfo &p_file)
{ {
for (auto it = m_lastClosedFiles.begin(); it != m_lastClosedFiles.end(); ++it) { for (auto it = m_lastClosedFiles.begin(); it != m_lastClosedFiles.end(); ++it) {

View File

@ -208,6 +208,9 @@ private:
// Prompt for user to apply a snippet. // Prompt for user to apply a snippet.
static bool applySnippetByCaptain(void *p_target, void *p_data); 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. // End Captain mode functions.
int curWindowIndex; int curWindowIndex;

View File

@ -366,6 +366,8 @@ signals:
void mouseReleased(QMouseEvent *p_event); void mouseReleased(QMouseEvent *p_event);
void cursorPositionChanged();
private slots: private slots:
// Timer for find-wrap label. // Timer for find-wrap label.
void labelTimerTimeout() void labelTimerTimeout()

View File

@ -118,6 +118,10 @@ public:
// Fetch tab stat info. // Fetch tab stat info.
virtual VWordCountInfo fetchWordCountInfo(bool p_editMode) const; virtual VWordCountInfo fetchWordCountInfo(bool p_editMode) const;
virtual void toggleLivePreview()
{
}
public slots: public slots:
// Enter edit mode // Enter edit mode
virtual void editFile() = 0; virtual void editFile() = 0;

235
src/vlivepreviewhelper.cpp Normal file
View File

@ -0,0 +1,235 @@
#include "vlivepreviewhelper.h"
#include <QDebug>
#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<VCodeBlock> &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);
}
}

63
src/vlivepreviewhelper.h Normal file
View File

@ -0,0 +1,63 @@
#ifndef VLIVEPREVIEWHELPER_H
#define VLIVEPREVIEWHELPER_H
#include <QObject>
#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<VCodeBlock> &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<CodeBlock> 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

View File

@ -38,7 +38,8 @@ VMdEditor::VMdEditor(VFile *p_file,
m_mdHighlighter(NULL), m_mdHighlighter(NULL),
m_freshEdit(true), m_freshEdit(true),
m_textToHtmlDialog(NULL), m_textToHtmlDialog(NULL),
m_zoomDelta(0) m_zoomDelta(0),
m_editTab(NULL)
{ {
Q_ASSERT(p_file->getDocType() == DocType::Markdown); Q_ASSERT(p_file->getDocType() == DocType::Markdown);
@ -97,6 +98,9 @@ VMdEditor::VMdEditor(VFile *p_file,
connect(this, &VTextEdit::cursorPositionChanged, connect(this, &VTextEdit::cursorPositionChanged,
this, &VMdEditor::updateCurrentHeader); this, &VMdEditor::updateCurrentHeader);
connect(this, &VTextEdit::cursorPositionChanged,
m_object, &VEditorObject::cursorPositionChanged);
setDisplayScaleFactor(VUtils::calculateScaleFactor()); setDisplayScaleFactor(VUtils::calculateScaleFactor());
updateFontAndPalette(); updateFontAndPalette();
@ -276,10 +280,7 @@ void VMdEditor::contextMenuEvent(QContextMenuEvent *p_event)
{ {
QScopedPointer<QMenu> menu(createStandardContextMenu()); QScopedPointer<QMenu> menu(createStandardContextMenu());
menu->setToolTipsVisible(true); menu->setToolTipsVisible(true);
if (m_editTab && m_editTab->isEditMode()) {
VEditTab *editTab = dynamic_cast<VEditTab *>(parent());
Q_ASSERT(editTab);
if (editTab->isEditMode()) {
const QList<QAction *> actions = menu->actions(); const QList<QAction *> actions = menu->actions();
if (textCursor().hasSelection()) { if (textCursor().hasSelection()) {
@ -303,9 +304,19 @@ void VMdEditor::contextMenuEvent(QContextMenuEvent *p_event)
emit m_object->discardAndRead(); 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->insertAction(discardExitAct, saveExitAct);
menu->insertSeparator(toggleLivePreviewAct);
if (!actions.isEmpty()) { if (!actions.isEmpty()) {
menu->insertSeparator(actions[0]); menu->insertSeparator(actions[0]);
} }
@ -1322,3 +1333,8 @@ VWordCountInfo VMdEditor::fetchWordCountInfo() const
info.m_charWithSpacesCount = cc; info.m_charWithSpacesCount = cc;
return info; return info;
} }
void VMdEditor::setEditTab(VEditTab *p_editTab)
{
m_editTab = p_editTab;
}

View File

@ -19,6 +19,7 @@ class VCodeBlockHighlightHelper;
class VDocument; class VDocument;
class VPreviewManager; class VPreviewManager;
class VCopyTextAsHtmlDialog; class VCopyTextAsHtmlDialog;
class VEditTab;
class VMdEditor : public VTextEdit, public VEditor class VMdEditor : public VTextEdit, public VEditor
{ {
@ -69,6 +70,10 @@ public:
VWordCountInfo fetchWordCountInfo() const Q_DECL_OVERRIDE; VWordCountInfo fetchWordCountInfo() const Q_DECL_OVERRIDE;
void setEditTab(VEditTab *p_editTab);
HGMarkdownHighlighter *getMarkdownHighlighter() const;
public slots: public slots:
bool jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) Q_DECL_OVERRIDE; bool jumpTitle(bool p_forward, int p_relativeLevel, int p_repeat) Q_DECL_OVERRIDE;
@ -260,5 +265,12 @@ private:
VCopyTextAsHtmlDialog *m_textToHtmlDialog; VCopyTextAsHtmlDialog *m_textToHtmlDialog;
int m_zoomDelta; int m_zoomDelta;
VEditTab *m_editTab;
}; };
inline HGMarkdownHighlighter *VMdEditor::getMarkdownHighlighter() const
{
return m_mdHighlighter;
}
#endif // VMDEDITOR_H #endif // VMDEDITOR_H

View File

@ -22,11 +22,13 @@
#include "vsnippet.h" #include "vsnippet.h"
#include "vinsertselector.h" #include "vinsertselector.h"
#include "vsnippetlist.h" #include "vsnippetlist.h"
#include "vlivepreviewhelper.h"
extern VMainWindow *g_mainWin; extern VMainWindow *g_mainWin;
extern VConfigManager *g_config; extern VConfigManager *g_config;
VMdTab::VMdTab(VFile *p_file, VEditArea *p_editArea, VMdTab::VMdTab(VFile *p_file, VEditArea *p_editArea,
OpenFileMode p_mode, QWidget *p_parent) OpenFileMode p_mode, QWidget *p_parent)
: VEditTab(p_file, p_editArea, 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_document(NULL),
m_mdConType(g_config->getMdConverterType()), m_mdConType(g_config->getMdConverterType()),
m_enableHeadingSequence(false), m_enableHeadingSequence(false),
m_backupFileChecked(false) m_backupFileChecked(false),
m_mode(Mode::InvalidMode),
m_livePreviewHelper(NULL)
{ {
V_ASSERT(m_file->getDocType() == DocType::Markdown); 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 = new VMdEditor(m_file, m_document, m_mdConType, this);
m_editor->setProperty("MainEditor", true); m_editor->setProperty("MainEditor", true);
m_editor->setEditTab(this);
connect(m_editor, &VMdEditor::headersChanged, connect(m_editor, &VMdEditor::headersChanged,
this, &VMdTab::updateOutlineFromHeaders); this, &VMdTab::updateOutlineFromHeaders);
connect(m_editor, SIGNAL(currentHeaderChanged(int)), connect(m_editor, SIGNAL(currentHeaderChanged(int)),
@ -467,10 +472,10 @@ void VMdTab::setupMarkdownEditor()
this, &VMdTab::updateStatus); this, &VMdTab::updateStatus);
connect(m_editor, &VMdEditor::textChanged, connect(m_editor, &VMdEditor::textChanged,
this, &VMdTab::updateStatus); this, &VMdTab::updateStatus);
connect(m_editor, &VMdEditor::cursorPositionChanged,
this, &VMdTab::updateCursorStatus);
connect(g_mainWin, &VMainWindow::editorConfigUpdated, connect(g_mainWin, &VMainWindow::editorConfigUpdated,
m_editor, &VMdEditor::updateConfig); m_editor, &VMdEditor::updateConfig);
connect(m_editor->object(), &VEditorObject::cursorPositionChanged,
this, &VMdTab::updateCursorStatus);
connect(m_editor->object(), &VEditorObject::saveAndRead, connect(m_editor->object(), &VEditorObject::saveAndRead,
this, &VMdTab::saveAndRead); this, &VMdTab::saveAndRead);
connect(m_editor->object(), &VEditorObject::discardAndRead, 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) void VMdTab::zoom(bool p_zoomIn, qreal p_step)
{ {
// Editor will handle it itself. if (!m_isEditMode || m_mode == Mode::EditPreview) {
Q_ASSERT(!m_isEditMode);
if (!m_isEditMode) {
zoomWebPage(p_zoomIn, p_step); zoomWebPage(p_zoomIn, p_step);
} }
} }
@ -1316,6 +1319,18 @@ VWordCountInfo VMdTab::fetchWordCountInfo(bool p_editMode) const
void VMdTab::setCurrentMode(Mode p_mode) 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) { switch (p_mode) {
case Mode::Read: case Mode::Read:
m_webViewer->show(); m_webViewer->show();
@ -1323,13 +1338,55 @@ void VMdTab::setCurrentMode(Mode p_mode)
m_editor->hide(); 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; break;
case Mode::EditPreview: case Mode::EditPreview:
case Mode::Edit:
Q_ASSERT(m_editor); Q_ASSERT(m_editor);
m_editor->show(); 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<int> 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<int> 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; break;
default: default:
@ -1340,3 +1397,19 @@ void VMdTab::setCurrentMode(Mode p_mode)
focusChild(); focusChild();
} }
void VMdTab::toggleLivePreview()
{
switch (m_mode) {
case Mode::EditPreview:
setCurrentMode(Mode::Edit);
break;
case Mode::Edit:
setCurrentMode(Mode::EditPreview);
break;
default:
break;
}
}

View File

@ -3,6 +3,7 @@
#include <QString> #include <QString>
#include <QPointer> #include <QPointer>
#include <QSharedPointer>
#include "vedittab.h" #include "vedittab.h"
#include "vconstants.h" #include "vconstants.h"
#include "vmarkdownconverter.h" #include "vmarkdownconverter.h"
@ -15,6 +16,7 @@ class VInsertSelector;
class QTimer; class QTimer;
class QWebEngineDownloadItem; class QWebEngineDownloadItem;
class QSplitter; class QSplitter;
class VLivePreviewHelper;
class VMdTab : public VEditTab class VMdTab : public VEditTab
{ {
@ -91,6 +93,9 @@ public:
// Fetch tab stat info. // Fetch tab stat info.
VWordCountInfo fetchWordCountInfo(bool p_editMode) const Q_DECL_OVERRIDE; VWordCountInfo fetchWordCountInfo(bool p_editMode) const Q_DECL_OVERRIDE;
// Toggle live preview in edit mode.
void toggleLivePreview() Q_DECL_OVERRIDE;
public slots: public slots:
// Enter edit mode. // Enter edit mode.
void editFile() Q_DECL_OVERRIDE; void editFile() Q_DECL_OVERRIDE;
@ -145,7 +150,12 @@ private slots:
private: private:
enum TabReady { None = 0, ReadMode = 0x1, EditMode = 0x2 }; 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. // Setup UI.
void setupUI(); void setupUI();
@ -238,6 +248,11 @@ private:
VVim::SearchItem m_lastSearchItem; VVim::SearchItem m_lastSearchItem;
Mode m_mode; Mode m_mode;
QSharedPointer<WebViewState> m_readWebViewState;
QSharedPointer<WebViewState> m_previewWebViewState;
VLivePreviewHelper *m_livePreviewHelper;
}; };
inline VMdEditor *VMdTab::getEditor() inline VMdEditor *VMdTab::getEditor()