Browse Source

refactor export

Le Tan 7 years ago
parent
commit
8ff520d0fd

+ 630 - 0
src/dialog/vexportdialog.cpp

@@ -0,0 +1,630 @@
+#include "vexportdialog.h"
+
+#include <QtWidgets>
+#include <QCoreApplication>
+
+#ifndef QT_NO_PRINTER
+#include <QPrinter>
+#include <QPageSetupDialog>
+#endif
+
+#include "utils/vutils.h"
+#include "vlineedit.h"
+#include "vnotebook.h"
+#include "vfile.h"
+#include "vdirectory.h"
+#include "vcart.h"
+#include "vconfigmanager.h"
+#include "vnotefile.h"
+#include "vnote.h"
+#include "vexporter.h"
+
+extern VConfigManager *g_config;
+
+extern VNote *g_vnote;
+
+QString VExportDialog::s_lastOutputFolder;
+
+#define LOGERR(x) do { QString msg = (x); \
+                       VUtils::addErrMsg(p_errMsg, msg); \
+                       appendLogLine(msg); \
+                  } while (0)
+
+VExportDialog::VExportDialog(VNotebook *p_notebook,
+                             VDirectory *p_directory,
+                             VFile *p_file,
+                             VCart *p_cart,
+                             MarkdownConverterType p_renderer,
+                             QWidget *p_parent)
+    : QDialog(p_parent),
+      m_notebook(p_notebook),
+      m_directory(p_directory),
+      m_file(p_file),
+      m_cart(p_cart),
+      m_pageLayout(QPageLayout(QPageSize(QPageSize::A4),
+                               QPageLayout::Portrait,
+                               QMarginsF(0.3, 0.3, 0.3, 0.3))),
+      m_inExport(false),
+      m_askedToStop(false)
+{
+    if (s_lastOutputFolder.isEmpty()) {
+        s_lastOutputFolder = g_config->getExportFolderPath();
+    }
+
+    setupUI();
+
+    m_exporter = new VExporter(this);
+
+    initUIFields(p_renderer);
+
+    handleInputChanged();
+}
+
+void VExportDialog::setupUI()
+{
+    // Notes to export.
+    m_srcCB = VUtils::getComboBox();
+    m_srcCB->setToolTip(tr("Choose notes to export"));
+    m_srcCB->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon);
+
+    // Target format.
+    m_formatCB = VUtils::getComboBox();
+    m_formatCB->setToolTip(tr("Choose target format to export as"));
+    m_formatCB->setSizeAdjustPolicy(QComboBox::AdjustToContents);
+    connect(m_formatCB, SIGNAL(currentIndexChanged(int)),
+            this, SLOT(handleCurrentFormatChanged(int)));
+
+    // Markdown renderer.
+    m_rendererCB = VUtils::getComboBox();
+    m_rendererCB->setToolTip(tr("Choose converter to render Markdown"));
+    m_rendererCB->setSizeAdjustPolicy(QComboBox::AdjustToContents);
+
+    // Output directory.
+    m_outputEdit = new VLineEdit(s_lastOutputFolder);
+    connect(m_outputEdit, &QLineEdit::textChanged,
+            this, &VExportDialog::handleInputChanged);
+    m_browseBtn = new QPushButton(tr("&Browse"));
+    connect(m_browseBtn, &QPushButton::clicked,
+            this, &VExportDialog::handleBrowseBtnClicked);
+    QHBoxLayout *outputLayout = new QHBoxLayout();
+    outputLayout->addWidget(m_outputEdit);
+    outputLayout->addWidget(m_browseBtn);
+
+    m_basicBox = new QGroupBox(tr("Information"));
+
+    m_settingBox = new QGroupBox(tr("Advanced Settings"));
+
+    m_consoleEdit = new QPlainTextEdit();
+    m_consoleEdit->setReadOnly(true);
+    m_consoleEdit->setLineWrapMode(QPlainTextEdit::NoWrap);
+    m_consoleEdit->setProperty("LineEdit", true);
+    m_consoleEdit->setPlaceholderText(tr("Output logs will be shown here"));
+
+    // Ok is the default button.
+    m_btnBox = new QDialogButtonBox(QDialogButtonBox::Close);
+    m_exportBtn = m_btnBox->addButton(tr("Export"), QDialogButtonBox::ActionRole);
+    m_openBtn = m_btnBox->addButton(tr("Open Output Directory"), QDialogButtonBox::ActionRole);
+    connect(m_btnBox, &QDialogButtonBox::rejected,
+            this, [this]() {
+                if (m_inExport) {
+                    // Just cancel the export. Do not exit.
+                    m_askedToStop = true;
+                } else {
+                    QDialog::reject();
+                }
+            });
+
+    m_exportBtn->setProperty("SpecialBtn", true);
+    connect(m_exportBtn, &QPushButton::clicked,
+            this, &VExportDialog::startExport);
+
+    connect(m_openBtn, &QPushButton::clicked,
+            this, [this]() {
+                QUrl url = QUrl::fromLocalFile(getOutputDirectory());
+                QDesktopServices::openUrl(url);
+            });
+
+    QFormLayout *basicLayout = new QFormLayout();
+    basicLayout->addRow(tr("Notes to export:"), m_srcCB);
+    basicLayout->addRow(tr("Target format:"), m_formatCB);
+    basicLayout->addRow(tr("Markdown renderer:"), m_rendererCB);
+    basicLayout->addRow(tr("Output directory:"), outputLayout);
+
+    m_basicBox->setLayout(basicLayout);
+
+    // Settings box.
+    m_pdfSettings = setupPDFAdvancedSettings();
+
+    QVBoxLayout *advLayout = new QVBoxLayout();
+    advLayout->addWidget(m_pdfSettings);
+
+    m_settingBox->setLayout(advLayout);
+
+    QVBoxLayout *mainLayout = new QVBoxLayout();
+    mainLayout->addWidget(m_basicBox);
+    mainLayout->addWidget(m_settingBox);
+    mainLayout->addWidget(m_consoleEdit);
+    mainLayout->addWidget(m_btnBox);
+
+    setLayout(mainLayout);
+
+    setWindowTitle(tr("Export"));
+}
+
+QWidget *VExportDialog::setupPDFAdvancedSettings()
+{
+    // Page layout settings.
+    m_layoutLabel = new QLabel();
+    m_layoutBtn = new QPushButton(tr("Settings"));
+
+#ifndef QT_NO_PRINTER
+    connect(m_layoutBtn, &QPushButton::clicked,
+            this, &VExportDialog::handleLayoutBtnClicked);
+#else
+    m_layoutBtn->hide();
+#endif
+
+    updatePageLayoutLabel();
+
+    QHBoxLayout *layoutLayout = new QHBoxLayout();
+    layoutLayout->addWidget(m_layoutLabel);
+    layoutLayout->addWidget(m_layoutBtn);
+    layoutLayout->addStretch();
+
+    QFormLayout *advLayout = new QFormLayout();
+    advLayout->addRow(tr("Page layout:"), layoutLayout);
+
+    advLayout->setContentsMargins(0, 0, 0, 0);
+
+    QWidget *wid = new QWidget();
+    wid->setLayout(advLayout);
+
+    return wid;
+}
+
+void VExportDialog::initUIFields(MarkdownConverterType p_renderer)
+{
+    // Notes to export.
+    if (m_file) {
+        m_srcCB->addItem(tr("Current Note (%1)").arg(m_file->getName()),
+                         (int)ExportSource::CurrentNote);
+    }
+
+    if (m_directory) {
+        m_srcCB->addItem(tr("Current Directory (%1)").arg(m_directory->getName()),
+                         (int)ExportSource::CurrentDirectory);
+    }
+
+    if (m_notebook) {
+        m_srcCB->addItem(tr("Current Notebook (%1)").arg(m_notebook->getName()),
+                         (int)ExportSource::CurrentNotebook);
+    }
+
+    if (m_cart && m_cart->count() > 0) {
+        m_srcCB->addItem(tr("Cart (%1)").arg(m_cart->count()),
+                         (int)ExportSource::Cart);
+    }
+
+    // Export format.
+    m_formatCB->addItem(tr("Markdown"), (int)ExportFormat::Markdown);
+    m_formatCB->addItem(tr("HTML"), (int)ExportFormat::HTML);
+    m_formatCB->addItem(tr("PDF"), (int)ExportFormat::PDF);
+
+    // Markdown renderer.
+    m_rendererCB->addItem(tr("Hoedown"), MarkdownConverterType::Hoedown);
+    m_rendererCB->addItem(tr("Marked"), MarkdownConverterType::Marked);
+    m_rendererCB->addItem(tr("Markdown-it"), MarkdownConverterType::MarkdownIt);
+    m_rendererCB->addItem(tr("Showdown"), MarkdownConverterType::Showdown);
+    m_rendererCB->setCurrentIndex(m_rendererCB->findData(p_renderer));
+}
+
+void VExportDialog::startExport()
+{
+    if (m_inExport) {
+        return;
+    }
+
+    m_exportBtn->setEnabled(false);
+    m_askedToStop = false;
+    m_inExport = true;
+
+    QString outputFolder = QDir::cleanPath(QDir(getOutputDirectory()).absolutePath());
+
+    ExportOption opt((ExportSource)m_srcCB->currentData().toInt(),
+                     (ExportFormat)m_formatCB->currentData().toInt(),
+                     (MarkdownConverterType)m_rendererCB->currentData().toInt(),
+                     &m_pageLayout);
+
+    m_consoleEdit->clear();
+    appendLogLine(tr("Export to %1.").arg(outputFolder));
+
+    if (opt.m_format == ExportFormat::PDF) {
+        m_exporter->prepareExport(opt);
+    }
+
+    int ret = 0;
+    QString msg;
+
+    switch ((int)opt.m_source) {
+    case (int)ExportSource::CurrentNote:
+        ret = doExport(m_file, opt, outputFolder, &msg);
+        break;
+
+    case (int)ExportSource::CurrentDirectory:
+        ret = doExport(m_directory, opt, outputFolder, &msg);
+        break;
+
+    case (int)ExportSource::CurrentNotebook:
+        ret = doExport(m_notebook, opt, outputFolder, &msg);
+        break;
+
+    case (int)ExportSource::Cart:
+        ret = doExport(m_cart, opt, outputFolder, &msg);
+        break;
+
+    default:
+        break;
+    }
+
+    if (m_askedToStop) {
+        appendLogLine(tr("User cancelled the export. Aborted!"));
+        m_askedToStop = false;
+    }
+
+    if (!msg.isEmpty()) {
+        VUtils::showMessage(QMessageBox::Warning,
+                            tr("Warning"),
+                            tr("Errors found during export."),
+                            msg,
+                            QMessageBox::Ok,
+                            QMessageBox::Ok,
+                            this);
+    }
+
+    appendLogLine(tr("%1 notes exported.").arg(ret));
+
+    m_inExport = false;
+    m_exportBtn->setEnabled(true);
+}
+
+QString VExportDialog::getOutputDirectory() const
+{
+    return m_outputEdit->text();
+}
+
+void VExportDialog::handleBrowseBtnClicked()
+{
+    QString initPath = getOutputDirectory();
+    if (!QFileInfo::exists(initPath)) {
+        initPath = g_config->getDocumentPathOrHomePath();
+    }
+
+    QString dirPath = QFileDialog::getExistingDirectory(this,
+                                                        tr("Select Output Directory To Export To"),
+                                                        initPath,
+                                                        QFileDialog::ShowDirsOnly
+                                                        | QFileDialog::DontResolveSymlinks);
+
+    if (!dirPath.isEmpty()) {
+        m_outputEdit->setText(dirPath);
+        s_lastOutputFolder = dirPath;
+    }
+}
+
+void VExportDialog::handleInputChanged()
+{
+    // Source.
+    bool sourceOk = true;
+    if (m_srcCB->count() == 0) {
+        sourceOk = false;
+    }
+
+    // Output folder.
+    bool pathOk = true;
+    QString path = getOutputDirectory();
+    if (path.isEmpty() || !VUtils::checkPathLegal(path)) {
+        pathOk = false;
+    }
+
+    m_exportBtn->setEnabled(sourceOk && pathOk);
+    m_openBtn->setEnabled(pathOk);
+}
+
+void VExportDialog::appendLogLine(const QString &p_text)
+{
+    m_consoleEdit->appendPlainText(p_text);
+    QCoreApplication::sendPostedEvents();
+}
+
+int VExportDialog::doExport(VFile *p_file,
+                            const ExportOption &p_opt,
+                            const QString &p_outputFolder,
+                            QString *p_errMsg)
+{
+    Q_ASSERT(p_file);
+
+    appendLogLine(tr("Exporting note %1.").arg(p_file->fetchPath()));
+
+    int ret = 0;
+    switch ((int)p_opt.m_format) {
+    case (int)ExportFormat::Markdown:
+        ret = doExportMarkdown(p_file, p_opt, p_outputFolder, p_errMsg);
+        break;
+
+    case (int)ExportFormat::PDF:
+        ret = doExportPDF(p_file, p_opt, p_outputFolder, p_errMsg);
+        break;
+
+    default:
+        break;
+    }
+
+    return ret;
+}
+
+int VExportDialog::doExport(VDirectory *p_directory,
+                            const ExportOption &p_opt,
+                            const QString &p_outputFolder,
+                            QString *p_errMsg)
+{
+    Q_ASSERT(p_directory);
+
+    bool opened = p_directory->isOpened();
+    if (!opened && !p_directory->open()) {
+        LOGERR(tr("Fail to open folder %1.").arg(p_directory->fetchRelativePath()));
+        return 0;
+    }
+
+    int ret = 0;
+
+    QString folderName = VUtils::getDirNameWithSequence(p_outputFolder,
+                                                        p_directory->getName());
+    QString outputPath = QDir(p_outputFolder).filePath(folderName);
+    if (!VUtils::makePath(outputPath)) {
+        LOGERR(tr("Fail to create directory %1.").arg(outputPath));
+        goto exit;
+    }
+
+    // Export child notes.
+    for (auto const & file : p_directory->getFiles()) {
+        if (!checkUserAction()) {
+            goto exit;
+        }
+
+        ret += doExport(file, p_opt, outputPath, p_errMsg);
+    }
+
+    // Export subfolder.
+    for (auto const & dir : p_directory->getSubDirs()) {
+        if (!checkUserAction()) {
+            goto exit;
+        }
+
+        ret += doExport(dir, p_opt, outputPath, p_errMsg);
+    }
+
+exit:
+    if (!opened) {
+        p_directory->close();
+    }
+
+    return ret;
+}
+
+int VExportDialog::doExport(VNotebook *p_notebook,
+                            const ExportOption &p_opt,
+                            const QString &p_outputFolder,
+                            QString *p_errMsg)
+{
+    Q_ASSERT(p_notebook);
+
+    bool opened = p_notebook->isOpened();
+    if (!opened && !p_notebook->open()) {
+        LOGERR(tr("Fail to open notebook %1.").arg(p_notebook->getName()));
+        return 0;
+    }
+
+    int ret = 0;
+
+    QString folderName = VUtils::getDirNameWithSequence(p_outputFolder,
+                                                        p_notebook->getName());
+    QString outputPath = QDir(p_outputFolder).filePath(folderName);
+    if (!VUtils::makePath(outputPath)) {
+        LOGERR(tr("Fail to create directory %1.").arg(outputPath));
+        goto exit;
+    }
+
+    // Export subfolder.
+    for (auto const & dir : p_notebook->getRootDir()->getSubDirs()) {
+        if (!checkUserAction()) {
+            goto exit;
+        }
+
+        ret += doExport(dir, p_opt, outputPath, p_errMsg);
+    }
+
+exit:
+    if (!opened) {
+        p_notebook->close();
+    }
+
+    return ret;
+}
+
+int VExportDialog::doExport(VCart *p_cart,
+                            const ExportOption &p_opt,
+                            const QString &p_outputFolder,
+                            QString *p_errMsg)
+{
+    Q_ASSERT(p_cart);
+
+    int ret = 0;
+
+    QVector<QString> files = m_cart->getFiles();
+    for (auto const & it : files) {
+        VFile *file = g_vnote->getFile(it);
+        if (!file) {
+            LOGERR(tr("Fail to open file %1.").arg(it));
+            continue;
+        }
+
+        ret += doExport(file, p_opt, p_outputFolder, p_errMsg);
+    }
+
+    return ret;
+}
+
+int VExportDialog::doExportMarkdown(VFile *p_file,
+                                    const ExportOption &p_opt,
+                                    const QString &p_outputFolder,
+                                    QString *p_errMsg)
+{
+    Q_UNUSED(p_opt);
+
+    QString srcFilePath(p_file->fetchPath());
+
+    if (p_file->getDocType() != DocType::Markdown) {
+        LOGERR(tr("Skip exporting non-Markdown file %1 as Markdown.").arg(srcFilePath));
+        return 0;
+    }
+
+    // Export it to a folder with the same name.
+    QString name = VUtils::getDirNameWithSequence(p_outputFolder, p_file->getName());
+    QString outputPath = QDir(p_outputFolder).filePath(name);
+    if (!VUtils::makePath(outputPath)) {
+        LOGERR(tr("Fail to create directory %1.").arg(outputPath));
+        return 0;
+    }
+
+    // Copy the note file.
+    QString destPath = QDir(outputPath).filePath(p_file->getName());
+    if (!VUtils::copyFile(srcFilePath, destPath, false)) {
+        LOGERR(tr("Fail to copy the note file %1.").arg(srcFilePath));
+        return 0;
+    }
+
+    // Copy images.
+    int ret = 1;
+    int nrImageCopied = 0;
+    QVector<ImageLink> images = VUtils::fetchImagesFromMarkdownFile(p_file,
+                                                                    ImageLink::LocalRelativeInternal);
+    if (!VNoteFile::copyInternalImages(images,
+                                       outputPath,
+                                       false,
+                                       &nrImageCopied,
+                                       p_errMsg)) {
+        ret = 0;
+        appendLogLine(tr("Fail to copy images of note %1.").arg(srcFilePath));
+    }
+
+    // Copy attachments.
+    if (p_file->getType() == FileType::Note) {
+        VNoteFile *noteFile = static_cast<VNoteFile *>(p_file);
+        QString attaFolder = noteFile->getAttachmentFolder();
+        if (!attaFolder.isEmpty()) {
+            QString attaFolderPath;
+            attaFolderPath = noteFile->fetchAttachmentFolderPath();
+            attaFolder = VUtils::getDirNameWithSequence(outputPath, attaFolder);
+            QString folderPath = QDir(outputPath).filePath(attaFolder);
+
+            // Copy attaFolder to folderPath.
+            if (!VUtils::copyDirectory(attaFolderPath, folderPath, false)) {
+                LOGERR(tr("Fail to copy attachments folder %1 to %2.")
+                         .arg(attaFolderPath).arg(folderPath));
+                ret = 0;
+            }
+        }
+    }
+
+    if (ret) {
+        appendLogLine(tr("Note %1 exported to %2.").arg(srcFilePath).arg(outputPath));
+    }
+
+    return ret;
+}
+
+int VExportDialog::doExportPDF(VFile *p_file,
+                               const ExportOption &p_opt,
+                               const QString &p_outputFolder,
+                               QString *p_errMsg)
+{
+    Q_UNUSED(p_opt);
+
+    QString srcFilePath(p_file->fetchPath());
+
+    if (p_file->getDocType() != DocType::Markdown) {
+        LOGERR(tr("Skip exporting non-Markdown file %1 as PDF.").arg(srcFilePath));
+        return 0;
+    }
+
+    // Get output file.
+    QString suffix = ".pdf";
+    QString name = VUtils::getFileNameWithSequence(p_outputFolder,
+                                                   QFileInfo(p_file->getName()).completeBaseName() + suffix);
+    QString outputPath = QDir(p_outputFolder).filePath(name);
+
+    if (m_exporter->exportPDF(p_file, p_opt, outputPath, p_errMsg)) {
+        appendLogLine(tr("Note %1 exported to %2.").arg(srcFilePath).arg(outputPath));
+        return 1;
+    } else {
+        appendLogLine(tr("Fail to export note %1.").arg(srcFilePath));
+        return 0;
+    }
+}
+
+bool VExportDialog::checkUserAction()
+{
+    if (m_askedToStop) {
+        return false;
+    }
+
+    QCoreApplication::processEvents();
+
+    return true;
+}
+
+void VExportDialog::handleLayoutBtnClicked()
+{
+#ifndef QT_NO_PRINTER
+    QPrinter printer;
+    printer.setPageLayout(m_pageLayout);
+
+    QPageSetupDialog dlg(&printer, this);
+    if (dlg.exec() != QDialog::Accepted) {
+        return;
+    }
+
+    m_pageLayout.setPageSize(printer.pageLayout().pageSize());
+    m_pageLayout.setMargins(printer.pageLayout().margins());
+    m_pageLayout.setOrientation(printer.pageLayout().orientation());
+
+    updatePageLayoutLabel();
+#endif
+}
+
+void VExportDialog::updatePageLayoutLabel()
+{
+    qDebug() << "page layout margins:" << m_pageLayout.margins();
+
+    m_layoutLabel->setText(QString("%1, %2").arg(m_pageLayout.pageSize().name())
+                                            .arg(m_pageLayout.orientation() == QPageLayout::Portrait ?
+                                                 tr("Portrait") : tr("Landscape")));
+}
+
+void VExportDialog::handleCurrentFormatChanged(int p_index)
+{
+    bool pdfEnabled = false;
+
+    if (p_index >= 0) {
+        switch (m_formatCB->currentData().toInt()) {
+        case (int)ExportFormat::PDF:
+            pdfEnabled = true;
+            break;
+
+        default:
+            break;
+        }
+    }
+
+    m_pdfSettings->setVisible(pdfEnabled);
+}

