Allow setting group icons to children groups/entries (#3273)

* Add combo menu button to apply an icon to children

- allow more options to apply icons (child groups, child entries)
- extend tests in TestGroup (applying icons for groups/entries only)
- prevent blue folder icon being set for entries (on entry creation only)

* Do not show the combo menu button for entries
This commit is contained in:
Matthias Drexler 2019-06-19 16:02:07 +02:00 committed by Jonathan White
parent 84eec03cb7
commit bb8377ae6a
11 changed files with 242 additions and 4 deletions

View File

@ -1080,8 +1080,10 @@ Entry* Group::addEntryWithPath(const QString& entryPath)
return entry;
}
void Group::applyGroupIconTo(Entry* entry)
void Group::applyGroupIconOnCreateTo(Entry* entry)
{
Q_ASSERT(entry);
if (!config()->get("UseGroupIconOnEntryCreation").toBool()) {
return;
}
@ -1090,6 +1092,13 @@ void Group::applyGroupIconTo(Entry* entry)
return;
}
applyGroupIconTo(entry);
}
void Group::applyGroupIconTo(Entry* entry)
{
Q_ASSERT(entry);
if (iconUuid().isNull()) {
entry->setIcon(iconNumber());
} else {
@ -1097,6 +1106,31 @@ void Group::applyGroupIconTo(Entry* entry)
}
}
void Group::applyGroupIconTo(Group* other)
{
Q_ASSERT(other);
if (iconUuid().isNull()) {
other->setIcon(iconNumber());
} else {
other->setIcon(iconUuid());
}
}
void Group::applyGroupIconToChildGroups()
{
for (Group* recursiveChild : groupsRecursive(false)) {
applyGroupIconTo(recursiveChild);
}
}
void Group::applyGroupIconToChildEntries()
{
for (Entry* recursiveEntry : entriesRecursive(false)) {
applyGroupIconTo(recursiveEntry);
}
}
void Group::sortChildrenRecursively(bool reverse)
{
std::sort(

View File

@ -168,7 +168,11 @@ public:
void addEntry(Entry* entry);
void removeEntry(Entry* entry);
void applyGroupIconOnCreateTo(Entry* entry);
void applyGroupIconTo(Entry* entry);
void applyGroupIconTo(Group* other);
void applyGroupIconToChildGroups();
void applyGroupIconToChildEntries();
void sortChildrenRecursively(bool reverse = false);

View File

@ -308,7 +308,7 @@ namespace FdoSecrets
entry->setUuid(QUuid::createUuid());
entry->setTitle(itemName);
entry->setUsername(m_backend->database()->metadata()->defaultUserName());
group->applyGroupIconTo(entry);
group->applyGroupIconOnCreateTo(entry);
entry->setGroup(group);

View File

@ -368,7 +368,7 @@ void DatabaseWidget::createEntry()
m_newEntry->setUuid(QUuid::createUuid());
m_newEntry->setUsername(m_db->metadata()->defaultUserName());
m_newParent = m_groupView->currentGroup();
m_newParent->applyGroupIconTo(m_newEntry.data());
m_newParent->applyGroupIconOnCreateTo(m_newEntry.data());
switchToEntryEdit(m_newEntry.data(), true);
}

View File

@ -45,6 +45,7 @@ EditWidgetIcons::EditWidgetIcons(QWidget* parent)
: QWidget(parent)
, m_ui(new Ui::EditWidgetIcons())
, m_db(nullptr)
, m_applyIconTo(ApplyIconToOptions::THIS_ONLY)
#ifdef WITH_XC_NETWORKING
, m_netMgr(new QNetworkAccessManager(this))
, m_reply(nullptr)
@ -57,6 +58,8 @@ EditWidgetIcons::EditWidgetIcons(QWidget* parent)
m_ui->defaultIconsView->setModel(m_defaultIconModel);
m_ui->customIconsView->setModel(m_customIconModel);
m_ui->applyIconToPushButton->setMenu(createApplyIconToMenu());
// clang-format off
connect(m_ui->defaultIconsView, SIGNAL(clicked(QModelIndex)), this, SLOT(updateRadioButtonDefaultIcons()));
connect(m_ui->customIconsView, SIGNAL(clicked(QModelIndex)), this, SLOT(updateRadioButtonCustomIcons()));
@ -65,6 +68,7 @@ EditWidgetIcons::EditWidgetIcons(QWidget* parent)
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*)));
connect(m_ui->defaultIconsRadio, SIGNAL(toggled(bool)), this, SIGNAL(widgetUpdated()));
connect(m_ui->defaultIconsView->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)),
@ -103,6 +107,8 @@ IconStruct EditWidgetIcons::state()
}
}
iconStruct.applyTo = m_applyIconTo;
return iconStruct;
}
@ -142,6 +148,30 @@ void EditWidgetIcons::load(const QUuid& currentUuid,
m_ui->defaultIconsRadio->setChecked(true);
}
}
m_applyIconTo = ApplyIconToOptions::THIS_ONLY;
m_ui->applyIconToPushButton->menu()->defaultAction()->activate(QAction::Trigger);
}
void EditWidgetIcons::setShowApplyIconToButton(bool state)
{
m_ui->applyIconToPushButton->setVisible(state);
}
QMenu* EditWidgetIcons::createApplyIconToMenu()
{
auto* applyIconToMenu = new QMenu(this);
QAction* defaultAction = applyIconToMenu->addAction(tr("Apply to this only"));
defaultAction->setData(QVariant::fromValue(ApplyIconToOptions::THIS_ONLY));
applyIconToMenu->setDefaultAction(defaultAction);
applyIconToMenu->addSeparator();
applyIconToMenu->addAction(tr("Also apply to child groups"))
->setData(QVariant::fromValue(ApplyIconToOptions::CHILD_GROUPS));
applyIconToMenu->addAction(tr("Also apply to child entries"))
->setData(QVariant::fromValue(ApplyIconToOptions::CHILD_ENTRIES));
applyIconToMenu->addAction(tr("Also apply to all children"))
->setData(QVariant::fromValue(ApplyIconToOptions::ALL_CHILDREN));
return applyIconToMenu;
}
void EditWidgetIcons::setUrl(const QString& url)
@ -528,3 +558,9 @@ void EditWidgetIcons::updateRadioButtonCustomIcons()
{
m_ui->customIconsRadio->setChecked(true);
}
void EditWidgetIcons::confirmApplyIconTo(QAction* action)
{
m_applyIconTo = action->data().value<ApplyIconToOptions>();
m_ui->applyIconToPushButton->setText(action->text());
}

