support manual sort

This commit is contained in:
Le Tan 2021-02-28 10:15:52 +08:00
parent 319da24989
commit aa00164dff
13 changed files with 472 additions and 12 deletions

View File

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

View File

@ -158,6 +158,8 @@ namespace vnotex
bool canRename(const QString &p_newName) const; 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); static bool isAncestor(const Node *p_ancestor, const Node *p_child);
protected: protected:

View File

@ -488,11 +488,10 @@ void VXNotebookConfigMgr::loadNode(Node *p_node) const
void VXNotebookConfigMgr::saveNode(const Node *p_node) void VXNotebookConfigMgr::saveNode(const Node *p_node)
{ {
Q_ASSERT(!p_node->isRoot());
if (p_node->isContainer()) { if (p_node->isContainer()) {
writeNodeConfig(p_node); writeNodeConfig(p_node);
} else { } else {
Q_ASSERT(!p_node->isRoot());
writeNodeConfig(p_node->getParent()); writeNodeConfig(p_node->getParent());
} }
} }

View File

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

View File

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

After

Width:  |  Height:  |  Size: 789 B

View File

@ -58,9 +58,7 @@ void NodeInfoWidget::setupUI(const Node *p_parentNode, Node::Flags p_newNodeFlag
if (!createMode) { if (!createMode) {
m_createdDateTimeLabel = new QLabel(this); m_createdDateTimeLabel = new QLabel(this);
m_mainLayout->addRow(tr("Created time:"), m_createdDateTimeLabel); m_mainLayout->addRow(tr("Created time:"), m_createdDateTimeLabel);
}
if (!createMode && isNote) {
m_modifiedDateTimeLabel = new QLabel(this); m_modifiedDateTimeLabel = new QLabel(this);
m_mainLayout->addRow(tr("Modified time:"), m_modifiedDateTimeLabel); 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()); auto createdTime = Utils::dateTimeString(m_node->getCreatedTimeUtc().toLocalTime());
m_createdDateTimeLabel->setText(createdTime); m_createdDateTimeLabel->setText(createdTime);
if (m_modifiedDateTimeLabel) { auto modifiedTime = Utils::dateTimeString(m_node->getModifiedTimeUtc().toLocalTime());
auto modifiedTime = Utils::dateTimeString(m_node->getModifiedTimeUtc().toLocalTime()); m_modifiedDateTimeLabel->setText(modifiedTime);
m_modifiedDateTimeLabel->setText(modifiedTime);
}
} }
} }

View File

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

View File

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

View File

@ -1,6 +1,11 @@
#include "notebooknodeexplorer.h" #include "notebooknodeexplorer.h"
#include <QtWidgets> #include <QTreeWidget>
#include <QVBoxLayout>
#include <QSplitter>
#include <QTreeWidget>
#include <QMenu>
#include <QAction>
#include <notebook/notebook.h> #include <notebook/notebook.h>
#include <notebook/node.h> #include <notebook/node.h>
@ -13,6 +18,7 @@
#include "dialogs/notepropertiesdialog.h" #include "dialogs/notepropertiesdialog.h"
#include "dialogs/folderpropertiesdialog.h" #include "dialogs/folderpropertiesdialog.h"
#include "dialogs/deleteconfirmdialog.h" #include "dialogs/deleteconfirmdialog.h"
#include "dialogs/sortdialog.h"
#include <utils/widgetutils.h> #include <utils/widgetutils.h>
#include <utils/pathutils.h> #include <utils/pathutils.h>
#include <utils/clipboardutils.h> #include <utils/clipboardutils.h>
@ -742,6 +748,11 @@ void NotebookNodeExplorer::createContextMenuOnNode(QMenu *p_menu, const Node *p_
act = createAction(Action::RemoveFromConfig, p_menu); act = createAction(Action::RemoveFromConfig, p_menu);
p_menu->addAction(act); p_menu->addAction(act);
p_menu->addSeparator();
act = createAction(Action::Sort, p_menu);
p_menu->addAction(act);
if (selectedSize == 1) { if (selectedSize == 1) {
p_menu->addSeparator(); p_menu->addSeparator();
@ -913,6 +924,7 @@ QAction *NotebookNodeExplorer::createAction(Action p_act, QObject *p_parent)
break; break;
case Action::DeleteFromRecycleBin: 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); act = new QAction(tr("&Delete From Recycle Bin"), p_parent);
connect(act, &QAction::triggered, connect(act, &QAction::triggered,
this, [this]() { this, [this]() {
@ -927,6 +939,14 @@ QAction *NotebookNodeExplorer::createAction(Action p_act, QObject *p_parent)
removeSelectedNodesFromConfig(); removeSelectedNodesFromConfig();
}); });
break; break;
case Action::Sort:
act = new QAction(generateMenuActionIcon("sort.svg"), tr("&Sort"), p_parent);
connect(act, &QAction::triggered,
this, [this]() {
manualSort();
});
break;
} }
return act; return act;
@ -1416,3 +1436,65 @@ void NotebookNodeExplorer::setRecycleBinNodeVisible(bool p_visible)
m_recycleBinNodeVisible = p_visible; m_recycleBinNodeVisible = p_visible;
reload(); 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);
}
}