+ 180 - 0
src/dialog/vexportdialog.h

@@ -0,0 +1,180 @@
+#ifndef VEXPORTDIALOG_H
+#define VEXPORTDIALOG_H
+
+#include <QDialog>
+#include <QPageLayout>
+
+#include "vconstants.h"
+
+class QLabel;
+class VLineEdit;
+class QDialogButtonBox;
+class QComboBox;
+class QPushButton;
+class QGroupBox;
+class QPlainTextEdit;
+class VNotebook;
+class VDirectory;
+class VFile;
+class VCart;
+class VExporter;
+
+
+enum class ExportSource
+{
+    CurrentNote = 0,
+    CurrentDirectory,
+    CurrentNotebook,
+    Cart
+};
+
+
+enum class ExportFormat
+{
+    Markdown = 0,
+    HTML,
+    PDF
+};
+
+
+struct ExportOption
+{
+    ExportOption(ExportSource p_source,
+                 ExportFormat p_format,
+                 MarkdownConverterType p_renderer,
+                 QPageLayout *p_layout)
+        : m_source(p_source),
+          m_format(p_format),
+          m_renderer(p_renderer),
+          m_layout(p_layout)
+    {
+    }
+
+    ExportSource m_source;
+    ExportFormat m_format;
+    MarkdownConverterType m_renderer;
+    QPageLayout *m_layout;
+};
+
+
+class VExportDialog : public QDialog
+{
+    Q_OBJECT
+public:
+    VExportDialog(VNotebook *p_notebook,
+                  VDirectory *p_directory,
+                  VFile *p_file,
+                  VCart *p_cart,
+                  MarkdownConverterType p_renderer,
+                  QWidget *p_parent = nullptr);
+
+private slots:
+    void startExport();
+
+    void handleBrowseBtnClicked();
+
+    void handleInputChanged();
+
+    void handleLayoutBtnClicked();
+
+    void handleCurrentFormatChanged(int p_index);
+
+private:
+    void setupUI();
+
+    QWidget *setupPDFAdvancedSettings();
+
+    void initUIFields(MarkdownConverterType p_renderer);
+
+    QString getOutputDirectory() const;
+
+    void appendLogLine(const QString &p_text);
+
+    // Return number of files exported.
+    int doExport(VFile *p_file,
+                 const ExportOption &p_opt,
+                 const QString &p_outputFolder,
+                 QString *p_errMsg = NULL);
+
+    int doExport(VDirectory *p_directory,
+                 const ExportOption &p_opt,
+                 const QString &p_outputFolder,
+                 QString *p_errMsg = NULL);
+
+    int doExport(VNotebook *p_notebook,
+                 const ExportOption &p_opt,
+                 const QString &p_outputFolder,
+                 QString *p_errMsg = NULL);
+
+    int doExport(VCart *p_cart,
+                 const ExportOption &p_opt,
+                 const QString &p_outputFolder,
+                 QString *p_errMsg = NULL);
+
+    int doExportMarkdown(VFile *p_file,
+                         const ExportOption &p_opt,
+                         const QString &p_outputFolder,
+                         QString *p_errMsg = NULL);
+
+    int doExportPDF(VFile *p_file,
+                    const ExportOption &p_opt,
+                    const QString &p_outputFolder,
+                    QString *p_errMsg = NULL);
+
+    // Return false if we could not continue.
+    bool checkUserAction();
+
+    void updatePageLayoutLabel();
+
+    QComboBox *m_srcCB;
+
+    QComboBox *m_formatCB;
+
+    QComboBox *m_rendererCB;
+
+    VLineEdit *m_outputEdit;
+
+    QPushButton *m_browseBtn;
+
+    QGroupBox *m_basicBox;
+
+    QGroupBox *m_settingBox;
+
+    QWidget *m_pdfSettings;
+
+    QPlainTextEdit *m_consoleEdit;
+
+    QDialogButtonBox *m_btnBox;
+
+    QPushButton *m_openBtn;
+
+    QPushButton *m_exportBtn;
+
+    QLabel *m_layoutLabel;
+
+    QPushButton *m_layoutBtn;
+
+    VNotebook *m_notebook;
+
+    VDirectory *m_directory;
+
+    VFile *m_file;
+
+    VCart *m_cart;
+
+    QPageLayout m_pageLayout;
+
+    // Whether we are exporting files.
+    bool m_inExport;
+
+    // Asked to stop exporting by user.
+    bool m_askedToStop;
+
+    // Exporter used to export PDF and HTML.
+    VExporter *m_exporter;
+
+    // Last output folder path.
+    static QString s_lastOutputFolder;
+};
+
+#endif // VEXPORTDIALOG_H

