Преглед на файлове

hard days for VNoteX project

Never say "refactor" again!!!
Le Tan преди 4 години
родител
ревизия
52702a32e9
променени са 100 файла, в които са добавени 10139 реда и са изтрити 0 реда
  1. 6 0
      .gitignore
  2. 3 0
      .gitmodules
  3. 33 0
      coc_update.cmd
  4. 5 0
      libs/libs.pro
  5. 1 0
      libs/vtextedit
  6. 22 0
      libs/vtitlebar/src/vtitlebar.cpp
  7. 19 0
      libs/vtitlebar/src/vtitlebar.h
  8. 118 0
      libs/vtitlebar/src/vtoolbar.cpp
  9. 54 0
      libs/vtitlebar/src/vtoolbar.h
  10. 16 0
      libs/vtitlebar/vtitlebar.pro
  11. 17 0
      libs/vtitlebar/vtitlebar_export.pri
  12. 552 0
      src/core/buffer/buffer.cpp
  13. 241 0
      src/core/buffer/buffer.h
  14. 22 0
      src/core/buffer/buffer.pri
  15. 24 0
      src/core/buffer/bufferprovider.cpp
  16. 82 0
      src/core/buffer/bufferprovider.h
  17. 165 0
      src/core/buffer/filebufferprovider.cpp
  18. 73 0
      src/core/buffer/filebufferprovider.h
  19. 54 0
      src/core/buffer/filetypehelper.cpp
  20. 33 0
      src/core/buffer/filetypehelper.h
  21. 24 0
      src/core/buffer/ibufferfactory.h
  22. 94 0
      src/core/buffer/markdownbuffer.cpp
  23. 47 0
      src/core/buffer/markdownbuffer.h
  24. 11 0
      src/core/buffer/markdownbufferfactory.cpp
  25. 17 0
      src/core/buffer/markdownbufferfactory.h
  26. 133 0
      src/core/buffer/nodebufferprovider.cpp
  27. 72 0
      src/core/buffer/nodebufferprovider.h
  28. 17 0
      src/core/buffer/textbuffer.cpp
  29. 20 0
      src/core/buffer/textbuffer.h
  30. 11 0
      src/core/buffer/textbufferfactory.cpp
  31. 17 0
      src/core/buffer/textbufferfactory.h
  32. 178 0
      src/core/buffermgr.cpp
  33. 55 0
      src/core/buffermgr.h
  34. 187 0
      src/core/clipboarddata.cpp
  35. 85 0
      src/core/clipboarddata.h
  36. 376 0
      src/core/configmgr.cpp
  37. 141 0
      src/core/configmgr.h
  38. 57 0
      src/core/core.pri
  39. 124 0
      src/core/coreconfig.cpp
  40. 73 0
      src/core/coreconfig.h
  41. 184 0
      src/core/editorconfig.cpp
  42. 118 0
      src/core/editorconfig.h
  43. 26 0
      src/core/events.h
  44. 89 0
      src/core/exception.h
  45. 48 0
      src/core/filelocator.h
  46. 35 0
      src/core/fileopenparameters.h
  47. 78 0
      src/core/global.h
  48. 161 0
      src/core/htmltemplatehelper.cpp
  49. 52 0
      src/core/htmltemplatehelper.h
  50. 155 0
      src/core/iconfig.h
  51. 116 0
      src/core/logger.cpp
  52. 27 0
      src/core/logger.h
  53. 113 0
      src/core/mainconfig.cpp
  54. 63 0
      src/core/mainconfig.h
  55. 197 0
      src/core/markdowneditorconfig.cpp
  56. 103 0
      src/core/markdowneditorconfig.h
  57. 50 0
      src/core/namebasedserver.h
  58. 60 0
      src/core/notebook/bundlenotebook.cpp
  59. 35 0
      src/core/notebook/bundlenotebook.h
  60. 104 0
      src/core/notebook/bundlenotebookfactory.cpp
  61. 38 0
      src/core/notebook/bundlenotebookfactory.h
  62. 130 0
      src/core/notebook/filenode.cpp
  63. 60 0
      src/core/notebook/filenode.h
  64. 65 0
      src/core/notebook/foldernode.cpp
  65. 40 0
      src/core/notebook/foldernode.h
  66. 46 0
      src/core/notebook/inotebookfactory.h
  67. 335 0
      src/core/notebook/node.cpp
  68. 196 0
      src/core/notebook/node.h
  69. 380 0
      src/core/notebook/notebook.cpp
  70. 184 0
      src/core/notebook/notebook.h
  71. 18 0
      src/core/notebook/notebook.pri
  72. 64 0
      src/core/notebook/notebookparameters.cpp
  73. 70 0
      src/core/notebook/notebookparameters.h
  74. 23 0
      src/core/notebookbackend/inotebookbackend.cpp
  75. 111 0
      src/core/notebookbackend/inotebookbackend.h
  76. 31 0
      src/core/notebookbackend/inotebookbackendfactory.h
  77. 170 0
      src/core/notebookbackend/localnotebookbackend.cpp
  78. 84 0
      src/core/notebookbackend/localnotebookbackend.h
  79. 34 0
      src/core/notebookbackend/localnotebookbackendfactory.cpp
  80. 24 0
      src/core/notebookbackend/localnotebookbackendfactory.h
  81. 10 0
      src/core/notebookbackend/notebookbackend.pri
  82. 96 0
      src/core/notebookconfigmgr/bundlenotebookconfigmgr.cpp
  83. 54 0
      src/core/notebookconfigmgr/bundlenotebookconfigmgr.h
  84. 37 0
      src/core/notebookconfigmgr/inotebookconfigmgr.cpp
  85. 96 0
      src/core/notebookconfigmgr/inotebookconfigmgr.h
  86. 33 0
      src/core/notebookconfigmgr/inotebookconfigmgrfactory.h
  87. 177 0
      src/core/notebookconfigmgr/nodecontentmediautils.cpp
  88. 52 0
      src/core/notebookconfigmgr/nodecontentmediautils.h
  89. 111 0
      src/core/notebookconfigmgr/notebookconfig.cpp
  90. 68 0
      src/core/notebookconfigmgr/notebookconfig.h
  91. 16 0
      src/core/notebookconfigmgr/notebookconfigmgr.pri
  92. 886 0
      src/core/notebookconfigmgr/vxnotebookconfigmgr.cpp
  93. 205 0
      src/core/notebookconfigmgr/vxnotebookconfigmgr.h
  94. 35 0
      src/core/notebookconfigmgr/vxnotebookconfigmgrfactory.cpp
  95. 25 0
      src/core/notebookconfigmgr/vxnotebookconfigmgrfactory.h
  96. 376 0
      src/core/notebookmgr.cpp
  97. 118 0
      src/core/notebookmgr.h
  98. 253 0
      src/core/sessionconfig.cpp
  99. 111 0
      src/core/sessionconfig.h
  100. 184 0
      src/core/singleinstanceguard.cpp

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+vnotex.pro.user
+vnotex.pro.user.*
+.ccls
+compile_commands.json
+compile_flags.txt
+*.plist

+ 3 - 0
.gitmodules

@@ -0,0 +1,3 @@
+[submodule "libs/vtextedit"]
+	path = libs/vtextedit
+	url = https://github.com/vnotex/vtextedit.git

+ 33 - 0
coc_update.cmd

@@ -0,0 +1,33 @@
+@echo off
+rem Update .ccls project file for ccls LPS and compile_flags.txt for clangd
+
+if "%~1"=="" (
+    echo missing argument: the location of Qt's include directory
+    EXIT /B 0
+)
+
+set qt_inc=%~1
+set qt_inc=%qt_inc:\=\\%
+
+(
+    echo clang
+    echo -fcxx-exceptions
+    echo -std=c++14
+    echo -Isrc\\core
+    echo -Isrc
+    echo -Ilibs\\vtextedit\\src\\editor\\include
+    echo -Ilibs\\vtitlebar\\src
+    echo -I%qt_inc%
+    echo -I%qt_inc%\\QtCore
+    echo -I%qt_inc%\\QtWebEngineWidgets
+    echo -I%qt_inc%\\QtSvg
+    echo -I%qt_inc%\\QtPrintSupport
+    echo -I%qt_inc%\\QtWidgets
+    echo -I%qt_inc%\\QtWebEngineCore
+    echo -I%qt_inc%\\QtGui
+    echo -I%qt_inc%\\QtWebChannel
+    echo -I%qt_inc%\\QtNetwork
+    echo -I%qt_inc%\\QtTest
+) > ".ccls"
+
+copy /Y .ccls compile_flags.txt

+ 5 - 0
libs/libs.pro

@@ -0,0 +1,5 @@
+TEMPLATE = subdirs
+
+SUBDIRS += \
+    vtextedit \
+    vtitlebar

+ 1 - 0
libs/vtextedit

@@ -0,0 +1 @@
+Subproject commit a75c9b8dd374dd4fed1cfd66b79408ecfea990c9

+ 22 - 0
libs/vtitlebar/src/vtitlebar.cpp

@@ -0,0 +1,22 @@
+#include "vtitlebar.h"
+
+#include <QStyleOption>
+#include <QPainter>
+
+using namespace vnotex;
+
+VTitleBar::VTitleBar(QWidget *p_parent)
+    : QWidget(p_parent)
+{
+
+}
+
+void VTitleBar::paintEvent(QPaintEvent *p_event)
+{
+    QStyleOption opt;
+    opt.init(this);
+    QPainter p(this);
+    style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
+
+    QWidget::paintEvent(p_event);
+}

+ 19 - 0
libs/vtitlebar/src/vtitlebar.h

@@ -0,0 +1,19 @@
+#ifndef VTITLEBAR_H
+#define VTITLEBAR_H
+
+#include <QWidget>
+
+namespace vnotex
+{
+    class VTitleBar : public QWidget
+    {
+        Q_OBJECT
+    public:
+        explicit VTitleBar(QWidget *p_parent = nullptr);
+
+    protected:
+        void paintEvent(QPaintEvent *p_event) Q_DECL_OVERRIDE;
+    };
+}
+
+#endif // VTITLEBAR_H

+ 118 - 0
libs/vtitlebar/src/vtoolbar.cpp

@@ -0,0 +1,118 @@
+#include "vtoolbar.h"
+
+#include <QDebug>
+#include <QMouseEvent>
+#include <QCoreApplication>
+#include <QToolButton>
+
+using namespace vnotex;
+
+VToolBar::VToolBar(QWidget *p_parent)
+    : QToolBar(p_parent),
+      m_window(p_parent)
+{
+    setupUI();
+    m_window->installEventFilter(this);
+}
+
+VToolBar::VToolBar(const QString &p_title, QWidget *p_parent)
+    : QToolBar(p_title, p_parent),
+      m_window(p_parent)
+{
+    setupUI();
+    m_window->installEventFilter(this);
+}
+
+void VToolBar::setupUI()
+{
+}
+
+void VToolBar::mousePressEvent(QMouseEvent *p_event)
+{
+    QToolBar::mousePressEvent(p_event);
+    m_lastPos = p_event->pos();
+}
+
+void VToolBar::mouseDoubleClickEvent(QMouseEvent *p_event)
+{
+    QToolBar::mouseDoubleClickEvent(p_event);
+    m_ignoreNextMove = true;
+    maximizeRestoreWindow();
+}
+
+void VToolBar::maximizeRestoreWindow()
+{
+    m_window->isMaximized() ? m_window->showNormal() : m_window->showMaximized();
+}
+
+void VToolBar::mouseMoveEvent(QMouseEvent *p_event)
+{
+    auto delta = p_event->pos() - m_lastPos;
+    if (!m_ignoreNextMove && !m_lastPos.isNull() && (qAbs(delta.x()) > 10 || qAbs(delta.y()) > 10)) {
+        if (m_window->isMaximized()) {
+            m_window->showNormal();
+        } else {
+            m_window->move(p_event->globalPos() - m_lastPos);
+        }
+    }
+    QToolBar::mouseMoveEvent(p_event);
+}
+
+void VToolBar::mouseReleaseEvent(QMouseEvent *p_event)
+{
+    QToolBar::mouseReleaseEvent(p_event);
+    m_ignoreNextMove = false;
+    m_lastPos = QPoint();
+}
+
+void VToolBar::addTitleBarIcons(const QIcon &p_minimizeIcon,
+                                const QIcon &p_maximizeIcon,
+                                const QIcon &p_restoreIcon,
+                                const QIcon &p_closeIcon)
+{
+    addSeparator();
+
+    addAction(p_minimizeIcon, tr("Minimize"),
+              this, [this]() {
+                  m_window->showMinimized();
+              });
+
+    m_maximizeIcon = p_maximizeIcon;
+    m_restoreIcon = p_restoreIcon;
+    m_maximizeAct = addAction(p_maximizeIcon, tr("Maximize"),
+                              this, [this]() {
+                                  maximizeRestoreWindow();
+                              });
+
+    {
+        auto closeAct = addAction(p_closeIcon, tr("Close"),
+                                  this, [this]() {
+                                      m_window->close();
+                                  });
+        auto btn = static_cast<QToolButton *>(widgetForAction(closeAct));
+        btn->setProperty("DangerousButton", true);
+    }
+
+    updateMaximizeAct();
+}
+
+bool VToolBar::eventFilter(QObject *p_obj, QEvent *p_event)
+{
+    if (p_obj == m_window) {
+        if (p_event->type() == QEvent::WindowStateChange) {
+            updateMaximizeAct();
+        }
+    }
+    return QToolBar::eventFilter(p_obj, p_event);
+}
+
+void VToolBar::updateMaximizeAct()
+{
+    if (m_window->isMaximized()) {
+        m_maximizeAct->setIcon(m_restoreIcon);
+        m_maximizeAct->setText(tr("Restore Down"));
+    } else {
+        m_maximizeAct->setIcon(m_maximizeIcon);
+        m_maximizeAct->setText(tr("Maximize"));
+    }
+}

+ 54 - 0
libs/vtitlebar/src/vtoolbar.h

@@ -0,0 +1,54 @@
+#ifndef VTOOLBAR_H
+#define VTOOLBAR_H
+
+#include <QToolBar>
+#include <QIcon>
+
+namespace vnotex
+{
+    class VToolBar : public QToolBar
+    {
+        Q_OBJECT
+    public:
+        explicit VToolBar(QWidget *p_parent = nullptr);
+
+        VToolBar(const QString &p_title, QWidget *p_parent = nullptr);
+
+        void addTitleBarIcons(const QIcon &p_minimizeIcon,
+                              const QIcon &p_maximizeIcon,
+                              const QIcon &p_restoreIcon,
+                              const QIcon &p_closeIcon);
+
+    protected:
+        void mousePressEvent(QMouseEvent *p_event) Q_DECL_OVERRIDE;
+
+        void mouseReleaseEvent(QMouseEvent *p_event) Q_DECL_OVERRIDE;
+
+        void mouseMoveEvent(QMouseEvent *p_event) Q_DECL_OVERRIDE;
+
+        void mouseDoubleClickEvent(QMouseEvent *p_event) Q_DECL_OVERRIDE;
+
+        bool eventFilter(QObject *p_obj, QEvent *p_event) Q_DECL_OVERRIDE;
+
+    private:
+        void setupUI();
+
+        void maximizeRestoreWindow();
+
+        void updateMaximizeAct();
+
+        QPoint m_lastPos;
+
+        bool m_ignoreNextMove = false;
+
+        QWidget *m_window = nullptr;
+
+        QAction *m_maximizeAct = nullptr;
+
+        QIcon m_maximizeIcon;
+
+        QIcon m_restoreIcon;
+    };
+}
+
+#endif // VTOOLBAR_H

+ 16 - 0
libs/vtitlebar/vtitlebar.pro

@@ -0,0 +1,16 @@
+QT += core gui widgets
+
+TARGET = vtitlebar
+
+TEMPLATE = lib
+
+# CONFIG += warn_off
+CONFIG += staticlib
+
+SOURCES += \
+    src/vtitlebar.cpp \
+    src/vtoolbar.cpp
+
+HEADERS += \
+    src/vtitlebar.h \
+    src/vtoolbar.h

+ 17 - 0
libs/vtitlebar/vtitlebar_export.pri

@@ -0,0 +1,17 @@
+INCLUDEPATH *= $$PWD/src
+
+DEPENDPATH *= $$PWD/src
+
+OUT_FOLDER = $$absolute_path($$relative_path($$PWD, $$_PRO_FILE_PWD_), $$OUT_PWD)
+win32:CONFIG(release, debug|release) {
+    LIBS += $$OUT_FOLDER/release/vtitlebar.lib
+    # For static library, we need to add this depends to let Qt re-build the target
+    # when there is a change in the library.
+    PRE_TARGETDEPS += $$OUT_FOLDER/release/vtitlebar.lib
+} else:win32:CONFIG(debug, debug|release) {
+    LIBS += $$OUT_FOLDER/debug/vtitlebar.lib
+    PRE_TARGETDEPS += $$OUT_FOLDER/debug/vtitlebar.lib
+} else:unix {
+    LIBS += $$OUT_FOLDER/libvtitlebar.a
+    PRE_TARGETDEPS += $$OUT_FOLDER/libvtitlebar.a
+}

+ 552 - 0
src/core/buffer/buffer.cpp

@@ -0,0 +1,552 @@
+#include "buffer.h"
+
+#include <QTimer>
+
+#include <notebook/node.h>
+#include <utils/fileutils.h>
+#include <widgets/viewwindow.h>
+#include <utils/pathutils.h>
+
+#include <core/configmgr.h>
+#include <core/editorconfig.h>
+
+#include "bufferprovider.h"
+#include "exception.h"
+
+using namespace vnotex;
+
+static vnotex::ID generateBufferID()
+{
+    static vnotex::ID id = 0;
+    return ++id;
+}
+
+Buffer::Buffer(const BufferParameters &p_parameters,
+               QObject *p_parent)
+    : QObject(p_parent),
+      m_provider(p_parameters.m_provider),
+      c_id(generateBufferID()),
+      m_readOnly(m_provider->isReadOnly())
+{
+    m_autoSaveTimer = new QTimer(this);
+    m_autoSaveTimer->setSingleShot(true);
+    m_autoSaveTimer->setInterval(1000);
+    connect(m_autoSaveTimer, &QTimer::timeout,
+            this, &Buffer::autoSave);
+
+    readContent();
+
+    checkBackupFileOfPreviousSession();
+}
+
+Buffer::~Buffer()
+{
+    Q_ASSERT(m_attachedViewWindowCount == 0);
+    Q_ASSERT(!m_viewWindowToSync);
+    Q_ASSERT(!isModified());
+    Q_ASSERT(m_backupFilePath.isEmpty());
+}
+
+int Buffer::getAttachViewWindowCount() const
+{
+    return m_attachedViewWindowCount;
+}
+
+void Buffer::attachViewWindow(ViewWindow *p_win)
+{
+    Q_UNUSED(p_win);
+    Q_ASSERT(!(m_state & StateFlag::Discarded));
+    ++m_attachedViewWindowCount;
+}
+
+void Buffer::detachViewWindow(ViewWindow *p_win)
+{
+    Q_ASSERT(p_win != m_viewWindowToSync);
+
+    --m_attachedViewWindowCount;
+    Q_ASSERT(m_attachedViewWindowCount >= 0);
+
+    if (m_attachedViewWindowCount == 0) {
+        emit attachedViewWindowEmpty();
+    }
+}
+
+ViewWindow *Buffer::createViewWindow(const QSharedPointer<FileOpenParameters> &p_paras, QWidget *p_parent)
+{
+    auto window = createViewWindowInternal(p_paras, p_parent);
+    Q_ASSERT(window);
+    window->attachToBuffer(this);
+    return window;
+}
+
+bool Buffer::match(const Node *p_node) const
+{
+    Q_ASSERT(p_node);
+    return m_provider->match(p_node);
+}
+
+bool Buffer::match(const QString &p_filePath) const
+{
+    return m_provider->match(p_filePath);
+}
+
+QString Buffer::getName() const
+{
+    return m_provider->getName();
+}
+
+QString Buffer::getPath() const
+{
+    return m_provider->getPath();
+}
+
+QString Buffer::getContentPath() const
+{
+    return m_provider->getContentPath();
+}
+
+QString Buffer::getContentBasePath() const
+{
+    return PathUtils::parentDirPath(getContentPath());
+}
+
+ID Buffer::getID() const
+{
+    return c_id;
+}
+
+const QString &Buffer::getContent() const
+{
+    const_cast<Buffer *>(this)->syncContent();
+    return m_content;
+}
+
+void Buffer::setContent(const QString &p_content, int &p_revision)
+{
+    m_viewWindowToSync = nullptr;
+    m_content = p_content;
+    p_revision = ++m_revision;
+    setModified(true);
+    m_autoSaveTimer->start();
+    emit contentsChanged();
+}
+
+void Buffer::invalidateContent(const ViewWindow *p_win,
+                               const std::function<void(int)> &p_setRevision)
+{
+    Q_ASSERT(!m_viewWindowToSync || m_viewWindowToSync == p_win);
+    ++m_revision;
+    p_setRevision(m_revision);
+    m_viewWindowToSync = p_win;
+    m_autoSaveTimer->start();
+    emit contentsChanged();
+}
+
+int Buffer::getRevision() const
+{
+    return m_revision;
+}
+
+void Buffer::syncContent(const ViewWindow *p_win)
+{
+    if (m_viewWindowToSync == p_win) {
+        syncContent();
+    }
+}
+
+void Buffer::syncContent()
+{
+    if (m_viewWindowToSync) {
+        // Need to sync content.
+        m_content = m_viewWindowToSync->getLatestContent();
+        m_viewWindowToSync = nullptr;
+    }
+}
+
+bool Buffer::isModified() const
+{
+    return m_modified;
+}
+
+void Buffer::setModified(bool p_modified)
+{
+    if (m_modified == p_modified) {
+        return;
+    }
+
+    m_modified = p_modified;
+    emit modified(m_modified);
+}
+
+bool Buffer::isReadOnly() const
+{
+    return m_readOnly;
+}
+
+Buffer::OperationCode Buffer::save(bool p_force)
+{
+    Q_ASSERT(!m_readOnly);
+    if (m_readOnly) {
+        return OperationCode::Failed;
+    }
+
+    if (m_modified
+        || p_force
+        || m_state & (StateFlag::FileMissingOnDisk | StateFlag::FileChangedOutside)) {
+        syncContent();
+
+        // We do not involve user here to handle file missing and changed outside cases.
+        // The active ViewWindow will check this periodically.
+        // Check if file still exists.
+        if (!p_force && !checkFileExistsOnDisk()) {
+            qWarning() << "failed to save buffer due to file missing on disk" << getPath();
+            return OperationCode::FileMissingOnDisk;
+        }
+
+        // Check if file is modified outside.
+        if (!p_force && checkFileChangedOutside()) {
+            qWarning() << "failed to save buffer due to file changed from outside" << getPath();
+            return OperationCode::FileChangedOutside;
+        }
+
+        m_provider->write(m_content);
+        setModified(false);
+        m_state &= ~(StateFlag::FileMissingOnDisk | StateFlag::FileChangedOutside);
+    }
+    return OperationCode::Success;
+}
+
+Buffer::OperationCode Buffer::reload()
+{
+    // Check if file is missing.
+    if (!checkFileExistsOnDisk()) {
+        qWarning() << "failed to save buffer due to file missing on disk" << getPath();
+        return OperationCode::FileMissingOnDisk;
+    }
+
+    if (m_modified
+        || m_state & (StateFlag::FileMissingOnDisk | StateFlag::FileChangedOutside)) {
+        readContent();
+
+        emit modified(m_modified);
+        emit contentsChanged();
+    }
+    return OperationCode::Success;
+}
+
+void Buffer::readContent()
+{
+    m_content = m_provider->read();
+    ++m_revision;
+
+    // Reset state.
+    m_viewWindowToSync = nullptr;
+    m_modified = false;
+}
+
+void Buffer::discard()
+{
+    Q_ASSERT(!(m_state & StateFlag::Discarded));
+    Q_ASSERT(m_attachedViewWindowCount == 1);
+    m_autoSaveTimer->stop();
+    m_content.clear();
+    m_state |= StateFlag::Discarded;
+    ++m_revision;
+
+    m_viewWindowToSync = nullptr;
+    m_modified = false;
+}
+
+void Buffer::close()
+{
+    // Delete the backup file if exists.
+    m_autoSaveTimer->stop();
+    if (!m_backupFilePath.isEmpty()) {
+        FileUtils::removeFile(m_backupFilePath);
+        m_backupFilePath.clear();
+    }
+}
+
+QString Buffer::getImageFolderPath() const
+{
+    return const_cast<Buffer *>(this)->m_provider->fetchImageFolderPath();
+}
+
+QString Buffer::insertImage(const QString &p_srcImagePath, const QString &p_imageFileName)
+{
+    Q_UNUSED(p_srcImagePath);
+    Q_UNUSED(p_imageFileName);
+    Q_ASSERT_X(false, "insertImage", "image insert is not supported");
+    return QString();
+}
+
+QString Buffer::insertImage(const QImage &p_image, const QString &p_imageFileName)
+{
+    Q_UNUSED(p_image);
+    Q_UNUSED(p_imageFileName);
+    Q_ASSERT_X(false, "insertImage", "image insert is not supported");
+    return QString();
+}
+
+void Buffer::removeImage(const QString &p_imagePath)
+{
+    Q_UNUSED(p_imagePath);
+    Q_ASSERT_X(false, "removeImage", "image remove is not supported");
+}
+
+void Buffer::autoSave()
+{
+    if (m_readOnly) {
+        m_autoSaveTimer->stop();
+        return;
+    }
+
+    if (m_state & (StateFlag::FileMissingOnDisk | StateFlag::FileChangedOutside)) {
+        qDebug() << "disable AutoSave due to file missing on disk or changed outside";
+        return;
+    }
+    Q_ASSERT(!(m_state & StateFlag::Discarded));
+    auto policy = ConfigMgr::getInst().getEditorConfig().getAutoSavePolicy();
+    switch (policy) {
+    case EditorConfig::AutoSavePolicy::None:
+        return;
+
+    case EditorConfig::AutoSavePolicy::AutoSave:
+        save(false);
+        break;
+
+    case EditorConfig::AutoSavePolicy::BackupFile:
+        writeBackupFile();
+        break;
+    }
+}
+
+void Buffer::writeBackupFile()
+{
+    if (m_backupFilePath.isEmpty()) {
+        const auto &config = ConfigMgr::getInst().getEditorConfig();
+        QString backupDirPath(QDir(getContentBasePath()).filePath(config.getBackupFileDirectory()));
+        backupDirPath = QDir::cleanPath(backupDirPath);
+        auto backupFileName = FileUtils::generateFileNameWithSequence(backupDirPath,
+                                                                      getName(),
+                                                                      config.getBackupFileExtension());
+        QDir backupDir(backupDirPath);
+        backupDir.mkpath(backupDirPath);
+        m_backupFilePath = backupDir.filePath(backupFileName);
+    }
+
+    Q_ASSERT(m_backupFilePathOfPreviousSession.isEmpty());
+
+    // Just use FileUtils instead of notebook backend.
+    FileUtils::writeFile(m_backupFilePath, generateBackupFileHead() + getContent());
+}
+
+QString Buffer::generateBackupFileHead() const
+{
+    return QString("vnotex_backup_file %1|").arg(getContentPath());
+}
+
+void Buffer::checkBackupFileOfPreviousSession()
+{
+    const auto &config = ConfigMgr::getInst().getEditorConfig();
+    if (config.getAutoSavePolicy() != EditorConfig::AutoSavePolicy::BackupFile) {
+        return;
+    }
+
+    QString backupDirPath(QDir(getContentBasePath()).filePath(config.getBackupFileDirectory()));
+    backupDirPath = QDir::cleanPath(backupDirPath);
+    QDir backupDir(backupDirPath);
+    QStringList backupFiles;
+    {
+        const QString nameFilter = QString("%1*%2").arg(getName(), config.getBackupFileExtension());
+        backupFiles = backupDir.entryList(QStringList(nameFilter),
+                                          QDir::Files | QDir::Hidden | QDir::NoSymLinks | QDir::NoDotAndDotDot);
+    }
+
+    if (backupFiles.isEmpty()) {
+        return;
+    }
+
+    for (const auto &file : backupFiles) {
+        const auto filePath = backupDir.filePath(file);
+        if (isBackupFileOfBuffer(filePath)) {
+            const auto backupContent = readBackupFile(filePath);
+            if (backupContent == getContent()) {
+                // Found backup file with identical content.
+                // Just discard the backup file.
+                FileUtils::removeFile(filePath);
+                qInfo() << "delete identical backup file of previous session" << filePath;
+            } else {
+                m_backupFilePathOfPreviousSession = filePath;
+                qInfo() << "found backup file of previous session" << filePath;
+            }
+            break;
+        }
+    }
+}
+
+bool Buffer::isBackupFileOfBuffer(const QString &p_file) const
+{
+    QFile file(p_file);
+    if (!file.open(QFile::ReadOnly | QIODevice::Text)) {
+        return false;
+    }
+
+    QTextStream st(&file);
+    const auto head = st.readLine();
+    return head.startsWith(generateBackupFileHead());
+}
+
+const QString &Buffer::getBackupFileOfPreviousSession() const
+{
+    return m_backupFilePathOfPreviousSession;
+}
+
+QString Buffer::readBackupFile(const QString &p_filePath)
+{
+    auto content = FileUtils::readTextFile(p_filePath);
+    return content.mid(content.indexOf(QLatin1Char('|')) + 1);
+}
+
+void Buffer::discardBackupFileOfPreviousSession()
+{
+    Q_ASSERT(!m_backupFilePathOfPreviousSession.isEmpty());
+
+    FileUtils::removeFile(m_backupFilePathOfPreviousSession);
+    qInfo() << "discard backup file of previous session" << m_backupFilePathOfPreviousSession;
+    m_backupFilePathOfPreviousSession.clear();
+}
+
+void Buffer::recoverFromBackupFileOfPreviousSession()
+{
+    Q_ASSERT(!m_backupFilePathOfPreviousSession.isEmpty());
+
+    m_content = readBackupFile(m_backupFilePathOfPreviousSession);
+    m_provider->write(m_content);
+    ++m_revision;
+
+    FileUtils::removeFile(m_backupFilePathOfPreviousSession);
+    qInfo() << "recover from backup file of previous session" << m_backupFilePathOfPreviousSession;
+    m_backupFilePathOfPreviousSession.clear();
+
+    // Reset state.
+    m_viewWindowToSync = nullptr;
+    m_modified = false;
+
+    emit modified(m_modified);
+    emit contentsChanged();
+}
+
+bool Buffer::isChildOf(const Node *p_node) const
+{
+    return m_provider->isChildOf(p_node);
+}
+
+bool Buffer::isAttachmentSupported() const
+{
+    return !m_readOnly && m_provider->isAttachmentSupported();
+}
+
+bool Buffer::hasAttachment() const
+{
+    if (!isAttachmentSupported()) {
+        return false;
+    }
+
+    if (m_provider->getAttachmentFolder().isEmpty()) {
+        return false;
+    }
+
+    QDir dir(getAttachmentFolderPath());
+    return !dir.isEmpty();
+}
+
+QString Buffer::getAttachmentFolderPath() const
+{
+    Q_ASSERT(isAttachmentSupported());
+    return const_cast<Buffer *>(this)->m_provider->fetchAttachmentFolderPath();
+}
+
+QStringList Buffer::addAttachment(const QString &p_destFolderPath, const QStringList &p_files)
+{
+    if (p_files.isEmpty()) {
+        return QStringList();
+    }
+    auto destFolderPath = p_destFolderPath.isEmpty() ? getAttachmentFolderPath() : p_destFolderPath;
+    Q_ASSERT(PathUtils::pathContains(getAttachmentFolderPath(), destFolderPath));
+    auto files = m_provider->addAttachment(destFolderPath, p_files);
+    if (!files.isEmpty()) {
+        emit attachmentChanged();
+    }
+    return files;
+}
+
+QString Buffer::newAttachmentFile(const QString &p_destFolderPath, const QString &p_name)
+{
+    Q_ASSERT(PathUtils::pathContains(getAttachmentFolderPath(), p_destFolderPath));
+    auto filePath = m_provider->newAttachmentFile(p_destFolderPath, p_name);
+    emit attachmentChanged();
+    return filePath;
+}
+
+QString Buffer::newAttachmentFolder(const QString &p_destFolderPath, const QString &p_name)
+{
+    Q_ASSERT(PathUtils::pathContains(getAttachmentFolderPath(), p_destFolderPath));
+    auto folderPath = m_provider->newAttachmentFolder(p_destFolderPath, p_name);
+    emit attachmentChanged();
+    return folderPath;
+}
+
+QString Buffer::renameAttachment(const QString &p_path, const QString &p_name)
+{
+    Q_ASSERT(PathUtils::pathContains(getAttachmentFolderPath(), p_path));
+    return m_provider->renameAttachment(p_path, p_name);
+}
+
+void Buffer::removeAttachment(const QStringList &p_paths)
+{
+    m_provider->removeAttachment(p_paths);
+    emit attachmentChanged();
+}
+
+bool Buffer::isAttachment(const QString &p_path) const
+{
+    return PathUtils::pathContains(getAttachmentFolderPath(), p_path);
+}
+
+Buffer::ProviderType Buffer::getProviderType() const
+{
+    return m_provider->getType();
+}
+
+Node *Buffer::getNode() const
+{
+    return m_provider->getNode();
+}
+
+bool Buffer::checkFileExistsOnDisk()
+{
+    if (m_provider->checkFileExistsOnDisk()) {
+        m_state &= ~StateFlag::FileMissingOnDisk;
+        return true;
+    } else {
+        m_state |= StateFlag::FileMissingOnDisk;
+        return false;
+    }
+}
+
+bool Buffer::checkFileChangedOutside()
+{
+    if (m_provider->checkFileChangedOutside()) {
+        m_state |= StateFlag::FileChangedOutside;
+        return true;
+    } else {
+        m_state &= ~StateFlag::FileChangedOutside;
+        return false;
+    }
+}
+
+Buffer::StateFlags Buffer::state() const
+{
+    return m_state;
+}

+ 241 - 0
src/core/buffer/buffer.h

@@ -0,0 +1,241 @@
+#ifndef BUFFER_H
+#define BUFFER_H
+
+#include <QObject>
+#include <QSharedPointer>
+
+#include <functional>
+
+#include <global.h>
+
+class QWidget;
+class QTimer;
+
+namespace vnotex
+{
+    class Node;
+    class Buffer;
+    class ViewWindow;
+    struct FileOpenParameters;
+    class BufferProvider;
+
+    struct BufferParameters
+    {
+        QSharedPointer<BufferProvider> m_provider;
+    };
+
+    class Buffer : public QObject
+    {
+        Q_OBJECT
+    public:
+        enum class ProviderType
+        {
+            Internal,
+            External
+        };
+
+        enum class OperationCode
+        {
+            Success,
+            FileMissingOnDisk,
+            FileChangedOutside,
+            Failed
+        };
+
+        enum StateFlag
+        {
+            Normal = 0,
+            FileMissingOnDisk = 0x1,
+            FileChangedOutside = 0x2,
+            Discarded = 0x4
+        };
+        Q_DECLARE_FLAGS(StateFlags, StateFlag);
+
+        Buffer(const BufferParameters &p_parameters,
+               QObject *p_parent = nullptr);
+
+        virtual ~Buffer();
+
+        int getAttachViewWindowCount() const;
+
+        void attachViewWindow(ViewWindow *p_win);
+        void detachViewWindow(ViewWindow *p_win);
+
+        // Create a view window to show the content of this buffer.
+        // Attach the created view window to this buffer.
+        ViewWindow *createViewWindow(const QSharedPointer<FileOpenParameters> &p_paras, QWidget *p_parent);
+
+        // Whether this buffer matches @p_node.
+        bool match(const Node *p_node) const;
+
+        // Whether this buffer matches @p_filePath.
+        bool match(const QString &p_filePath) const;
+
+        // Buffer name.
+        QString getName() const;
+
+        QString getPath() const;
+
+        // In some cases, getPath() may point to a ocntainer containting all the stuffs.
+        // getContentPath() will return the real path to the file providing the content.
+        QString getContentPath() const;
+
+        // Get the base path to resolve resources.
+        QString getContentBasePath() const;
+
+        ID getID() const;
+
+        // Get buffer content.
+        // It may differ from the content on disk.
+        // For performance, we need to sync the content with ViewWindow before returning
+        // the latest content.
+        const QString &getContent() const;
+
+        // @p_revision will be set before contentsChanged is emitted.
+        void setContent(const QString &p_content, int &p_revision);
+
+        // Invalidate the content of buffer.
+        // Need to sync with @p_win to get the latest content.
+        // @p_setRevision will be called to set revision before contentsChanged is emitted.
+        void invalidateContent(const ViewWindow *p_win,
+                               const std::function<void(int)> &p_setRevision);
+
+        // Sync content with @p_win if @p_win is the window needed to sync.
+        void syncContent(const ViewWindow *p_win);
+
+        int getRevision() const;
+
+        bool isModified() const;
+        void setModified(bool p_modified);
+
+        bool isReadOnly() const;
+
+        // Save buffer content to file.
+        OperationCode save(bool p_force);
+
+        // Discard changes and reload file.
+        OperationCode reload();
+
+        // Discard the buffer which will invalidate the buffer.
+        void discard();
+
+        // Buffer is about to be deleted.
+        void close();
+
+        // Insert image from @p_srcImagePath.
+        // Return inserted image file path.
+        virtual QString insertImage(const QString &p_srcImagePath, const QString &p_imageFileName);
+
+        virtual QString insertImage(const QImage &p_image, const QString &p_imageFileName);
+
+        virtual void removeImage(const QString &p_imagePath);
+
+        const QString &getBackupFileOfPreviousSession() const;
+
+        void discardBackupFileOfPreviousSession();
+
+        void recoverFromBackupFileOfPreviousSession();
+
+        // Whether this buffer's provider is a child of @p_node or an attachment of @p_node.
+        bool isChildOf(const Node *p_node) const;
+
+        Node *getNode() const;
+
+        bool isAttachmentSupported() const;
+
+        bool hasAttachment() const;
+
+        QString getAttachmentFolderPath() const;
+
+        // @p_destFolderPath: folder path locating in attachment folder. Use the root folder if empty.
+        QStringList addAttachment(const QString &p_destFolderPath, const QStringList &p_files);
+
+        QString newAttachmentFile(const QString &p_destFolderPath, const QString &p_name);
+
+        QString newAttachmentFolder(const QString &p_destFolderPath, const QString &p_name);
+
+        QString renameAttachment(const QString &p_path, const QString &p_name);
+
+        void removeAttachment(const QStringList &p_paths);
+
+        // Judge whether file @p_path is attachment.
+        bool isAttachment(const QString &p_path) const;
+
+        ProviderType getProviderType() const;
+
+        bool checkFileExistsOnDisk();
+
+        bool checkFileChangedOutside();
+
+        StateFlags state() const;
+
+        static QString readBackupFile(const QString &p_filePath);
+
+    signals:
+        void attachedViewWindowEmpty();
+
+        void modified(bool p_modified);
+
+        void contentsChanged();
+
+        void nameChanged();
+
+        void attachmentChanged();
+
+    protected:
+        virtual ViewWindow *createViewWindowInternal(const QSharedPointer<FileOpenParameters> &p_paras, QWidget *p_parent) = 0;
+
+        QSharedPointer<BufferProvider> m_provider;
+
+    private slots:
+        void autoSave();
+
+    private:
+        void syncContent();
+
+        void readContent();
+
+        // Get the path of the image folder.
+        QString getImageFolderPath() const;
+
+        void writeBackupFile();
+
+        // Generate backup file head.
+        QString generateBackupFileHead() const;
+
+        void checkBackupFileOfPreviousSession();
+
+        bool isBackupFileOfBuffer(const QString &p_file) const;
+
+        // Will be assigned uniquely once created.
+        const ID c_id = 0;
+
+        // Revision of contents.
+        int m_revision = 0;
+
+        // If the buffer is modified, m_content reflect the latest changes instead
+        // of the file content.
+        QString m_content;
+
+        bool m_readOnly = false;
+
+        bool m_modified = false;
+
+        int m_attachedViewWindowCount = 0;
+
+        const ViewWindow *m_viewWindowToSync = nullptr;
+
+        // Managed by QObject.
+        QTimer *m_autoSaveTimer = nullptr;
+
+        QString m_backupFilePath;
+
+        QString m_backupFilePathOfPreviousSession;
+
+        StateFlags m_state = StateFlag::Normal;
+    };
+} // ns vnotex
+
+Q_DECLARE_OPERATORS_FOR_FLAGS(vnotex::Buffer::StateFlags)
+
+#endif // BUFFER_H

+ 22 - 0
src/core/buffer/buffer.pri

@@ -0,0 +1,22 @@
+SOURCES += \
+    $$PWD/buffer.cpp \
+    $$PWD/bufferprovider.cpp \
+    $$PWD/filebufferprovider.cpp \
+    $$PWD/markdownbuffer.cpp \
+    $$PWD/markdownbufferfactory.cpp \
+    $$PWD/filetypehelper.cpp \
+    $$PWD/nodebufferprovider.cpp \
+    $$PWD/textbuffer.cpp \
+    $$PWD/textbufferfactory.cpp
+
+HEADERS += \
+    $$PWD/bufferprovider.h \
+    $$PWD/buffer.h \
+    $$PWD/filebufferprovider.h \
+    $$PWD/ibufferfactory.h \
+    $$PWD/markdownbuffer.h \
+    $$PWD/markdownbufferfactory.h \
+    $$PWD/filetypehelper.h \
+    $$PWD/nodebufferprovider.h \
+    $$PWD/textbuffer.h \
+    $$PWD/textbufferfactory.h

+ 24 - 0
src/core/buffer/bufferprovider.cpp

@@ -0,0 +1,24 @@
+#include "bufferprovider.h"
+
+#include <QFileInfo>
+
+using namespace vnotex;
+
+bool BufferProvider::checkFileExistsOnDisk() const
+{
+    return QFileInfo::exists(getContentPath());
+}
+
+QDateTime BufferProvider::getLastModifiedFromFile() const
+{
+    return QFileInfo(getContentPath()).lastModified();
+}
+
+bool BufferProvider::checkFileChangedOutside() const
+{
+    QFileInfo info(getContentPath());
+    if (!info.exists() || m_lastModified != info.lastModified()) {
+        return true;
+    }
+    return false;
+}

+ 82 - 0
src/core/buffer/bufferprovider.h

@@ -0,0 +1,82 @@
+#ifndef BUFFERPROVIDER_H
+#define BUFFERPROVIDER_H
+
+#include <QObject>
+#include <QDateTime>
+
+#include "buffer.h"
+
+namespace vnotex
+{
+    class Node;
+
+    // Content provider for Buffer.
+    class BufferProvider : public QObject
+    {
+        Q_OBJECT
+    public:
+        BufferProvider(QObject *p_parent = nullptr)
+            : QObject(p_parent)
+        {
+        }
+
+        virtual ~BufferProvider() {}
+
+        virtual Buffer::ProviderType getType() const = 0;
+
+        virtual bool match(const Node *p_node) const = 0;
+
+        virtual bool match(const QString &p_filePath) const = 0;
+
+        virtual QString getName() const = 0;
+
+        virtual QString getPath() const = 0;
+
+        virtual QString getContentPath() const = 0;
+
+        virtual void write(const QString &p_content) = 0;
+
+        virtual QString read() const = 0;
+
+        virtual QString fetchImageFolderPath() = 0;
+
+        virtual bool isChildOf(const Node *p_node) const = 0;
+
+        virtual Node *getNode() const = 0;
+
+        virtual QString getAttachmentFolder() const = 0;
+
+        virtual QString fetchAttachmentFolderPath() = 0;
+
+        virtual QStringList addAttachment(const QString &p_destFolderPath, const QStringList &p_files) = 0;
+
+        virtual QString newAttachmentFile(const QString &p_destFolderPath, const QString &p_name) = 0;
+
+        virtual QString newAttachmentFolder(const QString &p_destFolderPath, const QString &p_name) = 0;
+
+        virtual QString renameAttachment(const QString &p_path, const QString &p_name) = 0;
+
+        virtual void removeAttachment(const QStringList &p_paths) = 0;
+
+        virtual QString insertImage(const QString &p_srcImagePath, const QString &p_imageFileName) = 0;
+
+        virtual QString insertImage(const QImage &p_image, const QString &p_imageFileName) = 0;
+
+        virtual void removeImage(const QString &p_imagePath) = 0;
+
+        virtual bool isAttachmentSupported() const = 0;
+
+        virtual bool checkFileExistsOnDisk() const;
+
+        virtual bool checkFileChangedOutside() const;
+
+        virtual bool isReadOnly() const = 0;
+
+    protected:
+        virtual QDateTime getLastModifiedFromFile() const;
+
+        QDateTime m_lastModified;
+    };
+}
+
+#endif // BUFFERPROVIDER_H

+ 165 - 0
src/core/buffer/filebufferprovider.cpp

@@ -0,0 +1,165 @@
+#include "filebufferprovider.h"
+
+#include <QFileInfo>
+
+#include <utils/pathutils.h>
+#include <utils/fileutils.h>
+#include <notebook/node.h>
+
+using namespace vnotex;
+
+FileBufferProvider::FileBufferProvider(const QString &p_filePath,
+                                       Node *p_nodeAttachedTo,
+                                       bool p_readOnly,
+                                       QObject *p_parent)
+    : BufferProvider(p_parent),
+      c_filePath(p_filePath),
+      c_nodeAttachedTo(p_nodeAttachedTo),
+      m_readOnly(p_readOnly)
+{
+}
+
+Buffer::ProviderType FileBufferProvider::getType() const
+{
+    return Buffer::ProviderType::External;
+}
+
+bool FileBufferProvider::match(const Node *p_node) const
+{
+    Q_UNUSED(p_node);
+    return false;
+}
+
+bool FileBufferProvider::match(const QString &p_filePath) const
+{
+    return PathUtils::areSamePaths(c_filePath, p_filePath);
+}
+
+QString FileBufferProvider::getName() const
+{
+    return PathUtils::fileName(c_filePath);
+}
+
+QString FileBufferProvider::getPath() const
+{
+    return c_filePath;
+}
+
+QString FileBufferProvider::getContentPath() const
+{
+    // TODO.
+    return getPath();
+}
+
+void FileBufferProvider::write(const QString &p_content)
+{
+    FileUtils::writeFile(getContentPath(), p_content);
+    m_lastModified = getLastModifiedFromFile();
+}
+
+QString FileBufferProvider::read() const
+{
+    const_cast<FileBufferProvider *>(this)->m_lastModified = getLastModifiedFromFile();
+    return FileUtils::readTextFile(getContentPath());
+}
+
+QString FileBufferProvider::fetchImageFolderPath()
+{
+    auto pa = PathUtils::concatenateFilePath(PathUtils::parentDirPath(getContentPath()), QStringLiteral("vx_images"));
+    QDir().mkpath(pa);
+    return pa;
+}
+
+bool FileBufferProvider::isChildOf(const Node *p_node) const
+{
+    if (c_nodeAttachedTo) {
+        return c_nodeAttachedTo == p_node || Node::isAncestor(p_node, c_nodeAttachedTo);
+    }
+    return false;
+}
+
+QString FileBufferProvider::getAttachmentFolder() const
+{
+    Q_ASSERT(false);
+    return QString();
+}
+
+QString FileBufferProvider::fetchAttachmentFolderPath()
+{
+    Q_ASSERT(false);
+    return QString();
+}
+
+QStringList FileBufferProvider::addAttachment(const QString &p_destFolderPath, const QStringList &p_files)
+{
+    Q_UNUSED(p_destFolderPath);
+    Q_UNUSED(p_files);
+    Q_ASSERT(false);
+    return QStringList();
+}
+
+QString FileBufferProvider::newAttachmentFile(const QString &p_destFolderPath, const QString &p_name)
+{
+    Q_UNUSED(p_destFolderPath);
+    Q_UNUSED(p_name);
+    Q_ASSERT(false);
+    return QString();
+}
+
+QString FileBufferProvider::newAttachmentFolder(const QString &p_destFolderPath, const QString &p_name)
+{
+    Q_UNUSED(p_destFolderPath);
+    Q_UNUSED(p_name);
+    Q_ASSERT(false);
+    return QString();
+}
+
+QString FileBufferProvider::renameAttachment(const QString &p_path, const QString &p_name)
+{
+    Q_UNUSED(p_path);
+    Q_UNUSED(p_name);
+    Q_ASSERT(false);
+    return QString();
+}
+
+void FileBufferProvider::removeAttachment(const QStringList &p_paths)
+{
+    Q_UNUSED(p_paths);
+    Q_ASSERT(false);
+}
+
+QString FileBufferProvider::insertImage(const QString &p_srcImagePath, const QString &p_imageFileName)
+{
+    const auto imageFolderPath = fetchImageFolderPath();
+    auto destFilePath = FileUtils::renameIfExistsCaseInsensitive(PathUtils::concatenateFilePath(imageFolderPath, p_imageFileName));
+    FileUtils::copyFile(p_srcImagePath, destFilePath);
+    return destFilePath;
+}
+
+QString FileBufferProvider::insertImage(const QImage &p_image, const QString &p_imageFileName)
+{
+    const auto imageFolderPath = fetchImageFolderPath();
+    auto destFilePath = FileUtils::renameIfExistsCaseInsensitive(PathUtils::concatenateFilePath(imageFolderPath, p_imageFileName));
+    p_image.save(destFilePath);
+    return destFilePath;
+}
+
+void FileBufferProvider::removeImage(const QString &p_imagePath)
+{
+    FileUtils::removeFile(p_imagePath);
+}
+
+bool FileBufferProvider::isAttachmentSupported() const
+{
+    return false;
+}
+
+Node *FileBufferProvider::getNode() const
+{
+    return c_nodeAttachedTo;
+}
+
+bool FileBufferProvider::isReadOnly() const
+{
+    return m_readOnly;
+}

+ 73 - 0
src/core/buffer/filebufferprovider.h

@@ -0,0 +1,73 @@
+#ifndef FILEBUFFERPROVIDER_H
+#define FILEBUFFERPROVIDER_H
+
+#include "bufferprovider.h"
+
+namespace vnotex
+{
+    // Buffer provider based on external file.
+    class FileBufferProvider : public BufferProvider
+    {
+        Q_OBJECT
+    public:
+        FileBufferProvider(const QString &p_filePath,
+                           Node *p_nodeAttachedTo,
+                           bool p_readOnly,
+                           QObject *p_parent = nullptr);
+
+        Buffer::ProviderType getType() const Q_DECL_OVERRIDE;
+
+        bool match(const Node *p_node) const Q_DECL_OVERRIDE;
+
+        bool match(const QString &p_filePath) const Q_DECL_OVERRIDE;
+
+        QString getName() const Q_DECL_OVERRIDE;
+
+        QString getPath() const Q_DECL_OVERRIDE;
+
+        QString getContentPath() const Q_DECL_OVERRIDE;
+
+        void write(const QString &p_content) Q_DECL_OVERRIDE;
+
+        QString read() const Q_DECL_OVERRIDE;
+
+        QString fetchImageFolderPath() Q_DECL_OVERRIDE;
+
+        bool isChildOf(const Node *p_node) const Q_DECL_OVERRIDE;
+
+        Node *getNode() const Q_DECL_OVERRIDE;
+
+        QString getAttachmentFolder() const Q_DECL_OVERRIDE;
+
+        QString fetchAttachmentFolderPath() Q_DECL_OVERRIDE;
+
+        QStringList addAttachment(const QString &p_destFolderPath, const QStringList &p_files) Q_DECL_OVERRIDE;
+
+        QString newAttachmentFile(const QString &p_destFolderPath, const QString &p_name) Q_DECL_OVERRIDE;
+
+        QString newAttachmentFolder(const QString &p_destFolderPath, const QString &p_name) Q_DECL_OVERRIDE;
+
+        QString renameAttachment(const QString &p_path, const QString &p_name) Q_DECL_OVERRIDE;
+
+        void removeAttachment(const QStringList &p_paths) Q_DECL_OVERRIDE;
+
+        QString insertImage(const QString &p_srcImagePath, const QString &p_imageFileName) Q_DECL_OVERRIDE;
+
+        QString insertImage(const QImage &p_image, const QString &p_imageFileName) Q_DECL_OVERRIDE;
+
+        void removeImage(const QString &p_imagePath) Q_DECL_OVERRIDE;
+
+        bool isAttachmentSupported() const Q_DECL_OVERRIDE;
+
+        bool isReadOnly() const Q_DECL_OVERRIDE;
+
+    private:
+        const QString c_filePath;
+
+        Node *c_nodeAttachedTo = nullptr;
+
+        bool m_readOnly = false;
+    };
+}
+
+#endif // FILEBUFFERPROVIDER_H

+ 54 - 0
src/core/buffer/filetypehelper.cpp

@@ -0,0 +1,54 @@
+#include "filetypehelper.h"
+
+#include <QFileInfo>
+
+#include <utils/fileutils.h>
+
+using namespace vnotex;
+
+const FileType FileTypeHelper::s_markdownFileType = "markdown";
+
+const FileType FileTypeHelper::s_textFileType = "text";
+
+const FileType FileTypeHelper::s_unknownFileType = "unknown";
+
+QSharedPointer<QMap<QString, FileType>> FileTypeHelper::s_fileTypeMap;
+
+FileType FileTypeHelper::fileType(const QString &p_filePath)
+{
+    Q_ASSERT(!p_filePath.isEmpty());
+
+    if (!s_fileTypeMap) {
+        init();
+    }
+
+    QFileInfo fi(p_filePath);
+    auto suffix = fi.suffix().toLower();
+    auto it = s_fileTypeMap->find(suffix);
+    if (it != s_fileTypeMap->end()) {
+        return it.value();
+    }
+
+    // Treat all unknown text files as plain text files.
+    if (FileUtils::isText(p_filePath)) {
+        return s_fileTypeMap->value(QStringLiteral("txt"));
+    }
+
+    return s_unknownFileType;
+}
+
+#define ADD(x, y) s_fileTypeMap->insert((x), (y))
+
+void FileTypeHelper::init()
+{
+    // TODO: load mapping from configuration file.
+    s_fileTypeMap.reset(new QMap<QString, FileType>());
+
+    ADD(QStringLiteral("md"), s_markdownFileType);
+    ADD(QStringLiteral("markdown"), s_markdownFileType);
+    ADD(QStringLiteral("mkd"), s_markdownFileType);
+
+    ADD(QStringLiteral("txt"), s_textFileType);
+    ADD(QStringLiteral("text"), s_textFileType);
+    ADD(QStringLiteral("log"), s_textFileType);
+}

+ 33 - 0
src/core/buffer/filetypehelper.h

@@ -0,0 +1,33 @@
+#ifndef FILETYPEHELPER_H
+#define FILETYPEHELPER_H
+
+#include <QString>
+#include <QMap>
+#include <QSharedPointer>
+
+namespace vnotex
+{
+    typedef QString FileType;
+
+    // Map file suffix to file type.
+    class FileTypeHelper
+    {
+    public:
+        FileTypeHelper() = delete;
+
+        static FileType fileType(const QString &p_filePath);
+
+        static const FileType s_markdownFileType;
+
+        static const FileType s_textFileType;
+
+        static const FileType s_unknownFileType;
+
+    private:
+        static void init();
+
+        static QSharedPointer<QMap<QString, FileType>> s_fileTypeMap;
+    };
+} // ns vnotex
+
+#endif // FILETYPEHELPER_H

+ 24 - 0
src/core/buffer/ibufferfactory.h

@@ -0,0 +1,24 @@
+#ifndef IBUFFERFACTORY_H
+#define IBUFFERFACTORY_H
+
+#include <QSharedPointer>
+
+namespace vnotex
+{
+    class Buffer;
+    struct BufferParameters;
+
+    // Abstract factory to create buffer.
+    class IBufferFactory
+    {
+    public:
+        virtual ~IBufferFactory()
+        {
+        }
+
+        virtual Buffer *createBuffer(const BufferParameters &p_parameters,
+                                     QObject *p_parent) = 0;
+    };
+} // ns vnotex
+
+#endif // IBUFFERFACTORY_H

+ 94 - 0
src/core/buffer/markdownbuffer.cpp

@@ -0,0 +1,94 @@
+#include "markdownbuffer.h"
+
+#include <QDir>
+
+#include <widgets/markdownviewwindow.h>
+#include <notebook/node.h>
+#include <utils/pathutils.h>
+#include <buffer/bufferprovider.h>
+
+using namespace vnotex;
+
+MarkdownBuffer::MarkdownBuffer(const BufferParameters &p_parameters,
+                               QObject *p_parent)
+    : Buffer(p_parameters, p_parent)
+{
+    fetchInitialImages();
+}
+
+ViewWindow *MarkdownBuffer::createViewWindowInternal(const QSharedPointer<FileOpenParameters> &p_paras, QWidget *p_parent)
+{
+    return new MarkdownViewWindow(p_paras, p_parent);
+}
+
+QString MarkdownBuffer::insertImage(const QString &p_srcImagePath, const QString &p_imageFileName)
+{
+    return m_provider->insertImage(p_srcImagePath, p_imageFileName);
+}
+
+QString MarkdownBuffer::insertImage(const QImage &p_image, const QString &p_imageFileName)
+{
+    return m_provider->insertImage(p_image, p_imageFileName);
+}
+
+void MarkdownBuffer::fetchInitialImages()
+{
+    Q_ASSERT(m_initialImages.isEmpty());
+    m_initialImages = vte::MarkdownUtils::fetchImagesFromMarkdownText(getContent(),
+                                                                      getContentBasePath(),
+                                                                      vte::MarkdownLink::TypeFlag::LocalRelativeInternal);
+}
+
+void MarkdownBuffer::addInsertedImage(const QString &p_imagePath, const QString &p_urlInLink)
+{
+    vte::MarkdownLink link;
+    link.m_path = p_imagePath;
+    link.m_urlInLink = p_urlInLink;
+    link.m_type = vte::MarkdownLink::TypeFlag::LocalRelativeInternal;
+    m_insertedImages.append(link);
+}
+
+QSet<QString> MarkdownBuffer::clearObsoleteImages()
+{
+    QSet<QString> obsoleteImages;
+
+    Q_ASSERT(!isModified());
+    const bool discarded = state() & StateFlag::Discarded;
+    const auto latestImages =
+        vte::MarkdownUtils::fetchImagesFromMarkdownText(!discarded ? getContent() : m_provider->read(),
+                                                        getContentBasePath(),
+                                                        vte::MarkdownLink::TypeFlag::LocalRelativeInternal);
+    QSet<QString> latestImagesPath;
+    for (const auto &link : latestImages) {
+        latestImagesPath.insert(PathUtils::normalizePath(link.m_path));
+    }
+
+    for (const auto &link : m_insertedImages) {
+        if (!(link.m_type & vte::MarkdownLink::TypeFlag::LocalRelativeInternal)) {
+            continue;
+        }
+
+        if (!latestImagesPath.contains(PathUtils::normalizePath(link.m_path))) {
+            obsoleteImages.insert(link.m_path);
+        }
+    }
+
+    m_insertedImages.clear();
+
+    for (const auto &link : m_initialImages) {
+        Q_ASSERT(link.m_type & vte::MarkdownLink::TypeFlag::LocalRelativeInternal);
+        if (!latestImagesPath.contains(PathUtils::normalizePath(link.m_path))) {
+            obsoleteImages.insert(link.m_path);
+        }
+    }
+
+    m_initialImages = latestImages;
+
+    return obsoleteImages;
+}
+
+void MarkdownBuffer::removeImage(const QString &p_imagePath)
+{
+    qDebug() << "remove obsolete image" << p_imagePath;
+    m_provider->removeImage(p_imagePath);
+}

+ 47 - 0
src/core/buffer/markdownbuffer.h

@@ -0,0 +1,47 @@
+#ifndef MARKDOWNBUFFER_H
+#define MARKDOWNBUFFER_H
+
+#include "buffer.h"
+
+#include <QVector>
+#include <QSet>
+
+#include <vtextedit/markdownutils.h>
+
+namespace vnotex
+{
+    class MarkdownBuffer : public Buffer
+    {
+        Q_OBJECT
+    public:
+        MarkdownBuffer(const BufferParameters &p_parameters,
+                       QObject *p_parent = nullptr);
+
+        QString insertImage(const QString &p_srcImagePath, const QString &p_imageFileName) Q_DECL_OVERRIDE;
+
+        QString insertImage(const QImage &p_image, const QString &p_imageFileName) Q_DECL_OVERRIDE;
+
+        void removeImage(const QString &p_imagePath) Q_DECL_OVERRIDE;
+
+        void addInsertedImage(const QString &p_imagePath, const QString &p_urlInLink);
+
+        // Clear obsolete images.
+        // Won't delete images, just return a list of obsolete images path.
+        // Will re-init m_initialImages and clear m_insertedImages.
+        QSet<QString> clearObsoleteImages();
+
+    protected:
+        ViewWindow *createViewWindowInternal(const QSharedPointer<FileOpenParameters> &p_paras, QWidget *p_parent) Q_DECL_OVERRIDE;
+
+    private:
+        void fetchInitialImages();
+
+        // Images referenced in the file before opening this buffer.
+        QVector<vte::MarkdownLink> m_initialImages;
+
+        // Images newly inserted during this buffer's lifetime.
+        QVector<vte::MarkdownLink> m_insertedImages;
+    };
+} // ns vnotex
+
+#endif // MARKDOWNBUFFER_H

+ 11 - 0
src/core/buffer/markdownbufferfactory.cpp

@@ -0,0 +1,11 @@
+#include "markdownbufferfactory.h"
+
+#include "markdownbuffer.h"
+
+using namespace vnotex;
+
+Buffer *MarkdownBufferFactory::createBuffer(const BufferParameters &p_parameters,
+                                            QObject *p_parent)
+{
+    return new MarkdownBuffer(p_parameters, p_parent);
+}

+ 17 - 0
src/core/buffer/markdownbufferfactory.h

@@ -0,0 +1,17 @@
+#ifndef MARKDOWNBUFFERFACTORY_H
+#define MARKDOWNBUFFERFACTORY_H
+
+#include "ibufferfactory.h"
+
+namespace vnotex
+{
+    // Buffer factory for Markdown file.
+    class MarkdownBufferFactory : public IBufferFactory
+    {
+    public:
+        Buffer *createBuffer(const BufferParameters &p_parameters,
+                             QObject *p_parent) Q_DECL_OVERRIDE;
+    };
+} // vnotex
+
+#endif // MARKDOWNBUFFERFACTORY_H

+ 133 - 0
src/core/buffer/nodebufferprovider.cpp

@@ -0,0 +1,133 @@
+#include "nodebufferprovider.h"
+
+#include <QFileInfo>
+
+#include <notebook/node.h>
+#include <utils/pathutils.h>
+
+using namespace vnotex;
+
+NodeBufferProvider::NodeBufferProvider(Node *p_node, QObject *p_parent)
+    : BufferProvider(p_parent),
+      m_node(p_node),
+      m_path(m_node->fetchAbsolutePath()),
+      m_contentPath(m_node->fetchContentPath())
+{
+}
+
+Buffer::ProviderType NodeBufferProvider::getType() const
+{
+    return Buffer::ProviderType::Internal;
+}
+
+bool NodeBufferProvider::match(const Node *p_node) const
+{
+    return m_node == p_node;
+}
+
+bool NodeBufferProvider::match(const QString &p_filePath) const
+{
+    return PathUtils::areSamePaths(getPath(), p_filePath);
+}
+
+QString NodeBufferProvider::getName() const
+{
+    return m_node->getName();
+}
+
+QString NodeBufferProvider::getPath() const
+{
+    return m_path;
+}
+
+QString NodeBufferProvider::getContentPath() const
+{
+    return m_contentPath;
+}
+
+void NodeBufferProvider::write(const QString &p_content)
+{
+    m_node->write(p_content);
+    m_lastModified = getLastModifiedFromFile();
+}
+
+QString NodeBufferProvider::read() const
+{
+    const_cast<NodeBufferProvider *>(this)->m_lastModified = getLastModifiedFromFile();
+    return m_node->read();
+}
+
+QString NodeBufferProvider::fetchImageFolderPath()
+{
+    return m_node->fetchImageFolderPath();
+}
+
+bool NodeBufferProvider::isChildOf(const Node *p_node) const
+{
+    return Node::isAncestor(p_node, m_node);
+}
+
+QString NodeBufferProvider::getAttachmentFolder() const
+{
+    return m_node->getAttachmentFolder();
+}
+
+QString NodeBufferProvider::fetchAttachmentFolderPath()
+{
+    return m_node->fetchAttachmentFolderPath();
+}
+
+QStringList NodeBufferProvider::addAttachment(const QString &p_destFolderPath, const QStringList &p_files)
+{
+    return m_node->addAttachment(p_destFolderPath, p_files);
+}
+
+QString NodeBufferProvider::newAttachmentFile(const QString &p_destFolderPath, const QString &p_name)
+{
+    return m_node->newAttachmentFile(p_destFolderPath, p_name);
+}
+
+QString NodeBufferProvider::newAttachmentFolder(const QString &p_destFolderPath, const QString &p_name)
+{
+    return m_node->newAttachmentFolder(p_destFolderPath, p_name);
+}
+
+QString NodeBufferProvider::renameAttachment(const QString &p_path, const QString &p_name)
+{
+    return m_node->renameAttachment(p_path, p_name);
+}
+
+void NodeBufferProvider::removeAttachment(const QStringList &p_paths)
+{
+    return m_node->removeAttachment(p_paths);
+}
+
+QString NodeBufferProvider::insertImage(const QString &p_srcImagePath, const QString &p_imageFileName)
+{
+    return m_node->insertImage(p_srcImagePath, p_imageFileName);
+}
+
+QString NodeBufferProvider::insertImage(const QImage &p_image, const QString &p_imageFileName)
+{
+    return m_node->insertImage(p_image, p_imageFileName);
+}
+
+void NodeBufferProvider::removeImage(const QString &p_imagePath)
+{
+    m_node->removeImage(p_imagePath);
+}
+
+bool NodeBufferProvider::isAttachmentSupported() const
+{
+    return true;
+}
+
+Node *NodeBufferProvider::getNode() const
+{
+    return m_node;
+}
+
+bool NodeBufferProvider::isReadOnly() const
+{
+    return m_node->isReadOnly();
+}

+ 72 - 0
src/core/buffer/nodebufferprovider.h

@@ -0,0 +1,72 @@
+#ifndef NODEBUFFERPROVIDER_H
+#define NODEBUFFERPROVIDER_H
+
+#include "bufferprovider.h"
+
+namespace vnotex
+{
+    // Buffer provider based on an internal node.
+    class NodeBufferProvider : public BufferProvider
+    {
+        Q_OBJECT
+    public:
+        NodeBufferProvider(Node *p_node, QObject *p_parent = nullptr);
+
+        Buffer::ProviderType getType() const Q_DECL_OVERRIDE;
+
+        bool match(const Node *p_node) const Q_DECL_OVERRIDE;
+
+        bool match(const QString &p_filePath) const Q_DECL_OVERRIDE;
+
+        QString getName() const Q_DECL_OVERRIDE;
+
+        QString getPath() const Q_DECL_OVERRIDE;
+
+        QString getContentPath() const Q_DECL_OVERRIDE;
+
+        void write(const QString &p_content) Q_DECL_OVERRIDE;
+
+        QString read() const Q_DECL_OVERRIDE;
+
+        QString fetchImageFolderPath() Q_DECL_OVERRIDE;
+
+        bool isChildOf(const Node *p_node) const Q_DECL_OVERRIDE;
+
+        Node *getNode() const Q_DECL_OVERRIDE;
+
+        QString getAttachmentFolder() const Q_DECL_OVERRIDE;
+
+        QString fetchAttachmentFolderPath() Q_DECL_OVERRIDE;
+
+        QStringList addAttachment(const QString &p_destFolderPath, const QStringList &p_files) Q_DECL_OVERRIDE;
+
+        QString newAttachmentFile(const QString &p_destFolderPath, const QString &p_name) Q_DECL_OVERRIDE;
+
+        QString newAttachmentFolder(const QString &p_destFolderPath, const QString &p_name) Q_DECL_OVERRIDE;
+
+        QString renameAttachment(const QString &p_path, const QString &p_name) Q_DECL_OVERRIDE;
+
+        void removeAttachment(const QStringList &p_paths) Q_DECL_OVERRIDE;
+
+        QString insertImage(const QString &p_srcImagePath, const QString &p_imageFileName) Q_DECL_OVERRIDE;
+
+        QString insertImage(const QImage &p_image, const QString &p_imageFileName) Q_DECL_OVERRIDE;
+
+        void removeImage(const QString &p_imagePath) Q_DECL_OVERRIDE;
+
+        bool isAttachmentSupported() const Q_DECL_OVERRIDE;
+
+        bool isReadOnly() const Q_DECL_OVERRIDE;
+
+    private:
+        Node *m_node = nullptr;
+
+        // Used as cache.
+        QString m_path;
+
+        // Used as cache.
+        QString m_contentPath;
+    };
+}
+
+#endif // NODEBUFFERPROVIDER_H

+ 17 - 0
src/core/buffer/textbuffer.cpp

@@ -0,0 +1,17 @@
+#include "textbuffer.h"
+
+#include <widgets/textviewwindow.h>
+
+using namespace vnotex;
+
+TextBuffer::TextBuffer(const BufferParameters &p_parameters,
+                       QObject *p_parent)
+    : Buffer(p_parameters, p_parent)
+{
+}
+
+ViewWindow *TextBuffer::createViewWindowInternal(const QSharedPointer<FileOpenParameters> &p_paras, QWidget *p_parent)
+{
+    Q_UNUSED(p_paras);
+    return new TextViewWindow(p_parent);
+}

+ 20 - 0
src/core/buffer/textbuffer.h

@@ -0,0 +1,20 @@
+#ifndef TEXTBUFFER_H
+#define TEXTBUFFER_H
+
+#include "buffer.h"
+
+namespace vnotex
+{
+    class TextBuffer : public Buffer
+    {
+        Q_OBJECT
+    public:
+        TextBuffer(const BufferParameters &p_parameters,
+                   QObject *p_parent = nullptr);
+
+    protected:
+        ViewWindow *createViewWindowInternal(const QSharedPointer<FileOpenParameters> &p_paras, QWidget *p_parent) Q_DECL_OVERRIDE;
+    };
+}
+
+#endif // TEXTBUFFER_H

+ 11 - 0
src/core/buffer/textbufferfactory.cpp

@@ -0,0 +1,11 @@
+#include "textbufferfactory.h"
+
+#include "textbuffer.h"
+
+using namespace vnotex;
+
+Buffer *TextBufferFactory::createBuffer(const BufferParameters &p_parameters,
+                                        QObject *p_parent)
+{
+    return new TextBuffer(p_parameters, p_parent);
+}

+ 17 - 0
src/core/buffer/textbufferfactory.h

@@ -0,0 +1,17 @@
+#ifndef TEXTBUFFERFACTORY_H
+#define TEXTBUFFERFACTORY_H
+
+#include "ibufferfactory.h"
+
+namespace vnotex
+{
+    // Buffer factory for text file.
+    class TextBufferFactory : public IBufferFactory
+    {
+    public:
+        Buffer *createBuffer(const BufferParameters &p_parameters,
+                             QObject *p_parent) Q_DECL_OVERRIDE;
+    };
+}
+
+#endif // TEXTBUFFERFACTORY_H

+ 178 - 0
src/core/buffermgr.cpp

@@ -0,0 +1,178 @@
+#include "buffermgr.h"
+
+#include <QUrl>
+#include <QDebug>
+
+#include <notebook/node.h>
+#include <buffer/filetypehelper.h>
+#include <buffer/markdownbufferfactory.h>
+#include <buffer/textbufferfactory.h>
+#include <buffer/buffer.h>
+#include <buffer/nodebufferprovider.h>
+#include <buffer/filebufferprovider.h>
+#include <utils/widgetutils.h>
+#include "notebookmgr.h"
+#include "vnotex.h"
+
+#include "fileopenparameters.h"
+
+using namespace vnotex;
+
+BufferMgr::BufferMgr(QObject *p_parent)
+    : QObject(p_parent)
+{
+}
+
+BufferMgr::~BufferMgr()
+{
+    Q_ASSERT(m_buffers.isEmpty());
+}
+
+void BufferMgr::init()
+{
+    initBufferServer();
+}
+
+void BufferMgr::initBufferServer()
+{
+    m_bufferServer.reset(new NameBasedServer<IBufferFactory>);
+
+    // Markdown.
+    auto markdownFactory = QSharedPointer<MarkdownBufferFactory>::create();
+    m_bufferServer->registerItem(FileTypeHelper::s_markdownFileType, markdownFactory);
+
+    // Text.
+    auto textFactory = QSharedPointer<TextBufferFactory>::create();
+    m_bufferServer->registerItem(FileTypeHelper::s_textFileType, textFactory);
+}
+
+void BufferMgr::open(Node *p_node, const QSharedPointer<FileOpenParameters> &p_paras)
+{
+    if (!p_node) {
+        return;
+    }
+
+    if (p_node->getType() == Node::Type::Folder) {
+        return;
+    }
+
+    auto buffer = findBuffer(p_node);
+    if (!buffer) {
+        auto nodePath = p_node->fetchAbsolutePath();
+        auto fileType = FileTypeHelper::fileType(nodePath);
+        auto factory = m_bufferServer->getItem(fileType);
+        if (!factory) {
+            // No factory to open this file type.
+            qInfo() << "File will be opened by system:" << nodePath;
+            WidgetUtils::openUrlByDesktop(QUrl::fromLocalFile(nodePath));
+            return;
+        }
+
+        BufferParameters paras;
+        paras.m_provider.reset(new NodeBufferProvider(p_node));
+        buffer = factory->createBuffer(paras, this);
+        addBuffer(buffer);
+    }
+
+    Q_ASSERT(buffer);
+    emit bufferRequested(buffer, p_paras);
+}
+
+void BufferMgr::open(const QString &p_filePath, const QSharedPointer<FileOpenParameters> &p_paras)
+{
+    if (p_filePath.isEmpty()) {
+        return;
+    }
+
+    {
+        QFileInfo info(p_filePath);
+        if (!info.exists() || info.isDir()) {
+            qWarning() << QString("failed to open file %1 exists:%2 isDir:%3").arg(p_filePath).arg(info.exists()).arg(info.isDir());
+            return;
+        }
+    }
+
+    // Check if it is an internal node or not.
+    auto node = loadNodeByPath(p_filePath);
+    if (node) {
+        open(node.data(), p_paras);
+        return;
+    }
+
+    auto buffer = findBuffer(p_filePath);
+    if (!buffer) {
+        // Open it as external file.
+        auto fileType = FileTypeHelper::fileType(p_filePath);
+        auto factory = m_bufferServer->getItem(fileType);
+        if (!factory) {
+            // No factory to open this file type.
+            qInfo() << "File will be opened by system:" << p_filePath;
+            WidgetUtils::openUrlByDesktop(QUrl::fromLocalFile(p_filePath));
+            return;
+        }
+
+        BufferParameters paras;
+        paras.m_provider.reset(new FileBufferProvider(p_filePath,
+                                                      p_paras->m_nodeAttachedTo,
+                                                      p_paras->m_readOnly));
+        buffer = factory->createBuffer(paras, this);
+        addBuffer(buffer);
+    }
+
+    Q_ASSERT(buffer);
+    emit bufferRequested(buffer, p_paras);
+}
+
+Buffer *BufferMgr::findBuffer(const Node *p_node) const
+{
+    auto it = std::find_if(m_buffers.constBegin(),
+                           m_buffers.constEnd(),
+                           [p_node](const Buffer *p_buffer) {
+                               return p_buffer->match(p_node);
+                           });
+    if (it != m_buffers.constEnd()) {
+        return *it;
+    }
+
+    return nullptr;
+}
+
+Buffer *BufferMgr::findBuffer(const QString &p_filePath) const
+{
+    auto it = std::find_if(m_buffers.constBegin(),
+                           m_buffers.constEnd(),
+                           [p_filePath](const Buffer *p_buffer) {
+                               return p_buffer->match(p_filePath);
+                           });
+    if (it != m_buffers.constEnd()) {
+        return *it;
+    }
+
+    return nullptr;
+}
+
+void BufferMgr::addBuffer(Buffer *p_buffer)
+{
+    m_buffers.push_back(p_buffer);
+    connect(p_buffer, &Buffer::attachedViewWindowEmpty,
+            this, [this, p_buffer]() {
+                qDebug() << "delete buffer without attached view window"
+                         << p_buffer->getName();
+                m_buffers.removeAll(p_buffer);
+                p_buffer->close();
+                p_buffer->deleteLater();
+            });
+}
+
+QSharedPointer<Node> BufferMgr::loadNodeByPath(const QString &p_path)
+{
+    const auto &notebooks = VNoteX::getInst().getNotebookMgr().getNotebooks();
+    for (const auto &nb : notebooks) {
+        auto node = nb->loadNodeByPath(p_path);
+        if (node) {
+            return node;
+        }
+    }
+
+    return nullptr;
+}

+ 55 - 0
src/core/buffermgr.h

@@ -0,0 +1,55 @@
+#ifndef BUFFERMGR_H
+#define BUFFERMGR_H
+
+#include <QObject>
+#include <QScopedPointer>
+#include <QSharedPointer>
+#include <QVector>
+
+#include "namebasedserver.h"
+
+namespace vnotex
+{
+    class IBufferFactory;
+    class Node;
+    class Buffer;
+    struct FileOpenParameters;
+
+    class BufferMgr : public QObject
+    {
+        Q_OBJECT
+    public:
+        explicit BufferMgr(QObject *p_parent = nullptr);
+
+        ~BufferMgr();
+
+        void init();
+
+    public slots:
+        void open(Node *p_node, const QSharedPointer<FileOpenParameters> &p_paras);
+
+        void open(const QString &p_filePath, const QSharedPointer<FileOpenParameters> &p_paras);
+
+    signals:
+        void bufferRequested(Buffer *p_buffer, const QSharedPointer<FileOpenParameters> &p_paras);
+
+    private:
+        void initBufferServer();
+
+        Buffer *findBuffer(const Node *p_node) const;
+
+        Buffer *findBuffer(const QString &p_filePath) const;
+
+        void addBuffer(Buffer *p_buffer);
+
+        // Try to load @p_path as a node if it is within one notebook.
+        QSharedPointer<Node> loadNodeByPath(const QString &p_path);
+
+        QSharedPointer<NameBasedServer<IBufferFactory>> m_bufferServer;
+
+        // Managed by QObject.
+        QVector<Buffer *> m_buffers;
+    };
+} // ns vnotex
+
+#endif // BUFFERMGR_H

+ 187 - 0
src/core/clipboarddata.cpp

@@ -0,0 +1,187 @@
+#include "clipboarddata.h"
+
+#include <QJsonArray>
+#include <QJsonDocument>
+
+#include "exception.h"
+
+using namespace vnotex;
+
+const QString NodeClipboardDataItem::c_notebookId = "notebook_id";
+
+const QString NodeClipboardDataItem::c_nodePath = "node_path";
+
+NodeClipboardDataItem::NodeClipboardDataItem()
+{
+}
+
+NodeClipboardDataItem::NodeClipboardDataItem(ID p_notebookId, const QString &p_nodePath)
+    : m_notebookId(p_notebookId),
+      m_nodeRelativePath(p_nodePath)
+{
+}
+
+QJsonObject NodeClipboardDataItem::toJson() const
+{
+    QJsonObject jobj;
+    jobj[c_notebookId] = QString::number(m_notebookId);
+    jobj[c_nodePath] = m_nodeRelativePath;
+    return jobj;
+}
+
+void NodeClipboardDataItem::fromJson(const QJsonObject &p_jobj)
+{
+    Q_ASSERT(p_jobj.contains(c_notebookId) && p_jobj.contains(c_nodePath));
+    auto idRet = stringToID(p_jobj[c_notebookId].toString());
+    Q_ASSERT(idRet.first);
+    m_notebookId = idRet.second;
+    m_nodeRelativePath = p_jobj[c_nodePath].toString();
+}
+
+
+const QString ClipboardData::c_instanceId = "instance_id";
+
+const QString ClipboardData::c_action = "action";
+
+const QString ClipboardData::c_data = "data";
+
+ClipboardData::ClipboardData()
+{
+}
+
+ClipboardData::ClipboardData(ID p_instanceId, Action p_action)
+    : m_instanceId(p_instanceId),
+      m_action(p_action)
+{
+}
+
+void ClipboardData::fromJson(const QJsonObject &p_jobj)
+{
+    clear();
+
+    if (!p_jobj.contains(c_instanceId)
+        || !p_jobj.contains(c_action)
+        || !p_jobj.contains(c_data)) {
+        Exception::throwOne(Exception::Type::InvalidArgument,
+                            QString("fail to parse ClipboardData from json (%1)").arg(p_jobj.keys().join(',')));
+        return;
+    }
+
+    auto idRet = stringToID(p_jobj[c_instanceId].toString());
+    if (!idRet.first) {
+        Exception::throwOne(Exception::Type::InvalidArgument,
+                            QString("fail to parse ClipboardData from json (%1)").arg(p_jobj.keys().join(',')));
+        return;
+    }
+    m_instanceId = idRet.second;
+
+    int act = p_jobj[c_action].toInt(Action::Invalid);
+    m_action = intToAction(act);
+    if (m_action == Action::Invalid) {
+        Exception::throwOne(Exception::Type::InvalidArgument,
+                            QString("fail to parse ClipboardData from json (%1)").arg(p_jobj.keys().join(',')));
+        return;
+    }
+
+    for (const auto &item : p_jobj[c_data].toArray()) {
+        auto dataItem = createClipboardDataItem(m_action);
+        dataItem->fromJson(item.toObject());
+        m_data.push_back(dataItem);
+    }
+}
+
+QJsonObject ClipboardData::toJson() const
+{
+    QJsonObject jobj;
+    jobj[c_instanceId] = QString::number(m_instanceId);
+    jobj[c_action] = static_cast<int>(m_action);
+
+    QJsonArray data;
+    for (const auto& item : m_data) {
+        data.append(item->toJson());
+    }
+    jobj[c_data] = data;
+
+    return jobj;
+}
+
+ClipboardData::Action ClipboardData::intToAction(int p_act) const
+{
+    Action act = Action::Invalid;
+    if (p_act >= Action::CopyNode && p_act < Action::Invalid) {
+        act = static_cast<Action>(p_act);
+    }
+
+    return act;
+}
+
+void ClipboardData::clear()
+{
+    m_instanceId = 0;
+    m_action = Action::Invalid;
+    m_data.clear();
+}
+
+QSharedPointer<ClipboardDataItem> ClipboardData::createClipboardDataItem(Action p_act)
+{
+    switch (p_act) {
+    case Action::CopyNode:
+    case Action::MoveNode:
+        return QSharedPointer<NodeClipboardDataItem>::create();
+
+    case Action::Invalid:
+        Q_ASSERT(false);
+        return nullptr;
+    }
+
+    return nullptr;
+}
+
+void ClipboardData::addItem(const QSharedPointer<ClipboardDataItem> &p_item)
+{
+    Q_ASSERT(p_item);
+    m_data.push_back(p_item);
+}
+
+QString ClipboardData::toJsonText() const
+{
+    auto data = QJsonDocument(toJson()).toJson();
+    return QString::fromUtf8(data);
+}
+
+QSharedPointer<ClipboardData> ClipboardData::fromJsonText(const QString &p_json)
+{
+    if (p_json.isEmpty()) {
+        return nullptr;
+    }
+
+    auto data = QSharedPointer<ClipboardData>::create();
+    auto jobj = QJsonDocument::fromJson(p_json.toUtf8()).object();
+    if (jobj.isEmpty()) {
+        return nullptr;
+    }
+
+    try {
+        data->fromJson(jobj);
+    } catch (Exception &p_e) {
+        Q_UNUSED(p_e);
+        return nullptr;
+    }
+
+    return data;
+}
+
+const QVector<QSharedPointer<ClipboardDataItem>> &ClipboardData::getData() const
+{
+    return m_data;
+}
+
+ID ClipboardData::getInstanceId() const
+{
+    return m_instanceId;
+}
+
+ClipboardData::Action ClipboardData::getAction() const
+{
+    return m_action;
+}

+ 85 - 0
src/core/clipboarddata.h

@@ -0,0 +1,85 @@
+#ifndef CLIPBOARDDATA_H
+#define CLIPBOARDDATA_H
+
+#include <QVector>
+#include <QJsonObject>
+#include <QSharedPointer>
+
+#include "global.h"
+
+namespace vnotex
+{
+    class ClipboardDataItem
+    {
+    public:
+        virtual ~ClipboardDataItem()
+        {
+        }
+
+        virtual QJsonObject toJson() const = 0;
+
+        virtual void fromJson(const QJsonObject &p_jobj) = 0;
+    };
+
+    class NodeClipboardDataItem : public ClipboardDataItem
+    {
+    public:
+        NodeClipboardDataItem();
+
+        NodeClipboardDataItem(ID p_notebookId, const QString &p_nodePath);
+
+        QJsonObject toJson() const Q_DECL_OVERRIDE;
+
+        void fromJson(const QJsonObject &p_jobj) Q_DECL_OVERRIDE;
+
+        ID m_notebookId;
+        QString m_nodeRelativePath;
+
+    private:
+        static const QString c_notebookId;
+        static const QString c_nodePath;
+    };
+
+    class ClipboardData
+    {
+    public:
+        enum Action { CopyNode, MoveNode, Invalid };
+
+        ClipboardData();
+
+        ClipboardData(ID p_instanceId, Action p_action);
+
+        ID getInstanceId() const;
+
+        ClipboardData::Action getAction() const;
+
+        const QVector<QSharedPointer<ClipboardDataItem>> &getData() const;
+
+        void addItem(const QSharedPointer<ClipboardDataItem> &p_item);
+
+        QString toJsonText() const;
+
+        static QSharedPointer<ClipboardData> fromJsonText(const QString &p_json);
+
+    private:
+        void fromJson(const QJsonObject &p_jobj);
+
+        QJsonObject toJson() const;
+
+        ClipboardData::Action intToAction(int p_act) const;
+
+        void clear();
+
+        static QSharedPointer<ClipboardDataItem> createClipboardDataItem(Action p_act);
+
+        ID m_instanceId = 0;
+        Action m_action = Action::Invalid;
+        QVector<QSharedPointer<ClipboardDataItem>> m_data;
+
+        static const QString c_instanceId;
+        static const QString c_action;
+        static const QString c_data;
+    };
+} // ns vnotex
+
+#endif // CLIPBOARDDATA_H

+ 376 - 0
src/core/configmgr.cpp

@@ -0,0 +1,376 @@
+#include "configmgr.h"
+
+#include <QDir>
+#include <QCoreApplication>
+#include <QFileInfo>
+#include <QDebug>
+#include <QStandardPaths>
+#include <QJsonDocument>
+#include <QScopeGuard>
+#include <QResource>
+#include <QPixmap>
+#include <QSplashScreen>
+#include <QScopedPointer>
+
+#include <utils/pathutils.h>
+#include <utils/fileutils.h>
+#include "exception.h"
+#include <utils/utils.h>
+
+#include "mainconfig.h"
+#include "coreconfig.h"
+#include "sessionconfig.h"
+
+using namespace vnotex;
+
+#ifndef QT_NO_DEBUG
+#define VX_DEBUG_WEB
+#endif
+
+const QString ConfigMgr::c_orgName = "VNote";
+
+const QString ConfigMgr::c_appName = "VNoteX";
+
+const QString ConfigMgr::c_configFileName = "vnotex.json";
+
+const QString ConfigMgr::c_sessionFileName = "session.json";
+
+const QJsonObject &ConfigMgr::Settings::getJson() const
+{
+    return m_jobj;
+}
+
+QSharedPointer<ConfigMgr::Settings> ConfigMgr::Settings::fromFile(const QString &p_jsonFilePath)
+{
+    if (!QFileInfo::exists(p_jsonFilePath)) {
+        qWarning() << "return empty Settings from non-exist config file" << p_jsonFilePath;
+        return QSharedPointer<Settings>::create();
+    }
+
+    auto bytes = FileUtils::readFile(p_jsonFilePath);
+    return QSharedPointer<Settings>::create(QJsonDocument::fromJson(bytes).object());
+}
+
+void ConfigMgr::Settings::writeToFile(const QString &p_jsonFilePath) const
+{
+    FileUtils::writeFile(p_jsonFilePath, QJsonDocument(this->m_jobj).toJson());
+}
+
+ConfigMgr::ConfigMgr(QObject *p_parent)
+    : QObject(p_parent),
+      m_config(new MainConfig(this)),
+      m_sessionConfig(new SessionConfig(this))
+{
+    locateConfigFolder();
+
+    checkAppConfig();
+
+    m_config->init();
+    m_sessionConfig->init();
+}
+
+ConfigMgr::~ConfigMgr()
+{
+
+}
+
+void ConfigMgr::locateConfigFolder()
+{
+    // Check app config.
+    {
+        const QString configFolderName("vnotex_files");
+        QString folderPath(QCoreApplication::applicationDirPath()
+                           + '/' + configFolderName);
+        if (QDir(folderPath).exists()) {
+            // Config folder in app/.
+            m_appConfigFolderPath = PathUtils::cleanPath(folderPath);
+        } else {
+            m_appConfigFolderPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
+        }
+    }
+
+    // Check user config.
+    {
+        const QString configFolderName("user_files");
+        QString folderPath(QCoreApplication::applicationDirPath()
+                           + '/' + configFolderName);
+        if (QDir(folderPath).exists()) {
+            // Config folder in app/.
+            m_userConfigFolderPath = PathUtils::cleanPath(folderPath);
+        } else {
+            m_userConfigFolderPath = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
+
+            // Make sure it exists.
+            QDir dir(m_userConfigFolderPath);
+            dir.mkpath(m_userConfigFolderPath);
+        }
+    }
+
+    Q_ASSERT(m_appConfigFolderPath != m_userConfigFolderPath);
+    qInfo() << "app config folder" << m_appConfigFolderPath;
+    qInfo() << "user config folder" << m_userConfigFolderPath;
+}
+
+void ConfigMgr::checkAppConfig()
+{
+    bool needUpdate = false;
+    QDir appConfigDir(m_appConfigFolderPath);
+    if (!appConfigDir.exists()) {
+        needUpdate = true;
+        appConfigDir.mkpath(m_appConfigFolderPath);
+    } else {
+        if (!appConfigDir.exists(c_configFileName)) {
+            needUpdate = true;
+        } else {
+            // Check version of config file.
+            auto defaultSettings = getSettings(Source::Default);
+            auto appSettings = getSettings(Source::App);
+            auto defaultVersion = MainConfig::getVersion(defaultSettings->getJson());
+            auto appVersion = MainConfig::getVersion(appSettings->getJson());
+            if (defaultVersion != appVersion) {
+                needUpdate = true;
+            }
+        }
+
+        if (needUpdate) {
+            FileUtils::removeDir(m_appConfigFolderPath);
+            // Wait for the OS delete the folder.
+            Utils::sleepWait(1000);
+            appConfigDir.mkpath(m_appConfigFolderPath);
+        }
+    }
+
+    const auto mainConfigFilePath = appConfigDir.filePath(c_configFileName);
+
+#ifndef VX_DEBUG_WEB
+    if (!needUpdate) {
+        return;
+    }
+#endif
+
+    qInfo() << "update app config files in" << m_appConfigFolderPath;
+
+    Q_ASSERT(appConfigDir.exists());
+
+    QPixmap pixmap(":/vnotex/data/core/logo/vnote.png");
+    QScopedPointer<QSplashScreen> splash(new QSplashScreen(pixmap));
+    splash->show();
+
+    // Load extra data.
+    splash->showMessage("Loading extra resource data");
+    const QString extraRcc(QStringLiteral("extra.rcc"));
+    bool ret = QResource::registerResource(extraRcc);
+    if (!ret) {
+        Exception::throwOne(Exception::Type::FailToReadFile,
+                            QString("failed to register resource file %1").arg(extraRcc));
+    }
+    auto cleanup = qScopeGuard([extraRcc]() {
+        QResource::unregisterResource(extraRcc);
+    });
+
+    const QString extraDataRoot(QStringLiteral(":/vnotex/data/extra"));
+
+#ifdef VX_DEBUG_WEB
+    if (!needUpdate) {
+        // Always update main config file and web folder.
+        qDebug() << "forced to update main config file and web folder for debugging";
+        splash->showMessage("update main config file and web folder for debugging");
+
+        // Cancel the read-only permission of the main config file.
+        QFile::setPermissions(mainConfigFilePath, QFile::WriteUser);
+        FileUtils::removeFile(mainConfigFilePath);
+        FileUtils::removeDir(appConfigDir.filePath(QStringLiteral("web")));
+
+        // Wait for the OS delete the folder.
+        Utils::sleepWait(1000);
+
+        FileUtils::copyFile(getConfigFilePath(Source::Default), mainConfigFilePath);
+        FileUtils::copyDir(extraDataRoot + QStringLiteral("/web"),
+                           appConfigDir.filePath(QStringLiteral("web")));
+        return;
+    }
+#else
+    Q_ASSERT(needUpdate);
+#endif
+
+    // Copy themes.
+    qApp->processEvents();
+    splash->showMessage("Copying themes");
+    FileUtils::copyDir(extraDataRoot + QStringLiteral("/themes"),
+                       appConfigDir.filePath(QStringLiteral("themes")));
+
+    // Copy docs.
+    qApp->processEvents();
+    splash->showMessage("Copying docs");
+    FileUtils::copyDir(extraDataRoot + QStringLiteral("/docs"),
+                       appConfigDir.filePath(QStringLiteral("docs")));
+
+    // Copy syntax-highlighting.
+    qApp->processEvents();
+    splash->showMessage("Copying syntax-highlighting");
+    FileUtils::copyDir(extraDataRoot + QStringLiteral("/syntax-highlighting"),
+                       appConfigDir.filePath(QStringLiteral("syntax-highlighting")));
+
+    // Copy web.
+    qApp->processEvents();
+    splash->showMessage("Copying web");
+    FileUtils::copyDir(extraDataRoot + QStringLiteral("/web"),
+                       appConfigDir.filePath(QStringLiteral("web")));
+
+    // Main config file.
+    FileUtils::copyFile(getConfigFilePath(Source::Default), appConfigDir.filePath(c_configFileName));
+
+}
+
+QString ConfigMgr::getConfigFilePath(Source p_src) const
+{
+    QString configPath;
+    switch (p_src) {
+    case Source::Default:
+        configPath = QStringLiteral(":/vnotex/data/core/") + c_configFileName;
+        break;
+
+    case Source::App:
+        configPath = m_appConfigFolderPath + QLatin1Char('/') + c_configFileName;
+        break;
+
+    case Source::User:
+    {
+        configPath = m_userConfigFolderPath + QLatin1Char('/') + c_configFileName;
+        break;
+    }
+
+    case Source::Session:
+    {
+        configPath = m_userConfigFolderPath + QLatin1Char('/') + c_sessionFileName;
+        break;
+    }
+
+    default:
+        Q_ASSERT(false);
+    }
+
+    return configPath;
+}
+
+QSharedPointer<ConfigMgr::Settings> ConfigMgr::getSettings(Source p_src) const
+{
+
+    return ConfigMgr::Settings::fromFile(getConfigFilePath(p_src));
+}
+
+void ConfigMgr::writeUserSettings(const QJsonObject &p_jobj)
+{
+    Settings settings(p_jobj);
+    settings.writeToFile(getConfigFilePath(Source::User));
+}
+
+void ConfigMgr::writeSessionSettings(const QJsonObject &p_jobj)
+{
+    Settings settings(p_jobj);
+    settings.writeToFile(getConfigFilePath(Source::Session));
+}
+
+MainConfig &ConfigMgr::getConfig()
+{
+    return *m_config;
+}
+
+SessionConfig &ConfigMgr::getSessionConfig()
+{
+    return *m_sessionConfig;
+}
+
+CoreConfig &ConfigMgr::getCoreConfig()
+{
+    return m_config->getCoreConfig();
+}
+
+EditorConfig &ConfigMgr::getEditorConfig()
+{
+    return m_config->getEditorConfig();
+}
+
+WidgetConfig &ConfigMgr::getWidgetConfig()
+{
+    return m_config->getWidgetConfig();
+}
+
+QString ConfigMgr::getAppFolder() const
+{
+    return m_appConfigFolderPath;
+}
+
+QString ConfigMgr::getUserFolder() const
+{
+    return m_userConfigFolderPath;
+}
+
+QString ConfigMgr::getAppThemeFolder() const
+{
+    return PathUtils::concatenateFilePath(m_appConfigFolderPath, QStringLiteral("themes"));
+}
+
+QString ConfigMgr::getUserThemeFolder() const
+{
+    return PathUtils::concatenateFilePath(m_userConfigFolderPath, QStringLiteral("themes"));
+}
+
+QString ConfigMgr::getAppDocsFolder() const
+{
+    return PathUtils::concatenateFilePath(m_appConfigFolderPath, QStringLiteral("docs"));
+}
+
+QString ConfigMgr::getUserDocsFolder() const
+{
+    return PathUtils::concatenateFilePath(m_userConfigFolderPath, QStringLiteral("docs"));
+}
+
+QString ConfigMgr::getAppSyntaxHighlightingFolder() const
+{
+    return PathUtils::concatenateFilePath(m_appConfigFolderPath,
+                                          QStringLiteral("syntax-highlighting"));
+}
+
+QString ConfigMgr::getUserSyntaxHighlightingFolder() const
+{
+    return PathUtils::concatenateFilePath(m_userConfigFolderPath,
+                                          QStringLiteral("syntax-highlighting"));
+}
+
+QString ConfigMgr::getUserOrAppFile(const QString &p_filePath) const
+{
+    QFileInfo fi(p_filePath);
+    if (fi.isAbsolute()) {
+        return p_filePath;
+    }
+
+    // Check user folder first.
+    QDir userConfigDir(m_userConfigFolderPath);
+    if (userConfigDir.exists(p_filePath)) {
+        return userConfigDir.absoluteFilePath(p_filePath);
+    }
+
+    // App folder.
+    QDir appConfigDir(m_appConfigFolderPath);
+    return appConfigDir.absoluteFilePath(p_filePath);
+}
+
+QString ConfigMgr::locateSessionConfigFilePathAtBootstrap()
+{
+    // QApplication is not init yet, so org and app name are empty here.
+    auto folderPath = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation);
+    folderPath = PathUtils::concatenateFilePath(folderPath, c_orgName + "/" + c_appName);
+    QDir dir(folderPath);
+    if (dir.exists(c_sessionFileName)) {
+        qInfo() << "locateSessionConfigFilePathAtBootstrap" << folderPath;
+        return dir.filePath(c_sessionFileName);
+    }
+
+    return QString();
+}
+
+QString ConfigMgr::getLogFile() const
+{
+    return PathUtils::concatenateFilePath(ConfigMgr::getInst().getUserFolder(), "vnotex.log");
+}

+ 141 - 0
src/core/configmgr.h

@@ -0,0 +1,141 @@
+#ifndef CONFIGMGR_H
+#define CONFIGMGR_H
+
+#include <QObject>
+#include <QSharedPointer>
+#include <QJsonObject>
+#include <QScopedPointer>
+
+namespace vnotex
+{
+    class MainConfig;
+    class SessionConfig;
+    class CoreConfig;
+    class EditorConfig;
+    class WidgetConfig;
+
+    class ConfigMgr : public QObject
+    {
+        Q_OBJECT
+    public:
+        enum class Source
+        {
+            Default,
+            App,
+            User,
+            Session
+        };
+
+        class Settings
+        {
+        public:
+            Settings() = default;
+
+            Settings(const QJsonObject &p_jobj)
+                : m_jobj(p_jobj)
+            {
+            }
+
+            const QJsonObject &getJson() const;
+
+            void writeToFile(const QString &p_jsonFilePath) const;
+
+            static QSharedPointer<Settings> fromFile(const QString &p_jsonFilePath);
+
+        private:
+            QJsonObject m_jobj;
+        };
+
+        static ConfigMgr &getInst()
+        {
+            static ConfigMgr inst;
+            return inst;
+        }
+
+        ~ConfigMgr();
+
+        ConfigMgr(const ConfigMgr &) = delete;
+        void operator=(const ConfigMgr &) = delete;
+
+        MainConfig &getConfig();
+
+        SessionConfig &getSessionConfig();
+
+        CoreConfig &getCoreConfig();
+
+        EditorConfig &getEditorConfig();
+
+        WidgetConfig &getWidgetConfig();
+
+        QString getAppFolder() const;
+
+        QString getUserFolder() const;
+
+        QString getLogFile() const;
+
+        QString getAppThemeFolder() const;
+
+        QString getUserThemeFolder() const;
+
+        QString getAppDocsFolder() const;
+
+        QString getUserDocsFolder() const;
+
+        QString getAppSyntaxHighlightingFolder() const;
+
+        QString getUserSyntaxHighlightingFolder() const;
+
+        // If @p_filePath is absolute, just return it.
+        // Otherwise, first try to find it in user folder, then in app folder.
+        QString getUserOrAppFile(const QString &p_filePath) const;
+
+        QString getConfigFilePath(Source p_src) const;
+
+        // Called at boostrap without QApplication instance.
+        static QString locateSessionConfigFilePathAtBootstrap();
+
+        static const QString c_orgName;
+
+        static const QString c_appName;
+
+    public:
+        // Used by IConfig.
+        QSharedPointer<Settings> getSettings(Source p_src) const;
+
+        void writeUserSettings(const QJsonObject &p_jobj);
+
+        void writeSessionSettings(const QJsonObject &p_jobj);
+
+    signals:
+        void editorConfigChanged();
+
+    private:
+        explicit ConfigMgr(QObject *p_parent = nullptr);
+
+        // Locate the folder path where the config file exists.
+        void locateConfigFolder();
+
+        // Check if app config exists and is updated.
+        // Update it if in need.
+        void checkAppConfig();
+
+        QScopedPointer<MainConfig> m_config;;
+
+        // Session config.
+        QScopedPointer<SessionConfig> m_sessionConfig;
+
+        // Absolute path of the app config folder.
+        QString m_appConfigFolderPath;
+
+        // Absolute path of the user config folder.
+        QString m_userConfigFolderPath;
+
+        // Name of the core config file.
+        static const QString c_configFileName;
+
+        // Name of the session config file.
+        static const QString c_sessionFileName;
+    };
+} // ns vnotex
+
+#endif // CONFIGMGR_H

+ 57 - 0
src/core/core.pri

@@ -0,0 +1,57 @@
+INCLUDEPATH *= $$PWD
+
+include($$PWD/notebookbackend/notebookbackend.pri)
+
+include($$PWD/versioncontroller/versioncontroller.pri)
+
+include($$PWD/notebookconfigmgr/notebookconfigmgr.pri)
+
+include($$PWD/notebook/notebook.pri)
+
+include($$PWD/buffer/buffer.pri)
+
+SOURCES += \
+    $$PWD/buffermgr.cpp \
+    $$PWD/configmgr.cpp \
+    $$PWD/coreconfig.cpp \
+    $$PWD/editorconfig.cpp \
+    $$PWD/htmltemplatehelper.cpp \
+    $$PWD/logger.cpp \
+    $$PWD/mainconfig.cpp \
+    $$PWD/markdowneditorconfig.cpp \
+    $$PWD/singleinstanceguard.cpp \
+    $$PWD/texteditorconfig.cpp \
+    $$PWD/vnotex.cpp \
+    $$PWD/thememgr.cpp \
+    $$PWD/notebookmgr.cpp \
+    $$PWD/theme.cpp \
+    $$PWD/sessionconfig.cpp \
+    $$PWD/clipboarddata.cpp \
+    $$PWD/widgetconfig.cpp
+
+HEADERS += \
+    $$PWD/ViewerResource.h \
+    $$PWD/buffermgr.h \
+    $$PWD/configmgr.h \
+    $$PWD/coreconfig.h \
+    $$PWD/editorconfig.h \
+    $$PWD/events.h \
+    $$PWD/filelocator.h \
+    $$PWD/fileopenparameters.h \
+    $$PWD/htmltemplatehelper.h \
+    $$PWD/logger.h \
+    $$PWD/mainconfig.h \
+    $$PWD/markdowneditorconfig.h \
+    $$PWD/singleinstanceguard.h \
+    $$PWD/iconfig.h \
+    $$PWD/texteditorconfig.h \
+    $$PWD/vnotex.h \
+    $$PWD/thememgr.h \
+    $$PWD/global.h \
+    $$PWD/namebasedserver.h \
+    $$PWD/exception.h \
+    $$PWD/notebookmgr.h \
+    $$PWD/theme.h \
+    $$PWD/sessionconfig.h \
+    $$PWD/clipboarddata.h \
+    $$PWD/widgetconfig.h

+ 124 - 0
src/core/coreconfig.cpp

@@ -0,0 +1,124 @@
+#include "coreconfig.h"
+
+#include <QMetaEnum>
+#include <QLocale>
+
+using namespace vnotex;
+
+#define READSTR(key) readString(appObj, userObj, (key))
+#define READINT(key) readInt(appObj, userObj, (key))
+
+QStringList CoreConfig::s_availableLocales;
+
+CoreConfig::CoreConfig(ConfigMgr *p_mgr, IConfig *p_topConfig)
+    : IConfig(p_mgr, p_topConfig)
+{
+    m_sessionName = QStringLiteral("core");
+}
+
+const QString &CoreConfig::getTheme() const
+{
+    return m_theme;
+}
+
+void CoreConfig::init(const QJsonObject &p_app,
+                      const QJsonObject &p_user)
+{
+    const auto appObj = p_app.value(m_sessionName).toObject();
+    const auto userObj = p_user.value(m_sessionName).toObject();
+
+    m_theme = READSTR(QStringLiteral("theme"));
+
+    m_locale = READSTR(QStringLiteral("locale"));
+    if (!m_locale.isEmpty() && !getAvailableLocales().contains(m_locale)) {
+        m_locale = QStringLiteral("en_US");
+    }
+
+    loadShortcuts(appObj, userObj);
+
+    m_toolBarIconSize = READINT(QStringLiteral("toolbar_icon_size"));
+    if (m_toolBarIconSize <= 0) {
+        m_toolBarIconSize = 16;
+    }
+}
+
+QJsonObject CoreConfig::toJson() const
+{
+    QJsonObject obj;
+    obj[QStringLiteral("theme")] = m_theme;
+    obj[QStringLiteral("locale")] = m_locale;
+    obj[QStringLiteral("shortcuts")] = saveShortcuts();
+    obj[QStringLiteral("toolbar_icon_size")] = m_toolBarIconSize;
+    return obj;
+}
+
+const QString &CoreConfig::getLocale() const
+{
+    return m_locale;
+}
+
+void CoreConfig::setLocale(const QString &p_locale)
+{
+    updateConfig(m_locale,
+                 p_locale,
+                 this);
+}
+
+QString CoreConfig::getLocaleToUse() const
+{
+    return QLocale().name();
+}
+
+const QStringList &CoreConfig::getAvailableLocales()
+{
+    if (s_availableLocales.isEmpty()) {
+        s_availableLocales << QStringLiteral("en_US");
+        s_availableLocales << QStringLiteral("zh_CN");
+    }
+
+    return s_availableLocales;
+}
+
+void CoreConfig::loadShortcuts(const QJsonObject &p_app, const QJsonObject &p_user)
+{
+    const auto appObj = p_app.value(QStringLiteral("shortcuts")).toObject();
+    const auto userObj = p_user.value(QStringLiteral("shortcuts")).toObject();
+
+    static const auto indexOfShortcutEnum = CoreConfig::staticMetaObject.indexOfEnumerator("Shortcut");
+    Q_ASSERT(indexOfShortcutEnum >= 0);
+    const auto metaEnum = CoreConfig::staticMetaObject.enumerator(indexOfShortcutEnum);
+    // Skip the Max flag.
+    for (int i = 0; i < metaEnum.keyCount() - 1; ++i) {
+        m_shortcuts[i] = READSTR(metaEnum.key(i));
+    }
+}
+
+QJsonObject CoreConfig::saveShortcuts() const
+{
+    QJsonObject obj;
+    static const auto indexOfShortcutEnum = CoreConfig::staticMetaObject.indexOfEnumerator("Shortcut");
+    Q_ASSERT(indexOfShortcutEnum >= 0);
+    const auto metaEnum = CoreConfig::staticMetaObject.enumerator(indexOfShortcutEnum);
+    // Skip the Max flag.
+    for (int i = 0; i < metaEnum.keyCount() - 1; ++i) {
+        obj[metaEnum.key(i)] = m_shortcuts[i];
+    }
+    return obj;
+}
+
+const QString &CoreConfig::getShortcut(Shortcut p_shortcut) const
+{
+    Q_ASSERT(p_shortcut < Shortcut::MaxShortcut);
+    return m_shortcuts[p_shortcut];
+}
+
+int CoreConfig::getToolBarIconSize() const
+{
+    return m_toolBarIconSize;
+}
+
+void CoreConfig::setToolBarIconSize(int p_size)
+{
+    Q_ASSERT(p_size > 0);
+    updateConfig(m_toolBarIconSize, p_size, this);
+}

+ 73 - 0
src/core/coreconfig.h

@@ -0,0 +1,73 @@
+#ifndef CORECONFIG_H
+#define CORECONFIG_H
+
+#include "iconfig.h"
+
+#include <QtGlobal>
+#include <QString>
+#include <QStringList>
+
+namespace vnotex
+{
+    class CoreConfig : public IConfig
+    {
+        Q_GADGET
+    public:
+        enum Shortcut
+        {
+            FullScreen,
+            ExpandContentArea,
+            Settings,
+            NewNote,
+            CloseTab,
+            NavigationDock,
+            OutlineDock,
+            NavigationMode,
+            LocateNode,
+            MaxShortcut
+        };
+        Q_ENUM(Shortcut)
+
+        CoreConfig(ConfigMgr *p_mgr, IConfig *p_topConfig);
+
+        void init(const QJsonObject &p_app, const QJsonObject &p_user) Q_DECL_OVERRIDE;
+
+        QJsonObject toJson() const Q_DECL_OVERRIDE;
+
+        const QString &getTheme() const;
+
+        const QString &getLocale() const;
+        void setLocale(const QString &p_locale);
+
+        // Should be called after locale is properly set.
+        QString getLocaleToUse() const;
+
+        const QString &getShortcut(Shortcut p_shortcut) const;
+
+        int getToolBarIconSize() const;
+        void setToolBarIconSize(int p_size);
+
+        static const QStringList &getAvailableLocales();
+
+    private:
+        void loadShortcuts(const QJsonObject &p_app, const QJsonObject &p_user);
+
+        QJsonObject saveShortcuts() const;
+
+        // Theme name.
+        QString m_theme;
+
+        // User-specified locale, such as zh_CN, en_US.
+        // Empty if not specified.
+        QString m_locale;
+
+        QString m_shortcuts[Shortcut::MaxShortcut];
+
+        // Icon size of MainWindow tool bar.
+        int m_toolBarIconSize = 16;
+
+        static QStringList s_availableLocales;
+    };
+} // ns vnotex
+
+#endif // CORECONFIG_H

+ 184 - 0
src/core/editorconfig.cpp

@@ -0,0 +1,184 @@
+#include "editorconfig.h"
+
+#include <QMetaEnum>
+#include <QDebug>
+
+#include "texteditorconfig.h"
+#include "markdowneditorconfig.h"
+
+using namespace vnotex;
+
+#define READINT(key) readInt(appObj, userObj, (key))
+#define READSTR(key) readString(appObj, userObj, (key))
+
+EditorConfig::EditorConfig(ConfigMgr *p_mgr, IConfig *p_topConfig)
+    : IConfig(p_mgr, p_topConfig),
+      m_textEditorConfig(new TextEditorConfig(p_mgr, p_topConfig)),
+      m_markdownEditorConfig(new MarkdownEditorConfig(p_mgr, p_topConfig, m_textEditorConfig))
+{
+    m_sessionName = QStringLiteral("editor");
+}
+
+EditorConfig::~EditorConfig()
+{
+}
+
+void EditorConfig::init(const QJsonObject &p_app,
+                        const QJsonObject &p_user)
+{
+    const auto appObj = p_app.value(m_sessionName).toObject();
+    const auto userObj = p_user.value(m_sessionName).toObject();
+
+    loadCore(appObj, userObj);
+
+    m_textEditorConfig->init(appObj, userObj);
+    m_markdownEditorConfig->init(appObj, userObj);
+}
+
+void EditorConfig::loadCore(const QJsonObject &p_app, const QJsonObject &p_user)
+{
+    const auto appObj = p_app.value(QStringLiteral("core")).toObject();
+    const auto userObj = p_user.value(QStringLiteral("core")).toObject();
+
+    {
+        m_toolBarIconSize = READINT(QStringLiteral("toolbar_icon_size"));
+        if (m_toolBarIconSize <= 0) {
+            m_toolBarIconSize = 14;
+        }
+    }
+
+    {
+        auto autoSavePolicy = READSTR(QStringLiteral("auto_save_policy"));
+        m_autoSavePolicy = stringToAutoSavePolicy(autoSavePolicy);
+    }
+
+    m_backupFileDirectory = READSTR(QStringLiteral("backup_file_directory"));
+
+    m_backupFileExtension = READSTR(QStringLiteral("backup_file_extension"));
+
+    loadShortcuts(appObj, userObj);
+}
+
+QJsonObject EditorConfig::saveCore() const
+{
+    QJsonObject obj;
+    obj[QStringLiteral("toolbar_icon_size")] = m_toolBarIconSize;
+    obj[QStringLiteral("auto_save_policy")] = autoSavePolicyToString(m_autoSavePolicy);
+    obj[QStringLiteral("backup_file_directory")] = m_backupFileDirectory;
+    obj[QStringLiteral("backup_file_extension")] = m_backupFileExtension;
+    obj[QStringLiteral("shortcuts")] = saveShortcuts();
+    return obj;
+}
+
+void EditorConfig::loadShortcuts(const QJsonObject &p_app, const QJsonObject &p_user)
+{
+    const auto appObj = p_app.value(QStringLiteral("shortcuts")).toObject();
+    const auto userObj = p_user.value(QStringLiteral("shortcuts")).toObject();
+
+    static const auto indexOfShortcutEnum = EditorConfig::staticMetaObject.indexOfEnumerator("Shortcut");
+    Q_ASSERT(indexOfShortcutEnum >= 0);
+    const auto metaEnum = EditorConfig::staticMetaObject.enumerator(indexOfShortcutEnum);
+    // Skip the Max flag.
+    for (int i = 0; i < metaEnum.keyCount() - 1; ++i) {
+        m_shortcuts[i] = READSTR(metaEnum.key(i));
+    }
+}
+
+QJsonObject EditorConfig::saveShortcuts() const
+{
+    QJsonObject obj;
+    static const auto indexOfShortcutEnum = EditorConfig::staticMetaObject.indexOfEnumerator("Shortcut");
+    Q_ASSERT(indexOfShortcutEnum >= 0);
+    const auto metaEnum = EditorConfig::staticMetaObject.enumerator(indexOfShortcutEnum);
+    // Skip the Max flag.
+    for (int i = 0; i < metaEnum.keyCount() - 1; ++i) {
+        obj[metaEnum.key(i)] = m_shortcuts[i];
+    }
+    return obj;
+}
+
+QJsonObject EditorConfig::toJson() const
+{
+    QJsonObject obj;
+    obj[m_textEditorConfig->getSessionName()] = m_textEditorConfig->toJson();
+    obj[m_markdownEditorConfig->getSessionName()] = m_markdownEditorConfig->toJson();
+    obj[QStringLiteral("core")] = saveCore();
+    return obj;
+}
+
+TextEditorConfig &EditorConfig::getTextEditorConfig()
+{
+    return *m_textEditorConfig;
+}
+
+const TextEditorConfig &EditorConfig::getTextEditorConfig() const
+{
+    return *m_textEditorConfig;
+}
+
+MarkdownEditorConfig &EditorConfig::getMarkdownEditorConfig()
+{
+    return *m_markdownEditorConfig;
+}
+
+const MarkdownEditorConfig &EditorConfig::getMarkdownEditorConfig() const
+{
+    return *m_markdownEditorConfig;
+}
+
+int EditorConfig::getToolBarIconSize() const
+{
+    return m_toolBarIconSize;
+}
+
+const QString &EditorConfig::getShortcut(Shortcut p_shortcut) const
+{
+    Q_ASSERT(p_shortcut < Shortcut::MaxShortcut);
+    return m_shortcuts[p_shortcut];
+}
+
+QString EditorConfig::autoSavePolicyToString(AutoSavePolicy p_policy) const
+{
+    switch (p_policy) {
+    case AutoSavePolicy::None:
+        return QStringLiteral("none");
+
+    case AutoSavePolicy::AutoSave:
+        return QStringLiteral("autosave");
+
+    default:
+        return QStringLiteral("backupfile");
+    }
+}
+
+EditorConfig::AutoSavePolicy EditorConfig::stringToAutoSavePolicy(const QString &p_str) const
+{
+    auto policy = p_str.toLower();
+    if (policy == QStringLiteral("none")) {
+        return AutoSavePolicy::None;
+    } else if (policy == QStringLiteral("autosave")) {
+        return AutoSavePolicy::AutoSave;
+    } else {
+        return AutoSavePolicy::BackupFile;
+    }
+}
+
+EditorConfig::AutoSavePolicy EditorConfig::getAutoSavePolicy() const
+{
+    return m_autoSavePolicy;
+}
+
+void EditorConfig::setAutoSavePolicy(EditorConfig::AutoSavePolicy p_policy)
+{
+    updateConfig(m_autoSavePolicy, p_policy, this);
+}
+
+const QString &EditorConfig::getBackupFileDirectory() const
+{
+    return m_backupFileDirectory;
+}
+
+const QString &EditorConfig::getBackupFileExtension() const
+{
+    return m_backupFileExtension;
+}

+ 118 - 0
src/core/editorconfig.h

@@ -0,0 +1,118 @@
+#ifndef VNOTEX_EDITORCONFIG_H
+#define VNOTEX_EDITORCONFIG_H
+
+#include "iconfig.h"
+
+#include <QScopedPointer>
+#include <QSharedPointer>
+#include <QObject>
+
+namespace vnotex
+{
+    class TextEditorConfig;
+    class MarkdownEditorConfig;
+
+    class EditorConfig : public IConfig
+    {
+        Q_GADGET
+    public:
+        enum Shortcut
+        {
+            Save,
+            EditRead,
+            Discard,
+            TypeHeading1,
+            TypeHeading2,
+            TypeHeading3,
+            TypeHeading4,
+            TypeHeading5,
+            TypeHeading6,
+            TypeHeadingNone,
+            TypeBold,
+            TypeItalic,
+            TypeStrikethrough,
+            TypeUnorderedList,
+            TypeOrderedList,
+            TypeTodoList,
+            TypeCheckedTodoList,
+            TypeCode,
+            TypeCodeBlock,
+            TypeMath,
+            TypeMathBlock,
+            TypeQuote,
+            TypeLink,
+            TypeImage,
+            TypeTable,
+            Outline,
+            RichPaste,
+            FindAndReplace,
+            MaxShortcut
+        };
+        Q_ENUM(Shortcut)
+
+        enum AutoSavePolicy
+        {
+            None,
+            AutoSave,
+            BackupFile
+        };
+        Q_ENUM(AutoSavePolicy)
+
+        EditorConfig(ConfigMgr *p_mgr, IConfig *p_topConfig);
+
+        ~EditorConfig();
+
+        TextEditorConfig &getTextEditorConfig();
+        const TextEditorConfig &getTextEditorConfig() const;
+
+        MarkdownEditorConfig &getMarkdownEditorConfig();
+        const MarkdownEditorConfig &getMarkdownEditorConfig() const;
+
+        void init(const QJsonObject &p_app, const QJsonObject &p_user) Q_DECL_OVERRIDE;
+
+        QJsonObject toJson() const Q_DECL_OVERRIDE;
+
+        int getToolBarIconSize() const;
+
+        EditorConfig::AutoSavePolicy getAutoSavePolicy() const;
+        void setAutoSavePolicy(EditorConfig::AutoSavePolicy p_policy);
+
+        const QString &getBackupFileDirectory() const;
+
+        const QString &getBackupFileExtension() const;
+
+        const QString &getShortcut(Shortcut p_shortcut) const;
+
+    private:
+        void loadCore(const QJsonObject &p_app, const QJsonObject &p_user);
+
+        QJsonObject saveCore() const;
+
+        void loadShortcuts(const QJsonObject &p_app, const QJsonObject &p_user);
+
+        QJsonObject saveShortcuts() const;
+
+        QString autoSavePolicyToString(AutoSavePolicy p_policy) const;
+        AutoSavePolicy stringToAutoSavePolicy(const QString &p_str) const;
+
+        // Icon size of editor tool bar.
+        int m_toolBarIconSize = 14;
+
+        QString m_shortcuts[Shortcut::MaxShortcut];
+
+        AutoSavePolicy m_autoSavePolicy = AutoSavePolicy::AutoSave;
+
+        // Where to put backup file, relative to the content file itself.
+        QString m_backupFileDirectory;
+
+        // Backup file extension.
+        QString m_backupFileExtension;
+
+        // Will be shared with MarkdownEditorConfig.
+        QSharedPointer<TextEditorConfig> m_textEditorConfig;
+
+        QScopedPointer<MarkdownEditorConfig> m_markdownEditorConfig;
+    };
+}
+
+#endif // EDITORCONFIG_H

+ 26 - 0
src/core/events.h

@@ -0,0 +1,26 @@
+#ifndef EVENTS_H
+#define EVENTS_H
+
+#include <QVariant>
+
+namespace vnotex
+{
+    class Event
+    {
+    public:
+        void reset()
+        {
+            m_handled = false;
+            m_response.clear();
+        }
+
+        // Whether this event is handled.
+        // If it is handled, later handler should just ignore this event.
+        bool m_handled = false;
+
+        // Handler could use this field to return state to the event sender.
+        QVariant m_response = true;
+    };
+}
+
+#endif // EVENTS_H

+ 89 - 0
src/core/exception.h

@@ -0,0 +1,89 @@
+#ifndef EXCEPTION_H
+#define EXCEPTION_H
+
+#include <stdexcept>
+#include <QString>
+#include <QDebug>
+
+namespace vnotex
+{
+    class Exception : virtual public std::runtime_error
+    {
+    public:
+        enum class Type
+        {
+            InvalidPath,
+            FailToCreateDir,
+            FailToWriteFile,
+            FailToReadFile,
+            FailToRenameFile,
+            FailToCopyFile,
+            FailToCopyDir,
+            FailToRemoveFile,
+            FailToRemoveDir,
+            FileMissingOnDisk,
+            EssentialFileMissing,
+            InvalidArgument
+        };
+
+        Exception(Type p_type, const QString &p_what)
+            : std::runtime_error(p_what.toStdString()),
+              m_type(p_type)
+        {
+        }
+
+        Type m_type;
+
+        [[noreturn]] static void throwOne(Exception::Type p_type, const QString &p_what)
+        {
+            qCritical() << typeToString(p_type) << p_what;
+            throw Exception(p_type, p_what);
+        }
+
+    private:
+        static QString typeToString(Exception::Type p_type)
+        {
+            switch (p_type) {
+            case Type::InvalidPath:
+                return QString("InvalidPath");
+
+            case Type::FailToCreateDir:
+                return QString("FailToCreateDir");
+
+            case Type::FailToWriteFile:
+                return QString("FailToWriteFile");
+
+            case Type::FailToReadFile:
+                return QString("FailToReadFile");
+
+            case Type::FailToRenameFile:
+                return QString("FailToRenameFile");
+
+            case Type::FailToCopyFile:
+                return QString("FailToCopyFile");
+
+            case Type::FailToCopyDir:
+                return QString("FailToCopyDir");
+
+            case Type::FailToRemoveFile:
+                return QString("FailToRemoveFile");
+
+            case Type::FailToRemoveDir:
+                return QString("FailToRemoveDir");
+
+            case Type::FileMissingOnDisk:
+                return QString("FileMissingOnDisk");
+
+            case Type::EssentialFileMissing:
+                return QString("EssentialFileMissing");
+
+            case Type::InvalidArgument:
+                return QString("InvalidArgument");
+            }
+
+            return QString::number(static_cast<int>(p_type));
+        }
+    };
+} // ns vnotex
+
+#endif // EXCEPTION_H

+ 48 - 0
src/core/filelocator.h

@@ -0,0 +1,48 @@
+#ifndef FILELOCATOR_H
+#define FILELOCATOR_H
+
+#include <QString>
+
+namespace vnotex
+{
+    class Node;
+
+    // A unique locator for both internal Node and external file.
+    class FileLocator
+    {
+    public:
+        FileLocator(Node *p_node)
+            : m_node(p_node)
+        {
+        }
+
+        FileLocator(const QString &p_filePath)
+            : m_filePath(p_filePath)
+        {
+        }
+
+        bool isNode() const
+        {
+            return m_node;
+        }
+
+        Node *node() const
+        {
+            Q_ASSERT(isNode());
+            return m_node;
+        }
+
+        const QString &filePath() const
+        {
+            Q_ASSERT(!isNode());
+            return m_filePath;
+        }
+
+    private:
+        Node *m_node = nullptr;
+
+        QString m_filePath;
+    };
+}
+
+#endif // FILELOCATOR_H

+ 35 - 0
src/core/fileopenparameters.h

@@ -0,0 +1,35 @@
+#ifndef FILEOPENPARAMETERS_H
+#define FILEOPENPARAMETERS_H
+
+namespace vnotex
+{
+    class Node;
+
+    struct FileOpenParameters
+    {
+        // Some modes may be not supported by some editors.
+        enum Mode
+        {
+            Read,
+            Edit,
+            FullPreview,
+            FocusPreview
+        };
+
+        Mode m_mode = Mode::Read;
+
+        // Whether focus to the opened window.
+        bool m_focus = true;
+
+        // Whether it is a new file.
+        bool m_newFile = false;
+
+        // If this file is an attachment of a node, this field indicates it.
+        Node *m_nodeAttachedTo = nullptr;
+
+        // Open as read-only.
+        bool m_readOnly = false;
+    };
+}
+
+#endif // FILEOPENPARAMETERS_H

+ 78 - 0
src/core/global.h

@@ -0,0 +1,78 @@
+#ifndef VNOTEX_GLOBAL_H
+#define VNOTEX_GLOBAL_H
+
+#include <QString>
+#include <QPair>
+#include <QDebug>
+#include <QJsonObject>
+
+namespace vnotex
+{
+    typedef quint64 ID;
+
+    static QPair<bool, ID> stringToID(const QString &p_str)
+    {
+        bool ok;
+        ID id = p_str.toULongLong(&ok);
+        return qMakePair(ok, id);
+    }
+
+    static QString IDToString(ID p_id)
+    {
+        return QString::number(p_id);
+    }
+
+    typedef quint64 TimeStamp;
+
+    struct Info
+    {
+        Info(const QString &p_name, const QString &p_displayName, const QString &p_description)
+            : m_name(p_name), m_displayName(p_displayName), m_description(p_description)
+        {
+        }
+
+        // Name for identification.
+        QString m_name;
+
+        // User-visible name.
+        QString m_displayName;
+
+        QString m_description;
+    };
+
+    enum { CONTENTS_MARGIN = 2 };
+
+    static QString QJsonObjectToString(const QJsonObject &p_obj) {
+        QString str = "{";
+
+        auto keys = p_obj.keys();
+        for (auto &key : keys) {
+            str += "\"" + key + "\": \"" + p_obj.value(key).toString() + "\";";
+        }
+
+        str += "}";
+        return str;
+    }
+
+    static QDebug operator<<(QDebug p_debug, const QJsonObject &p_obj)
+    {
+        QDebugStateSaver saver(p_debug);
+        p_debug << QJsonObjectToString(p_obj);
+        return p_debug;
+    }
+
+    enum FindOption
+    {
+        None = 0,
+        CaseSensitive = 0x1U,
+        WholeWordOnly = 0x2U,
+        RegularExpression = 0x4U,
+        IncrementalSearch = 0x8U
+    };
+    Q_DECLARE_FLAGS(FindOptions, FindOption);
+
+} // ns vnotex
+
+Q_DECLARE_OPERATORS_FOR_FLAGS(vnotex::FindOptions);
+
+#endif // GLOBAL_H

+ 161 - 0
src/core/htmltemplatehelper.cpp

@@ -0,0 +1,161 @@
+#include "htmltemplatehelper.h"
+
+#include <QDebug>
+
+#include <core/markdowneditorconfig.h>
+#include <core/configmgr.h>
+#include <utils/fileutils.h>
+#include <utils/pathutils.h>
+#include <core/thememgr.h>
+#include <core/vnotex.h>
+
+using namespace vnotex;
+
+HtmlTemplateHelper::Template HtmlTemplateHelper::s_markdownViewerTemplate;
+
+QString WebGlobalOptions::toJavascriptObject() const
+{
+    return QStringLiteral("window.vxOptions = {\n")
+           + QString("webPlantUml: %1,\n").arg(boolToString(m_webPlantUml))
+           + QString("webGraphviz: %1,\n").arg(boolToString(m_webGraphviz))
+           + QString("constrainImageWidthEnabled: %1,\n").arg(boolToString(m_constrainImageWidthEnabled))
+           + QString("protectFromXss: %1,\n").arg(boolToString(m_protectFromXss))
+           + QString("sectionNumberEnabled: %1\n").arg(boolToString(m_sectionNumberEnabled))
+           + 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)
+{
+    QString styles;
+    for (const auto &ele : p_resource.m_resources) {
+        if (isGlobalStyles(ele)) {
+            if (ele.m_enabled) {
+                for (const auto &style : ele.m_styles) {
+                    // Read the style file content.
+                    auto styleFile = ConfigMgr::getInst().getUserOrAppFile(style);
+                    styles += FileUtils::readTextFile(styleFile);
+                }
+            }
+            break;
+        }
+    }
+
+    if (!styles.isEmpty()) {
+        p_template.replace(QStringLiteral("/* VX_GLOBAL_STYLES_PLACEHOLDER */"),
+                           styles);
+    }
+}
+
+static QString fillStyleTag(const QString &p_styleFile)
+{
+    if (p_styleFile.isEmpty()) {
+        return "";
+    }
+    auto url = PathUtils::pathToUrl(p_styleFile);
+    return QString("<link rel=\"stylesheet\" type=\"text/css\" href=\"%1\">\n").arg(url.toString());
+}
+
+static QString fillScriptTag(const QString &p_scriptFile)
+{
+    if (p_scriptFile.isEmpty()) {
+        return "";
+    }
+    auto url = PathUtils::pathToUrl(p_scriptFile);
+    return QString("<script type=\"text/javascript\" src=\"%1\"></script>\n").arg(url.toString());
+}
+
+static void fillThemeStyles(QString &p_template)
+{
+    QString styles;
+    const auto &themeMgr = VNoteX::getInst().getThemeMgr();
+
+    styles += fillStyleTag(themeMgr.getFile(Theme::File::WebStyleSheet));
+    styles += fillStyleTag(themeMgr.getFile(Theme::File::HighlightStyleSheet));
+
+    if (!styles.isEmpty()) {
+        p_template.replace(QStringLiteral("<!-- VX_THEME_STYLES_PLACEHOLDER -->"),
+                           styles);
+    }
+}
+
+static void fillGlobalOptions(QString &p_template, const WebGlobalOptions &p_opts)
+{
+    p_template.replace(QStringLiteral("/* VX_GLOBAL_OPTIONS_PLACEHOLDER */"),
+                       p_opts.toJavascriptObject());
+}
+
+// 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)
+{
+    QString styles;
+    QString scripts;
+
+    for (const auto &ele : p_resource.m_resources) {
+        if (ele.m_enabled && !isGlobalStyles(ele)) {
+            // Styles.
+            for (const auto &style : ele.m_styles) {
+                auto styleFile = ConfigMgr::getInst().getUserOrAppFile(style);
+                styles += fillStyleTag(styleFile);
+            }
+
+            // Scripts.
+            for (const auto &script : ele.m_scripts) {
+                auto scriptFile = ConfigMgr::getInst().getUserOrAppFile(script);
+                scripts += fillScriptTag(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;
+}
+
+void HtmlTemplateHelper::updateMarkdownViewerTemplate(const MarkdownEditorConfig &p_config)
+{
+    if (p_config.revision() == s_markdownViewerTemplate.m_revision) {
+        return;
+    }
+
+    s_markdownViewerTemplate.m_revision = p_config.revision();
+
+    const auto &viewerResource = p_config.getViewerResource();
+
+    {
+        auto templateFile = ConfigMgr::getInst().getUserOrAppFile(viewerResource.m_template);
+        s_markdownViewerTemplate.m_template = FileUtils::readTextFile(templateFile);
+    }
+
+    fillGlobalStyles(s_markdownViewerTemplate.m_template, viewerResource);
+
+    fillThemeStyles(s_markdownViewerTemplate.m_template);
+
+    {
+        WebGlobalOptions opts;
+        opts.m_webPlantUml = p_config.getWebPlantUml();
+        opts.m_webGraphviz = p_config.getWebGraphviz();
+        opts.m_sectionNumberEnabled = p_config.getSectionNumberEnabled();
+        opts.m_constrainImageWidthEnabled = p_config.getConstrainImageWidthEnabled();
+        opts.m_protectFromXss = p_config.getProtectFromXss();
+        fillGlobalOptions(s_markdownViewerTemplate.m_template, opts);
+    }
+
+    fillResources(s_markdownViewerTemplate.m_template, viewerResource);
+}

+ 52 - 0
src/core/htmltemplatehelper.h

@@ -0,0 +1,52 @@
+#ifndef HTMLTEMPLATEHELPER_H
+#define HTMLTEMPLATEHELPER_H
+
+#include <QString>
+
+namespace vnotex
+{
+    class MarkdownEditorConfig;
+
+    // Global options to be passed to Web side at the very beginning.
+    struct WebGlobalOptions
+    {
+        bool m_webPlantUml = true;
+
+        bool m_webGraphviz = true;
+
+        bool m_sectionNumberEnabled = true;
+
+        bool m_constrainImageWidthEnabled = true;
+
+        bool m_protectFromXss = false;
+
+        QString boolToString(bool p_val) const
+        {
+            return p_val ? QStringLiteral("true") : QStringLiteral("false");
+        }
+
+        QString toJavascriptObject() const;
+    };
+
+    // Help to generate and update HTML templates.
+    class HtmlTemplateHelper
+    {
+    public:
+        HtmlTemplateHelper() = delete;
+
+        static const QString &getMarkdownViewerTemplate();
+        static void updateMarkdownViewerTemplate(const MarkdownEditorConfig &p_config);
+
+    private:
+        struct Template
+        {
+            int m_revision = -1;
+            QString m_template;
+        };
+
+        // Template for MarkdownViewer.
+        static Template s_markdownViewerTemplate;
+    };
+}
+
+#endif // HTMLTEMPLATEHELPER_H

+ 155 - 0
src/core/iconfig.h

@@ -0,0 +1,155 @@
+#ifndef ICONFIG_H
+#define ICONFIG_H
+
+#include <QSharedPointer>
+#include <QJsonObject>
+
+namespace vnotex
+{
+    class ConfigMgr;
+
+    // Interface for Config.
+    class IConfig
+    {
+    public:
+        IConfig(ConfigMgr *p_mgr, IConfig *p_topConfig = nullptr)
+            : m_topConfig(p_topConfig),
+              m_mgr(p_mgr)
+        {
+        }
+
+        virtual ~IConfig()
+        {
+        }
+
+        // Called to init top level config.
+        virtual void init()
+        {
+            Q_ASSERT(false);
+        }
+
+        // Init from QJsonObject.
+        virtual void init(const QJsonObject &p_default, const QJsonObject &p_user)
+        {
+            Q_UNUSED(p_default);
+            Q_UNUSED(p_user);
+            Q_ASSERT(false);
+        }
+
+        virtual void writeToSettings() const
+        {
+            Q_ASSERT(m_topConfig);
+            m_topConfig->writeToSettings();
+        }
+
+        virtual QJsonObject toJson() const = 0;
+
+        const QString &getSessionName() const
+        {
+            return m_sessionName;
+        }
+
+        virtual int revision() const
+        {
+            return m_revision;
+        }
+
+    protected:
+        ConfigMgr *getMgr() const
+        {
+            return m_mgr;
+        }
+
+        // First read user config, then the default config.
+        static QJsonValue read(const QJsonObject &p_default,
+                               const QJsonObject &p_user,
+                               const QString &p_key)
+        {
+            auto it = p_user.find(p_key);
+            if (it != p_user.end()) {
+                return it.value();
+            } else {
+                return p_default.value(p_key);
+            }
+        }
+
+        static QString readString(const QJsonObject &p_default,
+                                  const QJsonObject &p_user,
+                                  const QString &p_key)
+        {
+            return read(p_default, p_user, p_key).toString();
+        }
+
+        static QString readString(const QJsonObject &p_obj,
+                                  const QString &p_key)
+        {
+            return p_obj.value(p_key).toString();
+        }
+
+        static QByteArray readByteArray(const QJsonObject &p_obj,
+                                        const QString &p_key)
+        {
+            return QByteArray::fromBase64(readString(p_obj, p_key).toLatin1());
+        }
+
+        static void writeByteArray(QJsonObject &p_obj,
+                                   const QString &p_key,
+                                   const QByteArray &p_bytes)
+        {
+            p_obj.insert(p_key, QLatin1String(p_bytes.toBase64()));
+        }
+
+        static bool readBool(const QJsonObject &p_default,
+                             const QJsonObject &p_user,
+                             const QString &p_key)
+        {
+            return read(p_default, p_user, p_key).toBool();
+        }
+
+        static bool readBool(const QJsonObject &p_obj,
+                             const QString &p_key)
+        {
+            return p_obj.value(p_key).toBool();
+        }
+
+        static int readInt(const QJsonObject &p_default,
+                           const QJsonObject &p_user,
+                           const QString &p_key)
+        {
+            return read(p_default, p_user, p_key).toInt();
+        }
+
+        static qreal readReal(const QJsonObject &p_default,
+                              const QJsonObject &p_user,
+                              const QString &p_key)
+        {
+            return read(p_default, p_user, p_key).toDouble();
+        }
+
+        template <typename T>
+        static void updateConfig(T &p_cur,
+                                 const T &p_new,
+                                 IConfig *p_config)
+        {
+            if (p_cur == p_new) {
+                return;
+            }
+
+            ++p_config->m_revision;
+            p_cur = p_new;
+            p_config->writeToSettings();
+        }
+
+        IConfig *m_topConfig = nullptr;
+
+        QString m_sessionName;
+
+        // Used to indicate whether there is change after last read.
+        int m_revision = 0;
+
+    private:
+        ConfigMgr *m_mgr = nullptr;
+    };
+} // ns vnotex
+
+#endif // ICONFIG_H

+ 116 - 0
src/core/logger.cpp

@@ -0,0 +1,116 @@
+#include "logger.h"
+
+#include <QFile>
+#include <QTextStream>
+#include "configmgr.h"
+
+using namespace vnotex;
+
+QFile Logger::s_file;
+
+bool Logger::s_debugLog = false;
+
+void Logger::init(bool p_debugLog)
+{
+    s_debugLog = p_debugLog;
+
+#if defined(QT_NO_DEBUG)
+    s_file.setFileName(ConfigMgr::getInst().getLogFile());
+    if (s_file.size() >= 5 * 1024 * 1024) {
+        s_file.open(QIODevice::WriteOnly | QIODevice::Text);
+    } else {
+        s_file.open(QIODevice::Append | QIODevice::Text);
+    }
+#endif
+
+    qInstallMessageHandler(Logger::log);
+}
+
+static QString getFileName(const char *p_file)
+{
+    QString file(p_file);
+    int idx = file.lastIndexOf(QChar('/'));
+    if (idx == -1) {
+        idx = file.lastIndexOf(QChar('\\'));
+    }
+
+    if (idx == -1) {
+        return file;
+    } else {
+        return file.mid(idx + 1);
+    }
+}
+
+void Logger::log(QtMsgType p_type, const QMessageLogContext &p_context, const QString &p_msg)
+{
+#if defined(QT_NO_DEBUG)
+    if (!s_debugLog && p_type == QtDebugMsg) {
+        return;
+    }
+#endif
+
+    QByteArray localMsg = p_msg.toUtf8();
+    QString header;
+
+    switch (p_type) {
+    case QtDebugMsg:
+        header = QStringLiteral("Debug:");
+        break;
+
+    case QtInfoMsg:
+        header = QStringLiteral("Info:");
+        break;
+
+    case QtWarningMsg:
+        header = QStringLiteral("Warning:");
+        break;
+
+    case QtCriticalMsg:
+        header = QStringLiteral("Critical:");
+        break;
+
+    case QtFatalMsg:
+        header = QStringLiteral("Fatal:");
+    }
+
+    QString fileName = getFileName(p_context.file);
+
+#if defined(QT_NO_DEBUG)
+    QTextStream stream(&s_file);
+    stream << header << (QString("(%1:%2) ").arg(fileName).arg(p_context.line))
+           << localMsg << "\n";
+
+    if (p_type == QtFatalMsg) {
+        s_file.close();
+        abort();
+    }
+#else
+    std::string fileStr = fileName.toStdString();
+    const char *file = fileStr.c_str();
+
+    switch (p_type) {
+    case QtDebugMsg:
+        fprintf(stderr, "%s(%s:%u) %s\n",
+                header.toStdString().c_str(), file, p_context.line, localMsg.constData());
+        break;
+    case QtInfoMsg:
+        fprintf(stderr, "%s(%s:%u) %s\n",
+                header.toStdString().c_str(), file, p_context.line, localMsg.constData());
+        break;
+    case QtWarningMsg:
+        fprintf(stderr, "%s(%s:%u) %s\n",
+                header.toStdString().c_str(), file, p_context.line, localMsg.constData());
+        break;
+    case QtCriticalMsg:
+        fprintf(stderr, "%s(%s:%u) %s\n",
+                header.toStdString().c_str(), file, p_context.line, localMsg.constData());
+        break;
+    case QtFatalMsg:
+        fprintf(stderr, "%s(%s:%u) %s\n",
+                header.toStdString().c_str(), file, p_context.line, localMsg.constData());
+        abort();
+    }
+
+    fflush(stderr);
+#endif
+}

+ 27 - 0
src/core/logger.h

@@ -0,0 +1,27 @@
+#ifndef LOGGER_H
+#define LOGGER_H
+
+#include <QString>
+#include <QMessageLogContext>
+
+class QFile;
+
+namespace vnotex
+{
+    class Logger
+    {
+    public:
+        Logger() = delete;
+
+        static void init(bool p_debugLog);
+
+    private:
+        static void log(QtMsgType p_type, const QMessageLogContext &p_context, const QString &p_msg);
+
+        static QFile s_file;
+
+        static bool s_debugLog;
+    };
+}
+
+#endif // LOGGER_H

+ 113 - 0
src/core/mainconfig.cpp

@@ -0,0 +1,113 @@
+#include "mainconfig.h"
+
+#include <QJsonObject>
+#include <QDebug>
+
+#include "configmgr.h"
+#include "coreconfig.h"
+#include "editorconfig.h"
+#include "widgetconfig.h"
+
+using namespace vnotex;
+
+bool MainConfig::s_versionChanged = false;
+
+MainConfig::MainConfig(ConfigMgr *p_mgr)
+    : IConfig(p_mgr, nullptr),
+      m_coreConfig(new CoreConfig(p_mgr, this)),
+      m_editorConfig(new EditorConfig(p_mgr, this)),
+      m_widgetConfig(new WidgetConfig(p_mgr, this))
+{
+}
+
+MainConfig::~MainConfig()
+{
+
+}
+
+void MainConfig::init()
+{
+    auto mgr = getMgr();
+    auto appSettings = mgr->getSettings(ConfigMgr::Source::App);
+    auto userSettings = mgr->getSettings(ConfigMgr::Source::User);
+    const auto &appJobj = appSettings->getJson();
+    const auto &userJobj = userSettings->getJson();
+
+    loadMetadata(appJobj, userJobj);
+
+    m_coreConfig->init(appJobj, userJobj);
+
+    m_editorConfig->init(appJobj, userJobj);
+
+    m_widgetConfig->init(appJobj, userJobj);
+
+    if (isVersionChanged()) {
+        // Update user config.
+        writeToSettings();
+    }
+}
+
+void MainConfig::loadMetadata(const QJsonObject &p_app, const QJsonObject &p_user)
+{
+    const auto appObj = p_app.value(QStringLiteral("metadata")).toObject();
+    const auto userObj = p_user.value(QStringLiteral("metadata")).toObject();
+
+    m_version = appObj.value(QStringLiteral("version")).toString();
+    m_userVersion = userObj.value(QStringLiteral("version")).toString();
+    s_versionChanged = m_version != m_userVersion;
+    qDebug() << "version" << m_version << "user version" << m_userVersion;
+}
+
+QJsonObject MainConfig::saveMetaData() const
+{
+    QJsonObject metaObj;
+    metaObj[QStringLiteral("version")] = m_version;
+
+    return metaObj;
+}
+
+bool MainConfig::isVersionChanged()
+{
+    return s_versionChanged;
+}
+
+CoreConfig &MainConfig::getCoreConfig()
+{
+    return *m_coreConfig;
+}
+
+EditorConfig &MainConfig::getEditorConfig()
+{
+    return *m_editorConfig;
+}
+
+WidgetConfig &MainConfig::getWidgetConfig()
+{
+    return *m_widgetConfig;
+}
+
+void MainConfig::writeToSettings() const
+{
+    getMgr()->writeUserSettings(toJson());
+}
+
+QJsonObject MainConfig::toJson() const
+{
+    QJsonObject obj;
+    obj[QStringLiteral("metadata")] = saveMetaData();
+    obj[m_coreConfig->getSessionName()] = m_coreConfig->toJson();
+    obj[m_editorConfig->getSessionName()] = m_editorConfig->toJson();
+    obj[m_widgetConfig->getSessionName()] = m_widgetConfig->toJson();
+    return obj;
+}
+
+const QString &MainConfig::getVersion() const
+{
+    return m_version;
+}
+
+QString MainConfig::getVersion(const QJsonObject &p_jobj)
+{
+    const auto metadataObj = p_jobj.value(QStringLiteral("metadata")).toObject();
+    return metadataObj.value(QStringLiteral("version")).toString();
+}

+ 63 - 0
src/core/mainconfig.h

@@ -0,0 +1,63 @@
+#ifndef MAINCONFIG_H
+#define MAINCONFIG_H
+
+#include "iconfig.h"
+
+#include <QtGlobal>
+#include <QString>
+
+class QJsonObject;
+
+namespace vnotex
+{
+    class CoreConfig;
+    class EditorConfig;
+    class WidgetConfig;
+
+    class MainConfig : public IConfig
+    {
+    public:
+        explicit MainConfig(ConfigMgr *p_mgr);
+
+        ~MainConfig();
+
+        void init() Q_DECL_OVERRIDE;
+
+        const QString &getVersion() const;
+
+        CoreConfig &getCoreConfig();
+
+        EditorConfig &getEditorConfig();
+
+        WidgetConfig &getWidgetConfig();
+
+        void writeToSettings() const Q_DECL_OVERRIDE;
+
+        QJsonObject toJson() const Q_DECL_OVERRIDE;
+
+        static QString getVersion(const QJsonObject &p_jobj);
+
+        static bool isVersionChanged();
+
+    private:
+        void loadMetadata(const QJsonObject &p_app, const QJsonObject &p_user);
+
+        QJsonObject saveMetaData() const;
+
+        // Version of VNoteX.
+        QString m_version;
+
+        // Version of user's configuration.
+        QString m_userVersion;
+
+        QScopedPointer<CoreConfig> m_coreConfig;
+
+        QScopedPointer<EditorConfig> m_editorConfig;
+
+        QScopedPointer<WidgetConfig> m_widgetConfig;
+
+        static bool s_versionChanged;
+    };
+} // ns vnotex
+
+#endif // MAINCONFIG_H

+ 197 - 0
src/core/markdowneditorconfig.cpp

@@ -0,0 +1,197 @@
+#include "markdowneditorconfig.h"
+
+#include <QDebug>
+
+#include "texteditorconfig.h"
+#include "mainconfig.h"
+
+using namespace vnotex;
+
+#define READSTR(key) readString(appObj, userObj, (key))
+#define READBOOL(key) readBool(appObj, userObj, (key))
+#define READREAL(key) readReal(appObj, userObj, (key))
+
+MarkdownEditorConfig::MarkdownEditorConfig(ConfigMgr *p_mgr,
+                                           IConfig *p_topConfig,
+                                           const QSharedPointer<TextEditorConfig> &p_textEditorConfig)
+    : IConfig(p_mgr, p_topConfig),
+      m_textEditorConfig(p_textEditorConfig)
+{
+    m_sessionName = QStringLiteral("markdown_editor");
+}
+
+void MarkdownEditorConfig::init(const QJsonObject &p_app, const QJsonObject &p_user)
+{
+    const auto appObj = p_app.value(m_sessionName).toObject();
+    const auto userObj = p_user.value(m_sessionName).toObject();
+
+    loadViewerResource(appObj, userObj);
+
+    m_webPlantUml = READBOOL(QStringLiteral("web_plantuml"));
+    m_webGraphviz = READBOOL(QStringLiteral("web_graphviz"));
+
+    m_prependDotInRelativeLink = READBOOL(QStringLiteral("prepend_dot_in_relative_link"));
+    m_confirmBeforeClearObsoleteImages = READBOOL(QStringLiteral("confirm_before_clear_obsolete_images"));
+    m_insertFileNameAsTitle = READBOOL(QStringLiteral("insert_file_name_as_title"));
+    m_sectionNumberEnabled = READBOOL(QStringLiteral("section_number"));
+    m_constrainImageWidthEnabled = READBOOL(QStringLiteral("constrain_image_width"));
+    m_constrainInPlacePreviewWidthEnabled = READBOOL(QStringLiteral("constrain_inplace_preview_width"));
+    m_zoomFactorInReadMode = READREAL(QStringLiteral("zoom_factor_in_read_mode"));
+    m_fetchImagesInParseAndPaste = READBOOL(QStringLiteral("fetch_images_in_parse_and_paste"));
+    m_protectFromXss = READBOOL(QStringLiteral("protect_from_xss"));
+}
+
+QJsonObject MarkdownEditorConfig::toJson() const
+{
+    QJsonObject obj;
+    obj[QStringLiteral("viewer_resource")] = saveViewerResource();
+    obj[QStringLiteral("web_plantuml")] = m_webPlantUml;
+    obj[QStringLiteral("web_graphviz")] = m_webGraphviz;
+    obj[QStringLiteral("prepend_dot_in_relative_link")] = m_prependDotInRelativeLink;
+    obj[QStringLiteral("confirm_before_clear_obsolete_images")] = m_confirmBeforeClearObsoleteImages;
+    obj[QStringLiteral("insert_file_name_as_title")] = m_insertFileNameAsTitle;
+    obj[QStringLiteral("section_number")] = m_sectionNumberEnabled;
+    obj[QStringLiteral("constrain_image_width")] = m_constrainImageWidthEnabled;
+    obj[QStringLiteral("constrain_inplace_preview_width")] = m_constrainInPlacePreviewWidthEnabled;
+    obj[QStringLiteral("zoom_factor_in_read_mode")] = m_zoomFactorInReadMode;
+    obj[QStringLiteral("fetch_images_in_parse_and_paste")] = m_fetchImagesInParseAndPaste;
+    obj[QStringLiteral("protect_from_xss")] = m_protectFromXss;
+    return obj;
+}
+
+TextEditorConfig &MarkdownEditorConfig::getTextEditorConfig()
+{
+    return *m_textEditorConfig;
+}
+
+const TextEditorConfig &MarkdownEditorConfig::getTextEditorConfig() const
+{
+    return *m_textEditorConfig;
+}
+
+int MarkdownEditorConfig::revision() const
+{
+    return m_revision + m_textEditorConfig->revision();
+}
+
+void MarkdownEditorConfig::loadViewerResource(const QJsonObject &p_app, const QJsonObject &p_user)
+{
+    const QString name(QStringLiteral("viewer_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_viewerResource.init(p_app[name].toObject());
+            return;
+        }
+    }
+
+    if (p_user.contains(name)) {
+        m_viewerResource.init(p_user[name].toObject());
+    } else {
+        m_viewerResource.init(p_app[name].toObject());
+    }
+}
+
+QJsonObject MarkdownEditorConfig::saveViewerResource() const
+{
+    return m_viewerResource.toJson();
+}
+
+const ViewerResource &MarkdownEditorConfig::getViewerResource() const
+{
+    return m_viewerResource;
+}
+
+bool MarkdownEditorConfig::getWebPlantUml() const
+{
+    return m_webPlantUml;
+}
+
+bool MarkdownEditorConfig::getWebGraphviz() const
+{
+    return m_webGraphviz;
+}
+
+bool MarkdownEditorConfig::getPrependDotInRelativeLink() const
+{
+    return m_prependDotInRelativeLink;
+}
+
+bool MarkdownEditorConfig::getConfirmBeforeClearObsoleteImages() const
+{
+    return m_confirmBeforeClearObsoleteImages;
+}
+
+void MarkdownEditorConfig::setConfirmBeforeClearObsoleteImages(bool p_confirm)
+{
+    updateConfig(m_confirmBeforeClearObsoleteImages,
+                 p_confirm,
+                 this);
+}
+
+bool MarkdownEditorConfig::getInsertFileNameAsTitle() const
+{
+    return m_insertFileNameAsTitle;
+}
+
+void MarkdownEditorConfig::setInsertFileNameAsTitle(bool p_enabled)
+{
+    updateConfig(m_insertFileNameAsTitle, p_enabled, this);
+}
+
+bool MarkdownEditorConfig::getSectionNumberEnabled() const
+{
+    return m_sectionNumberEnabled;
+}
+
+void MarkdownEditorConfig::setSectionNumberEnabled(bool p_enabled)
+{
+    updateConfig(m_sectionNumberEnabled, p_enabled, this);
+}
+
+bool MarkdownEditorConfig::getConstrainImageWidthEnabled() const
+{
+    return m_constrainImageWidthEnabled;
+}
+
+void MarkdownEditorConfig::setConstrainImageWidthEnabled(bool p_enabled)
+{
+    updateConfig(m_constrainImageWidthEnabled, p_enabled, this);
+}
+
+bool MarkdownEditorConfig::getConstrainInPlacePreviewWidthEnabled() const
+{
+    return m_constrainInPlacePreviewWidthEnabled;
+}
+
+void MarkdownEditorConfig::setConstrainInPlacePreviewWidthEnabled(bool p_enabled)
+{
+    updateConfig(m_constrainInPlacePreviewWidthEnabled, p_enabled, this);
+}
+
+qreal MarkdownEditorConfig::getZoomFactorInReadMode() const
+{
+    return m_zoomFactorInReadMode;
+}
+
+void MarkdownEditorConfig::setZoomFactorInReadMode(qreal p_factor)
+{
+    updateConfig(m_zoomFactorInReadMode, p_factor, this);
+}
+
+bool MarkdownEditorConfig::getFetchImagesInParseAndPaste() const
+{
+    return m_fetchImagesInParseAndPaste;
+}
+
+void MarkdownEditorConfig::setFetchImagesInParseAndPaste(bool p_enabled)
+{
+    updateConfig(m_fetchImagesInParseAndPaste, p_enabled, this);
+}
+
+bool MarkdownEditorConfig::getProtectFromXss() const
+{
+    return m_protectFromXss;
+}

+ 103 - 0
src/core/markdowneditorconfig.h

@@ -0,0 +1,103 @@
+#ifndef MARKDOWNEDITORCONFIG_H
+#define MARKDOWNEDITORCONFIG_H
+
+#include "iconfig.h"
+
+#include "viewerresource.h"
+
+#include <QSharedPointer>
+#include <QVector>
+
+namespace vnotex
+{
+    class TextEditorConfig;
+
+    class MarkdownEditorConfig : public IConfig
+    {
+    public:
+        MarkdownEditorConfig(ConfigMgr *p_mgr,
+                             IConfig *p_topConfig,
+                             const QSharedPointer<TextEditorConfig> &p_textEditorConfig);
+
+        void init(const QJsonObject &p_app, const QJsonObject &p_user) Q_DECL_OVERRIDE;
+
+        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;
+
+        bool getWebPlantUml() const;
+
+        bool getWebGraphviz() const;
+
+        bool getPrependDotInRelativeLink() const;
+
+        bool getConfirmBeforeClearObsoleteImages() const;
+        void setConfirmBeforeClearObsoleteImages(bool p_confirm);
+
+        bool getInsertFileNameAsTitle() const;
+        void setInsertFileNameAsTitle(bool p_enabled);
+
+        bool getSectionNumberEnabled() const;
+        void setSectionNumberEnabled(bool p_enabled);
+
+        bool getConstrainImageWidthEnabled() const;
+        void setConstrainImageWidthEnabled(bool p_enabled);
+
+        bool getConstrainInPlacePreviewWidthEnabled() const;
+        void setConstrainInPlacePreviewWidthEnabled(bool p_enabled);
+
+        qreal getZoomFactorInReadMode() const;
+        void setZoomFactorInReadMode(qreal p_factor);
+
+        bool getFetchImagesInParseAndPaste() const;
+        void setFetchImagesInParseAndPaste(bool p_enabled);
+
+        bool getProtectFromXss() const;
+
+    private:
+        QSharedPointer<TextEditorConfig> m_textEditorConfig;
+
+        ViewerResource m_viewerResource;
+
+        // Whether use javascript or external program to render PlantUML.
+        bool m_webPlantUml = true;
+
+        bool m_webGraphviz = true;
+
+        // Whether prepend a dot in front of the relative link, like images.
+        bool m_prependDotInRelativeLink = false;
+
+        // Whether ask for user confirmation before clearing obsolete images.
+        bool m_confirmBeforeClearObsoleteImages = true;
+
+        // Whether insert the name of the new file as title.
+        bool m_insertFileNameAsTitle = true;
+
+        // Whether enable section numbering.
+        bool m_sectionNumberEnabled = true;
+
+        // Whether enable image width constraint.
+        bool m_constrainImageWidthEnabled = true;
+
+        // Whether enable in-place preview width constraint.
+        bool m_constrainInPlacePreviewWidthEnabled = false;
+
+        qreal m_zoomFactorInReadMode = 1.0;
+
+        // Whether fetch images to local in Parse To Markdown And Paste.
+        bool m_fetchImagesInParseAndPaste = true;
+
+        // Whether protect from Cross-Site Scripting.
+        bool m_protectFromXss = false;
+    };
+}
+
+#endif // MARKDOWNEDITORCONFIG_H

+ 50 - 0
src/core/namebasedserver.h

@@ -0,0 +1,50 @@
+#ifndef NAMEBASEDSERVER_H
+#define NAMEBASEDSERVER_H
+
+#include <QHash>
+#include <QSharedPointer>
+#include <QDebug>
+#include <QList>
+
+namespace vnotex
+{
+    template <typename T>
+    class NameBasedServer
+    {
+    public:
+        // Register an item.
+        bool registerItem(const QString &p_name, const QSharedPointer<T> &p_item)
+        {
+            if (m_data.contains(p_name)) {
+                qWarning() << "item to register already exists with name" << p_name;
+                return false;
+            }
+
+            m_data.insert(p_name, p_item);
+            return true;
+        }
+
+        // Get an item.
+        QSharedPointer<T> getItem(const QString &p_name)
+        {
+            auto it = m_data.find(p_name);
+            if (it != m_data.end()) {
+                return it.value();
+            }
+
+            return nullptr;
+        }
+
+        QList<QSharedPointer<T>> getAllItems() const
+        {
+            return m_data.values();
+        }
+
+    private:
+        // Name to item mapping.
+        QHash<QString, QSharedPointer<T>> m_data;
+    };
+} // ns vnotex
+
+
+#endif // NAMEBASEDSERVER_H

+ 60 - 0
src/core/notebook/bundlenotebook.cpp

@@ -0,0 +1,60 @@
+#include "bundlenotebook.h"
+
+#include <QDebug>
+
+#include <notebookconfigmgr/bundlenotebookconfigmgr.h>
+#include <notebookconfigmgr/notebookconfig.h>
+#include <utils/fileutils.h>
+
+using namespace vnotex;
+
+BundleNotebook::BundleNotebook(const NotebookParameters &p_paras,
+                               QObject *p_parent)
+    : Notebook(p_paras, p_parent)
+{
+    auto configMgr = getBundleNotebookConfigMgr();
+    auto config = configMgr->readNotebookConfig();
+    m_nextNodeId = config->m_nextNodeId;
+}
+
+BundleNotebookConfigMgr *BundleNotebook::getBundleNotebookConfigMgr() const
+{
+    return dynamic_cast<BundleNotebookConfigMgr *>(getConfigMgr().data());
+}
+
+ID BundleNotebook::getNextNodeId() const
+{
+    return m_nextNodeId;
+}
+
+ID BundleNotebook::getAndUpdateNextNodeId()
+{
+    auto id = m_nextNodeId++;
+    getBundleNotebookConfigMgr()->writeNotebookConfig();
+    return id;
+}
+
+void BundleNotebook::updateNotebookConfig()
+{
+    getBundleNotebookConfigMgr()->writeNotebookConfig();
+}
+
+void BundleNotebook::removeNotebookConfig()
+{
+    getBundleNotebookConfigMgr()->removeNotebookConfig();
+}
+
+void BundleNotebook::remove()
+{
+    // Remove all nodes.
+    removeNode(getRootNode());
+
+    // Remove notebook config.
+    removeNotebookConfig();
+
+    // Remove notebook root folder if it is empty.
+    if (!FileUtils::removeDirIfEmpty(getRootFolderAbsolutePath())) {
+        qInfo() << QString("root folder of notebook (%1) is not empty and needs manual clean up")
+                          .arg(getRootFolderAbsolutePath());
+    }
+}

+ 35 - 0
src/core/notebook/bundlenotebook.h

@@ -0,0 +1,35 @@
+#ifndef BUNDLENOTEBOOK_H
+#define BUNDLENOTEBOOK_H
+
+#include "notebook.h"
+#include "global.h"
+
+namespace vnotex
+{
+    class BundleNotebookConfigMgr;
+
+    class BundleNotebook : public Notebook
+    {
+        Q_OBJECT
+    public:
+        BundleNotebook(const NotebookParameters &p_paras,
+                       QObject *p_parent = nullptr);
+
+        ID getNextNodeId() const Q_DECL_OVERRIDE;
+
+        ID getAndUpdateNextNodeId() Q_DECL_OVERRIDE;
+
+        void updateNotebookConfig() Q_DECL_OVERRIDE;
+
+        void removeNotebookConfig() Q_DECL_OVERRIDE;
+
+        void remove() Q_DECL_OVERRIDE;
+
+    private:
+        BundleNotebookConfigMgr *getBundleNotebookConfigMgr() const;
+
+        ID m_nextNodeId = 1;
+    };
+} // ns vnotex
+
+#endif // BUNDLENOTEBOOK_H

+ 104 - 0
src/core/notebook/bundlenotebookfactory.cpp

@@ -0,0 +1,104 @@
+#include "bundlenotebookfactory.h"
+
+#include <QObject>
+#include <QDebug>
+
+#include <utils/pathutils.h>
+#include "../exception.h"
+#include "notebookconfigmgr/bundlenotebookconfigmgr.h"
+#include "notebookparameters.h"
+#include "bundlenotebook.h"
+#include "notebookmgr.h"
+#include "notebookconfigmgr/notebookconfig.h"
+
+using namespace vnotex;
+
+BundleNotebookFactory::BundleNotebookFactory()
+{
+}
+
+QString BundleNotebookFactory::getName() const
+{
+    return QStringLiteral("bundle.vnotex");
+}
+
+QString BundleNotebookFactory::getDisplayName() const
+{
+    return QObject::tr("Bundled Notebook");
+}
+
+QString BundleNotebookFactory::getDescription() const
+{
+    return QObject::tr("A notebook with configuration files to track its content");
+}
+
+// Check if root folder is valid for a new notebook.
+static void checkRootFolderForNewNotebook(const NotebookParameters &p_paras)
+{
+    if (p_paras.m_rootFolderPath.isEmpty()) {
+        QString msg("no local root folder is specified");
+        qCritical() << msg;
+        throw Exception(Exception::Type::InvalidPath, msg);
+    } else if (p_paras.m_ensureEmptyRootFolder && !PathUtils::isEmptyDir(p_paras.m_rootFolderPath)) {
+        QString msg = QString("local root folder must be empty: %1 (%2)")
+                             .arg(p_paras.m_rootFolderPath, PathUtils::absolutePath(p_paras.m_rootFolderPath));
+        qCritical() << msg;
+        throw Exception(Exception::Type::InvalidPath, msg);
+    }
+}
+
+QSharedPointer<Notebook> BundleNotebookFactory::newNotebook(const NotebookParameters &p_paras)
+{
+    checkParameters(p_paras);
+
+    checkRootFolderForNewNotebook(p_paras);
+
+    p_paras.m_notebookConfigMgr->createEmptySkeleton(p_paras);
+
+    auto notebook = QSharedPointer<BundleNotebook>::create(p_paras);
+    return notebook;
+}
+
+QSharedPointer<Notebook> BundleNotebookFactory::createNotebook(const NotebookMgr &p_mgr,
+                                                               const QString &p_rootFolderPath,
+                                                               const QSharedPointer<INotebookBackend> &p_backend)
+{
+    // Read basic info about this notebook.
+    auto nbConfig = BundleNotebookConfigMgr::readNotebookConfig(p_backend);
+    auto paras = NotebookParameters::createNotebookParameters(p_mgr,
+                                                              p_backend,
+                                                              getName(),
+                                                              nbConfig->m_name,
+                                                              nbConfig->m_description,
+                                                              p_rootFolderPath,
+                                                              QIcon(),
+                                                              nbConfig->m_imageFolder,
+                                                              nbConfig->m_attachmentFolder,
+                                                              nbConfig->m_createdTimeUtc,
+                                                              nbConfig->m_versionController,
+                                                              nbConfig->m_notebookConfigMgr);
+    checkParameters(*paras);
+    auto notebook = QSharedPointer<BundleNotebook>::create(*paras);
+    return notebook;
+}
+
+void BundleNotebookFactory::checkParameters(const NotebookParameters &p_paras) const
+{
+    auto configMgr = dynamic_cast<BundleNotebookConfigMgr *>(p_paras.m_notebookConfigMgr.data());
+    if (!configMgr) {
+        Exception::throwOne(Exception::Type::InvalidArgument,
+                            QString("Invalid notebook configuration manager"));
+    }
+}
+
+bool BundleNotebookFactory::checkRootFolder(const QSharedPointer<INotebookBackend> &p_backend)
+{
+    try {
+        BundleNotebookConfigMgr::readNotebookConfig(p_backend);
+    } catch (Exception &p_e) {
+        Q_UNUSED(p_e);
+        return false;
+    }
+
+    return true;
+}

+ 38 - 0
src/core/notebook/bundlenotebookfactory.h

@@ -0,0 +1,38 @@
+#ifndef BUNDLENOTEBOOKFACTORY_H
+#define BUNDLENOTEBOOKFACTORY_H
+
+#include "inotebookfactory.h"
+
+
+namespace vnotex
+{
+    class BundleNotebookFactory : public INotebookFactory
+    {
+    public:
+        BundleNotebookFactory();
+
+        // Get the name of this factory.
+        QString getName() const Q_DECL_OVERRIDE;
+
+        // Get the display name of this factory.
+        QString getDisplayName() const Q_DECL_OVERRIDE;
+
+        // Get the description of this factory.
+        QString getDescription() const Q_DECL_OVERRIDE;
+
+        // New a notebook with given information and return an instance of that notebook.
+        QSharedPointer<Notebook> newNotebook(const NotebookParameters &p_paras) Q_DECL_OVERRIDE;
+
+        // Create a Notebook instance from existing root folder.
+        QSharedPointer<Notebook> createNotebook(const NotebookMgr &p_mgr,
+                                                const QString &p_rootFolderPath,
+                                                const QSharedPointer<INotebookBackend> &p_backend) Q_DECL_OVERRIDE;
+
+        bool checkRootFolder(const QSharedPointer<INotebookBackend> &p_backend) Q_DECL_OVERRIDE;
+
+    private:
+        void checkParameters(const NotebookParameters &p_paras) const;
+    };
+} // ns vnotex
+
+#endif // BUNDLENOTEBOOKFACTORY_H

+ 130 - 0
src/core/notebook/filenode.cpp

@@ -0,0 +1,130 @@
+#include "filenode.h"
+
+#include <notebookconfigmgr/inotebookconfigmgr.h>
+#include <notebookbackend/inotebookbackend.h>
+#include <utils/pathutils.h>
+#include <utils/fileutils.h>
+#include "notebook.h"
+
+using namespace vnotex;
+
+FileNode::FileNode(ID p_id,
+                   const QString &p_name,
+                   const QDateTime &p_createdTimeUtc,
+                   const QDateTime &p_modifiedTimeUtc,
+                   const QString &p_attachmentFolder,
+                   const QStringList &p_tags,
+                   Notebook *p_notebook,
+                   Node *p_parent)
+    : Node(Node::Type::File,
+           p_id,
+           p_name,
+           p_createdTimeUtc,
+           p_notebook,
+           p_parent),
+      m_modifiedTimeUtc(p_modifiedTimeUtc),
+      m_attachmentFolder(p_attachmentFolder),
+      m_tags(p_tags)
+{
+}
+
+QVector<QSharedPointer<Node>> FileNode::getChildren() const
+{
+    return QVector<QSharedPointer<Node>>();
+}
+
+int FileNode::getChildrenCount() const
+{
+    return 0;
+}
+
+void FileNode::addChild(const QSharedPointer<Node> &p_node)
+{
+    Q_ASSERT(false);
+    Q_UNUSED(p_node);
+}
+
+void FileNode::insertChild(int p_idx, const QSharedPointer<Node> &p_node)
+{
+    Q_ASSERT(false);
+    Q_UNUSED(p_idx);
+    Q_UNUSED(p_node);
+}
+
+void FileNode::removeChild(const QSharedPointer<Node> &p_child)
+{
+    Q_ASSERT(false);
+    Q_UNUSED(p_child);
+}
+
+QDateTime FileNode::getModifiedTimeUtc() const
+{
+    return m_modifiedTimeUtc;
+}
+
+void FileNode::setModifiedTimeUtc()
+{
+    m_modifiedTimeUtc = QDateTime::currentDateTimeUtc();
+}
+
+QString FileNode::getAttachmentFolder() const
+{
+    return m_attachmentFolder;
+}
+
+void FileNode::setAttachmentFolder(const QString &p_folder)
+{
+    m_attachmentFolder = p_folder;
+}
+
+QStringList FileNode::addAttachment(const QString &p_destFolderPath, const QStringList &p_files)
+{
+    QStringList addedFiles;
+    for (const auto &file : p_files) {
+        auto destFilePath = m_backend->renameIfExistsCaseInsensitive(
+            PathUtils::concatenateFilePath(p_destFolderPath, PathUtils::fileName(file)));
+        m_backend->copyFile(file, destFilePath);
+        addedFiles << destFilePath;
+    }
+
+    return addedFiles;
+}
+
+QString FileNode::newAttachmentFile(const QString &p_destFolderPath, const QString &p_name)
+{
+    auto destFilePath = m_backend->renameIfExistsCaseInsensitive(
+        PathUtils::concatenateFilePath(p_destFolderPath, p_name));
+    m_backend->writeFile(destFilePath, QByteArray());
+    return destFilePath;
+}
+
+QString FileNode::newAttachmentFolder(const QString &p_destFolderPath, const QString &p_name)
+{
+    auto destFilePath = m_backend->renameIfExistsCaseInsensitive(
+        PathUtils::concatenateFilePath(p_destFolderPath, p_name));
+    m_backend->makePath(destFilePath);
+    return destFilePath;
+}
+
+QString FileNode::renameAttachment(const QString &p_path, const QString &p_name)
+{
+    m_backend->renameFile(p_path, p_name);
+    return p_name;
+}
+
+void FileNode::removeAttachment(const QStringList &p_paths)
+{
+    // Just move it to recycle bin but not added as a child node of recycle bin.
+    for (const auto &pa : p_paths) {
+        if (QFileInfo(pa).isDir()) {
+            m_notebook->moveDirToRecycleBin(pa);
+        } else {
+            m_notebook->moveFileToRecycleBin(pa);
+        }
+    }
+}
+
+QStringList FileNode::getTags() const
+{
+    return m_tags;
+}

+ 60 - 0
src/core/notebook/filenode.h

@@ -0,0 +1,60 @@
+#ifndef FILENODE_H
+#define FILENODE_H
+
+#include "node.h"
+
+namespace vnotex
+{
+    // File node of notebook.
+    class FileNode : public Node
+    {
+    public:
+        FileNode(ID p_id,
+                 const QString &p_name,
+                 const QDateTime &p_createdTimeUtc,
+                 const QDateTime &p_modifiedTimeUtc,
+                 const QString &p_attachmentFolder,
+                 const QStringList &p_tags,
+                 Notebook *p_notebook,
+                 Node *p_parent = nullptr);
+
+        QVector<QSharedPointer<Node>> getChildren() const Q_DECL_OVERRIDE;
+
+        int getChildrenCount() const Q_DECL_OVERRIDE;
+
+        void addChild(const QSharedPointer<Node> &p_node) Q_DECL_OVERRIDE;
+
+        void insertChild(int p_idx, const QSharedPointer<Node> &p_node) Q_DECL_OVERRIDE;
+
+        void removeChild(const QSharedPointer<Node> &p_child) Q_DECL_OVERRIDE;
+
+        QDateTime getModifiedTimeUtc() const Q_DECL_OVERRIDE;
+
+        void setModifiedTimeUtc() Q_DECL_OVERRIDE;
+
+        QString getAttachmentFolder() const Q_DECL_OVERRIDE;
+
+        void setAttachmentFolder(const QString &p_folder) Q_DECL_OVERRIDE;
+
+        QStringList addAttachment(const QString &p_destFolderPath, const QStringList &p_files) Q_DECL_OVERRIDE;
+
+        QString newAttachmentFile(const QString &p_destFolderPath, const QString &p_name) Q_DECL_OVERRIDE;
+
+        QString newAttachmentFolder(const QString &p_destFolderPath, const QString &p_name) Q_DECL_OVERRIDE;
+
+        QString renameAttachment(const QString &p_path, const QString &p_name) Q_DECL_OVERRIDE;
+
+        void removeAttachment(const QStringList &p_paths) Q_DECL_OVERRIDE;
+
+        QStringList getTags() const Q_DECL_OVERRIDE;
+
+    private:
+        QDateTime m_modifiedTimeUtc;
+
+        QString m_attachmentFolder;
+
+        QStringList m_tags;
+    };
+}
+
+#endif // FILENODE_H

+ 65 - 0
src/core/notebook/foldernode.cpp

@@ -0,0 +1,65 @@
+#include "foldernode.h"
+
+using namespace vnotex;
+
+FolderNode::FolderNode(const QString &p_name,
+                       Notebook *p_notebook,
+                       Node *p_parent)
+    : Node(Node::Type::Folder,
+           p_name,
+           p_notebook,
+           p_parent)
+{
+}
+
+void FolderNode::loadFolder(ID p_id,
+                            const QDateTime &p_createdTimeUtc,
+                            const QVector<QSharedPointer<Node>> &p_children)
+{
+    Node::loadInfo(p_id, p_createdTimeUtc);
+    m_children = p_children;
+}
+
+QVector<QSharedPointer<Node>> FolderNode::getChildren() const
+{
+    return m_children;
+}
+
+int FolderNode::getChildrenCount() const
+{
+    return m_children.size();
+}
+
+void FolderNode::addChild(const QSharedPointer<Node> &p_node)
+{
+    insertChild(m_children.size(), p_node);
+}
+
+void FolderNode::insertChild(int p_idx, const QSharedPointer<Node> &p_node)
+{
+    p_node->setParent(this);
+
+    m_children.insert(p_idx, p_node);
+}
+
+void FolderNode::removeChild(const QSharedPointer<Node> &p_child)
+{
+    if (m_children.removeOne(p_child)) {
+        p_child->setParent(nullptr);
+    }
+}
+
+QDateTime FolderNode::getModifiedTimeUtc() const
+{
+    return getCreatedTimeUtc();
+}
+
+void FolderNode::setModifiedTimeUtc()
+{
+    Q_ASSERT(false);
+}
+
+QDir FolderNode::toDir() const
+{
+    return QDir(fetchAbsolutePath());
+}

+ 40 - 0
src/core/notebook/foldernode.h

@@ -0,0 +1,40 @@
+#ifndef FOLDERNODE_H
+#define FOLDERNODE_H
+
+#include "node.h"
+
+namespace vnotex
+{
+    class FolderNode : public Node
+    {
+    public:
+        FolderNode(const QString &p_name,
+                   Notebook *p_notebook,
+                   Node *p_parent = nullptr);
+
+        void loadFolder(ID p_id,
+                        const QDateTime &p_createdTimeUtc,
+                        const QVector<QSharedPointer<Node>> &p_children);
+
+        QVector<QSharedPointer<Node>> getChildren() const Q_DECL_OVERRIDE;
+
+        int getChildrenCount() const Q_DECL_OVERRIDE;
+
+        void addChild(const QSharedPointer<Node> &p_node) Q_DECL_OVERRIDE;
+
+        void insertChild(int p_idx, const QSharedPointer<Node> &p_node) Q_DECL_OVERRIDE;
+
+        void removeChild(const QSharedPointer<Node> &p_child) Q_DECL_OVERRIDE;
+
+        QDateTime getModifiedTimeUtc() const Q_DECL_OVERRIDE;
+
+        void setModifiedTimeUtc() Q_DECL_OVERRIDE;
+
+        QDir toDir() const Q_DECL_OVERRIDE;
+
+    private:
+        QVector<QSharedPointer<Node>> m_children;
+    };
+} // ns vnotex
+
+#endif // FOLDERNODE_H

+ 46 - 0
src/core/notebook/inotebookfactory.h

@@ -0,0 +1,46 @@
+#ifndef INOTEBOOKFACTORY_H
+#define INOTEBOOKFACTORY_H
+
+#include <QSharedPointer>
+#include <QIcon>
+
+namespace vnotex
+{
+    class Notebook;
+    class NotebookParameters;
+    class INotebookBackend;
+    class NotebookMgr;
+
+    // Abstract factory to create notebook.
+    class INotebookFactory
+    {
+    public:
+        virtual ~INotebookFactory()
+        {
+        }
+
+        // Get the name of this factory.
+        virtual QString getName() const = 0;
+
+        // Get the display name of this factory.
+        virtual QString getDisplayName() const = 0;
+
+        // Get the description of this factory.
+        virtual QString getDescription() const = 0;
+
+        // New a notebook with given information and return an instance of that notebook.
+        // The root folder should be empty.
+        virtual QSharedPointer<Notebook> newNotebook(const NotebookParameters &p_paras) = 0;
+
+        // Create a Notebook instance from existing root folder.
+        virtual QSharedPointer<Notebook> createNotebook(const NotebookMgr &p_mgr,
+                                                        const QString &p_rootFolderPath,
+                                                        const QSharedPointer<INotebookBackend> &p_backend) = 0;
+
+        // Check if @p_rootFolderPath is a valid root folder to use by this factory
+        // to create a notebook.
+        virtual bool checkRootFolder(const QSharedPointer<INotebookBackend> &p_backend) = 0;
+    };
+} // ns vnotex
+
+#endif // INOTEBOOKFACTORY_H

+ 335 - 0
src/core/notebook/node.cpp

@@ -0,0 +1,335 @@
+#include "node.h"
+
+#include <QDir>
+
+#include <notebookconfigmgr/inotebookconfigmgr.h>
+#include <notebookbackend/inotebookbackend.h>
+#include <utils/pathutils.h>
+#include <core/exception.h>
+#include "notebook.h"
+
+using namespace vnotex;
+
+Node::Node(Type p_type,
+           ID p_id,
+           const QString &p_name,
+           const QDateTime &p_createdTimeUtc,
+           Notebook *p_notebook,
+           Node *p_parent)
+    : m_type(p_type),
+      m_id(p_id),
+      m_name(p_name),
+      m_createdTimeUtc(p_createdTimeUtc),
+      m_loaded(true),
+      m_notebook(p_notebook),
+      m_parent(p_parent)
+{
+    if (m_notebook) {
+        m_configMgr = m_notebook->getConfigMgr();
+        m_backend = m_notebook->getBackend();
+    }
+}
+
+Node::Node(Type p_type,
+           const QString &p_name,
+           Notebook *p_notebook,
+           Node *p_parent)
+    : m_type(p_type),
+      m_name(p_name),
+      m_notebook(p_notebook),
+      m_parent(p_parent)
+{
+    if (m_notebook) {
+        m_configMgr = m_notebook->getConfigMgr();
+        m_backend = m_notebook->getBackend();
+    }
+}
+
+Node::~Node()
+{
+}
+
+bool Node::isLoaded() const
+{
+    return m_loaded;
+}
+
+void Node::setLoaded(bool p_loaded)
+{
+    m_loaded = p_loaded;
+}
+
+void Node::loadInfo(ID p_id, const QDateTime &p_createdTimeUtc)
+{
+    Q_ASSERT(!m_loaded);
+
+    m_id = p_id;
+    m_createdTimeUtc = p_createdTimeUtc;
+    m_loaded = true;
+}
+
+bool Node::isRoot() const
+{
+    return !m_parent;
+}
+
+const QString &Node::getName() const
+{
+    return m_name;
+}
+
+bool Node::hasChild(const QString &p_name, bool p_caseSensitive) const
+{
+    return findChild(p_name, p_caseSensitive) != nullptr;
+}
+
+bool Node::hasChild(const QSharedPointer<Node> &p_node) const
+{
+    return getChildren().indexOf(p_node) != -1;
+}
+
+QSharedPointer<Node> Node::findChild(const QString &p_name, bool p_caseSensitive) const
+{
+    auto targetName = p_caseSensitive ? p_name : p_name.toLower();
+    for (auto &child : getChildren()) {
+        if (p_caseSensitive ? child->getName() == targetName
+                            : child->getName().toLower() == targetName) {
+            return child;
+        }
+    }
+
+    return nullptr;
+}
+
+void Node::setParent(Node *p_parent)
+{
+    m_parent = p_parent;
+}
+
+Node *Node::getParent() const
+{
+    return m_parent;
+}
+
+Node::Type Node::getType() const
+{
+    return m_type;
+}
+
+Node::Flags Node::getFlags() const
+{
+    return m_flags;
+}
+
+void Node::setFlags(Node::Flags p_flags)
+{
+    m_flags = p_flags;
+}
+
+Node::Use Node::getUse() const
+{
+    return m_use;
+}
+
+void Node::setUse(Node::Use p_use)
+{
+    m_use = p_use;
+}
+
+ID Node::getId() const
+{
+    return m_id;
+}
+
+const QDateTime &Node::getCreatedTimeUtc() const
+{
+    return m_createdTimeUtc;
+}
+
+Notebook *Node::getNotebook() const
+{
+    return m_notebook;
+}
+
+QString Node::fetchRelativePath() const
+{
+    if (!m_parent) {
+        return QString();
+    } else {
+        return PathUtils::concatenateFilePath(m_parent->fetchRelativePath(), m_name);
+    }
+}
+
+QString Node::fetchAbsolutePath() const
+{
+    return PathUtils::concatenateFilePath(m_notebook->getRootFolderAbsolutePath(),
+                                          fetchRelativePath());
+}
+
+QString Node::fetchContentPath() const
+{
+    return fetchAbsolutePath();
+}
+
+void Node::load()
+{
+    Q_ASSERT(m_notebook);
+    m_notebook->load(this);
+}
+
+void Node::save()
+{
+    Q_ASSERT(m_notebook);
+    m_notebook->save(this);
+}
+
+void Node::setName(const QString &p_name)
+{
+    m_name = p_name;
+}
+
+void Node::updateName(const QString &p_name)
+{
+    if (m_name == p_name) {
+        return;
+    }
+
+    m_notebook->rename(this, p_name);
+    Q_ASSERT(m_name == p_name);
+}
+
+bool Node::isAncestor(const Node *p_ancestor, const Node *p_child)
+{
+    if (!p_ancestor || !p_child) {
+        return false;
+    }
+
+    while (p_child) {
+        p_child = p_child->getParent();
+        if (p_child == p_ancestor) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
+bool Node::existsOnDisk() const
+{
+    return m_configMgr->nodeExistsOnDisk(this);
+}
+
+QString Node::read() const
+{
+    return m_configMgr->readNode(this);
+}
+
+void Node::write(const QString &p_content)
+{
+    m_configMgr->writeNode(this, p_content);
+}
+
+QString Node::fetchImageFolderPath()
+{
+    return m_configMgr->fetchNodeImageFolderPath(this);
+}
+
+QString Node::insertImage(const QString &p_srcImagePath, const QString &p_imageFileName)
+{
+    const auto imageFolderPath = fetchImageFolderPath();
+    auto destFilePath = m_backend->renameIfExistsCaseInsensitive(PathUtils::concatenateFilePath(imageFolderPath, p_imageFileName));
+    m_backend->copyFile(p_srcImagePath, destFilePath);
+    return destFilePath;
+}
+
+QString Node::insertImage(const QImage &p_image, const QString &p_imageFileName)
+{
+    const auto imageFolderPath = fetchImageFolderPath();
+    auto destFilePath = m_backend->renameIfExistsCaseInsensitive(PathUtils::concatenateFilePath(imageFolderPath, p_imageFileName));
+    p_image.save(destFilePath);
+    m_backend->addFile(destFilePath);
+    return destFilePath;
+}
+
+void Node::removeImage(const QString &p_imagePath)
+{
+    // Just move it to recycle bin but not added as a child node of recycle bin.
+    m_notebook->moveFileToRecycleBin(p_imagePath);
+}
+
+QString Node::getAttachmentFolder() const
+{
+    Q_ASSERT(false);
+    return QString();
+}
+
+void Node::setAttachmentFolder(const QString &p_folder)
+{
+    Q_UNUSED(p_folder);
+    Q_ASSERT(false);
+}
+
+QString Node::fetchAttachmentFolderPath()
+{
+    return m_configMgr->fetchNodeAttachmentFolderPath(this);
+}
+
+QStringList Node::addAttachment(const QString &p_destFolderPath, const QStringList &p_files)
+{
+    Q_UNUSED(p_destFolderPath);
+    Q_UNUSED(p_files);
+    Q_ASSERT(false);
+    return QStringList();
+}
+
+QString Node::newAttachmentFile(const QString &p_destFolderPath, const QString &p_name)
+{
+    Q_UNUSED(p_destFolderPath);
+    Q_UNUSED(p_name);
+    Q_ASSERT(false);
+    return QString();
+}
+
+QString Node::newAttachmentFolder(const QString &p_destFolderPath, const QString &p_name)
+{
+    Q_UNUSED(p_destFolderPath);
+    Q_UNUSED(p_name);
+    Q_ASSERT(false);
+    return QString();
+}
+
+QString Node::renameAttachment(const QString &p_path, const QString &p_name)
+{
+    Q_UNUSED(p_path);
+    Q_UNUSED(p_name);
+    Q_ASSERT(false);
+    return QString();
+}
+
+void Node::removeAttachment(const QStringList &p_paths)
+{
+    Q_UNUSED(p_paths);
+    Q_ASSERT(false);
+}
+
+QStringList Node::getTags() const
+{
+    Q_ASSERT(false);
+    return QStringList();
+}
+
+QDir Node::toDir() const
+{
+    Q_ASSERT(false);
+    return QDir();
+}
+
+INotebookBackend *Node::getBackend() const
+{
+    return m_backend.data();
+}
+
+bool Node::isReadOnly() const
+{
+    return m_flags & Flag::ReadOnly;
+}

+ 196 - 0
src/core/notebook/node.h

@@ -0,0 +1,196 @@
+#ifndef NODE_H
+#define NODE_H
+
+#include <QDateTime>
+#include <QVector>
+#include <QSharedPointer>
+#include <QDir>
+
+#include <global.h>
+
+namespace vnotex
+{
+    class Notebook;
+    class INotebookConfigMgr;
+    class INotebookBackend;
+
+    // Used when add/new a node.
+    struct NodeParameters
+    {
+        QDateTime m_createdTimeUtc = QDateTime::currentDateTimeUtc();
+        QDateTime m_modifiedTimeUtc = QDateTime::currentDateTimeUtc();
+        QString m_attachmentFolder;
+        QStringList m_tags;
+    };
+
+    // Node of notebook.
+    class Node
+    {
+    public:
+        enum Type {
+            Folder,
+            File
+        };
+
+        enum Flag {
+            None = 0,
+            ReadOnly = 0x1
+        };
+        Q_DECLARE_FLAGS(Flags, Flag)
+
+        enum Use {
+            Normal,
+            RecycleBin
+        };
+
+        // Constructor with all information loaded.
+        Node(Type p_type,
+             ID p_id,
+             const QString &p_name,
+             const QDateTime &p_createdTimeUtc,
+             Notebook *p_notebook,
+             Node *p_parent = nullptr);
+
+        // Constructor not loaded.
+        Node(Type p_type,
+             const QString &p_name,
+             Notebook *p_notebook,
+             Node *p_parent = nullptr);
+
+        virtual ~Node();
+
+        enum { InvalidId = 0 };
+
+        bool isLoaded() const;
+
+        bool isRoot() const;
+
+        const QString &getName() const;
+        void setName(const QString &p_name);
+
+        // Change the config and backend file as well.
+        void updateName(const QString &p_name);
+
+        Node::Type getType() const;
+
+        Node::Flags getFlags() const;
+        void setFlags(Node::Flags p_flags);
+
+        Node::Use getUse() const;
+        void setUse(Node::Use p_use);
+
+        ID getId() const;
+
+        const QDateTime &getCreatedTimeUtc() const;
+
+        virtual QDateTime getModifiedTimeUtc() const = 0;
+        virtual void setModifiedTimeUtc() = 0;
+
+        virtual QString getAttachmentFolder() const;
+        virtual void setAttachmentFolder(const QString &p_folder);
+
+        virtual QVector<QSharedPointer<Node>> getChildren() const = 0;
+
+        virtual int getChildrenCount() const = 0;
+
+        QSharedPointer<Node> findChild(const QString &p_name, bool p_caseSensitive = true) const;
+
+        bool hasChild(const QString &p_name, bool p_caseSensitive = true) const;
+
+        bool hasChild(const QSharedPointer<Node> &p_node) const;
+
+        virtual void addChild(const QSharedPointer<Node> &p_node) = 0;
+
+        virtual void insertChild(int p_idx, const QSharedPointer<Node> &p_node) = 0;
+
+        virtual void removeChild(const QSharedPointer<Node> &p_node) = 0;
+
+        void setParent(Node *p_parent);
+        Node *getParent() const;
+
+        Notebook *getNotebook() const;
+
+        // Path to the node.
+        QString fetchRelativePath() const;
+
+        QString fetchAbsolutePath() const;
+
+        // A node may be a container of all the stuffs, so the node's path may not be identical with
+        // the content file path, like TextBundle.
+        virtual QString fetchContentPath() const;
+
+        // Get image folder path.
+        virtual QString fetchImageFolderPath();
+
+        virtual void load();
+        virtual void save();
+
+        static bool isAncestor(const Node *p_ancestor, const Node *p_child);
+
+        bool existsOnDisk() const;
+
+        QString read() const;
+        void write(const QString &p_content);
+
+        // Insert image from @p_srcImagePath.
+        // Return inserted image file path.
+        virtual QString insertImage(const QString &p_srcImagePath, const QString &p_imageFileName);
+
+        virtual QString insertImage(const QImage &p_image, const QString &p_imageFileName);
+
+        virtual void removeImage(const QString &p_imagePath);
+
+        // Get attachment folder path.
+        virtual QString fetchAttachmentFolderPath();
+
+        virtual QStringList addAttachment(const QString &p_destFolderPath, const QStringList &p_files);
+
+        virtual QString newAttachmentFile(const QString &p_destFolderPath, const QString &p_name);
+
+        virtual QString newAttachmentFolder(const QString &p_destFolderPath, const QString &p_name);
+
+        virtual QString renameAttachment(const QString &p_path, const QString &p_name);
+
+        virtual void removeAttachment(const QStringList &p_paths);
+
+        virtual QStringList getTags() const;
+
+        virtual QDir toDir() const;
+
+        INotebookBackend *getBackend() const;
+
+        bool isReadOnly() const;
+
+    protected:
+        void loadInfo(ID p_id, const QDateTime &p_createdTimeUtc);
+
+        void setLoaded(bool p_loaded);
+
+        Notebook *m_notebook = nullptr;
+
+        QSharedPointer<INotebookConfigMgr> m_configMgr;
+
+        QSharedPointer<INotebookBackend> m_backend;
+
+    private:
+        Type m_type = Type::Folder;
+
+        Flags m_flags = Flag::None;
+
+        Use m_use = Use::Normal;
+
+        ID m_id = InvalidId;
+
+        QString m_name;
+
+        QDateTime m_createdTimeUtc;
+
+        bool m_loaded = false;
+
+        Node *m_parent = nullptr;
+    };
+
+    Q_DECLARE_OPERATORS_FOR_FLAGS(Node::Flags)
+} // ns vnotex
+
+#endif // NODE_H

+ 380 - 0
src/core/notebook/notebook.cpp

@@ -0,0 +1,380 @@
+#include "notebook.h"
+
+#include <QFileInfo>
+
+#include <versioncontroller/iversioncontroller.h>
+#include <notebookbackend/inotebookbackend.h>
+#include <notebookconfigmgr/inotebookconfigmgr.h>
+#include <utils/pathutils.h>
+#include <utils/fileutils.h>
+#include "exception.h"
+
+using namespace vnotex;
+
+const QString Notebook::c_defaultAttachmentFolder = QStringLiteral("vx_attachments");
+
+const QString Notebook::c_defaultImageFolder = QStringLiteral("vx_images");
+
+static vnotex::ID generateNotebookID()
+{
+    static vnotex::ID id = Notebook::InvalidId;
+    return ++id;
+}
+
+Notebook::Notebook(const NotebookParameters &p_paras,
+                   QObject *p_parent)
+    : QObject(p_parent),
+      m_id(generateNotebookID()),
+      m_type(p_paras.m_type),
+      m_name(p_paras.m_name),
+      m_description(p_paras.m_description),
+      m_rootFolderPath(p_paras.m_rootFolderPath),
+      m_icon(p_paras.m_icon),
+      m_imageFolder(p_paras.m_imageFolder),
+      m_attachmentFolder(p_paras.m_attachmentFolder),
+      m_createdTimeUtc(p_paras.m_createdTimeUtc),
+      m_backend(p_paras.m_notebookBackend),
+      m_versionController(p_paras.m_versionController),
+      m_configMgr(p_paras.m_notebookConfigMgr)
+{
+    if (m_imageFolder.isEmpty()) {
+        m_imageFolder = c_defaultImageFolder;
+    }
+    if (m_attachmentFolder.isEmpty()) {
+        m_attachmentFolder = c_defaultAttachmentFolder;
+    }
+    m_configMgr->setNotebook(this);
+}
+
+Notebook::~Notebook()
+{
+}
+
+vnotex::ID Notebook::getId() const
+{
+    return m_id;
+}
+
+const QString &Notebook::getType() const
+{
+    return m_type;
+}
+
+const QString &Notebook::getName() const
+{
+    return m_name;
+}
+
+void Notebook::setName(const QString &p_name)
+{
+    m_name = p_name;
+}
+
+void Notebook::updateName(const QString &p_name)
+{
+    Q_ASSERT(!p_name.isEmpty());
+    if (p_name == m_name) {
+        return;
+    }
+
+    m_name = p_name;
+    updateNotebookConfig();
+    emit updated();
+}
+
+const QString &Notebook::getDescription() const
+{
+    return m_description;
+}
+
+void Notebook::setDescription(const QString &p_description)
+{
+    m_description = p_description;
+}
+
+void Notebook::updateDescription(const QString &p_description)
+{
+    if (p_description == m_description) {
+        return;
+    }
+
+    m_description = p_description;
+    updateNotebookConfig();
+    emit updated();
+}
+
+const QString &Notebook::getRootFolderPath() const
+{
+    return m_rootFolderPath;
+}
+
+QString Notebook::getRootFolderAbsolutePath() const
+{
+    return PathUtils::absolutePath(m_rootFolderPath);
+}
+
+const QIcon &Notebook::getIcon() const
+{
+    return m_icon;
+}
+
+void Notebook::setIcon(const QIcon &p_icon)
+{
+    m_icon = p_icon;
+}
+
+const QString &Notebook::getImageFolder() const
+{
+    return m_imageFolder;
+}
+
+const QString &Notebook::getAttachmentFolder() const
+{
+    return m_attachmentFolder;
+}
+
+const QSharedPointer<INotebookBackend> &Notebook::getBackend() const
+{
+    return m_backend;
+}
+
+const QSharedPointer<IVersionController> &Notebook::getVersionController() const
+{
+    return m_versionController;
+}
+
+const QSharedPointer<INotebookConfigMgr> &Notebook::getConfigMgr() const
+{
+    return m_configMgr;
+}
+
+const QSharedPointer<Node> &Notebook::getRootNode() const
+{
+    if (!m_root) {
+        const_cast<Notebook *>(this)->m_root = m_configMgr->loadRootNode();
+    }
+
+    return m_root;
+}
+
+QSharedPointer<Node> Notebook::getRecycleBinNode() const
+{
+    auto root = getRootNode();
+    auto children = root->getChildren();
+    auto it = std::find_if(children.begin(),
+                           children.end(),
+                           [this](const QSharedPointer<Node> &p_node) {
+                               return isRecycleBinNode(p_node.data());
+                           });
+
+    if (it != children.end()) {
+        return *it;
+    }
+
+    return nullptr;
+}
+
+QSharedPointer<Node> Notebook::newNode(Node *p_parent, Node::Type p_type, const QString &p_name)
+{
+    return m_configMgr->newNode(p_parent, p_type, p_name);
+}
+
+const QDateTime &Notebook::getCreatedTimeUtc() const
+{
+    return m_createdTimeUtc;
+}
+
+void Notebook::load(Node *p_node)
+{
+    Q_ASSERT(p_node->getNotebook() == this);
+    if (p_node->isLoaded()) {
+        return;
+    }
+
+    m_configMgr->loadNode(p_node);
+}
+
+void Notebook::save(const Node *p_node)
+{
+    Q_ASSERT(p_node->getNotebook() == this);
+    m_configMgr->saveNode(p_node);
+}
+
+void Notebook::rename(Node *p_node, const QString &p_name)
+{
+    Q_ASSERT(p_node->getNotebook() == this);
+    m_configMgr->renameNode(p_node, p_name);
+
+    emit nodeUpdated(p_node);
+}
+
+QSharedPointer<Node> Notebook::loadNodeByPath(const QString &p_path)
+{
+    if (!PathUtils::pathContains(m_rootFolderPath, p_path)) {
+        return nullptr;
+    }
+
+    QString relativePath;
+    QFileInfo fi(p_path);
+    if (fi.isAbsolute()) {
+        if (!fi.exists()) {
+            return nullptr;
+        }
+
+        relativePath = PathUtils::relativePath(m_rootFolderPath, p_path);
+    } else {
+        relativePath = p_path;
+    }
+
+    return m_configMgr->loadNodeByPath(m_root, relativePath);
+}
+
+QSharedPointer<Node> Notebook::copyNodeAsChildOf(const QSharedPointer<Node> &p_src, Node *p_dest, bool p_move)
+{
+    Q_ASSERT(p_src != p_dest);
+    Q_ASSERT(p_dest->getNotebook() == this);
+
+    if (Node::isAncestor(p_src.data(), p_dest)) {
+        Exception::throwOne(Exception::Type::InvalidArgument,
+                            QString("source (%1) is the ancestor of destination (%2)")
+                                   .arg(p_src->fetchRelativePath(), p_dest->fetchRelativePath()));
+        return nullptr;
+    }
+
+    if (p_src->getParent() == p_dest && p_move) {
+        return p_src;
+    }
+
+    return m_configMgr->copyNodeAsChildOf(p_src, p_dest, p_move);
+}
+
+void Notebook::removeNode(const QSharedPointer<Node> &p_node, bool p_force, bool p_configOnly)
+{
+    Q_ASSERT(p_node->getNotebook() == this);
+    m_configMgr->removeNode(p_node, p_force, p_configOnly);
+}
+
+void Notebook::removeNode(const Node *p_node, bool p_force, bool p_configOnly)
+{
+    Q_ASSERT(p_node && !p_node->isRoot());
+    auto children = p_node->getParent()->getChildren();
+    auto it = std::find(children.begin(), children.end(), p_node);
+    Q_ASSERT(it != children.end());
+    removeNode(*it, p_force, p_configOnly);
+}
+
+bool Notebook::isRecycleBinNode(const Node *p_node) const
+{
+    return p_node && p_node->getUse() == Node::Use::RecycleBin;
+}
+
+bool Notebook::isNodeInRecycleBin(const Node *p_node) const
+{
+    if (p_node) {
+        p_node = p_node->getParent();
+        while (p_node) {
+            if (isRecycleBinNode(p_node)) {
+                return true;
+            }
+
+            p_node = p_node->getParent();
+        }
+    }
+
+    return false;
+}
+
+void Notebook::moveNodeToRecycleBin(const Node *p_node)
+{
+    Q_ASSERT(p_node && !p_node->isRoot());
+    auto children = p_node->getParent()->getChildren();
+    for (auto &child : children) {
+        if (p_node == child) {
+            moveNodeToRecycleBin(child);
+            return;
+        }
+    }
+
+    Q_ASSERT(false);
+}
+
+void Notebook::moveNodeToRecycleBin(const QSharedPointer<Node> &p_node)
+{
+    auto destNode = getOrCreateRecycleBinDateNode();
+    copyNodeAsChildOf(p_node, destNode.data(), true);
+}
+
+QSharedPointer<Node> Notebook::getOrCreateRecycleBinDateNode()
+{
+    // Name after date.
+    auto dateNodeName = QDate::currentDate().toString(QStringLiteral("yyyyMMdd"));
+
+    auto recycleBinNode = getRecycleBinNode();
+    auto dateNode = recycleBinNode->findChild(dateNodeName,
+                                              FileUtils::isPlatformNameCaseSensitive());
+    if (!dateNode) {
+        // Create a date node.
+        dateNode = newNode(recycleBinNode.data(), Node::Type::Folder, dateNodeName);
+    }
+
+    return dateNode;
+}
+
+void Notebook::emptyNode(const Node *p_node, bool p_force)
+{
+    auto children = p_node->getChildren();
+    for (auto &child : children) {
+        removeNode(child, p_force);
+    }
+}
+
+void Notebook::moveFileToRecycleBin(const QString &p_filePath)
+{
+    auto node = getOrCreateRecycleBinDateNode();
+    auto destFilePath = PathUtils::concatenateFilePath(node->fetchRelativePath(),
+                                                       PathUtils::fileName(p_filePath));
+    destFilePath = getBackend()->renameIfExistsCaseInsensitive(destFilePath);
+    m_backend->copyFile(p_filePath, destFilePath);
+
+    getBackend()->removeFile(p_filePath);
+
+    emit nodeUpdated(node.data());
+}
+
+void Notebook::moveDirToRecycleBin(const QString &p_dirPath)
+{
+    auto node = getOrCreateRecycleBinDateNode();
+    auto destDirPath = PathUtils::concatenateFilePath(node->fetchRelativePath(),
+                                                       PathUtils::fileName(p_dirPath));
+    destDirPath = getBackend()->renameIfExistsCaseInsensitive(destDirPath);
+    m_backend->copyDir(p_dirPath, destDirPath);
+
+    getBackend()->removeDir(p_dirPath);
+
+    emit nodeUpdated(node.data());
+}
+
+QSharedPointer<Node> Notebook::addAsNode(Node *p_parent,
+                                         Node::Type p_type,
+                                         const QString &p_name,
+                                         const NodeParameters &p_paras)
+{
+    return m_configMgr->addAsNode(p_parent, p_type, p_name, p_paras);
+}
+
+bool Notebook::isBuiltInFile(const Node *p_node, const QString &p_name) const
+{
+    return m_configMgr->isBuiltInFile(p_node, p_name);
+}
+
+bool Notebook::isBuiltInFolder(const Node *p_node, const QString &p_name) const
+{
+    return m_configMgr->isBuiltInFolder(p_node, p_name);
+}
+
+QSharedPointer<Node> Notebook::copyAsNode(Node *p_parent,
+                                          Node::Type p_type,
+                                          const QString &p_path)
+{
+    return m_configMgr->copyAsNode(p_parent, p_type, p_path);
+}

+ 184 - 0
src/core/notebook/notebook.h

@@ -0,0 +1,184 @@
+#ifndef NOTEBOOK_H
+#define NOTEBOOK_H
+
+#include <QObject>
+#include <QIcon>
+#include <QSharedPointer>
+
+#include "notebookparameters.h"
+#include "../global.h"
+#include "node.h"
+
+namespace vnotex
+{
+    class INotebookBackend;
+    class IVersionController;
+    class INotebookConfigMgr;
+    struct NodeParameters;
+
+    // Base class of notebook.
+    class Notebook : public QObject
+    {
+        Q_OBJECT
+    public:
+        Notebook(const NotebookParameters &p_paras,
+                 QObject *p_parent = nullptr);
+
+        virtual ~Notebook();
+
+        enum { InvalidId = 0 };
+
+        ID getId() const;
+
+        const QString &getType() const;
+
+        const QString &getName() const;
+        void setName(const QString &p_name);
+        // Change the config and backend file as well.
+        void updateName(const QString &p_name);
+
+        const QString &getDescription() const;
+        void setDescription(const QString &p_description);
+        void updateDescription(const QString &p_description);
+
+        // Use getRootFolderAbsolutePath() instead for access.
+        const QString &getRootFolderPath() const;
+
+        QString getRootFolderAbsolutePath() const;
+
+        const QIcon &getIcon() const;
+        void setIcon(const QIcon &p_icon);
+
+        const QString &getImageFolder() const;
+
+        const QString &getAttachmentFolder() const;
+
+        const QDateTime &getCreatedTimeUtc() const;
+
+        const QSharedPointer<INotebookBackend> &getBackend() const;
+
+        const QSharedPointer<IVersionController> &getVersionController() const;
+
+        const QSharedPointer<INotebookConfigMgr> &getConfigMgr() const;
+
+        const QSharedPointer<Node> &getRootNode() const;
+
+        QSharedPointer<Node> getRecycleBinNode() const;
+
+        QSharedPointer<Node> newNode(Node *p_parent, Node::Type p_type, const QString &p_name);
+
+        // Add @p_name under @p_parent to add as a new node @p_type.
+        QSharedPointer<Node> addAsNode(Node *p_parent,
+                                       Node::Type p_type,
+                                       const QString &p_name,
+                                       const NodeParameters &p_paras);
+
+        // Copy @p_path to @p_parent and add as a new node @p_type.
+        QSharedPointer<Node> copyAsNode(Node *p_parent,
+                                        Node::Type p_type,
+                                        const QString &p_path);
+
+        virtual ID getNextNodeId() const = 0;
+
+        virtual ID getAndUpdateNextNodeId() = 0;
+
+        virtual void load(Node *p_node);
+        virtual void save(const Node *p_node);
+
+        virtual void rename(Node *p_node, const QString &p_name);
+
+        virtual void updateNotebookConfig() = 0;
+
+        virtual void removeNotebookConfig() = 0;
+
+        // @p_path could be absolute or relative.
+        virtual QSharedPointer<Node> loadNodeByPath(const QString &p_path);
+
+        // Copy @p_src as a child of @p_dest. They may belong to different notebooks.
+        virtual QSharedPointer<Node> copyNodeAsChildOf(const QSharedPointer<Node> &p_src, Node *p_dest, bool p_move);
+
+        // Remove @p_node and delete all related files from disk.
+        // @p_force: if true, will delete all files including files not tracked by configmgr.
+        // @p_configOnly: if true, will just remove node from config.
+        void removeNode(const QSharedPointer<Node> &p_node, bool p_force = false, bool p_configOnly = false);
+
+        void removeNode(const Node *p_node, bool p_force = false, bool p_configOnly = false);
+
+        void moveNodeToRecycleBin(const QSharedPointer<Node> &p_node);
+
+        void moveNodeToRecycleBin(const Node *p_node);
+
+        // Move @p_filePath to the recycle bin, without adding it as a child node.
+        void moveFileToRecycleBin(const QString &p_filePath);
+
+        // Move @p_dirPath to the recycle bin, without adding it as a child node.
+        void moveDirToRecycleBin(const QString &p_dirPath);
+
+        // Remove all files of this notebook from disk.
+        virtual void remove() = 0;
+
+        bool isRecycleBinNode(const Node *p_node) const;
+
+        bool isNodeInRecycleBin(const Node *p_node) const;
+
+        // Remove all children node of @p_node.
+        // @p_force: if true, just delete all folders and files under @p_node.
+        void emptyNode(const Node *p_node, bool p_force = false);
+
+        // Whether @p_name is a built-in file under @p_node.
+        bool isBuiltInFile(const Node *p_node, const QString &p_name) const;
+
+        bool isBuiltInFolder(const Node *p_node, const QString &p_name) const;
+
+        static const QString c_defaultAttachmentFolder;
+
+        static const QString c_defaultImageFolder;
+
+    signals:
+        void updated();
+
+        void nodeUpdated(const Node *p_node);
+
+    private:
+        QSharedPointer<Node> getOrCreateRecycleBinDateNode();
+
+        // ID of this notebook.
+        // Will be assigned uniquely once loaded.
+        ID m_id;
+
+        // Type of this notebook.
+        QString m_type;
+
+        // Name of this notebook.
+        QString m_name;
+
+        // Description of this notebook.
+        QString m_description;
+
+        // Path of the notebook root folder.
+        QString m_rootFolderPath;
+
+        QIcon m_icon;
+
+        // Name of the folder to hold images.
+        QString m_imageFolder;
+
+        // Name of the folder to hold attachments.
+        QString m_attachmentFolder;
+
+        QDateTime m_createdTimeUtc;
+
+        // Backend for file access and synchronization.
+        QSharedPointer<INotebookBackend> m_backend;
+
+        // Version controller.
+        QSharedPointer<IVersionController> m_versionController;
+
+        // Config manager to read/wirte config files.
+        QSharedPointer<INotebookConfigMgr> m_configMgr;
+
+        QSharedPointer<Node> m_root;
+    };
+} // ns vnotex
+
+#endif // NOTEBOOK_H

+ 18 - 0
src/core/notebook/notebook.pri

@@ -0,0 +1,18 @@
+SOURCES += \
+    $$PWD/notebook.cpp \
+    $$PWD/bundlenotebookfactory.cpp \
+    $$PWD/notebookparameters.cpp \
+    $$PWD/bundlenotebook.cpp \
+    $$PWD/node.cpp \
+    $$PWD/filenode.cpp \
+    $$PWD/foldernode.cpp
+
+HEADERS += \
+    $$PWD/notebook.h \
+    $$PWD/inotebookfactory.h \
+    $$PWD/bundlenotebookfactory.h \
+    $$PWD/notebookparameters.h \
+    $$PWD/bundlenotebook.h \
+    $$PWD/node.h \
+    $$PWD/filenode.h \
+    $$PWD/foldernode.h

+ 64 - 0
src/core/notebook/notebookparameters.cpp

@@ -0,0 +1,64 @@
+#include "notebookparameters.h"
+
+#include "notebookmgr.h"
+
+using namespace vnotex;
+
+QSharedPointer<NotebookParameters> NotebookParameters::createNotebookParameters(
+        const NotebookMgr &p_mgr,
+        const QString &p_type,
+        const QString &p_name,
+        const QString &p_description,
+        const QString &p_rootFolderPath,
+        const QIcon &p_icon,
+        const QString &p_imageFolder,
+        const QString &p_attachmentFolder,
+        const QDateTime &p_createdTimeUtc,
+        const QString &p_backend,
+        const QString &p_versionController,
+        const QString &p_configMgr)
+{
+    auto backend = p_mgr.createNotebookBackend(p_backend, p_rootFolderPath);
+    return createNotebookParameters(p_mgr,
+                                    backend,
+                                    p_type,
+                                    p_name,
+                                    p_description,
+                                    p_rootFolderPath,
+                                    p_icon,
+                                    p_imageFolder,
+                                    p_attachmentFolder,
+                                    p_createdTimeUtc,
+                                    p_versionController,
+                                    p_configMgr);
+}
+
+QSharedPointer<NotebookParameters> NotebookParameters::createNotebookParameters(
+                const NotebookMgr &p_mgr,
+                const QSharedPointer<INotebookBackend> &p_backend,
+                const QString &p_type,
+                const QString &p_name,
+                const QString &p_description,
+                const QString &p_rootFolderPath,
+                const QIcon &p_icon,
+                const QString &p_imageFolder,
+                const QString &p_attachmentFolder,
+                const QDateTime &p_createdTimeUtc,
+                const QString &p_versionController,
+                const QString &p_configMgr)
+{
+    auto paras = QSharedPointer<NotebookParameters>::create();
+    paras->m_type = p_type;
+    paras->m_name = p_name;
+    paras->m_description = p_description;
+    paras->m_rootFolderPath = p_rootFolderPath;
+    paras->m_icon = p_icon;
+    paras->m_imageFolder = p_imageFolder;
+    paras->m_attachmentFolder = p_attachmentFolder;
+    paras->m_createdTimeUtc = p_createdTimeUtc;
+    paras->m_notebookBackend = p_backend;
+    paras->m_versionController = p_mgr.createVersionController(p_versionController);
+    paras->m_notebookConfigMgr = p_mgr.createNotebookConfigMgr(p_configMgr,
+                                                               paras->m_notebookBackend);
+    return paras;
+}

+ 70 - 0
src/core/notebook/notebookparameters.h

@@ -0,0 +1,70 @@
+#ifndef NOTEBOOKPARAMETERS_H
+#define NOTEBOOKPARAMETERS_H
+
+#include <QSharedPointer>
+#include <QIcon>
+#include <QDateTime>
+
+namespace vnotex
+{
+    class NotebookMgr;
+    class INotebookBackend;
+    class IVersionController;
+    class INotebookConfigMgr;
+
+    // Used to new a notebook.
+    class NotebookParameters
+    {
+    public:
+        virtual ~NotebookParameters() {}
+
+        static QSharedPointer<NotebookParameters> createNotebookParameters(
+                const NotebookMgr &p_mgr,
+                const QString &p_type,
+                const QString &p_name,
+                const QString &p_description,
+                const QString &p_rootFolderPath,
+                const QIcon &p_icon,
+                const QString &p_imageFolder,
+                const QString &p_attachmentFolder,
+                const QDateTime &p_createdTimeUtc,
+                const QString &p_backend,
+                const QString &p_versionController,
+                const QString &p_configMgr);
+
+        static QSharedPointer<NotebookParameters> createNotebookParameters(
+                const NotebookMgr &p_mgr,
+                const QSharedPointer<INotebookBackend> &p_backend,
+                const QString &p_type,
+                const QString &p_name,
+                const QString &p_description,
+                const QString &p_rootFolderPath,
+                const QIcon &p_icon,
+                const QString &p_imageFolder,
+                const QString &p_attachmentFolder,
+                const QDateTime &p_createdTimeUtc,
+                const QString &p_versionController,
+                const QString &p_configMgr);
+
+        QString m_type;
+        QString m_name;
+        QString m_description;
+        QString m_rootFolderPath;
+        QIcon m_icon;
+
+        // Name of image folder.
+        QString m_imageFolder;
+
+        // Name of attachment folder.
+        QString m_attachmentFolder;
+
+        QDateTime m_createdTimeUtc;
+        QSharedPointer<INotebookBackend> m_notebookBackend;
+        QSharedPointer<IVersionController> m_versionController;
+        QSharedPointer<INotebookConfigMgr> m_notebookConfigMgr;
+
+        bool m_ensureEmptyRootFolder = true;
+    };
+} // ns vnotex
+
+#endif // NOTEBOOKPARAMETERS_H

+ 23 - 0
src/core/notebookbackend/inotebookbackend.cpp

@@ -0,0 +1,23 @@
+#include "inotebookbackend.h"
+
+#include <QDir>
+
+#include <exception.h>
+#include <utils/pathutils.h>
+
+using namespace vnotex;
+
+void INotebookBackend::constrainPath(const QString &p_path) const
+{
+    if (!PathUtils::pathContains(m_rootPath, p_path)) {
+        Exception::throwOne(Exception::Type::InvalidArgument,
+                            QString("path (%1) does not locate in root folder (%2)")
+                                   .arg(p_path, m_rootPath));
+    }
+}
+
+QString INotebookBackend::getFullPath(const QString &p_path) const
+{
+    constrainPath(p_path);
+    return QDir(m_rootPath).filePath(p_path);
+}

+ 111 - 0
src/core/notebookbackend/inotebookbackend.h

@@ -0,0 +1,111 @@
+#ifndef INOTEBOOKBACKEND_H
+#define INOTEBOOKBACKEND_H
+
+#include <QObject>
+
+#include <utils/pathutils.h>
+
+class QByteArray;
+class QJsonObject;
+
+namespace vnotex
+{
+    // Abstract class for notebook backend, which is responsible for file access
+    // and synchronization.
+    class INotebookBackend : public QObject
+    {
+        Q_OBJECT
+    public:
+        INotebookBackend(const QString &p_rootPath, QObject *p_parent = nullptr)
+            : QObject(p_parent),
+              m_rootPath(PathUtils::absolutePath(p_rootPath))
+        {
+        }
+
+        virtual ~INotebookBackend()
+        {
+        }
+
+        virtual QString getName() const = 0;
+
+        virtual QString getDisplayName() const = 0;
+
+        virtual QString getDescription() const = 0;
+
+        const QString &getRootPath() const
+        {
+            return m_rootPath;
+        }
+
+        void setRootPath(const QString &p_rootPath)
+        {
+            m_rootPath = p_rootPath;
+        }
+
+        // Whether @p_dirPath is an empty directory.
+        virtual bool isEmptyDir(const QString &p_dirPath) const = 0;
+
+        // Create the directory path @p_dirPath. Create all parent directories if necessary.
+        virtual void makePath(const QString &p_dirPath) = 0;
+
+        // Write @p_data to @p_filePath.
+        virtual void writeFile(const QString &p_filePath, const QByteArray &p_data) = 0;
+
+        // Write @p_text to @p_filePath.
+        virtual void writeFile(const QString &p_filePath, const QString &p_text) = 0;
+
+        // Write @p_jobj to @p_filePath.
+        virtual void writeFile(const QString &p_filePath, const QJsonObject &p_jobj) = 0;
+
+        // Read content from @p_filePath.
+        virtual QString readTextFile(const QString &p_filePath) = 0;
+
+        // Read file @p_filePath.
+        virtual QByteArray readFile(const QString &p_filePath) = 0;
+
+        QString getFullPath(const QString &p_path) const;
+
+        virtual bool exists(const QString &p_path) const = 0;
+
+        virtual bool childExistsCaseInsensitive(const QString &p_dirPath, const QString &p_name) const = 0;
+
+        virtual bool isFile(const QString &p_path) const = 0;
+
+        virtual void renameFile(const QString &p_filePath, const QString &p_name) = 0;
+
+        virtual void renameDir(const QString &p_dirPath, const QString &p_name) = 0;
+
+        // Copy @p_filePath to @p_destPath.
+        // @p_filePath could be outside notebook.
+        virtual void copyFile(const QString &p_filePath, const QString &p_destPath) = 0;
+
+        // Delete @p_filePath from disk.
+        virtual void removeFile(const QString &p_filePath) = 0;
+
+        // Copy  @p_dirPath to as @p_destPath.
+        virtual void copyDir(const QString &p_dirPath, const QString &p_destPath) = 0;
+
+        // Delete @p_dirPath from disk if it is empty.
+        // Return false if it is not deleted due to non-empty.
+        virtual bool removeDirIfEmpty(const QString &p_dirPath) = 0;
+
+        virtual void removeDir(const QString &p_dirPath) = 0;
+
+        virtual QString renameIfExistsCaseInsensitive(const QString &p_path) const = 0;
+
+        // Add one file to backend.
+        virtual void addFile(const QString &p_path) = 0;
+
+        virtual void removeEmptyDir(const QString &p_dirPath) = 0;
+
+    protected:
+        // Constrain @p_path within root path of the notebook.
+        void constrainPath(const QString &p_path) const;
+
+    private:
+        // Root path of the notebook.
+        QString m_rootPath;
+    };
+} // ns vnotex
+
+#endif // INOTEBOOKBACKEND_H

+ 31 - 0
src/core/notebookbackend/inotebookbackendfactory.h

@@ -0,0 +1,31 @@
+#ifndef INOTEBOOKBACKENDFACTORY_H
+#define INOTEBOOKBACKENDFACTORY_H
+
+#include <QSharedPointer>
+
+namespace vnotex
+{
+    class INotebookBackend;
+
+    class INotebookBackendFactory
+    {
+    public:
+        INotebookBackendFactory()
+        {
+        }
+
+        virtual ~INotebookBackendFactory()
+        {
+        }
+
+        virtual QString getName() const = 0;
+
+        virtual QString getDisplayName() const = 0;
+
+        virtual QString getDescription() const = 0;
+
+        virtual QSharedPointer<INotebookBackend> createNotebookBackend(const QString &p_rootPath) = 0;
+    };
+} // ns vnotex
+
+#endif // INOTEBOOKBACKENDFACTORY_H

+ 170 - 0
src/core/notebookbackend/localnotebookbackend.cpp

@@ -0,0 +1,170 @@
+#include "localnotebookbackend.h"
+
+#include <QDir>
+#include <QFile>
+#include <QTextStream>
+#include <QJsonObject>
+#include <QJsonDocument>
+
+#include <utils/pathutils.h>
+#include "exception.h"
+#include <utils/fileutils.h>
+
+using namespace vnotex;
+
+LocalNotebookBackend::LocalNotebookBackend(const QString &p_name,
+                                           const QString &p_displayName,
+                                           const QString &p_description,
+                                           const QString &p_rootPath,
+                                           QObject *p_parent)
+    : INotebookBackend(p_rootPath, p_parent),
+      m_info(p_name, p_displayName, p_description)
+{
+}
+
+QString LocalNotebookBackend::getName() const
+{
+    return m_info.m_name;
+}
+
+QString LocalNotebookBackend::getDisplayName() const
+{
+    return m_info.m_displayName;
+}
+
+QString LocalNotebookBackend::getDescription() const
+{
+    return m_info.m_description;
+}
+
+bool LocalNotebookBackend::isEmptyDir(const QString &p_dirPath) const
+{
+    return PathUtils::isEmptyDir(getFullPath(p_dirPath));
+}
+
+void LocalNotebookBackend::makePath(const QString &p_dirPath)
+{
+    constrainPath(p_dirPath);
+    QDir dir(getRootPath());
+    if (!dir.mkpath(p_dirPath)) {
+        Exception::throwOne(Exception::Type::FailToCreateDir,
+                            QString("fail to create directory: %1").arg(p_dirPath));
+    }
+}
+
+void LocalNotebookBackend::writeFile(const QString &p_filePath, const QByteArray &p_data)
+{
+    const auto filePath = getFullPath(p_filePath);
+    FileUtils::writeFile(filePath, p_data);
+}
+
+void LocalNotebookBackend::writeFile(const QString &p_filePath, const QString &p_text)
+{
+    const auto filePath = getFullPath(p_filePath);
+    FileUtils::writeFile(filePath, p_text);
+}
+
+void LocalNotebookBackend::writeFile(const QString &p_filePath, const QJsonObject &p_jobj)
+{
+    writeFile(p_filePath, QJsonDocument(p_jobj).toJson());
+}
+
+QString LocalNotebookBackend::readTextFile(const QString &p_filePath)
+{
+    const auto filePath = getFullPath(p_filePath);
+    return FileUtils::readTextFile(filePath);
+}
+
+QByteArray LocalNotebookBackend::readFile(const QString &p_filePath)
+{
+    const auto filePath = getFullPath(p_filePath);
+    return FileUtils::readFile(filePath);
+}
+
+bool LocalNotebookBackend::exists(const QString &p_path) const
+{
+    return QFileInfo::exists(getFullPath(p_path));
+}
+
+bool LocalNotebookBackend::childExistsCaseInsensitive(const QString &p_dirPath, const QString &p_name) const
+{
+    return FileUtils::childExistsCaseInsensitive(getFullPath(p_dirPath), p_name);
+}
+
+bool LocalNotebookBackend::isFile(const QString &p_path) const
+{
+    QFileInfo fi(getFullPath(p_path));
+    return fi.isFile();
+}
+
+void LocalNotebookBackend::renameFile(const QString &p_filePath, const QString &p_name)
+{
+    Q_ASSERT(isFile(p_filePath));
+    const auto filePath = getFullPath(p_filePath);
+    FileUtils::renameFile(filePath, p_name);
+}
+
+void LocalNotebookBackend::renameDir(const QString &p_dirPath, const QString &p_name)
+{
+    Q_ASSERT(!isFile(p_dirPath));
+    const auto dirPath = getFullPath(p_dirPath);
+    FileUtils::renameFile(dirPath, p_name);
+}
+
+void LocalNotebookBackend::copyFile(const QString &p_filePath, const QString &p_destPath)
+{
+    auto filePath = p_filePath;
+    if (QFileInfo(filePath).isRelative()) {
+        filePath = getFullPath(filePath);
+    }
+
+    Q_ASSERT(QFileInfo(filePath).isFile());
+
+    FileUtils::copyFile(filePath, getFullPath(p_destPath));
+}
+
+void LocalNotebookBackend::copyDir(const QString &p_dirPath, const QString &p_destPath)
+{
+    auto dirPath = p_dirPath;
+    if (QFileInfo(dirPath).isRelative()) {
+        dirPath = getFullPath(dirPath);
+    }
+
+    Q_ASSERT(QFileInfo(dirPath).isDir());
+
+    FileUtils::copyDir(dirPath, getFullPath(p_destPath));
+}
+
+void LocalNotebookBackend::removeFile(const QString &p_filePath)
+{
+    Q_ASSERT(isFile(p_filePath));
+    FileUtils::removeFile(getFullPath(p_filePath));
+}
+
+bool LocalNotebookBackend::removeDirIfEmpty(const QString &p_dirPath)
+{
+    Q_ASSERT(!isFile(p_dirPath));
+    return FileUtils::removeDirIfEmpty(getFullPath(p_dirPath));
+}
+
+void LocalNotebookBackend::removeDir(const QString &p_dirPath)
+{
+    Q_ASSERT(!isFile(p_dirPath));
+    return FileUtils::removeDir(getFullPath(p_dirPath));
+}
+
+QString LocalNotebookBackend::renameIfExistsCaseInsensitive(const QString &p_path) const
+{
+    return FileUtils::renameIfExistsCaseInsensitive(getFullPath(p_path));
+}
+
+void LocalNotebookBackend::addFile(const QString &p_path)
+{
+    Q_UNUSED(p_path);
+    // Do nothing for now.
+}
+
+void LocalNotebookBackend::removeEmptyDir(const QString &p_dirPath)
+{
+    FileUtils::removeEmptyDir(getFullPath(p_dirPath));
+}

+ 84 - 0
src/core/notebookbackend/localnotebookbackend.h

@@ -0,0 +1,84 @@
+#ifndef LOCALNOTEBOOKBACKEND_H
+#define LOCALNOTEBOOKBACKEND_H
+
+#include "inotebookbackend.h"
+
+#include "../global.h"
+
+namespace vnotex
+{
+    // Backend to access local file system.
+    class LocalNotebookBackend : public INotebookBackend
+    {
+        Q_OBJECT
+    public:
+        explicit LocalNotebookBackend(const QString &p_name,
+                                      const QString &p_displayName,
+                                      const QString &p_description,
+                                      const QString &p_rootPath,
+                                      QObject *p_parent = nullptr);
+
+        QString getName() const Q_DECL_OVERRIDE;
+
+        QString getDisplayName() const Q_DECL_OVERRIDE;
+
+        QString getDescription() const Q_DECL_OVERRIDE;
+
+        // Whether @p_dirPath is an empty directory.
+        bool isEmptyDir(const QString &p_dirPath) const Q_DECL_OVERRIDE;
+
+        // Create the directory path @p_dirPath. Create all parent directories if necessary.
+        void makePath(const QString &p_dirPath) Q_DECL_OVERRIDE;
+
+        // Write @p_data to @p_filePath.
+        void writeFile(const QString &p_filePath, const QByteArray &p_data) Q_DECL_OVERRIDE;
+
+        // Write @p_text to @p_filePath.
+        void writeFile(const QString &p_filePath, const QString &p_text) Q_DECL_OVERRIDE;
+
+        // Write @p_jobj to @p_filePath.
+        void writeFile(const QString &p_filePath, const QJsonObject &p_jobj) Q_DECL_OVERRIDE;
+
+        // Read content from @p_filePath.
+        QString readTextFile(const QString &p_filePath) Q_DECL_OVERRIDE;
+
+        // Read file @p_filePath.
+        QByteArray readFile(const QString &p_filePath) Q_DECL_OVERRIDE;
+
+        bool exists(const QString &p_path) const Q_DECL_OVERRIDE;
+
+        bool childExistsCaseInsensitive(const QString &p_dirPath, const QString &p_name) const Q_DECL_OVERRIDE;
+
+        bool isFile(const QString &p_path) const Q_DECL_OVERRIDE;
+
+        void renameFile(const QString &p_filePath, const QString &p_name) Q_DECL_OVERRIDE;
+
+        void renameDir(const QString &p_dirPath, const QString &p_name) Q_DECL_OVERRIDE;
+
+        // Delete @p_filePath from disk.
+        void removeFile(const QString &p_filePath) Q_DECL_OVERRIDE;
+
+        // Delete @p_dirPath from disk if it is empty.
+        bool removeDirIfEmpty(const QString &p_dirPath) Q_DECL_OVERRIDE;
+
+        void removeDir(const QString &p_dirPath) Q_DECL_OVERRIDE;
+
+        // Copy @p_filePath to @p_destPath.
+        // @p_filePath may beyond this notebook backend.
+        void copyFile(const QString &p_filePath, const QString &p_destPath) Q_DECL_OVERRIDE;
+
+        // Copy @p_dirPath to as @p_destPath.
+        void copyDir(const QString &p_dirPath, const QString &p_destPath) Q_DECL_OVERRIDE;
+
+        QString renameIfExistsCaseInsensitive(const QString &p_path) const Q_DECL_OVERRIDE;
+
+        void addFile(const QString &p_path) Q_DECL_OVERRIDE;
+
+        void removeEmptyDir(const QString &p_dirPath) Q_DECL_OVERRIDE;
+
+    private:
+        Info m_info;
+    };
+} // ns vnotex
+
+#endif // LOCALNOTEBOOKBACKEND_H

+ 34 - 0
src/core/notebookbackend/localnotebookbackendfactory.cpp

@@ -0,0 +1,34 @@
+#include "localnotebookbackendfactory.h"
+
+#include <QObject>
+
+#include "localnotebookbackend.h"
+
+using namespace vnotex;
+
+LocalNotebookBackendFactory::LocalNotebookBackendFactory()
+{
+}
+
+QString LocalNotebookBackendFactory::getName() const
+{
+    return QStringLiteral("local.vnotex");
+}
+
+QString LocalNotebookBackendFactory::getDisplayName() const
+{
+    return QObject::tr("Local Notebook Backend");
+}
+
+QString LocalNotebookBackendFactory::getDescription() const
+{
+    return QObject::tr("Local file system");
+}
+
+QSharedPointer<INotebookBackend> LocalNotebookBackendFactory::createNotebookBackend(const QString &p_rootPath)
+{
+    return QSharedPointer<LocalNotebookBackend>::create(getName(),
+                                                        getDisplayName(),
+                                                        getDescription(),
+                                                        p_rootPath);
+}

+ 24 - 0
src/core/notebookbackend/localnotebookbackendfactory.h

@@ -0,0 +1,24 @@
+#ifndef LOCALNOTEBOOKBACKENDFACTORY_H
+#define LOCALNOTEBOOKBACKENDFACTORY_H
+
+#include "inotebookbackendfactory.h"
+
+
+namespace vnotex
+{
+    class LocalNotebookBackendFactory : public INotebookBackendFactory
+    {
+    public:
+        LocalNotebookBackendFactory();
+
+        QString getName() const Q_DECL_OVERRIDE;
+
+        QString getDisplayName() const Q_DECL_OVERRIDE;
+
+        QString getDescription()const Q_DECL_OVERRIDE;
+
+        QSharedPointer<INotebookBackend> createNotebookBackend(const QString &p_rootPath) Q_DECL_OVERRIDE;
+    };
+} // ns vnotex
+
+#endif // LOCALNOTEBOOKBACKENDFACTORY_H

+ 10 - 0
src/core/notebookbackend/notebookbackend.pri

@@ -0,0 +1,10 @@
+SOURCES += \
+    $$PWD/localnotebookbackend.cpp \
+    $$PWD/localnotebookbackendfactory.cpp \
+    $$PWD/inotebookbackend.cpp
+
+HEADERS += \
+    $$PWD/inotebookbackend.h \
+    $$PWD/localnotebookbackend.h \
+    $$PWD/inotebookbackendfactory.h \
+    $$PWD/localnotebookbackendfactory.h

+ 96 - 0
src/core/notebookconfigmgr/bundlenotebookconfigmgr.cpp

@@ -0,0 +1,96 @@
+#include "bundlenotebookconfigmgr.h"
+
+#include <QJsonDocument>
+
+#include <notebookbackend/inotebookbackend.h>
+#include <notebook/notebookparameters.h>
+#include <notebook/bundlenotebook.h>
+#include "notebookconfig.h"
+#include <utils/pathutils.h>
+
+using namespace vnotex;
+
+const QString BundleNotebookConfigMgr::c_configFolderName = "vx_notebook";
+
+const QString BundleNotebookConfigMgr::c_configName = "vx_notebook.json";
+
+BundleNotebookConfigMgr::BundleNotebookConfigMgr(const QSharedPointer<INotebookBackend> &p_backend,
+                                                 QObject *p_parent)
+    : INotebookConfigMgr(p_backend, p_parent)
+{
+}
+
+void BundleNotebookConfigMgr::createEmptySkeleton(const NotebookParameters &p_paras)
+{
+    getBackend()->makePath(BundleNotebookConfigMgr::c_configFolderName);
+
+    auto config = NotebookConfig::fromNotebookParameters(getCodeVersion(), p_paras);
+    writeNotebookConfig(*config);
+}
+
+QSharedPointer<NotebookConfig> BundleNotebookConfigMgr::readNotebookConfig() const
+{
+    return readNotebookConfig(getBackend());
+}
+
+void BundleNotebookConfigMgr::writeNotebookConfig()
+{
+    auto config = NotebookConfig::fromNotebook(getCodeVersion(), getNotebook());
+    writeNotebookConfig(*config);
+}
+
+void BundleNotebookConfigMgr::writeNotebookConfig(const NotebookConfig &p_config)
+{
+    getBackend()->writeFile(getConfigFilePath(), p_config.toJson());
+}
+
+void BundleNotebookConfigMgr::removeNotebookConfig()
+{
+    getBackend()->removeDir(getConfigFolderName());
+}
+
+QSharedPointer<NotebookConfig> BundleNotebookConfigMgr::readNotebookConfig(
+        const QSharedPointer<INotebookBackend> &p_backend)
+{
+    auto data = p_backend->readFile(getConfigFilePath());
+
+    auto config = QSharedPointer<NotebookConfig>::create();
+    config->fromJson(QJsonDocument::fromJson(data).object());
+
+    return config;
+}
+
+const QString &BundleNotebookConfigMgr::getConfigFolderName()
+{
+    return c_configFolderName;
+}
+
+const QString &BundleNotebookConfigMgr::getConfigName()
+{
+    return c_configName;
+}
+
+QString BundleNotebookConfigMgr::getConfigFilePath()
+{
+    return PathUtils::concatenateFilePath(c_configFolderName, c_configName);
+}
+
+BundleNotebook *BundleNotebookConfigMgr::getBundleNotebook() const
+{
+    return dynamic_cast<BundleNotebook *>(getNotebook());
+}
+
+bool BundleNotebookConfigMgr::isBuiltInFile(const Node *p_node, const QString &p_name) const
+{
+    Q_UNUSED(p_node);
+    Q_UNUSED(p_name);
+    return false;
+}
+
+bool BundleNotebookConfigMgr::isBuiltInFolder(const Node *p_node, const QString &p_name) const
+{
+    if (p_node->isRoot()) {
+        return p_name.toLower() == c_configFolderName;
+    }
+    return false;
+}

+ 54 - 0
src/core/notebookconfigmgr/bundlenotebookconfigmgr.h

@@ -0,0 +1,54 @@
+#ifndef BUNDLENOTEBOOKCONFIGMGR_H
+#define BUNDLENOTEBOOKCONFIGMGR_H
+
+#include "inotebookconfigmgr.h"
+
+namespace vnotex
+{
+    class BundleNotebook;
+
+    class BundleNotebookConfigMgr : public INotebookConfigMgr
+    {
+        Q_OBJECT
+    public:
+        BundleNotebookConfigMgr(const QSharedPointer<INotebookBackend> &p_backend,
+                                QObject *p_parent = nullptr);
+
+        // Create an empty skeleton for an empty notebook.
+        virtual void createEmptySkeleton(const NotebookParameters &p_paras) Q_DECL_OVERRIDE;
+
+        QSharedPointer<NotebookConfig> readNotebookConfig() const;
+        void writeNotebookConfig();
+
+        void removeNotebookConfig();
+
+        bool isBuiltInFile(const Node *p_node, const QString &p_name) const Q_DECL_OVERRIDE;
+
+        bool isBuiltInFolder(const Node *p_node, const QString &p_name) const Q_DECL_OVERRIDE;
+
+        static const QString &getConfigFolderName();
+
+        static const QString &getConfigName();
+
+        static QString getConfigFilePath();
+
+        static QSharedPointer<NotebookConfig> readNotebookConfig(const QSharedPointer<INotebookBackend> &p_backend);
+
+        enum { RootNodeId = 1 };
+
+    protected:
+        BundleNotebook *getBundleNotebook() const;
+
+    private:
+        void writeNotebookConfig(const NotebookConfig &p_config);
+
+        // Folder name to store the notebook's config.
+        // This folder locates in the root folder of the notebook.
+        static const QString c_configFolderName;
+
+        // Name of the notebook's config file.
+        static const QString c_configName;
+    };
+} // ns vnotex
+
+#endif // BUNDLENOTEBOOKCONFIGMGR_H

+ 37 - 0
src/core/notebookconfigmgr/inotebookconfigmgr.cpp

@@ -0,0 +1,37 @@
+#include "inotebookconfigmgr.h"
+
+#include <notebookbackend/inotebookbackend.h>
+
+using namespace vnotex;
+
+INotebookConfigMgr::INotebookConfigMgr(const QSharedPointer<INotebookBackend> &p_backend,
+                                       QObject *p_parent)
+    : QObject(p_parent),
+      m_backend(p_backend)
+{
+}
+
+INotebookConfigMgr::~INotebookConfigMgr()
+{
+}
+
+const QSharedPointer<INotebookBackend> &INotebookConfigMgr::getBackend() const
+{
+    return m_backend;
+}
+
+QString INotebookConfigMgr::getCodeVersion() const
+{
+    const QString version("1");
+    return version;
+}
+
+Notebook *INotebookConfigMgr::getNotebook() const
+{
+    return m_notebook;
+}
+
+void INotebookConfigMgr::setNotebook(Notebook *p_notebook)
+{
+    m_notebook = p_notebook;
+}

+ 96 - 0
src/core/notebookconfigmgr/inotebookconfigmgr.h

@@ -0,0 +1,96 @@
+#ifndef INOTEBOOKCONFIGMGR_H
+#define INOTEBOOKCONFIGMGR_H
+
+#include <QObject>
+#include <QSharedPointer>
+
+#include "notebook/node.h"
+
+namespace vnotex
+{
+    class NotebookConfig;
+    class INotebookBackend;
+    class NotebookParameters;
+    class Notebook;
+    struct NodeParameters;
+
+    // Abstract class for notebook config manager, which is responsible for config
+    // files access and note nodes access.
+    class INotebookConfigMgr : public QObject
+    {
+        Q_OBJECT
+    public:
+        INotebookConfigMgr(const QSharedPointer<INotebookBackend> &p_backend,
+                           QObject *p_parent = nullptr);
+
+        virtual ~INotebookConfigMgr();
+
+        virtual QString getName() const = 0;
+
+        virtual QString getDisplayName() const = 0;
+
+        virtual QString getDescription() const = 0;
+
+        // Create an empty skeleton for an empty notebook.
+        virtual void createEmptySkeleton(const NotebookParameters &p_paras) = 0;
+
+        const QSharedPointer<INotebookBackend> &getBackend() const;
+
+        virtual QSharedPointer<Node> loadRootNode() const = 0;
+
+        virtual void loadNode(Node *p_node) const = 0;
+        virtual void saveNode(const Node *p_node) = 0;
+
+        virtual void renameNode(Node *p_node, const QString &p_name) = 0;
+
+        virtual QSharedPointer<Node> newNode(Node *p_parent,
+                                             Node::Type p_type,
+                                             const QString &p_name) = 0;
+
+        virtual QSharedPointer<Node> addAsNode(Node *p_parent,
+                                               Node::Type p_type,
+                                               const QString &p_name,
+                                               const NodeParameters &p_paras) = 0;
+
+        virtual QSharedPointer<Node> copyAsNode(Node *p_parent,
+                                                Node::Type p_type,
+                                                const QString &p_path) = 0;
+
+        Notebook *getNotebook() const;
+        void setNotebook(Notebook *p_notebook);
+
+        virtual QSharedPointer<Node> loadNodeByPath(const QSharedPointer<Node> &p_root,
+                                                    const QString &p_relativePath) = 0;
+
+        virtual QSharedPointer<Node> copyNodeAsChildOf(const QSharedPointer<Node> &p_src,
+                                                       Node *p_dest,
+                                                       bool p_move) = 0;
+
+        virtual void removeNode(const QSharedPointer<Node> &p_node, bool p_force, bool p_configOnly) = 0;
+
+        virtual bool nodeExistsOnDisk(const Node *p_node) const = 0;
+
+        virtual QString readNode(const Node *p_node) const = 0;
+        virtual void writeNode(Node *p_node, const QString &p_content) = 0;
+
+        virtual QString fetchNodeImageFolderPath(Node *p_node) = 0;
+
+        virtual QString fetchNodeAttachmentFolderPath(Node *p_node) = 0;
+
+        // Whether @p_name is a built-in file under @p_node.
+        virtual bool isBuiltInFile(const Node *p_node, const QString &p_name) const = 0;
+
+        virtual bool isBuiltInFolder(const Node *p_node, const QString &p_name) const = 0;
+
+    protected:
+        // Version of the config processing code.
+        virtual QString getCodeVersion() const;
+
+    private:
+        QSharedPointer<INotebookBackend> m_backend;
+
+        Notebook *m_notebook = nullptr;
+    };
+} // ns vnotex
+
+#endif // INOTEBOOKCONFIGMGR_H

+ 33 - 0
src/core/notebookconfigmgr/inotebookconfigmgrfactory.h

@@ -0,0 +1,33 @@
+#ifndef INOTEBOOKCONFIGMGRFACTORY_H
+#define INOTEBOOKCONFIGMGRFACTORY_H
+
+#include <QSharedPointer>
+
+namespace vnotex
+{
+    class INotebookConfigMgr;
+    class INotebookBackend;
+
+    class INotebookConfigMgrFactory
+    {
+    public:
+        INotebookConfigMgrFactory()
+        {
+        }
+
+        virtual ~INotebookConfigMgrFactory()
+        {
+        }
+
+        virtual QString getName() const = 0;
+
+        virtual QString getDisplayName() const = 0;
+
+        virtual QString getDescription() const = 0;
+
+        virtual QSharedPointer<INotebookConfigMgr> createNotebookConfigMgr(
+            const QSharedPointer<INotebookBackend> &p_backend) = 0;
+    };
+} // ns vnotex
+
+#endif // INOTEBOOKCONFIGMGRFACTORY_H

+ 177 - 0
src/core/notebookconfigmgr/nodecontentmediautils.cpp

@@ -0,0 +1,177 @@
+#include "nodecontentmediautils.h"
+
+#include <QDebug>
+#include <QSet>
+#include <QFileInfo>
+#include <QDir>
+#include <QHash>
+
+#include <notebookbackend/inotebookbackend.h>
+#include <notebook/node.h>
+
+#include <buffer/filetypehelper.h>
+
+#include <vtextedit/markdownutils.h>
+
+#include <utils/pathutils.h>
+#include <utils/fileutils.h>
+
+using namespace vnotex;
+
+void NodeContentMediaUtils::copyMediaFiles(const Node *p_node,
+                                           INotebookBackend *p_backend,
+                                           const QString &p_destFilePath)
+{
+    Q_ASSERT(p_node->getType() == Node::Type::File);
+    auto fileType = FileTypeHelper::fileType(p_node->fetchAbsolutePath());
+    if (fileType == QStringLiteral("markdown")) {
+        copyMarkdownMediaFiles(p_node->read(),
+                               PathUtils::parentDirPath(p_node->fetchContentPath()),
+                               p_backend,
+                               p_destFilePath);
+    }
+}
+
+void NodeContentMediaUtils::copyMediaFiles(const QString &p_filePath,
+                                           INotebookBackend *p_backend,
+                                           const QString &p_destFilePath)
+{
+    auto fileType = FileTypeHelper::fileType(p_filePath);
+    if (fileType == QStringLiteral("markdown")) {
+        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)
+{
+    auto content = p_content;
+
+    // Images.
+    const auto images =
+        vte::MarkdownUtils::fetchImagesFromMarkdownText(content,
+                                                        p_basePath,
+                                                        vte::MarkdownLink::TypeFlag::LocalRelativeInternal);
+
+    QDir destDir(PathUtils::parentDirPath(p_destFilePath));
+    QSet<QString> handledImages;
+    QHash<QString, QString> renamedImages;
+    int lastPos = content.size();
+    for (const auto &link : images) {
+        Q_ASSERT(link.m_urlInLinkPos < lastPos);
+        lastPos = link.m_urlInLinkPos;
+
+        if (handledImages.contains(link.m_path)) {
+            auto it = renamedImages.find(link.m_path);
+            if (it != renamedImages.end()) {
+                content.replace(link.m_urlInLinkPos, link.m_urlInLink.size(), it.value());
+            }
+            continue;
+        }
+
+        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;
+            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);
+        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);
+
+            // Update the text content.
+            auto newUrlInLink(link.m_urlInLink);
+            newUrlInLink.replace(newUrlInLink.size() - oldFileName.size(),
+                                 oldFileName.size(),
+                                 newFileName);
+
+            content.replace(link.m_urlInLinkPos, link.m_urlInLink.size(), newUrlInLink);
+            renamedImages.insert(link.m_path, newUrlInLink);
+        }
+
+        p_backend->copyFile(link.m_path, destFilePath);
+    }
+
+    if (!renamedImages.isEmpty()) {
+        p_backend->writeFile(p_destFilePath, content);
+    }
+}
+
+void NodeContentMediaUtils::removeMediaFiles(const Node *p_node)
+{
+    Q_ASSERT(p_node->getType() == Node::Type::File);
+    auto fileType = FileTypeHelper::fileType(p_node->fetchAbsolutePath());
+    if (fileType == QStringLiteral("markdown")) {
+        removeMarkdownMediaFiles(p_node);
+    }
+}
+
+void NodeContentMediaUtils::removeMarkdownMediaFiles(const Node *p_node)
+{
+    auto content = p_node->read();
+
+    // Images.
+    const auto images =
+        vte::MarkdownUtils::fetchImagesFromMarkdownText(content,
+                                                        PathUtils::parentDirPath(p_node->fetchContentPath()),
+                                                        vte::MarkdownLink::TypeFlag::LocalRelativeInternal);
+
+    auto backend = p_node->getBackend();
+    QSet<QString> handledImages;
+    for (const auto &link : images) {
+        if (handledImages.contains(link.m_path)) {
+            continue;
+        }
+
+        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;
+            continue;
+        }
+        backend->removeFile(link.m_path);
+    }
+}
+
+void NodeContentMediaUtils::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->getAttachmentFolder().isEmpty());
+
+    // Copy the whole folder.
+    const auto srcAttachmentFolderPath = p_node->fetchAttachmentFolderPath();
+    p_backend->copyDir(srcAttachmentFolderPath, p_destAttachmentFolderPath);
+
+    // Check if we need to modify links in content.
+    if (p_node->getAttachmentFolder() == PathUtils::dirName(p_destAttachmentFolderPath)) {
+        return;
+    }
+
+    auto fileType = FileTypeHelper::fileType(p_node->fetchAbsolutePath());
+    if (fileType == QStringLiteral("markdown")) {
+        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)
+{
+    // TODO.
+}

+ 52 - 0
src/core/notebookconfigmgr/nodecontentmediautils.h

@@ -0,0 +1,52 @@
+#ifndef NODECONTENTMEDIAUTILS_H
+#define NODECONTENTMEDIAUTILS_H
+
+#include <QString>
+
+namespace vnotex
+{
+    class INotebookBackend;
+    class Node;
+
+    // Utils to operate on the media files from node's content.
+    class NodeContentMediaUtils
+    {
+    public:
+        NodeContentMediaUtils() = 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,
+                                   INotebookBackend *p_backend,
+                                   const QString &p_destFilePath);
+
+        // @p_filePath: the file path to read the content for parse.
+        static void copyMediaFiles(const QString &p_filePath,
+                                   INotebookBackend *p_backend,
+                                   const QString &p_destFilePath);
+
+        static void removeMediaFiles(const Node *p_node);
+
+        // Copy attachment folder.
+        static void copyAttachment(Node *p_node,
+                                   INotebookBackend *p_backend,
+                                   const QString &p_destFilePath,
+                                   const QString &p_destAttachmentFolderPath);
+
+    private:
+        static void copyMarkdownMediaFiles(const QString &p_content,
+                                           const QString &p_basePath,
+                                           INotebookBackend *p_backend,
+                                           const QString &p_destFilePath);
+
+        static void removeMarkdownMediaFiles(const Node *p_node);
+
+        // Fix local relative internal links locating in @p_srcFolderPath.
+        static void fixMarkdownLinks(const QString &p_srcFolderPath,
+                                     INotebookBackend *p_backend,
+                                     const QString &p_destFilePath,
+                                     const QString &p_destFolderPath);
+    };
+}
+
+#endif // NODECONTENTMEDIAUTILS_H

+ 111 - 0
src/core/notebookconfigmgr/notebookconfig.cpp

@@ -0,0 +1,111 @@
+#include "notebookconfig.h"
+
+#include <notebook/notebookparameters.h>
+#include <notebook/notebook.h>
+#include <versioncontroller/iversioncontroller.h>
+#include <utils/utils.h>
+#include "exception.h"
+#include "global.h"
+
+using namespace vnotex;
+
+const QString NotebookConfig::c_version = "version";
+
+const QString NotebookConfig::c_name = "name";
+
+const QString NotebookConfig::c_description = "description";
+
+const QString NotebookConfig::c_imageFolder = "image_folder";
+
+const QString NotebookConfig::c_attachmentFolder = "attachment_folder";
+
+const QString NotebookConfig::c_createdTimeUtc = "created_time";
+
+const QString NotebookConfig::c_versionController = "version_controller";
+
+const QString NotebookConfig::c_configMgr = "config_mgr";
+
+const QString NotebookConfig::c_nextNodeId = "next_node_id";
+
+QSharedPointer<NotebookConfig> NotebookConfig::fromNotebookParameters(const QString &p_version,
+                                                                      const NotebookParameters &p_paras)
+{
+    auto config = QSharedPointer<NotebookConfig>::create();
+
+    config->m_version = p_version;
+    config->m_name = p_paras.m_name;
+    config->m_description = p_paras.m_description;
+    config->m_imageFolder = p_paras.m_imageFolder;
+    config->m_attachmentFolder = p_paras.m_attachmentFolder;
+    config->m_createdTimeUtc = p_paras.m_createdTimeUtc;
+    config->m_versionController = p_paras.m_versionController->getName();
+    config->m_notebookConfigMgr = p_paras.m_notebookConfigMgr->getName();
+
+    return config;
+}
+
+QJsonObject NotebookConfig::toJson() const
+{
+    QJsonObject jobj;
+
+    jobj[NotebookConfig::c_version] = m_version;
+    jobj[NotebookConfig::c_name] = m_name;
+    jobj[NotebookConfig::c_description] = m_description;
+    jobj[NotebookConfig::c_imageFolder] = m_imageFolder;
+    jobj[NotebookConfig::c_attachmentFolder] = m_attachmentFolder;
+    jobj[NotebookConfig::c_createdTimeUtc] = Utils::dateTimeStringUniform(m_createdTimeUtc);
+    jobj[NotebookConfig::c_versionController] = m_versionController;
+    jobj[NotebookConfig::c_configMgr] = m_notebookConfigMgr;
+    jobj[NotebookConfig::c_nextNodeId] = QString::number(m_nextNodeId);
+
+    return jobj;
+}
+
+void NotebookConfig::fromJson(const QJsonObject &p_jobj)
+{
+    if (!p_jobj.contains(NotebookConfig::c_version)
+        || !p_jobj.contains(NotebookConfig::c_name)
+        || !p_jobj.contains(NotebookConfig::c_createdTimeUtc)
+        || !p_jobj.contains(NotebookConfig::c_versionController)
+        || !p_jobj.contains(NotebookConfig::c_configMgr)) {
+        Exception::throwOne(Exception::Type::InvalidArgument,
+                            QString("fail to read notebook configuration from JSON (%1)").arg(QJsonObjectToString(p_jobj)));
+        return;
+    }
+
+    m_version = p_jobj[NotebookConfig::c_version].toString();
+    m_name = p_jobj[NotebookConfig::c_name].toString();
+    m_description = p_jobj[NotebookConfig::c_description].toString();
+    m_imageFolder = p_jobj[NotebookConfig::c_imageFolder].toString();
+    m_attachmentFolder = p_jobj[NotebookConfig::c_attachmentFolder].toString();
+    m_createdTimeUtc = Utils::dateTimeFromStringUniform(p_jobj[NotebookConfig::c_createdTimeUtc].toString());
+    m_versionController = p_jobj[NotebookConfig::c_versionController].toString();
+    m_notebookConfigMgr = p_jobj[NotebookConfig::c_configMgr].toString();
+
+    {
+        auto nextNodeIdStr = p_jobj[NotebookConfig::c_nextNodeId].toString();
+        bool ok;
+        m_nextNodeId = nextNodeIdStr.toULongLong(&ok);
+        if (!ok) {
+            m_nextNodeId = BundleNotebookConfigMgr::RootNodeId;
+        }
+    }
+}
+
+QSharedPointer<NotebookConfig> NotebookConfig::fromNotebook(const QString &p_version,
+                                                            const Notebook *p_notebook)
+{
+    auto config = QSharedPointer<NotebookConfig>::create();
+
+    config->m_version = p_version;
+    config->m_name = p_notebook->getName();
+    config->m_description = p_notebook->getDescription();
+    config->m_imageFolder = p_notebook->getImageFolder();
+    config->m_attachmentFolder = p_notebook->getAttachmentFolder();
+    config->m_createdTimeUtc = p_notebook->getCreatedTimeUtc();
+    config->m_versionController = p_notebook->getVersionController()->getName();
+    config->m_notebookConfigMgr = p_notebook->getConfigMgr()->getName();
+    config->m_nextNodeId = p_notebook->getNextNodeId();
+
+    return config;
+}

+ 68 - 0
src/core/notebookconfigmgr/notebookconfig.h

@@ -0,0 +1,68 @@
+#ifndef NOTEBOOKCONFIG_H
+#define NOTEBOOKCONFIG_H
+
+#include <QJsonObject>
+#include <QSharedPointer>
+#include <QDateTime>
+
+#include "bundlenotebookconfigmgr.h"
+#include "global.h"
+
+namespace vnotex
+{
+    class NotebookParameters;
+
+    class NotebookConfig
+    {
+    public:
+        virtual ~NotebookConfig() {}
+
+        static QSharedPointer<NotebookConfig> fromNotebookParameters(const QString &p_version,
+                                                                     const NotebookParameters &p_paras);
+
+        static QSharedPointer<NotebookConfig> fromNotebook(const QString &p_version,
+                                                           const Notebook *p_notebook);
+
+        virtual QJsonObject toJson() const;
+
+        virtual void fromJson(const QJsonObject &p_jobj);
+
+        QString m_version;
+
+        QString m_name;
+
+        QString m_description;
+
+        QString m_imageFolder;
+
+        QString m_attachmentFolder;
+
+        QDateTime m_createdTimeUtc;
+
+        QString m_versionController;
+
+        QString m_notebookConfigMgr;
+
+        ID m_nextNodeId = BundleNotebookConfigMgr::RootNodeId + 1;
+
+        static const QString c_version;
+
+        static const QString c_name;
+
+        static const QString c_description;
+
+        static const QString c_imageFolder;
+
+        static const QString c_attachmentFolder;
+
+        static const QString c_createdTimeUtc;
+
+        static const QString c_versionController;
+
+        static const QString c_configMgr;
+
+        static const QString c_nextNodeId;
+    };
+} // ns vnotex
+
+#endif // NOTEBOOKCONFIG_H

+ 16 - 0
src/core/notebookconfigmgr/notebookconfigmgr.pri

@@ -0,0 +1,16 @@
+SOURCES += \
+    $$PWD/nodecontentmediautils.cpp \
+    $$PWD/vxnotebookconfigmgr.cpp \
+    $$PWD/vxnotebookconfigmgrfactory.cpp \
+    $$PWD/inotebookconfigmgr.cpp \
+    $$PWD/notebookconfig.cpp \
+    $$PWD/bundlenotebookconfigmgr.cpp
+
+HEADERS += \
+    $$PWD/inotebookconfigmgr.h \
+    $$PWD/nodecontentmediautils.h \
+    $$PWD/vxnotebookconfigmgr.h \
+    $$PWD/inotebookconfigmgrfactory.h \
+    $$PWD/vxnotebookconfigmgrfactory.h \
+    $$PWD/notebookconfig.h \
+    $$PWD/bundlenotebookconfigmgr.h

+ 886 - 0
src/core/notebookconfigmgr/vxnotebookconfigmgr.cpp

@@ -0,0 +1,886 @@
+#include "vxnotebookconfigmgr.h"
+
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QDebug>
+
+#include <notebookbackend/inotebookbackend.h>
+#include <notebook/notebookparameters.h>
+#include <notebook/filenode.h>
+#include <notebook/foldernode.h>
+#include <notebook/bundlenotebook.h>
+#include <utils/utils.h>
+#include <utils/fileutils.h>
+#include <utils/pathutils.h>
+#include <exception.h>
+
+#include "nodecontentmediautils.h"
+
+using namespace vnotex;
+
+const QString VXNotebookConfigMgr::NodeConfig::c_version = "version";
+
+const QString VXNotebookConfigMgr::NodeConfig::c_id = "id";
+
+const QString VXNotebookConfigMgr::NodeConfig::c_createdTimeUtc = "created_time";
+
+const QString VXNotebookConfigMgr::NodeConfig::c_files = "files";
+
+const QString VXNotebookConfigMgr::NodeConfig::c_folders = "folders";
+
+const QString VXNotebookConfigMgr::NodeConfig::c_name = "name";
+
+const QString VXNotebookConfigMgr::NodeConfig::c_modifiedTimeUtc = "modified_time";
+
+const QString VXNotebookConfigMgr::NodeConfig::c_attachmentFolder = "attachment_folder";
+
+const QString VXNotebookConfigMgr::NodeConfig::c_tags = "tags";
+
+QJsonObject VXNotebookConfigMgr::NodeFileConfig::toJson() const
+{
+    QJsonObject jobj;
+
+    jobj[NodeConfig::c_name] = m_name;
+    jobj[NodeConfig::c_id] = QString::number(m_id);
+    jobj[NodeConfig::c_createdTimeUtc] = Utils::dateTimeStringUniform(m_createdTimeUtc);
+    jobj[NodeConfig::c_modifiedTimeUtc] = Utils::dateTimeStringUniform(m_modifiedTimeUtc);
+    jobj[NodeConfig::c_attachmentFolder] = m_attachmentFolder;
+    jobj[NodeConfig::c_tags] = QJsonArray::fromStringList(m_tags);
+
+    return jobj;
+}
+
+void VXNotebookConfigMgr::NodeFileConfig::fromJson(const QJsonObject &p_jobj)
+{
+    m_name = p_jobj[NodeConfig::c_name].toString();
+
+    {
+        auto idStr = p_jobj[NodeConfig::c_id].toString();
+        bool ok;
+        m_id = idStr.toULongLong(&ok);
+        if (!ok) {
+            m_id = Node::InvalidId;
+        }
+    }
+
+    m_createdTimeUtc = Utils::dateTimeFromStringUniform(p_jobj[NodeConfig::c_createdTimeUtc].toString());
+    m_modifiedTimeUtc = Utils::dateTimeFromStringUniform(p_jobj[NodeConfig::c_modifiedTimeUtc].toString());
+
+    m_attachmentFolder = p_jobj[NodeConfig::c_attachmentFolder].toString();
+
+    {
+        auto arr = p_jobj[NodeConfig::c_tags].toArray();
+        for (const auto &tag : arr) {
+            m_tags << tag.toString();
+        }
+    }
+}
+
+QJsonObject VXNotebookConfigMgr::NodeFolderConfig::toJson() const
+{
+    QJsonObject jobj;
+
+    jobj[NodeConfig::c_name] = m_name;
+
+    return jobj;
+}
+
+void VXNotebookConfigMgr::NodeFolderConfig::fromJson(const QJsonObject &p_jobj)
+{
+    m_name = p_jobj[NodeConfig::c_name].toString();
+}
+
+VXNotebookConfigMgr::NodeConfig::NodeConfig()
+{
+}
+
+VXNotebookConfigMgr::NodeConfig::NodeConfig(const QString &p_version,
+                                            ID p_id,
+                                            const QDateTime &p_createdTimeUtc)
+    : m_version(p_version),
+      m_id(p_id),
+      m_createdTimeUtc(p_createdTimeUtc)
+{
+}
+
+QJsonObject VXNotebookConfigMgr::NodeConfig::toJson() const
+{
+    QJsonObject jobj;
+
+    jobj[NodeConfig::c_version] = m_version;
+    jobj[NodeConfig::c_id] = QString::number(m_id);
+    jobj[NodeConfig::c_createdTimeUtc] = Utils::dateTimeStringUniform(m_createdTimeUtc);
+
+    QJsonArray files;
+    for (const auto &file : m_files) {
+        files.append(file.toJson());
+    }
+    jobj[NodeConfig::c_files] = files;
+
+    QJsonArray folders;
+    for (const auto& folder : m_folders) {
+        folders.append(folder.toJson());
+    }
+    jobj[NodeConfig::c_folders] = folders;
+
+    return jobj;
+}
+
+void VXNotebookConfigMgr::NodeConfig::fromJson(const QJsonObject &p_jobj)
+{
+    m_version = p_jobj[NodeConfig::c_version].toString();
+
+    {
+    auto idStr = p_jobj[NodeConfig::c_id].toString();
+    bool ok;
+    m_id = idStr.toULongLong(&ok);
+    if (!ok) {
+        m_id = Node::InvalidId;
+    }
+    }
+
+    m_createdTimeUtc = Utils::dateTimeFromStringUniform(p_jobj[NodeConfig::c_createdTimeUtc].toString());
+
+    auto filesJson = p_jobj[NodeConfig::c_files].toArray();
+    m_files.resize(filesJson.size());
+    for (int i = 0; i < filesJson.size(); ++i) {
+        m_files[i].fromJson(filesJson[i].toObject());
+    }
+
+    auto foldersJson = p_jobj[NodeConfig::c_folders].toArray();
+    m_folders.resize(foldersJson.size());
+    for (int i = 0; i < foldersJson.size(); ++i) {
+        m_folders[i].fromJson(foldersJson[i].toObject());
+    }
+}
+
+
+const QString VXNotebookConfigMgr::c_nodeConfigName = "vx.json";
+
+const QString VXNotebookConfigMgr::c_recycleBinFolderName = "vx_recycle_bin";
+
+VXNotebookConfigMgr::VXNotebookConfigMgr(const QString &p_name,
+                                         const QString &p_displayName,
+                                         const QString &p_description,
+                                         const QSharedPointer<INotebookBackend> &p_backend,
+                                         QObject *p_parent)
+    : BundleNotebookConfigMgr(p_backend, p_parent),
+      m_info(p_name, p_displayName, p_description)
+{
+}
+
+QString VXNotebookConfigMgr::getName() const
+{
+    return m_info.m_name;
+}
+
+QString VXNotebookConfigMgr::getDisplayName() const
+{
+    return m_info.m_displayName;
+}
+
+QString VXNotebookConfigMgr::getDescription() const
+{
+    return m_info.m_description;
+}
+
+void VXNotebookConfigMgr::createEmptySkeleton(const NotebookParameters &p_paras)
+{
+    BundleNotebookConfigMgr::createEmptySkeleton(p_paras);
+
+    createEmptyRootNode();
+}
+
+void VXNotebookConfigMgr::createEmptyRootNode()
+{
+    NodeConfig node(getCodeVersion(),
+                    BundleNotebookConfigMgr::RootNodeId,
+                    QDateTime::currentDateTimeUtc());
+    writeNodeConfig(c_nodeConfigName, node);
+}
+
+QSharedPointer<Node> VXNotebookConfigMgr::loadRootNode() const
+{
+    auto nodeConfig = readNodeConfig("");
+    QSharedPointer<Node> root = nodeConfigToNode(*nodeConfig, "", nullptr);
+    Q_ASSERT(root->isLoaded());
+
+    if (!markRecycleBinNode(root)) {
+        const_cast<VXNotebookConfigMgr *>(this)->createRecycleBinNode(root);
+    }
+
+    return root;
+}
+
+bool VXNotebookConfigMgr::markRecycleBinNode(const QSharedPointer<Node> &p_root) const
+{
+    auto node = p_root->findChild(c_recycleBinFolderName,
+                                  FileUtils::isPlatformNameCaseSensitive());
+    if (node) {
+        node->setUse(Node::Use::RecycleBin);
+        markNodeReadOnly(node.data());
+        return true;
+    }
+
+    return false;
+}
+
+void VXNotebookConfigMgr::markNodeReadOnly(Node *p_node) const
+{
+    auto flags = p_node->getFlags();
+    if (flags & Node::Flag::ReadOnly) {
+        return;
+    }
+
+    p_node->setFlags(flags | Node::Flag::ReadOnly);
+    for (auto &child : p_node->getChildren()) {
+        markNodeReadOnly(child.data());
+    }
+}
+
+void VXNotebookConfigMgr::createRecycleBinNode(const QSharedPointer<Node> &p_root)
+{
+    Q_ASSERT(p_root->isRoot());
+
+    auto node = newNode(p_root.data(), Node::Type::Folder, c_recycleBinFolderName);
+    node->setUse(Node::Use::RecycleBin);
+    markNodeReadOnly(node.data());
+}
+
+QSharedPointer<VXNotebookConfigMgr::NodeConfig> VXNotebookConfigMgr::readNodeConfig(const QString &p_path) const
+{
+    auto backend = getBackend();
+    if (!backend->exists(p_path)) {
+        Exception::throwOne(Exception::Type::InvalidArgument,
+                            QString("node path (%1) does not exist").arg(p_path));
+    }
+
+    if (backend->isFile(p_path)) {
+        Exception::throwOne(Exception::Type::InvalidArgument,
+                            QString("node (%1) is a file node without config").arg(p_path));
+    } else {
+        auto configPath = PathUtils::concatenateFilePath(p_path, c_nodeConfigName);
+        auto data = backend->readFile(configPath);
+        auto nodeConfig = QSharedPointer<NodeConfig>::create();
+        nodeConfig->fromJson(QJsonDocument::fromJson(data).object());
+        return nodeConfig;
+    }
+
+    return nullptr;
+}
+
+QString VXNotebookConfigMgr::getNodeConfigFilePath(const Node *p_node) const
+{
+    Q_ASSERT(p_node->getType() == Node::Type::Folder);
+    return PathUtils::concatenateFilePath(p_node->fetchRelativePath(), c_nodeConfigName);
+}
+
+void VXNotebookConfigMgr::writeNodeConfig(const QString &p_path, const NodeConfig &p_config) const
+{
+    getBackend()->writeFile(p_path, p_config.toJson());
+}
+
+void VXNotebookConfigMgr::writeNodeConfig(const Node *p_node)
+{
+    auto config = nodeToNodeConfig(p_node);
+    writeNodeConfig(getNodeConfigFilePath(p_node), *config);
+}
+
+QSharedPointer<Node> VXNotebookConfigMgr::nodeConfigToNode(const NodeConfig &p_config,
+                                                           const QString &p_name,
+                                                           Node *p_parent) const
+{
+    auto node = QSharedPointer<FolderNode>::create(p_name, getNotebook(), p_parent);
+    loadFolderNode(node.data(), p_config);
+    return node;
+}
+
+void VXNotebookConfigMgr::loadFolderNode(FolderNode *p_node, const NodeConfig &p_config) const
+{
+    QVector<QSharedPointer<Node>> children;
+    children.reserve(p_config.m_files.size() + p_config.m_folders.size());
+
+    for (const auto &folder : p_config.m_folders) {
+        auto folderNode = QSharedPointer<FolderNode>::create(folder.m_name,
+                                                             getNotebook(),
+                                                             p_node);
+        inheritNodeFlags(p_node, folderNode.data());
+        children.push_back(folderNode);
+    }
+
+    for (const auto &file : p_config.m_files) {
+        auto fileNode = QSharedPointer<FileNode>::create(file.m_id,
+                                                         file.m_name,
+                                                         file.m_createdTimeUtc,
+                                                         file.m_modifiedTimeUtc,
+                                                         file.m_attachmentFolder,
+                                                         file.m_tags,
+                                                         getNotebook(),
+                                                         p_node);
+        inheritNodeFlags(p_node, fileNode.data());
+        children.push_back(fileNode);
+    }
+
+    p_node->loadFolder(p_config.m_id, p_config.m_createdTimeUtc, children);
+}
+
+QSharedPointer<Node> VXNotebookConfigMgr::newNode(Node *p_parent,
+                                                  Node::Type p_type,
+                                                  const QString &p_name)
+{
+    Q_ASSERT(p_parent && p_parent->getType() == Node::Type::Folder);
+
+    QSharedPointer<Node> node;
+
+    switch (p_type) {
+    case Node::Type::File:
+        node = newFileNode(p_parent, p_name, true, NodeParameters());
+        break;
+
+    case Node::Type::Folder:
+        node = newFolderNode(p_parent, p_name, true, NodeParameters());
+        break;
+    }
+
+    return node;
+}
+
+QSharedPointer<Node> VXNotebookConfigMgr::addAsNode(Node *p_parent,
+                                                    Node::Type p_type,
+                                                    const QString &p_name,
+                                                    const NodeParameters &p_paras)
+{
+    Q_ASSERT(p_parent && p_parent->getType() == Node::Type::Folder);
+
+    QSharedPointer<Node> node;
+    switch (p_type) {
+    case Node::Type::File:
+        node = newFileNode(p_parent, p_name, false, p_paras);
+        break;
+
+    case Node::Type::Folder:
+        node = newFolderNode(p_parent, p_name, false, p_paras);
+        break;
+    }
+
+    return node;
+}
+
+QSharedPointer<Node> VXNotebookConfigMgr::copyAsNode(Node *p_parent,
+                                                     Node::Type p_type,
+                                                     const QString &p_path)
+{
+    Q_ASSERT(p_parent && p_parent->getType() == Node::Type::Folder);
+
+    QSharedPointer<Node> node;
+    switch (p_type) {
+    case Node::Type::File:
+        node = copyFileAsChildOf(p_path, p_parent);
+        break;
+
+    case Node::Type::Folder:
+        node = copyFolderAsChildOf(p_path, p_parent);
+        break;
+    }
+
+    return node;
+}
+
+QSharedPointer<Node> VXNotebookConfigMgr::newFileNode(Node *p_parent,
+                                                      const QString &p_name,
+                                                      bool p_create,
+                                                      const NodeParameters &p_paras)
+{
+    auto notebook = getNotebook();
+
+    // Create file node.
+    auto node = QSharedPointer<FileNode>::create(Node::InvalidId,
+                                                 p_name,
+                                                 p_paras.m_createdTimeUtc,
+                                                 p_paras.m_modifiedTimeUtc,
+                                                 p_paras.m_attachmentFolder,
+                                                 p_paras.m_tags,
+                                                 notebook,
+                                                 p_parent);
+
+    // Write empty file.
+    if (p_create) {
+        getBackend()->writeFile(node->fetchRelativePath(), QString());
+    }
+
+    addChildNode(p_parent, node);
+    writeNodeConfig(p_parent);
+
+    return node;
+}
+
+QSharedPointer<Node> VXNotebookConfigMgr::newFolderNode(Node *p_parent,
+                                                        const QString &p_name,
+                                                        bool p_create,
+                                                        const NodeParameters &p_paras)
+{
+    auto notebook = getNotebook();
+
+    // Create folder node.
+    auto node = QSharedPointer<FolderNode>::create(p_name, notebook, p_parent);
+    node->loadFolder(Node::InvalidId,
+                     p_paras.m_createdTimeUtc,
+                     QVector<QSharedPointer<Node>>());
+
+    // Make folder.
+    if (p_create) {
+        getBackend()->makePath(node->fetchRelativePath());
+    }
+
+    writeNodeConfig(node.data());
+
+    addChildNode(p_parent, node);
+    writeNodeConfig(p_parent);
+
+    return node;
+}
+
+QSharedPointer<VXNotebookConfigMgr::NodeConfig> VXNotebookConfigMgr::nodeToNodeConfig(const Node *p_node) const
+{
+    Q_ASSERT(p_node->getType() == Node::Type::Folder);
+
+    auto config = QSharedPointer<NodeConfig>::create(getCodeVersion(),
+                                                     p_node->getId(),
+                                                     p_node->getCreatedTimeUtc());
+
+    for (const auto &child : p_node->getChildren()) {
+        switch (child->getType()) {
+        case Node::Type::File:
+        {
+            NodeFileConfig fileConfig;
+            fileConfig.m_name = child->getName();
+            fileConfig.m_id = child->getId();
+            fileConfig.m_createdTimeUtc = child->getCreatedTimeUtc();
+            fileConfig.m_modifiedTimeUtc = child->getModifiedTimeUtc();
+            fileConfig.m_attachmentFolder = child->getAttachmentFolder();
+            fileConfig.m_tags = child->getTags();
+
+            config->m_files.push_back(fileConfig);
+            break;
+        }
+
+        case Node::Type::Folder:
+        {
+            NodeFolderConfig folderConfig;
+            folderConfig.m_name = child->getName();
+
+            config->m_folders.push_back(folderConfig);
+            break;
+        }
+        }
+    }
+
+    return config;
+}
+
+void VXNotebookConfigMgr::loadNode(Node *p_node) const
+{
+    if (p_node->isLoaded()) {
+        return;
+    }
+
+    auto config = readNodeConfig(p_node->fetchRelativePath());
+    auto folderNode = dynamic_cast<FolderNode *>(p_node);
+    loadFolderNode(folderNode, *config);
+}
+
+void VXNotebookConfigMgr::saveNode(const Node *p_node)
+{
+    Q_ASSERT(!p_node->isRoot());
+
+    if (p_node->getType() == Node::Type::Folder) {
+        writeNodeConfig(p_node);
+    } else {
+        writeNodeConfig(p_node->getParent());
+    }
+}
+
+void VXNotebookConfigMgr::renameNode(Node *p_node, const QString &p_name)
+{
+    Q_ASSERT(!p_node->isRoot());
+    switch (p_node->getType()) {
+    case Node::Type::Folder:
+        getBackend()->renameDir(p_node->fetchRelativePath(), p_name);
+        break;
+
+    case Node::Type::File:
+        getBackend()->renameFile(p_node->fetchRelativePath(), p_name);
+        break;
+    }
+
+    p_node->setName(p_name);
+    writeNodeConfig(p_node->getParent());
+}
+
+void VXNotebookConfigMgr::addChildNode(Node *p_parent, const QSharedPointer<Node> &p_child) const
+{
+    // Add @p_child after the last node of same type.
+    const auto type = p_child->getType();
+    switch (type) {
+    case Node::Type::Folder:
+    {
+        int idx = 0;
+        auto children = p_parent->getChildren();
+        for (; idx < children.size(); ++idx) {
+            if (children[idx]->getType() != type) {
+                break;
+            }
+        }
+
+        p_parent->insertChild(idx, p_child);
+        break;
+    }
+
+    case Node::Type::File:
+        p_parent->addChild(p_child);
+        break;
+    }
+
+    inheritNodeFlags(p_parent, p_child.data());
+}
+
+QSharedPointer<Node> VXNotebookConfigMgr::loadNodeByPath(const QSharedPointer<Node> &p_root, const QString &p_relativePath)
+{
+    auto p = PathUtils::cleanPath(p_relativePath);
+    auto paths = p.split('/', QString::SkipEmptyParts);
+    auto node = p_root;
+    for (auto &pa : paths) {
+        // Find child @pa in @node.
+        if (!node->isLoaded()) {
+            loadNode(node.data());
+        }
+
+        auto child = node->findChild(pa, FileUtils::isPlatformNameCaseSensitive());
+        if (!child) {
+            return nullptr;
+        }
+
+        node = child;
+    }
+
+    return node;
+}
+
+// @p_src may belong to different notebook or different kind of configmgr.
+// TODO: we could constrain @p_src within the same configrmgr?
+QSharedPointer<Node> VXNotebookConfigMgr::copyNodeAsChildOf(const QSharedPointer<Node> &p_src,
+                                                            Node *p_dest,
+                                                            bool p_move)
+{
+    Q_ASSERT(p_dest->getType() == Node::Type::Folder);
+    if (!p_src->existsOnDisk()) {
+        Exception::throwOne(Exception::Type::FileMissingOnDisk,
+                            QString("source node missing on disk (%1)").arg(p_src->fetchAbsolutePath()));
+        return nullptr;
+    }
+
+    QSharedPointer<Node> node;
+    switch (p_src->getType()) {
+    case Node::Type::File:
+        node = copyFileNodeAsChildOf(p_src, p_dest, p_move);
+        break;
+
+    case Node::Type::Folder:
+        node = copyFolderNodeAsChildOf(p_src, p_dest, p_move);
+        break;
+    }
+
+    return node;
+}
+
+QSharedPointer<Node> VXNotebookConfigMgr::copyFileNodeAsChildOf(const QSharedPointer<Node> &p_src,
+                                                                Node *p_dest,
+                                                                bool p_move)
+{
+    // Copy source file itself.
+    auto srcFilePath = p_src->fetchAbsolutePath();
+    auto destFilePath = PathUtils::concatenateFilePath(p_dest->fetchRelativePath(),
+                                                       PathUtils::fileName(srcFilePath));
+    destFilePath = getBackend()->renameIfExistsCaseInsensitive(destFilePath);
+    getBackend()->copyFile(srcFilePath, destFilePath);
+
+    // Copy media files fetched from content.
+    NodeContentMediaUtils::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);
+    }
+
+    // Create a file node.
+    auto notebook = getNotebook();
+    auto id = p_src->getId();
+    if (!p_move || p_src->getNotebook() != notebook) {
+        // Use a new id.
+        id = notebook->getAndUpdateNextNodeId();
+    }
+
+    auto destNode = QSharedPointer<FileNode>::create(id,
+                                                     PathUtils::fileName(destFilePath),
+                                                     p_src->getCreatedTimeUtc(),
+                                                     p_src->getModifiedTimeUtc(),
+                                                     attachmentFolder,
+                                                     p_src->getTags(),
+                                                     notebook,
+                                                     p_dest);
+    addChildNode(p_dest, destNode);
+    writeNodeConfig(p_dest);
+
+    if (p_move) {
+        // Delete src node.
+        p_src->getNotebook()->removeNode(p_src);
+    }
+
+    return destNode;
+}
+
+QSharedPointer<Node> VXNotebookConfigMgr::copyFolderNodeAsChildOf(const QSharedPointer<Node> &p_src,
+                                                                  Node *p_dest,
+                                                                  bool p_move)
+{
+    auto srcFolderPath = p_src->fetchAbsolutePath();
+    auto destFolderPath = PathUtils::concatenateFilePath(p_dest->fetchRelativePath(),
+                                                         PathUtils::fileName(srcFolderPath));
+    destFolderPath = getBackend()->renameIfExistsCaseInsensitive(destFolderPath);
+
+    // Make folder.
+    getBackend()->makePath(destFolderPath);
+
+    // Create a folder node.
+    auto notebook = getNotebook();
+    auto id = p_src->getId();
+    if (!p_move || p_src->getNotebook() != notebook) {
+        // Use a new id.
+        id = notebook->getAndUpdateNextNodeId();
+    }
+    auto destNode = QSharedPointer<FolderNode>::create(PathUtils::fileName(destFolderPath),
+                                                       notebook,
+                                                       p_dest);
+    destNode->loadFolder(id, p_src->getCreatedTimeUtc(), QVector<QSharedPointer<Node>>());
+
+    writeNodeConfig(destNode.data());
+
+    addChildNode(p_dest, destNode);
+    writeNodeConfig(p_dest);
+
+    // Copy children node.
+    for (const auto &childNode : p_src->getChildren()) {
+        copyNodeAsChildOf(childNode, destNode.data(), p_move);
+    }
+
+    if (p_move) {
+        p_src->getNotebook()->removeNode(p_src);
+    }
+
+    return destNode;
+}
+
+void VXNotebookConfigMgr::removeNode(const QSharedPointer<Node> &p_node, bool p_force, bool p_configOnly)
+{
+    auto parentNode = p_node->getParent();
+    if (!p_configOnly) {
+        // Remove all children.
+        for (auto &childNode : p_node->getChildren()) {
+            removeNode(childNode, p_force, p_configOnly);
+        }
+
+        removeFilesOfNode(p_node.data(), p_force);
+    }
+
+    if (parentNode) {
+        parentNode->removeChild(p_node);
+        writeNodeConfig(parentNode);
+    }
+}
+
+void VXNotebookConfigMgr::removeFilesOfNode(Node *p_node, bool p_force)
+{
+    Q_ASSERT(p_node->getNotebook() == getNotebook());
+    switch (p_node->getType()) {
+    case Node::Type::File:
+    {
+        // Delete attachment.
+        if (!p_node->getAttachmentFolder().isEmpty()) {
+            getBackend()->removeDir(p_node->fetchAttachmentFolderPath());
+        }
+
+        // Delete media files fetched from content.
+        NodeContentMediaUtils::removeMediaFiles(p_node);
+
+        // Delete node file itself.
+        auto filePath = p_node->fetchRelativePath();
+        getBackend()->removeFile(filePath);
+        break;
+    }
+
+    case Node::Type::Folder:
+    {
+        Q_ASSERT(p_node->getChildrenCount() == 0);
+        // Delete node config file and the dir if it is empty.
+        auto configFilePath = getNodeConfigFilePath(p_node);
+        getBackend()->removeFile(configFilePath);
+        auto folderPath = p_node->fetchRelativePath();
+        if (p_force) {
+            getBackend()->removeDir(folderPath);
+        } else {
+            getBackend()->removeEmptyDir(folderPath);
+            bool deleted = getBackend()->removeDirIfEmpty(folderPath);
+            if (!deleted) {
+                qWarning() << "folder is not deleted since it is not empty" << folderPath;
+            }
+        }
+        break;
+    }
+    }
+}
+
+bool VXNotebookConfigMgr::nodeExistsOnDisk(const Node *p_node) const
+{
+    return getBackend()->exists(p_node->fetchRelativePath());
+}
+
+QString VXNotebookConfigMgr::readNode(const Node *p_node) const
+{
+    Q_ASSERT(p_node->getType() == Node::Type::File);
+    return getBackend()->readTextFile(p_node->fetchRelativePath());
+}
+
+void VXNotebookConfigMgr::writeNode(Node *p_node, const QString &p_content)
+{
+    Q_ASSERT(p_node->getType() == Node::Type::File);
+    getBackend()->writeFile(p_node->fetchRelativePath(), p_content);
+
+    p_node->setModifiedTimeUtc();
+    writeNodeConfig(p_node->getParent());
+}
+
+QString VXNotebookConfigMgr::fetchNodeImageFolderPath(Node *p_node)
+{
+    auto pa = PathUtils::concatenateFilePath(PathUtils::parentDirPath(p_node->fetchAbsolutePath()),
+                                             getNotebook()->getImageFolder());
+    // Do not make the folder when it is a folder node request.
+    if (p_node->getType() == Node::Type::File) {
+        getBackend()->makePath(pa);
+    }
+    return pa;
+}
+
+QString VXNotebookConfigMgr::fetchNodeAttachmentFolderPath(Node *p_node)
+{
+    auto notebookFolder = PathUtils::concatenateFilePath(PathUtils::parentDirPath(p_node->fetchAbsolutePath()),
+                                                         getNotebook()->getAttachmentFolder());
+    if (p_node->getType() == Node::Type::File) {
+        auto nodeFolder = p_node->getAttachmentFolder();
+        if (nodeFolder.isEmpty()) {
+            auto folderPath = fetchNodeAttachmentFolder(p_node->fetchAbsolutePath(), nodeFolder);
+            p_node->setAttachmentFolder(nodeFolder);
+            saveNode(p_node);
+
+            getBackend()->makePath(folderPath);
+            return folderPath;
+        } else {
+            return PathUtils::concatenateFilePath(notebookFolder, nodeFolder);
+        }
+    } else {
+        // Do not make the folder when it is a folder node request.
+        return notebookFolder;
+    }
+}
+
+QString VXNotebookConfigMgr::fetchNodeAttachmentFolder(const QString &p_nodePath, QString &p_folderName)
+{
+    auto notebookFolder = PathUtils::concatenateFilePath(PathUtils::parentDirPath(p_nodePath),
+                                                         getNotebook()->getAttachmentFolder());
+    if (p_folderName.isEmpty()) {
+        p_folderName = FileUtils::generateUniqueFileName(notebookFolder, QString(), QString());
+    } else if (FileUtils::childExistsCaseInsensitive(notebookFolder, p_folderName)) {
+        p_folderName = FileUtils::generateFileNameWithSequence(notebookFolder, p_folderName, QString());
+    }
+    return PathUtils::concatenateFilePath(notebookFolder, p_folderName);
+}
+
+bool VXNotebookConfigMgr::isBuiltInFile(const Node *p_node, const QString &p_name) const
+{
+    const auto name = p_name.toLower();
+    if (name == c_nodeConfigName) {
+        return true;
+    }
+    return BundleNotebookConfigMgr::isBuiltInFile(p_node, p_name);
+}
+
+bool VXNotebookConfigMgr::isBuiltInFolder(const Node *p_node, const QString &p_name) const
+{
+    const auto name = p_name.toLower();
+    if (name == c_recycleBinFolderName
+        || name == getNotebook()->getImageFolder().toLower()
+        || name == getNotebook()->getAttachmentFolder().toLower()) {
+        return true;
+    }
+    return BundleNotebookConfigMgr::isBuiltInFolder(p_node, p_name);
+}
+
+QSharedPointer<Node> VXNotebookConfigMgr::copyFileAsChildOf(const QString &p_srcPath, Node *p_dest)
+{
+    // Copy source file itself.
+    auto destFilePath = PathUtils::concatenateFilePath(p_dest->fetchRelativePath(),
+                                                       PathUtils::fileName(p_srcPath));
+    destFilePath = getBackend()->renameIfExistsCaseInsensitive(destFilePath);
+    getBackend()->copyFile(p_srcPath, destFilePath);
+
+    // Copy media files fetched from content.
+    NodeContentMediaUtils::copyMediaFiles(p_srcPath, getBackend().data(), destFilePath);
+
+    // Create a file node.
+    auto currentTime = QDateTime::currentDateTimeUtc();
+    auto destNode = QSharedPointer<FileNode>::create(getNotebook()->getAndUpdateNextNodeId(),
+                                                     PathUtils::fileName(destFilePath),
+                                                     currentTime,
+                                                     currentTime,
+                                                     QString(),
+                                                     QStringList(),
+                                                     getNotebook(),
+                                                     p_dest);
+    addChildNode(p_dest, destNode);
+    writeNodeConfig(p_dest);
+
+    return destNode;
+}
+
+QSharedPointer<Node> VXNotebookConfigMgr::copyFolderAsChildOf(const QString &p_srcPath, Node *p_dest)
+{
+    auto destFolderPath = PathUtils::concatenateFilePath(p_dest->fetchRelativePath(),
+                                                         PathUtils::fileName(p_srcPath));
+    destFolderPath = getBackend()->renameIfExistsCaseInsensitive(destFolderPath);
+
+    // Copy folder.
+    getBackend()->copyDir(p_srcPath, destFolderPath);
+
+    // Create a folder node.
+    auto notebook = getNotebook();
+    auto destNode = QSharedPointer<FolderNode>::create(PathUtils::fileName(destFolderPath),
+                                                       notebook,
+                                                       p_dest);
+    destNode->loadFolder(notebook->getAndUpdateNextNodeId(), QDateTime::currentDateTimeUtc(), QVector<QSharedPointer<Node>>());
+
+    writeNodeConfig(destNode.data());
+
+    addChildNode(p_dest, destNode);
+    writeNodeConfig(p_dest);
+
+    return destNode;
+}
+
+void VXNotebookConfigMgr::inheritNodeFlags(const Node *p_node, Node *p_child) const
+{
+    if (p_node->getFlags() & Node::Flag::ReadOnly) {
+        markNodeReadOnly(p_child);
+    }
+}

+ 205 - 0
src/core/notebookconfigmgr/vxnotebookconfigmgr.h

@@ -0,0 +1,205 @@
+#ifndef VXNOTEBOOKCONFIGMGR_H
+#define VXNOTEBOOKCONFIGMGR_H
+
+#include "bundlenotebookconfigmgr.h"
+
+#include <QDateTime>
+#include <QVector>
+
+#include "../global.h"
+
+class QJsonObject;
+
+namespace vnotex
+{
+    class FolderNode;
+
+    // Config manager for VNoteX's bundle notebook.
+    class VXNotebookConfigMgr : public BundleNotebookConfigMgr
+    {
+        Q_OBJECT
+    public:
+        explicit VXNotebookConfigMgr(const QString &p_name,
+                                     const QString &p_displayName,
+                                     const QString &p_description,
+                                     const QSharedPointer<INotebookBackend> &p_backend,
+                                     QObject *p_parent = nullptr);
+
+        QString getName() const Q_DECL_OVERRIDE;
+
+        QString getDisplayName() const Q_DECL_OVERRIDE;
+
+        QString getDescription() const Q_DECL_OVERRIDE;
+
+        void createEmptySkeleton(const NotebookParameters &p_paras) Q_DECL_OVERRIDE;
+
+        QSharedPointer<Node> loadRootNode() const Q_DECL_OVERRIDE;
+
+        void loadNode(Node *p_node) const Q_DECL_OVERRIDE;
+        void saveNode(const Node *p_node) Q_DECL_OVERRIDE;
+
+        void renameNode(Node *p_node, const QString &p_name) Q_DECL_OVERRIDE;
+
+        QSharedPointer<Node> newNode(Node *p_parent,
+                                     Node::Type p_type,
+                                     const QString &p_name) Q_DECL_OVERRIDE;
+
+        QSharedPointer<Node> addAsNode(Node *p_parent,
+                                       Node::Type p_type,
+                                       const QString &p_name,
+                                       const NodeParameters &p_paras) Q_DECL_OVERRIDE;
+
+        QSharedPointer<Node> copyAsNode(Node *p_parent,
+                                        Node::Type p_type,
+                                        const QString &p_path) Q_DECL_OVERRIDE;
+
+        QSharedPointer<Node> loadNodeByPath(const QSharedPointer<Node> &p_root,
+                                            const QString &p_relativePath) Q_DECL_OVERRIDE;
+
+        QSharedPointer<Node> copyNodeAsChildOf(const QSharedPointer<Node> &p_src,
+                                               Node *p_dest,
+                                               bool p_move) Q_DECL_OVERRIDE;
+
+        void removeNode(const QSharedPointer<Node> &p_node, bool p_force = false, bool p_configOnly = false) Q_DECL_OVERRIDE;
+
+        bool nodeExistsOnDisk(const Node *p_node) const Q_DECL_OVERRIDE;
+
+        QString readNode(const Node *p_node) const Q_DECL_OVERRIDE;
+
+        void writeNode(Node *p_node, const QString &p_content) Q_DECL_OVERRIDE;
+
+        QString fetchNodeImageFolderPath(Node *p_node) Q_DECL_OVERRIDE;
+
+        QString fetchNodeAttachmentFolderPath(Node *p_node) Q_DECL_OVERRIDE;
+
+    private:
+        // Config of a file child.
+        struct NodeFileConfig
+        {
+            QJsonObject toJson() const;
+
+            void fromJson(const QJsonObject &p_jobj);
+
+            QString m_name;
+            ID m_id = Node::InvalidId;
+            QDateTime m_createdTimeUtc;
+            QDateTime m_modifiedTimeUtc;
+            QString m_attachmentFolder;
+            QStringList m_tags;
+        };
+
+        // Config of a folder child.
+        struct NodeFolderConfig
+        {
+            QJsonObject toJson() const;
+
+            void fromJson(const QJsonObject &p_jobj);
+
+            QString m_name;
+        };
+
+        // Config of a folder node.
+        struct NodeConfig
+        {
+            NodeConfig();
+
+            NodeConfig(const QString &p_version,
+                       ID p_id,
+                       const QDateTime &p_createdTimeUtc);
+
+            QJsonObject toJson() const;
+
+            void fromJson(const QJsonObject &p_jobj);
+
+            QString m_version;
+            ID m_id = Node::InvalidId;
+            QDateTime m_createdTimeUtc;
+            QVector<NodeFileConfig> m_files;
+            QVector<NodeFolderConfig> m_folders;
+
+            static const QString c_version;
+
+            static const QString c_id;
+
+            static const QString c_createdTimeUtc;
+
+            static const QString c_files;
+
+            static const QString c_folders;
+
+            static const QString c_name;
+
+            static const QString c_modifiedTimeUtc;
+
+            static const QString c_attachmentFolder;
+
+            static const QString c_tags;
+        };
+
+        void createEmptyRootNode();
+
+        QSharedPointer<VXNotebookConfigMgr::NodeConfig> readNodeConfig(const QString &p_path) const;
+        void writeNodeConfig(const QString &p_path, const NodeConfig &p_config) const;
+
+        void writeNodeConfig(const Node *p_node);
+
+        QSharedPointer<Node> nodeConfigToNode(const NodeConfig &p_config,
+                                              const QString &p_name,
+                                              Node *p_parent = nullptr) const;
+
+        void loadFolderNode(FolderNode *p_node, const NodeConfig &p_config) const;
+
+        QSharedPointer<VXNotebookConfigMgr::NodeConfig> nodeToNodeConfig(const Node *p_node) const;
+
+        QSharedPointer<Node> newFileNode(Node *p_parent,
+                                         const QString &p_name,
+                                         bool p_create,
+                                         const NodeParameters &p_paras);
+
+        QSharedPointer<Node> newFolderNode(Node *p_parent,
+                                           const QString &p_name,
+                                           bool p_create,
+                                           const NodeParameters &p_paras);
+
+        QString getNodeConfigFilePath(const Node *p_node) const;
+
+        void addChildNode(Node *p_parent, const QSharedPointer<Node> &p_child) const;
+
+        QSharedPointer<Node> copyFileNodeAsChildOf(const QSharedPointer<Node> &p_src, Node *p_dest, bool p_move);
+
+        QSharedPointer<Node> copyFolderNodeAsChildOf(const QSharedPointer<Node> &p_src, Node *p_dest, bool p_move);
+
+        QSharedPointer<Node> copyFileAsChildOf(const QString &p_srcPath, Node *p_dest);
+
+        QSharedPointer<Node> copyFolderAsChildOf(const QString &p_srcPath, Node *p_dest);
+
+        void removeFilesOfNode(Node *p_node, bool p_force);
+
+        bool markRecycleBinNode(const QSharedPointer<Node> &p_root) const;
+
+        void markNodeReadOnly(Node *p_node) const;
+
+        void createRecycleBinNode(const QSharedPointer<Node> &p_root);
+
+        // Generate node attachment folder.
+        // @p_folderName: suggested folder name if not empty, may be renamed due to conflicts.
+        // Return the attachment folder path.
+        QString fetchNodeAttachmentFolder(const QString &p_nodePath, QString &p_folderName);
+
+        bool isBuiltInFile(const Node *p_node, const QString &p_name) const Q_DECL_OVERRIDE;
+
+        bool isBuiltInFolder(const Node *p_node, const QString &p_name) const Q_DECL_OVERRIDE;
+
+        void inheritNodeFlags(const Node *p_node, Node *p_child) const;
+
+        Info m_info;
+
+        // Name of the node's config file.
+        static const QString c_nodeConfigName;
+
+        // Name of the recycle bin folder which should be a child of the root node.
+        static const QString c_recycleBinFolderName;
+    };
+} // ns vnotex
+
+#endif // VXNOTEBOOKCONFIGMGR_H

+ 35 - 0
src/core/notebookconfigmgr/vxnotebookconfigmgrfactory.cpp

@@ -0,0 +1,35 @@
+#include "vxnotebookconfigmgrfactory.h"
+
+#include <QObject>
+
+#include "vxnotebookconfigmgr.h"
+#include "../notebookbackend/inotebookbackend.h"
+
+using namespace vnotex;
+
+VXNotebookConfigMgrFactory::VXNotebookConfigMgrFactory()
+{
+}
+
+QString VXNotebookConfigMgrFactory::getName() const
+{
+    return QStringLiteral("vx.vnotex");
+}
+
+QString VXNotebookConfigMgrFactory::getDisplayName() const
+{
+    return QObject::tr("VNoteX Notebook Configuration");
+}
+
+QString VXNotebookConfigMgrFactory::getDescription() const
+{
+    return QObject::tr("Built-in VNoteX notebook configuration");
+}
+
+QSharedPointer<INotebookConfigMgr> VXNotebookConfigMgrFactory::createNotebookConfigMgr(const QSharedPointer<INotebookBackend> &p_backend)
+{
+    return QSharedPointer<VXNotebookConfigMgr>::create(getName(),
+                                                       getDisplayName(),
+                                                       getDescription(),
+                                                       p_backend);
+}

+ 25 - 0
src/core/notebookconfigmgr/vxnotebookconfigmgrfactory.h

@@ -0,0 +1,25 @@
+#ifndef VXNOTEBOOKCONFIGMGRFACTORY_H
+#define VXNOTEBOOKCONFIGMGRFACTORY_H
+
+
+#include "inotebookconfigmgrfactory.h"
+
+
+namespace vnotex
+{
+    class VXNotebookConfigMgrFactory : public INotebookConfigMgrFactory
+    {
+    public:
+        VXNotebookConfigMgrFactory();
+
+        QString getName() const Q_DECL_OVERRIDE;
+
+        QString getDisplayName() const Q_DECL_OVERRIDE;
+
+        QString getDescription()const Q_DECL_OVERRIDE;
+
+        QSharedPointer<INotebookConfigMgr> createNotebookConfigMgr(const QSharedPointer<INotebookBackend> &p_backend) Q_DECL_OVERRIDE;
+    };
+} // ns vnotex
+
+#endif // VXNOTEBOOKCONFIGMGRFACTORY_H

+ 376 - 0
src/core/notebookmgr.cpp

@@ -0,0 +1,376 @@
+#include "notebookmgr.h"
+
+#include <versioncontroller/dummyversioncontrollerfactory.h>
+#include <versioncontroller/iversioncontroller.h>
+#include <notebookconfigmgr/vxnotebookconfigmgrfactory.h>
+#include <notebookconfigmgr/inotebookconfigmgr.h>
+#include <notebookbackend/localnotebookbackendfactory.h>
+#include <notebookbackend/inotebookbackend.h>
+#include <notebook/bundlenotebookfactory.h>
+#include <notebook/notebook.h>
+#include <notebook/notebookparameters.h>
+#include "exception.h"
+#include "configmgr.h"
+#include <utils/pathutils.h>
+
+using namespace vnotex;
+
+NotebookMgr::NotebookMgr(QObject *p_parent)
+    : QObject(p_parent),
+      m_currentNotebookId(Notebook::InvalidId)
+{
+}
+
+void NotebookMgr::init()
+{
+    initVersionControllerServer();
+
+    initConfigMgrServer();
+
+    initBackendServer();
+
+    initNotebookServer();
+}
+
+void NotebookMgr::initVersionControllerServer()
+{
+    m_versionControllerServer.reset(new NameBasedServer<IVersionControllerFactory>);
+
+    // Dummy Version Controller.
+    auto dummyFactory = QSharedPointer<DummyVersionControllerFactory>::create();
+    m_versionControllerServer->registerItem(dummyFactory->getName(), dummyFactory);
+}
+
+void NotebookMgr::initConfigMgrServer()
+{
+    m_configMgrServer.reset(new NameBasedServer<INotebookConfigMgrFactory>);
+
+    // VX Notebook Config Manager.
+    auto vxFactory = QSharedPointer<VXNotebookConfigMgrFactory>::create();
+    m_configMgrServer->registerItem(vxFactory->getName(), vxFactory);
+
+}
+
+void NotebookMgr::initBackendServer()
+{
+    m_backendServer.reset(new NameBasedServer<INotebookBackendFactory>);
+
+    // Local Notebook Backend.
+    auto localFactory = QSharedPointer<LocalNotebookBackendFactory>::create();
+    m_backendServer->registerItem(localFactory->getName(), localFactory);
+}
+
+void NotebookMgr::initNotebookServer()
+{
+    m_notebookServer.reset(new NameBasedServer<INotebookFactory>);
+
+    // Bundle Notebook.
+    auto bundleFacotry = QSharedPointer<BundleNotebookFactory>::create();
+    m_notebookServer->registerItem(bundleFacotry->getName(), bundleFacotry);
+}
+
+QSharedPointer<INotebookFactory> NotebookMgr::getBundleNotebookFactory() const
+{
+    return m_notebookServer->getItem(QStringLiteral("bundle.vnotex"));
+}
+
+QList<QSharedPointer<INotebookFactory>> NotebookMgr::getAllNotebookFactories() const
+{
+    return m_notebookServer->getAllItems();
+}
+
+QList<QSharedPointer<IVersionControllerFactory>> NotebookMgr::getAllVersionControllerFactories() const
+{
+    return m_versionControllerServer->getAllItems();
+}
+
+QList<QSharedPointer<INotebookConfigMgrFactory>> NotebookMgr::getAllNotebookConfigMgrFactories() const
+{
+    return m_configMgrServer->getAllItems();
+}
+
+QList<QSharedPointer<INotebookBackendFactory>> NotebookMgr::getAllNotebookBackendFactories() const
+{
+    return m_backendServer->getAllItems();
+}
+
+QSharedPointer<INotebookBackend> NotebookMgr::createNotebookBackend(const QString &p_backendName,
+                                                                    const QString &p_rootFolderPath) const
+{
+    auto factory = m_backendServer->getItem(p_backendName);
+    if (factory) {
+        return factory->createNotebookBackend(p_rootFolderPath);
+    } else {
+        Exception::throwOne(Exception::Type::InvalidArgument,
+                            QString("fail to find notebook backend factory %1").arg(p_backendName));
+    }
+
+    return nullptr;
+}
+
+QSharedPointer<IVersionController> NotebookMgr::createVersionController(const QString &p_controllerName) const
+{
+    auto factory = m_versionControllerServer->getItem(p_controllerName);
+    if (factory) {
+        return factory->createVersionController();
+    } else {
+        Exception::throwOne(Exception::Type::InvalidArgument,
+                            QString("fail to find version controller factory %1").arg(p_controllerName));
+    }
+
+    return nullptr;
+}
+
+QSharedPointer<INotebookConfigMgr> NotebookMgr::createNotebookConfigMgr(const QString &p_mgrName,
+                                                                       const QSharedPointer<INotebookBackend> &p_backend) const
+{
+    auto factory = m_configMgrServer->getItem(p_mgrName);
+    if (factory) {
+        return factory->createNotebookConfigMgr(p_backend);
+    } else {
+        Exception::throwOne(Exception::Type::InvalidArgument,
+                            QString("fail to find notebook config manager factory %1").arg(p_mgrName));
+    }
+
+    return nullptr;
+}
+
+void NotebookMgr::loadNotebooks()
+{
+    readNotebooksFromConfig();
+
+    loadCurrentNotebookId();
+}
+
+static SessionConfig &getSessionConfig()
+{
+    return ConfigMgr::getInst().getSessionConfig();
+}
+
+void NotebookMgr::loadCurrentNotebookId()
+{
+    auto &rootFolderPath = getSessionConfig().getCurrentNotebookRootFolderPath();
+    auto notebook = findNotebookByRootFolderPath(rootFolderPath);
+    if (notebook) {
+        m_currentNotebookId = notebook->getId();
+    } else {
+        m_currentNotebookId = Notebook::InvalidId;
+    }
+
+    emit currentNotebookChanged(notebook);
+}
+
+QSharedPointer<Notebook> NotebookMgr::newNotebook(const QSharedPointer<NotebookParameters> &p_parameters)
+{
+    auto factory = m_notebookServer->getItem(p_parameters->m_type);
+    if (!factory) {
+        Exception::throwOne(Exception::Type::InvalidArgument,
+                            QString("fail to find notebook factory %1").arg(p_parameters->m_type));
+    }
+
+    auto notebook = factory->newNotebook(*p_parameters);
+    addNotebook(notebook);
+
+    saveNotebooksToConfig();
+
+    emit notebooksUpdated();
+
+    setCurrentNotebook(notebook->getId());
+
+    return notebook;
+}
+
+void NotebookMgr::importNotebook(const QSharedPointer<Notebook> &p_notebook)
+{
+    Q_ASSERT(p_notebook);
+    if (m_notebooks.indexOf(p_notebook) != -1) {
+        return;
+    }
+
+    addNotebook(p_notebook);
+
+    saveNotebooksToConfig();
+
+    emit notebooksUpdated();
+
+    setCurrentNotebook(p_notebook->getId());
+}
+
+static SessionConfig::NotebookItem notebookToSessionConfig(const QSharedPointer<const Notebook> &p_notebook)
+{
+    SessionConfig::NotebookItem item;
+    item.m_type = p_notebook->getType();
+    item.m_rootFolderPath = p_notebook->getRootFolderPath();
+    item.m_backend = p_notebook->getBackend()->getName();
+    return item;
+}
+
+void NotebookMgr::saveNotebooksToConfig() const
+{
+    QVector<SessionConfig::NotebookItem> items;
+    items.reserve(m_notebooks.size());
+    for (auto &nb : m_notebooks) {
+        items.push_back(notebookToSessionConfig(nb));
+    }
+
+    getSessionConfig().setNotebooks(items);
+}
+
+void NotebookMgr::readNotebooksFromConfig()
+{
+    Q_ASSERT(m_notebooks.isEmpty());
+    auto items = getSessionConfig().getNotebooks();
+    for (auto &item : items) {
+        try {
+            auto nb = readNotebookFromConfig(item);
+            addNotebook(nb);
+        } catch (Exception &p_e) {
+            qCritical("fail to read notebook (%s) from config (%s)",
+                      item.m_rootFolderPath.toStdString().c_str(),
+                      p_e.what());
+        }
+    }
+
+    emit notebooksUpdated();
+}
+
+QSharedPointer<Notebook> NotebookMgr::readNotebookFromConfig(const SessionConfig::NotebookItem &p_item)
+{
+    auto factory = m_notebookServer->getItem(p_item.m_type);
+    if (!factory) {
+        Exception::throwOne(Exception::Type::InvalidArgument,
+                            QString("fail to find notebook factory %1").arg(p_item.m_type));
+    }
+
+    auto backend = createNotebookBackend(p_item.m_backend, p_item.m_rootFolderPath);
+
+    auto notebook = factory->createNotebook(*this, p_item.m_rootFolderPath, backend);
+    return notebook;
+}
+
+const QVector<QSharedPointer<Notebook>> &NotebookMgr::getNotebooks() const
+{
+    return m_notebooks;
+}
+
+ID NotebookMgr::getCurrentNotebookId() const
+{
+    return m_currentNotebookId;
+}
+
+void NotebookMgr::setCurrentNotebook(ID p_notebookId)
+{
+    auto lastId = m_currentNotebookId;
+    m_currentNotebookId = p_notebookId;
+    auto nb = findNotebookById(p_notebookId);
+    if (!nb) {
+        m_currentNotebookId = Notebook::InvalidId;
+    }
+
+    if (lastId != m_currentNotebookId) {
+        emit currentNotebookChanged(nb);
+    }
+
+    getSessionConfig().setCurrentNotebookRootFolderPath(nb ? nb->getRootFolderPath() : "");
+}
+
+QSharedPointer<Notebook> NotebookMgr::findNotebookByRootFolderPath(const QString &p_rootFolderPath) const
+{
+    for (auto &nb : m_notebooks) {
+        if (PathUtils::areSamePaths(nb->getRootFolderPath(), p_rootFolderPath)) {
+            return nb;
+        }
+    }
+
+    return nullptr;
+}
+
+QSharedPointer<Notebook> NotebookMgr::findNotebookById(ID p_id) const
+{
+    for (auto &nb : m_notebooks) {
+        if (nb->getId() == p_id) {
+            return nb;
+        }
+    }
+
+    return nullptr;
+}
+
+void NotebookMgr::closeNotebook(ID p_id)
+{
+    auto it = std::find_if(m_notebooks.begin(),
+                           m_notebooks.end(),
+                           [p_id](const QSharedPointer<Notebook> &p_nb) {
+                               return p_nb->getId() == p_id;
+                           });
+    if (it == m_notebooks.end()) {
+        qWarning() << "fail to find notebook of given id to close" << p_id;
+        return;
+    }
+
+    auto notebookToClose = *it;
+    emit notebookAboutToClose(notebookToClose.data());
+
+    m_notebooks.erase(it);
+
+    saveNotebooksToConfig();
+
+    emit notebooksUpdated();
+    setCurrentNotebookAfterUpdate();
+
+    qInfo() << QString("notebook %1 (%2) is closed").arg(notebookToClose->getName(),
+                                                         notebookToClose->getRootFolderPath());
+}
+
+void NotebookMgr::removeNotebook(ID p_id)
+{
+    auto it = std::find_if(m_notebooks.begin(),
+                           m_notebooks.end(),
+                           [p_id](const QSharedPointer<Notebook> &p_nb) {
+                               return p_nb->getId() == p_id;
+                           });
+    if (it == m_notebooks.end()) {
+        qWarning() << "fail to find notebook of given id to remove" << p_id;
+        return;
+    }
+
+    auto nbToRemove = *it;
+    emit notebookAboutToRemove(nbToRemove.data());
+
+    m_notebooks.erase(it);
+
+    saveNotebooksToConfig();
+
+    emit notebooksUpdated();
+    setCurrentNotebookAfterUpdate();
+
+    try {
+        nbToRemove->remove();
+    } catch (Exception &p_e) {
+        qWarning() << QString("fail to remove notebook %1 (%2) (%3)").arg(nbToRemove->getName(),
+                                                                          nbToRemove->getRootFolderPath(),
+                                                                          p_e.what());
+        throw;
+    }
+
+    qInfo() << QString("notebook %1 (%2) is removed").arg(nbToRemove->getName(),
+                                                          nbToRemove->getRootFolderPath());
+}
+
+void NotebookMgr::setCurrentNotebookAfterUpdate()
+{
+    if (!m_notebooks.isEmpty()) {
+        setCurrentNotebook(m_notebooks.first()->getId());
+    } else {
+        setCurrentNotebook(Notebook::InvalidId);
+    }
+}
+
+void NotebookMgr::addNotebook(const QSharedPointer<Notebook> &p_notebook)
+{
+    m_notebooks.push_back(p_notebook);
+    connect(p_notebook.data(), &Notebook::updated,
+            this, [this, notebook = p_notebook.data()]() {
+                emit notebookUpdated(notebook);
+            });
+}

+ 118 - 0
src/core/notebookmgr.h

@@ -0,0 +1,118 @@
+#ifndef NOTEBOOKMGR_H
+#define NOTEBOOKMGR_H
+
+#include <QObject>
+#include <QScopedPointer>
+#include <QList>
+#include <QVector>
+
+#include "namebasedserver.h"
+#include "sessionconfig.h"
+#include "global.h"
+#include "notebook/notebook.h"
+
+namespace vnotex
+{
+    class IVersionController;
+    class IVersionControllerFactory;
+    class INotebookConfigMgr;
+    class INotebookConfigMgrFactory;
+    class INotebookBackend;
+    class INotebookBackendFactory;
+    class INotebookFactory;
+    class NotebookParameters;
+
+    class NotebookMgr : public QObject
+    {
+        Q_OBJECT
+    public:
+        explicit NotebookMgr(QObject *p_parent = nullptr);
+
+        void init();
+
+        QSharedPointer<INotebookFactory> getBundleNotebookFactory() const;
+
+        QList<QSharedPointer<INotebookFactory>> getAllNotebookFactories() const;
+
+        QList<QSharedPointer<IVersionControllerFactory>> getAllVersionControllerFactories() const;
+
+        QList<QSharedPointer<INotebookConfigMgrFactory>> getAllNotebookConfigMgrFactories() const;
+
+        QList<QSharedPointer<INotebookBackendFactory>> getAllNotebookBackendFactories() const;
+
+        QSharedPointer<INotebookBackend> createNotebookBackend(const QString &p_backendName,
+                                                               const QString &p_rootFolderPath) const;
+
+        QSharedPointer<IVersionController> createVersionController(const QString &p_controllerName) const;
+
+        QSharedPointer<INotebookConfigMgr> createNotebookConfigMgr(const QString &p_mgrName,
+                                                                   const QSharedPointer<INotebookBackend> &p_backend) const;
+
+        void loadNotebooks();
+
+        QSharedPointer<Notebook> newNotebook(const QSharedPointer<NotebookParameters> &p_parameters);
+
+        void importNotebook(const QSharedPointer<Notebook> &p_notebook);
+
+        const QVector<QSharedPointer<Notebook>> &getNotebooks() const;
+
+        ID getCurrentNotebookId() const;
+
+        // Find the notebook with the same directory as root folder.
+        QSharedPointer<Notebook> findNotebookByRootFolderPath(const QString &p_rootFolderPath) const;
+
+        QSharedPointer<Notebook> findNotebookById(ID p_id) const;
+
+        void closeNotebook(ID p_id);
+
+        void removeNotebook(ID p_id);
+
+    public slots:
+        void setCurrentNotebook(ID p_notebookId);
+
+    signals:
+        void notebooksUpdated();
+
+        void notebookUpdated(const Notebook *p_notebook);
+
+        void notebookAboutToClose(const Notebook *p_notebook);
+
+        void notebookAboutToRemove(const Notebook *p_notebook);
+
+        void currentNotebookChanged(const QSharedPointer<Notebook> &p_notebook);
+
+    private:
+        void initVersionControllerServer();
+
+        void initConfigMgrServer();
+
+        void initBackendServer();
+
+        void initNotebookServer();
+
+        void saveNotebooksToConfig() const;
+        void readNotebooksFromConfig();
+
+        void loadCurrentNotebookId();
+
+        QSharedPointer<Notebook> readNotebookFromConfig(const SessionConfig::NotebookItem &p_item);
+
+        void setCurrentNotebookAfterUpdate();
+
+        void addNotebook(const QSharedPointer<Notebook> &p_notebook);
+
+        QSharedPointer<NameBasedServer<IVersionControllerFactory>> m_versionControllerServer;
+
+        QSharedPointer<NameBasedServer<INotebookConfigMgrFactory>> m_configMgrServer;
+
+        QSharedPointer<NameBasedServer<INotebookBackendFactory>> m_backendServer;
+
+        QSharedPointer<NameBasedServer<INotebookFactory>> m_notebookServer;
+
+        QVector<QSharedPointer<Notebook>> m_notebooks;
+
+        ID m_currentNotebookId = 0;
+    };
+} // ns vnotex
+
+#endif // NOTEBOOKMGR_H

+ 253 - 0
src/core/sessionconfig.cpp

@@ -0,0 +1,253 @@
+#include "sessionconfig.h"
+
+#include <QDir>
+#include <QDebug>
+#include <QJsonArray>
+#include <QJsonDocument>
+
+#include <utils/fileutils.h>
+
+#include "configmgr.h"
+
+using namespace vnotex;
+
+bool SessionConfig::NotebookItem::operator==(const NotebookItem &p_other) const
+{
+    return m_type == p_other.m_type
+           && m_rootFolderPath == p_other.m_rootFolderPath
+           && m_backend == p_other.m_backend;
+}
+
+void SessionConfig::NotebookItem::fromJson(const QJsonObject &p_jobj)
+{
+    m_type = p_jobj[QStringLiteral("type")].toString();
+    m_rootFolderPath = p_jobj[QStringLiteral("root_folder")].toString();
+    m_backend = p_jobj[QStringLiteral("backend")].toString();
+}
+
+QJsonObject SessionConfig::NotebookItem::toJson() const
+{
+    QJsonObject jobj;
+
+    jobj[QStringLiteral("type")] = m_type;
+    jobj[QStringLiteral("root_folder")] = m_rootFolderPath;
+    jobj[QStringLiteral("backend")] = m_backend;
+
+    return jobj;
+}
+
+SessionConfig::SessionConfig(ConfigMgr *p_mgr)
+    : IConfig(p_mgr, nullptr)
+{
+}
+
+SessionConfig::~SessionConfig()
+{
+
+}
+
+void SessionConfig::init()
+{
+    auto mgr = getMgr();
+    auto sessionSettings = mgr->getSettings(ConfigMgr::Source::Session);
+    const auto &sessionJobj = sessionSettings->getJson();
+
+    loadCore(sessionJobj);
+}
+
+void SessionConfig::loadCore(const QJsonObject &p_session)
+{
+    const auto coreObj = p_session.value(QStringLiteral("core")).toObject();
+    m_newNotebookDefaultRootFolderPath = readString(coreObj,
+        QStringLiteral("new_notebook_default_root_folder_path"));
+    if (m_newNotebookDefaultRootFolderPath.isEmpty()) {
+        m_newNotebookDefaultRootFolderPath = QDir::homePath();
+    }
+
+    m_currentNotebookRootFolderPath = readString(coreObj,
+        QStringLiteral("current_notebook_root_folder_path"));
+
+    {
+        auto option = readString(coreObj, QStringLiteral("opengl"));
+        m_openGL = stringToOpenGL(option);
+    }
+
+    m_systemTitleBarEnabled = readBool(coreObj, QStringLiteral("system_title_bar"));
+}
+
+QJsonObject SessionConfig::saveCore() const
+{
+    QJsonObject coreObj;
+    coreObj[QStringLiteral("new_notebook_default_root_folder_path")] = m_newNotebookDefaultRootFolderPath;
+    coreObj[QStringLiteral("current_notebook_root_folder_path")] = m_currentNotebookRootFolderPath;
+    coreObj[QStringLiteral("opengl")] = openGLToString(m_openGL);
+    coreObj[QStringLiteral("system_title_bar")] = m_systemTitleBarEnabled;
+    return coreObj;
+}
+
+const QString &SessionConfig::getNewNotebookDefaultRootFolderPath() const
+{
+    return m_newNotebookDefaultRootFolderPath;
+}
+
+void SessionConfig::setNewNotebookDefaultRootFolderPath(const QString &p_path)
+{
+    updateConfig(m_newNotebookDefaultRootFolderPath,
+                 p_path,
+                 this);
+}
+
+const QVector<SessionConfig::NotebookItem> &SessionConfig::getNotebooks()
+{
+    if (m_notebooks.isEmpty()) {
+        auto mgr = getMgr();
+        auto sessionSettings = mgr->getSettings(ConfigMgr::Source::Session);
+        const auto &sessionJobj = sessionSettings->getJson();
+        loadNotebooks(sessionJobj);
+    }
+    return m_notebooks;
+}
+
+void SessionConfig::setNotebooks(const QVector<SessionConfig::NotebookItem> &p_notebooks)
+{
+    updateConfig(m_notebooks,
+                 p_notebooks,
+                 this);
+}
+
+void SessionConfig::loadNotebooks(const QJsonObject &p_session)
+{
+    const auto notebooksJson = p_session.value(QStringLiteral("notebooks")).toArray();
+    m_notebooks.resize(notebooksJson.size());
+    for (int i = 0; i < notebooksJson.size(); ++i) {
+        m_notebooks[i].fromJson(notebooksJson[i].toObject());
+    }
+}
+
+QJsonArray SessionConfig::saveNotebooks() const
+{
+    QJsonArray nbArray;
+    for (const auto &nb : m_notebooks) {
+        nbArray.append(nb.toJson());
+    }
+    return nbArray;
+}
+
+const QString &SessionConfig::getCurrentNotebookRootFolderPath() const
+{
+    return m_currentNotebookRootFolderPath;
+}
+
+void SessionConfig::setCurrentNotebookRootFolderPath(const QString &p_path)
+{
+    updateConfig(m_currentNotebookRootFolderPath,
+                 p_path,
+                 this);
+}
+
+void SessionConfig::writeToSettings() const
+{
+    getMgr()->writeSessionSettings(toJson());
+}
+
+QJsonObject SessionConfig::toJson() const
+{
+    QJsonObject obj;
+    obj[QStringLiteral("core")] = saveCore();
+    obj[QStringLiteral("notebooks")] = saveNotebooks();
+    obj[QStringLiteral("state_geometry")] = saveStateAndGeometry();
+    return obj;
+}
+
+QJsonObject SessionConfig::saveStateAndGeometry() const
+{
+    QJsonObject obj;
+    writeByteArray(obj, QStringLiteral("main_window_state"), m_mainWindowStateGeometry.m_mainState);
+    writeByteArray(obj, QStringLiteral("main_window_geometry"), m_mainWindowStateGeometry.m_mainGeometry);
+    return obj;
+}
+
+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;
+}
+
+void SessionConfig::setMainWindowStateGeometry(const SessionConfig::MainWindowStateGeometry &p_state)
+{
+    m_mainWindowStateGeometry = p_state;
+    ++m_revision;
+    writeToSettings();
+}
+
+SessionConfig::OpenGL SessionConfig::getOpenGLAtBootstrap()
+{
+    auto userConfigFile = ConfigMgr::locateSessionConfigFilePathAtBootstrap();
+    if (!userConfigFile.isEmpty()) {
+        auto bytes = FileUtils::readFile(userConfigFile);
+        auto obj = QJsonDocument::fromJson(bytes).object();
+        auto coreObj = obj.value(QStringLiteral("core")).toObject();
+        auto str = coreObj.value(QStringLiteral("opengl")).toString();
+        return stringToOpenGL(str);
+    }
+
+    return OpenGL::None;
+}
+
+SessionConfig::OpenGL SessionConfig::getOpenGL() const
+{
+    return m_openGL;
+}
+
+void SessionConfig::setOpenGL(OpenGL p_option)
+{
+    updateConfig(m_openGL, p_option, this);
+}
+
+QString SessionConfig::openGLToString(OpenGL p_option)
+{
+    switch (p_option) {
+    case OpenGL::Desktop:
+        return QStringLiteral("desktop");
+
+    case OpenGL::Angle:
+        return QStringLiteral("angle");
+
+    case OpenGL::Software:
+        return QStringLiteral("software");
+
+    default:
+        return QStringLiteral("none");
+    }
+}
+
+SessionConfig::OpenGL SessionConfig::stringToOpenGL(const QString &p_str)
+{
+    auto option = p_str.toLower();
+    if (option == QStringLiteral("software")) {
+        return OpenGL::Software;
+    } else if (option == QStringLiteral("desktop")) {
+        return OpenGL::Desktop;
+    } else if (option == QStringLiteral("angle")) {
+        return OpenGL::Angle;
+    } else {
+        return OpenGL::None;
+    }
+}
+
+bool SessionConfig::getSystemTitleBarEnabled() const
+{
+    return m_systemTitleBarEnabled;
+}
+
+void SessionConfig::setSystemTitleBarEnabled(bool p_enabled)
+{
+    updateConfig(m_systemTitleBarEnabled, p_enabled, this);
+}

+ 111 - 0
src/core/sessionconfig.h

@@ -0,0 +1,111 @@
+#ifndef SESSIONCONFIG_H
+#define SESSIONCONFIG_H
+
+#include "iconfig.h"
+
+#include <QString>
+#include <QVector>
+
+namespace vnotex
+{
+    class SessionConfig : public IConfig
+    {
+    public:
+        struct NotebookItem
+        {
+            NotebookItem() = default;
+
+            bool operator==(const NotebookItem &p_other) const;
+
+            void fromJson(const QJsonObject &p_jobj);
+
+            QJsonObject toJson() const;
+
+            QString m_type;
+            QString m_rootFolderPath;
+            QString m_backend;
+        };
+
+        struct MainWindowStateGeometry
+        {
+            bool operator==(const MainWindowStateGeometry &p_other) const
+            {
+                return m_mainState == p_other.m_mainState
+                       && m_mainGeometry == p_other.m_mainGeometry;
+            }
+
+            QByteArray m_mainState;
+            QByteArray m_mainGeometry;
+        };
+
+        enum OpenGL
+        {
+            None,
+            Desktop,
+            Angle,
+            Software
+        };
+
+        explicit SessionConfig(ConfigMgr *p_mgr);
+
+        ~SessionConfig();
+
+        void init() Q_DECL_OVERRIDE;
+
+        const QString &getNewNotebookDefaultRootFolderPath() const;
+        void setNewNotebookDefaultRootFolderPath(const QString &p_path);
+
+        const QString &getCurrentNotebookRootFolderPath() const;
+        void setCurrentNotebookRootFolderPath(const QString &p_path);
+
+        const QVector<SessionConfig::NotebookItem> &getNotebooks();
+        void setNotebooks(const QVector<SessionConfig::NotebookItem> &p_notebooks);
+
+        void writeToSettings() const Q_DECL_OVERRIDE;
+
+        QJsonObject toJson() const Q_DECL_OVERRIDE;
+
+        SessionConfig::MainWindowStateGeometry getMainWindowStateGeometry() const;
+        void setMainWindowStateGeometry(const SessionConfig::MainWindowStateGeometry &p_state);
+
+        OpenGL getOpenGL() const;
+        void setOpenGL(OpenGL p_option);
+
+        bool getSystemTitleBarEnabled() const;
+        void setSystemTitleBarEnabled(bool p_enabled);
+
+        static OpenGL getOpenGLAtBootstrap();
+
+        static QString openGLToString(OpenGL p_option);
+        static OpenGL stringToOpenGL(const QString &p_str);
+
+    private:
+        void loadCore(const QJsonObject &p_session);
+
+        QJsonObject saveCore() const;
+
+        void loadNotebooks(const QJsonObject &p_session);
+
+        QJsonArray saveNotebooks() const;
+
+        QJsonObject saveStateAndGeometry() const;
+
+        QString m_newNotebookDefaultRootFolderPath;
+
+        // Use root folder to identify a notebook uniquely.
+        QString m_currentNotebookRootFolderPath;
+
+        QVector<SessionConfig::NotebookItem> 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;
+
+        // Whether use system's title bar or not.
+        bool m_systemTitleBarEnabled = false;
+    };
+} // ns vnotex
+
+#endif // SESSIONCONFIG_H

+ 184 - 0
src/core/singleinstanceguard.cpp

@@ -0,0 +1,184 @@
+#include "singleinstanceguard.h"
+#include <QDebug>
+
+#include <utils/utils.h>
+
+using namespace vnotex;
+
+const QString SingleInstanceGuard::c_memKey = "vnotex_shared_memory";
+const int SingleInstanceGuard::c_magic = 376686683;
+
+SingleInstanceGuard::SingleInstanceGuard()
+    : m_online(false),
+      m_sharedMemory(c_memKey)
+{
+}
+
+bool SingleInstanceGuard::tryRun()
+{
+    m_online = false;
+
+    // If we can attach to the sharedmemory, there is another instance running.
+    // In Linux, crashes may cause the shared memory segment remains. In this case,
+    // this will attach to the old segment, then exit, freeing the old segment.
+    if (m_sharedMemory.attach()) {
+        qInfo() << "another instance is running";
+        return false;
+    }
+
+    // Try to create it.
+    bool ret = m_sharedMemory.create(sizeof(SharedStruct));
+    if (ret) {
+        // We created it.
+        m_sharedMemory.lock();
+        SharedStruct *str = (SharedStruct *)m_sharedMemory.data();
+        str->m_magic = c_magic;
+        str->m_filesBufIdx = 0;
+        str->m_askedToShow = false;
+        m_sharedMemory.unlock();
+
+        m_online = true;
+        return true;
+    } else {
+        qCritical() << "fail to create shared memory segment";
+        return false;
+    }
+}
+
+void SingleInstanceGuard::openExternalFiles(const QStringList &p_files)
+{
+    if (p_files.isEmpty()) {
+        return;
+    }
+
+    if (!m_sharedMemory.isAttached()) {
+        if (!m_sharedMemory.attach()) {
+            qCritical() << "fail to attach to the shared memory segment"
+                        << (m_sharedMemory.error() ? m_sharedMemory.errorString() : "");
+            return;
+        }
+    }
+
+    int idx = 0;
+    int tryCount = 100;
+    while (tryCount--) {
+        m_sharedMemory.lock();
+        SharedStruct *str = (SharedStruct *)m_sharedMemory.data();
+        V_ASSERT(str->m_magic == c_magic);
+        for (; idx < p_files.size(); ++idx) {
+            if (p_files[idx].size() + 1 > FilesBufCount) {
+                // Skip this long long name file.
+                continue;
+            }
+
+            if (!appendFileToBuffer(str, p_files[idx])) {
+                break;
+            }
+        }
+
+        m_sharedMemory.unlock();
+
+        if (idx < p_files.size()) {
+            Utils::sleepWait(500);
+        } else {
+            break;
+        }
+    }
+}
+
+bool SingleInstanceGuard::appendFileToBuffer(SharedStruct *p_str, const QString &p_file)
+{
+    if (p_file.isEmpty()) {
+        return true;
+    }
+
+    int strSize = p_file.size();
+    if (strSize + 1 > FilesBufCount - p_str->m_filesBufIdx) {
+        return false;
+    }
+
+    // Put the size first.
+    p_str->m_filesBuf[p_str->m_filesBufIdx++] = (ushort)strSize;
+    const QChar *data = p_file.constData();
+    for (int i = 0; i < strSize; ++i) {
+        p_str->m_filesBuf[p_str->m_filesBufIdx++] = data[i].unicode();
+    }
+
+    return true;
+}
+
+QStringList SingleInstanceGuard::fetchFilesToOpen()
+{
+    QStringList files;
+
+    if (!m_online) {
+        return files;
+    }
+
+    Q_ASSERT(m_sharedMemory.isAttached());
+    m_sharedMemory.lock();
+    SharedStruct *str = (SharedStruct *)m_sharedMemory.data();
+    V_ASSERT(str->m_magic == c_magic);
+    Q_ASSERT(str->m_filesBufIdx <= FilesBufCount);
+    int idx = 0;
+    while (idx < str->m_filesBufIdx) {
+        int strSize = str->m_filesBuf[idx++];
+        Q_ASSERT(strSize <= str->m_filesBufIdx - idx);
+        QString file;
+        for (int i = 0; i < strSize; ++i) {
+            file.append(QChar(str->m_filesBuf[idx++]));
+        }
+
+        files.append(file);
+    }
+
+    str->m_filesBufIdx = 0;
+    m_sharedMemory.unlock();
+
+    return files;
+}
+
+void SingleInstanceGuard::showInstance()
+{
+    if (!m_sharedMemory.isAttached()) {
+        if (!m_sharedMemory.attach()) {
+            qCritical() << "fail to attach to the shared memory segment"
+                        << (m_sharedMemory.error() ? m_sharedMemory.errorString() : "");
+            return;
+        }
+    }
+
+    m_sharedMemory.lock();
+    SharedStruct *str = (SharedStruct *)m_sharedMemory.data();
+    V_ASSERT(str->m_magic == c_magic);
+    str->m_askedToShow = true;
+    m_sharedMemory.unlock();
+}
+
+bool SingleInstanceGuard::fetchAskedToShow()
+{
+    if (!m_online) {
+        return false;
+    }
+
+    Q_ASSERT(m_sharedMemory.isAttached());
+    m_sharedMemory.lock();
+    SharedStruct *str = (SharedStruct *)m_sharedMemory.data();
+    V_ASSERT(str->m_magic == c_magic);
+    bool ret = str->m_askedToShow;
+    str->m_askedToShow = false;
+    m_sharedMemory.unlock();
+
+    return ret;
+}
+
+void SingleInstanceGuard::exit()
+{
+    if (!m_online) {
+        return;
+    }
+
+    Q_ASSERT(m_sharedMemory.isAttached());
+    m_sharedMemory.detach();
+    m_online = false;
+}

Някои файлове не бяха показани, защото твърде много файлове са промени