diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index df59971b2..7097654a2 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -1,42 +1,43 @@ remove_definitions(-DQT_NO_CAST_FROM_ASCII) include(ECMAddTests) find_package(Qt5Test ${REQUIRED_QT_VERSION} CONFIG QUIET) if(NOT Qt5Test_FOUND) message(STATUS "Qt5Test not found, autotests will not be built.") return() endif() ecm_add_test( flatkeylistmodeltest.cpp abstractkeylistmodeltest.cpp TEST_NAME flatkeylistmodeltest LINK_LIBRARIES KF5::Libkleo Qt::Test ) ecm_add_test( hierarchicalkeylistmodeltest.cpp abstractkeylistmodeltest.cpp TEST_NAME hierarchicalkeylistmodeltest LINK_LIBRARIES KF5::Libkleo Qt::Test ) ecm_add_test( keyresolvercoretest.cpp keyresolvercoretest.qrc TEST_NAME keyresolvercoretest LINK_LIBRARIES KF5::Libkleo Qt::Test ) ecm_add_tests( editdirectoryservicedialogtest.cpp LINK_LIBRARIES KF5::Libkleo KF5::WidgetsAddons Qt::Widgets Qt::Test ) ecm_add_tests( + keyselectioncombotest.cpp keyserverconfigtest.cpp newkeyapprovaldialogtest.cpp LINK_LIBRARIES KF5::Libkleo Qt::Widgets Qt::Test ) diff --git a/autotests/keyselectioncombotest.cpp b/autotests/keyselectioncombotest.cpp new file mode 100644 index 000000000..2cf85cfa6 --- /dev/null +++ b/autotests/keyselectioncombotest.cpp @@ -0,0 +1,208 @@ +/* + autotests/keyselectioncombotest.cpp + + This file is part of libkleopatra's test suite. + SPDX-FileCopyrightText: 2021 g10 Code GmbH + SPDX-FileContributor: Ingo Klöcker + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include +#include +#include + +#include +#include + +#include +#include + +#include + +#include + +using namespace Kleo; + +namespace +{ + +auto mapValidity(GpgME::UserID::Validity validity) +{ + switch (validity) { + default: + case GpgME::UserID::Unknown: return GPGME_VALIDITY_UNKNOWN; + case GpgME::UserID::Undefined: return GPGME_VALIDITY_UNDEFINED; + case GpgME::UserID::Never: return GPGME_VALIDITY_NEVER; + case GpgME::UserID::Marginal: return GPGME_VALIDITY_MARGINAL; + case GpgME::UserID::Full: return GPGME_VALIDITY_FULL; + case GpgME::UserID::Ultimate: return GPGME_VALIDITY_ULTIMATE; + } +} + +GpgME::Key createTestKey(const char *uid, GpgME::Protocol protocol = GpgME::UnknownProtocol, KeyUsage usage = KeyUsage::AnyUsage, + GpgME::UserID::Validity validity = GpgME::UserID::Full) +{ + static int count = 0; + count++; + + gpgme_key_t key; + gpgme_key_from_uid(&key, uid); + Q_ASSERT(key); + Q_ASSERT(key->uids); + if (protocol != GpgME::UnknownProtocol) { + key->protocol = protocol == GpgME::OpenPGP ? GPGME_PROTOCOL_OpenPGP : GPGME_PROTOCOL_CMS; + } + const QByteArray fingerprint = QByteArray::number(count, 16).rightJustified(40, '0'); + key->fpr = strdup(fingerprint.constData()); + key->revoked = 0; + key->expired = 0; + key->disabled = 0; + key->can_encrypt = int(usage == KeyUsage::AnyUsage || usage == KeyUsage::Encrypt); + key->can_sign = int(usage == KeyUsage::AnyUsage || usage == KeyUsage::Sign); + key->secret = 1; + key->uids->validity = mapValidity(validity); + + return GpgME::Key(key, false); +} + +auto testKey(const char *address, GpgME::Protocol protocol = GpgME::UnknownProtocol) +{ + const auto email = GpgME::UserID::addrSpecFromString(address); + const auto keys = KeyCache::instance()->findByEMailAddress(email); + for (const auto &key: keys) { + if (protocol == GpgME::UnknownProtocol || key.protocol() == protocol) { + return key; + } + } + return GpgME::Key(); +} + +void waitForKeySelectionComboBeingInitialized(const KeySelectionCombo *combo) +{ + QVERIFY(combo); + + const auto spy = std::make_unique(combo, &KeySelectionCombo::keyListingFinished); + QVERIFY(spy->isValid()); + QVERIFY(spy->wait(10)); +} + +} + +class KeySelectionComboTest: public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void init() + { + // hold a reference to the key cache to avoid rebuilding while the test is running + mKeyCache = KeyCache::instance(); + + KeyCache::mutableInstance()->setKeys({ + createTestKey("sender@example.net", GpgME::OpenPGP, KeyUsage::AnyUsage), + createTestKey("sender@example.net", GpgME::CMS, KeyUsage::AnyUsage), + createTestKey("Full Trust ", GpgME::OpenPGP, KeyUsage::Encrypt), + createTestKey("Trusted S/MIME ", GpgME::CMS, KeyUsage::Encrypt), + createTestKey("Marginal Validity ", GpgME::OpenPGP, KeyUsage::Encrypt, GpgME::UserID::Marginal), + }); + } + + void cleanup() + { + // verify that nobody else holds a reference to the key cache + QVERIFY(mKeyCache.use_count() == 1); + mKeyCache.reset(); + } + + void test__verify_test_keys() + { + QVERIFY(!testKey("sender@example.net", GpgME::OpenPGP).isNull()); + QVERIFY(!testKey("sender@example.net", GpgME::CMS).isNull()); + QVERIFY(!testKey("Full Trust ", GpgME::OpenPGP).isNull()); + QVERIFY(!testKey("Trusted S/MIME ", GpgME::CMS).isNull()); + QVERIFY(!testKey("Marginal Validity ", GpgME::OpenPGP).isNull()); + } + + void test__after_initialization_default_key_is_current_key() + { + const auto combo = std::make_unique(); + combo->setDefaultKey(QString::fromLatin1(testKey("Full Trust ", GpgME::OpenPGP).primaryFingerprint())); + waitForKeySelectionComboBeingInitialized(combo.get()); + + QCOMPARE(combo->currentKey().primaryFingerprint(), testKey("Full Trust ", GpgME::OpenPGP).primaryFingerprint()); + } + + void test__currently_selected_key_is_retained_if_cache_is_updated() + { + const auto combo = std::make_unique(); + combo->setDefaultKey(QString::fromLatin1(testKey("Full Trust ", GpgME::OpenPGP).primaryFingerprint())); + waitForKeySelectionComboBeingInitialized(combo.get()); + + combo->setCurrentIndex(3); + + QCOMPARE(combo->currentKey().primaryFingerprint(), testKey("Trusted S/MIME ", GpgME::CMS).primaryFingerprint()); + + Q_EMIT KeyCache::mutableInstance()->keyListingDone(GpgME::KeyListResult{}); + + QCOMPARE(combo->currentKey().primaryFingerprint(), testKey("Trusted S/MIME ", GpgME::CMS).primaryFingerprint()); + } + + void test__default_key_is_selected_if_currently_selected_key_is_gone_after_model_update() + { + const auto combo = std::make_unique(); + combo->setDefaultKey(QString::fromLatin1(testKey("Full Trust ", GpgME::OpenPGP).primaryFingerprint())); + waitForKeySelectionComboBeingInitialized(combo.get()); + + combo->setCurrentIndex(3); + + QCOMPARE(combo->currentKey().primaryFingerprint(), testKey("Trusted S/MIME ", GpgME::CMS).primaryFingerprint()); + + KeyCache::mutableInstance()->setKeys({ + testKey("sender@example.net", GpgME::OpenPGP), + testKey("sender@example.net", GpgME::CMS), + testKey("Full Trust ", GpgME::OpenPGP), + testKey("Marginal Validity ", GpgME::OpenPGP), + }); + + QCOMPARE(combo->currentKey().primaryFingerprint(), testKey("Full Trust ", GpgME::OpenPGP).primaryFingerprint()); + } + + void test__currently_selected_custom_item_is_retained_if_cache_is_updated() + { + const auto combo = std::make_unique(); + combo->prependCustomItem({}, {}, QStringLiteral("custom1")); + combo->appendCustomItem({}, {}, QStringLiteral("custom2")); + combo->setDefaultKey(QString::fromLatin1(testKey("Full Trust ", GpgME::OpenPGP).primaryFingerprint())); + waitForKeySelectionComboBeingInitialized(combo.get()); + + combo->setCurrentIndex(combo->count() - 1); + QCOMPARE(combo->currentData(), QStringLiteral("custom2")); + + Q_EMIT KeyCache::mutableInstance()->keyListingDone(GpgME::KeyListResult{}); + + QCOMPARE(combo->currentData(), QStringLiteral("custom2")); + } + + void test__default_key_is_selected_if_currently_selected_custom_item_is_gone_after_model_update() + { + const auto combo = std::make_unique(); + combo->prependCustomItem({}, {}, QStringLiteral("custom1")); + combo->appendCustomItem({}, {}, QStringLiteral("custom2")); + combo->setDefaultKey(QString::fromLatin1(testKey("Full Trust ", GpgME::OpenPGP).primaryFingerprint())); + waitForKeySelectionComboBeingInitialized(combo.get()); + + combo->setCurrentIndex(combo->count() - 1); + QCOMPARE(combo->currentData(), QStringLiteral("custom2")); + + combo->removeCustomItem(QStringLiteral("custom2")); + + QCOMPARE(combo->currentKey().primaryFingerprint(), testKey("Full Trust ", GpgME::OpenPGP).primaryFingerprint()); + } + +private: + std::shared_ptr mKeyCache; +}; + +QTEST_MAIN(KeySelectionComboTest) +#include "keyselectioncombotest.moc" diff --git a/src/ui/keyselectioncombo.cpp b/src/ui/keyselectioncombo.cpp index c4ef2925e..f60f4d498 100644 --- a/src/ui/keyselectioncombo.cpp +++ b/src/ui/keyselectioncombo.cpp @@ -1,588 +1,634 @@ /* This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2016 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include "keyselectioncombo.h" #include #include "kleo/dn.h" #include "models/keylist.h" #include "models/keylistmodel.h" #include "models/keylistsortfilterproxymodel.h" #include "models/keycache.h" #include "utils/formatting.h" #include "progressbar.h" #include "kleo/defaultkeyfilter.h" #include #include #include #include #include using namespace Kleo; Q_DECLARE_METATYPE(GpgME::Key) namespace { class SortFilterProxyModel : public KeyListSortFilterProxyModel { Q_OBJECT public: using KeyListSortFilterProxyModel::KeyListSortFilterProxyModel; void setAlwaysAcceptedKey(const QString &fingerprint) { if (fingerprint == mFingerprint) { return; } mFingerprint = fingerprint; invalidate(); } protected: bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override { if (!mFingerprint.isEmpty()) { const QModelIndex index = sourceModel()->index(source_row, 0, source_parent); const auto fingerprint = sourceModel()->data(index, KeyList::FingerprintRole).toString(); if (fingerprint == mFingerprint) { return true; } } return KeyListSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); } private: QString mFingerprint; }; class ProxyModel : public QSortFilterProxyModel { Q_OBJECT private: struct CustomItem { QIcon icon; QString text; QVariant data; QString toolTip; }; public: ProxyModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent) { } ~ProxyModel() override { qDeleteAll(mFrontItems); qDeleteAll(mBackItems); } bool lessThan(const QModelIndex &left, const QModelIndex &right) const override { const auto leftKey = sourceModel()->data(left, KeyList::KeyRole).value(); const auto rightKey = sourceModel()->data(right, KeyList::KeyRole).value(); if (leftKey.isNull()) { return false; } if (rightKey.isNull()) { return true; } // As we display UID(0) this is ok. We probably need a get Best UID at some point. const auto lUid = leftKey.userID(0); const auto rUid = rightKey.userID(0); if (lUid.isNull()) { return false; } if (rUid.isNull()) { return true; } int cmp = strcmp (lUid.id(), rUid.id()); if (cmp) { return cmp < 0; } if (lUid.validity() == rUid.validity()) { /* Both are the same check which one is newer. */ time_t oldTime = 0; for (const GpgME::Subkey &s: leftKey.subkeys()) { if (s.isRevoked() || s.isInvalid() || s.isDisabled()) { continue; } if (s.creationTime() > oldTime) { oldTime= s.creationTime(); } } time_t newTime = 0; for (const GpgME::Subkey &s: rightKey.subkeys()) { if (s.isRevoked() || s.isInvalid() || s.isDisabled()) { continue; } if (s.creationTime() > newTime) { newTime = s.creationTime(); } } return newTime < oldTime; } return lUid.validity() > rUid.validity(); } bool isCustomItem(const int row) const { return row < mFrontItems.count() || row >= mFrontItems.count() + QSortFilterProxyModel::rowCount(); } void prependItem(const QIcon &icon, const QString &text, const QVariant &data, const QString &toolTip) { beginInsertRows(QModelIndex(), 0, 0); mFrontItems.push_front(new CustomItem{ icon, text, data, toolTip }); endInsertRows(); } void appendItem(const QIcon &icon, const QString &text, const QVariant &data, const QString &toolTip) { beginInsertRows(QModelIndex(), rowCount(), rowCount()); mBackItems.push_back(new CustomItem{ icon, text, data, toolTip }); endInsertRows(); } void removeCustomItem(const QVariant &data) { for (int i = 0; i < mFrontItems.count(); ++i) { if (mFrontItems[i]->data == data) { beginRemoveRows(QModelIndex(), i, i); delete mFrontItems.takeAt(i); endRemoveRows(); return; } } for (int i = 0; i < mBackItems.count(); ++i) { if (mBackItems[i]->data == data) { const int index = mFrontItems.count() + QSortFilterProxyModel::rowCount() + i; beginRemoveRows(QModelIndex(), index, index); delete mBackItems.takeAt(i); endRemoveRows(); return; } } } int rowCount(const QModelIndex &parent = QModelIndex()) const override { return mFrontItems.count() + QSortFilterProxyModel::rowCount(parent) + mBackItems.count(); } QModelIndex mapToSource(const QModelIndex &index) const override { if (!isCustomItem(index.row())) { const int row = index.row() - mFrontItems.count(); return sourceModel()->index(row, index.column()); } else { return {}; } } QModelIndex mapFromSource(const QModelIndex &source_index) const override { const QModelIndex idx = QSortFilterProxyModel::mapFromSource(source_index); return createIndex(mFrontItems.count() + idx.row(), idx.column(), idx.internalPointer()); } QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override { if (row < 0 || row >= rowCount()) { return {}; } if (row < mFrontItems.count()) { return createIndex(row, column, mFrontItems[row]); } else if (row >= mFrontItems.count() + QSortFilterProxyModel::rowCount()) { return createIndex(row, column, mBackItems[row - mFrontItems.count() - QSortFilterProxyModel::rowCount()]); } else { const QModelIndex mi = QSortFilterProxyModel::index(row - mFrontItems.count(), column, parent); return createIndex(row, column, mi.internalPointer()); } } Qt::ItemFlags flags(const QModelIndex &index) const override { Q_UNUSED(index) return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemNeverHasChildren; } QModelIndex parent(const QModelIndex &) const override { // Flat list return {}; } QVariant data(const QModelIndex &index, int role) const override { if (!index.isValid()) { return QVariant(); } if (isCustomItem(index.row())) { Q_ASSERT(!mFrontItems.isEmpty() || !mBackItems.isEmpty()); auto ci = static_cast(index.internalPointer()); switch (role) { case Qt::DisplayRole: return ci->text; case Qt::DecorationRole: return ci->icon; case Qt::UserRole: return ci->data; case Qt::ToolTipRole: return ci->toolTip; default: return QVariant(); } } const auto key = QSortFilterProxyModel::data(index, KeyList::KeyRole).value(); Q_ASSERT(!key.isNull()); if (key.isNull()) { return QVariant(); } switch (role) { case Qt::DisplayRole: { const auto userID = key.userID(0); QString name, email; if (key.protocol() == GpgME::OpenPGP) { name = QString::fromUtf8(userID.name()); email = QString::fromUtf8(userID.email()); } else { const Kleo::DN dn(userID.id()); name = dn[QStringLiteral("CN")]; email = dn[QStringLiteral("EMAIL")]; } return i18nc("Name (validity, type, created: date)", "%1 (%2, %3 created: %4)", email.isEmpty() ? name : name.isEmpty() ? email : i18nc("Name ", "%1 <%2>", name, email), Kleo::Formatting::complianceStringShort(key), Kleo::KeyCache::instance()->pgpOnly() ? QString() : key.protocol() == GpgME::OpenPGP ? i18n("OpenPGP") + QLatin1Char(',') : i18n("S/MIME") + QLatin1Char(','), Kleo::Formatting::creationDateString(key)); } case Qt::ToolTipRole: return Kleo::Formatting::toolTip(key, Kleo::Formatting::Validity | Kleo::Formatting::Issuer | Kleo::Formatting::Subject | Kleo::Formatting::Fingerprint | Kleo::Formatting::ExpiryDates | Kleo::Formatting::UserIDs); case Qt::DecorationRole: return Kleo::Formatting::iconForUid(key.userID(0)); default: return QSortFilterProxyModel::data(index, role); } } private: QVector mFrontItems; QVector mBackItems; }; } // anonymous namespace namespace Kleo { class KeySelectionComboPrivate { public: KeySelectionComboPrivate(KeySelectionCombo *parent) : wasEnabled(true) , q(parent) { } /* Selects the first key with a UID addrSpec that matches * the mPerfectMatchMbox variable. * * The idea here is that if there are keys like: * * tom-store@abc.com * susi-store@abc.com * store@abc.com * * And the user wants to send a mail to "store@abc.com" * the filter should still show tom and susi (because they * both are part of store) but the key for "store" should * be preselected. * * Returns true if one was selected. False otherwise. */ bool selectPerfectIdMatch() const { if (mPerfectMatchMbox.isEmpty()) { return false; } for (int i = 0; i < proxyModel->rowCount(); ++i) { const auto idx = proxyModel->index(i, 0, QModelIndex()); const auto key = proxyModel->data(idx, KeyList::KeyRole).value(); if (key.isNull()) { // WTF? continue; } for (const auto &uid: key.userIDs()) { if (QString::fromStdString(uid.addrSpec()) == mPerfectMatchMbox) { q->setCurrentIndex(i); return true; } } } return false; } /* Updates the current key with the default key if the key matches * the current key filter. */ void updateWithDefaultKey() { GpgME::Protocol filterProto = GpgME::UnknownProtocol; const auto filter = dynamic_cast (sortFilterProxy->keyFilter().get()); if (filter && filter->isOpenPGP() == DefaultKeyFilter::Set) { filterProto = GpgME::OpenPGP; } else if (filter && filter->isOpenPGP() == DefaultKeyFilter::NotSet) { filterProto = GpgME::CMS; } QString defaultKey = defaultKeys.value (filterProto); if (defaultKey.isEmpty()) { // Fallback to unknown protocol defaultKey = defaultKeys.value (GpgME::UnknownProtocol); } // make sure that the default key is not filtered out unless it has the wrong protocol if (filterProto == GpgME::UnknownProtocol) { sortFilterProxy->setAlwaysAcceptedKey(defaultKey); } else { const auto key = KeyCache::instance()->findByFingerprint(defaultKey.toLatin1().constData()); if (!key.isNull() && key.protocol() == filterProto) { sortFilterProxy->setAlwaysAcceptedKey(defaultKey); } else { sortFilterProxy->setAlwaysAcceptedKey({}); } } q->setCurrentKey(defaultKey); } + void storeCurrentSelectionBeforeModelChange() + { + keyBeforeModelChange = q->currentKey(); + customItemBeforeModelChange = q->currentData(); + } + + void restoreCurrentSelectionAfterModelChange() + { + if (!keyBeforeModelChange.isNull()) { + q->setCurrentKey(keyBeforeModelChange); + } else if (customItemBeforeModelChange.isValid()) { + const auto index = q->findData(customItemBeforeModelChange); + if (index != -1) { + q->setCurrentIndex(index); + } else { + updateWithDefaultKey(); + } + } + } + Kleo::AbstractKeyListModel *model = nullptr; SortFilterProxyModel *sortFilterProxy = nullptr; ProxyModel *proxyModel = nullptr; std::shared_ptr cache; QMap defaultKeys; bool wasEnabled = false; bool useWasEnabled = false; - bool secretOnly; + bool secretOnly = false; + bool initialKeyListingDone = false; QString mPerfectMatchMbox; + GpgME::Key keyBeforeModelChange; + QVariant customItemBeforeModelChange; private: KeySelectionCombo * const q; }; } using namespace Kleo; KeySelectionCombo::KeySelectionCombo(QWidget* parent) : KeySelectionCombo(true, parent) {} KeySelectionCombo::KeySelectionCombo(bool secretOnly, QWidget* parent) : QComboBox(parent) , d(new KeySelectionComboPrivate(this)) { d->model = Kleo::AbstractKeyListModel::createFlatKeyListModel(this); d->secretOnly = secretOnly; d->sortFilterProxy = new SortFilterProxyModel(this); d->sortFilterProxy->setSourceModel(d->model); d->proxyModel = new ProxyModel(this); d->proxyModel->setSourceModel(d->sortFilterProxy); setModel(d->proxyModel); connect(this, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int row) { if (row >= 0 && row < d->proxyModel->rowCount()) { if (d->proxyModel->isCustomItem(row)) { - Q_EMIT customItemSelected(d->proxyModel->index(row, 0).data(Qt::UserRole)); + Q_EMIT customItemSelected(currentData(Qt::UserRole)); } else { Q_EMIT currentKeyChanged(currentKey()); } } }); d->cache = Kleo::KeyCache::mutableInstance(); + connect(model(), &QAbstractItemModel::rowsAboutToBeInserted, + this, [this] () { d->storeCurrentSelectionBeforeModelChange(); }); + connect(model(), &QAbstractItemModel::rowsInserted, + this, [this] () { d->restoreCurrentSelectionAfterModelChange(); }); + connect(model(), &QAbstractItemModel::rowsAboutToBeRemoved, + this, [this] () { d->storeCurrentSelectionBeforeModelChange(); }); + connect(model(), &QAbstractItemModel::rowsRemoved, + this, [this] () { d->restoreCurrentSelectionAfterModelChange(); }); + connect(model(), &QAbstractItemModel::modelAboutToBeReset, + this, [this] () { d->storeCurrentSelectionBeforeModelChange(); }); + connect(model(), &QAbstractItemModel::modelReset, + this, [this] () { d->restoreCurrentSelectionAfterModelChange(); }); + QTimer::singleShot(0, this, &KeySelectionCombo::init); } KeySelectionCombo::~KeySelectionCombo() { delete d; } void KeySelectionCombo::init() { connect(d->cache.get(), &Kleo::KeyCache::keyListingDone, this, [this]() { // Set useKeyCache ensures that the cache is populated // so this can be a blocking call if the cache is not initialized - d->model->useKeyCache(true, d->secretOnly ? KeyList::SecretKeysOnly : KeyList::AllKeys); - d->proxyModel->removeCustomItem(QStringLiteral("-libkleo-loading-keys")); + if (!d->initialKeyListingDone) { + d->model->useKeyCache(true, d->secretOnly ? KeyList::SecretKeysOnly : KeyList::AllKeys); + d->proxyModel->removeCustomItem(QStringLiteral("-libkleo-loading-keys")); + } // We use the useWasEnabled state variable to decide if we should // change the enable / disable state based on the keylist done signal. // If we triggered the refresh useWasEnabled is true and we want to // enable / disable again after our refresh, as the refresh disabled it. // // But if a keyListingDone signal comes from just a generic refresh // triggered by someone else we don't want to change the enable / disable // state. if (d->useWasEnabled) { setEnabled(d->wasEnabled); d->useWasEnabled = false; } Q_EMIT keyListingFinished(); }); connect(this, &KeySelectionCombo::keyListingFinished, this, [this]() { - d->updateWithDefaultKey(); + if (!d->initialKeyListingDone) { + d->updateWithDefaultKey(); + d->initialKeyListingDone = true; + } }); if (!d->cache->initialized()) { refreshKeys(); } else { d->model->useKeyCache(true, d->secretOnly ? KeyList::SecretKeysOnly : KeyList::AllKeys); Q_EMIT keyListingFinished(); } connect(this, QOverload::of(&QComboBox::currentIndexChanged), this, [this] () { setToolTip(currentData(Qt::ToolTipRole).toString()); }); } void KeySelectionCombo::setKeyFilter(const std::shared_ptr &kf) { d->sortFilterProxy->setKeyFilter(kf); d->proxyModel->sort(0); d->updateWithDefaultKey(); } std::shared_ptr KeySelectionCombo::keyFilter() const { return d->sortFilterProxy->keyFilter(); } void KeySelectionCombo::setIdFilter(const QString &id) { d->sortFilterProxy->setFilterRegExp(id); d->mPerfectMatchMbox = id; d->updateWithDefaultKey(); } QString KeySelectionCombo::idFilter() const { return d->sortFilterProxy->filterRegExp().pattern(); } GpgME::Key Kleo::KeySelectionCombo::currentKey() const { return currentData(KeyList::KeyRole).value(); } void Kleo::KeySelectionCombo::setCurrentKey(const GpgME::Key &key) { - const int idx = findData(QVariant::fromValue(key), KeyList::KeyRole, Qt::MatchExactly); + const int idx = findData(QString::fromLatin1(key.primaryFingerprint()), KeyList::FingerprintRole, Qt::MatchExactly); if (idx > -1) { setCurrentIndex(idx); - } else { - d->selectPerfectIdMatch(); + } else if (!d->selectPerfectIdMatch()) { + d->updateWithDefaultKey(); } setToolTip(currentData(Qt::ToolTipRole).toString()); } void Kleo::KeySelectionCombo::setCurrentKey(const QString &fingerprint) { const auto cur = currentKey(); if (!cur.isNull() && !fingerprint.isEmpty() && fingerprint == QLatin1String(cur.primaryFingerprint())) { // already set; still emit a changed signal because the current key may // have become the item at the current index by changes in the underlying model Q_EMIT currentKeyChanged(cur); return; } const int idx = findData(fingerprint, KeyList::FingerprintRole, Qt::MatchExactly); if (idx > -1) { setCurrentIndex(idx); } else if (!d->selectPerfectIdMatch()) { setCurrentIndex(0); } setToolTip(currentData(Qt::ToolTipRole).toString()); } void KeySelectionCombo::refreshKeys() { d->wasEnabled = isEnabled(); d->useWasEnabled = true; setEnabled(false); const bool wasBlocked = blockSignals(true); prependCustomItem(QIcon(), i18n("Loading keys ..."), QStringLiteral("-libkleo-loading-keys")); setCurrentIndex(0); blockSignals(wasBlocked); d->cache->startKeyListing(); } void KeySelectionCombo::appendCustomItem(const QIcon &icon, const QString &text, const QVariant &data, const QString &toolTip) { d->proxyModel->appendItem(icon, text, data, toolTip); } void KeySelectionCombo::appendCustomItem(const QIcon &icon, const QString &text, const QVariant &data) { appendCustomItem(icon, text, data, QString()); } void KeySelectionCombo::prependCustomItem(const QIcon &icon, const QString &text, const QVariant &data, const QString &toolTip) { d->proxyModel->prependItem(icon, text, data, toolTip); } void KeySelectionCombo::prependCustomItem(const QIcon &icon, const QString &text, const QVariant &data) { prependCustomItem(icon, text, data, QString()); } +void KeySelectionCombo::removeCustomItem(const QVariant &data) +{ + d->proxyModel->removeCustomItem(data); +} + void Kleo::KeySelectionCombo::setDefaultKey(const QString &fingerprint, GpgME::Protocol proto) { d->defaultKeys.insert(proto, fingerprint); d->updateWithDefaultKey(); } void Kleo::KeySelectionCombo::setDefaultKey(const QString &fingerprint) { setDefaultKey(fingerprint, GpgME::UnknownProtocol); } QString Kleo::KeySelectionCombo::defaultKey(GpgME::Protocol proto) const { return d->defaultKeys.value(proto); } QString Kleo::KeySelectionCombo::defaultKey() const { return defaultKey(GpgME::UnknownProtocol); } #include "keyselectioncombo.moc" diff --git a/src/ui/keyselectioncombo.h b/src/ui/keyselectioncombo.h index 43b4410f0..3b5746e16 100644 --- a/src/ui/keyselectioncombo.h +++ b/src/ui/keyselectioncombo.h @@ -1,71 +1,72 @@ /* This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2016 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include #include #include #include #include namespace GpgME { class Key; } namespace Kleo { class KeyFilter; class KeySelectionComboPrivate; class KLEO_EXPORT KeySelectionCombo : public QComboBox { Q_OBJECT public: explicit KeySelectionCombo(QWidget *parent = nullptr); explicit KeySelectionCombo(bool secretOnly, QWidget *parent = nullptr); ~KeySelectionCombo() override; void setKeyFilter(const std::shared_ptr &kf); std::shared_ptr keyFilter() const; void setIdFilter(const QString &id); QString idFilter() const; void refreshKeys(); GpgME::Key currentKey() const; void setCurrentKey(const GpgME::Key &key); void setCurrentKey(const QString &fingerprint); void setDefaultKey(const QString &fingerprint); void setDefaultKey(const QString &fingerprint, GpgME::Protocol proto); QString defaultKey() const; QString defaultKey(GpgME::Protocol proto) const; void prependCustomItem(const QIcon &icon, const QString &text, const QVariant &data); void appendCustomItem(const QIcon &icon, const QString &text, const QVariant &data); void prependCustomItem(const QIcon &icon, const QString &text, const QVariant &data, const QString &toolTip); void appendCustomItem(const QIcon &icon, const QString &text, const QVariant &data, const QString &toolTip); + void removeCustomItem(const QVariant &data); Q_SIGNALS: void customItemSelected(const QVariant &data); void currentKeyChanged(const GpgME::Key &key); void keyListingFinished(); protected: virtual void init(); private: KeySelectionComboPrivate * const d; }; }