From 0131569c02e0c1c64cadd41d92960b5da6b6b0d9 Mon Sep 17 00:00:00 2001 From: Le Tan Date: Sun, 21 May 2017 09:34:32 +0800 Subject: [PATCH] support exporting note as PDF file TODO: Currently the exported PDF does not have the outline which is needed to fix via third-party utils. --- src/dialog/vinsertimagedialog.cpp | 2 +- src/resources/hoedown.js | 7 +- src/resources/markdown-it.js | 8 +- src/resources/markdown_template.js | 6 + src/resources/marked.js | 8 +- src/resources/showdown.js | 8 +- src/src.pro | 6 +- src/utils/vutils.cpp | 14 + src/utils/vutils.h | 2 + src/vconstants.h | 4 + src/vdocument.cpp | 15 +- src/vdocument.h | 7 + src/vedittab.cpp | 46 +--- src/vedittab.h | 3 +- src/vexporter.cpp | 404 +++++++++++++++++++++++++++++ src/vexporter.h | 101 ++++++++ src/vfile.cpp | 23 ++ src/vfile.h | 4 + src/vmainwindow.cpp | 24 ++ src/vmainwindow.h | 2 + src/vmarkdownconverter.cpp | 28 ++ src/vmarkdownconverter.h | 5 +- src/vnote.cpp | 13 +- src/vnote.h | 1 + src/vwebview.cpp | 2 +- src/vwebview.h | 1 + 26 files changed, 696 insertions(+), 48 deletions(-) create mode 100644 src/vexporter.cpp create mode 100644 src/vexporter.h diff --git a/src/dialog/vinsertimagedialog.cpp b/src/dialog/vinsertimagedialog.cpp index ea026396..debe636b 100644 --- a/src/dialog/vinsertimagedialog.cpp +++ b/src/dialog/vinsertimagedialog.cpp @@ -94,7 +94,7 @@ void VInsertImageDialog::handleBrowseBtnClicked() static QString lastPath = QDir::homePath(); QString filePath = QFileDialog::getOpenFileName(this, tr("Select The Image To Be Inserted"), lastPath, tr("Images (*.png *.xpm *.jpg *.bmp *.gif)")); - if (filePath.isNull() || filePath.isEmpty()) { + if (filePath.isEmpty()) { return; } diff --git a/src/resources/hoedown.js b/src/resources/hoedown.js index 3f77ca6e..b6f41096 100644 --- a/src/resources/hoedown.js +++ b/src/resources/hoedown.js @@ -49,13 +49,18 @@ var updateHtml = function(html) { } } + // If you add new logics after handling MathJax, please pay attention to + // finishLoading logic. // MathJax may be not loaded for now. if (VEnableMathjax && (typeof MathJax != "undefined")) { try { - MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder]); + MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder, finishLoading]); } catch (err) { content.setLog("err: " + err); + finishLoading(); } + } else { + finishLoading(); } }; diff --git a/src/resources/markdown-it.js b/src/resources/markdown-it.js index 69c23b1e..6ea2f022 100644 --- a/src/resources/markdown-it.js +++ b/src/resources/markdown-it.js @@ -169,12 +169,18 @@ var updateText = function(text) { handleToc(needToc); insertImageCaption(); renderMermaid('lang-mermaid'); + + // If you add new logics after handling MathJax, please pay attention to + // finishLoading logic. if (VEnableMathjax) { try { - MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder]); + MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder, finishLoading]); } catch (err) { content.setLog("err: " + err); + finishLoading(); } + } else { + finishLoading(); } }; diff --git a/src/resources/markdown_template.js b/src/resources/markdown_template.js index 8c1fb6de..5d29859d 100644 --- a/src/resources/markdown_template.js +++ b/src/resources/markdown_template.js @@ -245,3 +245,9 @@ var insertImageCaption = function() { img.insertAdjacentElement('afterend', captionDiv); } } + +// The renderer specific code should call this function once thay have finished +// loading the page. +var finishLoading = function() { + content.finishLoading(); +}; diff --git a/src/resources/marked.js b/src/resources/marked.js index 49b1c4a2..d9c6f6b5 100644 --- a/src/resources/marked.js +++ b/src/resources/marked.js @@ -127,12 +127,18 @@ var updateText = function(text) { handleToc(needToc); insertImageCaption(); renderMermaid('lang-mermaid'); + + // If you add new logics after handling MathJax, please pay attention to + // finishLoading logic. if (VEnableMathjax) { try { - MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder]); + MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder, finishLoading]); } catch (err) { content.setLog("err: " + err); + finishLoading(); } + } else { + finishLoading(); } }; diff --git a/src/resources/showdown.js b/src/resources/showdown.js index 49716fd3..beaa4607 100644 --- a/src/resources/showdown.js +++ b/src/resources/showdown.js @@ -151,12 +151,18 @@ var updateText = function(text) { insertImageCaption(); highlightCodeBlocks(document, VEnableMermaid); renderMermaid('language-mermaid'); + + // If you add new logics after handling MathJax, please pay attention to + // finishLoading logic. if (VEnableMathjax) { try { - MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder]); + MathJax.Hub.Queue(["Typeset", MathJax.Hub, placeholder, finishLoading]); } catch (err) { content.setLog("err: " + err); + finishLoading(); } + } else { + finishLoading(); } }; diff --git a/src/src.pro b/src/src.pro index 7c6d578c..8ebf24c2 100644 --- a/src/src.pro +++ b/src/src.pro @@ -61,7 +61,8 @@ SOURCES += main.cpp\ vorphanfile.cpp \ vcodeblockhighlighthelper.cpp \ vwebview.cpp \ - vimagepreviewer.cpp + vimagepreviewer.cpp \ + vexporter.cpp HEADERS += vmainwindow.h \ vdirectorytree.h \ @@ -109,7 +110,8 @@ HEADERS += vmainwindow.h \ vorphanfile.h \ vcodeblockhighlighthelper.h \ vwebview.h \ - vimagepreviewer.h + vimagepreviewer.h \ + vexporter.h RESOURCES += \ vnote.qrc \ diff --git a/src/utils/vutils.cpp b/src/utils/vutils.cpp index 0c3a87f8..64a5475c 100644 --- a/src/utils/vutils.cpp +++ b/src/utils/vutils.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include "vfile.h" @@ -441,3 +442,16 @@ QString VUtils::getLocale() } return locale; } + +void VUtils::sleepWait(int p_milliseconds) +{ + if (p_milliseconds <= 0) { + return; + } + + QElapsedTimer t; + t.start(); + while (t.elapsed() < p_milliseconds) { + QCoreApplication::processEvents(); + } +} diff --git a/src/utils/vutils.h b/src/utils/vutils.h index ea6b967c..feafe45f 100644 --- a/src/utils/vutils.h +++ b/src/utils/vutils.h @@ -92,6 +92,8 @@ public: static QChar keyToChar(int p_key); static QString getLocale(); + static void sleepWait(int p_milliseconds); + // Regular expression for image link. // ![image title]( http://github.com/tamlok/vnote.jpg "alt \" text" ) // Captured texts (need to be trimmed): diff --git a/src/vconstants.h b/src/vconstants.h index 0fb94e05..046afec6 100644 --- a/src/vconstants.h +++ b/src/vconstants.h @@ -14,4 +14,8 @@ static const qreal c_webZoomFactorMax = 5; static const qreal c_webZoomFactorMin = 0.25; static const int c_tabSequenceBase = 1; + +// HTML and JS. +static const QString c_htmlJSHolder = "JS_PLACE_HOLDER"; +static const QString c_htmlExtraHolder = ""; #endif diff --git a/src/vdocument.cpp b/src/vdocument.cpp index ee5f874f..fa0734b6 100644 --- a/src/vdocument.cpp +++ b/src/vdocument.cpp @@ -9,7 +9,9 @@ VDocument::VDocument(const VFile *v_file, QObject *p_parent) void VDocument::updateText() { - emit textChanged(m_file->getContent()); + if (m_file) { + emit textChanged(m_file->getContent()); + } } void VDocument::setToc(const QString &toc) @@ -74,3 +76,14 @@ void VDocument::noticeReadyToHighlightText() { emit readyToHighlightText(); } + +void VDocument::setFile(const VFile *p_file) +{ + m_file = p_file; +} + +void VDocument::finishLoading() +{ + qDebug() << "Web side finished loading"; + emit loadFinished(); +} diff --git a/src/vdocument.h b/src/vdocument.h index 0f8a0d51..3d376017 100644 --- a/src/vdocument.h +++ b/src/vdocument.h @@ -14,6 +14,7 @@ class VDocument : public QObject Q_PROPERTY(QString html MEMBER m_html NOTIFY htmlChanged) public: + // @p_file could be NULL. VDocument(const VFile *p_file, QObject *p_parent = 0); QString getToc(); void scrollToAnchor(const QString &anchor); @@ -22,6 +23,8 @@ public: // Use p_id to identify the result. void highlightTextAsync(const QString &p_text, int p_id, int p_timeStamp); + void setFile(const VFile *p_file); + public slots: // Will be called in the HTML side void setToc(const QString &toc); @@ -32,6 +35,9 @@ public slots: void highlightTextCB(const QString &p_html, int p_id, int p_timeStamp); void noticeReadyToHighlightText(); + // Page is finished loading. + void finishLoading(); + signals: void textChanged(const QString &text); void tocChanged(const QString &toc); @@ -43,6 +49,7 @@ signals: void requestHighlightText(const QString &p_text, int p_id, int p_timeStamp); void textHighlighted(const QString &p_html, int p_id, int p_timeStamp); void readyToHighlightText(); + void loadFinished(); private: QString m_toc; diff --git a/src/vedittab.cpp b/src/vedittab.cpp index ecd68664..defc72df 100644 --- a/src/vedittab.cpp +++ b/src/vedittab.cpp @@ -155,25 +155,14 @@ void VEditTab::scrollPreviewToHeader(int p_outlineIndex) void VEditTab::previewByConverter() { VMarkdownConverter mdConverter; - const QString &content = m_file->getContent(); - QString html = mdConverter.generateHtml(content, vconfig.getMarkdownExtensions()); - QRegularExpression tocExp("

