From 04a57f4f8dc48919ef6cc8fbefb5ef181cd78425 Mon Sep 17 00:00:00 2001 From: Le Tan Date: Wed, 13 Jan 2021 21:47:45 +0800 Subject: [PATCH] support export --- src/core/buffer/buffer.cpp | 5 + src/core/buffer/buffer.h | 4 + src/core/buffer/bufferprovider.h | 3 + src/core/buffer/filebufferprovider.cpp | 5 + src/core/buffer/filebufferprovider.h | 2 + src/core/buffer/filetypehelper.cpp | 31 +- src/core/buffer/filetypehelper.h | 28 +- src/core/buffer/nodebufferprovider.cpp | 12 +- src/core/buffer/nodebufferprovider.h | 6 +- src/core/buffermgr.cpp | 15 +- src/core/configmgr.cpp | 29 +- src/core/configmgr.h | 6 + src/core/core.pri | 2 +- src/core/coreconfig.h | 1 + src/core/externalfile.cpp | 1 + src/core/file.cpp | 2 +- src/core/file.h | 4 +- src/core/htmltemplatehelper.cpp | 155 +++- src/core/htmltemplatehelper.h | 41 ++ src/core/markdowneditorconfig.cpp | 34 +- src/core/markdowneditorconfig.h | 19 +- src/core/notebook/node.cpp | 4 + src/core/notebook/vxnodefile.cpp | 1 + .../notebookconfigmgr/notebookconfigmgr.pri | 2 - .../notebookconfigmgr/vxnotebookconfigmgr.cpp | 10 +- src/core/sessionconfig.cpp | 36 +- src/core/sessionconfig.h | 11 +- src/core/theme.cpp | 7 +- src/core/theme.h | 2 + src/core/thememgr.cpp | 38 + src/core/thememgr.h | 11 +- src/core/vnotex.cpp | 2 + src/core/vnotex.h | 2 + src/core/{viewerresource.h => webresource.h} | 15 +- src/data/core/vnotex.json | 27 +- src/data/extra/extra.qrc | 6 +- src/data/extra/themes/moonlight/interface.qss | 14 +- src/data/extra/themes/moonlight/palette.json | 2 +- src/data/extra/themes/native/interface.qss | 6 +- src/data/extra/themes/pure/highlight.css | 4 +- src/data/extra/themes/pure/interface.qss | 14 +- src/data/extra/themes/pure/palette.json | 4 +- src/data/extra/themes/pure/text-editor.theme | 10 +- src/data/extra/themes/pure/web.css | 4 +- src/data/extra/web/css/exportglobalstyles.css | 3 + src/data/extra/web/css/globalstyles.css | 30 +- src/data/extra/web/css/outline.css | 205 ++++++ src/data/extra/web/js/graphviz.js | 3 +- src/data/extra/web/js/markdownit.js | 22 - src/data/extra/web/js/markdownviewer.js | 4 + src/data/extra/web/js/mathjax.js | 3 +- src/data/extra/web/js/outline.js | 241 +++++++ src/data/extra/web/js/plantuml.js | 3 +- src/data/extra/web/js/utils.js | 51 ++ src/data/extra/web/js/vnotex.js | 60 +- .../extra/web/markdown-export-template.html | 45 ++ ...ate.html => markdown-viewer-template.html} | 0 src/export/export.pri | 11 + src/export/exportdata.cpp | 126 ++++ src/export/exportdata.h | 114 +++ src/export/exporter.cpp | 312 ++++++++ src/export/exporter.h | 70 ++ src/export/webviewexporter.cpp | 568 +++++++++++++++ src/export/webviewexporter.h | 110 +++ src/src.pro | 2 + .../contentmediautils.cpp} | 123 ++-- .../contentmediautils.h} | 20 +- src/utils/fileutils.h | 2 +- src/utils/htmlutils.cpp | 6 + src/utils/htmlutils.h | 2 + src/utils/processutils.cpp | 158 ++++ src/utils/processutils.h | 52 ++ src/utils/textutils.cpp | 10 - src/utils/textutils.h | 3 - src/utils/utils.pri | 6 + src/utils/webutils.cpp | 103 +++ src/utils/webutils.h | 24 + src/utils/widgetutils.cpp | 13 - src/utils/widgetutils.h | 3 - src/widgets/dialogs/dialog.cpp | 38 +- src/widgets/dialogs/dialog.h | 13 +- src/widgets/dialogs/exportdialog.cpp | 679 ++++++++++++++++++ src/widgets/dialogs/exportdialog.h | 171 +++++ src/widgets/dialogs/filepropertiesdialog.cpp | 2 +- .../dialogs/folderfilesfilterwidget.cpp | 2 +- src/widgets/dialogs/linkinsertdialog.cpp | 2 +- src/widgets/dialogs/nodeinfowidget.cpp | 4 +- src/widgets/dialogs/notebookinfowidget.cpp | 4 +- src/widgets/dialogs/scrolldialog.cpp | 5 + src/widgets/dialogs/scrolldialog.h | 6 +- .../dialogs/settings/appearancepage.cpp | 2 +- src/widgets/dialogs/settings/editorpage.cpp | 2 +- src/widgets/dialogs/settings/generalpage.cpp | 2 +- .../dialogs/settings/markdowneditorpage.cpp | 6 +- .../dialogs/settings/texteditorpage.cpp | 2 +- src/widgets/editors/markdowneditor.cpp | 3 +- src/widgets/editors/markdownvieweradapter.cpp | 28 + src/widgets/editors/markdownvieweradapter.h | 19 + src/widgets/mainwindow.cpp | 20 +- src/widgets/mainwindow.h | 2 + src/widgets/notebookexplorer.cpp | 9 +- src/widgets/notebookexplorer.h | 6 +- src/widgets/propertydefs.cpp | 2 + src/widgets/propertydefs.h | 2 + src/widgets/toolbarhelper.cpp | 14 +- src/widgets/viewarea.cpp | 1 + src/widgets/webviewer.cpp | 4 +- src/widgets/widgets.pri | 2 + src/widgets/widgetsfactory.cpp | 23 + src/widgets/widgetsfactory.h | 6 + .../test_core/test_notebook/test_notebook.pro | 1 + tests/test_core/test_theme/test_theme.pro | 10 +- 112 files changed, 3953 insertions(+), 284 deletions(-) rename src/core/{viewerresource.h => webresource.h} (91%) create mode 100644 src/data/extra/web/css/exportglobalstyles.css create mode 100644 src/data/extra/web/css/outline.css create mode 100644 src/data/extra/web/js/outline.js create mode 100644 src/data/extra/web/markdown-export-template.html rename src/data/extra/web/{markdownviewertemplate.html => markdown-viewer-template.html} (100%) create mode 100644 src/export/export.pri create mode 100644 src/export/exportdata.cpp create mode 100644 src/export/exportdata.h create mode 100644 src/export/exporter.cpp create mode 100644 src/export/exporter.h create mode 100644 src/export/webviewexporter.cpp create mode 100644 src/export/webviewexporter.h rename src/{core/notebookconfigmgr/nodecontentmediautils.cpp => utils/contentmediautils.cpp} (53%) rename src/{core/notebookconfigmgr/nodecontentmediautils.h => utils/contentmediautils.h} (77%) create mode 100644 src/utils/processutils.cpp create mode 100644 src/utils/processutils.h create mode 100644 src/utils/webutils.cpp create mode 100644 src/utils/webutils.h create mode 100644 src/widgets/dialogs/exportdialog.cpp create mode 100644 src/widgets/dialogs/exportdialog.h diff --git a/src/core/buffer/buffer.cpp b/src/core/buffer/buffer.cpp index 819ec0b2..c331538b 100644 --- a/src/core/buffer/buffer.cpp +++ b/src/core/buffer/buffer.cpp @@ -562,3 +562,8 @@ Buffer::StateFlags Buffer::state() const { return m_state; } + +QSharedPointer Buffer::getFile() const +{ + return m_provider->getFile(); +} diff --git a/src/core/buffer/buffer.h b/src/core/buffer/buffer.h index a907e7b0..545f6f89 100644 --- a/src/core/buffer/buffer.h +++ b/src/core/buffer/buffer.h @@ -18,6 +18,7 @@ namespace vnotex class ViewWindow; struct FileOpenParameters; class BufferProvider; + class File; struct BufferParameters { @@ -83,6 +84,9 @@ namespace vnotex // Get the base path to resolve resources. QString getResourcePath() const; + // Return nullptr if not available. + QSharedPointer getFile() const; + ID getID() const; // Get buffer content. diff --git a/src/core/buffer/bufferprovider.h b/src/core/buffer/bufferprovider.h index aec87060..efdf8601 100644 --- a/src/core/buffer/bufferprovider.h +++ b/src/core/buffer/bufferprovider.h @@ -74,6 +74,9 @@ namespace vnotex virtual bool isReadOnly() const = 0; + // Return nullptr if not available. + virtual QSharedPointer getFile() const = 0; + protected: virtual QDateTime getLastModifiedFromFile() const; diff --git a/src/core/buffer/filebufferprovider.cpp b/src/core/buffer/filebufferprovider.cpp index 4be3e53f..c0cacbb8 100644 --- a/src/core/buffer/filebufferprovider.cpp +++ b/src/core/buffer/filebufferprovider.cpp @@ -178,3 +178,8 @@ bool FileBufferProvider::isReadOnly() const { return m_readOnly; } + +QSharedPointer FileBufferProvider::getFile() const +{ + return m_file; +} diff --git a/src/core/buffer/filebufferprovider.h b/src/core/buffer/filebufferprovider.h index 93dab3ef..52d447f0 100644 --- a/src/core/buffer/filebufferprovider.h +++ b/src/core/buffer/filebufferprovider.h @@ -65,6 +65,8 @@ namespace vnotex bool isReadOnly() const Q_DECL_OVERRIDE; + QSharedPointer getFile() const Q_DECL_OVERRIDE; + private: QSharedPointer m_file; diff --git a/src/core/buffer/filetypehelper.cpp b/src/core/buffer/filetypehelper.cpp index 0c471b0b..405c281b 100644 --- a/src/core/buffer/filetypehelper.cpp +++ b/src/core/buffer/filetypehelper.cpp @@ -8,6 +8,16 @@ using namespace vnotex; +QString FileType::preferredSuffix() const +{ + return m_suffixes.isEmpty() ? QString() : m_suffixes.first(); +} + +bool FileType::isMarkdown() const +{ + return m_type == Type::Markdown; +} + FileTypeHelper::FileTypeHelper() { setupBuiltInTypes(); @@ -21,7 +31,7 @@ void FileTypeHelper::setupBuiltInTypes() { { FileType type; - type.m_type = Type::Markdown; + type.m_type = FileType::Markdown; type.m_displayName = Buffer::tr("Markdown"); type.m_typeName = QStringLiteral("Markdown"); type.m_suffixes << QStringLiteral("md") @@ -33,7 +43,7 @@ void FileTypeHelper::setupBuiltInTypes() { FileType type; - type.m_type = Type::Text; + type.m_type = FileType::Text; type.m_typeName = QStringLiteral("Text"); type.m_displayName = Buffer::tr("Text"); type.m_suffixes << QStringLiteral("txt") << QStringLiteral("text") << QStringLiteral("log"); @@ -42,7 +52,7 @@ void FileTypeHelper::setupBuiltInTypes() { FileType type; - type.m_type = Type::Others; + type.m_type = FileType::Others; type.m_typeName = QStringLiteral("Others"); type.m_displayName = Buffer::tr("Others"); m_fileTypes.push_back(type); @@ -62,10 +72,10 @@ const FileType &FileTypeHelper::getFileType(const QString &p_filePath) const // Treat all unknown text files as plain text files. if (FileUtils::isText(p_filePath)) { - return m_fileTypes[Type::Text]; + return m_fileTypes[FileType::Text]; } - return m_fileTypes[Type::Others]; + return m_fileTypes[FileType::Others]; } const FileType &FileTypeHelper::getFileTypeBySuffix(const QString &p_suffix) const @@ -74,7 +84,7 @@ const FileType &FileTypeHelper::getFileTypeBySuffix(const QString &p_suffix) con if (it != m_suffixTypeMap.end()) { return m_fileTypes.at(it.value()); } else { - return m_fileTypes[Type::Others]; + return m_fileTypes[FileType::Others]; } } @@ -97,8 +107,9 @@ const QVector &FileTypeHelper::getAllFileTypes() const return m_fileTypes; } -const FileType &FileTypeHelper::getFileType(Type p_type) const +const FileType &FileTypeHelper::getFileType(int p_type) const { + Q_ASSERT(p_type < m_fileTypes.size()); return m_fileTypes[p_type]; } @@ -108,9 +119,9 @@ const FileTypeHelper &FileTypeHelper::getInst() return helper; } -bool FileTypeHelper::checkFileType(const QString &p_filePath, Type p_type) const +bool FileTypeHelper::checkFileType(const QString &p_filePath, int p_type) const { - return getFileType(p_filePath).m_type == static_cast(p_type); + return getFileType(p_filePath).m_type == p_type; } const FileType &FileTypeHelper::getFileTypeByName(const QString &p_typeName) const @@ -122,5 +133,5 @@ const FileType &FileTypeHelper::getFileTypeByName(const QString &p_typeName) con } Q_ASSERT(false); - return m_fileTypes[Type::Others]; + return m_fileTypes[FileType::Others]; } diff --git a/src/core/buffer/filetypehelper.h b/src/core/buffer/filetypehelper.h index 1c7647cd..0a62342a 100644 --- a/src/core/buffer/filetypehelper.h +++ b/src/core/buffer/filetypehelper.h @@ -10,7 +10,15 @@ namespace vnotex class FileType { public: - // FileTypeHelper::Type. + // There may be other types after Others. + enum Type + { + Markdown = 0, + Text, + Others + }; + + // Type. int m_type = -1; QString m_typeName; @@ -19,25 +27,17 @@ namespace vnotex QStringList m_suffixes; - QString preferredSuffix() const - { - return m_suffixes.isEmpty() ? QString() : m_suffixes.first(); - } + QString preferredSuffix() const; + + bool isMarkdown() const; }; class FileTypeHelper { public: - enum Type - { - Markdown = 0, - Text, - Others - }; - const FileType &getFileType(const QString &p_filePath) const; - const FileType &getFileType(Type p_type) const; + const FileType &getFileType(int p_type) const; const FileType &getFileTypeByName(const QString &p_typeName) const; @@ -45,7 +45,7 @@ namespace vnotex const QVector &getAllFileTypes() const; - bool checkFileType(const QString &p_filePath, Type p_type) const; + bool checkFileType(const QString &p_filePath, int p_type) const; static const FileTypeHelper &getInst(); diff --git a/src/core/buffer/nodebufferprovider.cpp b/src/core/buffer/nodebufferprovider.cpp index b8614dd4..63b0d1a8 100644 --- a/src/core/buffer/nodebufferprovider.cpp +++ b/src/core/buffer/nodebufferprovider.cpp @@ -8,12 +8,13 @@ using namespace vnotex; -NodeBufferProvider::NodeBufferProvider(const QSharedPointer &p_node, QObject *p_parent) +NodeBufferProvider::NodeBufferProvider(const QSharedPointer &p_node, + const QSharedPointer &p_file, + QObject *p_parent) : BufferProvider(p_parent), m_node(p_node), - m_nodeFile(p_node->getContentFile()) + m_nodeFile(p_file) { - Q_ASSERT(m_nodeFile); } Buffer::ProviderType NodeBufferProvider::getType() const @@ -156,3 +157,8 @@ bool NodeBufferProvider::isReadOnly() const { return m_node->isReadOnly(); } + +QSharedPointer NodeBufferProvider::getFile() const +{ + return m_nodeFile; +} diff --git a/src/core/buffer/nodebufferprovider.h b/src/core/buffer/nodebufferprovider.h index 69c95e75..4a06f8e2 100644 --- a/src/core/buffer/nodebufferprovider.h +++ b/src/core/buffer/nodebufferprovider.h @@ -15,7 +15,9 @@ namespace vnotex { Q_OBJECT public: - NodeBufferProvider(const QSharedPointer &p_node, QObject *p_parent = nullptr); + NodeBufferProvider(const QSharedPointer &p_node, + const QSharedPointer &p_file, + QObject *p_parent = nullptr); Buffer::ProviderType getType() const Q_DECL_OVERRIDE; @@ -65,6 +67,8 @@ namespace vnotex bool isReadOnly() const Q_DECL_OVERRIDE; + QSharedPointer getFile() const Q_DECL_OVERRIDE; + private: QSharedPointer m_node; diff --git a/src/core/buffermgr.cpp b/src/core/buffermgr.cpp index 6e33b6d5..9de26e4b 100644 --- a/src/core/buffermgr.cpp +++ b/src/core/buffermgr.cpp @@ -42,11 +42,11 @@ void BufferMgr::initBufferServer() // Markdown. auto markdownFactory = QSharedPointer::create(); - m_bufferServer->registerItem(helper.getFileType(FileTypeHelper::Markdown).m_typeName, markdownFactory); + m_bufferServer->registerItem(helper.getFileType(FileType::Markdown).m_typeName, markdownFactory); // Text. auto textFactory = QSharedPointer::create(); - m_bufferServer->registerItem(helper.getFileType(FileTypeHelper::Text).m_typeName, textFactory); + m_bufferServer->registerItem(helper.getFileType(FileType::Text).m_typeName, textFactory); } void BufferMgr::open(Node *p_node, const QSharedPointer &p_paras) @@ -62,7 +62,9 @@ void BufferMgr::open(Node *p_node, const QSharedPointer &p_p auto buffer = findBuffer(p_node); if (!buffer) { auto nodePath = p_node->fetchAbsolutePath(); - auto fileType = FileTypeHelper::getInst().getFileType(nodePath).m_typeName; + auto nodeFile = p_node->getContentFile(); + Q_ASSERT(nodeFile); + auto fileType = nodeFile->getContentType().m_typeName; auto factory = m_bufferServer->getItem(fileType); if (!factory) { // No factory to open this file type. @@ -72,7 +74,7 @@ void BufferMgr::open(Node *p_node, const QSharedPointer &p_p } BufferParameters paras; - paras.m_provider.reset(new NodeBufferProvider(p_node->sharedFromThis())); + paras.m_provider.reset(new NodeBufferProvider(p_node->sharedFromThis(), nodeFile)); buffer = factory->createBuffer(paras, this); addBuffer(buffer); } @@ -114,7 +116,8 @@ void BufferMgr::open(const QString &p_filePath, const QSharedPointer::create(p_filePath); + auto fileType = externalFile->getContentType().m_typeName; auto factory = m_bufferServer->getItem(fileType); if (!factory) { // No factory to open this file type. @@ -124,7 +127,7 @@ void BufferMgr::open(const QString &p_filePath, const QSharedPointer::create(p_filePath), + paras.m_provider.reset(new FileBufferProvider(externalFile, p_paras->m_nodeAttachedTo, p_paras->m_readOnly)); buffer = factory->createBuffer(paras, this); diff --git a/src/core/configmgr.cpp b/src/core/configmgr.cpp index 1fcad027..dd675a59 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"; @@ -320,6 +320,18 @@ QString ConfigMgr::getUserThemeFolder() const return folderPath; } +QString ConfigMgr::getAppWebStylesFolder() const +{ + return PathUtils::concatenateFilePath(m_appConfigFolderPath, QStringLiteral("web-styles")); +} + +QString ConfigMgr::getUserWebStylesFolder() const +{ + auto folderPath = PathUtils::concatenateFilePath(m_userConfigFolderPath, QStringLiteral("web-styles")); + QDir().mkpath(folderPath); + return folderPath; +} + QString ConfigMgr::getAppDocsFolder() const { return PathUtils::concatenateFilePath(m_appConfigFolderPath, QStringLiteral("docs")); @@ -406,3 +418,18 @@ QString ConfigMgr::getApplicationDirPath() { return PathUtils::parentDirPath(getApplicationFilePath()); } + +QString ConfigMgr::getDocumentOrHomePath() +{ + static QString docHomePath; + if (docHomePath.isEmpty()) { + QStringList folders = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation); + if (folders.isEmpty()) { + docHomePath = QDir::homePath(); + } else { + docHomePath = folders[0]; + } + } + + return docHomePath; +} diff --git a/src/core/configmgr.h b/src/core/configmgr.h index fe6ae975..48d1d637 100644 --- a/src/core/configmgr.h +++ b/src/core/configmgr.h @@ -77,6 +77,10 @@ namespace vnotex QString getUserThemeFolder() const; + QString getAppWebStylesFolder() const; + + QString getUserWebStylesFolder() const; + QString getAppDocsFolder() const; QString getUserDocsFolder() const; @@ -98,6 +102,8 @@ namespace vnotex static QString getApplicationDirPath(); + static QString getDocumentOrHomePath(); + static const QString c_orgName; static const QString c_appName; diff --git a/src/core/core.pri b/src/core/core.pri index d5a9ef69..2eccfd18 100644 --- a/src/core/core.pri +++ b/src/core/core.pri @@ -32,7 +32,6 @@ SOURCES += \ $$PWD/widgetconfig.cpp HEADERS += \ - $$PWD/ViewerResource.h \ $$PWD/buffermgr.h \ $$PWD/configmgr.h \ $$PWD/coreconfig.h \ @@ -58,4 +57,5 @@ HEADERS += \ $$PWD/theme.h \ $$PWD/sessionconfig.h \ $$PWD/clipboarddata.h \ + $$PWD/webresource.h \ $$PWD/widgetconfig.h diff --git a/src/core/coreconfig.h b/src/core/coreconfig.h index b6ceb5eb..dcae4ab3 100644 --- a/src/core/coreconfig.h +++ b/src/core/coreconfig.h @@ -31,6 +31,7 @@ namespace vnotex DistributeSplits, RemoveSplitAndWorkspace, NewWorkspace, + Export, MaxShortcut }; Q_ENUM(Shortcut) diff --git a/src/core/externalfile.cpp b/src/core/externalfile.cpp index 17f395be..7d78493f 100644 --- a/src/core/externalfile.cpp +++ b/src/core/externalfile.cpp @@ -8,6 +8,7 @@ using namespace vnotex; ExternalFile::ExternalFile(const QString &p_filePath) : c_filePath(p_filePath) { + setContentType(FileTypeHelper::getInst().getFileType(c_filePath).m_type); } QString ExternalFile::read() const diff --git a/src/core/file.cpp b/src/core/file.cpp index 9127d942..2a23fe3b 100644 --- a/src/core/file.cpp +++ b/src/core/file.cpp @@ -7,7 +7,7 @@ const FileType &File::getContentType() const return FileTypeHelper::getInst().getFileType(m_contentType); } -void File::setContentType(FileTypeHelper::Type p_type) +void File::setContentType(int p_type) { m_contentType = p_type; } diff --git a/src/core/file.h b/src/core/file.h index 6498a899..0cba68c7 100644 --- a/src/core/file.h +++ b/src/core/file.h @@ -60,10 +60,10 @@ namespace vnotex const FileType &getContentType() const; protected: - void setContentType(FileTypeHelper::Type p_type); + void setContentType(int p_type); private: - FileTypeHelper::Type m_contentType = FileTypeHelper::Others; + int m_contentType = FileType::Others; }; } diff --git a/src/core/htmltemplatehelper.cpp b/src/core/htmltemplatehelper.cpp index 7bb8d381..2c199af4 100644 --- a/src/core/htmltemplatehelper.cpp +++ b/src/core/htmltemplatehelper.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -14,6 +15,8 @@ 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") @@ -26,21 +29,22 @@ QString WebGlobalOptions::toJavascriptObject() const + QString("linkifyEnabled: %1,\n").arg(Utils::boolToString(m_linkifyEnabled)) + QString("indentFirstLineEnabled: %1,\n").arg(Utils::boolToString(m_indentFirstLineEnabled)) + QString("sectionNumberEnabled: %1,\n").arg(Utils::boolToString(m_sectionNumberEnabled)) + + QString("transparentBackgroundEnabled: %1,\n").arg(Utils::boolToString(m_transparentBackgroundEnabled)) + + QString("scrollable: %1,\n").arg(Utils::boolToString(m_scrollable)) + + QString("bodyWidth: %1,\n").arg(m_bodyWidth) + + QString("bodyHeight: %1,\n").arg(m_bodyHeight) + + QString("transformSvgToPngEnabled: %1,\n").arg(Utils::boolToString(m_transformSvgToPngEnabled)) + + QString("mathJaxScale: %1,\n").arg(m_mathJaxScale) + QString("sectionNumberBaseLevel: %1\n").arg(m_sectionNumberBaseLevel) + QStringLiteral("}"); } -static bool isGlobalStyles(const ViewerResource::Resource &p_resource) -{ - return p_resource.m_name == QStringLiteral("global_styles"); -} - // Read "global_styles" from resource and fill the holder with the content. -static void fillGlobalStyles(QString &p_template, const ViewerResource &p_resource) +static void fillGlobalStyles(QString &p_template, const WebResource &p_resource, const QString &p_additionalStyles) { QString styles; for (const auto &ele : p_resource.m_resources) { - if (isGlobalStyles(ele)) { + if (ele.isGlobal()) { if (ele.m_enabled) { for (const auto &style : ele.m_styles) { // Read the style file content. @@ -52,9 +56,10 @@ static void fillGlobalStyles(QString &p_template, const ViewerResource &p_resour } } + styles += p_additionalStyles; + if (!styles.isEmpty()) { - p_template.replace(QStringLiteral("/* VX_GLOBAL_STYLES_PLACEHOLDER */"), - styles); + p_template.replace(c_globalStylesPlaceholder, styles); } } @@ -76,13 +81,11 @@ static QString fillScriptTag(const QString &p_scriptFile) return QString("\n").arg(url.toString()); } -static void fillThemeStyles(QString &p_template) +static void fillThemeStyles(QString &p_template, const QString &p_webStyleSheetFile, const QString &p_highlightStyleSheetFile) { QString styles; - const auto &themeMgr = VNoteX::getInst().getThemeMgr(); - - styles += fillStyleTag(themeMgr.getFile(Theme::File::WebStyleSheet)); - styles += fillStyleTag(themeMgr.getFile(Theme::File::HighlightStyleSheet)); + styles += fillStyleTag(p_webStyleSheetFile); + styles += fillStyleTag(p_highlightStyleSheetFile); if (!styles.isEmpty()) { p_template.replace(QStringLiteral(""), @@ -97,13 +100,13 @@ static void fillGlobalOptions(QString &p_template, const WebGlobalOptions &p_opt } // Read all other resources in @p_resource and fill the holder with proper resource path. -static void fillResources(QString &p_template, const ViewerResource &p_resource) +static void fillResources(QString &p_template, const WebResource &p_resource) { QString styles; QString scripts; for (const auto &ele : p_resource.m_resources) { - if (ele.m_enabled && !isGlobalStyles(ele)) { + if (ele.m_enabled && !ele.isGlobal()) { // Styles. for (const auto &style : ele.m_styles) { auto styleFile = ConfigMgr::getInst().getUserOrAppFile(style); @@ -129,6 +132,36 @@ static void fillResources(QString &p_template, const ViewerResource &p_resource) } } +static void fillResourcesByContent(QString &p_template, const WebResource &p_resource) +{ + QString styles; + QString scripts; + + for (const auto &ele : p_resource.m_resources) { + if (ele.m_enabled && !ele.isGlobal()) { + // Styles. + for (const auto &style : ele.m_styles) { + auto styleFile = ConfigMgr::getInst().getUserOrAppFile(style); + styles += FileUtils::readTextFile(styleFile); + } + + // Scripts. + for (const auto &script : ele.m_scripts) { + auto scriptFile = ConfigMgr::getInst().getUserOrAppFile(script); + scripts += FileUtils::readTextFile(scriptFile); + } + } + } + + if (!styles.isEmpty()) { + p_template.replace(QStringLiteral("/* VX_STYLES_PLACEHOLDER */"), styles); + } + + if (!scripts.isEmpty()) { + p_template.replace(QStringLiteral("/* VX_SCRIPTS_PLACEHOLDER */"), scripts); + } +} + const QString &HtmlTemplateHelper::getMarkdownViewerTemplate() { return s_markdownViewerTemplate.m_template; @@ -142,16 +175,30 @@ void HtmlTemplateHelper::updateMarkdownViewerTemplate(const MarkdownEditorConfig s_markdownViewerTemplate.m_revision = p_config.revision(); + const auto &themeMgr = VNoteX::getInst().getThemeMgr(); + s_markdownViewerTemplate.m_template = + generateMarkdownViewerTemplate(p_config, + themeMgr.getFile(Theme::File::WebStyleSheet), + themeMgr.getFile(Theme::File::HighlightStyleSheet)); +} + +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) +{ const auto &viewerResource = p_config.getViewerResource(); + const auto templateFile = ConfigMgr::getInst().getUserOrAppFile(viewerResource.m_template); + auto htmlTemplate = FileUtils::readTextFile(templateFile); - { - auto templateFile = ConfigMgr::getInst().getUserOrAppFile(viewerResource.m_template); - s_markdownViewerTemplate.m_template = FileUtils::readTextFile(templateFile); - } + fillGlobalStyles(htmlTemplate, viewerResource, ""); - fillGlobalStyles(s_markdownViewerTemplate.m_template, viewerResource); - - fillThemeStyles(s_markdownViewerTemplate.m_template); + fillThemeStyles(htmlTemplate, p_webStyleSheetFile, p_highlightStyleSheetFile); { WebGlobalOptions opts; @@ -165,8 +212,66 @@ void HtmlTemplateHelper::updateMarkdownViewerTemplate(const MarkdownEditorConfig opts.m_autoBreakEnabled = p_config.getAutoBreakEnabled(); opts.m_linkifyEnabled = p_config.getLinkifyEnabled(); opts.m_indentFirstLineEnabled = p_config.getIndentFirstLineEnabled(); - fillGlobalOptions(s_markdownViewerTemplate.m_template, opts); + 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; + fillGlobalOptions(htmlTemplate, opts); } - fillResources(s_markdownViewerTemplate.m_template, viewerResource); + fillResources(htmlTemplate, viewerResource); + + return htmlTemplate; +} + +QString HtmlTemplateHelper::generateExportTemplate(const MarkdownEditorConfig &p_config, + bool p_addOutlinePanel) +{ + auto exportResource = p_config.getExportResource(); + const auto templateFile = ConfigMgr::getInst().getUserOrAppFile(exportResource.m_template); + auto htmlTemplate = FileUtils::readTextFile(templateFile); + + fillGlobalStyles(htmlTemplate, exportResource, ""); + + // Outline panel. + for (auto &ele : exportResource.m_resources) { + if (ele.m_name == QStringLiteral("outline")) { + ele.m_enabled = p_addOutlinePanel; + break; + } + } + + fillResourcesByContent(htmlTemplate, exportResource); + + return htmlTemplate; +} + +void HtmlTemplateHelper::fillTitle(QString &p_template, const QString &p_title) +{ + if (!p_title.isEmpty()) { + p_template.replace("", + QString("%1").arg(HtmlUtils::escapeHtml(p_title))); + } +} + +void HtmlTemplateHelper::fillStyleContent(QString &p_template, const QString &p_styles) +{ + p_template.replace("/* VX_STYLES_CONTENT_PLACEHOLDER */", p_styles); +} + +void HtmlTemplateHelper::fillHeadContent(QString &p_template, const QString &p_head) +{ + p_template.replace("", p_head); +} + +void HtmlTemplateHelper::fillContent(QString &p_template, const QString &p_content) +{ + p_template.replace("", p_content); +} + +void HtmlTemplateHelper::fillBodyClassList(QString &p_template, const QString &p_classList) +{ + p_template.replace("", p_classList); } diff --git a/src/core/htmltemplatehelper.h b/src/core/htmltemplatehelper.h index 22a515b9..f3b8d6c1 100644 --- a/src/core/htmltemplatehelper.h +++ b/src/core/htmltemplatehelper.h @@ -30,6 +30,24 @@ namespace vnotex bool m_indentFirstLineEnabled = false; + // Force to use transparent background. + bool m_transparentBackgroundEnabled = false; + + // Whether the content elements are scrollable. Like PDF, it is false. + bool m_scrollable = true; + + int m_bodyWidth = -1; + + int m_bodyHeight = -1; + + // Whether transform inlie SVG to PNG. + // For wkhtmltopdf converter, it could not render some inline SVG correctly. + // This is just a hint not mandatory. For now, PlantUML and Graphviz needs this. + bool m_transformSvgToPngEnabled = false; + + // wkhtmltopdf will make the MathJax formula too small. + qreal m_mathJaxScale = -1; + QString toJavascriptObject() const; }; @@ -42,6 +60,29 @@ namespace vnotex 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 generateExportTemplate(const MarkdownEditorConfig &p_config, + bool p_addOutlinePanel); + + static void fillTitle(QString &p_template, const QString &p_title); + + static void fillStyleContent(QString &p_template, const QString &p_styles); + + static void fillHeadContent(QString &p_template, const QString &p_head); + + static void fillContent(QString &p_template, const QString &p_content); + + static void fillBodyClassList(QString &p_template, const QString &p_classList); + private: struct Template { diff --git a/src/core/markdowneditorconfig.cpp b/src/core/markdowneditorconfig.cpp index cfa86fc1..42a0d27a 100644 --- a/src/core/markdowneditorconfig.cpp +++ b/src/core/markdowneditorconfig.cpp @@ -27,6 +27,7 @@ void MarkdownEditorConfig::init(const QJsonObject &p_app, const QJsonObject &p_u const auto userObj = p_user.value(m_sessionName).toObject(); loadViewerResource(appObj, userObj); + loadExportResource(appObj, userObj); m_webPlantUml = READBOOL(QStringLiteral("web_plantuml")); m_webGraphviz = READBOOL(QStringLiteral("web_graphviz")); @@ -58,6 +59,7 @@ QJsonObject MarkdownEditorConfig::toJson() const { QJsonObject obj; obj[QStringLiteral("viewer_resource")] = saveViewerResource(); + obj[QStringLiteral("export_resource")] = saveExportResource(); obj[QStringLiteral("web_plantuml")] = m_webPlantUml; obj[QStringLiteral("web_graphviz")] = m_webGraphviz; obj[QStringLiteral("prepend_dot_in_relative_link")] = m_prependDotInRelativeLink; @@ -122,11 +124,41 @@ QJsonObject MarkdownEditorConfig::saveViewerResource() const return m_viewerResource.toJson(); } -const ViewerResource &MarkdownEditorConfig::getViewerResource() const +void MarkdownEditorConfig::loadExportResource(const QJsonObject &p_app, const QJsonObject &p_user) +{ + const QString name(QStringLiteral("export_resource")); + + if (MainConfig::isVersionChanged()) { + bool needOverride = p_app[QStringLiteral("override_viewer_resource")].toBool(); + if (needOverride) { + qInfo() << "override \"viewer_resource\" in user configuration due to version change"; + m_exportResource.init(p_app[name].toObject()); + return; + } + } + + if (p_user.contains(name)) { + m_exportResource.init(p_user[name].toObject()); + } else { + m_exportResource.init(p_app[name].toObject()); + } +} + +QJsonObject MarkdownEditorConfig::saveExportResource() const +{ + return m_exportResource.toJson(); +} + +const WebResource &MarkdownEditorConfig::getViewerResource() const { return m_viewerResource; } +const WebResource &MarkdownEditorConfig::getExportResource() const +{ + return m_exportResource; +} + bool MarkdownEditorConfig::getWebPlantUml() const { return m_webPlantUml; diff --git a/src/core/markdowneditorconfig.h b/src/core/markdowneditorconfig.h index b9cde71a..b28be386 100644 --- a/src/core/markdowneditorconfig.h +++ b/src/core/markdowneditorconfig.h @@ -3,7 +3,7 @@ #include "iconfig.h" -#include "viewerresource.h" +#include "webresource.h" #include #include @@ -38,15 +38,14 @@ namespace vnotex QJsonObject toJson() const Q_DECL_OVERRIDE; - void loadViewerResource(const QJsonObject &p_app, const QJsonObject &p_user); - QJsonObject saveViewerResource() const; - int revision() const Q_DECL_OVERRIDE; TextEditorConfig &getTextEditorConfig(); const TextEditorConfig &getTextEditorConfig() const; - const ViewerResource &getViewerResource() const; + const WebResource &getViewerResource() const; + + const WebResource &getExportResource() const; bool getWebPlantUml() const; @@ -107,9 +106,17 @@ namespace vnotex QString sectionNumberStyleToString(SectionNumberStyle p_style) const; SectionNumberStyle stringToSectionNumberStyle(const QString &p_str) const; + void loadViewerResource(const QJsonObject &p_app, const QJsonObject &p_user); + QJsonObject saveViewerResource() const; + + void loadExportResource(const QJsonObject &p_app, const QJsonObject &p_user); + QJsonObject saveExportResource() const; + QSharedPointer m_textEditorConfig; - ViewerResource m_viewerResource; + WebResource m_viewerResource; + + WebResource m_exportResource; // Whether use javascript or external program to render PlantUML. bool m_webPlantUml = true; diff --git a/src/core/notebook/node.cpp b/src/core/notebook/node.cpp index 610ca2f2..acefbd72 100644 --- a/src/core/notebook/node.cpp +++ b/src/core/notebook/node.cpp @@ -263,6 +263,10 @@ QDir Node::toDir() const void Node::load() { + if (isLoaded()) { + return; + } + getConfigMgr()->loadNode(this); } diff --git a/src/core/notebook/vxnodefile.cpp b/src/core/notebook/vxnodefile.cpp index a904013b..9cf4161f 100644 --- a/src/core/notebook/vxnodefile.cpp +++ b/src/core/notebook/vxnodefile.cpp @@ -15,6 +15,7 @@ VXNodeFile::VXNodeFile(const QSharedPointer &p_node) : m_node(p_node) { Q_ASSERT(m_node && m_node->hasContent()); + setContentType(FileTypeHelper::getInst().getFileType(getContentPath()).m_type); } QString VXNodeFile::read() const diff --git a/src/core/notebookconfigmgr/notebookconfigmgr.pri b/src/core/notebookconfigmgr/notebookconfigmgr.pri index c60a3461..7cbd9fb1 100644 --- a/src/core/notebookconfigmgr/notebookconfigmgr.pri +++ b/src/core/notebookconfigmgr/notebookconfigmgr.pri @@ -1,5 +1,4 @@ SOURCES += \ - $$PWD/nodecontentmediautils.cpp \ $$PWD/vxnotebookconfigmgr.cpp \ $$PWD/vxnotebookconfigmgrfactory.cpp \ $$PWD/inotebookconfigmgr.cpp \ @@ -8,7 +7,6 @@ SOURCES += \ HEADERS += \ $$PWD/inotebookconfigmgr.h \ - $$PWD/nodecontentmediautils.h \ $$PWD/vxnotebookconfigmgr.h \ $$PWD/inotebookconfigmgrfactory.h \ $$PWD/vxnotebookconfigmgrfactory.h \ diff --git a/src/core/notebookconfigmgr/vxnotebookconfigmgr.cpp b/src/core/notebookconfigmgr/vxnotebookconfigmgr.cpp index 1b8f85e5..e1c4c612 100644 --- a/src/core/notebookconfigmgr/vxnotebookconfigmgr.cpp +++ b/src/core/notebookconfigmgr/vxnotebookconfigmgr.cpp @@ -14,7 +14,7 @@ #include #include -#include "nodecontentmediautils.h" +#include using namespace vnotex; @@ -581,13 +581,13 @@ QSharedPointer VXNotebookConfigMgr::copyFileNodeAsChildOf(const QSharedPoi getBackend()->copyFile(srcFilePath, destFilePath); // Copy media files fetched from content. - NodeContentMediaUtils::copyMediaFiles(p_src.data(), getBackend().data(), destFilePath); + ContentMediaUtils::copyMediaFiles(p_src.data(), getBackend().data(), destFilePath); // Copy attachment folder. Rename attachment folder if conflicts. QString attachmentFolder = p_src->getAttachmentFolder(); if (!attachmentFolder.isEmpty()) { auto destAttachmentFolderPath = fetchNodeAttachmentFolder(destFilePath, attachmentFolder); - NodeContentMediaUtils::copyAttachment(p_src.data(), getBackend().data(), destFilePath, destAttachmentFolderPath); + ContentMediaUtils::copyAttachment(p_src.data(), getBackend().data(), destFilePath, destAttachmentFolderPath); } // Create a file node. @@ -690,7 +690,7 @@ void VXNotebookConfigMgr::removeFilesOfNode(Node *p_node, bool p_force) } // Delete media files fetched from content. - NodeContentMediaUtils::removeMediaFiles(p_node); + ContentMediaUtils::removeMediaFiles(p_node); // Delete node file itself. auto filePath = p_node->fetchPath(); @@ -787,7 +787,7 @@ QSharedPointer VXNotebookConfigMgr::copyFileAsChildOf(const QString &p_src getBackend()->copyFile(p_srcPath, destFilePath); // Copy media files fetched from content. - NodeContentMediaUtils::copyMediaFiles(p_srcPath, getBackend().data(), destFilePath); + ContentMediaUtils::copyMediaFiles(p_srcPath, getBackend().data(), destFilePath); // Create a file node. auto currentTime = QDateTime::currentDateTimeUtc(); diff --git a/src/core/sessionconfig.cpp b/src/core/sessionconfig.cpp index 00690db8..58280373 100644 --- a/src/core/sessionconfig.cpp +++ b/src/core/sessionconfig.cpp @@ -55,9 +55,13 @@ void SessionConfig::init() loadCore(sessionJobj); + loadStateAndGeometry(sessionJobj); + if (MainConfig::isVersionChanged()) { doVersionSpecificOverride(); } + + m_exportOption.fromJson(sessionJobj[QStringLiteral("export_option")].toObject()); } void SessionConfig::loadCore(const QJsonObject &p_session) @@ -172,6 +176,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(); return obj; } @@ -185,22 +190,12 @@ QJsonObject SessionConfig::saveStateAndGeometry() const SessionConfig::MainWindowStateGeometry SessionConfig::getMainWindowStateGeometry() const { - auto sessionSettings = getMgr()->getSettings(ConfigMgr::Source::Session); - const auto &sessionJobj = sessionSettings->getJson(); - const auto obj = sessionJobj.value(QStringLiteral("state_geometry")).toObject(); - - MainWindowStateGeometry sg; - sg.m_mainState = readByteArray(obj, QStringLiteral("main_window_state")); - sg.m_mainGeometry = readByteArray(obj, QStringLiteral("main_window_geometry")); - - return sg; + return m_mainWindowStateGeometry; } void SessionConfig::setMainWindowStateGeometry(const SessionConfig::MainWindowStateGeometry &p_state) { - m_mainWindowStateGeometry = p_state; - ++m_revision; - writeToSettings(); + updateConfig(m_mainWindowStateGeometry, p_state, this); } SessionConfig::OpenGL SessionConfig::getOpenGLAtBootstrap() @@ -283,3 +278,20 @@ void SessionConfig::doVersionSpecificOverride() // In a new version, we may want to change one value by force. // SHOULD set the in memory variable only, or will override the notebook list. } + +const ExportOption &SessionConfig::getExportOption() const +{ + return m_exportOption; +} + +void SessionConfig::setExportOption(const ExportOption &p_option) +{ + updateConfig(m_exportOption, p_option, this); +} + +void SessionConfig::loadStateAndGeometry(const QJsonObject &p_session) +{ + const auto obj = p_session.value(QStringLiteral("state_geometry")).toObject(); + m_mainWindowStateGeometry.m_mainState = readByteArray(obj, QStringLiteral("main_window_state")); + m_mainWindowStateGeometry.m_mainGeometry = readByteArray(obj, QStringLiteral("main_window_geometry")); +} diff --git a/src/core/sessionconfig.h b/src/core/sessionconfig.h index ede480ee..16ba4236 100644 --- a/src/core/sessionconfig.h +++ b/src/core/sessionconfig.h @@ -6,6 +6,8 @@ #include #include +#include + namespace vnotex { class SessionConfig : public IConfig @@ -82,6 +84,9 @@ namespace vnotex int getMinimizeToSystemTray() const; void setMinimizeToSystemTray(bool p_enabled); + const ExportOption &getExportOption() const; + void setExportOption(const ExportOption &p_option); + private: void loadCore(const QJsonObject &p_session); @@ -91,6 +96,8 @@ namespace vnotex QJsonArray saveNotebooks() const; + void loadStateAndGeometry(const QJsonObject &p_session); + QJsonObject saveStateAndGeometry() const; void doVersionSpecificOverride(); @@ -102,8 +109,6 @@ namespace vnotex QVector m_notebooks; - // Used to store newly-set state and geometry, since there is no need to store the read-in - // data all the time. MainWindowStateGeometry m_mainWindowStateGeometry; OpenGL m_openGL = OpenGL::None; @@ -116,6 +121,8 @@ namespace vnotex // 0 for disabling minimizing to system tray; // 1 for enabling minimizing to system tray. int m_minimizeToSystemTray = -1; + + ExportOption m_exportOption; }; } // ns vnotex diff --git a/src/core/theme.cpp b/src/core/theme.cpp index c51d4fac..20bc4157 100644 --- a/src/core/theme.cpp +++ b/src/core/theme.cpp @@ -368,7 +368,12 @@ bool Theme::isRef(const QString &p_str) QString Theme::getFile(File p_fileType) const { - QDir dir(m_themeFolderPath); + return getFile(m_themeFolderPath, p_fileType); +} + +QString Theme::getFile(const QString &p_themeFolder, File p_fileType) +{ + QDir dir(p_themeFolder); if (dir.exists(getFileName(p_fileType))) { return dir.filePath(getFileName(p_fileType)); } else if (p_fileType == File::MarkdownEditorStyle) { diff --git a/src/core/theme.h b/src/core/theme.h index ea5534bf..fb49aa0c 100644 --- a/src/core/theme.h +++ b/src/core/theme.h @@ -54,6 +54,8 @@ namespace vnotex static QPixmap getCover(const QString &p_folder); + static QString getFile(const QString &p_themeFolder, File p_fileType); + private: struct Metadata { diff --git a/src/core/thememgr.cpp b/src/core/thememgr.cpp index 3a4d6b89..82d9b555 100644 --- a/src/core/thememgr.cpp +++ b/src/core/thememgr.cpp @@ -15,6 +15,8 @@ using namespace vnotex; QStringList ThemeMgr::s_searchPaths; +QStringList ThemeMgr::s_webStylesSearchPaths; + ThemeMgr::ThemeMgr(const QString &p_currentThemeName, QObject *p_parent) : QObject(p_parent) { @@ -203,3 +205,39 @@ void ThemeMgr::refresh() { loadAvailableThemes(); } + +void ThemeMgr::addWebStylesSearchPath(const QString &p_path) +{ + s_webStylesSearchPaths << p_path; +} + +QVector> ThemeMgr::getWebStyles() const +{ + QVector> styles; + + // From themes. + for (const auto &th : m_themes) { + auto filePath = Theme::getFile(th.m_folderPath, Theme::File::WebStyleSheet); + if (!filePath.isEmpty()) { + styles.push_back(qMakePair(tr("[Theme] %1 %2").arg(th.m_displayName, PathUtils::fileName(filePath)), + filePath)); + } + + filePath = Theme::getFile(th.m_folderPath, Theme::File::HighlightStyleSheet); + if (!filePath.isEmpty()) { + styles.push_back(qMakePair(tr("[Theme] %1 %2").arg(th.m_displayName, PathUtils::fileName(filePath)), + filePath)); + } + } + + // From search paths. + for (const auto &pa : s_webStylesSearchPaths) { + QDir dir(pa); + auto styleFiles = dir.entryList({"*.css"}, QDir::Files); + for (const auto &file : styleFiles) { + styles.push_back(qMakePair(file, dir.filePath(file))); + } + } + + return styles; +} diff --git a/src/core/thememgr.h b/src/core/thememgr.h index 21ba1f3d..2ad2d7ec 100644 --- a/src/core/thememgr.h +++ b/src/core/thememgr.h @@ -64,10 +64,16 @@ namespace vnotex // Won't affect current theme since we do not support changing theme real time for now. void refresh(); + // Return all web stylesheets available, including those from themes and web styles search paths. + // . + QVector> getWebStyles() const; + static void addSearchPath(const QString &p_path); static void addSyntaxHighlightingSearchPaths(const QStringList &p_paths); + static void addWebStylesSearchPath(const QString &p_path); + private: void loadAvailableThemes(); @@ -89,8 +95,11 @@ namespace vnotex // Set at runtime, not from the theme config. QColor m_baseBackground; - // List of path to search for themes. + // List of paths to search for themes. static QStringList s_searchPaths; + + // List of paths to search for CSS styles, including CSS syntax highlighting styles. + static QStringList s_webStylesSearchPaths; }; } // ns vnotex diff --git a/src/core/vnotex.cpp b/src/core/vnotex.cpp index addf8a0c..6a83f9db 100644 --- a/src/core/vnotex.cpp +++ b/src/core/vnotex.cpp @@ -46,6 +46,8 @@ void VNoteX::initThemeMgr() ThemeMgr::addSyntaxHighlightingSearchPaths( QStringList() << configMgr.getUserSyntaxHighlightingFolder() << configMgr.getAppSyntaxHighlightingFolder()); + ThemeMgr::addWebStylesSearchPath(configMgr.getAppWebStylesFolder()); + ThemeMgr::addWebStylesSearchPath(configMgr.getUserWebStylesFolder()); m_themeMgr = new ThemeMgr(configMgr.getCoreConfig().getTheme(), this); } diff --git a/src/core/vnotex.h b/src/core/vnotex.h index d8a215ae..7f836eb4 100644 --- a/src/core/vnotex.h +++ b/src/core/vnotex.h @@ -97,6 +97,8 @@ namespace vnotex // Requested to locate node in explorer. void locateNodeRequested(Node *p_node); + void exportRequested(); + private: explicit VNoteX(QObject *p_parent = nullptr); diff --git a/src/core/viewerresource.h b/src/core/webresource.h similarity index 91% rename from src/core/viewerresource.h rename to src/core/webresource.h index 795d8a78..3ca2b284 100644 --- a/src/core/viewerresource.h +++ b/src/core/webresource.h @@ -1,5 +1,5 @@ -#ifndef VIEWERRESOURCE_H -#define VIEWERRESOURCE_H +#ifndef WEBRESOURCE_H +#define WEBRESOURCE_H #include #include @@ -8,8 +8,8 @@ namespace vnotex { - // Resource for Web viewer. - struct ViewerResource + // Resource for Web. + struct WebResource { struct Resource { @@ -51,6 +51,11 @@ namespace vnotex return obj; } + bool isGlobal() const + { + return m_name == QStringLiteral("global_styles"); + } + QString m_name; bool m_enabled = true; @@ -96,4 +101,4 @@ namespace vnotex } -#endif // VIEWERRESOURCE_H +#endif // WEBRESOURCE_H diff --git a/src/data/core/vnotex.json b/src/data/core/vnotex.json index 888a2d6d..43b67ab3 100644 --- a/src/data/core/vnotex.json +++ b/src/data/core/vnotex.json @@ -24,7 +24,8 @@ "MaximizeSplit" : "Ctrl+G, Shift+\\", "DistributeSplits" : "Ctrl+G, =", "RemoveSplitAndWorkspace" : "Ctrl+G, R", - "NewWorkspace" : "Ctrl+G, N" + "NewWorkspace" : "Ctrl+G, N", + "Export" : "Ctrl+G, T" }, "toolbar_icon_size" : 16 }, @@ -83,7 +84,7 @@ "markdown_editor" : { "override_viewer_resource" : true, "viewer_resource" : { - "template" : "web/markdownviewertemplate.html", + "template" : "web/markdown-viewer-template.html", "resources" : [ { "name" : "global_styles", @@ -217,6 +218,28 @@ } ] }, + "export_resource" : { + "template" : "web/markdown-export-template.html", + "resources" : [ + { + "name" : "global_styles", + "enabled" : true, + "styles" : [ + "web/css/exportglobalstyles.css" + ] + }, + { + "name" : "outline", + "enabled" : true, + "styles" : [ + "web/css/outline.css" + ], + "scripts" : [ + "web/js/outline.js" + ] + } + ] + }, "//comment" : "Whether use javascript or external program to render PlantUML", "web_plantuml" : true, "//comment" : "Whether use javascript or external program to render Graphviz", diff --git a/src/data/extra/extra.qrc b/src/data/extra/extra.qrc index 887f41c7..2501ac27 100644 --- a/src/data/extra/extra.qrc +++ b/src/data/extra/extra.qrc @@ -8,10 +8,13 @@ docs/zh_CN/about_vnotex.txt docs/zh_CN/shortcuts.md docs/zh_CN/markdown_guide.md - web/markdownviewertemplate.html + web/markdown-viewer-template.html + web/markdown-export-template.html web/css/globalstyles.css web/css/markdownit.css web/css/imageviewer.css + web/css/outline.css + web/css/exportglobalstyles.css web/js/qwebchannel.js web/js/eventemitter.js web/js/utils.js @@ -26,6 +29,7 @@ web/js/imageviewer.js web/js/easyaccess.js web/js/crosscopy.js + web/js/outline.js web/js/markdown-it/markdown-it-container.min.js web/js/markdown-it/markdown-it-emoji.min.js web/js/markdown-it/markdown-it-footnote.min.js diff --git a/src/data/extra/themes/moonlight/interface.qss b/src/data/extra/themes/moonlight/interface.qss index 52f0c804..46d56ccf 100644 --- a/src/data/extra/themes/moonlight/interface.qss +++ b/src/data/extra/themes/moonlight/interface.qss @@ -20,15 +20,15 @@ QWidget[DialogCentralWidget="true"] { /* All widgets */ *[State="info"] { - border: 2px solid @widgets#qwidget#info#border; + border: 1px solid @widgets#qwidget#info#border; } *[State="warning"] { - border: 2px solid @widgets#qwidget#warning#border; + border: 1px solid @widgets#qwidget#warning#border; } *[State="error"] { - border: 2px solid @widgets#qwidget#error#border; + border: 1px solid @widgets#qwidget#error#border; } /* QAbstractScrollArea */ @@ -430,6 +430,14 @@ QLineEdit:disabled { color: @widgets#qlineedit#disabled#fg; } +/* QPlainTextEdit */ +QPlainTextEdit[ConsoleTextEdit="true"] { + color: @widgets#qlineedit#fg; + background-color: @widgets#qlineedit#bg; + selection-color: @widgets#qlineedit#selection#fg; + selection-background-color: @widgets#qlineedit#selection#bg; +} + /* QTabWidget */ QTabWidget { border: none; diff --git a/src/data/extra/themes/moonlight/palette.json b/src/data/extra/themes/moonlight/palette.json index d7857e1c..667c3323 100644 --- a/src/data/extra/themes/moonlight/palette.json +++ b/src/data/extra/themes/moonlight/palette.json @@ -44,7 +44,7 @@ "bg2_9" : "#919cd8", "fg10" : "#b71c1c", "fg11" : "#ab5683", - "fg12" : "#283593", + "fg12" : "#5768c4", "fg13" : "#b42b1f", "fg15_3" : "#4f5666", "fg15_4" : "#60697c", diff --git a/src/data/extra/themes/native/interface.qss b/src/data/extra/themes/native/interface.qss index d39cae4b..314752a5 100644 --- a/src/data/extra/themes/native/interface.qss +++ b/src/data/extra/themes/native/interface.qss @@ -10,15 +10,15 @@ /* All widgets */ *[State="info"] { - border: 2px solid @base#info#fg; + border: 1px solid @base#info#fg; } *[State="warning"] { - border: 2px solid @base#warning#fg; + border: 1px solid @base#warning#fg; } *[State="error"] { - border: 2px solid @base#error#fg; + border: 1px solid @base#error#fg; } /* ToolBox */ diff --git a/src/data/extra/themes/pure/highlight.css b/src/data/extra/themes/pure/highlight.css index dd00c564..4d980eb1 100644 --- a/src/data/extra/themes/pure/highlight.css +++ b/src/data/extra/themes/pure/highlight.css @@ -33,14 +33,14 @@ pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selectio code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { text-shadow: none; background-color: #1976d2; - color: #f1f1f1; + color: #f5f5f5; } pre[class*="language-"]::selection, pre[class*="language-"] ::selection, code[class*="language-"]::selection, code[class*="language-"] ::selection { text-shadow: none; background-color: #1976d2; - color: #f1f1f1; + color: #f5f5f5; } @media print { diff --git a/src/data/extra/themes/pure/interface.qss b/src/data/extra/themes/pure/interface.qss index 52f0c804..46d56ccf 100644 --- a/src/data/extra/themes/pure/interface.qss +++ b/src/data/extra/themes/pure/interface.qss @@ -20,15 +20,15 @@ QWidget[DialogCentralWidget="true"] { /* All widgets */ *[State="info"] { - border: 2px solid @widgets#qwidget#info#border; + border: 1px solid @widgets#qwidget#info#border; } *[State="warning"] { - border: 2px solid @widgets#qwidget#warning#border; + border: 1px solid @widgets#qwidget#warning#border; } *[State="error"] { - border: 2px solid @widgets#qwidget#error#border; + border: 1px solid @widgets#qwidget#error#border; } /* QAbstractScrollArea */ @@ -430,6 +430,14 @@ QLineEdit:disabled { color: @widgets#qlineedit#disabled#fg; } +/* QPlainTextEdit */ +QPlainTextEdit[ConsoleTextEdit="true"] { + color: @widgets#qlineedit#fg; + background-color: @widgets#qlineedit#bg; + selection-color: @widgets#qlineedit#selection#fg; + selection-background-color: @widgets#qlineedit#selection#bg; +} + /* QTabWidget */ QTabWidget { border: none; diff --git a/src/data/extra/themes/pure/palette.json b/src/data/extra/themes/pure/palette.json index 6265268d..a0dcbe23 100644 --- a/src/data/extra/themes/pure/palette.json +++ b/src/data/extra/themes/pure/palette.json @@ -20,7 +20,7 @@ "bg3_4" : "#dadada", "bg3_41" : "#e0e0e0", "bg3_5" : "#eaeaea", - "bg3_6" : "#f1f1f1", + "bg3_6" : "#f5f5f5", "fg3_5" : "#222222", "fg3_6" : "#646464", "fg3_7" : "#7a7a7a", @@ -33,7 +33,7 @@ "bg2_7" : "#e5f3f1", "fg10" : "#b71c1c", "fg11" : "#ab5683", - "fg12" : "#283593", + "fg12" : "#007b6e", "fg13" : "#b42b1f", "fg15_3" : "#b0b0b0", "fg15_4" : "#7a7a7a", diff --git a/src/data/extra/themes/pure/text-editor.theme b/src/data/extra/themes/pure/text-editor.theme index 398d5197..882282dd 100644 --- a/src/data/extra/themes/pure/text-editor.theme +++ b/src/data/extra/themes/pure/text-editor.theme @@ -10,8 +10,8 @@ "font-family" : "YaHei Consolas Hybrid, Consolas, Monaco, Andale Mono, Monospace, Courier New", "font-size" : 12, "text-color" : "#222222", - "background-color" : "#f1f1f1", - "selected-text-color" : "#f1f1f1", + "background-color" : "#f5f5f5", + "selected-text-color" : "#f5f5f5", "selected-background-color" : "#1976d2" }, "CursorLine" : { @@ -30,7 +30,7 @@ }, "IndicatorsBorder" : { "text-color" : "#aaaaaa", - "background-color" : "#ededed" + "background-color" : "#f1f1f1" }, "CurrentLineNumber" : { "text-color" : "#222222" @@ -70,8 +70,8 @@ "font-family" : "冬青黑体, YaHei Consolas Hybrid, Microsoft YaHei, 微软雅黑, Microsoft YaHei UI, WenQuanYi Micro Hei, 文泉驿雅黑, Dengxian, 等线体, STXihei, 华文细黑, Liberation Sans, Droid Sans, NSimSun, 新宋体, SimSun, 宋体, Verdana, Helvetica, sans-serif, Tahoma, Arial, Geneva, Georgia, Times New Roman", "font-size" : 12, "text-color" : "#222222", - "background-color" : "#f1f1f1", - "selected-text-color" : "#f1f1f1", + "background-color" : "#f5f5f5", + "selected-text-color" : "#f5f5f5", "selected-background-color" : "#1976d2" } }, diff --git a/src/data/extra/themes/pure/web.css b/src/data/extra/themes/pure/web.css index 8227e978..281849d9 100644 --- a/src/data/extra/themes/pure/web.css +++ b/src/data/extra/themes/pure/web.css @@ -4,7 +4,7 @@ body { color: #222222; line-height: 1.5; padding: 15px; - background-color: #f1f1f1; + background-color: #f5f5f5; font-size: 16px; } @@ -202,7 +202,7 @@ div.vx-plantuml-graph { ::selection { background-color: #1976d2; - color: #f1f1f1; + color: #f5f5f5; } ::-webkit-scrollbar { diff --git a/src/data/extra/web/css/exportglobalstyles.css b/src/data/extra/web/css/exportglobalstyles.css new file mode 100644 index 00000000..255262e5 --- /dev/null +++ b/src/data/extra/web/css/exportglobalstyles.css @@ -0,0 +1,3 @@ +div.code-toolbar > div.toolbar { + display: none; +} diff --git a/src/data/extra/web/css/globalstyles.css b/src/data/extra/web/css/globalstyles.css index 64edf872..a4f94436 100644 --- a/src/data/extra/web/css/globalstyles.css +++ b/src/data/extra/web/css/globalstyles.css @@ -73,9 +73,13 @@ content: counter(section1) "." counter(section2) "." counter(section3) "." counter(section4) "." counter(section5) "." counter(section6) ". "; } -#vx-content.vx-constrain-image-width img { - max-width: 100%; - height: auto; +#vx-content.vx-constrain-image-width img, +#vx-content.vx-constrain-image-width div.vx-plantuml-graph > svg, +#vx-content.vx-constrain-image-width div.vx-mermaid-graph, +#vx-content.vx-constrain-image-width div.vx-flowchartjs-graph, +#vx-content.vx-constrain-image-width div.vx-wavedrom-graph { + max-width: 100% !important; + height: auto !important; } /* Table of Contents */ @@ -133,3 +137,23 @@ #vx-content.vx-indent-first-line p { text-indent: 2em; } + +body.vx-transparent-background { + background-color: transparent !important; +} + +#vx-content.vx-nonscrollable pre { + white-space: pre-wrap !important; + word-break: break-all !important; + overflow: hidden !important; +} + +#vx-content.vx-nonscrollable pre code { + white-space: pre-wrap !important; + word-break: break-all !important; +} + +#vx-content.vx-nonscrollable code, +#vx-content.vx-nonscrollable a { + word-break: break-all !important; +} diff --git a/src/data/extra/web/css/outline.css b/src/data/extra/web/css/outline.css new file mode 100644 index 00000000..b73b2ba1 --- /dev/null +++ b/src/data/extra/web/css/outline.css @@ -0,0 +1,205 @@ +*, +*::before, +*::after { + box-sizing: border-box; +} + +.container-fluid { + width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +.col, .col-1, .col-10, .col-11, .col-12, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-auto, .col-lg, .col-lg-1, .col-lg-10, .col-lg-11, .col-lg-12, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-auto, .col-md, .col-md-1, .col-md-10, .col-md-11, .col-md-12, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-auto, .col-sm, .col-sm-1, .col-sm-10, .col-sm-11, .col-sm-12, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-auto, .col-xl, .col-xl-1, .col-xl-10, .col-xl-11, .col-xl-12, .col-xl-2, .col-xl-3, .col-xl-4, .col-xl-5, .col-xl-6, .col-xl-7, .col-xl-8, .col-xl-9, .col-xl-auto { + position: relative; + width: 100%; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; +} + +.col-12 { + -webkit-box-flex: 0; + -ms-flex: 0 0 100%; + flex: 0 0 100%; + max-width: 100%; +} + +@media (min-width: 768px) { + .col-md-3 { + -webkit-box-flex: 0; + -ms-flex: 0 0 25%; + flex: 0 0 25%; + max-width: 25%; + } +} + +@media (min-width: 768px) { + .col-md-9 { + -webkit-box-flex: 0; + -ms-flex: 0 0 75%; + flex: 0 0 75%; + max-width: 75%; + } +} + +@media (min-width: 1200px) { + .col-xl-2 { + -webkit-box-flex: 0; + -ms-flex: 0 0 16.666667%; + flex: 0 0 16.666667%; + max-width: 16.666667%; + } +} + +@media (min-width: 1200px) { + .col-xl-10 { + -webkit-box-flex: 0; + -ms-flex: 0 0 83.333333%; + flex: 0 0 83.333333%; + max-width: 83.333333%; + } +} + +@media (min-width: 768px) { + .pt-md-3, .py-md-3 { + padding-top: 1rem!important; + } +} + +@media (min-width: 768px) { + .pb-md-3, .py-md-3 { + padding-bottom: 1rem!important; + } +} + +@media (min-width: 768px) { + .pl-md-5, .px-md-5 { + padding-left: 3rem!important; + } +} + +.d-none { + display: none!important; +} + +@media (min-width: 1200px) { + .d-xl-block { + display: block!important; + } +} + +@media (min-width: 768px) { + .d-md-block { + display: block!important; + } +} + +.bd-content { + -webkit-box-ordinal-group: 1; + -ms-flex-order: 0; + order: 0; +} + +.bd-toc { + position: -webkit-sticky; + position: sticky; + top: 4rem; + height: calc(100vh - 10rem); + overflow-y: auto; +} + +.bd-toc { + -webkit-box-ordinal-group: 2; + -ms-flex-order: 1; + order: 1; + padding-top: 1.5rem; + padding-bottom: 1.5rem; + font-size: .875rem; +} + +.section-nav { + padding-left: 0; +} + +.section-nav ul { + font-size: .875rem; + list-style-type: none; +} + +.section-nav li { + font-size: .875rem; +} + +.section-nav a { + color: inherit !important; +} + +.row { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + margin-right: -15px; + margin-left: -15px; +} + +@media (min-width: 1200px) { + .flex-xl-nowrap { + flex-wrap: nowrap !important; + } +} + +#floating-button { + width: 2.5rem; + height: 2.5rem; + border-radius: 50%; + background: #00897B; + position: fixed; + top: .5rem; + right: .5rem; + cursor: pointer; + box-shadow: 0px 2px 5px #666; +} + +#floating-button .more { + color: #F5F5F5; + position: absolute; + top: 0; + display: block; + bottom: 0; + left: 0; + right: 0; + text-align: center; + padding: 0; + margin: 0; + line-height: 2.5rem; + font-size: 2rem; + font-family: 'monospace'; + font-weight: 300; +} + +.hide-none { + display: none !important; +} + +.col-expand { + -webkit-box-flex: 0; + -ms-flex: 0 0 100% !important; + flex: 0 0 100% !important; + max-width: 100% !important; + padding-right: 3rem !important; +} + +.outline-bold { + font-weight: bolder !important; +} + +@media print { + #floating-button { + display: none !important; + } +} diff --git a/src/data/extra/web/js/graphviz.js b/src/data/extra/web/js/graphviz.js index 6b1cbf10..eab94f98 100644 --- a/src/data/extra/web/js/graphviz.js +++ b/src/data/extra/web/js/graphviz.js @@ -19,7 +19,8 @@ class Graphviz extends GraphRenderer { registerInternal() { this.vnotex.on('basicMarkdownRendered', () => { this.reset(); - this.renderCodeNodes(this.vnotex.contentContainer, 'svg'); + this.renderCodeNodes(this.vnotex.contentContainer, + window.vxOptions.transformSvgToPngEnabled ? 'png' : 'svg'); }); this.vnotex.getWorker('markdownit').addLangsToSkipHighlight(this.langs); diff --git a/src/data/extra/web/js/markdownit.js b/src/data/extra/web/js/markdownit.js index 665b722a..3b07b546 100644 --- a/src/data/extra/web/js/markdownit.js +++ b/src/data/extra/web/js/markdownit.js @@ -240,10 +240,6 @@ class MarkdownIt extends VxWorker { } registerInternal() { - this.vnotex.on('ready', () => { - this.setConstrainImageWidthEnabled(window.vxOptions.constrainImageWidthEnabled); - this.setIndentFirstLineEnabled(window.vxOptions.indentFirstLineEnabled); - }); this.vnotex.on('markdownTextUpdated', (p_text) => { this.render(this.vnotex.contentContainer, p_text, @@ -251,24 +247,6 @@ class MarkdownIt extends VxWorker { }); } - setConstrainImageWidthEnabled(p_enabled) { - let constrainClass = 'vx-constrain-image-width'; - if (p_enabled) { - this.vnotex.contentContainer.classList.add(constrainClass); - } else { - this.vnotex.contentContainer.classList.remove(constrainClass); - } - } - - setIndentFirstLineEnabled(p_enabled) { - let constrainClass = 'vx-indent-first-line'; - if (p_enabled) { - this.vnotex.contentContainer.classList.add(constrainClass); - } else { - this.vnotex.contentContainer.classList.remove(constrainClass); - } - } - // Render Markdown @p_text to HTML in @p_node. // @p_finishCbStr will be called after finishing loading new content nodes. // This could prevent Mermaid Gantt from negative width error. diff --git a/src/data/extra/web/js/markdownviewer.js b/src/data/extra/web/js/markdownviewer.js index 275e7a43..e7f88d3a 100644 --- a/src/data/extra/web/js/markdownviewer.js +++ b/src/data/extra/web/js/markdownviewer.js @@ -43,6 +43,10 @@ new QWebChannel(qt.webChannelTransport, window.vnotex.findText(p_text, p_options); }); + adapter.contentRequested.connect(function() { + window.vnotex.saveContent(); + }); + console.log('QWebChannel has been set up'); if (window.vnotex.initialized) { window.vnotex.kickOffMarkdown(); diff --git a/src/data/extra/web/js/mathjax.js b/src/data/extra/web/js/mathjax.js index 6701423a..3ef770b8 100644 --- a/src/data/extra/web/js/mathjax.js +++ b/src/data/extra/web/js/mathjax.js @@ -18,7 +18,8 @@ window.MathJax = { }, svg: { // Make SVG self-contained. - fontCache: 'local' + fontCache: 'local', + scale: window.vxOptions.mathJaxScale > 0 ? window.vxOptions.mathJaxScale : 1 } }; diff --git a/src/data/extra/web/js/outline.js b/src/data/extra/web/js/outline.js new file mode 100644 index 00000000..748f9f85 --- /dev/null +++ b/src/data/extra/web/js/outline.js @@ -0,0 +1,241 @@ +var toc = []; + +var setVisible = function(node, visible) { + var cl = 'hide-none'; + if (visible) { + node.classList.remove(cl); + } else { + node.classList.add(cl); + } +}; + +var isVisible = function(node) { + var cl = 'hide-none'; + return !node.classList.contains(cl); +}; + +var setPostContentExpanded = function(node, expanded) { + var cl = 'col-expand'; + if (expanded) { + node.classList.add(cl); + } else { + node.classList.remove(cl); + } +}; + +var setOutlinePanelVisible = function(visible) { + var outlinePanel = document.getElementById('outline-panel'); + var postContent = document.getElementById('post-content'); + + setVisible(outlinePanel, visible); + setPostContentExpanded(postContent, !visible); +}; + +var isOutlinePanelVisible = function() { + var outlinePanel = document.getElementById('outline-panel'); + return isVisible(outlinePanel); +}; + +window.addEventListener('load', function() { + var outlinePanel = document.getElementById('outline-panel'); + outlinePanel.style.display = 'initial'; + + var floatingContainer = document.getElementById('container-floating'); + floatingContainer.style.display = 'initial'; + + var outlineContent = document.getElementById('outline-content'); + var postContent = document.getElementById('post-content'); + + // Escape @text to Html. + var escapeHtml = function(text) { + var map = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + + return text.replace(/[&<>"']/g, function(m) { return map[m]; }); + } + + // Fetch the outline. + var headers = postContent.querySelectorAll("h1, h2, h3, h4, h5, h6"); + toc = []; + for (var i = 0; i < headers.length; ++i) { + var header = headers[i]; + + toc.push({ + level: parseInt(header.tagName.substr(1)), + anchor: header.id, + title: escapeHtml(header.textContent) + }); + } + + if (toc.length == 0) { + setOutlinePanelVisible(false); + setVisible(floatingContainer, false); + return; + } + + var baseLevel = baseLevelOfToc(toc); + var tocTree = tocToTree(toPerfectToc(toc, baseLevel), baseLevel); + + outlineContent.innerHTML = tocTree; + setOutlinePanelVisible(true); + setVisible(floatingContainer, true); +}); + +// Return the topest level of @toc, starting from 1. +var baseLevelOfToc = function(p_toc) { + var level = -1; + for (i in p_toc) { + if (level == -1) { + level = p_toc[i].level; + } else if (level > p_toc[i].level) { + level = p_toc[i].level; + } + } + + if (level == -1) { + level = 1; + } + + return level; +}; + +// Handle wrong title levels, such as '#' followed by '###' +var toPerfectToc = function(p_toc, p_baseLevel) { + var i; + var curLevel = p_baseLevel - 1; + var perfToc = []; + for (i in p_toc) { + var item = p_toc[i]; + + // Insert empty header. + while (item.level > curLevel + 1) { + curLevel += 1; + var tmp = { level: curLevel, + anchor: '', + title: '[EMPTY]' + }; + perfToc.push(tmp); + } + + perfToc.push(item); + curLevel = item.level; + } + + return perfToc; +}; + +var itemToHtml = function(item) { + return '' + item.title + ''; +}; + +// Turn a perfect toc to a tree using
    +var tocToTree = function(p_toc, p_baseLevel) { + var i; + var front = '
  • '; + var ending = ['
  • ']; + var curLevel = p_baseLevel; + for (i in p_toc) { + var item = p_toc[i]; + if (item.level == curLevel) { + front += ''; + front += '
  • '; + front += itemToHtml(item); + } else if (item.level > curLevel) { + // assert(item.level - curLevel == 1) + front += '
      '; + ending.push('
    '); + front += '
  • '; + front += itemToHtml(item); + ending.push('
  • '); + curLevel = item.level; + } else { + while (item.level < curLevel) { + var ele = ending.pop(); + front += ele; + if (ele == '
') { + curLevel--; + } + } + front += ''; + front += '
  • '; + front += itemToHtml(item); + } + } + while (ending.length > 0) { + front += ending.pop(); + } + front = front.replace("
  • ", ""); + front = '
      ' + front + '
    '; + return front; +}; + +var toggleMore = function() { + if (toc.length == 0) { + return; + } + + var p = document.getElementById('floating-more'); + if (isOutlinePanelVisible()) { + p.textContent = '<'; + setOutlinePanelVisible(false); + } else { + p.textContent = '>'; + setOutlinePanelVisible(true); + } +}; + +window.addEventListener('scroll', function() { + if (toc.length == 0 || !isOutlinePanelVisible()) { + return; + } + + var postContent = document.getElementById('post-content'); + var scrollTop = document.documentElement.scrollTop + || document.body.scrollTop + || window.pageYOffset; + var eles = postContent.querySelectorAll("h1, h2, h3, h4, h5, h6"); + + if (eles.length == 0) { + return; + } + + var idx = -1; + var biaScrollTop = scrollTop + 50; + for (var i = 0; i < eles.length; ++i) { + if (biaScrollTop >= eles[i].offsetTop) { + idx = i; + } else { + break; + } + } + + var header = ''; + if (idx != -1) { + header = eles[idx].id; + } + + highlightItemOnlyInOutline(header); +}); + +var highlightItemOnlyInOutline = function(id) { + var cl = 'outline-bold'; + var outlineContent = document.getElementById('outline-content'); + var eles = outlineContent.querySelectorAll("a"); + var target = null; + for (var i = 0; i < eles.length; ++i) { + var ele = eles[i]; + if (ele.getAttribute('data') == id) { + target = ele; + ele.classList.add(cl); + } else { + ele.classList.remove(cl); + } + } + + // TODO: scroll target into view within the outline panel scroll area. +}; diff --git a/src/data/extra/web/js/plantuml.js b/src/data/extra/web/js/plantuml.js index a5954380..4d516338 100644 --- a/src/data/extra/web/js/plantuml.js +++ b/src/data/extra/web/js/plantuml.js @@ -19,7 +19,8 @@ class PlantUml extends GraphRenderer { registerInternal() { this.vnotex.on('basicMarkdownRendered', () => { this.reset(); - this.renderCodeNodes(this.vnotex.contentContainer, 'svg'); + this.renderCodeNodes(this.vnotex.contentContainer, + window.vxOptions.transformSvgToPngEnabled ? 'png' : 'svg'); }); this.vnotex.getWorker('markdownit').addLangsToSkipHighlight(this.langs); diff --git a/src/data/extra/web/js/utils.js b/src/data/extra/web/js/utils.js index 3c84958f..9ef623eb 100644 --- a/src/data/extra/web/js/utils.js +++ b/src/data/extra/web/js/utils.js @@ -113,4 +113,55 @@ class Utils { static headingSequenceRegExp() { return /^\d{1,3}(?:\.\d+)*\. /; } + + static fetchStyleContent() { + let styles = ""; + for (let styleIdx = 0; styleIdx < document.styleSheets.length; ++styleIdx) { + let styleSheet = document.styleSheets[styleIdx]; + if (styleSheet.cssRules) { + let baseUrl = null; + if (styleSheet.href) { + let scheme = Utils.getUrlScheme(styleSheet.href); + // We only translate local resources. + if (scheme === 'file' || scheme === 'qrc') { + baseUrl = styleSheet.href.substr(0, styleSheet.href.lastIndexOf('/')); + } + } + + for (let ruleIdx = 0; ruleIdx < styleSheet.cssRules.length; ++ruleIdx) { + let css = styleSheet.cssRules[ruleIdx].cssText; + if (baseUrl) { + // Try to replace the url() with absolute path. + css = Utils.translateCssUrlToAbsolute(baseUrl, css); + } + + styles = styles + css + "\n"; + } + } + } + + return styles; + } + + static translateCssUrlToAbsolute(p_baseUrl, p_css) { + let replaceCssUrl = function(baseUrl, match, p1, offset, str) { + if (Utils.getUrlScheme(p1)) { + return match; + } + + let url = baseUrl + '/' + p1; + return "url(\"" + url + "\");"; + }; + + return p_css.replace(/\burl\(\"([^\"\)]+)\"\);/g, replaceCssUrl.bind(undefined, p_baseUrl)); + } + + static getUrlScheme(p_url) { + let idx = p_url.indexOf(':'); + if (idx > -1) { + return p_url.substr(0, idx); + } else { + return null; + } + } } diff --git a/src/data/extra/web/js/vnotex.js b/src/data/extra/web/js/vnotex.js index 0676495b..325cb856 100644 --- a/src/data/extra/web/js/vnotex.js +++ b/src/data/extra/web/js/vnotex.js @@ -60,6 +60,18 @@ class VNoteX extends EventEmitter { this.sectionNumberBaseLevel = 3; } + this.setContentContainerOption('vx-constrain-image-width', + window.vxOptions.constrainImageWidthEnabled || !window.vxOptions.scrollable); + this.setContentContainerOption('vx-indent-first-line', + window.vxOptions.indentFirstLineEnabled); + this.setBodyOption('vx-transparent-background', + window.vxOptions.transparentBackgroundEnabled); + this.setContentContainerOption('vx-nonscrollable', + !window.vxOptions.scrollable); + + this.setBodySize(window.vxOptions.bodyWidth, window.vxOptions.bodyHeight); + document.body.style.height = '800'; + this.initialized = true; // Signal out. @@ -68,6 +80,22 @@ class VNoteX extends EventEmitter { }); } + setContentContainerOption(p_class, p_enabled) { + if (p_enabled) { + this.contentContainer.classList.add(p_class); + } else { + this.contentContainer.classList.remove(p_class); + } + } + + setBodyOption(p_class, p_enabled) { + if (p_enabled) { + document.body.classList.add(p_class); + } else { + document.body.classList.remove(p_class); + } + } + registerWorker(p_worker) { this.workers.set(p_worker.name, p_worker); @@ -79,6 +107,7 @@ class VNoteX extends EventEmitter { if (this.numOfOngoingWorkers == 0) { // Signal out anyway. this.emit('fullMarkdownRendered'); + window.vxMarkdownAdapter.setWorkFinished(); // Check pending work. if (this.pendingData.text) { @@ -211,13 +240,8 @@ class VNoteX extends EventEmitter { setSectionNumberEnabled(p_enabled) { let sectionClass = 'vx-section-number'; let sectionLevelClass = 'vx-section-number-' + this.sectionNumberBaseLevel; - if (p_enabled) { - this.contentContainer.classList.add(sectionClass); - this.contentContainer.classList.add(sectionLevelClass); - } else { - this.contentContainer.classList.remove(sectionClass); - this.contentContainer.classList.remove(sectionLevelClass); - } + this.setContentContainerOption(sectionClass, p_enabled); + this.setContentContainerOption(sectionLevelClass, p_enabled); } scroll(p_up) { @@ -261,6 +285,28 @@ class VNoteX extends EventEmitter { window.vxMarkdownAdapter.setFindText(p_text, p_totalMatches, p_currentMatchIndex); } + saveContent() { + if (!this.initialized) { + console.warn('saveContent() called before initialization'); + window.vxMarkdownAdapter.setSavedContent('', '', ''); + return; + } + window.vxMarkdownAdapter.setSavedContent("", + Utils.fetchStyleContent(), + this.contentContainer.outerHTML, + document.body.classList.value); + } + + setBodySize(p_width, p_height) { + if (p_width > 0) { + document.body.style.width = p_width + 'px'; + } + + if (p_height > 0) { + document.body.style.height = p_height + 'px'; + } + } + static detectOS() { let osName="Unknown OS"; if (navigator.appVersion.indexOf("Win")!=-1) { diff --git a/src/data/extra/web/markdown-export-template.html b/src/data/extra/web/markdown-export-template.html new file mode 100644 index 00000000..d2328685 --- /dev/null +++ b/src/data/extra/web/markdown-export-template.html @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + +
    +
    + +
    + +
    +
    +
    + + + + diff --git a/src/data/extra/web/markdownviewertemplate.html b/src/data/extra/web/markdown-viewer-template.html similarity index 100% rename from src/data/extra/web/markdownviewertemplate.html rename to src/data/extra/web/markdown-viewer-template.html diff --git a/src/export/export.pri b/src/export/export.pri new file mode 100644 index 00000000..f66abc10 --- /dev/null +++ b/src/export/export.pri @@ -0,0 +1,11 @@ +QT += widgets + +SOURCES += \ + $$PWD/exportdata.cpp \ + $$PWD/exporter.cpp \ + $$PWD/webviewexporter.cpp + +HEADERS += \ + $$PWD/exportdata.h \ + $$PWD/exporter.h \ + $$PWD/webviewexporter.h diff --git a/src/export/exportdata.cpp b/src/export/exportdata.cpp new file mode 100644 index 00000000..9ed952ef --- /dev/null +++ b/src/export/exportdata.cpp @@ -0,0 +1,126 @@ +#include "exportdata.h" + +#include +#include +#include + +using namespace vnotex; + +QJsonObject ExportHtmlOption::toJson() const +{ + QJsonObject obj; + obj["embed_styles"] = m_embedStyles; + obj["complete_page"] = m_completePage; + obj["embed_images"] = m_embedImages; + obj["use_mime_html_format"] = m_useMimeHtmlFormat; + obj["add_outline_panel"] = m_addOutlinePanel; + return obj; +} + +void ExportHtmlOption::fromJson(const QJsonObject &p_obj) +{ + if (p_obj.isEmpty()) { + return; + } + + m_embedStyles = p_obj["embed_styles"].toBool(); + m_completePage = p_obj["complete_page"].toBool(); + m_embedImages = p_obj["embed_images"].toBool(); + m_useMimeHtmlFormat = p_obj["use_mime_html_format"].toBool(); + m_addOutlinePanel = p_obj["add_outline_panel"].toBool(); +} + +bool ExportHtmlOption::operator==(const ExportHtmlOption &p_other) const +{ + return m_embedStyles == p_other.m_embedStyles + && m_completePage == p_other.m_completePage + && m_embedImages == p_other.m_embedImages + && m_useMimeHtmlFormat == p_other.m_useMimeHtmlFormat + && m_addOutlinePanel == p_other.m_addOutlinePanel; +} + +ExportPdfOption::ExportPdfOption() + : m_layout(new QPageLayout(QPageSize(QPageSize::A4), + QPageLayout::Portrait, + QMarginsF(10, 16, 10, 10), + QPageLayout::Millimeter)) +{ +} + +QJsonObject ExportPdfOption::toJson() const +{ + QJsonObject obj; + obj["add_table_of_contents"] = m_addTableOfContents; + obj["use_wkhtmltopdf"] = m_useWkhtmltopdf; + obj["wkhtmltopdf_exe_path"] = m_wkhtmltopdfExePath; + obj["wkhtmltopdf_args"] = m_wkhtmltopdfArgs; + return obj; +} + +void ExportPdfOption::fromJson(const QJsonObject &p_obj) +{ + if (p_obj.isEmpty()) { + return; + } + + m_addTableOfContents = p_obj["add_table_of_contents"].toBool(); + m_useWkhtmltopdf = p_obj["use_wkhtmltopdf"].toBool(); + m_wkhtmltopdfExePath = p_obj["wkhtmltopdf_exe_path"].toString(); + m_wkhtmltopdfArgs = p_obj["wkhtmltopdf_args"].toString(); +} + +bool ExportPdfOption::operator==(const ExportPdfOption &p_other) const +{ + return m_addTableOfContents == p_other.m_addTableOfContents + && m_useWkhtmltopdf == p_other.m_useWkhtmltopdf + && m_wkhtmltopdfExePath == p_other.m_wkhtmltopdfExePath + && m_wkhtmltopdfArgs == p_other.m_wkhtmltopdfArgs; +} + +QJsonObject ExportOption::toJson() const +{ + QJsonObject obj; + obj["use_transparent_bg"] = m_useTransparentBg; + obj["output_dir"] = m_outputDir; + obj["recursive"] = m_recursive; + obj["export_attachments"] = m_exportAttachments; + obj["html_option"] = m_htmlOption.toJson(); + obj["pdf_option"] = m_pdfOption.toJson(); + return obj; +} + +void ExportOption::fromJson(const QJsonObject &p_obj) +{ + if (p_obj.isEmpty()) { + return; + } + + m_useTransparentBg = p_obj["use_transparent_bg"].toBool(); + m_outputDir = p_obj["output_dir"].toString(); + m_recursive = p_obj["recursive"].toBool(); + m_exportAttachments = p_obj["export_attachments"].toBool(); + m_htmlOption.fromJson(p_obj["html_option"].toObject()); + m_pdfOption.fromJson(p_obj["pdf_option"].toObject()); +} + +bool ExportOption::operator==(const ExportOption &p_other) const +{ + bool ret = m_useTransparentBg == p_other.m_useTransparentBg + && m_outputDir == p_other.m_outputDir + && m_recursive == p_other.m_recursive + && m_exportAttachments == p_other.m_exportAttachments; + + if (!ret) { + return false; + } + + if (!(m_htmlOption == p_other.m_htmlOption)) { + return false; + } + + if (!(m_pdfOption == p_other.m_pdfOption)) { + return false; + } + + return true; +} diff --git a/src/export/exportdata.h b/src/export/exportdata.h new file mode 100644 index 00000000..6f0732cd --- /dev/null +++ b/src/export/exportdata.h @@ -0,0 +1,114 @@ +#ifndef EXPORTDATA_H +#define EXPORTDATA_H + +#include + +class QPageLayout; + +namespace vnotex +{ + enum class ExportSource + { + CurrentBuffer = 0, + CurrentFolder, + CurrentNotebook + }; + + enum class ExportFormat + { + Markdown = 0, + HTML, + PDF, + Custom + }; + + struct ExportHtmlOption + { + QJsonObject toJson() const; + void fromJson(const QJsonObject &p_obj); + + bool operator==(const ExportHtmlOption &p_other) const; + + bool m_embedStyles = true; + + bool m_completePage = true; + + bool m_embedImages = true; + + bool m_useMimeHtmlFormat = false; + + // Whether add outline panel. + bool m_addOutlinePanel = true; + }; + + struct ExportPdfOption + { + ExportPdfOption(); + + QJsonObject toJson() const; + void fromJson(const QJsonObject &p_obj); + + bool operator==(const ExportPdfOption &p_other) const; + + QSharedPointer m_layout; + + // Add TOC at the front to complement the missing of outline. + bool m_addTableOfContents = false; + + bool m_useWkhtmltopdf = false; + + QString m_wkhtmltopdfExePath; + + QString m_wkhtmltopdfArgs; + }; + + struct ExportOption + { + QJsonObject toJson() const; + void fromJson(const QJsonObject &p_obj); + + bool operator==(const ExportOption &p_other) const; + + ExportSource m_source = ExportSource::CurrentBuffer; + + ExportFormat m_targetFormat = ExportFormat::HTML; + + bool m_useTransparentBg = true; + + QString m_renderingStyleFile; + + QString m_syntaxHighlightStyleFile; + + QString m_outputDir; + + bool m_recursive = true; + + bool m_exportAttachments = true; + + ExportHtmlOption m_htmlOption; + + ExportPdfOption m_pdfOption; + }; + + inline QString exportFormatString(ExportFormat p_format) + { + switch (p_format) + { + case ExportFormat::Markdown: + return QStringLiteral("Markdown"); + + case ExportFormat::HTML: + return QStringLiteral("HTML"); + + case ExportFormat::PDF: + return QStringLiteral("PDF"); + + case ExportFormat::Custom: + return QStringLiteral("Custom"); + } + + return QStringLiteral("Unknown"); + } +} + +#endif // EXPORTDATA_H diff --git a/src/export/exporter.cpp b/src/export/exporter.cpp new file mode 100644 index 00000000..17c5a91d --- /dev/null +++ b/src/export/exporter.cpp @@ -0,0 +1,312 @@ +#include "exporter.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include "webviewexporter.h" + +using namespace vnotex; + +Exporter::Exporter(QWidget *p_parent) + : QObject(p_parent) +{ +} + +QString Exporter::doExport(const ExportOption &p_option, Buffer *p_buffer) +{ + m_askedToStop = false; + + QString outputFile; + auto file = p_buffer->getFile(); + if (!file) { + emit logRequested(tr("Skipped buffer (%1) without file base.").arg(p_buffer->getName())); + return outputFile; + } + + // Make sure output folder exists. + if (!QDir().mkpath(p_option.m_outputDir)) { + emit logRequested(tr("Failed to create output folder %1.").arg(p_option.m_outputDir)); + return outputFile; + } + + outputFile = doExport(p_option, p_option.m_outputDir, file.data()); + + cleanUp(); + + return outputFile; +} + +QString Exporter::doExportMarkdown(const ExportOption &p_option, const QString &p_outputDir, const File *p_file) +{ + QString outputFile; + if (!p_file->getContentType().isMarkdown()) { + emit logRequested(tr("Format %1 is not supported to export as Markdown.").arg(p_file->getContentType().m_displayName)); + return outputFile; + } + + // Export it to a folder with the same name. + auto name = FileUtils::generateFileNameWithSequence(p_outputDir, p_file->getName(), ""); + auto outputFolder = PathUtils::concatenateFilePath(p_outputDir, name); + QDir outDir(outputFolder); + if (!outDir.mkpath(outputFolder)) { + emit logRequested(tr("Failed to create output folder %1.").arg(outputFolder)); + return outputFile; + } + + // Copy source file itself. + const auto srcFilePath = p_file->getFilePath(); + auto destFilePath = outDir.filePath(p_file->getName()); + FileUtils::copyFile(srcFilePath, destFilePath, false); + outputFile = destFilePath; + + ContentMediaUtils::copyMediaFiles(p_file, destFilePath); + + // Copy attachments if available. + if (p_option.m_exportAttachments) { + exportAttachments(p_file->getNode(), srcFilePath, outputFolder, destFilePath); + } + + return outputFile; +} + +void Exporter::exportAttachments(Node *p_node, + const QString &p_srcFilePath, + const QString &p_outputFolder, + const QString &p_destFilePath) +{ + const auto &attachmentFolder = p_node->getAttachmentFolder(); + if (!attachmentFolder.isEmpty()) { + auto relativePath = PathUtils::relativePath(PathUtils::parentDirPath(p_srcFilePath), + p_node->fetchAttachmentFolderPath()); + auto destAttachmentFolderPath = QDir(p_outputFolder).filePath(relativePath); + destAttachmentFolderPath = FileUtils::renameIfExistsCaseInsensitive(destAttachmentFolderPath); + ContentMediaUtils::copyAttachment(p_node, nullptr, p_destFilePath, destAttachmentFolderPath); + } +} + +QStringList Exporter::doExport(const ExportOption &p_option, Node *p_folder) +{ + m_askedToStop = false; + + auto outputFiles = doExport(p_option, p_option.m_outputDir, p_folder); + + cleanUp(); + + return outputFiles; +} + +QStringList Exporter::doExport(const ExportOption &p_option, const QString &p_outputDir, Node *p_folder) +{ + Q_ASSERT(p_folder->isContainer()); + + QStringList outputFiles; + + // Make path. + auto name = FileUtils::generateFileNameWithSequence(p_outputDir, p_folder->getName()); + auto outputFolder = PathUtils::concatenateFilePath(p_outputDir, name); + if (!QDir().mkpath(outputFolder)) { + emit logRequested(tr("Failed to create output folder %1.").arg(outputFolder)); + return outputFiles; + } + + p_folder->load(); + const auto &children = p_folder->getChildren(); + emit progressUpdated(0, children.size()); + for (int i = 0; i < children.size(); ++i) { + if (checkAskedToStop()) { + break; + } + + const auto &child = children[i]; + if (child->hasContent()) { + auto outputFile = doExport(p_option, outputFolder, child->getContentFile().data()); + if (!outputFile.isEmpty()) { + outputFiles << outputFile; + } + } + if (p_option.m_recursive && child->isContainer() && child->getUse() == Node::Use::Normal) { + outputFiles.append(doExport(p_option, outputFolder, child.data())); + } + + emit progressUpdated(i + 1, children.size()); + } + + return outputFiles; +} + +QString Exporter::doExport(const ExportOption &p_option, const QString &p_outputDir, const File *p_file) +{ + QString outputFile; + + switch (p_option.m_targetFormat) { + case ExportFormat::Markdown: + outputFile = doExportMarkdown(p_option, p_outputDir, p_file); + break; + + case ExportFormat::HTML: + outputFile = doExportHtml(p_option, p_outputDir, p_file); + break; + + case ExportFormat::PDF: + outputFile = doExportPdf(p_option, p_outputDir, p_file); + break; + + default: + emit logRequested(tr("Unknown target format %1.").arg(exportFormatString(p_option.m_targetFormat))); + break; + } + + if (!outputFile.isEmpty()) { + emit logRequested(tr("File (%1) exported to (%2)").arg(p_file->getFilePath(), outputFile)); + } else { + emit logRequested(tr("Failed to export file (%1)").arg(p_file->getFilePath())); + } + + return outputFile; +} + +QStringList Exporter::doExport(const ExportOption &p_option, Notebook *p_notebook) +{ + m_askedToStop = false; + + QStringList outputFiles; + + // Make path. + auto name = FileUtils::generateFileNameWithSequence(p_option.m_outputDir, + tr("notebook_%1").arg(p_notebook->getName())); + auto outputFolder = PathUtils::concatenateFilePath(p_option.m_outputDir, name); + if (!QDir().mkpath(outputFolder)) { + emit logRequested(tr("Failed to create output folder %1.").arg(outputFolder)); + return outputFiles; + } + + auto rootNode = p_notebook->getRootNode(); + Q_ASSERT(rootNode->isLoaded()); + + const auto &children = rootNode->getChildren(); + emit progressUpdated(0, children.size()); + for (int i = 0; i < children.size(); ++i) { + if (checkAskedToStop()) { + break; + } + + const auto &child = children[i]; + if (child->hasContent()) { + auto outputFile = doExport(p_option, outputFolder, child->getContentFile().data()); + if (!outputFile.isEmpty()) { + outputFiles << outputFile; + } + } + if (child->isContainer() && child->getUse() == Node::Use::Normal) { + outputFiles.append(doExport(p_option, outputFolder, child.data())); + } + + emit progressUpdated(i + 1, children.size()); + } + + cleanUp(); + + return outputFiles; +} + +QString Exporter::doExportHtml(const ExportOption &p_option, const QString &p_outputDir, const File *p_file) +{ + QString outputFile; + if (!p_file->getContentType().isMarkdown()) { + emit logRequested(tr("Format %1 is not supported to export as HTML.").arg(p_file->getContentType().m_displayName)); + return outputFile; + } + + QString suffix = p_option.m_htmlOption.m_useMimeHtmlFormat ? QStringLiteral("mht") : QStringLiteral("html"); + auto fileName = FileUtils::generateFileNameWithSequence(p_outputDir, + QFileInfo(p_file->getName()).completeBaseName(), + suffix); + auto destFilePath = PathUtils::concatenateFilePath(p_outputDir, fileName); + + bool success = getWebViewExporter(p_option)->doExport(p_option, p_file, destFilePath); + if (success) { + outputFile = destFilePath; + + // Copy attachments if available. + if (p_option.m_exportAttachments) { + exportAttachments(p_file->getNode(), p_file->getFilePath(), p_outputDir, destFilePath); + } + } + return outputFile; +} + +WebViewExporter *Exporter::getWebViewExporter(const ExportOption &p_option) +{ + if (!m_webViewExporter) { + m_webViewExporter = new WebViewExporter(static_cast(parent())); + connect(m_webViewExporter, &WebViewExporter::logRequested, + this, &Exporter::logRequested); + m_webViewExporter->prepare(p_option); + } + + return m_webViewExporter; +} + +void Exporter::cleanUpWebViewExporter() +{ + if (m_webViewExporter) { + m_webViewExporter->clear(); + delete m_webViewExporter; + m_webViewExporter = nullptr; + } +} + +void Exporter::cleanUp() +{ + cleanUpWebViewExporter(); +} + +void Exporter::stop() +{ + m_askedToStop = true; + + if (m_webViewExporter) { + m_webViewExporter->stop(); + } +} + +bool Exporter::checkAskedToStop() const +{ + if (m_askedToStop) { + emit const_cast(this)->logRequested(tr("Asked to stop. Aborting.")); + return true; + } + + return false; +} + +QString Exporter::doExportPdf(const ExportOption &p_option, const QString &p_outputDir, const File *p_file) +{ + QString outputFile; + if (!p_file->getContentType().isMarkdown()) { + emit logRequested(tr("Format %1 is not supported to export as PDF.").arg(p_file->getContentType().m_displayName)); + return outputFile; + } + + auto fileName = FileUtils::generateFileNameWithSequence(p_outputDir, + QFileInfo(p_file->getName()).completeBaseName(), + "pdf"); + auto destFilePath = PathUtils::concatenateFilePath(p_outputDir, fileName); + + bool success = getWebViewExporter(p_option)->doExport(p_option, p_file, destFilePath); + if (success) { + outputFile = destFilePath; + + // Copy attachments if available. + if (p_option.m_exportAttachments) { + exportAttachments(p_file->getNode(), p_file->getFilePath(), p_outputDir, destFilePath); + } + } + return outputFile; +} diff --git a/src/export/exporter.h b/src/export/exporter.h new file mode 100644 index 00000000..949eb366 --- /dev/null +++ b/src/export/exporter.h @@ -0,0 +1,70 @@ +#ifndef EXPORTER_H +#define EXPORTER_H + +#include +#include + +#include "exportdata.h" + +namespace vnotex +{ + class Notebook; + class Node; + class Buffer; + class File; + class WebViewExporter; + + class Exporter : public QObject + { + Q_OBJECT + public: + // We need the QWidget as parent. + explicit Exporter(QWidget *p_parent); + + // Return exported output file. + QString doExport(const ExportOption &p_option, Buffer *p_buffer); + + // Return exported output files. + QStringList doExport(const ExportOption &p_option, Node *p_folder); + + QStringList doExport(const ExportOption &p_option, Notebook *p_notebook); + + void stop(); + + signals: + void progressUpdated(int p_val, int p_maximum); + + void logRequested(const QString &p_log); + + private: + QStringList doExport(const ExportOption &p_option, const QString &p_outputDir, Node *p_folder); + + QString doExport(const ExportOption &p_option, const QString &p_outputDir, const File *p_file); + + QString doExportMarkdown(const ExportOption &p_option, const QString &p_outputDir, const File *p_file); + + QString doExportHtml(const ExportOption &p_option, const QString &p_outputDir, const File *p_file); + + QString doExportPdf(const ExportOption &p_option, const QString &p_outputDir, const File *p_file); + + void exportAttachments(Node *p_node, + const QString &p_srcFilePath, + const QString &p_outputFolder, + const QString &p_destFilePath); + + WebViewExporter *getWebViewExporter(const ExportOption &p_option); + + void cleanUpWebViewExporter(); + + void cleanUp(); + + bool checkAskedToStop() const; + + // Managed by QObject. + WebViewExporter *m_webViewExporter = nullptr; + + bool m_askedToStop = false; + }; +} + +#endif // EXPORTER_H diff --git a/src/export/webviewexporter.cpp b/src/export/webviewexporter.cpp new file mode 100644 index 00000000..35197a34 --- /dev/null +++ b/src/export/webviewexporter.cpp @@ -0,0 +1,568 @@ +#include "webviewexporter.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace vnotex; + +static const QString c_imgRegExp = "]*)src=\"(?!data:)([^\"]+)\"([^>]*)>"; + +WebViewExporter::WebViewExporter(QWidget *p_parent) + : QObject(p_parent) +{ +} + +WebViewExporter::~WebViewExporter() +{ + clear(); +} + +void WebViewExporter::clear() +{ + m_askedToStop = false; + + delete m_viewer; + m_viewer = nullptr; + + m_htmlTemplate.clear(); + m_exportHtmlTemplate.clear(); + + m_exportOngoing = false; +} + +bool WebViewExporter::doExport(const ExportOption &p_option, + const File *p_file, + const QString &p_outputFile) +{ + bool ret = false; + m_askedToStop = false; + + Q_ASSERT(p_file->getContentType().isMarkdown()); + + Q_ASSERT(!m_exportOngoing); + m_exportOngoing = true; + + m_webViewStates = WebViewState::Started; + + auto baseUrl = PathUtils::pathToUrl(p_file->getContentPath()); + m_viewer->setHtml(m_htmlTemplate, baseUrl); + + auto textContent = p_file->read(); + if (p_option.m_targetFormat == ExportFormat::PDF + && p_option.m_pdfOption.m_addTableOfContents + && !p_option.m_pdfOption.m_useWkhtmltopdf) { + // Add `[TOC]` at the beginning. + m_viewer->adapter()->setText("[TOC]\n\n" + textContent); + } else { + m_viewer->adapter()->setText(textContent); + } + + while (!isWebViewReady()) { + Utils::sleepWait(100); + + if (m_askedToStop) { + goto exit_export; + } + + if (isWebViewFailed()) { + qWarning() << "WebView failed when exporting" << p_file->getFilePath(); + goto exit_export; + } + } + + qDebug() << "WebView is ready"; + + // Add extra wait to make sure Web side is really ready. + Utils::sleepWait(200); + + switch (p_option.m_targetFormat) { + case ExportFormat::HTML: + // TODO: not supported yet. + Q_ASSERT(!p_option.m_htmlOption.m_useMimeHtmlFormat); + ret = doExportHtml(p_option.m_htmlOption, p_outputFile, baseUrl); + break; + + case ExportFormat::PDF: + if (p_option.m_pdfOption.m_useWkhtmltopdf) { + ret = doExportWkhtmltopdf(p_option.m_pdfOption, p_outputFile, baseUrl); + } else { + ret = doExportPdf(p_option.m_pdfOption, p_outputFile); + } + break; + + default: + break; + } + +exit_export: + m_exportOngoing = false; + return ret; +} + +void WebViewExporter::stop() +{ + m_askedToStop = true; +} + +bool WebViewExporter::isWebViewReady() const +{ + return m_webViewStates == (WebViewState::LoadFinished | WebViewState::WorkFinished); +} + +bool WebViewExporter::isWebViewFailed() const +{ + return m_webViewStates & WebViewState::Failed; +} + +bool WebViewExporter::doExportHtml(const ExportHtmlOption &p_htmlOption, + const QString &p_outputFile, + const QUrl &p_baseUrl) +{ + ExportState state = ExportState::Busy; + + connect(m_viewer->adapter(), &MarkdownViewerAdapter::contentReady, + this, [&, this](const QString &p_headContent, + const QString &p_styleContent, + const QString &p_content, + const QString &p_bodyClassList) { + qDebug() << "doExportHtml contentReady"; + // Maybe unnecessary. Just to avoid duplicated signal connections. + disconnect(m_viewer->adapter(), &MarkdownViewerAdapter::contentReady, this, 0); + + if (p_content.isEmpty() || m_askedToStop) { + state = ExportState::Failed; + return; + } + + if (!writeHtmlFile(p_outputFile, + p_baseUrl, + p_headContent, + p_styleContent, + p_content, + p_bodyClassList, + p_htmlOption.m_embedStyles, + p_htmlOption.m_completePage, + p_htmlOption.m_embedImages)) { + state = ExportState::Failed; + return; + } + + state = ExportState::Finished; + }); + + m_viewer->adapter()->saveContent(); + + while (state == ExportState::Busy) { + Utils::sleepWait(100); + + if (m_askedToStop) { + break; + } + } + + return state == ExportState::Finished; +} + +bool WebViewExporter::writeHtmlFile(const QString &p_file, + const QUrl &p_baseUrl, + const QString &p_headContent, + QString p_styleContent, + const QString &p_content, + const QString &p_bodyClassList, + bool p_embedStyles, + bool p_completePage, + bool p_embedImages) +{ + const auto baseName = QFileInfo(p_file).completeBaseName(); + auto title = QString("%1 - %2").arg(baseName, ConfigMgr::c_appName); + const QString resourceFolderName = baseName + "_files"; + auto resourceFolder = PathUtils::concatenateFilePath(PathUtils::parentDirPath(p_file), resourceFolderName); + + qDebug() << "HTML files folder" << resourceFolder; + + auto htmlContent = m_exportHtmlTemplate; + HtmlTemplateHelper::fillTitle(htmlContent, title); + + if (!p_styleContent.isEmpty() && p_embedStyles) { + embedStyleResources(p_styleContent); + HtmlTemplateHelper::fillStyleContent(htmlContent, p_styleContent); + } + + if (!p_headContent.isEmpty()) { + HtmlTemplateHelper::fillHeadContent(htmlContent, p_headContent); + } + + if (p_completePage) { + QString content(p_content); + if (p_embedImages) { + embedBodyResources(p_baseUrl, content); + } else { + fixBodyResources(p_baseUrl, resourceFolder, content); + } + + HtmlTemplateHelper::fillContent(htmlContent, content); + } else { + HtmlTemplateHelper::fillContent(htmlContent, p_content); + } + + if (!p_bodyClassList.isEmpty()) { + HtmlTemplateHelper::fillBodyClassList(htmlContent, p_bodyClassList); + } + + FileUtils::writeFile(p_file, htmlContent); + + // Delete empty resource folder. + QDir dir(resourceFolder); + if (dir.exists() && dir.isEmpty()) { + dir.cdUp(); + dir.rmdir(resourceFolderName); + } + + return true; +} + +QSize WebViewExporter::pageLayoutSize(const QPageLayout &p_layout) const +{ + Q_ASSERT(m_viewer); + auto rect = p_layout.paintRect(QPageLayout::Inch); + return QSize(rect.width() * m_viewer->logicalDpiX(), rect.height() * m_viewer->logicalDpiY()); +} + +void WebViewExporter::prepare(const ExportOption &p_option) +{ + Q_ASSERT(!m_viewer && !m_exportOngoing); + { + // Adapter will be managed by MarkdownViewer. + auto adapter = new MarkdownViewerAdapter(this); + m_viewer = new MarkdownViewer(adapter, QColor(), 1, static_cast(parent())); + m_viewer->hide(); + connect(m_viewer->page(), &QWebEnginePage::loadFinished, + this, [this]() { + m_webViewStates |= WebViewState::LoadFinished; + }); + connect(adapter, &MarkdownViewerAdapter::workFinished, + this, [this]() { + m_webViewStates |= WebViewState::WorkFinished; + }); + } + + const bool scrollable = p_option.m_targetFormat != ExportFormat::PDF; + const auto &config = ConfigMgr::getInst().getEditorConfig().getMarkdownEditorConfig(); + bool useWkhtmltopdf = false; + QSize pageBodySize(1024, 768); + if (p_option.m_targetFormat == ExportFormat::PDF) { + 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); + + { + const bool addOutlinePanel = p_option.m_targetFormat == ExportFormat::HTML && p_option.m_htmlOption.m_addOutlinePanel; + m_exportHtmlTemplate = HtmlTemplateHelper::generateExportTemplate(config, addOutlinePanel); + } + + if (useWkhtmltopdf) { + prepareWkhtmltopdfArguments(p_option.m_pdfOption); + } +} + +static QString marginToStrMM(qreal p_margin) +{ + return QString("%1mm").arg(p_margin); +} + +void WebViewExporter::prepareWkhtmltopdfArguments(const ExportPdfOption &p_pdfOption) +{ + m_wkhtmltopdfArgs.clear(); + + // Page layout. + { + const auto &layout = p_pdfOption.m_layout; + m_wkhtmltopdfArgs << "--page-size" << layout->pageSize().key(); + m_wkhtmltopdfArgs << "--orientation" + << (layout->orientation() == QPageLayout::Portrait ? "Portrait" : "Landscape"); + + const auto marginsMM = layout->margins(QPageLayout::Millimeter); + m_wkhtmltopdfArgs << "--margin-bottom" << marginToStrMM(marginsMM.bottom()); + m_wkhtmltopdfArgs << "--margin-left" << marginToStrMM(marginsMM.left()); + m_wkhtmltopdfArgs << "--margin-right" << marginToStrMM(marginsMM.right()); + m_wkhtmltopdfArgs << "--margin-top" << marginToStrMM(marginsMM.top()); + + // Footer. + m_wkhtmltopdfArgs << "--footer-right" << "[page]" + << "--footer-spacing" << QString::number(marginsMM.bottom() / 3, 'f', 2); + } + + m_wkhtmltopdfArgs << "--encoding" << "utf-8"; + + // Delay 10 seconds for MathJax. + m_wkhtmltopdfArgs << "--javascript-delay" << "5000"; + + m_wkhtmltopdfArgs << "--enable-local-file-access"; + + // Append additional global option. + if (!p_pdfOption.m_wkhtmltopdfArgs.isEmpty()) { + m_wkhtmltopdfArgs.append(ProcessUtils::parseCombinedArgString(p_pdfOption.m_wkhtmltopdfArgs)); + } + + // Must be put after the global object options. + if (p_pdfOption.m_addTableOfContents) { + m_wkhtmltopdfArgs << "toc" << "--toc-text-size-shrink" << "1.0"; + } +} + +bool WebViewExporter::embedStyleResources(QString &p_html) const +{ + bool altered = false; + QRegExp reg("\\burl\\(\"((file|qrc):[^\"\\)]+)\"\\);"); + + int pos = 0; + while (pos < p_html.size()) { + int idx = p_html.indexOf(reg, pos); + if (idx == -1) { + break; + } + + QString dataURI = WebUtils::toDataUri(QUrl(reg.cap(1)), false); + if (dataURI.isEmpty()) { + pos = idx + reg.matchedLength(); + } else { + // Replace the url string in html. + QString newUrl = QString("url('%1');").arg(dataURI); + p_html.replace(idx, reg.matchedLength(), newUrl); + pos = idx + newUrl.size(); + altered = true; + } + } + + return altered; +} + +bool WebViewExporter::embedBodyResources(const QUrl &p_baseUrl, QString &p_html) +{ + bool altered = false; + if (p_baseUrl.isEmpty()) { + return altered; + } + + QRegExp reg(c_imgRegExp); + + int pos = 0; + while (pos < p_html.size()) { + int idx = p_html.indexOf(reg, pos); + if (idx == -1) { + break; + } + + if (reg.cap(2).isEmpty()) { + pos = idx + reg.matchedLength(); + continue; + } + + QUrl srcUrl(p_baseUrl.resolved(reg.cap(2))); + const auto dataURI = WebUtils::toDataUri(srcUrl, true); + if (dataURI.isEmpty()) { + pos = idx + reg.matchedLength(); + } else { + // Replace the url string in html. + QString newUrl = QString("").arg(reg.cap(1), dataURI, reg.cap(3)); + p_html.replace(idx, reg.matchedLength(), newUrl); + pos = idx + newUrl.size(); + altered = true; + } + } + + return altered; +} + +static QString getResourceRelativePath(const QString &p_file) +{ + int idx = p_file.lastIndexOf('/'); + int idx2 = p_file.lastIndexOf('/', idx - 1); + Q_ASSERT(idx > 0 && idx2 < idx); + return "." + p_file.mid(idx2); +} + +bool WebViewExporter::fixBodyResources(const QUrl &p_baseUrl, + const QString &p_folder, + QString &p_html) +{ + bool altered = false; + if (p_baseUrl.isEmpty()) { + return altered; + } + + QRegExp reg(c_imgRegExp); + + int pos = 0; + while (pos < p_html.size()) { + int idx = p_html.indexOf(reg, pos); + if (idx == -1) { + break; + } + + if (reg.cap(2).isEmpty()) { + pos = idx + reg.matchedLength(); + continue; + } + + QUrl srcUrl(p_baseUrl.resolved(reg.cap(2))); + QString targetFile = WebUtils::copyResource(srcUrl, p_folder); + if (targetFile.isEmpty()) { + pos = idx + reg.matchedLength(); + } else { + // Replace the url string in html. + QString newUrl = QString("").arg(reg.cap(1), getResourceRelativePath(targetFile), reg.cap(3)); + p_html.replace(idx, reg.matchedLength(), newUrl); + pos = idx + newUrl.size(); + altered = true; + } + } + + return altered; +} + +bool WebViewExporter::doExportPdf(const ExportPdfOption &p_pdfOption, const QString &p_outputFile) +{ + ExportState state = ExportState::Busy; + + m_viewer->page()->printToPdf([&, this](const QByteArray &p_result) { + qDebug() << "doExportPdf printToPdf ready"; + if (p_result.isEmpty() || m_askedToStop) { + state = ExportState::Failed; + return; + } + + Q_ASSERT(!p_outputFile.isEmpty()); + + FileUtils::writeFile(p_outputFile, p_result); + + state = ExportState::Finished; + }, *p_pdfOption.m_layout); + + while (state == ExportState::Busy) { + Utils::sleepWait(100); + + if (m_askedToStop) { + break; + } + } + + return state == ExportState::Finished; +} + +bool WebViewExporter::doExportWkhtmltopdf(const ExportPdfOption &p_pdfOption, const QString &p_outputFile, const QUrl &p_baseUrl) +{ + ExportState state = ExportState::Busy; + + connect(m_viewer->adapter(), &MarkdownViewerAdapter::contentReady, + this, [&, this](const QString &p_headContent, + const QString &p_styleContent, + const QString &p_content, + const QString &p_bodyClassList) { + qDebug() << "doExportWkhtmltopdf contentReady"; + // Maybe unnecessary. Just to avoid duplicated signal connections. + disconnect(m_viewer->adapter(), &MarkdownViewerAdapter::contentReady, this, 0); + + if (p_content.isEmpty() || m_askedToStop) { + state = ExportState::Failed; + return; + } + + // Save HTML to a temp dir. + QTemporaryDir tmpDir; + if (!tmpDir.isValid()) { + state = ExportState::Failed; + return; + } + + auto tmpHtmlFile = tmpDir.filePath("vnote_export_tmp.html"); + if (!writeHtmlFile(tmpHtmlFile, + p_baseUrl, + p_headContent, + p_styleContent, + p_content, + p_bodyClassList, + true, + true, + false)) { + state = ExportState::Failed; + return; + } + + // Convert HTML to PDF via wkhtmltopdf. + if (doWkhtmltopdf(p_pdfOption, QStringList() << tmpHtmlFile, p_outputFile)) { + state = ExportState::Finished; + } else { + state = ExportState::Failed; + } + }); + + m_viewer->adapter()->saveContent(); + + while (state == ExportState::Busy) { + Utils::sleepWait(100); + + if (m_askedToStop) { + break; + } + } + + return state == ExportState::Finished; +} + +bool WebViewExporter::doWkhtmltopdf(const ExportPdfOption &p_pdfOption, const QStringList &p_htmlFiles, const QString &p_outputFile) +{ + // Note: system's locale settings (Language for non-Unicode programs) is important to wkhtmltopdf. + // Input file could be encoded via QUrl::fromLocalFile(p_htmlFile).toString(QUrl::EncodeUnicode) to + // handle non-ASCII path. + + QStringList args(m_wkhtmltopdfArgs); + + // Prepare the args. + for (auto const &file : p_htmlFiles) { + args << QDir::toNativeSeparators(file); + } + + args << QDir::toNativeSeparators(p_outputFile); + + return startProcess(p_pdfOption.m_wkhtmltopdfExePath, args); +} + +bool WebViewExporter::startProcess(const QString &p_program, const QStringList &p_args) +{ + emit logRequested(p_program + " " + ProcessUtils::combineArgString(p_args)); + + auto ret = ProcessUtils::start(p_program, + p_args, + [this](const QString &p_log) { + emit logRequested(p_log); + }, + m_askedToStop); + return ret == ProcessUtils::State::Succeeded; +} diff --git a/src/export/webviewexporter.h b/src/export/webviewexporter.h new file mode 100644 index 00000000..169b380b --- /dev/null +++ b/src/export/webviewexporter.h @@ -0,0 +1,110 @@ +#ifndef WEBVIEWEXPORTER_H +#define WEBVIEWEXPORTER_H + +#include + +#include "exportdata.h" + +class QWidget; + +namespace vnotex +{ + class File; + class MarkdownViewer; + + class WebViewExporter : public QObject + { + Q_OBJECT + public: + enum WebViewState + { + Started = 0, + LoadFinished = 0x1, + WorkFinished = 0x2, + Failed = 0x4 + }; + Q_DECLARE_FLAGS(WebViewStates, WebViewState); + + // We need QWidget as parent. + explicit WebViewExporter(QWidget *p_parent); + + ~WebViewExporter(); + + bool doExport(const ExportOption &p_option, + const File *p_file, + const QString &p_outputFile); + + void prepare(const ExportOption &p_option); + + // Release resources after one batch of export. + void clear(); + + void stop(); + + signals: + void logRequested(const QString &p_log); + + private: + enum class ExportState + { + Busy = 0, + Finished, + Failed + }; + + bool isWebViewReady() const; + + bool isWebViewFailed() const; + + bool doExportHtml(const ExportHtmlOption &p_htmlOption, + const QString &p_outputFile, + const QUrl &p_baseUrl); + + bool writeHtmlFile(const QString &p_file, + const QUrl &p_baseUrl, + const QString &p_headContent, + QString p_styleContent, + const QString &p_content, + const QString &p_bodyClassList, + bool p_embedStyles, + bool p_completePage, + bool p_embedImages); + + bool embedStyleResources(QString &p_html) const; + + bool embedBodyResources(const QUrl &p_baseUrl, QString &p_html); + + bool fixBodyResources(const QUrl &p_baseUrl, const QString &p_folder, QString &p_html); + + bool doExportPdf(const ExportPdfOption &p_pdfOption, const QString &p_outputFile); + + bool doExportWkhtmltopdf(const ExportPdfOption &p_pdfOption, const QString &p_outputFile, const QUrl &p_baseUrl); + + QSize pageLayoutSize(const QPageLayout &p_layout) const; + + bool doWkhtmltopdf(const ExportPdfOption &p_pdfOption, const QStringList &p_htmlFiles, const QString &p_outputFile); + + void prepareWkhtmltopdfArguments(const ExportPdfOption &p_pdfOption); + + bool startProcess(const QString &p_program, const QStringList &p_args); + + bool m_askedToStop = false; + + bool m_exportOngoing = false; + + WebViewStates m_webViewStates = WebViewState::Started; + + // Managed by QObject. + MarkdownViewer *m_viewer = nullptr; + + QString m_htmlTemplate; + + QString m_exportHtmlTemplate; + + QStringList m_wkhtmltopdfArgs; + }; +} + +Q_DECLARE_OPERATORS_FOR_FLAGS(vnotex::WebViewExporter::WebViewStates) + +#endif // WEBVIEWEXPORTER_H diff --git a/src/src.pro b/src/src.pro index 2d82bf4f..c14903fd 100644 --- a/src/src.pro +++ b/src/src.pro @@ -42,6 +42,8 @@ include($$LIBS_FOLDER/vtextedit/src/libs/syntax-highlighting/syntax-highlighting include($$PWD/utils/utils.pri) +include($$PWD/export/export.pri) + include($$PWD/core/core.pri) include($$PWD/widgets/widgets.pri) diff --git a/src/core/notebookconfigmgr/nodecontentmediautils.cpp b/src/utils/contentmediautils.cpp similarity index 53% rename from src/core/notebookconfigmgr/nodecontentmediautils.cpp rename to src/utils/contentmediautils.cpp index b149f721..a72799b3 100644 --- a/src/core/notebookconfigmgr/nodecontentmediautils.cpp +++ b/src/utils/contentmediautils.cpp @@ -1,4 +1,4 @@ -#include "nodecontentmediautils.h" +#include "contentmediautils.h" #include #include @@ -15,46 +15,53 @@ #include #include +#include using namespace vnotex; -void NodeContentMediaUtils::copyMediaFiles(const Node *p_node, - INotebookBackend *p_backend, - const QString &p_destFilePath) +void ContentMediaUtils::copyMediaFiles(Node *p_node, + INotebookBackend *p_backend, + const QString &p_destFilePath) { Q_ASSERT(p_node->hasContent()); - /* - const auto &fileType = FileTypeHelper::getInst().getFileType(p_node->fetchAbsolutePath()); - if (fileType.m_type == FileTypeHelper::Markdown) { - copyMarkdownMediaFiles(p_node->read(), - PathUtils::parentDirPath(p_node->fetchContentPath()), + auto file = p_node->getContentFile(); + if (file->getContentType().isMarkdown()) { + copyMarkdownMediaFiles(file->read(), + PathUtils::parentDirPath(file->getContentPath()), p_backend, p_destFilePath); } - */ } -void NodeContentMediaUtils::copyMediaFiles(const QString &p_filePath, - INotebookBackend *p_backend, - const QString &p_destFilePath) +void ContentMediaUtils::copyMediaFiles(const QString &p_filePath, + INotebookBackend *p_backend, + const QString &p_destFilePath) { - /* const auto &fileType = FileTypeHelper::getInst().getFileType(p_filePath); - if (fileType.m_type == FileTypeHelper::Markdown) { + if (fileType.isMarkdown()) { copyMarkdownMediaFiles(FileUtils::readTextFile(p_filePath), PathUtils::parentDirPath(p_filePath), p_backend, p_destFilePath); } - */ } -void NodeContentMediaUtils::copyMarkdownMediaFiles(const QString &p_content, - const QString &p_basePath, - INotebookBackend *p_backend, - const QString &p_destFilePath) +void ContentMediaUtils::copyMediaFiles(const File *p_file, + const QString &p_destFilePath) +{ + if (p_file->getContentType().isMarkdown()) { + copyMarkdownMediaFiles(p_file->read(), + p_file->getResourcePath(), + nullptr, + p_destFilePath); + } +} + +void ContentMediaUtils::copyMarkdownMediaFiles(const QString &p_content, + const QString &p_basePath, + INotebookBackend *p_backend, + const QString &p_destFilePath) { - /* auto content = p_content; // Images. @@ -82,19 +89,20 @@ void NodeContentMediaUtils::copyMarkdownMediaFiles(const QString &p_content, handledImages.insert(link.m_path); if (!QFileInfo::exists(link.m_path)) { - qWarning() << "Image of Markdown file does not exist" << link.m_path << link.m_urlInLink; + qWarning() << "image of Markdown file does not exist" << link.m_path << link.m_urlInLink; continue; } // Get the relative path of the image and apply it to the dest file path. const auto oldDestFilePath = destDir.filePath(link.m_urlInLink); destDir.mkpath(PathUtils::parentDirPath(oldDestFilePath)); - auto destFilePath = p_backend->renameIfExistsCaseInsensitive(oldDestFilePath); + auto destFilePath = p_backend ? p_backend->renameIfExistsCaseInsensitive(oldDestFilePath) + : FileUtils::renameIfExistsCaseInsensitive(oldDestFilePath); if (oldDestFilePath != destFilePath) { // Rename happens. const auto oldFileName = PathUtils::fileName(oldDestFilePath); const auto newFileName = PathUtils::fileName(destFilePath); - qWarning() << QString("Image name conflicts when copy. Renamed from (%1) to (%2)").arg(oldFileName, newFileName); + qWarning() << QString("image name conflicts when copy. Renamed from (%1) to (%2)").arg(oldFileName, newFileName); // Update the text content. auto newUrlInLink(link.m_urlInLink); @@ -106,38 +114,41 @@ void NodeContentMediaUtils::copyMarkdownMediaFiles(const QString &p_content, renamedImages.insert(link.m_path, newUrlInLink); } - p_backend->copyFile(link.m_path, destFilePath); + if (p_backend) { + p_backend->copyFile(link.m_path, destFilePath); + } else { + FileUtils::copyFile(link.m_path, destFilePath); + } } if (!renamedImages.isEmpty()) { - p_backend->writeFile(p_destFilePath, content); + if (p_backend) { + p_backend->writeFile(p_destFilePath, content); + } else { + FileUtils::writeFile(p_destFilePath, content); + } } - */ } -void NodeContentMediaUtils::removeMediaFiles(const Node *p_node) +void ContentMediaUtils::removeMediaFiles(Node *p_node) { - /* - Q_ASSERT(p_node->getType() == Node::Type::File); - const auto &fileType = FileTypeHelper::getInst().getFileType(p_node->fetchAbsolutePath()); - if (fileType.m_type == FileTypeHelper::Markdown) { - removeMarkdownMediaFiles(p_node); + Q_ASSERT(p_node->hasContent()); + auto file = p_node->getContentFile(); + if (file->getContentType().isMarkdown()) { + removeMarkdownMediaFiles(file.data(), p_node->getBackend()); } - */ } -void NodeContentMediaUtils::removeMarkdownMediaFiles(const Node *p_node) +void ContentMediaUtils::removeMarkdownMediaFiles(const File *p_file, INotebookBackend *p_backend) { - /* - auto content = p_node->read(); + auto content = p_file->read(); // Images. const auto images = vte::MarkdownUtils::fetchImagesFromMarkdownText(content, - PathUtils::parentDirPath(p_node->fetchContentPath()), + p_file->getResourcePath(), vte::MarkdownLink::TypeFlag::LocalRelativeInternal); - auto backend = p_node->getBackend(); QSet handledImages; for (const auto &link : images) { if (handledImages.contains(link.m_path)) { @@ -150,40 +161,42 @@ void NodeContentMediaUtils::removeMarkdownMediaFiles(const Node *p_node) qWarning() << "Image of Markdown file does not exist" << link.m_path << link.m_urlInLink; continue; } - backend->removeFile(link.m_path); + p_backend->removeFile(link.m_path); } - */ } -void NodeContentMediaUtils::copyAttachment(Node *p_node, - INotebookBackend *p_backend, - const QString &p_destFilePath, - const QString &p_destAttachmentFolderPath) +void ContentMediaUtils::copyAttachment(Node *p_node, + INotebookBackend *p_backend, + const QString &p_destFilePath, + const QString &p_destAttachmentFolderPath) { - /* - Q_ASSERT(p_node->getType() == Node::Type::File); + Q_ASSERT(p_node->hasContent()); Q_ASSERT(!p_node->getAttachmentFolder().isEmpty()); // Copy the whole folder. const auto srcAttachmentFolderPath = p_node->fetchAttachmentFolderPath(); - p_backend->copyDir(srcAttachmentFolderPath, p_destAttachmentFolderPath); + if (p_backend) { + p_backend->copyDir(srcAttachmentFolderPath, p_destAttachmentFolderPath); + } else { + FileUtils::copyDir(srcAttachmentFolderPath, p_destAttachmentFolderPath); + } // Check if we need to modify links in content. + // FIXME: check the whole relative path. if (p_node->getAttachmentFolder() == PathUtils::dirName(p_destAttachmentFolderPath)) { return; } - const auto &fileType = FileTypeHelper::getInst().getFileType(p_node->fetchAbsolutePath()); - if (fileType.m_type == FileTypeHelper::Markdown) { + auto file = p_node->getContentFile(); + if (file->getContentType().isMarkdown()) { fixMarkdownLinks(srcAttachmentFolderPath, p_backend, p_destFilePath, p_destAttachmentFolderPath); } - */ } -void NodeContentMediaUtils::fixMarkdownLinks(const QString &p_srcFolderPath, - INotebookBackend *p_backend, - const QString &p_destFilePath, - const QString &p_destFolderPath) +void ContentMediaUtils::fixMarkdownLinks(const QString &p_srcFolderPath, + INotebookBackend *p_backend, + const QString &p_destFilePath, + const QString &p_destFolderPath) { // TODO. Q_UNUSED(p_srcFolderPath); diff --git a/src/core/notebookconfigmgr/nodecontentmediautils.h b/src/utils/contentmediautils.h similarity index 77% rename from src/core/notebookconfigmgr/nodecontentmediautils.h rename to src/utils/contentmediautils.h index ff8ba9c9..4288c04a 100644 --- a/src/core/notebookconfigmgr/nodecontentmediautils.h +++ b/src/utils/contentmediautils.h @@ -1,5 +1,5 @@ -#ifndef NODECONTENTMEDIAUTILS_H -#define NODECONTENTMEDIAUTILS_H +#ifndef CONTENTMEDIAUTILS_H +#define CONTENTMEDIAUTILS_H #include @@ -7,16 +7,17 @@ namespace vnotex { class INotebookBackend; class Node; + class File; // Utils to operate on the media files from node's content. - class NodeContentMediaUtils + class ContentMediaUtils { public: - NodeContentMediaUtils() = delete; + ContentMediaUtils() = delete; // Fetch media files from @p_node and copy them to dest folder. // @p_destFilePath: @p_node has been copied to @p_destFilePath. - static void copyMediaFiles(const Node *p_node, + static void copyMediaFiles(Node *p_node, INotebookBackend *p_backend, const QString &p_destFilePath); @@ -25,7 +26,10 @@ namespace vnotex INotebookBackend *p_backend, const QString &p_destFilePath); - static void removeMediaFiles(const Node *p_node); + static void copyMediaFiles(const File *p_file, + const QString &p_destFilePath); + + static void removeMediaFiles(Node *p_node); // Copy attachment folder. static void copyAttachment(Node *p_node, @@ -39,7 +43,7 @@ namespace vnotex INotebookBackend *p_backend, const QString &p_destFilePath); - static void removeMarkdownMediaFiles(const Node *p_node); + static void removeMarkdownMediaFiles(const File *p_file, INotebookBackend *p_backend); // Fix local relative internal links locating in @p_srcFolderPath. static void fixMarkdownLinks(const QString &p_srcFolderPath, @@ -49,4 +53,4 @@ namespace vnotex }; } -#endif // NODECONTENTMEDIAUTILS_H +#endif // CONTENTMEDIAUTILS_H diff --git a/src/utils/fileutils.h b/src/utils/fileutils.h index 95f125d1..eb747733 100644 --- a/src/utils/fileutils.h +++ b/src/utils/fileutils.h @@ -63,7 +63,7 @@ namespace vnotex static QString generateFileNameWithSequence(const QString &p_folderPath, const QString &p_baseName, - const QString &p_suffix); + const QString &p_suffix = QString()); static QTemporaryFile *createTemporaryFile(const QString &p_suffix); diff --git a/src/utils/htmlutils.cpp b/src/utils/htmlutils.cpp index f65bb83a..bddd4782 100644 --- a/src/utils/htmlutils.cpp +++ b/src/utils/htmlutils.cpp @@ -10,3 +10,9 @@ bool HtmlUtils::hasOnlyImgTag(const QString &p_html) QRegExp reg(QStringLiteral("<(?:p|span|div) ")); return !p_html.contains(reg); } + +QString HtmlUtils::escapeHtml(QString p_text) +{ + p_text.replace(">", ">").replace("<", "<").replace("&", "&"); + return p_text; +} diff --git a/src/utils/htmlutils.h b/src/utils/htmlutils.h index 01766541..f6feec63 100644 --- a/src/utils/htmlutils.h +++ b/src/utils/htmlutils.h @@ -11,6 +11,8 @@ namespace vnotex HtmlUtils() = delete; static bool hasOnlyImgTag(const QString &p_html); + + static QString escapeHtml(QString p_text); }; } diff --git a/src/utils/processutils.cpp b/src/utils/processutils.cpp new file mode 100644 index 00000000..5a32683d --- /dev/null +++ b/src/utils/processutils.cpp @@ -0,0 +1,158 @@ +#include "processutils.h" + +#include +#include +#include + +#include "utils.h" + +using namespace vnotex; + +ProcessUtils::State ProcessUtils::start(const QString &p_program, + const QStringList &p_args, + const QByteArray &p_stdIn, + int &p_exitCodeOnSuccess, + QByteArray &p_stdOut, + QByteArray &p_stdErr) +{ + QScopedPointer proc(new QProcess()); + proc->start(p_program, p_args); + return handleProcess(proc.data(), p_stdIn, p_exitCodeOnSuccess, p_stdOut, p_stdErr); +} + +ProcessUtils::State ProcessUtils::handleProcess(QProcess *p_process, + const QByteArray &p_stdIn, + int &p_exitCodeOnSuccess, + QByteArray &p_stdOut, + QByteArray &p_stdErr) +{ + if (!p_process->waitForStarted()) { + return State::FailedToStart; + } + + if (!p_stdIn.isEmpty()) { + if (p_process->write(p_stdIn) == -1) { + p_process->closeWriteChannel(); + qWarning() << "failed to write to stdin of QProcess" << p_process->errorString(); + return State::FailedToWrite; + } else { + p_process->closeWriteChannel(); + } + } + + p_process->waitForFinished(); + + State state = State::Succeeded; + if (p_process->exitStatus() == QProcess::CrashExit) { + state = State::Crashed; + } else { + p_exitCodeOnSuccess = p_process->exitCode(); + } + + p_stdOut = p_process->readAllStandardOutput(); + p_stdErr = p_process->readAllStandardError(); + return state; +} + +QStringList ProcessUtils::parseCombinedArgString(const QString &p_args) +{ + QStringList args; + QString tmp; + int quoteCount = 0; + bool inQuote = false; + + // Handle quoting. + // Tokens can be surrounded by double quotes "hello world". + // Three consecutive double quotes represent the quote character itself. + for (int i = 0; i < p_args.size(); ++i) { + if (p_args.at(i) == QLatin1Char('"')) { + ++quoteCount; + if (quoteCount == 3) { + // Third consecutive quote. + quoteCount = 0; + tmp += p_args.at(i); + } + + continue; + } + + if (quoteCount) { + if (quoteCount == 1) { + inQuote = !inQuote; + } + + quoteCount = 0; + } + + if (!inQuote && p_args.at(i).isSpace()) { + if (!tmp.isEmpty()) { + args += tmp; + tmp.clear(); + } + } else { + tmp += p_args.at(i); + } + } + + if (!tmp.isEmpty()) { + args += tmp; + } + + return args; +} + +QString ProcessUtils::combineArgString(const QStringList &p_args) +{ + QString argStr; + for (const auto &arg : p_args) { + QString tmp(arg); + tmp.replace("\"", "\"\"\""); + if (tmp.contains(' ')) { + tmp = '"' + tmp + '"'; + } + + if (argStr.isEmpty()) { + argStr = tmp; + } else { + argStr = argStr + ' ' + tmp; + } + } + + return argStr; +} + +ProcessUtils::State ProcessUtils::start(const QString &p_program, + const QStringList &p_args, + const std::function &p_logger, + const bool &p_askedToStop) +{ + QProcess proc; + proc.start(p_program, p_args); + + if (!proc.waitForStarted()) { + return State::FailedToStart; + } + + while (proc.state() != QProcess::NotRunning) { + Utils::sleepWait(100); + + auto outBa = proc.readAllStandardOutput(); + auto errBa = proc.readAllStandardError(); + QString msg; + if (!outBa.isEmpty()) { + msg += QString::fromLocal8Bit(outBa); + } + if (!errBa.isEmpty()) { + msg += QString::fromLocal8Bit(errBa); + } + if (!msg.isEmpty()) { + p_logger(msg); + } + + if (p_askedToStop) { + break; + } + } + + return proc.exitStatus() == QProcess::NormalExit ? State::Succeeded : State::Crashed; +} diff --git a/src/utils/processutils.h b/src/utils/processutils.h new file mode 100644 index 00000000..a04b47c4 --- /dev/null +++ b/src/utils/processutils.h @@ -0,0 +1,52 @@ +#ifndef PROCESSUTILS_H +#define PROCESSUTILS_H + +#include + +#include +#include + +class QProcess; + +namespace vnotex +{ + class ProcessUtils + { + public: + enum State + { + Succeeded, + Crashed, + FailedToStart, + FailedToWrite + }; + + ProcessUtils() = delete; + + static State start(const QString &p_program, + const QStringList &p_args, + const QByteArray &p_stdIn, + int &p_exitCodeOnSuccess, + QByteArray &p_stdOut, + QByteArray &p_stdErr); + + static State start(const QString &p_program, + const QStringList &p_args, + const std::function &p_logger, + const bool &p_askedToStop); + + // Copied from QProcess code. + static QStringList parseCombinedArgString(const QString &p_args); + + static QString combineArgString(const QStringList &p_args); + + private: + static State handleProcess(QProcess *p_process, + const QByteArray &p_stdIn, + int &p_exitCodeOnSuccess, + QByteArray &p_stdOut, + QByteArray &p_stdErr); + }; +} + +#endif // PROCESSUTILS_H diff --git a/src/utils/textutils.cpp b/src/utils/textutils.cpp index b8cdaa94..87b32123 100644 --- a/src/utils/textutils.cpp +++ b/src/utils/textutils.cpp @@ -75,13 +75,3 @@ QString TextUtils::unindentTextMultiLines(const QString &p_text) return res; } - -QString TextUtils::purifyUrl(const QString &p_url) -{ - int idx = p_url.indexOf('?'); - if (idx > -1) { - return p_url.left(idx); - } - - return p_url; -} diff --git a/src/utils/textutils.h b/src/utils/textutils.h index 2d3f487c..a432205b 100644 --- a/src/utils/textutils.h +++ b/src/utils/textutils.h @@ -20,9 +20,6 @@ namespace vnotex // Unindent multi-lines text according to the indentation of the first line. static QString unindentTextMultiLines(const QString &p_text); - - // Remove query in the url (?xxx). - static QString purifyUrl(const QString &p_url); }; } diff --git a/src/utils/utils.pri b/src/utils/utils.pri index 3b2ca05c..3affa106 100644 --- a/src/utils/utils.pri +++ b/src/utils/utils.pri @@ -1,25 +1,31 @@ QT += widgets svg SOURCES += \ + $$PWD/contentmediautils.cpp \ $$PWD/docsutils.cpp \ $$PWD/htmlutils.cpp \ $$PWD/pathutils.cpp \ + $$PWD/processutils.cpp \ $$PWD/textutils.cpp \ $$PWD/urldragdroputils.cpp \ $$PWD/utils.cpp \ $$PWD/fileutils.cpp \ $$PWD/iconutils.cpp \ + $$PWD/webutils.cpp \ $$PWD/widgetutils.cpp \ $$PWD/clipboardutils.cpp HEADERS += \ + $$PWD/contentmediautils.h \ $$PWD/docsutils.h \ $$PWD/htmlutils.h \ $$PWD/pathutils.h \ + $$PWD/processutils.h \ $$PWD/textutils.h \ $$PWD/urldragdroputils.h \ $$PWD/utils.h \ $$PWD/fileutils.h \ $$PWD/iconutils.h \ + $$PWD/webutils.h \ $$PWD/widgetutils.h \ $$PWD/clipboardutils.h diff --git a/src/utils/webutils.cpp b/src/utils/webutils.cpp new file mode 100644 index 00000000..866b1812 --- /dev/null +++ b/src/utils/webutils.cpp @@ -0,0 +1,103 @@ +#include "webutils.h" + +#include +#include +#include + +#include "fileutils.h" +#include "pathutils.h" +#include +#include + +using namespace vnotex; + +QString WebUtils::purifyUrl(const QString &p_url) +{ + int idx = p_url.indexOf('?'); + if (idx > -1) { + return p_url.left(idx); + } + + return p_url; +} + +QString WebUtils::toDataUri(const QUrl &p_url, bool p_keepTitle) +{ + QString uri; + Q_ASSERT(!p_url.isRelative()); + QString file = p_url.isLocalFile() ? p_url.toLocalFile() : p_url.toString(); + const auto filePath = purifyUrl(file); + const QFileInfo finfo(filePath); + const QString suffix(finfo.suffix().toLower()); + if (!QImageReader::supportedImageFormats().contains(suffix.toLatin1())) { + return uri; + } + + QByteArray data; + if (p_url.scheme() == "https" || p_url.scheme() == "http") { + // Download it. + data = vte::Downloader::download(p_url); + } else if (finfo.exists()) { + data = FileUtils::readFile(filePath); + } + + if (data.isEmpty()) { + return uri; + } + + if (suffix == "svg") { + uri = QString("data:image/svg+xml;utf8,%1").arg(QString::fromUtf8(data)); + uri.replace('\r', "").replace('\n', ""); + + // Using unescaped '#' characters in a data URI body is deprecated and + // will be removed in M68, around July 2018. Please use '%23' instead. + uri.replace("#", "%23"); + + // Escape "'" to avoid conflict with src='...' attribute. + uri.replace("'", "%27"); + + if (!p_keepTitle) { + // Remove .... + QRegExp reg(".*", Qt::CaseInsensitive); + uri.remove(reg); + } + } else { + uri = QString("data:image/%1;base64,%2").arg(suffix, QString::fromUtf8(data.toBase64())); + } + + return uri; +} + +QString WebUtils::copyResource(const QUrl &p_url, const QString &p_folder) +{ + Q_ASSERT(!p_url.isRelative()); + + QDir dir(p_folder); + if (!dir.exists()) { + dir.mkpath(p_folder); + } + + QString file = p_url.isLocalFile() ? p_url.toLocalFile() : p_url.toString(); + QFileInfo finfo(file); + auto fileName = FileUtils::generateFileNameWithSequence(p_folder, finfo.completeBaseName(), finfo.suffix()); + QString targetFile = dir.absoluteFilePath(fileName); + + bool succ = true; + try { + if (p_url.scheme() == "https" || p_url.scheme() == "http") { + // Download it. + auto data = vte::Downloader::download(p_url); + if (!data.isEmpty()) { + FileUtils::writeFile(targetFile, data); + } + } else if (finfo.exists()) { + // Do a copy. + FileUtils::copyFile(file, targetFile, false); + } + } catch (Exception &p_e) { + Q_UNUSED(p_e); + succ = false; + } + + return succ ? targetFile : QString(); +} diff --git a/src/utils/webutils.h b/src/utils/webutils.h new file mode 100644 index 00000000..9e20cf6c --- /dev/null +++ b/src/utils/webutils.h @@ -0,0 +1,24 @@ +#ifndef WEBUTILS_H +#define WEBUTILS_H + +#include + +class QUrl; + +namespace vnotex +{ + class WebUtils + { + public: + WebUtils() = delete; + + // Remove query in the url (?xxx). + static QString purifyUrl(const QString &p_url); + + static QString toDataUri(const QUrl &p_url, bool p_keepTitle); + + static QString copyResource(const QUrl &p_url, const QString &p_folder); + }; +} + +#endif // WEBUTILS_H diff --git a/src/utils/widgetutils.cpp b/src/utils/widgetutils.cpp index 43cf40d8..8ec95643 100644 --- a/src/utils/widgetutils.cpp +++ b/src/utils/widgetutils.cpp @@ -21,7 +21,6 @@ #include #include #include -#include #include using namespace vnotex; @@ -356,18 +355,6 @@ void WidgetUtils::insertActionAfter(QMenu *p_menu, QAction *p_after, QAction *p_ } } -QFormLayout *WidgetUtils::createFormLayout(QWidget *p_parent) -{ - auto layout = new QFormLayout(p_parent); - -#if defined(Q_OS_MACOS) - layout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); - layout->setFormAlignment(Qt::AlignLeft | Qt::AlignTop); -#endif - - return layout; -} - void WidgetUtils::selectBaseName(QLineEdit *p_lineEdit) { auto text = p_lineEdit->text(); diff --git a/src/utils/widgetutils.h b/src/utils/widgetutils.h index bd56d254..a00b28be 100644 --- a/src/utils/widgetutils.h +++ b/src/utils/widgetutils.h @@ -17,7 +17,6 @@ class QScrollArea; class QListView; class QMenu; class QShortcut; -class QFormLayout; class QLineEdit; namespace vnotex @@ -79,8 +78,6 @@ namespace vnotex static void insertActionAfter(QMenu *p_menu, QAction *p_after, QAction *p_action); - static QFormLayout *createFormLayout(QWidget *p_parent = nullptr); - // Select the base name part of the line edit content. static void selectBaseName(QLineEdit *p_lineEdit); diff --git a/src/widgets/dialogs/dialog.cpp b/src/widgets/dialogs/dialog.cpp index e8a93f26..a7cf6f3a 100644 --- a/src/widgets/dialogs/dialog.cpp +++ b/src/widgets/dialogs/dialog.cpp @@ -12,6 +12,7 @@ #include #include "../propertydefs.h" +#include "../widgetsfactory.h" using namespace vnotex; @@ -29,6 +30,11 @@ void Dialog::setCentralWidget(QWidget *p_widget) m_layout->addWidget(m_centralWidget); } +void Dialog::addBottomWidget(QWidget *p_widget) +{ + m_layout->insertWidget(m_layout->indexOf(m_centralWidget) + 1, p_widget); +} + void Dialog::setDialogButtonBox(QDialogButtonBox::StandardButtons p_buttons, QDialogButtonBox::StandardButton p_defaultButton) { @@ -38,7 +44,8 @@ void Dialog::setDialogButtonBox(QDialogButtonBox::StandardButtons p_buttons, m_dialogButtonBox = new QDialogButtonBox(p_buttons, this); connect(m_dialogButtonBox, &QDialogButtonBox::accepted, this, &Dialog::acceptedButtonClicked); - connect(m_dialogButtonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(m_dialogButtonBox, &QDialogButtonBox::rejected, + this, &Dialog::rejectedButtonClicked); connect(m_dialogButtonBox, &QDialogButtonBox::clicked, this, [this](QAbstractButton *p_button) { switch (m_dialogButtonBox->buttonRole(p_button)) { @@ -80,13 +87,13 @@ void Dialog::setInformationText(const QString &p_text, InformationLevel p_level) return; } - m_infoTextEdit = new QPlainTextEdit(this); - m_infoTextEdit->setReadOnly(true); + m_infoTextEdit = WidgetsFactory::createPlainTextConsole(this); m_infoTextEdit->setMaximumHeight(m_infoTextEdit->minimumSizeHint().height()); - m_layout->insertWidget(1, m_infoTextEdit); + m_layout->insertWidget(m_layout->count() - 1, m_infoTextEdit); } m_infoTextEdit->setPlainText(p_text); + m_infoTextEdit->ensureCursorVisible(); const bool visible = !p_text.isEmpty(); const bool needResize = visible != m_infoTextEdit->isVisible(); @@ -114,11 +121,34 @@ void Dialog::setInformationText(const QString &p_text, InformationLevel p_level) } } +void Dialog::appendInformationText(const QString &p_text) +{ + if (!m_infoTextEdit) { + setInformationText(p_text); + } else { + m_infoTextEdit->appendPlainText(p_text); + m_infoTextEdit->moveCursor(QTextCursor::End); + m_infoTextEdit->ensureCursorVisible(); + } +} + +void Dialog::clearInformationText() +{ + if (m_infoTextEdit) { + m_infoTextEdit->clear(); + } +} + void Dialog::acceptedButtonClicked() { QDialog::accept(); } +void Dialog::rejectedButtonClicked() +{ + QDialog::reject(); +} + void Dialog::resetButtonClicked() { } diff --git a/src/widgets/dialogs/dialog.h b/src/widgets/dialogs/dialog.h index d6910c8b..423c2f5b 100644 --- a/src/widgets/dialogs/dialog.h +++ b/src/widgets/dialogs/dialog.h @@ -15,8 +15,6 @@ namespace vnotex public: explicit Dialog(QWidget *p_parent = nullptr, Qt::WindowFlags p_flags = Qt::WindowFlags()); - virtual void setCentralWidget(QWidget *p_widget); - void setDialogButtonBox(QDialogButtonBox::StandardButtons p_buttons, QDialogButtonBox::StandardButton p_defaultButton = QDialogButtonBox::NoButton); @@ -31,6 +29,10 @@ namespace vnotex void setInformationText(const QString &p_text, InformationLevel p_level = InformationLevel::Info); + void appendInformationText(const QString &p_text); + + void clearInformationText(); + void setButtonEnabled(QDialogButtonBox::StandardButton p_button, bool p_enabled); // Dialog has completed but just stay the GUI to let user know information. @@ -41,10 +43,17 @@ namespace vnotex protected: virtual void acceptedButtonClicked(); + virtual void rejectedButtonClicked(); + virtual void resetButtonClicked(); virtual void appliedButtonClicked(); + virtual void setCentralWidget(QWidget *p_widget); + + // Add @p_widget below the central widget. + virtual void addBottomWidget(QWidget *p_widget); + QBoxLayout *m_layout = nullptr; QWidget *m_centralWidget = nullptr; diff --git a/src/widgets/dialogs/exportdialog.cpp b/src/widgets/dialogs/exportdialog.cpp new file mode 100644 index 00000000..5192cc09 --- /dev/null +++ b/src/widgets/dialogs/exportdialog.cpp @@ -0,0 +1,679 @@ +#include "exportdialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace vnotex; + +ExportDialog::ExportDialog(Notebook *p_notebook, + Node *p_folder, + Buffer *p_buffer, + QWidget *p_parent) + : ScrollDialog(p_parent), + m_notebook(p_notebook), + m_folder(p_folder), + m_buffer(p_buffer) +{ + setupUI(); + + initOptions(); + + restoreFields(m_option); + + connect(this, &QDialog::finished, + this, [this]() { + saveFields(m_option); + ConfigMgr::getInst().getSessionConfig().setExportOption(m_option); + }); +} + +void ExportDialog::setupUI() +{ + auto widget = new QWidget(this); + setCentralWidget(widget); + + auto mainLayout = new QVBoxLayout(widget); + + auto sourceBox = setupSourceGroup(widget); + mainLayout->addWidget(sourceBox); + + auto targetBox = setupTargetGroup(widget); + mainLayout->addWidget(targetBox); + + m_advancedGroupBox = setupAdvancedGroup(widget); + mainLayout->addWidget(m_advancedGroupBox); + + m_progressBar = new QProgressBar(widget); + m_progressBar->setRange(0, 0); + m_progressBar->hide(); + addBottomWidget(m_progressBar); + + setupButtonBox(); + + setWindowTitle(tr("Export")); +} + +QGroupBox *ExportDialog::setupSourceGroup(QWidget *p_parent) +{ + auto box = new QGroupBox(tr("Source"), p_parent); + auto layout = WidgetsFactory::createFormLayout(box); + + { + m_sourceComboBox = WidgetsFactory::createComboBox(box); + if (m_buffer) { + m_sourceComboBox->addItem(tr("Current Buffer (%1)").arg(m_buffer->getName()), + static_cast(ExportSource::CurrentBuffer)); + } + if (m_folder && m_folder->isContainer()) { + m_sourceComboBox->addItem(tr("Current Folder (%1)").arg(m_folder->getName()), + static_cast(ExportSource::CurrentFolder)); + } + if (m_notebook) { + m_sourceComboBox->addItem(tr("Current Notebook (%1)").arg(m_notebook->getName()), + static_cast(ExportSource::CurrentNotebook)); + } + layout->addRow(tr("Source:"), m_sourceComboBox); + } + + { + // TODO: Source format filtering. + } + + return box; +} + +QString ExportDialog::getDefaultOutputDir() const +{ + return PathUtils::concatenateFilePath(ConfigMgr::getDocumentOrHomePath(), tr("vnote_exports")); +} + +QGroupBox *ExportDialog::setupTargetGroup(QWidget *p_parent) +{ + auto box = new QGroupBox(tr("Target"), p_parent); + auto layout = WidgetsFactory::createFormLayout(box); + + { + m_targetFormatComboBox = WidgetsFactory::createComboBox(box); + m_targetFormatComboBox->addItem(tr("Markdown"), + static_cast(ExportFormat::Markdown)); + m_targetFormatComboBox->addItem(tr("HTML"), + static_cast(ExportFormat::HTML)); + m_targetFormatComboBox->addItem(tr("PDF"), + static_cast(ExportFormat::PDF)); + m_targetFormatComboBox->addItem(tr("Custom"), + static_cast(ExportFormat::Custom)); + connect(m_targetFormatComboBox, QOverload::of(&QComboBox::currentIndexChanged), + this, [this]() { + AdvancedSettings settings = AdvancedSettings::Max; + int format = m_targetFormatComboBox->currentData().toInt(); + switch (format) { + case ExportFormat::HTML: + settings = AdvancedSettings::HTML; + break; + + case ExportFormat::PDF: + settings = AdvancedSettings::PDF; + break; + + default: + break; + } + showAdvancedSettings(settings); + }); + layout->addRow(tr("Format:"), m_targetFormatComboBox); + } + + { + m_transparentBgCheckBox = WidgetsFactory::createCheckBox(tr("Use transparent background"), box); + layout->addRow(m_transparentBgCheckBox); + } + + { + const auto webStyles = VNoteX::getInst().getThemeMgr().getWebStyles(); + + m_renderingStyleComboBox = WidgetsFactory::createComboBox(box); + layout->addRow(tr("Rendering style:"), m_renderingStyleComboBox); + for (const auto &pa : webStyles) { + m_renderingStyleComboBox->addItem(pa.first, pa.second); + } + + m_syntaxHighlightStyleComboBox = WidgetsFactory::createComboBox(box); + layout->addRow(tr("Syntax highlighting style:"), m_syntaxHighlightStyleComboBox); + for (const auto &pa : webStyles) { + m_syntaxHighlightStyleComboBox->addItem(pa.first, pa.second); + } + } + + { + auto outputLayout = new QHBoxLayout(); + + m_outputDirLineEdit = WidgetsFactory::createLineEdit(box); + outputLayout->addWidget(m_outputDirLineEdit); + + auto browseBtn = new QPushButton(tr("Browse"), box); + outputLayout->addWidget(browseBtn); + connect(browseBtn, &QPushButton::clicked, + this, [this]() { + QString initPath = getOutputDir(); + if (!QFileInfo::exists(initPath)) { + initPath = getDefaultOutputDir(); + } + + QString dirPath = QFileDialog::getExistingDirectory(this, + tr("Select Export Output Directory"), + initPath, + QFileDialog::ShowDirsOnly + | QFileDialog::DontResolveSymlinks); + + if (!dirPath.isEmpty()) { + m_outputDirLineEdit->setText(dirPath); + } + }); + + layout->addRow(tr("Output directory:"), outputLayout); + } + + return box; +} + +QGroupBox *ExportDialog::setupAdvancedGroup(QWidget *p_parent) +{ + auto box = new QGroupBox(tr("Advanced"), p_parent); + auto layout = new QVBoxLayout(box); + + m_advancedSettings.resize(AdvancedSettings::Max); + + m_advancedSettings[AdvancedSettings::General] = setupGeneralAdvancedSettings(box); + layout->addWidget(m_advancedSettings[AdvancedSettings::General]); + + return box; +} + +QWidget *ExportDialog::setupGeneralAdvancedSettings(QWidget *p_parent) +{ + QWidget *widget = new QWidget(p_parent); + auto layout = WidgetsFactory::createFormLayout(widget); + layout->setContentsMargins(0, 0, 0, 0); + + { + m_recursiveCheckBox = WidgetsFactory::createCheckBox(tr("Process sub-folders"), widget); + layout->addRow(m_recursiveCheckBox); + } + + { + m_exportAttachmentsCheckBox = WidgetsFactory::createCheckBox(tr("Export attachments"), widget); + layout->addRow(m_exportAttachmentsCheckBox); + } + + return widget; +} + +void ExportDialog::setupButtonBox() +{ + setDialogButtonBox(QDialogButtonBox::Close); + + auto box = getDialogButtonBox(); + + m_exportBtn = box->addButton(tr("Export"), QDialogButtonBox::ActionRole); + connect(m_exportBtn, &QPushButton::clicked, + this, &ExportDialog::startExport); + + m_openDirBtn = box->addButton(tr("Open Directory"), QDialogButtonBox::ActionRole); + connect(m_openDirBtn, &QPushButton::clicked, + this, [this]() { + auto dir = getOutputDir(); + if (!dir.isEmpty()) { + WidgetUtils::openUrlByDesktop(QUrl::fromLocalFile(dir)); + } + }); + + m_copyContentBtn = box->addButton(tr("Copy Content"), QDialogButtonBox::ActionRole); + m_copyContentBtn->setToolTip(tr("Copy exported file content")); + m_copyContentBtn->setEnabled(false); + connect(m_copyContentBtn, &QPushButton::clicked, + this, [this]() { + if (m_exportedFile.isEmpty()) { + return; + } + + const auto content = FileUtils::readTextFile(m_exportedFile); + if (!content.isEmpty()) { + ClipboardUtils::setTextToClipboard(content); + } + }); +} + +QString ExportDialog::getOutputDir() const +{ + return m_outputDirLineEdit->text(); +} + +void ExportDialog::initOptions() +{ + // Read it from config. + m_option = ConfigMgr::getInst().getSessionConfig().getExportOption(); + + const auto &theme = VNoteX::getInst().getThemeMgr().getCurrentTheme(); + m_option.m_renderingStyleFile = theme.getFile(Theme::File::WebStyleSheet); + m_option.m_syntaxHighlightStyleFile = theme.getFile(Theme::File::HighlightStyleSheet); + + if (m_option.m_outputDir.isEmpty()) { + m_option.m_outputDir = getDefaultOutputDir(); + } +} + +void ExportDialog::restoreFields(const ExportOption &p_option) +{ + { + int idx = m_sourceComboBox->findData(static_cast(p_option.m_source)); + if (idx != -1) { + m_sourceComboBox->setCurrentIndex(idx); + } + } + + { + int idx = m_targetFormatComboBox->findData(static_cast(p_option.m_targetFormat)); + if (idx != -1) { + m_targetFormatComboBox->setCurrentIndex(idx); + } + } + + m_transparentBgCheckBox->setChecked(p_option.m_useTransparentBg); + + { + int idx = m_renderingStyleComboBox->findData(p_option.m_renderingStyleFile); + if (idx != -1) { + m_renderingStyleComboBox->setCurrentIndex(idx); + } + } + + { + int idx = m_syntaxHighlightStyleComboBox->findData(p_option.m_syntaxHighlightStyleFile); + if (idx != -1) { + m_syntaxHighlightStyleComboBox->setCurrentIndex(idx); + } + } + + m_outputDirLineEdit->setText(p_option.m_outputDir); + + m_recursiveCheckBox->setChecked(p_option.m_recursive); + + m_exportAttachmentsCheckBox->setChecked(p_option.m_exportAttachments); +} + +void ExportDialog::saveFields(ExportOption &p_option) +{ + p_option.m_source = static_cast(m_sourceComboBox->currentData().toInt()); + p_option.m_targetFormat = static_cast(m_targetFormatComboBox->currentData().toInt()); + p_option.m_useTransparentBg = m_transparentBgCheckBox->isChecked(); + p_option.m_renderingStyleFile = m_renderingStyleComboBox->currentData().toString(); + p_option.m_syntaxHighlightStyleFile = m_syntaxHighlightStyleComboBox->currentData().toString(); + p_option.m_outputDir = getOutputDir(); + p_option.m_recursive = m_recursiveCheckBox->isChecked(); + p_option.m_exportAttachments = m_exportAttachmentsCheckBox->isChecked(); + + if (m_advancedSettings[AdvancedSettings::HTML]) { + saveFields(p_option.m_htmlOption); + } + + if (m_advancedSettings[AdvancedSettings::PDF]) { + saveFields(p_option.m_pdfOption); + } +} + +void ExportDialog::startExport() +{ + if (m_exportOngoing) { + return; + } + + // On start. + { + m_exportedFile.clear(); + m_exportOngoing = true; + updateUIOnExport(); + } + + saveFields(m_option); + + int ret = doExport(m_option); + appendLog(tr("%n file(s) exported", "", ret)); + + // On end. + { + m_exportOngoing = false; + updateUIOnExport(); + } +} + +void ExportDialog::rejectedButtonClicked() +{ + if (m_exportOngoing) { + // Just cancel the export. + appendLog(tr("Cancelling the export.")); + m_exporter->stop(); + } else { + Dialog::rejectedButtonClicked(); + } +} + +void ExportDialog::appendLog(const QString &p_log) +{ + appendInformationText(">>> " + p_log); + QCoreApplication::sendPostedEvents(); +} + +void ExportDialog::updateUIOnExport() +{ + m_exportBtn->setEnabled(!m_exportOngoing); + if (m_exportOngoing) { + m_progressBar->setMaximum(0); + m_progressBar->show(); + } else { + m_progressBar->hide(); + } + m_copyContentBtn->setEnabled(!m_exportedFile.isEmpty()); +} + +int ExportDialog::doExport(ExportOption p_option) +{ + // TODO: Check ExportOption. + + int exportedFilesCount = 0; + + switch (p_option.m_source) { + case ExportSource::CurrentBuffer: + { + Q_ASSERT(m_buffer); + const auto outputFile = getExporter()->doExport(p_option, m_buffer); + exportedFilesCount = outputFile.isEmpty() ? 0 : 1; + if (exportedFilesCount == 1 && p_option.m_targetFormat == ExportFormat::HTML) { + m_exportedFile = outputFile; + } + break; + } + + case ExportSource::CurrentFolder: + { + Q_ASSERT(m_folder); + const auto outputFiles = getExporter()->doExport(p_option, m_folder); + exportedFilesCount = outputFiles.size(); + break; + } + + case ExportSource::CurrentNotebook: + { + Q_ASSERT(m_notebook); + const auto outputFiles = getExporter()->doExport(p_option, m_notebook); + exportedFilesCount = outputFiles.size(); + break; + } + } + + return exportedFilesCount; +} + +Exporter *ExportDialog::getExporter() +{ + if (!m_exporter) { + m_exporter = new Exporter(this); + connect(m_exporter, &Exporter::progressUpdated, + this, &ExportDialog::updateProgress); + connect(m_exporter, &Exporter::logRequested, + this, &ExportDialog::appendLog); + } + return m_exporter; +} + +void ExportDialog::updateProgress(int p_val, int p_maximum) +{ + m_progressBar->setMaximum(p_maximum); + m_progressBar->setValue(p_val); +} + +QWidget *ExportDialog::getHtmlAdvancedSettings() +{ + if (!m_advancedSettings[AdvancedSettings::HTML]) { + // Setup HTML advanced settings. + QWidget *widget = new QWidget(m_advancedGroupBox); + auto layout = WidgetsFactory::createFormLayout(widget); + layout->setContentsMargins(0, 0, 0, 0); + + { + m_embedStylesCheckBox = WidgetsFactory::createCheckBox(tr("Embed styles"), widget); + layout->addRow(m_embedStylesCheckBox); + } + + { + m_embedImagesCheckBox = WidgetsFactory::createCheckBox(tr("Embed images"), widget); + layout->addRow(m_embedImagesCheckBox); + } + + { + m_completePageCheckBox = WidgetsFactory::createCheckBox(tr("Complete page"), widget); + m_completePageCheckBox->setToolTip(tr("Export the whole page along with images which may change the links structure")); + connect(m_completePageCheckBox, &QCheckBox::stateChanged, + this, [this](int p_state) { + bool checked = p_state == Qt::Checked; + m_embedImagesCheckBox->setEnabled(checked); + }); + layout->addRow(m_completePageCheckBox); + } + + { + m_useMimeHtmlFormatCheckBox = WidgetsFactory::createCheckBox(tr("Mime HTML format"), widget); + connect(m_useMimeHtmlFormatCheckBox, &QCheckBox::stateChanged, + this, [this](int p_state) { + bool checked = p_state == Qt::Checked; + m_embedStylesCheckBox->setEnabled(!checked); + m_completePageCheckBox->setEnabled(!checked); + }); + // TODO: do not support MHTML for now. + m_useMimeHtmlFormatCheckBox->setEnabled(false); + layout->addRow(m_useMimeHtmlFormatCheckBox); + } + + { + m_addOutlinePanelCheckBox = WidgetsFactory::createCheckBox(tr("Add outline panel"), widget); + layout->addRow(m_addOutlinePanelCheckBox); + } + + m_advancedGroupBox->layout()->addWidget(widget); + + m_advancedSettings[AdvancedSettings::HTML] = widget; + + restoreFields(m_option.m_htmlOption); + } + + return m_advancedSettings[AdvancedSettings::HTML]; +} + +void ExportDialog::showAdvancedSettings(AdvancedSettings p_settings) +{ + for (int i = AdvancedSettings::General + 1; i < m_advancedSettings.size(); ++i) { + if (m_advancedSettings[i]) { + m_advancedSettings[i]->hide(); + } + } + + QWidget *widget = nullptr; + switch (p_settings) { + case AdvancedSettings::HTML: + widget = getHtmlAdvancedSettings(); + break; + + case AdvancedSettings::PDF: + widget = getPdfAdvancedSettings(); + break; + + default: + break; + } + + if (widget) { + widget->show(); + } +} + +void ExportDialog::restoreFields(const ExportHtmlOption &p_option) +{ + m_embedStylesCheckBox->setChecked(p_option.m_embedStyles); + m_embedImagesCheckBox->setChecked(p_option.m_embedImages); + m_completePageCheckBox->setChecked(p_option.m_completePage); + m_useMimeHtmlFormatCheckBox->setChecked(p_option.m_useMimeHtmlFormat); + m_addOutlinePanelCheckBox->setChecked(p_option.m_addOutlinePanel); +} + +void ExportDialog::saveFields(ExportHtmlOption &p_option) +{ + p_option.m_embedStyles = m_embedStylesCheckBox->isChecked(); + p_option.m_embedImages = m_embedImagesCheckBox->isChecked(); + p_option.m_completePage = m_completePageCheckBox->isChecked(); + p_option.m_useMimeHtmlFormat = m_useMimeHtmlFormatCheckBox->isChecked(); + p_option.m_addOutlinePanel = m_addOutlinePanelCheckBox->isChecked(); +} + +QWidget *ExportDialog::getPdfAdvancedSettings() +{ + if (!m_advancedSettings[AdvancedSettings::PDF]) { + QWidget *widget = new QWidget(m_advancedGroupBox); + auto layout = WidgetsFactory::createFormLayout(widget); + layout->setContentsMargins(0, 0, 0, 0); + + { + m_pageLayoutBtn = new QPushButton(tr("Settings"), widget); + connect(m_pageLayoutBtn, &QPushButton::clicked, + this, [this]() { + QPrinter printer; + printer.setPageLayout(*m_pageLayout); + + QPageSetupDialog dlg(&printer, this); + if (dlg.exec() != QDialog::Accepted) { + return; + } + + m_pageLayout->setUnits(QPageLayout::Millimeter); + m_pageLayout->setPageSize(printer.pageLayout().pageSize()); + m_pageLayout->setMargins(printer.pageLayout().margins(QPageLayout::Millimeter)); + m_pageLayout->setOrientation(printer.pageLayout().orientation()); + + updatePageLayoutButtonLabel(); + }); + layout->addRow(tr("Page layout:"), m_pageLayoutBtn); + } + + { + m_addTableOfContentsCheckBox = WidgetsFactory::createCheckBox(tr("Add Table-Of-Contents"), widget); + layout->addRow(m_addTableOfContentsCheckBox); + } + + { + auto useLayout = new QHBoxLayout(); + + m_useWkhtmltopdfCheckBox = WidgetsFactory::createCheckBox(tr("Use wkhtmltopdf"), widget); + useLayout->addWidget(m_useWkhtmltopdfCheckBox); + + auto downloadBtn = new QPushButton(tr("Download"), widget); + connect(downloadBtn, &QPushButton::clicked, + this, []() { + WidgetUtils::openUrlByDesktop(QUrl("https://wkhtmltopdf.org/downloads.html")); + }); + useLayout->addWidget(downloadBtn); + + layout->addRow(useLayout); + } + + { + auto pathLayout = new QHBoxLayout(); + + m_wkhtmltopdfExePathLineEdit = WidgetsFactory::createLineEdit(widget); + pathLayout->addWidget(m_wkhtmltopdfExePathLineEdit); + + auto browseBtn = new QPushButton(tr("Browse"), widget); + pathLayout->addWidget(browseBtn); + connect(browseBtn, &QPushButton::clicked, + this, [this]() { + QString filePath = QFileDialog::getOpenFileName(this, + tr("Select wkhtmltopdf Executable"), + QCoreApplication::applicationDirPath()); + + if (!filePath.isEmpty()) { + m_wkhtmltopdfExePathLineEdit->setText(filePath); + } + }); + + layout->addRow(tr("Wkhtmltopdf path:"), pathLayout); + } + + { + m_wkhtmltopdfArgsLineEdit = WidgetsFactory::createLineEdit(widget); + layout->addRow(tr("Wkhtmltopdf arguments:"), m_wkhtmltopdfArgsLineEdit); + } + + m_advancedGroupBox->layout()->addWidget(widget); + + m_advancedSettings[AdvancedSettings::PDF] = widget; + + restoreFields(m_option.m_pdfOption); + } + + return m_advancedSettings[AdvancedSettings::PDF]; +} + +void ExportDialog::restoreFields(const ExportPdfOption &p_option) +{ + m_pageLayout = p_option.m_layout; + updatePageLayoutButtonLabel(); + + m_addTableOfContentsCheckBox->setChecked(p_option.m_addTableOfContents); + m_useWkhtmltopdfCheckBox->setChecked(p_option.m_useWkhtmltopdf); + m_wkhtmltopdfExePathLineEdit->setText(p_option.m_wkhtmltopdfExePath); + m_wkhtmltopdfArgsLineEdit->setText(p_option.m_wkhtmltopdfArgs); +} + +void ExportDialog::saveFields(ExportPdfOption &p_option) +{ + p_option.m_layout = m_pageLayout; + p_option.m_addTableOfContents = m_addTableOfContentsCheckBox->isChecked(); + p_option.m_useWkhtmltopdf = m_useWkhtmltopdfCheckBox->isChecked(); + p_option.m_wkhtmltopdfExePath = m_wkhtmltopdfExePathLineEdit->text(); + p_option.m_wkhtmltopdfArgs = m_wkhtmltopdfArgsLineEdit->text(); +} + +void ExportDialog::updatePageLayoutButtonLabel() +{ + Q_ASSERT(m_pageLayout); + m_pageLayoutBtn->setText( + QString("%1, %2").arg(m_pageLayout->pageSize().name(), + m_pageLayout->orientation() == QPageLayout::Portrait ? tr("Portrait") : tr("Landscape"))); +} diff --git a/src/widgets/dialogs/exportdialog.h b/src/widgets/dialogs/exportdialog.h new file mode 100644 index 00000000..2a743884 --- /dev/null +++ b/src/widgets/dialogs/exportdialog.h @@ -0,0 +1,171 @@ +#ifndef EXPORTDIALOG_H +#define EXPORTDIALOG_H + +#include "scrolldialog.h" + +#include + +#include + +class QGroupBox; +class QPushButton; +class QComboBox; +class QCheckBox; +class QLineEdit; +class QProgressBar; +class QPlainTextEdit; +class QPageLayout; + +namespace vnotex +{ + class Notebook; + class Node; + class Buffer; + class Exporter; + + class ExportDialog : public ScrollDialog + { + Q_OBJECT + public: + // Current notebook/folder/buffer. + ExportDialog(Notebook *p_notebook, + Node *p_folder, + Buffer *p_buffer, + QWidget *p_parent = nullptr); + + protected: + void rejectedButtonClicked() Q_DECL_OVERRIDE; + + private slots: + void updateProgress(int p_val, int p_maximum); + + void appendLog(const QString &p_log); + + private: + enum AdvancedSettings + { + General, + HTML, + PDF, + Max + }; + + void setupUI(); + + QGroupBox *setupSourceGroup(QWidget *p_parent); + + QGroupBox *setupTargetGroup(QWidget *p_parent); + + QGroupBox *setupAdvancedGroup(QWidget *p_parent); + + QWidget *setupGeneralAdvancedSettings(QWidget *p_parent); + + QWidget *getHtmlAdvancedSettings(); + + QWidget *getPdfAdvancedSettings(); + + void showAdvancedSettings(AdvancedSettings p_settings); + + void setupButtonBox(); + + QString getOutputDir() const; + + void initOptions(); + + void restoreFields(const ExportOption &p_option); + + void saveFields(ExportOption &p_option); + + void restoreFields(const ExportHtmlOption &p_option); + + void saveFields(ExportHtmlOption &p_option); + + void restoreFields(const ExportPdfOption &p_option); + + void saveFields(ExportPdfOption &p_option); + + void startExport(); + + void updateUIOnExport(); + + // Return exported files count. + int doExport(ExportOption p_option); + + Exporter *getExporter(); + + QString getDefaultOutputDir() const; + + void updatePageLayoutButtonLabel(); + + // Managed by QObject. + Exporter *m_exporter = nullptr; + + Notebook *m_notebook = nullptr; + + Node *m_folder = nullptr; + + Buffer *m_buffer = nullptr; + + // Last exported single file. + QString m_exportedFile; + + bool m_exportOngoing = false; + + QPushButton *m_exportBtn = nullptr; + + QPushButton *m_openDirBtn = nullptr; + + QPushButton *m_copyContentBtn = nullptr; + + QComboBox *m_sourceComboBox = nullptr; + + QComboBox *m_targetFormatComboBox = nullptr; + + QCheckBox *m_transparentBgCheckBox = nullptr; + + QComboBox *m_renderingStyleComboBox = nullptr; + + QComboBox *m_syntaxHighlightStyleComboBox = nullptr; + + QLineEdit *m_outputDirLineEdit = nullptr; + + QProgressBar *m_progressBar = nullptr; + + QGroupBox *m_advancedGroupBox = nullptr; + + QVector m_advancedSettings; + + // General settings. + QCheckBox *m_recursiveCheckBox = nullptr; + + QCheckBox *m_exportAttachmentsCheckBox = nullptr; + + // HTML settings. + QCheckBox *m_embedStylesCheckBox = nullptr; + + QCheckBox *m_embedImagesCheckBox = nullptr; + + QCheckBox *m_completePageCheckBox = nullptr; + + QCheckBox *m_useMimeHtmlFormatCheckBox = nullptr; + + QCheckBox *m_addOutlinePanelCheckBox = nullptr; + + // PDF settings. + QPushButton *m_pageLayoutBtn = nullptr; + + QCheckBox *m_addTableOfContentsCheckBox = nullptr; + + QCheckBox *m_useWkhtmltopdfCheckBox = nullptr; + + QLineEdit *m_wkhtmltopdfExePathLineEdit = nullptr; + + QLineEdit *m_wkhtmltopdfArgsLineEdit = nullptr; + + QSharedPointer m_pageLayout; + + ExportOption m_option; + }; +} + +#endif // EXPORTDIALOG_H diff --git a/src/widgets/dialogs/filepropertiesdialog.cpp b/src/widgets/dialogs/filepropertiesdialog.cpp index 34bf7a92..705b25cc 100644 --- a/src/widgets/dialogs/filepropertiesdialog.cpp +++ b/src/widgets/dialogs/filepropertiesdialog.cpp @@ -30,7 +30,7 @@ void FilePropertiesDialog::setupUI() auto widget = new QWidget(this); setCentralWidget(widget); - auto mainLayout = WidgetUtils::createFormLayout(widget); + auto mainLayout = WidgetsFactory::createFormLayout(widget); mainLayout->setContentsMargins(0, 0, 0, 0); const QFileInfo info(m_path); diff --git a/src/widgets/dialogs/folderfilesfilterwidget.cpp b/src/widgets/dialogs/folderfilesfilterwidget.cpp index 8832dce0..e9084788 100644 --- a/src/widgets/dialogs/folderfilesfilterwidget.cpp +++ b/src/widgets/dialogs/folderfilesfilterwidget.cpp @@ -35,7 +35,7 @@ FolderFilesFilterWidget::FolderFilesFilterWidget(QWidget *p_parent) void FolderFilesFilterWidget::setupUI() { - auto mainLayout = WidgetUtils::createFormLayout(this); + auto mainLayout = WidgetsFactory::createFormLayout(this); mainLayout->setContentsMargins(0, 0, 0, 0); { diff --git a/src/widgets/dialogs/linkinsertdialog.cpp b/src/widgets/dialogs/linkinsertdialog.cpp index 2e155b80..d03f4179 100644 --- a/src/widgets/dialogs/linkinsertdialog.cpp +++ b/src/widgets/dialogs/linkinsertdialog.cpp @@ -30,7 +30,7 @@ void LinkInsertDialog::setupUI(const QString &p_title, auto mainWidget = new QWidget(this); setCentralWidget(mainWidget); - auto mainLayout = WidgetUtils::createFormLayout(mainWidget); + auto mainLayout = WidgetsFactory::createFormLayout(mainWidget); m_linkTextEdit = WidgetsFactory::createLineEdit(p_linkText, mainWidget); mainLayout->addRow(tr("&Text:"), m_linkTextEdit); diff --git a/src/widgets/dialogs/nodeinfowidget.cpp b/src/widgets/dialogs/nodeinfowidget.cpp index 9a8404cc..5d3a518a 100644 --- a/src/widgets/dialogs/nodeinfowidget.cpp +++ b/src/widgets/dialogs/nodeinfowidget.cpp @@ -36,7 +36,7 @@ void NodeInfoWidget::setupUI(const Node *p_parentNode, Node::Flags p_newNodeFlag const bool createMode = m_mode == Mode::Create; const bool isNote = p_newNodeFlags & Node::Flag::Content; - m_mainLayout = WidgetUtils::createFormLayout(this); + m_mainLayout = WidgetsFactory::createFormLayout(this); m_mainLayout->addRow(tr("Notebook:"), new QLabel(p_parentNode->getNotebook()->getName(), this)); @@ -84,7 +84,7 @@ void NodeInfoWidget::setupNameLineEdit(QWidget *p_parent) const auto &fileType = FileTypeHelper::getInst().getFileTypeBySuffix(suffix); typeName = fileType.m_typeName; } else { - typeName = FileTypeHelper::getInst().getFileType(FileTypeHelper::Others).m_typeName; + typeName = FileTypeHelper::getInst().getFileType(FileType::Others).m_typeName; } int idx = m_fileTypeComboBox->findData(typeName); diff --git a/src/widgets/dialogs/notebookinfowidget.cpp b/src/widgets/dialogs/notebookinfowidget.cpp index 553b4c69..15088ab9 100644 --- a/src/widgets/dialogs/notebookinfowidget.cpp +++ b/src/widgets/dialogs/notebookinfowidget.cpp @@ -44,7 +44,7 @@ void NotebookInfoWidget::setupUI() QGroupBox *NotebookInfoWidget::setupBasicInfoGroupBox(QWidget *p_parent) { auto box = new QGroupBox(tr("Basic Information"), p_parent); - auto mainLayout = WidgetUtils::createFormLayout(box); + auto mainLayout = WidgetsFactory::createFormLayout(box); { setupNotebookTypeComboBox(box); @@ -131,7 +131,7 @@ QLayout *NotebookInfoWidget::setupNotebookRootFolderPath(QWidget *p_parent) QGroupBox *NotebookInfoWidget::setupAdvancedInfoGroupBox(QWidget *p_parent) { auto box = new QGroupBox(tr("Advanced Information"), p_parent); - auto mainLayout = WidgetUtils::createFormLayout(box); + auto mainLayout = WidgetsFactory::createFormLayout(box); { setupConfigMgrComboBox(box); diff --git a/src/widgets/dialogs/scrolldialog.cpp b/src/widgets/dialogs/scrolldialog.cpp index f58889ba..3f25e002 100644 --- a/src/widgets/dialogs/scrolldialog.cpp +++ b/src/widgets/dialogs/scrolldialog.cpp @@ -33,6 +33,11 @@ void ScrollDialog::setCentralWidget(QWidget *p_widget) m_scrollArea->setWidget(p_widget); } +void ScrollDialog::addBottomWidget(QWidget *p_widget) +{ + m_layout->insertWidget(m_layout->indexOf(m_scrollArea) + 1, p_widget); +} + void ScrollDialog::showEvent(QShowEvent *p_event) { QDialog::showEvent(p_event); diff --git a/src/widgets/dialogs/scrolldialog.h b/src/widgets/dialogs/scrolldialog.h index 62137a24..df6baf62 100644 --- a/src/widgets/dialogs/scrolldialog.h +++ b/src/widgets/dialogs/scrolldialog.h @@ -13,11 +13,13 @@ namespace vnotex public: ScrollDialog(QWidget *p_parent = nullptr, Qt::WindowFlags p_flags = Qt::WindowFlags()); - void setCentralWidget(QWidget *p_widget) Q_DECL_OVERRIDE; - void resizeToHideScrollBarLater(bool p_vertical, bool p_horizontal); protected: + void setCentralWidget(QWidget *p_widget) Q_DECL_OVERRIDE; + + void addBottomWidget(QWidget *p_widget) Q_DECL_OVERRIDE; + void showEvent(QShowEvent *p_event) Q_DECL_OVERRIDE; private: diff --git a/src/widgets/dialogs/settings/appearancepage.cpp b/src/widgets/dialogs/settings/appearancepage.cpp index a538d81e..a5f2a7c3 100644 --- a/src/widgets/dialogs/settings/appearancepage.cpp +++ b/src/widgets/dialogs/settings/appearancepage.cpp @@ -20,7 +20,7 @@ AppearancePage::AppearancePage(QWidget *p_parent) void AppearancePage::setupUI() { - auto mainLayout = WidgetUtils::createFormLayout(this); + auto mainLayout = WidgetsFactory::createFormLayout(this); { const QString label(tr("System title bar")); diff --git a/src/widgets/dialogs/settings/editorpage.cpp b/src/widgets/dialogs/settings/editorpage.cpp index 40c676e9..4a2a409a 100644 --- a/src/widgets/dialogs/settings/editorpage.cpp +++ b/src/widgets/dialogs/settings/editorpage.cpp @@ -20,7 +20,7 @@ EditorPage::EditorPage(QWidget *p_parent) void EditorPage::setupUI() { - auto mainLayout = WidgetUtils::createFormLayout(this); + auto mainLayout = WidgetsFactory::createFormLayout(this); { m_autoSavePolicyComboBox = WidgetsFactory::createComboBox(this); diff --git a/src/widgets/dialogs/settings/generalpage.cpp b/src/widgets/dialogs/settings/generalpage.cpp index 64519f66..295f46f0 100644 --- a/src/widgets/dialogs/settings/generalpage.cpp +++ b/src/widgets/dialogs/settings/generalpage.cpp @@ -20,7 +20,7 @@ GeneralPage::GeneralPage(QWidget *p_parent) void GeneralPage::setupUI() { - auto mainLayout = WidgetUtils::createFormLayout(this); + auto mainLayout = WidgetsFactory::createFormLayout(this); { m_localeComboBox = WidgetsFactory::createComboBox(this); diff --git a/src/widgets/dialogs/settings/markdowneditorpage.cpp b/src/widgets/dialogs/settings/markdowneditorpage.cpp index d9f12fb1..c6bbd5df 100644 --- a/src/widgets/dialogs/settings/markdowneditorpage.cpp +++ b/src/widgets/dialogs/settings/markdowneditorpage.cpp @@ -125,7 +125,7 @@ QString MarkdownEditorPage::title() const QGroupBox *MarkdownEditorPage::setupReadGroup() { auto box = new QGroupBox(tr("Read"), this); - auto layout = WidgetUtils::createFormLayout(box); + auto layout = WidgetsFactory::createFormLayout(box); { const QString label(tr("Constrain image width")); @@ -197,7 +197,7 @@ QGroupBox *MarkdownEditorPage::setupReadGroup() QGroupBox *MarkdownEditorPage::setupEditGroup() { auto box = new QGroupBox(tr("Edit"), this); - auto layout = WidgetUtils::createFormLayout(box); + auto layout = WidgetsFactory::createFormLayout(box); { const QString label(tr("Insert file name as title")); @@ -245,7 +245,7 @@ QGroupBox *MarkdownEditorPage::setupEditGroup() QGroupBox *MarkdownEditorPage::setupGeneralGroup() { auto box = new QGroupBox(tr("General"), this); - auto layout = WidgetUtils::createFormLayout(box); + auto layout = WidgetsFactory::createFormLayout(box); { auto sectionLayout = new QHBoxLayout(); diff --git a/src/widgets/dialogs/settings/texteditorpage.cpp b/src/widgets/dialogs/settings/texteditorpage.cpp index ce6bc28a..9c2e2fa1 100644 --- a/src/widgets/dialogs/settings/texteditorpage.cpp +++ b/src/widgets/dialogs/settings/texteditorpage.cpp @@ -23,7 +23,7 @@ TextEditorPage::TextEditorPage(QWidget *p_parent) void TextEditorPage::setupUI() { - auto mainLayout = WidgetUtils::createFormLayout(this); + auto mainLayout = WidgetsFactory::createFormLayout(this); { m_lineNumberComboBox = WidgetsFactory::createComboBox(this); diff --git a/src/widgets/editors/markdowneditor.cpp b/src/widgets/editors/markdowneditor.cpp index c4045d83..a48a05c1 100644 --- a/src/widgets/editors/markdowneditor.cpp +++ b/src/widgets/editors/markdowneditor.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #include #include #include @@ -1075,7 +1076,7 @@ void MarkdownEditor::fetchImagesToLocalAndReplace(QString &p_text) // Only handle absolute file path or network path. QString srcImagePath; - QFileInfo info(TextUtils::purifyUrl(imageUrl)); + QFileInfo info(WebUtils::purifyUrl(imageUrl)); // For network image. QScopedPointer tmpFile; diff --git a/src/widgets/editors/markdownvieweradapter.cpp b/src/widgets/editors/markdownvieweradapter.cpp index ebfc5f93..94503c01 100644 --- a/src/widgets/editors/markdownvieweradapter.cpp +++ b/src/widgets/editors/markdownvieweradapter.cpp @@ -94,6 +94,16 @@ void MarkdownViewerAdapter::setText(int p_revision, } } +void MarkdownViewerAdapter::setText(const QString &p_text) +{ + m_revision = 0; + if (m_viewerReady) { + emit textUpdated(p_text); + } else { + m_pendingData.reset(new MarkdownData(p_text, -1, "")); + } +} + void MarkdownViewerAdapter::setReady(bool p_ready) { if (m_viewerReady == p_ready) { @@ -328,3 +338,21 @@ void MarkdownViewerAdapter::setFindText(const QString &p_text, int p_totalMatche { emit findTextReady(p_text, p_totalMatches, p_currentMatchIndex); } + +void MarkdownViewerAdapter::setWorkFinished() +{ + emit workFinished(); +} + +void MarkdownViewerAdapter::saveContent() +{ + emit contentRequested(); +} + +void MarkdownViewerAdapter::setSavedContent(const QString &p_headContent, + const QString &p_styleContent, + const QString &p_content, + const QString &p_bodyClassList) +{ + emit contentReady(p_headContent, p_styleContent, p_content, p_bodyClassList); +} diff --git a/src/widgets/editors/markdownvieweradapter.h b/src/widgets/editors/markdownvieweradapter.h index 72596a4f..cf2501f7 100644 --- a/src/widgets/editors/markdownvieweradapter.h +++ b/src/widgets/editors/markdownvieweradapter.h @@ -100,6 +100,8 @@ namespace vnotex const QString &p_text, int p_lineNumber); + void setText(const QString &p_text); + void scrollToPosition(const Position &p_pos); int getTopLineNumber() const; @@ -119,10 +121,14 @@ namespace vnotex void findText(const QString &p_text, FindOptions p_options); + void saveContent(); + // Functions to be called from web side. public slots: void setReady(bool p_ready); + void setWorkFinished(); + // The line number at the top. void setTopLineNumber(int p_lineNumber); @@ -161,6 +167,8 @@ namespace vnotex void setFindText(const QString &p_text, int p_totalMatches, int p_currentMatchIndex); + void setSavedContent(const QString &p_headContent, const QString &p_styleContent, const QString &p_content, const QString &p_bodyClassList); + // Signals to be connected at web side. signals: // Current Markdown text is updated. @@ -194,6 +202,9 @@ namespace vnotex void findTextRequested(const QString &p_text, const QJsonObject &p_options); + // Request to get the whole HTML content. + void contentRequested(); + // Signals to be connected at cpp side. signals: void graphPreviewDataReady(const PreviewData &p_data); @@ -202,6 +213,9 @@ namespace vnotex void viewerReady(); + // All rendering work has finished. + void workFinished(); + void headingsChanged(); void currentHeadingChanged(); @@ -216,6 +230,11 @@ namespace vnotex void findTextReady(const QString &p_text, int p_totalMatches, int p_currentMatchIndex); + void contentReady(const QString &p_headContent, + const QString &p_styleContent, + const QString &p_content, + const QString &p_bodyClassList); + private: void scrollToLine(int p_lineNumber); diff --git a/src/widgets/mainwindow.cpp b/src/widgets/mainwindow.cpp index 1a0dab21..1d9b4b4e 100644 --- a/src/widgets/mainwindow.cpp +++ b/src/widgets/mainwindow.cpp @@ -29,7 +29,7 @@ #include #include #include -#include +#include #include "viewwindow.h" #include "outlineviewer.h" #include @@ -57,6 +57,9 @@ MainWindow::MainWindow(QWidget *p_parent) // Note that no user interaction is possible in this state. connect(qApp, &QCoreApplication::aboutToQuit, this, &MainWindow::closeOnQuit); + + connect(&VNoteX::getInst(), &VNoteX::exportRequested, + this, &MainWindow::exportNotes); } MainWindow::~MainWindow() @@ -582,3 +585,18 @@ void MainWindow::updateTabBarStyle() tabBar->setDrawBase(false); } } + +void MainWindow::exportNotes() +{ + auto currentNotebook = m_notebookExplorer->currentNotebook().data(); + auto viewWindow = m_viewArea->getCurrentViewWindow(); + auto folderNode = m_notebookExplorer->currentExploredFolderNode(); + if (folderNode && (folderNode->isRoot() || currentNotebook->isRecycleBinNode(folderNode))) { + folderNode = nullptr; + } + ExportDialog dialog(currentNotebook, + folderNode, + viewWindow ? viewWindow->getBuffer() : nullptr, + this); + dialog.exec(); +} diff --git a/src/widgets/mainwindow.h b/src/widgets/mainwindow.h index 8b89260c..32eda91c 100644 --- a/src/widgets/mainwindow.h +++ b/src/widgets/mainwindow.h @@ -72,6 +72,8 @@ namespace vnotex void updateTabBarStyle(); + void exportNotes(); + private: // Index in m_docks. enum DockIndex diff --git a/src/widgets/notebookexplorer.cpp b/src/widgets/notebookexplorer.cpp index f8f84270..5338c0e1 100644 --- a/src/widgets/notebookexplorer.cpp +++ b/src/widgets/notebookexplorer.cpp @@ -179,7 +179,9 @@ void NotebookExplorer::newNote() Node *NotebookExplorer::currentExploredFolderNode() const { - Q_ASSERT(m_currentNotebook); + if (!m_currentNotebook) { + return nullptr; + } auto node = m_nodeExplorer->getCurrentNode(); if (node) { @@ -294,4 +296,7 @@ void NotebookExplorer::locateNode(Node *p_node) m_nodeExplorer->setFocus(); } - +const QSharedPointer &NotebookExplorer::currentNotebook() const +{ + return m_currentNotebook; +} diff --git a/src/widgets/notebookexplorer.h b/src/widgets/notebookexplorer.h index c76760b6..231d3042 100644 --- a/src/widgets/notebookexplorer.h +++ b/src/widgets/notebookexplorer.h @@ -20,6 +20,10 @@ namespace vnotex public: explicit NotebookExplorer(QWidget *p_parent = nullptr); + const QSharedPointer ¤tNotebook() const; + + Node *currentExploredFolderNode() const; + public slots: void newNotebook(); @@ -56,8 +60,6 @@ namespace vnotex TitleBar *setupTitleBar(QWidget *p_parent = nullptr); - Node *currentExploredFolderNode() const; - Node *checkNotebookAndGetCurrentExploredFolderNode() const; NotebookSelector *m_selector = nullptr; diff --git a/src/widgets/propertydefs.cpp b/src/widgets/propertydefs.cpp index e63902db..09affe95 100644 --- a/src/widgets/propertydefs.cpp +++ b/src/widgets/propertydefs.cpp @@ -15,3 +15,5 @@ const char *PropertyDefs::s_viewSplitCornerWidget = "ViewSplitCornerWidget"; const char *PropertyDefs::s_state = "State"; const char *PropertyDefs::s_viewWindowToolBar = "ViewWindowToolBar"; + +const char *PropertyDefs::s_consoleTextEdit = "ConsoleTextEdit"; diff --git a/src/widgets/propertydefs.h b/src/widgets/propertydefs.h index d213e4c5..6ba3046b 100644 --- a/src/widgets/propertydefs.h +++ b/src/widgets/propertydefs.h @@ -21,6 +21,8 @@ namespace vnotex static const char *s_viewWindowToolBar; + static const char *s_consoleTextEdit; + // Values: info/warning/error. static const char *s_state; }; diff --git a/src/widgets/toolbarhelper.cpp b/src/widgets/toolbarhelper.cpp index bf3271fa..2ef85d10 100644 --- a/src/widgets/toolbarhelper.cpp +++ b/src/widgets/toolbarhelper.cpp @@ -40,6 +40,8 @@ QToolBar *ToolBarHelper::setupFileToolBar(MainWindow *p_win, QToolBar *p_toolBar tb = createToolBar(p_win, MainWindow::tr("File"), "FileToolBar"); } + const auto &coreConfig = ConfigMgr::getInst().getCoreConfig(); + // Notebook. { auto act = tb->addAction(generateIcon("notebook_menu.svg"), MainWindow::tr("Notebook")); @@ -88,8 +90,6 @@ QToolBar *ToolBarHelper::setupFileToolBar(MainWindow *p_win, QToolBar *p_toolBar // New Note. { - const auto &coreConfig = ConfigMgr::getInst().getCoreConfig(); - auto newBtn = WidgetsFactory::createToolButton(tb); newBtn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); @@ -167,6 +167,16 @@ QToolBar *ToolBarHelper::setupFileToolBar(MainWindow *p_win, QToolBar *p_toolBar []() { emit VNoteX::getInst().importFolderRequested(); }); + + newMenu->addSeparator(); + + auto exportAct = newMenu->addAction(MainWindow::tr("Export"), + newMenu, + []() { + emit VNoteX::getInst().exportRequested(); + }); + WidgetUtils::addActionShortcut(exportAct, + coreConfig.getShortcut(CoreConfig::Shortcut::Export)); } return tb; diff --git a/src/widgets/viewarea.cpp b/src/widgets/viewarea.cpp index ac4c33d4..0f935c54 100644 --- a/src/widgets/viewarea.cpp +++ b/src/widgets/viewarea.cpp @@ -940,6 +940,7 @@ void ViewArea::handleTargetHit(void *p_item) { if (p_item) { setCurrentViewWindow(static_cast(p_item)); + focus(); } } diff --git a/src/widgets/webviewer.cpp b/src/widgets/webviewer.cpp index e1eb22e2..e98a215a 100644 --- a/src/widgets/webviewer.cpp +++ b/src/widgets/webviewer.cpp @@ -21,7 +21,9 @@ WebViewer::WebViewer(const QColor &p_background, // Avoid white flash before loading content. // Setting Qt::transparent will force GrayScale antialias rendering. - viewPage->setBackgroundColor(p_background); + if (p_background.isValid()) { + viewPage->setBackgroundColor(p_background); + } if (!Utils::fuzzyEqual(p_zoomFactor, 1.0)) { setZoomFactor(p_zoomFactor); diff --git a/src/widgets/widgets.pri b/src/widgets/widgets.pri index d6988e6b..714c33df 100644 --- a/src/widgets/widgets.pri +++ b/src/widgets/widgets.pri @@ -4,6 +4,7 @@ SOURCES += \ $$PWD/biaction.cpp \ $$PWD/combobox.cpp \ $$PWD/dialogs/dialog.cpp \ + $$PWD/dialogs/exportdialog.cpp \ $$PWD/dialogs/filepropertiesdialog.cpp \ $$PWD/dialogs/imageinsertdialog.cpp \ $$PWD/dialogs/importfolderdialog.cpp \ @@ -88,6 +89,7 @@ HEADERS += \ $$PWD/biaction.h \ $$PWD/combobox.h \ $$PWD/dialogs/dialog.h \ + $$PWD/dialogs/exportdialog.h \ $$PWD/dialogs/importfolderutils.h \ $$PWD/dialogs/filepropertiesdialog.h \ $$PWD/dialogs/imageinsertdialog.h \ diff --git a/src/widgets/widgetsfactory.cpp b/src/widgets/widgetsfactory.cpp index 72d9d359..a3a9710d 100644 --- a/src/widgets/widgetsfactory.cpp +++ b/src/widgets/widgetsfactory.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include "lineedit.h" #include "combobox.h" @@ -66,3 +68,24 @@ QToolButton *WidgetsFactory::createToolButton(QWidget *p_parent) tb->setPopupMode(QToolButton::MenuButtonPopup); return tb; } + +QFormLayout *WidgetsFactory::createFormLayout(QWidget *p_parent) +{ + auto layout = new QFormLayout(p_parent); + +#if defined(Q_OS_MACOS) + layout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); + layout->setFormAlignment(Qt::AlignLeft | Qt::AlignTop); +#endif + + return layout; +} + +QPlainTextEdit *WidgetsFactory::createPlainTextConsole(QWidget *p_parent) +{ + auto edit = new QPlainTextEdit(p_parent); + edit->setProperty("ConsoleTextEdit", true); + edit->setReadOnly(true); + edit->setLineWrapMode(QPlainTextEdit::WidgetWidth); + return edit; +} diff --git a/src/widgets/widgetsfactory.h b/src/widgets/widgetsfactory.h index a5eb5bce..68de9c65 100644 --- a/src/widgets/widgetsfactory.h +++ b/src/widgets/widgetsfactory.h @@ -10,6 +10,8 @@ class QCheckBox; class QSpinBox; class QToolButton; class QDoubleSpinBox; +class QFormLayout; +class QPlainTextEdit; namespace vnotex { @@ -35,6 +37,10 @@ namespace vnotex static QDoubleSpinBox *createDoubleSpinBox(QWidget *p_parent = nullptr); static QToolButton *createToolButton(QWidget *p_parent = nullptr); + + static QFormLayout *createFormLayout(QWidget *p_parent = nullptr); + + static QPlainTextEdit *createPlainTextConsole(QWidget *p_parent = nullptr); }; } // ns vnotex diff --git a/tests/test_core/test_notebook/test_notebook.pro b/tests/test_core/test_notebook/test_notebook.pro index 49345f64..065582cc 100644 --- a/tests/test_core/test_notebook/test_notebook.pro +++ b/tests/test_core/test_notebook/test_notebook.pro @@ -17,6 +17,7 @@ include($$LIBS_FOLDER/vtextedit/src/libs/syntax-highlighting/syntax-highlighting include($$CORE_FOLDER/core.pri) include($$SRC_FOLDER/widgets/widgets.pri) include($$SRC_FOLDER/utils/utils.pri) +include($$SRC_FOLDER/export/export.pri) SOURCES += \ test_notebook.cpp diff --git a/tests/test_core/test_theme/test_theme.pro b/tests/test_core/test_theme/test_theme.pro index 5a573c89..58ba01b5 100644 --- a/tests/test_core/test_theme/test_theme.pro +++ b/tests/test_core/test_theme/test_theme.pro @@ -10,13 +10,19 @@ UTILS_FOLDER = $$SRC_FOLDER/utils INCLUDEPATH *= $$SRC_FOLDER INCLUDEPATH *= $$SRC_FOLDER/core -include($$UTILS_FOLDER/utils.pri) - SOURCES += \ test_theme.cpp \ $$CORE_FOLDER/theme.cpp \ + $$UTILS_FOLDER/pathutils.cpp \ + $$UTILS_FOLDER/utils.cpp \ + $$UTILS_FOLDER/widgetutils.cpp \ + $$UTILS_FOLDER/fileutils.cpp \ HEADERS += \ test_theme.h \ $$CORE_FOLDER/exception.h \ $$CORE_FOLDER/theme.h \ + $$UTILS_FOLDER/pathutils.h \ + $$UTILS_FOLDER/utils.h \ + $$UTILS_FOLDER/widgetutils.h \ + $$UTILS_FOLDER/fileutils.h \