add sqlite database

This commit is contained in:
Le Tan 2021-09-10 20:52:55 +08:00
parent bb1598dde2
commit 6689e8c84c
65 changed files with 1935 additions and 594 deletions

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 \

View File

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

View File

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

View File

@ -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)
{

View File

@ -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:
};
}

View File

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

View File

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

View File

@ -20,12 +20,6 @@ const QSharedPointer<INotebookBackend> &INotebookConfigMgr::getBackend() const
return m_backend;
}
QString INotebookConfigMgr::getCodeVersion() const
{
const QString version("1");
return version;
}
Notebook *INotebookConfigMgr::getNotebook() const
{
return m_notebook;

View File

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

View File

@ -25,9 +25,7 @@ const QString NotebookConfig::c_versionController = "version_controller";
const QString NotebookConfig::c_configMgr = "config_mgr";
const QString NotebookConfig::c_nextNodeId = "next_node_id";
QSharedPointer<NotebookConfig> NotebookConfig::fromNotebookParameters(const QString &p_version,
QSharedPointer<NotebookConfig> NotebookConfig::fromNotebookParameters(int p_version,
const NotebookParameters &p_paras)
{
auto config = QSharedPointer<NotebookConfig>::create();
@ -56,7 +54,6 @@ QJsonObject NotebookConfig::toJson() const
jobj[NotebookConfig::c_createdTimeUtc] = Utils::dateTimeStringUniform(m_createdTimeUtc);
jobj[NotebookConfig::c_versionController] = m_versionController;
jobj[NotebookConfig::c_configMgr] = m_notebookConfigMgr;
jobj[NotebookConfig::c_nextNodeId] = QString::number(m_nextNodeId);
jobj[QStringLiteral("history")] = saveHistory();
@ -77,7 +74,7 @@ void NotebookConfig::fromJson(const QJsonObject &p_jobj)
return;
}
m_version = p_jobj[NotebookConfig::c_version].toString();
m_version = p_jobj[NotebookConfig::c_version].toInt();
m_name = p_jobj[NotebookConfig::c_name].toString();
m_description = p_jobj[NotebookConfig::c_description].toString();
m_imageFolder = p_jobj[NotebookConfig::c_imageFolder].toString();
@ -86,21 +83,12 @@ void NotebookConfig::fromJson(const QJsonObject &p_jobj)
m_versionController = p_jobj[NotebookConfig::c_versionController].toString();
m_notebookConfigMgr = p_jobj[NotebookConfig::c_configMgr].toString();
{
auto nextNodeIdStr = p_jobj[NotebookConfig::c_nextNodeId].toString();
bool ok;
m_nextNodeId = nextNodeIdStr.toULongLong(&ok);
if (!ok) {
m_nextNodeId = BundleNotebookConfigMgr::RootNodeId;
}
}
loadHistory(p_jobj);
m_extraConfigs = p_jobj[QStringLiteral("extra_configs")].toObject();
}
QSharedPointer<NotebookConfig> NotebookConfig::fromNotebook(const QString &p_version,
QSharedPointer<NotebookConfig> NotebookConfig::fromNotebook(int p_version,
const Notebook *p_notebook)
{
auto config = QSharedPointer<NotebookConfig>::create();
@ -113,7 +101,6 @@ QSharedPointer<NotebookConfig> NotebookConfig::fromNotebook(const QString &p_ver
config->m_createdTimeUtc = p_notebook->getCreatedTimeUtc();
config->m_versionController = p_notebook->getVersionController()->getName();
config->m_notebookConfigMgr = p_notebook->getConfigMgr()->getName();
config->m_nextNodeId = p_notebook->getNextNodeId();
config->m_history = p_notebook->getHistory();
config->m_extraConfigs = p_notebook->getExtraConfigs();

View File

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

View File

@ -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 \

View File

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

View File

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

View File

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

View File

@ -7,12 +7,21 @@
#include <QVector>
#include <QRegExp>
#include "../global.h"
#include <core/global.h>
class QJsonObject;
namespace vnotex
{
namespace vx_node_config
{
struct NodeFileConfig;
struct NodeFolderConfig;
struct NodeConfig;
}
class NotebookDatabaseAccess;
// Config manager for VNoteX's bundle notebook.
class VXNotebookConfigMgr : public BundleNotebookConfigMgr
{
@ -34,7 +43,7 @@ namespace vnotex
QSharedPointer<Node> loadRootNode() Q_DECL_OVERRIDE;
void loadNode(Node *p_node) const Q_DECL_OVERRIDE;
void loadNode(Node *p_node) Q_DECL_OVERRIDE;
void saveNode(const Node *p_node) Q_DECL_OVERRIDE;
void renameNode(Node *p_node, const QString &p_name) Q_DECL_OVERRIDE;
@ -77,85 +86,20 @@ namespace vnotex
QStringList scanAndImportExternalFiles(Node *p_node) Q_DECL_OVERRIDE;
private:
// Config of a file child.
struct NodeFileConfig
{
QJsonObject toJson() const;
void fromJson(const QJsonObject &p_jobj);
QString m_name;
ID m_id = Node::InvalidId;
QDateTime m_createdTimeUtc;
QDateTime m_modifiedTimeUtc;
QString m_attachmentFolder;
QStringList m_tags;
};
// Config of a folder child.
struct NodeFolderConfig
{
QJsonObject toJson() const;
void fromJson(const QJsonObject &p_jobj);
QString m_name;
};
// Config of a folder node.
struct NodeConfig
{
NodeConfig();
NodeConfig(const QString &p_version,
ID p_id,
const QDateTime &p_createdTimeUtc,
const QDateTime &p_modifiedTimeUtc);
QJsonObject toJson() const;
void fromJson(const QJsonObject &p_jobj);
QString m_version;
ID m_id = Node::InvalidId;
QDateTime m_createdTimeUtc;
QDateTime m_modifiedTimeUtc;
QVector<NodeFileConfig> m_files;
QVector<NodeFolderConfig> m_folders;
static const QString c_version;
static const QString c_id;
static const QString c_createdTimeUtc;
static const QString c_files;
static const QString c_folders;
static const QString c_name;
static const QString c_modifiedTimeUtc;
static const QString c_attachmentFolder;
static const QString c_tags;
};
void createEmptyRootNode();
QSharedPointer<VXNotebookConfigMgr::NodeConfig> readNodeConfig(const QString &p_path) const;
void writeNodeConfig(const QString &p_path, const NodeConfig &p_config) const;
QSharedPointer<vx_node_config::NodeConfig> readNodeConfig(const QString &p_path) const;
void writeNodeConfig(const QString &p_path, const vx_node_config::NodeConfig &p_config) const;
void writeNodeConfig(const Node *p_node);
QSharedPointer<Node> nodeConfigToNode(const NodeConfig &p_config,
QSharedPointer<Node> nodeConfigToNode(const vx_node_config::NodeConfig &p_config,
const QString &p_name,
Node *p_parent = nullptr) const;
Node *p_parent = nullptr);
void loadFolderNode(Node *p_node, const NodeConfig &p_config) const;
void loadFolderNode(Node *p_node, const vx_node_config::NodeConfig &p_config);
QSharedPointer<VXNotebookConfigMgr::NodeConfig> nodeToNodeConfig(const Node *p_node) const;
QSharedPointer<vx_node_config::NodeConfig> nodeToNodeConfig(const Node *p_node) const;
QSharedPointer<Node> newFileNode(Node *p_parent,
const QString &p_name,
@ -172,9 +116,20 @@ namespace vnotex
void addChildNode(Node *p_parent, const QSharedPointer<Node> &p_child) const;
QSharedPointer<Node> copyFileNodeAsChildOf(const QSharedPointer<Node> &p_src, Node *p_dest, bool p_move);
QSharedPointer<Node> copyNodeAsChildOf(const QSharedPointer<Node> &p_src,
Node *p_dest,
bool p_move,
bool p_updateDatabase);
QSharedPointer<Node> copyFolderNodeAsChildOf(const QSharedPointer<Node> &p_src, Node *p_dest, bool p_move);
QSharedPointer<Node> copyFileNodeAsChildOf(const QSharedPointer<Node> &p_src,
Node *p_dest,
bool p_move,
bool p_updateDatabase);
QSharedPointer<Node> copyFolderNodeAsChildOf(const QSharedPointer<Node> &p_src,
Node *p_dest,
bool p_move,
bool p_updateDatabase);
QSharedPointer<Node> copyFileAsChildOf(const QString &p_srcPath, Node *p_dest);
@ -197,6 +152,25 @@ namespace vnotex
bool isExcludedFromExternalNode(const QString &p_name) const;
void removeNode(const QSharedPointer<Node> &p_node,
bool p_force,
bool p_configOnly,
bool p_updateDatabase);
NotebookDatabaseAccess *getDatabaseAccess() const;
void updateNodeInDatabase(Node *p_node);
void ensureNodeInDatabase(Node *p_node);
void addNodeToDatabase(Node *p_node);
bool nodeExistsInDatabase(const Node *p_node);
void removeNodeFromDatabase(const Node *p_node);
bool sameNotebook(const Node *p_node) const;
static bool isLikelyImageFolder(const QString &p_dirPath);
Info m_info;

View File

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

View File

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

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

View File

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

View File

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

View File

@ -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)

View File

@ -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

View File

@ -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()) {

View File

@ -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)://");

View File

@ -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);

View File

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

View File

@ -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;
}

View File

@ -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);

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

@ -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();
}

View File

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

View File

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

View File

@ -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);
});

View File

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

View File

@ -0,0 +1,33 @@
#ifndef DUMMYNODE_H
#define DUMMYNODE_H
#include <notebook/node.h>
namespace tests
{
class DummyNode : public vnotex::Node
{
public:
DummyNode(Flags p_flags, vnotex::ID p_id, const QString &p_name, vnotex::Notebook *p_notebook, Node *p_parent);
QString fetchAbsolutePath() const Q_DECL_OVERRIDE;
QSharedPointer<vnotex::File> getContentFile() Q_DECL_OVERRIDE;
QStringList addAttachment(const QString &p_destFolderPath, const QStringList &p_files) Q_DECL_OVERRIDE;
QString newAttachmentFile(const QString &p_destFolderPath, const QString &p_name) Q_DECL_OVERRIDE;
QString newAttachmentFolder(const QString &p_destFolderPath, const QString &p_name) Q_DECL_OVERRIDE;
QString renameAttachment(const QString &p_path, const QString &p_name) Q_DECL_OVERRIDE;
void removeAttachment(const QStringList &p_paths) Q_DECL_OVERRIDE;
void load() Q_DECL_OVERRIDE;
void save() Q_DECL_OVERRIDE;
};
}
#endif // DUMMYNODE_H

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

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