\\[TOC\\]<\\/p>", QRegularExpression::CaseInsensitiveOption); - QString toc = mdConverter.generateToc(content, vconfig.getMarkdownExtensions()); - processHoedownToc(toc); - html.replace(tocExp, toc); + QString toc; + QString html = mdConverter.generateHtml(m_file->getContent(), + vconfig.getMarkdownExtensions(), + toc); document.setHtml(html); updateTocFromHtml(toc); } -void VEditTab::processHoedownToc(QString &p_toc) -{ - // Hoedown will add '\n'. - p_toc.replace("\n", ""); - // Hoedown will translate `_` in title to ``. - p_toc.replace("", "_"); - p_toc.replace("", "_"); -} - void VEditTab::showFileEditMode() { if (!m_file->isModifiable()) { @@ -294,8 +283,8 @@ void VEditTab::discardAndRead() void VEditTab::setupMarkdownPreview() { - const QString jsHolder("JS_PLACE_HOLDER"); - const QString extraHolder(""); + const QString &jsHolder = c_htmlJSHolder; + const QString &extraHolder = c_htmlExtraHolder; webPreviewer = new VWebView(m_file, this); connect(webPreviewer, &VWebView::editNote, @@ -373,24 +362,7 @@ void VEditTab::setupMarkdownPreview() htmlTemplate.replace(extraHolder, extraFile); } - // Need to judge the path: Url, local file, resource file. - QUrl baseUrl; - QString basePath = m_file->retriveBasePath(); - QFileInfo pathInfo(basePath); - if (pathInfo.exists()) { - if (pathInfo.isNativePath()) { - // Local file. - baseUrl = QUrl::fromLocalFile(basePath + QDir::separator()); - } else { - // Resource file. - baseUrl = QUrl("qrc" + basePath + QDir::separator()); - } - } else { - // Url. - baseUrl = QUrl(basePath + QDir::separator()); - } - - webPreviewer->setHtml(htmlTemplate, baseUrl); + webPreviewer->setHtml(htmlTemplate, m_file->getBaseUrl()); addWidget(webPreviewer); } @@ -733,3 +705,7 @@ VWebView *VEditTab::getWebViewer() const return webPreviewer; } +MarkdownConverterType VEditTab::getMarkdownConverterType() const +{ + return mdConverterType; +} diff --git a/src/vedittab.h b/src/vedittab.h index a1054df3..8c2fd8b1 100644 --- a/src/vedittab.h +++ b/src/vedittab.h @@ -51,6 +51,8 @@ public: VWebView *getWebViewer() const; + MarkdownConverterType getMarkdownConverterType() const; + public slots: // Enter edit mode void editFile(); @@ -82,7 +84,6 @@ private: void showFileEditMode(); void setupMarkdownPreview(); void previewByConverter(); - void processHoedownToc(QString &p_toc); inline bool isChild(QObject *obj); void parseTocUl(QXmlStreamReader &xml, QVector &headers, int level); void parseTocLi(QXmlStreamReader &xml, QVector &headers, int level); diff --git a/src/vexporter.cpp b/src/vexporter.cpp new file mode 100644 index 00000000..ffc90b2f --- /dev/null +++ b/src/vexporter.cpp @@ -0,0 +1,404 @@ +#include "vexporter.h" + +#include +#include +#include +#include +#include +#include +#include + +#ifndef QT_NO_PRINTER +#include +#include +#endif + +#include "vconfigmanager.h" +#include "utils/vutils.h" +#include "vfile.h" +#include "vwebview.h" +#include "vpreviewpage.h" +#include "vconstants.h" +#include "vnote.h" +#include "vmarkdownconverter.h" + +extern VConfigManager vconfig; + +QString VExporter::s_defaultPathDir = QDir::homePath(); + +VExporter::VExporter(MarkdownConverterType p_mdType, QWidget *p_parent) + : QDialog(p_parent), m_document(NULL, this), m_mdType(p_mdType), + m_file(NULL), m_type(ExportType::PDF), m_source(ExportSource::Invalid), + m_webReady(false), m_state(ExportState::Idle), + m_pageLayout(QPageLayout(QPageSize(QPageSize::A4), QPageLayout::Portrait, QMarginsF(0.0, 0.0, 0.0, 0.0))) +{ + setupUI(); +} + +void VExporter::setupUI() +{ + setupMarkdownViewer(); + + m_infoLabel = new QLabel(); + m_infoLabel->setWordWrap(true); + + // Target file path. + QLabel *pathLabel = new QLabel(tr("Target &path:")); + m_pathEdit = new QLineEdit(); + pathLabel->setBuddy(m_pathEdit); + m_browseBtn = new QPushButton(tr("&Browse")); + connect(m_browseBtn, &QPushButton::clicked, + this, &VExporter::handleBrowseBtnClicked); + + // Page layout. + QLabel *layoutLabel = new QLabel(tr("Page layout:")); + m_layoutLabel = new QLabel(); + m_layoutBtn = new QPushButton(tr("&Settings")); + +#ifndef QT_NO_PRINTER + connect(m_layoutBtn, &QPushButton::clicked, + this, &VExporter::handleLayoutBtnClicked); +#else + m_layoutBtn->hide(); +#endif + + // Progress. + m_proLabel = new QLabel(this); + m_proBar = new QProgressBar(this); + + // Ok is the default button. + m_btnBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + connect(m_btnBox, &QDialogButtonBox::accepted, this, &VExporter::startExport); + connect(m_btnBox, &QDialogButtonBox::rejected, this, &VExporter::cancelExport); + + QPushButton *okBtn = m_btnBox->button(QDialogButtonBox::Ok); + m_pathEdit->setMinimumWidth(okBtn->sizeHint().width() * 3); + + QGridLayout *mainLayout = new QGridLayout(); + mainLayout->addWidget(m_webViewer, 0, 0, 1, 3); + mainLayout->addWidget(m_infoLabel, 1, 0, 1, 3); + mainLayout->addWidget(pathLabel, 2, 0); + mainLayout->addWidget(m_pathEdit, 2, 1); + mainLayout->addWidget(m_browseBtn, 2, 2); + mainLayout->addWidget(layoutLabel, 3, 0); + mainLayout->addWidget(m_layoutLabel, 3, 1); + mainLayout->addWidget(m_layoutBtn, 3, 2); + mainLayout->addWidget(m_proLabel, 4, 1, 1, 2); + mainLayout->addWidget(m_proBar, 5, 1, 1, 2); + mainLayout->addWidget(m_btnBox, 6, 1, 1, 2); + + // Only use VWebView to do the conversion. + m_webViewer->hide(); + + m_proLabel->hide(); + m_proBar->hide(); + + setLayout(mainLayout); + mainLayout->setSizeConstraint(QLayout::SetFixedSize); + setWindowTitle(tr("Export Note")); + + updatePageLayoutLabel(); +} + +static QString exportTypeStr(ExportType p_type) +{ + if (p_type == ExportType::PDF) { + return "PDF"; + } else { + return "HTML"; + } +} + +void VExporter::handleBrowseBtnClicked() +{ + QFileInfo fi(getFilePath()); + QString fileType = m_type == ExportType::PDF ? + tr("Portable Document Format (*.pdf)") : + tr("WebPage, Complete (*.html)"); + QString path = QFileDialog::getSaveFileName(this, tr("Export As"), + fi.absolutePath(), + fileType); + if (path.isEmpty()) { + return; + } + + setFilePath(path); + s_defaultPathDir = VUtils::basePathFromPath(path); +} + +void VExporter::handleLayoutBtnClicked() +{ +#ifndef QT_NO_PRINTER + QPrinter printer; + printer.setPageLayout(m_pageLayout); + + QPageSetupDialog dlg(&printer, this); + if (dlg.exec() != QDialog::Accepted) { + return; + } + + m_pageLayout.setPageSize(printer.pageLayout().pageSize()); + m_pageLayout.setOrientation(printer.pageLayout().orientation()); + + updatePageLayoutLabel(); +#endif +} + +void VExporter::updatePageLayoutLabel() +{ + m_layoutLabel->setText(QString("%1, %2").arg(m_pageLayout.pageSize().name()) + .arg(m_pageLayout.orientation() == QPageLayout::Portrait ? + tr("Portrait") : tr("Landscape"))); +} + +QString VExporter::getFilePath() const +{ + return QDir::cleanPath(m_pathEdit->text()); +} + +void VExporter::setFilePath(const QString &p_path) +{ + m_pathEdit->setText(QDir::toNativeSeparators(p_path)); +} + +void VExporter::exportNote(VFile *p_file, ExportType p_type) +{ + m_file = p_file; + m_type = p_type; + m_source = ExportSource::Note; + + if (!m_file || m_file->getDocType() != DocType::Markdown) { + // Do not support non-Markdown note now. + m_btnBox->button(QDialogButtonBox::Ok)->setEnabled(false); + return; + } + + m_infoLabel->setText(tr("Export note %2 as %3.") + .arg(vconfig.c_dataTextStyle) + .arg(m_file->getName()) + .arg(exportTypeStr(p_type))); + + setWindowTitle(tr("Export As %1").arg(exportTypeStr(p_type))); + + setFilePath(QDir(s_defaultPathDir).filePath(QFileInfo(p_file->retrivePath()).baseName() + + "." + exportTypeStr(p_type).toLower())); +} + +void VExporter::setupMarkdownViewer() +{ + m_webViewer = new VWebView(NULL, this); + VPreviewPage *page = new VPreviewPage(this); + m_webViewer->setPage(page); + + QWebChannel *channel = new QWebChannel(this); + channel->registerObject(QStringLiteral("content"), &m_document); + page->setWebChannel(channel); + + connect(&m_document, &VDocument::loadFinished, + this, &VExporter::readyToExport); + + QString jsFile, extraFile; + switch (m_mdType) { + case MarkdownConverterType::Marked: + jsFile = "qrc" + VNote::c_markedJsFile; + extraFile = "\n"; + break; + + case MarkdownConverterType::Hoedown: + jsFile = "qrc" + VNote::c_hoedownJsFile; + // Use Marked to highlight code blocks. + extraFile = "\n"; + break; + + case MarkdownConverterType::MarkdownIt: + jsFile = "qrc" + VNote::c_markdownitJsFile; + extraFile = "\n" + + "\n" + + "\n"; + break; + + case MarkdownConverterType::Showdown: + jsFile = "qrc" + VNote::c_showdownJsFile; + extraFile = "\n" + + "\n"; + + break; + + default: + Q_ASSERT(false); + } + + if (vconfig.getEnableMermaid()) { + extraFile += "\n" + "\n" + + "\n"; + } + + if (vconfig.getEnableMathjax()) { + extraFile += "\n" + "\n" + + "\n"; + } + + if (vconfig.getEnableImageCaption()) { + extraFile += "\n"; + } + + m_htmlTemplate = VNote::s_markdownTemplatePDF; + m_htmlTemplate.replace(c_htmlJSHolder, jsFile); + if (!extraFile.isEmpty()) { + m_htmlTemplate.replace(c_htmlExtraHolder, extraFile); + } +} + +void VExporter::updateWebViewer(VFile *p_file) +{ + m_document.setFile(p_file); + + // Need to generate HTML using Hoedown. + if (m_mdType == MarkdownConverterType::Hoedown) { + VMarkdownConverter mdConverter; + QString toc; + QString html = mdConverter.generateHtml(p_file->getContent(), + vconfig.getMarkdownExtensions(), + toc); + m_document.setHtml(html); + } + + m_webViewer->setHtml(m_htmlTemplate, p_file->getBaseUrl()); +} + +void VExporter::readyToExport() +{ + Q_ASSERT(!m_webReady); + m_webReady = true; +} + +void VExporter::startExport() +{ + enableUserInput(false); + V_ASSERT(m_state == ExportState::Idle); + m_state = ExportState::Busy; + + if (m_source == ExportSource::Note) { + V_ASSERT(m_file); + bool isOpened = m_file->isOpened(); + if (!isOpened && !m_file->open()) { + goto exit; + } + + m_webReady = false; + updateWebViewer(m_file); + + // Update progress info. + m_proLabel->setText(tr("Exporting %1").arg(m_file->getName())); + m_proBar->setMinimum(0); + m_proBar->setMaximum(100); + m_proBar->reset(); + m_proLabel->show(); + m_proBar->show(); + + while (!m_webReady) { + VUtils::sleepWait(100); + if (m_proBar->value() < 70) { + m_proBar->setValue(m_proBar->value() + 1); + } + + if (m_state == ExportState::Cancelled) { + goto exit; + } + } + + // Wait to ensure Web side is really ready. + VUtils::sleepWait(200); + + if (m_state == ExportState::Cancelled) { + goto exit; + } + + m_proBar->setValue(80); + + exportToPDF(m_webViewer, getFilePath(), m_pageLayout); + + m_proBar->setValue(100); + + m_webReady = false; + + if (!isOpened) { + m_file->close(); + } + } + +exit: + m_proLabel->setText(""); + m_proBar->reset(); + m_proLabel->hide(); + m_proBar->hide(); + enableUserInput(true); + + if (m_state == ExportState::Cancelled) { + reject(); + } else { + accept(); + } + + m_state = ExportState::Idle; +} + +void VExporter::cancelExport() +{ + if (m_state == ExportState::Idle) { + reject(); + } else { + m_state = ExportState::Cancelled; + } +} + +void VExporter::exportToPDF(VWebView *p_webViewer, const QString &p_filePath, + const QPageLayout &p_layout) +{ + int pdfPrinted = 0; + p_webViewer->page()->printToPdf([&, this](const QByteArray &p_result) { + if (p_result.isEmpty() || this->m_state == ExportState::Cancelled) { + pdfPrinted = -1; + return; + } + + V_ASSERT(!p_filePath.isEmpty()); + + QFile file(p_filePath); + + if (!file.open(QFile::WriteOnly)) { + pdfPrinted = -1; + return; + } + + file.write(p_result.data(), p_result.size()); + file.close(); + + pdfPrinted = 1; + }, p_layout); + + while (pdfPrinted == 0) { + VUtils::sleepWait(100); + + if (m_state == ExportState::Cancelled) { + break; + } + } + + qDebug() << "export to PDF" << p_filePath << "state" << pdfPrinted; +} + +void VExporter::enableUserInput(bool p_enabled) +{ + m_btnBox->button(QDialogButtonBox::Ok)->setEnabled(p_enabled); + m_pathEdit->setEnabled(p_enabled); + m_browseBtn->setEnabled(p_enabled); + m_layoutBtn->setEnabled(p_enabled); +} diff --git a/src/vexporter.h b/src/vexporter.h new file mode 100644 index 00000000..76ded249 --- /dev/null +++ b/src/vexporter.h @@ -0,0 +1,101 @@ +#ifndef VEXPORTER_H +#define VEXPORTER_H + +#include +#include +#include +#include "vconfigmanager.h" +#include "vdocument.h" + +class VWebView; +class VFile; +class QLineEdit; +class QLabel; +class QDialogButtonBox; +class QPushButton; +class QProgressBar; + +enum class ExportType +{ + PDF = 0, + HTML +}; + +enum class ExportSource +{ + Note = 0, + Directory, + Notebook, + Invalid +}; + +enum class ExportState +{ + Idle = 0, + Cancelled, + Busy +}; + +class VExporter : public QDialog +{ + Q_OBJECT +public: + explicit VExporter(MarkdownConverterType p_mdType = MarkdownIt, QWidget *p_parent = 0); + + void exportNote(VFile *p_file, ExportType p_type); + +private slots: + void handleBrowseBtnClicked(); + void handleLayoutBtnClicked(); + void startExport(); + void cancelExport(); + +private: + void setupUI(); + + // Init m_webViewer, m_document, and m_htmlTemplate. + void setupMarkdownViewer(); + + void updatePageLayoutLabel(); + + void setFilePath(const QString &p_path); + + QString getFilePath() const; + + void updateWebViewer(VFile *p_file); + + void readyToExport(); + + void enableUserInput(bool p_enabled); + + void exportToPDF(VWebView *p_webViewer, const QString &p_filePath, const QPageLayout &p_layout); + + VWebView *m_webViewer; + VDocument m_document; + MarkdownConverterType m_mdType; + QString m_htmlTemplate; + VFile *m_file; + ExportType m_type; + ExportSource m_source; + bool m_webReady; + + ExportState m_state; + + QLabel *m_infoLabel; + QLineEdit *m_pathEdit; + QPushButton *m_browseBtn; + QLabel *m_layoutLabel; + QPushButton *m_layoutBtn; + QDialogButtonBox *m_btnBox; + + // Progress label and bar. + QLabel *m_proLabel; + QProgressBar *m_proBar; + + QPageLayout m_pageLayout; + + // The default directory. + static QString s_defaultPathDir; +}; + +#endif // VEXPORTER_H diff --git a/src/vfile.cpp b/src/vfile.cpp index 3e0cc1ed..31b5cafe 100644 --- a/src/vfile.cpp +++ b/src/vfile.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "utils/vutils.h" VFile::VFile(const QString &p_name, QObject *p_parent, @@ -214,3 +215,25 @@ bool VFile::isInternalImageFolder(const QString &p_path) const { return VUtils::basePathFromPath(p_path) == getDirectory()->retrivePath(); } + +QUrl VFile::getBaseUrl() const +{ + // Need to judge the path: Url, local file, resource file. + QUrl baseUrl; + QString basePath = retriveBasePath(); + QFileInfo pathInfo(basePath); + if (pathInfo.exists()) { + if (pathInfo.isNativePath()) { + // Local file. + baseUrl = QUrl::fromLocalFile(basePath + QDir::separator()); + } else { + // Resource file. + baseUrl = QUrl("qrc" + basePath + QDir::separator()); + } + } else { + // Url. + baseUrl = QUrl(basePath + QDir::separator()); + } + + return baseUrl; +} diff --git a/src/vfile.h b/src/vfile.h index 280cab0e..fc24a3b1 100644 --- a/src/vfile.h +++ b/src/vfile.h @@ -3,6 +3,7 @@ #include #include +#include #include "vdirectory.h" #include "vconstants.h" @@ -40,6 +41,9 @@ public: bool isOpened() const; FileType getType() const; + // Return the base URL for this file when loaded in VWebView. + QUrl getBaseUrl() const; + // Whether the directory @p_path is an internal image folder of this file. // It is true only when the folder is in the same directory as the parent // directory of this file. diff --git a/src/vmainwindow.cpp b/src/vmainwindow.cpp index d216ebc8..65599071 100644 --- a/src/vmainwindow.cpp +++ b/src/vmainwindow.cpp @@ -18,6 +18,7 @@ #include "vcaptain.h" #include "vedittab.h" #include "vwebview.h" +#include "vexporter.h" extern VConfigManager vconfig; @@ -453,6 +454,18 @@ void VMainWindow::initFileMenu() fileMenu->addSeparator(); + // Export as PDF. + m_exportAsPDFAct = new QAction(QIcon(":/resources/icons/export_pdf.svg"), + tr("Export As &PDF"), this); + m_exportAsPDFAct->setToolTip(tr("Export current note as PDF file")); + connect(m_exportAsPDFAct, &QAction::triggered, + this, &VMainWindow::exportAsPDF); + m_exportAsPDFAct->setEnabled(false); + + fileMenu->addAction(m_exportAsPDFAct); + + fileMenu->addSeparator(); + // Print. m_printAct = new QAction(QIcon(":/resources/icons/print.svg"), tr("&Print"), this); @@ -1016,6 +1029,7 @@ void VMainWindow::updateActionStateFromTabStatusChange(const VFile *p_file, bool p_editMode) { m_printAct->setEnabled(p_file && p_file->getDocType() == DocType::Markdown); + m_exportAsPDFAct->setEnabled(p_file && p_file->getDocType() == DocType::Markdown); editNoteAct->setVisible(p_file && p_file->isModifiable() && !p_editMode); discardExitAct->setVisible(p_file && p_editMode); @@ -1393,3 +1407,13 @@ void VMainWindow::printNote() } } +void VMainWindow::exportAsPDF() +{ + V_ASSERT(m_curTab); + V_ASSERT(m_curFile); + + VExporter exporter(m_curTab->getMarkdownConverterType(), this); + exporter.exportNote(m_curFile, ExportType::PDF); + exporter.exec(); +} + diff --git a/src/vmainwindow.h b/src/vmainwindow.h index 117167df..097c3048 100644 --- a/src/vmainwindow.h +++ b/src/vmainwindow.h @@ -81,6 +81,7 @@ private slots: void enableImageConstraint(bool p_checked); void enableImageCaption(bool p_checked); void printNote(); + void exportAsPDF(); protected: void closeEvent(QCloseEvent *event) Q_DECL_OVERRIDE; @@ -155,6 +156,7 @@ private: QAction *expandViewAct; QAction *m_importNoteAct; QAction *m_printAct; + QAction *m_exportAsPDFAct; QAction *m_insertImageAct; QAction *m_findReplaceAct; diff --git a/src/vmarkdownconverter.cpp b/src/vmarkdownconverter.cpp index 1f85d1a8..8d262845 100644 --- a/src/vmarkdownconverter.cpp +++ b/src/vmarkdownconverter.cpp @@ -1,4 +1,5 @@ #include "vmarkdownconverter.h" +#include VMarkdownConverter::VMarkdownConverter() { @@ -36,11 +37,35 @@ QString VMarkdownConverter::generateHtml(const QString &markdown, hoedown_extens return html; } +QString VMarkdownConverter::generateHtml(const QString &markdown, hoedown_extensions options, QString &toc) +{ + if (markdown.isEmpty()) { + return QString(); + } + + QString html = generateHtml(markdown, options); + QRegularExpression tocExp("

