mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-12-28 00:39:43 -05:00
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:
parent
43c82ccb09
commit
eb198271ac
2
COPYING
2
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
|
||||
|
1
share/icons/application/scalable/actions/move-down.svg
Normal file
1
share/icons/application/scalable/actions/move-down.svg
Normal 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 |
1
share/icons/application/scalable/actions/move-up.svg
Normal file
1
share/icons/application/scalable/actions/move-up.svg
Normal 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 |
@ -43,6 +43,8 @@
|
||||
<file>application/scalable/actions/key-enter.svg</file>
|
||||
<file>application/scalable/actions/keyboard-shortcuts.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-unlocked.svg</file>
|
||||
<file>application/scalable/actions/paperclip.svg</file>
|
||||
|
@ -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
|
||||
|
@ -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<FileWatcher> m_fileWatcher;
|
||||
bool m_modified = false;
|
||||
bool m_emitModified;
|
||||
bool m_hasNonDataChange = false;
|
||||
QString m_keyError;
|
||||
|
||||
QList<QString> m_commonUsernames;
|
||||
|
@ -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;
|
||||
|
@ -233,6 +233,9 @@ public:
|
||||
void beginUpdate();
|
||||
bool endUpdate();
|
||||
|
||||
void moveUp();
|
||||
void moveDown();
|
||||
|
||||
Group* group();
|
||||
const Group* group() const;
|
||||
void setGroup(Group* group);
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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<Database>::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();
|
||||
|
@ -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<Entry*> entries);
|
||||
void focusOnEntries();
|
||||
void focusOnGroups();
|
||||
void moveEntryUp();
|
||||
void moveEntryDown();
|
||||
void copyTitle();
|
||||
void copyUsername();
|
||||
void copyPassword();
|
||||
|
@ -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
|
||||
|
@ -309,6 +309,9 @@
|
||||
<addaction name="actionEntryClone"/>
|
||||
<addaction name="actionEntryDelete"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionEntryMoveUp"/>
|
||||
<addaction name="actionEntryMoveDown"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionEntryCopyUsername"/>
|
||||
<addaction name="actionEntryCopyPassword"/>
|
||||
<addaction name="menuEntryCopyAttribute"/>
|
||||
@ -578,6 +581,28 @@
|
||||
<string>&Clone Entry…</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionEntryMoveUp">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Move u&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&wn</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Move entry one step down</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionEntryCopyUsername">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
|
@ -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*)));
|
||||
}
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ private slots:
|
||||
void testResolveNonIdPlaceholdersToUuid();
|
||||
void testResolveClonedEntry();
|
||||
void testIsRecycled();
|
||||
void testMove();
|
||||
};
|
||||
|
||||
#endif // KEEPASSX_TESTENTRY_H
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ private slots:
|
||||
void testHierarchy();
|
||||
void testApplyGroupIconRecursively();
|
||||
void testUsernamesRecursive();
|
||||
void testMove();
|
||||
};
|
||||
|
||||
#endif // KEEPASSX_TESTGROUP_H
|
||||
|
@ -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 ;;
|
||||
|
Loading…
Reference in New Issue
Block a user