From 3b55b5186511394710e59880458d883d6b8da59c Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Sun, 21 Dec 2025 22:01:03 -0800 Subject: [PATCH 01/29] Add option to exclude Group from reports --- src/core/Group.cpp | 14 ++++++++++++++ src/core/Group.h | 3 +++ 2 files changed, 17 insertions(+) diff --git a/src/core/Group.cpp b/src/core/Group.cpp index 8b6cd75c5..03ff7e7ae 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -46,6 +46,7 @@ Group::Group() m_data.autoTypeEnabled = Inherit; m_data.searchingEnabled = Inherit; m_data.mergeMode = Default; + m_data.excludeFromReports = false; connect(m_customData, &CustomData::modified, this, &Group::modified); connect(this, &Group::modified, this, &Group::updateTimeinfo); @@ -1281,6 +1282,16 @@ void Group::setPreviousParentGroup(const Group* group) setPreviousParentGroupUuid(group ? group->uuid() : QUuid()); } +void Group::setExcludeFromReports(bool excluded) +{ + set(m_data.excludeFromReports, excluded); +} + +bool Group::excludeFromReports() const +{ + return m_data.excludeFromReports; +} + bool Group::GroupData::operator==(const Group::GroupData& other) const { return equals(other, CompareItemDefault); @@ -1327,5 +1338,8 @@ bool Group::GroupData::equals(const Group::GroupData& other, CompareItemOptions if (::compare(mergeMode, other.mergeMode, options) != 0) { return false; } + if (::compare(excludeFromReports, other.excludeFromReports, options) != 0) { + return false; + } return true; } diff --git a/src/core/Group.h b/src/core/Group.h index 01c0b2120..2b539985b 100644 --- a/src/core/Group.h +++ b/src/core/Group.h @@ -68,6 +68,7 @@ public: Group::MergeMode mergeMode; QString tags; QUuid previousParentGroupUuid; + bool excludeFromReports; bool operator==(const GroupData& other) const; bool operator!=(const GroupData& other) const; @@ -107,6 +108,7 @@ public: QString resolveCustomDataString(const QString& key, bool checkParent = true) const; const Group* previousParentGroup() const; QUuid previousParentGroupUuid() const; + bool excludeFromReports() const; bool equals(const Group* other, CompareItemOptions options) const; @@ -140,6 +142,7 @@ public: void setMergeMode(MergeMode newMode); void setPreviousParentGroup(const Group* group); void setPreviousParentGroupUuid(const QUuid& uuid); + void setExcludeFromReports(bool exclude); bool canUpdateTimeinfo() const; void setUpdateTimeinfo(bool value); From e23aee1f67b1a9599f712c29a956a26260b0c439 Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Mon, 22 Dec 2025 16:41:30 -0800 Subject: [PATCH 02/29] Move grid into vertical layout and add checkbox When I tried adding a checkbox, the label for it was too big and it pushed everything to the right. Adding a vertical layout, moving the grid into it, and putting the checkbox into the horizontal layout fixes this issue. --- src/gui/group/EditGroupWidgetMain.ui | 308 +++++++++++++-------------- 1 file changed, 151 insertions(+), 157 deletions(-) diff --git a/src/gui/group/EditGroupWidgetMain.ui b/src/gui/group/EditGroupWidgetMain.ui index faa8a30ff..a98e735fd 100644 --- a/src/gui/group/EditGroupWidgetMain.ui +++ b/src/gui/group/EditGroupWidgetMain.ui @@ -37,7 +37,7 @@ 523 - + 0 @@ -50,180 +50,185 @@ 0 - - 10 - - - 8 - - - - - Toggle expiration - - - Expires: - - - - - - - Name field - - - - - - - false - - - Expiration field - - - true - - - - - - - Use default Auto-Type sequence of parent group - - - - - - - Auto-Type: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Search: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Auto-Type toggle for this and sub groups - - - - - - - + + + + - Notes: + Search: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - Qt::Vertical + + + + Use default Auto-Type sequence of parent group - + + + + + + Auto-Type toggle for this and sub groups + + + + + + + + 0 + 0 + + + - 20 - 0 + 16777215 + 120 - + + Notes field + + - - - - - - - - Qt::Horizontal + + + + Toggle expiration - - QSizePolicy::Fixed + + Expires: - - - 30 - 0 - - - + - - + + + + Search toggle for this and sub groups + + + + + false - Default auto-type sequence field + Expiration field - - + + true + + + + + + + Name: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Set default Auto-Type sequence + + + + + + + Name field + + + + + + + + + Notes: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 30 + 0 + + + + + + + + false + + + Default auto-type sequence field + + + + + + + + + + + + Auto-Type: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - 0 - 0 - - - - - 16777215 - 120 - - - - Notes field - - - - - + + - Name: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Exclude from database reports - - - - Set default Auto-Type sequence - - - - - - - Search toggle for this and sub groups - - - - + Qt::Vertical @@ -239,17 +244,6 @@ - - editName - editNotes - expireCheck - expireDatePicker - searchComboBox - autotypeComboBox - autoTypeSequenceInherit - autoTypeSequenceCustomRadio - autoTypeSequenceCustomEdit - From c75a6887d3c231469a133891f072d7e02836e482 Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Mon, 22 Dec 2025 17:10:07 -0800 Subject: [PATCH 03/29] Connect signals and slots --- src/gui/group/EditGroupWidget.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/gui/group/EditGroupWidget.cpp b/src/gui/group/EditGroupWidget.cpp index 4e406b0d0..77c51e777 100644 --- a/src/gui/group/EditGroupWidget.cpp +++ b/src/gui/group/EditGroupWidget.cpp @@ -126,6 +126,7 @@ void EditGroupWidget::setupModifiedTracking() connect(m_mainUi->autoTypeSequenceInherit, SIGNAL(toggled(bool)), SLOT(setModified())); connect(m_mainUi->autoTypeSequenceCustomRadio, SIGNAL(toggled(bool)), SLOT(setModified())); connect(m_mainUi->autoTypeSequenceCustomEdit, SIGNAL(textChanged(QString)), SLOT(setModified())); + connect(m_mainUi->excludeReportsCheckBox, SIGNAL(stateChanged(int)), SLOT(setModified())); // Icon tab connect(m_editGroupWidgetIcons, SIGNAL(widgetUpdated()), SLOT(setModified())); @@ -171,6 +172,7 @@ void EditGroupWidget::loadGroup(Group* group, bool create, const QSharedPointer< m_mainUi->autoTypeSequenceCustomRadio->setChecked(true); } m_mainUi->autoTypeSequenceCustomEdit->setText(group->effectiveAutoTypeSequence()); + m_mainUi->excludeReportsCheckBox->setChecked(m_group->excludeFromReports()); if (config()->get(Config::GUI_MonospaceNotes).toBool()) { m_mainUi->editNotes->setFont(Font::fixedFont()); @@ -265,6 +267,7 @@ void EditGroupWidget::apply() m_temporaryGroup->setSearchingEnabled(triStateFromIndex(m_mainUi->searchComboBox->currentIndex())); m_temporaryGroup->setAutoTypeEnabled(triStateFromIndex(m_mainUi->autotypeComboBox->currentIndex())); + m_temporaryGroup->setExcludeFromReports(m_mainUi->excludeReportsCheckBox->isChecked()); if (m_mainUi->autoTypeSequenceInherit->isChecked()) { m_temporaryGroup->setDefaultAutoTypeSequence(QString()); From 316ea082b9308c3d32ab2aa099ad1b3cf2a6d11f Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Mon, 22 Dec 2025 20:30:07 -0800 Subject: [PATCH 04/29] Use `CustomData` for storing excluded groups This isn't part of the kdbx format so we can't write it there directly. --- src/core/Group.cpp | 9 +++------ src/core/Group.h | 1 - 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/core/Group.cpp b/src/core/Group.cpp index 03ff7e7ae..422c4bbb1 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -46,7 +46,6 @@ Group::Group() m_data.autoTypeEnabled = Inherit; m_data.searchingEnabled = Inherit; m_data.mergeMode = Default; - m_data.excludeFromReports = false; connect(m_customData, &CustomData::modified, this, &Group::modified); connect(this, &Group::modified, this, &Group::updateTimeinfo); @@ -1284,12 +1283,13 @@ void Group::setPreviousParentGroup(const Group* group) void Group::setExcludeFromReports(bool excluded) { - set(m_data.excludeFromReports, excluded); + customData()->set(CustomData::ExcludeFromReportsLegacy, excluded ? TRUE_STR : FALSE_STR); } bool Group::excludeFromReports() const { - return m_data.excludeFromReports; + return customData()->contains(CustomData::ExcludeFromReportsLegacy) + && customData()->value(CustomData::ExcludeFromReportsLegacy) == TRUE_STR; } bool Group::GroupData::operator==(const Group::GroupData& other) const @@ -1338,8 +1338,5 @@ bool Group::GroupData::equals(const Group::GroupData& other, CompareItemOptions if (::compare(mergeMode, other.mergeMode, options) != 0) { return false; } - if (::compare(excludeFromReports, other.excludeFromReports, options) != 0) { - return false; - } return true; } diff --git a/src/core/Group.h b/src/core/Group.h index 2b539985b..0fb633035 100644 --- a/src/core/Group.h +++ b/src/core/Group.h @@ -68,7 +68,6 @@ public: Group::MergeMode mergeMode; QString tags; QUuid previousParentGroupUuid; - bool excludeFromReports; bool operator==(const GroupData& other) const; bool operator!=(const GroupData& other) const; From 79ad4a6ae970e78d7e651084b056e637186513d8 Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Mon, 22 Dec 2025 20:30:49 -0800 Subject: [PATCH 05/29] Exclude groups from database reports --- src/gui/reports/ReportsWidgetBrowserStatistics.cpp | 2 +- src/gui/reports/ReportsWidgetHealthcheck.cpp | 2 +- src/gui/reports/ReportsWidgetHibp.cpp | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp index 63267d77f..22f58e86e 100644 --- a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp +++ b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp @@ -50,7 +50,7 @@ namespace , entry(e) , hasUrls(hU) , hasSettings(hS) - , exclude(e->excludeFromReports()) + , exclude(e->excludeFromReports() || g->excludeFromReports()) { } }; diff --git a/src/gui/reports/ReportsWidgetHealthcheck.cpp b/src/gui/reports/ReportsWidgetHealthcheck.cpp index 1c34c2f36..e50b5418f 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.cpp +++ b/src/gui/reports/ReportsWidgetHealthcheck.cpp @@ -47,7 +47,7 @@ namespace : group(g) , entry(e) , health(h) - , exclude(e->excludeFromReports()) + , exclude(e->excludeFromReports() || g->excludeFromReports()) { } diff --git a/src/gui/reports/ReportsWidgetHibp.cpp b/src/gui/reports/ReportsWidgetHibp.cpp index a559208aa..0d6860009 100644 --- a/src/gui/reports/ReportsWidgetHibp.cpp +++ b/src/gui/reports/ReportsWidgetHibp.cpp @@ -153,7 +153,7 @@ void ReportsWidgetHibp::makeHibpTable() auto title = entry->title(); // Hide entry if excluded unless explicitly requested - if (entry->excludeFromReports()) { + if (entry->excludeFromReports() || entry->group()->excludeFromReports()) { anyExcluded = true; if (!showExcluded) { continue; @@ -171,6 +171,10 @@ void ReportsWidgetHibp::makeHibpTable() row[1]->setToolTip(tr("This entry is being excluded from reports")); } + if(entry->group()->excludeFromReports()) { + row[1]->setToolTip(tr("The group for this entry is being excluded from reports")); + } + row[2]->setForeground(red); row[2]->setData(count, Qt::UserRole); m_referencesModel->appendRow(row); From b1fa002beff0026d7d82e1c6cecd27c1116cbe1c Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Mon, 22 Dec 2025 20:51:21 -0800 Subject: [PATCH 06/29] Add tooltips and info for group exclusions --- src/core/DatabaseStats.cpp | 6 +++++- src/core/DatabaseStats.h | 1 + src/gui/reports/ReportsWidgetBrowserStatistics.cpp | 6 +++++- src/gui/reports/ReportsWidgetHealthcheck.cpp | 7 ++++++- src/gui/reports/ReportsWidgetHibp.cpp | 4 +--- src/gui/reports/ReportsWidgetStatistics.cpp | 5 +++++ 6 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/core/DatabaseStats.cpp b/src/core/DatabaseStats.cpp index cf2364b08..fbe985cf6 100644 --- a/src/core/DatabaseStats.cpp +++ b/src/core/DatabaseStats.cpp @@ -76,6 +76,10 @@ void DatabaseStats::gatherStats(const QList& groups) ++groupCount; + if (group->excludeFromReports()) { + ++excludedGroups; + } + for (const auto* entry : group->entries()) { // Don't count anything in the recycle bin if (entry->isRecycled()) { @@ -107,7 +111,7 @@ void DatabaseStats::gatherStats(const QList& groups) ++weakPasswords; } - if (entry->excludeFromReports()) { + if (entry->excludeFromReports() || group->excludeFromReports()) { ++excludedEntries; } diff --git a/src/core/DatabaseStats.h b/src/core/DatabaseStats.h index 2c0ad7c76..d6446e834 100644 --- a/src/core/DatabaseStats.h +++ b/src/core/DatabaseStats.h @@ -30,6 +30,7 @@ public: int entryCount = 0; // Number of entries (across all groups) int expiredEntries = 0; // Number of expired entries int excludedEntries = 0; // Number of known bad entries + int excludedGroups = 0; // Number of excluded groups from reports int weakPasswords = 0; // Number of weak or poor passwords int shortPasswords = 0; // Number of passwords 8 characters or less in size int uniquePasswords = 0; // Number of unique passwords diff --git a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp index 22f58e86e..8feedf670 100644 --- a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp +++ b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp @@ -159,7 +159,11 @@ void ReportsWidgetBrowserStatistics::addStatisticsRow(bool hasUrls, row[3]->setToolTip(allowedUrlsToolTip); row[4]->setToolTip(deniedUrlsToolTip); if (excluded) { - row[0]->setToolTip(tr("This entry is being excluded from reports")); + if (group->excludeFromReports()) { + row[0]->setToolTip(tr("The group for this entry is being excluded from reports")); + } else { + row[0]->setToolTip(tr("This entry is being excluded from reports")); + } } // Store entry pointer per table row (used in double click handler) diff --git a/src/gui/reports/ReportsWidgetHealthcheck.cpp b/src/gui/reports/ReportsWidgetHealthcheck.cpp index e50b5418f..a6062aee2 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.cpp +++ b/src/gui/reports/ReportsWidgetHealthcheck.cpp @@ -219,7 +219,12 @@ void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer healt // Set tooltips row[0]->setToolTip(tip); if (excluded) { - row[1]->setToolTip(tr("This entry is being excluded from reports")); + if (group->excludeFromReports()) { + row[1]->setToolTip(tr("The group for this entry is being excluded from reports")); + } + else { + row[1]->setToolTip(tr("This entry is being excluded from reports")); + } } row[4]->setToolTip(health->scoreDetails()); diff --git a/src/gui/reports/ReportsWidgetHibp.cpp b/src/gui/reports/ReportsWidgetHibp.cpp index 0d6860009..04ac4dab3 100644 --- a/src/gui/reports/ReportsWidgetHibp.cpp +++ b/src/gui/reports/ReportsWidgetHibp.cpp @@ -169,9 +169,7 @@ void ReportsWidgetHibp::makeHibpTable() if (entry->excludeFromReports()) { row[1]->setToolTip(tr("This entry is being excluded from reports")); - } - - if(entry->group()->excludeFromReports()) { + } else if(entry->group()->excludeFromReports()) { row[1]->setToolTip(tr("The group for this entry is being excluded from reports")); } diff --git a/src/gui/reports/ReportsWidgetStatistics.cpp b/src/gui/reports/ReportsWidgetStatistics.cpp index 0aced6ae5..276389dc3 100644 --- a/src/gui/reports/ReportsWidgetStatistics.cpp +++ b/src/gui/reports/ReportsWidgetStatistics.cpp @@ -121,6 +121,11 @@ void ReportsWidgetStatistics::calculateStats() stats->excludedEntries > 0, tr("Excluding entries from reports, e. g. because they are known to have a poor password, isn't " "necessarily a problem but you should keep an eye on them.")); + addStatsRow(tr("Groups excluded from reports"), + QString::number(stats->excludedGroups), + stats->excludedGroups > 0, + tr("Excluding entire groups from reports isn't necessarily a problem but please exercise caution" + "when excluding entire groups.")); addStatsRow(tr("Average password length"), tr("%1 character(s)", "", stats->averagePwdLength()).arg(stats->averagePwdLength()), stats->isAvgPwdTooShort(), From 67e81fe58d71a9539a6bf835f80870d14e928958 Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Mon, 22 Dec 2025 22:29:48 -0800 Subject: [PATCH 07/29] Start working on including entries from excluded groups --- src/core/Group.cpp | 16 ++++ src/core/Group.h | 1 + .../ReportsWidgetBrowserStatistics.cpp | 30 ++++++- src/gui/reports/ReportsWidgetHealthcheck.cpp | 81 +++++++++++++++++-- 4 files changed, 117 insertions(+), 11 deletions(-) diff --git a/src/core/Group.cpp b/src/core/Group.cpp index 422c4bbb1..7b93dee21 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -1284,6 +1284,22 @@ void Group::setPreviousParentGroup(const Group* group) void Group::setExcludeFromReports(bool excluded) { customData()->set(CustomData::ExcludeFromReportsLegacy, excluded ? TRUE_STR : FALSE_STR); + + // Clear out the exclusion flag on entries when we set it on the + // group because it'll make it easier to individually set it for an + // entry later on + if (excluded) { + for (auto &entry : m_entries) { + entry->setExcludeFromReports(false); + } + } +} + +void Group::markAllEntriesExcludedFromReports() +{ + for (auto &entry : m_entries) { + entry->setExcludeFromReports(true); + } } bool Group::excludeFromReports() const diff --git a/src/core/Group.h b/src/core/Group.h index 0fb633035..b6cfbfe71 100644 --- a/src/core/Group.h +++ b/src/core/Group.h @@ -142,6 +142,7 @@ public: void setPreviousParentGroup(const Group* group); void setPreviousParentGroupUuid(const QUuid& uuid); void setExcludeFromReports(bool exclude); + void markAllEntriesExcludedFromReports(); bool canUpdateTimeinfo() const; void setUpdateTimeinfo(bool value); diff --git a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp index 8feedf670..e5515299c 100644 --- a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp +++ b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp @@ -300,21 +300,33 @@ void ReportsWidgetBrowserStatistics::customMenuRequested(QPoint pos) &ReportsWidgetBrowserStatistics::deletePluginDataFromSelectedEntries); // Create the "exclude from reports" menu item - const auto exclude = new QAction(icons()->icon("reports-exclude"), tr("Exclude from reports"), this); + const auto exclude = new QAction(icons()->icon("reports-exclude"), tr("Exclude Entry(s) from reports"), this); + const auto excludeGroups = new QAction(icons()->icon("reports-exclude"), tr("Exclude Group(s) from reports"), this); bool isExcluded = false; + bool isGroupExcluded = false; + for (auto index : selected) { auto row = m_modelProxy->mapToSource(index).row(); auto entry = m_rowToEntry[row].second; - if (entry && entry->excludeFromReports()) { + if (entry) { // If at least one entry is excluded switch to inclusion - isExcluded = true; + if (entry->excludeFromReports()) { + isExcluded = true; + } + if (entry->group()->excludeFromReports()) { + isGroupExcluded = true; + } + break; } } exclude->setCheckable(true); exclude->setChecked(isExcluded); + excludeGroups->setCheckable(true); + exclude->setChecked(isGroupExcluded); + menu->addAction(exclude); connect(exclude, &QAction::toggled, exclude, [this, selected](bool state) { for (auto index : selected) { @@ -327,6 +339,18 @@ void ReportsWidgetBrowserStatistics::customMenuRequested(QPoint pos) calculateBrowserStatistics(); }); + menu->addAction(excludeGroups); + connect(excludeGroups, &QAction::toggled, excludeGroups, [this, selected](bool state) { + for (const auto index : selected) { + auto row = m_modelProxy->mapToSource(index).row(); + auto entry = m_rowToEntry[row].second; + if (entry) { + entry->group()->setExcludeFromReports(state); + } + } + calculateBrowserStatistics(); + }); + // Show the context menu menu->popup(m_ui->browserStatisticsTableView->viewport()->mapToGlobal(pos)); } diff --git a/src/gui/reports/ReportsWidgetHealthcheck.cpp b/src/gui/reports/ReportsWidgetHealthcheck.cpp index a6062aee2..1e87df6de 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.cpp +++ b/src/gui/reports/ReportsWidgetHealthcheck.cpp @@ -25,6 +25,7 @@ #include "gui/GuiTools.h" #include "gui/Icons.h" #include "gui/styles/StateColorPalette.h" +#include "gui/MessageBox.h" #include #include @@ -348,28 +349,92 @@ void ReportsWidgetHealthcheck::customMenuRequested(QPoint pos) 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); + const auto excludeAction = new QAction(icons()->icon("reports-exclude"), tr("Exclude Entry(s) from reports"), this); + const auto excludeGroupsAction = new QAction(icons()->icon("reports-exclude"), tr("Exclude Group(s) from reports"), this); bool isExcluded = false; + bool isGroupExcluded = false; + for (auto index : selected) { auto row = m_modelProxy->mapToSource(index).row(); auto entry = m_rowToEntry[row].second; - if (entry && entry->excludeFromReports()) { + if (entry) { // If at least one entry is excluded switch to inclusion - isExcluded = true; + if (entry->excludeFromReports() || entry->group()->excludeFromReports()) { + isExcluded = true; + } + if (entry->group()->excludeFromReports()) { + isGroupExcluded = true; + } + break; } } - exclude->setCheckable(true); - exclude->setChecked(isExcluded); + excludeAction->setCheckable(true); + excludeAction->setChecked(isExcluded); + + excludeGroupsAction->setCheckable(true); + excludeGroupsAction->setChecked(isGroupExcluded); + + menu->addAction(excludeAction); + connect(excludeAction, &QAction::toggled, excludeAction, [this, selected](bool checked) { + QSet groups; + + // If we are including entries (checked is false) but a group is excluded, ask the user if they + // would like to include the rest of the group as well (or keep it excluded). + // If they exclude it, we need to include the whole group, and then exclude + // the entries that aren't selected here. + if (!checked) { + for(const auto index : selected) { + auto row = m_modelProxy->mapToSource(index).row(); + auto entry = m_rowToEntry[row].second; + + if (entry) { + auto *group = entry->group(); + if (group->excludeFromReports() && !groups.contains(group)) { + QString msg = tr("The Group for \"%1\" is excluded. Would you like to include all Entries from there as well?").arg(entry->title()); + auto response = MessageBox::question(this, tr("Include Group?"), msg, MessageBox::Yes | MessageBox::No | MessageBox::Cancel, MessageBox::No); + + if (response == MessageBox::Cancel) { + return; + } + else if (response == MessageBox::Yes) { + group->setExcludeFromReports(false); + } + else if (response == MessageBox::No) { + // We'll exclude all entries from the group here and then + // include the selected ones below + group->setExcludeFromReports(false); + group->markAllEntriesExcludedFromReports(); + } + + groups.insert(group); + } + } + } + } - 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 the containing group is excluded but the user wants to include + // this entry, ask if they want to keep the remaining items in the group + // excluded or included if (entry) { - entry->setExcludeFromReports(state); + entry->setExcludeFromReports(checked); + } + } + calculateHealth(); + }); + + menu->addAction(excludeGroupsAction); + connect(excludeGroupsAction, &QAction::toggled, excludeGroupsAction, [this, selected](bool checked) { + for (const auto index : selected) { + auto row = m_modelProxy->mapToSource(index).row(); + auto entry = m_rowToEntry[row].second; + if (entry) { + entry->group()->setExcludeFromReports(checked); } } calculateHealth(); From c1210d77076833409972dc90de3dd9854097ecf6 Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Mon, 22 Dec 2025 23:24:10 -0800 Subject: [PATCH 08/29] Add exclusion support for browser statistics --- .../ReportsWidgetBrowserStatistics.cpp | 63 +++++++++++++++---- 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp index e5515299c..917d6c867 100644 --- a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp +++ b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp @@ -25,6 +25,7 @@ #include "gui/GuiTools.h" #include "gui/Icons.h" #include "gui/styles/StateColorPalette.h" +#include "gui/MessageBox.h" #include #include @@ -300,8 +301,8 @@ void ReportsWidgetBrowserStatistics::customMenuRequested(QPoint pos) &ReportsWidgetBrowserStatistics::deletePluginDataFromSelectedEntries); // Create the "exclude from reports" menu item - const auto exclude = new QAction(icons()->icon("reports-exclude"), tr("Exclude Entry(s) from reports"), this); - const auto excludeGroups = new QAction(icons()->icon("reports-exclude"), tr("Exclude Group(s) from reports"), this); + const auto excludeAction = new QAction(icons()->icon("reports-exclude"), tr("Exclude Entry(s) from reports"), this); + const auto excludeGroupsAction = new QAction(icons()->icon("reports-exclude"), tr("Exclude Group(s) from reports"), this); bool isExcluded = false; bool isGroupExcluded = false; @@ -311,7 +312,7 @@ void ReportsWidgetBrowserStatistics::customMenuRequested(QPoint pos) auto entry = m_rowToEntry[row].second; if (entry) { // If at least one entry is excluded switch to inclusion - if (entry->excludeFromReports()) { + if (entry->excludeFromReports() || entry->group()->excludeFromReports()) { isExcluded = true; } if (entry->group()->excludeFromReports()) { @@ -321,31 +322,67 @@ void ReportsWidgetBrowserStatistics::customMenuRequested(QPoint pos) break; } } - exclude->setCheckable(true); - exclude->setChecked(isExcluded); + excludeAction->setCheckable(true); + excludeAction->setChecked(isExcluded); - excludeGroups->setCheckable(true); - exclude->setChecked(isGroupExcluded); + excludeGroupsAction->setCheckable(true); + excludeGroupsAction->setChecked(isGroupExcluded); + + menu->addAction(excludeAction); + connect(excludeAction, &QAction::toggled, excludeAction, [this, selected](bool checked) { + QSet groups; + + // If we are including entries (checked is false) but a group is excluded, ask the user if they + // would like to include the rest of the group as well (or keep it excluded). + // If they exclude it, we need to include the whole group, and then exclude + // the entries that aren't selected here. + if (!checked) { + for(const auto index : selected) { + auto row = m_modelProxy->mapToSource(index).row(); + auto entry = m_rowToEntry[row].second; + + if (entry) { + auto *group = entry->group(); + if (group->excludeFromReports() && !groups.contains(group)) { + QString msg = tr("The Group for \"%1\" is excluded. Would you like to include all Entries from there as well?").arg(entry->title()); + auto response = MessageBox::question(this, tr("Include Group?"), msg, MessageBox::Yes | MessageBox::No | MessageBox::Cancel, MessageBox::No); + + if (response == MessageBox::Cancel) { + return; + } + else if (response == MessageBox::Yes) { + group->setExcludeFromReports(false); + } + else if (response == MessageBox::No) { + // We'll exclude all entries from the group here and then + // include the selected ones below + group->setExcludeFromReports(false); + group->markAllEntriesExcludedFromReports(); + } + + groups.insert(group); + } + } + } + } - 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); + entry->setExcludeFromReports(checked); } } calculateBrowserStatistics(); }); - menu->addAction(excludeGroups); - connect(excludeGroups, &QAction::toggled, excludeGroups, [this, selected](bool state) { + menu->addAction(excludeGroupsAction); + connect(excludeGroupsAction, &QAction::toggled, excludeGroupsAction, [this, selected](bool checked) { for (const auto index : selected) { auto row = m_modelProxy->mapToSource(index).row(); auto entry = m_rowToEntry[row].second; if (entry) { - entry->group()->setExcludeFromReports(state); + entry->group()->setExcludeFromReports(checked); } } calculateBrowserStatistics(); From 09efef0a97159a492974bd64228cda79b7192064 Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Mon, 22 Dec 2025 23:31:24 -0800 Subject: [PATCH 09/29] Add exclusion support for HIBP report --- src/gui/reports/ReportsWidgetHibp.cpp | 82 ++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 8 deletions(-) diff --git a/src/gui/reports/ReportsWidgetHibp.cpp b/src/gui/reports/ReportsWidgetHibp.cpp index 04ac4dab3..807fc8529 100644 --- a/src/gui/reports/ReportsWidgetHibp.cpp +++ b/src/gui/reports/ReportsWidgetHibp.cpp @@ -23,6 +23,7 @@ #include "core/Metadata.h" #include "gui/GuiTools.h" #include "gui/Icons.h" +#include "gui/MessageBox.h" #include #include @@ -387,28 +388,93 @@ void ReportsWidgetHibp::customMenuRequested(QPoint pos) 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); + const auto excludeAction = new QAction(icons()->icon("reports-exclude"), tr("Exclude Entry(s) from reports"), this); + const auto excludeGroupsAction = new QAction(icons()->icon("reports-exclude"), tr("Exclude Group(s) from reports"), this); bool isExcluded = false; + bool isGroupExcluded = false; + for (auto index : selected) { auto row = m_modelProxy->mapToSource(index).row(); auto entry = m_rowToEntry[row]; - if (entry && entry->excludeFromReports()) { + if (entry) { // If at least one entry is excluded switch to inclusion - isExcluded = true; + if (entry->excludeFromReports() || entry->group()->excludeFromReports()) { + isExcluded = true; + } + if (entry->group()->excludeFromReports()) { + isGroupExcluded = true; + } + break; } } - exclude->setCheckable(true); - exclude->setChecked(isExcluded); - menu->addAction(exclude); - connect(exclude, &QAction::toggled, exclude, [this, selected](bool state) { + excludeAction->setCheckable(true); + excludeAction->setChecked(isExcluded); + + excludeGroupsAction->setCheckable(true); + excludeGroupsAction->setChecked(isGroupExcluded); + + menu->addAction(excludeAction); + connect(excludeAction, &QAction::toggled, excludeAction, [this, selected](bool checked) { + QSet groups; + + // If we are including entries (checked is false) but a group is excluded, ask the user if they + // would like to include the rest of the group as well (or keep it excluded). + // If they exclude it, we need to include the whole group, and then exclude + // the entries that aren't selected here. + if (!checked) { + for(const auto index : selected) { + auto row = m_modelProxy->mapToSource(index).row(); + auto entry = m_rowToEntry[row]; + + if (entry) { + auto *group = entry->group(); + if (group->excludeFromReports() && !groups.contains(group)) { + QString msg = tr("The Group for \"%1\" is excluded. Would you like to include all Entries from there as well?").arg(entry->title()); + auto response = MessageBox::question(this, tr("Include Group?"), msg, MessageBox::Yes | MessageBox::No | MessageBox::Cancel, MessageBox::No); + + if (response == MessageBox::Cancel) { + return; + } + else if (response == MessageBox::Yes) { + group->setExcludeFromReports(false); + } + else if (response == MessageBox::No) { + // We'll exclude all entries from the group here and then + // include the selected ones below + group->setExcludeFromReports(false); + group->markAllEntriesExcludedFromReports(); + } + + groups.insert(group); + } + } + } + } + for (auto index : selected) { auto row = m_modelProxy->mapToSource(index).row(); auto entry = m_rowToEntry[row]; + + // If the containing group is excluded but the user wants to include + // this entry, ask if they want to keep the remaining items in the group + // excluded or included if (entry) { - entry->setExcludeFromReports(state); + entry->setExcludeFromReports(checked); + } + } + makeHibpTable(); + }); + + menu->addAction(excludeGroupsAction); + connect(excludeGroupsAction, &QAction::toggled, excludeGroupsAction, [this, selected](bool checked) { + for (const auto index : selected) { + auto row = m_modelProxy->mapToSource(index).row(); + auto entry = m_rowToEntry[row]; + if (entry) { + entry->group()->setExcludeFromReports(checked); } } makeHibpTable(); From 4ffded316945663e9fef18b538598267ce76062f Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Mon, 22 Dec 2025 23:34:16 -0800 Subject: [PATCH 10/29] Annotate whether group or entry is excluded --- src/gui/reports/ReportsWidgetBrowserStatistics.cpp | 6 +++++- src/gui/reports/ReportsWidgetHealthcheck.cpp | 6 +++++- src/gui/reports/ReportsWidgetHibp.cpp | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp index 917d6c867..b8a857541 100644 --- a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp +++ b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp @@ -142,7 +142,11 @@ void ReportsWidgetBrowserStatistics::addStatisticsRow(bool hasUrls, auto title = entry->title(); if (excluded) { - title.append(tr(" (Excluded)")); + if(group->excludeFromReports()) { + title.append(tr(" (Group Excluded")); + } else { + title.append(tr(" (Excluded)")); + } } if (entry->isExpired()) { title.append(tr(" (Expired)")); diff --git a/src/gui/reports/ReportsWidgetHealthcheck.cpp b/src/gui/reports/ReportsWidgetHealthcheck.cpp index 1e87df6de..4cd9b64e6 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.cpp +++ b/src/gui/reports/ReportsWidgetHealthcheck.cpp @@ -198,7 +198,11 @@ void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer healt auto title = entry->title(); if (excluded) { - title.append(tr(" (Excluded)")); + if(group->excludeFromReports()) { + title.append(tr(" (Group Excluded")); + } else { + title.append(tr(" (Excluded)")); + } } if (entry->isExpired()) { title.append(tr(" (Expired)")); diff --git a/src/gui/reports/ReportsWidgetHibp.cpp b/src/gui/reports/ReportsWidgetHibp.cpp index 807fc8529..234041d53 100644 --- a/src/gui/reports/ReportsWidgetHibp.cpp +++ b/src/gui/reports/ReportsWidgetHibp.cpp @@ -160,7 +160,11 @@ void ReportsWidgetHibp::makeHibpTable() continue; } - title.append(tr(" (Excluded)")); + if(group->excludeFromReports()) { + title.append(tr(" (Group Excluded")); + } else { + title.append(tr(" (Excluded)")); + } } auto row = QList(); From 49e945d1c0bf7ed3be1e7d9a6f466ac36352ee32 Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Mon, 22 Dec 2025 23:47:43 -0800 Subject: [PATCH 11/29] Add test for database reports Adds utility functions to create groups and entries for them. Add test to make sure the right amount of entries show up in the database report for password health. Add signal when password health calculation is done so the test harness can wait on it; otherwise the test finishes too soon and doesn't get the right result from the application. --- src/gui/reports/ReportsWidgetHealthcheck.cpp | 2 + src/gui/reports/ReportsWidgetHealthcheck.h | 1 + tests/gui/TestGui.cpp | 109 +++++++++++++++++++ tests/gui/TestGui.h | 3 + 4 files changed, 115 insertions(+) diff --git a/src/gui/reports/ReportsWidgetHealthcheck.cpp b/src/gui/reports/ReportsWidgetHealthcheck.cpp index 4cd9b64e6..b3dd6f5e8 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.cpp +++ b/src/gui/reports/ReportsWidgetHealthcheck.cpp @@ -304,6 +304,8 @@ void ReportsWidgetHealthcheck::calculateHealth() // Only show the "show excluded" checkbox if there are any excluded entries in the database m_ui->showExcluded->setVisible(health->anyExcludedEntries()); + + emit tablePopulated(); } void ReportsWidgetHealthcheck::emitEntryActivated(const QModelIndex& index) diff --git a/src/gui/reports/ReportsWidgetHealthcheck.h b/src/gui/reports/ReportsWidgetHealthcheck.h index 9a46b36b1..f5a27d678 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.h +++ b/src/gui/reports/ReportsWidgetHealthcheck.h @@ -48,6 +48,7 @@ protected: signals: void entryActivated(Entry*); + void tablePopulated(); public slots: void calculateHealth(); diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 96c8b0703..3dde7edeb 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -62,6 +62,8 @@ #include "gui/group/GroupModel.h" #include "gui/group/GroupView.h" #include "gui/remote/RemoteHandler.h" +#include "gui/reports/ReportsWidgetHealthcheck.h" +#include "gui/reports/ReportsDialog.h" #include "gui/tag/TagsEdit.h" #include "gui/wizard/NewDatabaseWizard.h" #include "keys/FileKey.h" @@ -2464,6 +2466,68 @@ void TestGui::testMenuActionStates() QVERIFY(isActionEnabled("actionPasswordGenerator")); } +void TestGui::testDatabaseReports() +{ + addGroup("Finance"); + addGroup("Entertainment"); + + addEntry("Finance", "Chase", "user1", "password"); + addEntry("Finance", "Amex", "user1", "password123"); + addEntry("Finance", "Capital One", "user1", "lasdjfi309q23i8u2wjei"); + + addEntry("Entertainment", "Netflix", "user1", "password"); + addEntry("Entertainment", "Hulu", "user1", "jwafh783jiwaef"); + addEntry("Entertainment", "Apple TV", "user1", "21983yr2weuajfduiwife;j"); + + Group* entertainmentGroup = m_dbWidget->currentGroup()->findChildByName("Entertainment"); + QCOMPARE(entertainmentGroup->entries().size(), 3); + + Group* financeGroup = m_dbWidget->currentGroup()->findChildByName("Finance"); + QCOMPARE(financeGroup->entries().size(), 3); + + auto *actionReports = m_mainWindow->findChild("actionReports"); + QVERIFY(actionReports->isEnabled()); + + auto* toolBar = m_mainWindow->findChild("toolBar"); + QVERIFY(toolBar); + + QWidget* actionReportsWidget = toolBar->widgetForAction(actionReports); + QVERIFY(actionReportsWidget); + QVERIFY(actionReportsWidget->isVisible()); + QVERIFY(actionReportsWidget->isEnabled()); + + QTest::mouseClick(actionReportsWidget, Qt::LeftButton); + + auto *reportsDialog = m_dbWidget->findChild("reportsDialog"); + QVERIFY(reportsDialog); + + CategoryListWidget *categoryList = reportsDialog->findChild("categoryList"); + categoryList->setCurrentCategory(1); + + QStackedWidget *stackedWidget = reportsDialog->findChild("stackedWidget"); + QVERIFY(stackedWidget); + stackedWidget->setCurrentIndex(1); + + ReportsWidgetHealthcheck* healthCheckWidget = reportsDialog->findChild(); + QVERIFY(healthCheckWidget); + + QTest::mouseClick(healthCheckWidget, Qt::LeftButton); + QTableView *healthTable = healthCheckWidget->findChild("healthcheckTableView"); + QVERIFY(healthTable); + + QSignalSpy healthCheckWidgetSpy(healthCheckWidget, &ReportsWidgetHealthcheck::tablePopulated); + + QAbstractItemModel *healthModel = healthTable->model(); + QVERIFY(healthModel); + + QTRY_COMPARE(healthCheckWidgetSpy.count(), 1); + QCOMPARE(healthModel->rowCount(), 6); + + auto *reportsDialogButtonBox = reportsDialog->findChild("buttonBox"); + QTest::mouseClick(reportsDialogButtonBox->button(QDialogButtonBox::Close), Qt::LeftButton); + QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode); +} + void TestGui::addCannedEntries() { // Find buttons @@ -2494,6 +2558,51 @@ void TestGui::addCannedEntries() QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); } +void TestGui::addGroup(const QString &name) +{ + // Find buttons for group creation + auto* editGroupWidget = m_dbWidget->findChild("editGroupWidget"); + auto* nameEdit = editGroupWidget->findChild("editName"); + auto* editGroupWidgetButtonBox = editGroupWidget->findChild("buttonBox"); + + // Add group with specified name + Group *rootGroup = m_db->rootGroup(); + m_dbWidget->groupView()->setCurrentGroup(rootGroup); // Add group on root level + m_dbWidget->createGroup(); + QTest::keyClicks(nameEdit, name); + QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); + m_dbWidget->groupView()->setCurrentGroup(rootGroup); // Reset to root level +} + +void TestGui::addEntry(const QString &groupName, const QString &title, const QString &username, const QString &password) +{ + // Find buttons + auto* toolBar = m_mainWindow->findChild("toolBar"); + QWidget* entryNewWidget = toolBar->widgetForAction(m_mainWindow->findChild("actionEntryNew")); + auto* editEntryWidget = m_dbWidget->findChild("editEntryWidget"); + auto* titleEdit = editEntryWidget->findChild("titleEdit"); + auto* usernameComboBox = editEntryWidget->findChild("usernameComboBox"); + auto* passwordEdit = + editEntryWidget->findChild("passwordEdit")->findChild("passwordEdit"); + auto* editEntryWidgetButtonBox = editEntryWidget->findChild("buttonBox"); + + // Add entry to specified group + QVERIFY(m_dbWidget->currentGroup()); + Group* group = m_dbWidget->currentGroup()->findChildByName(groupName); + m_dbWidget->groupView()->setCurrentGroup(group); + + QTest::mouseClick(entryNewWidget, Qt::LeftButton); + QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditEntryMode); + + QTest::keyClicks(titleEdit, title); + QTest::keyClicks(usernameComboBox, username); + QTest::keyClicks(passwordEdit, password); + QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); + + QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode); + m_dbWidget->groupView()->setCurrentGroup(m_db->rootGroup()); +} + void TestGui::checkDatabase(const QString& filePath, const QString& expectedDbName) { auto key = QSharedPointer::create(); diff --git a/tests/gui/TestGui.h b/tests/gui/TestGui.h index 514f7ce95..1ff48d5c6 100644 --- a/tests/gui/TestGui.h +++ b/tests/gui/TestGui.h @@ -71,9 +71,12 @@ private slots: void testTrayRestoreHide(); void testShortcutConfig(); void testMenuActionStates(); + void testExcludeGroupFromReports(); private: void addCannedEntries(); + void addGroup(const QString &name); + void addEntry(const QString &groupName, const QString &title, const QString &username, const QString &password); void checkDatabase(const QString& filePath, const QString& expectedDbName); void checkDatabase(const QString& filePath = {}); void triggerAction(const QString& name); From 3b3bb9a949e7274bb985d30047df58e01e821da7 Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Tue, 23 Dec 2025 22:29:03 -0800 Subject: [PATCH 12/29] Add test for healthcheck exclusion Adds 2 groups, excludes one and makes sure it doesn't show up in the health check. --- tests/gui/TestGui.cpp | 110 ++++++++++++++++++++++++++++++++++++++++-- tests/gui/TestGui.h | 7 ++- 2 files changed, 111 insertions(+), 6 deletions(-) diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 3dde7edeb..03aba4a2b 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -2473,11 +2473,11 @@ void TestGui::testDatabaseReports() addEntry("Finance", "Chase", "user1", "password"); addEntry("Finance", "Amex", "user1", "password123"); - addEntry("Finance", "Capital One", "user1", "lasdjfi309q23i8u2wjei"); + addEntry("Finance", "Capital One", "user1", "password456"); addEntry("Entertainment", "Netflix", "user1", "password"); - addEntry("Entertainment", "Hulu", "user1", "jwafh783jiwaef"); - addEntry("Entertainment", "Apple TV", "user1", "21983yr2weuajfduiwife;j"); + addEntry("Entertainment", "Hulu", "user1", "password321"); + addEntry("Entertainment", "Apple TV", "user1", "password123"); Group* entertainmentGroup = m_dbWidget->currentGroup()->findChildByName("Entertainment"); QCOMPARE(entertainmentGroup->entries().size(), 3); @@ -2521,7 +2521,109 @@ void TestGui::testDatabaseReports() QVERIFY(healthModel); QTRY_COMPARE(healthCheckWidgetSpy.count(), 1); - QCOMPARE(healthModel->rowCount(), 6); + QCOMPARE(healthModel->rowCount(), 8); // account for 2 existing passwords at the start of each test case + + auto *reportsDialogButtonBox = reportsDialog->findChild("buttonBox"); + QTest::mouseClick(reportsDialogButtonBox->button(QDialogButtonBox::Close), Qt::LeftButton); + QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode); +} + +void TestGui::testExcludedDatabaseReports() +{ + addGroup("Finance"); + addGroup("Entertainment"); + + // Use bad passwords to make sure they all show up in health report + addEntry("Finance", "Chase", "user1", "password"); + addEntry("Finance", "Amex", "user1", "password123"); + addEntry("Finance", "Capital One", "user1", "password456"); + + addEntry("Entertainment", "Netflix", "user1", "password"); + addEntry("Entertainment", "Hulu", "user1", "password321"); + addEntry("Entertainment", "Apple TV", "user1", "password123"); + + Group* entertainmentGroup = m_dbWidget->currentGroup()->findChildByName("Entertainment"); + m_dbWidget->groupView()->setCurrentGroup(entertainmentGroup); + + auto* toolBar = m_mainWindow->findChild("toolBar"); + QVERIFY(toolBar); + + auto *editGroupAction = m_mainWindow->findChild("actionGroupEdit"); + QVERIFY(editGroupAction->isEnabled()); + triggerAction("actionGroupEdit"); + + auto* editGroupWidget = m_dbWidget->findChild("editGroupWidget"); + QVERIFY(editGroupWidget); + + // Bring up group edit page + QTest::mouseClick(editGroupWidget, Qt::LeftButton); + + QLineEdit* nameEdit = editGroupWidget->findChild("editName"); + QCOMPARE(nameEdit->text(), QString("Entertainment")); + + // Find database report exclusion checkbox and check it + QCheckBox *excludeGroupFromReportsCheckbox = editGroupWidget->findChild("excludeReportsCheckBox"); + QVERIFY(excludeGroupFromReportsCheckbox); + + excludeGroupFromReportsCheckbox->setChecked(true); + + auto* editGroupWidgetButtonBox = editGroupWidget->findChild("buttonBox"); + QVERIFY(editGroupWidgetButtonBox); + + // Apply and go back to main view + QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Apply), Qt::LeftButton); + QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); + QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode); + + QVERIFY(entertainmentGroup->excludeFromReports()); + + // Verify they don't show up in the report + auto *actionReports = m_mainWindow->findChild("actionReports"); + QVERIFY(actionReports->isEnabled()); + + QWidget* actionReportsWidget = toolBar->widgetForAction(actionReports); + QVERIFY(actionReportsWidget); + QVERIFY(actionReportsWidget->isVisible()); + QVERIFY(actionReportsWidget->isEnabled()); + + QTest::mouseClick(actionReportsWidget, Qt::LeftButton); + + auto *reportsDialog = m_dbWidget->findChild("reportsDialog"); + QVERIFY(reportsDialog); + + CategoryListWidget *categoryList = reportsDialog->findChild("categoryList"); + categoryList->setCurrentCategory(1); + + QStackedWidget *stackedWidget = reportsDialog->findChild("stackedWidget"); + QVERIFY(stackedWidget); + stackedWidget->setCurrentIndex(1); + + ReportsWidgetHealthcheck* healthCheckWidget = reportsDialog->findChild(); + QVERIFY(healthCheckWidget); + + QTest::mouseClick(healthCheckWidget, Qt::LeftButton); + QTableView *healthTable = healthCheckWidget->findChild("healthcheckTableView"); + QVERIFY(healthTable); + + QSignalSpy healthCheckWidgetSpy(healthCheckWidget, &ReportsWidgetHealthcheck::tablePopulated); + + QAbstractItemModel *healthModel = healthTable->model(); + QVERIFY(healthModel); + + // There should be 3 showing + QTRY_COMPARE(healthCheckWidgetSpy.count(), 1); + QCOMPARE(healthModel->rowCount(), 5); // account for 2 existing passwords at the start of each test case + + QCheckBox *showExcludedCheckBox = healthCheckWidget->findChild("showExcluded"); + QVERIFY(showExcludedCheckBox); + QCOMPARE(showExcludedCheckBox->isChecked(), false); + + showExcludedCheckBox->click(); + QVERIFY(showExcludedCheckBox->isChecked()); + QTRY_COMPARE(healthCheckWidgetSpy.count(), 2); + + healthModel = healthTable->model(); + QCOMPARE(healthModel->rowCount(), 8); // account for 2 existing passwords at the start of each test case auto *reportsDialogButtonBox = reportsDialog->findChild("buttonBox"); QTest::mouseClick(reportsDialogButtonBox->button(QDialogButtonBox::Close), Qt::LeftButton); diff --git a/tests/gui/TestGui.h b/tests/gui/TestGui.h index 1ff48d5c6..179ec5ece 100644 --- a/tests/gui/TestGui.h +++ b/tests/gui/TestGui.h @@ -37,6 +37,11 @@ private slots: void cleanup(); void cleanupTestCase(); + + void testDatabaseReports(); + void testExcludedDatabaseReports(); + +private: void testSettingsDefaultTabOrder(); void testCreateDatabase(); void testMergeDatabase(); @@ -71,9 +76,7 @@ private slots: void testTrayRestoreHide(); void testShortcutConfig(); void testMenuActionStates(); - void testExcludeGroupFromReports(); -private: void addCannedEntries(); void addGroup(const QString &name); void addEntry(const QString &groupName, const QString &title, const QString &username, const QString &password); From 8df50d1eb6c64567333378b41cd5afd6fed93400 Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Thu, 25 Dec 2025 23:51:38 -0800 Subject: [PATCH 13/29] Add test for reincluding excluded group entries Entries in the group get marked as excluded. Entry that was selected is marked for inclusion. --- src/gui/reports/ReportsWidgetHealthcheck.cpp | 5 + tests/gui/TestGui.cpp | 144 +++++++++++++++++++ tests/gui/TestGui.h | 1 + 3 files changed, 150 insertions(+) diff --git a/src/gui/reports/ReportsWidgetHealthcheck.cpp b/src/gui/reports/ReportsWidgetHealthcheck.cpp index b3dd6f5e8..8b5912fe7 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.cpp +++ b/src/gui/reports/ReportsWidgetHealthcheck.cpp @@ -332,10 +332,12 @@ void ReportsWidgetHealthcheck::customMenuRequested(QPoint pos) // Create the context menu const auto menu = new QMenu(this); + menu->setObjectName("customMenu"); // 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); + edit->setObjectName("contextMenuEditAction"); menu->addAction(edit); connect(edit, &QAction::triggered, edit, [this, selected] { auto row = m_modelProxy->mapToSource(selected[0]).row(); @@ -346,6 +348,7 @@ void ReportsWidgetHealthcheck::customMenuRequested(QPoint pos) // Create the "Expire entry" menu item const auto expEntry = new QAction(icons()->icon("entry-expire"), tr("Expire Entry(s)…", "", selected.size()), this); + expEntry->setObjectName("contextMenuExpireAction"); menu->addAction(expEntry); connect(expEntry, &QAction::triggered, this, &ReportsWidgetHealthcheck::expireSelectedEntries); @@ -356,7 +359,9 @@ void ReportsWidgetHealthcheck::customMenuRequested(QPoint pos) // Create the "exclude from reports" menu item const auto excludeAction = new QAction(icons()->icon("reports-exclude"), tr("Exclude Entry(s) from reports"), this); + excludeAction->setObjectName("contextMenuExcludeAction"); const auto excludeGroupsAction = new QAction(icons()->icon("reports-exclude"), tr("Exclude Group(s) from reports"), this); + excludeGroupsAction->setObjectName("contextMenuxcludeGroupAction"); bool isExcluded = false; bool isGroupExcluded = false; diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 03aba4a2b..4e1750488 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -2630,6 +2630,150 @@ void TestGui::testExcludedDatabaseReports() QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode); } +void TestGui::testIncludeExcludedGroupEntryInReports() +{ + addGroup("Finance"); + addGroup("Entertainment"); + + // Use bad passwords to make sure they all show up in health report + addEntry("Finance", "Chase", "user1", "password"); + addEntry("Finance", "Amex", "user1", "password123"); + addEntry("Finance", "Capital One", "user1", "password456"); + + addEntry("Entertainment", "Netflix", "user1", "password"); + addEntry("Entertainment", "Hulu", "user1", "password321"); + addEntry("Entertainment", "Apple TV", "user1", "password123"); + + Group* entertainmentGroup = m_dbWidget->currentGroup()->findChildByName("Entertainment"); + m_dbWidget->groupView()->setCurrentGroup(entertainmentGroup); + + auto* toolBar = m_mainWindow->findChild("toolBar"); + QVERIFY(toolBar); + + auto *editGroupAction = m_mainWindow->findChild("actionGroupEdit"); + QVERIFY(editGroupAction->isEnabled()); + triggerAction("actionGroupEdit"); + + auto* editGroupWidget = m_dbWidget->findChild("editGroupWidget"); + QVERIFY(editGroupWidget); + + // Bring up group edit page + QTest::mouseClick(editGroupWidget, Qt::LeftButton); + + QLineEdit* nameEdit = editGroupWidget->findChild("editName"); + QCOMPARE(nameEdit->text(), QString("Entertainment")); + + // Find database report exclusion checkbox and check it + QCheckBox *excludeGroupFromReportsCheckbox = editGroupWidget->findChild("excludeReportsCheckBox"); + QVERIFY(excludeGroupFromReportsCheckbox); + + excludeGroupFromReportsCheckbox->setChecked(true); + + auto* editGroupWidgetButtonBox = editGroupWidget->findChild("buttonBox"); + QVERIFY(editGroupWidgetButtonBox); + + // Apply and go back to main view + QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Apply), Qt::LeftButton); + QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); + QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode); + + QVERIFY(entertainmentGroup->excludeFromReports()); + + // Verify they don't show up in the report + auto *actionReports = m_mainWindow->findChild("actionReports"); + QVERIFY(actionReports->isEnabled()); + + QWidget* actionReportsWidget = toolBar->widgetForAction(actionReports); + QVERIFY(actionReportsWidget); + QVERIFY(actionReportsWidget->isVisible()); + QVERIFY(actionReportsWidget->isEnabled()); + + QTest::mouseClick(actionReportsWidget, Qt::LeftButton); + + auto *reportsDialog = m_dbWidget->findChild("reportsDialog"); + QVERIFY(reportsDialog); + + CategoryListWidget *categoryList = reportsDialog->findChild("categoryList"); + categoryList->setCurrentCategory(1); + + QStackedWidget *stackedWidget = reportsDialog->findChild("stackedWidget"); + QVERIFY(stackedWidget); + stackedWidget->setCurrentIndex(1); + + ReportsWidgetHealthcheck* healthCheckWidget = reportsDialog->findChild(); + QVERIFY(healthCheckWidget); + + QTest::mouseClick(healthCheckWidget, Qt::LeftButton); + QTableView *healthTable = healthCheckWidget->findChild("healthcheckTableView"); + QVERIFY(healthTable); + + QSignalSpy healthCheckWidgetSpy(healthCheckWidget, &ReportsWidgetHealthcheck::tablePopulated); + + QAbstractItemModel *healthModel = healthTable->model(); + QVERIFY(healthModel); + + // There should be 3 showing + QTRY_COMPARE(healthCheckWidgetSpy.count(), 1); + QCOMPARE(healthModel->rowCount(), 5); // account for 2 existing passwords at the start of each test case + + QCheckBox *showExcludedCheckBox = healthCheckWidget->findChild("showExcluded"); + QVERIFY(showExcludedCheckBox); + QCOMPARE(showExcludedCheckBox->isChecked(), false); + + showExcludedCheckBox->click(); + QVERIFY(showExcludedCheckBox->isChecked()); + QTRY_COMPARE(healthCheckWidgetSpy.count(), 2); + + healthModel = healthTable->model(); + QCOMPARE(healthModel->rowCount(), 8); // account for 2 existing passwords at the start of each test case + + for(int i = 0; i < healthModel->rowCount(); ++i) { + QModelIndex index = healthModel->index(i, 1); + QVariant data = healthModel->data(index); + + if(data.toString().contains("Netflix")) { + auto rect = healthTable->visualRect(index); + auto centerPoint = rect.center(); + QTest::mouseClick(healthTable->viewport(), Qt::LeftButton, Qt::NoModifier, centerPoint); + // QTest::mouseClick(healthTable->viewport(), Qt::RightButton, Qt::NoModifier, centerPoint); + // QTest::mouseClick(healthCheckWidget, Qt::RightButton); + healthCheckWidget->customMenuRequested(centerPoint); + + QMenu *menu = healthCheckWidget->findChild("customMenu"); + QVERIFY(menu); + QAction *excludeEntryAction = healthCheckWidget->findChild("contextMenuExcludeAction"); + QVERIFY(excludeEntryAction); + MessageBox::setNextAnswer(MessageBox::No); + + excludeEntryAction->trigger(); + QApplication::processEvents(); + break; + } + } + + QTRY_COMPARE(healthCheckWidgetSpy.count(), 3); + + for(int i = 0; i < healthModel->rowCount(); ++i) { + QModelIndex index = healthModel->index(i, 1); + QVariant data = healthModel->data(index); + + if(data.toString().contains("Netflix")) { + QVERIFY(!data.toString().contains("(Group Excluded)")); + break; + } + } + + showExcludedCheckBox->click(); + QVERIFY(!showExcludedCheckBox->isChecked()); + QTRY_COMPARE(healthCheckWidgetSpy.count(), 4); + + QCOMPARE(healthModel->rowCount(), 6); // 2 existing passwords from start, 3 from Finance, 1 from Entertainment + + auto *reportsDialogButtonBox = reportsDialog->findChild("buttonBox"); + QTest::mouseClick(reportsDialogButtonBox->button(QDialogButtonBox::Close), Qt::LeftButton); + QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode); +} + void TestGui::addCannedEntries() { // Find buttons diff --git a/tests/gui/TestGui.h b/tests/gui/TestGui.h index 179ec5ece..9ce10e28a 100644 --- a/tests/gui/TestGui.h +++ b/tests/gui/TestGui.h @@ -40,6 +40,7 @@ private slots: void testDatabaseReports(); void testExcludedDatabaseReports(); + void testIncludeExcludedGroupEntryInReports(); private: void testSettingsDefaultTabOrder(); From a27d82709051b926c6e2925db6216e7edc0b2c01 Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Sun, 28 Dec 2025 16:57:57 -0800 Subject: [PATCH 14/29] Remove redundant test and cleanup --- src/gui/group/EditGroupWidgetMain.ui | 15 ++++ tests/gui/TestGui.cpp | 104 +-------------------------- tests/gui/TestGui.h | 9 +-- 3 files changed, 19 insertions(+), 109 deletions(-) diff --git a/src/gui/group/EditGroupWidgetMain.ui b/src/gui/group/EditGroupWidgetMain.ui index a98e735fd..93511d8e9 100644 --- a/src/gui/group/EditGroupWidgetMain.ui +++ b/src/gui/group/EditGroupWidgetMain.ui @@ -223,6 +223,9 @@ + + Toggle exclusion from database reports + Exclude from database reports @@ -244,6 +247,18 @@ + + editName + editNotes + expireCheck + expireDatePicker + searchComboBox + autotypeComboBox + autoTypeSequenceInherit + autoTypeSequenceCustomRadio + autoTypeSequenceCustomEdit + excludeReportsCheckBox + diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 4e1750488..ed75b98a7 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -2528,109 +2528,7 @@ void TestGui::testDatabaseReports() QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode); } -void TestGui::testExcludedDatabaseReports() -{ - addGroup("Finance"); - addGroup("Entertainment"); - - // Use bad passwords to make sure they all show up in health report - addEntry("Finance", "Chase", "user1", "password"); - addEntry("Finance", "Amex", "user1", "password123"); - addEntry("Finance", "Capital One", "user1", "password456"); - - addEntry("Entertainment", "Netflix", "user1", "password"); - addEntry("Entertainment", "Hulu", "user1", "password321"); - addEntry("Entertainment", "Apple TV", "user1", "password123"); - - Group* entertainmentGroup = m_dbWidget->currentGroup()->findChildByName("Entertainment"); - m_dbWidget->groupView()->setCurrentGroup(entertainmentGroup); - - auto* toolBar = m_mainWindow->findChild("toolBar"); - QVERIFY(toolBar); - - auto *editGroupAction = m_mainWindow->findChild("actionGroupEdit"); - QVERIFY(editGroupAction->isEnabled()); - triggerAction("actionGroupEdit"); - - auto* editGroupWidget = m_dbWidget->findChild("editGroupWidget"); - QVERIFY(editGroupWidget); - - // Bring up group edit page - QTest::mouseClick(editGroupWidget, Qt::LeftButton); - - QLineEdit* nameEdit = editGroupWidget->findChild("editName"); - QCOMPARE(nameEdit->text(), QString("Entertainment")); - - // Find database report exclusion checkbox and check it - QCheckBox *excludeGroupFromReportsCheckbox = editGroupWidget->findChild("excludeReportsCheckBox"); - QVERIFY(excludeGroupFromReportsCheckbox); - - excludeGroupFromReportsCheckbox->setChecked(true); - - auto* editGroupWidgetButtonBox = editGroupWidget->findChild("buttonBox"); - QVERIFY(editGroupWidgetButtonBox); - - // Apply and go back to main view - QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Apply), Qt::LeftButton); - QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); - QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode); - - QVERIFY(entertainmentGroup->excludeFromReports()); - - // Verify they don't show up in the report - auto *actionReports = m_mainWindow->findChild("actionReports"); - QVERIFY(actionReports->isEnabled()); - - QWidget* actionReportsWidget = toolBar->widgetForAction(actionReports); - QVERIFY(actionReportsWidget); - QVERIFY(actionReportsWidget->isVisible()); - QVERIFY(actionReportsWidget->isEnabled()); - - QTest::mouseClick(actionReportsWidget, Qt::LeftButton); - - auto *reportsDialog = m_dbWidget->findChild("reportsDialog"); - QVERIFY(reportsDialog); - - CategoryListWidget *categoryList = reportsDialog->findChild("categoryList"); - categoryList->setCurrentCategory(1); - - QStackedWidget *stackedWidget = reportsDialog->findChild("stackedWidget"); - QVERIFY(stackedWidget); - stackedWidget->setCurrentIndex(1); - - ReportsWidgetHealthcheck* healthCheckWidget = reportsDialog->findChild(); - QVERIFY(healthCheckWidget); - - QTest::mouseClick(healthCheckWidget, Qt::LeftButton); - QTableView *healthTable = healthCheckWidget->findChild("healthcheckTableView"); - QVERIFY(healthTable); - - QSignalSpy healthCheckWidgetSpy(healthCheckWidget, &ReportsWidgetHealthcheck::tablePopulated); - - QAbstractItemModel *healthModel = healthTable->model(); - QVERIFY(healthModel); - - // There should be 3 showing - QTRY_COMPARE(healthCheckWidgetSpy.count(), 1); - QCOMPARE(healthModel->rowCount(), 5); // account for 2 existing passwords at the start of each test case - - QCheckBox *showExcludedCheckBox = healthCheckWidget->findChild("showExcluded"); - QVERIFY(showExcludedCheckBox); - QCOMPARE(showExcludedCheckBox->isChecked(), false); - - showExcludedCheckBox->click(); - QVERIFY(showExcludedCheckBox->isChecked()); - QTRY_COMPARE(healthCheckWidgetSpy.count(), 2); - - healthModel = healthTable->model(); - QCOMPARE(healthModel->rowCount(), 8); // account for 2 existing passwords at the start of each test case - - auto *reportsDialogButtonBox = reportsDialog->findChild("buttonBox"); - QTest::mouseClick(reportsDialogButtonBox->button(QDialogButtonBox::Close), Qt::LeftButton); - QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode); -} - -void TestGui::testIncludeExcludedGroupEntryInReports() +void TestGui::testExcludedGroupEntryInReports() { addGroup("Finance"); addGroup("Entertainment"); diff --git a/tests/gui/TestGui.h b/tests/gui/TestGui.h index 9ce10e28a..c9181effe 100644 --- a/tests/gui/TestGui.h +++ b/tests/gui/TestGui.h @@ -37,12 +37,6 @@ private slots: void cleanup(); void cleanupTestCase(); - - void testDatabaseReports(); - void testExcludedDatabaseReports(); - void testIncludeExcludedGroupEntryInReports(); - -private: void testSettingsDefaultTabOrder(); void testCreateDatabase(); void testMergeDatabase(); @@ -77,7 +71,10 @@ private: void testTrayRestoreHide(); void testShortcutConfig(); void testMenuActionStates(); + void testDatabaseReports(); + void testExcludedGroupEntryInReports(); +private: void addCannedEntries(); void addGroup(const QString &name); void addEntry(const QString &groupName, const QString &title, const QString &username, const QString &password); From 2a02e029d2169e1e29333b21c7db1e17fdea37ce Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Sun, 28 Dec 2025 22:30:32 -0800 Subject: [PATCH 15/29] Copilot PR feedback --- src/core/Group.cpp | 4 +- .../ReportsWidgetBrowserStatistics.cpp | 37 ++++++------ src/gui/reports/ReportsWidgetHealthcheck.cpp | 34 ++++++----- src/gui/reports/ReportsWidgetHibp.cpp | 43 +++++++------- tests/gui/TestGui.cpp | 56 +++++++++---------- tests/gui/TestGui.h | 4 +- 6 files changed, 96 insertions(+), 82 deletions(-) diff --git a/src/core/Group.cpp b/src/core/Group.cpp index 7b93dee21..29c765a6a 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -1289,7 +1289,7 @@ void Group::setExcludeFromReports(bool excluded) // group because it'll make it easier to individually set it for an // entry later on if (excluded) { - for (auto &entry : m_entries) { + for (auto& entry : m_entries) { entry->setExcludeFromReports(false); } } @@ -1297,7 +1297,7 @@ void Group::setExcludeFromReports(bool excluded) void Group::markAllEntriesExcludedFromReports() { - for (auto &entry : m_entries) { + for (auto& entry : m_entries) { entry->setExcludeFromReports(true); } } diff --git a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp index b8a857541..25f57aced 100644 --- a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp +++ b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp @@ -24,8 +24,8 @@ #include "core/Metadata.h" #include "gui/GuiTools.h" #include "gui/Icons.h" -#include "gui/styles/StateColorPalette.h" #include "gui/MessageBox.h" +#include "gui/styles/StateColorPalette.h" #include #include @@ -142,8 +142,8 @@ void ReportsWidgetBrowserStatistics::addStatisticsRow(bool hasUrls, auto title = entry->title(); if (excluded) { - if(group->excludeFromReports()) { - title.append(tr(" (Group Excluded")); + if (group->excludeFromReports()) { + title.append(tr(" (Group Excluded)")); } else { title.append(tr(" (Excluded)")); } @@ -306,7 +306,8 @@ void ReportsWidgetBrowserStatistics::customMenuRequested(QPoint pos) // Create the "exclude from reports" menu item const auto excludeAction = new QAction(icons()->icon("reports-exclude"), tr("Exclude Entry(s) from reports"), this); - const auto excludeGroupsAction = new QAction(icons()->icon("reports-exclude"), tr("Exclude Group(s) from reports"), this); + const auto excludeGroupsAction = + new QAction(icons()->icon("reports-exclude"), tr("Exclude Group(s) from reports"), this); bool isExcluded = false; bool isGroupExcluded = false; @@ -336,28 +337,32 @@ void ReportsWidgetBrowserStatistics::customMenuRequested(QPoint pos) connect(excludeAction, &QAction::toggled, excludeAction, [this, selected](bool checked) { QSet groups; - // If we are including entries (checked is false) but a group is excluded, ask the user if they - // would like to include the rest of the group as well (or keep it excluded). - // If they exclude it, we need to include the whole group, and then exclude - // the entries that aren't selected here. + // If we are including entries (checked is false) but a group is excluded, ask the user if they + // would like to include the rest of the group as well (or keep it excluded). + // If they exclude it, we need to include the whole group, and then exclude + // the entries that aren't selected here. if (!checked) { - for(const auto index : selected) { + for (const auto index : selected) { auto row = m_modelProxy->mapToSource(index).row(); auto entry = m_rowToEntry[row].second; if (entry) { - auto *group = entry->group(); + auto* group = entry->group(); if (group->excludeFromReports() && !groups.contains(group)) { - QString msg = tr("The Group for \"%1\" is excluded. Would you like to include all Entries from there as well?").arg(entry->title()); - auto response = MessageBox::question(this, tr("Include Group?"), msg, MessageBox::Yes | MessageBox::No | MessageBox::Cancel, MessageBox::No); + QString msg = tr("The Group for \"%1\" is excluded. Would you like to include all Entries from " + "there as well?") + .arg(entry->title()); + auto response = MessageBox::question(this, + tr("Include Group?"), + msg, + MessageBox::Yes | MessageBox::No | MessageBox::Cancel, + MessageBox::No); if (response == MessageBox::Cancel) { return; - } - else if (response == MessageBox::Yes) { + } else if (response == MessageBox::Yes) { group->setExcludeFromReports(false); - } - else if (response == MessageBox::No) { + } else if (response == MessageBox::No) { // We'll exclude all entries from the group here and then // include the selected ones below group->setExcludeFromReports(false); diff --git a/src/gui/reports/ReportsWidgetHealthcheck.cpp b/src/gui/reports/ReportsWidgetHealthcheck.cpp index 8b5912fe7..ee77eadf4 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.cpp +++ b/src/gui/reports/ReportsWidgetHealthcheck.cpp @@ -24,8 +24,8 @@ #include "core/PasswordHealth.h" #include "gui/GuiTools.h" #include "gui/Icons.h" -#include "gui/styles/StateColorPalette.h" #include "gui/MessageBox.h" +#include "gui/styles/StateColorPalette.h" #include #include @@ -198,8 +198,8 @@ void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer healt auto title = entry->title(); if (excluded) { - if(group->excludeFromReports()) { - title.append(tr(" (Group Excluded")); + if (group->excludeFromReports()) { + title.append(tr(" (Group Excluded)")); } else { title.append(tr(" (Excluded)")); } @@ -226,8 +226,7 @@ void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer healt if (excluded) { if (group->excludeFromReports()) { row[1]->setToolTip(tr("The group for this entry is being excluded from reports")); - } - else { + } else { row[1]->setToolTip(tr("This entry is being excluded from reports")); } } @@ -360,8 +359,9 @@ void ReportsWidgetHealthcheck::customMenuRequested(QPoint pos) // Create the "exclude from reports" menu item const auto excludeAction = new QAction(icons()->icon("reports-exclude"), tr("Exclude Entry(s) from reports"), this); excludeAction->setObjectName("contextMenuExcludeAction"); - const auto excludeGroupsAction = new QAction(icons()->icon("reports-exclude"), tr("Exclude Group(s) from reports"), this); - excludeGroupsAction->setObjectName("contextMenuxcludeGroupAction"); + const auto excludeGroupsAction = + new QAction(icons()->icon("reports-exclude"), tr("Exclude Group(s) from reports"), this); + excludeGroupsAction->setObjectName("contextMenuExcludeGroupAction"); bool isExcluded = false; bool isGroupExcluded = false; @@ -396,23 +396,27 @@ void ReportsWidgetHealthcheck::customMenuRequested(QPoint pos) // If they exclude it, we need to include the whole group, and then exclude // the entries that aren't selected here. if (!checked) { - for(const auto index : selected) { + for (const auto index : selected) { auto row = m_modelProxy->mapToSource(index).row(); auto entry = m_rowToEntry[row].second; if (entry) { - auto *group = entry->group(); + auto* group = entry->group(); if (group->excludeFromReports() && !groups.contains(group)) { - QString msg = tr("The Group for \"%1\" is excluded. Would you like to include all Entries from there as well?").arg(entry->title()); - auto response = MessageBox::question(this, tr("Include Group?"), msg, MessageBox::Yes | MessageBox::No | MessageBox::Cancel, MessageBox::No); + QString msg = tr("The Group for \"%1\" is excluded. Would you like to include all Entries from " + "there as well?") + .arg(entry->title()); + auto response = MessageBox::question(this, + tr("Include Group?"), + msg, + MessageBox::Yes | MessageBox::No | MessageBox::Cancel, + MessageBox::No); if (response == MessageBox::Cancel) { return; - } - else if (response == MessageBox::Yes) { + } else if (response == MessageBox::Yes) { group->setExcludeFromReports(false); - } - else if (response == MessageBox::No) { + } else if (response == MessageBox::No) { // We'll exclude all entries from the group here and then // include the selected ones below group->setExcludeFromReports(false); diff --git a/src/gui/reports/ReportsWidgetHibp.cpp b/src/gui/reports/ReportsWidgetHibp.cpp index 234041d53..21e8ac7fd 100644 --- a/src/gui/reports/ReportsWidgetHibp.cpp +++ b/src/gui/reports/ReportsWidgetHibp.cpp @@ -160,8 +160,8 @@ void ReportsWidgetHibp::makeHibpTable() continue; } - if(group->excludeFromReports()) { - title.append(tr(" (Group Excluded")); + if (group->excludeFromReports()) { + title.append(tr(" (Group Excluded)")); } else { title.append(tr(" (Excluded)")); } @@ -174,7 +174,7 @@ void ReportsWidgetHibp::makeHibpTable() if (entry->excludeFromReports()) { row[1]->setToolTip(tr("This entry is being excluded from reports")); - } else if(entry->group()->excludeFromReports()) { + } else if (entry->group()->excludeFromReports()) { row[1]->setToolTip(tr("The group for this entry is being excluded from reports")); } @@ -393,7 +393,8 @@ void ReportsWidgetHibp::customMenuRequested(QPoint pos) // Create the "exclude from reports" menu item const auto excludeAction = new QAction(icons()->icon("reports-exclude"), tr("Exclude Entry(s) from reports"), this); - const auto excludeGroupsAction = new QAction(icons()->icon("reports-exclude"), tr("Exclude Group(s) from reports"), this); + const auto excludeGroupsAction = + new QAction(icons()->icon("reports-exclude"), tr("Exclude Group(s) from reports"), this); bool isExcluded = false; bool isGroupExcluded = false; @@ -424,28 +425,32 @@ void ReportsWidgetHibp::customMenuRequested(QPoint pos) connect(excludeAction, &QAction::toggled, excludeAction, [this, selected](bool checked) { QSet groups; - // If we are including entries (checked is false) but a group is excluded, ask the user if they - // would like to include the rest of the group as well (or keep it excluded). - // If they exclude it, we need to include the whole group, and then exclude - // the entries that aren't selected here. + // If we are including entries (checked is false) but a group is excluded, ask the user if they + // would like to include the rest of the group as well (or keep it excluded). + // If they exclude it, we need to include the whole group, and then exclude + // the entries that aren't selected here. if (!checked) { - for(const auto index : selected) { + for (const auto index : selected) { auto row = m_modelProxy->mapToSource(index).row(); auto entry = m_rowToEntry[row]; if (entry) { - auto *group = entry->group(); + auto* group = entry->group(); if (group->excludeFromReports() && !groups.contains(group)) { - QString msg = tr("The Group for \"%1\" is excluded. Would you like to include all Entries from there as well?").arg(entry->title()); - auto response = MessageBox::question(this, tr("Include Group?"), msg, MessageBox::Yes | MessageBox::No | MessageBox::Cancel, MessageBox::No); + QString msg = tr("The Group for \"%1\" is excluded. Would you like to include all Entries from " + "there as well?") + .arg(entry->title()); + auto response = MessageBox::question(this, + tr("Include Group?"), + msg, + MessageBox::Yes | MessageBox::No | MessageBox::Cancel, + MessageBox::No); if (response == MessageBox::Cancel) { return; - } - else if (response == MessageBox::Yes) { + } else if (response == MessageBox::Yes) { group->setExcludeFromReports(false); - } - else if (response == MessageBox::No) { + } else if (response == MessageBox::No) { // We'll exclude all entries from the group here and then // include the selected ones below group->setExcludeFromReports(false); @@ -462,9 +467,9 @@ void ReportsWidgetHibp::customMenuRequested(QPoint pos) auto row = m_modelProxy->mapToSource(index).row(); auto entry = m_rowToEntry[row]; - // If the containing group is excluded but the user wants to include - // this entry, ask if they want to keep the remaining items in the group - // excluded or included + // If the containing group is excluded but the user wants to include + // this entry, ask if they want to keep the remaining items in the group + // excluded or included if (entry) { entry->setExcludeFromReports(checked); } diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index ed75b98a7..0ebb48681 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -62,8 +62,8 @@ #include "gui/group/GroupModel.h" #include "gui/group/GroupView.h" #include "gui/remote/RemoteHandler.h" -#include "gui/reports/ReportsWidgetHealthcheck.h" #include "gui/reports/ReportsDialog.h" +#include "gui/reports/ReportsWidgetHealthcheck.h" #include "gui/tag/TagsEdit.h" #include "gui/wizard/NewDatabaseWizard.h" #include "keys/FileKey.h" @@ -2485,7 +2485,7 @@ void TestGui::testDatabaseReports() Group* financeGroup = m_dbWidget->currentGroup()->findChildByName("Finance"); QCOMPARE(financeGroup->entries().size(), 3); - auto *actionReports = m_mainWindow->findChild("actionReports"); + auto* actionReports = m_mainWindow->findChild("actionReports"); QVERIFY(actionReports->isEnabled()); auto* toolBar = m_mainWindow->findChild("toolBar"); @@ -2498,13 +2498,13 @@ void TestGui::testDatabaseReports() QTest::mouseClick(actionReportsWidget, Qt::LeftButton); - auto *reportsDialog = m_dbWidget->findChild("reportsDialog"); + auto* reportsDialog = m_dbWidget->findChild("reportsDialog"); QVERIFY(reportsDialog); - CategoryListWidget *categoryList = reportsDialog->findChild("categoryList"); + CategoryListWidget* categoryList = reportsDialog->findChild("categoryList"); categoryList->setCurrentCategory(1); - QStackedWidget *stackedWidget = reportsDialog->findChild("stackedWidget"); + QStackedWidget* stackedWidget = reportsDialog->findChild("stackedWidget"); QVERIFY(stackedWidget); stackedWidget->setCurrentIndex(1); @@ -2512,18 +2512,18 @@ void TestGui::testDatabaseReports() QVERIFY(healthCheckWidget); QTest::mouseClick(healthCheckWidget, Qt::LeftButton); - QTableView *healthTable = healthCheckWidget->findChild("healthcheckTableView"); + QTableView* healthTable = healthCheckWidget->findChild("healthcheckTableView"); QVERIFY(healthTable); QSignalSpy healthCheckWidgetSpy(healthCheckWidget, &ReportsWidgetHealthcheck::tablePopulated); - QAbstractItemModel *healthModel = healthTable->model(); + QAbstractItemModel* healthModel = healthTable->model(); QVERIFY(healthModel); QTRY_COMPARE(healthCheckWidgetSpy.count(), 1); QCOMPARE(healthModel->rowCount(), 8); // account for 2 existing passwords at the start of each test case - auto *reportsDialogButtonBox = reportsDialog->findChild("buttonBox"); + auto* reportsDialogButtonBox = reportsDialog->findChild("buttonBox"); QTest::mouseClick(reportsDialogButtonBox->button(QDialogButtonBox::Close), Qt::LeftButton); QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode); } @@ -2548,21 +2548,21 @@ void TestGui::testExcludedGroupEntryInReports() auto* toolBar = m_mainWindow->findChild("toolBar"); QVERIFY(toolBar); - auto *editGroupAction = m_mainWindow->findChild("actionGroupEdit"); + auto* editGroupAction = m_mainWindow->findChild("actionGroupEdit"); QVERIFY(editGroupAction->isEnabled()); triggerAction("actionGroupEdit"); auto* editGroupWidget = m_dbWidget->findChild("editGroupWidget"); QVERIFY(editGroupWidget); - // Bring up group edit page + // Bring up group edit page QTest::mouseClick(editGroupWidget, Qt::LeftButton); QLineEdit* nameEdit = editGroupWidget->findChild("editName"); QCOMPARE(nameEdit->text(), QString("Entertainment")); // Find database report exclusion checkbox and check it - QCheckBox *excludeGroupFromReportsCheckbox = editGroupWidget->findChild("excludeReportsCheckBox"); + QCheckBox* excludeGroupFromReportsCheckbox = editGroupWidget->findChild("excludeReportsCheckBox"); QVERIFY(excludeGroupFromReportsCheckbox); excludeGroupFromReportsCheckbox->setChecked(true); @@ -2578,7 +2578,7 @@ void TestGui::testExcludedGroupEntryInReports() QVERIFY(entertainmentGroup->excludeFromReports()); // Verify they don't show up in the report - auto *actionReports = m_mainWindow->findChild("actionReports"); + auto* actionReports = m_mainWindow->findChild("actionReports"); QVERIFY(actionReports->isEnabled()); QWidget* actionReportsWidget = toolBar->widgetForAction(actionReports); @@ -2588,13 +2588,13 @@ void TestGui::testExcludedGroupEntryInReports() QTest::mouseClick(actionReportsWidget, Qt::LeftButton); - auto *reportsDialog = m_dbWidget->findChild("reportsDialog"); + auto* reportsDialog = m_dbWidget->findChild("reportsDialog"); QVERIFY(reportsDialog); - CategoryListWidget *categoryList = reportsDialog->findChild("categoryList"); + CategoryListWidget* categoryList = reportsDialog->findChild("categoryList"); categoryList->setCurrentCategory(1); - QStackedWidget *stackedWidget = reportsDialog->findChild("stackedWidget"); + QStackedWidget* stackedWidget = reportsDialog->findChild("stackedWidget"); QVERIFY(stackedWidget); stackedWidget->setCurrentIndex(1); @@ -2602,19 +2602,19 @@ void TestGui::testExcludedGroupEntryInReports() QVERIFY(healthCheckWidget); QTest::mouseClick(healthCheckWidget, Qt::LeftButton); - QTableView *healthTable = healthCheckWidget->findChild("healthcheckTableView"); + QTableView* healthTable = healthCheckWidget->findChild("healthcheckTableView"); QVERIFY(healthTable); QSignalSpy healthCheckWidgetSpy(healthCheckWidget, &ReportsWidgetHealthcheck::tablePopulated); - QAbstractItemModel *healthModel = healthTable->model(); + QAbstractItemModel* healthModel = healthTable->model(); QVERIFY(healthModel); // There should be 3 showing QTRY_COMPARE(healthCheckWidgetSpy.count(), 1); QCOMPARE(healthModel->rowCount(), 5); // account for 2 existing passwords at the start of each test case - QCheckBox *showExcludedCheckBox = healthCheckWidget->findChild("showExcluded"); + QCheckBox* showExcludedCheckBox = healthCheckWidget->findChild("showExcluded"); QVERIFY(showExcludedCheckBox); QCOMPARE(showExcludedCheckBox->isChecked(), false); @@ -2625,11 +2625,11 @@ void TestGui::testExcludedGroupEntryInReports() healthModel = healthTable->model(); QCOMPARE(healthModel->rowCount(), 8); // account for 2 existing passwords at the start of each test case - for(int i = 0; i < healthModel->rowCount(); ++i) { + for (int i = 0; i < healthModel->rowCount(); ++i) { QModelIndex index = healthModel->index(i, 1); QVariant data = healthModel->data(index); - if(data.toString().contains("Netflix")) { + if (data.toString().contains("Netflix")) { auto rect = healthTable->visualRect(index); auto centerPoint = rect.center(); QTest::mouseClick(healthTable->viewport(), Qt::LeftButton, Qt::NoModifier, centerPoint); @@ -2637,9 +2637,9 @@ void TestGui::testExcludedGroupEntryInReports() // QTest::mouseClick(healthCheckWidget, Qt::RightButton); healthCheckWidget->customMenuRequested(centerPoint); - QMenu *menu = healthCheckWidget->findChild("customMenu"); + QMenu* menu = healthCheckWidget->findChild("customMenu"); QVERIFY(menu); - QAction *excludeEntryAction = healthCheckWidget->findChild("contextMenuExcludeAction"); + QAction* excludeEntryAction = healthCheckWidget->findChild("contextMenuExcludeAction"); QVERIFY(excludeEntryAction); MessageBox::setNextAnswer(MessageBox::No); @@ -2651,11 +2651,11 @@ void TestGui::testExcludedGroupEntryInReports() QTRY_COMPARE(healthCheckWidgetSpy.count(), 3); - for(int i = 0; i < healthModel->rowCount(); ++i) { + for (int i = 0; i < healthModel->rowCount(); ++i) { QModelIndex index = healthModel->index(i, 1); QVariant data = healthModel->data(index); - if(data.toString().contains("Netflix")) { + if (data.toString().contains("Netflix")) { QVERIFY(!data.toString().contains("(Group Excluded)")); break; } @@ -2667,7 +2667,7 @@ void TestGui::testExcludedGroupEntryInReports() QCOMPARE(healthModel->rowCount(), 6); // 2 existing passwords from start, 3 from Finance, 1 from Entertainment - auto *reportsDialogButtonBox = reportsDialog->findChild("buttonBox"); + auto* reportsDialogButtonBox = reportsDialog->findChild("buttonBox"); QTest::mouseClick(reportsDialogButtonBox->button(QDialogButtonBox::Close), Qt::LeftButton); QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode); } @@ -2702,7 +2702,7 @@ void TestGui::addCannedEntries() QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); } -void TestGui::addGroup(const QString &name) +void TestGui::addGroup(const QString& name) { // Find buttons for group creation auto* editGroupWidget = m_dbWidget->findChild("editGroupWidget"); @@ -2710,7 +2710,7 @@ void TestGui::addGroup(const QString &name) auto* editGroupWidgetButtonBox = editGroupWidget->findChild("buttonBox"); // Add group with specified name - Group *rootGroup = m_db->rootGroup(); + Group* rootGroup = m_db->rootGroup(); m_dbWidget->groupView()->setCurrentGroup(rootGroup); // Add group on root level m_dbWidget->createGroup(); QTest::keyClicks(nameEdit, name); @@ -2718,7 +2718,7 @@ void TestGui::addGroup(const QString &name) m_dbWidget->groupView()->setCurrentGroup(rootGroup); // Reset to root level } -void TestGui::addEntry(const QString &groupName, const QString &title, const QString &username, const QString &password) +void TestGui::addEntry(const QString& groupName, const QString& title, const QString& username, const QString& password) { // Find buttons auto* toolBar = m_mainWindow->findChild("toolBar"); diff --git a/tests/gui/TestGui.h b/tests/gui/TestGui.h index c9181effe..fee7ab690 100644 --- a/tests/gui/TestGui.h +++ b/tests/gui/TestGui.h @@ -76,8 +76,8 @@ private slots: private: void addCannedEntries(); - void addGroup(const QString &name); - void addEntry(const QString &groupName, const QString &title, const QString &username, const QString &password); + void addGroup(const QString& name); + void addEntry(const QString& groupName, const QString& title, const QString& username, const QString& password); void checkDatabase(const QString& filePath, const QString& expectedDbName); void checkDatabase(const QString& filePath = {}); void triggerAction(const QString& name); From 44daeee9c0df2359e87eea078ccf8ecd7aa294b6 Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Sun, 28 Dec 2025 22:38:56 -0800 Subject: [PATCH 16/29] Test including all entries from excluded group --- tests/gui/TestGui.cpp | 141 +++++++++++++++++++++++++++++++++++++++++- tests/gui/TestGui.h | 1 + 2 files changed, 140 insertions(+), 2 deletions(-) diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 0ebb48681..71fef6729 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -2633,8 +2633,6 @@ void TestGui::testExcludedGroupEntryInReports() auto rect = healthTable->visualRect(index); auto centerPoint = rect.center(); QTest::mouseClick(healthTable->viewport(), Qt::LeftButton, Qt::NoModifier, centerPoint); - // QTest::mouseClick(healthTable->viewport(), Qt::RightButton, Qt::NoModifier, centerPoint); - // QTest::mouseClick(healthCheckWidget, Qt::RightButton); healthCheckWidget->customMenuRequested(centerPoint); QMenu* menu = healthCheckWidget->findChild("customMenu"); @@ -2672,6 +2670,145 @@ void TestGui::testExcludedGroupEntryInReports() QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode); } +void TestGui::testExcludedGroupReincludeAllEntries() +{ + addGroup("Finance"); + addGroup("Entertainment"); + + // Use bad passwords to make sure they all show up in health report + addEntry("Finance", "Chase", "user1", "password"); + addEntry("Finance", "Amex", "user1", "password123"); + addEntry("Finance", "Capital One", "user1", "password456"); + + addEntry("Entertainment", "Netflix", "user1", "password"); + addEntry("Entertainment", "Hulu", "user1", "password321"); + addEntry("Entertainment", "Apple TV", "user1", "password123"); + + Group* entertainmentGroup = m_dbWidget->currentGroup()->findChildByName("Entertainment"); + m_dbWidget->groupView()->setCurrentGroup(entertainmentGroup); + + auto* toolBar = m_mainWindow->findChild("toolBar"); + QVERIFY(toolBar); + + auto* editGroupAction = m_mainWindow->findChild("actionGroupEdit"); + QVERIFY(editGroupAction->isEnabled()); + triggerAction("actionGroupEdit"); + + auto* editGroupWidget = m_dbWidget->findChild("editGroupWidget"); + QVERIFY(editGroupWidget); + + // Bring up group edit page + QTest::mouseClick(editGroupWidget, Qt::LeftButton); + + QLineEdit* nameEdit = editGroupWidget->findChild("editName"); + QCOMPARE(nameEdit->text(), QString("Entertainment")); + + // Find database report exclusion checkbox and check it + QCheckBox* excludeGroupFromReportsCheckbox = editGroupWidget->findChild("excludeReportsCheckBox"); + QVERIFY(excludeGroupFromReportsCheckbox); + + excludeGroupFromReportsCheckbox->setChecked(true); + + auto* editGroupWidgetButtonBox = editGroupWidget->findChild("buttonBox"); + QVERIFY(editGroupWidgetButtonBox); + + // Apply and go back to main view + QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Apply), Qt::LeftButton); + QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); + QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode); + + QVERIFY(entertainmentGroup->excludeFromReports()); + + // Verify they don't show up in the report + auto* actionReports = m_mainWindow->findChild("actionReports"); + QVERIFY(actionReports->isEnabled()); + + QWidget* actionReportsWidget = toolBar->widgetForAction(actionReports); + QVERIFY(actionReportsWidget); + QVERIFY(actionReportsWidget->isVisible()); + QVERIFY(actionReportsWidget->isEnabled()); + + QTest::mouseClick(actionReportsWidget, Qt::LeftButton); + + auto* reportsDialog = m_dbWidget->findChild("reportsDialog"); + QVERIFY(reportsDialog); + + CategoryListWidget* categoryList = reportsDialog->findChild("categoryList"); + categoryList->setCurrentCategory(1); + + QStackedWidget* stackedWidget = reportsDialog->findChild("stackedWidget"); + QVERIFY(stackedWidget); + stackedWidget->setCurrentIndex(1); + + ReportsWidgetHealthcheck* healthCheckWidget = reportsDialog->findChild(); + QVERIFY(healthCheckWidget); + + QTest::mouseClick(healthCheckWidget, Qt::LeftButton); + QTableView* healthTable = healthCheckWidget->findChild("healthcheckTableView"); + QVERIFY(healthTable); + + QSignalSpy healthCheckWidgetSpy(healthCheckWidget, &ReportsWidgetHealthcheck::tablePopulated); + + QAbstractItemModel* healthModel = healthTable->model(); + QVERIFY(healthModel); + + // There should be 3 showing + QTRY_COMPARE(healthCheckWidgetSpy.count(), 1); + QCOMPARE(healthModel->rowCount(), 5); // account for 2 existing passwords at the start of each test case + + QCheckBox* showExcludedCheckBox = healthCheckWidget->findChild("showExcluded"); + QVERIFY(showExcludedCheckBox); + QCOMPARE(showExcludedCheckBox->isChecked(), false); + + showExcludedCheckBox->click(); + QVERIFY(showExcludedCheckBox->isChecked()); + QTRY_COMPARE(healthCheckWidgetSpy.count(), 2); + + healthModel = healthTable->model(); + QCOMPARE(healthModel->rowCount(), 8); // account for 2 existing passwords at the start of each test case + + for (int i = 0; i < healthModel->rowCount(); ++i) { + QModelIndex index = healthModel->index(i, 1); + QVariant data = healthModel->data(index); + + if (data.toString().contains("Netflix")) { + auto rect = healthTable->visualRect(index); + auto centerPoint = rect.center(); + QTest::mouseClick(healthTable->viewport(), Qt::LeftButton, Qt::NoModifier, centerPoint); + healthCheckWidget->customMenuRequested(centerPoint); + + QMenu* menu = healthCheckWidget->findChild("customMenu"); + QVERIFY(menu); + QAction* excludeEntryAction = healthCheckWidget->findChild("contextMenuExcludeAction"); + QVERIFY(excludeEntryAction); + MessageBox::setNextAnswer(MessageBox::Yes); + + excludeEntryAction->trigger(); + QApplication::processEvents(); + break; + } + } + + QTRY_COMPARE(healthCheckWidgetSpy.count(), 3); + + for (int i = 0; i < healthModel->rowCount(); ++i) { + QModelIndex index = healthModel->index(i, 1); + QVariant data = healthModel->data(index); + + QVERIFY(!data.toString().contains("(Group Excluded)")); + } + + showExcludedCheckBox->click(); + QVERIFY(!showExcludedCheckBox->isChecked()); + QTRY_COMPARE(healthCheckWidgetSpy.count(), 4); + + QCOMPARE(healthModel->rowCount(), 8); // 2 existing passwords from start, 3 from Finance, 3 from Entertainment + + auto* reportsDialogButtonBox = reportsDialog->findChild("buttonBox"); + QTest::mouseClick(reportsDialogButtonBox->button(QDialogButtonBox::Close), Qt::LeftButton); + QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode); +} + void TestGui::addCannedEntries() { // Find buttons diff --git a/tests/gui/TestGui.h b/tests/gui/TestGui.h index fee7ab690..ef6c3d61b 100644 --- a/tests/gui/TestGui.h +++ b/tests/gui/TestGui.h @@ -73,6 +73,7 @@ private slots: void testMenuActionStates(); void testDatabaseReports(); void testExcludedGroupEntryInReports(); + void testExcludedGroupReincludeAllEntries(); private: void addCannedEntries(); From 06f6b31d635f9e4de44f070863189c696ec1e234 Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Sun, 28 Dec 2025 22:43:21 -0800 Subject: [PATCH 17/29] Missed Copilot feedback --- src/gui/reports/ReportsWidgetStatistics.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/reports/ReportsWidgetStatistics.cpp b/src/gui/reports/ReportsWidgetStatistics.cpp index 276389dc3..ff0b305ad 100644 --- a/src/gui/reports/ReportsWidgetStatistics.cpp +++ b/src/gui/reports/ReportsWidgetStatistics.cpp @@ -124,7 +124,7 @@ void ReportsWidgetStatistics::calculateStats() addStatsRow(tr("Groups excluded from reports"), QString::number(stats->excludedGroups), stats->excludedGroups > 0, - tr("Excluding entire groups from reports isn't necessarily a problem but please exercise caution" + tr("Excluding entire groups from reports isn't necessarily a problem but please exercise caution " "when excluding entire groups.")); addStatsRow(tr("Average password length"), tr("%1 character(s)", "", stats->averagePwdLength()).arg(stats->averagePwdLength()), From 6042ee15d7b61f8ba0e1025bd4df8d6decd88336 Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Mon, 29 Dec 2025 15:53:58 -0800 Subject: [PATCH 18/29] Revert "Move grid into vertical layout and add checkbox" This reverts commit e23aee1f67b1a9599f712c29a956a26260b0c439. --- src/gui/group/EditGroupWidgetMain.ui | 301 +++++++++++++-------------- 1 file changed, 146 insertions(+), 155 deletions(-) diff --git a/src/gui/group/EditGroupWidgetMain.ui b/src/gui/group/EditGroupWidgetMain.ui index 93511d8e9..faa8a30ff 100644 --- a/src/gui/group/EditGroupWidgetMain.ui +++ b/src/gui/group/EditGroupWidgetMain.ui @@ -37,7 +37,7 @@ 523 - + 0 @@ -50,188 +50,180 @@ 0 - - - - + + 10 + + + 8 + + + + + Toggle expiration + + + Expires: + + + + + + + Name field + + + + + + + false + + + Expiration field + + + true + + + + + + + Use default Auto-Type sequence of parent group + + + + + + + Auto-Type: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Search: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Auto-Type toggle for this and sub groups + + + + + + + - Search: + Notes: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - Use default Auto-Type sequence of parent group + + + + Qt::Vertical - - - - - - Auto-Type toggle for this and sub groups - - - - - - - - 0 - 0 - - - + - 16777215 - 120 + 20 + 0 - - Notes field - - + - - - - Toggle expiration + + + + + + + + Qt::Horizontal - - Expires: + + QSizePolicy::Fixed - + + + 30 + 0 + + + - - - - Search toggle for this and sub groups - - - - - + + false - Expiration field + Default auto-type sequence field - - true - - - - - - - Name: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Set default Auto-Type sequence - - - - - - - Name field - - - - - - - - - Notes: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Qt::Vertical - - - - 20 - 0 - - - - - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 30 - 0 - - - - - - - - false - - - Default auto-type sequence field - - - - - - - - - - - - Auto-Type: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + - - - - Toggle exclusion from database reports + + + + + 0 + 0 + - - Exclude from database reports + + + 16777215 + 120 + + + + Notes field - + + + + Name: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Set default Auto-Type sequence + + + + + + + Search toggle for this and sub groups + + + + Qt::Vertical @@ -257,7 +249,6 @@ autoTypeSequenceInherit autoTypeSequenceCustomRadio autoTypeSequenceCustomEdit - excludeReportsCheckBox From 643ad2c792337638a8a3fb43a7ca59dd85a0fa08 Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Mon, 29 Dec 2025 16:16:08 -0800 Subject: [PATCH 19/29] Remove grid layout and add exclude checkbox --- src/gui/group/EditGroupWidgetMain.ui | 117 ++++++++++++++------------- 1 file changed, 62 insertions(+), 55 deletions(-) diff --git a/src/gui/group/EditGroupWidgetMain.ui b/src/gui/group/EditGroupWidgetMain.ui index faa8a30ff..26af28b6e 100644 --- a/src/gui/group/EditGroupWidgetMain.ui +++ b/src/gui/group/EditGroupWidgetMain.ui @@ -37,7 +37,7 @@ 523 - + 0 @@ -56,7 +56,7 @@ 8 - + Toggle expiration @@ -66,10 +66,10 @@ - - - - Name field + + + + Set default Auto-Type sequence @@ -86,30 +86,10 @@ - - - - Use default Auto-Type sequence of parent group - - - - - - - Auto-Type: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Search: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + Name field @@ -120,6 +100,13 @@ + + + + Search toggle for this and sub groups + + + @@ -199,31 +186,7 @@ - - - - Name: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Set default Auto-Type sequence - - - - - - - Search toggle for this and sub groups - - - - + Qt::Vertical @@ -236,6 +199,50 @@ + + + + Search: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Auto-Type: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Use default Auto-Type sequence of parent group + + + + + + + Name: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Exclude from database reports + + + From 1825bcad1da990ed5f0fa4deabb9af56273da389 Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Sun, 28 Dec 2025 17:49:33 -0800 Subject: [PATCH 20/29] Start refactoring report widgets --- src/CMakeLists.txt | 1 + src/gui/reports/ReportsWidgetBase.cpp | 198 ++++++++++++++++++++++++++ src/gui/reports/ReportsWidgetBase.h | 65 +++++++++ 3 files changed, 264 insertions(+) create mode 100644 src/gui/reports/ReportsWidgetBase.cpp create mode 100644 src/gui/reports/ReportsWidgetBase.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6a5eb5d80..c16a25b8d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -194,6 +194,7 @@ set(gui_SOURCES gui/remote/RemoteProcess.cpp gui/remote/RemoteSettings.cpp gui/reports/ReportsWidget.cpp + gui/reports/ReportsWidgetBase.cpp gui/reports/ReportsDialog.cpp gui/reports/ReportsWidgetHealthcheck.cpp gui/reports/ReportsPageHealthcheck.cpp diff --git a/src/gui/reports/ReportsWidgetBase.cpp b/src/gui/reports/ReportsWidgetBase.cpp new file mode 100644 index 000000000..59ccbd4a0 --- /dev/null +++ b/src/gui/reports/ReportsWidgetBase.cpp @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2024 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 "ReportsWidgetBase.h" + +#include "core/Group.h" +#include "core/Metadata.h" +#include "gui/Icons.h" +#include "gui/GuiTools.h" +#include "gui/MessageBox.h" + +#include +#include +#include +#include +#include + +ReportsWidgetBase::ReportsWidgetBase(QWidget* parent) + : QWidget{parent} +{ +} + +void ReportsWidgetBase::customMenuRequestedBase() +{ + auto selected = getTableView()->selectionModel()->selectedRows(); + if (selected.isEmpty()) { + return; + } + + // Create the context menu + const auto menu = new QMenu(this); + menu->setObjectName("customMenu"); + + // 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); + edit->setObjectName("contextMenuEditAction"); + 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 "Expire entry" menu item + const auto expEntry = new QAction(icons()->icon("entry-expire"), tr("Expire Entry(s)…", "", selected.size()), this); + expEntry->setObjectName("contextMenuExpireAction"); + menu->addAction(expEntry); + connect(expEntry, &QAction::triggered, this, &ReportsWidgetBase::expireSelectedEntries); + + // 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, &ReportsWidgetBase::deleteSelectedEntries); + + // Create the "exclude from reports" menu item + const auto excludeAction = new QAction(icons()->icon("reports-exclude"), tr("Exclude Entry(s) from reports"), this); + excludeAction->setObjectName("contextMenuExcludeAction"); + const auto excludeGroupsAction = new QAction(icons()->icon("reports-exclude"), tr("Exclude Group(s) from reports"), this); + excludeGroupsAction->setObjectName("contextMenuxcludeGroupAction"); + + bool isExcluded = false; + bool isGroupExcluded = false; + + for (auto index : selected) { + auto row = m_modelProxy->mapToSource(index).row(); + auto entry = m_rowToEntry[row].second; + if (entry) { + // If at least one entry is excluded switch to inclusion + if (entry->excludeFromReports() || entry->group()->excludeFromReports()) { + isExcluded = true; + } + if (entry->group()->excludeFromReports()) { + isGroupExcluded = true; + } + + break; + } + } + excludeAction->setCheckable(true); + excludeAction->setChecked(isExcluded); + + excludeGroupsAction->setCheckable(true); + excludeGroupsAction->setChecked(isGroupExcluded); + + menu->addAction(excludeAction); + connect(excludeAction, &QAction::toggled, excludeAction, [this, selected](bool checked) { + QSet groups; + + // If we are including entries (checked is false) but a group is excluded, ask the user if they + // would like to include the rest of the group as well (or keep it excluded). + // If they exclude it, we need to include the whole group, and then exclude + // the entries that aren't selected here. + if (!checked) { + for(const auto index : selected) { + auto row = m_modelProxy->mapToSource(index).row(); + auto entry = m_rowToEntry[row].second; + + if (entry) { + auto *group = entry->group(); + if (group->excludeFromReports() && !groups.contains(group)) { + QString msg = tr("The Group for \"%1\" is excluded. Would you like to include all Entries from there as well?").arg(entry->title()); + auto response = MessageBox::question(this, tr("Include Group?"), msg, MessageBox::Yes | MessageBox::No | MessageBox::Cancel, MessageBox::No); + + if (response == MessageBox::Cancel) { + return; + } + else if (response == MessageBox::Yes) { + group->setExcludeFromReports(false); + } + else if (response == MessageBox::No) { + // We'll exclude all entries from the group here and then + // include the selected ones below + group->setExcludeFromReports(false); + group->markAllEntriesExcludedFromReports(); + } + + groups.insert(group); + } + } + } + } + + for (auto index : selected) { + auto row = m_modelProxy->mapToSource(index).row(); + auto entry = m_rowToEntry[row].second; + + // If the containing group is excluded but the user wants to include + // this entry, ask if they want to keep the remaining items in the group + // excluded or included + if (entry) { + entry->setExcludeFromReports(checked); + } + } + updateWidget(); + }); + + menu->addAction(excludeGroupsAction); + connect(excludeGroupsAction, &QAction::toggled, excludeGroupsAction, [this, selected](bool checked) { + for (const auto index : selected) { + auto row = m_modelProxy->mapToSource(index).row(); + auto entry = m_rowToEntry[row].second; + if (entry) { + entry->group()->setExcludeFromReports(checked); + } + } + updateWidget(); + }); +} + +QList ReportsWidgetBase::getSelectedEntries() +{ + QList selectedEntries; + for (auto index : getTableView()->selectionModel()->selectedRows()) { + auto row = m_modelProxy->mapToSource(index).row(); + auto entry = m_rowToEntry[row].second; + if (entry) { + selectedEntries << entry; + } + } + return selectedEntries; +} + +void ReportsWidgetBase::expireSelectedEntries() +{ + for (auto entry : getSelectedEntries()) { + entry->expireNow(); + } + + updateWidget(); +} + +void ReportsWidgetBase::deleteSelectedEntries() +{ + const auto& selectedEntries = getSelectedEntries(); + bool permanent = !m_db->metadata()->recycleBinEnabled(); + + if (GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) { + GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent); + } + + updateWidget(); +} diff --git a/src/gui/reports/ReportsWidgetBase.h b/src/gui/reports/ReportsWidgetBase.h new file mode 100644 index 000000000..5ffa19f0f --- /dev/null +++ b/src/gui/reports/ReportsWidgetBase.h @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024 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_REPORTSWIDGETBASE_H +#define KEEPASSXC_REPORTSWIDGETBASE_H + +#include +#include + +#include "gui/entry/EntryModel.h" + +class Database; +class Entry; +class Group; +class PasswordHealth; +class QSortFilterProxyModel; +class QStandardItemModel; + +/** + * @brief The ReportsWidgetBase class implements functionality common across the various + * database report widgets. + */ +class ReportsWidgetBase : public QWidget +{ + Q_OBJECT +public: + explicit ReportsWidgetBase(QWidget* parent = nullptr); + +protected: + virtual QScopedPointer getTableView() = 0; + virtual void updateWidget() = 0; + void customMenuRequestedBase(); + +public slots: + void customMenuRequested(QPoint); + QList getSelectedEntries(); + void expireSelectedEntries(); + void deleteSelectedEntries(); + +signals: + void entryActivated(Entry*); + +protected: + bool m_calculated = false; + QScopedPointer m_referencesModel; + QScopedPointer m_modelProxy; + QSharedPointer m_db; + QList> m_rowToEntry; +}; + +#endif // KEEPASSXC_REPORTSWIDGETBASE_H From d7cb5b137461b553dd40c87c6a9c8cebf40f059d Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Sun, 28 Dec 2025 18:36:38 -0800 Subject: [PATCH 21/29] Factor out QSortProxyFilterModel implementations Had to delay initialization of these classes in `ReportsWidgetBase` because they caused crashes in the constructor initialization list. This is because the base class hasn't been initialized enough yet. --- src/gui/reports/ProxyModels.h | 72 ++++++++ src/gui/reports/ReportsWidgetBase.cpp | 27 ++- src/gui/reports/ReportsWidgetBase.h | 12 +- src/gui/reports/ReportsWidgetHealthcheck.cpp | 165 +------------------ src/gui/reports/ReportsWidgetHealthcheck.h | 17 +- 5 files changed, 120 insertions(+), 173 deletions(-) create mode 100644 src/gui/reports/ProxyModels.h diff --git a/src/gui/reports/ProxyModels.h b/src/gui/reports/ProxyModels.h new file mode 100644 index 000000000..a76f74f52 --- /dev/null +++ b/src/gui/reports/ProxyModels.h @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2025 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_PROXYMODELS_H +#define KEEPASSXC_PROXYMODELS_H + +#include +#include + +enum class SortProxyModelKind +{ + Default = 0, + Hibp, + Healthcheck, +}; + +class HibpReportSortPoxyModel : public QSortFilterProxyModel +{ +public: + HibpReportSortPoxyModel(QObject* parent) + : QSortFilterProxyModel(parent){} + ~HibpReportSortPoxyModel() override = default; + +protected: + bool lessThan(const QModelIndex& left, const QModelIndex& right) const override + { + // Sort count column by user data + if (left.column() == 2) { + return sourceModel()->data(left, Qt::UserRole).toInt() + < sourceModel()->data(right, Qt::UserRole).toInt(); + } + // Otherwise use default sorting + return QSortFilterProxyModel::lessThan(left, right); + } +}; + +class HealthcheckReportSortProxyModel : public QSortFilterProxyModel +{ +public: + HealthcheckReportSortProxyModel(QObject* parent) + : QSortFilterProxyModel(parent){} + ~HealthcheckReportSortProxyModel() override = default; + +protected: + bool lessThan(const QModelIndex& left, const QModelIndex& right) const override + { + // Check if the display data is a number, convert and compare if so + bool ok = false; + int leftInt = sourceModel()->data(left).toString().toInt(&ok); + if (ok) { + return leftInt < sourceModel()->data(right).toString().toInt(); + } + // Otherwise use default sorting + return QSortFilterProxyModel::lessThan(left, right); + } +}; + +#endif // KEEPASSXC_PROXYMODELS_H diff --git a/src/gui/reports/ReportsWidgetBase.cpp b/src/gui/reports/ReportsWidgetBase.cpp index 59ccbd4a0..fd37f67cb 100644 --- a/src/gui/reports/ReportsWidgetBase.cpp +++ b/src/gui/reports/ReportsWidgetBase.cpp @@ -28,17 +28,36 @@ #include #include #include +#include -ReportsWidgetBase::ReportsWidgetBase(QWidget* parent) +ReportsWidgetBase::ReportsWidgetBase(QWidget* parent, SortProxyModelKind proxyModel) : QWidget{parent} + , m_referencesModel(new QStandardItemModel(this)) { + // We have to initialize this here; if we do it in the constructor initializer list, + // the base object isn't setup enough and the constructor for QSortFilterProxyModel + // crashes. + switch (proxyModel) { + case SortProxyModelKind::Default: + m_modelProxy.reset(new QSortFilterProxyModel(this)); + break; + case SortProxyModelKind::Healthcheck: + m_modelProxy.reset(new HealthcheckReportSortProxyModel(this)); + break; + case SortProxyModelKind::Hibp: + m_modelProxy.reset(new HibpReportSortPoxyModel(this)); + break; + } } -void ReportsWidgetBase::customMenuRequestedBase() +ReportsWidgetBase::~ReportsWidgetBase() +{} + +QMenu *ReportsWidgetBase::customMenuRequestedBase() { auto selected = getTableView()->selectionModel()->selectedRows(); if (selected.isEmpty()) { - return; + return nullptr; } // Create the context menu @@ -161,6 +180,8 @@ void ReportsWidgetBase::customMenuRequestedBase() } updateWidget(); }); + + return menu; } QList ReportsWidgetBase::getSelectedEntries() diff --git a/src/gui/reports/ReportsWidgetBase.h b/src/gui/reports/ReportsWidgetBase.h index 5ffa19f0f..3208cf516 100644 --- a/src/gui/reports/ReportsWidgetBase.h +++ b/src/gui/reports/ReportsWidgetBase.h @@ -19,9 +19,9 @@ #define KEEPASSXC_REPORTSWIDGETBASE_H #include -#include #include "gui/entry/EntryModel.h" +#include "gui/reports/ProxyModels.h" class Database; class Entry; @@ -29,6 +29,8 @@ class Group; class PasswordHealth; class QSortFilterProxyModel; class QStandardItemModel; +class QMenu; +class QTableView; /** * @brief The ReportsWidgetBase class implements functionality common across the various @@ -38,15 +40,15 @@ class ReportsWidgetBase : public QWidget { Q_OBJECT public: - explicit ReportsWidgetBase(QWidget* parent = nullptr); + explicit ReportsWidgetBase(QWidget* parent, SortProxyModelKind); + virtual ~ReportsWidgetBase(); protected: - virtual QScopedPointer getTableView() = 0; + virtual QTableView *getTableView() = 0; virtual void updateWidget() = 0; - void customMenuRequestedBase(); + QMenu *customMenuRequestedBase(); public slots: - void customMenuRequested(QPoint); QList getSelectedEntries(); void expireSelectedEntries(); void deleteSelectedEntries(); diff --git a/src/gui/reports/ReportsWidgetHealthcheck.cpp b/src/gui/reports/ReportsWidgetHealthcheck.cpp index ee77eadf4..92edb0e2f 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.cpp +++ b/src/gui/reports/ReportsWidgetHealthcheck.cpp @@ -26,6 +26,7 @@ #include "gui/Icons.h" #include "gui/MessageBox.h" #include "gui/styles/StateColorPalette.h" +#include "gui/reports/ProxyModels.h" #include #include @@ -138,10 +139,8 @@ Health::Health(QSharedPointer db) } ReportsWidgetHealthcheck::ReportsWidgetHealthcheck(QWidget* parent) - : QWidget(parent) + : ReportsWidgetBase(parent, SortProxyModelKind::Healthcheck) , m_ui(new Ui::ReportsWidgetHealthcheck()) - , m_referencesModel(new QStandardItemModel(this)) - , m_modelProxy(new ReportSortProxyModel(this)) { m_ui->setupUi(this); @@ -324,137 +323,12 @@ void ReportsWidgetHealthcheck::emitEntryActivated(const QModelIndex& index) void ReportsWidgetHealthcheck::customMenuRequested(QPoint pos) { - auto selected = m_ui->healthcheckTableView->selectionModel()->selectedRows(); - if (selected.isEmpty()) { + auto menu = customMenuRequestedBase(); + + if(!menu) { return; } - // Create the context menu - const auto menu = new QMenu(this); - menu->setObjectName("customMenu"); - - // 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); - edit->setObjectName("contextMenuEditAction"); - 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 "Expire entry" menu item - const auto expEntry = new QAction(icons()->icon("entry-expire"), tr("Expire Entry(s)…", "", selected.size()), this); - expEntry->setObjectName("contextMenuExpireAction"); - menu->addAction(expEntry); - connect(expEntry, &QAction::triggered, this, &ReportsWidgetHealthcheck::expireSelectedEntries); - - // 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 excludeAction = new QAction(icons()->icon("reports-exclude"), tr("Exclude Entry(s) from reports"), this); - excludeAction->setObjectName("contextMenuExcludeAction"); - const auto excludeGroupsAction = - new QAction(icons()->icon("reports-exclude"), tr("Exclude Group(s) from reports"), this); - excludeGroupsAction->setObjectName("contextMenuExcludeGroupAction"); - - bool isExcluded = false; - bool isGroupExcluded = false; - - for (auto index : selected) { - auto row = m_modelProxy->mapToSource(index).row(); - auto entry = m_rowToEntry[row].second; - if (entry) { - // If at least one entry is excluded switch to inclusion - if (entry->excludeFromReports() || entry->group()->excludeFromReports()) { - isExcluded = true; - } - if (entry->group()->excludeFromReports()) { - isGroupExcluded = true; - } - - break; - } - } - excludeAction->setCheckable(true); - excludeAction->setChecked(isExcluded); - - excludeGroupsAction->setCheckable(true); - excludeGroupsAction->setChecked(isGroupExcluded); - - menu->addAction(excludeAction); - connect(excludeAction, &QAction::toggled, excludeAction, [this, selected](bool checked) { - QSet groups; - - // If we are including entries (checked is false) but a group is excluded, ask the user if they - // would like to include the rest of the group as well (or keep it excluded). - // If they exclude it, we need to include the whole group, and then exclude - // the entries that aren't selected here. - if (!checked) { - for (const auto index : selected) { - auto row = m_modelProxy->mapToSource(index).row(); - auto entry = m_rowToEntry[row].second; - - if (entry) { - auto* group = entry->group(); - if (group->excludeFromReports() && !groups.contains(group)) { - QString msg = tr("The Group for \"%1\" is excluded. Would you like to include all Entries from " - "there as well?") - .arg(entry->title()); - auto response = MessageBox::question(this, - tr("Include Group?"), - msg, - MessageBox::Yes | MessageBox::No | MessageBox::Cancel, - MessageBox::No); - - if (response == MessageBox::Cancel) { - return; - } else if (response == MessageBox::Yes) { - group->setExcludeFromReports(false); - } else if (response == MessageBox::No) { - // We'll exclude all entries from the group here and then - // include the selected ones below - group->setExcludeFromReports(false); - group->markAllEntriesExcludedFromReports(); - } - - groups.insert(group); - } - } - } - } - - for (auto index : selected) { - auto row = m_modelProxy->mapToSource(index).row(); - auto entry = m_rowToEntry[row].second; - - // If the containing group is excluded but the user wants to include - // this entry, ask if they want to keep the remaining items in the group - // excluded or included - if (entry) { - entry->setExcludeFromReports(checked); - } - } - calculateHealth(); - }); - - menu->addAction(excludeGroupsAction); - connect(excludeGroupsAction, &QAction::toggled, excludeGroupsAction, [this, selected](bool checked) { - for (const auto index : selected) { - auto row = m_modelProxy->mapToSource(index).row(); - auto entry = m_rowToEntry[row].second; - if (entry) { - entry->group()->setExcludeFromReports(checked); - } - } - calculateHealth(); - }); - // Show the context menu menu->popup(m_ui->healthcheckTableView->viewport()->mapToGlobal(pos)); } @@ -464,35 +338,12 @@ void ReportsWidgetHealthcheck::saveSettings() // nothing to do - the tab is passive } -QList ReportsWidgetHealthcheck::getSelectedEntries() +void ReportsWidgetHealthcheck::updateWidget() { - 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; - } - } - return selectedEntries; -} - -void ReportsWidgetHealthcheck::expireSelectedEntries() -{ - for (auto entry : getSelectedEntries()) { - entry->expireNow(); - } - calculateHealth(); } -void ReportsWidgetHealthcheck::deleteSelectedEntries() +QTableView *ReportsWidgetHealthcheck::getTableView() { - QList selectedEntries = getSelectedEntries(); - bool permanent = !m_db->metadata()->recycleBinEnabled(); - if (GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) { - GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent); - } - - calculateHealth(); + return m_ui->healthcheckTableView; } diff --git a/src/gui/reports/ReportsWidgetHealthcheck.h b/src/gui/reports/ReportsWidgetHealthcheck.h index f5a27d678..cbe0e9d55 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.h +++ b/src/gui/reports/ReportsWidgetHealthcheck.h @@ -19,6 +19,7 @@ #define KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H #include "gui/entry/EntryModel.h" +#include "gui/reports/ReportsWidgetBase.h" #include class Database; @@ -27,13 +28,14 @@ class Group; class PasswordHealth; class QSortFilterProxyModel; class QStandardItemModel; +class QTableView; namespace Ui { class ReportsWidgetHealthcheck; } -class ReportsWidgetHealthcheck : public QWidget +class ReportsWidgetHealthcheck : public ReportsWidgetBase { Q_OBJECT public: @@ -45,6 +47,8 @@ public: protected: void showEvent(QShowEvent* event) override; + void updateWidget() override; + QTableView *getTableView() override; signals: void entryActivated(Entry*); @@ -54,9 +58,6 @@ public slots: void calculateHealth(); void emitEntryActivated(const QModelIndex& index); void customMenuRequested(QPoint); - QList getSelectedEntries(); - void expireSelectedEntries(); - void deleteSelectedEntries(); private: void addHealthRow(QSharedPointer, Group*, Entry*, bool excluded); @@ -64,10 +65,10 @@ private: QScopedPointer m_ui; bool m_healthCalculated = false; - QScopedPointer m_referencesModel; - QScopedPointer m_modelProxy; - QSharedPointer m_db; - QList> m_rowToEntry; + // QScopedPointer m_referencesModel; + // QScopedPointer m_modelProxy; + // QSharedPointer m_db; + // QList> m_rowToEntry; }; #endif // KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H From bcac630a9f792aa52a18e5cfdce2db72e388e529 Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Mon, 29 Dec 2025 02:44:56 -0800 Subject: [PATCH 22/29] Use `ReportsWidgetBase` for browser statistics --- src/gui/reports/ProxyModels.h | 13 +- src/gui/reports/ReportsWidgetBase.cpp | 13 ++ src/gui/reports/ReportsWidgetBase.h | 4 +- .../ReportsWidgetBrowserStatistics.cpp | 179 ++---------------- .../reports/ReportsWidgetBrowserStatistics.h | 13 +- src/gui/reports/ReportsWidgetHealthcheck.cpp | 2 +- src/gui/reports/ReportsWidgetHealthcheck.h | 6 +- 7 files changed, 42 insertions(+), 188 deletions(-) diff --git a/src/gui/reports/ProxyModels.h b/src/gui/reports/ProxyModels.h index a76f74f52..4d7481783 100644 --- a/src/gui/reports/ProxyModels.h +++ b/src/gui/reports/ProxyModels.h @@ -18,8 +18,8 @@ #ifndef KEEPASSXC_PROXYMODELS_H #define KEEPASSXC_PROXYMODELS_H -#include #include +#include enum class SortProxyModelKind { @@ -32,7 +32,9 @@ class HibpReportSortPoxyModel : public QSortFilterProxyModel { public: HibpReportSortPoxyModel(QObject* parent) - : QSortFilterProxyModel(parent){} + : QSortFilterProxyModel(parent) + { + } ~HibpReportSortPoxyModel() override = default; protected: @@ -40,8 +42,7 @@ protected: { // Sort count column by user data if (left.column() == 2) { - return sourceModel()->data(left, Qt::UserRole).toInt() - < sourceModel()->data(right, Qt::UserRole).toInt(); + return sourceModel()->data(left, Qt::UserRole).toInt() < sourceModel()->data(right, Qt::UserRole).toInt(); } // Otherwise use default sorting return QSortFilterProxyModel::lessThan(left, right); @@ -52,7 +53,9 @@ class HealthcheckReportSortProxyModel : public QSortFilterProxyModel { public: HealthcheckReportSortProxyModel(QObject* parent) - : QSortFilterProxyModel(parent){} + : QSortFilterProxyModel(parent) + { + } ~HealthcheckReportSortProxyModel() override = default; protected: diff --git a/src/gui/reports/ReportsWidgetBase.cpp b/src/gui/reports/ReportsWidgetBase.cpp index fd37f67cb..ac28af320 100644 --- a/src/gui/reports/ReportsWidgetBase.cpp +++ b/src/gui/reports/ReportsWidgetBase.cpp @@ -184,6 +184,19 @@ QMenu *ReportsWidgetBase::customMenuRequestedBase() return menu; } +QList ReportsWidgetBase::getSelectedEntries() const +{ + QList selectedEntries; + for (auto index : getTableView()->selectionModel()->selectedRows()) { + auto row = m_modelProxy->mapToSource(index).row(); + auto entry = m_rowToEntry[row].second; + if (entry) { + selectedEntries << entry; + } + } + return selectedEntries; +} + QList ReportsWidgetBase::getSelectedEntries() { QList selectedEntries; diff --git a/src/gui/reports/ReportsWidgetBase.h b/src/gui/reports/ReportsWidgetBase.h index 3208cf516..0fac24d34 100644 --- a/src/gui/reports/ReportsWidgetBase.h +++ b/src/gui/reports/ReportsWidgetBase.h @@ -44,9 +44,11 @@ public: virtual ~ReportsWidgetBase(); protected: - virtual QTableView *getTableView() = 0; + virtual QTableView *getTableView() const = 0; virtual void updateWidget() = 0; + QMenu *customMenuRequestedBase(); + QList getSelectedEntries() const; public slots: QList getSelectedEntries(); diff --git a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp index 25f57aced..dfd56e9fc 100644 --- a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp +++ b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp @@ -93,10 +93,8 @@ BrowserStatistics::BrowserStatistics(QSharedPointer db) } ReportsWidgetBrowserStatistics::ReportsWidgetBrowserStatistics(QWidget* parent) - : QWidget(parent) + : ReportsWidgetBase(parent, SortProxyModelKind::Default) , m_ui(new Ui::ReportsWidgetBrowserStatistics()) - , m_referencesModel(new QStandardItemModel(this)) - , m_modelProxy(new QSortFilterProxyModel(this)) { m_ui->setupUi(this); @@ -265,35 +263,13 @@ void ReportsWidgetBrowserStatistics::emitEntryActivated(const QModelIndex& index void ReportsWidgetBrowserStatistics::customMenuRequested(QPoint pos) { - auto selected = m_ui->browserStatisticsTableView->selectionModel()->selectedRows(); - if (selected.isEmpty()) { + auto menu = customMenuRequestedBase(); + + if(!menu) { return; } - // Create the context menu - const auto menu = new QMenu(this); - - // 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 "expire entry" menu item - const auto expEntry = new QAction(icons()->icon("entry-expire"), tr("Expire Entry(s)…", "", selected.size()), this); - menu->addAction(expEntry); - connect(expEntry, &QAction::triggered, this, &ReportsWidgetBrowserStatistics::expireSelectedEntries); - - // Create the "delete entry" menu item - const auto deleteEntry = - new QAction(icons()->icon("entry-delete"), tr("Delete Entry(s)…", "", selected.size()), this); - menu->addAction(deleteEntry); - connect(deleteEntry, &QAction::triggered, this, &ReportsWidgetBrowserStatistics::deleteSelectedEntries); + auto selected = getTableView()->selectionModel()->selectedRows(); // Create the "delete plugin data" menu item const auto deletePluginData = @@ -304,99 +280,6 @@ void ReportsWidgetBrowserStatistics::customMenuRequested(QPoint pos) this, &ReportsWidgetBrowserStatistics::deletePluginDataFromSelectedEntries); - // Create the "exclude from reports" menu item - const auto excludeAction = new QAction(icons()->icon("reports-exclude"), tr("Exclude Entry(s) from reports"), this); - const auto excludeGroupsAction = - new QAction(icons()->icon("reports-exclude"), tr("Exclude Group(s) from reports"), this); - - bool isExcluded = false; - bool isGroupExcluded = false; - - for (auto index : selected) { - auto row = m_modelProxy->mapToSource(index).row(); - auto entry = m_rowToEntry[row].second; - if (entry) { - // If at least one entry is excluded switch to inclusion - if (entry->excludeFromReports() || entry->group()->excludeFromReports()) { - isExcluded = true; - } - if (entry->group()->excludeFromReports()) { - isGroupExcluded = true; - } - - break; - } - } - excludeAction->setCheckable(true); - excludeAction->setChecked(isExcluded); - - excludeGroupsAction->setCheckable(true); - excludeGroupsAction->setChecked(isGroupExcluded); - - menu->addAction(excludeAction); - connect(excludeAction, &QAction::toggled, excludeAction, [this, selected](bool checked) { - QSet groups; - - // If we are including entries (checked is false) but a group is excluded, ask the user if they - // would like to include the rest of the group as well (or keep it excluded). - // If they exclude it, we need to include the whole group, and then exclude - // the entries that aren't selected here. - if (!checked) { - for (const auto index : selected) { - auto row = m_modelProxy->mapToSource(index).row(); - auto entry = m_rowToEntry[row].second; - - if (entry) { - auto* group = entry->group(); - if (group->excludeFromReports() && !groups.contains(group)) { - QString msg = tr("The Group for \"%1\" is excluded. Would you like to include all Entries from " - "there as well?") - .arg(entry->title()); - auto response = MessageBox::question(this, - tr("Include Group?"), - msg, - MessageBox::Yes | MessageBox::No | MessageBox::Cancel, - MessageBox::No); - - if (response == MessageBox::Cancel) { - return; - } else if (response == MessageBox::Yes) { - group->setExcludeFromReports(false); - } else if (response == MessageBox::No) { - // We'll exclude all entries from the group here and then - // include the selected ones below - group->setExcludeFromReports(false); - group->markAllEntriesExcludedFromReports(); - } - - groups.insert(group); - } - } - } - } - - for (auto index : selected) { - auto row = m_modelProxy->mapToSource(index).row(); - auto entry = m_rowToEntry[row].second; - if (entry) { - entry->setExcludeFromReports(checked); - } - } - calculateBrowserStatistics(); - }); - - menu->addAction(excludeGroupsAction); - connect(excludeGroupsAction, &QAction::toggled, excludeGroupsAction, [this, selected](bool checked) { - for (const auto index : selected) { - auto row = m_modelProxy->mapToSource(index).row(); - auto entry = m_rowToEntry[row].second; - if (entry) { - entry->group()->setExcludeFromReports(checked); - } - } - calculateBrowserStatistics(); - }); - // Show the context menu menu->popup(m_ui->browserStatisticsTableView->viewport()->mapToGlobal(pos)); } @@ -406,40 +289,6 @@ void ReportsWidgetBrowserStatistics::saveSettings() // Nothing to do - the tab is passive } -QList ReportsWidgetBrowserStatistics::getSelectedEntries() -{ - QList selectedEntries; - for (auto index : m_ui->browserStatisticsTableView->selectionModel()->selectedRows()) { - auto row = m_modelProxy->mapToSource(index).row(); - auto entry = m_rowToEntry[row].second; - if (entry) { - selectedEntries << entry; - } - } - return selectedEntries; -} - -void ReportsWidgetBrowserStatistics::expireSelectedEntries() -{ - for (auto entry : getSelectedEntries()) { - entry->expireNow(); - } - - calculateBrowserStatistics(); -} - -void ReportsWidgetBrowserStatistics::deleteSelectedEntries() -{ - const auto& selectedEntries = getSelectedEntries(); - bool permanent = !m_db->metadata()->recycleBinEnabled(); - - if (GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) { - GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent); - } - - calculateBrowserStatistics(); -} - void ReportsWidgetBrowserStatistics::deletePluginDataFromSelectedEntries() { const auto& selectedEntries = getSelectedEntries(); @@ -488,16 +337,12 @@ QMap ReportsWidgetBrowserStatistics::getBrowserConfigFromE return configList; } -QList ReportsWidgetBrowserStatistics::getSelectedEntries() const +QTableView *ReportsWidgetBrowserStatistics::getTableView() const { - QList selectedEntries; - for (auto index : m_ui->browserStatisticsTableView->selectionModel()->selectedRows()) { - auto row = m_modelProxy->mapToSource(index).row(); - auto entry = m_rowToEntry[row].second; - if (entry) { - selectedEntries << entry; - } - } - - return selectedEntries; + return m_ui->browserStatisticsTableView; +} + +void ReportsWidgetBrowserStatistics::updateWidget() +{ + calculateBrowserStatistics(); } diff --git a/src/gui/reports/ReportsWidgetBrowserStatistics.h b/src/gui/reports/ReportsWidgetBrowserStatistics.h index 9b1cc7d60..845aaa83a 100644 --- a/src/gui/reports/ReportsWidgetBrowserStatistics.h +++ b/src/gui/reports/ReportsWidgetBrowserStatistics.h @@ -19,6 +19,7 @@ #define KEEPASSXC_REPORTSWIDGETBROWSERSTATISTICS_H #include "gui/entry/EntryModel.h" +#include "gui/reports/ReportsWidgetBase.h" #include class Database; @@ -33,7 +34,7 @@ namespace Ui class ReportsWidgetBrowserStatistics; } -class ReportsWidgetBrowserStatistics : public QWidget +class ReportsWidgetBrowserStatistics : public ReportsWidgetBase { Q_OBJECT public: @@ -45,6 +46,8 @@ public: protected: void showEvent(QShowEvent* event) override; + void updateWidget() override; + QTableView *getTableView() const override; signals: void entryActivated(Entry*); @@ -53,23 +56,15 @@ public slots: void calculateBrowserStatistics(); void emitEntryActivated(const QModelIndex& index); void customMenuRequested(QPoint); - QList getSelectedEntries(); - void expireSelectedEntries(); - void deleteSelectedEntries(); void deletePluginDataFromSelectedEntries(); private: void addStatisticsRow(bool hasUrls, bool hasSettings, Group*, Entry*, bool); - QList getSelectedEntries() const; QMap getBrowserConfigFromEntry(Entry* entry) const; QScopedPointer m_ui; bool m_statisticsCalculated = false; - QScopedPointer m_referencesModel; - QScopedPointer m_modelProxy; - QSharedPointer m_db; - QList> m_rowToEntry; }; #endif // KEEPASSXC_REPORTSWIDGETBROWSERSTATISTICS_H diff --git a/src/gui/reports/ReportsWidgetHealthcheck.cpp b/src/gui/reports/ReportsWidgetHealthcheck.cpp index 92edb0e2f..a4debf7c7 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.cpp +++ b/src/gui/reports/ReportsWidgetHealthcheck.cpp @@ -343,7 +343,7 @@ void ReportsWidgetHealthcheck::updateWidget() calculateHealth(); } -QTableView *ReportsWidgetHealthcheck::getTableView() +QTableView *ReportsWidgetHealthcheck::getTableView() const { return m_ui->healthcheckTableView; } diff --git a/src/gui/reports/ReportsWidgetHealthcheck.h b/src/gui/reports/ReportsWidgetHealthcheck.h index cbe0e9d55..fd0c53c24 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.h +++ b/src/gui/reports/ReportsWidgetHealthcheck.h @@ -48,7 +48,7 @@ public: protected: void showEvent(QShowEvent* event) override; void updateWidget() override; - QTableView *getTableView() override; + QTableView *getTableView() const override; signals: void entryActivated(Entry*); @@ -65,10 +65,6 @@ private: QScopedPointer m_ui; bool m_healthCalculated = false; - // QScopedPointer m_referencesModel; - // QScopedPointer m_modelProxy; - // QSharedPointer m_db; - // QList> m_rowToEntry; }; #endif // KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H From be137a2cac959cd581fb1277c19e32a1c98813af Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Mon, 29 Dec 2025 04:15:03 -0800 Subject: [PATCH 23/29] Use ReportsWidgetBase for HIBP report widget --- src/gui/reports/ReportsWidgetHibp.cpp | 212 +++----------------------- src/gui/reports/ReportsWidgetHibp.h | 15 +- 2 files changed, 29 insertions(+), 198 deletions(-) diff --git a/src/gui/reports/ReportsWidgetHibp.cpp b/src/gui/reports/ReportsWidgetHibp.cpp index 21e8ac7fd..7568eb903 100644 --- a/src/gui/reports/ReportsWidgetHibp.cpp +++ b/src/gui/reports/ReportsWidgetHibp.cpp @@ -24,42 +24,17 @@ #include "gui/GuiTools.h" #include "gui/Icons.h" #include "gui/MessageBox.h" +#include "gui/reports/ProxyModels.h" #include #include -#include #include #include -namespace -{ - class ReportSortProxyModel : public QSortFilterProxyModel - { - public: - ReportSortProxyModel(QObject* parent) - : QSortFilterProxyModel(parent){}; - ~ReportSortProxyModel() override = default; - - protected: - bool lessThan(const QModelIndex& left, const QModelIndex& right) const override - { - // Sort count column by user data - if (left.column() == 2) { - return sourceModel()->data(left, Qt::UserRole).toInt() - < sourceModel()->data(right, Qt::UserRole).toInt(); - } - // Otherwise use default sorting - return QSortFilterProxyModel::lessThan(left, right); - } - }; -} // namespace - ReportsWidgetHibp::ReportsWidgetHibp(QWidget* parent) - : QWidget(parent) + : ReportsWidgetBase(parent, SortProxyModelKind::Hibp) , m_ui(new Ui::ReportsWidgetHibp()) - , m_referencesModel(new QStandardItemModel(this)) - , m_modelProxy(new ReportSortProxyModel(this)) { m_ui->setupUi(this); @@ -183,7 +158,7 @@ void ReportsWidgetHibp::makeHibpTable() m_referencesModel->appendRow(row); // Store entry pointer per table row (used in double click handler) - m_rowToEntry.append(entry); + m_rowToEntry.append({group, entry}); } // If there was an error, append the error message to the table @@ -318,12 +293,12 @@ void ReportsWidgetHibp::emitEntryActivated(const QModelIndex& index) // Find which database entry was double-clicked auto mappedIndex = m_modelProxy->mapToSource(index); const auto entry = m_rowToEntry[mappedIndex.row()]; - if (entry) { + if (entry.second) { // Found it, invoke entry editor - m_editedEntry = entry; - m_editedPassword = entry->password(); - m_editedExcluded = entry->excludeFromReports(); - emit entryActivated(const_cast(entry)); + m_editedEntry = entry.second; + m_editedPassword = entry.second->password(); + m_editedExcluded = entry.second->excludeFromReports(); + emit entryActivated(const_cast(entry.second)); } } @@ -362,171 +337,28 @@ void ReportsWidgetHibp::refreshAfterEdit() void ReportsWidgetHibp::customMenuRequested(QPoint pos) { - auto selected = m_ui->hibpTableView->selectionModel()->selectedRows(); - if (selected.isEmpty()) { + // Create the context menu + const auto menu = customMenuRequestedBase(); + + if(!menu) { return; } - // Create the context menu - const auto menu = new QMenu(this); - - // 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 "Expire entry" menu item - const auto expEntry = new QAction(icons()->icon("entry-expire"), tr("Expire Entry(s)…", "", selected.size()), this); - menu->addAction(expEntry); - connect(expEntry, &QAction::triggered, this, &ReportsWidgetHibp::expireSelectedEntries); - - // 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 excludeAction = new QAction(icons()->icon("reports-exclude"), tr("Exclude Entry(s) from reports"), this); - const auto excludeGroupsAction = - new QAction(icons()->icon("reports-exclude"), tr("Exclude Group(s) from reports"), this); - - bool isExcluded = false; - bool isGroupExcluded = false; - - for (auto index : selected) { - auto row = m_modelProxy->mapToSource(index).row(); - auto entry = m_rowToEntry[row]; - if (entry) { - // If at least one entry is excluded switch to inclusion - if (entry->excludeFromReports() || entry->group()->excludeFromReports()) { - isExcluded = true; - } - if (entry->group()->excludeFromReports()) { - isGroupExcluded = true; - } - - break; - } - } - - excludeAction->setCheckable(true); - excludeAction->setChecked(isExcluded); - - excludeGroupsAction->setCheckable(true); - excludeGroupsAction->setChecked(isGroupExcluded); - - menu->addAction(excludeAction); - connect(excludeAction, &QAction::toggled, excludeAction, [this, selected](bool checked) { - QSet groups; - - // If we are including entries (checked is false) but a group is excluded, ask the user if they - // would like to include the rest of the group as well (or keep it excluded). - // If they exclude it, we need to include the whole group, and then exclude - // the entries that aren't selected here. - if (!checked) { - for (const auto index : selected) { - auto row = m_modelProxy->mapToSource(index).row(); - auto entry = m_rowToEntry[row]; - - if (entry) { - auto* group = entry->group(); - if (group->excludeFromReports() && !groups.contains(group)) { - QString msg = tr("The Group for \"%1\" is excluded. Would you like to include all Entries from " - "there as well?") - .arg(entry->title()); - auto response = MessageBox::question(this, - tr("Include Group?"), - msg, - MessageBox::Yes | MessageBox::No | MessageBox::Cancel, - MessageBox::No); - - if (response == MessageBox::Cancel) { - return; - } else if (response == MessageBox::Yes) { - group->setExcludeFromReports(false); - } else if (response == MessageBox::No) { - // We'll exclude all entries from the group here and then - // include the selected ones below - group->setExcludeFromReports(false); - group->markAllEntriesExcludedFromReports(); - } - - groups.insert(group); - } - } - } - } - - for (auto index : selected) { - auto row = m_modelProxy->mapToSource(index).row(); - auto entry = m_rowToEntry[row]; - - // If the containing group is excluded but the user wants to include - // this entry, ask if they want to keep the remaining items in the group - // excluded or included - if (entry) { - entry->setExcludeFromReports(checked); - } - } - makeHibpTable(); - }); - - menu->addAction(excludeGroupsAction); - connect(excludeGroupsAction, &QAction::toggled, excludeGroupsAction, [this, selected](bool checked) { - for (const auto index : selected) { - auto row = m_modelProxy->mapToSource(index).row(); - auto entry = m_rowToEntry[row]; - if (entry) { - entry->group()->setExcludeFromReports(checked); - } - } - makeHibpTable(); - }); - // Show the context menu menu->popup(m_ui->hibpTableView->viewport()->mapToGlobal(pos)); } -QList ReportsWidgetHibp::getSelectedEntries() -{ - 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; - } - } - return selectedEntries; -} - -void ReportsWidgetHibp::expireSelectedEntries() -{ - for (auto entry : getSelectedEntries()) { - entry->expireNow(); - } - - makeHibpTable(); -} - -void ReportsWidgetHibp::deleteSelectedEntries() -{ - QList selectedEntries = getSelectedEntries(); - bool permanent = !m_db->metadata()->recycleBinEnabled(); - if (GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) { - GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent); - } - - makeHibpTable(); -} - void ReportsWidgetHibp::saveSettings() { // nothing to do - the tab is passive } + +void ReportsWidgetHibp::updateWidget() +{ + makeHibpTable(); +} + +QTableView *ReportsWidgetHibp::getTableView() const +{ + return m_ui->hibpTableView; +} diff --git a/src/gui/reports/ReportsWidgetHibp.h b/src/gui/reports/ReportsWidgetHibp.h index 8e0d5e47b..5d6ec7e1f 100644 --- a/src/gui/reports/ReportsWidgetHibp.h +++ b/src/gui/reports/ReportsWidgetHibp.h @@ -20,6 +20,7 @@ #include "config-keepassx.h" #include "gui/entry/EntryModel.h" +#include "gui/reports/ReportsWidgetBase.h" #include @@ -32,13 +33,14 @@ class Entry; class Group; class QSortFilterProxyModel; class QStandardItemModel; +class QTableView; namespace Ui { class ReportsWidgetHibp; } -class ReportsWidgetHibp : public QWidget +class ReportsWidgetHibp : public ReportsWidgetBase { Q_OBJECT public: @@ -49,6 +51,10 @@ public: void saveSettings(); void refreshAfterEdit(); +protected: + void updateWidget() override; + QTableView *getTableView() const override; + signals: void entryActivated(Entry*); @@ -58,22 +64,15 @@ public slots: void fetchFailed(const QString& error); void makeHibpTable(); void customMenuRequested(QPoint); - QList getSelectedEntries(); - void expireSelectedEntries(); - void deleteSelectedEntries(); private: void startValidation(); static QString countToText(int count); QScopedPointer m_ui; - QScopedPointer m_referencesModel; - QScopedPointer m_modelProxy; - QSharedPointer m_db; 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 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 From c9aff28f7ee7c933f0171603af6769ea0a8f3d3a Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Mon, 29 Dec 2025 18:43:56 -0800 Subject: [PATCH 24/29] Move more methods into base class --- src/gui/reports/ReportsWidgetBase.cpp | 34 +++++++++++ src/gui/reports/ReportsWidgetBase.h | 6 +- .../ReportsWidgetBrowserStatistics.cpp | 38 +------------ .../reports/ReportsWidgetBrowserStatistics.h | 6 -- src/gui/reports/ReportsWidgetHealthcheck.cpp | 57 +------------------ src/gui/reports/ReportsWidgetHealthcheck.h | 7 +-- src/gui/reports/ReportsWidgetHibp.cpp | 18 ++---- src/gui/reports/ReportsWidgetHibp.h | 6 +- 8 files changed, 50 insertions(+), 122 deletions(-) diff --git a/src/gui/reports/ReportsWidgetBase.cpp b/src/gui/reports/ReportsWidgetBase.cpp index ac28af320..cc2b3dc8c 100644 --- a/src/gui/reports/ReportsWidgetBase.cpp +++ b/src/gui/reports/ReportsWidgetBase.cpp @@ -53,6 +53,24 @@ ReportsWidgetBase::ReportsWidgetBase(QWidget* parent, SortProxyModelKind proxyMo ReportsWidgetBase::~ReportsWidgetBase() {} +void ReportsWidgetBase::loadSettings(QSharedPointer db) +{ + m_db = std::move(db); + m_widgetDataCalculated = false; + m_referencesModel->clear(); + m_rowToEntry.clear(); + + auto row = QList(); + row << new QStandardItem(tr("Please wait, report is being calculated…")); + m_referencesModel->appendRow(row); +} + +void ReportsWidgetBase::saveSettings() +{ + // Most report tabs are passive, so override them in derived classes if they need to + // save settings +} + QMenu *ReportsWidgetBase::customMenuRequestedBase() { auto selected = getTableView()->selectionModel()->selectedRows(); @@ -230,3 +248,19 @@ void ReportsWidgetBase::deleteSelectedEntries() updateWidget(); } + +void ReportsWidgetBase::emitEntryActivated(const QModelIndex& index) +{ + if (!index.isValid()) { + return; + } + + auto mappedIndex = m_modelProxy->mapToSource(index); + const auto row = m_rowToEntry[mappedIndex.row()]; + const auto group = row.first; + const auto entry = row.second; + + if (group && entry) { + emit entryActivated(const_cast(entry)); + } +} diff --git a/src/gui/reports/ReportsWidgetBase.h b/src/gui/reports/ReportsWidgetBase.h index 0fac24d34..89124fd2d 100644 --- a/src/gui/reports/ReportsWidgetBase.h +++ b/src/gui/reports/ReportsWidgetBase.h @@ -43,6 +43,9 @@ public: explicit ReportsWidgetBase(QWidget* parent, SortProxyModelKind); virtual ~ReportsWidgetBase(); + virtual void loadSettings(QSharedPointer db); + virtual void saveSettings(); + protected: virtual QTableView *getTableView() const = 0; virtual void updateWidget() = 0; @@ -54,12 +57,13 @@ public slots: QList getSelectedEntries(); void expireSelectedEntries(); void deleteSelectedEntries(); + void emitEntryActivated(const QModelIndex& index); signals: void entryActivated(Entry*); protected: - bool m_calculated = false; + bool m_widgetDataCalculated = false; QScopedPointer m_referencesModel; QScopedPointer m_modelProxy; QSharedPointer m_db; diff --git a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp index dfd56e9fc..a1eabed13 100644 --- a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp +++ b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp @@ -24,7 +24,6 @@ #include "core/Metadata.h" #include "gui/GuiTools.h" #include "gui/Icons.h" -#include "gui/MessageBox.h" #include "gui/styles/StateColorPalette.h" #include @@ -174,25 +173,13 @@ void ReportsWidgetBrowserStatistics::addStatisticsRow(bool hasUrls, m_rowToEntry.append({group, entry}); } -void ReportsWidgetBrowserStatistics::loadSettings(QSharedPointer db) -{ - m_db = std::move(db); - m_statisticsCalculated = false; - m_referencesModel->clear(); - m_rowToEntry.clear(); - - auto row = QList(); - row << new QStandardItem(tr("Please wait, browser statistics is being calculated…")); - m_referencesModel->appendRow(row); -} - void ReportsWidgetBrowserStatistics::showEvent(QShowEvent* event) { QWidget::showEvent(event); - if (!m_statisticsCalculated) { + if (!m_widgetDataCalculated) { // Perform stats calculation on next event loop to allow widget to appear - m_statisticsCalculated = true; + m_widgetDataCalculated = true; QTimer::singleShot(0, this, SLOT(calculateBrowserStatistics())); } } @@ -245,22 +232,6 @@ void ReportsWidgetBrowserStatistics::calculateBrowserStatistics() m_ui->browserStatisticsTableView->resizeColumnsToContents(); } -void ReportsWidgetBrowserStatistics::emitEntryActivated(const QModelIndex& index) -{ - if (!index.isValid()) { - return; - } - - auto mappedIndex = m_modelProxy->mapToSource(index); - const auto row = m_rowToEntry[mappedIndex.row()]; - const auto group = row.first; - const auto entry = row.second; - - if (group && entry) { - emit entryActivated(const_cast(entry)); - } -} - void ReportsWidgetBrowserStatistics::customMenuRequested(QPoint pos) { auto menu = customMenuRequestedBase(); @@ -284,11 +255,6 @@ void ReportsWidgetBrowserStatistics::customMenuRequested(QPoint pos) menu->popup(m_ui->browserStatisticsTableView->viewport()->mapToGlobal(pos)); } -void ReportsWidgetBrowserStatistics::saveSettings() -{ - // Nothing to do - the tab is passive -} - void ReportsWidgetBrowserStatistics::deletePluginDataFromSelectedEntries() { const auto& selectedEntries = getSelectedEntries(); diff --git a/src/gui/reports/ReportsWidgetBrowserStatistics.h b/src/gui/reports/ReportsWidgetBrowserStatistics.h index 845aaa83a..9b17d221f 100644 --- a/src/gui/reports/ReportsWidgetBrowserStatistics.h +++ b/src/gui/reports/ReportsWidgetBrowserStatistics.h @@ -41,9 +41,6 @@ public: explicit ReportsWidgetBrowserStatistics(QWidget* parent = nullptr); ~ReportsWidgetBrowserStatistics() override; - void loadSettings(QSharedPointer db); - void saveSettings(); - protected: void showEvent(QShowEvent* event) override; void updateWidget() override; @@ -54,7 +51,6 @@ signals: public slots: void calculateBrowserStatistics(); - void emitEntryActivated(const QModelIndex& index); void customMenuRequested(QPoint); void deletePluginDataFromSelectedEntries(); @@ -63,8 +59,6 @@ private: QMap getBrowserConfigFromEntry(Entry* entry) const; QScopedPointer m_ui; - - bool m_statisticsCalculated = false; }; #endif // KEEPASSXC_REPORTSWIDGETBROWSERSTATISTICS_H diff --git a/src/gui/reports/ReportsWidgetHealthcheck.cpp b/src/gui/reports/ReportsWidgetHealthcheck.cpp index a4debf7c7..7d4a14089 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.cpp +++ b/src/gui/reports/ReportsWidgetHealthcheck.cpp @@ -24,13 +24,11 @@ #include "core/PasswordHealth.h" #include "gui/GuiTools.h" #include "gui/Icons.h" -#include "gui/MessageBox.h" #include "gui/styles/StateColorPalette.h" #include "gui/reports/ProxyModels.h" #include #include -#include #include namespace @@ -77,27 +75,6 @@ namespace QList> m_items; bool m_anyExcludedEntries = false; }; - - class ReportSortProxyModel : public QSortFilterProxyModel - { - public: - ReportSortProxyModel(QObject* parent) - : QSortFilterProxyModel(parent){}; - ~ReportSortProxyModel() override = default; - - protected: - bool lessThan(const QModelIndex& left, const QModelIndex& right) const override - { - // Check if the display data is a number, convert and compare if so - bool ok = false; - int leftInt = sourceModel()->data(left).toString().toInt(&ok); - if (ok) { - return leftInt < sourceModel()->data(right).toString().toInt(); - } - // Otherwise use default sorting - return QSortFilterProxyModel::lessThan(left, right); - } - }; } // namespace Health::Health(QSharedPointer db) @@ -238,15 +215,7 @@ void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer healt void ReportsWidgetHealthcheck::loadSettings(QSharedPointer db) { - m_db = std::move(db); - m_healthCalculated = false; - m_referencesModel->clear(); - m_rowToEntry.clear(); - - auto row = QList(); - row << new QStandardItem(tr("Please wait, health data is being calculated…")); - m_referencesModel->appendRow(row); - // Default sort by first column (health score) + ReportsWidgetBase::loadSettings(db); m_ui->healthcheckTableView->sortByColumn(0, Qt::AscendingOrder); } @@ -254,9 +223,9 @@ void ReportsWidgetHealthcheck::showEvent(QShowEvent* event) { QWidget::showEvent(event); - if (!m_healthCalculated) { + if (!m_widgetDataCalculated) { // Perform stats calculation on next event loop to allow widget to appear - m_healthCalculated = true; + m_widgetDataCalculated = true; QTimer::singleShot(0, this, SLOT(calculateHealth())); } } @@ -306,21 +275,6 @@ void ReportsWidgetHealthcheck::calculateHealth() emit tablePopulated(); } -void ReportsWidgetHealthcheck::emitEntryActivated(const QModelIndex& index) -{ - if (!index.isValid()) { - return; - } - - auto mappedIndex = m_modelProxy->mapToSource(index); - const auto row = m_rowToEntry[mappedIndex.row()]; - const auto group = row.first; - const auto entry = row.second; - if (group && entry) { - emit entryActivated(const_cast(entry)); - } -} - void ReportsWidgetHealthcheck::customMenuRequested(QPoint pos) { auto menu = customMenuRequestedBase(); @@ -333,11 +287,6 @@ void ReportsWidgetHealthcheck::customMenuRequested(QPoint pos) menu->popup(m_ui->healthcheckTableView->viewport()->mapToGlobal(pos)); } -void ReportsWidgetHealthcheck::saveSettings() -{ - // nothing to do - the tab is passive -} - void ReportsWidgetHealthcheck::updateWidget() { calculateHealth(); diff --git a/src/gui/reports/ReportsWidgetHealthcheck.h b/src/gui/reports/ReportsWidgetHealthcheck.h index fd0c53c24..1dec5d8a7 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.h +++ b/src/gui/reports/ReportsWidgetHealthcheck.h @@ -42,8 +42,7 @@ public: explicit ReportsWidgetHealthcheck(QWidget* parent = nullptr); ~ReportsWidgetHealthcheck() override; - void loadSettings(QSharedPointer db); - void saveSettings(); + void loadSettings(QSharedPointer db) override; protected: void showEvent(QShowEvent* event) override; @@ -51,20 +50,16 @@ protected: QTableView *getTableView() const override; signals: - void entryActivated(Entry*); void tablePopulated(); public slots: void calculateHealth(); - void emitEntryActivated(const QModelIndex& index); void customMenuRequested(QPoint); private: void addHealthRow(QSharedPointer, Group*, Entry*, bool excluded); QScopedPointer m_ui; - - bool m_healthCalculated = false; }; #endif // KEEPASSXC_REPORTSWIDGETHEALTHCHECK_H diff --git a/src/gui/reports/ReportsWidgetHibp.cpp b/src/gui/reports/ReportsWidgetHibp.cpp index 7568eb903..44a2d2131 100644 --- a/src/gui/reports/ReportsWidgetHibp.cpp +++ b/src/gui/reports/ReportsWidgetHibp.cpp @@ -23,7 +23,6 @@ #include "core/Metadata.h" #include "gui/GuiTools.h" #include "gui/Icons.h" -#include "gui/MessageBox.h" #include "gui/reports/ProxyModels.h" #include @@ -61,21 +60,17 @@ ReportsWidgetHibp::~ReportsWidgetHibp() = default; void ReportsWidgetHibp::loadSettings(QSharedPointer db) { - // Re-initialize - m_db = std::move(db); - m_referencesModel->clear(); - m_pwndPasswords.clear(); - m_error.clear(); - m_rowToEntry.clear(); - m_editedEntry = nullptr; #ifdef WITH_XC_NETWORKING m_ui->stackedWidget->setCurrentIndex(0); m_ui->validationButton->setEnabled(true); m_ui->progressBar->hide(); #else - // Compiled without networking, can't do anything + // Compiled without networking, can't do anything m_ui->stackedWidget->setCurrentIndex(2); #endif + + ReportsWidgetBase::loadSettings(db); + m_referencesModel->clear(); } /* @@ -348,11 +343,6 @@ void ReportsWidgetHibp::customMenuRequested(QPoint pos) menu->popup(m_ui->hibpTableView->viewport()->mapToGlobal(pos)); } -void ReportsWidgetHibp::saveSettings() -{ - // nothing to do - the tab is passive -} - void ReportsWidgetHibp::updateWidget() { makeHibpTable(); diff --git a/src/gui/reports/ReportsWidgetHibp.h b/src/gui/reports/ReportsWidgetHibp.h index 5d6ec7e1f..8148ea24b 100644 --- a/src/gui/reports/ReportsWidgetHibp.h +++ b/src/gui/reports/ReportsWidgetHibp.h @@ -47,17 +47,13 @@ public: explicit ReportsWidgetHibp(QWidget* parent = nullptr); ~ReportsWidgetHibp() override; - void loadSettings(QSharedPointer db); - void saveSettings(); + void loadSettings(QSharedPointer db) override; void refreshAfterEdit(); protected: void updateWidget() override; QTableView *getTableView() const override; -signals: - void entryActivated(Entry*); - public slots: void emitEntryActivated(const QModelIndex&); void addHibpResult(const QString&, int); From 98f70f4aafe6dbd84d6b604c03ed7e674ad6e939 Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Mon, 29 Dec 2025 20:29:56 -0800 Subject: [PATCH 25/29] Refactor passkey report widget --- src/gui/reports/ReportsWidgetPasskeys.cpp | 78 ++++------------------- src/gui/reports/ReportsWidgetPasskeys.h | 20 ++---- 2 files changed, 18 insertions(+), 80 deletions(-) diff --git a/src/gui/reports/ReportsWidgetPasskeys.cpp b/src/gui/reports/ReportsWidgetPasskeys.cpp index 831f4c721..e76f509f7 100644 --- a/src/gui/reports/ReportsWidgetPasskeys.cpp +++ b/src/gui/reports/ReportsWidgetPasskeys.cpp @@ -30,6 +30,7 @@ #include "gui/passkeys/PasskeyExporter.h" #include "gui/passkeys/PasskeyImporter.h" #include "gui/styles/StateColorPalette.h" +#include "gui/reports/ProxyModels.h" #include #include @@ -87,10 +88,8 @@ PasskeyList::PasskeyList(const QSharedPointer& db) } ReportsWidgetPasskeys::ReportsWidgetPasskeys(QWidget* parent) - : QWidget(parent) + : ReportsWidgetBase(parent, SortProxyModelKind::Default) , m_ui(new Ui::ReportsWidgetPasskeys()) - , m_referencesModel(new QStandardItemModel(this)) - , m_modelProxy(new QSortFilterProxyModel(this)) { m_ui->setupUi(this); @@ -146,25 +145,13 @@ void ReportsWidgetPasskeys::addPasskeyRow(Group* group, Entry* entry) m_rowToEntry.append({group, entry}); } -void ReportsWidgetPasskeys::loadSettings(QSharedPointer db) -{ - m_db = std::move(db); - m_entriesUpdated = false; - m_referencesModel->clear(); - m_rowToEntry.clear(); - - auto row = QList(); - row << new QStandardItem(tr("Please wait, list of entries with passkeys is being updated…")); - m_referencesModel->appendRow(row); -} - void ReportsWidgetPasskeys::showEvent(QShowEvent* event) { QWidget::showEvent(event); - if (!m_entriesUpdated) { + if (!m_widgetDataCalculated) { // Perform stats calculation on next event loop to allow widget to appear - m_entriesUpdated = true; + m_widgetDataCalculated = true; QTimer::singleShot(0, this, SLOT(updateEntries())); } } @@ -200,22 +187,6 @@ void ReportsWidgetPasskeys::updateEntries() m_ui->passkeysTableView->resizeColumnsToContents(); } -void ReportsWidgetPasskeys::emitEntryActivated(const QModelIndex& index) -{ - if (!index.isValid()) { - return; - } - - auto mappedIndex = m_modelProxy->mapToSource(index); - const auto row = m_rowToEntry[mappedIndex.row()]; - const auto group = row.first; - const auto entry = row.second; - - if (group && entry) { - emit entryActivated(entry); - } -} - void ReportsWidgetPasskeys::customMenuRequested(QPoint pos) { auto selected = m_ui->passkeysTableView->selectionModel()->selectedRows(); @@ -246,37 +217,6 @@ void ReportsWidgetPasskeys::customMenuRequested(QPoint pos) menu->popup(m_ui->passkeysTableView->viewport()->mapToGlobal(pos)); } -void ReportsWidgetPasskeys::saveSettings() -{ - // Nothing to do - the tab is passive -} - -void ReportsWidgetPasskeys::deleteSelectedEntries() -{ - auto selectedEntries = getSelectedEntries(); - bool permanent = !m_db->metadata()->recycleBinEnabled(); - - if (GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) { - GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent); - } - - updateEntries(); -} - -QList ReportsWidgetPasskeys::getSelectedEntries() -{ - QList selectedEntries; - for (auto index : m_ui->passkeysTableView->selectionModel()->selectedRows()) { - auto row = m_modelProxy->mapToSource(index).row(); - auto entry = m_rowToEntry[row].second; - if (entry) { - selectedEntries << entry; - } - } - - return selectedEntries; -} - void ReportsWidgetPasskeys::selectionChanged() { m_ui->exportButton->setEnabled(!m_ui->passkeysTableView->selectionModel()->selectedIndexes().isEmpty()); @@ -305,3 +245,13 @@ void ReportsWidgetPasskeys::exportPasskey() PasskeyExporter passkeyExporter(this); passkeyExporter.showExportDialog(getSelectedEntries()); } + +QTableView *ReportsWidgetPasskeys::getTableView() const +{ + return m_ui->passkeysTableView; +} + +void ReportsWidgetPasskeys::updateWidget() +{ + updateEntries(); +} diff --git a/src/gui/reports/ReportsWidgetPasskeys.h b/src/gui/reports/ReportsWidgetPasskeys.h index 3d0593350..3e6115013 100644 --- a/src/gui/reports/ReportsWidgetPasskeys.h +++ b/src/gui/reports/ReportsWidgetPasskeys.h @@ -19,6 +19,7 @@ #define KEEPASSXC_REPORTSWIDGETPASSKEYS_H #include "gui/entry/EntryModel.h" +#include "gui/reports/ReportsWidgetBase.h" #include class Database; @@ -33,27 +34,21 @@ namespace Ui class ReportsWidgetPasskeys; } -class ReportsWidgetPasskeys : public QWidget +class ReportsWidgetPasskeys : public ReportsWidgetBase { Q_OBJECT public: explicit ReportsWidgetPasskeys(QWidget* parent = nullptr); ~ReportsWidgetPasskeys() override; - void loadSettings(QSharedPointer db); - void saveSettings(); - protected: void showEvent(QShowEvent* event) override; - -signals: - void entryActivated(Entry*); + void updateWidget() override; + QTableView *getTableView() const override; public slots: void updateEntries(); - void emitEntryActivated(const QModelIndex& index); void customMenuRequested(QPoint); - void deleteSelectedEntries(); private slots: void selectionChanged(); @@ -62,15 +57,8 @@ private slots: private: void addPasskeyRow(Group*, Entry*); - QList getSelectedEntries(); QScopedPointer m_ui; - - bool m_entriesUpdated = false; - QScopedPointer m_referencesModel; - QScopedPointer m_modelProxy; - QSharedPointer m_db; - QList> m_rowToEntry; }; #endif // KEEPASSXC_REPORTSWIDGETPASSKEYS_H From 6036e7fa7b8661cef8fcea307c7b8a6d0eb47fd7 Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Mon, 29 Dec 2025 21:14:26 -0800 Subject: [PATCH 26/29] Fix formatting --- src/gui/reports/ReportsWidgetBase.cpp | 56 ++++++++++--------- src/gui/reports/ReportsWidgetBase.h | 4 +- .../ReportsWidgetBrowserStatistics.cpp | 4 +- .../reports/ReportsWidgetBrowserStatistics.h | 2 +- src/gui/reports/ReportsWidgetHealthcheck.cpp | 6 +- src/gui/reports/ReportsWidgetHealthcheck.h | 2 +- src/gui/reports/ReportsWidgetHibp.cpp | 6 +- src/gui/reports/ReportsWidgetHibp.h | 2 +- src/gui/reports/ReportsWidgetPasskeys.cpp | 4 +- src/gui/reports/ReportsWidgetPasskeys.h | 2 +- tests/gui/TestGui.cpp | 12 ++-- 11 files changed, 53 insertions(+), 47 deletions(-) diff --git a/src/gui/reports/ReportsWidgetBase.cpp b/src/gui/reports/ReportsWidgetBase.cpp index cc2b3dc8c..3f3506b31 100644 --- a/src/gui/reports/ReportsWidgetBase.cpp +++ b/src/gui/reports/ReportsWidgetBase.cpp @@ -19,13 +19,13 @@ #include "core/Group.h" #include "core/Metadata.h" -#include "gui/Icons.h" #include "gui/GuiTools.h" +#include "gui/Icons.h" #include "gui/MessageBox.h" -#include #include #include +#include #include #include #include @@ -51,7 +51,8 @@ ReportsWidgetBase::ReportsWidgetBase(QWidget* parent, SortProxyModelKind proxyMo } ReportsWidgetBase::~ReportsWidgetBase() -{} +{ +} void ReportsWidgetBase::loadSettings(QSharedPointer db) { @@ -71,18 +72,18 @@ void ReportsWidgetBase::saveSettings() // save settings } -QMenu *ReportsWidgetBase::customMenuRequestedBase() +QMenu* ReportsWidgetBase::customMenuRequestedBase() { auto selected = getTableView()->selectionModel()->selectedRows(); if (selected.isEmpty()) { return nullptr; } - // Create the context menu + // Create the context menu const auto menu = new QMenu(this); menu->setObjectName("customMenu"); - // Create the "edit entry" menu item (only if 1 row is selected) + // 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); edit->setObjectName("contextMenuEditAction"); @@ -94,21 +95,22 @@ QMenu *ReportsWidgetBase::customMenuRequestedBase() }); } - // Create the "Expire entry" menu item + // Create the "Expire entry" menu item const auto expEntry = new QAction(icons()->icon("entry-expire"), tr("Expire Entry(s)…", "", selected.size()), this); expEntry->setObjectName("contextMenuExpireAction"); menu->addAction(expEntry); connect(expEntry, &QAction::triggered, this, &ReportsWidgetBase::expireSelectedEntries); - // Create the "delete entry" menu item + // 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, &ReportsWidgetBase::deleteSelectedEntries); - // Create the "exclude from reports" menu item + // Create the "exclude from reports" menu item const auto excludeAction = new QAction(icons()->icon("reports-exclude"), tr("Exclude Entry(s) from reports"), this); excludeAction->setObjectName("contextMenuExcludeAction"); - const auto excludeGroupsAction = new QAction(icons()->icon("reports-exclude"), tr("Exclude Group(s) from reports"), this); + const auto excludeGroupsAction = + new QAction(icons()->icon("reports-exclude"), tr("Exclude Group(s) from reports"), this); excludeGroupsAction->setObjectName("contextMenuxcludeGroupAction"); bool isExcluded = false; @@ -139,28 +141,32 @@ QMenu *ReportsWidgetBase::customMenuRequestedBase() connect(excludeAction, &QAction::toggled, excludeAction, [this, selected](bool checked) { QSet groups; - // If we are including entries (checked is false) but a group is excluded, ask the user if they - // would like to include the rest of the group as well (or keep it excluded). - // If they exclude it, we need to include the whole group, and then exclude - // the entries that aren't selected here. + // If we are including entries (checked is false) but a group is excluded, ask the user if they + // would like to include the rest of the group as well (or keep it excluded). + // If they exclude it, we need to include the whole group, and then exclude + // the entries that aren't selected here. if (!checked) { - for(const auto index : selected) { + for (const auto index : selected) { auto row = m_modelProxy->mapToSource(index).row(); auto entry = m_rowToEntry[row].second; if (entry) { - auto *group = entry->group(); + auto* group = entry->group(); if (group->excludeFromReports() && !groups.contains(group)) { - QString msg = tr("The Group for \"%1\" is excluded. Would you like to include all Entries from there as well?").arg(entry->title()); - auto response = MessageBox::question(this, tr("Include Group?"), msg, MessageBox::Yes | MessageBox::No | MessageBox::Cancel, MessageBox::No); + QString msg = tr("The Group for \"%1\" is excluded. Would you like to include all Entries from " + "there as well?") + .arg(entry->title()); + auto response = MessageBox::question(this, + tr("Include Group?"), + msg, + MessageBox::Yes | MessageBox::No | MessageBox::Cancel, + MessageBox::No); if (response == MessageBox::Cancel) { return; - } - else if (response == MessageBox::Yes) { + } else if (response == MessageBox::Yes) { group->setExcludeFromReports(false); - } - else if (response == MessageBox::No) { + } else if (response == MessageBox::No) { // We'll exclude all entries from the group here and then // include the selected ones below group->setExcludeFromReports(false); @@ -177,9 +183,9 @@ QMenu *ReportsWidgetBase::customMenuRequestedBase() auto row = m_modelProxy->mapToSource(index).row(); auto entry = m_rowToEntry[row].second; - // If the containing group is excluded but the user wants to include - // this entry, ask if they want to keep the remaining items in the group - // excluded or included + // If the containing group is excluded but the user wants to include + // this entry, ask if they want to keep the remaining items in the group + // excluded or included if (entry) { entry->setExcludeFromReports(checked); } diff --git a/src/gui/reports/ReportsWidgetBase.h b/src/gui/reports/ReportsWidgetBase.h index 89124fd2d..abb856435 100644 --- a/src/gui/reports/ReportsWidgetBase.h +++ b/src/gui/reports/ReportsWidgetBase.h @@ -47,10 +47,10 @@ public: virtual void saveSettings(); protected: - virtual QTableView *getTableView() const = 0; + virtual QTableView* getTableView() const = 0; virtual void updateWidget() = 0; - QMenu *customMenuRequestedBase(); + QMenu* customMenuRequestedBase(); QList getSelectedEntries() const; public slots: diff --git a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp index a1eabed13..1e5d198cb 100644 --- a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp +++ b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp @@ -236,7 +236,7 @@ void ReportsWidgetBrowserStatistics::customMenuRequested(QPoint pos) { auto menu = customMenuRequestedBase(); - if(!menu) { + if (!menu) { return; } @@ -303,7 +303,7 @@ QMap ReportsWidgetBrowserStatistics::getBrowserConfigFromE return configList; } -QTableView *ReportsWidgetBrowserStatistics::getTableView() const +QTableView* ReportsWidgetBrowserStatistics::getTableView() const { return m_ui->browserStatisticsTableView; } diff --git a/src/gui/reports/ReportsWidgetBrowserStatistics.h b/src/gui/reports/ReportsWidgetBrowserStatistics.h index 9b17d221f..6ec8afb30 100644 --- a/src/gui/reports/ReportsWidgetBrowserStatistics.h +++ b/src/gui/reports/ReportsWidgetBrowserStatistics.h @@ -44,7 +44,7 @@ public: protected: void showEvent(QShowEvent* event) override; void updateWidget() override; - QTableView *getTableView() const override; + QTableView* getTableView() const override; signals: void entryActivated(Entry*); diff --git a/src/gui/reports/ReportsWidgetHealthcheck.cpp b/src/gui/reports/ReportsWidgetHealthcheck.cpp index 7d4a14089..086dd776d 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.cpp +++ b/src/gui/reports/ReportsWidgetHealthcheck.cpp @@ -24,8 +24,8 @@ #include "core/PasswordHealth.h" #include "gui/GuiTools.h" #include "gui/Icons.h" -#include "gui/styles/StateColorPalette.h" #include "gui/reports/ProxyModels.h" +#include "gui/styles/StateColorPalette.h" #include #include @@ -279,7 +279,7 @@ void ReportsWidgetHealthcheck::customMenuRequested(QPoint pos) { auto menu = customMenuRequestedBase(); - if(!menu) { + if (!menu) { return; } @@ -292,7 +292,7 @@ void ReportsWidgetHealthcheck::updateWidget() calculateHealth(); } -QTableView *ReportsWidgetHealthcheck::getTableView() const +QTableView* ReportsWidgetHealthcheck::getTableView() const { return m_ui->healthcheckTableView; } diff --git a/src/gui/reports/ReportsWidgetHealthcheck.h b/src/gui/reports/ReportsWidgetHealthcheck.h index 1dec5d8a7..7dfe0cdfc 100644 --- a/src/gui/reports/ReportsWidgetHealthcheck.h +++ b/src/gui/reports/ReportsWidgetHealthcheck.h @@ -47,7 +47,7 @@ public: protected: void showEvent(QShowEvent* event) override; void updateWidget() override; - QTableView *getTableView() const override; + QTableView* getTableView() const override; signals: void tablePopulated(); diff --git a/src/gui/reports/ReportsWidgetHibp.cpp b/src/gui/reports/ReportsWidgetHibp.cpp index 44a2d2131..b97b9986c 100644 --- a/src/gui/reports/ReportsWidgetHibp.cpp +++ b/src/gui/reports/ReportsWidgetHibp.cpp @@ -65,7 +65,7 @@ void ReportsWidgetHibp::loadSettings(QSharedPointer db) m_ui->validationButton->setEnabled(true); m_ui->progressBar->hide(); #else - // Compiled without networking, can't do anything + // Compiled without networking, can't do anything m_ui->stackedWidget->setCurrentIndex(2); #endif @@ -335,7 +335,7 @@ void ReportsWidgetHibp::customMenuRequested(QPoint pos) // Create the context menu const auto menu = customMenuRequestedBase(); - if(!menu) { + if (!menu) { return; } @@ -348,7 +348,7 @@ void ReportsWidgetHibp::updateWidget() makeHibpTable(); } -QTableView *ReportsWidgetHibp::getTableView() const +QTableView* ReportsWidgetHibp::getTableView() const { return m_ui->hibpTableView; } diff --git a/src/gui/reports/ReportsWidgetHibp.h b/src/gui/reports/ReportsWidgetHibp.h index 8148ea24b..2d59a090a 100644 --- a/src/gui/reports/ReportsWidgetHibp.h +++ b/src/gui/reports/ReportsWidgetHibp.h @@ -52,7 +52,7 @@ public: protected: void updateWidget() override; - QTableView *getTableView() const override; + QTableView* getTableView() const override; public slots: void emitEntryActivated(const QModelIndex&); diff --git a/src/gui/reports/ReportsWidgetPasskeys.cpp b/src/gui/reports/ReportsWidgetPasskeys.cpp index e76f509f7..737c72f68 100644 --- a/src/gui/reports/ReportsWidgetPasskeys.cpp +++ b/src/gui/reports/ReportsWidgetPasskeys.cpp @@ -29,8 +29,8 @@ #include "gui/MessageBox.h" #include "gui/passkeys/PasskeyExporter.h" #include "gui/passkeys/PasskeyImporter.h" -#include "gui/styles/StateColorPalette.h" #include "gui/reports/ProxyModels.h" +#include "gui/styles/StateColorPalette.h" #include #include @@ -246,7 +246,7 @@ void ReportsWidgetPasskeys::exportPasskey() passkeyExporter.showExportDialog(getSelectedEntries()); } -QTableView *ReportsWidgetPasskeys::getTableView() const +QTableView* ReportsWidgetPasskeys::getTableView() const { return m_ui->passkeysTableView; } diff --git a/src/gui/reports/ReportsWidgetPasskeys.h b/src/gui/reports/ReportsWidgetPasskeys.h index 3e6115013..30945bba6 100644 --- a/src/gui/reports/ReportsWidgetPasskeys.h +++ b/src/gui/reports/ReportsWidgetPasskeys.h @@ -44,7 +44,7 @@ public: protected: void showEvent(QShowEvent* event) override; void updateWidget() override; - QTableView *getTableView() const override; + QTableView* getTableView() const override; public slots: void updateEntries(); diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 71fef6729..c06906249 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -2675,7 +2675,7 @@ void TestGui::testExcludedGroupReincludeAllEntries() addGroup("Finance"); addGroup("Entertainment"); - // Use bad passwords to make sure they all show up in health report + // Use bad passwords to make sure they all show up in health report addEntry("Finance", "Chase", "user1", "password"); addEntry("Finance", "Amex", "user1", "password123"); addEntry("Finance", "Capital One", "user1", "password456"); @@ -2697,13 +2697,13 @@ void TestGui::testExcludedGroupReincludeAllEntries() auto* editGroupWidget = m_dbWidget->findChild("editGroupWidget"); QVERIFY(editGroupWidget); - // Bring up group edit page + // Bring up group edit page QTest::mouseClick(editGroupWidget, Qt::LeftButton); QLineEdit* nameEdit = editGroupWidget->findChild("editName"); QCOMPARE(nameEdit->text(), QString("Entertainment")); - // Find database report exclusion checkbox and check it + // Find database report exclusion checkbox and check it QCheckBox* excludeGroupFromReportsCheckbox = editGroupWidget->findChild("excludeReportsCheckBox"); QVERIFY(excludeGroupFromReportsCheckbox); @@ -2712,14 +2712,14 @@ void TestGui::testExcludedGroupReincludeAllEntries() auto* editGroupWidgetButtonBox = editGroupWidget->findChild("buttonBox"); QVERIFY(editGroupWidgetButtonBox); - // Apply and go back to main view + // Apply and go back to main view QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Apply), Qt::LeftButton); QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode); QVERIFY(entertainmentGroup->excludeFromReports()); - // Verify they don't show up in the report + // Verify they don't show up in the report auto* actionReports = m_mainWindow->findChild("actionReports"); QVERIFY(actionReports->isEnabled()); @@ -2752,7 +2752,7 @@ void TestGui::testExcludedGroupReincludeAllEntries() QAbstractItemModel* healthModel = healthTable->model(); QVERIFY(healthModel); - // There should be 3 showing + // There should be 3 showing QTRY_COMPARE(healthCheckWidgetSpy.count(), 1); QCOMPARE(healthModel->rowCount(), 5); // account for 2 existing passwords at the start of each test case From 27cd97a4a862c960a3077821fe97f877c867c648 Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Mon, 29 Dec 2025 21:43:36 -0800 Subject: [PATCH 27/29] Fix delete plugin menu action placement Make comment clearer. --- src/gui/reports/ReportsWidgetBase.cpp | 2 +- src/gui/reports/ReportsWidgetBrowserStatistics.cpp | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/gui/reports/ReportsWidgetBase.cpp b/src/gui/reports/ReportsWidgetBase.cpp index 3f3506b31..131114738 100644 --- a/src/gui/reports/ReportsWidgetBase.cpp +++ b/src/gui/reports/ReportsWidgetBase.cpp @@ -143,7 +143,7 @@ QMenu* ReportsWidgetBase::customMenuRequestedBase() // If we are including entries (checked is false) but a group is excluded, ask the user if they // would like to include the rest of the group as well (or keep it excluded). - // If they exclude it, we need to include the whole group, and then exclude + // If they choose "No", we need to include the whole group, and then exclude // the entries that aren't selected here. if (!checked) { for (const auto index : selected) { diff --git a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp index 1e5d198cb..d2caeb066 100644 --- a/src/gui/reports/ReportsWidgetBrowserStatistics.cpp +++ b/src/gui/reports/ReportsWidgetBrowserStatistics.cpp @@ -245,7 +245,8 @@ void ReportsWidgetBrowserStatistics::customMenuRequested(QPoint pos) // Create the "delete plugin data" menu item const auto deletePluginData = new QAction(icons()->icon("entry-delete"), tr("Delete plugin data from Entry(s)…", "", selected.size()), this); - menu->addAction(deletePluginData); + menu->insertAction(menu->actions().at(3), + deletePluginData); // Index 3 is the one after "Delete Entry" so place "Delete plugin" before it connect(deletePluginData, &QAction::triggered, this, From ac1a170cc31876906986ee37b736913d6ef49ffc Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Tue, 30 Dec 2025 16:12:52 -0800 Subject: [PATCH 28/29] Update translations and license dates --- share/translations/keepassxc_en.ts | 143 +++++++++++++------------- src/gui/reports/ReportsWidgetBase.cpp | 2 +- src/gui/reports/ReportsWidgetBase.h | 2 +- 3 files changed, 73 insertions(+), 74 deletions(-) diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index f974db170..cc225c4fe 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -3806,6 +3806,10 @@ Supported extensions are: %1. Search toggle for this and sub groups + + Exclude from database reports + + EditWidgetIcons @@ -9499,6 +9503,47 @@ This option is deprecated, use --set-key-file instead. + + ReportsWidgetBase + + Please wait, report is being calculated… + + + + Edit Entry… + + + + Expire Entry(s)… + + + + + + + Delete Entry(s)… + + + + + + + Exclude Entry(s) from reports + + + + Exclude Group(s) from reports + + + + The Group for "%1" is excluded. Would you like to include all Entries from there as well? + + + + Include Group? + + + ReportsWidgetBrowserStatistics @@ -9533,10 +9578,6 @@ This option is deprecated, use --set-key-file instead. This entry is being excluded from reports - - Please wait, browser statistics is being calculated… - - No entries with a URL, or none has browser extension settings saved. @@ -9553,28 +9594,6 @@ This option is deprecated, use --set-key-file instead. URLs - - Edit Entry… - - - - Delete Entry(s)… - - - - - - - Exclude from reports - - - - Expire Entry(s)… - - - - - Only show entries that have a URL @@ -9598,6 +9617,14 @@ This option is deprecated, use --set-key-file instead. + + (Group Excluded) + + + + The group for this entry is being excluded from reports + + ReportsWidgetHealthcheck @@ -9633,10 +9660,6 @@ This option is deprecated, use --set-key-file instead. This entry is being excluded from reports - - Please wait, health data is being calculated… - - Congratulations, everything is healthy! @@ -9657,32 +9680,18 @@ This option is deprecated, use --set-key-file instead. Reason - - Edit Entry… - - - - Delete Entry(s)… - - - - - - - Exclude from reports - - - - Expire Entry(s)… - - - - - Show entries that have been excluded from reports + + (Group Excluded) + + + + The group for this entry is being excluded from reports + + ReportsWidgetHibp @@ -9767,27 +9776,13 @@ This option is deprecated, use --set-key-file instead. - Edit Entry… + (Group Excluded) - - Delete Entry(s)… - - - - - - Exclude from reports + The group for this entry is being excluded from reports - - Expire Entry(s)… - - - - - ReportsWidgetPasskeys @@ -9850,10 +9845,6 @@ This option is deprecated, use --set-key-file instead. The passkey file will be vulnerable to theft and unauthorized use, if left unsecured. Are you sure you want to continue? - - Please wait, list of entries with passkeys is being updated… - - No entries with passkeys. @@ -9988,6 +9979,14 @@ This option is deprecated, use --set-key-file instead. + + Groups excluded from reports + + + + Excluding entire groups from reports isn't necessarily a problem but please exercise caution when excluding entire groups. + + SSHAgent diff --git a/src/gui/reports/ReportsWidgetBase.cpp b/src/gui/reports/ReportsWidgetBase.cpp index 131114738..5a42e6bc3 100644 --- a/src/gui/reports/ReportsWidgetBase.cpp +++ b/src/gui/reports/ReportsWidgetBase.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 KeePassXC Team + * Copyright (C) 2025 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 diff --git a/src/gui/reports/ReportsWidgetBase.h b/src/gui/reports/ReportsWidgetBase.h index abb856435..0ba4830ae 100644 --- a/src/gui/reports/ReportsWidgetBase.h +++ b/src/gui/reports/ReportsWidgetBase.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 KeePassXC Team + * Copyright (C) 2025 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 From 249352a2273a23acb2364cddf3fd32bf4e6c2c89 Mon Sep 17 00:00:00 2001 From: Agoston Szepessy Date: Tue, 30 Dec 2025 16:36:13 -0800 Subject: [PATCH 29/29] Copilot feedback --- src/gui/reports/ProxyModels.h | 6 +++--- src/gui/reports/ReportsWidgetBase.cpp | 17 ++--------------- src/gui/reports/ReportsWidgetBase.h | 3 +-- .../reports/ReportsWidgetBrowserStatistics.h | 3 --- 4 files changed, 6 insertions(+), 23 deletions(-) diff --git a/src/gui/reports/ProxyModels.h b/src/gui/reports/ProxyModels.h index 4d7481783..6311afeff 100644 --- a/src/gui/reports/ProxyModels.h +++ b/src/gui/reports/ProxyModels.h @@ -28,14 +28,14 @@ enum class SortProxyModelKind Healthcheck, }; -class HibpReportSortPoxyModel : public QSortFilterProxyModel +class HibpReportSortProxyModel : public QSortFilterProxyModel { public: - HibpReportSortPoxyModel(QObject* parent) + HibpReportSortProxyModel(QObject* parent) : QSortFilterProxyModel(parent) { } - ~HibpReportSortPoxyModel() override = default; + ~HibpReportSortProxyModel() override = default; protected: bool lessThan(const QModelIndex& left, const QModelIndex& right) const override diff --git a/src/gui/reports/ReportsWidgetBase.cpp b/src/gui/reports/ReportsWidgetBase.cpp index 5a42e6bc3..02b469c66 100644 --- a/src/gui/reports/ReportsWidgetBase.cpp +++ b/src/gui/reports/ReportsWidgetBase.cpp @@ -45,7 +45,7 @@ ReportsWidgetBase::ReportsWidgetBase(QWidget* parent, SortProxyModelKind proxyMo m_modelProxy.reset(new HealthcheckReportSortProxyModel(this)); break; case SortProxyModelKind::Hibp: - m_modelProxy.reset(new HibpReportSortPoxyModel(this)); + m_modelProxy.reset(new HibpReportSortProxyModel(this)); break; } } @@ -111,7 +111,7 @@ QMenu* ReportsWidgetBase::customMenuRequestedBase() excludeAction->setObjectName("contextMenuExcludeAction"); const auto excludeGroupsAction = new QAction(icons()->icon("reports-exclude"), tr("Exclude Group(s) from reports"), this); - excludeGroupsAction->setObjectName("contextMenuxcludeGroupAction"); + excludeGroupsAction->setObjectName("contextMenuExcludeGroupAction"); bool isExcluded = false; bool isGroupExcluded = false; @@ -221,19 +221,6 @@ QList ReportsWidgetBase::getSelectedEntries() const return selectedEntries; } -QList ReportsWidgetBase::getSelectedEntries() -{ - QList selectedEntries; - for (auto index : getTableView()->selectionModel()->selectedRows()) { - auto row = m_modelProxy->mapToSource(index).row(); - auto entry = m_rowToEntry[row].second; - if (entry) { - selectedEntries << entry; - } - } - return selectedEntries; -} - void ReportsWidgetBase::expireSelectedEntries() { for (auto entry : getSelectedEntries()) { diff --git a/src/gui/reports/ReportsWidgetBase.h b/src/gui/reports/ReportsWidgetBase.h index 0ba4830ae..c0ac484a3 100644 --- a/src/gui/reports/ReportsWidgetBase.h +++ b/src/gui/reports/ReportsWidgetBase.h @@ -51,10 +51,9 @@ protected: virtual void updateWidget() = 0; QMenu* customMenuRequestedBase(); - QList getSelectedEntries() const; public slots: - QList getSelectedEntries(); + QList getSelectedEntries() const; void expireSelectedEntries(); void deleteSelectedEntries(); void emitEntryActivated(const QModelIndex& index); diff --git a/src/gui/reports/ReportsWidgetBrowserStatistics.h b/src/gui/reports/ReportsWidgetBrowserStatistics.h index 6ec8afb30..71a08a3a2 100644 --- a/src/gui/reports/ReportsWidgetBrowserStatistics.h +++ b/src/gui/reports/ReportsWidgetBrowserStatistics.h @@ -46,9 +46,6 @@ protected: void updateWidget() override; QTableView* getTableView() const override; -signals: - void entryActivated(Entry*); - public slots: void calculateBrowserStatistics(); void customMenuRequested(QPoint);