diff --git a/src/conf/groupsconfigwidget.cpp b/src/conf/groupsconfigwidget.cpp index cdb4fd94c..ffb460abf 100644 --- a/src/conf/groupsconfigwidget.cpp +++ b/src/conf/groupsconfigwidget.cpp @@ -1,356 +1,356 @@ /* conf/groupsconfigwidget.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include "groupsconfigwidget.h" #include "commands/exportgroupscommand.h" #include "dialogs/editgroupdialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::Dialogs; Q_DECLARE_METATYPE(KeyGroup) namespace { class ProxyModel : public AbstractKeyListSortFilterProxyModel { Q_OBJECT public: ProxyModel(QObject *parent = nullptr) : AbstractKeyListSortFilterProxyModel(parent) { } ~ProxyModel() override = default; ProxyModel *clone() const override { // compiler-generated copy ctor is fine! return new ProxyModel(*this); } }; struct Selection { KeyGroup current; std::vector selected; }; } class GroupsConfigWidget::Private { friend class ::Kleo::GroupsConfigWidget; GroupsConfigWidget *const q; struct { QLineEdit *groupsFilter = nullptr; QListView *groupsList = nullptr; QPushButton *newButton = nullptr; QPushButton *editButton = nullptr; QPushButton *deleteButton = nullptr; QPushButton *exportButton = nullptr; } ui; AbstractKeyListModel *groupsModel = nullptr; ProxyModel *groupsFilterModel = nullptr; public: Private(GroupsConfigWidget *qq) : q(qq) { auto mainLayout = new QVBoxLayout(q); - auto groupsLayout = new QGridLayout(); + auto groupsLayout = new QGridLayout; groupsLayout->setColumnStretch(0, 1); groupsLayout->setRowStretch(1, 1); - ui.groupsFilter = new QLineEdit(); + ui.groupsFilter = new QLineEdit(q); ui.groupsFilter->setClearButtonEnabled(true); ui.groupsFilter->setPlaceholderText(i18nc("@info::placeholder", "Search...")); groupsLayout->addWidget(ui.groupsFilter, 0, 0); groupsModel = AbstractKeyListModel::createFlatKeyListModel(q); groupsFilterModel = new ProxyModel(q); groupsFilterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); groupsFilterModel->setFilterKeyColumn(KeyList::Summary); groupsFilterModel->setSortCaseSensitivity(Qt::CaseInsensitive); groupsFilterModel->setSourceModel(groupsModel); groupsFilterModel->sort(KeyList::Summary, Qt::AscendingOrder); - ui.groupsList = new QListView(); + ui.groupsList = new QListView(q); ui.groupsList->setModel(groupsFilterModel); ui.groupsList->setModelColumn(KeyList::Summary); ui.groupsList->setSelectionBehavior(QAbstractItemView::SelectRows); ui.groupsList->setSelectionMode(QAbstractItemView::ExtendedSelection); groupsLayout->addWidget(ui.groupsList, 1, 0); - auto groupsButtonLayout = new QVBoxLayout(); + auto groupsButtonLayout = new QVBoxLayout; - ui.newButton = new QPushButton(i18nc("@action::button", "New")); + ui.newButton = new QPushButton(i18nc("@action::button", "New"), q); groupsButtonLayout->addWidget(ui.newButton); - ui.editButton = new QPushButton(i18nc("@action::button", "Edit")); + ui.editButton = new QPushButton(i18nc("@action::button", "Edit"), q); ui.editButton->setEnabled(false); groupsButtonLayout->addWidget(ui.editButton); - ui.deleteButton = new QPushButton(i18nc("@action::button", "Delete")); + ui.deleteButton = new QPushButton(i18nc("@action::button", "Delete"), q); ui.deleteButton->setEnabled(false); groupsButtonLayout->addWidget(ui.deleteButton); - ui.exportButton = new QPushButton{i18nc("@action::button", "Export")}; + ui.exportButton = new QPushButton{i18nc("@action::button", "Export"), q}; ui.exportButton->setEnabled(false); groupsButtonLayout->addWidget(ui.exportButton); groupsButtonLayout->addStretch(1); groupsLayout->addLayout(groupsButtonLayout, 1, 1); mainLayout->addLayout(groupsLayout, /*stretch=*/ 1); connect(ui.groupsFilter, &QLineEdit::textChanged, q, [this](const auto &s) { groupsFilterModel->setFilterRegularExpression(QRegularExpression::escape(s)); }); connect(ui.groupsList->selectionModel(), &QItemSelectionModel::selectionChanged, q, [this] () { selectionChanged(); }); connect(ui.groupsList, &QListView::doubleClicked, q, [this] (const QModelIndex &index) { editGroup(index); }); connect(ui.newButton, &QPushButton::clicked, q, [this] () { addGroup(); }); connect(ui.editButton, &QPushButton::clicked, q, [this] () { editGroup(); }); connect(ui.deleteButton, &QPushButton::clicked, q, [this] () { deleteGroup(); }); connect(ui.exportButton, &QPushButton::clicked, q, [this] () { exportGroup(); }); } ~Private() { } private: auto getGroupIndex(const KeyGroup &group) { QModelIndex index; if (const KeyListModelInterface *const klmi = dynamic_cast(ui.groupsList->model())) { index = klmi->index(group); } return index; } auto selectedRows() { return ui.groupsList->selectionModel()->selectedRows(); } auto getGroup(const QModelIndex &index) { return index.isValid() ? ui.groupsList->model()->data(index, KeyList::GroupRole).value() : KeyGroup{}; } auto getGroups(const QModelIndexList &indexes) { std::vector groups; std::transform(std::begin(indexes), std::end(indexes), std::back_inserter(groups), [this](const auto &index) { return getGroup(index); }); return groups; } Selection saveSelection() { return {getGroup(ui.groupsList->selectionModel()->currentIndex()), getGroups(selectedRows())}; } void restoreSelection(const Selection &selection) { auto selectionModel = ui.groupsList->selectionModel(); selectionModel->clearSelection(); for (const auto &group : selection.selected) { selectionModel->select(getGroupIndex(group), QItemSelectionModel::Select | QItemSelectionModel::Rows); } auto currentIndex = getGroupIndex(selection.current); if (currentIndex.isValid()) { // keep current item if old current group is gone selectionModel->setCurrentIndex(currentIndex, QItemSelectionModel::NoUpdate); } } void selectionChanged() { const auto selectedGroups = getGroups(selectedRows()); const bool allSelectedGroupsAreEditable = std::all_of(std::begin(selectedGroups), std::end(selectedGroups), [](const auto &g) { return !g.isNull() && !g.isImmutable(); }); ui.editButton->setEnabled(selectedGroups.size() == 1 && allSelectedGroupsAreEditable); ui.deleteButton->setEnabled(!selectedGroups.empty() && allSelectedGroupsAreEditable); ui.exportButton->setEnabled(selectedGroups.size() == 1); } KeyGroup showEditGroupDialog(KeyGroup group, const QString &windowTitle, EditGroupDialog::FocusWidget focusWidget) { auto dialog = std::make_unique(q); dialog->setWindowTitle(windowTitle); dialog->setGroupName(group.name()); const KeyGroup::Keys &keys = group.keys(); dialog->setGroupKeys(std::vector(keys.cbegin(), keys.cend())); dialog->setInitialFocus(focusWidget); const int result = dialog->exec(); if (result == QDialog::Rejected) { return KeyGroup(); } group.setName(dialog->groupName()); group.setKeys(dialog->groupKeys()); return group; } void addGroup() { const KeyGroup::Id newId = KRandom::randomString(8); KeyGroup group = KeyGroup(newId, i18nc("default name for new group of keys", "New Group"), {}, KeyGroup::ApplicationConfig); group.setIsImmutable(false); const KeyGroup newGroup = showEditGroupDialog( group, i18nc("@title:window a group of keys", "New Group"), EditGroupDialog::GroupName); if (newGroup.isNull()) { return; } const QModelIndex newIndex = groupsModel->addGroup(newGroup); if (!newIndex.isValid()) { qCDebug(KLEOPATRA_LOG) << "Adding group to model failed"; return; } Q_EMIT q->changed(); } void editGroup(const QModelIndex &index = {}) { QModelIndex groupIndex; if (index.isValid()) { groupIndex = index; } else { const auto selection = selectedRows(); if (selection.size() != 1) { qCDebug(KLEOPATRA_LOG) << (selection.empty() ? "selection is empty" : "more than one group is selected"); return; } groupIndex = selection.front(); } const KeyGroup group = getGroup(groupIndex); if (group.isNull()) { qCDebug(KLEOPATRA_LOG) << "selected group is null"; return; } if (group.isImmutable()) { qCDebug(KLEOPATRA_LOG) << "selected group is immutable"; return; } const KeyGroup updatedGroup = showEditGroupDialog( group, i18nc("@title:window a group of keys", "Edit Group"), EditGroupDialog::KeysFilter); if (updatedGroup.isNull()) { return; } // look up index of updated group; the groupIndex used above may have become invalid const auto updatedGroupIndex = getGroupIndex(updatedGroup); if (!updatedGroupIndex.isValid()) { qCDebug(KLEOPATRA_LOG) << __func__ << "Failed to find index of group" << updatedGroup; return; } const bool success = ui.groupsList->model()->setData(updatedGroupIndex, QVariant::fromValue(updatedGroup)); if (!success) { qCDebug(KLEOPATRA_LOG) << "Updating group in model failed"; return; } Q_EMIT q->changed(); } void deleteGroup() { const auto selectedGroups = getGroups(selectedRows()); if (selectedGroups.empty()) { qCDebug(KLEOPATRA_LOG) << "selection is empty"; return; } for (const auto &group : selectedGroups) { const bool success = groupsModel->removeGroup(group); if (!success) { qCDebug(KLEOPATRA_LOG) << "Removing group from model failed:" << group; } } Q_EMIT q->changed(); } void exportGroup() { const auto selectedGroups = getGroups(selectedRows()); if (selectedGroups.empty()) { qCDebug(KLEOPATRA_LOG) << "selection is empty"; return; } // execute export group command auto cmd = new ExportGroupsCommand(selectedGroups); cmd->start(); } }; GroupsConfigWidget::GroupsConfigWidget(QWidget *parent) : QWidget(parent) , d(new Private(this)) { } GroupsConfigWidget::~GroupsConfigWidget() = default; void GroupsConfigWidget::setGroups(const std::vector &groups) { const auto selection = d->saveSelection(); d->groupsModel->setGroups(groups); d->restoreSelection(selection); } std::vector GroupsConfigWidget::groups() const { std::vector result; result.reserve(d->groupsModel->rowCount()); for (int row = 0; row < d->groupsModel->rowCount(); ++row) { const QModelIndex index = d->groupsModel->index(row, 0); result.push_back(d->groupsModel->group(index)); } return result; } #include "groupsconfigwidget.moc" diff --git a/src/dialogs/editgroupdialog.cpp b/src/dialogs/editgroupdialog.cpp index 6b3923e10..e3bce57c9 100644 --- a/src/dialogs/editgroupdialog.cpp +++ b/src/dialogs/editgroupdialog.cpp @@ -1,308 +1,308 @@ /* dialogs/editgroupdialog.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include "editgroupdialog.h" #include "commands/detailscommand.h" #include "view/keytreeview.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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(); filter->setIsOpenPGP(DefaultKeyFilter::Set); return filter; } } 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; } ui; AbstractKeyListModel *availableKeysModel = nullptr; AbstractKeyListModel *groupKeysModel = nullptr; public: Private(EditGroupDialog *qq) : q(qq) { auto mainLayout = new QVBoxLayout(q); auto groupNameLayout = new QHBoxLayout(); - groupNameLayout->addWidget(new QLabel(i18nc("Name of a group of keys", "Name:"))); - ui.groupNameEdit = new QLineEdit(); + groupNameLayout->addWidget(new QLabel(i18nc("Name of a group of keys", "Name:"), q)); + ui.groupNameEdit = new QLineEdit(q); groupNameLayout->addWidget(ui.groupNameEdit); mainLayout->addLayout(groupNameLayout); - mainLayout->addWidget(new KSeparator(Qt::Horizontal)); + mainLayout->addWidget(new KSeparator(Qt::Horizontal, q)); - auto centerLayout = new QVBoxLayout(); + auto centerLayout = new QVBoxLayout; - auto availableKeysLayout = new QVBoxLayout(); - availableKeysLayout->addWidget(new QLabel(i18n("Available keys:"))); + auto availableKeysLayout = new QVBoxLayout; + availableKeysLayout->addWidget(new QLabel(i18n("Available keys:"), q)); - ui.availableKeysFilter = new QLineEdit(); + ui.availableKeysFilter = new QLineEdit(q); ui.availableKeysFilter->setClearButtonEnabled(true); ui.availableKeysFilter->setPlaceholderText(i18nc("Placeholder text", "Search...")); availableKeysLayout->addWidget(ui.availableKeysFilter); availableKeysModel = AbstractKeyListModel::createFlatKeyListModel(q); availableKeysModel->useKeyCache(true, KeyList::AllKeys); ui.availableKeysList = new KeyTreeView(q); 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->addLayout(availableKeysLayout, /*stretch=*/ 1); - auto buttonsLayout = new QHBoxLayout(); + auto buttonsLayout = new QHBoxLayout; buttonsLayout->addStretch(1); - auto addButton = new QPushButton(); + auto addButton = new QPushButton(q); addButton->setIcon(QIcon::fromTheme(QStringLiteral("arrow-down"))); addButton->setAccessibleName(i18nc("@action:button", "Add Selected Keys")); addButton->setToolTip(i18n("Add the selected keys to the group")); addButton->setEnabled(false); buttonsLayout->addWidget(addButton); - auto removeButton = new QPushButton(); + auto removeButton = new QPushButton(q); removeButton->setIcon(QIcon::fromTheme(QStringLiteral("arrow-up"))); addButton->setAccessibleName(i18nc("@action:button", "Remove Selected Keys")); removeButton->setToolTip(i18n("Remove the selected keys from the group")); removeButton->setEnabled(false); buttonsLayout->addWidget(removeButton); buttonsLayout->addStretch(1); centerLayout->addLayout(buttonsLayout); - auto groupKeysLayout = new QVBoxLayout(); - groupKeysLayout->addWidget(new QLabel(i18n("Group keys:"))); + auto groupKeysLayout = new QVBoxLayout; + groupKeysLayout->addWidget(new QLabel(i18n("Group keys:"), q)); - ui.groupKeysFilter = new QLineEdit(); + ui.groupKeysFilter = new QLineEdit(q); ui.groupKeysFilter->setClearButtonEnabled(true); ui.groupKeysFilter->setPlaceholderText(i18nc("Placeholder text", "Search...")); groupKeysLayout->addWidget(ui.groupKeysFilter); groupKeysModel = AbstractKeyListModel::createFlatKeyListModel(q); ui.groupKeysList = new KeyTreeView(q); ui.groupKeysList->view()->setRootIsDecorated(false); ui.groupKeysList->setFlatModel(groupKeysModel); ui.groupKeysList->setHierarchicalView(false); groupKeysLayout->addWidget(ui.groupKeysList, /*stretch=*/ 1); centerLayout->addLayout(groupKeysLayout, /*stretch=*/ 1); mainLayout->addLayout(centerLayout); - mainLayout->addWidget(new KSeparator(Qt::Horizontal)); + mainLayout->addWidget(new KSeparator(Qt::Horizontal, q)); - ui.buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + ui.buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, q); QPushButton *okButton = ui.buttonBox->button(QDialogButtonBox::Ok); KGuiItem::assign(okButton, KStandardGuiItem::ok()); KGuiItem::assign(ui.buttonBox->button(QDialogButtonBox::Cancel), KStandardGuiItem::cancel()); okButton->setEnabled(false); mainLayout->addWidget(ui.buttonBox); connect(ui.groupNameEdit, &QLineEdit::textChanged, q, [okButton] (const QString &text) { okButton->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); // 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); } ~Private() { saveLayout(); } private: void saveLayout() { KConfigGroup configGroup(KSharedConfig::openConfig(), "EditGroupDialog"); configGroup.writeEntry("Size", q->size()); KConfigGroup availableKeysConfig = configGroup.group("AvailableKeysView"); ui.availableKeysList->saveLayout(availableKeysConfig); KConfigGroup groupKeysConfig = configGroup.group("GroupKeysView"); ui.groupKeysList->saveLayout(groupKeysConfig); configGroup.sync(); } void restoreLayout(const QSize &defaultSize) { const KConfigGroup configGroup(KSharedConfig::openConfig(), "EditGroupDialog"); const KConfigGroup availableKeysConfig = configGroup.group("AvailableKeysView"); ui.availableKeysList->restoreLayout(availableKeysConfig); const KConfigGroup groupKeysConfig = configGroup.group("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(); if (!key.isNull()) { auto cmd = new DetailsCommand(key, nullptr); cmd->setParentWidget(q); cmd->start(); } } void addKeysToGroup(); void removeKeysFromGroup(); }; void EditGroupDialog::Private::addKeysToGroup() { const std::vector selectedGroupKeys = ui.groupKeysList->selectedKeys(); const std::vector selectedKeys = ui.availableKeysList->selectedKeys(); groupKeysModel->addKeys(selectedKeys); ui.groupKeysList->selectKeys(selectedGroupKeys); } void EditGroupDialog::Private::removeKeysFromGroup() { const std::vector selectedKeys = ui.groupKeysList->selectedKeys(); for (const Key &key : selectedKeys) { groupKeysModel->removeKey(key); } } 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::setGroupName(const QString &name) { d->ui.groupNameEdit->setText(name); } QString EditGroupDialog::groupName() const { return d->ui.groupNameEdit->text().trimmed(); } void EditGroupDialog::setGroupKeys(const std::vector &keys) { d->groupKeysModel->setKeys(keys); } std::vector EditGroupDialog::groupKeys() const { std::vector 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)); } return keys; }