MdEditor: support copying diagram in puml and graphviz

This commit is contained in:
Le Tan 2018-08-22 20:01:31 +08:00
parent 1fe975b1ad
commit d4daf32f20
14 changed files with 337 additions and 58 deletions

View File

@ -44,6 +44,8 @@ public:
const QVector<VElementRegion> &getImageRegions() const;
const QVector<VCodeBlock> &getCodeBlocks() const;
public slots:
// Parse and rehighlight immediately.
void updateHighlight();
@ -360,4 +362,9 @@ inline bool PegMarkdownHighlighter::isFastParseBlock(int p_blockNum) const
{
return p_blockNum >= m_fastParseBlocks.first && p_blockNum <= m_fastParseBlocks.second;
}
inline const QVector<VCodeBlock> &PegMarkdownHighlighter::getCodeBlocks() const
{
return m_result->m_codeBlocks;
}
#endif // PEGMARKDOWNHIGHLIGHTER_H

View File

@ -7,6 +7,7 @@
#include <QScrollBar>
#include "vutils.h"
#include "vcodeblockhighlighthelper.h"
void VEditUtils::removeBlock(QTextBlock &p_block, QString *p_text)
{
@ -1083,3 +1084,11 @@ bool VEditUtils::isWordSeparator(QChar p_char)
return false;
}
QString VEditUtils::removeCodeBlockFence(const QString &p_text)
{
QString text = VCodeBlockHighlightHelper::unindentCodeBlock(p_text);
Q_ASSERT(text.startsWith("```") && text.endsWith("```"));
int idx = text.indexOf('\n') + 1;
return text.mid(idx, text.size() - idx - 3);
}

View File

@ -211,6 +211,9 @@ public:
static bool isWordSeparator(QChar p_char);
// Remove the fence of fenced code block.
static QString removeCodeBlockFence(const QString &p_text);
private:
VEditUtils() {}
};

View File

@ -11,24 +11,36 @@ int VProcessUtils::startProcess(const QString &p_program,
QByteArray &p_out,
QByteArray &p_err)
{
int ret = 0;
QScopedPointer<QProcess> process(new QProcess());
process->start(p_program, p_args);
return startProcess(process.data(),
p_in,
p_exitCode,
p_out,
p_err);
}
int VProcessUtils::startProcess(QProcess *p_process,
const QByteArray &p_in,
int &p_exitCode,
QByteArray &p_out,
QByteArray &p_err)
{
int ret = 0;
if (!p_in.isEmpty()) {
if (process->write(p_in) == -1) {
process->closeWriteChannel();
qWarning() << "fail to write to QProcess:" << process->errorString();
if (p_process->write(p_in) == -1) {
p_process->closeWriteChannel();
qWarning() << "fail to write to QProcess:" << p_process->errorString();
return -1;
} else {
process->closeWriteChannel();
p_process->closeWriteChannel();
}
}
bool finished = false;
bool started = false;
while (true) {
QProcess::ProcessError err = process->error();
QProcess::ProcessError err = p_process->error();
if (err == QProcess::FailedToStart
|| err == QProcess::Crashed) {
if (err == QProcess::FailedToStart) {
@ -41,34 +53,34 @@ int VProcessUtils::startProcess(const QString &p_program,
}
if (started) {
if (process->state() == QProcess::NotRunning) {
if (p_process->state() == QProcess::NotRunning) {
finished = true;
}
} else {
if (process->state() != QProcess::NotRunning) {
if (p_process->state() != QProcess::NotRunning) {
started = true;
}
}
if (process->waitForFinished(500)) {
if (p_process->waitForFinished(500)) {
// Finished.
finished = true;
}
if (finished) {
QProcess::ExitStatus sta = process->exitStatus();
QProcess::ExitStatus sta = p_process->exitStatus();
if (sta == QProcess::CrashExit) {
ret = -1;
break;
}
p_exitCode = process->exitCode();
p_exitCode = p_process->exitCode();
break;
}
}
p_out = process->readAllStandardOutput();
p_err = process->readAllStandardError();
p_out = p_process->readAllStandardOutput();
p_err = p_process->readAllStandardError();
return ret;
}
@ -86,3 +98,18 @@ int VProcessUtils::startProcess(const QString &p_program,
p_out,
p_err);
}
int VProcessUtils::startProcess(const QString &p_cmd,
const QByteArray &p_in,
int &p_exitCode,
QByteArray &p_out,
QByteArray &p_err)
{
QScopedPointer<QProcess> process(new QProcess());
process->start(p_cmd);
return startProcess(process.data(),
p_in,
p_exitCode,
p_out,
p_err);
}

View File

@ -4,6 +4,7 @@
#include <QStringList>
#include <QByteArray>
class QProcess;
class VProcessUtils
{
@ -25,8 +26,20 @@ public:
QByteArray &p_out,
QByteArray &p_err);
static int startProcess(const QString &p_cmd,
const QByteArray &p_in,
int &p_exitCode,
QByteArray &p_out,
QByteArray &p_err);
private:
VProcessUtils() {}
static int startProcess(QProcess *p_process,
const QByteArray &p_in,
int &p_exitCode,
QByteArray &p_out,
QByteArray &p_err);
};
#endif // VPROCESSUTILS_H

View File

@ -35,6 +35,17 @@ VEditor::~VEditor()
if (m_completer->widget() == m_editor) {
m_completer->setWidget(NULL);
}
cleanUp();
}
void VEditor::cleanUp()
{
for (auto const & file : m_tempFiles) {
VUtils::deleteFile(file);
}
m_tempFiles.clear();
}
void VEditor::init()

View File

@ -254,6 +254,8 @@ protected:
virtual int lineNumberAreaWidth() const = 0;
void addTempFile(const QString &p_file);
QWidget *m_editor;
VEditorObject *m_object;
@ -309,6 +311,8 @@ private:
QStringList generateCompletionCandidates() const;
void cleanUp();
QLabel *m_wrapLabel;
QTimer *m_labelTimer;
@ -352,6 +356,9 @@ private:
QSharedPointer<VTextEditCompleter> m_completer;
// Temp files needed to be delete.
QStringList m_tempFiles;
// Functions for private slots.
private:
void labelTimerTimeout();
@ -456,4 +463,8 @@ inline QWidget *VEditor::getEditor() const
return m_editor;
}
inline void VEditor::addTempFile(const QString &p_file)
{
m_tempFiles.append(p_file);
}
#endif // VEDITOR_H

View File

@ -119,3 +119,26 @@ bool VGraphvizHelper::testGraphviz(const QString &p_dot, QString &p_msg)
return ret == 0 && exitCode == 0;
}
QByteArray VGraphvizHelper::process(const QString &p_format, const QString &p_text)
{
VGraphvizHelper inst;
int exitCode = -1;
QByteArray out, err;
QStringList args(inst.m_args);
args << ("-T" + p_format);
int ret = VProcessUtils::startProcess(inst.m_program,
args,
p_text.toUtf8(),
exitCode,
out,
err);
if (ret != 0 || exitCode < 0) {
qWarning() << "Graphviz fail" << ret << exitCode << QString::fromLocal8Bit(err);
}
return out;
}

View File

@ -16,10 +16,10 @@ public:
void processAsync(int p_id, TimeStamp p_timeStamp, const QString &p_format, const QString &p_text);
void prepareCommand(QString &p_cmd, QStringList &p_args) const;
static bool testGraphviz(const QString &p_dot, QString &p_msg);
static QByteArray process(const QString &p_format, const QString &p_text);
signals:
void resultReady(int p_id, TimeStamp p_timeStamp, const QString &p_format, const QString &p_result);
@ -27,6 +27,8 @@ private slots:
void handleProcessFinished(int p_exitCode, QProcess::ExitStatus p_exitStatus);
private:
void prepareCommand(QString &p_cmd, QStringList &p_args) const;
QString m_program;
QStringList m_args;
};

View File

