mirror of
https://gitee.com/vnotex/vnote.git
synced 2025-07-05 13:59:52 +08:00
VMdTab: live preview
This commit is contained in:
parent
051088be31
commit
c6b7561864
@ -892,8 +892,8 @@ int VExportDialog::doExport(VCart *p_cart,
|
||||
QString *p_errMsg,
|
||||
QList<QString> *p_outputFiles)
|
||||
{
|
||||
Q_UNUSED(p_cart);
|
||||
Q_ASSERT(p_cart);
|
||||
|
||||
int ret = 0;
|
||||
|
||||
QVector<QString> files = m_cart->getFiles();
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -30,8 +30,10 @@
|
||||
<script src="qrc:/resources/markdown_template.js" defer></script>
|
||||
</head>
|
||||
<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>
|
||||
</html>
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -307,3 +307,7 @@ table.hljs-ln tr td.hljs-ln-code {
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: #64B5F6;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -307,3 +307,7 @@ table.hljs-ln tr td.hljs-ln-code {
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: #64B5F6;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 \
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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<VEditArea *>(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) {
|
||||
|
@ -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;
|
||||
|
@ -366,6 +366,8 @@ signals:
|
||||
|
||||
void mouseReleased(QMouseEvent *p_event);
|
||||
|
||||
void cursorPositionChanged();
|
||||
|
||||
private slots:
|
||||
// Timer for find-wrap label.
|
||||
void labelTimerTimeout()
|
||||
|
@ -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;
|
||||
|
235
src/vlivepreviewhelper.cpp
Normal file
235
src/vlivepreviewhelper.cpp
Normal 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
63
src/vlivepreviewhelper.h
Normal 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
|
@ -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<QMenu> menu(createStandardContextMenu());
|
||||
menu->setToolTipsVisible(true);
|
||||
|
||||
VEditTab *editTab = dynamic_cast<VEditTab *>(parent());
|
||||
Q_ASSERT(editTab);
|
||||
if (editTab->isEditMode()) {
|
||||
if (m_editTab && m_editTab->isEditMode()) {
|
||||
const QList<QAction *> 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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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<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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
17
src/vmdtab.h
17
src/vmdtab.h
@ -3,6 +3,7 @@
|
||||
|
||||
#include <QString>
|
||||
#include <QPointer>
|
||||
#include <QSharedPointer>
|
||||
#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<WebViewState> m_readWebViewState;
|
||||
QSharedPointer<WebViewState> m_previewWebViewState;
|
||||
|
||||
VLivePreviewHelper *m_livePreviewHelper;
|
||||
};
|
||||
|
||||
inline VMdEditor *VMdTab::getEditor()
|
||||
|
Loading…
x
Reference in New Issue
Block a user