+ 1 - 1
src/dialog/vnewnotebookdialog.cpp

@@ -143,7 +143,7 @@ void VNewNotebookDialog::handleBrowseBtnClicked()
     if (defaultPath.isEmpty()) {
         defaultPath = g_config->getVnoteNotebookFolderPath();
         if (!QFileInfo::exists(defaultPath)) {
-            defaultPath = QDir::homePath();
+            defaultPath = g_config->getDocumentPathOrHomePath();
         }
     }
 

+ 0 - 1
src/dialog/vnotebookinfodialog.h

@@ -8,7 +8,6 @@ class QLabel;
 class VLineEdit;
 class VMetaWordLineEdit;
 class QDialogButtonBox;
-class QString;
 class VNotebook;
 
 class VNotebookInfoDialog : public QDialog

+ 6 - 4
src/src.pro

@@ -60,7 +60,6 @@ SOURCES += main.cpp\
     vorphanfile.cpp \
     vcodeblockhighlighthelper.cpp \
     vwebview.cpp \
-    vexporter.cpp \
     vmdtab.cpp \
     vhtmltab.cpp \
     utils/vvim.cpp \
@@ -112,7 +111,9 @@ SOURCES += main.cpp\
     vlistwidget.cpp \
     vsimplesearchinput.cpp \
     vstyleditemdelegate.cpp \
