From 6689e8c84cee6cd7632ecbc17bc45112dd7e9f3a Mon Sep 17 00:00:00 2001 From: Le Tan Date: Fri, 10 Sep 2021 20:52:55 +0800 Subject: [PATCH] add sqlite database --- libs/vtextedit | 2 +- src/core/configmgr.cpp | 30 +- src/core/configmgr.h | 17 +- src/core/notebook/bundlenotebook.cpp | 96 +++- src/core/notebook/bundlenotebook.h | 23 +- src/core/notebook/node.cpp | 67 ++- src/core/notebook/node.h | 38 +- src/core/notebook/nodeparameters.cpp | 8 + src/core/notebook/nodeparameters.h | 34 ++ src/core/notebook/notebook.cpp | 27 +- src/core/notebook/notebook.h | 18 +- src/core/notebook/notebook.pri | 4 + src/core/notebook/notebookdatabaseaccess.cpp | 381 ++++++++++++++++ src/core/notebook/notebookdatabaseaccess.h | 97 ++++ src/core/notebook/vxnode.cpp | 14 +- src/core/notebook/vxnode.h | 10 +- .../bundlenotebookconfigmgr.cpp | 10 + .../bundlenotebookconfigmgr.h | 6 +- .../notebookconfigmgr/inotebookconfigmgr.cpp | 6 - .../notebookconfigmgr/inotebookconfigmgr.h | 7 +- src/core/notebookconfigmgr/notebookconfig.cpp | 19 +- src/core/notebookconfigmgr/notebookconfig.h | 10 +- .../notebookconfigmgr/notebookconfigmgr.pri | 2 + src/core/notebookconfigmgr/vxnodeconfig.cpp | 173 ++++++++ src/core/notebookconfigmgr/vxnodeconfig.h | 107 +++++ .../notebookconfigmgr/vxnotebookconfigmgr.cpp | 420 +++++++++--------- .../notebookconfigmgr/vxnotebookconfigmgr.h | 124 ++---- src/core/notebookmgr.cpp | 1 + src/core/vnotex.cpp | 1 + src/data/core/core.qrc | 2 - src/data/core/icons/horizontal_split.svg | 15 - src/data/core/icons/vertical_split.svg | 15 - src/data/extra/docs/en/features_tips.txt | 7 + src/data/extra/docs/zh_CN/features_tips.txt | 7 + src/data/extra/extra.qrc | 2 + src/export/webviewexporter.cpp | 26 +- src/src.pro | 3 +- src/utils/fileutils.cpp | 5 +- src/utils/pathutils.cpp | 2 +- src/widgets/dialogs/exportdialog.cpp | 2 + src/widgets/dialogs/importfolderutils.cpp | 1 + src/widgets/dialogs/newfolderdialog.cpp | 2 +- src/widgets/editors/markdowneditor.cpp | 14 +- src/widgets/editors/markdowneditor.h | 2 + src/widgets/editors/texteditor.cpp | 28 +- src/widgets/editors/texteditor.h | 6 + src/widgets/mainwindow.cpp | 30 +- src/widgets/markdownviewwindow.cpp | 3 + src/widgets/notebookexplorer.cpp | 39 +- src/widgets/notebookexplorer.h | 2 + src/widgets/notebooknodeexplorer.cpp | 19 +- src/widgets/notebooknodeexplorer.h | 9 +- src/widgets/searchpanel.cpp | 9 +- src/widgets/searchpanel.h | 3 + src/widgets/textviewwindow.cpp | 3 + src/widgets/viewsplit.cpp | 11 +- tests/test_core/test_notebook/dummynode.cpp | 69 +++ tests/test_core/test_notebook/dummynode.h | 33 ++ .../test_core/test_notebook/dummynotebook.cpp | 59 +++ tests/test_core/test_notebook/dummynotebook.h | 38 ++ .../test_core/test_notebook/test_notebook.cpp | 93 +--- tests/test_core/test_notebook/test_notebook.h | 20 +- .../test_core/test_notebook/test_notebook.pro | 12 +- .../test_notebook/testnotebookdatabase.cpp | 148 ++++++ .../test_notebook/testnotebookdatabase.h | 38 ++ 65 files changed, 1935 insertions(+), 594 deletions(-) create mode 100644 src/core/notebook/nodeparameters.cpp create mode 100644 src/core/notebook/nodeparameters.h create mode 100644 src/core/notebook/notebookdatabaseaccess.cpp create mode 100644 src/core/notebook/notebookdatabaseaccess.h create mode 100644 src/core/notebookconfigmgr/vxnodeconfig.cpp create mode 100644 src/core/notebookconfigmgr/vxnodeconfig.h delete mode 100644 src/data/core/icons/horizontal_split.svg delete mode 100644 src/data/core/icons/vertical_split.svg create mode 100644 src/data/extra/docs/en/features_tips.txt create mode 100644 src/data/extra/docs/zh_CN/features_tips.txt create mode 100644 tests/test_core/test_notebook/dummynode.cpp create mode 100644 tests/test_core/test_notebook/dummynode.h create mode 100644 tests/test_core/test_notebook/dummynotebook.cpp create mode 100644 tests/test_core/test_notebook/dummynotebook.h create mode 100644 tests/test_core/test_notebook/testnotebookdatabase.cpp create mode 100644 tests/test_core/test_notebook/testnotebookdatabase.h diff --git a/libs/vtextedit b/libs/vtextedit index 0733259f..c9192972 160000 --- a/libs/vtextedit +++ b/libs/vtextedit @@ -1 +1 @@ -Subproject commit 0733259fed01ecaa11678fac00fd67397ff7c39c +Subproject commit c91929729fdd7b47e067c721d6148c7c3e6f9ced diff --git a/src/core/configmgr.cpp b/src/core/configmgr.cpp index 5cca0ec8..b98b7b43 100644 --- a/src/core/configmgr.cpp +++ b/src/core/configmgr.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -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(); diff --git a/src/core/configmgr.h b/src/core/configmgr.h index 448a1833..5fa2fa6f 100644 --- a/src/core/configmgr.h +++ b/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 m_dirForUnitTest; + // Name of the core config file. static const QString c_configFileName; diff --git a/src/core/notebook/bundlenotebook.cpp b/src/core/notebook/bundlenotebook.cpp index 71b15445..88962bfd 100644 --- a/src/core/notebook/bundlenotebook.cpp +++ b/src/core/notebook/bundlenotebook.cpp @@ -1,40 +1,65 @@ #include "bundlenotebook.h" #include +#include #include #include #include #include +#include #include +#include "notebookdatabaseaccess.h" + using namespace vnotex; BundleNotebook::BundleNotebook(const NotebookParameters &p_paras, const QSharedPointer &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) { - m_nextNodeId = p_notebookConfig->m_nextNodeId; - m_history = p_notebookConfig->m_history; - m_extraConfigs = p_notebookConfig->m_extraConfigs; + setupDatabase(); +} + +BundleNotebook::~BundleNotebook() +{ + m_dbAccess->close(); } BundleNotebookConfigMgr *BundleNotebook::getBundleNotebookConfigMgr() const { - return dynamic_cast(getConfigMgr().data()); + return static_cast(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; +} diff --git a/src/core/notebook/bundlenotebook.h b/src/core/notebook/bundlenotebook.h index cd905b23..8a3fbb8e 100644 --- a/src/core/notebook/bundlenotebook.h +++ b/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 &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 m_history; QJsonObject m_extraConfigs; + + // Managed by QObject. + NotebookDatabaseAccess *m_dbAccess = nullptr; }; } // ns vnotex diff --git a/src/core/notebook/node.cpp b/src/core/notebook/node.cpp index 3afeb13c..e3b43220 100644 --- a/src/core/notebook/node.cpp +++ b/src/core/notebook/node.cpp @@ -7,30 +7,30 @@ #include #include #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> &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> Node::collectFiles() return files; } + +ID Node::generateSignature() +{ + return static_cast(QDateTime::currentDateTime().toSecsSinceEpoch() + (static_cast(qrand()) << 32)); +} + +void Node::checkSignature() +{ + if (m_signature == InvalidId) { + m_signature = generateSignature(); + } +} diff --git a/src/core/notebook/node.h b/src/core/notebook/node.h index 91f510bc..2474f701 100644 --- a/src/core/notebook/node.h +++ b/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 @@ -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 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> &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; diff --git a/src/core/notebook/nodeparameters.cpp b/src/core/notebook/nodeparameters.cpp new file mode 100644 index 00000000..3ed70689 --- /dev/null +++ b/src/core/notebook/nodeparameters.cpp @@ -0,0 +1,8 @@ +#include "nodeparameters.h" + +using namespace vnotex; + +NodeParameters::NodeParameters(ID p_id) + : m_id(p_id) +{ +} diff --git a/src/core/notebook/nodeparameters.h b/src/core/notebook/nodeparameters.h new file mode 100644 index 00000000..b36d5970 --- /dev/null +++ b/src/core/notebook/nodeparameters.h @@ -0,0 +1,34 @@ +#ifndef NODEPARAMETERS_H +#define NODEPARAMETERS_H + +#include +#include + +#include + +#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 diff --git a/src/core/notebook/notebook.cpp b/src/core/notebook/notebook.cpp index 18da9087..13672da4 100644 --- a/src/core/notebook/notebook.cpp +++ b/src/core/notebook/notebook.cpp @@ -7,7 +7,8 @@ #include #include #include -#include "exception.h" +#include +#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 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; +} diff --git a/src/core/notebook/notebook.h b/src/core/notebook/notebook.h index f02ac65b..fe1d139b 100644 --- a/src/core/notebook/notebook.h +++ b/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 getOrCreateRecycleBinDateNode(); + bool m_initialized = false; + // ID of this notebook. // Will be assigned uniquely once loaded. ID m_id; diff --git a/src/core/notebook/notebook.pri b/src/core/notebook/notebook.pri index 917a31fc..9618cf77 100644 --- a/src/core/notebook/notebook.pri +++ b/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 \ diff --git a/src/core/notebook/notebookdatabaseaccess.cpp b/src/core/notebook/notebookdatabaseaccess.cpp new file mode 100644 index 00000000..8605bb11 --- /dev/null +++ b/src/core/notebook/notebookdatabaseaccess.cpp @@ -0,0 +1,381 @@ +#include "notebookdatabaseaccess.h" + +#include +#include + +#include + +#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::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::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; +} diff --git a/src/core/notebook/notebookdatabaseaccess.h b/src/core/notebook/notebookdatabaseaccess.h new file mode 100644 index 00000000..771feb05 --- /dev/null +++ b/src/core/notebook/notebookdatabaseaccess.h @@ -0,0 +1,97 @@ +#ifndef NOTEBOOKDATABASEACCESS_H +#define NOTEBOOKDATABASEACCESS_H + +#include +#include +#include +#include + +#include + +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 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 m_obsoleteNodes; + }; +} + +#endif // NOTEBOOKDATABASEACCESS_H diff --git a/src/core/notebook/vxnode.cpp b/src/core/notebook/vxnode.cpp index c7516b53..1e4756d9 100644 --- a/src/core/notebook/vxnode.cpp +++ b/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) { diff --git a/src/core/notebook/vxnode.h b/src/core/notebook/vxnode.h index ddbcccb8..52be6237 100644 --- a/src/core/notebook/vxnode.h +++ b/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: }; } diff --git a/src/core/notebookconfigmgr/bundlenotebookconfigmgr.cpp b/src/core/notebookconfigmgr/bundlenotebookconfigmgr.cpp index 719e55b1..fd0f103a 100644 --- a/src/core/notebookconfigmgr/bundlenotebookconfigmgr.cpp +++ b/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(getNotebook()); @@ -94,3 +99,8 @@ bool BundleNotebookConfigMgr::isBuiltInFolder(const Node *p_node, const QString } return false; } + +int BundleNotebookConfigMgr::getCodeVersion() const +{ + return 2; +} diff --git a/src/core/notebookconfigmgr/bundlenotebookconfigmgr.h b/src/core/notebookconfigmgr/bundlenotebookconfigmgr.h index 79a387d4..4420e125 100644 --- a/src/core/notebookconfigmgr/bundlenotebookconfigmgr.h +++ b/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 readNotebookConfig(const QSharedPointer &p_backend); + static QString getDatabasePath(); - enum { RootNodeId = 1 }; + static QSharedPointer readNotebookConfig(const QSharedPointer &p_backend); protected: BundleNotebook *getBundleNotebook() const; diff --git a/src/core/notebookconfigmgr/inotebookconfigmgr.cpp b/src/core/notebookconfigmgr/inotebookconfigmgr.cpp index 3343ae04..48b2e7f9 100644 --- a/src/core/notebookconfigmgr/inotebookconfigmgr.cpp +++ b/src/core/notebookconfigmgr/inotebookconfigmgr.cpp @@ -20,12 +20,6 @@ const QSharedPointer &INotebookConfigMgr::getBackend() const return m_backend; } -QString INotebookConfigMgr::getCodeVersion() const -{ - const QString version("1"); - return version; -} - Notebook *INotebookConfigMgr::getNotebook() const { return m_notebook; diff --git a/src/core/notebookconfigmgr/inotebookconfigmgr.h b/src/core/notebookconfigmgr/inotebookconfigmgr.h index b6177372..843819d2 100644 --- a/src/core/notebookconfigmgr/inotebookconfigmgr.h +++ b/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 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 m_backend; diff --git a/src/core/notebookconfigmgr/notebookconfig.cpp b/src/core/notebookconfigmgr/notebookconfig.cpp index 3466afd2..7c565a5d 100644 --- a/src/core/notebookconfigmgr/notebookconfig.cpp +++ b/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::fromNotebookParameters(const QString &p_version, +QSharedPointer NotebookConfig::fromNotebookParameters(int p_version, const NotebookParameters &p_paras) { auto config = QSharedPointer::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::fromNotebook(const QString &p_version, +QSharedPointer NotebookConfig::fromNotebook(int p_version, const Notebook *p_notebook) { auto config = QSharedPointer::create(); @@ -113,7 +101,6 @@ QSharedPointer 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(); diff --git a/src/core/notebookconfigmgr/notebookconfig.h b/src/core/notebookconfigmgr/notebookconfig.h index 86ac65a0..e6c6be10 100644 --- a/src/core/notebookconfigmgr/notebookconfig.h +++ b/src/core/notebookconfigmgr/notebookconfig.h @@ -20,17 +20,17 @@ namespace vnotex public: virtual ~NotebookConfig() {} - static QSharedPointer fromNotebookParameters(const QString &p_version, + static QSharedPointer fromNotebookParameters(int p_version, const NotebookParameters &p_paras); - static QSharedPointer fromNotebook(const QString &p_version, + static QSharedPointer 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 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 diff --git a/src/core/notebookconfigmgr/notebookconfigmgr.pri b/src/core/notebookconfigmgr/notebookconfigmgr.pri index 7cbd9fb1..de570d37 100644 --- a/src/core/notebookconfigmgr/notebookconfigmgr.pri +++ b/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 \ diff --git a/src/core/notebookconfigmgr/vxnodeconfig.cpp b/src/core/notebookconfigmgr/vxnodeconfig.cpp new file mode 100644 index 00000000..c5f148dd --- /dev/null +++ b/src/core/notebookconfigmgr/vxnodeconfig.cpp @@ -0,0 +1,173 @@ +#include "vxnodeconfig.h" + +#include +#include + +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; +} diff --git a/src/core/notebookconfigmgr/vxnodeconfig.h b/src/core/notebookconfigmgr/vxnodeconfig.h new file mode 100644 index 00000000..24066b40 --- /dev/null +++ b/src/core/notebookconfigmgr/vxnodeconfig.h @@ -0,0 +1,107 @@ +#ifndef VXNODECONFIG_H +#define VXNODECONFIG_H + +#include +#include +#include + +#include +#include +#include + +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 m_files; + + QVector 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 diff --git a/src/core/notebookconfigmgr/vxnotebookconfigmgr.cpp b/src/core/notebookconfigmgr/vxnotebookconfigmgr.cpp index 0d15c15d..0a3b8df3 100644 --- a/src/core/notebookconfigmgr/vxnotebookconfigmgr.cpp +++ b/src/core/notebookconfigmgr/vxnotebookconfigmgr.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -20,148 +21,11 @@ #include +#include "vxnodeconfig.h" + using namespace vnotex; -const QString VXNotebookConfigMgr::NodeConfig::c_version = "version"; - -const QString VXNotebookConfigMgr::NodeConfig::c_id = "id"; - -const QString VXNotebookConfigMgr::NodeConfig::c_createdTimeUtc = "created_time"; - -const QString VXNotebookConfigMgr::NodeConfig::c_files = "files"; - -const QString VXNotebookConfigMgr::NodeConfig::c_folders = "folders"; - -const QString VXNotebookConfigMgr::NodeConfig::c_name = "name"; - -const QString VXNotebookConfigMgr::NodeConfig::c_modifiedTimeUtc = "modified_time"; - -const QString VXNotebookConfigMgr::NodeConfig::c_attachmentFolder = "attachment_folder"; - -const QString VXNotebookConfigMgr::NodeConfig::c_tags = "tags"; - -QJsonObject VXNotebookConfigMgr::NodeFileConfig::toJson() const -{ - QJsonObject jobj; - - jobj[NodeConfig::c_name] = m_name; - jobj[NodeConfig::c_id] = QString::number(m_id); - jobj[NodeConfig::c_createdTimeUtc] = Utils::dateTimeStringUniform(m_createdTimeUtc); - jobj[NodeConfig::c_modifiedTimeUtc] = Utils::dateTimeStringUniform(m_modifiedTimeUtc); - jobj[NodeConfig::c_attachmentFolder] = m_attachmentFolder; - jobj[NodeConfig::c_tags] = QJsonArray::fromStringList(m_tags); - - return jobj; -} - -void VXNotebookConfigMgr::NodeFileConfig::fromJson(const QJsonObject &p_jobj) -{ - m_name = p_jobj[NodeConfig::c_name].toString(); - - { - auto idStr = p_jobj[NodeConfig::c_id].toString(); - bool ok; - m_id = idStr.toULongLong(&ok); - if (!ok) { - m_id = Node::InvalidId; - } - } - - m_createdTimeUtc = Utils::dateTimeFromStringUniform(p_jobj[NodeConfig::c_createdTimeUtc].toString()); - m_modifiedTimeUtc = Utils::dateTimeFromStringUniform(p_jobj[NodeConfig::c_modifiedTimeUtc].toString()); - - m_attachmentFolder = p_jobj[NodeConfig::c_attachmentFolder].toString(); - - { - auto arr = p_jobj[NodeConfig::c_tags].toArray(); - for (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::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 &p_roo markNodeReadOnly(node.data()); } -QSharedPointer VXNotebookConfigMgr::readNodeConfig(const QString &p_path) const +QSharedPointer 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 VXNotebookConfigMgr::nodeConfigToNode(const NodeConfig &p_config, const QString &p_name, - Node *p_parent) const + Node *p_parent) { auto node = QSharedPointer::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> 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::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::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 VXNotebookConfigMgr::newNode(Node *p_parent, @@ -435,15 +302,13 @@ QSharedPointer VXNotebookConfigMgr::newFileNode(Node *p_parent, bool p_create, const NodeParameters &p_paras) { + ensureNodeInDatabase(p_parent); + auto notebook = getNotebook(); // Create file node. - auto node = QSharedPointer::create(Node::InvalidId, - p_name, - p_paras.m_createdTimeUtc, - p_paras.m_modifiedTimeUtc, - p_paras.m_tags, - p_paras.m_attachmentFolder, + auto node = QSharedPointer::create(p_name, + p_paras, notebook, p_parent); @@ -458,6 +323,8 @@ QSharedPointer VXNotebookConfigMgr::newFileNode(Node *p_parent, addChildNode(p_parent, node); writeNodeConfig(p_parent); + addNodeToDatabase(node.data()); + return node; } @@ -466,15 +333,13 @@ QSharedPointer VXNotebookConfigMgr::newFolderNode(Node *p_parent, bool p_create, const NodeParameters &p_paras) { + ensureNodeInDatabase(p_parent); + auto notebook = getNotebook(); // Create folder node. auto node = QSharedPointer::create(p_name, notebook, p_parent); - node->loadCompleteInfo(Node::InvalidId, - p_paras.m_createdTimeUtc, - p_paras.m_modifiedTimeUtc, - QStringList(), - QVector>()); + node->loadCompleteInfo(p_paras, QVector>()); // Make folder. if (p_create) { @@ -489,15 +354,19 @@ QSharedPointer VXNotebookConfigMgr::newFolderNode(Node *p_parent, addChildNode(p_parent, node); writeNodeConfig(p_parent); + addNodeToDatabase(node.data()); + return node; } -QSharedPointer VXNotebookConfigMgr::nodeToNodeConfig(const Node *p_node) const +QSharedPointer VXNotebookConfigMgr::nodeToNodeConfig(const Node *p_node) const { Q_ASSERT(p_node->isContainer()); + Q_ASSERT(p_node->isLoaded()); auto config = QSharedPointer::create(getCodeVersion(), p_node->getId(), + p_node->getSignature(), p_node->getCreatedTimeUtc(), p_node->getModifiedTimeUtc()); @@ -506,6 +375,7 @@ QSharedPointer 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::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 &p_child) const { if (p_child->isContainer()) { @@ -604,11 +479,20 @@ QSharedPointer VXNotebookConfigMgr::loadNodeByPath(const QSharedPointer VXNotebookConfigMgr::copyNodeAsChildOf(const QSharedPointer &p_src, Node *p_dest, bool p_move) +{ + return copyNodeAsChildOf(p_src, p_dest, p_move, true); +} + +QSharedPointer VXNotebookConfigMgr::copyNodeAsChildOf(const QSharedPointer &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 VXNotebookConfigMgr::copyNodeAsChildOf(const QSharedPointer QSharedPointer 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 VXNotebookConfigMgr::copyNodeAsChildOf(const QSharedPointer QSharedPointer VXNotebookConfigMgr::copyFileNodeAsChildOf(const QSharedPointer &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 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::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::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 VXNotebookConfigMgr::copyFileNodeAsChildOf(const QSharedPoi QSharedPointer VXNotebookConfigMgr::copyFolderNodeAsChildOf(const QSharedPointer &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 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::create(PathUtils::fileName(destFolderPath), notebook, p_dest); - destNode->loadCompleteInfo(id, - p_src->getCreatedTimeUtc(), - p_src->getModifiedTimeUtc(), - QStringList(), - QVector>()); - 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>()); + } + 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 &p_node, bool p_force, bool p_configOnly) +{ + removeNode(p_node, p_force, p_configOnly, true); +} + +void VXNotebookConfigMgr::removeNode(const QSharedPointer &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 &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 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::create(getNotebook()->getAndUpdateNextNodeId(), - name, - currentTime, - currentTime, - QStringList(), - QString(), + destNode = QSharedPointer::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 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::create(name, notebook, p_dest); - auto currentTime = QDateTime::currentDateTimeUtc(); - destNode->loadCompleteInfo(notebook->getAndUpdateNextNodeId(), - currentTime, - currentTime, - QStringList(), - QVector>()); + destNode->loadCompleteInfo(NodeParameters(), QVector>()); destNode->setExists(true); writeNodeConfig(destNode.data()); @@ -920,6 +864,8 @@ QSharedPointer 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(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; +} diff --git a/src/core/notebookconfigmgr/vxnotebookconfigmgr.h b/src/core/notebookconfigmgr/vxnotebookconfigmgr.h index 19928431..074a53ae 100644 --- a/src/core/notebookconfigmgr/vxnotebookconfigmgr.h +++ b/src/core/notebookconfigmgr/vxnotebookconfigmgr.h @@ -7,12 +7,21 @@ #include #include -#include "../global.h" +#include 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 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 m_files; - QVector 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 readNodeConfig(const QString &p_path) const; - void writeNodeConfig(const QString &p_path, const NodeConfig &p_config) const; + QSharedPointer 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 nodeConfigToNode(const NodeConfig &p_config, + QSharedPointer 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 nodeToNodeConfig(const Node *p_node) const; + QSharedPointer nodeToNodeConfig(const Node *p_node) const; QSharedPointer newFileNode(Node *p_parent, const QString &p_name, @@ -172,9 +116,20 @@ namespace vnotex void addChildNode(Node *p_parent, const QSharedPointer &p_child) const; - QSharedPointer copyFileNodeAsChildOf(const QSharedPointer &p_src, Node *p_dest, bool p_move); + QSharedPointer copyNodeAsChildOf(const QSharedPointer &p_src, + Node *p_dest, + bool p_move, + bool p_updateDatabase); - QSharedPointer copyFolderNodeAsChildOf(const QSharedPointer &p_src, Node *p_dest, bool p_move); + QSharedPointer copyFileNodeAsChildOf(const QSharedPointer &p_src, + Node *p_dest, + bool p_move, + bool p_updateDatabase); + + QSharedPointer copyFolderNodeAsChildOf(const QSharedPointer &p_src, + Node *p_dest, + bool p_move, + bool p_updateDatabase); QSharedPointer 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 &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; diff --git a/src/core/notebookmgr.cpp b/src/core/notebookmgr.cpp index 24b25948..d6e1243c 100644 --- a/src/core/notebookmgr.cpp +++ b/src/core/notebookmgr.cpp @@ -369,6 +369,7 @@ void NotebookMgr::setCurrentNotebookAfterUpdate() void NotebookMgr::addNotebook(const QSharedPointer &p_notebook) { + p_notebook->initialize(); m_notebooks.push_back(p_notebook); connect(p_notebook.data(), &Notebook::updated, this, [this, notebook = p_notebook.data()]() { diff --git a/src/core/vnotex.cpp b/src/core/vnotex.cpp index df6008e5..0259bdec 100644 --- a/src/core/vnotex.cpp +++ b/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(); } diff --git a/src/data/core/core.qrc b/src/data/core/core.qrc index c669818e..37dfa9aa 100644 --- a/src/data/core/core.qrc +++ b/src/data/core/core.qrc @@ -45,8 +45,6 @@ icons/attachment_full_editor.svg icons/split_menu.svg icons/split_window_list.svg - icons/horizontal_split.svg - icons/vertical_split.svg icons/type_heading_editor.svg icons/type_bold_editor.svg icons/type_italic_editor.svg diff --git a/src/data/core/icons/horizontal_split.svg b/src/data/core/icons/horizontal_split.svg deleted file mode 100644 index 18a163e5..00000000 --- a/src/data/core/icons/horizontal_split.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - diff --git a/src/data/core/icons/vertical_split.svg b/src/data/core/icons/vertical_split.svg deleted file mode 100644 index c7543b0f..00000000 --- a/src/data/core/icons/vertical_split.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - diff --git a/src/data/extra/docs/en/features_tips.txt b/src/data/extra/docs/en/features_tips.txt new file mode 100644 index 00000000..77a4cd2f --- /dev/null +++ b/src/data/extra/docs/en/features_tips.txt @@ -0,0 +1,7 @@ +