View File

@ -19,6 +19,7 @@
#ifndef KEEPASSX_EDITWIDGETICONS_H
#define KEEPASSX_EDITWIDGETICONS_H
#include <QMenu>
#include <QUrl>
#include <QUuid>
#include <QWidget>
@ -40,12 +41,23 @@ namespace Ui
class EditWidgetIcons;
}
enum ApplyIconToOptions
{
THIS_ONLY = 0b00,
CHILD_GROUPS = 0b10,
CHILD_ENTRIES = 0b01,
ALL_CHILDREN = 0b11
};
Q_DECLARE_METATYPE(ApplyIconToOptions)
struct IconStruct
{
IconStruct();
QUuid uuid;
int number;
ApplyIconToOptions applyTo;
};
class EditWidgetIcons : public QWidget
@ -62,6 +74,7 @@ public:
const QSharedPointer<Database>& database,
const IconStruct& iconStruct,
const QString& url = "");
void setShowApplyIconToButton(bool state);
public slots:
void setUrl(const QString& url);
@ -84,11 +97,15 @@ private slots:
void updateWidgetsCustomIcons(bool checked);
void updateRadioButtonDefaultIcons();
void updateRadioButtonCustomIcons();
void confirmApplyIconTo(QAction* action);
private:
QMenu* createApplyIconToMenu();
const QScopedPointer<Ui::EditWidgetIcons> m_ui;
QSharedPointer<Database> m_db;
QUuid m_currentUuid;
ApplyIconToOptions m_applyIconTo;
#ifdef WITH_XC_NETWORKING
QUrl m_url;
QUrl m_fetchUrl;

