From aa00164dff6f45b52c1c7cce29f635906d8033e6 Mon Sep 17 00:00:00 2001 From: Le Tan Date: Sun, 28 Feb 2021 10:15:52 +0800 Subject: [PATCH] support manual sort --- src/core/notebook/node.cpp | 20 ++ src/core/notebook/node.h | 2 + .../notebookconfigmgr/vxnotebookconfigmgr.cpp | 3 +- src/data/core/core.qrc | 1 + src/data/core/icons/sort.svg | 8 + src/widgets/dialogs/nodeinfowidget.cpp | 8 +- src/widgets/dialogs/sortdialog.cpp | 257 ++++++++++++++++++ src/widgets/dialogs/sortdialog.h | 44 +++ src/widgets/notebooknodeexplorer.cpp | 84 +++++- src/widgets/notebooknodeexplorer.h | 22 +- src/widgets/treewidget.cpp | 27 ++ src/widgets/treewidget.h | 6 + src/widgets/widgets.pri | 2 + 13 files changed, 472 insertions(+), 12 deletions(-) create mode 100644 src/data/core/icons/sort.svg create mode 100644 src/widgets/dialogs/sortdialog.cpp create mode 100644 src/widgets/dialogs/sortdialog.h diff --git a/src/core/notebook/node.cpp b/src/core/notebook/node.cpp index 51a19a1a..668ccbd7 100644 --- a/src/core/notebook/node.cpp +++ b/src/core/notebook/node.cpp @@ -327,3 +327,23 @@ bool Node::canRename(const QString &p_newName) const return true; } + +void Node::sortChildren(const QVector &p_beforeIdx, const QVector &p_afterIdx) +{ + Q_ASSERT(isContainer()); + + Q_ASSERT(p_beforeIdx.size() == p_afterIdx.size()); + + if (p_beforeIdx == p_afterIdx) { + return; + } + + auto ori = m_children; + for (int i = 0; i < p_beforeIdx.size(); ++i) { + if (p_beforeIdx[i] != p_afterIdx[i]) { + m_children[p_beforeIdx[i]] = ori[p_afterIdx[i]]; + } + } + + save(); +} diff --git a/src/core/notebook/node.h b/src/core/notebook/node.h index acd79f8f..70d60dcc 100644 --- a/src/core/notebook/node.h +++ b/src/core/notebook/node.h @@ -158,6 +158,8 @@ namespace vnotex bool canRename(const QString &p_newName) const; + void sortChildren(const QVector &p_beforeIdx, const QVector &p_afterIdx); + static bool isAncestor(const Node *p_ancestor, const Node *p_child); protected: diff --git a/src/core/notebookconfigmgr/vxnotebookconfigmgr.cpp b/src/core/notebookconfigmgr/vxnotebookconfigmgr.cpp index e1c4c612..b7ae387f 100644 --- a/src/core/notebookconfigmgr/vxnotebookconfigmgr.cpp +++ b/src/core/notebookconfigmgr/vxnotebookconfigmgr.cpp @@ -488,11 +488,10 @@ void VXNotebookConfigMgr::loadNode(Node *p_node) const void VXNotebookConfigMgr::saveNode(const Node *p_node) { - Q_ASSERT(!p_node->isRoot()); - if (p_node->isContainer()) { writeNodeConfig(p_node); } else { + Q_ASSERT(!p_node->isRoot()); writeNodeConfig(p_node->getParent()); } } diff --git a/src/data/core/core.qrc b/src/data/core/core.qrc index db9cfab6..bf3f485a 100644 --- a/src/data/core/core.qrc +++ b/src/data/core/core.qrc @@ -75,6 +75,7 @@ icons/outline_editor.svg icons/find_replace_editor.svg icons/section_number_editor.svg + icons/sort.svg logo/vnote.svg logo/vnote.png logo/256x256/vnote.png diff --git a/src/data/core/icons/sort.svg b/src/data/core/icons/sort.svg new file mode 100644 index 00000000..1059e3ea --- /dev/null +++ b/src/data/core/icons/sort.svg @@ -0,0 +1,8 @@ + + + + Layer 2 + A + Z + + diff --git a/src/widgets/dialogs/nodeinfowidget.cpp b/src/widgets/dialogs/nodeinfowidget.cpp index 5d3a518a..4f5726fa 100644 --- a/src/widgets/dialogs/nodeinfowidget.cpp +++ b/src/widgets/dialogs/nodeinfowidget.cpp @@ -58,9 +58,7 @@ void NodeInfoWidget::setupUI(const Node *p_parentNode, Node::Flags p_newNodeFlag if (!createMode) { m_createdDateTimeLabel = new QLabel(this); m_mainLayout->addRow(tr("Created time:"), m_createdDateTimeLabel); - } - if (!createMode && isNote) { m_modifiedDateTimeLabel = new QLabel(this); m_mainLayout->addRow(tr("Modified time:"), m_modifiedDateTimeLabel); } @@ -135,10 +133,8 @@ void NodeInfoWidget::setNode(const Node *p_node) auto createdTime = Utils::dateTimeString(m_node->getCreatedTimeUtc().toLocalTime()); m_createdDateTimeLabel->setText(createdTime); - if (m_modifiedDateTimeLabel) { - auto modifiedTime = Utils::dateTimeString(m_node->getModifiedTimeUtc().toLocalTime()); - m_modifiedDateTimeLabel->setText(modifiedTime); - } + auto modifiedTime = Utils::dateTimeString(m_node->getModifiedTimeUtc().toLocalTime()); + m_modifiedDateTimeLabel->setText(modifiedTime); } } diff --git a/src/widgets/dialogs/sortdialog.cpp b/src/widgets/dialogs/sortdialog.cpp new file mode 100644 index 00000000..473e9de7 --- /dev/null +++ b/src/widgets/dialogs/sortdialog.cpp @@ -0,0 +1,257 @@ +#include "sortdialog.h" + +#include +#include +#include +#include +#include + +#include +#include + +using namespace vnotex; + +SortDialog::SortDialog(const QString &p_title, + const QString &p_info, + QWidget *p_parent) + : ScrollDialog(p_parent) +{ + setupUI(p_title, p_info); +} + +void SortDialog::setupUI(const QString &p_title, const QString &p_info) +{ + auto mainWidget = new QWidget(this); + setCentralWidget(mainWidget); + + auto mainLayout = new QVBoxLayout(mainWidget); + + if (!p_info.isEmpty()) { + auto infoLabel = new QLabel(p_info, mainWidget); + infoLabel->setWordWrap(true); + mainLayout->addWidget(infoLabel); + } + + { + auto bodyLayout = new QHBoxLayout(); + mainLayout->addLayout(bodyLayout); + + // Tree widget. + m_treeWidget = new TreeWidget(mainWidget); + 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. + auto btnLayout = new QVBoxLayout(); + bodyLayout->addLayout(btnLayout); + + auto topBtn = new QPushButton(tr("&Top"), mainWidget); + connect(topBtn, &QPushButton::clicked, + this, [this]() { + handleMoveOperation(MoveOperation::Top); + }); + btnLayout->addWidget(topBtn); + + auto upBtn = new QPushButton(tr("&Up"), mainWidget); + connect(upBtn, &QPushButton::clicked, + this, [this]() { + handleMoveOperation(MoveOperation::Up); + }); + btnLayout->addWidget(upBtn); + + auto downBtn = new QPushButton(tr("&Down"), mainWidget); + connect(downBtn, &QPushButton::clicked, + this, [this]() { + handleMoveOperation(MoveOperation::Down); + }); + btnLayout->addWidget(downBtn); + + auto bottomBtn = new QPushButton(tr("&Bottom"), mainWidget); + connect(bottomBtn, &QPushButton::clicked, + this, [this]() { + handleMoveOperation(MoveOperation::Bottom); + }); + btnLayout->addWidget(bottomBtn); + + btnLayout->addStretch(); + } + + setDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + setWindowTitle(p_title); +} + +QTreeWidget *SortDialog::getTreeWidget() const +{ + return m_treeWidget; +} + +void SortDialog::updateTreeWidget() +{ + int cols = m_treeWidget->columnCount(); + for (int i = 0; i < cols; ++i) { + m_treeWidget->resizeColumnToContents(i); + } + + QHeaderView *header = m_treeWidget->header(); + if (header) { + header->setStretchLastSection(true); + } + + // We just need single level. + int cnt = m_treeWidget->topLevelItemCount(); + for (int i = 0; i < cnt; ++i) { + QTreeWidgetItem *item = m_treeWidget->topLevelItem(i); + item->setFlags(item->flags() & ~Qt::ItemIsDropEnabled); + } + + m_treeWidget->sortByColumn(-1); + m_treeWidget->setSortingEnabled(true); +} + +QVector SortDialog::getSortedData() const +{ + const int cnt = m_treeWidget->topLevelItemCount(); + QVector data(cnt); + for (int i = 0; i < cnt; ++i) { + QTreeWidgetItem *item = m_treeWidget->topLevelItem(i); + Q_ASSERT(item); + data[i] = item->data(0, Qt::UserRole); + } + + return data; +} + +void SortDialog::handleMoveOperation(MoveOperation p_op) +{ + const QList selectedItems = m_treeWidget->selectedItems(); + if (selectedItems.isEmpty()) { + return; + } + + int first = m_treeWidget->topLevelItemCount(); + int last = -1; + for (const auto &it : selectedItems) { + int idx = m_treeWidget->indexOfTopLevelItem(it); + Q_ASSERT(idx > -1); + if (idx < first) { + first = idx; + } + + if (idx > last) { + last = idx; + } + } + + Q_ASSERT(first <= last && (last - first + 1) == selectedItems.size()); + QTreeWidgetItem *firstItem = nullptr; + + m_treeWidget->sortByColumn(-1); + + switch (p_op) { + case MoveOperation::Top: + if (first == 0) { + break; + } + + m_treeWidget->clearSelection(); + + // Insert item[last] to index 0 repeatedly. + for (int i = last - first; i >= 0; --i) { + QTreeWidgetItem *item = m_treeWidget->takeTopLevelItem(last); + Q_ASSERT(item); + m_treeWidget->insertTopLevelItem(0, item); + item->setSelected(true); + } + + firstItem = m_treeWidget->topLevelItem(0); + + break; + + case MoveOperation::Up: + if (first == 0) { + break; + } + + m_treeWidget->clearSelection(); + + // Insert item[last] to index (first -1) repeatedly. + for (int i = last - first; i >= 0; --i) { + QTreeWidgetItem *item = m_treeWidget->takeTopLevelItem(last); + Q_ASSERT(item); + m_treeWidget->insertTopLevelItem(first - 1, item); + item->setSelected(true); + } + + firstItem = m_treeWidget->topLevelItem(first - 1); + + break; + + case MoveOperation::Down: + if (last == m_treeWidget->topLevelItemCount() - 1) { + break; + } + + m_treeWidget->clearSelection(); + + // Insert item[first] to index (last) repeatedly. + for (int i = last - first; i >= 0; --i) { + QTreeWidgetItem *item = m_treeWidget->takeTopLevelItem(first); + Q_ASSERT(item); + m_treeWidget->insertTopLevelItem(last + 1, item); + item->setSelected(true); + + if (!firstItem) { + firstItem = item; + } + } + + break; + + case MoveOperation::Bottom: + if (last == m_treeWidget->topLevelItemCount() - 1) { + break; + } + + m_treeWidget->clearSelection(); + + // Insert item[first] to the last of the tree repeatedly. + for (int i = last - first; i >= 0; --i) { + QTreeWidgetItem *item = m_treeWidget->takeTopLevelItem(first); + Q_ASSERT(item); + m_treeWidget->addTopLevelItem(item); + item->setSelected(true); + + if (!firstItem) { + firstItem = item; + } + } + + break; + + default: + return; + } + + if (firstItem) { + m_treeWidget->setCurrentItem(firstItem); + m_treeWidget->scrollToItem(firstItem); + } +} diff --git a/src/widgets/dialogs/sortdialog.h b/src/widgets/dialogs/sortdialog.h new file mode 100644 index 00000000..b8c04884 --- /dev/null +++ b/src/widgets/dialogs/sortdialog.h @@ -0,0 +1,44 @@ +#ifndef SORTDIALOG_H +#define SORTDIALOG_H + +#include "scrolldialog.h" + +class QTreeWidget; +class QPushButton; + +namespace vnotex +{ + class SortDialog : public ScrollDialog + { + Q_OBJECT + public: + SortDialog(const QString &p_title, const QString &p_info, QWidget *p_parent = nullptr); + + QTreeWidget *getTreeWidget() const; + + // Called after updating the QTreeWidget from getTreeWidget(). + void updateTreeWidget(); + + // Get user data of column 0 from sorted items. + QVector getSortedData() const; + + private: + enum MoveOperation + { + Top, + Up, + Down, + Bottom + }; + + private slots: + void handleMoveOperation(MoveOperation p_op); + + private: + void setupUI(const QString &p_title, const QString &p_info); + + QTreeWidget *m_treeWidget = nullptr; + }; +} + +#endif // SORTDIALOG_H diff --git a/src/widgets/notebooknodeexplorer.cpp b/src/widgets/notebooknodeexplorer.cpp index a0dc3f29..2ab13dbc 100644 --- a/src/widgets/notebooknodeexplorer.cpp +++ b/src/widgets/notebooknodeexplorer.cpp @@ -1,6 +1,11 @@ #include "notebooknodeexplorer.h" -#include +#include +#include +#include +#include +#include +#include #include #include @@ -13,6 +18,7 @@ #include "dialogs/notepropertiesdialog.h" #include "dialogs/folderpropertiesdialog.h" #include "dialogs/deleteconfirmdialog.h" +#include "dialogs/sortdialog.h" #include #include #include @@ -742,6 +748,11 @@ void NotebookNodeExplorer::createContextMenuOnNode(QMenu *p_menu, const Node *p_ act = createAction(Action::RemoveFromConfig, p_menu); p_menu->addAction(act); + p_menu->addSeparator(); + + act = createAction(Action::Sort, p_menu); + p_menu->addAction(act); + if (selectedSize == 1) { p_menu->addSeparator(); @@ -913,6 +924,7 @@ QAction *NotebookNodeExplorer::createAction(Action p_act, QObject *p_parent) break; case Action::DeleteFromRecycleBin: + // It is fine to have &D with Action::Delete since they won't be at the same context. act = new QAction(tr("&Delete From Recycle Bin"), p_parent); connect(act, &QAction::triggered, this, [this]() { @@ -927,6 +939,14 @@ QAction *NotebookNodeExplorer::createAction(Action p_act, QObject *p_parent) removeSelectedNodesFromConfig(); }); break; + + case Action::Sort: + act = new QAction(generateMenuActionIcon("sort.svg"), tr("&Sort"), p_parent); + connect(act, &QAction::triggered, + this, [this]() { + manualSort(); + }); + break; } return act; @@ -1416,3 +1436,65 @@ void NotebookNodeExplorer::setRecycleBinNodeVisible(bool p_visible) m_recycleBinNodeVisible = p_visible; reload(); } + +void NotebookNodeExplorer::manualSort() +{ + auto node = getCurrentNode(); + if (!node) { + return; + } + + auto parentNode = node->getParent(); + bool isNotebook = parentNode->isRoot(); + + // Check whether sort files or folders based on current node type. + bool sortFolders = node->isContainer(); + + SortDialog sortDlg(sortFolders ? tr("Sort Folders") : tr("Sort Notes"), + tr("Sort nodes under %1 (%2) in the configuration file.").arg( + isNotebook ? tr("notebook") : tr("folder"), + isNotebook ? m_notebook->getName() : parentNode->getName()), + VNoteX::getInst().getMainWindow()); + + QVector selectedIdx; + + // Update the tree. + { + auto treeWidget = sortDlg.getTreeWidget(); + treeWidget->clear(); + treeWidget->setColumnCount(2); + treeWidget->setHeaderLabels({tr("Name"), tr("Created Time"), tr("Modified Time")}); + + const auto &children = parentNode->getChildren(); + for (int i = 0; i < children.size(); ++i) { + const auto &child = children[i]; + if (m_notebook->isRecycleBinNode(child.data())) { + continue; + } + + bool selected = sortFolders ? child->isContainer() : !child->isContainer(); + if (selected) { + selectedIdx.push_back(i); + + QStringList cols {child->getName(), + Utils::dateTimeString(child->getCreatedTimeUtc().toLocalTime()), + Utils::dateTimeString(child->getModifiedTimeUtc().toLocalTime())}; + auto item = new QTreeWidgetItem(treeWidget, cols); + item->setData(0, Qt::UserRole, i); + } + } + + sortDlg.updateTreeWidget(); + } + + if (sortDlg.exec() == QDialog::Accepted) { + const auto data = sortDlg.getSortedData(); + Q_ASSERT(data.size() == selectedIdx.size()); + QVector sortedIdx(data.size(), -1); + for (int i = 0; i < data.size(); ++i) { + sortedIdx[i] = data[i].toInt(); + } + parentNode->sortChildren(selectedIdx, sortedIdx); + updateNode(parentNode); + } +} diff --git a/src/widgets/notebooknodeexplorer.h b/src/widgets/notebooknodeexplorer.h index 1498b203..788a17a0 100644 --- a/src/widgets/notebooknodeexplorer.h +++ b/src/widgets/notebooknodeexplorer.h @@ -118,9 +118,22 @@ namespace vnotex private: enum Column { Name = 0 }; - enum Action { NewNote, NewFolder, Properties, OpenLocation, CopyPath, - Copy, Cut, Paste, EmptyRecycleBin, Delete, - DeleteFromRecycleBin, RemoveFromConfig }; + enum class Action + { + NewNote, + NewFolder, + Properties, + OpenLocation, + CopyPath, + Copy, + Cut, + Paste, + EmptyRecycleBin, + Delete, + DeleteFromRecycleBin, + RemoveFromConfig, + Sort + }; void setupUI(); @@ -210,6 +223,9 @@ namespace vnotex // [p_start, p_end). void sortNodes(QVector> &p_nodes, int p_start, int p_end, int p_viewOrder) const; + // Sort nodes in config file. + void manualSort(); + static NotebookNodeExplorer::NodeData getItemNodeData(const QTreeWidgetItem *p_item); static void setItemNodeData(QTreeWidgetItem *p_item, const NodeData &p_data); diff --git a/src/widgets/treewidget.cpp b/src/widgets/treewidget.cpp index 9ff0f78a..e49051b7 100644 --- a/src/widgets/treewidget.cpp +++ b/src/widgets/treewidget.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include @@ -212,3 +213,29 @@ QVector TreeWidget::getVisibleItems(const QTreeWidget *p_widg return items; } + +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); +} diff --git a/src/widgets/treewidget.h b/src/widgets/treewidget.h index f7c1bc32..9d41b5eb 100644 --- a/src/widgets/treewidget.h +++ b/src/widgets/treewidget.h @@ -34,11 +34,17 @@ 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); + protected: void mousePressEvent(QMouseEvent *p_event) Q_DECL_OVERRIDE; void keyPressEvent(QKeyEvent *p_event) Q_DECL_OVERRIDE; + void dropEvent(QDropEvent *p_event) Q_DECL_OVERRIDE; + private: static QTreeWidgetItem *findItemHelper(QTreeWidgetItem *p_item, const QVariant &p_data); diff --git a/src/widgets/widgets.pri b/src/widgets/widgets.pri index 714c33df..14460e73 100644 --- a/src/widgets/widgets.pri +++ b/src/widgets/widgets.pri @@ -23,6 +23,7 @@ SOURCES += \ $$PWD/dialogs/settings/settingsdialog.cpp \ $$PWD/dialogs/settings/texteditorpage.cpp \ $$PWD/dialogs/settings/themepage.cpp \ + $$PWD/dialogs/sortdialog.cpp \ $$PWD/dialogs/tableinsertdialog.cpp \ $$PWD/dragdropareaindicator.cpp \ $$PWD/editors/editormarkdownvieweradapter.cpp \ @@ -109,6 +110,7 @@ HEADERS += \ $$PWD/dialogs/settings/settingsdialog.h \ $$PWD/dialogs/settings/texteditorpage.h \ $$PWD/dialogs/settings/themepage.h \ + $$PWD/dialogs/sortdialog.h \ $$PWD/dialogs/tableinsertdialog.h \ $$PWD/dragdropareaindicator.h \ $$PWD/editors/editormarkdownvieweradapter.h \