\\[TOC\\]<\\/p>", QRegularExpression::CaseInsensitiveOption); + toc = generateToc(markdown, options); + html.replace(tocExp, toc); + + return html; +} + +static void processToc(QString &p_toc) +{ + // Hoedown will add '\n'. + p_toc.replace("\n", ""); + // Hoedown will translate `_` in title to ``. + p_toc.replace("", "_"); + p_toc.replace("", "_"); +} + QString VMarkdownConverter::generateToc(const QString &markdown, hoedown_extensions options) { if (markdown.isEmpty()) { return QString(); } + hoedown_document *document = hoedown_document_new(tocRenderer, options, nestingLevel); QByteArray data = markdown.toUtf8(); hoedown_buffer *outBuf = hoedown_buffer_new(16); @@ -48,5 +73,8 @@ QString VMarkdownConverter::generateToc(const QString &markdown, hoedown_extensi hoedown_document_free(document); QString toc = QString::fromUtf8(hoedown_buffer_cstr(outBuf)); hoedown_buffer_free(outBuf); + + processToc(toc); + return toc; } diff --git a/src/vmarkdownconverter.h b/src/vmarkdownconverter.h index 84fe4bf3..cdadfe25 100644 --- a/src/vmarkdownconverter.h +++ b/src/vmarkdownconverter.h @@ -14,10 +14,13 @@ public: VMarkdownConverter(); ~VMarkdownConverter(); - QString generateHtml(const QString &markdown, hoedown_extensions options); + QString generateHtml(const QString &markdown, hoedown_extensions options, QString &toc); + QString generateToc(const QString &markdown, hoedown_extensions options); private: + QString generateHtml(const QString &markdown, hoedown_extensions options); + // VMarkdownDocument *generateDocument(const QString &markdown); hoedown_html_flags hoedownHtmlFlags; int nestingLevel; diff --git a/src/vnote.cpp b/src/vnote.cpp index 1e72629e..f4db89e0 100644 --- a/src/vnote.cpp +++ b/src/vnote.cpp @@ -16,6 +16,7 @@ extern VConfigManager vconfig; QString VNote::s_markdownTemplate; +QString VNote::s_markdownTemplatePDF; const QString VNote::c_hoedownJsFile = ":/resources/hoedown.js"; const QString VNote::c_markedJsFile = ":/resources/marked.js"; @@ -170,14 +171,22 @@ void VNote::updateTemplate() cssStyle += "img { max-width: 100% !important; height: auto !important; }\n"; } - QString styleHolder(""); - QString cssHolder("CSS_PLACE_HOLDER"); + const QString styleHolder(""); + const QString cssHolder("CSS_PLACE_HOLDER"); s_markdownTemplate = VUtils::readFileFromDisk(c_markdownTemplatePath); s_markdownTemplate.replace(cssHolder, vconfig.getTemplateCssUrl()); + + s_markdownTemplatePDF = s_markdownTemplate; + if (!cssStyle.isEmpty()) { s_markdownTemplate.replace(styleHolder, cssStyle); } + + // Shoudl not display scrollbar in PDF. + cssStyle += "pre code { white-space: pre-wrap !important; " + "word-break: break-all !important; }\n"; + s_markdownTemplatePDF.replace(styleHolder, cssStyle); } const QVector &VNote::getNotebooks() const diff --git a/src/vnote.h b/src/vnote.h index 3769f305..4239ad3d 100644 --- a/src/vnote.h +++ b/src/vnote.h @@ -28,6 +28,7 @@ public: void initTemplate(); static QString s_markdownTemplate; + static QString s_markdownTemplatePDF; // Hoedown static const QString c_hoedownJsFile; diff --git a/src/vwebview.cpp b/src/vwebview.cpp index 2540e825..05ea4d5c 100644 --- a/src/vwebview.cpp +++ b/src/vwebview.cpp @@ -20,7 +20,7 @@ void VWebView::contextMenuEvent(QContextMenuEvent *p_event) const QList actions = menu->actions(); - if (!hasSelection() && m_file->isModifiable()) { + if (!hasSelection() && m_file && m_file->isModifiable()) { QAction *editAct= new QAction(QIcon(":/resources/icons/edit_note.svg"), tr("&Edit"), this); editAct->setToolTip(tr("Edit current note")); diff --git a/src/vwebview.h b/src/vwebview.h index 4d65a59d..3780397d 100644 --- a/src/vwebview.h +++ b/src/vwebview.h @@ -9,6 +9,7 @@ class VWebView : public QWebEngineView { Q_OBJECT public: + // @p_file could be NULL. explicit VWebView(VFile *p_file, QWidget *p_parent = Q_NULLPTR); signals: