Add group sorting feature

* Enabling sorting of groups and their children in ascending and descending direction
This commit is contained in:
Balazs Gyurak 2019-06-18 21:58:47 +01:00 committed by Jonathan White
parent 0c2d1bcc50
commit 09181fab13
15 changed files with 373 additions and 0 deletions

View File

@ -1,3 +1,7 @@
2.5.0-Beta1 (2019-07-05)
=========================
- Group sorting feature [#3282]
2.4.3 (2019-06-12)
=========================

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -281,6 +281,9 @@
<addaction name="actionGroupEdit"/>
<addaction name="actionGroupDelete"/>
<addaction name="actionGroupEmptyRecycleBin"/>
<addaction name="separator"/>
<addaction name="actionGroupSortAsc"/>
<addaction name="actionGroupSortDesc"/>
</widget>
<widget class="QMenu" name="menuTools">
<property name="title">
@ -452,6 +455,22 @@
<string>&amp;Delete group</string>
</property>
</action>
<action name="actionGroupSortAsc">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Sort &amp;A-Z</string>
</property>
</action>
<action name="actionGroupSortDesc">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Sort &amp;Z-A</string>
</property>
</action>
<action name="actionDatabaseSaveAs">
<property name="enabled">
<bool>false</bool>

View File

@ -410,3 +410,30 @@ void GroupModel::groupMoved()
{
endMoveRows();
}
void GroupModel::sortChildren(Group* rootGroup, bool reverse)
{
emit layoutAboutToBeChanged();
QList<QModelIndex> oldIndexes;
collectIndexesRecursively(oldIndexes, rootGroup->children());
rootGroup->sortChildrenRecursively(reverse);
QList<QModelIndex> newIndexes;
collectIndexesRecursively(newIndexes, rootGroup->children());
for (int i = 0; i < oldIndexes.count(); i++) {
changePersistentIndex(oldIndexes[i], newIndexes[i]);
}
emit layoutChanged();
}
void GroupModel::collectIndexesRecursively(QList<QModelIndex>& indexes, QList<Group*> groups)
{
for (auto group : groups) {
indexes.append(index(group));
collectIndexesRecursively(indexes, group->children());
}
}

View File

@ -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<QModelIndex>& indexes, QList<Group*> groups);
private slots:
void groupDataChanged(Group* group);

View File

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

View File

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

View File

@ -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<Group*> 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;
}

View File

@ -45,6 +45,7 @@ private slots:
void testIsRecycled();
void testCopyDataFrom();
void testEquals();
void sortChildrenRecursively();
};
#endif // KEEPASSX_TESTGROUP_H

View File

@ -1280,6 +1280,94 @@ void TestGui::testDragAndDropKdbxFiles()
QTRY_COMPARE(m_tabWidget->count(), openedDatabasesCount);
}
void TestGui::testSortGroups()
{
auto* editGroupWidget = m_dbWidget->findChild<EditGroupWidget*>("editGroupWidget");
auto* nameEdit = editGroupWidget->findChild<QLineEdit*>("editName");
auto* editGroupWidgetButtonBox = editGroupWidget->findChild<QDialogButtonBox*>("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<Group*> 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<Group*> 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()) {

View File

@ -69,6 +69,7 @@ private slots:
void testKeePass1Import();
void testDatabaseLocking();
void testDragAndDropKdbxFiles();
void testSortGroups();
void testTrayRestoreHide();
private: