From 0a2bdc7033de8d5d120744451345ebda3174d396 Mon Sep 17 00:00:00 2001 From: Le Tan Date: Fri, 24 Sep 2021 21:10:58 +0800 Subject: [PATCH] support tags --- src/core/buffer/buffer.cpp | 5 + src/core/buffer/buffer.h | 2 + src/core/buffer/bufferprovider.h | 2 + src/core/buffer/filebufferprovider.cpp | 5 + src/core/buffer/filebufferprovider.h | 2 + src/core/buffer/nodebufferprovider.cpp | 6 + src/core/buffer/nodebufferprovider.h | 2 + src/core/coreconfig.h | 1 + src/core/editorconfig.h | 1 + src/core/fileopenparameters.h | 3 + src/core/historymgr.cpp | 15 +- src/core/notebook/bundlenotebook.cpp | 86 ++++ src/core/notebook/bundlenotebook.h | 33 +- src/core/notebook/historyi.h | 23 + src/core/notebook/node.cpp | 11 + src/core/notebook/node.h | 1 + src/core/notebook/notebook.cpp | 10 + src/core/notebook/notebook.h | 16 +- src/core/notebook/notebook.pri | 6 + src/core/notebook/notebookdatabaseaccess.cpp | 416 ++++++++++++++++- src/core/notebook/notebookdatabaseaccess.h | 53 ++- src/core/notebook/notebooktagmgr.cpp | 318 +++++++++++++ src/core/notebook/notebooktagmgr.h | 78 ++++ src/core/notebook/tag.cpp | 47 ++ src/core/notebook/tag.h | 37 ++ src/core/notebook/tagi.h | 37 ++ .../bundlenotebookconfigmgr.cpp | 4 +- src/core/notebookconfigmgr/notebookconfig.cpp | 67 ++- src/core/notebookconfigmgr/notebookconfig.h | 22 +- src/core/sessionconfig.cpp | 2 + src/core/sessionconfig.h | 5 +- src/core/widgetconfig.cpp | 13 +- src/core/widgetconfig.h | 6 + src/data/core/core.qrc | 7 +- src/data/core/icons/properties.svg | 11 +- src/data/core/icons/recycle_bin.svg | 11 +- src/data/core/icons/sort.svg | 9 +- .../core/icons/{tag_explorer.svg => tag.svg} | 0 src/data/core/icons/tag_dock.svg | 12 + src/data/core/icons/tag_editor.svg | 12 + src/data/core/icons/tag_selected.svg | 1 + .../{up_parent_node.svg => up_level.svg} | 0 src/data/core/icons/view.svg | 16 +- src/data/core/vnotex.json | 7 +- src/data/extra/docs/en/shortcuts.md | 8 +- src/data/extra/docs/zh_CN/shortcuts.md | 8 +- src/main.cpp | 2 +- src/search/searcher.cpp | 5 + src/utils/pathutils.cpp | 9 +- .../dialogs/levellabelwithupbutton.cpp | 80 ++++ src/widgets/dialogs/levellabelwithupbutton.h | 52 +++ src/widgets/dialogs/newfolderdialog.cpp | 9 +- src/widgets/dialogs/newtagdialog.cpp | 104 +++++ src/widgets/dialogs/newtagdialog.h | 42 ++ src/widgets/dialogs/nodeinfowidget.cpp | 33 +- src/widgets/dialogs/nodeinfowidget.h | 4 +- src/widgets/dialogs/nodelabelwithupbutton.cpp | 76 ---- src/widgets/dialogs/nodelabelwithupbutton.h | 43 -- src/widgets/dialogs/notepropertiesdialog.cpp | 13 +- src/widgets/dialogs/renametagdialog.cpp | 82 ++++ src/widgets/dialogs/renametagdialog.h | 37 ++ src/widgets/dialogs/sortdialog.cpp | 16 - src/widgets/dialogs/viewtagsdialog.cpp | 58 +++ src/widgets/dialogs/viewtagsdialog.h | 35 ++ src/widgets/dockwidgethelper.cpp | 25 +- src/widgets/dockwidgethelper.h | 5 +- src/widgets/historypanel.cpp | 46 +- src/widgets/historypanel.h | 17 +- src/widgets/listwidget.cpp | 25 ++ src/widgets/listwidget.h | 8 + src/widgets/locationlist.cpp | 12 +- src/widgets/locationlist.h | 2 - src/widgets/mainwindow.cpp | 36 +- src/widgets/mainwindow.h | 9 +- src/widgets/markdownviewwindow.cpp | 13 +- src/widgets/navigationmodemgr.h | 1 + src/widgets/notebookexplorer.cpp | 28 +- src/widgets/notebookexplorer.h | 3 - src/widgets/notebooknodeexplorer.cpp | 242 +++++----- src/widgets/notebooknodeexplorer.h | 31 +- src/widgets/outlinepopup.cpp | 7 +- src/widgets/quickselector.cpp | 13 +- src/widgets/snippetpanel.cpp | 15 +- src/widgets/snippetpanel.h | 7 +- src/widgets/styleditemdelegate.h | 4 +- src/widgets/tagexplorer.cpp | 423 ++++++++++++++++++ src/widgets/tagexplorer.h | 105 +++++ src/widgets/tagpopup.cpp | 49 ++ src/widgets/tagpopup.h | 34 ++ src/widgets/tagviewer.cpp | 323 +++++++++++++ src/widgets/tagviewer.h | 79 ++++ src/widgets/textviewwindow.cpp | 2 + src/widgets/toolbarhelper.cpp | 2 +- src/widgets/treewidget.cpp | 29 +- src/widgets/treewidget.h | 8 +- src/widgets/viewarea.cpp | 4 +- src/widgets/viewwindow.cpp | 22 + src/widgets/viewwindow.h | 16 +- src/widgets/viewwindowtoolbarhelper.cpp | 18 + src/widgets/viewwindowtoolbarhelper.h | 1 + src/widgets/widgets.pri | 16 +- .../test_core/test_notebook/dummynotebook.cpp | 14 - tests/test_core/test_notebook/dummynotebook.h | 6 - .../test_notebook/testnotebookdatabase.cpp | 186 +++++++- .../test_notebook/testnotebookdatabase.h | 12 +- 105 files changed, 3434 insertions(+), 601 deletions(-) create mode 100644 src/core/notebook/historyi.h create mode 100644 src/core/notebook/notebooktagmgr.cpp create mode 100644 src/core/notebook/notebooktagmgr.h create mode 100644 src/core/notebook/tag.cpp create mode 100644 src/core/notebook/tag.h create mode 100644 src/core/notebook/tagi.h rename src/data/core/icons/{tag_explorer.svg => tag.svg} (100%) create mode 100644 src/data/core/icons/tag_dock.svg create mode 100644 src/data/core/icons/tag_editor.svg create mode 100644 src/data/core/icons/tag_selected.svg rename src/data/core/icons/{up_parent_node.svg => up_level.svg} (100%) create mode 100644 src/widgets/dialogs/levellabelwithupbutton.cpp create mode 100644 src/widgets/dialogs/levellabelwithupbutton.h create mode 100644 src/widgets/dialogs/newtagdialog.cpp create mode 100644 src/widgets/dialogs/newtagdialog.h delete mode 100644 src/widgets/dialogs/nodelabelwithupbutton.cpp delete mode 100644 src/widgets/dialogs/nodelabelwithupbutton.h create mode 100644 src/widgets/dialogs/renametagdialog.cpp create mode 100644 src/widgets/dialogs/renametagdialog.h create mode 100644 src/widgets/dialogs/viewtagsdialog.cpp create mode 100644 src/widgets/dialogs/viewtagsdialog.h create mode 100644 src/widgets/tagexplorer.cpp create mode 100644 src/widgets/tagexplorer.h create mode 100644 src/widgets/tagpopup.cpp create mode 100644 src/widgets/tagpopup.h create mode 100644 src/widgets/tagviewer.cpp create mode 100644 src/widgets/tagviewer.h diff --git a/src/core/buffer/buffer.cpp b/src/core/buffer/buffer.cpp index fe433c78..f84dc1b0 100644 --- a/src/core/buffer/buffer.cpp +++ b/src/core/buffer/buffer.cpp @@ -526,6 +526,11 @@ bool Buffer::isAttachment(const QString &p_path) const return PathUtils::pathContains(getAttachmentFolderPath(), p_path); } +bool Buffer::isTagSupported() const +{ + return m_provider->isTagSupported(); +} + Buffer::ProviderType Buffer::getProviderType() const { return m_provider->getType(); diff --git a/src/core/buffer/buffer.h b/src/core/buffer/buffer.h index 6aa896cd..8c0f4651 100644 --- a/src/core/buffer/buffer.h +++ b/src/core/buffer/buffer.h @@ -165,6 +165,8 @@ namespace vnotex // Judge whether file @p_path is attachment. bool isAttachment(const QString &p_path) const; + bool isTagSupported() const; + ProviderType getProviderType() const; bool checkFileExistsOnDisk(); diff --git a/src/core/buffer/bufferprovider.h b/src/core/buffer/bufferprovider.h index efdf8601..5c4bf398 100644 --- a/src/core/buffer/bufferprovider.h +++ b/src/core/buffer/bufferprovider.h @@ -68,6 +68,8 @@ namespace vnotex virtual bool isAttachmentSupported() const = 0; + virtual bool isTagSupported() const = 0; + virtual bool checkFileExistsOnDisk() const; virtual bool checkFileChangedOutside() const; diff --git a/src/core/buffer/filebufferprovider.cpp b/src/core/buffer/filebufferprovider.cpp index c0cacbb8..ec04c56e 100644 --- a/src/core/buffer/filebufferprovider.cpp +++ b/src/core/buffer/filebufferprovider.cpp @@ -169,6 +169,11 @@ bool FileBufferProvider::isAttachmentSupported() const return false; } +bool FileBufferProvider::isTagSupported() const +{ + return false; +} + Node *FileBufferProvider::getNode() const { return c_nodeAttachedTo; diff --git a/src/core/buffer/filebufferprovider.h b/src/core/buffer/filebufferprovider.h index 52d447f0..16111679 100644 --- a/src/core/buffer/filebufferprovider.h +++ b/src/core/buffer/filebufferprovider.h @@ -63,6 +63,8 @@ namespace vnotex bool isAttachmentSupported() const Q_DECL_OVERRIDE; + bool isTagSupported() const Q_DECL_OVERRIDE; + bool isReadOnly() const Q_DECL_OVERRIDE; QSharedPointer getFile() const Q_DECL_OVERRIDE; diff --git a/src/core/buffer/nodebufferprovider.cpp b/src/core/buffer/nodebufferprovider.cpp index 63b0d1a8..532d14ae 100644 --- a/src/core/buffer/nodebufferprovider.cpp +++ b/src/core/buffer/nodebufferprovider.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -148,6 +149,11 @@ bool NodeBufferProvider::isAttachmentSupported() const return true; } +bool NodeBufferProvider::isTagSupported() const +{ + return m_node->getNotebook()->tag() != nullptr; +} + Node *NodeBufferProvider::getNode() const { return m_node.data(); diff --git a/src/core/buffer/nodebufferprovider.h b/src/core/buffer/nodebufferprovider.h index 4a06f8e2..cab88d2e 100644 --- a/src/core/buffer/nodebufferprovider.h +++ b/src/core/buffer/nodebufferprovider.h @@ -65,6 +65,8 @@ namespace vnotex bool isAttachmentSupported() const Q_DECL_OVERRIDE; + bool isTagSupported() const Q_DECL_OVERRIDE; + bool isReadOnly() const Q_DECL_OVERRIDE; QSharedPointer getFile() const Q_DECL_OVERRIDE; diff --git a/src/core/coreconfig.h b/src/core/coreconfig.h index c5ff4d80..46977f45 100644 --- a/src/core/coreconfig.h +++ b/src/core/coreconfig.h @@ -29,6 +29,7 @@ namespace vnotex SnippetDock, LocationListDock, HistoryDock, + TagDock, Search, NavigationMode, LocateNode, diff --git a/src/core/editorconfig.h b/src/core/editorconfig.h index 9252ce4c..d15a1e6a 100644 --- a/src/core/editorconfig.h +++ b/src/core/editorconfig.h @@ -58,6 +58,7 @@ namespace vnotex FindNext, FindPrevious, ApplySnippet, + Tag, MaxShortcut }; Q_ENUM(Shortcut) diff --git a/src/core/fileopenparameters.h b/src/core/fileopenparameters.h index 3a5c61fa..9ecd6b20 100644 --- a/src/core/fileopenparameters.h +++ b/src/core/fileopenparameters.h @@ -38,6 +38,9 @@ namespace vnotex // If not empty, use this token to do a search text highlight. QSharedPointer m_searchToken; + + // Whether should save this file into session. + bool m_sessionEnabled = true; }; } diff --git a/src/core/historymgr.cpp b/src/core/historymgr.cpp index 754f9568..3fbecd45 100644 --- a/src/core/historymgr.cpp +++ b/src/core/historymgr.cpp @@ -8,6 +8,7 @@ #include "vnotex.h" #include "notebookmgr.h" #include +#include #include #include "exception.h" @@ -57,7 +58,11 @@ void HistoryMgr::loadHistory() if (m_perNotebookHistoryEnabled) { const auto ¬ebooks = VNoteX::getInst().getNotebookMgr().getNotebooks(); for (const auto &nb : notebooks) { - const auto &history = nb->getHistory(); + auto historyI = nb->history(); + if (!historyI) { + continue; + } + const auto &history = historyI->getHistory(); const auto &backend = nb->getBackend(); for (const auto &item : history) { auto fullItem = QSharedPointer::create(); @@ -102,8 +107,8 @@ void HistoryMgr::add(const QString &p_path, HistoryItem item(p_path, p_lineNumber, QDateTime::currentDateTimeUtc()); - if (p_notebook && m_perNotebookHistoryEnabled) { - p_notebook->addHistory(item); + if (p_notebook && m_perNotebookHistoryEnabled && p_notebook->history()) { + p_notebook->history()->addHistory(item); } else { auto &sessionConfig = ConfigMgr::getInst().getSessionConfig(); sessionConfig.addHistory(item); @@ -176,7 +181,9 @@ void HistoryMgr::clear() if (m_perNotebookHistoryEnabled) { const auto ¬ebooks = VNoteX::getInst().getNotebookMgr().getNotebooks(); for (const auto &nb : notebooks) { - nb->clearHistory(); + if (auto historyI = nb->history()) { + historyI->clearHistory(); + } } } diff --git a/src/core/notebook/bundlenotebook.cpp b/src/core/notebook/bundlenotebook.cpp index 88962bfd..dd3d1884 100644 --- a/src/core/notebook/bundlenotebook.cpp +++ b/src/core/notebook/bundlenotebook.cpp @@ -11,6 +11,7 @@ #include #include "notebookdatabaseaccess.h" +#include "notebooktagmgr.h" using namespace vnotex; @@ -20,6 +21,7 @@ BundleNotebook::BundleNotebook(const NotebookParameters &p_paras, : Notebook(p_paras, p_parent), m_configVersion(p_notebookConfig->m_version), m_history(p_notebookConfig->m_history), + m_tagGraph(p_notebookConfig->m_tagGraph), m_extraConfigs(p_notebookConfig->m_extraConfigs) { setupDatabase(); @@ -59,6 +61,15 @@ void BundleNotebook::initDatabase() int cnt = 0; fillNodeTableFromConfig(getRootNode().data(), m_configVersion < 2, cnt); qDebug() << "fillNodeTableFromConfig nodes count" << cnt; + + fillTagTableFromTagGraph(); + + cnt = 0; + fillTagTableFromConfig(getRootNode().data(), cnt); + } + + if (m_tagMgr) { + m_tagMgr->update(); } } @@ -87,6 +98,11 @@ void BundleNotebook::remove() } } +HistoryI *BundleNotebook::history() +{ + return this; +} + const QVector &BundleNotebook::getHistory() const { return m_history; @@ -166,5 +182,75 @@ bool BundleNotebook::rebuildDatabase() setupDatabase(); initDatabase(); + + emit tagsUpdated(); + return true; } + +const QString &BundleNotebook::getTagGraph() const +{ + return m_tagGraph; +} + +void BundleNotebook::updateTagGraph(const QString &p_tagGraph) +{ + if (m_tagGraph == p_tagGraph) { + return; + } + + m_tagGraph = p_tagGraph; + updateNotebookConfig(); +} + +void BundleNotebook::fillTagTableFromTagGraph() +{ + auto tagGraph = NotebookTagMgr::stringToTagGraph(m_tagGraph); + for (const auto &tagPair : tagGraph) { + if (!m_dbAccess->addTag(tagPair.m_parent)) { + qWarning() << "failed to add tag to DB" << tagPair.m_parent; + continue; + } + + if (!m_dbAccess->addTag(tagPair.m_child, tagPair.m_parent)) { + qWarning() << "failed to add tag to DB" << tagPair.m_child; + continue; + } + } + + QCoreApplication::processEvents(); +} + +void BundleNotebook::fillTagTableFromConfig(Node *p_node, int &p_totalCnt) +{ + // @p_node must already exists in node table. + bool ret = m_dbAccess->updateNodeTags(p_node); + if (!ret) { + qWarning() << "failed to add tags of node to DB" << p_node->getName() << p_node->getTags(); + return; + } + + if (++p_totalCnt % 10) { + QCoreApplication::processEvents(); + } + + const auto &children = p_node->getChildrenRef(); + for (const auto &child : children) { + fillTagTableFromConfig(child.data(), p_totalCnt); + } +} + +NotebookTagMgr *BundleNotebook::getTagMgr() const +{ + if (!m_tagMgr) { + auto th = const_cast(this); + th->m_tagMgr = new NotebookTagMgr(th); + } + + return m_tagMgr; +} + +TagI *BundleNotebook::tag() +{ + return getTagMgr(); +} diff --git a/src/core/notebook/bundlenotebook.h b/src/core/notebook/bundlenotebook.h index 8a3fbb8e..7c8956e5 100644 --- a/src/core/notebook/bundlenotebook.h +++ b/src/core/notebook/bundlenotebook.h @@ -3,14 +3,17 @@ #include "notebook.h" #include "global.h" +#include "historyi.h" namespace vnotex { class BundleNotebookConfigMgr; class NotebookConfig; class NotebookDatabaseAccess; + class NotebookTagMgr; - class BundleNotebook : public Notebook + class BundleNotebook : public Notebook, + public HistoryI { Q_OBJECT public: @@ -26,9 +29,8 @@ namespace vnotex void remove() Q_DECL_OVERRIDE; - const QVector &getHistory() const Q_DECL_OVERRIDE; - void addHistory(const HistoryItem &p_item) Q_DECL_OVERRIDE; - void clearHistory() Q_DECL_OVERRIDE; + const QString &getTagGraph() const; + void updateTagGraph(const QString &p_tagGraph); const QJsonObject &getExtraConfigs() const Q_DECL_OVERRIDE; void setExtraConfig(const QString &p_key, const QJsonObject &p_obj) Q_DECL_OVERRIDE; @@ -37,6 +39,18 @@ namespace vnotex NotebookDatabaseAccess *getDatabaseAccess() const; + TagI *tag() Q_DECL_OVERRIDE; + + // HistoryI. + public: + HistoryI *history() Q_DECL_OVERRIDE; + + const QVector &getHistory() const Q_DECL_OVERRIDE; + + void addHistory(const HistoryItem &p_item) Q_DECL_OVERRIDE; + + void clearHistory() Q_DECL_OVERRIDE; + protected: void initializeInternal() Q_DECL_OVERRIDE; @@ -49,14 +63,25 @@ namespace vnotex void initDatabase(); + void fillTagTableFromTagGraph(); + + void fillTagTableFromConfig(Node *p_node, int &p_totalCnt); + + NotebookTagMgr *getTagMgr() const; + const int m_configVersion; QVector m_history; + QString m_tagGraph; + QJsonObject m_extraConfigs; // Managed by QObject. NotebookDatabaseAccess *m_dbAccess = nullptr; + + // Managed by QObject. + NotebookTagMgr *m_tagMgr = nullptr; }; } // ns vnotex diff --git a/src/core/notebook/historyi.h b/src/core/notebook/historyi.h new file mode 100644 index 00000000..14297777 --- /dev/null +++ b/src/core/notebook/historyi.h @@ -0,0 +1,23 @@ +#ifndef HISTORYI_H +#define HISTORYI_H + +#include + +#include + +namespace vnotex +{ + // History interface for notebook. + class HistoryI + { + public: + virtual ~HistoryI() = default; + + virtual const QVector &getHistory() const = 0; + + virtual void addHistory(const HistoryItem &p_item) = 0; + + virtual void clearHistory() = 0; + }; +} +#endif // HISTORYI_H diff --git a/src/core/notebook/node.cpp b/src/core/notebook/node.cpp index e3b43220..db9bf6fa 100644 --- a/src/core/notebook/node.cpp +++ b/src/core/notebook/node.cpp @@ -245,6 +245,17 @@ const QStringList &Node::getTags() const return m_tags; } +void Node::updateTags(const QStringList &p_tags) +{ + if (p_tags == m_tags) { + return; + } + + m_tags = p_tags; + save(); + emit m_notebook->nodeUpdated(this); +} + bool Node::isReadOnly() const { return m_flags & Flag::ReadOnly; diff --git a/src/core/notebook/node.h b/src/core/notebook/node.h index 2474f701..aaa4be5d 100644 --- a/src/core/notebook/node.h +++ b/src/core/notebook/node.h @@ -132,6 +132,7 @@ namespace vnotex virtual void save(); const QStringList &getTags() const; + void updateTags(const QStringList &p_tags); const QString &getAttachmentFolder() const; void setAttachmentFolder(const QString &p_attachmentFolder); diff --git a/src/core/notebook/notebook.cpp b/src/core/notebook/notebook.cpp index 13672da4..62af3f34 100644 --- a/src/core/notebook/notebook.cpp +++ b/src/core/notebook/notebook.cpp @@ -406,3 +406,13 @@ bool Notebook::rebuildDatabase() { return false; } + +HistoryI *Notebook::history() +{ + return nullptr; +} + +TagI *Notebook::tag() +{ + return nullptr; +} diff --git a/src/core/notebook/notebook.h b/src/core/notebook/notebook.h index fe1d139b..fe4d329e 100644 --- a/src/core/notebook/notebook.h +++ b/src/core/notebook/notebook.h @@ -8,7 +8,6 @@ #include "notebookparameters.h" #include #include "node.h" -#include namespace vnotex { @@ -17,6 +16,8 @@ namespace vnotex class INotebookConfigMgr; class NodeParameters; class File; + class HistoryI; + class TagI; // Base class of notebook. class Notebook : public QObject @@ -133,10 +134,6 @@ namespace vnotex void reloadNodes(); - virtual const QVector &getHistory() const = 0; - virtual void addHistory(const HistoryItem &p_item) = 0; - virtual void clearHistory() = 0; - // Hold extra 3rd party configs. virtual const QJsonObject &getExtraConfigs() const = 0; QJsonObject getExtraConfig(const QString &p_key) const; @@ -153,11 +150,20 @@ namespace vnotex static const QString c_defaultImageFolder; + public: + // Return null if history is not suported. + virtual HistoryI *history(); + + // Return null if tag is not suported. + virtual TagI *tag(); + signals: void updated(); void nodeUpdated(const Node *p_node); + void tagsUpdated(); + protected: virtual void initializeInternal() = 0; diff --git a/src/core/notebook/notebook.pri b/src/core/notebook/notebook.pri index 9618cf77..f7a3dac3 100644 --- a/src/core/notebook/notebook.pri +++ b/src/core/notebook/notebook.pri @@ -7,11 +7,14 @@ SOURCES += \ $$PWD/notebookparameters.cpp \ $$PWD/bundlenotebook.cpp \ $$PWD/node.cpp \ + $$PWD/notebooktagmgr.cpp \ + $$PWD/tag.cpp \ $$PWD/vxnode.cpp \ $$PWD/vxnodefile.cpp HEADERS += \ $$PWD/externalnode.h \ + $$PWD/historyi.h \ $$PWD/nodeparameters.h \ $$PWD/notebook.h \ $$PWD/inotebookfactory.h \ @@ -20,5 +23,8 @@ HEADERS += \ $$PWD/notebookparameters.h \ $$PWD/bundlenotebook.h \ $$PWD/node.h \ + $$PWD/notebooktagmgr.h \ + $$PWD/tag.h \ + $$PWD/tagi.h \ $$PWD/vxnode.h \ $$PWD/vxnodefile.h diff --git a/src/core/notebook/notebookdatabaseaccess.cpp b/src/core/notebook/notebookdatabaseaccess.cpp index 8605bb11..a1e7db51 100644 --- a/src/core/notebook/notebookdatabaseaccess.cpp +++ b/src/core/notebook/notebookdatabaseaccess.cpp @@ -2,6 +2,7 @@ #include #include +#include #include @@ -12,6 +13,10 @@ using namespace vnotex; static QString c_nodeTableName = "node"; +static QString c_tagTableName = "tag"; + +static QString c_nodeTagTableName = "tag_node"; + NotebookDatabaseAccess::NotebookDatabaseAccess(Notebook *p_notebook, const QString &p_databaseFile, QObject *p_parent) : QObject(p_parent), m_notebook(p_notebook), @@ -64,18 +69,40 @@ void NotebookDatabaseAccess::setupTables(QSqlDatabase &p_db, int p_configVersion QSqlQuery query(p_db); - // Node. if (m_fresh) { + // Node. bool ret = query.exec(QString("CREATE TABLE %1 (\n" " id INTEGER PRIMARY KEY,\n" - " name text NOT NULL,\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)); + " parent_id INTEGER NULL REFERENCES %1(id) ON DELETE CASCADE ON UPDATE 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; } + + // Tag. + ret = query.exec(QString("CREATE TABLE %1 (\n" + " name TEXT PRIMARY KEY,\n" + " parent_name TEXT NULL REFERENCES %1(name) ON DELETE CASCADE ON UPDATE CASCADE) WITHOUT ROWID\n").arg(c_tagTableName)); + if (!ret) { + qWarning() << QString("failed to create database table (%1) (%2)").arg(c_tagTableName, query.lastError().text()); + m_valid = false; + return; + } + + // Node_Tag. + ret = query.exec(QString("CREATE TABLE %1 (\n" + " node_id INTEGER REFERENCES %2(id) ON DELETE CASCADE ON UPDATE CASCADE,\n" + " tag_name TEXT REFERENCES %3(name) ON DELETE CASCADE ON UPDATE CASCADE)\n").arg(c_nodeTagTableName, + c_nodeTableName, + c_tagTableName)); + if (!ret) { + qWarning() << QString("failed to create database table (%1) (%2)").arg(c_nodeTagTableName, query.lastError().text()); + m_valid = false; + return; + } } } @@ -113,7 +140,7 @@ bool NotebookDatabaseAccess::addNode(Node *p_node, bool p_ignoreId) if (p_node->getId() != InvalidId) { auto nodeRec = queryNode(p_node->getId()); if (nodeRec) { - auto nodePath = queryNodePath(p_node->getId()); + auto nodePath = queryNodeParentPath(p_node->getId()); if (existsNode(p_node, nodeRec.data(), nodePath)) { return true; } @@ -156,7 +183,7 @@ bool NotebookDatabaseAccess::addNode(Node *p_node, bool p_ignoreId) } if (!query.exec()) { - qWarning() << "failed to add node by query" << query.executedQuery() << query.lastError().text(); + qWarning() << "failed to add node" << query.executedQuery() << query.lastError().text(); return false; } @@ -164,13 +191,7 @@ bool NotebookDatabaseAccess::addNode(Node *p_node, bool p_ignoreId) 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); + qDebug() << "added node id" << id << p_node->getName(); return true; } @@ -178,7 +199,7 @@ QSharedPointer NotebookDatabaseAccess::query { auto db = getDatabase(); QSqlQuery query(db); - query.prepare(QString("SELECT id, name, signature, parent_id from %1 where id = :id").arg(c_nodeTableName)); + 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(); @@ -210,7 +231,7 @@ bool NotebookDatabaseAccess::existsNode(const Node *p_node) return existsNode(p_node, queryNode(p_node->getId()).data(), - queryNodePath(p_node->getId())); + queryNodeParentPath(p_node->getId())); } bool NotebookDatabaseAccess::existsNode(const Node *p_node, const NodeRecord *p_rec, const QStringList &p_nodePath) @@ -226,7 +247,7 @@ bool NotebookDatabaseAccess::existsNode(const Node *p_node, const NodeRecord *p_ return checkNodePath(p_node, p_nodePath); } -QStringList NotebookDatabaseAccess::queryNodePath(ID p_id) +QStringList NotebookDatabaseAccess::queryNodeParentPath(ID p_id) { auto db = getDatabase(); QSqlQuery query(db); @@ -239,7 +260,7 @@ QStringList NotebookDatabaseAccess::queryNodePath(ID p_id) " 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)); + "SELECT id, name, parent_id 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(); @@ -259,6 +280,22 @@ QStringList NotebookDatabaseAccess::queryNodePath(ID p_id) return ret; } +QString NotebookDatabaseAccess::queryNodePath(ID p_id) +{ + auto parentPath = queryNodeParentPath(p_id); + if (parentPath.isEmpty()) { + return QString(); + } + + if (parentPath.size() == 1) { + return parentPath.first(); + } + + QString relativePath = parentPath.join(QLatin1Char('/')); + Q_ASSERT(relativePath[0] == QLatin1Char('/')); + return relativePath.mid(1); +} + bool NotebookDatabaseAccess::updateNode(const Node *p_node) { Q_ASSERT(p_node->getParent()); @@ -379,3 +416,350 @@ bool NotebookDatabaseAccess::checkNodePath(const Node *p_node, const QStringList return true; } + +bool NotebookDatabaseAccess::addTag(const QString &p_name, const QString &p_parentName) +{ + return addTag(p_name, p_parentName, true); +} + +bool NotebookDatabaseAccess::addTag(const QString &p_name) +{ + return addTag(p_name, QString(), false); +} + +bool NotebookDatabaseAccess::addTag(const QString &p_name, const QString &p_parentName, bool p_updateOnExists) +{ + { + auto tagRec = queryTag(p_name); + if (tagRec) { + if (!p_updateOnExists || tagRec->m_parentName == p_parentName) { + return true; + } + + return updateTagParent(p_name, p_parentName); + } + } + + auto db = getDatabase(); + QSqlQuery query(db); + query.prepare(QString("INSERT INTO %1 (name, parent_name)\n" + " VALUES (:name, :parent_name)").arg(c_tagTableName)); + query.bindValue(":name", p_name); + query.bindValue(":parent_name", p_parentName.isEmpty() ? QVariant() : p_parentName); + + if (!query.exec()) { + qWarning() << "failed to add tag" << query.executedQuery() << query.lastError().text(); + return false; + } + + qDebug() << "added tag" << p_name << "parentName" << p_parentName; + return true; +} + +QSharedPointer NotebookDatabaseAccess::queryTag(const QString &p_name) +{ + auto db = getDatabase(); + QSqlQuery query(db); + query.prepare(QString("SELECT name, parent_name FROM %1 WHERE name = :name").arg(c_tagTableName)); + query.bindValue(":name", p_name); + if (!query.exec()) { + qWarning() << "failed to query tag" << query.executedQuery() << query.lastError().text(); + return nullptr; + } + + if (query.next()) { + auto tagRec = QSharedPointer::create(); + tagRec->m_name = query.value(0).toString(); + tagRec->m_parentName = query.value(1).toString(); + return tagRec; + } + + return nullptr; +} + +bool NotebookDatabaseAccess::updateTagParent(const QString &p_name, const QString &p_parentName) +{ + auto db = getDatabase(); + QSqlQuery query(db); + query.prepare(QString("UPDATE %1\n" + "SET parent_name = :parent_name\n" + "WHERE name = :name").arg(c_tagTableName)); + query.bindValue(":name", p_name); + query.bindValue(":parent_name", p_parentName.isEmpty() ? QVariant() : p_parentName); + if (!query.exec()) { + qWarning() << "failed to update tag" << query.executedQuery() << query.lastError().text(); + return false; + } + + qDebug() << "updated tag parent" << p_name << p_parentName; + + return true; +} + +bool NotebookDatabaseAccess::renameTag(const QString &p_name, const QString &p_newName) +{ + Q_ASSERT(!p_newName.isEmpty()); + if (p_name == p_newName) { + return true; + } + + auto db = getDatabase(); + QSqlQuery query(db); + query.prepare(QString("UPDATE %1\n" + "SET name = :new_name\n" + "WHERE name = :name").arg(c_tagTableName)); + query.bindValue(":name", p_name); + query.bindValue(":new_name", p_newName); + if (!query.exec()) { + qWarning() << "failed to update tag" << query.executedQuery() << query.lastError().text(); + return false; + } + + qDebug() << "updated tag name" << p_name << p_newName; + + return true; +} + +bool NotebookDatabaseAccess::removeTag(const QString &p_name) +{ + auto db = getDatabase(); + QSqlQuery query(db); + query.prepare(QString("DELETE FROM %1\n" + "WHERE name = :name").arg(c_tagTableName)); + query.bindValue(":name", p_name); + if (!query.exec()) { + qWarning() << "failed to remove tag" << query.executedQuery() << query.lastError().text(); + return false; + } + qDebug() << "removed tag" << p_name; + return true; +} + +bool NotebookDatabaseAccess::updateNodeTags(Node *p_node) +{ + p_node->load(); + + if (p_node->getId() == Node::InvalidId) { + qWarning() << "failed to update tags of node with invalid id" << p_node->fetchPath(); + return false; + } + + const auto &nodeTags = p_node->getTags(); + + { + const auto tags = QSet::fromList(queryNodeTags(p_node->getId())); + if (tags.isEmpty() && nodeTags.isEmpty()) { + return true; + } + + bool needUpdate = false; + if (tags.size() != nodeTags.size()) { + needUpdate = true; + } + + for (const auto &tag : nodeTags) { + if (tags.find(tag) == tags.end()) { + needUpdate = true; + + if (!addTag(tag)) { + qWarning() << "failed to add tag before addNodeTags" << p_node->getId() << tag; + return false; + } + } + } + + if (!needUpdate) { + return true; + } + } + + bool ret = removeNodeTags(p_node->getId()); + if (!ret) { + return false; + } + + return addNodeTags(p_node->getId(), nodeTags); +} + +QStringList NotebookDatabaseAccess::queryNodeTags(ID p_id) +{ + auto db = getDatabase(); + QSqlQuery query(db); + query.prepare(QString("SELECT tag_name FROM %1 WHERE node_id = :node_id").arg(c_nodeTagTableName)); + query.bindValue(":node_id", p_id); + if (!query.exec()) { + qWarning() << "failed to query node's tags" << query.executedQuery() << query.lastError().text(); + return QStringList(); + } + + QStringList tags; + while (query.next()) { + tags.append(query.value(0).toString()); + } + return tags; +} + +bool NotebookDatabaseAccess::removeNodeTags(ID p_id) +{ + auto db = getDatabase(); + QSqlQuery query(db); + query.prepare(QString("DELETE FROM %1\n" + "WHERE node_id = :node_id").arg(c_nodeTagTableName)); + query.bindValue(":node_id", p_id); + if (!query.exec()) { + qWarning() << "failed to remove tags of node" << query.executedQuery() << query.lastError().text(); + return false; + } + qDebug() << "removed tags of node" << p_id; + return true; +} + +bool NotebookDatabaseAccess::addNodeTags(ID p_id, const QStringList &p_tags) +{ + Q_ASSERT(p_id != Node::InvalidId); + if (p_tags.isEmpty()) { + return true; + } + + auto db = getDatabase(); + QSqlQuery query(db); + query.prepare(QString("INSERT INTO %1 (node_id, tag_name)\n" + " VALUES (?, ?)").arg(c_nodeTagTableName)); + + QVariantList ids; + QVariantList tagNames; + for (const auto &tag : p_tags) { + ids << p_id; + tagNames << tag; + } + + query.addBindValue(ids); + query.addBindValue(tagNames); + + if (!query.execBatch()) { + qWarning() << "failed to add tags of node" << query.executedQuery() << query.lastError().text(); + return false; + } + + qDebug() << "added tags of node" << p_id << p_tags; + return true; +} + +QList NotebookDatabaseAccess::queryTagNodes(const QString &p_tag) +{ + QList nodes; + auto db = getDatabase(); + QSqlQuery query(db); + query.prepare(QString("SELECT node_id FROM %1 WHERE tag_name = :tag_name").arg(c_nodeTagTableName)); + query.bindValue(":tag_name", p_tag); + if (!query.exec()) { + qWarning() << "failed to query nodes of tag" << query.executedQuery() << query.lastError().text(); + return nodes; + } + + while (query.next()) { + nodes.append(query.value(0).toULongLong()); + } + return nodes; +} + +QList NotebookDatabaseAccess::queryTagNodesRecursive(const QString &p_tag) +{ + auto tags = queryTagAndChildren(p_tag); + if (tags.size() <= 1) { + return queryTagNodes(p_tag); + } + + QSet allIds; + for (const auto &tag : tags) { + auto ids = queryTagNodes(tag); + for (const auto &id : ids) { + allIds.insert(id); + } + } + + return allIds.toList(); +} + +QStringList NotebookDatabaseAccess::queryTagAndChildren(const QString &p_tag) +{ + auto db = getDatabase(); + QSqlQuery query(db); + query.prepare(QString("WITH RECURSIVE cte_children(name, parent_name) AS (\n" + " SELECT tag.name, tag.parent_name\n" + " FROM %1 tag\n" + " WHERE tag.name = :name\n" + " UNION ALL\n" + " SELECT tag.name, tag.parent_name\n" + " FROM %1 tag\n" + " JOIN cte_children cte ON tag.parent_name = cte.name\n" + " LIMIT 5000)\n" + "SELECT name FROM cte_children").arg(c_tagTableName)); + query.bindValue(":name", p_tag); + if (!query.exec()) { + qWarning() << "failed to query tag and its children" << query.executedQuery() << query.lastError().text(); + return QStringList(); + } + + QStringList ret; + while (query.next()) { + ret.append(query.value(0).toString()); + } + + qDebug() << "tag and its children" << p_tag << ret; + return ret; +} + +QStringList NotebookDatabaseAccess::getNodesOfTags(const QStringList &p_tags) +{ + QStringList ret; + if (p_tags.isEmpty()) { + return ret; + } + + QList nodeIds; + + if (p_tags.size() == 1) { + nodeIds = queryTagNodesRecursive(p_tags.first()); + } else { + QSet allIds; + for (const auto &tag : p_tags) { + auto ids = queryTagNodesRecursive(tag); + for (const auto &id : ids) { + allIds.insert(id); + } + } + nodeIds = allIds.toList(); + } + + for (const auto &id : nodeIds) { + auto nodePath = queryNodePath(id); + if (nodePath.isNull()) { + continue; + } + + ret.append(nodePath); + } + + return ret; +} + +QList NotebookDatabaseAccess::getAllTags() +{ + QList ret; + + auto db = getDatabase(); + QSqlQuery query(db); + query.prepare(QString("SELECT name, parent_name FROM %1 ORDER BY parent_name, name").arg(c_tagTableName)); + if (!query.exec()) { + qWarning() << "failed to query tags" << query.executedQuery() << query.lastError().text(); + return ret; + } + + while (query.next()) { + ret.append(TagRecord()); + ret.last().m_name = query.value(0).toString(); + ret.last().m_parentName = query.value(1).toString(); + } + return ret; +} diff --git a/src/core/notebook/notebookdatabaseaccess.h b/src/core/notebook/notebookdatabaseaccess.h index 771feb05..60eb50aa 100644 --- a/src/core/notebook/notebookdatabaseaccess.h +++ b/src/core/notebook/notebookdatabaseaccess.h @@ -24,6 +24,13 @@ namespace vnotex public: enum { InvalidId = 0 }; + struct TagRecord + { + QString m_name; + + QString m_parentName; + }; + friend class tests::TestNotebookDatabase; NotebookDatabaseAccess(Notebook *p_notebook, const QString &p_databaseFile, QObject *p_parent = nullptr); @@ -38,6 +45,8 @@ namespace vnotex void close(); + // Node table. + public: bool addNode(Node *p_node, bool p_ignoreId); // Whether there is a record with the same ID in DB and has the same path. @@ -49,6 +58,29 @@ namespace vnotex bool removeNode(const Node *p_node); + // Tag table. + public: + // Will update the tag if exists. + bool addTag(const QString &p_name, const QString &p_parentName); + + bool addTag(const QString &p_name); + + bool renameTag(const QString &p_name, const QString &p_newName); + + bool removeTag(const QString &p_name); + + // Sorted by parent_name. + QList getAllTags(); + + QStringList queryTagAndChildren(const QString &p_tag); + + // Node_tag table. + public: + bool updateNodeTags(Node *p_node); + + // Return the relative path of nodes of tags @p_tags. + QStringList getNodesOfTags(const QStringList &p_tags); + private: struct NodeRecord { @@ -68,7 +100,9 @@ namespace vnotex // Return null if not exists. QSharedPointer queryNode(ID p_id); - QStringList queryNodePath(ID p_id); + QStringList queryNodeParentPath(ID p_id); + + QString queryNodePath(ID p_id); bool nodeEqual(const NodeRecord *p_rec, const Node *p_node) const; @@ -78,6 +112,23 @@ namespace vnotex bool removeNode(ID p_id); + // Return null if not exists. + QSharedPointer queryTag(const QString &p_name); + + bool updateTagParent(const QString &p_name, const QString &p_parentName); + + bool addTag(const QString &p_name, const QString &p_parentName, bool p_updateOnExists); + + QStringList queryNodeTags(ID p_id); + + QList queryTagNodes(const QString &p_tag); + + QList queryTagNodesRecursive(const QString &p_tag); + + bool removeNodeTags(ID p_id); + + bool addNodeTags(ID p_id, const QStringList &p_tags); + Notebook *m_notebook = nullptr; QString m_databaseFile; diff --git a/src/core/notebook/notebooktagmgr.cpp b/src/core/notebook/notebooktagmgr.cpp new file mode 100644 index 00000000..30232e85 --- /dev/null +++ b/src/core/notebook/notebooktagmgr.cpp @@ -0,0 +1,318 @@ +#include "notebooktagmgr.h" + +#include +#include + +#include "bundlenotebook.h" +#include "tag.h" + +using namespace vnotex; + +NotebookTagMgr::NotebookTagMgr(BundleNotebook *p_notebook) + : QObject(p_notebook), + m_notebook(p_notebook) +{ + update(); +} + +QVector NotebookTagMgr::stringToTagGraph(const QString &p_text) +{ + // parent>chlid;parent2>chlid2. + QVector tagGraph; + auto pairs = p_text.split(QLatin1Char(';')); + for (const auto &pa : pairs) { + auto paCh = pa.split(QLatin1Char('>')); + if (paCh.size() != 2 || paCh[0].isEmpty() || paCh[1].isEmpty()) { + qWarning() << "ignore invalid tag pair" << pa; + continue; + } + + TagGraphPair tagPair; + tagPair.m_parent = paCh[0]; + tagPair.m_child = paCh[1]; + tagGraph.push_back(tagPair); + } + + return tagGraph; +} + +QString NotebookTagMgr::tagGraphToString(const QVector &p_tagGraph) +{ + QString text; + if (p_tagGraph.isEmpty()) { + return text; + } + + text = p_tagGraph[0].m_parent + QLatin1Char('>') + p_tagGraph[0].m_child; + for (int i = 1; i < p_tagGraph.size(); ++i) { + text += QLatin1Char(';') + p_tagGraph[i].m_parent + QLatin1Char('>') + p_tagGraph[i].m_child; + } + + return text; +} + +const QVector> &NotebookTagMgr::getTopLevelTags() const +{ + return m_topLevelTags; +} + +void NotebookTagMgr::update() +{ + auto db = m_notebook->getDatabaseAccess(); + const auto allTags = db->getAllTags(); + + update(allTags); +} + +void NotebookTagMgr::update(const QList &p_allTags) +{ + m_topLevelTags.clear(); + + QHash nameToTag; + + QVector todoIdx; + todoIdx.reserve(p_allTags.size()); + for (int i = 0; i < p_allTags.size(); ++i) { + todoIdx.push_back(i); + } + + while (!todoIdx.isEmpty()) { + QVector pendingIdx; + pendingIdx.reserve(p_allTags.size()); + + for (int i = 0; i < todoIdx.size(); ++i) { + const auto &rec = p_allTags[todoIdx[i]]; + Q_ASSERT(!nameToTag.contains(rec.m_name)); + QSharedPointer newTag; + if (rec.m_parentName.isEmpty()) { + // Top level. + newTag = QSharedPointer::create(rec.m_name); + m_topLevelTags.push_back(newTag); + } else { + auto parentIt = nameToTag.find(rec.m_parentName); + if (parentIt == nameToTag.end()) { + // Need to process its parent first. + pendingIdx.push_back(todoIdx[i]); + continue; + } else { + newTag = QSharedPointer::create(rec.m_name); + parentIt.value()->addChild(newTag); + } + } + + nameToTag.insert(newTag->name(), newTag.data()); + } + + if (todoIdx.size() == pendingIdx.size()) { + qWarning() << "cyclic parent-chlid tag definition detected"; + break; + } + + todoIdx = pendingIdx; + } +} + +QStringList NotebookTagMgr::findNodesOfTag(const QString &p_name) +{ + auto db = m_notebook->getDatabaseAccess(); + return db->getNodesOfTags(QStringList(p_name)); +} + +QSharedPointer NotebookTagMgr::findTag(const QString &p_name) +{ + QSharedPointer tag; + forEachTag([&tag, p_name](const QSharedPointer &p_tag) { + if (p_tag->name() == p_name) { + tag = p_tag; + return false; + } + return true; + }); + + return tag; +} + +void NotebookTagMgr::forEachTag(const TagFinder &p_func) const +{ + for (const auto &tag : m_topLevelTags) { + if (!forEachTag(tag, p_func)) { + return; + } + } +} + +bool NotebookTagMgr::forEachTag(const QSharedPointer &p_tag, const TagFinder &p_func) const +{ + if (!p_func(p_tag)) { + return false; + } + + for (const auto &child : p_tag->getChildren()) { + if (!forEachTag(child, p_func)) { + return false; + } + } + + return true; +} + +bool NotebookTagMgr::newTag(const QString &p_name, const QString &p_parentName) +{ + if (p_name.isEmpty()) { + return false; + } + + auto db = m_notebook->getDatabaseAccess(); + bool ret = db->addTag(p_name, p_parentName); + if (ret) { + const auto allTags = db->getAllTags(); + update(allTags); + if (!p_parentName.isEmpty()) { + updateNotebookTagGraph(allTags); + } + emit m_notebook->tagsUpdated(); + return true; + } else { + qWarning() << "failed to new tag" << p_name << p_parentName; + return false; + } +} + +bool NotebookTagMgr::updateNodeTags(Node *p_node) +{ + auto db = m_notebook->getDatabaseAccess(); + + // Make sure the node exists in DB. + if (!db->addNode(p_node, false)) { + qWarning() << "failed to add node to DB" << p_node->fetchPath() << p_node->getId(); + return false; + } + + if (db->updateNodeTags(p_node)) { + update(); + emit m_notebook->tagsUpdated(); + return true; + } + + return false; +} + +bool NotebookTagMgr::updateNodeTags(Node *p_node, const QStringList &p_newTags) +{ + p_node->updateTags(p_newTags); + return updateNodeTags(p_node); +} + +bool NotebookTagMgr::renameTag(const QString &p_name, const QString &p_newName) +{ + const auto nodePaths = findNodesOfTag(p_name); + + auto db = m_notebook->getDatabaseAccess(); + if (!db->renameTag(p_name, p_newName)) { + return false; + } + + const auto allTags = db->getAllTags(); + update(allTags); + + updateNotebookTagGraph(allTags); + + // Update node tag. + for (const auto &pa : nodePaths) { + auto node = m_notebook->loadNodeByPath(pa); + if (!node) { + qWarning() << "node belongs to tag in DB but not exists" << p_name << pa; + continue; + } + + auto tags = node->getTags(); + for (auto &tag : tags) { + if (tag == p_name) { + tag = p_newName; + break; + } + } + node->updateTags(tags); + } + + emit m_notebook->tagsUpdated(); + return true; +} + +void NotebookTagMgr::updateNotebookTagGraph(const QList &p_allTags) +{ + QVector graph; + graph.reserve(p_allTags.size()); + for (const auto &tag : p_allTags) { + if (tag.m_parentName.isEmpty()) { + continue; + } + TagGraphPair pa; + pa.m_parent = tag.m_parentName; + pa.m_child = tag.m_name; + graph.push_back(pa); + } + m_notebook->updateTagGraph(tagGraphToString(graph)); +} + +bool NotebookTagMgr::removeTag(const QString &p_name) +{ + const auto nodePaths = findNodesOfTag(p_name); + + auto db = m_notebook->getDatabaseAccess(); + QStringList tagsAndChildren; + if (!nodePaths.isEmpty()) { + tagsAndChildren = db->queryTagAndChildren(p_name); + if (tagsAndChildren.isEmpty()) { + qWarning() << "failed to query tag and its children" << p_name; + return false; + } + } + + if (!db->removeTag(p_name)) { + return false; + } + + const auto allTags = db->getAllTags(); + update(allTags); + + updateNotebookTagGraph(allTags); + + // Update node tag. + for (const auto &pa : nodePaths) { + auto node = m_notebook->loadNodeByPath(pa); + if (!node) { + qWarning() << "node belongs to tag in DB but not exists" << p_name << pa; + continue; + } + + const auto &tags = node->getTags(); + QStringList newTags; + for (const auto &tag : tags) { + if (tagsAndChildren.contains(tag)) { + continue; + } + newTags.append(tag); + } + node->updateTags(newTags); + } + + emit m_notebook->tagsUpdated(); + return true; +} + +bool NotebookTagMgr::moveTag(const QString &p_name, const QString &p_newParentName) +{ + auto db = m_notebook->getDatabaseAccess(); + if (!db->addTag(p_name, p_newParentName)) { + return false; + } + + const auto allTags = db->getAllTags(); + update(allTags); + + updateNotebookTagGraph(allTags); + + emit m_notebook->tagsUpdated(); + return true; +} diff --git a/src/core/notebook/notebooktagmgr.h b/src/core/notebook/notebooktagmgr.h new file mode 100644 index 00000000..ffdd4990 --- /dev/null +++ b/src/core/notebook/notebooktagmgr.h @@ -0,0 +1,78 @@ +#ifndef NOTEBOOKTAGMGR_H +#define NOTEBOOKTAGMGR_H + +#include + +#include "tagi.h" + +#include + +#include +#include + +#include "notebookdatabaseaccess.h" + +namespace vnotex +{ + class BundleNotebook; + class Tag; + + class NotebookTagMgr : public QObject, public TagI + { + Q_OBJECT + public: + struct TagGraphPair + { + QString m_parent; + + QString m_child; + }; + + explicit NotebookTagMgr(BundleNotebook *p_notebook); + + void update(); + + static QVector stringToTagGraph(const QString &p_text); + + static QString tagGraphToString(const QVector &p_tagGraph); + + // TagI. + public: + const QVector> &getTopLevelTags() const Q_DECL_OVERRIDE; + + QStringList findNodesOfTag(const QString &p_name) Q_DECL_OVERRIDE; + + QSharedPointer findTag(const QString &p_name) Q_DECL_OVERRIDE; + + bool newTag(const QString &p_name, const QString &p_parentName) Q_DECL_OVERRIDE; + + bool renameTag(const QString &p_name, const QString &p_newName) Q_DECL_OVERRIDE; + + bool updateNodeTags(Node *p_node) Q_DECL_OVERRIDE; + + bool updateNodeTags(Node *p_node, const QStringList &p_newTags) Q_DECL_OVERRIDE; + + bool removeTag(const QString &p_name) Q_DECL_OVERRIDE; + + bool moveTag(const QString &p_name, const QString &p_newParentName) Q_DECL_OVERRIDE; + + private: + typedef std::function &p_tag)> TagFinder; + + // @p_func: return false to abort the search. + void forEachTag(const TagFinder &p_func) const; + + // Return false if abort. + bool forEachTag(const QSharedPointer &p_tag, const TagFinder &p_func) const; + + void update(const QList &p_allTags); + + void updateNotebookTagGraph(const QList &p_allTags); + + BundleNotebook *m_notebook = nullptr; + + QVector> m_topLevelTags; + }; +} + +#endif // NOTEBOOKTAGMGR_H diff --git a/src/core/notebook/tag.cpp b/src/core/notebook/tag.cpp new file mode 100644 index 00000000..6db23751 --- /dev/null +++ b/src/core/notebook/tag.cpp @@ -0,0 +1,47 @@ +#include "tag.h" + +#include + +#include + +using namespace vnotex; + +Tag::Tag(const QString &p_name) + : m_name(p_name) +{ +} + +const QVector> &Tag::getChildren() const +{ + return m_children; +} + +void Tag::addChild(const QSharedPointer &p_tag) +{ + p_tag->m_parent = this; + m_children.push_back(p_tag); +} + +const QString &Tag::name() const +{ + return m_name; +} + +Tag *Tag::getParent() const +{ + return m_parent; +} + +QString Tag::fetchPath() const +{ + if (!m_parent) { + return m_name; + } else { + return PathUtils::concatenateFilePath(m_parent->fetchPath(), m_name); + } +} + +bool Tag::isValidName(const QString &p_name) +{ + return !p_name.isEmpty() && !p_name.contains(QRegularExpression("[>/]")); +} diff --git a/src/core/notebook/tag.h b/src/core/notebook/tag.h new file mode 100644 index 00000000..341b01c5 --- /dev/null +++ b/src/core/notebook/tag.h @@ -0,0 +1,37 @@ +#ifndef TAG_H +#define TAG_H + +#include +#include +#include +#include + +namespace vnotex +{ + class Tag : public QEnableSharedFromThis + { + public: + Tag(const QString &p_name); + + const QVector> &getChildren() const; + + const QString &name() const; + + Tag *getParent() const; + + void addChild(const QSharedPointer &p_tag); + + QString fetchPath() const; + + static bool isValidName(const QString &p_name); + + private: + Tag *m_parent = nullptr; + + QString m_name; + + QVector> m_children; + }; +} + +#endif // TAG_H diff --git a/src/core/notebook/tagi.h b/src/core/notebook/tagi.h new file mode 100644 index 00000000..665a0240 --- /dev/null +++ b/src/core/notebook/tagi.h @@ -0,0 +1,37 @@ +#ifndef TAGI_H +#define TAGI_H + +#include + +#include "tag.h" + +namespace vnotex +{ + class Node; + + // Tag interface for notebook. + class TagI + { + public: + virtual ~TagI() = default; + + virtual const QVector> &getTopLevelTags() const = 0; + + virtual QStringList findNodesOfTag(const QString &p_name) = 0; + + virtual QSharedPointer findTag(const QString &p_name) = 0; + + virtual bool newTag(const QString &p_name, const QString &p_parentName) = 0; + + virtual bool renameTag(const QString &p_name, const QString &p_newName) = 0; + + virtual bool updateNodeTags(Node *p_node) = 0; + + virtual bool updateNodeTags(Node *p_node, const QStringList &p_newTags) = 0; + + virtual bool removeTag(const QString &p_name) = 0; + + virtual bool moveTag(const QString &p_name, const QString &p_newParentName) = 0; + }; +} +#endif // TAGI_H diff --git a/src/core/notebookconfigmgr/bundlenotebookconfigmgr.cpp b/src/core/notebookconfigmgr/bundlenotebookconfigmgr.cpp index fd0f103a..6f2d3b6a 100644 --- a/src/core/notebookconfigmgr/bundlenotebookconfigmgr.cpp +++ b/src/core/notebookconfigmgr/bundlenotebookconfigmgr.cpp @@ -35,7 +35,7 @@ QSharedPointer BundleNotebookConfigMgr::readNotebookConfig() con void BundleNotebookConfigMgr::writeNotebookConfig() { - auto config = NotebookConfig::fromNotebook(getCodeVersion(), getNotebook()); + auto config = NotebookConfig::fromNotebook(getCodeVersion(), getBundleNotebook()); writeNotebookConfig(*config); } @@ -82,7 +82,7 @@ QString BundleNotebookConfigMgr::getDatabasePath() BundleNotebook *BundleNotebookConfigMgr::getBundleNotebook() const { - return dynamic_cast(getNotebook()); + return static_cast(getNotebook()); } bool BundleNotebookConfigMgr::isBuiltInFile(const Node *p_node, const QString &p_name) const diff --git a/src/core/notebookconfigmgr/notebookconfig.cpp b/src/core/notebookconfigmgr/notebookconfig.cpp index 7c565a5d..43a0f1e4 100644 --- a/src/core/notebookconfigmgr/notebookconfig.cpp +++ b/src/core/notebookconfigmgr/notebookconfig.cpp @@ -1,7 +1,7 @@ #include "notebookconfig.h" #include -#include +#include #include #include #include "exception.h" @@ -9,22 +9,6 @@ using namespace vnotex; -const QString NotebookConfig::c_version = "version"; - -const QString NotebookConfig::c_name = "name"; - -const QString NotebookConfig::c_description = "description"; - -const QString NotebookConfig::c_imageFolder = "image_folder"; - -const QString NotebookConfig::c_attachmentFolder = "attachment_folder"; - -const QString NotebookConfig::c_createdTimeUtc = "created_time"; - -const QString NotebookConfig::c_versionController = "version_controller"; - -const QString NotebookConfig::c_configMgr = "config_mgr"; - QSharedPointer NotebookConfig::fromNotebookParameters(int p_version, const NotebookParameters &p_paras) { @@ -46,17 +30,19 @@ QJsonObject NotebookConfig::toJson() const { QJsonObject jobj; - jobj[NotebookConfig::c_version] = m_version; - jobj[NotebookConfig::c_name] = m_name; - jobj[NotebookConfig::c_description] = m_description; - jobj[NotebookConfig::c_imageFolder] = m_imageFolder; - jobj[NotebookConfig::c_attachmentFolder] = m_attachmentFolder; - jobj[NotebookConfig::c_createdTimeUtc] = Utils::dateTimeStringUniform(m_createdTimeUtc); - jobj[NotebookConfig::c_versionController] = m_versionController; - jobj[NotebookConfig::c_configMgr] = m_notebookConfigMgr; + jobj[QStringLiteral("version")] = m_version; + jobj[QStringLiteral("name")] = m_name; + jobj[QStringLiteral("description")] = m_description; + jobj[QStringLiteral("image_folder")] = m_imageFolder; + jobj[QStringLiteral("attachment_folder")] = m_attachmentFolder; + jobj[QStringLiteral("created_time")] = Utils::dateTimeStringUniform(m_createdTimeUtc); + jobj[QStringLiteral("version_controller")] = m_versionController; + jobj[QStringLiteral("config_mgr")] = m_notebookConfigMgr; jobj[QStringLiteral("history")] = saveHistory(); + jobj[QStringLiteral("tag_graph")] = m_tagGraph; + jobj[QStringLiteral("extra_configs")] = m_extraConfigs; return jobj; @@ -64,32 +50,34 @@ QJsonObject NotebookConfig::toJson() const void NotebookConfig::fromJson(const QJsonObject &p_jobj) { - if (!p_jobj.contains(NotebookConfig::c_version) - || !p_jobj.contains(NotebookConfig::c_name) - || !p_jobj.contains(NotebookConfig::c_createdTimeUtc) - || !p_jobj.contains(NotebookConfig::c_versionController) - || !p_jobj.contains(NotebookConfig::c_configMgr)) { + if (!p_jobj.contains(QStringLiteral("version")) + || !p_jobj.contains(QStringLiteral("name")) + || !p_jobj.contains(QStringLiteral("created_time")) + || !p_jobj.contains(QStringLiteral("version_controller")) + || !p_jobj.contains(QStringLiteral("config_mgr"))) { Exception::throwOne(Exception::Type::InvalidArgument, QString("failed to read notebook configuration from JSON (%1)").arg(QJsonObjectToString(p_jobj))); return; } - 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(); - m_attachmentFolder = p_jobj[NotebookConfig::c_attachmentFolder].toString(); - m_createdTimeUtc = Utils::dateTimeFromStringUniform(p_jobj[NotebookConfig::c_createdTimeUtc].toString()); - m_versionController = p_jobj[NotebookConfig::c_versionController].toString(); - m_notebookConfigMgr = p_jobj[NotebookConfig::c_configMgr].toString(); + m_version = p_jobj[QStringLiteral("version")].toInt(); + m_name = p_jobj[QStringLiteral("name")].toString(); + m_description = p_jobj[QStringLiteral("description")].toString(); + m_imageFolder = p_jobj[QStringLiteral("image_folder")].toString(); + m_attachmentFolder = p_jobj[QStringLiteral("attachment_folder")].toString(); + m_createdTimeUtc = Utils::dateTimeFromStringUniform(p_jobj[QStringLiteral("created_time")].toString()); + m_versionController = p_jobj[QStringLiteral("version_controller")].toString(); + m_notebookConfigMgr = p_jobj[QStringLiteral("config_mgr")].toString(); loadHistory(p_jobj); + m_tagGraph = p_jobj[QStringLiteral("tag_graph")].toString(); + m_extraConfigs = p_jobj[QStringLiteral("extra_configs")].toObject(); } QSharedPointer NotebookConfig::fromNotebook(int p_version, - const Notebook *p_notebook) + const BundleNotebook *p_notebook) { auto config = QSharedPointer::create(); @@ -102,6 +90,7 @@ QSharedPointer NotebookConfig::fromNotebook(int p_version, config->m_versionController = p_notebook->getVersionController()->getName(); config->m_notebookConfigMgr = p_notebook->getConfigMgr()->getName(); config->m_history = p_notebook->getHistory(); + config->m_tagGraph = p_notebook->getTagGraph(); config->m_extraConfigs = p_notebook->getExtraConfigs(); return config; diff --git a/src/core/notebookconfigmgr/notebookconfig.h b/src/core/notebookconfigmgr/notebookconfig.h index e6c6be10..f068a0f6 100644 --- a/src/core/notebookconfigmgr/notebookconfig.h +++ b/src/core/notebookconfigmgr/notebookconfig.h @@ -15,6 +15,7 @@ namespace vnotex { class NotebookParameters; + // Notebook config of BundleNotebook. class NotebookConfig { public: @@ -24,7 +25,7 @@ namespace vnotex const NotebookParameters &p_paras); static QSharedPointer fromNotebook(int p_version, - const Notebook *p_notebook); + const BundleNotebook *p_notebook); virtual QJsonObject toJson() const; @@ -48,6 +49,9 @@ namespace vnotex QVector m_history; + // Graph of tags of this notebook like "parent>chlid;parent2>chlid2". + QString m_tagGraph; + // Hold all the extra configs for other components or 3rd party plugins. // Use a unique name as the key and the value is a QJsonObject. QJsonObject m_extraConfigs; @@ -56,22 +60,6 @@ namespace vnotex QJsonArray saveHistory() const; void loadHistory(const QJsonObject &p_jobj); - - static const QString c_version; - - static const QString c_name; - - static const QString c_description; - - static const QString c_imageFolder; - - static const QString c_attachmentFolder; - - static const QString c_createdTimeUtc; - - static const QString c_versionController; - - static const QString c_configMgr; }; } // ns vnotex diff --git a/src/core/sessionconfig.cpp b/src/core/sessionconfig.cpp index 270b7f14..179797c8 100644 --- a/src/core/sessionconfig.cpp +++ b/src/core/sessionconfig.cpp @@ -221,6 +221,7 @@ QJsonObject SessionConfig::saveStateAndGeometry() const writeByteArray(obj, QStringLiteral("main_window_state"), m_mainWindowStateGeometry.m_mainState); writeByteArray(obj, QStringLiteral("main_window_geometry"), m_mainWindowStateGeometry.m_mainGeometry); writeStringList(obj, QStringLiteral("visible_docks_before_expand"), m_mainWindowStateGeometry.m_visibleDocksBeforeExpand); + writeByteArray(obj, QStringLiteral("tag_explorer_state"), m_mainWindowStateGeometry.m_tagExplorerState); return obj; } @@ -351,6 +352,7 @@ void SessionConfig::loadStateAndGeometry(const QJsonObject &p_session) m_mainWindowStateGeometry.m_mainState = readByteArray(obj, QStringLiteral("main_window_state")); m_mainWindowStateGeometry.m_mainGeometry = readByteArray(obj, QStringLiteral("main_window_geometry")); m_mainWindowStateGeometry.m_visibleDocksBeforeExpand = readStringList(obj, QStringLiteral("visible_docks_before_expand")); + m_mainWindowStateGeometry.m_tagExplorerState = readByteArray(obj, QStringLiteral("tag_explorer_state")); } QByteArray SessionConfig::getViewAreaSessionAndClear() diff --git a/src/core/sessionconfig.h b/src/core/sessionconfig.h index 52b72edf..0926ba6f 100644 --- a/src/core/sessionconfig.h +++ b/src/core/sessionconfig.h @@ -36,7 +36,8 @@ namespace vnotex { return m_mainState == p_other.m_mainState && m_mainGeometry == p_other.m_mainGeometry - && m_visibleDocksBeforeExpand == p_other.m_visibleDocksBeforeExpand; + && m_visibleDocksBeforeExpand == p_other.m_visibleDocksBeforeExpand + && m_tagExplorerState == p_other.m_tagExplorerState; } QByteArray m_mainState; @@ -44,6 +45,8 @@ namespace vnotex QByteArray m_mainGeometry; QStringList m_visibleDocksBeforeExpand; + + QByteArray m_tagExplorerState; }; enum OpenGL diff --git a/src/core/widgetconfig.cpp b/src/core/widgetconfig.cpp index 80f65896..29bfa151 100644 --- a/src/core/widgetconfig.cpp +++ b/src/core/widgetconfig.cpp @@ -42,6 +42,8 @@ void WidgetConfig::init(const QJsonObject &p_app, m_mainWindowKeepDocksExpandingContentArea = READSTRLIST(QStringLiteral("main_window_keep_docks_expanding_content_area")); m_snippetPanelBuiltInSnippetsVisible = READBOOL(QStringLiteral("snippet_panel_builtin_snippets_visible")); + + m_tagExplorerTwoColumnsEnabled = READBOOL(QStringLiteral("tag_explorer_two_columns_enabled")); } QJsonObject WidgetConfig::toJson() const @@ -59,7 +61,7 @@ QJsonObject WidgetConfig::toJson() const obj[QStringLiteral("node_explorer_close_before_open_with_enabled")] = m_nodeExplorerCloseBeforeOpenWithEnabled; obj[QStringLiteral("search_panel_advanced_settings_visible")] = m_searchPanelAdvancedSettingsVisible; - obj[QStringLiteral("snippet_panel_builtin_snippets_visible")] = m_snippetPanelBuiltInSnippetsVisible; + obj[QStringLiteral("tag_explorer_two_columns_enabled")] = m_tagExplorerTwoColumnsEnabled; writeStringList(obj, QStringLiteral("main_window_keep_docks_expanding_content_area"), m_mainWindowKeepDocksExpandingContentArea); @@ -176,3 +178,12 @@ void WidgetConfig::setSnippetPanelBuiltInSnippetsVisible(bool p_visible) updateConfig(m_snippetPanelBuiltInSnippetsVisible, p_visible, this); } +bool WidgetConfig::getTagExplorerTwoColumnsEnabled() const +{ + return m_tagExplorerTwoColumnsEnabled; +} + +void WidgetConfig::setTagExplorerTwoColumnsEnabled(bool p_enabled) +{ + updateConfig(m_tagExplorerTwoColumnsEnabled, p_enabled, this); +} diff --git a/src/core/widgetconfig.h b/src/core/widgetconfig.h index c9378078..e70c5a2e 100644 --- a/src/core/widgetconfig.h +++ b/src/core/widgetconfig.h @@ -51,6 +51,9 @@ namespace vnotex bool isSnippetPanelBuiltInSnippetsVisible() const; void setSnippetPanelBuiltInSnippetsVisible(bool p_visible); + bool getTagExplorerTwoColumnsEnabled() const; + void setTagExplorerTwoColumnsEnabled(bool p_enabled); + private: int m_outlineAutoExpandedLevel = 6; @@ -74,6 +77,9 @@ namespace vnotex QStringList m_mainWindowKeepDocksExpandingContentArea; bool m_snippetPanelBuiltInSnippetsVisible = true; + + // Whether enable two columns for tag explorer. + bool m_tagExplorerTwoColumnsEnabled = false; }; } diff --git a/src/data/core/core.qrc b/src/data/core/core.qrc index 37dfa9aa..334c0a3a 100644 --- a/src/data/core/core.qrc +++ b/src/data/core/core.qrc @@ -15,7 +15,9 @@ icons/read_editor.svg icons/expand.svg icons/fullscreen.svg - icons/tag_explorer.svg + icons/tag_dock.svg + icons/tag.svg + icons/tag_selected.svg icons/help.svg icons/menu.svg icons/settings.svg @@ -34,7 +36,7 @@ icons/file_node.svg icons/folder_node.svg icons/manage_notebooks.svg - icons/up_parent_node.svg + icons/up_level.svg icons/properties.svg icons/recycle_bin.svg icons/scan_import.svg @@ -43,6 +45,7 @@ icons/buffer.svg icons/attachment_editor.svg icons/attachment_full_editor.svg + icons/tag_editor.svg icons/split_menu.svg icons/split_window_list.svg icons/type_heading_editor.svg diff --git a/src/data/core/icons/properties.svg b/src/data/core/icons/properties.svg index 768fefeb..6b15964e 100644 --- a/src/data/core/icons/properties.svg +++ b/src/data/core/icons/properties.svg @@ -1,10 +1 @@ - - - - - - - - - + \ No newline at end of file diff --git a/src/data/core/icons/recycle_bin.svg b/src/data/core/icons/recycle_bin.svg index 7e3ceb6a..c9fe395d 100644 --- a/src/data/core/icons/recycle_bin.svg +++ b/src/data/core/icons/recycle_bin.svg @@ -1,10 +1 @@ - - - Layer 1 - - - - Layer 1 copy - - - + \ No newline at end of file diff --git a/src/data/core/icons/sort.svg b/src/data/core/icons/sort.svg index 1059e3ea..cea6834c 100644 --- a/src/data/core/icons/sort.svg +++ b/src/data/core/icons/sort.svg @@ -1,8 +1 @@ - - - - Layer 2 - A - Z - - + \ No newline at end of file diff --git a/src/data/core/icons/tag_explorer.svg b/src/data/core/icons/tag.svg similarity index 100% rename from src/data/core/icons/tag_explorer.svg rename to src/data/core/icons/tag.svg diff --git a/src/data/core/icons/tag_dock.svg b/src/data/core/icons/tag_dock.svg new file mode 100644 index 00000000..05fa0e67 --- /dev/null +++ b/src/data/core/icons/tag_dock.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/src/data/core/icons/tag_editor.svg b/src/data/core/icons/tag_editor.svg new file mode 100644 index 00000000..05fa0e67 --- /dev/null +++ b/src/data/core/icons/tag_editor.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/src/data/core/icons/tag_selected.svg b/src/data/core/icons/tag_selected.svg new file mode 100644 index 00000000..493e1c79 --- /dev/null +++ b/src/data/core/icons/tag_selected.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/data/core/icons/up_parent_node.svg b/src/data/core/icons/up_level.svg similarity index 100% rename from src/data/core/icons/up_parent_node.svg rename to src/data/core/icons/up_level.svg diff --git a/src/data/core/icons/view.svg b/src/data/core/icons/view.svg index 176f2647..9729e6f5 100644 --- a/src/data/core/icons/view.svg +++ b/src/data/core/icons/view.svg @@ -1,15 +1 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/src/data/core/vnotex.json b/src/data/core/vnotex.json index beb7c755..bc9d7c40 100644 --- a/src/data/core/vnotex.json +++ b/src/data/core/vnotex.json @@ -23,6 +23,7 @@ "SnippetDock" : "Ctrl+G, S", "LocationListDock" : "Ctrl+G, C", "HistoryDock" : "", + "TagDock" : "", "Search" : "Ctrl+Alt+F", "NavigationMode" : "Ctrl+G, W", "LocateNode" : "Ctrl+G, D", @@ -117,7 +118,8 @@ "FindAndReplace" : "Ctrl+F", "FindNext" : "F3", "FindPrevious" : "Shift+F3", - "ApplySnippet" : "Ctrl+G, I" + "ApplySnippet" : "Ctrl+G, I", + "Tag" : "Ctrl+G, B" }, "spell_check_auto_detect_language" : false, "spell_check_default_dictionary" : "en_US", @@ -379,6 +381,7 @@ "search_panel_advanced_settings_visible" : true, "//comment" : "Docks to ignore when expanding content area of main window", "main_window_keep_docks_expanding_content_area": ["OutlineDock.vnotex"], - "snippet_panel_builtin_snippets_visible" : true + "snippet_panel_builtin_snippets_visible" : true, + "tag_explorer_two_columns_enabled" : true } } diff --git a/src/data/extra/docs/en/shortcuts.md b/src/data/extra/docs/en/shortcuts.md index 770f2f63..f5bccfd8 100644 --- a/src/data/extra/docs/en/shortcuts.md +++ b/src/data/extra/docs/en/shortcuts.md @@ -2,7 +2,7 @@ 1. All the keys without special notice are **case insensitive**; 2. On macOS, `Ctrl` corresponds to `Command` except in Vi mode; 3. The key sequence `Ctrl+G, I` means that first press both `Ctrl` and `G` simultaneously, release them, then press `I` and release; -4. For a **complete shortcuts list**, please view the `vnotex.json` configuration file. +4. For a **complete latest shortcuts list**, please view the `vnotex.json` configuration file. ## General - `Ctrl+G E` @@ -101,10 +101,10 @@ Insert italic. Press `Ctrl+I` again to exit. Current selected text will be chang Insert inline code. Press `Ctrl+;` again to exit. Current selected text will be changed to inline code if exists. - `Ctrl+'` Insert fenced code block. Press `Ctrl+'` again to exit. Current selected text will be wrapped into a code block if exists. -- `Ctrl+,` -Insert inline math. Press `Ctrl+,` again to exit. Current selected text will be changed to inline math if exists. - `Ctrl+.` -Insert math block. Press `Ctrl+.` again to exit. Current selected text will be changed to math block if exists. +Insert inline math. Press `Ctrl+.` again to exit. Current selected text will be changed to inline math if exists. +- `Ctrl+G, .` +Insert math block. Press `Ctrl+G, .` again to exit. Current selected text will be changed to math block if exists. - `Ctrl+/` Insert table. - `Ctrl+` diff --git a/src/data/extra/docs/zh_CN/shortcuts.md b/src/data/extra/docs/zh_CN/shortcuts.md index a7bffcbc..d3c76963 100644 --- a/src/data/extra/docs/zh_CN/shortcuts.md +++ b/src/data/extra/docs/zh_CN/shortcuts.md @@ -2,7 +2,7 @@ 1. 以下按键除特别说明外,都不区分大小写; 2. 在 macOS 下,`Ctrl` 对应于 `Command`,在 Vi 模式下除外; 3. 按键序列 `Ctrl+G, I` 表示先同时按下 `Ctrl` 和 `G`,释放,然后按下 `I` 并释放; -4. 可以通过查看配置文件 `vnotex.json` 来获取一个**完整的快捷键列表**。 +4. 可以通过查看配置文件 `vnotex.json` 来获取一个**完整的最新的快捷键列表**。 ## 通用 - `Ctrl+G E` @@ -101,10 +101,10 @@ VNote 的很多部件均支持`Ctrl+J`和`Ctrl+K`导航。 插入行内代码;再次按`Ctrl+;`退出。如果已经选择文本,则将当前选择文本改为行内代码。 - `Ctrl+'` 插入代码块;再次按`Ctrl+'`退出。如果已经选择文本,则将当前选择文本嵌入到代码块中。 -- `Ctrl+,` -插入公式;再次按`Ctrl+,`退出。如果已经选择文本,则将当前选择文本改为公式。 - `Ctrl+.` -插入公式块;再次按`Ctrl+.`退出。如果已经选择文本,则将当前选择文本改为公式块。 +插入公式;再次按`Ctrl+.`退出。如果已经选择文本,则将当前选择文本改为公式。 +- `Ctrl+G, .` +插入公式块;再次按`Ctrl+G, .`退出。如果已经选择文本,则将当前选择文本改为公式块。 - `Ctrl+/` 插入表格。 - `Ctrl+` diff --git a/src/main.cpp b/src/main.cpp index e9092ca7..a3220778 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -48,7 +48,7 @@ int main(int argc, char *argv[]) #if defined(Q_OS_WIN) { auto option = SessionConfig::getOpenGLAtBootstrap(); - qInfo() << "OpenGL option" << SessionConfig::openGLToString(option); + qDebug() << "OpenGL option" << SessionConfig::openGLToString(option); switch (option) { case SessionConfig::OpenGL::Desktop: QCoreApplication::setAttribute(Qt::AA_UseDesktopOpenGL); diff --git a/src/search/searcher.cpp b/src/search/searcher.cpp index 1b6becb7..2da88547 100644 --- a/src/search/searcher.cpp +++ b/src/search/searcher.cpp @@ -470,6 +470,11 @@ bool Searcher::firstPhaseSearch(Notebook *p_notebook, QVectorisRecycleBinNode(child.data())) { + qDebug() << "skipped searching recycle bin"; + continue; + } + if (child->hasContent() && testTarget(SearchTarget::SearchFile)) { if (!firstPhaseSearch(child.data(), p_secondPhaseItems)) { return false; diff --git a/src/utils/pathutils.cpp b/src/utils/pathutils.cpp index 530f8a9b..1e21ce1c 100644 --- a/src/utils/pathutils.cpp +++ b/src/utils/pathutils.cpp @@ -4,8 +4,6 @@ #include #include #include -#include -#include using namespace vnotex; @@ -138,13 +136,12 @@ bool PathUtils::isLegalPath(const QString &p_path) } bool ret = false; - int pos = -1; QString basePath = parentDirPath(p_path); QString name = dirName(p_path); - QScopedPointer validator(new QRegularExpressionValidator(QRegularExpression(c_fileNameRegularExpression))); + QRegularExpression nameRegExp(c_fileNameRegularExpression); while (!name.isEmpty()) { - QValidator::State validFile = validator->validate(name, pos); - if (validFile != QValidator::Acceptable) { + auto match = nameRegExp.match(name); + if (!match.hasMatch()) { break; } diff --git a/src/widgets/dialogs/levellabelwithupbutton.cpp b/src/widgets/dialogs/levellabelwithupbutton.cpp new file mode 100644 index 00000000..3c7df606 --- /dev/null +++ b/src/widgets/dialogs/levellabelwithupbutton.cpp @@ -0,0 +1,80 @@ +#include "levellabelwithupbutton.h" + +#include +#include +#include + +#include +#include + +using namespace vnotex; + +LevelLabelWithUpButton::LevelLabelWithUpButton(QWidget *p_parent) + : QWidget(p_parent) +{ + setupUI(); +} + +void LevelLabelWithUpButton::setupUI() +{ + auto mainLayout = new QHBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); + + m_label = new QLabel(this); + mainLayout->addWidget(m_label, 1); + + const auto iconFile = VNoteX::getInst().getThemeMgr().getIconFile("up_level.svg"); + m_upButton = new QPushButton(IconUtils::fetchIconWithDisabledState(iconFile), + tr("Up"), + this); + m_upButton->setToolTip(tr("Go one level up")); + connect(m_upButton, &QPushButton::clicked, + this, [this]() { + if (m_levelIdx < m_levels.size() - 1) { + ++m_levelIdx; + updateLabelAndButton(); + emit levelChanged(); + } + }); + mainLayout->addWidget(m_upButton, 0); + + updateLabelAndButton(); +} + +void LevelLabelWithUpButton::updateLabelAndButton() +{ + if (m_levels.isEmpty()) { + m_label->clear(); + } else { + Q_ASSERT(m_levelIdx < m_levels.size()); + m_label->setText(m_levels[m_levelIdx].m_name); + } + + m_upButton->setVisible(!m_readOnly && (m_levelIdx < m_levels.size() - 1)); +} + +const LevelLabelWithUpButton::Level &LevelLabelWithUpButton::getLevel() const +{ + Q_ASSERT(m_levelIdx < m_levels.size()); + return m_levels[m_levelIdx]; +} + +void LevelLabelWithUpButton::setLevels(const QVector &p_levels) +{ + m_levels = p_levels; + Q_ASSERT(!m_levels.isEmpty()); + m_levelIdx = 0; + + updateLabelAndButton(); + emit levelChanged(); +} + +void LevelLabelWithUpButton::setReadOnly(bool p_readonly) +{ + if (m_readOnly == p_readonly) { + return; + } + + m_readOnly = p_readonly; + updateLabelAndButton(); +} diff --git a/src/widgets/dialogs/levellabelwithupbutton.h b/src/widgets/dialogs/levellabelwithupbutton.h new file mode 100644 index 00000000..eac59e6c --- /dev/null +++ b/src/widgets/dialogs/levellabelwithupbutton.h @@ -0,0 +1,52 @@ +#ifndef LEVELLABELWITHUPBUTTON_H +#define LEVELLABELWITHUPBUTTON_H + +#include + +class QLabel; +class QPushButton; + +namespace vnotex +{ + // Used to navigate through a series of levels. + class LevelLabelWithUpButton : public QWidget + { + Q_OBJECT + public: + struct Level + { + QString m_name; + + const void *m_data = nullptr; + }; + + LevelLabelWithUpButton(QWidget *p_parent = nullptr); + + const Level &getLevel() const; + + // From bottom to up. + void setLevels(const QVector &p_levels); + + void setReadOnly(bool p_readonly); + + signals: + void levelChanged(); + + private: + void setupUI(); + + void updateLabelAndButton(); + + QLabel *m_label = nullptr; + + QPushButton *m_upButton = nullptr; + + QVector m_levels; + + int m_levelIdx = -1; + + bool m_readOnly = false; + }; +} // ns vnotex + +#endif // LEVELLABELWITHUPBUTTON_H diff --git a/src/widgets/dialogs/newfolderdialog.cpp b/src/widgets/dialogs/newfolderdialog.cpp index fe5888d3..aeac5c01 100644 --- a/src/widgets/dialogs/newfolderdialog.cpp +++ b/src/widgets/dialogs/newfolderdialog.cpp @@ -2,11 +2,12 @@ #include -#include "notebook/notebook.h" -#include "notebook/node.h" -#include "../widgetsfactory.h" +#include +#include #include -#include "exception.h" +#include + +#include "../widgetsfactory.h" #include "nodeinfowidget.h" using namespace vnotex; diff --git a/src/widgets/dialogs/newtagdialog.cpp b/src/widgets/dialogs/newtagdialog.cpp new file mode 100644 index 00000000..2cfff0a5 --- /dev/null +++ b/src/widgets/dialogs/newtagdialog.cpp @@ -0,0 +1,104 @@ +#include "newtagdialog.h" + +#include + +#include + +#include "../widgetsfactory.h" +#include "levellabelwithupbutton.h" +#include "../lineeditwithsnippet.h" + +using namespace vnotex; + +NewTagDialog::NewTagDialog(TagI *p_tagI, Tag *p_tag, QWidget *p_parent) + : ScrollDialog(p_parent), + m_tagI(p_tagI), + m_parentTag(p_tag) +{ + setupUI(); + + m_nameLineEdit->setFocus(); +} + +static QVector tagToLevels(const Tag *p_tag) +{ + QVector levels; + while (p_tag) { + LevelLabelWithUpButton::Level level; + level.m_name = p_tag->fetchPath(); + level.m_data = static_cast(p_tag); + levels.push_back(level); + p_tag = p_tag->getParent(); + } + + // Append an empty level. + levels.push_back(LevelLabelWithUpButton::Level()); + + return levels; +} + +void NewTagDialog::setupUI() +{ + auto mainWidget = new QWidget(this); + setCentralWidget(mainWidget); + + auto mainLayout = WidgetsFactory::createFormLayout(mainWidget); + + { + m_parentTagLabel = new LevelLabelWithUpButton(this); + m_parentTagLabel->setLevels(tagToLevels(m_parentTag)); + mainLayout->addRow(tr("Location:"), m_parentTagLabel); + } + + m_nameLineEdit = WidgetsFactory::createLineEditWithSnippet(mainWidget); + mainLayout->addRow(tr("Name:"), m_nameLineEdit); + + setDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + + setWindowTitle(tr("New Tag")); +} + +bool NewTagDialog::validateInputs() +{ + bool valid = true; + QString msg; + + auto name = getTagName(); + if (!Tag::isValidName(name)) { + valid = false; + msg = tr("Please specify a valid name for the tag."); + } else if (m_tagI->findTag(name)) { + valid = false; + msg = tr("Name conflicts with existing tag."); + } + + setInformationText(msg, valid ? ScrollDialog::InformationLevel::Info + : ScrollDialog::InformationLevel::Error); + return valid; +} + +bool NewTagDialog::newTag() +{ + const Tag *parentTag = static_cast(m_parentTagLabel->getLevel().m_data); + const auto parentName = parentTag ? parentTag->name() : QString(); + const auto name = getTagName(); + if (!m_tagI->newTag(name, parentName)) { + setInformationText(tr("Failed to create tag (%1).").arg(name), ScrollDialog::InformationLevel::Error); + // Tags maybe updated. Don't allow operation for now. + setButtonEnabled(QDialogButtonBox::Ok, false); + return false; + } + return true; +} + +void NewTagDialog::acceptedButtonClicked() +{ + if (validateInputs() && newTag()) { + accept(); + } +} + +QString NewTagDialog::getTagName() const +{ + return m_nameLineEdit->evaluatedText(); +} diff --git a/src/widgets/dialogs/newtagdialog.h b/src/widgets/dialogs/newtagdialog.h new file mode 100644 index 00000000..ff52ee44 --- /dev/null +++ b/src/widgets/dialogs/newtagdialog.h @@ -0,0 +1,42 @@ +#ifndef NEWTAGDIALOG_H +#define NEWTAGDIALOG_H + +#include "scrolldialog.h" + +namespace vnotex +{ + class TagI; + class Tag; + class LevelLabelWithUpButton; + class LineEditWithSnippet; + + class NewTagDialog : public ScrollDialog + { + Q_OBJECT + public: + // New a tag under @p_tag. + NewTagDialog(TagI *p_tagI, Tag *p_tag, QWidget *p_parent = nullptr); + + protected: + void acceptedButtonClicked() Q_DECL_OVERRIDE; + + private: + void setupUI(); + + bool validateInputs(); + + bool newTag(); + + QString getTagName() const; + + TagI *m_tagI = nullptr; + + Tag *m_parentTag = nullptr; + + LevelLabelWithUpButton *m_parentTagLabel = nullptr; + + LineEditWithSnippet *m_nameLineEdit = nullptr; + }; +} + +#endif // NEWTAGDIALOG_H diff --git a/src/widgets/dialogs/nodeinfowidget.cpp b/src/widgets/dialogs/nodeinfowidget.cpp index 2dd74b18..2c0a8efb 100644 --- a/src/widgets/dialogs/nodeinfowidget.cpp +++ b/src/widgets/dialogs/nodeinfowidget.cpp @@ -10,7 +10,7 @@ #include #include #include "exception.h" -#include "nodelabelwithupbutton.h" +#include "levellabelwithupbutton.h" #include #include #include "../lineeditwithsnippet.h" @@ -35,6 +35,20 @@ NodeInfoWidget::NodeInfoWidget(const Node *p_parentNode, setupUI(p_parentNode, p_flags); } +static QVector nodeToLevels(const Node *p_node) +{ + QVector levels; + while (p_node) { + LevelLabelWithUpButton::Level level; + level.m_name = p_node->fetchPath(); + level.m_data = static_cast(p_node); + levels.push_back(level); + p_node = p_node->getParent(); + } + + return levels; +} + void NodeInfoWidget::setupUI(const Node *p_parentNode, Node::Flags p_newNodeFlags) { const bool createMode = m_mode == Mode::Create; @@ -45,11 +59,14 @@ void NodeInfoWidget::setupUI(const Node *p_parentNode, Node::Flags p_newNodeFlag m_mainLayout->addRow(tr("Notebook:"), new QLabel(p_parentNode->getNotebook()->getName(), this)); - m_parentNodeLabel = new NodeLabelWithUpButton(p_parentNode, this); - m_parentNodeLabel->setReadOnly(!createMode); - connect(m_parentNodeLabel, &NodeLabelWithUpButton::nodeChanged, - this, &NodeInfoWidget::inputEdited); - m_mainLayout->addRow(tr("Location:"), m_parentNodeLabel); + { + m_parentNodeLabel = new LevelLabelWithUpButton(this); + m_parentNodeLabel->setReadOnly(!createMode); + m_parentNodeLabel->setLevels(nodeToLevels(p_parentNode)); + connect(m_parentNodeLabel, &LevelLabelWithUpButton::levelChanged, + this, &NodeInfoWidget::inputEdited); + m_mainLayout->addRow(tr("Location:"), m_parentNodeLabel); + } if (createMode && isNote) { setupFileTypeComboBox(this); @@ -115,7 +132,7 @@ const Notebook *NodeInfoWidget::getNotebook() const const Node *NodeInfoWidget::getParentNode() const { - return m_parentNodeLabel->getNode(); + return static_cast(m_parentNodeLabel->getLevel().m_data); } void NodeInfoWidget::setNode(const Node *p_node) @@ -129,7 +146,7 @@ void NodeInfoWidget::setNode(const Node *p_node) if (m_node) { Q_ASSERT(getNotebook() == m_node->getNotebook()); m_nameLineEdit->setText(m_node->getName()); - m_parentNodeLabel->setNode(m_node->getParent()); + m_parentNodeLabel->setLevels(nodeToLevels(m_node->getParent())); auto createdTime = Utils::dateTimeString(m_node->getCreatedTimeUtc().toLocalTime()); m_createdDateTimeLabel->setText(createdTime); diff --git a/src/widgets/dialogs/nodeinfowidget.h b/src/widgets/dialogs/nodeinfowidget.h index c044a90d..81f4d803 100644 --- a/src/widgets/dialogs/nodeinfowidget.h +++ b/src/widgets/dialogs/nodeinfowidget.h @@ -13,7 +13,7 @@ class QComboBox; namespace vnotex { class Notebook; - class NodeLabelWithUpButton; + class LevelLabelWithUpButton; class LineEditWithSnippet; class NodeInfoWidget : public QWidget @@ -59,7 +59,7 @@ namespace vnotex LineEditWithSnippet *m_nameLineEdit = nullptr; - NodeLabelWithUpButton *m_parentNodeLabel = nullptr; + LevelLabelWithUpButton *m_parentNodeLabel = nullptr; QLabel *m_createdDateTimeLabel = nullptr; diff --git a/src/widgets/dialogs/nodelabelwithupbutton.cpp b/src/widgets/dialogs/nodelabelwithupbutton.cpp deleted file mode 100644 index 35bf0f62..00000000 --- a/src/widgets/dialogs/nodelabelwithupbutton.cpp +++ /dev/null @@ -1,76 +0,0 @@ -#include "nodelabelwithupbutton.h" - -#include -#include -#include - -#include "notebook/node.h" -#include -#include "vnotex.h" - -using namespace vnotex; - -NodeLabelWithUpButton::NodeLabelWithUpButton(const Node *p_node, QWidget *p_parent) - : QWidget(p_parent), - m_node(p_node) -{ - setupUI(); -} - -void NodeLabelWithUpButton::setupUI() -{ - auto mainLayout = new QHBoxLayout(this); - mainLayout->setContentsMargins(0, 0, 0, 0); - - m_label = new QLabel(this); - mainLayout->addWidget(m_label, 1); - - auto iconFile = VNoteX::getInst().getThemeMgr().getIconFile("up_parent_node.svg"); - m_upButton = new QPushButton(IconUtils::fetchIconWithDisabledState(iconFile), - tr("Up"), - this); - m_upButton->setToolTip(tr("Create note under an upper level node")); - connect(m_upButton, &QPushButton::clicked, - this, [this]() { - if (!m_node->isRoot()) { - m_node = m_node->getParent(); - updateLabelAndButton(); - emit nodeChanged(m_node); - } - }); - mainLayout->addWidget(m_upButton, 0); - - updateLabelAndButton(); -} - -void NodeLabelWithUpButton::updateLabelAndButton() -{ - m_label->setText(m_node->fetchPath()); - m_upButton->setVisible(!m_readOnly && !m_node->isRoot()); -} - -const Node *NodeLabelWithUpButton::getNode() const -{ - return m_node; -} - -void NodeLabelWithUpButton::setNode(const Node *p_node) -{ - if (m_node == p_node) { - return; - } - - m_node = p_node; - updateLabelAndButton(); - emit nodeChanged(m_node); -} - -void NodeLabelWithUpButton::setReadOnly(bool p_readonly) -{ - if (m_readOnly == p_readonly) { - return; - } - - m_readOnly = p_readonly; - updateLabelAndButton(); -} diff --git a/src/widgets/dialogs/nodelabelwithupbutton.h b/src/widgets/dialogs/nodelabelwithupbutton.h deleted file mode 100644 index c17a8a4a..00000000 --- a/src/widgets/dialogs/nodelabelwithupbutton.h +++ /dev/null @@ -1,43 +0,0 @@ -#ifndef NODELABELWITHUPBUTTON_H -#define NODELABELWITHUPBUTTON_H - -#include - -class QLabel; -class QPushButton; - -namespace vnotex -{ - class Node; - - class NodeLabelWithUpButton : public QWidget - { - Q_OBJECT - public: - NodeLabelWithUpButton(const Node *p_node, QWidget *p_parent = nullptr); - - const Node *getNode() const; - - void setNode(const Node *p_node); - - void setReadOnly(bool p_readonly); - - signals: - void nodeChanged(const Node *p_node); - - private: - void setupUI(); - - void updateLabelAndButton(); - - QLabel *m_label = nullptr; - - QPushButton *m_upButton = nullptr; - - const Node *m_node = nullptr; - - bool m_readOnly = false; - }; -} // ns vnotex - -#endif // NODELABELWITHUPBUTTON_H diff --git a/src/widgets/dialogs/notepropertiesdialog.cpp b/src/widgets/dialogs/notepropertiesdialog.cpp index 5a614166..7c6262f8 100644 --- a/src/widgets/dialogs/notepropertiesdialog.cpp +++ b/src/widgets/dialogs/notepropertiesdialog.cpp @@ -1,15 +1,16 @@ #include "notepropertiesdialog.h" -#include "notebook/notebook.h" -#include "notebook/node.h" -#include "../widgetsfactory.h" +#include +#include #include -#include "exception.h" -#include "nodeinfowidget.h" -#include "../lineedit.h" +#include #include #include +#include "../widgetsfactory.h" +#include "nodeinfowidget.h" +#include "../lineedit.h" + using namespace vnotex; NotePropertiesDialog::NotePropertiesDialog(Node *p_node, QWidget *p_parent) diff --git a/src/widgets/dialogs/renametagdialog.cpp b/src/widgets/dialogs/renametagdialog.cpp new file mode 100644 index 00000000..5ec1318d --- /dev/null +++ b/src/widgets/dialogs/renametagdialog.cpp @@ -0,0 +1,82 @@ +#include "renametagdialog.h" + +#include + +#include +#include + +#include "../widgetsfactory.h" +#include "../lineeditwithsnippet.h" + +using namespace vnotex; + +RenameTagDialog::RenameTagDialog(TagI *p_tagI, const QString &p_name, QWidget *p_parent) + : ScrollDialog(p_parent), + m_tagI(p_tagI), + m_tagName(p_name) +{ + setupUI(); + + m_nameLineEdit->setFocus(); + WidgetUtils::selectBaseName(m_nameLineEdit); +} + +void RenameTagDialog::setupUI() +{ + auto mainWidget = new QWidget(this); + setCentralWidget(mainWidget); + + auto mainLayout = WidgetsFactory::createFormLayout(mainWidget); + + m_nameLineEdit = WidgetsFactory::createLineEditWithSnippet(m_tagName, mainWidget); + mainLayout->addRow(tr("Name:"), m_nameLineEdit); + + setDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + + setWindowTitle(tr("Rename Tag")); +} + +bool RenameTagDialog::validateInputs() +{ + bool valid = true; + QString msg; + + auto name = getTagName(); + if (name == m_tagName) { + return true; + } + + if (!Tag::isValidName(name)) { + valid = false; + msg = tr("Please specify a valid name for the tag."); + } else if (m_tagI->findTag(name)) { + valid = false; + msg = tr("Name conflicts with existing tag."); + } + + setInformationText(msg, valid ? ScrollDialog::InformationLevel::Info + : ScrollDialog::InformationLevel::Error); + return valid; +} + +bool RenameTagDialog::renameTag() +{ + if (!m_tagI->renameTag(m_tagName, getTagName())) { + setInformationText(tr("Failed to rename tag (%1) to (%2).").arg(m_tagName, getTagName()), ScrollDialog::InformationLevel::Error); + return false; + } + + return true; +} + +void RenameTagDialog::acceptedButtonClicked() +{ + if (validateInputs() && renameTag()) { + accept(); + } +} + +QString RenameTagDialog::getTagName() const +{ + return m_nameLineEdit->evaluatedText(); +} diff --git a/src/widgets/dialogs/renametagdialog.h b/src/widgets/dialogs/renametagdialog.h new file mode 100644 index 00000000..10b703e9 --- /dev/null +++ b/src/widgets/dialogs/renametagdialog.h @@ -0,0 +1,37 @@ +#ifndef RENAMETAGDIALOG_H +#define RENAMETAGDIALOG_H + +#include "scrolldialog.h" + +namespace vnotex +{ + class TagI; + class LineEditWithSnippet; + + class RenameTagDialog : public ScrollDialog + { + Q_OBJECT + public: + RenameTagDialog(TagI *p_tagI, const QString &p_name, QWidget *p_parent = nullptr); + + QString getTagName() const; + + protected: + void acceptedButtonClicked() Q_DECL_OVERRIDE; + + private: + void setupUI(); + + bool validateInputs(); + + bool renameTag(); + + TagI *m_tagI = nullptr; + + const QString m_tagName; + + LineEditWithSnippet *m_nameLineEdit = nullptr; + }; +} + +#endif // RENAMETAGDIALOG_H diff --git a/src/widgets/dialogs/sortdialog.cpp b/src/widgets/dialogs/sortdialog.cpp index b5c28326..cee3bbb6 100644 --- a/src/widgets/dialogs/sortdialog.cpp +++ b/src/widgets/dialogs/sortdialog.cpp @@ -44,22 +44,6 @@ void SortDialog::setupUI(const QString &p_title, const QString &p_info) m_treeWidget->setRootIsDecorated(false); m_treeWidget->setSelectionMode(QAbstractItemView::ContiguousSelection); m_treeWidget->setDragDropMode(QAbstractItemView::InternalMove); - connect(static_cast(m_treeWidget), &TreeWidget::rowsMoved, - this, [this](int p_first, int p_last, int p_row) { - auto item = m_treeWidget->topLevelItem(p_row); - if (item) { - // Keep all items selected. - m_treeWidget->setCurrentItem(item); - - const int cnt = p_last - p_first + 1; - for (int i = 0; i < cnt; ++i) { - auto it = m_treeWidget->topLevelItem(p_row + i); - if (it) { - it->setSelected(true); - } - } - } - }); bodyLayout->addWidget(m_treeWidget); // Buttons for top/up/down/bottom. diff --git a/src/widgets/dialogs/viewtagsdialog.cpp b/src/widgets/dialogs/viewtagsdialog.cpp new file mode 100644 index 00000000..fa92569d --- /dev/null +++ b/src/widgets/dialogs/viewtagsdialog.cpp @@ -0,0 +1,58 @@ +#include "viewtagsdialog.h" + +#include +#include + +#include + +#include "../widgetsfactory.h" +#include "../tagviewer.h" + +using namespace vnotex; + +ViewTagsDialog::ViewTagsDialog(Node *p_node, QWidget *p_parent) + : Dialog(p_parent) +{ + setupUI(); + + setNode(p_node); + + m_tagViewer->setFocus(); +} + +void ViewTagsDialog::setupUI() +{ + auto mainWidget = new QWidget(this); + setCentralWidget(mainWidget); + + auto mainLayout = WidgetsFactory::createFormLayout(mainWidget); + + m_nodeNameLabel = new QLabel(mainWidget); + mainLayout->addRow(tr("Name:"), m_nodeNameLabel); + + m_tagViewer = new TagViewer(mainWidget); + mainLayout->addRow(tr("Tags:"), m_tagViewer); + + setDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + + setWindowTitle(tr("Tags")); +} + +void ViewTagsDialog::acceptedButtonClicked() +{ + m_tagViewer->save(); + accept(); +} + +void ViewTagsDialog::setNode(Node *p_node) +{ + if (m_node == p_node) { + return; + } + + m_node = p_node; + Q_ASSERT(m_node); + + m_nodeNameLabel->setText(m_node->getName()); + m_tagViewer->setNode(m_node); +} diff --git a/src/widgets/dialogs/viewtagsdialog.h b/src/widgets/dialogs/viewtagsdialog.h new file mode 100644 index 00000000..4abc15c2 --- /dev/null +++ b/src/widgets/dialogs/viewtagsdialog.h @@ -0,0 +1,35 @@ +#ifndef VIEWTAGSDIALOG_H +#define VIEWTAGSDIALOG_H + +#include "dialog.h" + +class QLabel; + +namespace vnotex +{ + class Node; + class TagViewer; + + class ViewTagsDialog : public Dialog + { + Q_OBJECT + public: + ViewTagsDialog(Node *p_node, QWidget *p_parent = nullptr); + + protected: + void acceptedButtonClicked() Q_DECL_OVERRIDE; + + private: + void setupUI(); + + void setNode(Node *p_node); + + Node *m_node = nullptr; + + QLabel *m_nodeNameLabel = nullptr; + + TagViewer *m_tagViewer = nullptr; + }; +} + +#endif // VIEWTAGSDIALOG_H diff --git a/src/widgets/dockwidgethelper.cpp b/src/widgets/dockwidgethelper.cpp index 6aca18f2..c7ddaabb 100644 --- a/src/widgets/dockwidgethelper.cpp +++ b/src/widgets/dockwidgethelper.cpp @@ -23,6 +23,7 @@ #include "searchpanel.h" #include "snippetpanel.h" #include "historypanel.h" +#include "tagexplorer.h" using namespace vnotex; @@ -68,6 +69,8 @@ QString DockWidgetHelper::iconFileName(DockIndex p_dockIndex) return "outline_dock.svg"; case DockIndex::HistoryDock: return "history_dock.svg"; + case DockIndex::TagDock: + return "tag_dock.svg"; case DockIndex::SearchDock: return "search_dock.svg"; case DockIndex::SnippetDock: @@ -95,17 +98,20 @@ void DockWidgetHelper::setupDocks() tabifiedDockIndex.append(m_docks.size()); setupNavigationDock(); - setupOutlineDock(); - tabifiedDockIndex.append(m_docks.size()); setupHistoryDock(); + tabifiedDockIndex.append(m_docks.size()); + setupTagDock(); + tabifiedDockIndex.append(m_docks.size()); setupSearchDock(); tabifiedDockIndex.append(m_docks.size()); setupSnippetDock(); + setupOutlineDock(); + setupLocationListDock(); setupShortcuts(); @@ -175,6 +181,18 @@ void DockWidgetHelper::setupHistoryDock() m_mainWindow->addDockWidget(Qt::LeftDockWidgetArea, dock); } +void DockWidgetHelper::setupTagDock() +{ + auto dock = createDockWidget(DockIndex::TagDock, tr("Tags"), m_mainWindow); + + dock->setObjectName(QStringLiteral("TagDock.vnotex")); + dock->setAllowedAreas(Qt::AllDockWidgetAreas); + + dock->setWidget(m_mainWindow->m_tagExplorer); + dock->setFocusProxy(m_mainWindow->m_tagExplorer); + m_mainWindow->addDockWidget(Qt::LeftDockWidgetArea, dock); +} + void DockWidgetHelper::setupLocationListDock() { auto dock = createDockWidget(DockIndex::LocationListDock, tr("Location List"), m_mainWindow); @@ -248,6 +266,9 @@ void DockWidgetHelper::setupShortcuts() setupDockActivateShortcut(m_docks[DockIndex::HistoryDock], coreConfig.getShortcut(CoreConfig::Shortcut::HistoryDock)); + setupDockActivateShortcut(m_docks[DockIndex::TagDock], + coreConfig.getShortcut(CoreConfig::Shortcut::TagDock)); + setupDockActivateShortcut(m_docks[DockIndex::SearchDock], coreConfig.getShortcut(CoreConfig::Shortcut::SearchDock)); // Extra shortcut for SearchDock. diff --git a/src/widgets/dockwidgethelper.h b/src/widgets/dockwidgethelper.h index abfffba2..e71e4001 100644 --- a/src/widgets/dockwidgethelper.h +++ b/src/widgets/dockwidgethelper.h @@ -24,10 +24,11 @@ namespace vnotex enum DockIndex { NavigationDock = 0, - OutlineDock, HistoryDock, + TagDock, SearchDock, SnippetDock, + OutlineDock, LocationListDock, MaxDock }; @@ -102,6 +103,8 @@ namespace vnotex void setupHistoryDock(); + void setupTagDock(); + void setupLocationListDock(); QDockWidget *createDockWidget(DockIndex p_dockIndex, const QString &p_title, QWidget *p_parent); diff --git a/src/widgets/historypanel.cpp b/src/widgets/historypanel.cpp index cb48abd8..e301bce4 100644 --- a/src/widgets/historypanel.cpp +++ b/src/widgets/historypanel.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -18,26 +19,33 @@ #include "listwidget.h" #include "mainwindow.h" #include "messageboxhelper.h" +#include "navigationmodemgr.h" using namespace vnotex; HistoryPanel::HistoryPanel(QWidget *p_parent) : QFrame(p_parent) { + initIcons(); + setupUI(); updateSeparators(); } +void HistoryPanel::initIcons() +{ + const auto &themeMgr = VNoteX::getInst().getThemeMgr(); + m_fileIcon = IconUtils::fetchIcon(themeMgr.getIconFile(QStringLiteral("file_node.svg"))); +} + void HistoryPanel::setupUI() { auto mainLayout = new QVBoxLayout(this); WidgetUtils::setContentsMargins(mainLayout); - { - setupTitleBar(QString(), this); - mainLayout->addWidget(m_titleBar); - } + setupTitleBar(QString(), this); + mainLayout->addWidget(m_titleBar); m_historyList = new ListWidget(true, this); m_historyList->setContextMenuPolicy(Qt::CustomContextMenu); @@ -48,6 +56,9 @@ void HistoryPanel::setupUI() this, &HistoryPanel::openItem); mainLayout->addWidget(m_historyList); + m_navigationWrapper.reset(new NavigationModeWrapper(m_historyList)); + NavigationModeMgr::getInst().registerNavigationTarget(m_navigationWrapper.data()); + setFocusProxy(m_historyList); } @@ -132,8 +143,6 @@ bool HistoryPanel::isValidItem(const QListWidgetItem *p_item) const void HistoryPanel::updateHistoryList() { - m_pendingUpdate = false; - m_historyList->clear(); const auto &history = HistoryMgr::getInst().getHistory(); @@ -168,19 +177,12 @@ void HistoryPanel::updateHistoryList() } } -void HistoryPanel::showEvent(QShowEvent *p_event) +void HistoryPanel::initialize() { - QFrame::showEvent(p_event); + connect(&HistoryMgr::getInst(), &HistoryMgr::historyUpdated, + this, &HistoryPanel::updateHistoryList); - if (!m_initialized) { - m_initialized = true; - connect(&HistoryMgr::getInst(), &HistoryMgr::historyUpdated, - this, &HistoryPanel::updateHistoryListIfProper); - } - - if (m_pendingUpdate) { - updateHistoryList(); - } + updateHistoryList(); } void HistoryPanel::updateSeparators() @@ -199,21 +201,13 @@ void HistoryPanel::updateSeparators() m_separators[2].m_dateUtc = curDateTime.addDays(-7).toUTC(); } -void HistoryPanel::updateHistoryListIfProper() -{ - if (isVisible()) { - updateHistoryList(); - } else { - m_pendingUpdate = true; - } -} - void HistoryPanel::addItem(const HistoryItemFull &p_hisItem) { auto item = new QListWidgetItem(m_historyList); item->setText(PathUtils::fileNameCheap(p_hisItem.m_item.m_path)); item->setData(Qt::UserRole, p_hisItem.m_item.m_path); + item->setIcon(m_fileIcon); if (p_hisItem.m_notebookName.isEmpty()) { item->setToolTip(tr("%1\n%2").arg(p_hisItem.m_item.m_path, Utils::dateTimeString(p_hisItem.m_item.m_lastAccessedTimeUtc.toLocalTime()))); diff --git a/src/widgets/historypanel.h b/src/widgets/historypanel.h index fdb07127..f7246d81 100644 --- a/src/widgets/historypanel.h +++ b/src/widgets/historypanel.h @@ -3,6 +3,10 @@ #include #include +#include +#include + +#include "navigationmodewrapper.h" class QListWidget; class QListWidgetItem; @@ -18,8 +22,7 @@ namespace vnotex public: explicit HistoryPanel(QWidget *p_parent = nullptr); - protected: - void showEvent(QShowEvent *p_event) Q_DECL_OVERRIDE; + void initialize(); private slots: void handleContextMenuRequested(const QPoint &p_pos); @@ -36,14 +39,14 @@ namespace vnotex QDateTime m_dateUtc; }; + void initIcons(); + void setupUI(); void setupTitleBar(const QString &p_title, QWidget *p_parent = nullptr); void updateHistoryList(); - void updateHistoryListIfProper(); - void updateSeparators(); void addItem(const HistoryItemFull &p_hisItem); @@ -56,11 +59,11 @@ namespace vnotex QListWidget *m_historyList = nullptr; - bool m_initialized = false; - - bool m_pendingUpdate = true; + QScopedPointer> m_navigationWrapper; QVector m_separators; + + QIcon m_fileIcon; }; } diff --git a/src/widgets/listwidget.cpp b/src/widgets/listwidget.cpp index 04620deb..7c0711c7 100644 --- a/src/widgets/listwidget.cpp +++ b/src/widgets/listwidget.cpp @@ -99,3 +99,28 @@ bool ListWidget::isSeparatorItem(const QListWidgetItem *p_item) { return p_item->type() == ItemTypeSeparator; } + +QListWidgetItem *ListWidget::findItem(const QListWidget *p_widget, const QVariant &p_data) +{ + QListWidgetItem *item = nullptr; + forEachItem(p_widget, [&item, &p_data](QListWidgetItem *itemIter) { + if (itemIter->data(Qt::UserRole) == p_data) { + item = itemIter; + return false; + } + + return true; + }); + + return item; +} + +void ListWidget::forEachItem(const QListWidget *p_widget, const std::function &p_func) +{ + int cnt = p_widget->count(); + for (int i = 0; i < cnt; ++i) { + if (!p_func(p_widget->item(i))) { + return; + } + } +} diff --git a/src/widgets/listwidget.h b/src/widgets/listwidget.h index 3dfd698f..a7c146b3 100644 --- a/src/widgets/listwidget.h +++ b/src/widgets/listwidget.h @@ -2,6 +2,9 @@ #define LISTWIDGET_H #include + +#include + #include namespace vnotex @@ -20,6 +23,11 @@ namespace vnotex static bool isSeparatorItem(const QListWidgetItem *p_item); + static QListWidgetItem *findItem(const QListWidget *p_widget, const QVariant &p_data); + + // @p_func: return false to abort the iteration. + static void forEachItem(const QListWidget *p_widget, const std::function &p_func); + protected: void keyPressEvent(QKeyEvent *p_event) Q_DECL_OVERRIDE; diff --git a/src/widgets/locationlist.cpp b/src/widgets/locationlist.cpp index 3ef2d46d..85864331 100644 --- a/src/widgets/locationlist.cpp +++ b/src/widgets/locationlist.cpp @@ -9,6 +9,7 @@ #include "widgetsfactory.h" #include "titlebar.h" #include "styleditemdelegate.h" +#include "navigationmodemgr.h" #include #include @@ -56,6 +57,9 @@ void LocationList::setupUI() }); mainLayout->addWidget(m_tree); + m_navigationWrapper.reset(new NavigationModeWrapper(m_tree)); + NavigationModeMgr::getInst().registerNavigationTarget(m_navigationWrapper.data()); + setFocusProxy(m_tree); } @@ -90,14 +94,6 @@ const QIcon &LocationList::getItemIcon(LocationType p_type) } } -NavigationModeWrapper *LocationList::getNavigationModeWrapper() -{ - if (!m_navigationWrapper) { - m_navigationWrapper.reset(new NavigationModeWrapper(m_tree)); - } - return m_navigationWrapper.data(); -} - void LocationList::setupTitleBar(const QString &p_title, QWidget *p_parent) { m_titleBar = new TitleBar(p_title, true, TitleBar::Action::None, p_parent); diff --git a/src/widgets/locationlist.h b/src/widgets/locationlist.h index b987bf98..dcba13ca 100644 --- a/src/widgets/locationlist.h +++ b/src/widgets/locationlist.h @@ -24,8 +24,6 @@ namespace vnotex explicit LocationList(QWidget *p_parent = nullptr); - NavigationModeWrapper *getNavigationModeWrapper(); - void clear(); void addLocation(const ComplexLocation &p_location); diff --git a/src/widgets/mainwindow.cpp b/src/widgets/mainwindow.cpp index ca19058c..dd0dc329 100644 --- a/src/widgets/mainwindow.cpp +++ b/src/widgets/mainwindow.cpp @@ -53,6 +53,7 @@ #include #include #include "dialogs/updater.h" +#include "tagexplorer.h" using namespace vnotex; @@ -118,6 +119,8 @@ void MainWindow::kickOffOnStart(const QStringList &p_paths) checkNotebooksFailedToLoad(); + loadWidgetsData(); + demoWidget(); openFiles(p_paths); @@ -140,6 +143,7 @@ void MainWindow::kickOffOnStart(const QStringList &p_paths) if (!file.isEmpty()) { auto paras = QSharedPointer::create(); paras->m_readOnly = true; + paras->m_sessionEnabled = false; emit VNoteX::getInst().openFileRequested(file, paras); } } @@ -247,7 +251,9 @@ void MainWindow::setupCentralWidget() void MainWindow::setupDocks() { - setupNotebookExplorer(this); + setupNotebookExplorer(); + + setupTagExplorer(); setupOutlineViewer(); @@ -298,13 +304,11 @@ void MainWindow::setupLocationList() { m_locationList = new LocationList(this); m_locationList->setObjectName("LocationList.vnotex"); - - NavigationModeMgr::getInst().registerNavigationTarget(m_locationList->getNavigationModeWrapper()); } -void MainWindow::setupNotebookExplorer(QWidget *p_parent) +void MainWindow::setupNotebookExplorer() { - m_notebookExplorer = new NotebookExplorer(p_parent); + m_notebookExplorer = new NotebookExplorer(this); connect(&VNoteX::getInst(), &VNoteX::newNotebookRequested, m_notebookExplorer, &NotebookExplorer::newNotebook); connect(&VNoteX::getInst(), &VNoteX::newNotebookFromFolderRequested, @@ -387,6 +391,8 @@ void MainWindow::closeEvent(QCloseEvent *p_event) return; } + m_trayIcon->hide(); + QMainWindow::closeEvent(p_event); qApp->exit(exitCode > -1 ? exitCode : 0); } else { @@ -408,6 +414,7 @@ void MainWindow::saveStateAndGeometry() sg.m_mainState = saveState(); sg.m_mainGeometry = saveGeometry(); sg.m_visibleDocksBeforeExpand = m_visibleDocksBeforeExpand; + sg.m_tagExplorerState = m_tagExplorer->saveState(); auto& sessionConfig = ConfigMgr::getInst().getSessionConfig(); sessionConfig.setMainWindowStateGeometry(sg); @@ -434,6 +441,10 @@ void MainWindow::loadStateAndGeometry(bool p_stateOnly) m_visibleDocksBeforeExpand = m_dockWidgetHelper.getVisibleDocks(); } } + + if (!sg.m_tagExplorerState.isEmpty()) { + m_tagExplorer->restoreState(sg.m_tagExplorerState); + } } void MainWindow::resetStateAndGeometry() @@ -484,6 +495,7 @@ void MainWindow::setupOutlineViewer() m_outlineViewer = new OutlineViewer(QString(), this); m_outlineViewer->setObjectName("OutlineViewer.vnotex"); + // There are OutlineViewers in each ViewWindow. We only need to register navigation mode for the outline panel. NavigationModeMgr::getInst().registerNavigationTarget(m_outlineViewer->getNavigationModeWrapper()); connect(m_viewArea, &ViewArea::currentViewWindowChanged, @@ -738,3 +750,17 @@ void MainWindow::checkNotebooksFailedToLoad() notebookMgr.clearNotebooksFailedToLoad(); } } + +void MainWindow::setupTagExplorer() +{ + m_tagExplorer = new TagExplorer(this); + connect(&VNoteX::getInst().getNotebookMgr(), &NotebookMgr::currentNotebookChanged, + m_tagExplorer, &TagExplorer::setNotebook); +} + +void MainWindow::loadWidgetsData() +{ + m_historyPanel->initialize(); + + m_snippetPanel->initialize(); +} diff --git a/src/widgets/mainwindow.h b/src/widgets/mainwindow.h index da20e7c2..e951f6bd 100644 --- a/src/widgets/mainwindow.h +++ b/src/widgets/mainwindow.h @@ -19,6 +19,7 @@ namespace vnotex { class ToolBox; class NotebookExplorer; + class TagExplorer; class ViewArea; class Event; class OutlineViewer; @@ -110,7 +111,9 @@ namespace vnotex void setupHistoryPanel(); - void setupNotebookExplorer(QWidget *p_parent = nullptr); + void setupNotebookExplorer(); + + void setupTagExplorer(); void setupDocks(); @@ -143,6 +146,8 @@ namespace vnotex void checkNotebooksFailedToLoad(); + void loadWidgetsData(); + ToolBarHelper m_toolBarHelper; StatusBarHelper m_statusBarHelper; @@ -153,6 +158,8 @@ namespace vnotex NotebookExplorer *m_notebookExplorer = nullptr; + TagExplorer *m_tagExplorer = nullptr; + ViewArea *m_viewArea = nullptr; QWidget *m_viewAreaStatusWidget = nullptr; diff --git a/src/widgets/markdownviewwindow.cpp b/src/widgets/markdownviewwindow.cpp index 6625d8eb..4bec41c3 100644 --- a/src/widgets/markdownviewwindow.cpp +++ b/src/widgets/markdownviewwindow.cpp @@ -260,6 +260,8 @@ void MarkdownViewWindow::setupToolBar() addAction(toolBar, ViewWindowToolBarHelper::Attachment); + addAction(toolBar, ViewWindowToolBarHelper::Tag); + toolBar->addSeparator(); addAction(toolBar, ViewWindowToolBarHelper::SectionNumber); @@ -1115,18 +1117,21 @@ QPoint MarkdownViewWindow::getFloatingWidgetPosition() QString MarkdownViewWindow::selectedText() const { switch (m_mode) { - case ViewWindowMode::FullPreview: - case ViewWindowMode::Invalid: - Q_FALLTHROUGH(); case ViewWindowMode::Read: Q_ASSERT(m_viewer); return m_viewer->selectedText(); + case ViewWindowMode::Edit: + Q_FALLTHROUGH(); + case ViewWindowMode::FullPreview: + Q_FALLTHROUGH(); case ViewWindowMode::FocusPreview: Q_ASSERT(m_editor); return m_editor->getTextEdit()->selectedText(); + + default: + return QString(); } - return QString(""); } void MarkdownViewWindow::handleImageHostChanged(const QString &p_hostName) diff --git a/src/widgets/navigationmodemgr.h b/src/widgets/navigationmodemgr.h index 526b365e..65452ccb 100644 --- a/src/widgets/navigationmodemgr.h +++ b/src/widgets/navigationmodemgr.h @@ -18,6 +18,7 @@ namespace vnotex public: ~NavigationModeMgr(); + // Maybe we need a unregisterNavigationTarget()? void registerNavigationTarget(NavigationMode *p_target); static NavigationModeMgr &getInst(); diff --git a/src/widgets/notebookexplorer.cpp b/src/widgets/notebookexplorer.cpp index de1b1e06..ad113dfa 100644 --- a/src/widgets/notebookexplorer.cpp +++ b/src/widgets/notebookexplorer.cpp @@ -16,10 +16,10 @@ #include "dialogs/importnotebookdialog.h" #include "dialogs/importfolderdialog.h" #include "dialogs/importlegacynotebookdialog.h" -#include "vnotex.h" +#include #include "mainwindow.h" -#include "notebook/notebook.h" -#include "notebookmgr.h" +#include +#include #include #include #include @@ -144,8 +144,8 @@ TitleBar *NotebookExplorer::setupTitleBar(QWidget *p_parent) return; } int ret = MessageBoxHelper::questionOkCancel(MessageBoxHelper::Warning, - tr("Scan the whole notebook (%1) and import external files automatically.").arg(m_currentNotebook->getName()), - tr("This operation helps importing external files that are added outside VNote. " + tr("Scan the whole notebook (%1) and import external files automatically?").arg(m_currentNotebook->getName()), + tr("This operation helps importing external files that are added outside from VNote. " "It may import unexpected files."), tr("It is recommended to always manage files within VNote."), VNoteX::getInst().getMainWindow()); @@ -220,8 +220,6 @@ void NotebookExplorer::loadNotebooks() auto ¬ebookMgr = VNoteX::getInst().getNotebookMgr(); const auto ¬ebooks = notebookMgr.getNotebooks(); m_selector->setNotebooks(notebooks); - - emit updateTitleBarMenuActions(); } void NotebookExplorer::reloadNotebook(const Notebook *p_notebook) @@ -241,8 +239,6 @@ void NotebookExplorer::setCurrentNotebook(const QSharedPointer &p_note m_nodeExplorer->setNotebook(p_notebook); recoverSession(); - - emit updateTitleBarMenuActions(); } void NotebookExplorer::newNotebook() @@ -546,6 +542,14 @@ void NotebookExplorer::recoverSession() void NotebookExplorer::rebuildDatabase() { if (m_currentNotebook) { + int okRet = MessageBoxHelper::questionOkCancel(MessageBoxHelper::Warning, + tr("Rebuild the database of notebook (%1)?").arg(m_currentNotebook->getName()), + tr("This operation will rebuild the notebook database from configuration files. It may take time."), + tr("A notebook may use a database for cache, such as IDs of nodes and tags."), + VNoteX::getInst().getMainWindow()); + if (okRet != QMessageBox::Ok) { + return; + } QProgressDialog proDlg(tr("Rebuilding notebook database..."), QString(), @@ -563,10 +567,12 @@ void NotebookExplorer::rebuildDatabase() if (ret) { MessageBoxHelper::notify(MessageBoxHelper::Type::Information, - tr("Notebook database has been rebuilt.")); + tr("Notebook database has been rebuilt."), + VNoteX::getInst().getMainWindow()); } else { MessageBoxHelper::notify(MessageBoxHelper::Type::Warning, - tr("Failed to rebuild notebook database.")); + tr("Failed to rebuild notebook database."), + VNoteX::getInst().getMainWindow()); } } } diff --git a/src/widgets/notebookexplorer.h b/src/widgets/notebookexplorer.h index 93955100..affb28c7 100644 --- a/src/widgets/notebookexplorer.h +++ b/src/widgets/notebookexplorer.h @@ -60,9 +60,6 @@ namespace vnotex signals: void notebookActivated(ID p_notebookId); - // Internal use only. - void updateTitleBarMenuActions(); - private: void setupUI(); diff --git a/src/widgets/notebooknodeexplorer.cpp b/src/widgets/notebooknodeexplorer.cpp index 4b0c446a..ba3c5582 100644 --- a/src/widgets/notebooknodeexplorer.cpp +++ b/src/widgets/notebooknodeexplorer.cpp @@ -23,6 +23,7 @@ #include "dialogs/folderpropertiesdialog.h" #include "dialogs/deleteconfirmdialog.h" #include "dialogs/sortdialog.h" +#include "dialogs/viewtagsdialog.h" #include #include #include @@ -39,19 +40,7 @@ using namespace vnotex; -QIcon NotebookNodeExplorer::s_folderNodeIcon; - -QIcon NotebookNodeExplorer::s_fileNodeIcon; - -QIcon NotebookNodeExplorer::s_invalidFolderNodeIcon; - -QIcon NotebookNodeExplorer::s_invalidFileNodeIcon; - -QIcon NotebookNodeExplorer::s_recycleBinNodeIcon; - -QIcon NotebookNodeExplorer::s_externalFolderNodeIcon; - -QIcon NotebookNodeExplorer::s_externalFileNodeIcon; +QIcon NotebookNodeExplorer::s_nodeIcons[NodeIcon::MaxIcons]; NotebookNodeExplorer::NodeData::NodeData() { @@ -188,7 +177,7 @@ NotebookNodeExplorer::NotebookNodeExplorer(QWidget *p_parent) void NotebookNodeExplorer::initNodeIcons() const { - if (!s_folderNodeIcon.isNull()) { + if (!s_nodeIcons[0].isNull()) { return; } @@ -205,13 +194,13 @@ void NotebookNodeExplorer::initNodeIcons() const const QString fileIconName("file_node.svg"); const QString recycleBinIconName("recycle_bin.svg"); - s_folderNodeIcon = IconUtils::fetchIcon(themeMgr.getIconFile(folderIconName), fg); - s_fileNodeIcon = IconUtils::fetchIcon(themeMgr.getIconFile(fileIconName), fg); - s_invalidFolderNodeIcon = IconUtils::fetchIcon(themeMgr.getIconFile(folderIconName), invalidFg); - s_invalidFileNodeIcon = IconUtils::fetchIcon(themeMgr.getIconFile(fileIconName), invalidFg); - s_recycleBinNodeIcon = IconUtils::fetchIcon(themeMgr.getIconFile(recycleBinIconName), fg); - s_externalFolderNodeIcon = IconUtils::fetchIcon(themeMgr.getIconFile(folderIconName), externalFg); - s_externalFileNodeIcon = IconUtils::fetchIcon(themeMgr.getIconFile(fileIconName), externalFg); + s_nodeIcons[NodeIcon::FolderNode] = IconUtils::fetchIcon(themeMgr.getIconFile(folderIconName), fg); + s_nodeIcons[NodeIcon::FileNode] = IconUtils::fetchIcon(themeMgr.getIconFile(fileIconName), fg); + s_nodeIcons[NodeIcon::InvalidFolderNode] = IconUtils::fetchIcon(themeMgr.getIconFile(folderIconName), invalidFg); + s_nodeIcons[NodeIcon::InvalidFileNode] = IconUtils::fetchIcon(themeMgr.getIconFile(fileIconName), invalidFg); + s_nodeIcons[NodeIcon::RecycleBinNode] = IconUtils::fetchIcon(themeMgr.getIconFile(recycleBinIconName), fg); + s_nodeIcons[NodeIcon::ExternalFolderNode] = IconUtils::fetchIcon(themeMgr.getIconFile(folderIconName), externalFg); + s_nodeIcons[NodeIcon::ExternalFileNode] = IconUtils::fetchIcon(themeMgr.getIconFile(fileIconName), externalFg); } void NotebookNodeExplorer::setupUI() @@ -497,7 +486,7 @@ void NotebookNodeExplorer::fillTreeItem(QTreeWidgetItem *p_item, Node *p_node, b setItemNodeData(p_item, NodeData(p_node, p_loaded)); p_item->setText(Column::Name, p_node->getName()); p_item->setIcon(Column::Name, getNodeItemIcon(p_node)); - p_item->setToolTip(Column::Name, p_node->exists() ? p_node->getName() : (tr("[Invalid] %1").arg(p_node->getName()))); + p_item->setToolTip(Column::Name, generateToolTip(p_node)); } void NotebookNodeExplorer::fillTreeItem(QTreeWidgetItem *p_item, const QSharedPointer &p_node) const @@ -511,19 +500,19 @@ void NotebookNodeExplorer::fillTreeItem(QTreeWidgetItem *p_item, const QSharedPo const QIcon &NotebookNodeExplorer::getNodeItemIcon(const Node *p_node) const { if (p_node->hasContent()) { - return p_node->exists() ? s_fileNodeIcon : s_invalidFileNodeIcon; + return p_node->exists() ? s_nodeIcons[NodeIcon::FileNode] : s_nodeIcons[NodeIcon::InvalidFileNode]; } else { if (p_node->getUse() == Node::Use::RecycleBin) { - return s_recycleBinNodeIcon; + return s_nodeIcons[NodeIcon::RecycleBinNode]; } - return p_node->exists() ? s_folderNodeIcon : s_invalidFolderNodeIcon; + return p_node->exists() ? s_nodeIcons[NodeIcon::FolderNode] : s_nodeIcons[NodeIcon::InvalidFolderNode]; } } const QIcon &NotebookNodeExplorer::getNodeItemIcon(const ExternalNode *p_node) const { - return p_node->isFolder() ? s_externalFolderNodeIcon : s_externalFileNodeIcon; + return p_node->isFolder() ? s_nodeIcons[NodeIcon::ExternalFolderNode] : s_nodeIcons[NodeIcon::ExternalFileNode]; } Node *NotebookNodeExplorer::getCurrentNode() const @@ -752,158 +741,126 @@ void NotebookNodeExplorer::clearStateCache(const Notebook *p_notebook) void NotebookNodeExplorer::createContextMenuOnRoot(QMenu *p_menu) { - auto act = createAction(Action::NewNote, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::NewNote, p_menu); - act = createAction(Action::NewFolder, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::NewFolder, p_menu); if (isPasteOnNodeAvailable(nullptr)) { p_menu->addSeparator(); - act = createAction(Action::Paste, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::Paste, p_menu); } p_menu->addSeparator(); - act = createAction(Action::Reload, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::Reload, p_menu); - act = createAction(Action::ReloadIndex, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::ReloadIndex, p_menu); - act = createAction(Action::OpenLocation, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::OpenLocation, p_menu); } void NotebookNodeExplorer::createContextMenuOnNode(QMenu *p_menu, const Node *p_node) { const int selectedSize = m_masterExplorer->selectedItems().size(); - QAction *act = nullptr; - if (m_notebook->isRecycleBinNode(p_node)) { // Recycle bin node. - act = createAction(Action::Reload, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::Reload, p_menu); - act = createAction(Action::ReloadIndex, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::ReloadIndex, p_menu); if (selectedSize == 1) { - act = createAction(Action::EmptyRecycleBin, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::EmptyRecycleBin, p_menu); - act = createAction(Action::OpenLocation, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::OpenLocation, p_menu); } } else if (m_notebook->isNodeInRecycleBin(p_node)) { // Node in recycle bin. - act = createAction(Action::Open, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::Open, p_menu); addOpenWithMenu(p_menu); p_menu->addSeparator(); if (selectedSize == 1 && p_node->isContainer()) { - act = createAction(Action::ExpandAll, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::ExpandAll, p_menu); } p_menu->addSeparator(); - act = createAction(Action::Cut, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::Cut, p_menu); - act = createAction(Action::DeleteFromRecycleBin, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::DeleteFromRecycleBin, p_menu); p_menu->addSeparator(); - act = createAction(Action::Reload, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::Reload, p_menu); - act = createAction(Action::ReloadIndex, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::ReloadIndex, p_menu); if (selectedSize == 1) { p_menu->addSeparator(); - act = createAction(Action::CopyPath, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::CopyPath, p_menu); - act = createAction(Action::OpenLocation, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::OpenLocation, p_menu); } } else { - act = createAction(Action::Open, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::Open, p_menu); addOpenWithMenu(p_menu); p_menu->addSeparator(); if (selectedSize == 1 && p_node->isContainer()) { - act = createAction(Action::ExpandAll, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::ExpandAll, p_menu); } p_menu->addSeparator(); - act = createAction(Action::NewNote, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::NewNote, p_menu); - act = createAction(Action::NewFolder, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::NewFolder, p_menu); p_menu->addSeparator(); - act = createAction(Action::Copy, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::Copy, p_menu); - act = createAction(Action::Cut, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::Cut, p_menu); if (selectedSize == 1 && isPasteOnNodeAvailable(p_node)) { - act = createAction(Action::Paste, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::Paste, p_menu); } - act = createAction(Action::Delete, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::Delete, p_menu); - act = createAction(Action::RemoveFromConfig, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::RemoveFromConfig, p_menu); p_menu->addSeparator(); - act = createAction(Action::Reload, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::Reload, p_menu); - act = createAction(Action::ReloadIndex, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::ReloadIndex, p_menu); - act = createAction(Action::Sort, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::Sort, p_menu); - { + if (selectedSize == 1 + && m_notebook->tag() + && !p_node->isContainer()) { p_menu->addSeparator(); - act = createAction(Action::PinToQuickAccess, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::Tag, p_menu); } + p_menu->addSeparator(); + + createAndAddAction(Action::PinToQuickAccess, p_menu); + if (selectedSize == 1) { - p_menu->addSeparator(); + createAndAddAction(Action::CopyPath, p_menu); - act = createAction(Action::CopyPath, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::OpenLocation, p_menu); - act = createAction(Action::OpenLocation, p_menu); - p_menu->addAction(act); - - act = createAction(Action::Properties, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::Properties, p_menu); } } } @@ -913,33 +870,22 @@ void NotebookNodeExplorer::createContextMenuOnExternalNode(QMenu *p_menu, const Q_UNUSED(p_node); const int selectedSize = m_masterExplorer->selectedItems().size(); - QAction *act = nullptr; - - act = createAction(Action::Open, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::Open, p_menu); addOpenWithMenu(p_menu); p_menu->addSeparator(); - act = createAction(Action::ImportToConfig, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::ImportToConfig, p_menu); - { - p_menu->addSeparator(); + p_menu->addSeparator(); - act = createAction(Action::PinToQuickAccess, p_menu); - p_menu->addAction(act); - } + createAndAddAction(Action::PinToQuickAccess, p_menu); if (selectedSize == 1) { - p_menu->addSeparator(); + createAndAddAction(Action::CopyPath, p_menu); - act = createAction(Action::CopyPath, p_menu); - p_menu->addAction(act); - - act = createAction(Action::OpenLocation, p_menu); - p_menu->addAction(act); + createAndAddAction(Action::OpenLocation, p_menu); } } @@ -1191,31 +1137,73 @@ QAction *NotebookNodeExplorer::createAction(Action p_act, QObject *p_parent) break; case Action::PinToQuickAccess: - act = new QAction(tr("Pin To &Quick Access"), p_parent); + act = new QAction(generateMenuActionIcon(QStringLiteral("quick_access_menu.svg")), + tr("Pin To &Quick Access"), + p_parent); connect(act, &QAction::triggered, this, [this]() { auto nodes = getSelectedNodes(); QStringList files; + bool hasFilteredAway = false; for (const auto &node : nodes.first) { if (node->hasContent()) { files.push_back(node->fetchAbsolutePath()); + } else { + hasFilteredAway = true; } } for (const auto &node : nodes.second) { if (!node->isFolder()) { files.push_back(node->fetchAbsolutePath()); + } else { + hasFilteredAway = true; } } if (!files.isEmpty()) { emit VNoteX::getInst().pinToQuickAccessRequested(files); } + if (hasFilteredAway) { + VNoteX::getInst().showStatusMessageShort(tr("Folder is not supported by quick access")); + } }); break; + + case Action::Tag: + act = new QAction(generateMenuActionIcon(QStringLiteral("tag.svg")), tr("&Tags"), p_parent); + connect(act, &QAction::triggered, + this, [this]() { + auto item = m_masterExplorer->currentItem(); + if (!item || !m_notebook->tag()) { + return; + } + auto data = getItemNodeData(item); + if (data.isNode()) { + auto node = data.getNode(); + if (checkInvalidNode(node)) { + return; + } + + ViewTagsDialog dialog(node, VNoteX::getInst().getMainWindow()); + dialog.exec(); + } + }); + break; + + default: + Q_ASSERT(false); + break; } return act; } +QAction *NotebookNodeExplorer::createAndAddAction(Action p_act, QMenu *p_menu) +{ + auto act = createAction(p_act, p_menu); + p_menu->addAction(act); + return act; +} + void NotebookNodeExplorer::copySelectedNodes(bool p_move) { auto nodes = getSelectedNodes().first; @@ -2082,3 +2070,23 @@ void NotebookNodeExplorer::loadItemChildren(QTreeWidgetItem *p_item) const } } } + +QString NotebookNodeExplorer::generateToolTip(const Node *p_node) +{ + Q_ASSERT(p_node->isLoaded()); + QString tip = p_node->exists() ? p_node->getName() : (tr("[Invalid] %1").arg(p_node->getName())); + tip += QLatin1String("\n\n"); + + if (!p_node->getTags().isEmpty()) { + const auto &tags = p_node->getTags(); + QString tagString = tags.first(); + for (int i = 1; i < tags.size(); ++i) { + tagString += QLatin1String("; ") + tags[i]; + } + tip += tr("Tags: %1\n").arg(tagString); + } + + tip += tr("Created Time: %1\n").arg(Utils::dateTimeString(p_node->getCreatedTimeUtc().toLocalTime())); + tip += tr("Modified Time: %1").arg(Utils::dateTimeString(p_node->getModifiedTimeUtc().toLocalTime())); + return tip; +} diff --git a/src/widgets/notebooknodeexplorer.h b/src/widgets/notebooknodeexplorer.h index 5e1401cc..b8aec079 100644 --- a/src/widgets/notebooknodeexplorer.h +++ b/src/widgets/notebooknodeexplorer.h @@ -115,6 +115,8 @@ namespace vnotex Node *currentExploredNode() const; + static QString generateToolTip(const Node *p_node); + signals: void nodeActivated(Node *p_node, const QSharedPointer &p_paras); @@ -154,7 +156,8 @@ namespace vnotex ImportToConfig, Open, ExpandAll, - PinToQuickAccess + PinToQuickAccess, + Tag }; void setupUI(); @@ -214,6 +217,8 @@ namespace vnotex // Factory function to create action. QAction *createAction(Action p_act, QObject *p_parent); + QAction *createAndAddAction(Action p_act, QMenu *p_menu); + void copySelectedNodes(bool p_move); void pasteNodesFromClipboard(); @@ -301,19 +306,19 @@ namespace vnotex bool m_autoImportExternalFiles = true; - static QIcon s_folderNodeIcon; + enum NodeIcon + { + FolderNode = 0, + FileNode, + InvalidFolderNode, + InvalidFileNode, + RecycleBinNode, + ExternalFolderNode, + ExternalFileNode, + MaxIcons + }; - static QIcon s_fileNodeIcon; - - static QIcon s_invalidFolderNodeIcon; - - static QIcon s_invalidFileNodeIcon; - - static QIcon s_recycleBinNodeIcon; - - static QIcon s_externalFolderNodeIcon; - - static QIcon s_externalFileNodeIcon; + static QIcon s_nodeIcons[NodeIcon::MaxIcons]; }; } diff --git a/src/widgets/outlinepopup.cpp b/src/widgets/outlinepopup.cpp index f64c3c8a..12ffd68a 100644 --- a/src/widgets/outlinepopup.cpp +++ b/src/widgets/outlinepopup.cpp @@ -14,6 +14,11 @@ OutlinePopup::OutlinePopup(QToolButton *p_btn, QWidget *p_parent) m_button(p_btn) { setupUI(); + + connect(this, &QMenu::aboutToShow, + this, [this]() { + m_viewer->setFocus(); + }); } void OutlinePopup::setupUI() @@ -36,8 +41,6 @@ void OutlinePopup::showEvent(QShowEvent* p_event) { QMenu::showEvent(p_event); - m_viewer->setFocus(); - // Move it to be right-aligned. if (m_button->isVisible()) { const auto p = pos(); diff --git a/src/widgets/quickselector.cpp b/src/widgets/quickselector.cpp index fec02b40..0ac4657d 100644 --- a/src/widgets/quickselector.cpp +++ b/src/widgets/quickselector.cpp @@ -1,8 +1,6 @@ #include "quickselector.h" #include -#include -#include #include #include #include @@ -72,10 +70,14 @@ void QuickSelector::setupUI(const QString &p_title) mainLayout->addWidget(new QLabel(p_title, this)); } - m_searchLineEdit = dynamic_cast(WidgetsFactory::createLineEdit(this)); + m_searchLineEdit = static_cast(WidgetsFactory::createLineEdit(this)); m_searchLineEdit->setInputMethodEnabled(false); connect(m_searchLineEdit, &QLineEdit::textEdited, this, &QuickSelector::searchAndFilter); + connect(m_searchLineEdit, &QLineEdit::returnPressed, + this, [this]() { + activateItem(m_itemList->currentItem()); + }); mainLayout->addWidget(m_searchLineEdit); setFocusProxy(m_searchLineEdit); @@ -154,11 +156,6 @@ bool QuickSelector::eventFilter(QObject *p_obj, QEvent *p_event) m_searchLineEdit->setFocus(); } return true; - } else if (key == Qt::Key_Enter || key == Qt::Key_Return) { - if (p_obj == m_searchLineEdit) { - activateItem(m_itemList->currentItem()); - return true; - } } } return FloatingWidget::eventFilter(p_obj, p_event); diff --git a/src/widgets/snippetpanel.cpp b/src/widgets/snippetpanel.cpp index 880c4c60..35081942 100644 --- a/src/widgets/snippetpanel.cpp +++ b/src/widgets/snippetpanel.cpp @@ -50,6 +50,11 @@ void SnippetPanel::setupUI() setFocusProxy(m_snippetList); } +void SnippetPanel::initialize() +{ + updateSnippetList(); +} + void SnippetPanel::setupTitleBar(const QString &p_title, QWidget *p_parent) { m_titleBar = new TitleBar(p_title, true, TitleBar::Action::Menu, p_parent); @@ -125,16 +130,6 @@ void SnippetPanel::updateSnippetList() updateItemsCountLabel(); } -void SnippetPanel::showEvent(QShowEvent *p_event) -{ - QFrame::showEvent(p_event); - - if (!m_listInitialized) { - m_listInitialized = true; - updateSnippetList(); - } -} - void SnippetPanel::handleContextMenuRequested(const QPoint &p_pos) { auto item = m_snippetList->itemAt(p_pos); diff --git a/src/widgets/snippetpanel.h b/src/widgets/snippetpanel.h index c0487c2d..16439d42 100644 --- a/src/widgets/snippetpanel.h +++ b/src/widgets/snippetpanel.h @@ -16,12 +16,11 @@ namespace vnotex public: explicit SnippetPanel(QWidget *p_parent = nullptr); + void initialize(); + signals: void applySnippetRequested(const QString &p_name); - protected: - void showEvent(QShowEvent *p_event) Q_DECL_OVERRIDE; - private slots: void newSnippet(); @@ -48,8 +47,6 @@ namespace vnotex QListWidget *m_snippetList = nullptr; - bool m_listInitialized = false; - bool m_builtInSnippetsVisible = true; }; } diff --git a/src/widgets/styleditemdelegate.h b/src/widgets/styleditemdelegate.h index 4806c52c..e7e8ced8 100644 --- a/src/widgets/styleditemdelegate.h +++ b/src/widgets/styleditemdelegate.h @@ -18,7 +18,9 @@ namespace vnotex enum { - HighlightsRole = 0x0101 + // Qt::UserRole = 0x0100 + UserRole2 = 0x0101, + HighlightsRole = 0x0102 }; diff --git a/src/widgets/tagexplorer.cpp b/src/widgets/tagexplorer.cpp new file mode 100644 index 00000000..fea13cc1 --- /dev/null +++ b/src/widgets/tagexplorer.cpp @@ -0,0 +1,423 @@ +#include "tagexplorer.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "titlebar.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "widgetsfactory.h" +#include "listwidget.h" +#include "treewidget.h" +#include "navigationmodemgr.h" +#include "notebooknodeexplorer.h" +#include "mainwindow.h" +#include "messageboxhelper.h" +#include "dialogs/newtagdialog.h" +#include "dialogs/renametagdialog.h" + +using namespace vnotex; + +TagExplorer::TagExplorer(QWidget *p_parent) + : QFrame(p_parent) +{ + initIcons(); + + setupUI(); +} + +void TagExplorer::initIcons() +{ + const auto &themeMgr = VNoteX::getInst().getThemeMgr(); + m_tagIcon = IconUtils::fetchIcon(themeMgr.getIconFile(QStringLiteral("tag.svg"))); + m_nodeIcon = IconUtils::fetchIcon(themeMgr.getIconFile(QStringLiteral("file_node.svg"))); +} + +void TagExplorer::setupUI() +{ + auto mainLayout = new QVBoxLayout(this); + WidgetUtils::setContentsMargins(mainLayout); + + setupTitleBar(this); + mainLayout->addWidget(m_titleBar); + + m_splitter = new QSplitter(this); + mainLayout->addWidget(m_splitter); + + setTwoColumnsEnabled(ConfigMgr::getInst().getWidgetConfig().getTagExplorerTwoColumnsEnabled()); + + setupTagTree(m_splitter); + m_splitter->addWidget(m_tagTree); + + setupNodeList(m_splitter); + m_splitter->addWidget(m_nodeList); + + setFocusProxy(m_tagTree); +} + +void TagExplorer::setupTitleBar(QWidget *p_parent) +{ + m_titleBar = new TitleBar(QString(), false, TitleBar::Action::Menu, p_parent); + m_titleBar->setActionButtonsAlwaysShown(true); + + auto twoColumnsAct = m_titleBar->addMenuAction(tr("Two Columns"), + m_titleBar, + [this](bool p_checked) { + ConfigMgr::getInst().getWidgetConfig().setTagExplorerTwoColumnsEnabled(p_checked); + setTwoColumnsEnabled(p_checked); + }); + twoColumnsAct->setCheckable(true); + twoColumnsAct->setChecked(ConfigMgr::getInst().getWidgetConfig().getTagExplorerTwoColumnsEnabled()); +} + +void TagExplorer::setTwoColumnsEnabled(bool p_enabled) +{ + if (m_splitter) { + m_splitter->setOrientation(p_enabled ? Qt::Horizontal : Qt::Vertical); + } +} + +void TagExplorer::setupTagTree(QWidget *p_parent) +{ + auto timer = new QTimer(this); + timer->setSingleShot(true); + timer->setInterval(500); + connect(timer, &QTimer::timeout, + this, &TagExplorer::activateTagItem); + + m_tagTree = new TreeWidget(TreeWidget::ClickSpaceToClearSelection, p_parent); + TreeWidget::setupSingleColumnHeaderlessTree(m_tagTree, true, false); + TreeWidget::showHorizontalScrollbar(m_tagTree); + m_tagTree->setDragDropMode(QAbstractItemView::InternalMove); + connect(m_tagTree, &QTreeWidget::currentItemChanged, + timer, QOverload::of(&QTimer::start)); + connect(m_tagTree, &QTreeWidget::itemClicked, + timer, QOverload::of(&QTimer::start)); + connect(m_tagTree, &QTreeWidget::customContextMenuRequested, + this, &TagExplorer::handleTagTreeContextMenuRequested); + connect(m_tagTree, &TreeWidget::itemMoved, + this, &TagExplorer::handleTagMoved); + + m_tagTreeNavigationWrapper.reset(new NavigationModeWrapper(m_tagTree)); + NavigationModeMgr::getInst().registerNavigationTarget(m_tagTreeNavigationWrapper.data()); +} + +void TagExplorer::setupNodeList(QWidget *p_parent) +{ + m_nodeList = new ListWidget(p_parent); + m_nodeList->setContextMenuPolicy(Qt::CustomContextMenu); + m_nodeList->setSelectionMode(QAbstractItemView::ExtendedSelection); + connect(m_nodeList, &QListWidget::customContextMenuRequested, + this, &TagExplorer::handleNodeListContextMenuRequested); + connect(m_nodeList, &QListWidget::itemActivated, + this, &TagExplorer::openItem); + + m_nodeListNavigationWrapper.reset(new NavigationModeWrapper(m_nodeList)); + NavigationModeMgr::getInst().registerNavigationTarget(m_nodeListNavigationWrapper.data()); +} + +QByteArray TagExplorer::saveState() const +{ + return m_splitter->saveState(); +} + +void TagExplorer::restoreState(const QByteArray &p_data) +{ + m_splitter->restoreState(p_data); +} + +void TagExplorer::setNotebook(const QSharedPointer &p_notebook) +{ + if (m_notebook == p_notebook) { + return; + } + + if (m_notebook) { + disconnect(m_notebook.data(), nullptr, this, nullptr); + } + + m_notebook = p_notebook; + if (m_notebook) { + connect(m_notebook.data(), &Notebook::tagsUpdated, + this, &TagExplorer::updateTags); + } + + m_lastTagName.clear(); + + updateTags(); +} + +void TagExplorer::updateTags() +{ + m_tagTree->clear(); + + auto tagI = m_notebook ? m_notebook->tag() : nullptr; + if (!tagI) { + return; + } + + const auto &topLevelTags = tagI->getTopLevelTags(); + for (const auto &tag : topLevelTags) { + auto item = new QTreeWidgetItem(m_tagTree); + fillTagItem(tag, item); + loadTagChildren(tag, item); + } + + m_tagTree->expandAll(); + + scrollToTag(m_lastTagName); +} + +void TagExplorer::loadTagChildren(const QSharedPointer &p_tag, QTreeWidgetItem *p_parentItem) +{ + for (const auto &child : p_tag->getChildren()) { + auto item = new QTreeWidgetItem(p_parentItem); + fillTagItem(child, item); + loadTagChildren(child, item); + } +} + +void TagExplorer::fillTagItem(const QSharedPointer &p_tag, QTreeWidgetItem *p_item) const +{ + p_item->setText(Column::Name, p_tag->name()); + p_item->setToolTip(Column::Name, p_tag->name()); + p_item->setIcon(Column::Name, m_tagIcon); + p_item->setData(Column::Name, Qt::UserRole, p_tag->name()); +} + +void TagExplorer::activateTagItem() +{ + auto items = m_tagTree->selectedItems(); + if (items.size() != 1) { + m_lastTagName.clear(); + m_nodeList->clear(); + return; + } + + m_lastTagName = itemTag(items[0]); + updateNodeList(m_lastTagName); +} + +QString TagExplorer::itemTag(const QTreeWidgetItem *p_item) const +{ + return p_item->data(Column::Name, Qt::UserRole).toString(); +} + +QString TagExplorer::itemNode(const QListWidgetItem *p_item) const +{ + return p_item->data(Qt::UserRole).toString(); +} + +void TagExplorer::updateNodeList(const QString &p_tag) +{ + m_nodeList->clear(); + + Q_ASSERT(m_notebook); + auto tagI = m_notebook->tag(); + Q_ASSERT(tagI); + const auto nodePaths = tagI->findNodesOfTag(p_tag); + for (const auto &pa : nodePaths) { + auto node = m_notebook->loadNodeByPath(pa); + if (!node) { + qWarning() << "node belongs to tag in DB but not exists" << p_tag << pa; + continue; + } + + if (m_notebook->isNodeInRecycleBin(node.data())) { + qDebug() << "skipped node in recycle bin" << p_tag << pa; + continue; + } + + auto item = new QListWidgetItem(m_nodeList); + item->setText(node->getName()); + item->setToolTip(NotebookNodeExplorer::generateToolTip(node.data())); + item->setIcon(m_nodeIcon); + item->setData(Qt::UserRole, pa); + } + + VNoteX::getInst().showStatusMessageShort(tr("Search of tag succeeded: %1").arg(p_tag)); +} + +void TagExplorer::handleNodeListContextMenuRequested(const QPoint &p_pos) +{ + if (!m_notebook) { + return; + } + + auto item = m_nodeList->itemAt(p_pos); + if (!item) { + return; + } + + QMenu menu(this); + + const int selectedCount = m_nodeList->selectedItems().size(); + + menu.addAction(tr("&Open"), + &menu, + [this]() { + const auto selectedItems = m_nodeList->selectedItems(); + for (const auto &selectedItem : selectedItems) { + openItem(selectedItem); + } + }); + + if (selectedCount == 1) { + menu.addAction(tr("&Locate Node"), + &menu, + [this]() { + auto item = m_nodeList->currentItem(); + if (!item) { + return; + } + + auto node = m_notebook->loadNodeByPath(itemNode(item)); + Q_ASSERT(node); + if (node) { + emit VNoteX::getInst().locateNodeRequested(node.data()); + } + }); + } + + menu.exec(m_nodeList->mapToGlobal(p_pos)); +} + +void TagExplorer::openItem(const QListWidgetItem *p_item) +{ + if (!p_item) { + return; + } + + Q_ASSERT(m_notebook); + auto node = m_notebook->loadNodeByPath(itemNode(p_item)); + if (node) { + emit VNoteX::getInst().openNodeRequested(node.data(), QSharedPointer::create()); + } +} + +void TagExplorer::handleTagTreeContextMenuRequested(const QPoint &p_pos) +{ + if (!m_notebook) { + return; + } + + QMenu menu(this); + + auto item = m_tagTree->itemAt(p_pos); + + menu.addAction(tr("&New Tag"), this, &TagExplorer::newTag); + + if (item && m_tagTree->selectedItems().size() == 1) { + menu.addAction(tr("&Rename"), this, &TagExplorer::renameTag); + + menu.addAction(tr("&Delete"), this, &TagExplorer::removeTag); + } + + menu.exec(m_tagTree->mapToGlobal(p_pos)); +} + +void TagExplorer::newTag() +{ + Q_ASSERT(m_notebook); + + QSharedPointer parentTag; + + auto item = m_tagTree->currentItem(); + if (item) { + const auto tagName = itemTag(item); + parentTag = m_notebook->tag()->findTag(tagName); + Q_ASSERT(parentTag); + } + + NewTagDialog dialog(m_notebook->tag(), parentTag.data(), VNoteX::getInst().getMainWindow()); + dialog.exec(); +} + +void TagExplorer::renameTag() +{ + Q_ASSERT(m_notebook); + auto item = m_tagTree->currentItem(); + if (!item) { + return; + } + + RenameTagDialog dialog(m_notebook->tag(), itemTag(item), VNoteX::getInst().getMainWindow()); + if (dialog.exec() == QDialog::Accepted) { + scrollToTag(dialog.getTagName()); + } +} + +void TagExplorer::removeTag() +{ + Q_ASSERT(m_notebook); + auto item = m_tagTree->currentItem(); + if (!item) { + return; + } + + const auto tagName = itemTag(item); + int okRet = MessageBoxHelper::questionOkCancel(MessageBoxHelper::Warning, + tr("Delete the tag and all its chlidren tags?"), + tr("Only tags and the references of them will be deleted."), + QString(), + VNoteX::getInst().getMainWindow()); + if (okRet != QMessageBox::Ok) { + return; + } + + if (m_notebook->tag()->removeTag(tagName)) { + VNoteX::getInst().showStatusMessageShort(tr("Tag deleted")); + } else { + VNoteX::getInst().showStatusMessageShort(tr("Failed to delete tag: %1").arg(tagName)); + } +} + +void TagExplorer::handleTagMoved(QTreeWidgetItem *p_item) +{ + const auto tagName = itemTag(p_item); + auto tag = m_notebook->tag()->findTag(tagName); + Q_ASSERT(tag); + const auto oldParentName = tag->getParent() ? tag->getParent()->name() : QString(); + const auto newParentName = p_item->parent() ? itemTag(p_item->parent()) : QString(); + if (oldParentName == newParentName) { + // Sorting tags is not supported for now. + return; + } + + qDebug() << "re-parent tag" << tagName << oldParentName << "->" << newParentName; + bool ret = m_notebook->tag()->moveTag(tagName, newParentName); + if (!ret) { + MessageBoxHelper::notify(MessageBoxHelper::Type::Warning, + tr("Failed to move tag (%1).").arg(tagName), + VNoteX::getInst().getMainWindow()); + } +} + +void TagExplorer::scrollToTag(const QString &p_name) +{ + if (p_name.isEmpty()) { + return; + } + + auto item = TreeWidget::findItem(m_tagTree, p_name, Column::Name); + if (item) { + m_tagTree->setCurrentItem(item); + m_tagTree->scrollToItem(item); + } +} diff --git a/src/widgets/tagexplorer.h b/src/widgets/tagexplorer.h new file mode 100644 index 00000000..11a5e777 --- /dev/null +++ b/src/widgets/tagexplorer.h @@ -0,0 +1,105 @@ +#ifndef TAGEXPLORER_H +#define TAGEXPLORER_H + +#include +#include +#include + +#include "navigationmodewrapper.h" + +class QListWidget; +class QListWidgetItem; +class QTreeWidget; +class QTreeWidgetItem; +class QSplitter; + +namespace vnotex +{ + class TitleBar; + class Notebook; + class Tag; + class TreeWidget; + + class TagExplorer : public QFrame + { + Q_OBJECT + public: + explicit TagExplorer(QWidget *p_parent = nullptr); + + QByteArray saveState() const; + + void restoreState(const QByteArray &p_data); + + public slots: + void setNotebook(const QSharedPointer &p_notebook); + + private slots: + void handleNodeListContextMenuRequested(const QPoint &p_pos); + + void handleTagTreeContextMenuRequested(const QPoint &p_pos); + + void handleTagMoved(QTreeWidgetItem *p_item); + + private: + enum Column { Name = 0 }; + + void initIcons(); + + void setupUI(); + + void setupTitleBar(QWidget *p_parent = nullptr); + + void setupTagTree(QWidget *p_parent = nullptr); + + void setupNodeList(QWidget *p_parent = nullptr); + + void setTwoColumnsEnabled(bool p_enabled); + + void updateTags(); + + void loadTagChildren(const QSharedPointer &p_tag, QTreeWidgetItem *p_parentItem); + + void fillTagItem(const QSharedPointer &p_tag, QTreeWidgetItem *p_item) const; + + void activateTagItem(); + + QString itemTag(const QTreeWidgetItem *p_item) const; + + QString itemNode(const QListWidgetItem *p_item) const; + + void updateNodeList(const QString &p_tag); + + void openItem(const QListWidgetItem *p_item); + + void newTag(); + + void renameTag(); + + void removeTag(); + + void scrollToTag(const QString &p_name); + + QSharedPointer m_notebook; + + // Used to cache current selected tag after update. + QString m_lastTagName; + + TitleBar *m_titleBar = nullptr; + + QSplitter *m_splitter = nullptr; + + TreeWidget *m_tagTree = nullptr; + + QScopedPointer> m_tagTreeNavigationWrapper; + + QListWidget *m_nodeList = nullptr; + + QScopedPointer> m_nodeListNavigationWrapper; + + QIcon m_tagIcon; + + QIcon m_nodeIcon; + }; +} + +#endif // TAGEXPLORER_H diff --git a/src/widgets/tagpopup.cpp b/src/widgets/tagpopup.cpp new file mode 100644 index 00000000..0452fdfc --- /dev/null +++ b/src/widgets/tagpopup.cpp @@ -0,0 +1,49 @@ +#include "tagpopup.h" + +#include +#include + +#include +#include + +#include "tagviewer.h" + +using namespace vnotex; + +TagPopup::TagPopup(QToolButton *p_btn, QWidget *p_parent) + : QMenu(p_parent), + m_button(p_btn) +{ + setupUI(); + + connect(this, &QMenu::aboutToShow, + this, [this]() { + m_tagViewer->setNode(m_buffer ? m_buffer->getNode() : nullptr); + // Enable input method. + m_tagViewer->activateWindow(); + m_tagViewer->setFocus(); + }); + + connect(this, &QMenu::aboutToHide, + m_tagViewer, &TagViewer::save); +} + +void TagPopup::setupUI() +{ + auto mainLayout = new QVBoxLayout(this); + WidgetUtils::setContentsMargins(mainLayout); + + m_tagViewer = new TagViewer(this); + mainLayout->addWidget(m_tagViewer); + + setMinimumSize(256, 320); +} + +void TagPopup::setBuffer(Buffer *p_buffer) +{ + if (m_buffer == p_buffer) { + return; + } + + m_buffer = p_buffer; +} diff --git a/src/widgets/tagpopup.h b/src/widgets/tagpopup.h new file mode 100644 index 00000000..d549baea --- /dev/null +++ b/src/widgets/tagpopup.h @@ -0,0 +1,34 @@ +#ifndef TAGPOPUP_H +#define TAGPOPUP_H + +#include + +class QToolButton; + +namespace vnotex +{ + class Buffer; + class TagViewer; + + class TagPopup : public QMenu + { + Q_OBJECT + public: + TagPopup(QToolButton *p_btn, QWidget *p_parent = nullptr); + + void setBuffer(Buffer *p_buffer); + + private: + void setupUI(); + + Buffer *m_buffer = nullptr; + + // Button for this menu. + QToolButton *m_button = nullptr; + + // Managed by QObject. + TagViewer *m_tagViewer = nullptr; + }; +} + +#endif // TAGPOPUP_H diff --git a/src/widgets/tagviewer.cpp b/src/widgets/tagviewer.cpp new file mode 100644 index 00000000..64ce7e90 --- /dev/null +++ b/src/widgets/tagviewer.cpp @@ -0,0 +1,323 @@ +#include "tagviewer.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "lineedit.h" +#include "listwidget.h" +#include "widgetsfactory.h" +#include "styleditemdelegate.h" +#include "messageboxhelper.h" +#include "mainwindow.h" + +using namespace vnotex; + +QIcon TagViewer::s_tagIcon; + +QIcon TagViewer::s_selectedTagIcon; + +TagViewer::TagViewer(QWidget *p_parent) + : QFrame(p_parent) +{ + initIcons(); + + setupUI(); +} + +void TagViewer::setupUI() +{ + auto mainLayout = new QVBoxLayout(this); + + m_searchLineEdit = static_cast(WidgetsFactory::createLineEdit(this)); + m_searchLineEdit->setPlaceholderText(tr("Enter to add a tag")); + m_searchLineEdit->setToolTip(tr("[Shift+Enter] to add current selected tag in the list")); + connect(m_searchLineEdit, &QLineEdit::textChanged, + this, &TagViewer::searchAndFilter); + connect(m_searchLineEdit, &QLineEdit::returnPressed, + this, &TagViewer::handleSearchLineEditReturnPressed); + mainLayout->addWidget(m_searchLineEdit); + + auto tagNameValidator = new QRegularExpressionValidator(QRegularExpression("[^>]*"), m_searchLineEdit); + m_searchLineEdit->setValidator(tagNameValidator); + + setFocusProxy(m_searchLineEdit); + m_searchLineEdit->installEventFilter(this); + + m_tagList = new ListWidget(this); + m_tagList->setWrapping(true); + m_tagList->setFlow(QListView::LeftToRight); + m_tagList->setIconSize(QSize(18, 18)); + connect(m_tagList, &QListWidget::itemClicked, + this, &TagViewer::toggleItemTag); + connect(m_tagList, &QListWidget::itemActivated, + this, &TagViewer::toggleItemTag); + mainLayout->addWidget(m_tagList); + + m_tagList->installEventFilter(this); +} + +bool TagViewer::eventFilter(QObject *p_obj, QEvent *p_event) +{ + if ((p_obj == m_searchLineEdit || p_obj == m_tagList) + && p_event->type() == QEvent::KeyPress) { + auto keyEve = static_cast(p_event); + const auto key = keyEve->key(); + if (key == Qt::Key_Tab || key == Qt::Key_Backtab) { + // Change focus. + if (p_obj == m_searchLineEdit) { + m_tagList->setFocus(); + } else { + m_searchLineEdit->setFocus(); + } + return true; + } + } + return QFrame::eventFilter(p_obj, p_event); +} + +void TagViewer::setNode(Node *p_node) +{ + // Since there may be update on tags, always update the list. + // When first time viewing the tags of one node, it is a good chance to sync the node's tag to DB. + if (m_node != p_node) { + m_node = p_node; + if (m_node) { + bool ret = tagI()->updateNodeTags(m_node); + if (!ret) { + qWarning() << "failed to update tags of node" << m_node->fetchPath(); + } + } + } + + m_hasChange = false; + + updateTagList(); +} + +void TagViewer::updateTagList() +{ + m_tagList->clear(); + if (!m_node) { + return; + } + + QSet tagsAdded; + const auto &nodeTags = m_node->getTags(); + for (const auto &tag : nodeTags) { + if (tagsAdded.contains(tag)) { + continue; + } + + tagsAdded.insert(tag); + addTagItem(tag, true); + } + + const auto &allTags = tagI()->getTopLevelTags(); + for (const auto &tag : allTags) { + addTags(tag, tagsAdded); + } + + if (!tagsAdded.isEmpty()) { + m_tagList->setCurrentRow(0); + // Qt's BUG: need to set it again to make it in grid form after setCurrentRow(). + m_tagList->setWrapping(true); + } +} + +void TagViewer::addTags(const QSharedPointer &p_tag, QSet &p_addedTags) +{ + // Itself. + if (!p_addedTags.contains(p_tag->name())) { + p_addedTags.insert(p_tag->name()); + addTagItem(p_tag->name(), false); + } + + // Children. + for (const auto &child : p_tag->getChildren()) { + addTags(child, p_addedTags); + } +} + +void TagViewer::initIcons() +{ + if (!s_tagIcon.isNull()) { + return; + } + + const auto &themeMgr = VNoteX::getInst().getThemeMgr(); + s_tagIcon = IconUtils::fetchIcon(themeMgr.getIconFile(QStringLiteral("tag.svg"))); + s_selectedTagIcon = IconUtils::fetchIcon(themeMgr.getIconFile(QStringLiteral("tag_selected.svg"))); +} + +void TagViewer::addTagItem(const QString &p_tagName, bool p_selected, bool p_prepend) +{ + auto item = new QListWidgetItem(p_tagName); + if (!p_prepend) { + m_tagList->addItem(item); + } else { + m_tagList->insertItem(0, item); + } + + item->setToolTip(p_tagName); + item->setData(Qt::UserRole, p_tagName); + setItemTagSelected(item, p_selected); +} + +QString TagViewer::itemTag(const QListWidgetItem *p_item) const +{ + return p_item->data(Qt::UserRole).toString(); +} + +bool TagViewer::isItemTagSelected(const QListWidgetItem *p_item) const +{ + return p_item->data(UserRole2).toBool(); +} + +TagI *TagViewer::tagI() +{ + return m_node->getNotebook()->tag(); +} + +void TagViewer::searchAndFilter(const QString &p_text) +{ + // Take the last tag for search. + const auto text = p_text.trimmed(); + + if (text.isEmpty()) { + // Show all items. + filterItems([](const QListWidgetItem *) { + return true; + }); + return; + } + + filterItems([this, &text](const QListWidgetItem *p_item) { + if (itemTag(p_item).contains(text)) { + return true; + } + return false; + }); +} + +void TagViewer::filterItems(const std::function &p_judge) +{ + QListWidgetItem *firstHit = nullptr; + ListWidget::forEachItem(m_tagList, [&firstHit, &p_judge](QListWidgetItem *itemIter) { + if (p_judge(itemIter)) { + if (!firstHit) { + firstHit = itemIter; + } + itemIter->setHidden(false); + } else { + itemIter->setHidden(true); + } + return true; + }); + m_tagList->setCurrentItem(firstHit); +} + +void TagViewer::handleSearchLineEditReturnPressed() +{ + if (QGuiApplication::keyboardModifiers() == Qt::ShiftModifier) { + // Add current selected tag in the list. + auto item = m_tagList->currentItem(); + if (item && !isItemTagSelected(item)) { + setItemTagSelected(item, true); + m_searchLineEdit->clear(); + m_hasChange = true; + } + } else { + // Decode input text and add tags. + const auto tagName = m_searchLineEdit->text().trimmed(); + if (tagName.isEmpty()) { + return; + } + + if (auto item = findItem(tagName)) { + // Add existing tag. + setItemTagSelected(item, true); + } else { + // Add new tag. + addTagItem(tagName, true, true); + } + + m_searchLineEdit->clear(); + m_hasChange = true; + } +} + +void TagViewer::toggleItemTag(QListWidgetItem *p_item) +{ + m_hasChange = true; + setItemTagSelected(p_item, !isItemTagSelected(p_item)); +} + +void TagViewer::setItemTagSelected(QListWidgetItem *p_item, bool p_selected) +{ + p_item->setIcon(p_selected ? s_selectedTagIcon : s_tagIcon); + p_item->setData(UserRole2, p_selected); +} + +QListWidgetItem *TagViewer::findItem(const QString &p_tagName) const +{ + return ListWidget::findItem(m_tagList, p_tagName); +} + +void TagViewer::save() +{ + if (!m_node || !m_hasChange) { + return; + } + + QHash selectedTags; + ListWidget::forEachItem(m_tagList, [this, &selectedTags](QListWidgetItem *itemIter) { + if (isItemTagSelected(itemIter)) { + selectedTags.insert(itemTag(itemIter), 0); + } + return true; + }); + + if (selectedTags.size() == m_node->getTags().size()) { + bool same = true; + for (const auto &tag : m_node->getTags()) { + auto iter = selectedTags.find(tag); + if (iter == selectedTags.end()) { + same = false; + break; + } else { + iter.value()++; + if (iter.value() > 1) { + same = false; + break; + } + } + } + + if (same) { + return; + } + } + + bool ret = tagI()->updateNodeTags(m_node, selectedTags.keys()); + if (ret) { + VNoteX::getInst().showStatusMessageShort(tr("Tags updated: %1").arg(m_node->getTags().join(QLatin1String("; ")))); + } else { + MessageBoxHelper::notify(MessageBoxHelper::Type::Warning, + tr("Failed to update tags of node (%1).").arg(m_node->getName()), + VNoteX::getInst().getMainWindow()); + } +} diff --git a/src/widgets/tagviewer.h b/src/widgets/tagviewer.h new file mode 100644 index 00000000..2da7d37f --- /dev/null +++ b/src/widgets/tagviewer.h @@ -0,0 +1,79 @@ +#ifndef TAGVIEWER_H +#define TAGVIEWER_H + +#include + +#include + +#include +#include +#include + +class QListWidget; +class QListWidgetItem; + +namespace vnotex +{ + class LineEdit; + class Node; + class TagI; + class Tag; + + class TagViewer : public QFrame + { + Q_OBJECT + public: + explicit TagViewer(QWidget *p_parent = nullptr); + + void setNode(Node *p_node); + + void save(); + + protected: + bool eventFilter(QObject *p_obj, QEvent *p_event) Q_DECL_OVERRIDE; + + private: + void setupUI(); + + void updateTagList(); + + TagI *tagI(); + + void addTagItem(const QString &p_tagName, bool p_selected, bool p_prepend = false); + + QString itemTag(const QListWidgetItem *p_item) const; + + bool isItemTagSelected(const QListWidgetItem *p_item) const; + + void addTags(const QSharedPointer &p_tag, QSet &p_addedTags); + + void searchAndFilter(const QString &p_text); + + void filterItems(const std::function &p_judge); + + void handleSearchLineEditReturnPressed(); + + void toggleItemTag(QListWidgetItem *p_item); + + void setItemTagSelected(QListWidgetItem *p_item, bool p_selected); + + QListWidgetItem *findItem(const QString &p_tagName) const; + + static void initIcons(); + + // View the tags of @m_node. + Node *m_node = nullptr; + + bool m_hasChange = false; + + LineEdit *m_searchLineEdit = nullptr; + + QListWidget *m_tagList = nullptr; + + static QIcon s_tagIcon; + + static QIcon s_selectedTagIcon; + }; +} + +#endif // TAGVIEWER_H diff --git a/src/widgets/textviewwindow.cpp b/src/widgets/textviewwindow.cpp index 44b11519..89054e9c 100644 --- a/src/widgets/textviewwindow.cpp +++ b/src/widgets/textviewwindow.cpp @@ -68,6 +68,8 @@ void TextViewWindow::setupToolBar() addAction(toolBar, ViewWindowToolBarHelper::Attachment); + addAction(toolBar, ViewWindowToolBarHelper::Tag); + ToolBarHelper::addSpacer(toolBar); addAction(toolBar, ViewWindowToolBarHelper::FindAndReplace); } diff --git a/src/widgets/toolbarhelper.cpp b/src/widgets/toolbarhelper.cpp index b2b6ac35..f4b12fc0 100644 --- a/src/widgets/toolbarhelper.cpp +++ b/src/widgets/toolbarhelper.cpp @@ -352,7 +352,7 @@ QToolBar *ToolBarHelper::setupSettingsToolBar(MainWindow *p_win, QToolBar *p_too menu->addAction(fullScreenAct); } - auto stayOnTopAct = menu->addAction(generateIcon("stay_on_top.svg"), MainWindow::tr("Stay On Top"), + auto stayOnTopAct = menu->addAction(generateIcon("stay_on_top.svg"), MainWindow::tr("Stay on Top"), p_win, &MainWindow::setStayOnTop); stayOnTopAct->setCheckable(true); WidgetUtils::addActionShortcut(stayOnTopAct, diff --git a/src/widgets/treewidget.cpp b/src/widgets/treewidget.cpp index 1b82f253..64aefc00 100644 --- a/src/widgets/treewidget.cpp +++ b/src/widgets/treewidget.cpp @@ -58,11 +58,11 @@ void TreeWidget::showHorizontalScrollbar(QTreeWidget *p_tree) p_tree->header()->setStretchLastSection(false); } -QTreeWidgetItem *TreeWidget::findItem(const QTreeWidget *p_widget, const QVariant &p_data) +QTreeWidgetItem *TreeWidget::findItem(const QTreeWidget *p_widget, const QVariant &p_data, int p_column) { int nrTop = p_widget->topLevelItemCount(); for (int i = 0; i < nrTop; ++i) { - auto item = findItemHelper(p_widget->topLevelItem(i), p_data); + auto item = findItemHelper(p_widget->topLevelItem(i), p_data, p_column); if (item) { return item; } @@ -71,7 +71,7 @@ QTreeWidgetItem *TreeWidget::findItem(const QTreeWidget *p_widget, const QVarian return nullptr; } -QTreeWidgetItem *TreeWidget::findItemHelper(QTreeWidgetItem *p_item, const QVariant &p_data) +QTreeWidgetItem *TreeWidget::findItemHelper(QTreeWidgetItem *p_item, const QVariant &p_data, int p_column) { if (!p_item) { return nullptr; @@ -83,7 +83,7 @@ QTreeWidgetItem *TreeWidget::findItemHelper(QTreeWidgetItem *p_item, const QVari int nrChild = p_item->childCount(); for (int i = 0; i < nrChild; ++i) { - auto item = findItemHelper(p_item->child(i), p_data); + auto item = findItemHelper(p_item->child(i), p_data, p_column); if (item) { return item; } @@ -217,24 +217,9 @@ void TreeWidget::dropEvent(QDropEvent *p_event) { auto dragItems = selectedItems(); - int first = -1, last = -1; - QTreeWidgetItem *firstItem = NULL; - for (int i = 0; i < dragItems.size(); ++i) { - int row = indexFromItem(dragItems[i]).row(); - if (row > last) { - last = row; - } - - if (first == -1 || row < first) { - first = row; - firstItem = dragItems[i]; - } - } - - Q_ASSERT(firstItem); - QTreeWidget::dropEvent(p_event); - int target = indexFromItem(firstItem).row(); - emit rowsMoved(first, last, target); + if (dragItems.size() == 1) { + emit itemMoved(dragItems[0]); + } } diff --git a/src/widgets/treewidget.h b/src/widgets/treewidget.h index c3f710e9..1d4a4d3d 100644 --- a/src/widgets/treewidget.h +++ b/src/widgets/treewidget.h @@ -26,7 +26,7 @@ namespace vnotex static void showHorizontalScrollbar(QTreeWidget *p_tree); - static QTreeWidgetItem *findItem(const QTreeWidget *p_widget, const QVariant &p_data); + static QTreeWidgetItem *findItem(const QTreeWidget *p_widget, const QVariant &p_data, int p_column = 0); // Next visible item. static QTreeWidgetItem *nextItem(const QTreeWidget* p_tree, @@ -36,8 +36,8 @@ namespace vnotex static QVector getVisibleItems(const QTreeWidget *p_widget); signals: - // Rows [@p_first, @p_last] were moved to @p_row. - void rowsMoved(int p_first, int p_last, int p_row); + // Emit when single item is selected and Drag&Drop to move internally. + void itemMoved(QTreeWidgetItem *p_item); protected: void mousePressEvent(QMouseEvent *p_event) Q_DECL_OVERRIDE; @@ -47,7 +47,7 @@ namespace vnotex void dropEvent(QDropEvent *p_event) Q_DECL_OVERRIDE; private: - static QTreeWidgetItem *findItemHelper(QTreeWidgetItem *p_item, const QVariant &p_data); + static QTreeWidgetItem *findItemHelper(QTreeWidgetItem *p_item, const QVariant &p_data, int p_column); static QTreeWidgetItem *nextSibling(const QTreeWidget *p_widget, QTreeWidgetItem *p_item, diff --git a/src/widgets/viewarea.cpp b/src/widgets/viewarea.cpp index a83c13cd..505ae145 100644 --- a/src/widgets/viewarea.cpp +++ b/src/widgets/viewarea.cpp @@ -1292,7 +1292,9 @@ void ViewArea::takeSnapshot(ViewAreaSession &p_session) const } wsSnap.m_currentViewWindowIndex = ws->m_currentViewWindowIndex; for (auto win : ws->m_viewWindows) { - wsSnap.m_viewWindows.push_back(win->saveSession()); + if (win->isSessionEnabled()) { + wsSnap.m_viewWindows.push_back(win->saveSession()); + } } } } diff --git a/src/widgets/viewwindow.cpp b/src/widgets/viewwindow.cpp index 799b5360..7a1c4701 100644 --- a/src/widgets/viewwindow.cpp +++ b/src/widgets/viewwindow.cpp @@ -29,6 +29,7 @@ #include "editreaddiscardaction.h" #include "viewsplit.h" #include "attachmentpopup.h" +#include "tagpopup.h" #include "outlinepopup.h" #include "dragdropareaindicator.h" #include "attachmentdragdropareaindicator.h" @@ -141,6 +142,8 @@ void ViewWindow::handleBufferChanged(const QSharedPointer &p this, &ViewWindow::attachmentChanged); } + m_sessionEnabled = p_paras->m_sessionEnabled; + handleBufferChangedInternal(p_paras); emit bufferChanged(); @@ -399,6 +402,19 @@ QAction *ViewWindow::addAction(QToolBar *p_toolBar, ViewWindowToolBarHelper::Act break; } + case ViewWindowToolBarHelper::Tag: + { + act = ViewWindowToolBarHelper::addAction(p_toolBar, p_action); + auto popup = static_cast(static_cast(p_toolBar->widgetForAction(act))->menu()); + connect(this, &ViewWindow::bufferChanged, + this, [this, act, popup]() { + auto buffer = getBuffer(); + act->setEnabled(buffer ? buffer->isTagSupported() : false); + popup->setBuffer(buffer); + }); + break; + } + case ViewWindowToolBarHelper::Outline: { act = ViewWindowToolBarHelper::addAction(p_toolBar, p_action); @@ -1152,6 +1168,7 @@ QToolBar *ViewWindow::createToolBar(QWidget *p_parent) toolBar->setIconSize(QSize(iconSize, iconSize)); /* + // The extension button of tool bar. auto extBtn = toolBar->findChild(QLatin1String("qt_toolbar_ext_button")); Q_ASSERT(extBtn); */ @@ -1239,3 +1256,8 @@ void ViewWindow::updateLastFindInfo(const QStringList &p_texts, FindOptions p_op m_findInfo.m_texts = p_texts; m_findInfo.m_options = p_options; } + +bool ViewWindow::isSessionEnabled() const +{ + return m_sessionEnabled; +} diff --git a/src/widgets/viewwindow.h b/src/widgets/viewwindow.h index 3d97c737..f289365b 100644 --- a/src/widgets/viewwindow.h +++ b/src/widgets/viewwindow.h @@ -95,6 +95,8 @@ namespace vnotex // Return the result from the FloatingWidget. QVariant showFloatingWidget(FloatingWidget *p_widget); + bool isSessionEnabled() const; + public slots: virtual void handleEditorConfigChange() = 0; @@ -316,6 +318,14 @@ namespace vnotex Buffer *m_buffer = nullptr; + // Whether check file missing or changed outside. + bool m_fileChangeCheckEnabled = true; + + // Last find info. + FindInfo m_findInfo; + + bool m_sessionEnabled = true; + // Null if this window has not been added to any split. ViewSplit *m_viewSplit = nullptr; @@ -342,12 +352,6 @@ namespace vnotex // Managed by QObject. QToolBar *m_toolBar = nullptr; - // Whether check file missing or changed outside. - bool m_fileChangeCheckEnabled = true; - - // Last find info. - FindInfo m_findInfo; - QSharedPointer m_statusWidget; EditReadDiscardAction *m_editReadDiscardAct = nullptr; diff --git a/src/widgets/viewwindowtoolbarhelper.cpp b/src/widgets/viewwindowtoolbarhelper.cpp index 889a0d15..931baa22 100644 --- a/src/widgets/viewwindowtoolbarhelper.cpp +++ b/src/widgets/viewwindowtoolbarhelper.cpp @@ -18,6 +18,7 @@ #include "editreaddiscardaction.h" #include "widgetsfactory.h" #include "attachmentpopup.h" +#include "tagpopup.h" #include "propertydefs.h" #include "outlinepopup.h" #include "viewwindow.h" @@ -293,6 +294,23 @@ QAction *ViewWindowToolBarHelper::addAction(QToolBar *p_tb, Action p_action) break; } + case Action::Tag: + { + act = p_tb->addAction(ToolBarHelper::generateIcon("tag_editor.svg"), + ViewWindow::tr("Tags")); + + auto toolBtn = dynamic_cast(p_tb->widgetForAction(act)); + Q_ASSERT(toolBtn); + toolBtn->setPopupMode(QToolButton::InstantPopup); + toolBtn->setProperty(PropertyDefs::c_toolButtonWithoutMenuIndicator, true); + + addButtonShortcut(toolBtn, editorConfig.getShortcut(Shortcut::Tag), viewWindow); + + auto menu = new TagPopup(toolBtn, p_tb); + toolBtn->setMenu(menu); + break; + } + case Action::Outline: { act = p_tb->addAction(ToolBarHelper::generateIcon("outline_editor.svg"), diff --git a/src/widgets/viewwindowtoolbarhelper.h b/src/widgets/viewwindowtoolbarhelper.h index e2a93d62..0e1ebf14 100644 --- a/src/widgets/viewwindowtoolbarhelper.h +++ b/src/widgets/viewwindowtoolbarhelper.h @@ -41,6 +41,7 @@ namespace vnotex TypeMax, Attachment, + Tag, Outline, FindAndReplace, SectionNumber, diff --git a/src/widgets/widgets.pri b/src/widgets/widgets.pri index 5dfa076b..ecb61cde 100644 --- a/src/widgets/widgets.pri +++ b/src/widgets/widgets.pri @@ -11,9 +11,12 @@ SOURCES += \ $$PWD/dialogs/importlegacynotebookdialog.cpp \ $$PWD/dialogs/importnotebookdialog.cpp \ $$PWD/dialogs/legacynotebookutils.cpp \ + $$PWD/dialogs/levellabelwithupbutton.cpp \ $$PWD/dialogs/linkinsertdialog.cpp \ $$PWD/dialogs/newnotebookfromfolderdialog.cpp \ $$PWD/dialogs/newsnippetdialog.cpp \ + $$PWD/dialogs/newtagdialog.cpp \ + $$PWD/dialogs/renametagdialog.cpp \ $$PWD/dialogs/selectdialog.cpp \ $$PWD/dialogs/selectionitemwidget.cpp \ $$PWD/dialogs/settings/appearancepage.cpp \ @@ -35,6 +38,7 @@ SOURCES += \ $$PWD/dialogs/sortdialog.cpp \ $$PWD/dialogs/tableinsertdialog.cpp \ $$PWD/dialogs/updater.cpp \ + $$PWD/dialogs/viewtagsdialog.cpp \ $$PWD/dockwidgethelper.cpp \ $$PWD/dragdropareaindicator.cpp \ $$PWD/editors/editormarkdownvieweradapter.cpp \ @@ -78,6 +82,9 @@ SOURCES += \ $$PWD/snippetpanel.cpp \ $$PWD/styleditemdelegate.cpp \ $$PWD/systemtrayhelper.cpp \ + $$PWD/tagexplorer.cpp \ + $$PWD/tagpopup.cpp \ + $$PWD/tagviewer.cpp \ $$PWD/textviewwindow.cpp \ $$PWD/toolbarhelper.cpp \ $$PWD/treeview.cpp \ @@ -104,7 +111,6 @@ SOURCES += \ $$PWD/dialogs/newnotedialog.cpp \ $$PWD/dialogs/managenotebooksdialog.cpp \ $$PWD/dialogs/notebookinfowidget.cpp \ - $$PWD/dialogs/nodelabelwithupbutton.cpp \ $$PWD/dialogs/notepropertiesdialog.cpp \ $$PWD/dialogs/folderpropertiesdialog.cpp \ $$PWD/dialogs/nodeinfowidget.cpp \ @@ -128,9 +134,12 @@ HEADERS += \ $$PWD/dialogs/importlegacynotebookdialog.h \ $$PWD/dialogs/importnotebookdialog.h \ $$PWD/dialogs/legacynotebookutils.h \ + $$PWD/dialogs/levellabelwithupbutton.h \ $$PWD/dialogs/linkinsertdialog.h \ $$PWD/dialogs/newnotebookfromfolderdialog.h \ $$PWD/dialogs/newsnippetdialog.h \ + $$PWD/dialogs/newtagdialog.h \ + $$PWD/dialogs/renametagdialog.h \ $$PWD/dialogs/selectdialog.h \ $$PWD/dialogs/selectionitemwidget.h \ $$PWD/dialogs/settings/appearancepage.h \ @@ -152,6 +161,7 @@ HEADERS += \ $$PWD/dialogs/sortdialog.h \ $$PWD/dialogs/tableinsertdialog.h \ $$PWD/dialogs/updater.h \ + $$PWD/dialogs/viewtagsdialog.h \ $$PWD/dockwidgethelper.h \ $$PWD/dragdropareaindicator.h \ $$PWD/editors/editormarkdownvieweradapter.h \ @@ -196,6 +206,9 @@ HEADERS += \ $$PWD/snippetpanel.h \ $$PWD/styleditemdelegate.h \ $$PWD/systemtrayhelper.h \ + $$PWD/tagexplorer.h \ + $$PWD/tagpopup.h \ + $$PWD/tagviewer.h \ $$PWD/textviewwindow.h \ $$PWD/textviewwindowhelper.h \ $$PWD/toolbarhelper.h \ @@ -224,7 +237,6 @@ HEADERS += \ $$PWD/dialogs/newnotedialog.h \ $$PWD/dialogs/managenotebooksdialog.h \ $$PWD/dialogs/notebookinfowidget.h \ - $$PWD/dialogs/nodelabelwithupbutton.h \ $$PWD/dialogs/notepropertiesdialog.h \ $$PWD/dialogs/folderpropertiesdialog.h \ $$PWD/dialogs/nodeinfowidget.h \ diff --git a/tests/test_core/test_notebook/dummynotebook.cpp b/tests/test_core/test_notebook/dummynotebook.cpp index 60d5e7b3..c53e02fd 100644 --- a/tests/test_core/test_notebook/dummynotebook.cpp +++ b/tests/test_core/test_notebook/dummynotebook.cpp @@ -23,20 +23,6 @@ void DummyNotebook::remove() { } -const QVector &DummyNotebook::getHistory() const -{ - return m_history; -} - -void DummyNotebook::addHistory(const vnotex::HistoryItem &p_item) -{ - Q_UNUSED(p_item); -} - -void DummyNotebook::clearHistory() -{ -} - void DummyNotebook::initializeInternal() { } diff --git a/tests/test_core/test_notebook/dummynotebook.h b/tests/test_core/test_notebook/dummynotebook.h index 2361b6cf..2d6fcc3e 100644 --- a/tests/test_core/test_notebook/dummynotebook.h +++ b/tests/test_core/test_notebook/dummynotebook.h @@ -17,10 +17,6 @@ namespace tests void remove() Q_DECL_OVERRIDE; - const QVector &getHistory() const Q_DECL_OVERRIDE; - void addHistory(const vnotex::HistoryItem &p_item) Q_DECL_OVERRIDE; - void clearHistory() Q_DECL_OVERRIDE; - const QJsonObject &getExtraConfigs() const Q_DECL_OVERRIDE; void setExtraConfig(const QString &p_key, const QJsonObject &p_obj) Q_DECL_OVERRIDE; @@ -29,8 +25,6 @@ namespace tests protected: void initializeInternal() Q_DECL_OVERRIDE; - QVector m_history; - QJsonObject m_extraConfigs; }; } diff --git a/tests/test_core/test_notebook/testnotebookdatabase.cpp b/tests/test_core/test_notebook/testnotebookdatabase.cpp index 77f49b34..4fa40871 100644 --- a/tests/test_core/test_notebook/testnotebookdatabase.cpp +++ b/tests/test_core/test_notebook/testnotebookdatabase.cpp @@ -9,6 +9,14 @@ using namespace tests; using namespace vnotex; +template +static void checkStringListEqual(T p_actual, T p_expected) +{ + std::sort(p_actual.begin(), p_actual.end()); + std::sort(p_expected.begin(), p_expected.end()); + QCOMPARE(p_actual, p_expected); +} + TestNotebookDatabase::TestNotebookDatabase() { QVERIFY(m_testDir.isValid()); @@ -31,6 +39,10 @@ TestNotebookDatabase::~TestNotebookDatabase() void TestNotebookDatabase::test() { testNode(); + + testTag(); + + testNodeTag(); } void TestNotebookDatabase::testNode() @@ -60,7 +72,7 @@ void TestNotebookDatabase::testNode() QVERIFY(node3->getId() != 0); // Node 4, deep level. - QScopedPointer node4(new DummyNode(Node::Flag::Content, 11, "ca", m_notebook.data(), node3.data())); + QScopedPointer node4(new DummyNode(Node::Flag::Container, 11, "ca", m_notebook.data(), node3.data())); addAndQueryNode(node4.data(), false); // Node 5, deep level. @@ -71,15 +83,15 @@ void TestNotebookDatabase::testNode() QScopedPointer node6(new DummyNode(Node::Flag::Content, 5, "cab", m_notebook.data(), node4.data())); addAndQueryNode(node6.data(), false); - // queryNodePath(). + // queryNodeParentPath(). { - testQueryNodePath(rootNode.data()); - testQueryNodePath(node1.data()); - testQueryNodePath(node2.data()); - testQueryNodePath(node3.data()); - testQueryNodePath(node4.data()); - testQueryNodePath(node5.data()); - testQueryNodePath(node6.data()); + testQueryNodeParentPath(rootNode.data()); + testQueryNodeParentPath(node1.data()); + testQueryNodeParentPath(node2.data()); + testQueryNodeParentPath(node3.data()); + testQueryNodeParentPath(node4.data()); + testQueryNodeParentPath(node5.data()); + testQueryNodeParentPath(node6.data()); } // updateNode(). @@ -135,9 +147,9 @@ void TestNotebookDatabase::queryAndVerifyNode(const vnotex::Node *p_node) QCOMPARE(nodeRec->m_parentId, p_node->getParent() ? p_node->getParent()->getId() : NotebookDatabaseAccess::InvalidId); } -void TestNotebookDatabase::testQueryNodePath(const vnotex::Node *p_node) +void TestNotebookDatabase::testQueryNodeParentPath(const vnotex::Node *p_node) { - auto nodePath = m_dbAccess->queryNodePath(p_node->getId()); + auto nodePath = m_dbAccess->queryNodeParentPath(p_node->getId()); auto node = p_node; for (int i = nodePath.size() - 1; i >= 0; --i) { QVERIFY(node); @@ -145,4 +157,156 @@ void TestNotebookDatabase::testQueryNodePath(const vnotex::Node *p_node) node = node->getParent(); } QVERIFY(m_dbAccess->checkNodePath(p_node, nodePath)); + + QCOMPARE(m_dbAccess->queryNodePath(p_node->getId()), p_node->fetchPath()); +} + +void TestNotebookDatabase::testTag() +{ + // Invalid tag. + { + auto nodeRec = m_dbAccess->queryTag("1"); + QVERIFY(nodeRec == nullptr); + } + + // Tag 1. + const QString tag1("1"); + addAndQueryTag(tag1, ""); + + // Tag 2. + QString tag2("2"); + addAndQueryTag(tag2, ""); + + // Tag 3 as child of tag 2. + QString tag3("21"); + addAndQueryTag(tag3, tag2); + checkStringListEqual({tag2, tag3}, m_dbAccess->queryTagAndChildren(tag2)); + + // Tag 4 as child of tag 2. + const QString tag4("22"); + addAndQueryTag(tag4, tag2); + checkStringListEqual({tag2, tag3, tag4}, m_dbAccess->queryTagAndChildren(tag2)); + + // Tag 5 as child of tag 4. + const QString tag5("221"); + addAndQueryTag(tag5, tag4); + checkStringListEqual({tag4, tag5}, m_dbAccess->queryTagAndChildren(tag4)); + checkStringListEqual({tag2, tag3, tag4, tag5}, m_dbAccess->queryTagAndChildren(tag2)); + + // Add with update. + addAndQueryTag(tag3, tag1); + + // Add without update. + { + bool ret = m_dbAccess->addTag(tag3); + QVERIFY(ret); + queryAndVerifyTag(tag3, tag1); + + ret = m_dbAccess->addTag("3"); + QVERIFY(ret); + queryAndVerifyTag("3", ""); + } + + // Rename. + { + bool ret = m_dbAccess->renameTag(tag3, "11"); + QVERIFY(ret); + queryAndVerifyTag("11", tag1); + + // Tag should be gone. + QVERIFY(!m_dbAccess->queryTag(tag3)); + tag3 = "11"; + + ret = m_dbAccess->renameTag(tag2, "new2"); + QVERIFY(ret); + queryAndVerifyTag("new2", ""); + + QVERIFY(!m_dbAccess->queryTag(tag2)); + tag2 = "new2"; + + queryAndVerifyTag(tag4, tag2); + queryAndVerifyTag(tag5, tag4); + } + + // removeTag(). + { + bool ret = m_dbAccess->removeTag(tag3); + QVERIFY(ret); + QVERIFY(!m_dbAccess->queryTag(tag3)); + + ret = m_dbAccess->removeTag(tag2); + QVERIFY(ret); + QVERIFY(!m_dbAccess->queryTag(tag2)); + QVERIFY(!m_dbAccess->queryTag(tag4)); + QVERIFY(!m_dbAccess->queryTag(tag5)); + + // Add back tags. + addAndQueryTag(tag3, tag1); + addAndQueryTag(tag2, ""); + addAndQueryTag(tag4, tag2); + addAndQueryTag(tag5, tag4); + } +} + +void TestNotebookDatabase::addAndQueryTag(const QString &p_name, const QString &p_parentName) +{ + bool ret = m_dbAccess->addTag(p_name, p_parentName); + QVERIFY(ret); + queryAndVerifyTag(p_name, p_parentName); +} + +void TestNotebookDatabase::queryAndVerifyTag(const QString &p_name, const QString &p_parentName) +{ + auto tagRec = m_dbAccess->queryTag(p_name); + QVERIFY(tagRec); + QCOMPARE(tagRec->m_name, p_name); + QCOMPARE(tagRec->m_parentName, p_parentName); +} + +void TestNotebookDatabase::testNodeTag() +{ + // Dummy root. + QScopedPointer rootNode(new DummyNode(Node::Flag::Container, 1, "", m_notebook.data(), nullptr)); + + // Node 10 -> tag1. + QScopedPointer node10(new DummyNode(Node::Flag::Content, 0, "o", m_notebook.data(), rootNode.data())); + addAndQueryNode(node10.data(), true); + node10->updateTags({"1"}); + updateNodeTagsAndCheck(node10.data()); + + // Node 11 -> tag2, tag3, tag1. + QScopedPointer node11(new DummyNode(Node::Flag::Container, 0, "p", m_notebook.data(), rootNode.data())); + addAndQueryNode(node11.data(), true); + node11->updateTags({"new2", "11", "1"}); + updateNodeTagsAndCheck(node11.data()); + + // Node 12 -> tag4, tag100. + QScopedPointer node12(new DummyNode(Node::Flag::Content, 0, "pa", m_notebook.data(), node11.data())); + addAndQueryNode(node12.data(), true); + node12->updateTags({"22", "100"}); + updateNodeTagsAndCheck(node12.data()); + + // Node 13 -> tag5. + QScopedPointer node13(new DummyNode(Node::Flag::Content, 0, "pb", m_notebook.data(), node11.data())); + addAndQueryNode(node13.data(), true); + node13->updateTags({"221"}); + updateNodeTagsAndCheck(node13.data()); + + checkStringListEqual(m_dbAccess->queryTagNodes("1"), {node10->getId(), node11->getId()}); + checkStringListEqual(m_dbAccess->queryTagNodes("11"), {node11->getId()}); + checkStringListEqual(m_dbAccess->queryTagNodes("new2"), {node11->getId()}); + checkStringListEqual(m_dbAccess->queryTagNodes("22"), {node12->getId()}); + checkStringListEqual(m_dbAccess->queryTagNodes("100"), {node12->getId()}); + checkStringListEqual(m_dbAccess->queryTagNodes("221"), {node13->getId()}); + + checkStringListEqual(m_dbAccess->queryTagNodesRecursive("1"), {node10->getId(), node11->getId()}); + checkStringListEqual(m_dbAccess->queryTagNodesRecursive("new2"), {node11->getId(), node12->getId(), node13->getId()}); + checkStringListEqual(m_dbAccess->queryTagNodesRecursive("22"), {node12->getId(), node13->getId()}); + checkStringListEqual(m_dbAccess->queryTagNodesRecursive("221"), {node13->getId()}); +} + +void TestNotebookDatabase::updateNodeTagsAndCheck(vnotex::Node *p_node) +{ + m_dbAccess->updateNodeTags(p_node); + checkStringListEqual(m_dbAccess->queryNodeTags(p_node->getId()), p_node->getTags()); } diff --git a/tests/test_core/test_notebook/testnotebookdatabase.h b/tests/test_core/test_notebook/testnotebookdatabase.h index e4c56e08..c99405a5 100644 --- a/tests/test_core/test_notebook/testnotebookdatabase.h +++ b/tests/test_core/test_notebook/testnotebookdatabase.h @@ -20,13 +20,23 @@ namespace tests private: void testNode(); + void testTag(); + + void testNodeTag(); + private: void addAndQueryNode(vnotex::Node *p_node, bool p_ignoreId); - void testQueryNodePath(const vnotex::Node *p_node); + void testQueryNodeParentPath(const vnotex::Node *p_node); void queryAndVerifyNode(const vnotex::Node *p_node); + void addAndQueryTag(const QString &p_name, const QString &p_parentName); + + void queryAndVerifyTag(const QString &p_name, const QString &p_parentName); + + void updateNodeTagsAndCheck(vnotex::Node *p_node); + QTemporaryDir m_testDir; QScopedPointer m_notebook;