From eb198271acd79310b59bdb415167d72971309760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Holger=20B=C3=B6hnke?= Date: Thu, 21 May 2020 21:43:00 -0400 Subject: [PATCH] Add natural sort of entry list Introduce a third unsorted status that shows entries in the order they occur in the KDBX file. * Add keyboard shortcut Ctrl+Alt+Up/Down to move entries up and down in sort order * Add entry context menu icons to achieve movement up/down * Only show menu icons when in natural sort order * Add Material Design icons for moving up/down * Add feature to track non-data changes and force a save on exit to ensure they are not lost when locking a database. This allows users to make entry movements and group expand/collapse operations and not lose that state. Remove saveas --- COPYING | 2 + .../scalable/actions/move-down.svg | 1 + .../application/scalable/actions/move-up.svg | 1 + share/icons/icons.qrc | 2 + src/core/Database.cpp | 10 +- src/core/Database.h | 4 +- src/core/Entry.cpp | 14 +++ src/core/Entry.h | 3 + src/core/Group.cpp | 44 ++++++- src/core/Group.h | 7 ++ src/gui/DatabaseWidget.cpp | 32 ++++- src/gui/DatabaseWidget.h | 4 + src/gui/MainWindow.cpp | 19 +++ src/gui/MainWindow.ui | 25 ++++ src/gui/entry/EntryModel.cpp | 37 +++++- src/gui/entry/EntryModel.h | 4 + src/gui/entry/EntryView.cpp | 45 ++++++- src/gui/entry/EntryView.h | 5 + tests/TestEntry.cpp | 111 ++++++++++++++++++ tests/TestEntry.h | 1 + tests/TestEntryModel.cpp | 26 ++++ tests/TestGroup.cpp | 111 ++++++++++++++++++ tests/TestGroup.h | 1 + utils/makeicons.sh | 2 + 24 files changed, 500 insertions(+), 11 deletions(-) create mode 100644 share/icons/application/scalable/actions/move-down.svg create mode 100644 share/icons/application/scalable/actions/move-up.svg diff --git a/COPYING b/COPYING index 69195ae43..fbca3c12c 100644 --- a/COPYING +++ b/COPYING @@ -155,6 +155,8 @@ Files: share/icons/application/scalable/actions/application-exit.svg share/icons/application/scalable/actions/help-about.svg share/icons/application/scalable/actions/key-enter.svg share/icons/application/scalable/actions/message-close.svg + share/icons/application/scalable/actions/move-down.svg + share/icons/application/scalable/actions/move-up.svg share/icons/application/scalable/actions/paperclip.svg share/icons/application/scalable/actions/password-copy.svg share/icons/application/scalable/actions/password-generate.svg diff --git a/share/icons/application/scalable/actions/move-down.svg b/share/icons/application/scalable/actions/move-down.svg new file mode 100644 index 000000000..edcf11814 --- /dev/null +++ b/share/icons/application/scalable/actions/move-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/application/scalable/actions/move-up.svg b/share/icons/application/scalable/actions/move-up.svg new file mode 100644 index 000000000..7ba0a9f13 --- /dev/null +++ b/share/icons/application/scalable/actions/move-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/icons.qrc b/share/icons/icons.qrc index f4f551e14..4f01feca4 100644 --- a/share/icons/icons.qrc +++ b/share/icons/icons.qrc @@ -43,6 +43,8 @@ application/scalable/actions/key-enter.svg application/scalable/actions/keyboard-shortcuts.svg application/scalable/actions/message-close.svg + application/scalable/actions/move-down.svg + application/scalable/actions/move-up.svg application/scalable/actions/object-locked.svg application/scalable/actions/object-unlocked.svg application/scalable/actions/paperclip.svg diff --git a/src/core/Database.cpp b/src/core/Database.cpp index 49f83aa1a..e567b9dfa 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -836,9 +836,9 @@ void Database::setEmitModified(bool value) m_emitModified = value; } -bool Database::isModified() const +bool Database::isModified(bool includeNonDataChanges) const { - return m_modified; + return m_modified || (includeNonDataChanges && m_hasNonDataChange); } void Database::markAsModified() @@ -855,11 +855,17 @@ void Database::markAsClean() bool emitSignal = m_modified; m_modified = false; m_modifiedTimer.stop(); + m_hasNonDataChange = false; if (emitSignal) { emit databaseSaved(); } } +void Database::markNonDataChange() +{ + m_hasNonDataChange = true; +} + /** * @param uuid UUID of the database * @return pointer to the database or nullptr if no such database exists diff --git a/src/core/Database.h b/src/core/Database.h index 8feb7e56b..c5b7e965d 100644 --- a/src/core/Database.h +++ b/src/core/Database.h @@ -81,7 +81,7 @@ public: void releaseData(); bool isInitialized() const; - bool isModified() const; + bool isModified(bool includeNonDataChanges = false) const; void setEmitModified(bool value); bool isReadOnly() const; void setReadOnly(bool readOnly); @@ -138,6 +138,7 @@ public slots: void markAsModified(); void markAsClean(); void updateCommonUsernames(int topN = 10); + void markNonDataChange(); signals: void filePathChanged(const QString& oldPath, const QString& newPath); @@ -210,6 +211,7 @@ private: QPointer m_fileWatcher; bool m_modified = false; bool m_emitModified; + bool m_hasNonDataChange = false; QString m_keyError; QList m_commonUsernames; diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index 364407119..a5e372d07 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -1066,6 +1066,20 @@ QString Entry::referenceFieldValue(EntryReferenceType referenceType) const return QString(); } +void Entry::moveUp() +{ + if (m_group) { + m_group->moveEntryUp(this); + } +} + +void Entry::moveDown() +{ + if (m_group) { + m_group->moveEntryDown(this); + } +} + Group* Entry::group() { return m_group; diff --git a/src/core/Entry.h b/src/core/Entry.h index 671f840e5..3d42692c8 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -233,6 +233,9 @@ public: void beginUpdate(); bool endUpdate(); + void moveUp(); + void moveDown(); + Group* group(); const Group* group() const; void setGroup(Group* group); diff --git a/src/core/Group.cpp b/src/core/Group.cpp index ce29a5cc4..7ce795f14 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -48,6 +48,7 @@ Group::Group() connect(m_customData, SIGNAL(customDataModified()), this, SIGNAL(groupModified())); connect(this, SIGNAL(groupModified()), SLOT(updateTimeinfo())); + connect(this, SIGNAL(groupNonDataChange()), SLOT(updateTimeinfo())); } Group::~Group() @@ -364,11 +365,11 @@ void Group::setExpanded(bool expanded) { if (m_data.isExpanded != expanded) { m_data.isExpanded = expanded; - if (!config()->get(Config::TrackNonDataChanges).toBool()) { - updateTimeinfo(); - return; + if (config()->get(Config::TrackNonDataChanges).toBool()) { + emit groupModified(); + } else { + emit groupNonDataChange(); } - emit groupModified(); } } @@ -964,6 +965,40 @@ void Group::removeEntry(Entry* entry) emit entryRemoved(entry); } +void Group::moveEntryUp(Entry* entry) +{ + int row = m_entries.indexOf(entry); + if (row <= 0) { + return; + } + + emit entryAboutToMoveUp(row); + m_entries.move(row, row - 1); + emit entryMovedUp(); + if (config()->get(Config::TrackNonDataChanges).toBool()) { + emit groupModified(); + } else { + emit groupNonDataChange(); + } +} + +void Group::moveEntryDown(Entry* entry) +{ + int row = m_entries.indexOf(entry); + if (row >= m_entries.size() - 1) { + return; + } + + emit entryAboutToMoveDown(row); + m_entries.move(row, row + 1); + emit entryMovedDown(); + if (config()->get(Config::TrackNonDataChanges).toBool()) { + emit groupModified(); + } else { + emit groupNonDataChange(); + } +} + void Group::connectDatabaseSignalsRecursive(Database* db) { if (m_db) { @@ -989,6 +1024,7 @@ void Group::connectDatabaseSignalsRecursive(Database* db) connect(this, SIGNAL(aboutToMove(Group*,Group*,int)), db, SIGNAL(groupAboutToMove(Group*,Group*,int))); connect(this, SIGNAL(groupMoved()), db, SIGNAL(groupMoved())); connect(this, SIGNAL(groupModified()), db, SLOT(markAsModified())); + connect(this, SIGNAL(groupNonDataChange()), db, SLOT(markNonDataChange())); // clang-format on } diff --git a/src/core/Group.h b/src/core/Group.h index cfeb9feee..7adabc8b5 100644 --- a/src/core/Group.h +++ b/src/core/Group.h @@ -167,6 +167,8 @@ public: void addEntry(Entry* entry); void removeEntry(Entry* entry); + void moveEntryUp(Entry* entry); + void moveEntryDown(Entry* entry); void applyGroupIconOnCreateTo(Entry* entry); void applyGroupIconTo(Entry* entry); @@ -185,10 +187,15 @@ signals: void aboutToMove(Group* group, Group* toGroup, int index); void groupMoved(); void groupModified(); + void groupNonDataChange(); void entryAboutToAdd(Entry* entry); void entryAdded(Entry* entry); void entryAboutToRemove(Entry* entry); void entryRemoved(Entry* entry); + void entryAboutToMoveUp(int row); + void entryMovedUp(); + void entryAboutToMoveDown(int row); + void entryMovedDown(); void entryDataChanged(Entry* entry); private slots: diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 88aaf9565..7664c64b3 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -279,6 +279,11 @@ bool DatabaseWidget::isSaving() const return m_db->isSaving(); } +bool DatabaseWidget::isSorted() const +{ + return m_entryView->isSorted(); +} + bool DatabaseWidget::isSearchActive() const { return m_entryView->inSearchMode(); @@ -645,6 +650,24 @@ void DatabaseWidget::focusOnGroups() } } +void DatabaseWidget::moveEntryUp() +{ + auto currentEntry = currentSelectedEntry(); + if (currentEntry) { + currentEntry->moveUp(); + m_entryView->setCurrentEntry(currentEntry); + } +} + +void DatabaseWidget::moveEntryDown() +{ + auto currentEntry = currentSelectedEntry(); + if (currentEntry) { + currentEntry->moveDown(); + m_entryView->setCurrentEntry(currentEntry); + } +} + void DatabaseWidget::copyTitle() { auto currentEntry = currentSelectedEntry(); @@ -1510,7 +1533,7 @@ bool DatabaseWidget::lock() } } - if (m_db->isModified()) { + if (m_db->isModified(true)) { bool saved = false; // Attempt to save on exit, but don't block locking if it fails if (config()->get(Config::AutoSaveOnExit).toBool() @@ -1594,7 +1617,7 @@ void DatabaseWidget::reloadDatabaseFile() QString error; auto db = QSharedPointer::create(m_db->filePath()); if (db->open(database()->key(), &error)) { - if (m_db->isModified()) { + if (m_db->isModified(true)) { // Ask if we want to merge changes into new database auto result = MessageBox::question( this, @@ -1641,6 +1664,11 @@ int DatabaseWidget::numberOfSelectedEntries() const return m_entryView->numberOfSelectedEntries(); } +int DatabaseWidget::currentEntryIndex() const +{ + return m_entryView->currentEntryIndex(); +} + QStringList DatabaseWidget::customEntryAttributes() const { Entry* entry = m_entryView->currentEntry(); diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index 3f25a4361..b78e00e7c 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -83,6 +83,7 @@ public: DatabaseWidget::Mode currentMode() const; bool isLocked() const; bool isSaving() const; + bool isSorted() const; bool isSearchActive() const; bool isEntryViewActive() const; bool isEntryEditActive() const; @@ -99,6 +100,7 @@ public: bool isGroupSelected() const; bool isRecycleBinSelected() const; int numberOfSelectedEntries() const; + int currentEntryIndex() const; QStringList customEntryAttributes() const; bool isEditWidgetModified() const; @@ -167,6 +169,8 @@ public slots: void deleteEntries(QList entries); void focusOnEntries(); void focusOnGroups(); + void moveEntryUp(); + void moveEntryDown(); void copyTitle(); void copyUsername(); void copyPassword(); diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index c52298377..69ef33a80 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -128,6 +128,9 @@ MainWindow::MainWindow() m_entryContextMenu->addAction(m_ui->actionEntryDelete); m_entryContextMenu->addAction(m_ui->actionEntryNew); m_entryContextMenu->addSeparator(); + m_entryContextMenu->addAction(m_ui->actionEntryMoveUp); + m_entryContextMenu->addAction(m_ui->actionEntryMoveDown); + m_entryContextMenu->addSeparator(); m_entryContextMenu->addAction(m_ui->actionEntryOpenUrl); m_entryContextMenu->addAction(m_ui->actionEntryDownloadIcon); @@ -236,6 +239,8 @@ MainWindow::MainWindow() m_ui->actionEntryTotp->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_T); m_ui->actionEntryDownloadIcon->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_D); m_ui->actionEntryCopyTotp->setShortcut(Qt::CTRL + Qt::Key_T); + m_ui->actionEntryMoveUp->setShortcut(Qt::CTRL + Qt::ALT + Qt::Key_Up); + m_ui->actionEntryMoveDown->setShortcut(Qt::CTRL + Qt::ALT + Qt::Key_Down); m_ui->actionEntryCopyUsername->setShortcut(Qt::CTRL + Qt::Key_B); m_ui->actionEntryCopyPassword->setShortcut(Qt::CTRL + Qt::Key_C); m_ui->actionEntryAutoType->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_V); @@ -254,6 +259,8 @@ MainWindow::MainWindow() m_ui->actionEntryTotp->setShortcutVisibleInContextMenu(true); m_ui->actionEntryDownloadIcon->setShortcutVisibleInContextMenu(true); m_ui->actionEntryCopyTotp->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryMoveUp->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryMoveDown->setShortcutVisibleInContextMenu(true); m_ui->actionEntryCopyUsername->setShortcutVisibleInContextMenu(true); m_ui->actionEntryCopyPassword->setShortcutVisibleInContextMenu(true); m_ui->actionEntryAutoType->setShortcutVisibleInContextMenu(true); @@ -336,6 +343,8 @@ MainWindow::MainWindow() m_ui->actionEntryEdit->setIcon(resources()->icon("entry-edit")); m_ui->actionEntryDelete->setIcon(resources()->icon("entry-delete")); m_ui->actionEntryAutoType->setIcon(resources()->icon("auto-type")); + m_ui->actionEntryMoveUp->setIcon(resources()->icon("move-up")); + m_ui->actionEntryMoveDown->setIcon(resources()->icon("move-down")); m_ui->actionEntryCopyUsername->setIcon(resources()->icon("username-copy")); m_ui->actionEntryCopyPassword->setIcon(resources()->icon("password-copy")); m_ui->actionEntryCopyURL->setIcon(resources()->icon("url-copy")); @@ -420,6 +429,8 @@ MainWindow::MainWindow() m_actionMultiplexer.connect(m_ui->actionEntryCopyTotp, SIGNAL(triggered()), SLOT(copyTotp())); m_actionMultiplexer.connect(m_ui->actionEntryTotpQRCode, SIGNAL(triggered()), SLOT(showTotpKeyQrCode())); m_actionMultiplexer.connect(m_ui->actionEntryCopyTitle, SIGNAL(triggered()), SLOT(copyTitle())); + m_actionMultiplexer.connect(m_ui->actionEntryMoveUp, SIGNAL(triggered()), SLOT(moveEntryUp())); + m_actionMultiplexer.connect(m_ui->actionEntryMoveDown, SIGNAL(triggered()), SLOT(moveEntryDown())); m_actionMultiplexer.connect(m_ui->actionEntryCopyUsername, SIGNAL(triggered()), SLOT(copyUsername())); m_actionMultiplexer.connect(m_ui->actionEntryCopyPassword, SIGNAL(triggered()), SLOT(copyPassword())); m_actionMultiplexer.connect(m_ui->actionEntryCopyURL, SIGNAL(triggered()), SLOT(copyURL())); @@ -662,11 +673,19 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) bool currentGroupHasChildren = dbWidget->currentGroup()->hasChildren(); bool currentGroupHasEntries = !dbWidget->currentGroup()->entries().isEmpty(); bool recycleBinSelected = dbWidget->isRecycleBinSelected(); + bool sorted = dbWidget->isSorted(); + int entryIndex = dbWidget->currentEntryIndex(); + int numEntries = dbWidget->currentGroup()->entries().size(); m_ui->actionEntryNew->setEnabled(true); m_ui->actionEntryClone->setEnabled(singleEntrySelected); m_ui->actionEntryEdit->setEnabled(singleEntrySelected); m_ui->actionEntryDelete->setEnabled(entriesSelected); + m_ui->actionEntryMoveUp->setVisible(!sorted); + m_ui->actionEntryMoveDown->setVisible(!sorted); + m_ui->actionEntryMoveUp->setEnabled(singleEntrySelected && !sorted && entryIndex > 0); + m_ui->actionEntryMoveDown->setEnabled(singleEntrySelected && !sorted && entryIndex >= 0 + && entryIndex < numEntries - 1); m_ui->actionEntryCopyTitle->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTitle()); m_ui->actionEntryCopyUsername->setEnabled(singleEntrySelected && dbWidget->currentEntryHasUsername()); // NOTE: Copy password is enabled even if the selected entry's password is blank to prevent Ctrl+C diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index aa4dd7dd1..10e29ff28 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -309,6 +309,9 @@ + + + @@ -578,6 +581,28 @@ &Clone Entry… + + + false + + + Move u&p + + + Move entry one step up + + + + + false + + + Move do&wn + + + Move entry one step down + + false diff --git a/src/gui/entry/EntryModel.cpp b/src/gui/entry/EntryModel.cpp index 3dfdf20bc..e6311f6b5 100644 --- a/src/gui/entry/EntryModel.cpp +++ b/src/gui/entry/EntryModel.cpp @@ -497,10 +497,41 @@ void EntryModel::entryRemoved() if (m_group) { m_entries = m_group->entries(); } - endRemoveRows(); } +void EntryModel::entryAboutToMoveUp(int row) +{ + beginMoveRows(QModelIndex(), row, row, QModelIndex(), row - 1); + if (m_group) { + m_entries.move(row, row - 1); + } +} + +void EntryModel::entryMovedUp() +{ + if (m_group) { + m_entries = m_group->entries(); + } + endMoveRows(); +} + +void EntryModel::entryAboutToMoveDown(int row) +{ + beginMoveRows(QModelIndex(), row, row, QModelIndex(), row + 2); + if (m_group) { + m_entries.move(row, row + 1); + } +} + +void EntryModel::entryMovedDown() +{ + if (m_group) { + m_entries = m_group->entries(); + } + endMoveRows(); +} + void EntryModel::entryDataChanged(Entry* entry) { int row = m_entries.indexOf(entry); @@ -524,6 +555,10 @@ void EntryModel::makeConnections(const Group* group) connect(group, SIGNAL(entryAdded(Entry*)), SLOT(entryAdded(Entry*))); connect(group, SIGNAL(entryAboutToRemove(Entry*)), SLOT(entryAboutToRemove(Entry*))); connect(group, SIGNAL(entryRemoved(Entry*)), SLOT(entryRemoved())); + connect(group, SIGNAL(entryAboutToMoveUp(int)), SLOT(entryAboutToMoveUp(int))); + connect(group, SIGNAL(entryMovedUp()), SLOT(entryMovedUp())); + connect(group, SIGNAL(entryAboutToMoveDown(int)), SLOT(entryAboutToMoveDown(int))); + connect(group, SIGNAL(entryMovedDown()), SLOT(entryMovedDown())); connect(group, SIGNAL(entryDataChanged(Entry*)), SLOT(entryDataChanged(Entry*))); } diff --git a/src/gui/entry/EntryModel.h b/src/gui/entry/EntryModel.h index 055455e57..78da7194c 100644 --- a/src/gui/entry/EntryModel.h +++ b/src/gui/entry/EntryModel.h @@ -78,6 +78,10 @@ private slots: void entryAdded(Entry* entry); void entryAboutToRemove(Entry* entry); void entryRemoved(); + void entryAboutToMoveUp(int row); + void entryMovedUp(); + void entryAboutToMoveDown(int row); + void entryMovedDown(); void entryDataChanged(Entry* entry); private: diff --git a/src/gui/entry/EntryView.cpp b/src/gui/entry/EntryView.cpp index fd317cdd3..18a69687d 100644 --- a/src/gui/entry/EntryView.cpp +++ b/src/gui/entry/EntryView.cpp @@ -30,6 +30,8 @@ EntryView::EntryView(QWidget* parent) : QTreeView(parent) , m_model(new EntryModel(this)) , m_sortModel(new SortFilterHideProxyModel(this)) + , m_lastIndex(-1) + , m_lastOrder(Qt::AscendingOrder) , m_inSearchMode(false) { m_sortModel->setSourceModel(m_model); @@ -120,7 +122,7 @@ EntryView::EntryView(QWidget* parent) // clang-format on // clang-format off - connect(header(), SIGNAL(sortIndicatorChanged(int,Qt::SortOrder)), SIGNAL(viewStateChanged())); + connect(header(), SIGNAL(sortIndicatorChanged(int,Qt::SortOrder)), SLOT(sortIndicatorChanged(int,Qt::SortOrder))); // clang-format on } @@ -132,6 +134,31 @@ void EntryView::contextMenuShortcutPressed() } } +void EntryView::sortIndicatorChanged(int logicalIndex, Qt::SortOrder order) +{ + int oldIndex = m_lastIndex; + m_lastIndex = logicalIndex; + Qt::SortOrder oldOrder = m_lastOrder; + m_lastOrder = order; + + if (oldIndex == logicalIndex // same index + && oldOrder == Qt::DescendingOrder // old order is descending + && order == Qt::AscendingOrder) // new order is ascending + { + // a change from descending to ascending on the same column occurred + // this sets the header into no sort order + header()->setSortIndicator(-1, Qt::AscendingOrder); + // do not emit any signals, header()->setSortIndicator recursively calls this + // function and the signals are emitted in the else part + } else { + // call emitEntrySelectionChanged even though the selection did not really change + // this triggers the evaluation of the menu activation and anyway, the position + // of the selected entry within the widget did change + emitEntrySelectionChanged(); + emit viewStateChanged(); + } +} + void EntryView::keyPressEvent(QKeyEvent* event) { if ((event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return) && currentIndex().isValid()) { @@ -211,6 +238,11 @@ bool EntryView::inSearchMode() return m_inSearchMode; } +bool EntryView::isSorted() +{ + return header()->sortIndicatorSection() != -1; +} + void EntryView::emitEntryActivated(const QModelIndex& index) { Entry* entry = entryFromIndex(index); @@ -258,6 +290,17 @@ Entry* EntryView::entryFromIndex(const QModelIndex& index) } } +int EntryView::currentEntryIndex() +{ + QModelIndexList list = selectionModel()->selectedRows(); + if (list.size() == 1) { + auto index = m_sortModel->mapToSource(list.first()); + return index.row(); + } else { + return -1; + } +} + /** * Get current state of 'Hide Usernames' setting (NOTE: just pass-through for * m_model) diff --git a/src/gui/entry/EntryView.h b/src/gui/entry/EntryView.h index f3786ed37..e32aa4729 100644 --- a/src/gui/entry/EntryView.h +++ b/src/gui/entry/EntryView.h @@ -39,7 +39,9 @@ public: Entry* currentEntry(); void setCurrentEntry(Entry* entry); Entry* entryFromIndex(const QModelIndex& index); + int currentEntryIndex(); bool inSearchMode(); + bool isSorted(); int numberOfSelectedEntries(); void setFirstEntryActive(); bool isUsernamesHidden() const; @@ -74,12 +76,15 @@ private slots: void fitColumnsToContents(); void resetViewToDefaults(); void contextMenuShortcutPressed(); + void sortIndicatorChanged(int logicalIndex, Qt::SortOrder order); private: void resetFixedColumns(); EntryModel* const m_model; SortFilterHideProxyModel* const m_sortModel; + int m_lastIndex; + Qt::SortOrder m_lastOrder; bool m_inSearchMode; bool m_columnsNeedRelayout = true; diff --git a/tests/TestEntry.cpp b/tests/TestEntry.cpp index 39e4bd12c..c6cb1271d 100644 --- a/tests/TestEntry.cpp +++ b/tests/TestEntry.cpp @@ -612,3 +612,114 @@ void TestEntry::testIsRecycled() db.recycleGroup(group1); QVERIFY(entry1->isRecycled()); } + +void TestEntry::testMove() +{ + Database db; + Group* root = db.rootGroup(); + QVERIFY(root); + + Entry* entry0 = new Entry(); + QVERIFY(entry0); + entry0->setGroup(root); + Entry* entry1 = new Entry(); + QVERIFY(entry1); + entry1->setGroup(root); + Entry* entry2 = new Entry(); + QVERIFY(entry2); + entry2->setGroup(root); + Entry* entry3 = new Entry(); + QVERIFY(entry3); + entry3->setGroup(root); + // default order, straight + QCOMPARE(root->entries().at(0), entry0); + QCOMPARE(root->entries().at(1), entry1); + QCOMPARE(root->entries().at(2), entry2); + QCOMPARE(root->entries().at(3), entry3); + + entry0->moveDown(); + QCOMPARE(root->entries().at(0), entry1); + QCOMPARE(root->entries().at(1), entry0); + QCOMPARE(root->entries().at(2), entry2); + QCOMPARE(root->entries().at(3), entry3); + + entry0->moveDown(); + QCOMPARE(root->entries().at(0), entry1); + QCOMPARE(root->entries().at(1), entry2); + QCOMPARE(root->entries().at(2), entry0); + QCOMPARE(root->entries().at(3), entry3); + + entry0->moveDown(); + QCOMPARE(root->entries().at(0), entry1); + QCOMPARE(root->entries().at(1), entry2); + QCOMPARE(root->entries().at(2), entry3); + QCOMPARE(root->entries().at(3), entry0); + + // no effect + entry0->moveDown(); + QCOMPARE(root->entries().at(0), entry1); + QCOMPARE(root->entries().at(1), entry2); + QCOMPARE(root->entries().at(2), entry3); + QCOMPARE(root->entries().at(3), entry0); + + entry0->moveUp(); + QCOMPARE(root->entries().at(0), entry1); + QCOMPARE(root->entries().at(1), entry2); + QCOMPARE(root->entries().at(2), entry0); + QCOMPARE(root->entries().at(3), entry3); + + entry0->moveUp(); + QCOMPARE(root->entries().at(0), entry1); + QCOMPARE(root->entries().at(1), entry0); + QCOMPARE(root->entries().at(2), entry2); + QCOMPARE(root->entries().at(3), entry3); + + entry0->moveUp(); + QCOMPARE(root->entries().at(0), entry0); + QCOMPARE(root->entries().at(1), entry1); + QCOMPARE(root->entries().at(2), entry2); + QCOMPARE(root->entries().at(3), entry3); + + // no effect + entry0->moveUp(); + QCOMPARE(root->entries().at(0), entry0); + QCOMPARE(root->entries().at(1), entry1); + QCOMPARE(root->entries().at(2), entry2); + QCOMPARE(root->entries().at(3), entry3); + + entry2->moveUp(); + QCOMPARE(root->entries().at(0), entry0); + QCOMPARE(root->entries().at(1), entry2); + QCOMPARE(root->entries().at(2), entry1); + QCOMPARE(root->entries().at(3), entry3); + + entry0->moveDown(); + QCOMPARE(root->entries().at(0), entry2); + QCOMPARE(root->entries().at(1), entry0); + QCOMPARE(root->entries().at(2), entry1); + QCOMPARE(root->entries().at(3), entry3); + + entry3->moveUp(); + QCOMPARE(root->entries().at(0), entry2); + QCOMPARE(root->entries().at(1), entry0); + QCOMPARE(root->entries().at(2), entry3); + QCOMPARE(root->entries().at(3), entry1); + + entry3->moveUp(); + QCOMPARE(root->entries().at(0), entry2); + QCOMPARE(root->entries().at(1), entry3); + QCOMPARE(root->entries().at(2), entry0); + QCOMPARE(root->entries().at(3), entry1); + + entry2->moveDown(); + QCOMPARE(root->entries().at(0), entry3); + QCOMPARE(root->entries().at(1), entry2); + QCOMPARE(root->entries().at(2), entry0); + QCOMPARE(root->entries().at(3), entry1); + + entry1->moveUp(); + QCOMPARE(root->entries().at(0), entry3); + QCOMPARE(root->entries().at(1), entry2); + QCOMPARE(root->entries().at(2), entry1); + QCOMPARE(root->entries().at(3), entry0); +} diff --git a/tests/TestEntry.h b/tests/TestEntry.h index ff0cfe07f..9fc7e158b 100644 --- a/tests/TestEntry.h +++ b/tests/TestEntry.h @@ -38,6 +38,7 @@ private slots: void testResolveNonIdPlaceholdersToUuid(); void testResolveClonedEntry(); void testIsRecycled(); + void testMove(); }; #endif // KEEPASSX_TESTENTRY_H diff --git a/tests/TestEntryModel.cpp b/tests/TestEntryModel.cpp index 6c4b97454..26cb0dfec 100644 --- a/tests/TestEntryModel.cpp +++ b/tests/TestEntryModel.cpp @@ -55,6 +55,9 @@ void TestEntryModel::test() EntryModel* model = new EntryModel(this); + QSignalSpy spyAboutToBeMoved(model, SIGNAL(rowsAboutToBeMoved(QModelIndex, int, int, QModelIndex, int))); + QSignalSpy spyMoved(model, SIGNAL(rowsMoved(QModelIndex, int, int, QModelIndex, int))); + ModelTest* modelTest = new ModelTest(model, this); model->setGroup(group1); @@ -79,6 +82,29 @@ void TestEntryModel::test() Entry* entry3 = new Entry(); entry3->setGroup(group1); + QCOMPARE(spyAboutToBeMoved.count(), 0); + QCOMPARE(spyMoved.count(), 0); + + entry1->moveDown(); + QCOMPARE(spyAboutToBeMoved.count(), 1); + QCOMPARE(spyMoved.count(), 1); + + entry1->moveDown(); + QCOMPARE(spyAboutToBeMoved.count(), 2); + QCOMPARE(spyMoved.count(), 2); + + entry1->moveDown(); + QCOMPARE(spyAboutToBeMoved.count(), 2); + QCOMPARE(spyMoved.count(), 2); + + entry3->moveUp(); + QCOMPARE(spyAboutToBeMoved.count(), 3); + QCOMPARE(spyMoved.count(), 3); + + entry3->moveUp(); + QCOMPARE(spyAboutToBeMoved.count(), 3); + QCOMPARE(spyMoved.count(), 3); + QCOMPARE(spyAboutToAdd.count(), 1); QCOMPARE(spyAdded.count(), 1); QCOMPARE(spyAboutToRemove.count(), 0); diff --git a/tests/TestGroup.cpp b/tests/TestGroup.cpp index 47a917e43..a9acb3dcc 100644 --- a/tests/TestGroup.cpp +++ b/tests/TestGroup.cpp @@ -1208,3 +1208,114 @@ void TestGroup::testUsernamesRecursive() QVERIFY(usernames.contains("Name2")); QVERIFY(usernames.indexOf("Name2") < usernames.indexOf("Name1")); } + +void TestGroup::testMove() +{ + Database database; + Group* root = database.rootGroup(); + QVERIFY(root); + + Entry* entry0 = new Entry(); + QVERIFY(entry0); + entry0->setGroup(root); + Entry* entry1 = new Entry(); + QVERIFY(entry1); + entry1->setGroup(root); + Entry* entry2 = new Entry(); + QVERIFY(entry2); + entry2->setGroup(root); + Entry* entry3 = new Entry(); + QVERIFY(entry3); + entry3->setGroup(root); + // default order, straight + QCOMPARE(root->entries().at(0), entry0); + QCOMPARE(root->entries().at(1), entry1); + QCOMPARE(root->entries().at(2), entry2); + QCOMPARE(root->entries().at(3), entry3); + + root->moveEntryDown(entry0); + QCOMPARE(root->entries().at(0), entry1); + QCOMPARE(root->entries().at(1), entry0); + QCOMPARE(root->entries().at(2), entry2); + QCOMPARE(root->entries().at(3), entry3); + + root->moveEntryDown(entry0); + QCOMPARE(root->entries().at(0), entry1); + QCOMPARE(root->entries().at(1), entry2); + QCOMPARE(root->entries().at(2), entry0); + QCOMPARE(root->entries().at(3), entry3); + + root->moveEntryDown(entry0); + QCOMPARE(root->entries().at(0), entry1); + QCOMPARE(root->entries().at(1), entry2); + QCOMPARE(root->entries().at(2), entry3); + QCOMPARE(root->entries().at(3), entry0); + + // no effect + root->moveEntryDown(entry0); + QCOMPARE(root->entries().at(0), entry1); + QCOMPARE(root->entries().at(1), entry2); + QCOMPARE(root->entries().at(2), entry3); + QCOMPARE(root->entries().at(3), entry0); + + root->moveEntryUp(entry0); + QCOMPARE(root->entries().at(0), entry1); + QCOMPARE(root->entries().at(1), entry2); + QCOMPARE(root->entries().at(2), entry0); + QCOMPARE(root->entries().at(3), entry3); + + root->moveEntryUp(entry0); + QCOMPARE(root->entries().at(0), entry1); + QCOMPARE(root->entries().at(1), entry0); + QCOMPARE(root->entries().at(2), entry2); + QCOMPARE(root->entries().at(3), entry3); + + root->moveEntryUp(entry0); + QCOMPARE(root->entries().at(0), entry0); + QCOMPARE(root->entries().at(1), entry1); + QCOMPARE(root->entries().at(2), entry2); + QCOMPARE(root->entries().at(3), entry3); + + // no effect + root->moveEntryUp(entry0); + QCOMPARE(root->entries().at(0), entry0); + QCOMPARE(root->entries().at(1), entry1); + QCOMPARE(root->entries().at(2), entry2); + QCOMPARE(root->entries().at(3), entry3); + + root->moveEntryUp(entry2); + QCOMPARE(root->entries().at(0), entry0); + QCOMPARE(root->entries().at(1), entry2); + QCOMPARE(root->entries().at(2), entry1); + QCOMPARE(root->entries().at(3), entry3); + + root->moveEntryDown(entry0); + QCOMPARE(root->entries().at(0), entry2); + QCOMPARE(root->entries().at(1), entry0); + QCOMPARE(root->entries().at(2), entry1); + QCOMPARE(root->entries().at(3), entry3); + + root->moveEntryUp(entry3); + QCOMPARE(root->entries().at(0), entry2); + QCOMPARE(root->entries().at(1), entry0); + QCOMPARE(root->entries().at(2), entry3); + QCOMPARE(root->entries().at(3), entry1); + + root->moveEntryUp(entry3); + QCOMPARE(root->entries().at(0), entry2); + QCOMPARE(root->entries().at(1), entry3); + QCOMPARE(root->entries().at(2), entry0); + QCOMPARE(root->entries().at(3), entry1); + + root->moveEntryDown(entry2); + QCOMPARE(root->entries().at(0), entry3); + QCOMPARE(root->entries().at(1), entry2); + QCOMPARE(root->entries().at(2), entry0); + QCOMPARE(root->entries().at(3), entry1); + + root->moveEntryUp(entry1); + QCOMPARE(root->entries().at(0), entry3); + QCOMPARE(root->entries().at(1), entry2); + QCOMPARE(root->entries().at(2), entry1); + QCOMPARE(root->entries().at(3), entry0); +} diff --git a/tests/TestGroup.h b/tests/TestGroup.h index dbe5d6f4d..9de86fd2c 100644 --- a/tests/TestGroup.h +++ b/tests/TestGroup.h @@ -49,6 +49,7 @@ private slots: void testHierarchy(); void testApplyGroupIconRecursively(); void testUsernamesRecursive(); + void testMove(); }; #endif // KEEPASSX_TESTGROUP_H diff --git a/utils/makeicons.sh b/utils/makeicons.sh index c9753a639..feb2d25fe 100644 --- a/utils/makeicons.sh +++ b/utils/makeicons.sh @@ -107,6 +107,8 @@ map() { key-enter) echo keyboard-variant ;; keyboard-shortcuts) echo apple-keyboard-command ;; message-close) echo close ;; + move-down) echo chevron-double-down ;; + move-up) echo chevron-double-up ;; object-locked) echo lock-outline ;; object-unlocked) echo lock-open-variant-outline ;; paperclip) echo paperclip ;;