diff --git a/COPYING b/COPYING
index 69195ae4..fbca3c12 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 00000000..edcf1181
--- /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 00000000..7ba0a9f1
--- /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 f4f551e1..4f01feca 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 49f83aa1..e567b9df 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 8feb7e56..c5b7e965 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 36440711..a5e372d0 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 671f840e..3d42692c 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 ce29a5cc..7ce795f1 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 cfeb9fee..7adabc8b 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 88aaf956..7664c64b 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 3f25a436..b78e00e7 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 c5229837..69ef33a8 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 aa4dd7dd..10e29ff2 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 3dfdf20b..e6311f6b 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 055455e5..78da7194 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 fd317cdd..18a69687 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 f3786ed3..e32aa472 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 39e4bd12..c6cb1271 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 ff0cfe07..9fc7e158 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 6c4b9745..26cb0dfe 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 47a917e4..a9acb3dc 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 dbe5d6f4..9de86fd2 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 c9753a63..feb2d25f 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 ;;