Libkleo

keyselectioncombo.cpp
1/* This file is part of Kleopatra, the KDE keymanager
2 SPDX-FileCopyrightText: 2016 Klarälvdalens Datakonsult AB
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include <config-libkleo.h>
8
9#include "keyselectioncombo.h"
10
11#include "progressbar.h"
12
13#include <libkleo/defaultkeyfilter.h>
14#include <libkleo/dn.h>
15#include <libkleo/formatting.h>
16#include <libkleo/keycache.h>
17#include <libkleo/keylist.h>
18#include <libkleo/keylistmodel.h>
19#include <libkleo/keylistsortfilterproxymodel.h>
20
21#include <kleo_ui_debug.h>
22
23#include <KLocalizedString>
24
25#include <QAbstractProxyModel>
26#include <QList>
27#include <QSortFilterProxyModel>
28#include <QTimer>
29
30#include <gpgme++/key.h>
31
32using namespace Kleo;
33
34#if !UNITY_BUILD
35Q_DECLARE_METATYPE(GpgME::Key)
36#endif
37namespace
38{
39class SortFilterProxyModel : public KeyListSortFilterProxyModel
40{
41 Q_OBJECT
42
43public:
44 using KeyListSortFilterProxyModel::KeyListSortFilterProxyModel;
45
46 void setAlwaysAcceptedKey(const QString &fingerprint)
47 {
48 if (fingerprint == mFingerprint) {
49 return;
50 }
51 mFingerprint = fingerprint;
52 invalidate();
53 }
54
55protected:
56 bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override
57 {
58 if (!mFingerprint.isEmpty()) {
59 const QModelIndex index = sourceModel()->index(source_row, 0, source_parent);
60 const auto fingerprint = sourceModel()->data(index, KeyList::FingerprintRole).toString();
61 if (fingerprint == mFingerprint) {
62 return true;
63 }
64 }
65
66 return KeyListSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
67 }
68
69private:
70 QString mFingerprint;
71};
72
73static QString formatUserID(const GpgME::Key &key)
74{
75 const auto userID = key.userID(0);
76 QString name;
77 QString email;
78
79 if (key.protocol() == GpgME::OpenPGP) {
80 name = QString::fromUtf8(userID.name());
81 email = QString::fromUtf8(userID.email());
82 } else {
83 const Kleo::DN dn(userID.id());
84 name = dn[QStringLiteral("CN")];
85 email = dn[QStringLiteral("EMAIL")];
86 }
87 return email.isEmpty() ? name : name.isEmpty() ? email : i18nc("Name <email>", "%1 <%2>", name, email);
88}
89
90class SortAndFormatCertificatesProxyModel : public QSortFilterProxyModel
91{
92 Q_OBJECT
93
94public:
95 SortAndFormatCertificatesProxyModel(KeyUsage::Flags usageFlags, QObject *parent = nullptr)
96 : QSortFilterProxyModel{parent}
97 , mIconProvider{usageFlags}
98 {
99 }
100
101private:
102 bool lessThan(const QModelIndex &left, const QModelIndex &right) const override
103 {
104 const auto leftKey = sourceModel()->data(left, KeyList::KeyRole).value<GpgME::Key>();
105 const auto rightKey = sourceModel()->data(right, KeyList::KeyRole).value<GpgME::Key>();
106 if (leftKey.isNull()) {
107 return false;
108 }
109 if (rightKey.isNull()) {
110 return true;
111 }
112 // As we display UID(0) this is ok. We probably need a get Best UID at some point.
113 const auto lUid = leftKey.userID(0);
114 const auto rUid = rightKey.userID(0);
115 if (lUid.isNull()) {
116 return false;
117 }
118 if (rUid.isNull()) {
119 return true;
120 }
121 const auto leftNameAndEmail = formatUserID(leftKey);
122 const auto rightNameAndEmail = formatUserID(rightKey);
123 const int cmp = QString::localeAwareCompare(leftNameAndEmail, rightNameAndEmail);
124 if (cmp) {
125 return cmp < 0;
126 }
127
128 if (lUid.validity() != rUid.validity()) {
129 return lUid.validity() > rUid.validity();
130 }
131
132 /* Both have the same validity, check which one is newer. */
133 time_t leftTime = 0;
134 for (const GpgME::Subkey &s : leftKey.subkeys()) {
135 if (s.isBad()) {
136 continue;
137 }
138 if (s.creationTime() > leftTime) {
139 leftTime = s.creationTime();
140 }
141 }
142 time_t rightTime = 0;
143 for (const GpgME::Subkey &s : rightKey.subkeys()) {
144 if (s.isBad()) {
145 continue;
146 }
147 if (s.creationTime() > rightTime) {
148 rightTime = s.creationTime();
149 }
150 }
151 if (rightTime != leftTime) {
152 return leftTime > rightTime;
153 }
154
155 // as final resort we compare the fingerprints
156 return strcmp(leftKey.primaryFingerprint(), rightKey.primaryFingerprint()) < 0;
157 }
158
159protected:
160 QVariant data(const QModelIndex &index, int role) const override
161 {
162 if (!index.isValid()) {
163 return QVariant();
164 }
165
166 const auto key = QSortFilterProxyModel::data(index, KeyList::KeyRole).value<GpgME::Key>();
167 Q_ASSERT(!key.isNull());
168 if (key.isNull()) {
169 return QVariant();
170 }
171
172 switch (role) {
173 case Qt::DisplayRole:
175 const auto nameAndEmail = formatUserID(key);
176 if (Kleo::KeyCache::instance()->pgpOnly()) {
177 return i18nc("Name <email> (validity, created: date)",
178 "%1 (%2, created: %3)",
179 nameAndEmail,
180 Kleo::Formatting::complianceStringShort(key),
181 Kleo::Formatting::creationDateString(key));
182 } else {
183 return i18nc("Name <email> (validity, type, created: date)",
184 "%1 (%2, %3, created: %4)",
185 nameAndEmail,
186 Kleo::Formatting::complianceStringShort(key),
187 Formatting::displayName(key.protocol()),
188 Kleo::Formatting::creationDateString(key));
189 }
190 }
191 case Qt::ToolTipRole: {
192 using namespace Kleo::Formatting;
193 return Kleo::Formatting::toolTip(key, Validity | Issuer | Subject | Fingerprint | ExpiryDates | UserIDs);
194 }
195 case Qt::DecorationRole: {
196 return mIconProvider.icon(key);
197 }
198 default:
199 return QSortFilterProxyModel::data(index, role);
200 }
201 }
202
203private:
204 Formatting::IconProvider mIconProvider;
205};
206
207class CustomItemsProxyModel : public QAbstractProxyModel
208{
209 Q_OBJECT
210
211private:
212 struct CustomItem {
213 QIcon icon;
214 QString text;
215 QVariant data;
216 QString toolTip;
217 };
218
219public:
220 CustomItemsProxyModel(QObject *parent = nullptr)
221 : QAbstractProxyModel(parent)
222 {
223 }
224
225 ~CustomItemsProxyModel() override
226 {
227 qDeleteAll(mFrontItems);
228 qDeleteAll(mBackItems);
229 }
230
231 void setSourceModel(QAbstractItemModel *newSourceModel) override
232 {
233 if (newSourceModel == sourceModel())
234 return;
235
236 beginResetModel();
237
238 if (sourceModel()) {
239 disconnect(sourceModel(), &QAbstractItemModel::dataChanged, this, &CustomItemsProxyModel::onSourceDataChanged);
240 disconnect(sourceModel(), &QAbstractItemModel::headerDataChanged, this, &CustomItemsProxyModel::onSourceHeaderDataChanged);
241 disconnect(sourceModel(), &QAbstractItemModel::rowsAboutToBeInserted, this, &CustomItemsProxyModel::onSourceRowsAboutToBeInserted);
242 disconnect(sourceModel(), &QAbstractItemModel::rowsInserted, this, &CustomItemsProxyModel::onSourceRowsInserted);
243 disconnect(sourceModel(), &QAbstractItemModel::rowsAboutToBeRemoved, this, &CustomItemsProxyModel::onSourceRowsAboutToBeRemoved);
244 disconnect(sourceModel(), &QAbstractItemModel::rowsRemoved, this, &CustomItemsProxyModel::onSourceRowsRemoved);
245 disconnect(sourceModel(), &QAbstractItemModel::rowsAboutToBeMoved, this, &CustomItemsProxyModel::onSourceRowsAboutToBeMoved);
246 disconnect(sourceModel(), &QAbstractItemModel::rowsMoved, this, &CustomItemsProxyModel::onSourceRowsMoved);
247 disconnect(sourceModel(), &QAbstractItemModel::columnsAboutToBeInserted, this, &CustomItemsProxyModel::onSourceColumnsAboutToBeInserted);
248 disconnect(sourceModel(), &QAbstractItemModel::columnsInserted, this, &CustomItemsProxyModel::onSourceColumnsInserted);
249 disconnect(sourceModel(), &QAbstractItemModel::columnsAboutToBeRemoved, this, &CustomItemsProxyModel::onSourceColumnsAboutToBeRemoved);
250 disconnect(sourceModel(), &QAbstractItemModel::columnsRemoved, this, &CustomItemsProxyModel::onSourceColumnsRemoved);
251 disconnect(sourceModel(), &QAbstractItemModel::columnsAboutToBeMoved, this, &CustomItemsProxyModel::onSourceColumnsAboutToBeMoved);
252 disconnect(sourceModel(), &QAbstractItemModel::columnsMoved, this, &CustomItemsProxyModel::onSourceColumnsMoved);
253 disconnect(sourceModel(), &QAbstractItemModel::layoutAboutToBeChanged, this, &CustomItemsProxyModel::onSourceLayoutAboutToBeChanged);
254 disconnect(sourceModel(), &QAbstractItemModel::layoutChanged, this, &CustomItemsProxyModel::onSourceLayoutChanged);
255 disconnect(sourceModel(), &QAbstractItemModel::modelAboutToBeReset, this, &CustomItemsProxyModel::onSourceAboutToBeReset);
256 disconnect(sourceModel(), &QAbstractItemModel::modelReset, this, &CustomItemsProxyModel::onSourceReset);
257 }
258
260
261 if (sourceModel()) {
262 connect(sourceModel(), &QAbstractItemModel::dataChanged, this, &CustomItemsProxyModel::onSourceDataChanged);
263 connect(sourceModel(), &QAbstractItemModel::headerDataChanged, this, &CustomItemsProxyModel::onSourceHeaderDataChanged);
264 connect(sourceModel(), &QAbstractItemModel::rowsAboutToBeInserted, this, &CustomItemsProxyModel::onSourceRowsAboutToBeInserted);
265 connect(sourceModel(), &QAbstractItemModel::rowsInserted, this, &CustomItemsProxyModel::onSourceRowsInserted);
266 connect(sourceModel(), &QAbstractItemModel::rowsAboutToBeRemoved, this, &CustomItemsProxyModel::onSourceRowsAboutToBeRemoved);
267 connect(sourceModel(), &QAbstractItemModel::rowsRemoved, this, &CustomItemsProxyModel::onSourceRowsRemoved);
268 connect(sourceModel(), &QAbstractItemModel::rowsAboutToBeMoved, this, &CustomItemsProxyModel::onSourceRowsAboutToBeMoved);
269 connect(sourceModel(), &QAbstractItemModel::rowsMoved, this, &CustomItemsProxyModel::onSourceRowsMoved);
270 connect(sourceModel(), &QAbstractItemModel::columnsAboutToBeInserted, this, &CustomItemsProxyModel::onSourceColumnsAboutToBeInserted);
271 connect(sourceModel(), &QAbstractItemModel::columnsInserted, this, &CustomItemsProxyModel::onSourceColumnsInserted);
272 connect(sourceModel(), &QAbstractItemModel::columnsAboutToBeRemoved, this, &CustomItemsProxyModel::onSourceColumnsAboutToBeRemoved);
273 connect(sourceModel(), &QAbstractItemModel::columnsRemoved, this, &CustomItemsProxyModel::onSourceColumnsRemoved);
274 connect(sourceModel(), &QAbstractItemModel::columnsAboutToBeMoved, this, &CustomItemsProxyModel::onSourceColumnsAboutToBeMoved);
275 connect(sourceModel(), &QAbstractItemModel::columnsMoved, this, &CustomItemsProxyModel::onSourceColumnsMoved);
276 connect(sourceModel(), &QAbstractItemModel::layoutAboutToBeChanged, this, &CustomItemsProxyModel::onSourceLayoutAboutToBeChanged);
277 connect(sourceModel(), &QAbstractItemModel::layoutChanged, this, &CustomItemsProxyModel::onSourceLayoutChanged);
278 connect(sourceModel(), &QAbstractItemModel::modelAboutToBeReset, this, &CustomItemsProxyModel::onSourceAboutToBeReset);
279 connect(sourceModel(), &QAbstractItemModel::modelReset, this, &CustomItemsProxyModel::onSourceReset);
280 }
281
282 endResetModel();
283 }
284
285 bool isCustomItem(const int row) const
286 {
287 const int sourceRowCount = sourceModel() ? sourceModel()->rowCount() : 0;
288 return row < mFrontItems.size() || row >= mFrontItems.size() + sourceRowCount;
289 }
290
291 void prependItem(const QIcon &icon, const QString &text, const QVariant &data, const QString &toolTip)
292 {
293 beginInsertRows(QModelIndex(), 0, 0);
294 mFrontItems.push_front(new CustomItem{icon, text, data, toolTip});
295 endInsertRows();
296 }
297
298 void appendItem(const QIcon &icon, const QString &text, const QVariant &data, const QString &toolTip)
299 {
300 beginInsertRows(QModelIndex(), rowCount(), rowCount());
301 mBackItems.push_back(new CustomItem{icon, text, data, toolTip});
302 endInsertRows();
303 }
304
305 void removeCustomItem(const QVariant &data)
306 {
307 for (int i = 0; i < mFrontItems.count(); ++i) {
308 if (mFrontItems[i]->data == data) {
309 beginRemoveRows(QModelIndex(), i, i);
310 delete mFrontItems.takeAt(i);
311 endRemoveRows();
312 return;
313 }
314 }
315 const int sourceRowCount = sourceModel() ? sourceModel()->rowCount() : 0;
316 for (int i = 0; i < mBackItems.count(); ++i) {
317 if (mBackItems[i]->data == data) {
318 const int row = mFrontItems.count() + sourceRowCount + i;
319 beginRemoveRows(QModelIndex(), row, row);
320 delete mBackItems.takeAt(i);
321 endRemoveRows();
322 return;
323 }
324 }
325 }
326
327 int rowCount(const QModelIndex &parent = QModelIndex()) const override
328 {
329 Q_UNUSED(parent)
330 const int sourceRowCount = sourceModel() ? sourceModel()->rowCount() : 0;
331 return mFrontItems.count() + sourceRowCount + mBackItems.count();
332 }
333
334 int columnCount(const QModelIndex &parent = QModelIndex()) const override
335 {
336 Q_UNUSED(parent)
337 // pretend that there is only one column to workaround a bug in
338 // QAccessibleTable which provides the accessibility interface for the
339 // pop-up of the combo box
340 return 1;
341 }
342
343 QModelIndex mapToSource(const QModelIndex &proxyIndex) const override
344 {
345 if (!proxyIndex.isValid()) {
346 return {};
347 }
348 if (!isCustomItem(proxyIndex.row())) {
349 const int sourceRow = proxyIndex.row() - mFrontItems.count();
350 return sourceModel()->index(sourceRow, proxyIndex.column());
351 }
352 return {};
353 }
354
355 QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override
356 {
357 if (!sourceIndex.isValid())
358 return {};
359 return createIndex(mFrontItems.count() + sourceIndex.row(), sourceIndex.column(), sourceIndex.internalPointer());
360 }
361
362 QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override
363 {
364 if (row < 0 || row >= rowCount()) {
365 return {};
366 }
367 const int sourceRowCount = sourceModel() ? sourceModel()->rowCount() : 0;
368 if (row < mFrontItems.count()) {
369 return createIndex(row, column, mFrontItems[row]);
370 } else if (row >= mFrontItems.count() + sourceRowCount) {
371 return createIndex(row, column, mBackItems[row - mFrontItems.count() - sourceRowCount]);
372 } else {
373 const QModelIndex mi = sourceModel()->index(row - mFrontItems.count(), column, parent);
374 return createIndex(row, column, mi.internalPointer());
375 }
376 }
377
378 Qt::ItemFlags flags(const QModelIndex &index) const override
379 {
380 Q_UNUSED(index)
382 }
383
384 QModelIndex parent(const QModelIndex &) const override
385 {
386 // Flat list
387 return {};
388 }
389
390 QVariant data(const QModelIndex &index, int role) const override
391 {
392 if (!index.isValid()) {
393 return QVariant();
394 }
395
396 if (isCustomItem(index.row())) {
397 Q_ASSERT(!mFrontItems.isEmpty() || !mBackItems.isEmpty());
398 auto ci = static_cast<CustomItem *>(index.internalPointer());
399 switch (role) {
400 case Qt::DisplayRole:
401 return ci->text;
403 return ci->icon;
404 case Qt::UserRole:
405 return ci->data;
406 case Qt::ToolTipRole:
407 return ci->toolTip;
408 default:
409 return QVariant();
410 }
411 }
412
413 return QAbstractProxyModel::data(index, role);
414 }
415
416 void onSourceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles)
417 {
418 Q_EMIT dataChanged(mapFromSource(topLeft), mapFromSource(bottomRight), roles);
419 }
420
421 void onSourceHeaderDataChanged(Qt::Orientation orientation, int first, int last)
422 {
423 Q_EMIT headerDataChanged(orientation, first, last);
424 }
425
426 void onSourceRowsAboutToBeInserted(const QModelIndex &parent, int start, int end)
427 {
428 if (parent.isValid()) {
429 // not supported, the proxy is a flat model
430 return;
431 }
432 beginInsertRows({}, mFrontItems.count() + start, mFrontItems.count() + end);
433 }
434
435 void onSourceRowsInserted(const QModelIndex &parent, int start, int end)
436 {
437 Q_UNUSED(start)
438 Q_UNUSED(end)
439 if (parent.isValid()) {
440 // not supported, the proxy is a flat model
441 return;
442 }
443 endInsertRows();
444 }
445
446 void onSourceRowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
447 {
448 if (parent.isValid()) {
449 // not supported, the proxy is a flat model
450 return;
451 }
452 beginRemoveRows({}, mFrontItems.count() + start, mFrontItems.count() + end);
453 }
454
455 void onSourceRowsRemoved(const QModelIndex &parent, int start, int end)
456 {
457 Q_UNUSED(start)
458 Q_UNUSED(end)
459 if (parent.isValid()) {
460 // not supported, the proxy is a flat model
461 return;
462 }
463 endRemoveRows();
464 }
465
466 void onSourceRowsAboutToBeMoved(const QModelIndex &sourceParent, int sourceFirst, int sourceLast, const QModelIndex &destParent, int destRow)
467 {
468 if (sourceParent.isValid() || destParent.isValid()) {
469 // not supported, the proxy is a flat model
470 return;
471 }
472 beginMoveRows({}, mFrontItems.count() + sourceFirst, mFrontItems.count() + sourceLast, {}, mFrontItems.count() + destRow);
473 }
474
475 void onSourceRowsMoved(const QModelIndex &sourceParent, int sourceFirst, int sourceLast, const QModelIndex &destParent, int destRow)
476 {
477 Q_UNUSED(sourceFirst)
478 Q_UNUSED(sourceLast)
479 Q_UNUSED(destRow)
480 if (sourceParent.isValid() || destParent.isValid()) {
481 // not supported, the proxy is a flat model
482 return;
483 }
484 endMoveRows();
485 }
486
487 void onSourceColumnsAboutToBeInserted(const QModelIndex &parent, int start, int end)
488 {
489 if (parent.isValid()) {
490 // not supported, the proxy is a flat model
491 return;
492 }
493 beginInsertColumns({}, start, end);
494 }
495
496 void onSourceColumnsInserted(const QModelIndex &parent, int start, int end)
497 {
498 Q_UNUSED(start)
499 Q_UNUSED(end)
500 if (parent.isValid()) {
501 // not supported, the proxy is a flat model
502 return;
503 }
504 endInsertColumns();
505 }
506
507 void onSourceColumnsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
508 {
509 if (parent.isValid()) {
510 // not supported, the proxy is a flat model
511 return;
512 }
513 beginRemoveColumns({}, start, end);
514 }
515
516 void onSourceColumnsRemoved(const QModelIndex &parent, int start, int end)
517 {
518 Q_UNUSED(start)
519 Q_UNUSED(end)
520 if (parent.isValid()) {
521 // not supported, the proxy is a flat model
522 return;
523 }
524 endRemoveColumns();
525 }
526
527 void onSourceColumnsAboutToBeMoved(const QModelIndex &sourceParent, int sourceFirst, int sourceLast, const QModelIndex &destParent, int destColumn)
528 {
529 if (sourceParent.isValid() || destParent.isValid()) {
530 // not supported, the proxy is a flat model
531 return;
532 }
533 beginMoveColumns({}, sourceFirst, sourceLast, {}, destColumn);
534 }
535
536 void onSourceColumnsMoved(const QModelIndex &sourceParent, int sourceFirst, int sourceLast, const QModelIndex &destParent, int destColumn)
537 {
538 Q_UNUSED(sourceFirst)
539 Q_UNUSED(sourceLast)
540 Q_UNUSED(destColumn)
541 if (sourceParent.isValid() || destParent.isValid()) {
542 // not supported, the proxy is a flat model
543 return;
544 }
545 endMoveColumns();
546 }
547
548 void onSourceLayoutAboutToBeChanged(const QList<QPersistentModelIndex> &sourceParents, QAbstractItemModel::LayoutChangeHint hint)
549 {
550 // adapted from QConcatenateTablesProxyModel
551 if (!sourceParents.isEmpty() && !sourceParents.contains(QModelIndex())) {
552 // not supported, the proxy is a flat model
553 return;
554 }
555
556 Q_EMIT layoutAboutToBeChanged({}, hint);
557
558 const QModelIndexList persistentIndexList = this->persistentIndexList();
559 layoutChangeSourcePersistentIndexes.reserve(persistentIndexList.size());
560 layoutChangeProxyIndexes.reserve(persistentIndexList.size());
561
562 for (const QModelIndex &proxyPersistentIndex : persistentIndexList) {
563 if (!isCustomItem(proxyPersistentIndex.row())) {
564 layoutChangeProxyIndexes.append(proxyPersistentIndex);
565 Q_ASSERT(proxyPersistentIndex.isValid());
566 const QPersistentModelIndex srcPersistentIndex = mapToSource(proxyPersistentIndex);
567 Q_ASSERT(srcPersistentIndex.isValid());
568 layoutChangeSourcePersistentIndexes.append(srcPersistentIndex);
569 }
570 }
571 }
572
573 void onSourceLayoutChanged(const QList<QPersistentModelIndex> &sourceParents, QAbstractItemModel::LayoutChangeHint hint)
574 {
575 // adapted from QConcatenateTablesProxyModel
576 if (!sourceParents.isEmpty() && !sourceParents.contains(QModelIndex())) {
577 // not supported, the proxy is a flat model
578 return;
579 }
580 for (int i = 0; i < layoutChangeProxyIndexes.size(); ++i) {
581 const QModelIndex proxyIdx = layoutChangeProxyIndexes.at(i);
582 const QModelIndex newProxyIdx = mapFromSource(layoutChangeSourcePersistentIndexes.at(i));
583 changePersistentIndex(proxyIdx, newProxyIdx);
584 }
585
586 layoutChangeSourcePersistentIndexes.clear();
587 layoutChangeProxyIndexes.clear();
588
589 Q_EMIT layoutChanged({}, hint);
590 }
591
592 void onSourceAboutToBeReset()
593 {
594 beginResetModel();
595 }
596
597 void onSourceReset()
598 {
599 endResetModel();
600 }
601
602private:
603 QList<CustomItem *> mFrontItems;
604 QList<CustomItem *> mBackItems;
605
606 // for layoutAboutToBeChanged/layoutChanged
607 QVector<QPersistentModelIndex> layoutChangeSourcePersistentIndexes;
608 QVector<QModelIndex> layoutChangeProxyIndexes;
609};
610
611} // anonymous namespace
612
613namespace Kleo
614{
615class KeySelectionComboPrivate
616{
617public:
618 KeySelectionComboPrivate(KeySelectionCombo *parent, bool secretOnly_, KeyUsage::Flags usage)
619 : wasEnabled(true)
620 , secretOnly{secretOnly_}
621 , usageFlags{usage}
622 , q{parent}
623 {
624 }
625
626 /* Selects the first key with a UID addrSpec that matches
627 * the mPerfectMatchMbox variable.
628 *
629 * The idea here is that if there are keys like:
630 *
631 * tom-store@abc.com
632 * susi-store@abc.com
633 * store@abc.com
634 *
635 * And the user wants to send a mail to "store@abc.com"
636 * the filter should still show tom and susi (because they
637 * both are part of store) but the key for "store" should
638 * be preselected.
639 *
640 * Returns true if one was selected. False otherwise. */
641 bool selectPerfectIdMatch() const
642 {
643 if (mPerfectMatchMbox.isEmpty()) {
644 return false;
645 }
646
647 for (int i = 0; i < proxyModel->rowCount(); ++i) {
648 const auto idx = proxyModel->index(i, 0, QModelIndex());
649 const auto key = proxyModel->data(idx, KeyList::KeyRole).value<GpgME::Key>();
650 if (key.isNull()) {
651 // WTF?
652 continue;
653 }
654 for (const auto &uid : key.userIDs()) {
655 if (QString::fromStdString(uid.addrSpec()) == mPerfectMatchMbox) {
656 q->setCurrentIndex(i);
657 return true;
658 }
659 }
660 }
661 return false;
662 }
663
664 /* Updates the current key with the default key if the key matches
665 * the current key filter. */
666 void updateWithDefaultKey()
667 {
668 GpgME::Protocol filterProto = GpgME::UnknownProtocol;
669
670 const auto filter = dynamic_cast<const DefaultKeyFilter *>(sortFilterProxy->keyFilter().get());
671 if (filter && filter->isOpenPGP() == DefaultKeyFilter::Set) {
672 filterProto = GpgME::OpenPGP;
673 } else if (filter && filter->isOpenPGP() == DefaultKeyFilter::NotSet) {
674 filterProto = GpgME::CMS;
675 }
676
677 QString defaultKey = defaultKeys.value(filterProto);
678 if (defaultKey.isEmpty()) {
679 // Fallback to unknown protocol
680 defaultKey = defaultKeys.value(GpgME::UnknownProtocol);
681 }
682 // make sure that the default key is not filtered out unless it has the wrong protocol
683 if (filterProto == GpgME::UnknownProtocol) {
684 sortFilterProxy->setAlwaysAcceptedKey(defaultKey);
685 } else {
686 const auto key = KeyCache::instance()->findByFingerprint(defaultKey.toLatin1().constData());
687 if (!key.isNull() && key.protocol() == filterProto) {
688 sortFilterProxy->setAlwaysAcceptedKey(defaultKey);
689 } else {
690 sortFilterProxy->setAlwaysAcceptedKey({});
691 }
692 }
693 q->setCurrentKey(defaultKey);
694 }
695
696 void storeCurrentSelectionBeforeModelChange()
697 {
698 keyBeforeModelChange = q->currentKey();
699 customItemBeforeModelChange = q->currentData();
700 }
701
702 void restoreCurrentSelectionAfterModelChange()
703 {
704 if (!keyBeforeModelChange.isNull()) {
705 q->setCurrentKey(keyBeforeModelChange);
706 } else if (customItemBeforeModelChange.isValid()) {
707 const auto index = q->findData(customItemBeforeModelChange);
708 if (index != -1) {
709 q->setCurrentIndex(index);
710 } else {
711 updateWithDefaultKey();
712 }
713 }
714 }
715
716 Kleo::AbstractKeyListModel *model = nullptr;
717 SortFilterProxyModel *sortFilterProxy = nullptr;
718 SortAndFormatCertificatesProxyModel *sortAndFormatProxy = nullptr;
719 CustomItemsProxyModel *proxyModel = nullptr;
720 std::shared_ptr<Kleo::KeyCache> cache;
722 bool wasEnabled = false;
723 bool useWasEnabled = false;
724 bool secretOnly = false;
725 bool initialKeyListingDone = false;
726 QString mPerfectMatchMbox;
727 GpgME::Key keyBeforeModelChange;
728 QVariant customItemBeforeModelChange;
729 KeyUsage::Flags usageFlags;
730
731private:
732 KeySelectionCombo *const q;
733};
734
735}
736
737using namespace Kleo;
738
739KeySelectionCombo::KeySelectionCombo(QWidget *parent)
740 : KeySelectionCombo(true, KeyUsage::None, parent)
741{
742}
743
744KeySelectionCombo::KeySelectionCombo(bool secretOnly, QWidget *parent)
745 : KeySelectionCombo(secretOnly, KeyUsage::None, parent)
746{
747}
748
749KeySelectionCombo::KeySelectionCombo(KeyUsage::Flags usage, QWidget *parent)
750 : KeySelectionCombo{false, usage, parent}
751{
752}
753
754KeySelectionCombo::KeySelectionCombo(KeyUsage::Flag usage, QWidget *parent)
755 : KeySelectionCombo{false, usage, parent}
756{
757}
758
759KeySelectionCombo::KeySelectionCombo(bool secretOnly, KeyUsage::Flags usage, QWidget *parent)
760 : QComboBox(parent)
761 , d(new KeySelectionComboPrivate(this, secretOnly, usage))
762{
763 // set a non-empty string as accessible description to prevent screen readers
764 // from reading the tool tip which isn't meant for screen readers
765 setAccessibleDescription(QStringLiteral(" "));
766 d->model = Kleo::AbstractKeyListModel::createFlatKeyListModel(this);
767
768 d->sortFilterProxy = new SortFilterProxyModel(this);
769 d->sortFilterProxy->setSourceModel(d->model);
770
771 d->sortAndFormatProxy = new SortAndFormatCertificatesProxyModel{usage, this};
772 d->sortAndFormatProxy->setSourceModel(d->sortFilterProxy);
773 // initialize dynamic sorting
774 d->sortAndFormatProxy->sort(0);
775
776 d->proxyModel = new CustomItemsProxyModel{this};
777 d->proxyModel->setSourceModel(d->sortAndFormatProxy);
778
779 setModel(d->proxyModel);
780 connect(this, &QComboBox::currentIndexChanged, this, [this](int row) {
781 if (row >= 0 && row < d->proxyModel->rowCount()) {
782 if (d->proxyModel->isCustomItem(row)) {
783 Q_EMIT customItemSelected(currentData(Qt::UserRole));
784 } else {
785 Q_EMIT currentKeyChanged(currentKey());
786 }
787 }
788 });
789
790 d->cache = Kleo::KeyCache::mutableInstance();
791
792 connect(model(), &QAbstractItemModel::rowsAboutToBeInserted, this, [this]() {
793 d->storeCurrentSelectionBeforeModelChange();
794 });
795 connect(model(), &QAbstractItemModel::rowsInserted, this, [this]() {
796 d->restoreCurrentSelectionAfterModelChange();
797 });
798 connect(model(), &QAbstractItemModel::rowsAboutToBeRemoved, this, [this]() {
799 d->storeCurrentSelectionBeforeModelChange();
800 });
801 connect(model(), &QAbstractItemModel::rowsRemoved, this, [this]() {
802 d->restoreCurrentSelectionAfterModelChange();
803 });
804 connect(model(), &QAbstractItemModel::modelAboutToBeReset, this, [this]() {
805 d->storeCurrentSelectionBeforeModelChange();
806 });
807 connect(model(), &QAbstractItemModel::modelReset, this, [this]() {
808 d->restoreCurrentSelectionAfterModelChange();
809 });
810
811 QTimer::singleShot(0, this, &KeySelectionCombo::init);
812}
813
814KeySelectionCombo::~KeySelectionCombo() = default;
815
816void KeySelectionCombo::init()
817{
818 connect(d->cache.get(), &Kleo::KeyCache::keyListingDone, this, [this]() {
819 // Set useKeyCache ensures that the cache is populated
820 // so this can be a blocking call if the cache is not initialized
821 if (!d->initialKeyListingDone) {
822 d->model->useKeyCache(true, d->secretOnly ? KeyList::SecretKeysOnly : KeyList::AllKeys);
823 }
824 d->proxyModel->removeCustomItem(QStringLiteral("-libkleo-loading-keys"));
825
826 // We use the useWasEnabled state variable to decide if we should
827 // change the enable / disable state based on the keylist done signal.
828 // If we triggered the refresh useWasEnabled is true and we want to
829 // enable / disable again after our refresh, as the refresh disabled it.
830 //
831 // But if a keyListingDone signal comes from just a generic refresh
832 // triggered by someone else we don't want to change the enable / disable
833 // state.
834 if (d->useWasEnabled) {
835 setEnabled(d->wasEnabled);
836 d->useWasEnabled = false;
837 }
838 Q_EMIT keyListingFinished();
839 });
840
841 connect(this, &KeySelectionCombo::keyListingFinished, this, [this]() {
842 if (!d->initialKeyListingDone) {
843 d->updateWithDefaultKey();
844 d->initialKeyListingDone = true;
845 }
846 });
847
848 if (!d->cache->initialized()) {
849 refreshKeys();
850 } else {
851 d->model->useKeyCache(true, d->secretOnly ? KeyList::SecretKeysOnly : KeyList::AllKeys);
852 Q_EMIT keyListingFinished();
853 }
854
855 connect(this, &QComboBox::currentIndexChanged, this, [this]() {
856 setToolTip(currentData(Qt::ToolTipRole).toString());
857 });
858}
859
860void KeySelectionCombo::setKeyFilter(const std::shared_ptr<const KeyFilter> &kf)
861{
862 d->sortFilterProxy->setKeyFilter(kf);
863 d->updateWithDefaultKey();
864}
865
866std::shared_ptr<const KeyFilter> KeySelectionCombo::keyFilter() const
867{
868 return d->sortFilterProxy->keyFilter();
869}
870
871void KeySelectionCombo::setIdFilter(const QString &id)
872{
873 d->sortFilterProxy->setFilterRegularExpression(id);
874 d->mPerfectMatchMbox = id;
875 d->updateWithDefaultKey();
876}
877
878QString KeySelectionCombo::idFilter() const
879{
880 return d->sortFilterProxy->filterRegularExpression().pattern();
881}
882
883GpgME::Key Kleo::KeySelectionCombo::currentKey() const
884{
885 return currentData(KeyList::KeyRole).value<GpgME::Key>();
886}
887
888void Kleo::KeySelectionCombo::setCurrentKey(const GpgME::Key &key)
889{
890 const int idx = findData(QString::fromLatin1(key.primaryFingerprint()), KeyList::FingerprintRole, Qt::MatchExactly);
891 if (idx > -1) {
892 setCurrentIndex(idx);
893 } else if (!d->selectPerfectIdMatch()) {
894 d->updateWithDefaultKey();
895 }
896 setToolTip(currentData(Qt::ToolTipRole).toString());
897}
898
899void Kleo::KeySelectionCombo::setCurrentKey(const QString &fingerprint)
900{
901 const auto cur = currentKey();
902 if (!cur.isNull() && !fingerprint.isEmpty() && fingerprint == QLatin1StringView(cur.primaryFingerprint())) {
903 // already set; still emit a changed signal because the current key may
904 // have become the item at the current index by changes in the underlying model
905 Q_EMIT currentKeyChanged(cur);
906 return;
907 }
908 const int idx = findData(fingerprint, KeyList::FingerprintRole, Qt::MatchExactly);
909 if (idx > -1) {
910 setCurrentIndex(idx);
911 } else if (!d->selectPerfectIdMatch()) {
912 setCurrentIndex(0);
913 }
914 setToolTip(currentData(Qt::ToolTipRole).toString());
915}
916
917void KeySelectionCombo::refreshKeys()
918{
919 d->wasEnabled = isEnabled();
920 d->useWasEnabled = true;
921 setEnabled(false);
922 const bool wasBlocked = blockSignals(true);
923 prependCustomItem(QIcon(), i18n("Loading keys ..."), QStringLiteral("-libkleo-loading-keys"));
925 blockSignals(wasBlocked);
926 d->cache->startKeyListing();
927}
928
929void KeySelectionCombo::appendCustomItem(const QIcon &icon, const QString &text, const QVariant &data, const QString &toolTip)
930{
931 d->proxyModel->appendItem(icon, text, data, toolTip);
932}
933
934void KeySelectionCombo::appendCustomItem(const QIcon &icon, const QString &text, const QVariant &data)
935{
936 appendCustomItem(icon, text, data, QString());
937}
938
939void KeySelectionCombo::prependCustomItem(const QIcon &icon, const QString &text, const QVariant &data, const QString &toolTip)
940{
941 d->proxyModel->prependItem(icon, text, data, toolTip);
942}
943
944void KeySelectionCombo::prependCustomItem(const QIcon &icon, const QString &text, const QVariant &data)
945{
946 prependCustomItem(icon, text, data, QString());
947}
948
949void KeySelectionCombo::removeCustomItem(const QVariant &data)
950{
951 d->proxyModel->removeCustomItem(data);
952}
953
954void Kleo::KeySelectionCombo::setDefaultKey(const QString &fingerprint, GpgME::Protocol proto)
955{
956 d->defaultKeys.insert(proto, fingerprint);
957 d->updateWithDefaultKey();
958}
959
960void Kleo::KeySelectionCombo::setDefaultKey(const QString &fingerprint)
961{
962 setDefaultKey(fingerprint, GpgME::UnknownProtocol);
963}
964
965QString Kleo::KeySelectionCombo::defaultKey(GpgME::Protocol proto) const
966{
967 return d->defaultKeys.value(proto);
968}
969
970QString Kleo::KeySelectionCombo::defaultKey() const
971{
972 return defaultKey(GpgME::UnknownProtocol);
973}
974#include "keyselectioncombo.moc"
975
976#include "moc_keyselectioncombo.cpp"
DN parser and reorderer.
Definition dn.h:27
Default implementation of key filter class.
Q_SCRIPTABLE Q_NOREPLY void start()
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
char * toString(const EngineQuery &query)
QAction * hint(const QObject *recvr, const char *slot, QObject *parent)
KIOCORE_EXPORT TransferJob * get(const QUrl &url, LoadType reload=NoReload, JobFlags flags=DefaultFlags)
const QList< QKeySequence > & end()
void columnsAboutToBeInserted(const QModelIndex &parent, int first, int last)
void columnsAboutToBeMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destinationParent, int destinationColumn)
void columnsAboutToBeRemoved(const QModelIndex &parent, int first, int last)
void columnsInserted(const QModelIndex &parent, int first, int last)
void columnsMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destinationParent, int destinationColumn)
void columnsRemoved(const QModelIndex &parent, int first, int last)
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList< int > &roles)
void headerDataChanged(Qt::Orientation orientation, int first, int last)
void layoutAboutToBeChanged(const QList< QPersistentModelIndex > &parents, QAbstractItemModel::LayoutChangeHint hint)
void layoutChanged(const QList< QPersistentModelIndex > &parents, QAbstractItemModel::LayoutChangeHint hint)
void modelAboutToBeReset()
void rowsAboutToBeInserted(const QModelIndex &parent, int start, int end)
void rowsAboutToBeMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destinationParent, int destinationRow)
void rowsAboutToBeRemoved(const QModelIndex &parent, int first, int last)
void rowsInserted(const QModelIndex &parent, int first, int last)
void rowsMoved(const QModelIndex &sourceParent, int sourceStart, int sourceEnd, const QModelIndex &destinationParent, int destinationRow)
void rowsRemoved(const QModelIndex &parent, int first, int last)
virtual QVariant data(const QModelIndex &proxyIndex, int role) const const override
virtual void setSourceModel(QAbstractItemModel *sourceModel)
const char * constData() const const
void setCurrentIndex(int index)
void currentIndexChanged(int index)
int findData(const QVariant &data, int role, Qt::MatchFlags flags) const const
bool contains(const AT &value) const const
bool isEmpty() const const
T value(const Key &key, const T &defaultValue) const const
int column() const const
void * internalPointer() const const
bool isValid() const const
int row() const const
Q_EMITQ_EMIT
bool blockSignals(bool block)
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool isValid() const const
virtual QVariant data(const QModelIndex &index, int role) const const override
QChar * data()
QString fromLatin1(QByteArrayView str)
QString fromStdString(const std::string &str)
QString fromUtf8(QByteArrayView str)
bool isEmpty() const const
int localeAwareCompare(QStringView s1, QStringView s2)
QByteArray toLatin1() const const
DisplayRole
typedef ItemFlags
MatchExactly
Orientation
QFuture< void > filter(QThreadPool *pool, Sequence &sequence, KeepFunctor &&filterFunction)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
bool isValid() const const
T value() const const
bool isEnabled() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Oct 11 2024 12:11:57 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.