-    vtreewidget.cpp
+    vtreewidget.cpp \
+    dialog/vexportdialog.cpp \
+    vexporter.cpp
 
 HEADERS  += vmainwindow.h \
     vdirectorytree.h \
@@ -158,7 +159,6 @@ HEADERS  += vmainwindow.h \
     vorphanfile.h \
     vcodeblockhighlighthelper.h \
     vwebview.h \
-    vexporter.h \
     vmdtab.h \
     vhtmltab.h \
     utils/vvim.h \
@@ -211,7 +211,9 @@ HEADERS  += vmainwindow.h \
     vlistwidget.h \
     vsimplesearchinput.h \
     vstyleditemdelegate.h \
-    vtreewidget.h
+    vtreewidget.h \
+    dialog/vexportdialog.h \
+    vexporter.h
 
 RESOURCES += \
     vnote.qrc \

+ 16 - 0
src/vcart.cpp

@@ -191,3 +191,19 @@ QString VCart::getFilePath(const QListWidgetItem *p_item) const
 {
     return p_item->data(Qt::UserRole).toString();
 }
+
+int VCart::count() const
+{
+    return m_itemList->count();
+}
+
+QVector<QString> VCart::getFiles() const
+{
+    QVector<QString> files;
+    int cnt = m_itemList->count();
+    for (int i = 0; i < cnt; ++i) {
+        files.append(getFilePath(m_itemList->item(i)));
+    }
+
+    return files;
+}

+ 4 - 0
src/vcart.h

@@ -22,6 +22,10 @@ public:
 
     void addFile(const QString &p_filePath);
 
+    int count() const;
+
+    QVector<QString> getFiles() const;
+
 private slots:
     void handleContextMenuRequested(QPoint p_pos);
 

+ 22 - 0
src/vconfigmanager.cpp

@@ -47,6 +47,8 @@ const QString VConfigManager::c_dataTextStyle = QString("font: bold");
 
 const QString VConfigManager::c_vnoteNotebookFolderName = QString("vnote_notebooks");
 
+const QString VConfigManager::c_exportFolderName = QString("vnote_exports");
+
 VConfigManager::VConfigManager(QObject *p_parent)
     : QObject(p_parent),
       m_hasReset(false),
@@ -930,6 +932,26 @@ QString VConfigManager::getVnoteNotebookFolderPath()
     }
 }
 
+QString VConfigManager::getExportFolderPath()
+{
+    QStringList folders = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation);
+    if (folders.isEmpty()) {
+        return QDir::home().filePath(c_exportFolderName);
+    } else {
+        return QDir(folders[0]).filePath(c_exportFolderName);
+    }
+}
+
+QString VConfigManager::getDocumentPathOrHomePath()
+{
+    QStringList folders = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation);
+    if (folders.isEmpty()) {
+        return QDir::homePath();
+    } else {
+        return folders[0];
+    }
+}
+
 QHash<QString, QString> VConfigManager::readShortcutsFromSettings(QSettings *p_settings,
                                                                   const QString &p_group)
 {

+ 10 - 11
src/vconfigmanager.h

@@ -14,20 +14,9 @@
 #include "vfilesessioninfo.h"
 #include "utils/vmetawordmanager.h"
 
-
 class QJsonObject;
 class QString;
 
-
-enum MarkdownConverterType
-{
-    Hoedown = 0,
-    Marked,
-    MarkdownIt,
-    Showdown
-};
-
-
 struct VColor
 {
     QString m_name;
@@ -57,6 +46,7 @@ struct MarkdownitOption
     bool m_linkify;
 };
 
+
 // Type of heading sequence.
 enum class HeadingSequenceType
 {
@@ -69,6 +59,7 @@ enum class HeadingSequenceType
     Invalid
 };
 
+
 class VConfigManager : public QObject
 {
 public:
@@ -89,6 +80,11 @@ public:
     // Get the path of the folder used to store default notebook.
     static QString getVnoteNotebookFolderPath();
 
+    // Get the path of the default export folder.
+    static QString getExportFolderPath();
+
+    static QString getDocumentPathOrHomePath();
+
     // Constants
     static const QString orgName;
     static const QString appName;
@@ -892,6 +888,9 @@ private:
 
     // The folder name to store all notebooks if user does not specify one.
     static const QString c_vnoteNotebookFolderName;
+
+    // The default export output folder name.
+    static const QString c_exportFolderName;
 };
 
 

+ 8 - 0
src/vconstants.h

@@ -141,4 +141,12 @@ enum class UpdateAction
     Moved
 };
 
+enum MarkdownConverterType
+{
+    Hoedown = 0,
+    Marked,
+    MarkdownIt,
+    Showdown
+};
+
 #endif