View File

@ -88,7 +88,7 @@
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<layout class="QHBoxLayout" name="customIconButtonsHorizontalLayout">
<item>
<widget class="QPushButton" name="addButton">
<property name="text">
@ -112,6 +112,39 @@
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="applyToChildrenHorizontalLayout" stretch="0,0">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="applyIconToPushButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="styleSheet">
<string notr="true">padding: 4px 10px</string>
</property>
<property name="text">
<string>Apply icon &amp;to ...</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<tabstops>
@ -121,6 +154,7 @@
<tabstop>customIconsView</tabstop>
<tabstop>addButton</tabstop>
<tabstop>deleteButton</tabstop>
<tabstop>applyIconToPushButton</tabstop>
</tabstops>
<resources/>
<connections/>

View File

@ -188,6 +188,7 @@ void EditEntryWidget::setupAdvanced()
void EditEntryWidget::setupIcon()
{
m_iconsWidget->setShowApplyIconToButton(false);
addPage(tr("Icon"), FilePath::instance()->icon("apps", "preferences-desktop-icons"), m_iconsWidget);
connect(this, SIGNAL(accepted()), m_iconsWidget, SLOT(abortRequests()));
connect(this, SIGNAL(rejected()), m_iconsWidget, SLOT(abortRequests()));

View File

@ -212,6 +212,17 @@ void EditGroupWidget::apply()
// Icons add/remove are applied globally outside the transaction!
m_group->copyDataFrom(m_temporaryGroup.data());
// Assign the icon to children if selected
if (iconStruct.applyTo == ApplyIconToOptions::CHILD_GROUPS
|| iconStruct.applyTo == ApplyIconToOptions::ALL_CHILDREN) {
m_group->applyGroupIconToChildGroups();
}
if (iconStruct.applyTo == ApplyIconToOptions::CHILD_ENTRIES
|| iconStruct.applyTo == ApplyIconToOptions::ALL_CHILDREN) {
m_group->applyGroupIconToChildEntries();
}
setModified(false);
}

View File