@ -8,10 +8,10 @@
#include "vconfigmanager.h"
#include "vgraphvizhelper.h"
#include "vplantumlhelper.h"
#include "vcodeblockhighlighthelper.h"
#include "vmainwindow.h"
#include "veditarea.h"
#include "vmathjaxpreviewhelper.h"
#include "utils/veditutils.h"
extern VConfigManager *g_config;
@ -249,14 +249,6 @@ void VLivePreviewHelper::handleCursorPositionChanged()
}
}
static QString removeFence(const QString &p_text)
{
QString text = VCodeBlockHighlightHelper::unindentCodeBlock(p_text);
Q_ASSERT(text.startsWith("```") && text.endsWith("```"));
int idx = text.indexOf('\n') + 1;
return text.mid(idx, text.size() - idx - 3);
}
void VLivePreviewHelper::updateLivePreview()
{
if (m_cbIndex < 0) {
@ -277,7 +269,7 @@ void VLivePreviewHelper::updateLivePreview()
m_graphvizHelper->processAsync(m_cbIndex | LANG_PREFIX_GRAPHVIZ | TYPE_LIVE_PREVIEW,
m_timeStamp,
"svg",
removeFence(vcb.m_text));
VEditUtils::removeCodeBlockFence(vcb.m_text));
} else {
m_document->setPreviewContent(vcb.m_lang, cb.imageData());
}
@ -292,7 +284,7 @@ void VLivePreviewHelper::updateLivePreview()
m_plantUMLHelper->processAsync(m_cbIndex | LANG_PREFIX_PLANTUML | TYPE_LIVE_PREVIEW,
m_timeStamp,
"svg",
removeFence(vcb.m_text));
VEditUtils::removeCodeBlockFence(vcb.m_text));
} else {
m_document->setPreviewContent(vcb.m_lang, cb.imageData());
}
@ -300,7 +292,7 @@ void VLivePreviewHelper::updateLivePreview()
// No need to live preview MathJax.
m_document->previewCodeBlock(m_cbIndex,
vcb.m_lang,
removeFence(vcb.m_text),
VEditUtils::removeCodeBlockFence(vcb.m_text),
true);
}
}
@ -418,7 +410,7 @@ void VLivePreviewHelper::processForInplacePreview(int p_idx)
m_graphvizHelper->processAsync(p_idx | LANG_PREFIX_GRAPHVIZ | TYPE_INPLACE_PREVIEW,
m_timeStamp,
"svg",
removeFence(vcb.m_text));
VEditUtils::removeCodeBlockFence(vcb.m_text));
} else if (vcb.m_lang == "puml" && m_plantUMLMode == PlantUMLMode::LocalPlantUML) {
if (!m_plantUMLHelper) {
m_plantUMLHelper = new VPlantUMLHelper(this);
@ -429,19 +421,19 @@ void VLivePreviewHelper::processForInplacePreview(int p_idx)
m_plantUMLHelper->processAsync(p_idx | LANG_PREFIX_PLANTUML | TYPE_INPLACE_PREVIEW,
m_timeStamp,
"svg",
removeFence(vcb.m_text));
VEditUtils::removeCodeBlockFence(vcb.m_text));
} else if (vcb.m_lang == "flow"
|| vcb.m_lang == "flowchart") {
m_mathJaxHelper->previewDiagram(m_mathJaxID,
p_idx,
m_timeStamp,
vcb.m_lang,
removeFence(vcb.m_text));
VEditUtils::removeCodeBlockFence(vcb.m_text));
} else if (vcb.m_lang == "mathjax") {
m_mathJaxHelper->previewMathJax(m_mathJaxID,
p_idx,
m_timeStamp,
removeFence(vcb.m_text));
VEditUtils::removeCodeBlockFence(vcb.m_text));
}
}

View File

