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