From 6acd0b25aee3f3197fe62f09935f613974e964ec Mon Sep 17 00:00:00 2001 From: Xavier Valls Date: Mon, 31 May 2021 16:40:20 +0200 Subject: [PATCH] Add a context menu entry to delete entries from health check reports (#6537) * Closes #4986 - Allow deleting entries from the reports view * Closes #4533 - Exclude & delete multiple entries in a report * Also allow deleting selected entries using the delete key * Introduce GuiTools namespace to collect shared GUI prompts and actions * Add functionality to HIBP report to mirror health check report Co-authored-by: Jonathan White --- src/CMakeLists.txt | 1 + src/gui/DatabaseWidget.cpp | 94 +------------- src/gui/DatabaseWidget.h | 3 +- src/gui/GuiTools.cpp | 122 +++++++++++++++++++ src/gui/GuiTools.h | 31 +++++ src/gui/reports/ReportsWidgetHealthcheck.cpp | 108 ++++++++++------ src/gui/reports/ReportsWidgetHealthcheck.h | 7 +- src/gui/reports/ReportsWidgetHealthcheck.ui | 7 +- src/gui/reports/ReportsWidgetHibp.cpp | 91 +++++++++----- src/gui/reports/ReportsWidgetHibp.h | 7 +- src/gui/reports/ReportsWidgetHibp.ui | 5 +- 11 files changed, 307 insertions(+), 169 deletions(-) create mode 100644 src/gui/GuiTools.cpp create mode 100644 src/gui/GuiTools.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c4b09e16..2ec868cb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -114,6 +114,7 @@ set(keepassx_SOURCES gui/EditWidgetProperties.cpp gui/FileDialog.cpp gui/Font.cpp + gui/GuiTools.cpp gui/IconModels.cpp gui/KeePass1OpenWidget.cpp gui/KMessageWidget.cpp diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 16195fa7..d2166b70 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -50,6 +50,7 @@ #include "gui/DatabaseOpenWidget.h" #include "gui/EntryPreviewWidget.h" #include "gui/FileDialog.h" +#include "gui/GuiTools.h" #include "gui/KeePass1OpenWidget.h" #include "gui/MainWindow.h" #include "gui/MessageBox.h" @@ -487,57 +488,11 @@ void DatabaseWidget::deleteEntries(QList selectedEntries, bool confirm) bool permanent = (recycleBin && recycleBin->findEntryByUuid(selectedEntries.first()->uuid())) || !m_db->metadata()->recycleBinEnabled(); - if (confirm && !confirmDeleteEntries(selectedEntries, permanent)) { + if (confirm && !GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) { return; } - // Find references to selected entries and prompt for direction if necessary - auto it = selectedEntries.begin(); - while (confirm && it != selectedEntries.end()) { - auto references = m_db->rootGroup()->referencesRecursive(*it); - if (!references.isEmpty()) { - // Ignore references that are selected for deletion - for (auto* entry : selectedEntries) { - references.removeAll(entry); - } - - if (!references.isEmpty()) { - // Prompt for reference handling - auto result = MessageBox::question( - this, - tr("Replace references to entry?"), - tr("Entry \"%1\" has %2 reference(s). " - "Do you want to overwrite references with values, skip this entry, or delete anyway?", - "", - references.size()) - .arg((*it)->title().toHtmlEscaped()) - .arg(references.size()), - MessageBox::Overwrite | MessageBox::Skip | MessageBox::Delete, - MessageBox::Overwrite); - - if (result == MessageBox::Overwrite) { - for (auto* entry : references) { - entry->replaceReferencesWithValues(*it); - } - } else if (result == MessageBox::Skip) { - it = selectedEntries.erase(it); - continue; - } - } - } - - it++; - } - - if (permanent) { - for (auto* entry : asConst(selectedEntries)) { - delete entry; - } - } else { - for (auto* entry : asConst(selectedEntries)) { - m_db->recycleEntry(entry); - } - } + GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent); refreshSearch(); @@ -550,49 +505,6 @@ void DatabaseWidget::deleteEntries(QList selectedEntries, bool confirm) } } -bool DatabaseWidget::confirmDeleteEntries(QList entries, bool permanent) -{ - if (entries.isEmpty()) { - return false; - } - - if (permanent) { - QString prompt; - if (entries.size() == 1) { - prompt = tr("Do you really want to delete the entry \"%1\" for good?") - .arg(entries.first()->title().toHtmlEscaped()); - } else { - prompt = tr("Do you really want to delete %n entry(s) for good?", "", entries.size()); - } - - auto answer = MessageBox::question(this, - tr("Delete entry(s)?", "", entries.size()), - prompt, - MessageBox::Delete | MessageBox::Cancel, - MessageBox::Cancel); - - return answer == MessageBox::Delete; - } else if (config()->get(Config::Security_NoConfirmMoveEntryToRecycleBin).toBool()) { - return true; - } else { - QString prompt; - if (entries.size() == 1) { - prompt = tr("Do you really want to move entry \"%1\" to the recycle bin?") - .arg(entries.first()->title().toHtmlEscaped()); - } else { - prompt = tr("Do you really want to move %n entry(s) to the recycle bin?", "", entries.size()); - } - - auto answer = MessageBox::question(this, - tr("Move entry(s) to recycle bin?", "", entries.size()), - prompt, - MessageBox::Move | MessageBox::Cancel, - MessageBox::Cancel); - - return answer == MessageBox::Move; - } -} - void DatabaseWidget::setFocus(Qt::FocusReason reason) { if (reason == Qt::BacktabFocusReason) { diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index a8c08d6a..dc5dd6e4 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -74,7 +74,7 @@ public: explicit DatabaseWidget(QSharedPointer db, QWidget* parent = nullptr); explicit DatabaseWidget(const QString& filePath, QWidget* parent = nullptr); - ~DatabaseWidget(); + ~DatabaseWidget() override; void setFocus(Qt::FocusReason reason); @@ -255,7 +255,6 @@ private: void setClipboardTextAndMinimize(const QString& text); void processAutoOpen(); void openDatabaseFromEntry(const Entry* entry, bool inBackground = true); - bool confirmDeleteEntries(QList entries, bool permanent); void performIconDownloads(const QList& entries, bool force = false); bool performSave(QString& errorMessage, const QString& fileName = {}); diff --git a/src/gui/GuiTools.cpp b/src/gui/GuiTools.cpp new file mode 100644 index 00000000..72932724 --- /dev/null +++ b/src/gui/GuiTools.cpp @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2021 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "GuiTools.h" + +#include "core/Config.h" +#include "core/Database.h" +#include "core/Entry.h" +#include "core/Group.h" +#include "gui/MessageBox.h" + +namespace GuiTools +{ + bool confirmDeleteEntries(QWidget* parent, const QList& entries, bool permanent) + { + if (!parent || entries.isEmpty()) { + return false; + } + + if (permanent) { + QString prompt; + if (entries.size() == 1) { + prompt = QObject::tr("Do you really want to delete the entry \"%1\" for good?") + .arg(entries.first()->title().toHtmlEscaped()); + } else { + prompt = QObject::tr("Do you really want to delete %n entry(s) for good?", "", entries.size()); + } + + auto answer = MessageBox::question(parent, + QObject::tr("Delete entry(s)?", "", entries.size()), + prompt, + MessageBox::Delete | MessageBox::Cancel, + MessageBox::Cancel); + + return answer == MessageBox::Delete; + } else if (config()->get(Config::Security_NoConfirmMoveEntryToRecycleBin).toBool()) { + return true; + } else { + QString prompt; + if (entries.size() == 1) { + prompt = QObject::tr("Do you really want to move entry \"%1\" to the recycle bin?") + .arg(entries.first()->title().toHtmlEscaped()); + } else { + prompt = QObject::tr("Do you really want to move %n entry(s) to the recycle bin?", "", entries.size()); + } + + auto answer = MessageBox::question(parent, + QObject::tr("Move entry(s) to recycle bin?", "", entries.size()), + prompt, + MessageBox::Move | MessageBox::Cancel, + MessageBox::Cancel); + + return answer == MessageBox::Move; + } + } + + void deleteEntriesResolveReferences(QWidget* parent, const QList& entries, bool permanent) + { + if (!parent || entries.isEmpty()) { + return; + } + + QList selectedEntries; + // Find references to entries and prompt for direction if necessary + for (auto entry : entries) { + if (permanent) { + auto references = entry->database()->rootGroup()->referencesRecursive(entry); + if (!references.isEmpty()) { + // Ignore references that are part of this cohort + for (auto e : entries) { + references.removeAll(e); + } + // Prompt the user on what to do with the reference (Overwrite, Delete, Skip) + auto result = MessageBox::question( + parent, + QObject::tr("Replace references to entry?"), + QObject::tr( + "Entry \"%1\" has %2 reference(s). " + "Do you want to overwrite references with values, skip this entry, or delete anyway?", + "", + references.size()) + .arg(entry->resolvePlaceholder(entry->title()).toHtmlEscaped()) + .arg(references.size()), + MessageBox::Overwrite | MessageBox::Skip | MessageBox::Delete, + MessageBox::Overwrite); + + if (result == MessageBox::Overwrite) { + for (auto ref : references) { + ref->replaceReferencesWithValues(entry); + } + } else if (result == MessageBox::Skip) { + continue; + } + } + } + // Marked for deletion + selectedEntries << entry; + } + + for (auto entry : asConst(selectedEntries)) { + if (permanent) { + delete entry; + } else { + entry->database()->recycleEntry(entry); + } + } + } +} // namespace GuiTools diff --git a/src/gui/GuiTools.h b/src/gui/GuiTools.h new file mode 100644 index 00000000..14a54ab7 --- /dev/null +++ b/src/gui/GuiTools.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2021 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_GUITOOLS_H +#define KEEPASSXC_GUITOOLS_H + +#include +#include + +class Entry; + +namespace GuiTools +{ + bool confirmDeleteEntries(QWidget* parent, const QList& entries, bool permanent); + void deleteEntriesResolveReferences(QWidget* parent, const QList& entries, bool permanent); +} // namespace GuiTools +#endif // KEEPASSXC_GUITOOLS_H diff --git a/src/gui/reports/ReportsWidgetHealthcheck.cpp b/src/gui/reports/ReportsWidgetHealthcheck.cpp index ad88cdc3..f3b40d73 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.cpp +++ b/src/gui/reports/ReportsWidgetHealthcheck.cpp @@ -22,12 +22,15 @@ #include "core/Database.h" #include "core/Global.h" #include "core/Group.h" +#include "core/Metadata.h" #include "core/PasswordHealth.h" +#include "gui/GuiTools.h" #include "gui/Icons.h" #include "gui/styles/StateColorPalette.h" #include #include +#include #include #include @@ -38,12 +41,12 @@ namespace public: struct Item { - QPointer group; - QPointer entry; + QPointer group; + QPointer entry; QSharedPointer health; bool exclude = false; - Item(const Group* g, const Entry* e, QSharedPointer h) + Item(Group* g, Entry* e, QSharedPointer h) : group(g) , entry(e) , health(h) @@ -102,13 +105,13 @@ Health::Health(QSharedPointer db) : m_db(db) , m_checker(db) { - for (const auto* group : db->rootGroup()->groupsRecursive(true)) { + for (auto group : db->rootGroup()->groupsRecursive(true)) { // Skip recycle bin if (group->isRecycled()) { continue; } - for (const auto* entry : group->entries()) { + for (auto entry : group->entries()) { if (entry->isRecycled()) { continue; } @@ -147,16 +150,15 @@ ReportsWidgetHealthcheck::ReportsWidgetHealthcheck(QWidget* parent) m_modelProxy->setSourceModel(m_referencesModel.data()); m_modelProxy->setSortLocaleAware(true); m_ui->healthcheckTableView->setModel(m_modelProxy.data()); - m_ui->healthcheckTableView->setSelectionMode(QAbstractItemView::NoSelection); m_ui->healthcheckTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); m_ui->healthcheckTableView->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); - m_ui->healthcheckTableView->setSortingEnabled(true); - m_ui->healthcheckTableView->setWordWrap(true); connect(m_ui->healthcheckTableView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(customMenuRequested(QPoint))); connect(m_ui->healthcheckTableView, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex))); connect(m_ui->showKnownBadCheckBox, SIGNAL(stateChanged(int)), this, SLOT(calculateHealth())); connect(m_ui->excludeExpired, SIGNAL(stateChanged(int)), this, SLOT(calculateHealth())); + + new QShortcut(Qt::Key_Delete, this, SLOT(deleteSelectedEntries())); } ReportsWidgetHealthcheck::~ReportsWidgetHealthcheck() @@ -164,8 +166,8 @@ ReportsWidgetHealthcheck::~ReportsWidgetHealthcheck() } void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer health, - const Group* group, - const Entry* entry, + Group* group, + Entry* entry, bool knownBad) { QString descr, tip; @@ -312,50 +314,82 @@ void ReportsWidgetHealthcheck::emitEntryActivated(const QModelIndex& index) void ReportsWidgetHealthcheck::customMenuRequested(QPoint pos) { - - // Find which entry has been clicked - const auto index = m_ui->healthcheckTableView->indexAt(pos); - if (!index.isValid()) { - return; - } - auto mappedIndex = m_modelProxy->mapToSource(index); - m_contextmenuEntry = const_cast(m_rowToEntry[mappedIndex.row()].second); - if (!m_contextmenuEntry) { + auto selected = m_ui->healthcheckTableView->selectionModel()->selectedRows(); + if (selected.isEmpty()) { return; } // Create the context menu const auto menu = new QMenu(this); - // Create the "edit entry" menu item - const auto edit = new QAction(icons()->icon("entry-edit"), tr("Edit Entry…"), this); - menu->addAction(edit); - connect(edit, SIGNAL(triggered()), SLOT(editFromContextmenu())); + // Create the "edit entry" menu item (only if 1 row is selected) + if (selected.size() == 1) { + const auto edit = new QAction(icons()->icon("entry-edit"), tr("Edit Entry…"), this); + menu->addAction(edit); + connect(edit, &QAction::triggered, edit, [this, selected] { + auto row = m_modelProxy->mapToSource(selected[0]).row(); + auto entry = m_rowToEntry[row].second; + emit entryActivated(entry); + }); + } + + // Create the "delete entry" menu item + const auto delEntry = new QAction(icons()->icon("entry-delete"), tr("Delete Entry(s)…", "", selected.size()), this); + menu->addAction(delEntry); + connect(delEntry, &QAction::triggered, this, &ReportsWidgetHealthcheck::deleteSelectedEntries); // Create the "exclude from reports" menu item const auto exclude = new QAction(icons()->icon("reports-exclude"), tr("Exclude from reports"), this); - exclude->setCheckable(true); - exclude->setChecked(m_contextmenuEntry->excludeFromReports()); - menu->addAction(exclude); - connect(exclude, &QAction::toggled, exclude, [this](bool state) { - if (m_contextmenuEntry) { - m_contextmenuEntry->setExcludeFromReports(state); - calculateHealth(); + + bool isExcluded = false; + for (auto index : selected) { + auto row = m_modelProxy->mapToSource(index).row(); + auto entry = m_rowToEntry[row].second; + if (entry && entry->excludeFromReports()) { + // If at least one entry is excluded switch to inclusion + isExcluded = true; + break; } + } + exclude->setCheckable(true); + exclude->setChecked(isExcluded); + + menu->addAction(exclude); + connect(exclude, &QAction::toggled, exclude, [this, selected](bool state) { + for (auto index : selected) { + auto row = m_modelProxy->mapToSource(index).row(); + auto entry = m_rowToEntry[row].second; + if (entry) { + entry->setExcludeFromReports(state); + } + } + calculateHealth(); }); // Show the context menu menu->popup(m_ui->healthcheckTableView->viewport()->mapToGlobal(pos)); } -void ReportsWidgetHealthcheck::editFromContextmenu() -{ - if (m_contextmenuEntry) { - emit entryActivated(m_contextmenuEntry); - } -} - void ReportsWidgetHealthcheck::saveSettings() { // nothing to do - the tab is passive } + +void ReportsWidgetHealthcheck::deleteSelectedEntries() +{ + QList selectedEntries; + for (auto index : m_ui->healthcheckTableView->selectionModel()->selectedRows()) { + auto row = m_modelProxy->mapToSource(index).row(); + auto entry = m_rowToEntry[row].second; + if (entry) { + selectedEntries << entry; + } + } + + bool permanent = !m_db->metadata()->recycleBinEnabled(); + if (GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) { + GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent); + } + + calculateHealth(); +} diff --git a/src/gui/reports/ReportsWidgetHealthcheck.h b/src/gui/reports/ReportsWidgetHealthcheck.h index 51030174..cf83af26 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.h +++ b/src/gui/reports/ReportsWidgetHealthcheck.h @@ -56,10 +56,10 @@ public slots: void calculateHealth(); void emitEntryActivated(const QModelIndex& index); void customMenuRequested(QPoint); - void editFromContextmenu(); + void deleteSelectedEntries(); private: - void addHealthRow(QSharedPointer, const Group*, const Entry*, bool knownBad); + void addHealthRow(QSharedPointer, Group*, Entry*, bool knownBad); QScopedPointer m_ui; @@ -67,8 +67,7 @@ private: QScopedPointer m_referencesModel; QScopedPointer m_modelProxy; QSharedPointer m_db; - QList> m_rowToEntry; - Entry* m_contextmenuEntry = nullptr; + QList> m_rowToEntry; }; #endif // KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H diff --git a/src/gui/reports/ReportsWidgetHealthcheck.ui b/src/gui/reports/ReportsWidgetHealthcheck.ui index b9f27c4c..e2ed44e1 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.ui +++ b/src/gui/reports/ReportsWidgetHealthcheck.ui @@ -10,7 +10,7 @@ 379 - + 0 @@ -37,11 +37,14 @@ true + + QAbstractItemView::SelectRows + Qt::ElideMiddle - false + true true diff --git a/src/gui/reports/ReportsWidgetHibp.cpp b/src/gui/reports/ReportsWidgetHibp.cpp index 5f1a2882..b15a4ccd 100644 --- a/src/gui/reports/ReportsWidgetHibp.cpp +++ b/src/gui/reports/ReportsWidgetHibp.cpp @@ -22,11 +22,14 @@ #include "core/Database.h" #include "core/Global.h" #include "core/Group.h" +#include "core/Metadata.h" #include "core/PasswordHealth.h" +#include "gui/GuiTools.h" #include "gui/Icons.h" #include "gui/MessageBox.h" #include +#include #include #include @@ -64,10 +67,8 @@ ReportsWidgetHibp::ReportsWidgetHibp(QWidget* parent) m_modelProxy->setSourceModel(m_referencesModel.data()); m_modelProxy->setSortLocaleAware(true); m_ui->hibpTableView->setModel(m_modelProxy.data()); - m_ui->hibpTableView->setSelectionMode(QAbstractItemView::NoSelection); m_ui->hibpTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); m_ui->hibpTableView->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); - m_ui->hibpTableView->setSortingEnabled(true); connect(m_ui->hibpTableView, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex))); connect(m_ui->hibpTableView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(customMenuRequested(QPoint))); @@ -78,6 +79,8 @@ ReportsWidgetHibp::ReportsWidgetHibp(QWidget* parent) connect(m_ui->validationButton, &QPushButton::pressed, [this] { startValidation(); }); #endif + + new QShortcut(Qt::Key_Delete, this, SLOT(deleteSelectedEntries())); } ReportsWidgetHibp::~ReportsWidgetHibp() @@ -124,8 +127,8 @@ void ReportsWidgetHibp::makeHibpTable() m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Title") << tr("Path") << tr("Password exposed…")); // Search database for passwords that we've found so far - QList> items; - for (const auto* entry : m_db->rootGroup()->entriesRecursive()) { + QList> items; + for (auto entry : m_db->rootGroup()->entriesRecursive()) { if (!entry->isRecycled()) { const auto found = m_pwndPasswords.find(entry->password()); if (found != m_pwndPasswords.end()) { @@ -135,7 +138,7 @@ void ReportsWidgetHibp::makeHibpTable() } // Sort decending by the number the password has been exposed - qSort(items.begin(), items.end(), [](QPair& lhs, QPair& rhs) { + qSort(items.begin(), items.end(), [](QPair& lhs, QPair& rhs) { return lhs.second > rhs.second; }); @@ -356,47 +359,79 @@ void ReportsWidgetHibp::refreshAfterEdit() void ReportsWidgetHibp::customMenuRequested(QPoint pos) { - - // Find which entry has been clicked - const auto index = m_ui->hibpTableView->indexAt(pos); - if (!index.isValid()) { - return; - } - auto mappedIndex = m_modelProxy->mapToSource(index); - m_contextmenuEntry = const_cast(m_rowToEntry[mappedIndex.row()]); - if (!m_contextmenuEntry) { + auto selected = m_ui->hibpTableView->selectionModel()->selectedRows(); + if (selected.isEmpty()) { return; } // Create the context menu const auto menu = new QMenu(this); - // Create the "edit entry" menu item - const auto edit = new QAction(icons()->icon("entry-edit"), tr("Edit Entry…"), this); - menu->addAction(edit); - connect(edit, SIGNAL(triggered()), SLOT(editFromContextmenu())); + // Create the "edit entry" menu item if 1 row is selected + if (selected.size() == 1) { + const auto edit = new QAction(icons()->icon("entry-edit"), tr("Edit Entry…"), this); + menu->addAction(edit); + connect(edit, &QAction::triggered, edit, [this, selected] { + auto row = m_modelProxy->mapToSource(selected[0]).row(); + auto entry = m_rowToEntry[row]; + emit entryActivated(entry); + }); + } + + // Create the "delete entry" menu item + const auto delEntry = new QAction(icons()->icon("entry-delete"), tr("Delete Entry(s)…", "", selected.size()), this); + menu->addAction(delEntry); + connect(delEntry, &QAction::triggered, this, &ReportsWidgetHibp::deleteSelectedEntries); // Create the "exclude from reports" menu item const auto exclude = new QAction(icons()->icon("reports-exclude"), tr("Exclude from reports"), this); - exclude->setCheckable(true); - exclude->setChecked(m_contextmenuEntry->excludeFromReports()); - menu->addAction(exclude); - connect(exclude, &QAction::toggled, exclude, [this](bool state) { - if (m_contextmenuEntry) { - m_contextmenuEntry->setExcludeFromReports(state); - makeHibpTable(); + + bool isExcluded = false; + for (auto index : selected) { + auto row = m_modelProxy->mapToSource(index).row(); + auto entry = m_rowToEntry[row]; + if (entry && entry->excludeFromReports()) { + // If at least one entry is excluded switch to inclusion + isExcluded = true; + break; } + } + exclude->setCheckable(true); + exclude->setChecked(isExcluded); + + menu->addAction(exclude); + connect(exclude, &QAction::toggled, exclude, [this, selected](bool state) { + for (auto index : selected) { + auto row = m_modelProxy->mapToSource(index).row(); + auto entry = m_rowToEntry[row]; + if (entry) { + entry->setExcludeFromReports(state); + } + } + makeHibpTable(); }); // Show the context menu menu->popup(m_ui->hibpTableView->viewport()->mapToGlobal(pos)); } -void ReportsWidgetHibp::editFromContextmenu() +void ReportsWidgetHibp::deleteSelectedEntries() { - if (m_contextmenuEntry) { - emit entryActivated(m_contextmenuEntry); + QList selectedEntries; + for (auto index : m_ui->hibpTableView->selectionModel()->selectedRows()) { + auto row = m_modelProxy->mapToSource(index).row(); + auto entry = m_rowToEntry[row]; + if (entry) { + selectedEntries << entry; + } } + + bool permanent = !m_db->metadata()->recycleBinEnabled(); + if (GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) { + GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent); + } + + makeHibpTable(); } void ReportsWidgetHibp::saveSettings() diff --git a/src/gui/reports/ReportsWidgetHibp.h b/src/gui/reports/ReportsWidgetHibp.h index 5907cb6d..bccd5978 100644 --- a/src/gui/reports/ReportsWidgetHibp.h +++ b/src/gui/reports/ReportsWidgetHibp.h @@ -61,7 +61,7 @@ public slots: void fetchFailed(const QString& error); void makeHibpTable(); void customMenuRequested(QPoint); - void editFromContextmenu(); + void deleteSelectedEntries(); private: void startValidation(); @@ -74,11 +74,10 @@ private: QMap m_pwndPasswords; // Passwords we found to have been pwned (value is pwn count) QString m_error; // Error message if download failed, else empty - QList m_rowToEntry; // List index is table row - QPointer m_editedEntry; // The entry we're currently editing + QList m_rowToEntry; // List index is table row + QPointer m_editedEntry; // The entry we're currently editing QString m_editedPassword; // The old password of the entry we're editing bool m_editedExcluded; // The old "known bad" flag of the entry we're editing - Entry* m_contextmenuEntry = nullptr; // The entry that was right-clicked #ifdef WITH_XC_NETWORKING HibpDownloader m_downloader; // This performs the actual HIBP online query diff --git a/src/gui/reports/ReportsWidgetHibp.ui b/src/gui/reports/ReportsWidgetHibp.ui index 94582e8c..b5f0a489 100644 --- a/src/gui/reports/ReportsWidgetHibp.ui +++ b/src/gui/reports/ReportsWidgetHibp.ui @@ -151,11 +151,14 @@ true + + QAbstractItemView::SelectRows + Qt::ElideMiddle - false + true true