Browse Source

support manual sort

Le Tan 4 năm trước cách đây
mục cha
commit
aa00164dff

+ 20 - 0
src/core/notebook/node.cpp

@@ -327,3 +327,23 @@ bool Node::canRename(const QString &p_newName) const
 
     return true;
 }
+
+void Node::sortChildren(const QVector<int> &p_beforeIdx, const QVector<int> &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();
+}

+ 2 - 0
src/core/notebook/node.h

@@ -158,6 +158,8 @@ namespace vnotex
 
         bool canRename(const QString &p_newName) const;
 
+        void sortChildren(const QVector<int> &p_beforeIdx, const QVector<int> &p_afterIdx);
+
         static bool isAncestor(const Node *p_ancestor, const Node *p_child);
 
     protected:

+ 1 - 2
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());
     }
 }

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

@@ -75,6 +75,7 @@
         <file>icons/outline_editor.svg</file>
         <file>icons/find_replace_editor.svg</file>
         <file>icons/section_number_editor.svg</file>
+        <file>icons/sort.svg</file>
         <file>logo/vnote.svg</file>
         <file>logo/vnote.png</file>
         <file>logo/256x256/vnote.png</file>

+ 8 - 0
src/data/core/icons/sort.svg

@@ -0,0 +1,8 @@
+<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
+ <!-- Created with SVG-edit - http://svg-edit.googlecode.com/ -->
+ <g>
+  <title>Layer 2</title>
+  <text fill="#000000" stroke-width="0" x="50.16984" y="94.53242" id="svg_1" font-size="24" font-family="Sans-serif" text-anchor="middle" xml:space="preserve" transform="matrix(14.638787563577418,0,0,15.425402876908336,-563.6705867690341,-1147.819919301283) " stroke="#000000">A</text>
+  <text id="svg_3" fill="#000000" stroke-width="0" x="42.10906" y="100.56143" font-size="24" font-family="Sans-serif" text-anchor="middle" xml:space="preserve" transform="matrix(14.638787563577418,0,0,15.425402876908336,-251.02721518700292,-1086.2459218037245) " stroke="#000000">Z</text>
+ </g>
+</svg>

+ 2 - 6
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);
     }
 }
 

+ 257 - 0
src/widgets/dialogs/sortdialog.cpp

@@ -0,0 +1,257 @@
+#include "sortdialog.h"
+
+#include <QLabel>
+#include <QPushButton>
+#include <QVBoxLayout>
+#include <QHBoxLayout>
+#include <QHeaderView>
+
+#include <widgets/treewidget.h>
+#include <widgets/widgetsfactory.h>
+
+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<TreeWidget *>(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<QVariant> SortDialog::getSortedData() const
+{
+    const int cnt = m_treeWidget->topLevelItemCount();
+    QVector<QVariant> 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<QTreeWidgetItem *> 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);
+    }
+}

+ 44 - 0
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<QVariant> 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

+ 83 - 1
src/widgets/notebooknodeexplorer.cpp

@@ -1,6 +1,11 @@
 #include "notebooknodeexplorer.h"
 
-#include <QtWidgets>
+#include <QTreeWidget>
+#include <QVBoxLayout>
+#include <QSplitter>
+#include <QTreeWidget>
+#include <QMenu>
+#include <QAction>
 
 #include <notebook/notebook.h>
 #include <notebook/node.h>
@@ -13,6 +18,7 @@
 #include "dialogs/notepropertiesdialog.h"
 #include "dialogs/folderpropertiesdialog.h"
 #include "dialogs/deleteconfirmdialog.h"
+#include "dialogs/sortdialog.h"
 #include <utils/widgetutils.h>
 #include <utils/pathutils.h>
 #include <utils/clipboardutils.h>
@@ -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<int> 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<int> sortedIdx(data.size(), -1);
+        for (int i = 0; i < data.size(); ++i) {
+            sortedIdx[i] = data[i].toInt();
+        }
+        parentNode->sortChildren(selectedIdx, sortedIdx);
+        updateNode(parentNode);
+    }
+}

+ 19 - 3
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<QSharedPointer<Node>> &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);

+ 27 - 0
src/widgets/treewidget.cpp

@@ -3,6 +3,7 @@
 #include <QMouseEvent>
 #include <QHeaderView>
 #include <QKeyEvent>
+#include <QDropEvent>
 
 #include <utils/widgetutils.h>
 
@@ -212,3 +213,29 @@ QVector<QTreeWidgetItem *> 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);
+}

+ 6 - 0
src/widgets/treewidget.h

@@ -34,11 +34,17 @@ namespace vnotex
 
         static QVector<QTreeWidgetItem *> 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);
 

+ 2 - 0
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 \