@ -25,6 +25,8 @@
#include "utils/vwebutils.h"
#include "dialog/vinsertlinkdialog.h"
#include "utils/vclipboardutils.h"
#include "vplantumlhelper.h"
#include "vgraphvizhelper.h"
extern VWebUtils *g_webUtils;
@ -1604,7 +1606,16 @@ void VMdEditor::initLinkAndPreviewMenu(QAction *p_before, QMenu *p_menu, const Q
return;
}
bool needSeparator = false;
if (initInPlacePreviewMenu(p_before, p_menu, block, pos)) {
needSeparator = true;
}
if (initExportAndCopyMenu(p_before, p_menu, block, pos)) {
needSeparator = true;
}
if (needSeparator) {
p_menu->insertSeparator(p_before ? p_before : NULL);
}
}
@ -1665,3 +1676,117 @@ bool VMdEditor::initInPlacePreviewMenu(QAction *p_before,
p_menu->insertAction(p_before, copyImageAct);
return true;
}
bool VMdEditor::initExportAndCopyMenu(QAction *p_before,
QMenu *p_menu,
const QTextBlock &p_block,
int p_pos)
{
Q_UNUSED(p_pos);
int state = p_block.userState();
if (state != HighlightBlockState::CodeBlockStart
&& state != HighlightBlockState::CodeBlock
&& state != HighlightBlockState::CodeBlockEnd) {
return false;
}
int blockNum = p_block.blockNumber();
const QVector<VCodeBlock> &cbs = m_pegHighlighter->getCodeBlocks();
int idx = 0;
for (idx = 0; idx < cbs.size(); ++idx) {
if (cbs[idx].m_startBlock <= blockNum
&& cbs[idx].m_endBlock >= blockNum) {
break;
}
}
if (idx >= cbs.size()) {
return false;
}
const VCodeBlock &cb = cbs[idx];
if (cb.m_lang != "puml" && cb.m_lang != "dot") {
return false;
}
QMenu *subMenu = new QMenu(tr("Copy Diagram"), p_menu);
subMenu->setToolTipsVisible(true);
QAction *pngAct = new QAction(tr("PNG"), subMenu);
pngAct->setToolTip(tr("Export diagram as PNG to a temporary file and copy"));
connect(pngAct, &QAction::triggered,
this, [this, lang = cb.m_lang, text = cb.m_text]() {
exportDiagramAndCopy(lang, text, "png");
});
subMenu->addAction(pngAct);
QAction *svgAct = new QAction(tr("SVG"), subMenu);
svgAct->setToolTip(tr("Export diagram as SVG to a temporary file and copy"));
connect(svgAct, &QAction::triggered,
this, [this, lang = cb.m_lang, text = cb.m_text]() {
exportDiagramAndCopy(lang, text, "svg");
});
subMenu->addAction(svgAct);
p_menu->insertMenu(p_before, subMenu);
return true;
}
void VMdEditor::exportDiagramAndCopy(const QString &p_lang,
const QString &p_text,
const QString &p_format)
{
m_exportTempFile.reset(new QTemporaryFile(QDir::tempPath()
+ QDir::separator()
+ "XXXXXX." + p_format));
if (!m_exportTempFile->open()) {
VUtils::showMessage(QMessageBox::Warning,
tr("Warning"),
tr("Fail to open a temporary file for export."),
"",
QMessageBox::Ok,
QMessageBox::Ok,
this);
m_exportTempFile.clear();
return;
}
emit m_object->statusMessage(tr("Exporting diagram"));
QString filePath(m_exportTempFile->fileName());
QByteArray out;
if (p_lang == "puml") {
out = VPlantUMLHelper::process(p_format,
VEditUtils::removeCodeBlockFence(p_text));
} else if (p_lang == "dot") {
out = VGraphvizHelper::process(p_format,
VEditUtils::removeCodeBlockFence(p_text));
}
if (out.isEmpty() || m_exportTempFile->write(out) == -1) {
VUtils::showMessage(QMessageBox::Warning,
tr("Warning"),
tr("Fail to export diagram."),
"",
QMessageBox::Ok,
QMessageBox::Ok,
this);
} else {
QClipboard *clipboard = QApplication::clipboard();
clipboard->clear();
QImage img;
img.loadFromData(out, p_format.toLocal8Bit().data());
if (!img.isNull()) {
VClipboardUtils::setImageAndLinkToClipboard(clipboard,
img,
filePath,
QClipboard::Clipboard);
emit m_object->statusMessage(tr("Diagram exported and copied"));
} else {
emit m_object->statusMessage(tr("Fail to read exported image: %1").arg(filePath));
}
}
m_exportTempFile->close();
}

View File

@ -6,6 +6,8 @@
#include <QClipboard>
#include <QImage>
#include <QUrl>
#include <QTemporaryFile>
#include <QSharedPointer>
#include "vtextedit.h"
#include "veditor.h"
@ -284,12 +286,21 @@ private:
const QTextBlock &p_block,
int p_pos);
bool initExportAndCopyMenu(QAction *p_before,
QMenu *p_menu,
const QTextBlock &p_block,
int p_pos);
void insertImageLink(const QString &p_text, const QString &p_url);
void setFontPointSizeByStyleSheet(int p_ptSize);
void setFontAndPaletteByStyleSheet(const QFont &p_font, const QPalette &p_palette);
void exportDiagramAndCopy(const QString &p_lang,
const QString &p_text,
const QString &p_format);
PegMarkdownHighlighter *m_pegHighlighter;
VCodeBlockHighlightHelper *m_cbHighlighter;
@ -314,6 +325,9 @@ private:
VEditTab *m_editTab;
int m_copyTimeStamp;
// Temp file used for ExportAndCopy.
QSharedPointer<QTemporaryFile> m_exportTempFile;
};
inline PegMarkdownHighlighter *VMdEditor::getMarkdownHighlighter() const

View File

