File indexing completed on 2024-06-23 05:14:03

0001 /*
0002     dialogs/editgroupdialog.cpp
0003 
0004     This file is part of Kleopatra, the KDE keymanager
0005     SPDX-FileCopyrightText: 2021 g10 Code GmbH
0006     SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
0007 
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 
0011 #include "editgroupdialog.h"
0012 
0013 #include "commands/detailscommand.h"
0014 #include "utils/gui-helper.h"
0015 #include "view/keytreeview.h"
0016 #include <settings.h>
0017 
0018 #include <Libkleo/Algorithm>
0019 #include <Libkleo/DefaultKeyFilter>
0020 #include <Libkleo/KeyCache>
0021 #include <Libkleo/KeyListModel>
0022 
0023 #include <KConfigGroup>
0024 #include <KGuiItem>
0025 #include <KLocalizedString>
0026 #include <KSeparator>
0027 #include <KSharedConfig>
0028 #include <KStandardGuiItem>
0029 
0030 #include <QDialogButtonBox>
0031 #include <QGroupBox>
0032 #include <QHBoxLayout>
0033 #include <QItemSelectionModel>
0034 #include <QLabel>
0035 #include <QLineEdit>
0036 #include <QPushButton>
0037 #include <QTreeView>
0038 #include <QVBoxLayout>
0039 
0040 #include "kleopatra_debug.h"
0041 
0042 using namespace Kleo;
0043 using namespace Kleo::Commands;
0044 using namespace Kleo::Dialogs;
0045 using namespace GpgME;
0046 
0047 Q_DECLARE_METATYPE(GpgME::Key)
0048 
0049 namespace
0050 {
0051 auto createOpenPGPOnlyKeyFilter()
0052 {
0053     auto filter = std::make_shared<DefaultKeyFilter>();
0054     filter->setIsOpenPGP(DefaultKeyFilter::Set);
0055     return filter;
0056 }
0057 }
0058 
0059 class EditGroupDialog::Private
0060 {
0061     friend class ::Kleo::Dialogs::EditGroupDialog;
0062     EditGroupDialog *const q;
0063 
0064     struct {
0065         QLineEdit *groupNameEdit = nullptr;
0066         QLineEdit *availableKeysFilter = nullptr;
0067         KeyTreeView *availableKeysList = nullptr;
0068         QLineEdit *groupKeysFilter = nullptr;
0069         KeyTreeView *groupKeysList = nullptr;
0070         QDialogButtonBox *buttonBox = nullptr;
0071     } ui;
0072     AbstractKeyListModel *availableKeysModel = nullptr;
0073     AbstractKeyListModel *groupKeysModel = nullptr;
0074 
0075 public:
0076     Private(EditGroupDialog *qq)
0077         : q(qq)
0078     {
0079         auto mainLayout = new QVBoxLayout(q);
0080 
0081         {
0082             auto groupNameLayout = new QHBoxLayout();
0083             auto label = new QLabel(i18nc("Name of a group of keys", "Name:"), q);
0084             groupNameLayout->addWidget(label);
0085             ui.groupNameEdit = new QLineEdit(q);
0086             label->setBuddy(ui.groupNameEdit);
0087             groupNameLayout->addWidget(ui.groupNameEdit);
0088             mainLayout->addLayout(groupNameLayout);
0089         }
0090 
0091         mainLayout->addWidget(new KSeparator(Qt::Horizontal, q));
0092 
0093         auto centerLayout = new QVBoxLayout;
0094 
0095         auto availableKeysGroupBox = new QGroupBox{i18nc("@title", "Available Keys"), q};
0096         availableKeysGroupBox->setFlat(true);
0097         auto availableKeysLayout = new QVBoxLayout{availableKeysGroupBox};
0098 
0099         {
0100             auto hbox = new QHBoxLayout;
0101             auto label = new QLabel{i18nc("@label", "Search:")};
0102             label->setAccessibleName(i18nc("@label", "Search available keys"));
0103             label->setToolTip(i18nc("@info:tooltip", "Search the list of available keys for keys matching the search term."));
0104             hbox->addWidget(label);
0105 
0106             ui.availableKeysFilter = new QLineEdit(q);
0107             ui.availableKeysFilter->setClearButtonEnabled(true);
0108             ui.availableKeysFilter->setAccessibleName(i18nc("@label", "Search available keys"));
0109             ui.availableKeysFilter->setToolTip(i18nc("@info:tooltip", "Search the list of available keys for keys matching the search term."));
0110             ui.availableKeysFilter->setPlaceholderText(i18nc("@info::placeholder", "Enter search term"));
0111             ui.availableKeysFilter->setCursorPosition(0); // prevent emission of accessible text cursor event before accessible focus event
0112             label->setBuddy(ui.availableKeysFilter);
0113             hbox->addWidget(ui.availableKeysFilter, 1);
0114 
0115             availableKeysLayout->addLayout(hbox);
0116         }
0117 
0118         availableKeysModel = AbstractKeyListModel::createFlatKeyListModel(q);
0119         availableKeysModel->setKeys(KeyCache::instance()->keys());
0120         ui.availableKeysList = new KeyTreeView(q);
0121         ui.availableKeysList->view()->setAccessibleName(i18n("available keys"));
0122         ui.availableKeysList->view()->setRootIsDecorated(false);
0123         ui.availableKeysList->setFlatModel(availableKeysModel);
0124         ui.availableKeysList->setHierarchicalView(false);
0125         if (!Settings{}.cmsEnabled()) {
0126             ui.availableKeysList->setKeyFilter(createOpenPGPOnlyKeyFilter());
0127         }
0128         availableKeysLayout->addWidget(ui.availableKeysList, /*stretch=*/1);
0129 
0130         centerLayout->addWidget(availableKeysGroupBox, /*stretch=*/1);
0131 
0132         auto buttonsLayout = new QHBoxLayout;
0133         buttonsLayout->addStretch(1);
0134 
0135         auto addButton = new QPushButton(q);
0136         addButton->setIcon(QIcon::fromTheme(QStringLiteral("arrow-down")));
0137         addButton->setAccessibleName(i18nc("@action:button", "Add Selected Keys"));
0138         addButton->setToolTip(i18n("Add the selected keys to the group"));
0139         addButton->setEnabled(false);
0140         buttonsLayout->addWidget(addButton);
0141 
0142         auto removeButton = new QPushButton(q);
0143         removeButton->setIcon(QIcon::fromTheme(QStringLiteral("arrow-up")));
0144         removeButton->setAccessibleName(i18nc("@action:button", "Remove Selected Keys"));
0145         removeButton->setToolTip(i18n("Remove the selected keys from the group"));
0146         removeButton->setEnabled(false);
0147         buttonsLayout->addWidget(removeButton);
0148 
0149         buttonsLayout->addStretch(1);
0150 
0151         centerLayout->addLayout(buttonsLayout);
0152 
0153         auto groupKeysGroupBox = new QGroupBox{i18nc("@title", "Group Keys"), q};
0154         groupKeysGroupBox->setFlat(true);
0155         auto groupKeysLayout = new QVBoxLayout{groupKeysGroupBox};
0156 
0157         {
0158             auto hbox = new QHBoxLayout;
0159             auto label = new QLabel{i18nc("@label", "Search:")};
0160             label->setAccessibleName(i18nc("@label", "Search group keys"));
0161             label->setToolTip(i18nc("@info:tooltip", "Search the list of group keys for keys matching the search term."));
0162             hbox->addWidget(label);
0163 
0164             ui.groupKeysFilter = new QLineEdit(q);
0165             ui.groupKeysFilter->setClearButtonEnabled(true);
0166             ui.groupKeysFilter->setAccessibleName(i18nc("@label", "Search group keys"));
0167             ui.groupKeysFilter->setToolTip(i18nc("@info:tooltip", "Search the list of group keys for keys matching the search term."));
0168             ui.groupKeysFilter->setPlaceholderText(i18nc("@info::placeholder", "Enter search term"));
0169             ui.groupKeysFilter->setCursorPosition(0); // prevent emission of accessible text cursor event before accessible focus event
0170             label->setBuddy(ui.groupKeysFilter);
0171             hbox->addWidget(ui.groupKeysFilter, 1);
0172 
0173             groupKeysLayout->addLayout(hbox);
0174         }
0175 
0176         groupKeysModel = AbstractKeyListModel::createFlatKeyListModel(q);
0177         ui.groupKeysList = new KeyTreeView(q);
0178         ui.groupKeysList->view()->setAccessibleName(i18n("group keys"));
0179         ui.groupKeysList->view()->setRootIsDecorated(false);
0180         ui.groupKeysList->setFlatModel(groupKeysModel);
0181         ui.groupKeysList->setHierarchicalView(false);
0182         groupKeysLayout->addWidget(ui.groupKeysList, /*stretch=*/1);
0183 
0184         centerLayout->addWidget(groupKeysGroupBox, /*stretch=*/1);
0185 
0186         mainLayout->addLayout(centerLayout);
0187 
0188         mainLayout->addWidget(new KSeparator(Qt::Horizontal, q));
0189 
0190         ui.buttonBox = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel, q);
0191         QPushButton *saveButton = ui.buttonBox->button(QDialogButtonBox::Save);
0192         KGuiItem::assign(saveButton, KStandardGuiItem::save());
0193         KGuiItem::assign(ui.buttonBox->button(QDialogButtonBox::Cancel), KStandardGuiItem::cancel());
0194         saveButton->setEnabled(false);
0195         mainLayout->addWidget(ui.buttonBox);
0196 
0197         // prevent accidental closing of dialog when pressing Enter while a search field has focus
0198         Kleo::unsetAutoDefaultButtons(q);
0199 
0200         connect(ui.groupNameEdit, &QLineEdit::textChanged, q, [saveButton](const QString &text) {
0201             saveButton->setEnabled(!text.trimmed().isEmpty());
0202         });
0203         connect(ui.availableKeysFilter, &QLineEdit::textChanged, ui.availableKeysList, &KeyTreeView::setStringFilter);
0204         connect(ui.availableKeysList->view()->selectionModel(),
0205                 &QItemSelectionModel::selectionChanged,
0206                 q,
0207                 [addButton](const QItemSelection &selected, const QItemSelection &) {
0208                     addButton->setEnabled(!selected.isEmpty());
0209                 });
0210         connect(ui.availableKeysList->view(), &QAbstractItemView::doubleClicked, q, [this](const QModelIndex &index) {
0211             showKeyDetails(index);
0212         });
0213         connect(ui.groupKeysFilter, &QLineEdit::textChanged, ui.groupKeysList, &KeyTreeView::setStringFilter);
0214         connect(ui.groupKeysList->view()->selectionModel(),
0215                 &QItemSelectionModel::selectionChanged,
0216                 q,
0217                 [removeButton](const QItemSelection &selected, const QItemSelection &) {
0218                     removeButton->setEnabled(!selected.isEmpty());
0219                 });
0220         connect(ui.groupKeysList->view(), &QAbstractItemView::doubleClicked, q, [this](const QModelIndex &index) {
0221             showKeyDetails(index);
0222         });
0223         connect(addButton, &QPushButton::clicked, q, [this]() {
0224             addKeysToGroup();
0225         });
0226         connect(removeButton, &QPushButton::clicked, q, [this]() {
0227             removeKeysFromGroup();
0228         });
0229         connect(ui.buttonBox, &QDialogButtonBox::accepted, q, &EditGroupDialog::accept);
0230         connect(ui.buttonBox, &QDialogButtonBox::rejected, q, &EditGroupDialog::reject);
0231 
0232         connect(KeyCache::instance().get(), &KeyCache::keysMayHaveChanged, q, [this] {
0233             updateFromKeyCache();
0234         });
0235 
0236         // calculate default size with enough space for the key list
0237         const auto fm = q->fontMetrics();
0238         const QSize sizeHint = q->sizeHint();
0239         const QSize defaultSize = QSize(qMax(sizeHint.width(), 150 * fm.horizontalAdvance(QLatin1Char('x'))), sizeHint.height());
0240         restoreLayout(defaultSize);
0241     }
0242 
0243     ~Private()
0244     {
0245         saveLayout();
0246     }
0247 
0248 private:
0249     void saveLayout()
0250     {
0251         KConfigGroup configGroup(KSharedConfig::openConfig(), QStringLiteral("EditGroupDialog"));
0252         configGroup.writeEntry("Size", q->size());
0253 
0254         configGroup.sync();
0255     }
0256 
0257     void restoreLayout(const QSize &defaultSize)
0258     {
0259         const KConfigGroup configGroup(KSharedConfig::openConfig(), QStringLiteral("EditGroupDialog"));
0260 
0261         const KConfigGroup availableKeysConfig = configGroup.group(QStringLiteral("AvailableKeysView"));
0262         ui.availableKeysList->restoreLayout(availableKeysConfig);
0263 
0264         const KConfigGroup groupKeysConfig = configGroup.group(QStringLiteral("GroupKeysView"));
0265         ui.groupKeysList->restoreLayout(groupKeysConfig);
0266 
0267         const QSize size = configGroup.readEntry("Size", defaultSize);
0268         if (size.isValid()) {
0269             q->resize(size);
0270         }
0271     }
0272 
0273     void showKeyDetails(const QModelIndex &index)
0274     {
0275         if (!index.isValid()) {
0276             return;
0277         }
0278         const auto key = index.model()->data(index, KeyList::KeyRole).value<GpgME::Key>();
0279         if (!key.isNull()) {
0280             auto cmd = new DetailsCommand(key);
0281             cmd->setParentWidget(q);
0282             cmd->start();
0283         }
0284     }
0285 
0286     void addKeysToGroup();
0287     void removeKeysFromGroup();
0288     void updateFromKeyCache();
0289 };
0290 
0291 void EditGroupDialog::Private::addKeysToGroup()
0292 {
0293     const std::vector<Key> selectedGroupKeys = ui.groupKeysList->selectedKeys();
0294 
0295     const std::vector<Key> selectedKeys = ui.availableKeysList->selectedKeys();
0296     groupKeysModel->addKeys(selectedKeys);
0297     for (const Key &key : selectedKeys) {
0298         availableKeysModel->removeKey(key);
0299     }
0300 
0301     ui.groupKeysList->selectKeys(selectedGroupKeys);
0302 }
0303 
0304 void EditGroupDialog::Private::removeKeysFromGroup()
0305 {
0306     const auto selectedOtherKeys = ui.availableKeysList->selectedKeys();
0307 
0308     const std::vector<Key> selectedKeys = ui.groupKeysList->selectedKeys();
0309     for (const Key &key : selectedKeys) {
0310         groupKeysModel->removeKey(key);
0311     }
0312     availableKeysModel->addKeys(selectedKeys);
0313 
0314     ui.availableKeysList->selectKeys(selectedOtherKeys);
0315 }
0316 
0317 void EditGroupDialog::Private::updateFromKeyCache()
0318 {
0319     const auto selectedGroupKeys = ui.groupKeysList->selectedKeys();
0320     const auto selectedOtherKeys = ui.availableKeysList->selectedKeys();
0321 
0322     const auto oldGroupKeys = q->groupKeys();
0323     const auto wasGroupKey = [oldGroupKeys](const Key &key) {
0324         return Kleo::any_of(oldGroupKeys, [key](const auto &k) {
0325             return _detail::ByFingerprint<std::equal_to>()(k, key);
0326         });
0327     };
0328     const auto allKeys = KeyCache::instance()->keys();
0329     std::vector<Key> groupKeys;
0330     groupKeys.reserve(allKeys.size());
0331     std::vector<Key> otherKeys;
0332     otherKeys.reserve(otherKeys.size());
0333     std::partition_copy(allKeys.begin(), allKeys.end(), std::back_inserter(groupKeys), std::back_inserter(otherKeys), wasGroupKey);
0334     groupKeysModel->setKeys(groupKeys);
0335     availableKeysModel->setKeys(otherKeys);
0336 
0337     ui.groupKeysList->selectKeys(selectedGroupKeys);
0338     ui.availableKeysList->selectKeys(selectedOtherKeys);
0339 }
0340 
0341 EditGroupDialog::EditGroupDialog(QWidget *parent)
0342     : QDialog(parent)
0343     , d(new Private(this))
0344 {
0345     setWindowTitle(i18nc("@title:window", "Edit Group"));
0346 }
0347 
0348 EditGroupDialog::~EditGroupDialog() = default;
0349 
0350 void EditGroupDialog::setInitialFocus(FocusWidget widget)
0351 {
0352     switch (widget) {
0353     case GroupName:
0354         d->ui.groupNameEdit->setFocus();
0355         break;
0356     case KeysFilter:
0357         d->ui.availableKeysFilter->setFocus();
0358         break;
0359     default:
0360         qCDebug(KLEOPATRA_LOG) << "EditGroupDialog::setInitialFocus - invalid focus widget:" << widget;
0361     }
0362 }
0363 
0364 void EditGroupDialog::setGroupName(const QString &name)
0365 {
0366     d->ui.groupNameEdit->setText(name);
0367 }
0368 
0369 QString EditGroupDialog::groupName() const
0370 {
0371     return d->ui.groupNameEdit->text().trimmed();
0372 }
0373 
0374 void EditGroupDialog::setGroupKeys(const std::vector<Key> &groupKeys)
0375 {
0376     d->groupKeysModel->setKeys(groupKeys);
0377 
0378     // update the keys in the "available keys" list
0379     const auto isGroupKey = [groupKeys](const Key &key) {
0380         return Kleo::any_of(groupKeys, [key](const auto &k) {
0381             return _detail::ByFingerprint<std::equal_to>()(k, key);
0382         });
0383     };
0384     auto otherKeys = KeyCache::instance()->keys();
0385     Kleo::erase_if(otherKeys, isGroupKey);
0386     d->availableKeysModel->setKeys(otherKeys);
0387 }
0388 
0389 std::vector<Key> EditGroupDialog::groupKeys() const
0390 {
0391     std::vector<Key> keys;
0392     keys.reserve(d->groupKeysModel->rowCount());
0393     for (int row = 0; row < d->groupKeysModel->rowCount(); ++row) {
0394         const QModelIndex index = d->groupKeysModel->index(row, 0);
0395         keys.push_back(d->groupKeysModel->key(index));
0396     }
0397     return keys;
0398 }
0399 
0400 void EditGroupDialog::showEvent(QShowEvent *event)
0401 {
0402     QDialog::showEvent(event);
0403 
0404     // prevent accidental closing of dialog when pressing Enter while a search field has focus
0405     Kleo::unsetDefaultButtons(d->ui.buttonBox);
0406 }
0407 
0408 #include "moc_editgroupdialog.cpp"