View File

@ -118,9 +118,22 @@ namespace vnotex
private: private:
enum Column { Name = 0 }; enum Column { Name = 0 };
enum Action { NewNote, NewFolder, Properties, OpenLocation, CopyPath, enum class Action
Copy, Cut, Paste, EmptyRecycleBin, Delete, {
DeleteFromRecycleBin, RemoveFromConfig }; NewNote,
NewFolder,
Properties,
OpenLocation,
CopyPath,
Copy,
Cut,
Paste,
EmptyRecycleBin,
Delete,
DeleteFromRecycleBin,
RemoveFromConfig,
Sort
};
void setupUI(); void setupUI();
@ -210,6 +223,9 @@ namespace vnotex
// [p_start, p_end). // [p_start, p_end).
void sortNodes(QVector<QSharedPointer<Node>> &p_nodes, int p_start, int p_end, int p_viewOrder) const; 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 NotebookNodeExplorer::NodeData getItemNodeData(const QTreeWidgetItem *p_item);
static void setItemNodeData(QTreeWidgetItem *p_item, const NodeData &p_data); static void setItemNodeData(QTreeWidgetItem *p_item, const NodeData &p_data);

View File

@ -3,6 +3,7 @@
#include <QMouseEvent> #include <QMouseEvent>
#include <QHeaderView> #include <QHeaderView>
#include <QKeyEvent> #include <QKeyEvent>
#include <QDropEvent>
#include <utils/widgetutils.h> #include <utils/widgetutils.h>
@ -212,3 +213,29 @@ QVector<QTreeWidgetItem *> TreeWidget::getVisibleItems(const QTreeWidget *p_widg
return items; 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);
}

View File

@ -34,11 +34,17 @@ namespace vnotex
static QVector<QTreeWidgetItem *> getVisibleItems(const QTreeWidget *p_widget); 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: protected:
void mousePressEvent(QMouseEvent *p_event) Q_DECL_OVERRIDE; void mousePressEvent(QMouseEvent *p_event) Q_DECL_OVERRIDE;
void keyPressEvent(QKeyEvent *p_event) Q_DECL_OVERRIDE; void keyPressEvent(QKeyEvent *p_event) Q_DECL_OVERRIDE;
void dropEvent(QDropEvent *p_event) Q_DECL_OVERRIDE;
private: private:
static QTreeWidgetItem *findItemHelper(QTreeWidgetItem *p_item, const QVariant &p_data); static QTreeWidgetItem *findItemHelper(QTreeWidgetItem *p_item, const QVariant &p_data);

View File

@ -23,6 +23,7 @@ SOURCES += \
$$PWD/dialogs/settings/settingsdialog.cpp \ $$PWD/dialogs/settings/settingsdialog.cpp \
$$PWD/dialogs/settings/texteditorpage.cpp \ $$PWD/dialogs/settings/texteditorpage.cpp \
$$PWD/dialogs/settings/themepage.cpp \ $$PWD/dialogs/settings/themepage.cpp \
$$PWD/dialogs/sortdialog.cpp \
$$PWD/dialogs/tableinsertdialog.cpp \ $$PWD/dialogs/tableinsertdialog.cpp \
$$PWD/dragdropareaindicator.cpp \ $$PWD/dragdropareaindicator.cpp \
$$PWD/editors/editormarkdownvieweradapter.cpp \ $$PWD/editors/editormarkdownvieweradapter.cpp \
@ -109,6 +110,7 @@ HEADERS += \
$$PWD/dialogs/settings/settingsdialog.h \ $$PWD/dialogs/settings/settingsdialog.h \
$$PWD/dialogs/settings/texteditorpage.h \ $$PWD/dialogs/settings/texteditorpage.h \
$$PWD/dialogs/settings/themepage.h \ $$PWD/dialogs/settings/themepage.h \
$$PWD/dialogs/sortdialog.h \
$$PWD/dialogs/tableinsertdialog.h \ $$PWD/dialogs/tableinsertdialog.h \
$$PWD/dragdropareaindicator.h \ $$PWD/dragdropareaindicator.h \
$$PWD/editors/editormarkdownvieweradapter.h \ $$PWD/editors/editormarkdownvieweradapter.h \