@ -15,7 +15,19 @@ extern VConfigManager *g_config;
VPlantUMLHelper::VPlantUMLHelper(QObject *p_parent)
: QObject(p_parent)
{
prepareCommand(m_customCmd, m_program, m_args);
m_customCmd = g_config->getPlantUMLCmd();
if (m_customCmd.isEmpty()) {
prepareCommand(m_program, m_args);
}
}
VPlantUMLHelper::VPlantUMLHelper(const QString &p_jar, QObject *p_parent)
: QObject(p_parent)
{
m_customCmd = g_config->getPlantUMLCmd();
if (m_customCmd.isEmpty()) {
prepareCommand(m_program, m_args, p_jar);
}
}
void VPlantUMLHelper::processAsync(int p_id,
@ -49,18 +61,13 @@ void VPlantUMLHelper::processAsync(int p_id,
process->closeWriteChannel();
}
void VPlantUMLHelper::prepareCommand(QString &p_customCmd,
QString &p_program,
QStringList &p_args) const
void VPlantUMLHelper::prepareCommand(QString &p_program,
QStringList &p_args,
const QString &p_jar) const
{
p_customCmd = g_config->getPlantUMLCmd();
if (!p_customCmd.isEmpty()) {
return;
}
p_program = "java";
p_args << "-jar" << g_config->getPlantUMLJar();
p_args << "-jar" << (p_jar.isEmpty() ? g_config->getPlantUMLJar() : p_jar);
p_args << "-charset" << "UTF-8";
int nbthread = QThread::idealThreadCount();
@ -124,28 +131,23 @@ void VPlantUMLHelper::handleProcessFinished(int p_exitCode, QProcess::ExitStatus
bool VPlantUMLHelper::testPlantUMLJar(const QString &p_jar, QString &p_msg)
{
QString program("java");
QStringList args;
args << "-jar" << p_jar;
args << "-charset" << "UTF-8";
const QString &dot = g_config->getGraphvizDot();
if (!dot.isEmpty()) {
args << "-graphvizdot";
args << dot;
}
args << "-pipe";
VPlantUMLHelper inst(p_jar);
QStringList args(inst.m_args);
args << "-tsvg";
QString testGraph("VNote->Markdown : hello");
int exitCode = -1;
QByteArray out, err;
int ret = VProcessUtils::startProcess(program, args, testGraph.toUtf8(), exitCode, out, err);
int ret = VProcessUtils::startProcess(inst.m_program,
args,
testGraph.toUtf8(),
exitCode,
out,
err);
p_msg = QString("Command: %1 %2\nExitCode: %3\nOutput: %4\nError: %5")
.arg(program)
.arg(inst.m_program)
.arg(args.join(' '))
.arg(exitCode)
.arg(QString::fromLocal8Bit(out))
@ -153,3 +155,36 @@ bool VPlantUMLHelper::testPlantUMLJar(const QString &p_jar, QString &p_msg)
return ret == 0 && exitCode == 0;
}
QByteArray VPlantUMLHelper::process(const QString &p_format, const QString &p_text)
{
VPlantUMLHelper inst;
int exitCode = -1;
QByteArray out, err;
int ret = -1;
if (inst.m_customCmd.isEmpty()) {
QStringList args(inst.m_args);
args << ("-t" + p_format);
ret = VProcessUtils::startProcess(inst.m_program,
args,
p_text.toUtf8(),
exitCode,
out,
err);
} else {
QString cmd(inst.m_customCmd);
cmd.replace("%0", p_format);
ret = VProcessUtils::startProcess(cmd,
p_text.toUtf8(),
exitCode,
out,
err);
}
if (ret != 0 || exitCode < 0) {
qWarning() << "PlantUML fail" << ret << exitCode << QString::fromLocal8Bit(err);
}
return out;
}

View File

@ -19,17 +19,24 @@ public:
const QString &p_format,
const QString &p_text);
void prepareCommand(QString &p_customCmd, QString &p_cmd, QStringList &p_args) const;
static bool testPlantUMLJar(const QString &p_jar, QString &p_msg);
static QByteArray process(const QString &p_format, const QString &p_text);
signals:
void resultReady(int p_id, TimeStamp p_timeStamp, const QString &p_format, const QString &p_result);
void resultReady(int p_id,
TimeStamp p_timeStamp,
const QString &p_format,
const QString &p_result);
private slots:
void handleProcessFinished(int p_exitCode, QProcess::ExitStatus p_exitStatus);
private:
VPlantUMLHelper(const QString &p_jar, QObject *p_parent = nullptr);
void prepareCommand(QString &p_cmd, QStringList &p_args, const QString &p_jar= QString()) const;
QString m_program;
QStringList m_args;