+ 10 - 0
src/vdirectorytree.cpp

@@ -1207,3 +1207,13 @@ void VDirectoryTree::sortItems(VDirectory *p_dir)
         updateItemDirectChildren(findVDirectory(p_dir));
     }
 }
+
+VDirectory *VDirectoryTree::currentDirectory() const
+{
+    QTreeWidgetItem *item = currentItem();
+    if (item) {
+        return getVDirectory(item);
+    }
+
+    return NULL;
+}

+ 2 - 0
src/vdirectorytree.h

@@ -30,6 +30,8 @@ public:
 
     const VNotebook *currentNotebook() const;
 
+    VDirectory *currentDirectory() const;
+
     // Implementations for VNavigationMode.
     void showNavigation() Q_DECL_OVERRIDE;
     bool handleKeyNavigation(int p_key, bool &p_succeed) Q_DECL_OVERRIDE;

+ 71 - 301
src/vexporter.cpp

@@ -1,206 +1,111 @@
 #include "vexporter.h"
 
-#include <QtWidgets>
-#include <QFileInfo>
-#include <QDir>
-#include <QWebChannel>
 #include <QDebug>
-#include <QVBoxLayout>
-#include <QShowEvent>
-
-#ifndef QT_NO_PRINTER
-#include <QPrinter>
-#include <QPageSetupDialog>
-#endif
+#include <QWidget>
+#include <QWebChannel>
 
 #include "vconfigmanager.h"
-#include "utils/vutils.h"
 #include "vfile.h"
 #include "vwebview.h"
+#include "utils/vutils.h"
 #include "vpreviewpage.h"
 #include "vconstants.h"
-#include "vnote.h"
 #include "vmarkdownconverter.h"
 #include "vdocument.h"
-#include "vlineedit.h"
 
 extern VConfigManager *g_config;
 
-QString VExporter::s_defaultPathDir = QDir::homePath();
-
-VExporter::VExporter(MarkdownConverterType p_mdType, QWidget *p_parent)
-    : QDialog(p_parent), m_webViewer(NULL), m_mdType(p_mdType),
-      m_file(NULL), m_type(ExportType::PDF), m_source(ExportSource::Invalid),
-      m_noteState(NoteState::NotReady), m_state(ExportState::Idle),
-      m_pageLayout(QPageLayout(QPageSize(QPageSize::A4), QPageLayout::Portrait, QMarginsF(0.0, 0.0, 0.0, 0.0))),
-      m_exported(false)
+VExporter::VExporter(QWidget *p_parent)
+    : QObject(p_parent),
+      m_webViewer(NULL),
+      m_state(ExportState::Idle)
 {
-    initMarkdownTemplate();
-
-    setupUI();
 }
 
-void VExporter::initMarkdownTemplate()
+void VExporter::prepareExport(const ExportOption &p_opt)
 {
-    m_htmlTemplate = VUtils::generateHtmlTemplate(m_mdType, true);
+    m_htmlTemplate = VUtils::generateHtmlTemplate(p_opt.m_renderer, true);
+    m_pageLayout = *(p_opt.m_layout);
 }
 
-void VExporter::setupUI()
+bool VExporter::exportPDF(VFile *p_file,
+                          const ExportOption &p_opt,
+                          const QString &p_outputFile,
+                          QString *p_errMsg)
 {
-    m_infoLabel = new QLabel();
-    m_infoLabel->setWordWrap(true);
-
-    // Target file path.
-    QLabel *pathLabel = new QLabel(tr("Target &path:"));
-    m_pathEdit = new VLineEdit();
-    pathLabel->setBuddy(m_pathEdit);
-    m_browseBtn = new QPushButton(tr("&Browse"));
-    connect(m_browseBtn, &QPushButton::clicked,
-            this, &VExporter::handleBrowseBtnClicked);
-
-    // Page layout.
-    QLabel *layoutLabel = new QLabel(tr("Page layout:"));
-    m_layoutLabel = new QLabel();
-    m_layoutBtn = new QPushButton(tr("&Settings"));
-
-#ifndef QT_NO_PRINTER
-    connect(m_layoutBtn, &QPushButton::clicked,
-            this, &VExporter::handleLayoutBtnClicked);
-#else
-    m_layoutBtn->hide();
-#endif
-
-    // Progress.
-    m_proLabel = new QLabel(this);
-    m_proBar = new QProgressBar(this);
-
-    // Ok is the default button.
-    m_btnBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
-    m_openBtn = m_btnBox->addButton(tr("Open File Location"), QDialogButtonBox::ActionRole);
-    connect(m_btnBox, &QDialogButtonBox::accepted, this, &VExporter::startExport);
-    connect(m_btnBox, &QDialogButtonBox::rejected, this, &VExporter::cancelExport);
-    connect(m_openBtn, &QPushButton::clicked, this, &VExporter::openTargetPath);
-
-    QPushButton *okBtn = m_btnBox->button(QDialogButtonBox::Ok);
-    okBtn->setProperty("SpecialBtn", true);
-    m_pathEdit->setMinimumWidth(okBtn->sizeHint().width() * 3);
-
-    QGridLayout *mainLayout = new QGridLayout();
-    mainLayout->addWidget(m_infoLabel, 0, 0, 1, 3);
-    mainLayout->addWidget(pathLabel, 1, 0);
-    mainLayout->addWidget(m_pathEdit, 1, 1);
-    mainLayout->addWidget(m_browseBtn, 1, 2);
-    mainLayout->addWidget(layoutLabel, 2, 0);
-    mainLayout->addWidget(m_layoutLabel, 2, 1);
-    mainLayout->addWidget(m_layoutBtn, 2, 2);
-    mainLayout->addWidget(m_proLabel, 3, 1, 1, 2);
-    mainLayout->addWidget(m_proBar, 4, 1, 1, 2);
-    mainLayout->addWidget(m_btnBox, 5, 1, 1, 2);
-
-    m_proLabel->hide();
-    m_proBar->hide();
-
-    setLayout(mainLayout);
-    mainLayout->setSizeConstraint(QLayout::SetFixedSize);
-    setWindowTitle(tr("Export Note"));
-
-    m_openBtn->hide();
-
-    updatePageLayoutLabel();
-}
+    Q_UNUSED(p_errMsg);
 
-static QString exportTypeStr(ExportType p_type)
-{
-    if (p_type == ExportType::PDF) {
-        return "PDF";
-    } else {
-        return "HTML";
-    }
-}
+    bool ret = false;
 