Some features not to be missed in VNote:

+

Markdown Editor

+
    +
  • Parse to Markdown and Paste on the context menu: parse rich format text to Markdown text and fetch images to local if necessary.
  • +
  • Rich Paste on the context menu: paste as image, attachment, or link.
  • +
  • Cross Copy on the context menu: copy selected text as rich format text.
  • +
diff --git a/src/data/extra/docs/zh_CN/features_tips.txt b/src/data/extra/docs/zh_CN/features_tips.txt new file mode 100644 index 00000000..e89041db --- /dev/null +++ b/src/data/extra/docs/zh_CN/features_tips.txt @@ -0,0 +1,7 @@ +

VNote 中一些不容错过的特性:

+

Markdown 编辑器

+
    +
  • 上下文菜单中的 解析为 Markdown 并粘贴: 解析富文本为 Markdown 文本,并按需获取图片到本地。
  • +
  • 上下文菜单中的 多功能粘贴: 粘贴为图片、附件或者连接。
  • +
  • 上下文菜单中的 交叉复制: 将所选文本复制为富文本。
  • +
diff --git a/src/data/extra/extra.qrc b/src/data/extra/extra.qrc index e2505015..6a8338a7 100644 --- a/src/data/extra/extra.qrc +++ b/src/data/extra/extra.qrc @@ -6,12 +6,14 @@ docs/en/markdown_guide.md docs/en/external_programs.md docs/en/welcome.md + docs/en/features_tips.txt docs/zh_CN/get_started.txt docs/zh_CN/about_vnotex.txt docs/zh_CN/shortcuts.md docs/zh_CN/markdown_guide.md docs/zh_CN/external_programs.md docs/zh_CN/welcome.md + docs/zh_CN/features_tips.txt web/markdown-viewer-template.html web/markdown-export-template.html web/css/globalstyles.css diff --git a/src/export/webviewexporter.cpp b/src/export/webviewexporter.cpp index d45fb677..ffb3e843 100644 --- a/src/export/webviewexporter.cpp +++ b/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); } - args << QDir::toNativeSeparators(p_outputFile); + // To handle non-ASCII path, export it to a temp file and then copy it. + QTemporaryDir tmpDir; + if (!tmpDir.isValid()) { + return false; + } - return startProcess(p_pdfOption.m_wkhtmltopdfExePath, args); + const auto tmpFile = tmpDir.filePath("vx_tmp_output.pdf"); + args << QDir::toNativeSeparators(tmpFile); + + 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) diff --git a/src/src.pro b/src/src.pro index 6556b947..0b59f535 100644 --- a/src/src.pro +++ b/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 diff --git a/src/utils/fileutils.cpp b/src/utils/fileutils.cpp index d64ac892..0f12ca13 100644 --- a/src/utils/fileutils.cpp +++ b/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()) { diff --git a/src/utils/pathutils.cpp b/src/utils/pathutils.cpp index d735e451..530f8a9b 100644 --- a/src/utils/pathutils.cpp +++ b/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)://"); diff --git a/src/widgets/dialogs/exportdialog.cpp b/src/widgets/dialogs/exportdialog.cpp index 3d269f84..a21fa00e 100644 --- a/src/widgets/dialogs/exportdialog.cpp +++ b/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); diff --git a/src/widgets/dialogs/importfolderutils.cpp b/src/widgets/dialogs/importfolderutils.cpp index 607e305d..b87d393a 100644 --- a/src/widgets/dialogs/importfolderutils.cpp +++ b/src/widgets/dialogs/importfolderutils.cpp @@ -1,6 +1,7 @@ #include "importfolderutils.h" #include +#include #include #include #include "legacynotebookutils.h" diff --git a/src/widgets/dialogs/newfolderdialog.cpp b/src/widgets/dialogs/newfolderdialog.cpp index 8e9f7d63..fe5888d3 100644 --- a/src/widgets/dialogs/newfolderdialog.cpp +++ b/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; } diff --git a/src/widgets/editors/markdowneditor.cpp b/src/widgets/editors/markdowneditor.cpp index 1f99b15d..962ee147 100644 --- a/src/widgets/editors/markdowneditor.cpp +++ b/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); diff --git a/src/widgets/editors/markdowneditor.h b/src/widgets/editors/markdowneditor.h index dbc72fb2..b266e748 100644 --- a/src/widgets/editors/markdowneditor.h +++ b/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); diff --git a/src/widgets/editors/texteditor.cpp b/src/widgets/editors/texteditor.cpp index 2aebf620..ccca2d7e 100644 --- a/src/widgets/editors/texteditor.cpp +++ b/src/widgets/editors/texteditor.cpp @@ -1,6 +1,14 @@ #include "texteditor.h" +#include +#include + #include +#include + +#include +#include +#include using namespace vnotex; @@ -9,5 +17,23 @@ TextEditor::TextEditor(const QSharedPointer &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 *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); } diff --git a/src/widgets/editors/texteditor.h b/src/widgets/editors/texteditor.h index 870fe439..36391204 100644 --- a/src/widgets/editors/texteditor.h +++ b/src/widgets/editors/texteditor.h @@ -12,6 +12,12 @@ namespace vnotex TextEditor(const QSharedPointer &p_config, const QSharedPointer &p_paras, QWidget *p_parent = nullptr); + + signals: + void applySnippetRequested(); + + private slots: + void handleContextMenuEvent(QContextMenuEvent *p_event, bool *p_handled, QScopedPointer *p_menu); }; } diff --git a/src/widgets/mainwindow.cpp b/src/widgets/mainwindow.cpp index a2ca46b7..ca19058c 100644 --- a/src/widgets/mainwindow.cpp +++ b/src/widgets/mainwindow.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include "toolbox.h" #include "notebookexplorer.h" @@ -31,6 +32,7 @@ #include #include #include +#include #include #include #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::create(); diff --git a/src/widgets/markdownviewwindow.cpp b/src/widgets/markdownviewwindow.cpp index 75143451..c278e2d0 100644 --- a/src/widgets/markdownviewwindow.cpp +++ b/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 diff --git a/src/widgets/notebookexplorer.cpp b/src/widgets/notebookexplorer.cpp index d2fd712b..de1b1e06 100644 --- a/src/widgets/notebookexplorer.cpp +++ b/src/widgets/notebookexplorer.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #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.")); + } + } +} diff --git a/src/widgets/notebookexplorer.h b/src/widgets/notebookexplorer.h index b9f345cd..93955100 100644 --- a/src/widgets/notebookexplorer.h +++ b/src/widgets/notebookexplorer.h @@ -80,6 +80,8 @@ namespace vnotex void recoverSession(); + void rebuildDatabase(); + NotebookSelector *m_selector = nullptr; NotebookNodeExplorer *m_nodeExplorer = nullptr; diff --git a/src/widgets/notebooknodeexplorer.cpp b/src/widgets/notebooknodeexplorer.cpp index 96f43004..4b0c446a 100644 --- a/src/widgets/notebooknodeexplorer.cpp +++ b/src/widgets/notebooknodeexplorer.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #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 &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(nrItems))); } -QPair, QVector> NotebookNodeExplorer::getSelectedNodes() const +QPair, QVector>> NotebookNodeExplorer::getSelectedNodes() const { - QPair, QVector> nodes; + QPair, QVector>> nodes; auto items = m_masterExplorer->selectedItems(); for (auto &item : items) { @@ -1871,7 +1872,7 @@ QStringList NotebookNodeExplorer::getSelectedNodesPath() const return files; } -QSharedPointer NotebookNodeExplorer::importToIndex(const ExternalNode *p_node) +QSharedPointer NotebookNodeExplorer::importToIndex(QSharedPointer p_node) { auto node = m_notebook->addAsNode(p_node->getNode(), p_node->isFolder() ? Node::Flag::Container : Node::Flag::Content, @@ -1884,12 +1885,12 @@ QSharedPointer NotebookNodeExplorer::importToIndex(const ExternalNode *p_n return node; } -void NotebookNodeExplorer::importToIndex(const QVector &p_nodes) +void NotebookNodeExplorer::importToIndex(const QVector> &p_nodes) { QSet 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(), diff --git a/src/widgets/notebooknodeexplorer.h b/src/widgets/notebooknodeexplorer.h index 7911dda1..5e1401cc 100644 --- a/src/widgets/notebooknodeexplorer.h +++ b/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 &getExternalNode() const; void clear(); @@ -217,7 +218,7 @@ namespace vnotex void pasteNodesFromClipboard(); - QPair, QVector> getSelectedNodes() const; + QPair, QVector>> getSelectedNodes() const; void removeSelectedNodes(bool p_skipRecycleBin); @@ -258,9 +259,9 @@ namespace vnotex void openSelectedNodes(); - QSharedPointer importToIndex(const ExternalNode *p_node); + QSharedPointer importToIndex(QSharedPointer p_node); - void importToIndex(const QVector &p_nodes); + void importToIndex(const QVector> &p_nodes); // Check whether @p_node is a valid node. Will notify user. // Return true if it is invalid. diff --git a/src/widgets/searchpanel.cpp b/src/widgets/searchpanel.cpp index 66850556..4580e9f5 100644 --- a/src/widgets/searchpanel.cpp +++ b/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(); +} diff --git a/src/widgets/searchpanel.h b/src/widgets/searchpanel.h index dec580ef..71a88864 100644 --- a/src/widgets/searchpanel.h +++ b/src/widgets/searchpanel.h @@ -50,6 +50,9 @@ namespace vnotex public: SearchPanel(const QSharedPointer &p_provider, QWidget *p_parent = nullptr); + protected: + void focusInEvent(QFocusEvent *p_event) Q_DECL_OVERRIDE; + private slots: void startSearch(); diff --git a/src/widgets/textviewwindow.cpp b/src/widgets/textviewwindow.cpp index a20bb3e9..e26ec10a 100644 --- a/src/widgets/textviewwindow.cpp +++ b/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(); } diff --git a/src/widgets/viewsplit.cpp b/src/widgets/viewsplit.cpp index 25db3282..335753bf 100644 --- a/src/widgets/viewsplit.cpp +++ b/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); }); diff --git a/tests/test_core/test_notebook/dummynode.cpp b/tests/test_core/test_notebook/dummynode.cpp new file mode 100644 index 00000000..141f2b2d --- /dev/null +++ b/tests/test_core/test_notebook/dummynode.cpp @@ -0,0 +1,69 @@ +#include "dummynode.h" + +#include +#include + +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 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() +{ +} diff --git a/tests/test_core/test_notebook/dummynode.h b/tests/test_core/test_notebook/dummynode.h new file mode 100644 index 00000000..6d7a40c0 --- /dev/null +++ b/tests/test_core/test_notebook/dummynode.h @@ -0,0 +1,33 @@ +#ifndef DUMMYNODE_H +#define DUMMYNODE_H + +#include + +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 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 diff --git a/tests/test_core/test_notebook/dummynotebook.cpp b/tests/test_core/test_notebook/dummynotebook.cpp new file mode 100644 index 00000000..60d5e7b3 --- /dev/null +++ b/tests/test_core/test_notebook/dummynotebook.cpp @@ -0,0 +1,59 @@ +#include "dummynotebook.h" + +#include + +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 &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 DummyNotebook::loadNodeByPath(const QString &p_path) +{ + Q_UNUSED(p_path); + return nullptr; +} diff --git a/tests/test_core/test_notebook/dummynotebook.h b/tests/test_core/test_notebook/dummynotebook.h new file mode 100644 index 00000000..2361b6cf --- /dev/null +++ b/tests/test_core/test_notebook/dummynotebook.h @@ -0,0 +1,38 @@ +#ifndef DUMMYNOTEBOOK_H +#define DUMMYNOTEBOOK_H + +#include + +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 &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 loadNodeByPath(const QString &p_path) Q_DECL_OVERRIDE; + + protected: + void initializeInternal() Q_DECL_OVERRIDE; + + QVector m_history; + + QJsonObject m_extraConfigs; + }; +} + +#endif // DUMMYNOTEBOOK_H diff --git a/tests/test_core/test_notebook/test_notebook.cpp b/tests/test_core/test_notebook/test_notebook.cpp index f5115575..23dee15c 100644 --- a/tests/test_core/test_notebook/test_notebook.cpp +++ b/tests/test_core/test_notebook/test_notebook.cpp @@ -16,6 +16,8 @@ #include #include +#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() +void TestNotebook::testNotebookDatabase() { - Q_ASSERT(!m_vcServer); - - m_vcServer.reset(new NameBasedServer); - - // Dummy Version Controller. - auto dummyFactory = QSharedPointer::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); - - // VX Notebook Config Manager. - auto vxFactory = QSharedPointer::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); - - // Local Notebook Backend. - auto localFactory = QSharedPointer::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); - - // Bundle Notebook. - auto bundleFacotry = QSharedPointer::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(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 -{ - return m_testDir->path(); + TestNotebookDatabase test; + test.test(); } QTEST_MAIN(tests::TestNotebook) diff --git a/tests/test_core/test_notebook/test_notebook.h b/tests/test_core/test_notebook/test_notebook.h index d0d58511..3374c2bb 100644 --- a/tests/test_core/test_notebook/test_notebook.h +++ b/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 m_testDir; - - QSharedPointer> m_vcServer; - QSharedPointer> m_ncmServer; - QSharedPointer> m_backendServer; - QSharedPointer> m_nbServer; + void testNotebookDatabase(); }; } // ns tests diff --git a/tests/test_core/test_notebook/test_notebook.pro b/tests/test_core/test_notebook/test_notebook.pro index bf529c00..0f2dc15f 100644 --- a/tests/test_core/test_notebook/test_notebook.pro +++ b/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 diff --git a/tests/test_core/test_notebook/testnotebookdatabase.cpp b/tests/test_core/test_notebook/testnotebookdatabase.cpp new file mode 100644 index 00000000..77f49b34 --- /dev/null +++ b/tests/test_core/test_notebook/testnotebookdatabase.cpp @@ -0,0 +1,148 @@ +#include "testnotebookdatabase.h" + +#include + +#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 rootNode(new DummyNode(Node::Flag::Container, 0, "", m_notebook.data(), nullptr)); + addAndQueryNode(rootNode.data(), true); + + // Node 1. + QScopedPointer node1(new DummyNode(Node::Flag::Content, 10, "a", m_notebook.data(), rootNode.data())); + addAndQueryNode(node1.data(), true); + + // Node 2, respect id. + QScopedPointer 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 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 node4(new DummyNode(Node::Flag::Content, 11, "ca", m_notebook.data(), node3.data())); + addAndQueryNode(node4.data(), false); + + // Node 5, deep level. + QScopedPointer node5(new DummyNode(Node::Flag::Content, 60, "caa", m_notebook.data(), node4.data())); + addAndQueryNode(node5.data(), false); + + // Node 6, deep level. + QScopedPointer 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)); +} diff --git a/tests/test_core/test_notebook/testnotebookdatabase.h b/tests/test_core/test_notebook/testnotebookdatabase.h new file mode 100644 index 00000000..e4c56e08 --- /dev/null +++ b/tests/test_core/test_notebook/testnotebookdatabase.h @@ -0,0 +1,38 @@ +#ifndef TESTNOTEBOOKDATABASE_H +#define TESTNOTEBOOKDATABASE_H + +#include +#include + +#include + +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 m_notebook; + + QScopedPointer m_dbAccess; + }; +} + +#endif // TESTNOTEBOOKDATABASE_H