Prechádzať zdrojové kódy

add sqlite database

Le Tan 4 rokov pred
rodič
commit
6689e8c84c
65 zmenil súbory, kde vykonal 1935 pridanie a 594 odobranie
  1. 1 1
      libs/vtextedit
  2. 27 3
      src/core/configmgr.cpp
  3. 10 7
      src/core/configmgr.h
  4. 85 11
      src/core/notebook/bundlenotebook.cpp
  5. 19 4
      src/core/notebook/bundlenotebook.h
  6. 49 18
      src/core/notebook/node.cpp
  7. 17 21
      src/core/notebook/node.h
  8. 8 0
      src/core/notebook/nodeparameters.cpp
  9. 34 0
      src/core/notebook/nodeparameters.h
  10. 25 2
      src/core/notebook/notebook.cpp
  11. 13 5
      src/core/notebook/notebook.h
  12. 4 0
      src/core/notebook/notebook.pri
  13. 381 0
      src/core/notebook/notebookdatabaseaccess.cpp
  14. 97 0
      src/core/notebook/notebookdatabaseaccess.h
  15. 3 11
      src/core/notebook/vxnode.cpp
  16. 2 8
      src/core/notebook/vxnode.h
  17. 10 0
      src/core/notebookconfigmgr/bundlenotebookconfigmgr.cpp
  18. 4 2
      src/core/notebookconfigmgr/bundlenotebookconfigmgr.h
  19. 0 6
      src/core/notebookconfigmgr/inotebookconfigmgr.cpp
  20. 3 4
      src/core/notebookconfigmgr/inotebookconfigmgr.h
  21. 3 16
      src/core/notebookconfigmgr/notebookconfig.cpp
  22. 3 7
      src/core/notebookconfigmgr/notebookconfig.h
  23. 2 0
      src/core/notebookconfigmgr/notebookconfigmgr.pri
  24. 173 0
      src/core/notebookconfigmgr/vxnodeconfig.cpp
  25. 107 0
      src/core/notebookconfigmgr/vxnodeconfig.h
  26. 210 210
      src/core/notebookconfigmgr/vxnotebookconfigmgr.cpp
  27. 49 75
      src/core/notebookconfigmgr/vxnotebookconfigmgr.h
  28. 1 0
      src/core/notebookmgr.cpp
  29. 1 0
      src/core/vnotex.cpp
  30. 0 2
      src/data/core/core.qrc
  31. 0 15
      src/data/core/icons/horizontal_split.svg
  32. 0 15
      src/data/core/icons/vertical_split.svg
  33. 7 0
      src/data/extra/docs/en/features_tips.txt
  34. 7 0
      src/data/extra/docs/zh_CN/features_tips.txt
  35. 2 0
      src/data/extra/extra.qrc
  36. 19 7
      src/export/webviewexporter.cpp
  37. 2 1
      src/src.pro
  38. 3 2
      src/utils/fileutils.cpp
  39. 1 1
      src/utils/pathutils.cpp
  40. 2 0
      src/widgets/dialogs/exportdialog.cpp
  41. 1 0
      src/widgets/dialogs/importfolderutils.cpp
  42. 1 1
      src/widgets/dialogs/newfolderdialog.cpp
  43. 11 3
      src/widgets/editors/markdowneditor.cpp
  44. 2 0
      src/widgets/editors/markdowneditor.h
  45. 27 1
      src/widgets/editors/texteditor.cpp
  46. 6 0
      src/widgets/editors/texteditor.h
  47. 28 2
      src/widgets/mainwindow.cpp
  48. 3 0
      src/widgets/markdownviewwindow.cpp
  49. 37 2
      src/widgets/notebookexplorer.cpp
  50. 2 0
      src/widgets/notebookexplorer.h
  51. 10 9
      src/widgets/notebooknodeexplorer.cpp
  52. 5 4
      src/widgets/notebooknodeexplorer.h
  53. 8 1
      src/widgets/searchpanel.cpp
  54. 3 0
      src/widgets/searchpanel.h
  55. 3 0
      src/widgets/textviewwindow.cpp
  56. 3 8
      src/widgets/viewsplit.cpp
  57. 69 0
      tests/test_core/test_notebook/dummynode.cpp
  58. 33 0
      tests/test_core/test_notebook/dummynode.h
  59. 59 0
      tests/test_core/test_notebook/dummynotebook.cpp
  60. 38 0
      tests/test_core/test_notebook/dummynotebook.h
  61. 5 88
      tests/test_core/test_notebook/test_notebook.cpp
  62. 1 19
      tests/test_core/test_notebook/test_notebook.h
  63. 10 2
      tests/test_core/test_notebook/test_notebook.pro
  64. 148 0
      tests/test_core/test_notebook/testnotebookdatabase.cpp
  65. 38 0
      tests/test_core/test_notebook/testnotebookdatabase.h

+ 1 - 1
libs/vtextedit

@@ -1 +1 @@
-Subproject commit 0733259fed01ecaa11678fac00fd67397ff7c39c
+Subproject commit c91929729fdd7b47e067c721d6148c7c3e6f9ced

+ 27 - 3
src/core/configmgr.cpp

@@ -11,6 +11,7 @@
 #include <QPixmap>
 #include <QSplashScreen>
 #include <QScopedPointer>
+#include <QTemporaryDir>
 
 #include <utils/pathutils.h>
 #include <utils/fileutils.h>
@@ -56,14 +57,26 @@ void ConfigMgr::Settings::writeToFile(const QString &p_jsonFilePath) const
     FileUtils::writeFile(p_jsonFilePath, QJsonDocument(this->m_jobj).toJson());
 }
 
-ConfigMgr::ConfigMgr(QObject *p_parent)
+ConfigMgr::ConfigMgr(bool p_isUnitTest, QObject *p_parent)
     : QObject(p_parent),
       m_config(new MainConfig(this)),
       m_sessionConfig(new SessionConfig(this))
 {
-    locateConfigFolder();
+    if (p_isUnitTest) {
+        m_dirForUnitTest.reset(new QTemporaryDir());
+        if (!m_dirForUnitTest->isValid()) {
+            qWarning() << "failed to init ConfigMgr for UnitTest";
+            return;
+        }
+        m_appConfigFolderPath = m_dirForUnitTest->filePath("vnotex_files");
+        m_userConfigFolderPath = m_dirForUnitTest->filePath("user_files");
 
-    checkAppConfig();
+        FileUtils::copyFile(getConfigFilePath(Source::Default), PathUtils::concatenateFilePath(m_appConfigFolderPath, c_configFileName));
+    } else {
+        locateConfigFolder();
+
+        checkAppConfig();
+    }
 
     m_config->init();
     m_sessionConfig->init();
@@ -73,6 +86,17 @@ ConfigMgr::~ConfigMgr()
 {
 }
 
+ConfigMgr &ConfigMgr::getInst(bool p_isUnitTest)
+{
+    static ConfigMgr inst(p_isUnitTest);
+    return inst;
+}
+
+void ConfigMgr::initForUnitTest()
+{
+    getInst(true);
+}
+
 void ConfigMgr::locateConfigFolder()
 {
     const auto appDirPath = getApplicationDirPath();

+ 10 - 7
src/core/configmgr.h

@@ -8,6 +8,8 @@
 
 #include "noncopyable.h"
 
+class QTemporaryDir;
+
 namespace vnotex
 {
     class MainConfig;
@@ -48,14 +50,10 @@ namespace vnotex
             QJsonObject m_jobj;
         };
 
-        static ConfigMgr &getInst()
-        {
-            static ConfigMgr inst;
-            return inst;
-        }
-
         ~ConfigMgr();
 
+        static ConfigMgr &getInst(bool p_isUnitTest = false);
+
         MainConfig &getConfig();
 
         SessionConfig &getSessionConfig();
@@ -112,6 +110,8 @@ namespace vnotex
 
         static QString getApplicationVersion();
 
+        static void initForUnitTest();
+
         static const QString c_orgName;
 
         static const QString c_appName;
@@ -128,7 +128,7 @@ namespace vnotex
         void editorConfigChanged();
 
     private:
-        explicit ConfigMgr(QObject *p_parent = nullptr);
+        ConfigMgr(bool p_isUnitTest, QObject *p_parent = nullptr);
 
         // Locate the folder path where the config file exists.
         void locateConfigFolder();
@@ -150,6 +150,9 @@ namespace vnotex
         // Absolute path of the user config folder.
         QString m_userConfigFolderPath;
 
+        // In UnitTest, we use a temp dir to hold the user files and app files.
+        QScopedPointer<QTemporaryDir> m_dirForUnitTest;
+
         // Name of the core config file.
         static const QString c_configFileName;
 

+ 85 - 11
src/core/notebook/bundlenotebook.cpp

@@ -1,40 +1,65 @@
 #include "bundlenotebook.h"
 
 #include <QDebug>
+#include <QCoreApplication>
 
 #include <notebookconfigmgr/bundlenotebookconfigmgr.h>
 #include <notebookconfigmgr/notebookconfig.h>
 #include <utils/fileutils.h>
 #include <core/historymgr.h>
+#include <core/exception.h>
 #include <notebookbackend/inotebookbackend.h>
 
+#include "notebookdatabaseaccess.h"
+
 using namespace vnotex;
 
 BundleNotebook::BundleNotebook(const NotebookParameters &p_paras,
                                const QSharedPointer<NotebookConfig> &p_notebookConfig,
                                QObject *p_parent)
-    : Notebook(p_paras, p_parent)
+    : Notebook(p_paras, p_parent),
+      m_configVersion(p_notebookConfig->m_version),
+      m_history(p_notebookConfig->m_history),
+      m_extraConfigs(p_notebookConfig->m_extraConfigs)
+{
+    setupDatabase();
+}
+
+BundleNotebook::~BundleNotebook()
 {
-    m_nextNodeId = p_notebookConfig->m_nextNodeId;
-    m_history = p_notebookConfig->m_history;
-    m_extraConfigs = p_notebookConfig->m_extraConfigs;
+    m_dbAccess->close();
 }
 
 BundleNotebookConfigMgr *BundleNotebook::getBundleNotebookConfigMgr() const
 {
-    return dynamic_cast<BundleNotebookConfigMgr *>(getConfigMgr().data());
+    return static_cast<BundleNotebookConfigMgr *>(getConfigMgr().data());
 }
 
-ID BundleNotebook::getNextNodeId() const
+void BundleNotebook::setupDatabase()
 {
-    return m_nextNodeId;
+    auto dbPath = getBackend()->getFullPath(BundleNotebookConfigMgr::getDatabasePath());
+    m_dbAccess = new NotebookDatabaseAccess(this, dbPath, this);
 }
 
-ID BundleNotebook::getAndUpdateNextNodeId()
+void BundleNotebook::initializeInternal()
 {
-    auto id = m_nextNodeId++;
-    getBundleNotebookConfigMgr()->writeNotebookConfig();
-    return id;
+    initDatabase();
+
+    if (m_configVersion != getConfigMgr()->getCodeVersion()) {
+        updateNotebookConfig();
+    }
+}
+
+void BundleNotebook::initDatabase()
+{
+    m_dbAccess->initialize(m_configVersion);
+
+    if (m_dbAccess->isFresh()) {
+        // For previous version notebook without DB, just ignore the node Id from config.
+        int cnt = 0;
+        fillNodeTableFromConfig(getRootNode().data(), m_configVersion < 2, cnt);
+        qDebug() << "fillNodeTableFromConfig nodes count" << cnt;
+    }
 }
 
 void BundleNotebook::updateNotebookConfig()
@@ -94,3 +119,52 @@ void BundleNotebook::setExtraConfig(const QString &p_key, const QJsonObject &p_o
 
     updateNotebookConfig();
 }
+
+void BundleNotebook::fillNodeTableFromConfig(Node *p_node, bool p_ignoreId, int &p_totalCnt)
+{
+    bool ret = m_dbAccess->addNode(p_node, p_ignoreId);
+    if (!ret) {
+        qWarning() << "failed to add node to DB" << p_node->getName() << p_ignoreId;
+        return;
+    }
+
+    if (++p_totalCnt % 10) {
+        QCoreApplication::processEvents();
+    }
+
+    const auto &children = p_node->getChildrenRef();
+    for (const auto &child : children) {
+        fillNodeTableFromConfig(child.data(), p_ignoreId, p_totalCnt);
+    }
+}
+
+NotebookDatabaseAccess *BundleNotebook::getDatabaseAccess() const
+{
+    return m_dbAccess;
+}
+
+bool BundleNotebook::rebuildDatabase()
+{
+    Q_ASSERT(m_dbAccess);
+    m_dbAccess->close();
+
+    auto backend = getBackend();
+    const auto dbPath = BundleNotebookConfigMgr::getDatabasePath();
+    if (backend->exists(dbPath)) {
+        try {
+            backend->removeFile(dbPath);
+        } catch (Exception &p_e) {
+            qWarning() << "failed to delete database file" << dbPath << p_e.what();
+            if (!m_dbAccess->open()) {
+                qWarning() << "failed to open notebook database (restart is needed)";
+            }
+            return false;
+        }
+    }
+
+    m_dbAccess->deleteLater();
+
+    setupDatabase();
+    initDatabase();
+    return true;
+}

+ 19 - 4
src/core/notebook/bundlenotebook.h

@@ -8,6 +8,7 @@ namespace vnotex
 {
     class BundleNotebookConfigMgr;
     class NotebookConfig;
+    class NotebookDatabaseAccess;
 
     class BundleNotebook : public Notebook
     {
@@ -17,9 +18,7 @@ namespace vnotex
                        const QSharedPointer<NotebookConfig> &p_notebookConfig,
                        QObject *p_parent = nullptr);
 
-        ID getNextNodeId() const Q_DECL_OVERRIDE;
-
-        ID getAndUpdateNextNodeId() Q_DECL_OVERRIDE;
+        ~BundleNotebook();
 
         void updateNotebookConfig() Q_DECL_OVERRIDE;
 
@@ -34,14 +33,30 @@ namespace vnotex
         const QJsonObject &getExtraConfigs() const Q_DECL_OVERRIDE;
         void setExtraConfig(const QString &p_key, const QJsonObject &p_obj) Q_DECL_OVERRIDE;
 
+        bool rebuildDatabase() Q_DECL_OVERRIDE;
+
+        NotebookDatabaseAccess *getDatabaseAccess() const;
+
+    protected:
+        void initializeInternal() Q_DECL_OVERRIDE;
+
     private:
         BundleNotebookConfigMgr *getBundleNotebookConfigMgr() const;
 
-        ID m_nextNodeId = 1;
+        void setupDatabase();
+
+        void fillNodeTableFromConfig(Node *p_node, bool p_ignoreId, int &p_totalCnt);
+
+        void initDatabase();
+
+        const int m_configVersion;
 
         QVector<HistoryItem> m_history;
 
         QJsonObject m_extraConfigs;
+
+        // Managed by QObject.
+        NotebookDatabaseAccess *m_dbAccess = nullptr;
     };
 } // ns vnotex
 

+ 49 - 18
src/core/notebook/node.cpp

@@ -7,30 +7,30 @@
 #include <utils/pathutils.h>
 #include <core/exception.h>
 #include "notebook.h"
+#include "nodeparameters.h"
 
 using namespace vnotex;
 
 Node::Node(Flags p_flags,
-           ID p_id,
            const QString &p_name,
-           const QDateTime &p_createdTimeUtc,
-           const QDateTime &p_modifiedTimeUtc,
-           const QStringList &p_tags,
-           const QString &p_attachmentFolder,
+           const NodeParameters &p_paras,
            Notebook *p_notebook,
            Node *p_parent)
     : m_notebook(p_notebook),
       m_loaded(true),
       m_flags(p_flags),
-      m_id(p_id),
+      m_id(p_paras.m_id),
+      m_signature(p_paras.m_signature),
       m_name(p_name),
-      m_createdTimeUtc(p_createdTimeUtc),
-      m_modifiedTimeUtc(p_modifiedTimeUtc),
-      m_tags(p_tags),
-      m_attachmentFolder(p_attachmentFolder),
+      m_createdTimeUtc(p_paras.m_createdTimeUtc),
+      m_modifiedTimeUtc(p_paras.m_modifiedTimeUtc),
+      m_tags(p_paras.m_tags),
+      m_attachmentFolder(p_paras.m_attachmentFolder),
       m_parent(p_parent)
 {
     Q_ASSERT(m_notebook);
+
+    checkSignature();
 }
 
 Node::Node(Flags p_flags,
@@ -54,19 +54,22 @@ bool Node::isLoaded() const
     return m_loaded;
 }
 
-void Node::loadCompleteInfo(ID p_id,
-                            const QDateTime &p_createdTimeUtc,
-                            const QDateTime &p_modifiedTimeUtc,
-                            const QStringList &p_tags,
+void Node::loadCompleteInfo(const NodeParameters &p_paras,
                             const QVector<QSharedPointer<Node>> &p_children)
 {
     Q_ASSERT(!m_loaded);
-    m_id = p_id;
-    m_createdTimeUtc = p_createdTimeUtc;
-    m_modifiedTimeUtc = p_modifiedTimeUtc;
-    m_tags = p_tags;
+
+    m_id = p_paras.m_id;
+    m_signature = p_paras.m_signature;
+    m_createdTimeUtc = p_paras.m_createdTimeUtc;
+    m_modifiedTimeUtc = p_paras.m_modifiedTimeUtc;
+    Q_ASSERT(p_paras.m_tags.isEmpty());
+    Q_ASSERT(p_paras.m_attachmentFolder.isEmpty());
+
     m_children = p_children;
     m_loaded = true;
+
+    checkSignature();
 }
 
 bool Node::isRoot() const
@@ -149,6 +152,22 @@ ID Node::getId() const
     return m_id;
 }
 
+void Node::updateId(ID p_id)
+{
+    if (m_id == p_id) {
+        return;
+    }
+
+    m_id = p_id;
+    save();
+    emit m_notebook->nodeUpdated(this);
+}
+
+ID Node::getSignature() const
+{
+    return m_signature;
+}
+
 const QDateTime &Node::getCreatedTimeUtc() const
 {
     return m_createdTimeUtc;
@@ -432,3 +451,15 @@ QList<QSharedPointer<File>> Node::collectFiles()
 
     return files;
 }
+
+ID Node::generateSignature()
+{
+    return static_cast<ID>(QDateTime::currentDateTime().toSecsSinceEpoch() + (static_cast<qulonglong>(qrand()) << 32));
+}
+
+void Node::checkSignature()
+{
+    if (m_signature == InvalidId) {
+        m_signature = generateSignature();
+    }
+}

+ 17 - 21
src/core/notebook/node.h

@@ -16,15 +16,7 @@ namespace vnotex
     class INotebookBackend;
     class File;
     class ExternalNode;
-
-    // Used when add/new a node.
-    struct NodeParameters
-    {
-        QDateTime m_createdTimeUtc = QDateTime::currentDateTimeUtc();
-        QDateTime m_modifiedTimeUtc = QDateTime::currentDateTimeUtc();
-        QString m_attachmentFolder;
-        QStringList m_tags;
-    };
+    class NodeParameters;
 
     // Node of notebook.
     class Node : public QEnableSharedFromThis<Node>
@@ -52,12 +44,8 @@ namespace vnotex
 
         // Constructor with all information loaded.
         Node(Flags p_flags,
-             ID p_id,
              const QString &p_name,
-             const QDateTime &p_createdTimeUtc,
-             const QDateTime &p_modifiedTimeUtc,
-             const QStringList &p_tags,
-             const QString &p_attachmentFolder,
+             const NodeParameters &p_paras,
              Notebook *p_notebook,
              Node *p_parent);
 
@@ -102,6 +90,9 @@ namespace vnotex
         void setUse(Node::Use p_use);
 
         ID getId() const;
+        void updateId(ID p_id);
+
+        ID getSignature() const;
 
         const QDateTime &getCreatedTimeUtc() const;
 
@@ -137,8 +128,8 @@ namespace vnotex
 
         Notebook *getNotebook() const;
 
-        void load();
-        void save();
+        virtual void load();
+        virtual void save();
 
         const QStringList &getTags() const;
 
@@ -165,10 +156,7 @@ namespace vnotex
         // Get File if this node has content.
         virtual QSharedPointer<File> getContentFile() = 0;
 
-        void loadCompleteInfo(ID p_id,
-                              const QDateTime &p_createdTimeUtc,
-                              const QDateTime &p_modifiedTimeUtc,
-                              const QStringList &p_tags,
+        void loadCompleteInfo(const NodeParameters &p_paras,
                               const QVector<QSharedPointer<Node>> &p_children);
 
         INotebookConfigMgr *getConfigMgr() const;
@@ -184,18 +172,26 @@ namespace vnotex
 
         static bool isAncestor(const Node *p_ancestor, const Node *p_child);
 
+        static ID generateSignature();
+
     protected:
         Notebook *m_notebook = nullptr;
 
-    private:
         bool m_loaded = false;
 
+    private:
+        void checkSignature();
+
         Flags m_flags = Flag::None;
 
         Use m_use = Use::Normal;
 
         ID m_id = InvalidId;
 
+        // A long random number created when the node is created.
+        // Use to avoid conflicts of m_id.
+        ID m_signature = InvalidId;
+
         QString m_name;
 
         QDateTime m_createdTimeUtc;

+ 8 - 0
src/core/notebook/nodeparameters.cpp

@@ -0,0 +1,8 @@
+#include "nodeparameters.h"
+
+using namespace vnotex;
+
+NodeParameters::NodeParameters(ID p_id)
+    : m_id(p_id)
+{
+}

+ 34 - 0
src/core/notebook/nodeparameters.h

@@ -0,0 +1,34 @@
+#ifndef NODEPARAMETERS_H
+#define NODEPARAMETERS_H
+
+#include <QDateTime>
+#include <QStringList>
+
+#include <core/global.h>
+
+#include "node.h"
+
+namespace vnotex
+{
+    class NodeParameters
+    {
+    public:
+        NodeParameters() = default;
+
+        NodeParameters(ID p_id);
+
+        ID m_id = Node::InvalidId;
+
+        ID m_signature = Node::InvalidId;
+
+        QDateTime m_createdTimeUtc = QDateTime::currentDateTimeUtc();
+
+        QDateTime m_modifiedTimeUtc = QDateTime::currentDateTimeUtc();
+
+        QStringList m_tags;
+
+        QString m_attachmentFolder;
+    };
+}
+
+#endif // NODEPARAMETERS_H

+ 25 - 2
src/core/notebook/notebook.cpp

@@ -7,7 +7,8 @@
 #include <notebookconfigmgr/inotebookconfigmgr.h>
 #include <utils/pathutils.h>
 #include <utils/fileutils.h>
-#include "exception.h"
+#include <core/exception.h>
+#include "nodeparameters.h"
 
 using namespace vnotex;
 
@@ -43,13 +44,30 @@ Notebook::Notebook(const NotebookParameters &p_paras,
     if (m_attachmentFolder.isEmpty()) {
         m_attachmentFolder = c_defaultAttachmentFolder;
     }
+
     m_configMgr->setNotebook(this);
 }
 
+Notebook::Notebook(const QString &p_name, QObject *p_parent)
+    : QObject(p_parent),
+      m_name(p_name)
+{
+}
+
 Notebook::~Notebook()
 {
 }
 
+void Notebook::initialize()
+{
+    if (m_initialized) {
+        return;
+    }
+
+    m_initialized = true;
+    initializeInternal();
+}
+
 vnotex::ID Notebook::getId() const
 {
     return m_id;
@@ -292,7 +310,7 @@ QSharedPointer<Node> Notebook::getOrCreateRecycleBinDateNode()
 
 void Notebook::emptyNode(const Node *p_node, bool p_force)
 {
-    // Copy the children.
+    // Empty the children.
     auto children = p_node->getChildren();
     for (const auto &child : children) {
         removeNode(child, p_force);
@@ -383,3 +401,8 @@ QStringList Notebook::scanAndImportExternalFiles()
 {
     return m_configMgr->scanAndImportExternalFiles(getRootNode().data());
 }
+
+bool Notebook::rebuildDatabase()
+{
+    return false;
+}

+ 13 - 5
src/core/notebook/notebook.h

@@ -15,7 +15,7 @@ namespace vnotex
     class INotebookBackend;
     class IVersionController;
     class INotebookConfigMgr;
-    struct NodeParameters;
+    class NodeParameters;
     class File;
 
     // Base class of notebook.
@@ -26,8 +26,13 @@ namespace vnotex
         Notebook(const NotebookParameters &p_paras,
                  QObject *p_parent = nullptr);
 
+        // Used for UT only.
+        Notebook(const QString &p_name, QObject *p_parent = nullptr);
+
         virtual ~Notebook();
 
+        void initialize();
+
         enum { InvalidId = 0 };
 
         ID getId() const;
@@ -83,10 +88,6 @@ namespace vnotex
                                         Node::Flags p_flags,
                                         const QString &p_path);
 
-        virtual ID getNextNodeId() const = 0;
-
-        virtual ID getAndUpdateNextNodeId() = 0;
-
         virtual void updateNotebookConfig() = 0;
 
         virtual void removeNotebookConfig() = 0;
@@ -146,6 +147,8 @@ namespace vnotex
 
         QStringList scanAndImportExternalFiles();
 
+        virtual bool rebuildDatabase();
+
         static const QString c_defaultAttachmentFolder;
 
         static const QString c_defaultImageFolder;
@@ -155,9 +158,14 @@ namespace vnotex
 
         void nodeUpdated(const Node *p_node);
 
+    protected:
+        virtual void initializeInternal() = 0;
+
     private:
         QSharedPointer<Node> getOrCreateRecycleBinDateNode();
 
+        bool m_initialized = false;
+
         // ID of this notebook.
         // Will be assigned uniquely once loaded.
         ID m_id;

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

@@ -1,7 +1,9 @@
 SOURCES += \
     $$PWD/externalnode.cpp \
+    $$PWD/nodeparameters.cpp \
     $$PWD/notebook.cpp \
     $$PWD/bundlenotebookfactory.cpp \
+    $$PWD/notebookdatabaseaccess.cpp \
     $$PWD/notebookparameters.cpp \
     $$PWD/bundlenotebook.cpp \
     $$PWD/node.cpp \
@@ -10,9 +12,11 @@ SOURCES += \
 
 HEADERS += \
     $$PWD/externalnode.h \
+    $$PWD/nodeparameters.h \
     $$PWD/notebook.h \
     $$PWD/inotebookfactory.h \
     $$PWD/bundlenotebookfactory.h \
+    $$PWD/notebookdatabaseaccess.h \
     $$PWD/notebookparameters.h \
     $$PWD/bundlenotebook.h \
     $$PWD/node.h \

+ 381 - 0
src/core/notebook/notebookdatabaseaccess.cpp

@@ -0,0 +1,381 @@
+#include "notebookdatabaseaccess.h"
+
+#include <QtSql>
+#include <QDebug>
+
+#include <core/exception.h>
+
+#include "notebook.h"
+#include "node.h"
+
+using namespace vnotex;
+
+static QString c_nodeTableName = "node";
+
+NotebookDatabaseAccess::NotebookDatabaseAccess(Notebook *p_notebook, const QString &p_databaseFile, QObject *p_parent)
+    : QObject(p_parent),
+      m_notebook(p_notebook),
+      m_databaseFile(p_databaseFile),
+      m_connectionName(p_databaseFile)
+{
+}
+
+bool NotebookDatabaseAccess::open()
+{
+    auto db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), m_connectionName);
+    db.setDatabaseName(m_databaseFile);
+    if (!db.open()) {
+        qWarning() << QString("failed to open notebook database (%1) (%2)").arg(m_databaseFile, db.lastError().text());
+        return false;
+    }
+
+    {
+        // Enable foreign key support.
+        QSqlQuery query(db);
+        if (!query.exec("PRAGMA foreign_keys = ON")) {
+            qWarning() << "failed to turn on foreign key support" << query.lastError().text();
+            return false;
+        }
+    }
+
+    m_valid = true;
+    m_fresh = db.tables().isEmpty();
+    return true;
+}
+
+bool NotebookDatabaseAccess::isFresh() const
+{
+    return m_fresh;
+}
+
+bool NotebookDatabaseAccess::isValid() const
+{
+    return m_valid;
+}
+
+// Maybe insert new table according to @p_configVersion.
+void NotebookDatabaseAccess::setupTables(QSqlDatabase &p_db, int p_configVersion)
+{
+    Q_UNUSED(p_configVersion);
+
+    if (!m_valid) {
+        return;
+    }
+
+    QSqlQuery query(p_db);
+
+    // Node.
+    if (m_fresh) {
+        bool ret = query.exec(QString("CREATE TABLE %1 (\n"
+                                      "    id INTEGER PRIMARY KEY,\n"
+                                      "    name text NOT NULL,\n"
+                                      "    signature INTEGER NOT NULL,\n"
+                                      "    parent_id INTEGER NULL REFERENCES %1(id) ON DELETE CASCADE)\n").arg(c_nodeTableName));
+        if (!ret) {
+            qWarning() << QString("failed to create database table (%1) (%2)").arg(c_nodeTableName, query.lastError().text());
+            m_valid = false;
+            return;
+        }
+    }
+}
+
+void NotebookDatabaseAccess::initialize(int p_configVersion)
+{
+    open();
+
+    auto db = getDatabase();
+    setupTables(db, p_configVersion);
+}
+
+void NotebookDatabaseAccess::close()
+{
+    getDatabase().close();
+    QSqlDatabase::removeDatabase(m_connectionName);
+    m_valid = false;
+}
+
+bool NotebookDatabaseAccess::addNode(Node *p_node, bool p_ignoreId)
+{
+    p_node->load();
+
+    Q_ASSERT(p_node->getSignature() != Node::InvalidId);
+
+    auto db = getDatabase();
+    QSqlQuery query(db);
+    if (p_ignoreId) {
+        query.prepare(QString("INSERT INTO %1 (name, signature, parent_id)\n"
+                              "    VALUES (:name, :signature, :parent_id)").arg(c_nodeTableName));
+        query.bindValue(":name", p_node->getName());
+        query.bindValue(":signature", p_node->getSignature());
+        query.bindValue(":parent_id", p_node->getParent() ? p_node->getParent()->getId() : QVariant());
+    } else {
+        bool useNewId = false;
+        if (p_node->getId() != InvalidId) {
+            auto nodeRec = queryNode(p_node->getId());
+            if (nodeRec) {
+                auto nodePath = queryNodePath(p_node->getId());
+                if (existsNode(p_node, nodeRec.data(), nodePath)) {
+                    return true;
+                }
+
+                if (nodePath.isEmpty()) {
+                    useNewId = true;
+                    m_obsoleteNodes.insert(nodeRec->m_id);
+                } else {
+                    auto relativePath = nodePath.join(QLatin1Char('/'));
+                    auto oldNode = m_notebook->loadNodeByPath(relativePath);
+                    Q_ASSERT(oldNode != p_node);
+                    if (oldNode) {
+                        // The node with the same id still exists.
+                        useNewId = true;
+                    } else if (nodeRec->m_signature == p_node->getSignature() && nodeRec->m_name == p_node->getName()) {
+                        // @p_node should be the same node as @nodeRec.
+                        return updateNode(p_node);
+                    } else {
+                        // @nodeRec is now an obsolete node.
+                        useNewId = true;
+                        m_obsoleteNodes.insert(nodeRec->m_id);
+                    }
+                }
+            }
+        } else {
+            useNewId = true;
+        }
+
+        if (useNewId) {
+            query.prepare(QString("INSERT INTO %1 (name, signature, parent_id)\n"
+                                  "    VALUES (:name, :signature, :parent_id)").arg(c_nodeTableName));
+        } else {
+            query.prepare(QString("INSERT INTO %1 (id, name, signature, parent_id)\n"
+                                  "    VALUES (:id, :name, :signature, :parent_id)").arg(c_nodeTableName));
+            query.bindValue(":id", p_node->getId());
+        }
+        query.bindValue(":name", p_node->getName());
+        query.bindValue(":signature", p_node->getSignature());
+        query.bindValue(":parent_id", p_node->getParent() ? p_node->getParent()->getId() : QVariant());
+    }
+
+    if (!query.exec()) {
+        qWarning() << "failed to add node by query" << query.executedQuery() << query.lastError().text();
+        return false;
+    }
+
+    const ID id = query.lastInsertId().toULongLong();
+    const ID preId = p_node->getId();
+    p_node->updateId(id);
+
+    qDebug("added node id %llu preId %llu ignoreId %d sig %llu name %s parentId %llu",
+           id,
+           preId,
+           p_ignoreId,
+           p_node->getSignature(),
+           p_node->getName().toStdString(),
+           p_node->getParent() ? p_node->getParent()->getId() : Node::InvalidId);
+    return true;
+}
+
+QSharedPointer<NotebookDatabaseAccess::NodeRecord> NotebookDatabaseAccess::queryNode(ID p_id)
+{
+    auto db = getDatabase();
+    QSqlQuery query(db);
+    query.prepare(QString("SELECT id, name, signature, parent_id from %1 where id = :id").arg(c_nodeTableName));
+    query.bindValue(":id", p_id);
+    if (!query.exec()) {
+        qWarning() << "failed to query node" << query.executedQuery() << query.lastError().text();
+        return nullptr;
+    }
+
+    if (query.next()) {
+        auto nodeRec = QSharedPointer<NodeRecord>::create();
+        nodeRec->m_id = query.value(0).toULongLong();
+        nodeRec->m_name = query.value(1).toString();
+        nodeRec->m_signature = query.value(2).toULongLong();
+        nodeRec->m_parentId = query.value(3).toULongLong();
+        return nodeRec;
+    }
+
+    return nullptr;
+}
+
+QSqlDatabase NotebookDatabaseAccess::getDatabase() const
+{
+    return QSqlDatabase::database(m_connectionName);
+}
+
+bool NotebookDatabaseAccess::existsNode(const Node *p_node)
+{
+    if (!p_node) {
+        return false;
+    }
+
+    return existsNode(p_node,
+                      queryNode(p_node->getId()).data(),
+                      queryNodePath(p_node->getId()));
+}
+
+bool NotebookDatabaseAccess::existsNode(const Node *p_node, const NodeRecord *p_rec, const QStringList &p_nodePath)
+{
+    if (p_nodePath.isEmpty()) {
+        return false;
+    }
+
+    if (!nodeEqual(p_rec, p_node)) {
+        return false;
+    }
+
+    return checkNodePath(p_node, p_nodePath);
+}
+
+QStringList NotebookDatabaseAccess::queryNodePath(ID p_id)
+{
+    auto db = getDatabase();
+    QSqlQuery query(db);
+    query.prepare(QString("WITH RECURSIVE cte_parents(id, name, parent_id) AS (\n"
+                          "    SELECT node.id, node.name, node.parent_id\n"
+                          "    FROM %1 node\n"
+                          "    WHERE node.id = :id\n"
+                          "    UNION ALL\n"
+                          "    SELECT node.id, node.name, node.parent_id\n"
+                          "    FROM %1 node\n"
+                          "    JOIN cte_parents cte ON node.id = cte.parent_id\n"
+                          "    LIMIT 5000)\n"
+                          "SELECT * FROM cte_parents").arg(c_nodeTableName));
+    query.bindValue(":id", p_id);
+    if (!query.exec()) {
+        qWarning() << "failed to query node's path" << query.executedQuery() << query.lastError().text();
+        return QStringList();
+    }
+
+    QStringList ret;
+    ID lastParentId = p_id;
+    bool hasResult = false;
+    while (query.next()) {
+        hasResult = true;
+        Q_ASSERT(lastParentId == query.value(0).toULongLong());
+        ret.prepend(query.value(1).toString());
+        lastParentId = query.value(2).toULongLong();
+    }
+    Q_ASSERT(!hasResult || lastParentId == InvalidId);
+    return ret;
+}
+
+bool NotebookDatabaseAccess::updateNode(const Node *p_node)
+{
+    Q_ASSERT(p_node->getParent());
+
+    auto db = getDatabase();
+    QSqlQuery query(db);
+    query.prepare(QString("UPDATE %1\n"
+                          "SET name = :name,\n"
+                          "    signature = :signature,\n"
+                          "    parent_id = :parent_id\n"
+                          "WHERE id = :id").arg(c_nodeTableName));
+    query.bindValue(":name", p_node->getName());
+    query.bindValue(":signature", p_node->getSignature());
+    query.bindValue(":parent_id", p_node->getParent()->getId());
+    query.bindValue(":id", p_node->getId());
+    if (!query.exec()) {
+        qWarning() << "failed to update node" << query.executedQuery() << query.lastError().text();
+        return false;
+    }
+
+    qDebug() << "updated node"
+             << p_node->getId()
+             << p_node->getSignature()
+             << p_node->getName()
+             << p_node->getParent()->getId();
+
+    return true;
+}
+
+void NotebookDatabaseAccess::clearObsoleteNodes()
+{
+    if (m_obsoleteNodes.isEmpty()) {
+        return;
+    }
+
+    for (auto it : m_obsoleteNodes) {
+        if (!removeNode(it)) {
+            qWarning() << "failed to clear obsolete node" << it;
+            continue;
+        }
+    }
+
+    m_obsoleteNodes.clear();
+}
+
+bool NotebookDatabaseAccess::removeNode(const Node *p_node)
+{
+    if (existsNode(p_node)) {
+        return removeNode(p_node->getId());
+    }
+
+    return true;
+}
+
+bool NotebookDatabaseAccess::removeNode(ID p_id)
+{
+    auto db = getDatabase();
+    QSqlQuery query(db);
+    query.prepare(QString("DELETE FROM %1\n"
+                          "WHERE id = :id").arg(c_nodeTableName));
+    query.bindValue(":id", p_id);
+    if (!query.exec()) {
+        qWarning() << "failed to remove node" << query.executedQuery() << query.lastError().text();
+        return false;
+    }
+    qDebug() << "removed node" << p_id;
+    return true;
+}
+
+bool NotebookDatabaseAccess::nodeEqual(const NodeRecord *p_rec, const Node *p_node) const
+{
+    if (!p_rec) {
+        if (p_node) {
+            return false;
+        } else {
+            return true;
+        }
+    } else if (!p_node) {
+        return false;
+    }
+
+    if (p_rec->m_id != p_node->getId()) {
+        return false;
+    }
+    if (p_rec->m_name != p_node->getName()) {
+        return false;
+    }
+    if (p_rec->m_signature != p_node->getSignature()) {
+        return false;
+    }
+    if (p_node->getParent()) {
+        if (p_rec->m_parentId != p_node->getParent()->getId()) {
+            return false;
+        }
+    } else if (p_rec->m_parentId != Node::InvalidId) {
+        return false;
+    }
+
+    return true;
+}
+
+bool NotebookDatabaseAccess::checkNodePath(const Node *p_node, const QStringList &p_nodePath) const
+{
+    for (int i = p_nodePath.size() - 1; i >= 0; --i) {
+        if (!p_node) {
+            return false;
+        }
+
+        if (p_nodePath[i] != p_node->getName()) {
+            return false;
+        }
+        p_node = p_node->getParent();
+    }
+
+    if (p_node) {
+        return false;
+    }
+
+    return true;
+}

+ 97 - 0
src/core/notebook/notebookdatabaseaccess.h

@@ -0,0 +1,97 @@
+#ifndef NOTEBOOKDATABASEACCESS_H
+#define NOTEBOOKDATABASEACCESS_H
+
+#include <QObject>
+#include <QSharedPointer>
+#include <QtSql/QSqlDatabase>
+#include <QSet>
+
+#include <core/global.h>
+
+namespace tests
+{
+    class TestNotebookDatabase;
+}
+
+namespace vnotex
+{
+    class Node;
+    class Notebook;
+
+    class NotebookDatabaseAccess : public QObject
+    {
+        Q_OBJECT
+    public:
+        enum { InvalidId = 0 };
+
+        friend class tests::TestNotebookDatabase;
+
+        NotebookDatabaseAccess(Notebook *p_notebook, const QString &p_databaseFile, QObject *p_parent = nullptr);
+
+        bool isFresh() const;
+
+        bool isValid() const;
+
+        void initialize(int p_configVersion);
+
+        bool open();
+
+        void close();
+
+        bool addNode(Node *p_node, bool p_ignoreId);
+
+        // Whether there is a record with the same ID in DB and has the same path.
+        bool existsNode(const Node *p_node);
+
+        void clearObsoleteNodes();
+
+        bool updateNode(const Node *p_node);
+
+        bool removeNode(const Node *p_node);
+
+    private:
+        struct NodeRecord
+        {
+            ID m_id = InvalidId;
+
+            QString m_name;
+
+            ID m_signature = InvalidId;
+
+            ID m_parentId = InvalidId;
+        };
+
+        void setupTables(QSqlDatabase &p_db, int p_configVersion);
+
+        QSqlDatabase getDatabase() const;
+
+        // Return null if not exists.
+        QSharedPointer<NodeRecord> queryNode(ID p_id);
+
+        QStringList queryNodePath(ID p_id);
+
+        bool nodeEqual(const NodeRecord *p_rec, const Node *p_node) const;
+
+        bool existsNode(const Node *p_node, const NodeRecord *p_rec, const QStringList &p_nodePath);
+
+        bool checkNodePath(const Node *p_node, const QStringList &p_nodePath) const;
+
+        bool removeNode(ID p_id);
+
+        Notebook *m_notebook = nullptr;
+
+        QString m_databaseFile;
+
+        // From Qt's docs: It is highly recommended that you do not keep a copy of the QSqlDatabase around as a member of a class, as this will prevent the instance from being correctly cleaned up on shutdown.
+        QString m_connectionName;
+
+        // Whether it is a new data base whether any tables.
+        bool m_fresh = false;
+
+        bool m_valid = false;
+
+        QSet<ID> m_obsoleteNodes;
+    };
+}
+
+#endif // NOTEBOOKDATABASEACCESS_H

+ 3 - 11
src/core/notebook/vxnode.cpp

@@ -10,21 +10,13 @@
 
 using namespace vnotex;
 
-VXNode::VXNode(ID p_id,
-               const QString &p_name,
-               const QDateTime &p_createdTimeUtc,
-               const QDateTime &p_modifiedTimeUtc,
-               const QStringList &p_tags,
-               const QString &p_attachmentFolder,
+VXNode::VXNode(const QString &p_name,
+               const NodeParameters &p_paras,
                Notebook *p_notebook,
                Node *p_parent)
     : Node(Node::Flag::Content,
-           p_id,
            p_name,
-           p_createdTimeUtc,
-           p_modifiedTimeUtc,
-           p_tags,
-           p_attachmentFolder,
+           p_paras,
            p_notebook,
            p_parent)
 {

+ 2 - 8
src/core/notebook/vxnode.h

@@ -10,12 +10,8 @@ namespace vnotex
     {
     public:
         // For content node.
-        VXNode(ID p_id,
-               const QString &p_name,
-               const QDateTime &p_createdTimeUtc,
-               const QDateTime &p_modifiedTimeUtc,
-               const QStringList &p_tags,
-               const QString &p_attachmentFolder,
+        VXNode(const QString &p_name,
+               const NodeParameters &p_paras,
                Notebook *p_notebook,
                Node *p_parent);
 
@@ -37,8 +33,6 @@ namespace vnotex
         QString renameAttachment(const QString &p_path, const QString &p_name) Q_DECL_OVERRIDE;
 
         void removeAttachment(const QStringList &p_paths) Q_DECL_OVERRIDE;
-
-    private:
     };
 }
 

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

@@ -75,6 +75,11 @@ QString BundleNotebookConfigMgr::getConfigFilePath()
     return PathUtils::concatenateFilePath(c_configFolderName, c_configName);
 }
 
+QString BundleNotebookConfigMgr::getDatabasePath()
+{
+    return PathUtils::concatenateFilePath(c_configFolderName, "notebook.db");
+}
+
 BundleNotebook *BundleNotebookConfigMgr::getBundleNotebook() const
 {
     return dynamic_cast<BundleNotebook *>(getNotebook());
@@ -94,3 +99,8 @@ bool BundleNotebookConfigMgr::isBuiltInFolder(const Node *p_node, const QString
     }
     return false;
 }
+
+int BundleNotebookConfigMgr::getCodeVersion() const
+{
+    return 2;
+}

+ 4 - 2
src/core/notebookconfigmgr/bundlenotebookconfigmgr.h

@@ -26,15 +26,17 @@ namespace vnotex
 
         bool isBuiltInFolder(const Node *p_node, const QString &p_name) const Q_DECL_OVERRIDE;
 
+        int getCodeVersion() const Q_DECL_OVERRIDE;
+
         static const QString &getConfigFolderName();
 
         static const QString &getConfigName();
 
         static QString getConfigFilePath();
 
-        static QSharedPointer<NotebookConfig> readNotebookConfig(const QSharedPointer<INotebookBackend> &p_backend);
+        static QString getDatabasePath();
 
-        enum { RootNodeId = 1 };
+        static QSharedPointer<NotebookConfig> readNotebookConfig(const QSharedPointer<INotebookBackend> &p_backend);
 
     protected:
         BundleNotebook *getBundleNotebook() const;

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

@@ -20,12 +20,6 @@ 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;

+ 3 - 4
src/core/notebookconfigmgr/inotebookconfigmgr.h

@@ -12,7 +12,7 @@ namespace vnotex
     class INotebookBackend;
     class NotebookParameters;
     class Notebook;
-    struct NodeParameters;
+    class NodeParameters;
 
     // Abstract class for notebook config manager, which is responsible for config
     // files access and note nodes access.
@@ -38,7 +38,7 @@ namespace vnotex
 
         virtual QSharedPointer<Node> loadRootNode() = 0;
 
-        virtual void loadNode(Node *p_node) const = 0;
+        virtual void loadNode(Node *p_node) = 0;
         virtual void saveNode(const Node *p_node) = 0;
 
         virtual void renameNode(Node *p_node, const QString &p_name) = 0;
@@ -82,9 +82,8 @@ namespace vnotex
 
         virtual QStringList scanAndImportExternalFiles(Node *p_node) = 0;
 
-    protected:
         // Version of the config processing code.
-        virtual QString getCodeVersion() const;
+        virtual int getCodeVersion() const = 0;
 
     private:
         QSharedPointer<INotebookBackend> m_backend;

+ 3 - 16
src/core/notebookconfigmgr/notebookconfig.cpp

@@ -25,9 +25,7 @@ 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,
+QSharedPointer<NotebookConfig> NotebookConfig::fromNotebookParameters(int p_version,
                                                                       const NotebookParameters &p_paras)
 {
     auto config = QSharedPointer<NotebookConfig>::create();
@@ -56,7 +54,6 @@ QJsonObject NotebookConfig::toJson() const
     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);
 
     jobj[QStringLiteral("history")] = saveHistory();
 
@@ -77,7 +74,7 @@ void NotebookConfig::fromJson(const QJsonObject &p_jobj)
         return;
     }
 
-    m_version = p_jobj[NotebookConfig::c_version].toString();
+    m_version = p_jobj[NotebookConfig::c_version].toInt();
     m_name = p_jobj[NotebookConfig::c_name].toString();
     m_description = p_jobj[NotebookConfig::c_description].toString();
     m_imageFolder = p_jobj[NotebookConfig::c_imageFolder].toString();
@@ -86,21 +83,12 @@ void NotebookConfig::fromJson(const QJsonObject &p_jobj)
     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;
-        }
-    }
-
     loadHistory(p_jobj);
 
     m_extraConfigs = p_jobj[QStringLiteral("extra_configs")].toObject();
 }
 
