From 09181fab13d1b806bcee1444bec75b9825c5147d Mon Sep 17 00:00:00 2001 From: Balazs Gyurak Date: Tue, 18 Jun 2019 21:58:47 +0100 Subject: [PATCH] Add group sorting feature * Enabling sorting of groups and their children in ascending and descending direction --- CHANGELOG | 4 + src/core/Group.cpp | 22 +++++ src/core/Group.h | 3 + src/gui/DatabaseWidget.cpp | 10 ++ src/gui/DatabaseWidget.h | 2 + src/gui/MainWindow.cpp | 5 + src/gui/MainWindow.ui | 19 ++++ src/gui/group/GroupModel.cpp | 27 ++++++ src/gui/group/GroupModel.h | 2 + src/gui/group/GroupView.cpp | 8 ++ src/gui/group/GroupView.h | 1 + tests/TestGroup.cpp | 180 +++++++++++++++++++++++++++++++++++ tests/TestGroup.h | 1 + tests/gui/TestGui.cpp | 88 +++++++++++++++++ tests/gui/TestGui.h | 1 + 15 files changed, 373 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 4a4c921a2..4a6c9d3f1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +2.5.0-Beta1 (2019-07-05) +========================= +- Group sorting feature [#3282] + 2.4.3 (2019-06-12) ========================= diff --git a/src/core/Group.cpp b/src/core/Group.cpp index 814ac2f3b..a5d5087f0 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -521,6 +521,11 @@ QStringList Group::hierarchy() const return hierarchy; } +bool Group::hasChildren() const +{ + return !children().isEmpty(); +} + Database* Group::database() { return m_db; @@ -1074,6 +1079,23 @@ void Group::applyGroupIconTo(Entry* entry) } } +void Group::sortChildrenRecursively(bool reverse) +{ + std::sort( + m_children.begin(), m_children.end(), [reverse](const Group* childGroup1, const Group* childGroup2) -> bool { + QString name1 = childGroup1->name(); + QString name2 = childGroup2->name(); + return reverse ? name1.compare(name2, Qt::CaseInsensitive) > 0 + : name1.compare(name2, Qt::CaseInsensitive) < 0; + }); + + for (auto child : m_children) { + child->sortChildrenRecursively(reverse); + } + + emit groupModified(); +} + bool Group::GroupData::operator==(const Group::GroupData& other) const { return equals(other, CompareItemDefault); diff --git a/src/core/Group.h b/src/core/Group.h index 59e455ac0..048c08854 100644 --- a/src/core/Group.h +++ b/src/core/Group.h @@ -144,6 +144,7 @@ public: const Group* parentGroup() const; void setParent(Group* parent, int index = -1); QStringList hierarchy() const; + bool hasChildren() const; Database* database(); const Database* database() const; @@ -169,6 +170,8 @@ public: void applyGroupIconTo(Entry* entry); + void sortChildrenRecursively(bool reverse = false); + signals: void groupDataChanged(Group* group); void groupAboutToAdd(Group* group, int index); diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 59e6fcf39..824dab371 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -1073,6 +1073,16 @@ void DatabaseWidget::switchToGroupEdit() switchToGroupEdit(group, false); } +void DatabaseWidget::sortGroupsAsc() +{ + m_groupView->sortGroups(); +} + +void DatabaseWidget::sortGroupsDesc() +{ + m_groupView->sortGroups(true); +} + void DatabaseWidget::switchToMasterKeyChange() { switchToDatabaseSettings(); diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index 73baae6b7..7da6b7a40 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -175,6 +175,8 @@ public slots: void switchToMainView(bool previousDialogAccepted = false); void switchToEntryEdit(); void switchToGroupEdit(); + void sortGroupsAsc(); + void sortGroupsDesc(); void switchToMasterKeyChange(); void switchToDatabaseSettings(); void switchToOpenDatabase(); diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 5c9047608..e58070463 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -380,6 +380,8 @@ MainWindow::MainWindow() m_actionMultiplexer.connect(m_ui->actionGroupEdit, SIGNAL(triggered()), SLOT(switchToGroupEdit())); m_actionMultiplexer.connect(m_ui->actionGroupDelete, SIGNAL(triggered()), SLOT(deleteGroup())); m_actionMultiplexer.connect(m_ui->actionGroupEmptyRecycleBin, SIGNAL(triggered()), SLOT(emptyRecycleBin())); + m_actionMultiplexer.connect(m_ui->actionGroupSortAsc, SIGNAL(triggered()), SLOT(sortGroupsAsc())); + m_actionMultiplexer.connect(m_ui->actionGroupSortDesc, SIGNAL(triggered()), SLOT(sortGroupsDesc())); connect(m_ui->actionSettings, SIGNAL(toggled(bool)), SLOT(switchToSettings(bool))); connect(m_ui->actionPasswordGenerator, SIGNAL(toggled(bool)), SLOT(switchToPasswordGen(bool))); @@ -570,6 +572,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) bool singleEntrySelected = dbWidget->numberOfSelectedEntries() == 1 && hasFocus; bool entriesSelected = dbWidget->numberOfSelectedEntries() > 0 && hasFocus; bool groupSelected = dbWidget->isGroupSelected(); + bool currentGroupHasChildren = dbWidget->currentGroup()->hasChildren(); bool recycleBinSelected = dbWidget->isRecycleBinSelected(); m_ui->actionEntryNew->setEnabled(true); @@ -592,6 +595,8 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) m_ui->actionGroupNew->setEnabled(groupSelected); m_ui->actionGroupEdit->setEnabled(groupSelected); m_ui->actionGroupDelete->setEnabled(groupSelected && dbWidget->canDeleteCurrentGroup()); + m_ui->actionGroupSortAsc->setEnabled(groupSelected && currentGroupHasChildren); + m_ui->actionGroupSortDesc->setEnabled(groupSelected && currentGroupHasChildren); m_ui->actionGroupEmptyRecycleBin->setVisible(recycleBinSelected); m_ui->actionGroupEmptyRecycleBin->setEnabled(recycleBinSelected); m_ui->actionChangeMasterKey->setEnabled(true); diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index 0003c7015..ebfeba3f9 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -281,6 +281,9 @@ + + + @@ -452,6 +455,22 @@ &Delete group + + + false + + + Sort &A-Z + + + + + false + + + Sort &Z-A + + false diff --git a/src/gui/group/GroupModel.cpp b/src/gui/group/GroupModel.cpp index dae9f759a..d3f2f40f6 100644 --- a/src/gui/group/GroupModel.cpp +++ b/src/gui/group/GroupModel.cpp @@ -410,3 +410,30 @@ void GroupModel::groupMoved() { endMoveRows(); } + +void GroupModel::sortChildren(Group* rootGroup, bool reverse) +{ + emit layoutAboutToBeChanged(); + + QList oldIndexes; + collectIndexesRecursively(oldIndexes, rootGroup->children()); + + rootGroup->sortChildrenRecursively(reverse); + + QList newIndexes; + collectIndexesRecursively(newIndexes, rootGroup->children()); + + for (int i = 0; i < oldIndexes.count(); i++) { + changePersistentIndex(oldIndexes[i], newIndexes[i]); + } + + emit layoutChanged(); +} + +void GroupModel::collectIndexesRecursively(QList& indexes, QList groups) +{ + for (auto group : groups) { + indexes.append(index(group)); + collectIndexesRecursively(indexes, group->children()); + } +} diff --git a/src/gui/group/GroupModel.h b/src/gui/group/GroupModel.h index ca5370d78..dae2bb930 100644 --- a/src/gui/group/GroupModel.h +++ b/src/gui/group/GroupModel.h @@ -45,9 +45,11 @@ public: dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; QStringList mimeTypes() const override; QMimeData* mimeData(const QModelIndexList& indexes) const override; + void sortChildren(Group* rootGroup, bool reverse = false); private: QModelIndex parent(Group* group) const; + void collectIndexesRecursively(QList& indexes, QList groups); private slots: void groupDataChanged(Group* group); diff --git a/src/gui/group/GroupView.cpp b/src/gui/group/GroupView.cpp index fa7023351..77b5bff86 100644 --- a/src/gui/group/GroupView.cpp +++ b/src/gui/group/GroupView.cpp @@ -115,6 +115,14 @@ void GroupView::expandGroup(Group* group, bool expand) setExpanded(index, expand); } +void GroupView::sortGroups(bool reverse) +{ + Group* group = currentGroup(); + if (group) { + m_model->sortChildren(group, reverse); + } +} + void GroupView::setModel(QAbstractItemModel* model) { Q_UNUSED(model); diff --git a/src/gui/group/GroupView.h b/src/gui/group/GroupView.h index 609ab0e03..76425c6c3 100644 --- a/src/gui/group/GroupView.h +++ b/src/gui/group/GroupView.h @@ -35,6 +35,7 @@ public: Group* currentGroup(); void setCurrentGroup(Group* group); void expandGroup(Group* group, bool expand = true); + void sortGroups(bool reverse = false); signals: void groupSelectionChanged(Group* group); diff --git a/tests/TestGroup.cpp b/tests/TestGroup.cpp index 3e4568c35..7d42f161a 100644 --- a/tests/TestGroup.cpp +++ b/tests/TestGroup.cpp @@ -840,3 +840,183 @@ void TestGroup::testEquals() QVERIFY(group->equals(group.data(), CompareItemDefault)); } + +void TestGroup::sortChildrenRecursively() +{ + auto createTestGroupWithUnorderedChildren = []() -> Group* { + Group* parent = new Group(); + + Group* group1 = new Group(); + group1->setName("B"); + group1->setParent(parent); + Group* group2 = new Group(); + group2->setName("e"); + group2->setParent(parent); + Group* group3 = new Group(); + group3->setName("Test999"); + group3->setParent(parent); + Group* group4 = new Group(); + group4->setName("A"); + group4->setParent(parent); + Group* group5 = new Group(); + group5->setName("z"); + group5->setParent(parent); + Group* group6 = new Group(); + group6->setName("045"); + group6->setParent(parent); + Group* group7 = new Group(); + group7->setName("60"); + group7->setParent(parent); + Group* group8 = new Group(); + group8->setName("04test"); + group8->setParent(parent); + Group* group9 = new Group(); + group9->setName("Test12"); + group9->setParent(parent); + Group* group10 = new Group(); + group10->setName("i"); + group10->setParent(parent); + + Group* subGroup1 = new Group(); + subGroup1->setName("sub_xte"); + subGroup1->setParent(group10); + Group* subGroup2 = new Group(); + subGroup2->setName("sub_010"); + subGroup2->setParent(group10); + Group* subGroup3 = new Group(); + subGroup3->setName("sub_000"); + subGroup3->setParent(group10); + Group* subGroup4 = new Group(); + subGroup4->setName("sub_M"); + subGroup4->setParent(group10); + Group* subGroup5 = new Group(); + subGroup5->setName("sub_p"); + subGroup5->setParent(group10); + Group* subGroup6 = new Group(); + subGroup6->setName("sub_45p"); + subGroup6->setParent(group10); + Group* subGroup7 = new Group(); + subGroup7->setName("sub_6p"); + subGroup7->setParent(group10); + Group* subGroup8 = new Group(); + subGroup8->setName("sub_tt"); + subGroup8->setParent(group10); + Group* subGroup9 = new Group(); + subGroup9->setName("sub_t0"); + subGroup9->setParent(group10); + + return parent; + }; + + Group* parent = createTestGroupWithUnorderedChildren(); + Group* subParent = parent->children().last(); + parent->sortChildrenRecursively(); + QList children = parent->children(); + QCOMPARE(children.size(), 10); + QCOMPARE(children[0]->name(), QString("045")); + QCOMPARE(children[1]->name(), QString("04test")); + QCOMPARE(children[2]->name(), QString("60")); + QCOMPARE(children[3]->name(), QString("A")); + QCOMPARE(children[4]->name(), QString("B")); + QCOMPARE(children[5]->name(), QString("e")); + QCOMPARE(children[6]->name(), QString("i")); + QCOMPARE(children[7]->name(), QString("Test12")); + QCOMPARE(children[8]->name(), QString("Test999")); + QCOMPARE(children[9]->name(), QString("z")); + children = subParent->children(); + QCOMPARE(children.size(), 9); + QCOMPARE(children[0]->name(), QString("sub_000")); + QCOMPARE(children[1]->name(), QString("sub_010")); + QCOMPARE(children[2]->name(), QString("sub_45p")); + QCOMPARE(children[3]->name(), QString("sub_6p")); + QCOMPARE(children[4]->name(), QString("sub_M")); + QCOMPARE(children[5]->name(), QString("sub_p")); + QCOMPARE(children[6]->name(), QString("sub_t0")); + QCOMPARE(children[7]->name(), QString("sub_tt")); + QCOMPARE(children[8]->name(), QString("sub_xte")); + delete parent; + + parent = createTestGroupWithUnorderedChildren(); + subParent = parent->children().last(); + parent->sortChildrenRecursively(true); + children = parent->children(); + QCOMPARE(children.size(), 10); + QCOMPARE(children[0]->name(), QString("z")); + QCOMPARE(children[1]->name(), QString("Test999")); + QCOMPARE(children[2]->name(), QString("Test12")); + QCOMPARE(children[3]->name(), QString("i")); + QCOMPARE(children[4]->name(), QString("e")); + QCOMPARE(children[5]->name(), QString("B")); + QCOMPARE(children[6]->name(), QString("A")); + QCOMPARE(children[7]->name(), QString("60")); + QCOMPARE(children[8]->name(), QString("04test")); + QCOMPARE(children[9]->name(), QString("045")); + children = subParent->children(); + QCOMPARE(children.size(), 9); + QCOMPARE(children[0]->name(), QString("sub_xte")); + QCOMPARE(children[1]->name(), QString("sub_tt")); + QCOMPARE(children[2]->name(), QString("sub_t0")); + QCOMPARE(children[3]->name(), QString("sub_p")); + QCOMPARE(children[4]->name(), QString("sub_M")); + QCOMPARE(children[5]->name(), QString("sub_6p")); + QCOMPARE(children[6]->name(), QString("sub_45p")); + QCOMPARE(children[7]->name(), QString("sub_010")); + QCOMPARE(children[8]->name(), QString("sub_000")); + delete parent; + + parent = createTestGroupWithUnorderedChildren(); + subParent = parent->children().last(); + subParent->sortChildrenRecursively(); + children = parent->children(); + QCOMPARE(children.size(), 10); + QCOMPARE(children[0]->name(), QString("B")); + QCOMPARE(children[1]->name(), QString("e")); + QCOMPARE(children[2]->name(), QString("Test999")); + QCOMPARE(children[3]->name(), QString("A")); + QCOMPARE(children[4]->name(), QString("z")); + QCOMPARE(children[5]->name(), QString("045")); + QCOMPARE(children[6]->name(), QString("60")); + QCOMPARE(children[7]->name(), QString("04test")); + QCOMPARE(children[8]->name(), QString("Test12")); + QCOMPARE(children[9]->name(), QString("i")); + children = subParent->children(); + QCOMPARE(children.size(), 9); + QCOMPARE(children[0]->name(), QString("sub_000")); + QCOMPARE(children[1]->name(), QString("sub_010")); + QCOMPARE(children[2]->name(), QString("sub_45p")); + QCOMPARE(children[3]->name(), QString("sub_6p")); + QCOMPARE(children[4]->name(), QString("sub_M")); + QCOMPARE(children[5]->name(), QString("sub_p")); + QCOMPARE(children[6]->name(), QString("sub_t0")); + QCOMPARE(children[7]->name(), QString("sub_tt")); + QCOMPARE(children[8]->name(), QString("sub_xte")); + delete parent; + + parent = createTestGroupWithUnorderedChildren(); + subParent = parent->children().last(); + subParent->sortChildrenRecursively(true); + children = parent->children(); + QCOMPARE(children.size(), 10); + QCOMPARE(children[0]->name(), QString("B")); + QCOMPARE(children[1]->name(), QString("e")); + QCOMPARE(children[2]->name(), QString("Test999")); + QCOMPARE(children[3]->name(), QString("A")); + QCOMPARE(children[4]->name(), QString("z")); + QCOMPARE(children[5]->name(), QString("045")); + QCOMPARE(children[6]->name(), QString("60")); + QCOMPARE(children[7]->name(), QString("04test")); + QCOMPARE(children[8]->name(), QString("Test12")); + QCOMPARE(children[9]->name(), QString("i")); + children = subParent->children(); + QCOMPARE(children.size(), 9); + QCOMPARE(children[0]->name(), QString("sub_xte")); + QCOMPARE(children[1]->name(), QString("sub_tt")); + QCOMPARE(children[2]->name(), QString("sub_t0")); + QCOMPARE(children[3]->name(), QString("sub_p")); + QCOMPARE(children[4]->name(), QString("sub_M")); + QCOMPARE(children[5]->name(), QString("sub_6p")); + QCOMPARE(children[6]->name(), QString("sub_45p")); + QCOMPARE(children[7]->name(), QString("sub_010")); + QCOMPARE(children[8]->name(), QString("sub_000")); + delete parent; +} diff --git a/tests/TestGroup.h b/tests/TestGroup.h index 9fd5c2efd..9355a0c98 100644 --- a/tests/TestGroup.h +++ b/tests/TestGroup.h @@ -45,6 +45,7 @@ private slots: void testIsRecycled(); void testCopyDataFrom(); void testEquals(); + void sortChildrenRecursively(); }; #endif // KEEPASSX_TESTGROUP_H diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 0db2a5dfb..3579d90f3 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -1280,6 +1280,94 @@ void TestGui::testDragAndDropKdbxFiles() QTRY_COMPARE(m_tabWidget->count(), openedDatabasesCount); } +void TestGui::testSortGroups() +{ + auto* editGroupWidget = m_dbWidget->findChild("editGroupWidget"); + auto* nameEdit = editGroupWidget->findChild("editName"); + auto* editGroupWidgetButtonBox = editGroupWidget->findChild("buttonBox"); + + // Create some sub-groups + Group* rootGroup = m_db->rootGroup(); + Group* internetGroup = rootGroup->findGroupByPath("Internet"); + m_dbWidget->groupView()->setCurrentGroup(internetGroup); + m_dbWidget->createGroup(); + QTest::keyClicks(nameEdit, "Google"); + QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); + m_dbWidget->groupView()->setCurrentGroup(internetGroup); + m_dbWidget->createGroup(); + QTest::keyClicks(nameEdit, "eBay"); + QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); + m_dbWidget->groupView()->setCurrentGroup(internetGroup); + m_dbWidget->createGroup(); + QTest::keyClicks(nameEdit, "Amazon"); + QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); + m_dbWidget->groupView()->setCurrentGroup(internetGroup); + m_dbWidget->createGroup(); + QTest::keyClicks(nameEdit, "Facebook"); + QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); + m_dbWidget->groupView()->setCurrentGroup(rootGroup); + + triggerAction("actionGroupSortAsc"); + QList children = rootGroup->children(); + QCOMPARE(children[0]->name(), QString("eMail")); + QCOMPARE(children[1]->name(), QString("General")); + QCOMPARE(children[2]->name(), QString("Homebanking")); + QCOMPARE(children[3]->name(), QString("Internet")); + QCOMPARE(children[4]->name(), QString("Network")); + QCOMPARE(children[5]->name(), QString("Windows")); + QList subChildren = internetGroup->children(); + QCOMPARE(subChildren[0]->name(), QString("Amazon")); + QCOMPARE(subChildren[1]->name(), QString("eBay")); + QCOMPARE(subChildren[2]->name(), QString("Facebook")); + QCOMPARE(subChildren[3]->name(), QString("Google")); + + triggerAction("actionGroupSortDesc"); + children = rootGroup->children(); + QCOMPARE(children[0]->name(), QString("Windows")); + QCOMPARE(children[1]->name(), QString("Network")); + QCOMPARE(children[2]->name(), QString("Internet")); + QCOMPARE(children[3]->name(), QString("Homebanking")); + QCOMPARE(children[4]->name(), QString("General")); + QCOMPARE(children[5]->name(), QString("eMail")); + subChildren = internetGroup->children(); + QCOMPARE(subChildren[0]->name(), QString("Google")); + QCOMPARE(subChildren[1]->name(), QString("Facebook")); + QCOMPARE(subChildren[2]->name(), QString("eBay")); + QCOMPARE(subChildren[3]->name(), QString("Amazon")); + + m_dbWidget->groupView()->setCurrentGroup(internetGroup); + triggerAction("actionGroupSortAsc"); + children = rootGroup->children(); + QCOMPARE(children[0]->name(), QString("Windows")); + QCOMPARE(children[1]->name(), QString("Network")); + QCOMPARE(children[2]->name(), QString("Internet")); + QCOMPARE(children[3]->name(), QString("Homebanking")); + QCOMPARE(children[4]->name(), QString("General")); + QCOMPARE(children[5]->name(), QString("eMail")); + subChildren = internetGroup->children(); + QCOMPARE(subChildren[0]->name(), QString("Amazon")); + QCOMPARE(subChildren[1]->name(), QString("eBay")); + QCOMPARE(subChildren[2]->name(), QString("Facebook")); + QCOMPARE(subChildren[3]->name(), QString("Google")); + + m_dbWidget->groupView()->setCurrentGroup(rootGroup); + triggerAction("actionGroupSortAsc"); + m_dbWidget->groupView()->setCurrentGroup(internetGroup); + triggerAction("actionGroupSortDesc"); + children = rootGroup->children(); + QCOMPARE(children[0]->name(), QString("eMail")); + QCOMPARE(children[1]->name(), QString("General")); + QCOMPARE(children[2]->name(), QString("Homebanking")); + QCOMPARE(children[3]->name(), QString("Internet")); + QCOMPARE(children[4]->name(), QString("Network")); + QCOMPARE(children[5]->name(), QString("Windows")); + subChildren = internetGroup->children(); + QCOMPARE(subChildren[0]->name(), QString("Google")); + QCOMPARE(subChildren[1]->name(), QString("Facebook")); + QCOMPARE(subChildren[2]->name(), QString("eBay")); + QCOMPARE(subChildren[3]->name(), QString("Amazon")); +} + void TestGui::testTrayRestoreHide() { if (!QSystemTrayIcon::isSystemTrayAvailable()) { diff --git a/tests/gui/TestGui.h b/tests/gui/TestGui.h index 532600bdc..22f779936 100644 --- a/tests/gui/TestGui.h +++ b/tests/gui/TestGui.h @@ -69,6 +69,7 @@ private slots: void testKeePass1Import(); void testDatabaseLocking(); void testDragAndDropKdbxFiles(); + void testSortGroups(); void testTrayRestoreHide(); private: