diff --git a/src/core/configmgr.cpp b/src/core/configmgr.cpp index df08bb62..5cca0ec8 100644 --- a/src/core/configmgr.cpp +++ b/src/core/configmgr.cpp @@ -24,7 +24,7 @@ using namespace vnotex; #ifndef QT_NO_DEBUG -// #define VX_DEBUG_WEB + // #define VX_DEBUG_WEB #endif const QString ConfigMgr::c_orgName = "VNote"; diff --git a/src/core/htmltemplatehelper.cpp b/src/core/htmltemplatehelper.cpp index 2c199af4..8bb809ee 100644 --- a/src/core/htmltemplatehelper.cpp +++ b/src/core/htmltemplatehelper.cpp @@ -15,8 +15,6 @@ using namespace vnotex; HtmlTemplateHelper::Template HtmlTemplateHelper::s_markdownViewerTemplate; -static const QString c_globalStylesPlaceholder = "/* VX_GLOBAL_STYLES_PLACEHOLDER */"; - QString WebGlobalOptions::toJavascriptObject() const { return QStringLiteral("window.vxOptions = {\n") @@ -35,6 +33,7 @@ QString WebGlobalOptions::toJavascriptObject() const + QString("bodyHeight: %1,\n").arg(m_bodyHeight) + QString("transformSvgToPngEnabled: %1,\n").arg(Utils::boolToString(m_transformSvgToPngEnabled)) + QString("mathJaxScale: %1,\n").arg(m_mathJaxScale) + + QString("removeCodeToolBarEnabled: %1,\n").arg(Utils::boolToString(m_removeCodeToolBarEnabled)) + QString("sectionNumberBaseLevel: %1\n").arg(m_sectionNumberBaseLevel) + QStringLiteral("}"); } @@ -59,7 +58,7 @@ static void fillGlobalStyles(QString &p_template, const WebResource &p_resource, styles += p_additionalStyles; if (!styles.isEmpty()) { - p_template.replace(c_globalStylesPlaceholder, styles); + p_template.replace("/* VX_GLOBAL_STYLES_PLACEHOLDER */", styles); } } @@ -175,22 +174,15 @@ void HtmlTemplateHelper::updateMarkdownViewerTemplate(const MarkdownEditorConfig s_markdownViewerTemplate.m_revision = p_config.revision(); + Paras paras; const auto &themeMgr = VNoteX::getInst().getThemeMgr(); - s_markdownViewerTemplate.m_template = - generateMarkdownViewerTemplate(p_config, - themeMgr.getFile(Theme::File::WebStyleSheet), - themeMgr.getFile(Theme::File::HighlightStyleSheet)); + paras.m_webStyleSheetFile = themeMgr.getFile(Theme::File::WebStyleSheet); + paras.m_highlightStyleSheetFile = themeMgr.getFile(Theme::File::HighlightStyleSheet); + + s_markdownViewerTemplate.m_template = generateMarkdownViewerTemplate(p_config, paras); } -QString HtmlTemplateHelper::generateMarkdownViewerTemplate(const MarkdownEditorConfig &p_config, - const QString &p_webStyleSheetFile, - const QString &p_highlightStyleSheetFile, - bool p_useTransparentBg, - bool p_scrollable, - int p_bodyWidth, - int p_bodyHeight, - bool p_transformSvgToPng, - qreal p_mathJaxScale) +QString HtmlTemplateHelper::generateMarkdownViewerTemplate(const MarkdownEditorConfig &p_config, const Paras &p_paras) { const auto &viewerResource = p_config.getViewerResource(); const auto templateFile = ConfigMgr::getInst().getUserOrAppFile(viewerResource.m_template); @@ -198,7 +190,7 @@ QString HtmlTemplateHelper::generateMarkdownViewerTemplate(const MarkdownEditorC fillGlobalStyles(htmlTemplate, viewerResource, ""); - fillThemeStyles(htmlTemplate, p_webStyleSheetFile, p_highlightStyleSheetFile); + fillThemeStyles(htmlTemplate, p_paras.m_webStyleSheetFile, p_paras.m_highlightStyleSheetFile); { WebGlobalOptions opts; @@ -212,12 +204,13 @@ QString HtmlTemplateHelper::generateMarkdownViewerTemplate(const MarkdownEditorC opts.m_autoBreakEnabled = p_config.getAutoBreakEnabled(); opts.m_linkifyEnabled = p_config.getLinkifyEnabled(); opts.m_indentFirstLineEnabled = p_config.getIndentFirstLineEnabled(); - opts.m_transparentBackgroundEnabled = p_useTransparentBg; - opts.m_scrollable = p_scrollable; - opts.m_bodyWidth = p_bodyWidth; - opts.m_bodyHeight = p_bodyHeight; - opts.m_transformSvgToPngEnabled = p_transformSvgToPng; - opts.m_mathJaxScale = p_mathJaxScale; + opts.m_transparentBackgroundEnabled = p_paras.m_transparentBackgroundEnabled; + opts.m_scrollable = p_paras.m_scrollable; + opts.m_bodyWidth = p_paras.m_bodyWidth; + opts.m_bodyHeight = p_paras.m_bodyHeight; + opts.m_transformSvgToPngEnabled = p_paras.m_transformSvgToPngEnabled; + opts.m_mathJaxScale = p_paras.m_mathJaxScale; + opts.m_removeCodeToolBarEnabled = p_paras.m_removeCodeToolBarEnabled; fillGlobalOptions(htmlTemplate, opts); } @@ -235,17 +228,36 @@ QString HtmlTemplateHelper::generateExportTemplate(const MarkdownEditorConfig &p fillGlobalStyles(htmlTemplate, exportResource, ""); - // Outline panel. - for (auto &ele : exportResource.m_resources) { + fillOutlinePanel(htmlTemplate, exportResource, p_addOutlinePanel); + + fillResourcesByContent(htmlTemplate, exportResource); + + return htmlTemplate; +} + +void HtmlTemplateHelper::fillOutlinePanel(QString &p_template, WebResource &p_exportResource, bool p_addOutlinePanel) +{ + for (auto &ele : p_exportResource.m_resources) { if (ele.m_name == QStringLiteral("outline")) { ele.m_enabled = p_addOutlinePanel; break; } } - fillResourcesByContent(htmlTemplate, exportResource); + // Remove static content to make the page clean. + if (!p_addOutlinePanel) { + int startIdx = p_template.indexOf(""); + QString endMark(""); + int endIdx = p_template.lastIndexOf(endMark); + Q_ASSERT(startIdx > -1 && endIdx > startIdx); + p_template.remove(startIdx, endIdx + endMark.size() - startIdx); - return htmlTemplate; + startIdx = p_template.indexOf(""); + endMark = ""; + endIdx = p_template.lastIndexOf(endMark); + Q_ASSERT(startIdx > -1 && endIdx > startIdx); + p_template.remove(startIdx, endIdx + endMark.size() - startIdx); + } } void HtmlTemplateHelper::fillTitle(QString &p_template, const QString &p_title) diff --git a/src/core/htmltemplatehelper.h b/src/core/htmltemplatehelper.h index f3b8d6c1..e49e2847 100644 --- a/src/core/htmltemplatehelper.h +++ b/src/core/htmltemplatehelper.h @@ -6,6 +6,7 @@ namespace vnotex { class MarkdownEditorConfig; + struct WebResource; // Global options to be passed to Web side at the very beginning. struct WebGlobalOptions @@ -48,6 +49,9 @@ namespace vnotex // wkhtmltopdf will make the MathJax formula too small. qreal m_mathJaxScale = -1; + // Whether remove the tool bar of code blocks added by Prism.js. + bool m_removeCodeToolBarEnabled = false; + QString toJavascriptObject() const; }; @@ -55,20 +59,33 @@ namespace vnotex class HtmlTemplateHelper { public: + struct Paras + { + QString m_webStyleSheetFile; + + QString m_highlightStyleSheetFile; + + bool m_transparentBackgroundEnabled = false; + + bool m_scrollable = true; + + int m_bodyWidth = -1; + + int m_bodyHeight = -1; + + bool m_transformSvgToPngEnabled = false; + + qreal m_mathJaxScale = -1; + + bool m_removeCodeToolBarEnabled = false; + }; + HtmlTemplateHelper() = delete; static const QString &getMarkdownViewerTemplate(); static void updateMarkdownViewerTemplate(const MarkdownEditorConfig &p_config); - static QString generateMarkdownViewerTemplate(const MarkdownEditorConfig &p_config, - const QString &p_webStyleSheetFile, - const QString &p_highlightStyleSheetFile, - bool p_useTransparentBg = false, - bool p_scrollable = true, - int p_bodyWidth = -1, - int p_bodyHeight = -1, - bool p_transformSvgToPng = false, - qreal p_mathJaxScale = -1); + static QString generateMarkdownViewerTemplate(const MarkdownEditorConfig &p_config, const Paras &p_paras); static QString generateExportTemplate(const MarkdownEditorConfig &p_config, bool p_addOutlinePanel); @@ -83,6 +100,8 @@ namespace vnotex static void fillBodyClassList(QString &p_template, const QString &p_classList); + static void fillOutlinePanel(QString &p_template, WebResource &p_exportResource, bool p_addOutlinePanel); + private: struct Template { diff --git a/src/core/notebook/node.cpp b/src/core/notebook/node.cpp index 4096431e..3afeb13c 100644 --- a/src/core/notebook/node.cpp +++ b/src/core/notebook/node.cpp @@ -413,3 +413,22 @@ bool Node::checkExists() } return after; } + +QList> Node::collectFiles() +{ + QList> files; + + load(); + + if (hasContent()) { + files.append(getContentFile()); + } + + if (isContainer()) { + for (const auto &child : m_children) { + files.append(child->collectFiles()); + } + } + + return files; +} diff --git a/src/core/notebook/node.h b/src/core/notebook/node.h index 2d48130c..91f510bc 100644 --- a/src/core/notebook/node.h +++ b/src/core/notebook/node.h @@ -179,6 +179,9 @@ namespace vnotex void sortChildren(const QVector &p_beforeIdx, const QVector &p_afterIdx); + // Get content files recursively. + QList> collectFiles(); + static bool isAncestor(const Node *p_ancestor, const Node *p_child); protected: diff --git a/src/core/notebook/notebook.cpp b/src/core/notebook/notebook.cpp index 5f27b074..b1f67283 100644 --- a/src/core/notebook/notebook.cpp +++ b/src/core/notebook/notebook.cpp @@ -361,3 +361,20 @@ QJsonObject Notebook::getExtraConfig(const QString &p_key) const const auto &configs = getExtraConfigs(); return configs.value(p_key).toObject(); } + +QList> Notebook::collectFiles() +{ + QList> files; + + auto rootNode = getRootNode(); + + const auto &children = rootNode->getChildrenRef(); + for (const auto &child : children) { + if (child->getUse() != Node::Use::Normal) { + continue; + } + files.append(child->collectFiles()); + } + + return files; +} diff --git a/src/core/notebook/notebook.h b/src/core/notebook/notebook.h index 0c3c969c..b4d84ab0 100644 --- a/src/core/notebook/notebook.h +++ b/src/core/notebook/notebook.h @@ -16,6 +16,7 @@ namespace vnotex class IVersionController; class INotebookConfigMgr; struct NodeParameters; + class File; // Base class of notebook. class Notebook : public QObject @@ -140,6 +141,9 @@ namespace vnotex QJsonObject getExtraConfig(const QString &p_key) const; virtual void setExtraConfig(const QString &p_key, const QJsonObject &p_obj) = 0; + // Get content files recursively. + QList> collectFiles(); + static const QString c_defaultAttachmentFolder; static const QString c_defaultImageFolder; diff --git a/src/core/notebook/vxnode.cpp b/src/core/notebook/vxnode.cpp index b731cff5..c7516b53 100644 --- a/src/core/notebook/vxnode.cpp +++ b/src/core/notebook/vxnode.cpp @@ -48,6 +48,7 @@ QString VXNode::fetchAbsolutePath() const QSharedPointer VXNode::getContentFile() { + Q_ASSERT(hasContent()); // We should not keep the shared ptr of VXNodeFile, or there is a cyclic ref. return QSharedPointer::create(sharedFromThis().dynamicCast()); } diff --git a/src/core/sessionconfig.cpp b/src/core/sessionconfig.cpp index 09a0bf6b..270b7f14 100644 --- a/src/core/sessionconfig.cpp +++ b/src/core/sessionconfig.cpp @@ -76,7 +76,7 @@ void SessionConfig::init() loadStateAndGeometry(sessionJobj); - m_exportOption.fromJson(sessionJobj[QStringLiteral("export_option")].toObject()); + loadExportOption(sessionJobj); m_searchOption.fromJson(sessionJobj[QStringLiteral("search_option")].toObject()); @@ -206,7 +206,7 @@ QJsonObject SessionConfig::toJson() const obj[QStringLiteral("core")] = saveCore(); obj[QStringLiteral("notebooks")] = saveNotebooks(); obj[QStringLiteral("state_geometry")] = saveStateAndGeometry(); - obj[QStringLiteral("export_option")] = m_exportOption.toJson(); + obj[QStringLiteral("export")] = saveExportOption(); obj[QStringLiteral("search_option")] = m_searchOption.toJson(); writeByteArray(obj, QStringLiteral("viewarea_session"), m_viewAreaSession); writeByteArray(obj, QStringLiteral("notebook_explorer_session"), m_notebookExplorerSession); @@ -325,6 +325,16 @@ void SessionConfig::setExportOption(const ExportOption &p_option) updateConfig(m_exportOption, p_option, this); } +const QVector &SessionConfig::getCustomExportOptions() const +{ + return m_customExportOptions; +} + +void SessionConfig::setCustomExportOptions(const QVector &p_options) +{ + updateConfig(m_customExportOptions, p_options, this); +} + const SearchOption &SessionConfig::getSearchOption() const { return m_searchOption; @@ -444,3 +454,31 @@ QJsonArray SessionConfig::saveHistory() const } return arr; } + +void SessionConfig::loadExportOption(const QJsonObject &p_session) +{ + auto exportObj = p_session[QStringLiteral("export")].toObject(); + + m_exportOption.fromJson(exportObj[QStringLiteral("export_option")].toObject()); + + auto customArr = exportObj[QStringLiteral("custom_options")].toArray(); + m_customExportOptions.resize(customArr.size()); + for (int i = 0; i < customArr.size(); ++i) { + m_customExportOptions[i].fromJson(customArr[i].toObject()); + } +} + +QJsonObject SessionConfig::saveExportOption() const +{ + QJsonObject obj; + + obj[QStringLiteral("export_option")] = m_exportOption.toJson(); + + QJsonArray customArr; + for (int i = 0; i < m_customExportOptions.size(); ++i) { + customArr.push_back(m_customExportOptions[i].toJson()); + } + obj[QStringLiteral("custom_options")] = customArr; + + return obj; +} diff --git a/src/core/sessionconfig.h b/src/core/sessionconfig.h index a57c41c4..52b72edf 100644 --- a/src/core/sessionconfig.h +++ b/src/core/sessionconfig.h @@ -107,6 +107,9 @@ namespace vnotex const ExportOption &getExportOption() const; void setExportOption(const ExportOption &p_option); + const QVector &getCustomExportOptions() const; + void setCustomExportOptions(const QVector &p_options); + const SearchOption &getSearchOption() const; void setSearchOption(const SearchOption &p_option); @@ -151,6 +154,10 @@ namespace vnotex QJsonArray saveHistory() const; + void loadExportOption(const QJsonObject &p_session); + + QJsonObject saveExportOption() const; + QString m_newNotebookDefaultRootFolderPath; // Use root folder to identify a notebook uniquely. @@ -173,6 +180,8 @@ namespace vnotex ExportOption m_exportOption; + QVector m_customExportOptions; + SearchOption m_searchOption; QByteArray m_viewAreaSession; diff --git a/src/data/extra/web/js/prism.js b/src/data/extra/web/js/prism.js index 055020b5..3899c58c 100644 --- a/src/data/extra/web/js/prism.js +++ b/src/data/extra/web/js/prism.js @@ -86,10 +86,24 @@ class PrismRenderer extends VxWorker { p_containerNode.classList.add('line-numbers'); Prism.highlightAllUnder(p_containerNode, false /* async or not */); + + // Remove the toolbar. + if (window.vxOptions.removeCodeToolBarEnabled) { + this.removeToolBar(p_containerNode); + } } this.finishWork(); } + + removeToolBar(p_containerNode) { + // Static list. + let toolBarNodes = p_containerNode.querySelectorAll('div.code-toolbar > div.toolbar'); + for (let i = 0; i < toolBarNodes.length; ++i) { + toolBarNodes[i].outerHTML = ''; + delete toolBarNodes[i]; + } + } } window.vnotex.registerWorker(new PrismRenderer()); diff --git a/src/data/extra/web/markdown-export-template.html b/src/data/extra/web/markdown-export-template.html index d2328685..999f386a 100644 --- a/src/data/extra/web/markdown-export-template.html +++ b/src/data/extra/web/markdown-export-template.html @@ -27,19 +27,24 @@
+ + +
+ + diff --git a/src/export/exportdata.cpp b/src/export/exportdata.cpp index ddb0e403..d548b58d 100644 --- a/src/export/exportdata.cpp +++ b/src/export/exportdata.cpp @@ -83,6 +83,45 @@ bool ExportPdfOption::operator==(const ExportPdfOption &p_other) const && m_wkhtmltopdfArgs == p_other.m_wkhtmltopdfArgs; } +QJsonObject ExportCustomOption::toJson() const +{ + QJsonObject obj; + obj["name"] = m_name; + obj["target_suffix"] = m_targetSuffix; + obj["command"] = m_command; + obj["use_html_input"] = m_useHtmlInput; + obj["all_in_one"] = m_allInOne; + obj["target_page_scrollable"] = m_targetPageScrollable; + obj["resource_path_separator"] = m_resourcePathSeparator; + return obj; +} + +void ExportCustomOption::fromJson(const QJsonObject &p_obj) +{ + if (p_obj.isEmpty()) { + return; + } + + m_name = p_obj["name"].toString(); + m_targetSuffix = p_obj["target_suffix"].toString(); + m_command = p_obj["command"].toString(); + m_useHtmlInput = p_obj["use_html_input"].toBool(); + m_allInOne = p_obj["all_in_one"].toBool(); + m_targetPageScrollable = p_obj["target_page_scrollable"].toBool(); + m_resourcePathSeparator = p_obj["resource_path_separator"].toString(); +} + +bool ExportCustomOption::operator==(const ExportCustomOption &p_other) const +{ + return m_name == p_other.m_name + && m_useHtmlInput == p_other.m_useHtmlInput + && m_targetSuffix == p_other.m_targetSuffix + && m_command == p_other.m_command + && m_allInOne == p_other.m_allInOne + && m_targetPageScrollable == p_other.m_targetPageScrollable + && m_resourcePathSeparator == p_other.m_resourcePathSeparator; +} + QJsonObject ExportOption::toJson() const { QJsonObject obj; @@ -93,6 +132,7 @@ QJsonObject ExportOption::toJson() const obj["export_attachments"] = m_exportAttachments; obj["html_option"] = m_htmlOption.toJson(); obj["pdf_option"] = m_pdfOption.toJson(); + obj["custom_export"] = m_customExport; return obj; } @@ -131,6 +171,7 @@ void ExportOption::fromJson(const QJsonObject &p_obj) m_exportAttachments = p_obj["export_attachments"].toBool(); m_htmlOption.fromJson(p_obj["html_option"].toObject()); m_pdfOption.fromJson(p_obj["pdf_option"].toObject()); + m_customExport = p_obj["custom_export"].toString(); } bool ExportOption::operator==(const ExportOption &p_other) const diff --git a/src/export/exportdata.h b/src/export/exportdata.h index 152ce9e7..7f26dbfc 100644 --- a/src/export/exportdata.h +++ b/src/export/exportdata.h @@ -69,6 +69,34 @@ namespace vnotex QString m_wkhtmltopdfArgs; }; + struct ExportCustomOption + { + QJsonObject toJson() const; + void fromJson(const QJsonObject &p_obj); + + bool operator==(const ExportCustomOption &p_other) const; + + QString m_name; + + QString m_targetSuffix; + + QString m_command; + + bool m_useHtmlInput = true; + + bool m_allInOne = false; + + // Whether the page of target format is scrollable. + bool m_targetPageScrollable = false; + + // The default value here follows the rules of Pandoc. +#if defined(Q_OS_WIN) + QString m_resourcePathSeparator = ";"; +#else + QString m_resourcePathSeparator = ":"; +#endif + }; + struct ExportOption { QJsonObject toJson() const; @@ -95,6 +123,15 @@ namespace vnotex ExportHtmlOption m_htmlOption; ExportPdfOption m_pdfOption; + + QString m_customExport; + + // Following fields are used in runtime only. + ExportCustomOption *m_customOption = nullptr; + + bool m_transformSvgToPngEnabled = false; + + bool m_removeCodeToolBarEnabled = true; }; inline QString exportFormatString(ExportFormat p_format) diff --git a/src/export/exporter.cpp b/src/export/exporter.cpp index 823e1eb0..7f3e09a6 100644 --- a/src/export/exporter.cpp +++ b/src/export/exporter.cpp @@ -8,9 +8,12 @@ #include #include #include +#include #include +#include #include #include "webviewexporter.h" +#include using namespace vnotex; @@ -125,10 +128,13 @@ QString Exporter::doExport(const ExportOption &p_option, Node *p_note) return outputFile; } -QString Exporter::doExportAllInOne(const ExportOption &p_option, Node *p_folder) +QString Exporter::doExportPdfAllInOne(const ExportOption &p_option, Notebook *p_notebook, Node *p_folder) { + Q_ASSERT((p_notebook || p_folder) && !(p_notebook && p_folder)); + // Make path. - const auto outputFolder = makeOutputFolder(p_option.m_outputDir, p_folder->getName()); + const auto name = p_notebook ? tr("notebook_%1").arg(p_notebook->getName()) : p_folder->getName(); + const auto outputFolder = makeOutputFolder(p_option.m_outputDir, name); if (outputFolder.isEmpty()) { emit logRequested(tr("Failed to create output folder under (%1).").arg(p_option.m_outputDir)); return QString(); @@ -143,7 +149,15 @@ QString Exporter::doExportAllInOne(const ExportOption &p_option, Node *p_folder) auto tmpOption(getExportOptionForIntermediateHtml(p_option, tmpDir.path())); - auto htmlFiles = doExport(tmpOption, tmpDir.path(), p_folder); + QStringList htmlFiles; + if (p_notebook) { + htmlFiles = doExportNotebook(tmpOption, tmpDir.path(), p_notebook); + } else { + htmlFiles = doExport(tmpOption, tmpDir.path(), p_folder); + } + + cleanUpWebViewExporter(); + if (htmlFiles.isEmpty()) { return QString(); } @@ -152,8 +166,6 @@ QString Exporter::doExportAllInOne(const ExportOption &p_option, Node *p_folder) return QString(); } - cleanUpWebViewExporter(); - auto fileName = FileUtils::generateFileNameWithSequence(outputFolder, tr("all_in_one_export"), "pdf"); @@ -166,40 +178,78 @@ QString Exporter::doExportAllInOne(const ExportOption &p_option, Node *p_folder) return QString(); } -QString Exporter::doExportAllInOne(const ExportOption &p_option, Notebook *p_notebook) +QString Exporter::doExportCustomAllInOne(const ExportOption &p_option, Notebook *p_notebook, Node *p_folder) { + Q_ASSERT((p_notebook || p_folder) && !(p_notebook && p_folder)); + // Make path. - const auto outputFolder = makeOutputFolder(p_option.m_outputDir, tr("notebook_%1").arg(p_notebook->getName())); + const auto name = p_notebook ? tr("notebook_%1").arg(p_notebook->getName()) : p_folder->getName(); + const auto outputFolder = makeOutputFolder(p_option.m_outputDir, name); if (outputFolder.isEmpty()) { emit logRequested(tr("Failed to create output folder under (%1).").arg(p_option.m_outputDir)); return QString(); } - // Export to HTML to a tmp dir first. + QStringList inputFiles; + QStringList resourcePaths; + QTemporaryDir tmpDir; - if (!tmpDir.isValid()) { - emit logRequested(tr("Failed to create temporary dir to hold HTML files.")); - return QString(); + if (p_option.m_customOption->m_useHtmlInput) { + // Export to HTML to a tmp dir first. + if (!tmpDir.isValid()) { + emit logRequested(tr("Failed to create temporary dir to hold HTML files.")); + return QString(); + } + + auto tmpOption(getExportOptionForIntermediateHtml(p_option, tmpDir.path())); + + QStringList htmlFiles; + if (p_notebook) { + htmlFiles = doExportNotebook(tmpOption, tmpDir.path(), p_notebook); + } else { + htmlFiles = doExport(tmpOption, tmpDir.path(), p_folder); + } + + cleanUpWebViewExporter(); + + if (htmlFiles.isEmpty()) { + return QString(); + } + + if (checkAskedToStop()) { + return QString(); + } + + inputFiles = htmlFiles; + for (const auto &file : htmlFiles) { + resourcePaths << PathUtils::parentDirPath(file); + } + } else { + // Collect source files. + if (p_notebook) { + collectFiles(p_notebook->collectFiles(), inputFiles, resourcePaths); + } else { + collectFiles(p_folder->collectFiles(), inputFiles, resourcePaths); + } + + if (checkAskedToStop()) { + return QString(); + } } - auto tmpOption(getExportOptionForIntermediateHtml(p_option, tmpDir.path())); - - auto htmlFiles = doExportNotebook(tmpOption, tmpDir.path(), p_notebook); - if (htmlFiles.isEmpty()) { + if (inputFiles.isEmpty()) { return QString(); } - if (checkAskedToStop()) { - return QString(); - } - - cleanUpWebViewExporter(); - auto fileName = FileUtils::generateFileNameWithSequence(outputFolder, tr("all_in_one_export"), - "pdf"); + p_option.m_customOption->m_targetSuffix); auto destFilePath = PathUtils::concatenateFilePath(outputFolder, fileName); - if (getWebViewExporter(p_option)->htmlToPdfViaWkhtmltopdf(p_option.m_pdfOption, htmlFiles, destFilePath)) { + bool success = doExportCustom(p_option, + inputFiles, + resourcePaths, + destFilePath); + if (success) { emit logRequested(tr("Exported to (%1).").arg(destFilePath)); return destFilePath; } @@ -213,8 +263,16 @@ QStringList Exporter::doExportFolder(const ExportOption &p_option, Node *p_folde QStringList outputFiles; - if (p_option.m_targetFormat == ExportFormat::PDF && p_option.m_pdfOption.m_useWkhtmltopdf && p_option.m_pdfOption.m_allInOne) { - auto file = doExportAllInOne(p_option, p_folder); + if (p_option.m_targetFormat == ExportFormat::PDF + && p_option.m_pdfOption.m_useWkhtmltopdf + && p_option.m_pdfOption.m_allInOne) { + auto file = doExportPdfAllInOne(p_option, nullptr, p_folder); + if (!file.isEmpty()) { + outputFiles << file; + } + } else if (p_option.m_targetFormat == ExportFormat::Custom + && p_option.m_customOption->m_allInOne) { + auto file = doExportCustomAllInOne(p_option, nullptr, p_folder); if (!file.isEmpty()) { outputFiles << file; } @@ -240,7 +298,15 @@ QStringList Exporter::doExport(const ExportOption &p_option, const QString &p_ou return outputFiles; } - p_folder->load(); + try { + p_folder->load(); + } catch (Exception &p_e) { + QString msg = tr("Failed to load node (%1) (%2).").arg(p_folder->fetchPath(), p_e.what()); + qWarning() << msg; + emit logRequested(msg); + return outputFiles; + } + const auto &children = p_folder->getChildrenRef(); emit progressUpdated(0, children.size()); for (int i = 0; i < children.size(); ++i) { @@ -282,6 +348,10 @@ QString Exporter::doExport(const ExportOption &p_option, const QString &p_output outputFile = doExportPdf(p_option, p_outputDir, p_file); break; + case ExportFormat::Custom: + outputFile = doExportCustom(p_option, p_outputDir, p_file); + break; + default: emit logRequested(tr("Unknown target format %1.").arg(exportFormatString(p_option.m_targetFormat))); break; @@ -302,8 +372,16 @@ QStringList Exporter::doExport(const ExportOption &p_option, Notebook *p_noteboo QStringList outputFiles; - if (p_option.m_targetFormat == ExportFormat::PDF && p_option.m_pdfOption.m_useWkhtmltopdf && p_option.m_pdfOption.m_allInOne) { - auto file = doExportAllInOne(p_option, p_notebook); + if (p_option.m_targetFormat == ExportFormat::PDF + && p_option.m_pdfOption.m_useWkhtmltopdf + && p_option.m_pdfOption.m_allInOne) { + auto file = doExportPdfAllInOne(p_option, p_notebook, nullptr); + if (!file.isEmpty()) { + outputFiles << file; + } + } else if (p_option.m_targetFormat == ExportFormat::Custom + && p_option.m_customOption->m_allInOne) { + auto file = doExportCustomAllInOne(p_option, p_notebook, nullptr); if (!file.isEmpty()) { outputFiles << file; } @@ -454,15 +532,158 @@ QString Exporter::doExportPdf(const ExportOption &p_option, const QString &p_out return outputFile; } +QString Exporter::doExportCustom(const ExportOption &p_option, const QString &p_outputDir, const File *p_file) +{ + Q_ASSERT(p_option.m_customOption); + QStringList inputFiles; + QStringList resourcePaths; + + QTemporaryDir tmpDir; + if (p_option.m_customOption->m_useHtmlInput) { + // Export to HTML to a tmp dir first. + if (!tmpDir.isValid()) { + emit logRequested(tr("Failed to create temporary dir to hold HTML files.")); + return QString(); + } + + auto tmpOption(getExportOptionForIntermediateHtml(p_option, tmpDir.path())); + auto htmlFile = doExport(tmpOption, tmpDir.path(), p_file); + if (htmlFile.isEmpty()) { + return QString(); + } + + if (checkAskedToStop()) { + return QString(); + } + + cleanUpWebViewExporter(); + + inputFiles << htmlFile; + resourcePaths << PathUtils::parentDirPath(htmlFile); + } else { + inputFiles << p_file->getContentPath(); + resourcePaths << p_file->getResourcePath(); + } + + auto fileName = FileUtils::generateFileNameWithSequence(p_outputDir, + QFileInfo(p_file->getName()).completeBaseName(), + p_option.m_customOption->m_targetSuffix); + auto destFilePath = PathUtils::concatenateFilePath(p_outputDir, fileName); + + bool success = doExportCustom(p_option, + inputFiles, + resourcePaths, + destFilePath); + if (success) { + // Copy attachments if available. + if (p_option.m_exportAttachments) { + exportAttachments(p_file->getNode(), p_file->getFilePath(), p_outputDir, destFilePath); + } + + return destFilePath; + } + + return QString(); +} + ExportOption Exporter::getExportOptionForIntermediateHtml(const ExportOption &p_option, const QString &p_outputDir) { ExportOption tmpOption(p_option); + tmpOption.m_exportAttachments = false; tmpOption.m_targetFormat = ExportFormat::HTML; + tmpOption.m_transformSvgToPngEnabled = true; + tmpOption.m_removeCodeToolBarEnabled = true; + tmpOption.m_htmlOption.m_embedStyles = true; tmpOption.m_htmlOption.m_completePage = true; tmpOption.m_htmlOption.m_embedImages = false; + tmpOption.m_htmlOption.m_useMimeHtmlFormat = false; + tmpOption.m_htmlOption.m_addOutlinePanel = false; tmpOption.m_htmlOption.m_scrollable = false; + if (p_option.m_targetFormat == ExportFormat::Custom && p_option.m_customOption->m_targetPageScrollable) { + tmpOption.m_htmlOption.m_scrollable = true; + } tmpOption.m_outputDir = p_outputDir; - return tmpOption; } + +bool Exporter::doExportCustom(const ExportOption &p_option, + const QStringList &p_files, + const QStringList &p_resourcePaths, + const QString &p_filePath) +{ + const auto cmd = evaluateCommand(p_option, + p_files, + p_resourcePaths, + p_filePath); + + emit logRequested(tr("Custom command: %1").arg(cmd)); + qDebug() << "custom export" << cmd; + + auto state = ProcessUtils::start(cmd, + [this](const QString &msg) { + emit logRequested(msg); + }, + m_askedToStop); + + return state == ProcessUtils::Succeeded; +} + +QString Exporter::evaluateCommand(const ExportOption &p_option, + const QStringList &p_files, + const QStringList &p_resourcePaths, + const QString &p_filePath) +{ + auto cmd(p_option.m_customOption->m_command); + + QString inputs; + for (int i = 0; i < p_files.size(); ++i) { + if (i > 0) { + inputs += " "; + } + + inputs += getQuotedPath(p_files[i]); + } + + QString resourcePath; + for (int i = 0; i < p_resourcePaths.size(); ++i) { + bool duplicated = false; + for (int j = 0; j < i; ++j) { + if (p_resourcePaths[j] == p_resourcePaths[i]) { + // Deduplicate. + duplicated = true; + break; + } + } + + if (duplicated) { + continue; + } + + if (i > 0) { + resourcePath += p_option.m_customOption->m_resourcePathSeparator; + } + + resourcePath += getQuotedPath(p_resourcePaths[i]); + } + + cmd.replace("%1", inputs); + cmd.replace("%2", resourcePath); + cmd.replace("%3", getQuotedPath(p_option.m_renderingStyleFile)); + cmd.replace("%4", getQuotedPath(p_option.m_syntaxHighlightStyleFile)); + cmd.replace("%5", getQuotedPath(p_filePath)); + return cmd; +} + +QString Exporter::getQuotedPath(const QString &p_path) +{ + return QString("\"%1\"").arg(QDir::toNativeSeparators(p_path)); +} + +void Exporter::collectFiles(const QList> &p_files, QStringList &p_inputFiles, QStringList &p_resourcePaths) +{ + for (const auto &file : p_files) { + p_inputFiles << file->getContentPath(); + p_resourcePaths << file->getResourcePath(); + } +} diff --git a/src/export/exporter.h b/src/export/exporter.h index 1859fcf3..3027e095 100644 --- a/src/export/exporter.h +++ b/src/export/exporter.h @@ -50,9 +50,17 @@ namespace vnotex QString doExportPdf(const ExportOption &p_option, const QString &p_outputDir, const File *p_file); - QString doExportAllInOne(const ExportOption &p_option, Node *p_folder); + QString doExportCustom(const ExportOption &p_option, const QString &p_outputDir, const File *p_file); - QString doExportAllInOne(const ExportOption &p_option, Notebook *p_notebook); + bool doExportCustom(const ExportOption &p_option, + const QStringList &p_files, + const QStringList &p_resourcePaths, + const QString &p_filePath); + + // Export @p_notebook or @p_folder. @p_folder will be considered only when @p_notebook is null. + QString doExportPdfAllInOne(const ExportOption &p_option, Notebook *p_notebook, Node *p_folder); + + QString doExportCustomAllInOne(const ExportOption &p_option, Notebook *p_notebook, Node *p_folder); void exportAttachments(Node *p_node, const QString &p_srcFilePath, @@ -71,6 +79,15 @@ namespace vnotex static ExportOption getExportOptionForIntermediateHtml(const ExportOption &p_option, const QString &p_outputDir); + static QString evaluateCommand(const ExportOption &p_option, + const QStringList &p_files, + const QStringList &p_resourcePaths, + const QString &p_filePath); + + static QString getQuotedPath(const QString &p_path); + + static void collectFiles(const QList> &p_files, QStringList &p_inputFiles, QStringList &p_resourcePaths); + // Managed by QObject. WebViewExporter *m_webViewExporter = nullptr; diff --git a/src/export/webviewexporter.cpp b/src/export/webviewexporter.cpp index d2f3c9f2..d45fb677 100644 --- a/src/export/webviewexporter.cpp +++ b/src/export/webviewexporter.cpp @@ -248,6 +248,8 @@ QSize WebViewExporter::pageLayoutSize(const QPageLayout &p_layout) const void WebViewExporter::prepare(const ExportOption &p_option) { Q_ASSERT(!m_viewer && !m_exportOngoing); + Q_ASSERT(p_option.m_targetFormat == ExportFormat::PDF || p_option.m_targetFormat == ExportFormat::HTML); + { // Adapter will be managed by MarkdownViewer. auto adapter = new MarkdownViewerAdapter(this); @@ -265,7 +267,8 @@ void WebViewExporter::prepare(const ExportOption &p_option) bool scrollable = true; if (p_option.m_targetFormat == ExportFormat::PDF - || (p_option.m_targetFormat == ExportFormat::HTML && !p_option.m_htmlOption.m_scrollable)) { + || (p_option.m_targetFormat == ExportFormat::HTML && !p_option.m_htmlOption.m_scrollable) + || (p_option.m_targetFormat == ExportFormat::Custom && !p_option.m_customOption->m_targetPageScrollable)) { scrollable = false; } @@ -276,16 +279,21 @@ void WebViewExporter::prepare(const ExportOption &p_option) useWkhtmltopdf = p_option.m_pdfOption.m_useWkhtmltopdf; pageBodySize = pageLayoutSize(*(p_option.m_pdfOption.m_layout)); } + qDebug() << "export page body size" << pageBodySize; - m_htmlTemplate = HtmlTemplateHelper::generateMarkdownViewerTemplate(config, - p_option.m_renderingStyleFile, - p_option.m_syntaxHighlightStyleFile, - p_option.m_useTransparentBg, - scrollable, - pageBodySize.width(), - pageBodySize.height(), - useWkhtmltopdf, - useWkhtmltopdf ? 2.5 : -1); + + HtmlTemplateHelper::Paras paras; + paras.m_webStyleSheetFile = p_option.m_renderingStyleFile; + paras.m_highlightStyleSheetFile = p_option.m_syntaxHighlightStyleFile; + paras.m_transparentBackgroundEnabled = p_option.m_useTransparentBg; + paras.m_scrollable = scrollable; + paras.m_bodyWidth = pageBodySize.width(); + paras.m_bodyHeight = pageBodySize.height(); + paras.m_transformSvgToPngEnabled = p_option.m_transformSvgToPngEnabled; + paras.m_mathJaxScale = useWkhtmltopdf ? 2.5 : -1; + paras.m_removeCodeToolBarEnabled = p_option.m_removeCodeToolBarEnabled; + + m_htmlTemplate = HtmlTemplateHelper::generateMarkdownViewerTemplate(config, paras); { const bool addOutlinePanel = p_option.m_targetFormat == ExportFormat::HTML && p_option.m_htmlOption.m_addOutlinePanel; diff --git a/src/utils/processutils.cpp b/src/utils/processutils.cpp index 6fb9f1b1..edf34510 100644 --- a/src/utils/processutils.cpp +++ b/src/utils/processutils.cpp @@ -128,16 +128,31 @@ ProcessUtils::State ProcessUtils::start(const QString &p_program, { QProcess proc; proc.start(p_program, p_args); + return handleProcess(&proc, p_logger, p_askedToStop); +} - if (!proc.waitForStarted()) { +ProcessUtils::State ProcessUtils::start(const QString &p_command, + const std::function &p_logger, + const bool &p_askedToStop) +{ + QProcess proc; + proc.start(p_command); + return handleProcess(&proc, p_logger, p_askedToStop); +} + +ProcessUtils::State ProcessUtils::handleProcess(QProcess *p_process, + const std::function &p_logger, + const bool &p_askedToStop) +{ + if (!p_process->waitForStarted()) { return State::FailedToStart; } - while (proc.state() != QProcess::NotRunning) { + while (p_process->state() != QProcess::NotRunning) { Utils::sleepWait(100); - auto outBa = proc.readAllStandardOutput(); - auto errBa = proc.readAllStandardError(); + auto outBa = p_process->readAllStandardOutput(); + auto errBa = p_process->readAllStandardError(); QString msg; if (!outBa.isEmpty()) { msg += QString::fromLocal8Bit(outBa); @@ -154,7 +169,7 @@ ProcessUtils::State ProcessUtils::start(const QString &p_program, } } - return proc.exitStatus() == QProcess::NormalExit ? State::Succeeded : State::Crashed; + return p_process->exitStatus() == QProcess::NormalExit ? State::Succeeded : State::Crashed; } void ProcessUtils::startDetached(const QString &p_command) diff --git a/src/utils/processutils.h b/src/utils/processutils.h index 6cffd416..6873e484 100644 --- a/src/utils/processutils.h +++ b/src/utils/processutils.h @@ -35,6 +35,10 @@ namespace vnotex const std::function &p_logger, const bool &p_askedToStop); + static State start(const QString &p_command, + const std::function &p_logger, + const bool &p_askedToStop); + static void startDetached(const QString &p_command); // Copied from QProcess code. @@ -48,6 +52,10 @@ namespace vnotex int &p_exitCodeOnSuccess, QByteArray &p_stdOut, QByteArray &p_stdErr); + + static State handleProcess(QProcess *p_process, + const std::function &p_logger, + const bool &p_askedToStop); }; } diff --git a/src/widgets/dialogs/exportdialog.cpp b/src/widgets/dialogs/exportdialog.cpp index fa4e78c2..d53e53c4 100644 --- a/src/widgets/dialogs/exportdialog.cpp +++ b/src/widgets/dialogs/exportdialog.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -33,6 +34,7 @@ #include #include #include +#include using namespace vnotex; @@ -154,6 +156,10 @@ QGroupBox *ExportDialog::setupTargetGroup(QWidget *p_parent) settings = AdvancedSettings::PDF; break; + case static_cast(ExportFormat::Custom): + settings = AdvancedSettings::Custom; + break; + default: break; } @@ -283,7 +289,9 @@ QString ExportDialog::getOutputDir() const void ExportDialog::initOptions() { // Read it from config. - m_option = ConfigMgr::getInst().getSessionConfig().getExportOption(); + const auto &sessionConfig = ConfigMgr::getInst().getSessionConfig(); + m_option = sessionConfig.getExportOption(); + m_customOptions = sessionConfig.getCustomExportOptions(); const auto &theme = VNoteX::getInst().getThemeMgr().getCurrentTheme(); m_option.m_renderingStyleFile = theme.getFile(Theme::File::WebStyleSheet); @@ -292,6 +300,10 @@ void ExportDialog::initOptions() if (m_option.m_outputDir.isEmpty()) { m_option.m_outputDir = getDefaultOutputDir(); } + + if (findCustomOption(m_option.m_customExport) == -1) { + m_option.m_customExport = m_customOptions.isEmpty() ? QString() : m_customOptions[0].m_name; + } } void ExportDialog::restoreFields(const ExportOption &p_option) @@ -351,6 +363,10 @@ void ExportDialog::saveFields(ExportOption &p_option) if (m_advancedSettings[AdvancedSettings::PDF]) { saveFields(p_option.m_pdfOption); } + + if (m_advancedSettings[AdvancedSettings::Custom]) { + saveCustomFields(p_option); + } } void ExportDialog::startExport() @@ -417,6 +433,17 @@ int ExportDialog::doExport(ExportOption p_option) appendLog(tr("Please specify a valid wkhtmltopdf executable file (%1)").arg(wkExePath)); return 0; } + + p_option.m_transformSvgToPngEnabled = true; + p_option.m_removeCodeToolBarEnabled = true; + } else if (p_option.m_targetFormat == ExportFormat::Custom) { + int optIdx = findCustomOption(p_option.m_customExport); + if (optIdx == -1) { + appendLog(tr("Please specify a valid scheme")); + return 0; + } + + p_option.m_customOption = &m_customOptions[optIdx]; } int exportedFilesCount = 0; @@ -562,6 +589,10 @@ void ExportDialog::showAdvancedSettings(AdvancedSettings p_settings) widget = getPdfAdvancedSettings(); break; + case AdvancedSettings::Custom: + widget = getCustomAdvancedSettings(); + break; + default: break; } @@ -640,7 +671,7 @@ QWidget *ExportDialog::getPdfAdvancedSettings() } { - m_allInOneCheckBox = WidgetsFactory::createCheckBox(tr("All In One"), widget); + m_allInOneCheckBox = WidgetsFactory::createCheckBox(tr("All-In-One"), widget); m_allInOneCheckBox->setToolTip(tr("Export all source files into one file")); connect(m_useWkhtmltopdfCheckBox, &QCheckBox::stateChanged, this, [this](int p_state) { @@ -686,6 +717,103 @@ QWidget *ExportDialog::getPdfAdvancedSettings() return m_advancedSettings[AdvancedSettings::PDF]; } +QWidget *ExportDialog::getCustomAdvancedSettings() +{ + if (!m_advancedSettings[AdvancedSettings::Custom]) { + QWidget *widget = new QWidget(m_advancedGroupBox); + auto layout = WidgetsFactory::createFormLayout(widget); + layout->setContentsMargins(0, 0, 0, 0); + + { + auto schemeLayout = new QHBoxLayout(); + layout->addRow(tr("Scheme:"), schemeLayout); + + m_customExportComboBox = WidgetsFactory::createComboBox(widget); + m_customExportComboBox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + schemeLayout->addWidget(m_customExportComboBox, 1); + + auto addBtn = new QPushButton(tr("New"), widget); + addBtn->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); + connect(addBtn, &QPushButton::clicked, + this, &ExportDialog::addCustomExportScheme); + schemeLayout->addWidget(addBtn); + + auto delBtn = new QPushButton(tr("Delete"), widget); + delBtn->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); + connect(delBtn, &QPushButton::clicked, + this, &ExportDialog::removeCustomExportScheme); + schemeLayout->addWidget(delBtn); + } + + { + m_targetSuffixLineEdit = WidgetsFactory::createLineEdit(widget); + m_targetSuffixLineEdit->setToolTip(tr("Suffix of the target file like docs/pdf/epub")); + m_targetSuffixLineEdit->setEnabled(false); + layout->addRow(tr("Target file suffix:"), m_targetSuffixLineEdit); + } + + { + m_resourcePathSeparatorLineEdit = WidgetsFactory::createLineEdit(widget); + m_resourcePathSeparatorLineEdit->setToolTip(tr("Separator used to concatenate resource folder paths")); + m_resourcePathSeparatorLineEdit->setEnabled(false); + layout->addRow(tr("Resource path separator:"), m_resourcePathSeparatorLineEdit); + } + + { + m_useHtmlInputCheckBox = WidgetsFactory::createCheckBox(tr("Use HTML format as input"), widget); + m_useHtmlInputCheckBox->setToolTip(tr("Convert to HTMl format first as the input of the custom export command")); + m_useHtmlInputCheckBox->setEnabled(false); + layout->addRow(m_useHtmlInputCheckBox); + } + + { + m_allInOneCheckBox = WidgetsFactory::createCheckBox(tr("All-In-One"), widget); + m_allInOneCheckBox->setToolTip(tr("Export all source files into one file")); + m_allInOneCheckBox->setEnabled(false); + layout->addRow(m_allInOneCheckBox); + } + + { + m_targetPageScrollableCheckBox = WidgetsFactory::createCheckBox(tr("Target page scrollable"), widget); + m_targetPageScrollableCheckBox->setToolTip(tr("Whether the page of the target file is scrollable")); + m_targetPageScrollableCheckBox->setEnabled(false); + layout->addRow(m_targetPageScrollableCheckBox); + } + + { + auto usage = tr("%1: List of input files.\n" + "%2: List of paths to search for images and other resources.\n" + "%3: Path of rendering CSS style sheet.\n" + "%4: Path of syntax highlighting CSS style sheet.\n" + "%5: Path of output file.\n"); + layout->addRow(tr("Command usage:"), new QLabel(usage, widget)); + } + + { + m_commandTextEdit = WidgetsFactory::createPlainTextEdit(widget); +#if defined(Q_OS_WIN) + m_commandTextEdit->setPlaceholderText("pandoc.exe --resource-path=.;%2 --css=%3 --css=%4 -s -o %5 %1"); +#else + m_commandTextEdit->setPlaceholderText("pandoc --resource-path=.:%2 --css=%3 --css=%4 -s -o %5 %1"); +#endif + m_commandTextEdit->setMaximumHeight(m_commandTextEdit->minimumSizeHint().height()); + m_commandTextEdit->setEnabled(false); + layout->addRow(tr("Command:"), m_commandTextEdit); + } + + connect(m_customExportComboBox, QOverload::of(&QComboBox::currentIndexChanged), + this, &ExportDialog::customExportCurrentSchemeChanged); + + m_advancedGroupBox->layout()->addWidget(widget); + + m_advancedSettings[AdvancedSettings::Custom] = widget; + + restoreCustomFields(m_option); + } + + return m_advancedSettings[AdvancedSettings::Custom]; +} + void ExportDialog::restoreFields(const ExportPdfOption &p_option) { m_pageLayout = p_option.m_layout; @@ -708,6 +836,42 @@ void ExportDialog::saveFields(ExportPdfOption &p_option) p_option.m_wkhtmltopdfArgs = m_wkhtmltopdfArgsLineEdit->text(); } +void ExportDialog::restoreCustomFields(const ExportOption &p_option) +{ + m_customExportComboBox->clear(); + int curIndex = -1; + for (int i = 0; i < m_customOptions.size(); ++i) { + m_customExportComboBox->addItem(m_customOptions[i].m_name, m_customOptions[i].m_name); + if (m_customOptions[i].m_name == p_option.m_customExport) { + curIndex = i; + } + } + m_customExportComboBox->setCurrentIndex(curIndex); +} + +void ExportDialog::saveCustomFields(ExportOption &p_option) +{ + p_option.m_customExport = m_customExportComboBox->currentData().toString(); + + int idx = findCustomOption(p_option.m_customExport); + if (idx > -1) { + auto &opt = m_customOptions[idx]; + opt.m_targetSuffix = m_targetSuffixLineEdit->text(); + opt.m_resourcePathSeparator = m_resourcePathSeparatorLineEdit->text(); + opt.m_useHtmlInput = m_useHtmlInputCheckBox->isChecked(); + opt.m_allInOne = m_allInOneCheckBox->isChecked(); + opt.m_targetPageScrollable = m_targetPageScrollableCheckBox->isChecked(); + + opt.m_command = m_commandTextEdit->toPlainText().trimmed(); + int lineIdx = opt.m_command.indexOf(QLatin1Char('\n')); + if (lineIdx > -1) { + opt.m_command = opt.m_command.left(lineIdx); + } + + ConfigMgr::getInst().getSessionConfig().setCustomExportOptions(m_customOptions); + } +} + void ExportDialog::updatePageLayoutButtonLabel() { Q_ASSERT(m_pageLayout); @@ -715,3 +879,107 @@ void ExportDialog::updatePageLayoutButtonLabel() QString("%1, %2").arg(m_pageLayout->pageSize().name(), m_pageLayout->orientation() == QPageLayout::Portrait ? tr("Portrait") : tr("Landscape"))); } + +int ExportDialog::findCustomOption(const QString &p_name) const +{ + if (p_name.isEmpty()) { + return -1; + } + + for (int i = 0; i < m_customOptions.size(); ++i) { + if (m_customOptions[i].m_name == p_name) { + return i; + } + } + + return -1; +} + +void ExportDialog::addCustomExportScheme() +{ + QString name; + while (true) { + name = QInputDialog::getText(this, tr("New Custom Export Scheme"), tr("Scheme name:")); + if (name.isEmpty()) { + return; + } + + if (findCustomOption(name) != -1) { + MessageBoxHelper::notify(MessageBoxHelper::Warning, + tr("Name conflicts with existing scheme."), + this); + } else { + break; + } + } + + // Based on current scheme. + ExportCustomOption newOption; + + { + int curIndex = findCustomOption(m_customExportComboBox->currentData().toString()); + if (curIndex > -1) { + newOption = m_customOptions[curIndex]; + } + } + + newOption.m_name = name; + m_customOptions.append(newOption); + ConfigMgr::getInst().getSessionConfig().setCustomExportOptions(m_customOptions); + + // Add it to combo box. + m_customExportComboBox->addItem(name, name); + m_customExportComboBox->setCurrentIndex(m_customExportComboBox->findData(name)); +} + +void ExportDialog::removeCustomExportScheme() +{ + auto name = m_customExportComboBox->currentData().toString(); + if (name.isEmpty()) { + return; + } + + int ret = MessageBoxHelper::questionOkCancel(MessageBoxHelper::Warning, + tr("Delete scheme (%1)?").arg(name), + QString(), + QString(), + this); + if (ret != QMessageBox::Ok) { + return; + } + + int idx = findCustomOption(name); + Q_ASSERT(idx > -1); + m_customOptions.remove(idx); + ConfigMgr::getInst().getSessionConfig().setCustomExportOptions(m_customOptions); + + m_customExportComboBox->removeItem(m_customExportComboBox->currentIndex()); +} + +void ExportDialog::customExportCurrentSchemeChanged(int p_comboIdx) +{ + const bool enabled = p_comboIdx >= 0; + m_targetSuffixLineEdit->setEnabled(enabled); + m_resourcePathSeparatorLineEdit->setEnabled(enabled); + m_useHtmlInputCheckBox->setEnabled(enabled); + m_allInOneCheckBox->setEnabled(enabled); + m_targetPageScrollableCheckBox->setEnabled(enabled); + m_commandTextEdit->setEnabled(enabled); + + if (p_comboIdx < 0) { + m_option.m_customExport.clear(); + return; + } + + auto name = m_customExportComboBox->currentData().toString(); + m_option.m_customExport = name; + int curIndex = findCustomOption(name); + Q_ASSERT(curIndex > -1); + const auto &opt = m_customOptions[curIndex]; + m_targetSuffixLineEdit->setText(opt.m_targetSuffix); + m_resourcePathSeparatorLineEdit->setText(opt.m_resourcePathSeparator); + m_useHtmlInputCheckBox->setChecked(opt.m_useHtmlInput); + m_allInOneCheckBox->setChecked(opt.m_allInOne); + m_targetPageScrollableCheckBox->setChecked(opt.m_targetPageScrollable); + m_commandTextEdit->setPlainText(opt.m_command); +} diff --git a/src/widgets/dialogs/exportdialog.h b/src/widgets/dialogs/exportdialog.h index 98fe51b2..659ced94 100644 --- a/src/widgets/dialogs/exportdialog.h +++ b/src/widgets/dialogs/exportdialog.h @@ -43,12 +43,15 @@ namespace vnotex void appendLog(const QString &p_log); + void customExportCurrentSchemeChanged(int p_comboIdx); + private: enum AdvancedSettings { General, HTML, PDF, + Custom, Max }; @@ -66,6 +69,8 @@ namespace vnotex QWidget *getPdfAdvancedSettings(); + QWidget *getCustomAdvancedSettings(); + void showAdvancedSettings(AdvancedSettings p_settings); void setupButtonBox(); @@ -86,6 +91,10 @@ namespace vnotex void saveFields(ExportPdfOption &p_option); + void restoreCustomFields(const ExportOption &p_option); + + void saveCustomFields(ExportOption &p_option); + void startExport(); void updateUIOnExport(); @@ -99,6 +108,12 @@ namespace vnotex void updatePageLayoutButtonLabel(); + int findCustomOption(const QString &p_name) const; + + void addCustomExportScheme(); + + void removeCustomExportScheme(); + // Managed by QObject. Exporter *m_exporter = nullptr; @@ -170,7 +185,24 @@ namespace vnotex QSharedPointer m_pageLayout; + // Custom settings. + QComboBox *m_customExportComboBox = nullptr; + + QLineEdit *m_targetSuffixLineEdit = nullptr; + + QLineEdit *m_resourcePathSeparatorLineEdit = nullptr; + + QCheckBox *m_useHtmlInputCheckBox = nullptr; + + QCheckBox *m_customAllInOneCheckBox = nullptr; + + QCheckBox *m_targetPageScrollableCheckBox = nullptr; + + QPlainTextEdit *m_commandTextEdit = nullptr; + ExportOption m_option; + + QVector m_customOptions; }; } diff --git a/src/widgets/dialogs/importfolderutils.cpp b/src/widgets/dialogs/importfolderutils.cpp index 96056b19..607e305d 100644 --- a/src/widgets/dialogs/importfolderutils.cpp +++ b/src/widgets/dialogs/importfolderutils.cpp @@ -52,7 +52,7 @@ void ImportFolderUtils::importFolderContentsByLegacyConfig(Notebook *p_notebook, try { config = LegacyNotebookUtils::getFolderConfig(rootDir.absolutePath()); } catch (Exception &p_e) { - Utils::appendMsg(p_errMsg, ImportFolderUtilsTranslate::tr("Failed to read folder config (%1).").arg(rootDir.absolutePath())); + Utils::appendMsg(p_errMsg, ImportFolderUtilsTranslate::tr("Failed to read folder config (%1) (%2).").arg(rootDir.absolutePath(), p_e.what())); return; } diff --git a/src/widgets/widgetsfactory.cpp b/src/widgets/widgetsfactory.cpp index 99336a0f..ca88909b 100644 --- a/src/widgets/widgetsfactory.cpp +++ b/src/widgets/widgetsfactory.cpp @@ -109,5 +109,6 @@ QPlainTextEdit *WidgetsFactory::createPlainTextConsole(QWidget *p_parent) QPlainTextEdit *WidgetsFactory::createPlainTextEdit(QWidget *p_parent) { auto edit = new QPlainTextEdit(p_parent); + edit->setProperty("ConsoleTextEdit", true); return edit; }