diff --git a/share/demo.kdbx b/share/demo.kdbx index 7c51608e..7be77579 100644 Binary files a/share/demo.kdbx and b/share/demo.kdbx differ diff --git a/share/icons/application/scalable/actions/entry-restore.svg b/share/icons/application/scalable/actions/entry-restore.svg new file mode 100644 index 00000000..da5b69da --- /dev/null +++ b/share/icons/application/scalable/actions/entry-restore.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/icons.qrc b/share/icons/icons.qrc index 05e880c0..b35ad77b 100644 --- a/share/icons/icons.qrc +++ b/share/icons/icons.qrc @@ -34,6 +34,7 @@ application/scalable/actions/edit-clear-locationbar-rtl.svg application/scalable/actions/entry-clone.svg application/scalable/actions/entry-delete.svg + application/scalable/actions/entry-restore.svg application/scalable/actions/entry-edit.svg application/scalable/actions/entry-new.svg application/scalable/actions/favicon-download.svg diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index e671d762..daef0a8d 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -5227,6 +5227,10 @@ We recommend you use the AppImage available on our downloads page. Please present or touch your YubiKey to continue… + + Restore Entry(s) + + ManageDatabase diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index 034a295d..d6e07222 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -1381,6 +1381,14 @@ QString Entry::resolveUrl(const QString& url) const return {}; } +Group* Entry::previousParentGroup() +{ + if (!database() || !database()->rootGroup()) { + return nullptr; + } + return database()->rootGroup()->findGroupByUuid(m_data.previousParentGroupUuid); +} + const Group* Entry::previousParentGroup() const { if (!database() || !database()->rootGroup()) { diff --git a/src/core/Entry.h b/src/core/Entry.h index 6a19078a..50c3427e 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -107,6 +107,7 @@ public: QString totp() const; QString totpSettingsString() const; QSharedPointer totpSettings() const; + Group* previousParentGroup(); const Group* previousParentGroup() const; QUuid previousParentGroupUuid() const; int size() const; diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 6d88a661..1183810b 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -475,6 +475,26 @@ void DatabaseWidget::deleteSelectedEntries() deleteEntries(std::move(selectedEntries)); } +void DatabaseWidget::restoreSelectedEntries() +{ + const QModelIndexList selected = m_entryView->selectionModel()->selectedRows(); + if (selected.isEmpty()) { + return; + } + + // Resolve entries from the selection model + QList selectedEntries; + for (auto& index : selected) { + selectedEntries.append(m_entryView->entryFromIndex(index)); + } + + for (auto* entry : selectedEntries) { + if (entry->previousParentGroup()) { + entry->setGroup(entry->previousParentGroup()); + } + } +} + void DatabaseWidget::deleteEntries(QList selectedEntries, bool confirm) { if (selectedEntries.isEmpty()) { diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index 3909dfd7..c329b98f 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -164,6 +164,7 @@ public slots: void createEntry(); void cloneEntry(); void deleteSelectedEntries(); + void restoreSelectedEntries(); void deleteEntries(QList entries, bool confirm = true); void focusOnEntries(bool editIfFocused = false); void focusOnGroups(bool editIfFocused = false); diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 1f99448b..5aa39ad5 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -129,6 +129,7 @@ MainWindow::MainWindow() m_countDefaultAttributes = m_ui->menuEntryCopyAttribute->actions().size(); m_entryContextMenu = new QMenu(this); + m_entryContextMenu->setSeparatorsCollapsible(true); m_entryContextMenu->addAction(m_ui->actionEntryCopyUsername); m_entryContextMenu->addAction(m_ui->actionEntryCopyPassword); m_entryContextMenu->addAction(m_ui->menuEntryCopyAttribute->menuAction()); @@ -146,6 +147,8 @@ MainWindow::MainWindow() m_entryContextMenu->addSeparator(); m_entryContextMenu->addAction(m_ui->actionEntryOpenUrl); m_entryContextMenu->addAction(m_ui->actionEntryDownloadIcon); + m_entryContextMenu->addSeparator(); + m_entryContextMenu->addAction(m_ui->actionEntryRestore); m_entryNewContextMenu = new QMenu(this); m_entryNewContextMenu->addAction(m_ui->actionEntryNew); @@ -275,6 +278,7 @@ MainWindow::MainWindow() m_ui->actionEntryAutoTypeSequence->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_V); m_ui->actionEntryOpenUrl->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_U); m_ui->actionEntryCopyURL->setShortcut(Qt::CTRL + Qt::Key_U); + m_ui->actionEntryRestore->setShortcut(Qt::CTRL + Qt::Key_R); // Prevent conflicts with global Mac shortcuts (force Control on all platforms) #ifdef Q_OS_MAC @@ -291,6 +295,7 @@ MainWindow::MainWindow() m_ui->actionEntryNew->setShortcutVisibleInContextMenu(true); m_ui->actionEntryEdit->setShortcutVisibleInContextMenu(true); m_ui->actionEntryDelete->setShortcutVisibleInContextMenu(true); + m_ui->actionEntryRestore->setShortcutVisibleInContextMenu(true); m_ui->actionEntryClone->setShortcutVisibleInContextMenu(true); m_ui->actionEntryTotp->setShortcutVisibleInContextMenu(true); m_ui->actionEntryDownloadIcon->setShortcutVisibleInContextMenu(true); @@ -374,6 +379,7 @@ MainWindow::MainWindow() m_ui->actionEntryClone->setIcon(icons()->icon("entry-clone")); m_ui->actionEntryEdit->setIcon(icons()->icon("entry-edit")); m_ui->actionEntryDelete->setIcon(icons()->icon("entry-delete")); + m_ui->actionEntryRestore->setIcon(icons()->icon("entry-restore")); m_ui->actionEntryAutoType->setIcon(icons()->icon("auto-type")); m_ui->actionEntryAutoTypeSequence->setIcon(icons()->icon("auto-type")); m_ui->actionEntryAutoTypeUsername->setIcon(icons()->icon("auto-type")); @@ -461,6 +467,7 @@ MainWindow::MainWindow() m_actionMultiplexer.connect(m_ui->actionEntryClone, SIGNAL(triggered()), SLOT(cloneEntry())); m_actionMultiplexer.connect(m_ui->actionEntryEdit, SIGNAL(triggered()), SLOT(switchToEntryEdit())); m_actionMultiplexer.connect(m_ui->actionEntryDelete, SIGNAL(triggered()), SLOT(deleteSelectedEntries())); + m_actionMultiplexer.connect(m_ui->actionEntryRestore, SIGNAL(triggered()), SLOT(restoreSelectedEntries())); m_actionMultiplexer.connect(m_ui->actionEntryTotp, SIGNAL(triggered()), SLOT(showTotp())); m_actionMultiplexer.connect(m_ui->actionEntrySetupTotp, SIGNAL(triggered()), SLOT(setupTotp())); @@ -800,6 +807,9 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) m_ui->actionEntryClone->setEnabled(singleEntrySelected); m_ui->actionEntryEdit->setEnabled(singleEntrySelected); m_ui->actionEntryDelete->setEnabled(entriesSelected); + m_ui->actionEntryRestore->setVisible(entriesSelected && recycleBinSelected); + m_ui->actionEntryRestore->setEnabled(entriesSelected && recycleBinSelected); + m_ui->actionEntryRestore->setText(tr("Restore Entry(s)", "", dbWidget->numberOfSelectedEntries())); m_ui->actionEntryMoveUp->setVisible(!sorted); m_ui->actionEntryMoveDown->setVisible(!sorted); m_ui->actionEntryMoveUp->setEnabled(singleEntrySelected && !sorted && entryIndex > 0); diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index 8b36b97a..f83e8077 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -216,7 +216,7 @@ 0 0 800 - 22 + 21 @@ -283,6 +283,9 @@ &Entries + + true + false @@ -330,6 +333,8 @@ + + @@ -1068,6 +1073,17 @@ Clone Group... + + + Restore Entry(s) + + + Restore Entry(s) + + + Ctrl+R + +