1 /*
2     conf/groupsconfigwidget.cpp
3 
4     This file is part of Kleopatra, the KDE keymanager
5     SPDX-FileCopyrightText: 2021 g10 Code GmbH
6     SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
7 
8     SPDX-License-Identifier: GPL-2.0-or-later
9 */
10 
11 #include "groupsconfigwidget.h"
12 
13 #include "dialogs/editgroupdialog.h"
14 
15 #include <Libkleo/KeyCache>
16 #include <Libkleo/KeyGroup>
17 #include <Libkleo/KeyListModel>
18 #include <Libkleo/KeyListSortFilterProxyModel>
19 
20 #include <KLocalizedString>
21 #include <KRandom>
22 
23 #include <QLabel>
24 #include <QLineEdit>
25 #include <QListView>
26 #include <QPointer>
27 #include <QPushButton>
28 #include <QVBoxLayout>
29 
30 #include "kleopatra_debug.h"
31 
32 using namespace Kleo;
33 using namespace Kleo::Dialogs;
34 
35 Q_DECLARE_METATYPE(KeyGroup)
36 
37 namespace
38 {
39 
40 class ProxyModel : public AbstractKeyListSortFilterProxyModel
41 {
42     Q_OBJECT
43 public:
ProxyModel(QObject * parent=nullptr)44     ProxyModel(QObject *parent = nullptr)
45         : AbstractKeyListSortFilterProxyModel(parent)
46     {
47     }
48 
49     ~ProxyModel() override = default;
50 
clone() const51     ProxyModel *clone() const override
52     {
53         // compiler-generated copy ctor is fine!
54         return new ProxyModel(*this);
55     }
56 };
57 
58 }
59 
60 class GroupsConfigWidget::Private
61 {
62     friend class ::Kleo::GroupsConfigWidget;
63     GroupsConfigWidget *const q;
64 
65     struct {
66         QLineEdit *groupsFilter = nullptr;
67         QListView *groupsList = nullptr;
68         QPushButton *newButton = nullptr;
69         QPushButton *editButton = nullptr;
70         QPushButton *deleteButton = nullptr;
71     } ui;
72     AbstractKeyListModel *groupsModel = nullptr;
73     ProxyModel *groupsFilterModel = nullptr;
74 
75 public:
Private(GroupsConfigWidget * qq)76     Private(GroupsConfigWidget *qq)
77         : q(qq)
78     {
79         auto mainLayout = new QVBoxLayout(q);
80 
81         auto groupsLayout = new QGridLayout();
82         groupsLayout->setColumnStretch(0, 1);
83         groupsLayout->setRowStretch(1, 1);
84 
85         ui.groupsFilter = new QLineEdit();
86         ui.groupsFilter->setClearButtonEnabled(true);
87         ui.groupsFilter->setPlaceholderText(i18nc("Placeholder text", "Search..."));
88         groupsLayout->addWidget(ui.groupsFilter, 0, 0);
89 
90         groupsModel = AbstractKeyListModel::createFlatKeyListModel(q);
91         groupsFilterModel = new ProxyModel(q);
92         groupsFilterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
93         groupsFilterModel->setFilterKeyColumn(KeyList::Summary);
94         groupsFilterModel->setSortCaseSensitivity(Qt::CaseInsensitive);
95         groupsFilterModel->setSourceModel(groupsModel);
96         groupsFilterModel->sort(KeyList::Summary, Qt::AscendingOrder);
97         ui.groupsList = new QListView();
98         ui.groupsList->setModel(groupsFilterModel);
99         ui.groupsList->setModelColumn(KeyList::Summary);
100         ui.groupsList->setSelectionBehavior(QAbstractItemView::SelectRows);
101         ui.groupsList->setSelectionMode(QAbstractItemView::SingleSelection);
102 
103         groupsLayout->addWidget(ui.groupsList, 1, 0);
104 
105         auto groupsButtonLayout = new QVBoxLayout();
106 
107         ui.newButton = new QPushButton(i18n("New"));
108         groupsButtonLayout->addWidget(ui.newButton);
109 
110         ui.editButton = new QPushButton(i18n("Edit"));
111         ui.editButton->setEnabled(false);
112         groupsButtonLayout->addWidget(ui.editButton);
113 
114         ui.deleteButton = new QPushButton(i18n("Delete"));
115         ui.deleteButton->setEnabled(false);
116         groupsButtonLayout->addWidget(ui.deleteButton);
117 
118         groupsButtonLayout->addStretch(1);
119 
120         groupsLayout->addLayout(groupsButtonLayout, 1, 1);
121 
122         mainLayout->addLayout(groupsLayout, /*stretch=*/ 1);
123 
124         connect(ui.groupsFilter, &QLineEdit::textChanged, groupsFilterModel, &QSortFilterProxyModel::setFilterFixedString);
125         connect(ui.groupsList->selectionModel(), &QItemSelectionModel::selectionChanged,
126                 q, [this] () { selectionChanged(); });
127         connect(ui.groupsList, &QListView::doubleClicked,
128                 q, [this] (const QModelIndex &index) { editGroup(index); });
129         connect(ui.newButton, &QPushButton::clicked, q, [this] () { addGroup(); });
130         connect(ui.editButton, &QPushButton::clicked, q, [this] () { editGroup(); });
131         connect(ui.deleteButton, &QPushButton::clicked, q, [this] () { deleteGroup(); });
132     }
133 
~Private()134     ~Private()
135     {
136     }
137 
138 private:
selectedIndex()139     QModelIndex selectedIndex()
140     {
141         const QModelIndexList selected = ui.groupsList->selectionModel()->selectedRows();
142         return selected.empty() ? QModelIndex() : selected[0];
143     }
144 
getGroup(const QModelIndex & index)145     KeyGroup getGroup(const QModelIndex &index)
146     {
147         return index.isValid() ? ui.groupsList->model()->data(index, KeyList::GroupRole).value<KeyGroup>() : KeyGroup();
148     }
149 
selectionChanged()150     void selectionChanged()
151     {
152         const KeyGroup selectedGroup = getGroup(selectedIndex());
153         const bool selectedGroupIsEditable = !selectedGroup.isNull() && !selectedGroup.isImmutable();
154         ui.editButton->setEnabled(selectedGroupIsEditable);
155         ui.deleteButton->setEnabled(selectedGroupIsEditable);
156     }
157 
showEditGroupDialog(KeyGroup group,const QString & windowTitle,EditGroupDialog::FocusWidget focusWidget)158     KeyGroup showEditGroupDialog(KeyGroup group, const QString &windowTitle, EditGroupDialog::FocusWidget focusWidget)
159     {
160         auto dialog = std::make_unique<EditGroupDialog>(q);
161         dialog->setWindowTitle(windowTitle);
162         dialog->setGroupName(group.name());
163         const KeyGroup::Keys &keys = group.keys();
164         dialog->setGroupKeys(std::vector<GpgME::Key>(keys.cbegin(), keys.cend()));
165         dialog->setInitialFocus(focusWidget);
166 
167         const int result = dialog->exec();
168         if (result == QDialog::Rejected) {
169             return KeyGroup();
170         }
171 
172         group.setName(dialog->groupName());
173         group.setKeys(dialog->groupKeys());
174 
175         return group;
176     }
177 
addGroup()178     void addGroup()
179     {
180         const KeyGroup::Id newId = KRandom::randomString(8);
181         KeyGroup group = KeyGroup(newId, i18nc("default name for new group of keys", "New Group"), {}, KeyGroup::ApplicationConfig);
182         group.setIsImmutable(false);
183 
184         const KeyGroup newGroup = showEditGroupDialog(
185             group, i18nc("@title:window a group of keys", "New Group"), EditGroupDialog::GroupName);
186         if (newGroup.isNull()) {
187             return;
188         }
189 
190         const QModelIndex newIndex = groupsModel->addGroup(newGroup);
191         if (!newIndex.isValid()) {
192             qCDebug(KLEOPATRA_LOG) << "Adding group to model failed";
193             return;
194         }
195 
196         Q_EMIT q->changed();
197     }
198 
editGroup(const QModelIndex & index=QModelIndex ())199     void editGroup(const QModelIndex &index = QModelIndex())
200     {
201         const QModelIndex groupIndex = index.isValid() ? index : selectedIndex();
202         if (!groupIndex.isValid()) {
203             qCDebug(KLEOPATRA_LOG) << "selection is empty";
204             return;
205         }
206         const KeyGroup group = getGroup(groupIndex);
207         if (group.isNull()) {
208             qCDebug(KLEOPATRA_LOG) << "selected group is null";
209             return;
210         }
211         if (group.isImmutable()) {
212             qCDebug(KLEOPATRA_LOG) << "selected group is immutable";
213             return;
214         }
215 
216         const KeyGroup updatedGroup = showEditGroupDialog(
217             group, i18nc("@title:window a group of keys", "Edit Group"), EditGroupDialog::KeysFilter);
218         if (updatedGroup.isNull()) {
219             return;
220         }
221 
222         const bool success = ui.groupsList->model()->setData(groupIndex, QVariant::fromValue(updatedGroup));
223         if (!success) {
224             qCDebug(KLEOPATRA_LOG) << "Updating group in model failed";
225             return;
226         }
227 
228         Q_EMIT q->changed();
229     }
230 
deleteGroup()231     void deleteGroup()
232     {
233         const QModelIndex groupIndex = selectedIndex();
234         if (!groupIndex.isValid()) {
235             qCDebug(KLEOPATRA_LOG) << "selection is empty";
236             return;
237         }
238         const KeyGroup group = getGroup(groupIndex);
239         if (group.isNull()) {
240             qCDebug(KLEOPATRA_LOG) << "selected group is null";
241             return;
242         }
243 
244         const bool success = groupsModel->removeGroup(group);
245         if (!success) {
246             qCDebug(KLEOPATRA_LOG) << "Removing group from model failed";
247             return;
248         }
249 
250         Q_EMIT q->changed();
251     }
252 };
253 
GroupsConfigWidget(QWidget * parent)254 GroupsConfigWidget::GroupsConfigWidget(QWidget *parent)
255     : QWidget(parent)
256     , d(new Private(this))
257 {
258 }
259 
260 GroupsConfigWidget::~GroupsConfigWidget() = default;
261 
setGroups(const std::vector<KeyGroup> & groups)262 void GroupsConfigWidget::setGroups(const std::vector<KeyGroup> &groups)
263 {
264     d->groupsModel->setGroups(groups);
265 }
266 
groups() const267 std::vector<KeyGroup> GroupsConfigWidget::groups() const
268 {
269     std::vector<KeyGroup> result;
270     result.reserve(d->groupsModel->rowCount());
271     for (int row = 0; row < d->groupsModel->rowCount(); ++row) {
272         const QModelIndex index = d->groupsModel->index(row, 0);
273         result.push_back(d->groupsModel->group(index));
274     }
275     return result;
276 }
277 
278 #include "groupsconfigwidget.moc"
279