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
This commit is contained in:
Bernhard Kirchen 2021-01-19 19:05:53 +01:00 committed by Jonathan White
parent b9ea6fd2e7
commit 4e8b00da34
12 changed files with 428 additions and 98 deletions

View File

@ -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

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13.78 15.3L19.78 21.3L21.89 19.14L15.89 13.14L13.78 15.3M17.5 10.1C17.11 10.1 16.69 10.05 16.36 9.91L4.97 21.25L2.86 19.14L10.27 11.74L8.5 9.96L7.78 10.66L6.33 9.25V12.11L5.63 12.81L2.11 9.25L2.81 8.55H5.62L4.22 7.14L7.78 3.58C8.95 2.41 10.83 2.41 12 3.58L9.89 5.74L11.3 7.14L10.59 7.85L12.38 9.63L14.2 7.75C14.06 7.42 14 7 14 6.63C14 4.66 15.56 3.11 17.5 3.11C18.09 3.11 18.61 3.25 19.08 3.53L16.41 6.2L17.91 7.7L20.58 5.03C20.86 5.5 21 6 21 6.63C21 8.55 19.45 10.1 17.5 10.1Z" /></svg>

After

Width:  |  Height:  |  Size: 773 B

View File

@ -42,6 +42,7 @@
<file>application/scalable/actions/group-edit.svg</file>
<file>application/scalable/actions/group-empty-trash.svg</file>
<file>application/scalable/actions/group-new.svg</file>
<file>application/scalable/actions/hammer-wrench.svg</file>
<file>application/scalable/actions/health.svg</file>
<file>application/scalable/actions/help-about.svg</file>
<file>application/scalable/actions/hibp.svg</file>
@ -59,7 +60,7 @@
<file>application/scalable/actions/password-generator.svg</file>
<file>application/scalable/actions/password-show-off.svg</file>
<file>application/scalable/actions/password-show-on.svg</file>
<file>application/scalable/actions/refresh.svg</file>
<file>application/scalable/actions/refresh.svg</file>
<file>application/scalable/actions/reports.svg</file>
<file>application/scalable/actions/reports-exclude.svg</file>
<file>application/scalable/actions/sort-alphabetical-ascending.svg</file>

View File

@ -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

View File

@ -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<Entry*> allEntries = m_db->rootGroup()->entriesRecursive(true);
QList<Entry*> entriesWithSameIcon;
QList<Entry*> 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<Group*> allGroups = m_db->rootGroup()->groupsRecursive(true);
QList<Group*> 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);
}
}

View File

@ -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();

View File

@ -112,13 +112,6 @@
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="deleteButton">
<property name="text">
<string>Delete custom icon</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="faviconButton">
<property name="toolTip">
@ -181,7 +174,6 @@
<tabstop>customIconsRadio</tabstop>
<tabstop>customIconsView</tabstop>
<tabstop>addButton</tabstop>
<tabstop>deleteButton</tabstop>
<tabstop>faviconButton</tabstop>
<tabstop>applyIconToPushButton</tabstop>
</tabstops>

View File

@ -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<Database>& db)
#ifdef WITH_XC_BROWSER
m_browserWidget->load(db);
#endif
m_maintenanceWidget->load(db);
for (const ExtraPage& page : asConst(m_extraPages)) {
page.loadSettings(db);
}

View File

@ -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<DatabaseSettingsWidgetBrowser> m_browserWidget;
#endif
QPointer<DatabaseSettingsWidgetMaintenance> m_maintenanceWidget;
class ExtraPage;
QList<ExtraPage> m_extraPages;

View File

@ -0,0 +1,226 @@
/*
* Copyright (C) 2021 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "DatabaseSettingsWidgetMaintenance.h"
#include "ui_DatabaseSettingsWidgetMaintenance.h"
#include <QProgressDialog>
#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<Database> 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<QModelIndex> 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<QModelIndex> indexes = m_ui->customIconsView->selectionModel()->selectedIndexes();
for (auto index : indexes) {
removeSingleCustomIcon(database, index);
}
populateIcons(database);
}
void DatabaseSettingsWidgetMaintenance::removeSingleCustomIcon(QSharedPointer<Database> database, QModelIndex index)
{
QUuid iconUuid = m_customIconModel->uuidFromIndex(index);
const QList<Entry*> allEntries = database->rootGroup()->entriesRecursive(true);
QList<Entry*> entriesWithSelectedIcon;
QList<Entry*> 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<Group*> allGroups = database->rootGroup()->groupsRecursive(true);
QList<Group*> 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<Entry*> historyEntries;
QSet<QUuid> historicIcons;
QSet<QUuid> iconsInUse;
const QList<Entry*> 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<Group*> allGroups = database->rootGroup()->groupsRecursive(true);
for (Group* group : allGroups) {
iconsInUse.insert(group->iconUuid());
}
int purgeCounter = 0;
QList<QUuid> 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);
}

View File

@ -0,0 +1,72 @@
/*
* Copyright (C) 2021 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_DATABASESETTINGSWIDGETMAINTENANCE_H
#define KEEPASSXC_DATABASESETTINGSWIDGETMAINTENANCE_H
#include "DatabaseSettingsWidget.h"
#include <QScopedPointer>
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<Database> db);
void removeSingleCustomIcon(QSharedPointer<Database> database, QModelIndex index);
protected:
const QScopedPointer<Ui::DatabaseSettingsWidgetMaintenance> m_ui;
private:
CustomIconModel* const m_customIconModel;
uint64_t m_deletionDecision;
};
#endif // KEEPASSXC_DATABASESETTINGSWIDGETMAINTENANCE_H

View File

@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DatabaseSettingsWidgetMaintenance</class>
<widget class="QWidget" name="DatabaseSettingsWidgetMaintenance">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>669</width>
<height>395</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>450</width>
<height>0</height>
</size>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Manage Custom Icons</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_1">
<item>
<widget class="QListView" name="customIconsView">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::MultiSelection</enum>
</property>
<property name="movement">
<enum>QListView::Static</enum>
</property>
<property name="flow">
<enum>QListView::LeftToRight</enum>
</property>
<property name="isWrapping" stdset="0">
<bool>true</bool>
</property>
<property name="resizeMode">
<enum>QListView::Adjust</enum>
</property>
<property name="spacing">
<number>4</number>
</property>
<property name="viewMode">
<enum>QListView::ListMode</enum>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="customIconButtonsHorizontalLayout">
<item>
<widget class="QPushButton" name="deleteButton">
<property name="text">
<string>Delete selected icon(s)</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="purgeButton">
<property name="toolTip">
<string>Delete all custom icons not in use by any entry or group</string>
</property>
<property name="accessibleName">
<string>Delete all custom icons not in use by any entry or group</string>
</property>
<property name="text">
<string>Purge unused icons</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>