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"