-void VExporter::handleBrowseBtnClicked()
-{
-    QFileInfo fi(getFilePath());
-    QString fileType = m_type == ExportType::PDF ?
-                       tr("Portable Document Format (*.pdf)") :
-                       tr("WebPage, Complete (*.html)");
-    QString path = QFileDialog::getSaveFileName(this, tr("Export As"),
-                                                fi.absoluteFilePath(),
-                                                fileType);
-    if (path.isEmpty()) {
-        return;
+    bool isOpened = p_file->isOpened();
+    if (!isOpened && !p_file->open()) {
+        goto exit;
     }
 
-    setFilePath(path);
-    s_defaultPathDir = VUtils::basePathFromPath(path);
+    Q_ASSERT(m_state == ExportState::Idle);
+    m_state = ExportState::Busy;
+
+    clearNoteState();
 
-    m_openBtn->hide();
-}
+    initWebViewer(p_file, p_opt);
 
-void VExporter::handleLayoutBtnClicked()
-{
-#ifndef QT_NO_PRINTER
-    QPrinter printer;
-    printer.setPageLayout(m_pageLayout);
+    while (!isNoteStateReady()) {
+        VUtils::sleepWait(100);
+
+        if (m_state == ExportState::Cancelled) {
+            goto exit;
+        }
 
-    QPageSetupDialog dlg(&printer, this);
-    if (dlg.exec() != QDialog::Accepted) {
-        return;
+        if (isNoteStateFailed()) {
+            m_state = ExportState::Failed;
+            goto exit;
+        }
     }
 
-    m_pageLayout.setPageSize(printer.pageLayout().pageSize());
-    m_pageLayout.setOrientation(printer.pageLayout().orientation());
+    // Wait to ensure Web side is really ready.
+    VUtils::sleepWait(200);
 
-    updatePageLayoutLabel();
-#endif
-}
+    if (m_state == ExportState::Cancelled) {
+        goto exit;
+    }
 
-void VExporter::updatePageLayoutLabel()
-{
-    m_layoutLabel->setText(QString("%1, %2").arg(m_pageLayout.pageSize().name())
-                                            .arg(m_pageLayout.orientation() == QPageLayout::Portrait ?
-                                                 tr("Portrait") : tr("Landscape")));
-}
+    {
+    bool exportRet = exportToPDF(m_webViewer,
+                                 p_outputFile,
+                                 m_pageLayout);
 
-QString VExporter::getFilePath() const
-{
-    return QDir::cleanPath(m_pathEdit->text());
-}
+    clearNoteState();
 
-void VExporter::setFilePath(const QString &p_path)
-{
-    m_pathEdit->setText(QDir::toNativeSeparators(p_path));
-}
+    if (!isOpened) {
+        p_file->close();
+    }
 
-void VExporter::exportNote(VFile *p_file, ExportType p_type)
-{
-    m_file = p_file;
-    m_type = p_type;
-    m_source = ExportSource::Note;
-
-    if (!m_file || m_file->getDocType() != DocType::Markdown) {
-        // Do not support non-Markdown note now.
-        m_btnBox->button(QDialogButtonBox::Ok)->setEnabled(false);
-        return;
+    if (exportRet) {
+        m_state = ExportState::Successful;
+    } else {
+        m_state = ExportState::Failed;
+    }
     }
 
-    m_infoLabel->setText(tr("Export note <span style=\"%1\">%2</span> as %3.")
-                            .arg(g_config->c_dataTextStyle)
-                            .arg(m_file->getName())
-                            .arg(exportTypeStr(p_type)));
+exit:
+    clearWebViewer();
 
-    setWindowTitle(tr("Export As %1").arg(exportTypeStr(p_type)));
+    if (m_state == ExportState::Successful) {
+        ret = true;
+    }
 
-    setFilePath(QDir(s_defaultPathDir).filePath(QFileInfo(p_file->fetchPath()).baseName() +
-                                                "." + exportTypeStr(p_type).toLower()));
+    m_state = ExportState::Idle;
+
+    return ret;
 }
 
-void VExporter::initWebViewer(VFile *p_file)
+void VExporter::initWebViewer(VFile *p_file, const ExportOption &p_opt)
 {
-    V_ASSERT(!m_webViewer);
+    Q_ASSERT(!m_webViewer);
 
-    m_webViewer = new VWebView(p_file, this);
+    m_webViewer = new VWebView(p_file, static_cast<QWidget *>(parent()));
     m_webViewer->hide();
+
     VPreviewPage *page = new VPreviewPage(m_webViewer);
     m_webViewer->setPage(page);
 
@@ -216,7 +121,7 @@ void VExporter::initWebViewer(VFile *p_file)
     page->setWebChannel(channel);
 
     // Need to generate HTML using Hoedown.
-    if (m_mdType == MarkdownConverterType::Hoedown) {
+    if (p_opt.m_renderer == MarkdownConverterType::Hoedown) {
         VMarkdownConverter mdConverter;
         QString toc;
         QString html = mdConverter.generateHtml(p_file->getContent(),
@@ -228,14 +133,6 @@ void VExporter::initWebViewer(VFile *p_file)
     m_webViewer->setHtml(m_htmlTemplate, p_file->getBaseUrl());
 }
 
-void VExporter::clearWebViewer()
-{
-    if (m_webViewer) {
-        delete m_webViewer;
-        m_webViewer = NULL;
-    }
-}
-
 void VExporter::handleLogicsFinished()
 {
     Q_ASSERT(!(m_noteState & NoteState::WebLogicsReady));
@@ -252,130 +149,16 @@ void VExporter::handleLoadFinished(bool p_ok)
     }
 }
 
-void VExporter::clearNoteState()
-{
-    m_noteState = NoteState::NotReady;
-}
-
-bool VExporter::isNoteStateReady() const
-{
-    return m_noteState == NoteState::Ready;
-}
-
-bool VExporter::isNoteStateFailed() const
-{
-    return m_noteState & NoteState::Failed;
-}
-
-void VExporter::startExport()
-{
-    QPushButton *cancelBtn = m_btnBox->button(QDialogButtonBox::Cancel);
-
-    if (m_exported) {
-        cancelBtn->show();
-        m_exported = false;
-        accept();
-    }
-
-    int exportedNum = 0;
-    enableUserInput(false);
-    V_ASSERT(m_state == ExportState::Idle);
-    m_state = ExportState::Busy;
-
-    m_openBtn->hide();
-
-    if (m_source == ExportSource::Note) {
-        V_ASSERT(m_file);
-        bool isOpened = m_file->isOpened();
-        if (!isOpened && !m_file->open()) {
-            goto exit;
-        }
-
-        clearNoteState();
-        initWebViewer(m_file);
-
-        // Update progress info.
-        m_proLabel->setText(tr("Exporting %1").arg(m_file->getName()));
-        m_proBar->setEnabled(true);
-        m_proBar->setMinimum(0);
-        m_proBar->setMaximum(100);
-        m_proBar->reset();
-        m_proLabel->show();
-        m_proBar->show();
-
-        while (!isNoteStateReady()) {
-            VUtils::sleepWait(100);
-            if (m_proBar->value() < 70) {
-                m_proBar->setValue(m_proBar->value() + 1);
-            }
-
-            if (m_state == ExportState::Cancelled) {
-                goto exit;
-            }
-
-            if (isNoteStateFailed()) {
-                m_state = ExportState::Failed;
-                goto exit;
-            }
-        }
-
-        // Wait to ensure Web side is really ready.
-        VUtils::sleepWait(200);
-
-        if (m_state == ExportState::Cancelled) {
-            goto exit;
-        }
-
-        m_proBar->setValue(80);
-
-        bool exportRet = exportToPDF(m_webViewer, getFilePath(), m_pageLayout);
-
-        clearNoteState();
-
-        if (!isOpened) {
-            m_file->close();
-        }
-
-        if (exportRet) {
-            m_proBar->setValue(100);
-            m_state = ExportState::Successful;
-            exportedNum++;
-        } else {
-            m_proBar->setEnabled(false);
-            m_state = ExportState::Failed;
-        }
-    }
-
-exit:
-    clearWebViewer();
-
-    m_proLabel->setText("");
-    m_proLabel->hide();
-    enableUserInput(true);
-
-    if (m_state == ExportState::Cancelled) {
-        reject();
-    }
-
-    if (exportedNum) {
-        m_exported = true;
-        m_openBtn->show();
-        cancelBtn->hide();
-    }
-
-    m_state = ExportState::Idle;
-}
-
-void VExporter::cancelExport()
+void VExporter::clearWebViewer()
 {
-    if (m_state == ExportState::Idle) {
-        reject();
-    } else {
-        m_state = ExportState::Cancelled;
+    if (m_webViewer) {
+        delete m_webViewer;
+        m_webViewer = NULL;
     }
 }
 
-bool VExporter::exportToPDF(VWebView *p_webViewer, const QString &p_filePath,
+bool VExporter::exportToPDF(VWebView *p_webViewer,
+                            const QString &p_filePath,
                             const QPageLayout &p_layout)
 {
     int pdfPrinted = 0;
@@ -411,16 +194,3 @@ bool VExporter::exportToPDF(VWebView *p_webViewer, const QString &p_filePath,
     return pdfPrinted == 1;
 }
 
-void VExporter::enableUserInput(bool p_enabled)
-{
-    m_btnBox->button(QDialogButtonBox::Ok)->setEnabled(p_enabled);
-    m_pathEdit->setEnabled(p_enabled);
-    m_browseBtn->setEnabled(p_enabled);
-    m_layoutBtn->setEnabled(p_enabled);
-}
-
-void VExporter::openTargetPath() const
-{
-    QUrl url = QUrl::fromLocalFile(VUtils::basePathFromPath(getFilePath()));
-    QDesktopServices::openUrl(url);
-}

+ 37 - 68
src/vexporter.h

@@ -1,51 +1,32 @@
 #ifndef VEXPORTER_H
 #define VEXPORTER_H
 
-#include <QDialog>
+#include <QObject>
 #include <QPageLayout>
-#include <QString>
-#include "vconfigmanager.h"
 
+#include "dialog/vexportdialog.h"
+
+class QWidget;
 class VWebView;
-class VFile;
-class VLineEdit;
-class QLabel;
-class QDialogButtonBox;
-class QPushButton;
-class QProgressBar;
-
-enum class ExportType
-{
-    PDF = 0,
-    HTML
-};
 
-class VExporter : public QDialog
+class VExporter : public QObject
 {
-    Q_OBJECT
 public:
-    explicit VExporter(MarkdownConverterType p_mdType = MarkdownIt, QWidget *p_parent = 0);
+    explicit VExporter(QWidget *p_parent = nullptr);
+
+    void prepareExport(const ExportOption &p_opt);
 
-    void exportNote(VFile *p_file, ExportType p_type);
+    bool exportPDF(VFile *p_file,
+                   const ExportOption &p_opt,
+                   const QString &p_outputFile,
+                   QString *p_errMsg = NULL);
 
 private slots:
-    void handleBrowseBtnClicked();
-    void handleLayoutBtnClicked();
-    void startExport();
-    void cancelExport();
     void handleLogicsFinished();
+
     void handleLoadFinished(bool p_ok);
-    void openTargetPath() const;
 
 private:
-    enum class ExportSource
-    {
-        Note = 0,
-        Directory,
-        Notebook,
-        Invalid
-    };
-
     enum class ExportState
     {
         Idle = 0,
@@ -55,6 +36,7 @@ private:
         Successful
     };
 
+
     enum NoteState
     {
         NotReady = 0,
@@ -64,59 +46,46 @@ private:
         Failed = 0x4
     };
 
-    void setupUI();
-
-    void initMarkdownTemplate();
-
-    void updatePageLayoutLabel();
-
-    void setFilePath(const QString &p_path);
 
-    QString getFilePath() const;
-
-    void initWebViewer(VFile *p_file);
+    void initWebViewer(VFile *p_file, const ExportOption &p_opt);
 
     void clearWebViewer();
 
-    void enableUserInput(bool p_enabled);
-
-    bool exportToPDF(VWebView *p_webViewer, const QString &p_filePath, const QPageLayout &p_layout);
-
     void clearNoteState();
+
     bool isNoteStateReady() const;
+
     bool isNoteStateFailed() const;
 
+    bool exportToPDF(VWebView *p_webViewer,
+                     const QString &p_filePath,
+                     const QPageLayout &p_layout);
+
+    QPageLayout m_pageLayout;
+
     // Will be allocated and free for each conversion.
     VWebView *m_webViewer;
 
-    MarkdownConverterType m_mdType;
     QString m_htmlTemplate;
-    VFile *m_file;
-    ExportType m_type;
-    ExportSource m_source;
+
     NoteState m_noteState;
 
     ExportState m_state;
+};
 
-    QLabel *m_infoLabel;
-    VLineEdit *m_pathEdit;
-    QPushButton *m_browseBtn;
-    QLabel *m_layoutLabel;
-    QPushButton *m_layoutBtn;
-    QDialogButtonBox *m_btnBox;
-    QPushButton *m_openBtn;
-
-    // Progress label and bar.
-    QLabel *m_proLabel;
-    QProgressBar *m_proBar;
-
-    QPageLayout m_pageLayout;
+inline void VExporter::clearNoteState()
+{
+    m_noteState = NoteState::NotReady;
+}
 
-    // Whether a PDF has been exported.
-    bool m_exported;
+inline bool VExporter::isNoteStateReady() const
+{
+    return m_noteState == NoteState::Ready;
+}
 
-    // The default directory.
-    static QString s_defaultPathDir;
-};
+inline bool VExporter::isNoteStateFailed() const
+{
+    return m_noteState & NoteState::Failed;
+}
 
 #endif // VEXPORTER_H

+ 23 - 23
src/vmainwindow.cpp

@@ -37,6 +37,7 @@
 #include "utils/viconutils.h"
 #include "dialog/vtipsdialog.h"
 #include "vcart.h"
+#include "dialog/vexportdialog.h"
 
 extern VConfigManager *g_config;
 
@@ -994,13 +995,12 @@ void VMainWindow::initFileMenu()
     fileMenu->addSeparator();
 
     // Export as PDF.
-    m_exportAsPDFAct = new QAction(tr("Export As &PDF"), this);
-    m_exportAsPDFAct->setToolTip(tr("Export current note as PDF file"));
-    connect(m_exportAsPDFAct, &QAction::triggered,
-            this, &VMainWindow::exportAsPDF);
-    m_exportAsPDFAct->setEnabled(false);
+    m_exportAct = new QAction(tr("E&xport"), this);
+    m_exportAct->setToolTip(tr("Export notes"));
+    connect(m_exportAct, &QAction::triggered,
+            this, &VMainWindow::handleExportAct);
 
-    fileMenu->addAction(m_exportAsPDFAct);
+    fileMenu->addAction(m_exportAct);
 
     // Print.
     m_printAct = new QAction(VIconUtils::menuIcon(":/resources/icons/print.svg"),
@@ -1349,8 +1349,6 @@ void VMainWindow::changeMarkdownConverter(QAction *action)
 
     MarkdownConverterType type = (MarkdownConverterType)action->data().toInt();
 
-    qDebug() << "switch to converter" << type;
-
     g_config->setMarkdownConverterType(type);
 }
 
@@ -1418,7 +1416,7 @@ void VMainWindow::setEditorBackgroundColor(QAction *action)
 
 void VMainWindow::initConverterMenu(QMenu *p_menu)
 {
-    QMenu *converterMenu = p_menu->addMenu(tr("&Converter"));
+    QMenu *converterMenu = p_menu->addMenu(tr("&Renderer"));
     converterMenu->setToolTipsVisible(true);
 
     QActionGroup *converterAct = new QActionGroup(this);
@@ -1828,7 +1826,6 @@ void VMainWindow::updateActionsStateFromTab(const VEditTab *p_tab)
                       && dynamic_cast<const VOrphanFile *>(file)->isSystemFile();
 
     m_printAct->setEnabled(file && file->getDocType() == DocType::Markdown);
-    m_exportAsPDFAct->setEnabled(file && file->getDocType() == DocType::Markdown);
 
     updateEditReadAct(p_tab);
 
@@ -2380,19 +2377,6 @@ void VMainWindow::printNote()
     }
 }
 
-void VMainWindow::exportAsPDF()
-{
-    V_ASSERT(m_curTab);
-    V_ASSERT(m_curFile);
-
-    if (m_curFile->getDocType() == DocType::Markdown) {
-        VMdTab *mdTab = dynamic_cast<VMdTab *>((VEditTab *)m_curTab);
-        VExporter exporter(mdTab->getMarkdownConverterType(), this);
-        exporter.exportNote(m_curFile, ExportType::PDF);
-        exporter.exec();
-    }
-}
-
 QAction *VMainWindow::newAction(const QIcon &p_icon,
                                 const QString &p_text,
                                 QObject *p_parent)
@@ -3015,3 +2999,19 @@ void VMainWindow::updateEditReadAct(const VEditTab *p_tab)
 
     m_editReadAct->setEnabled(p_tab);
 }
+
+void VMainWindow::handleExportAct()
+{
+    VExportDialog dialog(notebookSelector->currentNotebook(),
+                         directoryTree->currentDirectory(),
+                         m_curFile,
+                         m_cart,
+                         g_config->getMdConverterType(),
+                         this);
+    dialog.exec();
+}
+
+VNotebook *VMainWindow::getCurrentNotebook() const
+{
+    return notebookSelector->currentNotebook();
+}

+ 6 - 2
src/vmainwindow.h

@@ -97,6 +97,8 @@ public:
 
     VEditTab *getCurrentTab() const;
 
+    VNotebook *getCurrentNotebook() const;
+
 signals:
     // Emit when editor related configurations were changed by user.
     void editorConfigUpdated();
@@ -139,7 +141,9 @@ private slots:
     void enableImageConstraint(bool p_checked);
     void enableImageCaption(bool p_checked);
     void printNote();
-    void exportAsPDF();
+
+    // Open export dialog.
+    void handleExportAct();
 
     // Set the panel view properly.
     void enableCompactMode(bool p_enabled);
@@ -349,7 +353,7 @@ private:
 
     QAction *m_printAct;
 
-    QAction *m_exportAsPDFAct;
+    QAction *m_exportAct;
 
     QAction *m_findReplaceAct;
 

+ 15 - 10
src/vnote.cpp

@@ -10,7 +10,6 @@
 #include "vnote.h"
 #include "utils/vutils.h"
 #include "vconfigmanager.h"
-#include "vmainwindow.h"
 #include "vorphanfile.h"
 #include "vnotefile.h"
 #include "vpalette.h"
@@ -238,15 +237,7 @@ VOrphanFile *VNote::getOrphanFile(const QString &p_path, bool p_modifiable, bool
         }
     }
 
-    for (int i = 0; i < m_externalFiles.size(); ++i) {
-        VOrphanFile *file = m_externalFiles[i];
-        if (!file->isOpened()) {
-            qDebug() << "release orphan file" << file;
-            m_externalFiles.removeAt(i);
-            delete file;
-            --i;
-        }
-    }
+    freeOrphanFiles();
 
     // Create a VOrphanFile for path.
     VOrphanFile *file = new VOrphanFile(this, path, p_modifiable, p_systemFile);
@@ -296,3 +287,17 @@ VDirectory *VNote::getInternalDirectory(const QString &p_path)
     return dir;
 
 }
+
+void VNote::freeOrphanFiles()
+{
+    for (int i = 0; i < m_externalFiles.size();) {
+        VOrphanFile *file = m_externalFiles[i];
+        if (!file->isOpened()) {
+            qDebug() << "release orphan file" << file;
+            m_externalFiles.removeAt(i);
+            delete file;
+        } else {
+            ++i;
+        }
+    }
+}

+ 2 - 0
src/vnote.h

@@ -99,6 +99,8 @@ public:
     // Otherwise, returns NULL.
     VDirectory *getInternalDirectory(const QString &p_path);
 
+    void freeOrphanFiles();
+
 public slots:
     void updateTemplate();
 

+ 5 - 0
src/vnotebookselector.cpp

@@ -621,3 +621,8 @@ VNotebook *VNotebookSelector::getNotebook(const QListWidgetItem *p_item) const
 
     return NULL;
 }
+
+VNotebook *VNotebookSelector::currentNotebook() const
+{
+    return getNotebook(currentIndex());
+}

+ 2 - 0
src/vnotebookselector.h

@@ -27,6 +27,8 @@ public:
     // Add notebook on popup if no notebooks currently.
     void showPopup() Q_DECL_OVERRIDE;
 
+    VNotebook *currentNotebook() const;
+
     // Implementations for VNavigationMode.
     void registerNavigation(QChar p_majorKey) Q_DECL_OVERRIDE;
     void showNavigation() Q_DECL_OVERRIDE;

+ 56 - 37
src/vnotefile.cpp

@@ -8,7 +8,6 @@
 #include <QJsonArray>
 #include <QDebug>
 
-#include "utils/vutils.h"
 #include "vdirectory.h"
 
 VNoteFile::VNoteFile(VDirectory *p_directory,
@@ -510,42 +509,12 @@ bool VNoteFile::copyFile(VDirectory *p_destDir,
     }
 
     // Copy images.
-    QDir parentDir(destFile->fetchBasePath());
-    QSet<QString> processedImages;
-    for (int i = 0; i < images.size(); ++i) {
-        const ImageLink &link = images[i];
-        if (processedImages.contains(link.m_path)) {
-            continue;
-        }
-
-        processedImages.insert(link.m_path);
-
-        if (!QFileInfo::exists(link.m_path)) {
-            VUtils::addErrMsg(p_errMsg, tr("Source image %1 does not exist.")
-                                          .arg(link.m_path));
-            ret = false;
-            continue;
-        }
-
-        QString imageFolder = VUtils::directoryNameFromPath(VUtils::basePathFromPath(link.m_path));
-        QString destImagePath = QDir(parentDir.filePath(imageFolder)).filePath(VUtils::fileNameFromPath(link.m_path));
-
-        if (VUtils::equalPath(link.m_path, destImagePath)) {
-            VUtils::addErrMsg(p_errMsg, tr("Skip image with the same source and target path %1.")
-                                          .arg(link.m_path));
-            ret = false;
-            continue;
-        }
-
-        if (!VUtils::copyFile(link.m_path, destImagePath, p_isCut)) {
-            VUtils::addErrMsg(p_errMsg, tr("Fail to %1 image %2 to %3. "
-                                           "Please manually %1 it and modify the note.")
-                                          .arg(opStr).arg(link.m_path).arg(destImagePath));
-            ret = false;
-        } else {
-            ++nrImageCopied;
-            qDebug() << opStr << "image" << link.m_path << "to" << destImagePath;
-        }
+    if (!copyInternalImages(images,
+                            destFile->fetchBasePath(),
+                            p_isCut,
+                            &nrImageCopied,
+                            p_errMsg)) {
+        ret = false;
     }
 
     // Copy attachment folder.
@@ -587,3 +556,53 @@ bool VNoteFile::copyFile(VDirectory *p_destDir,
     return ret;
 }
 
+bool VNoteFile::copyInternalImages(const QVector<ImageLink> &p_images,
+                                   const QString &p_destDirPath,
+                                   bool p_isCut,
+                                   int *p_nrImageCopied,
+                                   QString *p_errMsg)
+{
+    bool ret = true;
+    QDir parentDir(p_destDirPath);
+    QSet<QString> processedImages;
+    QString opStr = p_isCut ? tr("cut") : tr("copy");
+    int nrImageCopied = 0;
+    for (int i = 0; i < p_images.size(); ++i) {
+        const ImageLink &link = p_images[i];
+        if (processedImages.contains(link.m_path)) {
+            continue;
+        }
+
+        processedImages.insert(link.m_path);
+
+        if (!QFileInfo::exists(link.m_path)) {
+            VUtils::addErrMsg(p_errMsg, tr("Source image %1 does not exist.")
+                                          .arg(link.m_path));
+            ret = false;
+            continue;
+        }
+
+        QString imageFolder = VUtils::directoryNameFromPath(VUtils::basePathFromPath(link.m_path));
+        QString destImagePath = QDir(parentDir.filePath(imageFolder)).filePath(VUtils::fileNameFromPath(link.m_path));
+
+        if (VUtils::equalPath(link.m_path, destImagePath)) {
+            VUtils::addErrMsg(p_errMsg, tr("Skip image with the same source and target path %1.")
+                                          .arg(link.m_path));
+            ret = false;
+            continue;
+        }
+
+        if (!VUtils::copyFile(link.m_path, destImagePath, p_isCut)) {
+            VUtils::addErrMsg(p_errMsg, tr("Fail to %1 image %2 to %3. "
+                                           "Please manually %1 it and modify the note.")
+                                          .arg(opStr).arg(link.m_path).arg(destImagePath));
+            ret = false;
+        } else {
+            ++nrImageCopied;
+            qDebug() << opStr << "image" << link.m_path << "to" << destImagePath;
+        }
+    }
+
+    *p_nrImageCopied = nrImageCopied;
+    return ret;
+}

+ 8 - 0
src/vnotefile.h

@@ -5,6 +5,7 @@
 #include <QString>
 
 #include "vfile.h"
+#include "utils/vutils.h"
 
 class VDirectory;
 class VNotebook;
@@ -129,6 +130,13 @@ public:
                          VNoteFile **p_targetFile,
                          QString *p_errMsg = NULL);
 
+    // Copy images @p_images of a file to @p_destDirPath.
+    static bool copyInternalImages(const QVector<ImageLink> &p_images,
+                                   const QString &p_destDirPath,
+                                   bool p_isCut,
+                                   int *p_nrImageCopied,
+                                   QString *p_errMsg = NULL);
+
 private:
     // Delete internal images of this file.
     // Return true only when all internal images were deleted successfully.