-QSharedPointer<NotebookConfig> NotebookConfig::fromNotebook(const QString &p_version,
+QSharedPointer<NotebookConfig> NotebookConfig::fromNotebook(int p_version,
                                                             const Notebook *p_notebook)
 {
     auto config = QSharedPointer<NotebookConfig>::create();
@@ -113,7 +101,6 @@ QSharedPointer<NotebookConfig> NotebookConfig::fromNotebook(const QString &p_ver
     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();
     config->m_history = p_notebook->getHistory();
     config->m_extraConfigs = p_notebook->getExtraConfigs();
 

+ 3 - 7
src/core/notebookconfigmgr/notebookconfig.h

@@ -20,17 +20,17 @@ namespace vnotex
     public:
         virtual ~NotebookConfig() {}
 
-        static QSharedPointer<NotebookConfig> fromNotebookParameters(const QString &p_version,
+        static QSharedPointer<NotebookConfig> fromNotebookParameters(int p_version,
                                                                      const NotebookParameters &p_paras);
 
-        static QSharedPointer<NotebookConfig> fromNotebook(const QString &p_version,
+        static QSharedPointer<NotebookConfig> fromNotebook(int p_version,
                                                            const Notebook *p_notebook);
 
         virtual QJsonObject toJson() const;
 
         virtual void fromJson(const QJsonObject &p_jobj);
 
-        QString m_version;
+        int m_version = 0;
 
         QString m_name;
 
@@ -46,8 +46,6 @@ namespace vnotex
 
         QString m_notebookConfigMgr;
 
-        ID m_nextNodeId = BundleNotebookConfigMgr::RootNodeId + 1;
-
         QVector<HistoryItem> m_history;
 
         // Hold all the extra configs for other components or 3rd party plugins.
@@ -74,8 +72,6 @@ namespace vnotex
         static const QString c_versionController;
 
         static const QString c_configMgr;
-
-        static const QString c_nextNodeId;
     };
 } // ns vnotex
 

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

@@ -1,4 +1,5 @@
 SOURCES += \
+    $$PWD/vxnodeconfig.cpp \
     $$PWD/vxnotebookconfigmgr.cpp \
     $$PWD/vxnotebookconfigmgrfactory.cpp \
     $$PWD/inotebookconfigmgr.cpp \
@@ -7,6 +8,7 @@ SOURCES += \
 
 HEADERS += \
     $$PWD/inotebookconfigmgr.h \
+    $$PWD/vxnodeconfig.h \
     $$PWD/vxnotebookconfigmgr.h \
     $$PWD/inotebookconfigmgrfactory.h \
     $$PWD/vxnotebookconfigmgrfactory.h \

+ 173 - 0
src/core/notebookconfigmgr/vxnodeconfig.cpp

@@ -0,0 +1,173 @@
+#include "vxnodeconfig.h"
+
+#include <utils/utils.h>
+#include <QJsonArray>
+
+using namespace vnotex;
+
+using namespace vnotex::vx_node_config;
+
+const QString NodeConfig::c_version = "version";
+
+const QString NodeConfig::c_id = "id";
+
+const QString NodeConfig::c_signature = "signature";
+
+const QString NodeConfig::c_createdTimeUtc = "created_time";
+
+const QString NodeConfig::c_files = "files";
+
+const QString NodeConfig::c_folders = "folders";
+
+const QString NodeConfig::c_name = "name";
+
+const QString NodeConfig::c_modifiedTimeUtc = "modified_time";
+
+const QString NodeConfig::c_attachmentFolder = "attachment_folder";
+
+const QString NodeConfig::c_tags = "tags";
+
+static ID stringToNodeId(const QString &p_idStr)
+{
+    auto ret = stringToID(p_idStr);
+    if (!ret.first) {
+        return Node::InvalidId;
+    }
+    return ret.second;
+}
+
+QJsonObject NodeFileConfig::toJson() const
+{
+    QJsonObject jobj;
+
+    jobj[NodeConfig::c_name] = m_name;
+    jobj[NodeConfig::c_id] = IDToString(m_id);
+    jobj[NodeConfig::c_signature] = IDToString(m_signature);
+    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 NodeFileConfig::fromJson(const QJsonObject &p_jobj)
+{
+    m_name = p_jobj[NodeConfig::c_name].toString();
+
+    m_id = stringToNodeId(p_jobj[NodeConfig::c_id].toString());
+    m_signature = stringToNodeId(p_jobj[NodeConfig::c_signature].toString());
+
+    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 (int i = 0; i < arr.size(); ++i) {
+            m_tags << arr[i].toString();
+        }
+    }
+}
+
+NodeParameters NodeFileConfig::toNodeParameters() const
+{
+    NodeParameters paras;
+    paras.m_id = m_id;
+    paras.m_signature = m_signature;
+    paras.m_createdTimeUtc = m_createdTimeUtc;
+    paras.m_modifiedTimeUtc = m_modifiedTimeUtc;
+    paras.m_tags = m_tags;
+    paras.m_attachmentFolder = m_attachmentFolder;
+    return paras;
+}
+
+QJsonObject NodeFolderConfig::toJson() const
+{
+    QJsonObject jobj;
+
+    jobj[NodeConfig::c_name] = m_name;
+
+    return jobj;
+}
+
+void NodeFolderConfig::fromJson(const QJsonObject &p_jobj)
+{
+    m_name = p_jobj[NodeConfig::c_name].toString();
+}
+
+NodeConfig::NodeConfig()
+{
+}
+
+NodeConfig::NodeConfig(int p_version,
+                       ID p_id,
+                       ID p_signature,
+                       const QDateTime &p_createdTimeUtc,
+                       const QDateTime &p_modifiedTimeUtc)
+    : m_version(p_version),
+      m_id(p_id),
+      m_signature(p_signature),
+      m_createdTimeUtc(p_createdTimeUtc),
+      m_modifiedTimeUtc(p_modifiedTimeUtc)
+{
+}
+
+QJsonObject NodeConfig::toJson() const
+{
+    QJsonObject jobj;
+
+    jobj[NodeConfig::c_version] = m_version;
+    jobj[NodeConfig::c_id] = IDToString(m_id);
+    jobj[NodeConfig::c_signature] = IDToString(m_signature);
+    jobj[NodeConfig::c_createdTimeUtc] = Utils::dateTimeStringUniform(m_createdTimeUtc);
+    jobj[NodeConfig::c_modifiedTimeUtc] = Utils::dateTimeStringUniform(m_modifiedTimeUtc);
+
+    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 NodeConfig::fromJson(const QJsonObject &p_jobj)
+{
+    m_version = p_jobj[NodeConfig::c_version].toInt();
+
+    m_id = stringToNodeId(p_jobj[NodeConfig::c_id].toString());
+    m_signature = stringToNodeId(p_jobj[NodeConfig::c_signature].toString());
+
+    m_createdTimeUtc = Utils::dateTimeFromStringUniform(p_jobj[NodeConfig::c_createdTimeUtc].toString());
+    m_modifiedTimeUtc = Utils::dateTimeFromStringUniform(p_jobj[NodeConfig::c_modifiedTimeUtc].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());
+    }
+}
+
+NodeParameters NodeConfig::toNodeParameters() const
+{
+    NodeParameters paras;
+    paras.m_id = m_id;
+    paras.m_signature = m_signature;
+    paras.m_createdTimeUtc = m_createdTimeUtc;
+    paras.m_modifiedTimeUtc = m_modifiedTimeUtc;
+    return paras;
+}

+ 107 - 0
src/core/notebookconfigmgr/vxnodeconfig.h

@@ -0,0 +1,107 @@
+#ifndef VXNODECONFIG_H
+#define VXNODECONFIG_H
+
+#include <QJsonObject>
+#include <QDateTime>
+#include <QVector>
+
+#include <core/global.h>
+#include <notebook/node.h>
+#include <notebook/nodeparameters.h>
+
+namespace vnotex
+{
+    // Config structures for VXNotebookConfigMgr.
+    namespace vx_node_config
+    {
+        // Config of a file child.
+        struct NodeFileConfig
+        {
+            QJsonObject toJson() const;
+
+            void fromJson(const QJsonObject &p_jobj);
+
+            NodeParameters toNodeParameters() const;
+
+            QString m_name;
+
+            ID m_id = Node::InvalidId;
+
+            ID m_signature = 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(int p_version,
+                       ID p_id,
+                       ID p_signature,
+                       const QDateTime &p_createdTimeUtc,
+                       const QDateTime &p_modifiedTimeUtc);
+
+            QJsonObject toJson() const;
+
+            void fromJson(const QJsonObject &p_jobj);
+
+            NodeParameters toNodeParameters() const;
+
+            int m_version = 0;
+
+            ID m_id = Node::InvalidId;
+
+            ID m_signature = Node::InvalidId;
+
+            QDateTime m_createdTimeUtc;
+
+            QDateTime m_modifiedTimeUtc;
+
+            QVector<NodeFileConfig> m_files;
+
+            QVector<NodeFolderConfig> m_folders;
+
+            static const QString c_version;
+
+            static const QString c_id;
+
+            static const QString c_signature;
+
+            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;
+        };
+    }
+}
+
+#endif // VXNODECONFIG_H

+ 210 - 210
src/core/notebookconfigmgr/vxnotebookconfigmgr.cpp

@@ -10,6 +10,7 @@
 #include <notebook/vxnode.h>
 #include <notebook/externalnode.h>
 #include <notebook/bundlenotebook.h>
+#include <notebook/notebookdatabaseaccess.h>
 #include <utils/utils.h>
 #include <utils/fileutils.h>
 #include <utils/pathutils.h>
@@ -20,148 +21,11 @@
 
 #include <utils/contentmediautils.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";
+#include "vxnodeconfig.h"
 
-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 (int i = 0; i < arr.size(); ++i) {
-            m_tags << arr[i].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,
-                                            const QDateTime &p_modifiedTimeUtc)
-    : m_version(p_version),
-      m_id(p_id),
-      m_createdTimeUtc(p_createdTimeUtc),
-      m_modifiedTimeUtc(p_modifiedTimeUtc)
-{
-}
-
-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);
-    jobj[NodeConfig::c_modifiedTimeUtc] = Utils::dateTimeStringUniform(m_modifiedTimeUtc);
-
-    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());
-    m_modifiedTimeUtc = Utils::dateTimeFromStringUniform(p_jobj[NodeConfig::c_modifiedTimeUtc].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());
-    }
-}
+using namespace vnotex;
 
+using namespace vnotex::vx_node_config;
 
 const QString VXNotebookConfigMgr::c_nodeConfigName = "vx.json";
 
@@ -218,7 +82,8 @@ void VXNotebookConfigMgr::createEmptyRootNode()
 {
     auto currentTime = QDateTime::currentDateTimeUtc();
     NodeConfig node(getCodeVersion(),
-                    BundleNotebookConfigMgr::RootNodeId,
+                    Node::InvalidId,
+                    Node::InvalidId,
                     currentTime,
                     currentTime);
     writeNodeConfig(c_nodeConfigName, node);
@@ -278,7 +143,7 @@ void VXNotebookConfigMgr::createRecycleBinNode(const QSharedPointer<Node> &p_roo
     markNodeReadOnly(node.data());
 }
 
-QSharedPointer<VXNotebookConfigMgr::NodeConfig> VXNotebookConfigMgr::readNodeConfig(const QString &p_path) const
+QSharedPointer<NodeConfig> VXNotebookConfigMgr::readNodeConfig(const QString &p_path) const
 {
     auto backend = getBackend();
     if (!backend->exists(p_path)) {
@@ -319,19 +184,21 @@ void VXNotebookConfigMgr::writeNodeConfig(const Node *p_node)
 
 QSharedPointer<Node> VXNotebookConfigMgr::nodeConfigToNode(const NodeConfig &p_config,
                                                            const QString &p_name,
-                                                           Node *p_parent) const
+                                                           Node *p_parent)
 {
     auto node = QSharedPointer<VXNode>::create(p_name, getNotebook(), p_parent);
     loadFolderNode(node.data(), p_config);
     return node;
 }
 
-void VXNotebookConfigMgr::loadFolderNode(Node *p_node, const NodeConfig &p_config) const
+void VXNotebookConfigMgr::loadFolderNode(Node *p_node, const NodeConfig &p_config)
 {
     QVector<QSharedPointer<Node>> children;
     children.reserve(p_config.m_files.size() + p_config.m_folders.size());
     const auto basePath = p_node->fetchPath();
 
+    bool needUpdateConfig = false;
+
     for (const auto &folder : p_config.m_folders) {
         if (folder.m_name.isEmpty()) {
             // Skip empty name node.
@@ -354,12 +221,11 @@ void VXNotebookConfigMgr::loadFolderNode(Node *p_node, const NodeConfig &p_confi
             continue;
         }
 
-        auto fileNode = QSharedPointer<VXNode>::create(file.m_id,
-                                                       file.m_name,
-                                                       file.m_createdTimeUtc,
-                                                       file.m_modifiedTimeUtc,
-                                                       file.m_tags,
-                                                       file.m_attachmentFolder,
+        // For compability only.
+        needUpdateConfig = needUpdateConfig || file.m_signature == Node::InvalidId;
+
+        auto fileNode = QSharedPointer<VXNode>::create(file.m_name,
+                                                       file.toNodeParameters(),
                                                        getNotebook(),
                                                        p_node);
         inheritNodeFlags(p_node, fileNode.data());
@@ -367,11 +233,12 @@ void VXNotebookConfigMgr::loadFolderNode(Node *p_node, const NodeConfig &p_confi
         children.push_back(fileNode);
     }
 
-    p_node->loadCompleteInfo(p_config.m_id,
-                             p_config.m_createdTimeUtc,
-                             p_config.m_modifiedTimeUtc,
-                             QStringList(),
-                             children);
+    p_node->loadCompleteInfo(p_config.toNodeParameters(), children);
+
+    needUpdateConfig = needUpdateConfig || p_config.m_signature == Node::InvalidId;
+    if (needUpdateConfig) {
+        writeNodeConfig(p_node);
+    }
 }
 
 QSharedPointer<Node> VXNotebookConfigMgr::newNode(Node *p_parent,
@@ -435,15 +302,13 @@ QSharedPointer<Node> VXNotebookConfigMgr::newFileNode(Node *p_parent,
                                                       bool p_create,
                                                       const NodeParameters &p_paras)
 {
+    ensureNodeInDatabase(p_parent);
+
     auto notebook = getNotebook();
 
     // Create file node.
-    auto node = QSharedPointer<VXNode>::create(Node::InvalidId,
-                                               p_name,
-                                               p_paras.m_createdTimeUtc,
-                                               p_paras.m_modifiedTimeUtc,
-                                               p_paras.m_tags,
-                                               p_paras.m_attachmentFolder,
+    auto node = QSharedPointer<VXNode>::create(p_name,
+                                               p_paras,
                                                notebook,
                                                p_parent);
 
@@ -458,6 +323,8 @@ QSharedPointer<Node> VXNotebookConfigMgr::newFileNode(Node *p_parent,
     addChildNode(p_parent, node);
     writeNodeConfig(p_parent);
 
+    addNodeToDatabase(node.data());
+
     return node;
 }
 
@@ -466,15 +333,13 @@ QSharedPointer<Node> VXNotebookConfigMgr::newFolderNode(Node *p_parent,
                                                         bool p_create,
                                                         const NodeParameters &p_paras)
 {
+    ensureNodeInDatabase(p_parent);
+
     auto notebook = getNotebook();
 
     // Create folder node.
     auto node = QSharedPointer<VXNode>::create(p_name, notebook, p_parent);
-    node->loadCompleteInfo(Node::InvalidId,
-                           p_paras.m_createdTimeUtc,
-                           p_paras.m_modifiedTimeUtc,
-                           QStringList(),
-                           QVector<QSharedPointer<Node>>());
+    node->loadCompleteInfo(p_paras, QVector<QSharedPointer<Node>>());
 
     // Make folder.
     if (p_create) {
@@ -489,15 +354,19 @@ QSharedPointer<Node> VXNotebookConfigMgr::newFolderNode(Node *p_parent,
     addChildNode(p_parent, node);
     writeNodeConfig(p_parent);
 
+    addNodeToDatabase(node.data());
+
     return node;
 }
 
-QSharedPointer<VXNotebookConfigMgr::NodeConfig> VXNotebookConfigMgr::nodeToNodeConfig(const Node *p_node) const
+QSharedPointer<NodeConfig> VXNotebookConfigMgr::nodeToNodeConfig(const Node *p_node) const
 {
     Q_ASSERT(p_node->isContainer());
+    Q_ASSERT(p_node->isLoaded());
 
     auto config = QSharedPointer<NodeConfig>::create(getCodeVersion(),
                                                      p_node->getId(),
+                                                     p_node->getSignature(),
                                                      p_node->getCreatedTimeUtc(),
                                                      p_node->getModifiedTimeUtc());
 
@@ -506,6 +375,7 @@ QSharedPointer<VXNotebookConfigMgr::NodeConfig> VXNotebookConfigMgr::nodeToNodeC
             NodeFileConfig fileConfig;
             fileConfig.m_name = child->getName();
             fileConfig.m_id = child->getId();
+            fileConfig.m_signature = child->getSignature();
             fileConfig.m_createdTimeUtc = child->getCreatedTimeUtc();
             fileConfig.m_modifiedTimeUtc = child->getModifiedTimeUtc();
             fileConfig.m_attachmentFolder = child->getAttachmentFolder();
@@ -524,7 +394,7 @@ QSharedPointer<VXNotebookConfigMgr::NodeConfig> VXNotebookConfigMgr::nodeToNodeC
     return config;
 }
 
-void VXNotebookConfigMgr::loadNode(Node *p_node) const
+void VXNotebookConfigMgr::loadNode(Node *p_node)
 {
     if (p_node->isLoaded() || !p_node->exists()) {
         return;
@@ -548,6 +418,7 @@ void VXNotebookConfigMgr::saveNode(const Node *p_node)
 void VXNotebookConfigMgr::renameNode(Node *p_node, const QString &p_name)
 {
     Q_ASSERT(!p_node->isRoot());
+
     if (p_node->isContainer()) {
         getBackend()->renameDir(p_node->fetchPath(), p_name);
     } else {
@@ -556,8 +427,12 @@ void VXNotebookConfigMgr::renameNode(Node *p_node, const QString &p_name)
 
     p_node->setName(p_name);
     writeNodeConfig(p_node->getParent());
+
+    ensureNodeInDatabase(p_node);
+    updateNodeInDatabase(p_node);
 }
 
+// Do not touch DB here since it will be called at different scenarios.
 void VXNotebookConfigMgr::addChildNode(Node *p_parent, const QSharedPointer<Node> &p_child) const
 {
     if (p_child->isContainer()) {
@@ -604,11 +479,20 @@ QSharedPointer<Node> VXNotebookConfigMgr::loadNodeByPath(const QSharedPointer<No
 QSharedPointer<Node> VXNotebookConfigMgr::copyNodeAsChildOf(const QSharedPointer<Node> &p_src,
                                                             Node *p_dest,
                                                             bool p_move)
+{
+    return copyNodeAsChildOf(p_src, p_dest, p_move, true);
+}
+
+QSharedPointer<Node> VXNotebookConfigMgr::copyNodeAsChildOf(const QSharedPointer<Node> &p_src,
+                                                            Node *p_dest,
+                                                            bool p_move,
+                                                            bool p_updateDatabase)
 {
     Q_ASSERT(p_dest->isContainer());
 
     if (!p_src->exists()) {
         if (p_move) {
+            // It is OK to always update the database.
             p_src->getNotebook()->removeNode(p_src);
         }
         return nullptr;
@@ -616,9 +500,9 @@ QSharedPointer<Node> VXNotebookConfigMgr::copyNodeAsChildOf(const QSharedPointer
 
     QSharedPointer<Node> node;
     if (p_src->isContainer()) {
-        node = copyFolderNodeAsChildOf(p_src, p_dest, p_move);
+        node = copyFolderNodeAsChildOf(p_src, p_dest, p_move, p_updateDatabase);
     } else {
-        node = copyFileNodeAsChildOf(p_src, p_dest, p_move);
+        node = copyFileNodeAsChildOf(p_src, p_dest, p_move, p_updateDatabase);
     }
 
     return node;
@@ -626,7 +510,8 @@ QSharedPointer<Node> VXNotebookConfigMgr::copyNodeAsChildOf(const QSharedPointer
 
 QSharedPointer<Node> VXNotebookConfigMgr::copyFileNodeAsChildOf(const QSharedPointer<Node> &p_src,
                                                                 Node *p_dest,
-                                                                bool p_move)
+                                                                bool p_move,
+                                                                bool p_updateDatabase)
 {
     // Copy source file itself.
     auto srcFilePath = p_src->fetchAbsolutePath();
@@ -647,27 +532,51 @@ QSharedPointer<Node> VXNotebookConfigMgr::copyFileNodeAsChildOf(const QSharedPoi
 
     // 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();
+    const bool sameNotebook = p_src->getNotebook() == notebook;
+
+    if (p_updateDatabase) {
+        ensureNodeInDatabase(p_dest);
+        if (sameNotebook) {
+            ensureNodeInDatabase(p_src.data());
+        }
     }
 
-    auto destNode = QSharedPointer<VXNode>::create(id,
-                                                   PathUtils::fileName(destFilePath),
-                                                   p_src->getCreatedTimeUtc(),
-                                                   p_src->getModifiedTimeUtc(),
-                                                   p_src->getTags(),
-                                                   attachmentFolder,
+    NodeParameters paras;
+    if (p_move && sameNotebook) {
+        paras.m_id = p_src->getId();
+        paras.m_signature = p_src->getSignature();
+    }
+    paras.m_createdTimeUtc = p_src->getCreatedTimeUtc();
+    paras.m_modifiedTimeUtc = p_src->getModifiedTimeUtc();
+    paras.m_tags = p_src->getTags();
+    paras.m_attachmentFolder = attachmentFolder;
+    auto destNode = QSharedPointer<VXNode>::create(PathUtils::fileName(destFilePath),
+                                                   paras,
                                                    notebook,
                                                    p_dest);
     destNode->setExists(true);
+
     addChildNode(p_dest, destNode);
     writeNodeConfig(p_dest);
 
+    if (p_updateDatabase) {
+        if (p_move && sameNotebook) {
+            updateNodeInDatabase(destNode.data());
+        } else {
+            addNodeToDatabase(destNode.data());
+        }
+
+        Q_ASSERT(nodeExistsInDatabase(destNode.data()));
+    }
+
     if (p_move) {
-        // Delete src node.
-        p_src->getNotebook()->removeNode(p_src);
+        if (sameNotebook) {
+            // The same notebook. Do not directly call removeNode() since we need to update the record
+            // in database directly.
+            removeNode(p_src, false, false, false);
+        } else {
+            p_src->getNotebook()->removeNode(p_src);
+        }
     }
 
     return destNode;
@@ -675,7 +584,8 @@ QSharedPointer<Node> VXNotebookConfigMgr::copyFileNodeAsChildOf(const QSharedPoi
 
 QSharedPointer<Node> VXNotebookConfigMgr::copyFolderNodeAsChildOf(const QSharedPointer<Node> &p_src,
                                                                   Node *p_dest,
-                                                                  bool p_move)
+                                                                  bool p_move,
+                                                                  bool p_updateDatabase)
 {
     auto srcFolderPath = p_src->fetchAbsolutePath();
     auto destFolderPath = PathUtils::concatenateFilePath(p_dest->fetchPath(),
@@ -687,47 +597,78 @@ QSharedPointer<Node> VXNotebookConfigMgr::copyFolderNodeAsChildOf(const QSharedP
 
     // 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();
+    const bool sameNotebook = p_src->getNotebook() == notebook;
+
+    if (p_updateDatabase) {
+        ensureNodeInDatabase(p_dest);
+        if (sameNotebook) {
+            ensureNodeInDatabase(p_src.data());
+        }
     }
+
     auto destNode = QSharedPointer<VXNode>::create(PathUtils::fileName(destFolderPath),
                                                    notebook,
                                                    p_dest);
-    destNode->loadCompleteInfo(id,
-                               p_src->getCreatedTimeUtc(),
-                               p_src->getModifiedTimeUtc(),
-                               QStringList(),
-                               QVector<QSharedPointer<Node>>());
-    destNode->setExists(true);
+    {
+        NodeParameters paras;
+        if (p_move && sameNotebook) {
+            paras.m_id = p_src->getId();
+            paras.m_signature = p_src->getSignature();
+        }
+        paras.m_createdTimeUtc = p_src->getCreatedTimeUtc();
+        paras.m_modifiedTimeUtc = p_src->getModifiedTimeUtc();
+        destNode->loadCompleteInfo(paras, QVector<QSharedPointer<Node>>());
+    }
 
+    destNode->setExists(true);
     writeNodeConfig(destNode.data());
 
     addChildNode(p_dest, destNode);
     writeNodeConfig(p_dest);
 
+    if (p_updateDatabase) {
+        if (p_move && sameNotebook) {
+            p_updateDatabase = false;
+            updateNodeInDatabase(destNode.data());
+        } else {
+            addNodeToDatabase(destNode.data());
+        }
+    }
+
     // Copy children node.
     auto children = p_src->getChildren();
     for (const auto &childNode : children) {
-        copyNodeAsChildOf(childNode, destNode.data(), p_move);
+        copyNodeAsChildOf(childNode, destNode.data(), p_move, p_updateDatabase);
     }
 
     if (p_move) {
-        p_src->getNotebook()->removeNode(p_src);
+        if (sameNotebook) {
+            removeNode(p_src, false, false, false);
+        } else {
+            p_src->getNotebook()->removeNode(p_src);
+        }
     }
 
     return destNode;
 }
 
 void VXNotebookConfigMgr::removeNode(const QSharedPointer<Node> &p_node, bool p_force, bool p_configOnly)
+{
+    removeNode(p_node, p_force, p_configOnly, true);
+}
+
+void VXNotebookConfigMgr::removeNode(const QSharedPointer<Node> &p_node,
+                                     bool p_force,
+                                     bool p_configOnly,
+                                     bool p_updateDatabase)
 {
     auto parentNode = p_node->getParent();
     if (!p_configOnly && p_node->exists()) {
         // Remove all children.
         auto children = p_node->getChildren();
         for (const auto &childNode : children) {
-            removeNode(childNode, p_force, p_configOnly);
+            // With DELETE CASCADE, we could just touch the DB at parent level.
+            removeNode(childNode, p_force, p_configOnly, false);
         }
 
         try {
@@ -737,6 +678,11 @@ void VXNotebookConfigMgr::removeNode(const QSharedPointer<Node> &p_node, bool p_
         }
     }
 
+    if (p_updateDatabase) {
+        // Remove it from data base before modifying the parent.
+        removeNodeFromDatabase(p_node.data());
+    }
+
     if (parentNode) {
         parentNode->removeChild(p_node);
         writeNodeConfig(parentNode);
@@ -861,27 +807,27 @@ QSharedPointer<Node> VXNotebookConfigMgr::copyFileAsChildOf(const QString &p_src
         ContentMediaUtils::copyMediaFiles(p_srcPath, getBackend().data(), destFilePath);
     }
 
+    ensureNodeInDatabase(p_dest);
+
     const auto name = PathUtils::fileName(destFilePath);
     auto destNode = p_dest->findChild(name, true);
     if (destNode) {
         // Already have the node.
+        ensureNodeInDatabase(destNode.data());
         return destNode;
     }
 
     // Create a file node.
-    auto currentTime = QDateTime::currentDateTimeUtc();
-    destNode = QSharedPointer<VXNode>::create(getNotebook()->getAndUpdateNextNodeId(),
-                                              name,
-                                              currentTime,
-                                              currentTime,
-                                              QStringList(),
-                                              QString(),
+    destNode = QSharedPointer<VXNode>::create(name,
+                                              NodeParameters(),
                                               getNotebook(),
                                               p_dest);
     destNode->setExists(true);
     addChildNode(p_dest, destNode);
     writeNodeConfig(p_dest);
 
+    addNodeToDatabase(destNode.data());
+
     return destNode;
 }
 
@@ -897,22 +843,20 @@ QSharedPointer<Node> VXNotebookConfigMgr::copyFolderAsChildOf(const QString &p_s
         getBackend()->copyDir(p_srcPath, destFolderPath);
     }
 
+    ensureNodeInDatabase(p_dest);
+
     const auto name = PathUtils::fileName(destFolderPath);
     auto destNode = p_dest->findChild(name, true);
     if (destNode) {
         // Already have the node.
+        ensureNodeInDatabase(destNode.data());
         return destNode;
     }
 
     // Create a folder node.
     auto notebook = getNotebook();
     destNode = QSharedPointer<VXNode>::create(name, notebook, p_dest);
-    auto currentTime = QDateTime::currentDateTimeUtc();
-    destNode->loadCompleteInfo(notebook->getAndUpdateNextNodeId(),
-                               currentTime,
-                               currentTime,
-                               QStringList(),
-                               QVector<QSharedPointer<Node>>());
+    destNode->loadCompleteInfo(NodeParameters(), QVector<QSharedPointer<Node>>());
     destNode->setExists(true);
 
     writeNodeConfig(destNode.data());
@@ -920,6 +864,8 @@ QSharedPointer<Node> VXNotebookConfigMgr::copyFolderAsChildOf(const QString &p_s
     addChildNode(p_dest, destNode);
     writeNodeConfig(p_dest);
 
+    addNodeToDatabase(destNode.data());
+
     return destNode;
 }
 
@@ -1052,3 +998,57 @@ bool VXNotebookConfigMgr::isLikelyImageFolder(const QString &p_dirPath)
 
     return true;
 }
+
+NotebookDatabaseAccess *VXNotebookConfigMgr::getDatabaseAccess() const
+{
+    return static_cast<BundleNotebook *>(getNotebook())->getDatabaseAccess();
+}
+
+void VXNotebookConfigMgr::updateNodeInDatabase(Node *p_node)
+{
+    Q_ASSERT(sameNotebook(p_node));
+    getDatabaseAccess()->updateNode(p_node);
+}
+
+void VXNotebookConfigMgr::ensureNodeInDatabase(Node *p_node)
+{
+    if (!p_node) {
+        return;
+    }
+
+    Q_ASSERT(sameNotebook(p_node));
+
+    auto db = getDatabaseAccess();
+    if (db->existsNode(p_node)) {
+        return;
+    }
+
+    ensureNodeInDatabase(p_node->getParent());
+    db->addNode(p_node, false);
+    db->clearObsoleteNodes();
+}
+
+void VXNotebookConfigMgr::addNodeToDatabase(Node *p_node)
+{
+    Q_ASSERT(sameNotebook(p_node));
+    auto db = getDatabaseAccess();
+    db->addNode(p_node, false);
+    db->clearObsoleteNodes();
+}
+
+bool VXNotebookConfigMgr::nodeExistsInDatabase(const Node *p_node)
+{
+    Q_ASSERT(sameNotebook(p_node));
+    return getDatabaseAccess()->existsNode(p_node);
+}
+
+void VXNotebookConfigMgr::removeNodeFromDatabase(const Node *p_node)
+{
+    Q_ASSERT(sameNotebook(p_node));
+    getDatabaseAccess()->removeNode(p_node);
+}
+
+bool VXNotebookConfigMgr::sameNotebook(const Node *p_node) const
+{
+    return p_node ? p_node->getNotebook() == getNotebook() : true;
+}

+ 49 - 75
src/core/notebookconfigmgr/vxnotebookconfigmgr.h

@@ -7,12 +7,21 @@
 #include <QVector>
 #include <QRegExp>
 
-#include "../global.h"
+#include <core/global.h>
 
 class QJsonObject;
 
 namespace vnotex
 {
+    namespace vx_node_config
+    {
+        struct NodeFileConfig;
+        struct NodeFolderConfig;
+        struct NodeConfig;
+    }
+
+    class NotebookDatabaseAccess;
+
     // Config manager for VNoteX's bundle notebook.
     class VXNotebookConfigMgr : public BundleNotebookConfigMgr
     {
@@ -34,7 +43,7 @@ namespace vnotex
 
         QSharedPointer<Node> loadRootNode() Q_DECL_OVERRIDE;
 
-        void loadNode(Node *p_node) const Q_DECL_OVERRIDE;
+        void loadNode(Node *p_node) Q_DECL_OVERRIDE;
         void saveNode(const Node *p_node) Q_DECL_OVERRIDE;
 
         void renameNode(Node *p_node, const QString &p_name) Q_DECL_OVERRIDE;
@@ -77,85 +86,20 @@ namespace vnotex
         QStringList scanAndImportExternalFiles(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,
-                       const QDateTime &p_modifiedTimeUtc);
-
-            QJsonObject toJson() const;
-
-            void fromJson(const QJsonObject &p_jobj);
-
-            QString m_version;
-            ID m_id = Node::InvalidId;
-            QDateTime m_createdTimeUtc;
-            QDateTime m_modifiedTimeUtc;
-            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;
+        QSharedPointer<vx_node_config::NodeConfig> readNodeConfig(const QString &p_path) const;
+        void writeNodeConfig(const QString &p_path, const vx_node_config::NodeConfig &p_config) const;
 
         void writeNodeConfig(const Node *p_node);
 
-        QSharedPointer<Node> nodeConfigToNode(const NodeConfig &p_config,
+        QSharedPointer<Node> nodeConfigToNode(const vx_node_config::NodeConfig &p_config,
                                               const QString &p_name,
-                                              Node *p_parent = nullptr) const;
+                                              Node *p_parent = nullptr);
 
-        void loadFolderNode(Node *p_node, const NodeConfig &p_config) const;
+        void loadFolderNode(Node *p_node, const vx_node_config::NodeConfig &p_config);
 
-        QSharedPointer<VXNotebookConfigMgr::NodeConfig> nodeToNodeConfig(const Node *p_node) const;
+        QSharedPointer<vx_node_config::NodeConfig> nodeToNodeConfig(const Node *p_node) const;
 
         QSharedPointer<Node> newFileNode(Node *p_parent,
                                          const QString &p_name,
@@ -172,9 +116,20 @@ namespace vnotex
 
         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> copyNodeAsChildOf(const QSharedPointer<Node> &p_src,
+                                               Node *p_dest,
+                                               bool p_move,
+                                               bool p_updateDatabase);
+
+        QSharedPointer<Node> copyFileNodeAsChildOf(const QSharedPointer<Node> &p_src,
+                                                   Node *p_dest,
+                                                   bool p_move,
+                                                   bool p_updateDatabase);
 
-        QSharedPointer<Node> copyFolderNodeAsChildOf(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,
+                                                     bool p_updateDatabase);
 
         QSharedPointer<Node> copyFileAsChildOf(const QString &p_srcPath, Node *p_dest);
 
@@ -197,6 +152,25 @@ namespace vnotex
 
         bool isExcludedFromExternalNode(const QString &p_name) const;
 
+        void removeNode(const QSharedPointer<Node> &p_node,
+                        bool p_force,
+                        bool p_configOnly,
+                        bool p_updateDatabase);
+
+        NotebookDatabaseAccess *getDatabaseAccess() const;
+
+        void updateNodeInDatabase(Node *p_node);
+
+        void ensureNodeInDatabase(Node *p_node);
+
+        void addNodeToDatabase(Node *p_node);
+
+        bool nodeExistsInDatabase(const Node *p_node);
+
+        void removeNodeFromDatabase(const Node *p_node);
+
+        bool sameNotebook(const Node *p_node) const;
+
         static bool isLikelyImageFolder(const QString &p_dirPath);
 
         Info m_info;

+ 1 - 0
src/core/notebookmgr.cpp

@@ -369,6 +369,7 @@ void NotebookMgr::setCurrentNotebookAfterUpdate()
 
 void NotebookMgr::addNotebook(const QSharedPointer<Notebook> &p_notebook)
 {
+    p_notebook->initialize();
     m_notebooks.push_back(p_notebook);
     connect(p_notebook.data(), &Notebook::updated,
             this, [this, notebook = p_notebook.data()]() {

+ 1 - 0
src/core/vnotex.cpp

@@ -38,6 +38,7 @@ VNoteX::VNoteX(QObject *p_parent)
 
 void VNoteX::initLoad()
 {
+    qDebug() << "start init which may take a while";
     m_notebookMgr->loadNotebooks();
 }
 

+ 0 - 2
src/data/core/core.qrc

@@ -45,8 +45,6 @@
         <file>icons/attachment_full_editor.svg</file>
         <file>icons/split_menu.svg</file>
         <file>icons/split_window_list.svg</file>
-        <file>icons/horizontal_split.svg</file>
-        <file>icons/vertical_split.svg</file>
         <file>icons/type_heading_editor.svg</file>
         <file>icons/type_bold_editor.svg</file>
         <file>icons/type_italic_editor.svg</file>

+ 0 - 15
src/data/core/icons/horizontal_split.svg

@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="iso-8859-1"?>
-<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
-	 width="468.067px" height="468.067px" viewBox="0 0 468.067 468.067" style="enable-background:new 0 0 468.067 468.067;"
-	 xml:space="preserve">
-<g>
-	<path fill="#000000" d="M431.38,0.225H36.685C16.458,0.225,0,16.674,0,36.898v394.268c0,20.221,16.458,36.677,36.685,36.677H431.38
-		c20.232,0,36.688-16.456,36.688-36.677V36.898C468.062,16.668,451.606,0.225,431.38,0.225z M406.519,41.969
-		c8.678,0,15.711,7.04,15.711,15.72c0,8.683-7.033,15.717-15.711,15.717c-8.688,0-15.723-7.04-15.723-15.717
-		C390.796,49.009,397.83,41.969,406.519,41.969z M350.189,41.969c8.688,0,15.723,7.04,15.723,15.72
-		c0,8.683-7.034,15.717-15.723,15.717c-8.684,0-15.711-7.04-15.711-15.717C334.479,49.009,341.513,41.969,350.189,41.969z
-		 M426.143,112.429v143.519H41.919V112.429H426.143z M41.919,425.924V272.09h384.224v153.84H41.919V425.924z"/>
-</g>
-</svg>

+ 0 - 15
src/data/core/icons/vertical_split.svg

@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="iso-8859-1"?>
-<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
-	 width="468.062px" height="468.062px" viewBox="0 0 468.062 468.062" style="enable-background:new 0 0 468.062 468.062;"
-	 xml:space="preserve">
-<g>
-	<path fill="#000000" d="M431.379,0.222h-394.7C16.456,0.222,0,16.671,0,36.895v394.268c0,20.221,16.456,36.677,36.679,36.677h394.7
-		c20.228,0,36.683-16.456,36.683-36.677V36.895C468.062,16.665,451.606,0.222,431.379,0.222z M406.519,41.966
-		c8.689,0,15.723,7.04,15.723,15.72c0,8.683-7.033,15.717-15.723,15.717c-8.688,0-15.723-7.04-15.723-15.717
-		C390.796,49.006,397.83,41.966,406.519,41.966z M350.189,41.966c8.688,0,15.723,7.04,15.723,15.72
-		c0,8.683-7.034,15.717-15.723,15.717c-8.684,0-15.711-7.04-15.711-15.717C334.479,49.006,341.506,41.966,350.189,41.966z
-		 M41.913,112.426h184.055v313.495H41.913V112.426z M426.148,425.921H242.104V112.426h184.044V425.921z"/>
-</g>
-</svg>

+ 7 - 0
src/data/extra/docs/en/features_tips.txt

@@ -0,0 +1,7 @@
+<p>Some features not to be missed in VNote:</p>
+<h3>Markdown Editor</h3>
+<ul>
+<li><strong>Parse to Markdown and Paste</strong> on the context menu: parse rich format text to Markdown text and fetch images to local if necessary.</li>
+<li><strong>Rich Paste</strong> on the context menu: paste as image, attachment, or link.</li>
+<li><strong>Cross Copy</strong> on the context menu: copy selected text as rich format text.</li>
+</ul>

+ 7 - 0
src/data/extra/docs/zh_CN/features_tips.txt

@@ -0,0 +1,7 @@
+<p>VNote 中一些不容错过的特性:</p>
+<h3 id="markdown-">Markdown 编辑器</h3>
+<ul>
+<li>上下文菜单中的 <strong>解析为 Markdown 并粘贴</strong>: 解析富文本为 Markdown 文本,并按需获取图片到本地。</li>
+<li>上下文菜单中的 <strong>多功能粘贴</strong>: 粘贴为图片、附件或者连接。</li>
+<li>上下文菜单中的 <strong>交叉复制</strong>: 将所选文本复制为富文本。</li>
+</ul>

+ 2 - 0
src/data/extra/extra.qrc

@@ -6,12 +6,14 @@
         <file>docs/en/markdown_guide.md</file>
         <file>docs/en/external_programs.md</file>
         <file>docs/en/welcome.md</file>
+        <file>docs/en/features_tips.txt</file>
         <file>docs/zh_CN/get_started.txt</file>
         <file>docs/zh_CN/about_vnotex.txt</file>
         <file>docs/zh_CN/shortcuts.md</file>
         <file>docs/zh_CN/markdown_guide.md</file>
         <file>docs/zh_CN/external_programs.md</file>
         <file>docs/zh_CN/welcome.md</file>
+        <file>docs/zh_CN/features_tips.txt</file>
         <file>web/markdown-viewer-template.html</file>
         <file>web/markdown-export-template.html</file>
         <file>web/css/globalstyles.css</file>

+ 19 - 7
src/export/webviewexporter.cpp

@@ -559,20 +559,32 @@ bool WebViewExporter::doExportWkhtmltopdf(const ExportPdfOption &p_pdfOption, co
 
 bool WebViewExporter::htmlToPdfViaWkhtmltopdf(const ExportPdfOption &p_pdfOption, const QStringList &p_htmlFiles, const QString &p_outputFile)
 {
-    // Note: system's locale settings (Language for non-Unicode programs) is important to wkhtmltopdf.
-    // Input file could be encoded via QUrl::fromLocalFile(p_htmlFile).toString(QUrl::EncodeUnicode) to
-    // handle non-ASCII path.
-
     QStringList args(m_wkhtmltopdfArgs);
 
     // Prepare the args.
     for (auto const &file : p_htmlFiles) {
-        args << QDir::toNativeSeparators(file);
+        // Note: system's locale settings (Language for non-Unicode programs) is important to wkhtmltopdf.
+        // Input file could be encoded via QUrl::fromLocalFile(p_htmlFile).toString(QUrl::EncodeUnicode) to
+        // handle non-ASCII path. But for the output file, it is useless.
+        args << QUrl::fromLocalFile(QDir::toNativeSeparators(file)).toString(QUrl::EncodeUnicode);
+    }
+
+    // To handle non-ASCII path, export it to a temp file and then copy it.
+    QTemporaryDir tmpDir;
+    if (!tmpDir.isValid()) {
+        return false;
     }
 
-    args << QDir::toNativeSeparators(p_outputFile);
+    const auto tmpFile = tmpDir.filePath("vx_tmp_output.pdf");
+    args << QDir::toNativeSeparators(tmpFile);
 
-    return startProcess(p_pdfOption.m_wkhtmltopdfExePath, args);
+    bool ret = startProcess(QDir::toNativeSeparators(p_pdfOption.m_wkhtmltopdfExePath), args);
+    if (ret && QFileInfo::exists(tmpFile)) {
+        emit logRequested(tr("Copy output file (%1) to (%2).").arg(tmpFile, p_outputFile));
+        FileUtils::copyFile(tmpFile, p_outputFile);
+    }
+
+    return ret;
 }
 
 bool WebViewExporter::startProcess(const QString &p_program, const QStringList &p_args)

+ 2 - 1
src/src.pro

@@ -3,6 +3,7 @@ lessThan(QT_MAJOR_VERSION, 5): error("requires Qt 5 and above")
 equals(QT_MAJOR_VERSION, 5):lessThan(QT_MINOR_VERSION, 12): error("requires Qt 5.12 and above")
 
 QT += core gui widgets webenginewidgets webchannel network svg printsupport
+QT += sql
 
 CONFIG -= qtquickcompiler
 
@@ -88,7 +89,7 @@ macx {
     app_target = $${app_bundle_dir}/$${TARGET}
     QMAKE_POST_LINK += \
         install_name_tool -add_rpath $${vte_lib_dir} $${app_target} && \
-        install_name_tool -change $${vte_lib_full_name} @rpath/$${vte_lib_full_name} $${app_target} &&
+        install_name_tool -change $${vte_lib_full_name} @rpath/$${vte_lib_full_name} $${app_target} && \
 
     # Process VSyntaxHighlighting framework
     sh_lib_name = VSyntaxHighlighting

+ 3 - 2
src/utils/fileutils.cpp

@@ -310,8 +310,9 @@ QString FileUtils::generateRandomFileName(const QString &p_hints, const QString
 {
     Q_UNUSED(p_hints);
 
-    const QString timeStamp(QDateTime::currentDateTime().toString(QStringLiteral("sszzzmmHHMMdd")));
-    QString baseName = QString::number(timeStamp.toLongLong() + qrand());
+    // Do not use toSecsSinceEpoch() here since we want a short name.
+    const QString timeStamp(QDateTime::currentDateTime().toString(QStringLiteral("sszzzmmHHyyMMdd")));
+    const QString baseName(QString::number(timeStamp.toLongLong() + qrand()));
 
     QString suffix;
     if (!p_suffix.isEmpty()) {

+ 1 - 1
src/utils/pathutils.cpp

@@ -240,7 +240,7 @@ bool PathUtils::isDir(const QString &p_path)
 bool PathUtils::isLocalFile(const QString &p_path)
 {
     if (p_path.isEmpty()) {
-        return false;
+        return true;
     }
 
     QRegularExpression regExp("^(?:ftp|http|https)://");

+ 2 - 0
src/widgets/dialogs/exportdialog.cpp

@@ -553,6 +553,7 @@ QWidget *ExportDialog::getHtmlAdvancedSettings()
                     });
             // TODO: do not support MHTML for now.
             m_useMimeHtmlFormatCheckBox->setEnabled(false);
+            m_useMimeHtmlFormatCheckBox->hide();
             layout->addRow(m_useMimeHtmlFormatCheckBox);
         }
 
@@ -673,6 +674,7 @@ QWidget *ExportDialog::getPdfAdvancedSettings()
         {
             m_allInOneCheckBox = WidgetsFactory::createCheckBox(tr("All-in-One"), widget);
             m_allInOneCheckBox->setToolTip(tr("Export all source files into one file"));
+            m_allInOneCheckBox->setEnabled(false);
             connect(m_useWkhtmltopdfCheckBox, &QCheckBox::stateChanged,
                     this, [this](int p_state) {
                         m_allInOneCheckBox->setEnabled(p_state == Qt::Checked);

+ 1 - 0
src/widgets/dialogs/importfolderutils.cpp

@@ -1,6 +1,7 @@
 #include "importfolderutils.h"
 
 #include <notebook/notebook.h>
+#include <notebook/nodeparameters.h>
 #include <core/exception.h>
 #include <QCoreApplication>
 #include "legacynotebookutils.h"

+ 1 - 1
src/widgets/dialogs/newfolderdialog.cpp

@@ -51,7 +51,7 @@ bool NewFolderDialog::validateNameInput(QString &p_msg)
     p_msg.clear();
 
     auto name = m_infoWidget->getName();
-    if (name.isEmpty()) {
+    if (name.isEmpty() || !PathUtils::isLegalFileName(name)) {
         p_msg = tr("Please specify a name for the folder.");
         return false;
     }

+ 11 - 3
src/widgets/editors/markdowneditor.cpp

@@ -482,7 +482,7 @@ void MarkdownEditor::handleInsertFromMimeData(const QMimeData *p_source, bool *p
         // Default paste.
         // Give tips about the Rich Paste and Parse As Markdown And Paste features.
         VNoteX::getInst().showStatusMessageShort(
-            tr("For advanced paste, try the \"Rich Paste\" and \"Parse To Markdown And Paste\" on the editor's context menu"));
+            tr("For advanced paste, try the \"Rich Paste\" and \"Parse to Markdown and Paste\" on the editor's context menu"));
         return;
     } else {
         clipboard->setProperty(c_clipboardPropertyMark, false);
@@ -985,14 +985,22 @@ void MarkdownEditor::handleContextMenuEvent(QContextMenuEvent *p_event, bool *p_
         WidgetUtils::insertActionAfter(menu, pasteAct, richPasteAct);
 
         if (mimeData->hasHtml()) {
-            // Parse To Markdown And Paste.
-            auto parsePasteAct = new QAction(tr("Parse To Markdown And Paste"), menu);
+            // Parse to Markdown and Paste.
+            auto parsePasteAct = new QAction(tr("Parse to Markdown and Paste"), menu);
             connect(parsePasteAct, &QAction::triggered,
                     this, &MarkdownEditor::parseToMarkdownAndPaste);
             WidgetUtils::insertActionAfter(menu, richPasteAct, parsePasteAct);
         }
     }
 
+    {
+        menu->addSeparator();
+
+        auto snippetAct = menu->addAction(tr("Insert Snippet"), this, &MarkdownEditor::applySnippetRequested);
+        WidgetUtils::addActionShortcutText(snippetAct,
+                                           ConfigMgr::getInst().getEditorConfig().getShortcut(EditorConfig::Shortcut::ApplySnippet));
+    }
+
     appendImageHostMenu(menu);
 
     appendSpellCheckMenu(p_event, menu);

+ 2 - 0
src/widgets/editors/markdowneditor.h

@@ -117,6 +117,8 @@ namespace vnotex
 
         void readRequested();
 
+        void applySnippetRequested();
+
     private slots:
         void handleCanInsertFromMimeData(const QMimeData *p_source, bool *p_handled, bool *p_allowed);
 

+ 27 - 1
src/widgets/editors/texteditor.cpp

@@ -1,6 +1,14 @@
 #include "texteditor.h"
 
+#include <QContextMenuEvent>
+#include <QMenu>
+
 #include <vtextedit/texteditorconfig.h>
+#include <vtextedit/vtextedit.h>
+
+#include <core/configmgr.h>
+#include <core/editorconfig.h>
+#include <utils/widgetutils.h>
 
 using namespace vnotex;
 
@@ -9,5 +17,23 @@ TextEditor::TextEditor(const QSharedPointer<vte::TextEditorConfig> &p_config,
                        QWidget *p_parent)
     : vte::VTextEditor(p_config, p_paras, p_parent)
 {
-    enableInternalContextMenu();
+    connect(m_textEdit, &vte::VTextEdit::contextMenuEventRequested,
+            this, &TextEditor::handleContextMenuEvent);
+}
+
+void TextEditor::handleContextMenuEvent(QContextMenuEvent *p_event, bool *p_handled, QScopedPointer<QMenu> *p_menu)
+{
+    *p_handled = true;
+    p_menu->reset(m_textEdit->createStandardContextMenu(p_event->pos()));
+    auto menu = p_menu->data();
+
+    {
+        menu->addSeparator();
+
+        auto snippetAct = menu->addAction(tr("Insert Snippet"), this, &TextEditor::applySnippetRequested);
+        WidgetUtils::addActionShortcutText(snippetAct,
+                                           ConfigMgr::getInst().getEditorConfig().getShortcut(EditorConfig::Shortcut::ApplySnippet));
+    }
+
+    appendSpellCheckMenu(p_event, menu);
 }

+ 6 - 0
src/widgets/editors/texteditor.h

@@ -12,6 +12,12 @@ namespace vnotex
         TextEditor(const QSharedPointer<vte::TextEditorConfig> &p_config,
                    const QSharedPointer<vte::TextEditorParameters> &p_paras,
                    QWidget *p_parent = nullptr);
+
+    signals:
+        void applySnippetRequested();
+
+    private slots:
+        void handleContextMenuEvent(QContextMenuEvent *p_event, bool *p_handled, QScopedPointer<QMenu> *p_menu);
     };
 }
 

+ 28 - 2
src/widgets/mainwindow.cpp

@@ -18,6 +18,7 @@
 #include <QSystemTrayIcon>
 #include <QWindowStateChangeEvent>
 #include <QTimer>
+#include <QProgressDialog>
 
 #include "toolbox.h"
 #include "notebookexplorer.h"
@@ -31,6 +32,7 @@
 #include <core/mainconfig.h>
 #include <core/widgetconfig.h>
 #include <core/events.h>
+#include <core/exception.h>
 #include <core/fileopenparameters.h>
 #include <widgets/dialogs/exportdialog.h>
 #include "viewwindow.h"
@@ -94,9 +96,20 @@ void MainWindow::kickOffOnStart(const QStringList &p_paths)
         // Need to load the state of dock widgets again after the main window is shown.
         loadStateAndGeometry(true);
 
-        VNoteX::getInst().initLoad();
+        {
+            QProgressDialog proDlg(tr("Initializing core components..."),
+                                   QString(),
+                                   0,
+                                   0,
+                                   this);
+            proDlg.setWindowFlags(proDlg.windowFlags() & ~Qt::WindowCloseButtonHint);
+            proDlg.setWindowModality(Qt::WindowModal);
+            proDlg.setValue(0);
 
-        setupSpellCheck();
+            VNoteX::getInst().initLoad();
+
+            setupSpellCheck();
+        }
 
         // Do necessary stuffs before emitting this signal.
         emit mainWindowStarted();
@@ -110,6 +123,19 @@ void MainWindow::kickOffOnStart(const QStringList &p_paths)
         openFiles(p_paths);
 
         if (MainConfig::isVersionChanged()) {
+            QString tips;
+            try {
+                tips = DocsUtils::getDocText("features_tips.txt");
+            } catch (Exception &p_e) {
+                // Just ignore it.
+                Q_UNUSED(p_e);
+            }
+            if (!tips.isEmpty()) {
+                MessageBoxHelper::notify(MessageBoxHelper::Information,
+                                         tips,
+                                         this);
+            }
+
             const auto file = DocsUtils::getDocFile(QStringLiteral("welcome.md"));
             if (!file.isEmpty()) {
                 auto paras = QSharedPointer<FileOpenParameters>::create();

+ 3 - 0
src/widgets/markdownviewwindow.cpp

@@ -361,6 +361,9 @@ void MarkdownViewWindow::setupTextEditor()
             this, [this]() {
                 read(true);
             });
+
+    connect(m_editor, &MarkdownEditor::applySnippetRequested,
+            this, QOverload<>::of(&MarkdownViewWindow::applySnippet));
 }
 
 QStackedWidget *MarkdownViewWindow::getMainStatusWidget() const

+ 37 - 2
src/widgets/notebookexplorer.cpp

@@ -5,6 +5,7 @@
 #include <QToolButton>
 #include <QMenu>
 #include <QActionGroup>
+#include <QProgressDialog>
 
 #include "titlebar.h"
 #include "dialogs/newnotebookdialog.h"
@@ -170,6 +171,12 @@ TitleBar *NotebookExplorer::setupTitleBar(QWidget *p_parent)
                 this, &NotebookExplorer::manageNotebooks);
     }
 
+    titleBar->addMenuAction(tr("Rebuild Notebook Database"),
+                            titleBar,
+                            [this]() {
+                                rebuildDatabase();
+                            });
+
     // External Files menu.
     {
         auto subMenu = titleBar->addMenuSubMenu(tr("External Files"));
@@ -186,7 +193,7 @@ TitleBar *NotebookExplorer::setupTitleBar(QWidget *p_parent)
 
         auto importAct = titleBar->addMenuAction(
             subMenu,
-            tr("Import External Files When Activated"),
+            tr("Import External Files when Activated"),
             titleBar,
             [](bool p_checked) {
                 ConfigMgr::getInst().getWidgetConfig().setNodeExplorerAutoImportExternalFilesEnabled(p_checked);
@@ -196,7 +203,7 @@ TitleBar *NotebookExplorer::setupTitleBar(QWidget *p_parent)
     }
 
     {
-        auto act = titleBar->addMenuAction(tr("Close File Before Open With External Program"),
+        auto act = titleBar->addMenuAction(tr("Close File Before Open with External Program"),
                                            titleBar,
                                            [](bool p_checked) {
                                                ConfigMgr::getInst().getWidgetConfig().setNodeExplorerCloseBeforeOpenWithEnabled(p_checked);
@@ -535,3 +542,31 @@ void NotebookExplorer::recoverSession()
         }
     }
 }
+
+void NotebookExplorer::rebuildDatabase()
+{
+    if (m_currentNotebook) {
+
+        QProgressDialog proDlg(tr("Rebuilding notebook database..."),
+                               QString(),
+                               0,
+                               0,
+                               this);
+        proDlg.setWindowFlags(proDlg.windowFlags() & ~Qt::WindowCloseButtonHint);
+        proDlg.setWindowModality(Qt::WindowModal);
+        proDlg.setMinimumDuration(1000);
+        proDlg.setValue(0);
+
+        bool ret = m_currentNotebook->rebuildDatabase();
+
+        proDlg.cancel();
+
+        if (ret) {
+            MessageBoxHelper::notify(MessageBoxHelper::Type::Information,
+                                     tr("Notebook database has been rebuilt."));
+        } else {
+            MessageBoxHelper::notify(MessageBoxHelper::Type::Warning,
+                                     tr("Failed to rebuild notebook database."));
+        }
+    }
+}

+ 2 - 0
src/widgets/notebookexplorer.h

@@ -80,6 +80,8 @@ namespace vnotex
 
         void recoverSession();
 
+        void rebuildDatabase();
+
         NotebookSelector *m_selector = nullptr;
 
         NotebookNodeExplorer *m_nodeExplorer = nullptr;

+ 10 - 9
src/widgets/notebooknodeexplorer.cpp

@@ -10,6 +10,7 @@
 #include <notebook/notebook.h>
 #include <notebook/node.h>
 #include <notebook/externalnode.h>
+#include <notebook/nodeparameters.h>
 #include <core/exception.h>
 #include "messageboxhelper.h"
 #include "vnotex.h"
@@ -146,10 +147,10 @@ Node *NotebookNodeExplorer::NodeData::getNode() const
     return m_node;
 }
 
-ExternalNode *NotebookNodeExplorer::NodeData::getExternalNode() const
+const QSharedPointer<ExternalNode> &NotebookNodeExplorer::NodeData::getExternalNode() const
 {
     Q_ASSERT(isExternalNode());
-    return m_externalNode.data();
+    return m_externalNode;
 }
 
 void NotebookNodeExplorer::NodeData::clear()
@@ -258,7 +259,7 @@ void NotebookNodeExplorer::setupMasterExplorer(QWidget *p_parent)
                     if (data.isNode()) {
                         createContextMenuOnNode(menu.data(), data.getNode());
                     } else if (data.isExternalNode()) {
-                        createContextMenuOnExternalNode(menu.data(), data.getExternalNode());
+                        createContextMenuOnExternalNode(menu.data(), data.getExternalNode().data());
                     }
                 }
 
@@ -1023,7 +1024,7 @@ QAction *NotebookNodeExplorer::createAction(Action p_act, QObject *p_parent)
                             locationPath = PathUtils::parentDirPath(locationPath);
                         }
                     } else if (data.isExternalNode()) {
-                        auto externalNode = data.getExternalNode();
+                        const auto &externalNode = data.getExternalNode();
                         locationPath = externalNode->fetchAbsolutePath();
                         if (!externalNode->isFolder()) {
                             locationPath = PathUtils::parentDirPath(locationPath);
@@ -1243,9 +1244,9 @@ void NotebookNodeExplorer::copySelectedNodes(bool p_move)
     VNoteX::getInst().showStatusMessageShort(tr("Copied %n item(s)", "", static_cast<int>(nrItems)));
 }
 
-QPair<QVector<Node *>, QVector<ExternalNode *>> NotebookNodeExplorer::getSelectedNodes() const
+QPair<QVector<Node *>, QVector<QSharedPointer<ExternalNode>>> NotebookNodeExplorer::getSelectedNodes() const
 {
-    QPair<QVector<Node *>, QVector<ExternalNode *>> nodes;
+    QPair<QVector<Node *>, QVector<QSharedPointer<ExternalNode>>> nodes;
 
     auto items = m_masterExplorer->selectedItems();
     for (auto &item : items) {
@@ -1871,7 +1872,7 @@ QStringList NotebookNodeExplorer::getSelectedNodesPath() const
     return files;
 }
 
-QSharedPointer<Node> NotebookNodeExplorer::importToIndex(const ExternalNode *p_node)
+QSharedPointer<Node> NotebookNodeExplorer::importToIndex(QSharedPointer<ExternalNode> p_node)
 {
     auto node = m_notebook->addAsNode(p_node->getNode(),
                                       p_node->isFolder() ? Node::Flag::Container : Node::Flag::Content,
@@ -1884,12 +1885,12 @@ QSharedPointer<Node> NotebookNodeExplorer::importToIndex(const ExternalNode *p_n
     return node;
 }
 
-void NotebookNodeExplorer::importToIndex(const QVector<ExternalNode *> &p_nodes)
+void NotebookNodeExplorer::importToIndex(const QVector<QSharedPointer<ExternalNode>> &p_nodes)
 {
     QSet<Node *> nodesToUpdate;
     Node *currentNode = nullptr;
 
-    for (auto externalNode : p_nodes) {
+    for (const auto &externalNode : p_nodes) {
         auto node = m_notebook->addAsNode(externalNode->getNode(),
                                           externalNode->isFolder() ? Node::Flag::Container : Node::Flag::Content,
                                           externalNode->getName(),

+ 5 - 4
src/widgets/notebooknodeexplorer.h

@@ -58,7 +58,8 @@ namespace vnotex
 
             Node *getNode() const;
 
-            ExternalNode *getExternalNode() const;
+            // Return shared ptr to avoid wild pointer after destruction of item.
+            const QSharedPointer<ExternalNode> &getExternalNode() const;
 
             void clear();
 
@@ -217,7 +218,7 @@ namespace vnotex
 
         void pasteNodesFromClipboard();
 
-        QPair<QVector<Node *>, QVector<ExternalNode *>> getSelectedNodes() const;
+        QPair<QVector<Node *>, QVector<QSharedPointer<ExternalNode>>> getSelectedNodes() const;
 
         void removeSelectedNodes(bool p_skipRecycleBin);
 
@@ -258,9 +259,9 @@ namespace vnotex
 
         void openSelectedNodes();
 
-        QSharedPointer<Node> importToIndex(const ExternalNode *p_node);
+        QSharedPointer<Node> importToIndex(QSharedPointer<ExternalNode> p_node);
 
-        void importToIndex(const QVector<ExternalNode *> &p_nodes);
+        void importToIndex(const QVector<QSharedPointer<ExternalNode>> &p_nodes);
 
         // Check whether @p_node is a valid node. Will notify user.
         // Return true if it is invalid.

+ 8 - 1
src/widgets/searchpanel.cpp

@@ -79,7 +79,6 @@ void SearchPanel::setupUI()
     m_keywordComboBox->setLineEdit(WidgetsFactory::createLineEdit(mainWidget));
     m_keywordComboBox->lineEdit()->setProperty(PropertyDefs::c_embeddedLineEdit, true);
     m_keywordComboBox->completer()->setCaseSensitivity(Qt::CaseSensitive);
-    setFocusProxy(m_keywordComboBox);
     connect(m_keywordComboBox->lineEdit(), &QLineEdit::returnPressed,
             this, [this]() {
                 m_searchBtn->animateClick();
@@ -576,3 +575,11 @@ void SearchPanel::handleLocationActivated(const Location &p_location)
     paras->m_searchToken = m_searchTokenOfSession;
     emit VNoteX::getInst().openFileRequested(p_location.m_path, paras);
 }
+
+void SearchPanel::focusInEvent(QFocusEvent *p_event)
+{
+    QFrame::focusInEvent(p_event);
+
+    WidgetUtils::selectBaseName(m_keywordComboBox->lineEdit());
+    m_keywordComboBox->setFocus();
+}

+ 3 - 0
src/widgets/searchpanel.h

@@ -50,6 +50,9 @@ namespace vnotex
     public:
         SearchPanel(const QSharedPointer<ISearchInfoProvider> &p_provider, QWidget *p_parent = nullptr);
 
+    protected:
+        void focusInEvent(QFocusEvent *p_event) Q_DECL_OVERRIDE;
+
     private slots:
         void startSearch();
 

+ 3 - 0
src/widgets/textviewwindow.cpp

@@ -39,6 +39,9 @@ void TextViewWindow::setupUI()
                                   this);
         setCentralWidget(m_editor);
 
+        connect(m_editor, &TextEditor::applySnippetRequested,
+                this, QOverload<>::of(&TextViewWindow::applySnippet));
+
         updateEditorFromConfig();
     }
 

+ 3 - 8
src/widgets/viewsplit.cpp

@@ -437,8 +437,6 @@ void ViewSplit::updateMenu(QMenu *p_menu)
 
     p_menu->clear();
 
-    const auto &themeMgr = VNoteX::getInst().getThemeMgr();
-
     // Workspaces.
     {
         p_menu->addSection(tr("Workspaces"));
@@ -488,18 +486,15 @@ void ViewSplit::updateMenu(QMenu *p_menu)
 
     // Splits.
     {
+        // Do not add icon here since it will consume too much space.
         p_menu->addSection(tr("Split"));
-        auto icon = themeMgr.getIconFile(QStringLiteral("vertical_split.svg"));
-        auto act = p_menu->addAction(IconUtils::fetchIconWithDisabledState(icon),
-                                     tr("Vertical Split"),
+        auto act = p_menu->addAction(tr("Vertical Split"),
                                      [this]() {
                                          emit verticalSplitRequested(this);
                                      });
         WidgetUtils::addActionShortcutText(act, coreConfig.getShortcut(CoreConfig::VerticalSplit));
 
-        icon = themeMgr.getIconFile(QStringLiteral("horizontal_split.svg"));
-        act = p_menu->addAction(IconUtils::fetchIconWithDisabledState(icon),
-                                tr("Horizontal Split"),
+        act = p_menu->addAction(tr("Horizontal Split"),
                                 [this]() {
                                     emit horizontalSplitRequested(this);
                                 });

+ 69 - 0
tests/test_core/test_notebook/dummynode.cpp

@@ -0,0 +1,69 @@
+#include "dummynode.h"
+
+#include <utils/pathutils.h>
+#include <notebook/nodeparameters.h>
+
+using namespace tests;
+
+using namespace vnotex;
+
+DummyNode::DummyNode(Flags p_flags, ID p_id, const QString &p_name, Notebook *p_notebook, Node *p_parent)
+    : Node(p_flags,
+           p_name,
+           NodeParameters(p_id),
+           p_notebook,
+           p_parent)
+{
+}
+
+QString DummyNode::fetchAbsolutePath() const
+{
+    return PathUtils::concatenateFilePath("/", fetchPath());
+}
+
+QSharedPointer<File> DummyNode::getContentFile()
+{
+    return nullptr;
+}
+
+QStringList DummyNode::addAttachment(const QString &p_destFolderPath, const QStringList &p_files)
+{
+    Q_UNUSED(p_destFolderPath);
+    Q_UNUSED(p_files);
+    return QStringList();
+}
+
+QString DummyNode::newAttachmentFile(const QString &p_destFolderPath, const QString &p_name)
+{
+    Q_UNUSED(p_destFolderPath);
+    Q_UNUSED(p_name);
+    return QString();
+}
+
+QString DummyNode::newAttachmentFolder(const QString &p_destFolderPath, const QString &p_name)
+{
+    Q_UNUSED(p_destFolderPath);
+    Q_UNUSED(p_name);
+    return QString();
+}
+
+QString DummyNode::renameAttachment(const QString &p_path, const QString &p_name)
+{
+    Q_UNUSED(p_path);
+    Q_UNUSED(p_name);
+    return QString();
+}
+
+void DummyNode::removeAttachment(const QStringList &p_paths)
+{
+    Q_UNUSED(p_paths);
+}
+
+void DummyNode::load()
+{
+    m_loaded = true;
+}
+
+void DummyNode::save()
+{
+}

+ 33 - 0
tests/test_core/test_notebook/dummynode.h

@@ -0,0 +1,33 @@
+#ifndef DUMMYNODE_H
+#define DUMMYNODE_H
+
+#include <notebook/node.h>
+
+namespace tests
+{
+    class DummyNode : public vnotex::Node
+    {
+    public:
+        DummyNode(Flags p_flags, vnotex::ID p_id, const QString &p_name, vnotex::Notebook *p_notebook, Node *p_parent);
+
+        QString fetchAbsolutePath() const Q_DECL_OVERRIDE;
+
+        QSharedPointer<vnotex::File> getContentFile() 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;
+
+        void load() Q_DECL_OVERRIDE;
+
+        void save() Q_DECL_OVERRIDE;
+    };
+}
+
+#endif // DUMMYNODE_H

+ 59 - 0
tests/test_core/test_notebook/dummynotebook.cpp

@@ -0,0 +1,59 @@
+#include "dummynotebook.h"
+
+#include <notebook/node.h>
+
+using namespace tests;
+
+using namespace vnotex;
+
+DummyNotebook::DummyNotebook(const QString &p_name, QObject *p_parent)
+    : Notebook(p_name, p_parent)
+{
+}
+
+void DummyNotebook::updateNotebookConfig()
+{
+}
+
+void DummyNotebook::removeNotebookConfig()
+{
+}
+
+void DummyNotebook::remove()
+{
+}
+
+const QVector<vnotex::HistoryItem> &DummyNotebook::getHistory() const
+{
+    return m_history;
+}
+
+void DummyNotebook::addHistory(const vnotex::HistoryItem &p_item)
+{
+    Q_UNUSED(p_item);
+}
+
+void DummyNotebook::clearHistory()
+{
+}
+
+void DummyNotebook::initializeInternal()
+{
+}
+
+const QJsonObject &DummyNotebook::getExtraConfigs() const
+{
+    return m_extraConfigs;
+}
+
+void DummyNotebook::setExtraConfig(const QString &p_key, const QJsonObject &p_obj)
+{
+    Q_UNUSED(p_key);
+    Q_UNUSED(p_obj);
+}
+
+QSharedPointer<vnotex::Node> DummyNotebook::loadNodeByPath(const QString &p_path)
+{
+    Q_UNUSED(p_path);
+    return nullptr;
+}

+ 38 - 0
tests/test_core/test_notebook/dummynotebook.h

@@ -0,0 +1,38 @@
+#ifndef DUMMYNOTEBOOK_H
+#define DUMMYNOTEBOOK_H
+
+#include <notebook/notebook.h>
+
+namespace tests
+{
+    class DummyNotebook : public vnotex::Notebook
+    {
+        Q_OBJECT
+    public:
+        DummyNotebook(const QString &p_name, QObject *p_parent = nullptr);
+
+        void updateNotebookConfig() Q_DECL_OVERRIDE;
+
+        void removeNotebookConfig() Q_DECL_OVERRIDE;
+
+        void remove() Q_DECL_OVERRIDE;
+
+        const QVector<vnotex::HistoryItem> &getHistory() const Q_DECL_OVERRIDE;
+        void addHistory(const vnotex::HistoryItem &p_item) Q_DECL_OVERRIDE;
+        void clearHistory() Q_DECL_OVERRIDE;
+
+        const QJsonObject &getExtraConfigs() const Q_DECL_OVERRIDE;
+        void setExtraConfig(const QString &p_key, const QJsonObject &p_obj) Q_DECL_OVERRIDE;
+
+        QSharedPointer<vnotex::Node> loadNodeByPath(const QString &p_path) Q_DECL_OVERRIDE;
+
+    protected:
+        void initializeInternal() Q_DECL_OVERRIDE;
+
+        QVector<vnotex::HistoryItem> m_history;
+
+        QJsonObject m_extraConfigs;
+    };
+}
+
+#endif // DUMMYNOTEBOOK_H

+ 5 - 88
tests/test_core/test_notebook/test_notebook.cpp

@@ -16,6 +16,8 @@
 #include <notebook/notebookparameters.h>
 #include <utils/pathutils.h>
 
+#include "testnotebookdatabase.h"
+
 using namespace tests;
 
 using namespace vnotex;
@@ -23,97 +25,12 @@ using namespace vnotex;
 TestNotebook::TestNotebook(QObject *p_parent)
     : QObject(p_parent)
 {
-    m_testDir.reset(new QTemporaryDir);
-    Q_ASSERT(m_testDir->isValid());
-}
-
-void TestNotebook::testVersionControllerServer()
-{
-    Q_ASSERT(!m_vcServer);
-
-    m_vcServer.reset(new NameBasedServer<IVersionControllerFactory>);
-
-    // Dummy Version Controller.
-    auto dummyFactory = QSharedPointer<DummyVersionControllerFactory>::create();
-    m_vcServer->registerItem(dummyFactory->getName(), dummyFactory);
-
-    auto factory = m_vcServer->getItem(dummyFactory->getName());
-    auto dummyVC = factory->createVersionController();
-    QCOMPARE(dummyVC->getName(), dummyFactory->getName());
-}
-
-void TestNotebook::testNotebookConfigMgrServer()
-{
-    Q_ASSERT(!m_ncmServer);
-
-    m_ncmServer.reset(new NameBasedServer<INotebookConfigMgrFactory>);
-
-    // VX Notebook Config Manager.
-    auto vxFactory = QSharedPointer<VXNotebookConfigMgrFactory>::create();
-    m_ncmServer->registerItem(vxFactory->getName(), vxFactory);
-
-    auto factory = m_ncmServer->getItem(vxFactory->getName());
-    auto vxConfigMgr = factory->createNotebookConfigMgr(nullptr);
-    QCOMPARE(vxConfigMgr->getName(), vxFactory->getName());
-}
-
-void TestNotebook::testNotebookBackendServer()
-{
-    Q_ASSERT(!m_backendServer);
-
-    m_backendServer.reset(new NameBasedServer<INotebookBackendFactory>);
-
-    // Local Notebook Backend.
-    auto localFactory = QSharedPointer<LocalNotebookBackendFactory>::create();
-    m_backendServer->registerItem(localFactory->getName(), localFactory);
-
-    auto factory = m_backendServer->getItem(localFactory->getName());
-    auto localBackend = factory->createNotebookBackend("");
-    QCOMPARE(localBackend->getName(), localFactory->getName());
-}
-
-void TestNotebook::testNotebookServer()
-{
-    Q_ASSERT(!m_nbServer);
-
-    m_nbServer.reset(new NameBasedServer<INotebookFactory>);
-
-    // Bundle Notebook.
-    auto bundleFacotry = QSharedPointer<BundleNotebookFactory>::create();
-    m_nbServer->registerItem(bundleFacotry->getName(), bundleFacotry);
-
-    auto factory = m_nbServer->getItem(bundleFacotry->getName());
-    QVERIFY(factory == bundleFacotry);
-}
-
-void TestNotebook::testBundleNotebookFactoryNewNotebook()
-{
-    auto nbFactory = m_nbServer->getItem("bundle.vnotex");
-
-    NotebookParameters para;
-    para.m_name = "test_notebook";
-    para.m_description = "notebook description";
-    para.m_rootFolderPath = getTestFolderPath();
-    para.m_notebookBackend = m_backendServer->getItem("local.vnotex")
-                                            ->createNotebookBackend(para.m_rootFolderPath);
-    para.m_versionController = m_vcServer->getItem("dummy.vnotex")->createVersionController();
-    para.m_notebookConfigMgr = m_ncmServer->getItem("vx.vnotex")->createNotebookConfigMgr(para.m_notebookBackend);
-
-    auto notebook = nbFactory->newNotebook(para);
-
-    // Verify the notebook is created.
-    QVERIFY(QDir(para.m_rootFolderPath).exists());
-    auto configMgr = dynamic_cast<BundleNotebookConfigMgr *>(para.m_notebookConfigMgr.data());
-    const auto notebookConfigFolder = PathUtils::concatenateFilePath(para.m_rootFolderPath,
-                                                                     configMgr->getConfigFolderName());
-    const auto notebookConfigPath = PathUtils::concatenateFilePath(notebookConfigFolder,
-                                                                   configMgr->getConfigName());
-    QVERIFY(QFileInfo::exists(notebookConfigPath));
 }
 
-QString TestNotebook::getTestFolderPath() const
+void TestNotebook::testNotebookDatabase()
 {
-    return m_testDir->path();
+    TestNotebookDatabase test;
+    test.test();
 }
 
 QTEST_MAIN(tests::TestNotebook)

+ 1 - 19
tests/test_core/test_notebook/test_notebook.h

@@ -26,25 +26,7 @@ namespace tests
 
     private slots:
         // Define test cases here per slot.
-        void testVersionControllerServer();
-
-        void testNotebookConfigMgrServer();
-
-        void testNotebookBackendServer();
-
-        void testNotebookServer();
-
-        void testBundleNotebookFactoryNewNotebook();
-
-    private:
-        QString getTestFolderPath() const;
-
-        QSharedPointer<QTemporaryDir> m_testDir;
-
-        QSharedPointer<vnotex::NameBasedServer<vnotex::IVersionControllerFactory>> m_vcServer;
-        QSharedPointer<vnotex::NameBasedServer<vnotex::INotebookConfigMgrFactory>> m_ncmServer;
-        QSharedPointer<vnotex::NameBasedServer<vnotex::INotebookBackendFactory>> m_backendServer;
-        QSharedPointer<vnotex::NameBasedServer<vnotex::INotebookFactory>> m_nbServer;
+        void testNotebookDatabase();
     };
 } // ns tests
 

+ 10 - 2
tests/test_core/test_notebook/test_notebook.pro

@@ -1,5 +1,7 @@
 include($$PWD/../../common.pri)
 
+QT += sql
+
 TARGET = test_notebook
 TEMPLATE = app
 
@@ -23,7 +25,13 @@ include($$SRC_FOLDER/snippet/snippet.pri)
 include($$SRC_FOLDER/imagehost/imagehost.pri)
 
 SOURCES += \
-    test_notebook.cpp
+    dummynode.cpp \
+    dummynotebook.cpp \
+    test_notebook.cpp \
+    testnotebookdatabase.cpp
 
 HEADERS += \
-    test_notebook.h
+    dummynode.h \
+    dummynotebook.h \
+    test_notebook.h \
+    testnotebookdatabase.h

+ 148 - 0
tests/test_core/test_notebook/testnotebookdatabase.cpp

@@ -0,0 +1,148 @@
+#include "testnotebookdatabase.h"
+
+#include <QtTest>
+
+#include "dummynode.h"
+#include "dummynotebook.h"
+
+using namespace tests;
+
+using namespace vnotex;
+
+TestNotebookDatabase::TestNotebookDatabase()
+{
+    QVERIFY(m_testDir.isValid());
+
+    m_notebook.reset(new DummyNotebook("test_notebook"));
+
+    m_dbAccess.reset(new NotebookDatabaseAccess(m_notebook.data(), m_testDir.filePath("test.db")));
+
+    m_dbAccess->initialize(0);
+    QVERIFY(m_dbAccess->isFresh());
+    QVERIFY(m_dbAccess->isValid());
+}
+
+TestNotebookDatabase::~TestNotebookDatabase()
+{
+    m_dbAccess->close();
+    m_dbAccess.reset();
+}
+
+void TestNotebookDatabase::test()
+{
+    testNode();
+}
+
+void TestNotebookDatabase::testNode()
+{
+    // Invlaid node.
+    {
+        auto nodeRec = m_dbAccess->queryNode(1);
+        QVERIFY(nodeRec == nullptr);
+    }
+
+    // Root node.
+    QScopedPointer<DummyNode> rootNode(new DummyNode(Node::Flag::Container, 0, "", m_notebook.data(), nullptr));
+    addAndQueryNode(rootNode.data(), true);
+
+    // Node 1.
+    QScopedPointer<DummyNode> node1(new DummyNode(Node::Flag::Content, 10, "a", m_notebook.data(), rootNode.data()));
+    addAndQueryNode(node1.data(), true);
+
+    // Node 2, respect id.
+    QScopedPointer<DummyNode> node2(new DummyNode(Node::Flag::Content, 50, "b", m_notebook.data(), rootNode.data()));
+    addAndQueryNode(node2.data(), false);
+    QCOMPARE(node2->getId(), 50);
+
+    // Node 3, respect id with invalid id.
+    QScopedPointer<DummyNode> node3(new DummyNode(Node::Flag::Container, 0, "c", m_notebook.data(), rootNode.data()));
+    addAndQueryNode(node3.data(), false);
+    QVERIFY(node3->getId() != 0);
+
+    // Node 4, deep level.
+    QScopedPointer<DummyNode> node4(new DummyNode(Node::Flag::Content, 11, "ca", m_notebook.data(), node3.data()));
+    addAndQueryNode(node4.data(), false);
+
+    // Node 5, deep level.
+    QScopedPointer<DummyNode> node5(new DummyNode(Node::Flag::Content, 60, "caa", m_notebook.data(), node4.data()));
+    addAndQueryNode(node5.data(), false);
+
+    // Node 6, deep level.
+    QScopedPointer<DummyNode> node6(new DummyNode(Node::Flag::Content, 5, "cab", m_notebook.data(), node4.data()));
+    addAndQueryNode(node6.data(), false);
+
+    // queryNodePath().
+    {
+        testQueryNodePath(rootNode.data());
+        testQueryNodePath(node1.data());
+        testQueryNodePath(node2.data());
+        testQueryNodePath(node3.data());
+        testQueryNodePath(node4.data());
+        testQueryNodePath(node5.data());
+        testQueryNodePath(node6.data());
+    }
+
+    // updateNode().
+    {
+        node6->setParent(node5.data());
+        node6->setName("caaa");
+        bool ret = m_dbAccess->updateNode(node6.data());
+        QVERIFY(ret);
+        queryAndVerifyNode(node6.data());
+    }
+
+    // removeNode().
+    {
+        QVERIFY(m_dbAccess->existsNode(node6.data()));
+        bool ret = m_dbAccess->removeNode(node6->getId());
+        QVERIFY(ret);
+        QVERIFY(!m_dbAccess->existsNode(node6.data()));
+
+        // DELETE CASCADE.
+        QVERIFY(m_dbAccess->existsNode(node3.data()));
+        QVERIFY(m_dbAccess->existsNode(node4.data()));
+        QVERIFY(m_dbAccess->existsNode(node5.data()));
+        ret = m_dbAccess->removeNode(node3->getId());
+        QVERIFY(ret);
+        QVERIFY(!m_dbAccess->existsNode(node3.data()));
+        QVERIFY(!m_dbAccess->existsNode(node4.data()));
+        QVERIFY(!m_dbAccess->existsNode(node5.data()));
+
+        // Add back nodes.
+        addAndQueryNode(node3.data(), false);
+        addAndQueryNode(node4.data(), false);
+        addAndQueryNode(node5.data(), false);
+        addAndQueryNode(node6.data(), false);
+    }
+}
+
+void TestNotebookDatabase::addAndQueryNode(Node *p_node, bool p_ignoreId)
+{
+    bool ret = m_dbAccess->addNode(p_node, p_ignoreId);
+    QVERIFY(ret);
+    QVERIFY(p_node->getId() != NotebookDatabaseAccess::InvalidId);
+    queryAndVerifyNode(p_node);
+    QVERIFY(m_dbAccess->existsNode(p_node));
+}
+
+void TestNotebookDatabase::queryAndVerifyNode(const vnotex::Node *p_node)
+{
+    auto nodeRec = m_dbAccess->queryNode(p_node->getId());
+    QVERIFY(nodeRec);
+    QCOMPARE(nodeRec->m_id, p_node->getId());
+    QCOMPARE(nodeRec->m_name, p_node->getName());
+    QCOMPARE(nodeRec->m_signature, p_node->getSignature());
+    QCOMPARE(nodeRec->m_parentId, p_node->getParent() ? p_node->getParent()->getId() : NotebookDatabaseAccess::InvalidId);
+}
+
+void TestNotebookDatabase::testQueryNodePath(const vnotex::Node *p_node)
+{
+    auto nodePath = m_dbAccess->queryNodePath(p_node->getId());
+    auto node = p_node;
+    for (int i = nodePath.size() - 1; i >= 0; --i) {
+        QVERIFY(node);
+        QCOMPARE(nodePath[i], node->getName());
+        node = node->getParent();
+    }
+    QVERIFY(m_dbAccess->checkNodePath(p_node, nodePath));
+}

+ 38 - 0
tests/test_core/test_notebook/testnotebookdatabase.h

@@ -0,0 +1,38 @@
+#ifndef TESTNOTEBOOKDATABASE_H
+#define TESTNOTEBOOKDATABASE_H
+
+#include <QScopedPointer>
+#include <QTemporaryDir>
+
+#include <notebook/notebookdatabaseaccess.h>
+
+namespace tests
+{
+    class TestNotebookDatabase
+    {
+    public:
+        TestNotebookDatabase();
+
+        ~TestNotebookDatabase();
+
+        void test();
+
+    private:
+        void testNode();
+
+    private:
+        void addAndQueryNode(vnotex::Node *p_node, bool p_ignoreId);
+
+        void testQueryNodePath(const vnotex::Node *p_node);
+
+        void queryAndVerifyNode(const vnotex::Node *p_node);
+
+        QTemporaryDir m_testDir;
+
+        QScopedPointer<vnotex::Notebook> m_notebook;
+
+        QScopedPointer<vnotex::NotebookDatabaseAccess> m_dbAccess;
+    };
+}
+
+#endif // TESTNOTEBOOKDATABASE_H