@ -1081,3 +1081,103 @@ void TestGroup::testHierarchy()
QVERIFY(hierarchy.contains("group2"));
QVERIFY(hierarchy.contains("group3"));
}
void TestGroup::testApplyGroupIconRecursively()
{
// Create a database with two nested groups with one entry each
Database* database = new Database();
Group* subgroup = new Group();
subgroup->setName("Subgroup");
subgroup->setParent(database->rootGroup());
QVERIFY(subgroup);
Group* subsubgroup = new Group();
subsubgroup->setName("Subsubgroup");
subsubgroup->setParent(subgroup);
QVERIFY(subsubgroup);
Entry* subgroupEntry = subgroup->addEntryWithPath("Subgroup entry");
QVERIFY(subgroupEntry);
subgroup->setIcon(1);
Entry* subsubgroupEntry = subsubgroup->addEntryWithPath("Subsubgroup entry");
QVERIFY(subsubgroupEntry);
subsubgroup->setIcon(2);
// Set an icon per number to the root group and apply recursively
// -> all groups and entries have the same icon
const int rootIconNumber = 42;
database->rootGroup()->setIcon(rootIconNumber);
QVERIFY(database->rootGroup()->iconNumber() == rootIconNumber);
database->rootGroup()->applyGroupIconToChildGroups();
database->rootGroup()->applyGroupIconToChildEntries();
QVERIFY(subgroup->iconNumber() == rootIconNumber);
QVERIFY(subgroupEntry->iconNumber() == rootIconNumber);
QVERIFY(subsubgroup->iconNumber() == rootIconNumber);
QVERIFY(subsubgroupEntry->iconNumber() == rootIconNumber);
// Set an icon per number to the subsubgroup and apply recursively
// -> only the subsubgroup related groups and entries have updated icons
const int subsubgroupIconNumber = 24;
subsubgroup->setIcon(subsubgroupIconNumber);
QVERIFY(subsubgroup->iconNumber() == subsubgroupIconNumber);
subsubgroup->applyGroupIconToChildGroups();
subsubgroup->applyGroupIconToChildEntries();
QVERIFY(database->rootGroup()->iconNumber() == rootIconNumber);
QVERIFY(subgroup->iconNumber() == rootIconNumber);
QVERIFY(subgroupEntry->iconNumber() == rootIconNumber);
QVERIFY(subsubgroup->iconNumber() == subsubgroupIconNumber);
QVERIFY(subsubgroupEntry->iconNumber() == subsubgroupIconNumber);
// Set an icon per UUID to the subgroup and apply recursively
// -> all groups and entries except the root group have the same icon
const QUuid subgroupIconUuid = QUuid::createUuid();
QImage subgroupIcon(16, 16, QImage::Format_RGB32);
subgroupIcon.setPixel(0, 0, qRgb(255, 0, 0));
database->metadata()->addCustomIcon(subgroupIconUuid, subgroupIcon);
subgroup->setIcon(subgroupIconUuid);
subgroup->applyGroupIconToChildGroups();
subgroup->applyGroupIconToChildEntries();
QVERIFY(database->rootGroup()->iconNumber() == rootIconNumber);
QCOMPARE(subgroup->iconUuid(), subgroupIconUuid);
QCOMPARE(subgroup->icon(), subgroupIcon);
QCOMPARE(subgroupEntry->iconUuid(), subgroupIconUuid);
QCOMPARE(subgroupEntry->icon(), subgroupIcon);
QCOMPARE(subsubgroup->iconUuid(), subgroupIconUuid);
QCOMPARE(subsubgroup->icon(), subgroupIcon);
QCOMPARE(subsubgroupEntry->iconUuid(), subgroupIconUuid);
QCOMPARE(subsubgroupEntry->icon(), subgroupIcon);
// Reset all icons to root icon
database->rootGroup()->setIcon(rootIconNumber);
QVERIFY(database->rootGroup()->iconNumber() == rootIconNumber);
database->rootGroup()->applyGroupIconToChildGroups();
database->rootGroup()->applyGroupIconToChildEntries();
QVERIFY(subgroup->iconNumber() == rootIconNumber);
QVERIFY(subgroupEntry->iconNumber() == rootIconNumber);
QVERIFY(subsubgroup->iconNumber() == rootIconNumber);
QVERIFY(subsubgroupEntry->iconNumber() == rootIconNumber);
// Apply only for child groups
const int iconForGroups = 10;
database->rootGroup()->setIcon(iconForGroups);
QVERIFY(database->rootGroup()->iconNumber() == iconForGroups);
database->rootGroup()->applyGroupIconToChildGroups();
QVERIFY(database->rootGroup()->iconNumber() == iconForGroups);
QVERIFY(subgroup->iconNumber() == iconForGroups);
QVERIFY(subgroupEntry->iconNumber() == rootIconNumber);
QVERIFY(subsubgroup->iconNumber() == iconForGroups);
QVERIFY(subsubgroupEntry->iconNumber() == rootIconNumber);
// Apply only for child entries
const int iconForEntries = 20;
database->rootGroup()->setIcon(iconForEntries);
QVERIFY(database->rootGroup()->iconNumber() == iconForEntries);
database->rootGroup()->applyGroupIconToChildEntries();
QVERIFY(database->rootGroup()->iconNumber() == iconForEntries);
QVERIFY(subgroup->iconNumber() == iconForGroups);
QVERIFY(subgroupEntry->iconNumber() == iconForEntries);
QVERIFY(subsubgroup->iconNumber() == iconForGroups);
QVERIFY(subsubgroupEntry->iconNumber() == iconForEntries);
}

View File

@ -47,6 +47,7 @@ private slots:
void testEquals();
void testChildrenSort();
void testHierarchy();
void testApplyGroupIconRecursively();
};
#endif // KEEPASSX_TESTGROUP_H