File indexing completed on 2024-06-23 05:13:47
0001 /* 0002 conf/groupsconfigwidget.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 "groupsconfigwidget.h" 0012 0013 #include <commands/certifygroupcommand.h> 0014 #include <commands/exportgroupscommand.h> 0015 #include <dialogs/editgroupdialog.h> 0016 0017 #include <Libkleo/Algorithm> 0018 #include <Libkleo/Debug> 0019 #include <Libkleo/Formatting> 0020 #include <Libkleo/KeyCache> 0021 #include <Libkleo/KeyGroup> 0022 #include <Libkleo/KeyHelpers> 0023 #include <Libkleo/KeyListModel> 0024 #include <Libkleo/KeyListSortFilterProxyModel> 0025 0026 #include <KLocalizedString> 0027 #include <KMessageBox> 0028 #include <KRandom> 0029 0030 #include <QItemSelectionModel> 0031 #include <QLabel> 0032 #include <QLineEdit> 0033 #include <QListView> 0034 #include <QPushButton> 0035 #include <QVBoxLayout> 0036 0037 #include "kleopatra_debug.h" 0038 0039 using namespace Kleo; 0040 using namespace Kleo::Dialogs; 0041 0042 Q_DECLARE_METATYPE(KeyGroup) 0043 0044 namespace 0045 { 0046 0047 class ListView : public QListView 0048 { 0049 Q_OBJECT 0050 public: 0051 using QListView::QListView; 0052 0053 protected: 0054 void currentChanged(const QModelIndex ¤t, const QModelIndex &previous) override 0055 { 0056 // workaround bug in QListView::currentChanged which sends an accessible focus event 0057 // even if the list view doesn't have focus 0058 if (hasFocus()) { 0059 QListView::currentChanged(current, previous); 0060 } else { 0061 // skip the reimplementation of currentChanged in QListView 0062 QAbstractItemView::currentChanged(current, previous); 0063 } 0064 } 0065 0066 void focusInEvent(QFocusEvent *event) override 0067 { 0068 QListView::focusInEvent(event); 0069 // select current item if it isn't selected 0070 if (currentIndex().isValid() && !selectionModel()->isSelected(currentIndex())) { 0071 selectionModel()->select(currentIndex(), QItemSelectionModel::ClearAndSelect); 0072 } 0073 } 0074 }; 0075 0076 class ProxyModel : public AbstractKeyListSortFilterProxyModel 0077 { 0078 Q_OBJECT 0079 public: 0080 ProxyModel(QObject *parent = nullptr) 0081 : AbstractKeyListSortFilterProxyModel(parent) 0082 { 0083 } 0084 0085 ~ProxyModel() override = default; 0086 0087 ProxyModel *clone() const override 0088 { 0089 // compiler-generated copy ctor is fine! 0090 return new ProxyModel(*this); 0091 } 0092 0093 int columnCount(const QModelIndex &parent = {}) const override 0094 { 0095 Q_UNUSED(parent) 0096 // pretend that there is only one column to workaround a bug in 0097 // QAccessibleTable which provides the accessibility interface for the 0098 // list view 0099 return 1; 0100 } 0101 0102 QVariant data(const QModelIndex &idx, int role) const override 0103 { 0104 if (!idx.isValid()) { 0105 return {}; 0106 } 0107 0108 return AbstractKeyListSortFilterProxyModel::data(index(idx.row(), KeyList::Summary), role); 0109 } 0110 }; 0111 0112 struct Selection { 0113 KeyGroup current; 0114 std::vector<KeyGroup> selected; 0115 }; 0116 0117 } 0118 0119 class GroupsConfigWidget::Private 0120 { 0121 friend class ::Kleo::GroupsConfigWidget; 0122 GroupsConfigWidget *const q; 0123 0124 struct { 0125 QLineEdit *groupsFilter = nullptr; 0126 QListView *groupsList = nullptr; 0127 QPushButton *newButton = nullptr; 0128 QPushButton *editButton = nullptr; 0129 QPushButton *deleteButton = nullptr; 0130 QPushButton *certifyButton = nullptr; 0131 QPushButton *exportButton = nullptr; 0132 } ui; 0133 AbstractKeyListModel *groupsModel = nullptr; 0134 ProxyModel *groupsFilterModel = nullptr; 0135 0136 public: 0137 Private(GroupsConfigWidget *qq) 0138 : q(qq) 0139 { 0140 auto mainLayout = new QVBoxLayout(q); 0141 0142 auto groupsLayout = new QGridLayout; 0143 groupsLayout->setContentsMargins(q->style()->pixelMetric(QStyle::PM_LayoutLeftMargin), 0144 q->style()->pixelMetric(QStyle::PM_LayoutTopMargin), 0145 q->style()->pixelMetric(QStyle::PM_LayoutRightMargin), 0146 q->style()->pixelMetric(QStyle::PM_LayoutBottomMargin)); 0147 groupsLayout->setColumnStretch(0, 1); 0148 groupsLayout->setRowStretch(1, 1); 0149 int row = -1; 0150 0151 row++; 0152 { 0153 auto hbox = new QHBoxLayout; 0154 auto label = new QLabel{i18nc("@label", "Search:")}; 0155 label->setAccessibleName(i18nc("@label", "Search groups")); 0156 label->setToolTip(i18nc("@info:tooltip", "Search the list for groups matching the search term.")); 0157 hbox->addWidget(label); 0158 0159 ui.groupsFilter = new QLineEdit(q); 0160 ui.groupsFilter->setClearButtonEnabled(true); 0161 ui.groupsFilter->setAccessibleName(i18nc("@label", "Search groups")); 0162 ui.groupsFilter->setToolTip(i18nc("@info:tooltip", "Search the list for groups matching the search term.")); 0163 ui.groupsFilter->setPlaceholderText(i18nc("@info::placeholder", "Enter search term")); 0164 ui.groupsFilter->setCursorPosition(0); // prevent emission of accessible text cursor event before accessible focus event 0165 label->setBuddy(ui.groupsFilter); 0166 hbox->addWidget(ui.groupsFilter, 1); 0167 0168 groupsLayout->addLayout(hbox, row, 0); 0169 } 0170 0171 row++; 0172 groupsModel = AbstractKeyListModel::createFlatKeyListModel(q); 0173 groupsFilterModel = new ProxyModel(q); 0174 groupsFilterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); 0175 groupsFilterModel->setFilterKeyColumn(KeyList::Summary); 0176 groupsFilterModel->setSortCaseSensitivity(Qt::CaseInsensitive); 0177 groupsFilterModel->setSourceModel(groupsModel); 0178 groupsFilterModel->sort(KeyList::Summary, Qt::AscendingOrder); 0179 ui.groupsList = new ListView(q); 0180 ui.groupsList->setAccessibleName(i18nc("groups of keys", "groups")); 0181 ui.groupsList->setModel(groupsFilterModel); 0182 ui.groupsList->setSelectionBehavior(QAbstractItemView::SelectRows); 0183 ui.groupsList->setSelectionMode(QAbstractItemView::ExtendedSelection); 0184 0185 groupsLayout->addWidget(ui.groupsList, row, 0); 0186 0187 auto groupsButtonLayout = new QVBoxLayout; 0188 0189 ui.newButton = new QPushButton(i18nc("@action:button", "New"), q); 0190 groupsButtonLayout->addWidget(ui.newButton); 0191 0192 ui.editButton = new QPushButton(i18nc("@action:button", "Edit"), q); 0193 ui.editButton->setEnabled(false); 0194 groupsButtonLayout->addWidget(ui.editButton); 0195 0196 ui.deleteButton = new QPushButton(i18nc("@action:button", "Delete"), q); 0197 ui.deleteButton->setEnabled(false); 0198 groupsButtonLayout->addWidget(ui.deleteButton); 0199 0200 ui.certifyButton = new QPushButton{i18nc("@action:button", "Certify"), q}; 0201 ui.certifyButton->setToolTip(i18nc("@info:tooltip", "Start the certification process for all certificates in the group.")); 0202 ui.certifyButton->setEnabled(false); 0203 groupsButtonLayout->addWidget(ui.certifyButton); 0204 0205 ui.exportButton = new QPushButton{i18nc("@action:button", "Export"), q}; 0206 ui.exportButton->setEnabled(false); 0207 groupsButtonLayout->addWidget(ui.exportButton); 0208 0209 groupsButtonLayout->addStretch(1); 0210 0211 groupsLayout->addLayout(groupsButtonLayout, row, 1); 0212 0213 mainLayout->addLayout(groupsLayout, /*stretch=*/1); 0214 0215 connect(ui.groupsFilter, &QLineEdit::textChanged, q, [this](const auto &s) { 0216 groupsFilterModel->setFilterRegularExpression(QRegularExpression::escape(s)); 0217 }); 0218 connect(ui.groupsList->selectionModel(), &QItemSelectionModel::selectionChanged, q, [this]() { 0219 selectionChanged(); 0220 }); 0221 connect(ui.groupsList, &QListView::doubleClicked, q, [this](const QModelIndex &index) { 0222 editGroup(index); 0223 }); 0224 connect(ui.newButton, &QPushButton::clicked, q, [this]() { 0225 addGroup(); 0226 }); 0227 connect(ui.editButton, &QPushButton::clicked, q, [this]() { 0228 editGroup(); 0229 }); 0230 connect(ui.deleteButton, &QPushButton::clicked, q, [this]() { 0231 deleteGroup(); 0232 }); 0233 connect(ui.certifyButton, &QPushButton::clicked, q, [this]() { 0234 certifyGroup(); 0235 }); 0236 connect(ui.exportButton, &QPushButton::clicked, q, [this]() { 0237 exportGroup(); 0238 }); 0239 } 0240 0241 private: 0242 auto getGroupIndex(const KeyGroup &group) 0243 { 0244 QModelIndex index; 0245 if (const KeyListModelInterface *const klmi = dynamic_cast<KeyListModelInterface *>(ui.groupsList->model())) { 0246 index = klmi->index(group); 0247 } 0248 return index; 0249 } 0250 0251 auto selectedRows() 0252 { 0253 return ui.groupsList->selectionModel()->selectedRows(); 0254 } 0255 0256 auto getGroup(const QModelIndex &index) 0257 { 0258 return index.isValid() ? ui.groupsList->model()->data(index, KeyList::GroupRole).value<KeyGroup>() : KeyGroup{}; 0259 } 0260 0261 auto getGroups(const QModelIndexList &indexes) 0262 { 0263 std::vector<KeyGroup> groups; 0264 std::transform(std::begin(indexes), std::end(indexes), std::back_inserter(groups), [this](const auto &index) { 0265 return getGroup(index); 0266 }); 0267 return groups; 0268 } 0269 0270 Selection saveSelection() 0271 { 0272 return {getGroup(ui.groupsList->selectionModel()->currentIndex()), getGroups(selectedRows())}; 0273 } 0274 0275 void restoreSelection(const Selection &selection) 0276 { 0277 auto selectionModel = ui.groupsList->selectionModel(); 0278 selectionModel->clearSelection(); 0279 for (const auto &group : selection.selected) { 0280 selectionModel->select(getGroupIndex(group), QItemSelectionModel::Select | QItemSelectionModel::Rows); 0281 } 0282 auto currentIndex = getGroupIndex(selection.current); 0283 if (currentIndex.isValid()) { 0284 // keep current item if old current group is gone 0285 selectionModel->setCurrentIndex(currentIndex, QItemSelectionModel::NoUpdate); 0286 } 0287 } 0288 0289 void selectionChanged() 0290 { 0291 const auto selectedGroups = getGroups(selectedRows()); 0292 const bool allSelectedGroupsAreEditable = std::all_of(std::begin(selectedGroups), std::end(selectedGroups), [](const auto &g) { 0293 return !g.isNull() && !g.isImmutable(); 0294 }); 0295 ui.editButton->setEnabled(selectedGroups.size() == 1 && allSelectedGroupsAreEditable); 0296 ui.deleteButton->setEnabled(!selectedGroups.empty() && allSelectedGroupsAreEditable); 0297 ui.certifyButton->setEnabled(selectedGroups.size() == 1 // 0298 && !selectedGroups.front().keys().empty() // 0299 && allKeysHaveProtocol(selectedGroups.front().keys(), GpgME::OpenPGP)); 0300 ui.exportButton->setEnabled(selectedGroups.size() == 1); 0301 } 0302 0303 KeyGroup showEditGroupDialog(KeyGroup group, const QString &windowTitle, EditGroupDialog::FocusWidget focusWidget) 0304 { 0305 auto dialog = std::make_unique<EditGroupDialog>(q); 0306 dialog->setWindowTitle(windowTitle); 0307 dialog->setGroupName(group.name()); 0308 const KeyGroup::Keys &keys = group.keys(); 0309 dialog->setGroupKeys(std::vector<GpgME::Key>(keys.cbegin(), keys.cend())); 0310 dialog->setInitialFocus(focusWidget); 0311 0312 const int result = dialog->exec(); 0313 if (result == QDialog::Rejected) { 0314 return KeyGroup(); 0315 } 0316 0317 group.setName(dialog->groupName()); 0318 group.setKeys(dialog->groupKeys()); 0319 0320 return group; 0321 } 0322 0323 void addGroup() 0324 { 0325 const KeyGroup::Id newId = KRandom::randomString(8); 0326 KeyGroup group = KeyGroup(newId, i18nc("default name for new group of keys", "New Group"), {}, KeyGroup::ApplicationConfig); 0327 group.setIsImmutable(false); 0328 0329 const KeyGroup newGroup = showEditGroupDialog(group, i18nc("@title:window a group of keys", "New Group"), EditGroupDialog::GroupName); 0330 if (newGroup.isNull()) { 0331 return; 0332 } 0333 0334 const QModelIndex newIndex = groupsModel->addGroup(newGroup); 0335 if (!newIndex.isValid()) { 0336 qCDebug(KLEOPATRA_LOG) << "Adding group to model failed"; 0337 return; 0338 } 0339 0340 Q_EMIT q->changed(); 0341 } 0342 0343 void editGroup(const QModelIndex &index = {}) 0344 { 0345 QModelIndex groupIndex; 0346 if (index.isValid()) { 0347 groupIndex = index; 0348 } else { 0349 const auto selection = selectedRows(); 0350 if (selection.size() != 1) { 0351 qCDebug(KLEOPATRA_LOG) << (selection.empty() ? "selection is empty" : "more than one group is selected"); 0352 return; 0353 } 0354 groupIndex = selection.front(); 0355 } 0356 const KeyGroup group = getGroup(groupIndex); 0357 if (group.isNull()) { 0358 qCDebug(KLEOPATRA_LOG) << "selected group is null"; 0359 return; 0360 } 0361 if (group.isImmutable()) { 0362 qCDebug(KLEOPATRA_LOG) << "selected group is immutable"; 0363 return; 0364 } 0365 0366 const KeyGroup updatedGroup = showEditGroupDialog(group, i18nc("@title:window a group of keys", "Edit Group"), EditGroupDialog::KeysFilter); 0367 if (updatedGroup.isNull()) { 0368 return; 0369 } 0370 0371 // look up index of updated group; the groupIndex used above may have become invalid 0372 const auto updatedGroupIndex = getGroupIndex(updatedGroup); 0373 if (updatedGroupIndex.isValid()) { 0374 const bool success = ui.groupsList->model()->setData(updatedGroupIndex, QVariant::fromValue(updatedGroup)); 0375 if (!success) { 0376 qCDebug(KLEOPATRA_LOG) << "Updating group in model failed"; 0377 return; 0378 } 0379 } else { 0380 qCDebug(KLEOPATRA_LOG) << __func__ << "Failed to find index of group" << updatedGroup << "; maybe it was removed behind our back; re-add it"; 0381 const QModelIndex newIndex = groupsModel->addGroup(updatedGroup); 0382 if (!newIndex.isValid()) { 0383 qCDebug(KLEOPATRA_LOG) << "Re-adding group to model failed"; 0384 return; 0385 } 0386 } 0387 0388 Q_EMIT q->changed(); 0389 } 0390 0391 bool confirmDeletion(const std::vector<KeyGroup> &groups) 0392 { 0393 QString message; 0394 QStringList groupSummaries; 0395 if (groups.size() == 1) { 0396 message = xi18nc("@info", 0397 "<para>Do you really want to delete this group?</para>" 0398 "<para><emphasis>%1</emphasis></para>" 0399 "<para>Once deleted, it cannot be restored.</para>") 0400 .arg(Formatting::summaryLine(groups.front())); 0401 } else { 0402 message = xi18ncp("@info", 0403 "<para>Do you really want to delete this %1 group?</para>" 0404 "<para>Once deleted, it cannot be restored.</para>", 0405 "<para>Do you really want to delete these %1 groups?</para>" 0406 "<para>Once deleted, they cannot be restored.</para>", 0407 groups.size()); 0408 Kleo::transform(groups, std::back_inserter(groupSummaries), [](const auto &g) { 0409 return Formatting::summaryLine(g); 0410 }); 0411 } 0412 const auto answer = KMessageBox::questionTwoActionsList(q, 0413 message, 0414 groupSummaries, 0415 i18ncp("@title:window", "Delete Group", "Delete Groups", groups.size()), 0416 KStandardGuiItem::del(), 0417 KStandardGuiItem::cancel(), 0418 {}, 0419 KMessageBox::Notify | KMessageBox::Dangerous); 0420 return answer == KMessageBox::PrimaryAction; 0421 } 0422 0423 void deleteGroup() 0424 { 0425 const auto selectedGroups = getGroups(selectedRows()); 0426 if (selectedGroups.empty()) { 0427 qCDebug(KLEOPATRA_LOG) << "selection is empty"; 0428 return; 0429 } 0430 0431 if (!confirmDeletion(selectedGroups)) { 0432 return; 0433 } 0434 0435 for (const auto &group : selectedGroups) { 0436 const bool success = groupsModel->removeGroup(group); 0437 if (!success) { 0438 qCDebug(KLEOPATRA_LOG) << "Removing group from model failed:" << group; 0439 } 0440 } 0441 0442 Q_EMIT q->changed(); 0443 } 0444 0445 void certifyGroup() 0446 { 0447 const auto selectedGroups = getGroups(selectedRows()); 0448 if (selectedGroups.size() != 1) { 0449 qCDebug(KLEOPATRA_LOG) << __func__ << (selectedGroups.empty() ? "selection is empty" : "more than one group is selected"); 0450 return; 0451 } 0452 0453 auto cmd = new CertifyGroupCommand{selectedGroups.front()}; 0454 cmd->setParentWidget(q->window()); 0455 cmd->start(); 0456 } 0457 0458 void exportGroup() 0459 { 0460 const auto selectedGroups = getGroups(selectedRows()); 0461 if (selectedGroups.empty()) { 0462 qCDebug(KLEOPATRA_LOG) << "selection is empty"; 0463 return; 0464 } 0465 0466 auto cmd = new ExportGroupsCommand(selectedGroups); 0467 cmd->start(); 0468 } 0469 }; 0470 0471 GroupsConfigWidget::GroupsConfigWidget(QWidget *parent) 0472 : QWidget(parent) 0473 , d(new Private(this)) 0474 { 0475 } 0476 0477 GroupsConfigWidget::~GroupsConfigWidget() = default; 0478 0479 void GroupsConfigWidget::setGroups(const std::vector<KeyGroup> &groups) 0480 { 0481 const auto selection = d->saveSelection(); 0482 d->groupsModel->setGroups(groups); 0483 d->restoreSelection(selection); 0484 } 0485 0486 std::vector<KeyGroup> GroupsConfigWidget::groups() const 0487 { 0488 std::vector<KeyGroup> result; 0489 result.reserve(d->groupsModel->rowCount()); 0490 for (int row = 0; row < d->groupsModel->rowCount(); ++row) { 0491 const QModelIndex index = d->groupsModel->index(row, 0); 0492 result.push_back(d->groupsModel->group(index)); 0493 } 0494 return result; 0495 } 0496 0497 #include "groupsconfigwidget.moc" 0498 0499 #include "moc_groupsconfigwidget.cpp"