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
This commit is contained in:
Holger Böhnke 2020-05-21 21:43:00 -04:00 committed by Jonathan White
parent 43c82ccb09
commit eb198271ac
24 changed files with 500 additions and 11 deletions

View File

@ -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/help-about.svg
share/icons/application/scalable/actions/key-enter.svg share/icons/application/scalable/actions/key-enter.svg
share/icons/application/scalable/actions/message-close.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/paperclip.svg
share/icons/application/scalable/actions/password-copy.svg share/icons/application/scalable/actions/password-copy.svg
share/icons/application/scalable/actions/password-generate.svg share/icons/application/scalable/actions/password-generate.svg

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M16.59,5.59L18,7L12,13L6,7L7.41,5.59L12,10.17L16.59,5.59M16.59,11.59L18,13L12,19L6,13L7.41,11.59L12,16.17L16.59,11.59Z" /></svg>

After

Width:  |  Height:  |  Size: 413 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M7.41,18.41L6,17L12,11L18,17L16.59,18.41L12,13.83L7.41,18.41M7.41,12.41L6,11L12,5L18,11L16.59,12.41L12,7.83L7.41,12.41Z" /></svg>

After

Width:  |  Height:  |  Size: 414 B

View File

@ -43,6 +43,8 @@
<file>application/scalable/actions/key-enter.svg</file> <file>application/scalable/actions/key-enter.svg</file>
<file>application/scalable/actions/keyboard-shortcuts.svg</file> <file>application/scalable/actions/keyboard-shortcuts.svg</file>
<file>application/scalable/actions/message-close.svg</file> <file>application/scalable/actions/message-close.svg</file>
<file>application/scalable/actions/move-down.svg</file>
<file>application/scalable/actions/move-up.svg</file>
<file>application/scalable/actions/object-locked.svg</file> <file>application/scalable/actions/object-locked.svg</file>
<file>application/scalable/actions/object-unlocked.svg</file> <file>application/scalable/actions/object-unlocked.svg</file>
<file>application/scalable/actions/paperclip.svg</file> <file>application/scalable/actions/paperclip.svg</file>

View File

@ -836,9 +836,9 @@ void Database::setEmitModified(bool value)
m_emitModified = 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() void Database::markAsModified()
@ -855,11 +855,17 @@ void Database::markAsClean()
bool emitSignal = m_modified; bool emitSignal = m_modified;
m_modified = false; m_modified = false;
m_modifiedTimer.stop(); m_modifiedTimer.stop();
m_hasNonDataChange = false;
if (emitSignal) { if (emitSignal) {
emit databaseSaved(); emit databaseSaved();
} }
} }
void Database::markNonDataChange()
{
m_hasNonDataChange = true;
}
/** /**
* @param uuid UUID of the database * @param uuid UUID of the database
* @return pointer to the database or nullptr if no such database exists * @return pointer to the database or nullptr if no such database exists

View File

@ -81,7 +81,7 @@ public:
void releaseData(); void releaseData();
bool isInitialized() const; bool isInitialized() const;
bool isModified() const; bool isModified(bool includeNonDataChanges = false) const;
void setEmitModified(bool value); void setEmitModified(bool value);
bool isReadOnly() const; bool isReadOnly() const;
void setReadOnly(bool readOnly); void setReadOnly(bool readOnly);
@ -138,6 +138,7 @@ public slots:
void markAsModified(); void markAsModified();
void markAsClean(); void markAsClean();
void updateCommonUsernames(int topN = 10); void updateCommonUsernames(int topN = 10);
void markNonDataChange();
signals: signals:
void filePathChanged(const QString& oldPath, const QString& newPath); void filePathChanged(const QString& oldPath, const QString& newPath);
@ -210,6 +211,7 @@ private:
QPointer<FileWatcher> m_fileWatcher; QPointer<FileWatcher> m_fileWatcher;
bool m_modified = false; bool m_modified = false;
bool m_emitModified; bool m_emitModified;
bool m_hasNonDataChange = false;
QString m_keyError; QString m_keyError;
QList<QString> m_commonUsernames; QList<QString> m_commonUsernames;

View File

@ -1066,6 +1066,20 @@ QString Entry::referenceFieldValue(EntryReferenceType referenceType) const
return QString(); 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() Group* Entry::group()
{ {
return m_group; return m_group;

View File

@ -233,6 +233,9 @@ public:
void beginUpdate(); void beginUpdate();
bool endUpdate(); bool endUpdate();
void moveUp();
void moveDown();
Group* group(); Group* group();
const Group* group() const; const Group* group() const;
void setGroup(Group* group); void setGroup(Group* group);

View File

@ -48,6 +48,7 @@ Group::Group()
connect(m_customData, SIGNAL(customDataModified()), this, SIGNAL(groupModified())); connect(m_customData, SIGNAL(customDataModified()), this, SIGNAL(groupModified()));
connect(this, SIGNAL(groupModified()), SLOT(updateTimeinfo())); connect(this, SIGNAL(groupModified()), SLOT(updateTimeinfo()));
connect(this, SIGNAL(groupNonDataChange()), SLOT(updateTimeinfo()));
} }
Group::~Group() Group::~Group()
@ -364,11 +365,11 @@ void Group::setExpanded(bool expanded)
{ {
if (m_data.isExpanded != expanded) { if (m_data.isExpanded != expanded) {
m_data.isExpanded = expanded; m_data.isExpanded = expanded;
if (!config()->get(Config::TrackNonDataChanges).toBool()) { if (config()->get(Config::TrackNonDataChanges).toBool()) {
updateTimeinfo(); emit groupModified();
return; } else {
emit groupNonDataChange();
} }
emit groupModified();
} }
} }
@ -964,6 +965,40 @@ void Group::removeEntry(Entry* entry)
emit entryRemoved(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) void Group::connectDatabaseSignalsRecursive(Database* db)
{ {
if (m_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(aboutToMove(Group*,Group*,int)), db, SIGNAL(groupAboutToMove(Group*,Group*,int)));
connect(this, SIGNAL(groupMoved()), db, SIGNAL(groupMoved())); connect(this, SIGNAL(groupMoved()), db, SIGNAL(groupMoved()));
connect(this, SIGNAL(groupModified()), db, SLOT(markAsModified())); connect(this, SIGNAL(groupModified()), db, SLOT(markAsModified()));
connect(this, SIGNAL(groupNonDataChange()), db, SLOT(markNonDataChange()));
// clang-format on // clang-format on
} }

View File

@ -167,6 +167,8 @@ public:
void addEntry(Entry* entry); void addEntry(Entry* entry);
void removeEntry(Entry* entry); void removeEntry(Entry* entry);
void moveEntryUp(Entry* entry);
void moveEntryDown(Entry* entry);
void applyGroupIconOnCreateTo(Entry* entry); void applyGroupIconOnCreateTo(Entry* entry);
void applyGroupIconTo(Entry* entry); void applyGroupIconTo(Entry* entry);
@ -185,10 +187,15 @@ signals:
void aboutToMove(Group* group, Group* toGroup, int index); void aboutToMove(Group* group, Group* toGroup, int index);
void groupMoved(); void groupMoved();
void groupModified(); void groupModified();
void groupNonDataChange();
void entryAboutToAdd(Entry* entry); void entryAboutToAdd(Entry* entry);
void entryAdded(Entry* entry); void entryAdded(Entry* entry);
void entryAboutToRemove(Entry* entry); void entryAboutToRemove(Entry* entry);
void entryRemoved(Entry* entry); void entryRemoved(Entry* entry);
void entryAboutToMoveUp(int row);
void entryMovedUp();
void entryAboutToMoveDown(int row);
void entryMovedDown();
void entryDataChanged(Entry* entry); void entryDataChanged(Entry* entry);
private slots: private slots:

View File

@ -279,6 +279,11 @@ bool DatabaseWidget::isSaving() const
return m_db->isSaving(); return m_db->isSaving();
} }
bool DatabaseWidget::isSorted() const
{
return m_entryView->isSorted();
}
bool DatabaseWidget::isSearchActive() const bool DatabaseWidget::isSearchActive() const
{ {
return m_entryView->inSearchMode(); 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() void DatabaseWidget::copyTitle()
{ {
auto currentEntry = currentSelectedEntry(); auto currentEntry = currentSelectedEntry();
@ -1510,7 +1533,7 @@ bool DatabaseWidget::lock()
} }
} }
if (m_db->isModified()) { if (m_db->isModified(true)) {
bool saved = false; bool saved = false;
// Attempt to save on exit, but don't block locking if it fails // Attempt to save on exit, but don't block locking if it fails
if (config()->get(Config::AutoSaveOnExit).toBool() if (config()->get(Config::AutoSaveOnExit).toBool()
@ -1594,7 +1617,7 @@ void DatabaseWidget::reloadDatabaseFile()
QString error; QString error;
auto db = QSharedPointer<Database>::create(m_db->filePath()); auto db = QSharedPointer<Database>::create(m_db->filePath());
if (db->open(database()->key(), &error)) { 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 // Ask if we want to merge changes into new database
auto result = MessageBox::question( auto result = MessageBox::question(
this, this,
@ -1641,6 +1664,11 @@ int DatabaseWidget::numberOfSelectedEntries() const
return m_entryView->numberOfSelectedEntries(); return m_entryView->numberOfSelectedEntries();
} }
int DatabaseWidget::currentEntryIndex() const
{
return m_entryView->currentEntryIndex();
}
QStringList DatabaseWidget::customEntryAttributes() const QStringList DatabaseWidget::customEntryAttributes() const
{ {
Entry* entry = m_entryView->currentEntry(); Entry* entry = m_entryView->currentEntry();

View File

@ -83,6 +83,7 @@ public:
DatabaseWidget::Mode currentMode() const; DatabaseWidget::Mode currentMode() const;
bool isLocked() const; bool isLocked() const;
bool isSaving() const; bool isSaving() const;
bool isSorted() const;
bool isSearchActive() const; bool isSearchActive() const;
bool isEntryViewActive() const; bool isEntryViewActive() const;
bool isEntryEditActive() const; bool isEntryEditActive() const;
@ -99,6 +100,7 @@ public:
bool isGroupSelected() const; bool isGroupSelected() const;
bool isRecycleBinSelected() const; bool isRecycleBinSelected() const;
int numberOfSelectedEntries() const; int numberOfSelectedEntries() const;
int currentEntryIndex() const;
QStringList customEntryAttributes() const; QStringList customEntryAttributes() const;
bool isEditWidgetModified() const; bool isEditWidgetModified() const;
@ -167,6 +169,8 @@ public slots:
void deleteEntries(QList<Entry*> entries); void deleteEntries(QList<Entry*> entries);
void focusOnEntries(); void focusOnEntries();
void focusOnGroups(); void focusOnGroups();
void moveEntryUp();
void moveEntryDown();
void copyTitle(); void copyTitle();
void copyUsername(); void copyUsername();
void copyPassword(); void copyPassword();

View File

@ -128,6 +128,9 @@ MainWindow::MainWindow()
m_entryContextMenu->addAction(m_ui->actionEntryDelete); m_entryContextMenu->addAction(m_ui->actionEntryDelete);
m_entryContextMenu->addAction(m_ui->actionEntryNew); m_entryContextMenu->addAction(m_ui->actionEntryNew);
m_entryContextMenu->addSeparator(); 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->actionEntryOpenUrl);
m_entryContextMenu->addAction(m_ui->actionEntryDownloadIcon); 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->actionEntryTotp->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_T);
m_ui->actionEntryDownloadIcon->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_D); m_ui->actionEntryDownloadIcon->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_D);
m_ui->actionEntryCopyTotp->setShortcut(Qt::CTRL + Qt::Key_T); 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->actionEntryCopyUsername->setShortcut(Qt::CTRL + Qt::Key_B);
m_ui->actionEntryCopyPassword->setShortcut(Qt::CTRL + Qt::Key_C); m_ui->actionEntryCopyPassword->setShortcut(Qt::CTRL + Qt::Key_C);
m_ui->actionEntryAutoType->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_V); m_ui->actionEntryAutoType->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_V);
@ -254,6 +259,8 @@ MainWindow::MainWindow()
m_ui->actionEntryTotp->setShortcutVisibleInContextMenu(true); m_ui->actionEntryTotp->setShortcutVisibleInContextMenu(true);
m_ui->actionEntryDownloadIcon->setShortcutVisibleInContextMenu(true); m_ui->actionEntryDownloadIcon->setShortcutVisibleInContextMenu(true);
m_ui->actionEntryCopyTotp->setShortcutVisibleInContextMenu(true); m_ui->actionEntryCopyTotp->setShortcutVisibleInContextMenu(true);
m_ui->actionEntryMoveUp->setShortcutVisibleInContextMenu(true);
m_ui->actionEntryMoveDown->setShortcutVisibleInContextMenu(true);
m_ui->actionEntryCopyUsername->setShortcutVisibleInContextMenu(true); m_ui->actionEntryCopyUsername->setShortcutVisibleInContextMenu(true);
m_ui->actionEntryCopyPassword->setShortcutVisibleInContextMenu(true); m_ui->actionEntryCopyPassword->setShortcutVisibleInContextMenu(true);
m_ui->actionEntryAutoType->setShortcutVisibleInContextMenu(true); m_ui->actionEntryAutoType->setShortcutVisibleInContextMenu(true);
@ -336,6 +343,8 @@ MainWindow::MainWindow()
m_ui->actionEntryEdit->setIcon(resources()->icon("entry-edit")); m_ui->actionEntryEdit->setIcon(resources()->icon("entry-edit"));
m_ui->actionEntryDelete->setIcon(resources()->icon("entry-delete")); m_ui->actionEntryDelete->setIcon(resources()->icon("entry-delete"));
m_ui->actionEntryAutoType->setIcon(resources()->icon("auto-type")); 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->actionEntryCopyUsername->setIcon(resources()->icon("username-copy"));
m_ui->actionEntryCopyPassword->setIcon(resources()->icon("password-copy")); m_ui->actionEntryCopyPassword->setIcon(resources()->icon("password-copy"));
m_ui->actionEntryCopyURL->setIcon(resources()->icon("url-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->actionEntryCopyTotp, SIGNAL(triggered()), SLOT(copyTotp()));
m_actionMultiplexer.connect(m_ui->actionEntryTotpQRCode, SIGNAL(triggered()), SLOT(showTotpKeyQrCode())); 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->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->actionEntryCopyUsername, SIGNAL(triggered()), SLOT(copyUsername()));
m_actionMultiplexer.connect(m_ui->actionEntryCopyPassword, SIGNAL(triggered()), SLOT(copyPassword())); m_actionMultiplexer.connect(m_ui->actionEntryCopyPassword, SIGNAL(triggered()), SLOT(copyPassword()));
m_actionMultiplexer.connect(m_ui->actionEntryCopyURL, SIGNAL(triggered()), SLOT(copyURL())); 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 currentGroupHasChildren = dbWidget->currentGroup()->hasChildren();
bool currentGroupHasEntries = !dbWidget->currentGroup()->entries().isEmpty(); bool currentGroupHasEntries = !dbWidget->currentGroup()->entries().isEmpty();
bool recycleBinSelected = dbWidget->isRecycleBinSelected(); 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->actionEntryNew->setEnabled(true);
m_ui->actionEntryClone->setEnabled(singleEntrySelected); m_ui->actionEntryClone->setEnabled(singleEntrySelected);
m_ui->actionEntryEdit->setEnabled(singleEntrySelected); m_ui->actionEntryEdit->setEnabled(singleEntrySelected);
m_ui->actionEntryDelete->setEnabled(entriesSelected); 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->actionEntryCopyTitle->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTitle());
m_ui->actionEntryCopyUsername->setEnabled(singleEntrySelected && dbWidget->currentEntryHasUsername()); 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 // NOTE: Copy password is enabled even if the selected entry's password is blank to prevent Ctrl+C

View File

@ -309,6 +309,9 @@
<addaction name="actionEntryClone"/> <addaction name="actionEntryClone"/>
<addaction name="actionEntryDelete"/> <addaction name="actionEntryDelete"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionEntryMoveUp"/>
<addaction name="actionEntryMoveDown"/>
<addaction name="separator"/>
<addaction name="actionEntryCopyUsername"/> <addaction name="actionEntryCopyUsername"/>
<addaction name="actionEntryCopyPassword"/> <addaction name="actionEntryCopyPassword"/>
<addaction name="menuEntryCopyAttribute"/> <addaction name="menuEntryCopyAttribute"/>
@ -578,6 +581,28 @@
<string>&amp;Clone Entry…</string> <string>&amp;Clone Entry…</string>
</property> </property>
</action> </action>
<action name="actionEntryMoveUp">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Move u&amp;p</string>
</property>
<property name="toolTip">
<string>Move entry one step up</string>
</property>
</action>
<action name="actionEntryMoveDown">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Move do&amp;wn</string>
</property>
<property name="toolTip">
<string>Move entry one step down</string>
</property>
</action>
<action name="actionEntryCopyUsername"> <action name="actionEntryCopyUsername">
<property name="enabled"> <property name="enabled">
<bool>false</bool> <bool>false</bool>

View File

@ -497,10 +497,41 @@ void EntryModel::entryRemoved()
if (m_group) { if (m_group) {
m_entries = m_group->entries(); m_entries = m_group->entries();
} }
endRemoveRows(); 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) void EntryModel::entryDataChanged(Entry* entry)
{ {
int row = m_entries.indexOf(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(entryAdded(Entry*)), SLOT(entryAdded(Entry*)));
connect(group, SIGNAL(entryAboutToRemove(Entry*)), SLOT(entryAboutToRemove(Entry*))); connect(group, SIGNAL(entryAboutToRemove(Entry*)), SLOT(entryAboutToRemove(Entry*)));
connect(group, SIGNAL(entryRemoved(Entry*)), SLOT(entryRemoved())); 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*))); connect(group, SIGNAL(entryDataChanged(Entry*)), SLOT(entryDataChanged(Entry*)));
} }

View File

@ -78,6 +78,10 @@ private slots:
void entryAdded(Entry* entry); void entryAdded(Entry* entry);
void entryAboutToRemove(Entry* entry); void entryAboutToRemove(Entry* entry);
void entryRemoved(); void entryRemoved();
void entryAboutToMoveUp(int row);
void entryMovedUp();
void entryAboutToMoveDown(int row);
void entryMovedDown();
void entryDataChanged(Entry* entry); void entryDataChanged(Entry* entry);
private: private:

View File

@ -30,6 +30,8 @@ EntryView::EntryView(QWidget* parent)
: QTreeView(parent) : QTreeView(parent)
, m_model(new EntryModel(this)) , m_model(new EntryModel(this))
, m_sortModel(new SortFilterHideProxyModel(this)) , m_sortModel(new SortFilterHideProxyModel(this))
, m_lastIndex(-1)
, m_lastOrder(Qt::AscendingOrder)
, m_inSearchMode(false) , m_inSearchMode(false)
{ {
m_sortModel->setSourceModel(m_model); m_sortModel->setSourceModel(m_model);
@ -120,7 +122,7 @@ EntryView::EntryView(QWidget* parent)
// clang-format on // clang-format on
// clang-format off // 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 // 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) void EntryView::keyPressEvent(QKeyEvent* event)
{ {
if ((event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return) && currentIndex().isValid()) { if ((event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return) && currentIndex().isValid()) {
@ -211,6 +238,11 @@ bool EntryView::inSearchMode()
return m_inSearchMode; return m_inSearchMode;
} }
bool EntryView::isSorted()
{
return header()->sortIndicatorSection() != -1;
}
void EntryView::emitEntryActivated(const QModelIndex& index) void EntryView::emitEntryActivated(const QModelIndex& index)
{ {
Entry* entry = entryFromIndex(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 * Get current state of 'Hide Usernames' setting (NOTE: just pass-through for
* m_model) * m_model)

View File

@ -39,7 +39,9 @@ public:
Entry* currentEntry(); Entry* currentEntry();
void setCurrentEntry(Entry* entry); void setCurrentEntry(Entry* entry);
Entry* entryFromIndex(const QModelIndex& index); Entry* entryFromIndex(const QModelIndex& index);
int currentEntryIndex();
bool inSearchMode(); bool inSearchMode();
bool isSorted();
int numberOfSelectedEntries(); int numberOfSelectedEntries();
void setFirstEntryActive(); void setFirstEntryActive();
bool isUsernamesHidden() const; bool isUsernamesHidden() const;
@ -74,12 +76,15 @@ private slots:
void fitColumnsToContents(); void fitColumnsToContents();
void resetViewToDefaults(); void resetViewToDefaults();
void contextMenuShortcutPressed(); void contextMenuShortcutPressed();
void sortIndicatorChanged(int logicalIndex, Qt::SortOrder order);
private: private:
void resetFixedColumns(); void resetFixedColumns();
EntryModel* const m_model; EntryModel* const m_model;
SortFilterHideProxyModel* const m_sortModel; SortFilterHideProxyModel* const m_sortModel;
int m_lastIndex;
Qt::SortOrder m_lastOrder;
bool m_inSearchMode; bool m_inSearchMode;
bool m_columnsNeedRelayout = true; bool m_columnsNeedRelayout = true;

View File

@ -612,3 +612,114 @@ void TestEntry::testIsRecycled()
db.recycleGroup(group1); db.recycleGroup(group1);
QVERIFY(entry1->isRecycled()); 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);
}

View File

@ -38,6 +38,7 @@ private slots:
void testResolveNonIdPlaceholdersToUuid(); void testResolveNonIdPlaceholdersToUuid();
void testResolveClonedEntry(); void testResolveClonedEntry();
void testIsRecycled(); void testIsRecycled();
void testMove();
}; };
#endif // KEEPASSX_TESTENTRY_H #endif // KEEPASSX_TESTENTRY_H

View File

@ -55,6 +55,9 @@ void TestEntryModel::test()
EntryModel* model = new EntryModel(this); 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); ModelTest* modelTest = new ModelTest(model, this);
model->setGroup(group1); model->setGroup(group1);
@ -79,6 +82,29 @@ void TestEntryModel::test()
Entry* entry3 = new Entry(); Entry* entry3 = new Entry();
entry3->setGroup(group1); 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(spyAboutToAdd.count(), 1);
QCOMPARE(spyAdded.count(), 1); QCOMPARE(spyAdded.count(), 1);
QCOMPARE(spyAboutToRemove.count(), 0); QCOMPARE(spyAboutToRemove.count(), 0);

View File

@ -1208,3 +1208,114 @@ void TestGroup::testUsernamesRecursive()
QVERIFY(usernames.contains("Name2")); QVERIFY(usernames.contains("Name2"));
QVERIFY(usernames.indexOf("Name2") < usernames.indexOf("Name1")); 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);
}

View File

@ -49,6 +49,7 @@ private slots:
void testHierarchy(); void testHierarchy();
void testApplyGroupIconRecursively(); void testApplyGroupIconRecursively();
void testUsernamesRecursive(); void testUsernamesRecursive();
void testMove();
}; };
#endif // KEEPASSX_TESTGROUP_H #endif // KEEPASSX_TESTGROUP_H

View File

@ -107,6 +107,8 @@ map() {
key-enter) echo keyboard-variant ;; key-enter) echo keyboard-variant ;;
keyboard-shortcuts) echo apple-keyboard-command ;; keyboard-shortcuts) echo apple-keyboard-command ;;
message-close) echo close ;; message-close) echo close ;;
move-down) echo chevron-double-down ;;
move-up) echo chevron-double-up ;;
object-locked) echo lock-outline ;; object-locked) echo lock-outline ;;
object-unlocked) echo lock-open-variant-outline ;; object-unlocked) echo lock-open-variant-outline ;;
paperclip) echo paperclip ;; paperclip) echo paperclip ;;