Page Menu
Home
GnuPG
Search
Configure Global Search
Log In
Files
F37955183
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Size
23 KB
Subscribers
None
View Options
diff --git a/src/dialogs/editgroupdialog.cpp b/src/dialogs/editgroupdialog.cpp
index 6a238f58e..9e712fdbe 100644
--- a/src/dialogs/editgroupdialog.cpp
+++ b/src/dialogs/editgroupdialog.cpp
@@ -1,487 +1,595 @@
/*
dialogs/editgroupdialog.cpp
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2021 g10 Code GmbH
SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "editgroupdialog.h"
#include "commands/detailscommand.h"
#include "utils/gui-helper.h"
#include "view/keytreeview.h"
#include <settings.h>
#include <Libkleo/Algorithm>
#include <Libkleo/Compat>
#include <Libkleo/DefaultKeyFilter>
#include <Libkleo/KeyCache>
+#include <Libkleo/KeyFilter>
+#include <Libkleo/KeyFilterManager>
#include <Libkleo/KeyListModel>
#include <Libkleo/KeyListSortFilterProxyModel>
#include <KColorScheme>
#include <KConfigGroup>
#include <KGuiItem>
#include <KLocalizedString>
#include <KSeparator>
#include <KSharedConfig>
#include <KStandardGuiItem>
#include <QApplication>
+#include <QComboBox>
+#include <QConcatenateTablesProxyModel>
#include <QDialogButtonBox>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QItemSelectionModel>
#include <QLabel>
#include <QLineEdit>
#include <QPalette>
#include <QPushButton>
#include <QTreeView>
#include <QVBoxLayout>
#include "kleopatra_debug.h"
using namespace Kleo;
using namespace Kleo::Commands;
using namespace Kleo::Dialogs;
using namespace GpgME;
Q_DECLARE_METATYPE(GpgME::Key)
namespace
{
auto createOpenPGPOnlyKeyFilter()
{
auto filter = std::make_shared<DefaultKeyFilter>();
filter->setIsOpenPGP(DefaultKeyFilter::Set);
return filter;
}
}
+namespace
+{
+
+class CanEncryptKeyFilterModel : public QAbstractListModel
+{
+ using QAbstractListModel::QAbstractListModel;
+
+ int rowCount(const QModelIndex &) const override
+ {
+ return 1;
+ }
+
+ QVariant data(const QModelIndex &index, int role) const override
+ {
+ if (index.row() != 0) {
+ return {};
+ }
+ static std::shared_ptr<DefaultKeyFilter> filter;
+ if (!filter) {
+ filter = std::make_shared<DefaultKeyFilter>();
+ filter->setCanEncrypt(DefaultKeyFilter::Set);
+ filter->setIsBad(DefaultKeyFilter::NotSet);
+ filter->setName(i18n("Usable for Encryption"));
+ filter->setDescription(i18n("Certificates that can be used for encryption"));
+ filter->setId(QStringLiteral("CanEncryptFilter"));
+ if (!Settings{}.cmsEnabled()) {
+ filter->setIsOpenPGP(DefaultKeyFilter::Set);
+ }
+ }
+
+ switch (role) {
+ case Qt::DecorationRole:
+ return filter->icon();
+
+ case Qt::DisplayRole:
+ case Qt::EditRole:
+ return filter->name();
+ case Qt::ToolTipRole:
+ return filter->description();
+
+ case KeyFilterManager::FilterIdRole:
+ return filter->id();
+
+ case KeyFilterManager::FilterMatchContextsRole:
+ return QVariant::fromValue(filter->availableMatchContexts());
+
+ case KeyFilterManager::FilterRole:
+ return QVariant::fromValue(std::static_pointer_cast<KeyFilter>(filter));
+
+ default:
+ return QVariant();
+ }
+ }
+};
+
+class FiltersProxyModel : public QSortFilterProxyModel
+{
+ Q_OBJECT
+
+public:
+ using QSortFilterProxyModel::QSortFilterProxyModel;
+
+protected:
+ bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
+ {
+ const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent);
+ const auto matchContexts = qvariant_cast<KeyFilter::MatchContexts>(sourceModel()->data(index, KeyFilterManager::FilterMatchContextsRole));
+ return matchContexts & KeyFilter::Filtering;
+ }
+};
+
+}
+
class WarnNonEncryptionKeysProxyModel : public Kleo::AbstractKeyListSortFilterProxyModel
{
Q_OBJECT
public:
using Kleo::AbstractKeyListSortFilterProxyModel::AbstractKeyListSortFilterProxyModel;
WarnNonEncryptionKeysProxyModel *clone() const override
{
return new WarnNonEncryptionKeysProxyModel(this->parent());
}
QVariant data(const QModelIndex &index, int role) const override
{
const auto sourceIndex = sourceModel()->index(index.row(), index.column());
if (!Kleo::keyHasEncrypt(sourceIndex.data(KeyList::KeyRole).value<Key>())) {
if (role == Qt::DecorationRole && index.column() == KeyList::Columns::Validity) {
return QIcon::fromTheme(QStringLiteral("data-error"));
}
if (role == Qt::DisplayRole && index.column() == KeyList::Columns::Validity) {
return i18nc("@info as in 'this certificate is unusable'", "unusable");
}
if (role == Qt::ToolTipRole) {
return i18nc("@info:tooltip", "This certificate cannot be used for encryption.");
}
}
return sourceIndex.data(role);
}
};
class DisableNonEncryptionKeysProxyModel : public Kleo::AbstractKeyListSortFilterProxyModel
{
Q_OBJECT
public:
using Kleo::AbstractKeyListSortFilterProxyModel::AbstractKeyListSortFilterProxyModel;
~DisableNonEncryptionKeysProxyModel() override;
DisableNonEncryptionKeysProxyModel *clone() const override
{
return new DisableNonEncryptionKeysProxyModel(this->parent());
}
QVariant data(const QModelIndex &index, int role) const override
{
const auto sourceIndex = sourceModel()->index(index.row(), index.column());
if (!Kleo::keyHasEncrypt(sourceIndex.data(KeyList::KeyRole).value<Key>())) {
- if (role == Qt::ForegroundRole) {
- return qApp->palette().color(QPalette::Disabled, QPalette::Text);
+ if (role == Qt::DecorationRole && index.column() == KeyList::Columns::Validity) {
+ return QIcon::fromTheme(QStringLiteral("data-error"));
}
- if (role == Qt::BackgroundRole) {
- return KColorScheme(QPalette::Disabled, KColorScheme::View).background(KColorScheme::NeutralBackground).color();
+ if (role == Qt::DisplayRole && index.column() == KeyList::Columns::Validity) {
+ return i18nc("@info as in 'this certificate is unusable'", "unusable");
}
if (role == Qt::ToolTipRole) {
return i18nc("@info:tooltip", "This certificate cannot be added to the group as it cannot be used for encryption.");
}
}
return sourceIndex.data(role);
}
Qt::ItemFlags flags(const QModelIndex &index) const override
{
auto originalFlags = index.model()->QAbstractItemModel::flags(index);
- if (Kleo::keyHasEncrypt(index.data(KeyList::KeyRole).value<Key>())) {
+ const auto key = index.data(KeyList::KeyRole).value<Key>();
+ if (Kleo::keyHasEncrypt(key) && !key.isBad()) {
return originalFlags;
} else {
return (originalFlags & ~Qt::ItemIsEnabled);
}
return {};
}
};
DisableNonEncryptionKeysProxyModel::~DisableNonEncryptionKeysProxyModel() = default;
class EditGroupDialog::Private
{
friend class ::Kleo::Dialogs::EditGroupDialog;
EditGroupDialog *const q;
struct {
QLineEdit *groupNameEdit = nullptr;
QLineEdit *availableKeysFilter = nullptr;
KeyTreeView *availableKeysList = nullptr;
QLineEdit *groupKeysFilter = nullptr;
KeyTreeView *groupKeysList = nullptr;
QDialogButtonBox *buttonBox = nullptr;
+ QComboBox *combo = nullptr;
} ui;
AbstractKeyListModel *availableKeysModel = nullptr;
AbstractKeyListModel *groupKeysModel = nullptr;
KeyGroup keyGroup;
std::vector<GpgME::Key> oldKeys;
+ FiltersProxyModel *filtersProxyModel = nullptr;
public:
Private(EditGroupDialog *qq)
: q(qq)
{
auto mainLayout = new QVBoxLayout(q);
{
auto groupNameLayout = new QHBoxLayout();
auto label = new QLabel(i18nc("Name of a group of keys", "Name:"), q);
groupNameLayout->addWidget(label);
ui.groupNameEdit = new QLineEdit(q);
label->setBuddy(ui.groupNameEdit);
groupNameLayout->addWidget(ui.groupNameEdit);
mainLayout->addLayout(groupNameLayout);
}
mainLayout->addWidget(new KSeparator(Qt::Horizontal, q));
auto centerLayout = new QVBoxLayout;
auto availableKeysGroupBox = new QGroupBox{i18nc("@title", "Available Keys"), q};
availableKeysGroupBox->setFlat(true);
auto availableKeysLayout = new QVBoxLayout{availableKeysGroupBox};
{
auto hbox = new QHBoxLayout;
auto label = new QLabel{i18nc("@label", "Search:")};
label->setAccessibleName(i18nc("@label", "Search available keys"));
label->setToolTip(i18nc("@info:tooltip", "Search the list of available keys for keys matching the search term."));
hbox->addWidget(label);
ui.availableKeysFilter = new QLineEdit(q);
ui.availableKeysFilter->setClearButtonEnabled(true);
ui.availableKeysFilter->setAccessibleName(i18nc("@label", "Search available keys"));
ui.availableKeysFilter->setToolTip(i18nc("@info:tooltip", "Search the list of available keys for keys matching the search term."));
ui.availableKeysFilter->setPlaceholderText(i18nc("@info::placeholder", "Enter search term"));
ui.availableKeysFilter->setCursorPosition(0); // prevent emission of accessible text cursor event before accessible focus event
label->setBuddy(ui.availableKeysFilter);
hbox->addWidget(ui.availableKeysFilter, 1);
+ ui.combo = new QComboBox;
+ ui.combo->setAccessibleName(i18n("Filter certificates by category"));
+ ui.combo->setToolTip(i18nc("@info:tooltip", "Show only certificates that belong to the selected category."));
+
+ hbox->addWidget(ui.combo);
+
+ filtersProxyModel = new FiltersProxyModel{q};
+ auto filtersConcatenateModel = new QConcatenateTablesProxyModel{q};
+ filtersConcatenateModel->addSourceModel(KeyFilterManager::instance()->model());
+ filtersConcatenateModel->addSourceModel(new CanEncryptKeyFilterModel{q});
+ filtersProxyModel->setSourceModel(filtersConcatenateModel);
+ filtersProxyModel->sort(0, Qt::AscendingOrder);
+ ui.combo->setModel(filtersProxyModel);
+
+ connect(ui.combo, &QComboBox::currentIndexChanged, q, [this](int index) {
+ const auto filter =
+ filtersProxyModel->data(filtersProxyModel->index(index, 0), KeyFilterManager::FilterRole).value<std::shared_ptr<KeyFilter>>();
+ ui.availableKeysList->setKeyFilter(filter);
+ });
+
availableKeysLayout->addLayout(hbox);
}
availableKeysModel = AbstractKeyListModel::createFlatKeyListModel(q);
availableKeysModel->setKeys(KeyCache::instance()->keys());
auto proxyModel = new DisableNonEncryptionKeysProxyModel(q);
proxyModel->setSourceModel(availableKeysModel);
ui.availableKeysList = new KeyTreeView({}, nullptr, proxyModel, q, {});
ui.availableKeysList->view()->setAccessibleName(i18n("available keys"));
ui.availableKeysList->view()->setRootIsDecorated(false);
ui.availableKeysList->setFlatModel(availableKeysModel);
ui.availableKeysList->setHierarchicalView(false);
if (!Settings{}.cmsEnabled()) {
ui.availableKeysList->setKeyFilter(createOpenPGPOnlyKeyFilter());
}
availableKeysLayout->addWidget(ui.availableKeysList, /*stretch=*/1);
centerLayout->addWidget(availableKeysGroupBox, /*stretch=*/1);
auto buttonsLayout = new QHBoxLayout;
buttonsLayout->addStretch(1);
auto addButton = new QPushButton(q);
addButton->setIcon(QIcon::fromTheme(QStringLiteral("arrow-down")));
addButton->setAccessibleName(i18nc("@action:button", "Add Selected Keys"));
addButton->setToolTip(i18nc("@info:tooltip", "Add the selected keys to the group"));
addButton->setEnabled(false);
buttonsLayout->addWidget(addButton);
auto removeButton = new QPushButton(q);
removeButton->setIcon(QIcon::fromTheme(QStringLiteral("arrow-up")));
removeButton->setAccessibleName(i18nc("@action:button", "Remove Selected Keys"));
removeButton->setToolTip(i18nc("@info:tooltip", "Remove the selected keys from the group"));
removeButton->setEnabled(false);
buttonsLayout->addWidget(removeButton);
buttonsLayout->addStretch(1);
centerLayout->addLayout(buttonsLayout);
auto groupKeysGroupBox = new QGroupBox{i18nc("@title", "Group Keys"), q};
groupKeysGroupBox->setFlat(true);
auto groupKeysLayout = new QVBoxLayout{groupKeysGroupBox};
{
auto hbox = new QHBoxLayout;
auto label = new QLabel{i18nc("@label", "Search:")};
label->setAccessibleName(i18nc("@label", "Search group keys"));
label->setToolTip(i18nc("@info:tooltip", "Search the list of group keys for keys matching the search term."));
hbox->addWidget(label);
ui.groupKeysFilter = new QLineEdit(q);
ui.groupKeysFilter->setClearButtonEnabled(true);
ui.groupKeysFilter->setAccessibleName(i18nc("@label", "Search group keys"));
ui.groupKeysFilter->setToolTip(i18nc("@info:tooltip", "Search the list of group keys for keys matching the search term."));
ui.groupKeysFilter->setPlaceholderText(i18nc("@info::placeholder", "Enter search term"));
ui.groupKeysFilter->setCursorPosition(0); // prevent emission of accessible text cursor event before accessible focus event
label->setBuddy(ui.groupKeysFilter);
hbox->addWidget(ui.groupKeysFilter, 1);
groupKeysLayout->addLayout(hbox);
}
groupKeysModel = AbstractKeyListModel::createFlatKeyListModel(q);
auto warnNonEncryptionProxyModel = new WarnNonEncryptionKeysProxyModel(q);
ui.groupKeysList = new KeyTreeView({}, nullptr, warnNonEncryptionProxyModel, q, {});
ui.groupKeysList->view()->setAccessibleName(i18n("group keys"));
ui.groupKeysList->view()->setRootIsDecorated(false);
ui.groupKeysList->setFlatModel(groupKeysModel);
ui.groupKeysList->setHierarchicalView(false);
groupKeysLayout->addWidget(ui.groupKeysList, /*stretch=*/1);
centerLayout->addWidget(groupKeysGroupBox, /*stretch=*/1);
mainLayout->addLayout(centerLayout);
mainLayout->addWidget(new KSeparator(Qt::Horizontal, q));
ui.buttonBox = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel, q);
QPushButton *saveButton = ui.buttonBox->button(QDialogButtonBox::Save);
KGuiItem::assign(saveButton, KStandardGuiItem::save());
KGuiItem::assign(ui.buttonBox->button(QDialogButtonBox::Cancel), KStandardGuiItem::cancel());
saveButton->setEnabled(false);
mainLayout->addWidget(ui.buttonBox);
// prevent accidental closing of dialog when pressing Enter while a search field has focus
Kleo::unsetAutoDefaultButtons(q);
connect(ui.groupNameEdit, &QLineEdit::textChanged, q, [saveButton](const QString &text) {
saveButton->setEnabled(!text.trimmed().isEmpty());
});
connect(ui.availableKeysFilter, &QLineEdit::textChanged, ui.availableKeysList, &KeyTreeView::setStringFilter);
connect(ui.availableKeysList->view()->selectionModel(),
&QItemSelectionModel::selectionChanged,
q,
[addButton](const QItemSelection &selected, const QItemSelection &) {
addButton->setEnabled(!selected.isEmpty());
});
connect(ui.availableKeysList->view(), &QAbstractItemView::doubleClicked, q, [this](const QModelIndex &index) {
showKeyDetails(index);
});
connect(ui.groupKeysFilter, &QLineEdit::textChanged, ui.groupKeysList, &KeyTreeView::setStringFilter);
connect(ui.groupKeysList->view()->selectionModel(),
&QItemSelectionModel::selectionChanged,
q,
[removeButton](const QItemSelection &selected, const QItemSelection &) {
removeButton->setEnabled(!selected.isEmpty());
});
connect(ui.groupKeysList->view(), &QAbstractItemView::doubleClicked, q, [this](const QModelIndex &index) {
showKeyDetails(index);
});
connect(addButton, &QPushButton::clicked, q, [this]() {
addKeysToGroup();
});
connect(removeButton, &QPushButton::clicked, q, [this]() {
removeKeysFromGroup();
});
connect(ui.buttonBox, &QDialogButtonBox::accepted, q, &EditGroupDialog::accept);
connect(ui.buttonBox, &QDialogButtonBox::rejected, q, &EditGroupDialog::reject);
connect(KeyCache::instance().get(), &KeyCache::keysMayHaveChanged, q, [this] {
updateFromKeyCache();
});
// calculate default size with enough space for the key list
const auto fm = q->fontMetrics();
const QSize sizeHint = q->sizeHint();
const QSize defaultSize = QSize(qMax(sizeHint.width(), 150 * fm.horizontalAdvance(QLatin1Char('x'))), sizeHint.height());
restoreLayout(defaultSize);
+
+ for (auto i = 0; i < filtersProxyModel->rowCount(); ++i) {
+ if (filtersProxyModel->data(filtersProxyModel->index(i, 0), KeyFilterManager::FilterRole).value<std::shared_ptr<KeyFilter>>()->id()
+ == QStringLiteral("CanEncryptFilter")) {
+ ui.combo->setCurrentIndex(i);
+ break;
+ };
+ }
}
~Private()
{
saveLayout();
}
private:
void saveLayout()
{
KConfigGroup configGroup(KSharedConfig::openConfig(), QStringLiteral("EditGroupDialog"));
configGroup.writeEntry("Size", q->size());
configGroup.sync();
}
void restoreLayout(const QSize &defaultSize)
{
const KConfigGroup configGroup(KSharedConfig::openConfig(), QStringLiteral("EditGroupDialog"));
const KConfigGroup availableKeysConfig = configGroup.group(QStringLiteral("AvailableKeysView"));
ui.availableKeysList->restoreLayout(availableKeysConfig);
const KConfigGroup groupKeysConfig = configGroup.group(QStringLiteral("GroupKeysView"));
ui.groupKeysList->restoreLayout(groupKeysConfig);
const QSize size = configGroup.readEntry("Size", defaultSize);
if (size.isValid()) {
q->resize(size);
}
}
void showKeyDetails(const QModelIndex &index)
{
if (!index.isValid()) {
return;
}
const auto key = index.model()->data(index, KeyList::KeyRole).value<GpgME::Key>();
if (!key.isNull()) {
auto cmd = new DetailsCommand(key);
cmd->setParentWidget(q);
cmd->start();
}
}
void addKeysToGroup();
void removeKeysFromGroup();
void updateFromKeyCache();
};
void EditGroupDialog::Private::addKeysToGroup()
{
const std::vector<Key> selectedGroupKeys = ui.groupKeysList->selectedKeys();
const std::vector<Key> selectedKeys = ui.availableKeysList->selectedKeys();
groupKeysModel->addKeys(selectedKeys);
for (const Key &key : selectedKeys) {
availableKeysModel->removeKey(key);
}
ui.groupKeysList->selectKeys(selectedGroupKeys);
}
void EditGroupDialog::Private::removeKeysFromGroup()
{
const auto selectedOtherKeys = ui.availableKeysList->selectedKeys();
const std::vector<Key> selectedKeys = ui.groupKeysList->selectedKeys();
for (const Key &key : selectedKeys) {
groupKeysModel->removeKey(key);
}
availableKeysModel->addKeys(selectedKeys);
ui.availableKeysList->selectKeys(selectedOtherKeys);
}
void EditGroupDialog::Private::updateFromKeyCache()
{
const auto selectedGroupKeys = ui.groupKeysList->selectedKeys();
const auto selectedOtherKeys = ui.availableKeysList->selectedKeys();
const auto wasGroupKey = [this](const Key &key) {
return std::ranges::any_of(oldKeys, [key](const auto &k) {
return _detail::ByFingerprint<std::equal_to>()(k, key);
});
};
const auto allKeys = KeyCache::instance()->keys();
std::vector<Key> groupKeys;
groupKeys.reserve(allKeys.size());
std::vector<Key> otherKeys;
otherKeys.reserve(otherKeys.size());
std::partition_copy(allKeys.begin(), allKeys.end(), std::back_inserter(groupKeys), std::back_inserter(otherKeys), wasGroupKey);
groupKeysModel->setKeys(groupKeys);
availableKeysModel->setKeys(otherKeys);
ui.groupKeysList->selectKeys(selectedGroupKeys);
ui.availableKeysList->selectKeys(selectedOtherKeys);
}
EditGroupDialog::EditGroupDialog(QWidget *parent)
: QDialog(parent)
, d(new Private(this))
{
setWindowTitle(i18nc("@title:window", "Edit Group"));
}
EditGroupDialog::~EditGroupDialog() = default;
void EditGroupDialog::setInitialFocus(FocusWidget widget)
{
switch (widget) {
case GroupName:
d->ui.groupNameEdit->setFocus();
break;
case KeysFilter:
d->ui.availableKeysFilter->setFocus();
break;
default:
qCDebug(KLEOPATRA_LOG) << "EditGroupDialog::setInitialFocus - invalid focus widget:" << widget;
}
}
void EditGroupDialog::showEvent(QShowEvent *event)
{
QDialog::showEvent(event);
// prevent accidental closing of dialog when pressing Enter while a search field has focus
Kleo::unsetDefaultButtons(d->ui.buttonBox);
}
KeyGroup EditGroupDialog::keyGroup() const
{
std::vector<Key> keys;
keys.reserve(d->groupKeysModel->rowCount());
for (int row = 0; row < d->groupKeysModel->rowCount(); ++row) {
const QModelIndex index = d->groupKeysModel->index(row, 0);
keys.push_back(d->groupKeysModel->key(index));
}
d->keyGroup.setKeys(keys);
d->keyGroup.setName(d->ui.groupNameEdit->text().trimmed());
return d->keyGroup;
}
void EditGroupDialog::setKeyGroup(const KeyGroup &keyGroup)
{
d->keyGroup = keyGroup;
const auto &keys = keyGroup.keys();
d->oldKeys = std::vector<GpgME::Key>(keys.begin(), keys.end());
d->groupKeysModel->setKeys(d->oldKeys);
// update the keys in the "available keys" list
const auto isGroupKey = [keys](const Key &key) {
return std::ranges::any_of(keys, [key](const auto &k) {
return _detail::ByFingerprint<std::equal_to>()(k, key);
});
};
auto otherKeys = KeyCache::instance()->keys();
Kleo::erase_if(otherKeys, isGroupKey);
d->availableKeysModel->setKeys(otherKeys);
d->ui.groupNameEdit->setText(keyGroup.name());
}
#include "editgroupdialog.moc"
#include "moc_editgroupdialog.cpp"
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Mar 19, 6:10 PM (1 d, 20 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
e4/20/6529f7ed88e9f2f46b85c3ef54da
Attached To
rKLEOPATRA Kleopatra
Event Timeline
Log In to Comment