From 4e8b00da347a6ff7b4ea58a03aaffce6e18b563f Mon Sep 17 00:00:00 2001 From: Bernhard Kirchen Date: Tue, 19 Jan 2021 19:05:53 +0100 Subject: [PATCH] Add custom icon purging and bulk deletion This change adds a new database settings widget named "maintenance", using a wrench icon. This widget is designated to be the home for database related maintenance tasks. Initially, managing custom icons is now possible from that new tab. The feature includes bulk removing of any number of selected custom icons and automatic purging of unused custom icons by the click of a button. Fixes #2110 --- COPYING | 2 + .../scalable/actions/hammer-wrench.svg | 1 + share/icons/icons.qrc | 3 +- src/CMakeLists.txt | 1 + src/gui/EditWidgetIcons.cpp | 88 ------- src/gui/EditWidgetIcons.h | 1 - src/gui/EditWidgetIcons.ui | 8 - src/gui/dbsettings/DatabaseSettingsDialog.cpp | 6 + src/gui/dbsettings/DatabaseSettingsDialog.h | 2 + .../DatabaseSettingsWidgetMaintenance.cpp | 226 ++++++++++++++++++ .../DatabaseSettingsWidgetMaintenance.h | 72 ++++++ .../DatabaseSettingsWidgetMaintenance.ui | 116 +++++++++ 12 files changed, 428 insertions(+), 98 deletions(-) create mode 100644 share/icons/application/scalable/actions/hammer-wrench.svg create mode 100644 src/gui/dbsettings/DatabaseSettingsWidgetMaintenance.cpp create mode 100644 src/gui/dbsettings/DatabaseSettingsWidgetMaintenance.h create mode 100644 src/gui/dbsettings/DatabaseSettingsWidgetMaintenance.ui diff --git a/COPYING b/COPYING index a20889ac9..c18b82201 100644 --- a/COPYING +++ b/COPYING @@ -164,6 +164,8 @@ Files: share/icons/application/scalable/actions/chevron-double-down.svg share/icons/application/scalable/actions/group-edit.svg share/icons/application/scalable/actions/group-empty-trash.svg share/icons/application/scalable/actions/group-new.svg + share/icons/application/scalable/actions/hammer-wrench.svg + share/icons/application/scalable/actions/health.svg share/icons/application/scalable/actions/help-about.svg share/icons/application/scalable/actions/key-enter.svg share/icons/application/scalable/actions/lock-question.svg diff --git a/share/icons/application/scalable/actions/hammer-wrench.svg b/share/icons/application/scalable/actions/hammer-wrench.svg new file mode 100644 index 000000000..79abcc38d --- /dev/null +++ b/share/icons/application/scalable/actions/hammer-wrench.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/share/icons/icons.qrc b/share/icons/icons.qrc index d42bfc52b..b4aa3950f 100644 --- a/share/icons/icons.qrc +++ b/share/icons/icons.qrc @@ -42,6 +42,7 @@ application/scalable/actions/group-edit.svg application/scalable/actions/group-empty-trash.svg application/scalable/actions/group-new.svg + application/scalable/actions/hammer-wrench.svg application/scalable/actions/health.svg application/scalable/actions/help-about.svg application/scalable/actions/hibp.svg @@ -59,7 +60,7 @@ application/scalable/actions/password-generator.svg application/scalable/actions/password-show-off.svg application/scalable/actions/password-show-on.svg - application/scalable/actions/refresh.svg + application/scalable/actions/refresh.svg application/scalable/actions/reports.svg application/scalable/actions/reports-exclude.svg application/scalable/actions/sort-alphabetical-ascending.svg diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 80ce5247c..e3c764e0c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -155,6 +155,7 @@ set(keepassx_SOURCES gui/dbsettings/DatabaseSettingsWidget.cpp gui/dbsettings/DatabaseSettingsDialog.cpp gui/dbsettings/DatabaseSettingsWidgetGeneral.cpp + gui/dbsettings/DatabaseSettingsWidgetMaintenance.cpp gui/dbsettings/DatabaseSettingsWidgetMetaDataSimple.cpp gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp gui/dbsettings/DatabaseSettingsWidgetDatabaseKey.cpp diff --git a/src/gui/EditWidgetIcons.cpp b/src/gui/EditWidgetIcons.cpp index bbd87605a..f1d5689b1 100644 --- a/src/gui/EditWidgetIcons.cpp +++ b/src/gui/EditWidgetIcons.cpp @@ -63,7 +63,6 @@ EditWidgetIcons::EditWidgetIcons(QWidget* parent) connect(m_ui->defaultIconsRadio, SIGNAL(toggled(bool)), this, SLOT(updateWidgetsDefaultIcons(bool))); connect(m_ui->customIconsRadio, SIGNAL(toggled(bool)), this, SLOT(updateWidgetsCustomIcons(bool))); connect(m_ui->addButton, SIGNAL(clicked()), SLOT(addCustomIconFromFile())); - connect(m_ui->deleteButton, SIGNAL(clicked()), SLOT(removeCustomIcon())); connect(m_ui->faviconButton, SIGNAL(clicked()), SLOT(downloadFavicon())); connect(m_ui->applyIconToPushButton->menu(), SIGNAL(triggered(QAction*)), SLOT(confirmApplyIconTo(QAction*))); @@ -310,91 +309,6 @@ bool EditWidgetIcons::addCustomIcon(const QImage& icon) return added; } -void EditWidgetIcons::removeCustomIcon() -{ - if (m_db) { - QModelIndex index = m_ui->customIconsView->currentIndex(); - if (index.isValid()) { - QUuid iconUuid = m_customIconModel->uuidFromIndex(index); - - const QList allEntries = m_db->rootGroup()->entriesRecursive(true); - QList entriesWithSameIcon; - QList historyEntriesWithSameIcon; - - for (Entry* entry : allEntries) { - if (iconUuid == entry->iconUuid()) { - // Check if this is a history entry (no assigned group) - if (!entry->group()) { - historyEntriesWithSameIcon << entry; - } else if (m_currentUuid != entry->uuid()) { - entriesWithSameIcon << entry; - } - } - } - - const QList allGroups = m_db->rootGroup()->groupsRecursive(true); - QList groupsWithSameIcon; - - for (Group* group : allGroups) { - if (iconUuid == group->iconUuid() && m_currentUuid != group->uuid()) { - groupsWithSameIcon << group; - } - } - - int iconUseCount = entriesWithSameIcon.size() + groupsWithSameIcon.size(); - if (iconUseCount > 0) { - - auto result = MessageBox::question(this, - tr("Confirm Delete"), - tr("This icon is used by %n entry(s), and will be replaced " - "by the default icon. Are you sure you want to delete it?", - "", - iconUseCount), - MessageBox::Delete | MessageBox::Cancel, - MessageBox::Cancel); - - if (result == MessageBox::Cancel) { - // Early out, nothing is changed - return; - } else { - // Revert matched entries to the default entry icon - for (Entry* entry : asConst(entriesWithSameIcon)) { - entry->setIcon(Entry::DefaultIconNumber); - } - - // Revert matched groups to the default group icon - for (Group* group : asConst(groupsWithSameIcon)) { - group->setIcon(Group::DefaultIconNumber); - } - } - } - - // Remove the icon from history entries - for (Entry* entry : asConst(historyEntriesWithSameIcon)) { - entry->setUpdateTimeinfo(false); - entry->setIcon(0); - entry->setUpdateTimeinfo(true); - } - - // Remove the icon from the database - m_db->metadata()->removeCustomIcon(iconUuid); - m_customIconModel->setIcons(m_db->metadata()->customIconsPixmaps(IconSize::Default), - m_db->metadata()->customIconsOrder()); - - // Reset the current icon view - updateRadioButtonDefaultIcons(); - - if (m_db->rootGroup()->findEntryByUuid(m_currentUuid) != nullptr) { - m_ui->defaultIconsView->setCurrentIndex(m_defaultIconModel->index(Entry::DefaultIconNumber)); - } else { - m_ui->defaultIconsView->setCurrentIndex(m_defaultIconModel->index(Group::DefaultIconNumber)); - } - - emit widgetUpdated(); - } - } -} - void EditWidgetIcons::updateWidgetsDefaultIcons(bool check) { if (check) { @@ -405,7 +319,6 @@ void EditWidgetIcons::updateWidgetsDefaultIcons(bool check) m_ui->defaultIconsView->setCurrentIndex(index); } m_ui->customIconsView->selectionModel()->clearSelection(); - m_ui->deleteButton->setEnabled(false); } } @@ -419,7 +332,6 @@ void EditWidgetIcons::updateWidgetsCustomIcons(bool check) m_ui->customIconsView->setCurrentIndex(index); } m_ui->defaultIconsView->selectionModel()->clearSelection(); - m_ui->deleteButton->setEnabled(true); } } diff --git a/src/gui/EditWidgetIcons.h b/src/gui/EditWidgetIcons.h index 2a95445f9..7d695494c 100644 --- a/src/gui/EditWidgetIcons.h +++ b/src/gui/EditWidgetIcons.h @@ -90,7 +90,6 @@ private slots: void iconReceived(const QString& url, const QImage& icon); void addCustomIconFromFile(); bool addCustomIcon(const QImage& icon); - void removeCustomIcon(); void updateWidgetsDefaultIcons(bool checked); void updateWidgetsCustomIcons(bool checked); void updateRadioButtonDefaultIcons(); diff --git a/src/gui/EditWidgetIcons.ui b/src/gui/EditWidgetIcons.ui index 5c17a2de6..e9fc6fbca 100644 --- a/src/gui/EditWidgetIcons.ui +++ b/src/gui/EditWidgetIcons.ui @@ -112,13 +112,6 @@ - - - - Delete custom icon - - - @@ -181,7 +174,6 @@ customIconsRadio customIconsView addButton - deleteButton faviconButton applyIconToPushButton diff --git a/src/gui/dbsettings/DatabaseSettingsDialog.cpp b/src/gui/dbsettings/DatabaseSettingsDialog.cpp index 485f6c2ad..e2437573a 100644 --- a/src/gui/dbsettings/DatabaseSettingsDialog.cpp +++ b/src/gui/dbsettings/DatabaseSettingsDialog.cpp @@ -25,6 +25,7 @@ #ifdef WITH_XC_BROWSER #include "DatabaseSettingsWidgetBrowser.h" #endif +#include "DatabaseSettingsWidgetMaintenance.h" #if defined(WITH_XC_KEESHARE) #include "keeshare/DatabaseSettingsPageKeeShare.h" #endif @@ -72,6 +73,7 @@ DatabaseSettingsDialog::DatabaseSettingsDialog(QWidget* parent) #ifdef WITH_XC_BROWSER , m_browserWidget(new DatabaseSettingsWidgetBrowser(this)) #endif + , m_maintenanceWidget(new DatabaseSettingsWidgetMaintenance(this)) { m_ui->setupUi(this); @@ -115,6 +117,9 @@ DatabaseSettingsDialog::DatabaseSettingsDialog(QWidget* parent) m_ui->stackedWidget->addWidget(m_browserWidget); #endif + m_ui->categoryList->addCategory(tr("Maintenance"), icons()->icon("hammer-wrench")); + m_ui->stackedWidget->addWidget(m_maintenanceWidget); + pageChanged(); } @@ -131,6 +136,7 @@ void DatabaseSettingsDialog::load(const QSharedPointer& db) #ifdef WITH_XC_BROWSER m_browserWidget->load(db); #endif + m_maintenanceWidget->load(db); for (const ExtraPage& page : asConst(m_extraPages)) { page.loadSettings(db); } diff --git a/src/gui/dbsettings/DatabaseSettingsDialog.h b/src/gui/dbsettings/DatabaseSettingsDialog.h index ba708c65b..290810411 100644 --- a/src/gui/dbsettings/DatabaseSettingsDialog.h +++ b/src/gui/dbsettings/DatabaseSettingsDialog.h @@ -32,6 +32,7 @@ class DatabaseSettingsWidgetDatabaseKey; #ifdef WITH_XC_BROWSER class DatabaseSettingsWidgetBrowser; #endif +class DatabaseSettingsWidgetMaintenance; class QTabWidget; namespace Ui @@ -90,6 +91,7 @@ private: #ifdef WITH_XC_BROWSER QPointer m_browserWidget; #endif + QPointer m_maintenanceWidget; class ExtraPage; QList m_extraPages; diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetMaintenance.cpp b/src/gui/dbsettings/DatabaseSettingsWidgetMaintenance.cpp new file mode 100644 index 000000000..c6f5173aa --- /dev/null +++ b/src/gui/dbsettings/DatabaseSettingsWidgetMaintenance.cpp @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2021 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "DatabaseSettingsWidgetMaintenance.h" +#include "ui_DatabaseSettingsWidgetMaintenance.h" + +#include + +#include "core/Database.h" +#include "core/Entry.h" +#include "core/Group.h" +#include "core/Metadata.h" +#include "gui/IconModels.h" +#include "gui/MessageBox.h" + +DatabaseSettingsWidgetMaintenance::DatabaseSettingsWidgetMaintenance(QWidget* parent) + : DatabaseSettingsWidget(parent) + , m_ui(new Ui::DatabaseSettingsWidgetMaintenance()) + , m_customIconModel(new CustomIconModel(this)) + , m_deletionDecision(MessageBox::NoButton) +{ + m_ui->setupUi(this); + + m_ui->customIconsView->setModel(m_customIconModel); + + connect(m_ui->deleteButton, SIGNAL(clicked()), SLOT(removeCustomIcon())); + connect(m_ui->purgeButton, SIGNAL(clicked()), SLOT(purgeUnusedCustomIcons())); + connect(m_ui->customIconsView->selectionModel(), + SIGNAL(selectionChanged(QItemSelection, QItemSelection)), + this, + SLOT(selectionChanged())); +} + +DatabaseSettingsWidgetMaintenance::~DatabaseSettingsWidgetMaintenance() +{ +} + +void DatabaseSettingsWidgetMaintenance::populateIcons(QSharedPointer db) +{ + m_customIconModel->setIcons(db->metadata()->customIconsPixmaps(IconSize::Default), + db->metadata()->customIconsOrder()); + m_ui->deleteButton->setEnabled(false); +} + +void DatabaseSettingsWidgetMaintenance::initialize() +{ + auto database = DatabaseSettingsWidget::getDatabase(); + if (!database) { + return; + } + populateIcons(database); +} + +void DatabaseSettingsWidgetMaintenance::selectionChanged() +{ + QList indexes = m_ui->customIconsView->selectionModel()->selectedIndexes(); + if (indexes.isEmpty()) { + m_ui->deleteButton->setEnabled(false); + } else { + m_ui->deleteButton->setEnabled(true); + } +} + +void DatabaseSettingsWidgetMaintenance::removeCustomIcon() +{ + auto database = DatabaseSettingsWidget::getDatabase(); + if (!database) { + return; + } + + m_deletionDecision = MessageBox::NoButton; + + QList indexes = m_ui->customIconsView->selectionModel()->selectedIndexes(); + for (auto index : indexes) { + removeSingleCustomIcon(database, index); + } + + populateIcons(database); +} + +void DatabaseSettingsWidgetMaintenance::removeSingleCustomIcon(QSharedPointer database, QModelIndex index) +{ + QUuid iconUuid = m_customIconModel->uuidFromIndex(index); + + const QList allEntries = database->rootGroup()->entriesRecursive(true); + QList entriesWithSelectedIcon; + QList historicEntriesWithSelectedIcon; + + for (Entry* entry : allEntries) { + if (iconUuid == entry->iconUuid()) { + // Check if this is a history entry (no assigned group) + if (!entry->group()) { + historicEntriesWithSelectedIcon << entry; + } else { + entriesWithSelectedIcon << entry; + } + } + } + + const QList allGroups = database->rootGroup()->groupsRecursive(true); + QList groupsWithSameIcon; + + for (Group* group : allGroups) { + if (iconUuid == group->iconUuid()) { + groupsWithSameIcon << group; + } + } + + int iconUseCount = entriesWithSelectedIcon.size() + groupsWithSameIcon.size(); + if (iconUseCount > 0) { + if (m_deletionDecision == MessageBox::NoButton) { + m_deletionDecision = MessageBox::question( + this, + tr("Confirm Deletion"), + tr("At least one of the selected icons is currently in use by at least one entry or group. " + "The icons of all affected entries and groups will be replaced by the default icon. " + "Are you sure you want to delete icons that are currently in use?"), + MessageBox::Delete | MessageBox::Skip, + MessageBox::Skip); + } + + if (m_deletionDecision == MessageBox::Skip) { + // Early out, nothing is changed + return; + } else { + // Revert matched entries to the default entry icon + for (Entry* entry : asConst(entriesWithSelectedIcon)) { + entry->setIcon(Entry::DefaultIconNumber); + } + + // Revert matched groups to the default group icon + for (Group* group : asConst(groupsWithSameIcon)) { + group->setIcon(Group::DefaultIconNumber); + } + } + } + + // Remove the icon from history entries + for (Entry* entry : asConst(historicEntriesWithSelectedIcon)) { + entry->setUpdateTimeinfo(false); + entry->setIcon(0); + entry->setUpdateTimeinfo(true); + } + + // Remove the icon from the database + database->metadata()->removeCustomIcon(iconUuid); +} + +void DatabaseSettingsWidgetMaintenance::purgeUnusedCustomIcons() +{ + auto database = DatabaseSettingsWidget::getDatabase(); + if (!database) { + return; + } + + QList historyEntries; + QSet historicIcons; + QSet iconsInUse; + + const QList allEntries = database->rootGroup()->entriesRecursive(true); + for (Entry* entry : allEntries) { + if (!entry->group()) { + // Icons exclusively in use by historic entries (no + // group assigned) are also purged from the database + historyEntries << entry; + historicIcons << entry->iconUuid(); + } else { + iconsInUse << entry->iconUuid(); + } + } + + const QList allGroups = database->rootGroup()->groupsRecursive(true); + for (Group* group : allGroups) { + iconsInUse.insert(group->iconUuid()); + } + + int purgeCounter = 0; + QList customIcons = database->metadata()->customIconsOrder(); + for (QUuid iconUuid : customIcons) { + if (iconsInUse.contains(iconUuid)) { + continue; + } + + if (historicIcons.contains(iconUuid)) { + // Remove the icon from history entries using this icon + for (Entry* historicEntry : asConst(historyEntries)) { + if (historicEntry->iconUuid() != iconUuid) { + continue; + } + historicEntry->setUpdateTimeinfo(false); + historicEntry->setIcon(0); + historicEntry->setUpdateTimeinfo(true); + } + } + + ++purgeCounter; + database->metadata()->removeCustomIcon(iconUuid); + } + + if (0 == purgeCounter) { + MessageBox::information(this, + tr("Custom Icons Are In Use"), + tr("All custom icons are in use by at least one entry or group."), + MessageBox::Ok); + return; + } + + populateIcons(database); + + MessageBox::information( + this, tr("Purged Unused Icons"), tr("Purged %n icon(s) from the database.", "", purgeCounter), MessageBox::Ok); +} diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetMaintenance.h b/src/gui/dbsettings/DatabaseSettingsWidgetMaintenance.h new file mode 100644 index 000000000..5d6a9e54f --- /dev/null +++ b/src/gui/dbsettings/DatabaseSettingsWidgetMaintenance.h @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2021 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_DATABASESETTINGSWIDGETMAINTENANCE_H +#define KEEPASSXC_DATABASESETTINGSWIDGETMAINTENANCE_H + +#include "DatabaseSettingsWidget.h" + +#include + +class QItemSelection; +class CustomIconModel; +class Database; +namespace Ui +{ + class DatabaseSettingsWidgetMaintenance; +} + +class DatabaseSettingsWidgetMaintenance : public DatabaseSettingsWidget +{ + Q_OBJECT + +public: + explicit DatabaseSettingsWidgetMaintenance(QWidget* parent = nullptr); + Q_DISABLE_COPY(DatabaseSettingsWidgetMaintenance); + ~DatabaseSettingsWidgetMaintenance() override; + + inline bool hasAdvancedMode() const override + { + return false; + } + +public slots: + void initialize() override; + void uninitialize() override{}; + inline bool save() override + { + return true; + }; + +private slots: + void selectionChanged(); + void removeCustomIcon(); + void purgeUnusedCustomIcons(); + +private: + void populateIcons(QSharedPointer db); + void removeSingleCustomIcon(QSharedPointer database, QModelIndex index); + +protected: + const QScopedPointer m_ui; + +private: + CustomIconModel* const m_customIconModel; + uint64_t m_deletionDecision; +}; + +#endif // KEEPASSXC_DATABASESETTINGSWIDGETMAINTENANCE_H diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetMaintenance.ui b/src/gui/dbsettings/DatabaseSettingsWidgetMaintenance.ui new file mode 100644 index 000000000..e1d06f6f5 --- /dev/null +++ b/src/gui/dbsettings/DatabaseSettingsWidgetMaintenance.ui @@ -0,0 +1,116 @@ + + + DatabaseSettingsWidgetMaintenance + + + + 0 + 0 + 669 + 395 + + + + + 0 + 0 + + + + + 450 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Manage Custom Icons + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::MultiSelection + + + QListView::Static + + + QListView::LeftToRight + + + true + + + QListView::Adjust + + + 4 + + + QListView::ListMode + + + + + + + + + Delete selected icon(s) + + + + + + + Delete all custom icons not in use by any entry or group + + + Delete all custom icons not in use by any entry or group + + + Purge unused icons + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + +