diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 470adedd2..e45307223 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,227 +1,228 @@ # target_include_directories does not handle empty include paths include_directories(${GPGME_INCLUDES}) add_definitions(-DTRANSLATION_DOMAIN=\"libkleopatra\") #add_definitions( -DQT_NO_CAST_FROM_ASCII ) #add_definitions( -DQT_NO_CAST_TO_ASCII ) kde_enable_exceptions() add_definitions( -DGPGMEPP_ERR_SOURCE_DEFAULT=13 ) # 13 is GPG_ERR_SOURCE_KLEO, even if gpg-error's too old to know about add_subdirectory( pics ) if (BUILD_TESTING) add_subdirectory( tests ) endif() ########### next target ############### set(libkleo_core_SRCS kleo/checksumdefinition.cpp kleo/defaultkeyfilter.cpp kleo/defaultkeygenerationjob.cpp kleo/dn.cpp kleo/enum.cpp kleo/kconfigbasedkeyfilter.cpp kleo/keyfiltermanager.cpp kleo/keyresolver.cpp kleo/kleoexception.cpp kleo/oidmap.cpp models/keycache.cpp models/keylistmodel.cpp models/keylistmodelinterface.cpp models/keylistsortfilterproxymodel.cpp models/keyrearrangecolumnsproxymodel.cpp models/subkeylistmodel.cpp models/useridlistmodel.cpp utils/filesystemwatcher.cpp utils/formatting.cpp utils/classify.cpp utils/gnupg.cpp utils/gnupg-registry.c utils/hex.cpp ) ecm_qt_declare_logging_category(libkleo_core_SRCS HEADER libkleo_debug.h IDENTIFIER LIBKLEO_LOG CATEGORY_NAME org.kde.pim.libkleo DESCRIPTION "libkleo (kleo_core)" EXPORT LIBKLEO ) set(libkleo_ui_common_SRCS ui/dnattributeorderconfigwidget.cpp ui/kdhorizontalline.cpp ui/filenamerequester.cpp ui/messagebox.cpp ui/cryptoconfigmodule.cpp ui/cryptoconfigdialog.cpp ui/directoryserviceswidget.cpp ui/progressbar.cpp ui/progressdialog.cpp ui/auditlogviewer.cpp ) ecm_qt_declare_logging_category(libkleo_ui_common_SRCS HEADER kleo_ui_debug.h IDENTIFIER KLEO_UI_LOG CATEGORY_NAME org.kde.pim.kleo_ui DESCRIPTION "libkleo (kleo_ui)" OLD_CATEGORY_NAMES log_kleo_ui EXPORT LIBKLEO ) set(libkleo_ui_SRCS # make this a separate lib. ui/keylistview.cpp ui/keyselectiondialog.cpp ui/keyrequester.cpp ui/keyapprovaldialog.cpp ui/newkeyapprovaldialog.cpp ui/keyselectioncombo.cpp ) ki18n_wrap_ui(libkleo_ui_common_SRCS ui/directoryserviceswidget.ui ) set(kleo_LIB_SRCS ${libkleo_core_SRCS} ${libkleo_ui_SRCS} ${libkleo_ui_common_SRCS}) set(kleo_LIB_LIBS PUBLIC QGpgme Gpgmepp PRIVATE Qt5::Widgets KF5::I18n KF5::Completion KF5::ConfigCore KF5::CoreAddons KF5::WidgetsAddons KF5::ItemModels KF5::Codecs) if (KF5PimTextEdit_FOUND) add_definitions(-DHAVE_PIMTEXTEDIT) set(kleo_LIB_LIBS ${kleo_LIB_LIBS} PRIVATE KF5::PimTextEdit) endif() add_library(KF5Libkleo ${kleo_LIB_SRCS}) if (COMPILE_WITH_UNITY_CMAKE_SUPPORT) set_target_properties(KF5Libkleo PROPERTIES UNITY_BUILD ON) endif() generate_export_header(KF5Libkleo BASE_NAME kleo) add_library(KF5::Libkleo ALIAS KF5Libkleo) if(WIN32) target_link_libraries(KF5Libkleo ${kleo_LIB_LIBS} ${GPGME_VANILLA_LIBRARIES} ) else() target_link_libraries(KF5Libkleo ${kleo_LIB_LIBS} ) endif() set_target_properties(KF5Libkleo PROPERTIES VERSION ${LIBKLEO_VERSION_STRING} SOVERSION ${LIBKLEO_SOVERSION} EXPORT_NAME Libkleo ) install(TARGETS KF5Libkleo EXPORT KF5LibkleoTargets ${KF5_INSTALL_TARGETS_DEFAULT_ARGS} ) target_include_directories(KF5Libkleo PUBLIC "$") target_include_directories(KF5Libkleo INTERFACE "$") ecm_generate_headers(libkleo_CamelCase_HEADERS HEADER_NAMES ChecksumDefinition DefaultKeyFilter DefaultKeyGenerationJob Dn Enum KConfigBasedKeyFilter KeyFilter KeyFilterManager KeyResolver KleoException OidMap Predicates Stl_Util REQUIRED_HEADERS libkleo_HEADERS PREFIX Libkleo RELATIVE kleo ) ecm_generate_headers(libkleo_CamelCase_models_HEADERS HEADER_NAMES KeyCache + KeyList KeyListModel KeyListModelInterface KeyListSortFilterProxyModel KeyRearrangeColumnsProxyModel SubkeyListModel UserIDListModel REQUIRED_HEADERS libkleo_models_HEADERS PREFIX Libkleo RELATIVE models ) ecm_generate_headers(libkleo_CamelCase_utils_HEADERS HEADER_NAMES Classify FileSystemWatcher Formatting GnuPG REQUIRED_HEADERS libkleo_utils_HEADERS PREFIX Libkleo RELATIVE utils ) ecm_generate_headers(libkleo_CamelCase_ui_HEADERS HEADER_NAMES CryptoConfigDialog CryptoConfigModule DNAttributeOrderConfigWidget DirectoryServicesWidget FileNameRequester KDHorizontalLine KeyApprovalDialog NewKeyApprovalDialog KeyRequester KeySelectionCombo KeySelectionDialog MessageBox ProgressDialog REQUIRED_HEADERS libkleo_ui_HEADERS PREFIX Libkleo RELATIVE ui ) ecm_generate_pri_file(BASE_NAME Libkleo LIB_NAME KF5Libkleo DEPS "QGpgme" FILENAME_VAR PRI_FILENAME INCLUDE_INSTALL_DIR ${KDE_INSTALL_INCLUDEDIR_KF5}/Libkleo ) install(FILES ${libkleo_CamelCase_HEADERS} ${libkleo_CamelCase_models_HEADERS} ${libkleo_CamelCase_ui_HEADERS} ${libkleo_CamelCase_utils_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/Libkleo COMPONENT Devel ) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/kleo_export.h ${libkleo_HEADERS} ${libkleo_models_HEADERS} ${libkleo_ui_HEADERS} ${libkleo_utils_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/libkleo COMPONENT Devel ) install(FILES ${PRI_FILENAME} DESTINATION ${ECM_MKSPECS_INSTALL_DIR}) if ( WIN32 ) install ( FILES libkleopatrarc-win32.desktop DESTINATION ${KDE_INSTALL_CONFDIR} RENAME libkleopatrarc ) else () install ( FILES libkleopatrarc.desktop DESTINATION ${KDE_INSTALL_CONFDIR} RENAME libkleopatrarc ) endif () diff --git a/src/models/keylist.h b/src/models/keylist.h new file mode 100644 index 000000000..74bd22396 --- /dev/null +++ b/src/models/keylist.h @@ -0,0 +1,43 @@ +/* + models/keylist.h + + This file is part of libkleopatra, the KDE keymanagement library + SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#ifndef LIBKLEO_KEYLIST_H +#define LIBKLEO_KEYLIST_H + +namespace Kleo +{ +namespace KeyList +{ + static const int FingerprintRole = 0xF1; + static const int KeyRole = 0xF2; + + enum Columns { + PrettyName, + PrettyEMail, + ValidFrom, + ValidUntil, + TechnicalDetails, + ShortKeyID, + KeyID, + Fingerprint, + Issuer, + SerialNumber, + OwnerTrust, + Origin, + LastUpdate, + Validity, + Summary, // Short summary line + Remarks, // Additional remark notations + NumColumns, + Icon = PrettyName // which column shall the icon be displayed in? + }; +} +} + +#endif /* LIBKLEO_KEYLIST_H */ diff --git a/src/models/keylistmodel.cpp b/src/models/keylistmodel.cpp index e4ffd6bf3..09102d6f5 100644 --- a/src/models/keylistmodel.cpp +++ b/src/models/keylistmodel.cpp @@ -1,1004 +1,1007 @@ /* -*- mode: c++; c-basic-offset:4 -*- models/keylistmodel.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include "keylistmodel.h" + #include "keycache.h" +#include "keylist.h" #include "kleo/predicates.h" #include "kleo/keyfiltermanager.h" #include "kleo/keyfilter.h" #include "utils/formatting.h" #ifdef KLEO_MODEL_TEST # include #endif #include #include #include #include #include #include #include #include #include #ifndef Q_MOC_RUN // QTBUG-22829 #include #include #endif #include #include #include #include #include #if GPGMEPP_VERSION >= 0x10E00 // 1.14.0 # define GPGME_HAS_REMARKS #endif using namespace GpgME; using namespace Kleo; +using namespace Kleo::KeyList; Q_DECLARE_METATYPE(GpgME::Key) class AbstractKeyListModel::Private { public: int m_toolTipOptions = Formatting::Validity; mutable QHash prettyEMailCache; mutable QHash remarksCache; bool m_useKeyCache = false; bool m_secretOnly = false; std::vector m_remarkKeys; }; AbstractKeyListModel::AbstractKeyListModel(QObject *p) : QAbstractItemModel(p), KeyListModelInterface(), d(new Private) { } AbstractKeyListModel::~AbstractKeyListModel() {} void AbstractKeyListModel::setToolTipOptions(int opts) { d->m_toolTipOptions = opts; } int AbstractKeyListModel::toolTipOptions() const { return d->m_toolTipOptions; } void AbstractKeyListModel::setRemarkKeys(const std::vector &keys) { d->m_remarkKeys = keys; } std::vector AbstractKeyListModel::remarkKeys() const { return d->m_remarkKeys; } Key AbstractKeyListModel::key(const QModelIndex &idx) const { if (idx.isValid()) { return doMapToKey(idx); } else { return Key::null; } } std::vector AbstractKeyListModel::keys(const QList &indexes) const { std::vector result; result.reserve(indexes.size()); std::transform(indexes.begin(), indexes.end(), std::back_inserter(result), [this](const QModelIndex &idx) { return this->key(idx); }); result.erase(std::unique(result.begin(), result.end(), _detail::ByFingerprint()), result.end()); return result; } QModelIndex AbstractKeyListModel::index(const Key &key, int col) const { if (key.isNull() || col < 0 || col >= NumColumns) { return {}; } else { return doMapFromKey(key, col); } } QList AbstractKeyListModel::indexes(const std::vector &keys) const { QList result; result.reserve(keys.size()); std::transform(keys.begin(), keys.end(), std::back_inserter(result), [this](const Key &key) { return this->index(key); }); return result; } void AbstractKeyListModel::setKeys(const std::vector &keys) { clear(); addKeys(keys); } QModelIndex AbstractKeyListModel::addKey(const Key &key) { const std::vector vec(1, key); const QList l = doAddKeys(vec); return l.empty() ? QModelIndex() : l.front(); } void AbstractKeyListModel::removeKey(const Key &key) { if (key.isNull()) { return; } doRemoveKey(key); d->prettyEMailCache.remove(key.primaryFingerprint()); d->remarksCache.remove(key.primaryFingerprint()); } QList AbstractKeyListModel::addKeys(const std::vector &keys) { std::vector sorted; sorted.reserve(keys.size()); std::remove_copy_if(keys.begin(), keys.end(), std::back_inserter(sorted), std::mem_fn(&Key::isNull)); std::sort(sorted.begin(), sorted.end(), _detail::ByFingerprint()); return doAddKeys(sorted); } void AbstractKeyListModel::clear() { beginResetModel(); doClear(); d->prettyEMailCache.clear(); d->remarksCache.clear(); endResetModel(); } int AbstractKeyListModel::columnCount(const QModelIndex &) const { return NumColumns; } QVariant AbstractKeyListModel::headerData(int section, Qt::Orientation o, int role) const { if (o == Qt::Horizontal) if (role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::ToolTipRole) switch (section) { case PrettyName: return i18n("Name"); case PrettyEMail: return i18n("E-Mail"); case Validity: return i18n("User-IDs"); case ValidFrom: return i18n("Valid From"); case ValidUntil: return i18n("Valid Until"); case TechnicalDetails: return i18n("Protocol"); case ShortKeyID: return i18n("Key-ID"); case KeyID: return i18n("Key-ID"); case Fingerprint: return i18n("Fingerprint"); case Issuer: return i18n("Issuer"); case SerialNumber: return i18n("Serial Number"); case Origin: return i18n("Origin"); case LastUpdate: return i18n("Last Update"); case OwnerTrust: return i18n("Certification Trust"); case Remarks: return i18n("Tags"); case NumColumns:; } return QVariant(); } static QVariant returnIfValid(const QColor &t) { if (t.isValid()) { return t; } else { return QVariant(); } } static QVariant returnIfValid(const QIcon &t) { if (!t.isNull()) { return t; } else { return QVariant(); } } QVariant AbstractKeyListModel::data(const QModelIndex &index, int role) const { const Key key = this->key(index); if (key.isNull()) { return QVariant(); } const int column = index.column(); if (role == Qt::DisplayRole || role == Qt::EditRole) { switch (column) { case PrettyName: return Formatting::prettyName(key); case PrettyEMail: if (const char *const fpr = key.primaryFingerprint()) { const QHash::const_iterator it = d->prettyEMailCache.constFind(fpr); if (it != d->prettyEMailCache.constEnd()) { return *it; } else { return d->prettyEMailCache[fpr] = Formatting::prettyEMail(key); } } else { return QVariant(); } case Validity: return Formatting::complianceStringShort(key); case ValidFrom: if (role == Qt::EditRole) { return Formatting::creationDate(key); } else { return Formatting::creationDateString(key); } case ValidUntil: if (role == Qt::EditRole) { return Formatting::expirationDate(key); } else { return Formatting::expirationDateString(key); } case TechnicalDetails: return Formatting::type(key); case ShortKeyID: return QString::fromLatin1(key.shortKeyID()); case KeyID: return Formatting::prettyID(key.keyID()); case Summary: return Formatting::summaryLine(key); case Fingerprint: return Formatting::prettyID(key.primaryFingerprint()); case Issuer: return QString::fromUtf8(key.issuerName()); case Origin: return Formatting::origin(key.origin()); case LastUpdate: return Formatting::dateString(key.lastUpdate()); case SerialNumber: return QString::fromUtf8(key.issuerSerial()); case OwnerTrust: return Formatting::ownerTrustShort(key.ownerTrust()); case Remarks: #ifdef GPGME_HAS_REMARKS { const char *const fpr = key.primaryFingerprint(); if (fpr && key.protocol() == GpgME::OpenPGP && key.numUserIDs() && d->m_remarkKeys.size()) { if (!(key.keyListMode() & GpgME::SignatureNotations)) { return i18n("Loading..."); } const QHash::const_iterator it = d->remarksCache.constFind(fpr); if (it != d->remarksCache.constEnd()) { return *it; } else { GpgME::Error err; const auto remarks = key.userID(0).remarks(d->m_remarkKeys, err); if (remarks.size() == 1) { const auto remark = QString::fromStdString(remarks[0]); return d->remarksCache[fpr] = remark; } else { QStringList remarkList; remarkList.reserve(remarks.size()); for (const auto &rem: remarks) { remarkList << QString::fromStdString(rem); } const auto remark = remarkList.join(QStringLiteral("; ")); return d->remarksCache[fpr] = remark; } } } else { return QVariant(); } } #endif return QVariant(); case NumColumns: break; } } else if (role == Qt::ToolTipRole) { return Formatting::toolTip(key, toolTipOptions()); } else if (role == Qt::FontRole) { return KeyFilterManager::instance()->font(key, (column == ShortKeyID || column == KeyID || column == Fingerprint) ? QFont(QStringLiteral("monospace")) : QFont()); } else if (role == Qt::DecorationRole) { return column == Icon ? returnIfValid(KeyFilterManager::instance()->icon(key)) : QVariant(); } else if (role == Qt::BackgroundRole) { return returnIfValid(KeyFilterManager::instance()->bgColor(key)); } else if (role == Qt::ForegroundRole) { return returnIfValid(KeyFilterManager::instance()->fgColor(key)); } else if (role == FingerprintRole) { return QString::fromLatin1(key.primaryFingerprint()); } else if (role == KeyRole) { return QVariant::fromValue(key); } return QVariant(); } namespace { template class TableModelMixin : public Base { public: explicit TableModelMixin(QObject *p = nullptr) : Base(p) {} ~TableModelMixin() {} using Base::index; QModelIndex index(int row, int column, const QModelIndex &pidx = QModelIndex()) const override { return this->hasIndex(row, column, pidx) ? this->createIndex(row, column, nullptr) : QModelIndex(); } private: QModelIndex parent(const QModelIndex &) const override { return QModelIndex(); } bool hasChildren(const QModelIndex &pidx) const override { return (pidx.model() == this || !pidx.isValid()) && this->rowCount(pidx) > 0 && this->columnCount(pidx) > 0; } }; class FlatKeyListModel #ifndef Q_MOC_RUN : public TableModelMixin #else : public AbstractKeyListModel #endif { Q_OBJECT public: explicit FlatKeyListModel(QObject *parent = nullptr); ~FlatKeyListModel() override; int rowCount(const QModelIndex &pidx) const override { return pidx.isValid() ? 0 : mKeysByFingerprint.size(); } private: Key doMapToKey(const QModelIndex &index) const override; QModelIndex doMapFromKey(const Key &key, int col) const override; QList doAddKeys(const std::vector &keys) override; void doRemoveKey(const Key &key) override; void doClear() override { mKeysByFingerprint.clear(); } private: std::vector mKeysByFingerprint; }; class HierarchicalKeyListModel : public AbstractKeyListModel { Q_OBJECT public: explicit HierarchicalKeyListModel(QObject *parent = nullptr); ~HierarchicalKeyListModel() override; int rowCount(const QModelIndex &pidx) const override; using AbstractKeyListModel::index; QModelIndex index(int row, int col, const QModelIndex &pidx) const override; QModelIndex parent(const QModelIndex &idx) const override; bool hasChildren(const QModelIndex &pidx) const override { return rowCount(pidx) > 0; } private: Key doMapToKey(const QModelIndex &index) const override; QModelIndex doMapFromKey(const Key &key, int col) const override; QList doAddKeys(const std::vector &keys) override; void doRemoveKey(const Key &key) override; void doClear() override { mTopLevels.clear(); mKeysByFingerprint.clear(); mKeysByExistingParent.clear(); mKeysByNonExistingParent.clear(); } private: void addTopLevelKey(const Key &key); void addKeyWithParent(const char *issuer_fpr, const Key &key); void addKeyWithoutParent(const char *issuer_fpr, const Key &key); private: typedef std::map< std::string, std::vector > Map; std::vector mKeysByFingerprint; // all keys Map mKeysByExistingParent, mKeysByNonExistingParent; // parent->child map std::vector mTopLevels; // all roots + parent-less }; static const char *cleanChainID(const Key &key) { if (key.isRoot()) { return ""; } if (const char *chid = key.chainID()) { return chid; } return ""; } } FlatKeyListModel::FlatKeyListModel(QObject *p) : TableModelMixin(p), mKeysByFingerprint() { } FlatKeyListModel::~FlatKeyListModel() {} Key FlatKeyListModel::doMapToKey(const QModelIndex &idx) const { Q_ASSERT(idx.isValid()); if (static_cast(idx.row()) < mKeysByFingerprint.size() && idx.column() < NumColumns) { return mKeysByFingerprint[ idx.row() ]; } else { return Key::null; } } QModelIndex FlatKeyListModel::doMapFromKey(const Key &key, int col) const { Q_ASSERT(!key.isNull()); const std::vector::const_iterator it = std::lower_bound(mKeysByFingerprint.begin(), mKeysByFingerprint.end(), key, _detail::ByFingerprint()); if (it == mKeysByFingerprint.end() || !_detail::ByFingerprint()(*it, key)) { return {}; } else { return createIndex(it - mKeysByFingerprint.begin(), col); } } QList FlatKeyListModel::doAddKeys(const std::vector &keys) { Q_ASSERT(std::is_sorted(keys.begin(), keys.end(), _detail::ByFingerprint())); if (keys.empty()) { return QList(); } for (auto it = keys.begin(), end = keys.end(); it != end; ++it) { // find an insertion point: const std::vector::iterator pos = std::upper_bound(mKeysByFingerprint.begin(), mKeysByFingerprint.end(), *it, _detail::ByFingerprint()); const unsigned int idx = std::distance(mKeysByFingerprint.begin(), pos); if (idx > 0 && qstrcmp(mKeysByFingerprint[idx - 1].primaryFingerprint(), it->primaryFingerprint()) == 0) { // key existed before - replace with new one: mKeysByFingerprint[idx - 1] = *it; Q_EMIT dataChanged(createIndex(idx - 1, 0), createIndex(idx - 1, NumColumns - 1)); } else { // new key - insert: beginInsertRows(QModelIndex(), idx, idx); mKeysByFingerprint.insert(pos, *it); endInsertRows(); } } return indexes(keys); } void FlatKeyListModel::doRemoveKey(const Key &key) { const std::vector::iterator it = qBinaryFind(mKeysByFingerprint.begin(), mKeysByFingerprint.end(), key, _detail::ByFingerprint()); if (it == mKeysByFingerprint.end()) { return; } const unsigned int row = std::distance(mKeysByFingerprint.begin(), it); beginRemoveRows(QModelIndex(), row, row); mKeysByFingerprint.erase(it); endRemoveRows(); } HierarchicalKeyListModel::HierarchicalKeyListModel(QObject *p) : AbstractKeyListModel(p), mKeysByFingerprint(), mKeysByExistingParent(), mKeysByNonExistingParent(), mTopLevels() { } HierarchicalKeyListModel::~HierarchicalKeyListModel() {} int HierarchicalKeyListModel::rowCount(const QModelIndex &pidx) const { // toplevel item: if (!pidx.isValid()) { return mTopLevels.size(); } if (pidx.column() != 0) { return 0; } // non-toplevel item - find the number of subjects for this issuer: const Key issuer = this->key(pidx); const char *const fpr = issuer.primaryFingerprint(); if (!fpr || !*fpr) { return 0; } const Map::const_iterator it = mKeysByExistingParent.find(fpr); if (it == mKeysByExistingParent.end()) { return 0; } return it->second.size(); } QModelIndex HierarchicalKeyListModel::index(int row, int col, const QModelIndex &pidx) const { if (row < 0 || col < 0 || col >= NumColumns) { return {}; } // toplevel item: if (!pidx.isValid()) { if (static_cast(row) < mTopLevels.size()) { return index(mTopLevels[row], col); } else { return QModelIndex(); } } // non-toplevel item - find the row'th subject of this key: const Key issuer = this->key(pidx); const char *const fpr = issuer.primaryFingerprint(); if (!fpr || !*fpr) { return QModelIndex(); } const Map::const_iterator it = mKeysByExistingParent.find(fpr); if (it == mKeysByExistingParent.end() || static_cast(row) >= it->second.size()) { return QModelIndex(); } return index(it->second[row], col); } QModelIndex HierarchicalKeyListModel::parent(const QModelIndex &idx) const { const Key key = this->key(idx); if (key.isNull() || key.isRoot()) { return {}; } const std::vector::const_iterator it = qBinaryFind(mKeysByFingerprint.begin(), mKeysByFingerprint.end(), cleanChainID(key), _detail::ByFingerprint()); return it != mKeysByFingerprint.end() ? index(*it) : QModelIndex(); } Key HierarchicalKeyListModel::doMapToKey(const QModelIndex &idx) const { if (!idx.isValid()) { return Key::null; } const char *const issuer_fpr = static_cast(idx.internalPointer()); if (!issuer_fpr || !*issuer_fpr) { // top-level: if (static_cast(idx.row()) >= mTopLevels.size()) { return Key::null; } else { return mTopLevels[idx.row()]; } } // non-toplevel: const Map::const_iterator it = mKeysByExistingParent.find(issuer_fpr); if (it == mKeysByExistingParent.end() || static_cast(idx.row()) >= it->second.size()) { return Key::null; } return it->second[idx.row()]; } QModelIndex HierarchicalKeyListModel::doMapFromKey(const Key &key, int col) const { if (key.isNull()) { return {}; } const char *issuer_fpr = cleanChainID(key); // we need to look in the toplevels list,... const std::vector *v = &mTopLevels; if (issuer_fpr && *issuer_fpr) { const std::map< std::string, std::vector >::const_iterator it = mKeysByExistingParent.find(issuer_fpr); // ...unless we find an existing parent: if (it != mKeysByExistingParent.end()) { v = &it->second; } else { issuer_fpr = nullptr; // force internalPointer to zero for toplevels } } const std::vector::const_iterator it = std::lower_bound(v->begin(), v->end(), key, _detail::ByFingerprint()); if (it == v->end() || !_detail::ByFingerprint()(*it, key)) { return QModelIndex(); } const unsigned int row = std::distance(v->begin(), it); return createIndex(row, col, const_cast(issuer_fpr)); } void HierarchicalKeyListModel::addKeyWithParent(const char *issuer_fpr, const Key &key) { Q_ASSERT(issuer_fpr); Q_ASSERT(*issuer_fpr); Q_ASSERT(!key.isNull()); std::vector &subjects = mKeysByExistingParent[issuer_fpr]; // find insertion point: const std::vector::iterator it = std::lower_bound(subjects.begin(), subjects.end(), key, _detail::ByFingerprint()); const int row = std::distance(subjects.begin(), it); if (it != subjects.end() && qstricmp(it->primaryFingerprint(), key.primaryFingerprint()) == 0) { // exists -> replace *it = key; Q_EMIT dataChanged(createIndex(row, 0, const_cast(issuer_fpr)), createIndex(row, NumColumns - 1, const_cast(issuer_fpr))); } else { // doesn't exist -> insert const std::vector::const_iterator pos = qBinaryFind(mKeysByFingerprint.begin(), mKeysByFingerprint.end(), issuer_fpr, _detail::ByFingerprint()); Q_ASSERT(pos != mKeysByFingerprint.end()); beginInsertRows(index(*pos), row, row); subjects.insert(it, key); endInsertRows(); } } void HierarchicalKeyListModel::addKeyWithoutParent(const char *issuer_fpr, const Key &key) { Q_ASSERT(issuer_fpr); Q_ASSERT(*issuer_fpr); Q_ASSERT(!key.isNull()); std::vector &subjects = mKeysByNonExistingParent[issuer_fpr]; // find insertion point: const std::vector::iterator it = std::lower_bound(subjects.begin(), subjects.end(), key, _detail::ByFingerprint()); if (it != subjects.end() && qstricmp(it->primaryFingerprint(), key.primaryFingerprint()) == 0) // exists -> replace { *it = key; } else // doesn't exist -> insert { subjects.insert(it, key); } addTopLevelKey(key); } void HierarchicalKeyListModel::addTopLevelKey(const Key &key) { // find insertion point: const std::vector::iterator it = std::lower_bound(mTopLevels.begin(), mTopLevels.end(), key, _detail::ByFingerprint()); const int row = std::distance(mTopLevels.begin(), it); if (it != mTopLevels.end() && qstricmp(it->primaryFingerprint(), key.primaryFingerprint()) == 0) { // exists -> replace *it = key; Q_EMIT dataChanged(createIndex(row, 0), createIndex(row, NumColumns - 1)); } else { // doesn't exist -> insert beginInsertRows(QModelIndex(), row, row); mTopLevels.insert(it, key); endInsertRows(); } } // sorts 'keys' such that parent always come before their children: static std::vector topological_sort(const std::vector &keys) { boost::adjacency_list<> graph(keys.size()); // add edges from children to parents: for (unsigned int i = 0, end = keys.size(); i != end; ++i) { const char *const issuer_fpr = cleanChainID(keys[i]); if (!issuer_fpr || !*issuer_fpr) { continue; } const std::vector::const_iterator it = qBinaryFind(keys.begin(), keys.end(), issuer_fpr, _detail::ByFingerprint()); if (it == keys.end()) { continue; } add_edge(i, std::distance(keys.begin(), it), graph); } std::vector order; order.reserve(keys.size()); topological_sort(graph, std::back_inserter(order)); Q_ASSERT(order.size() == keys.size()); std::vector result; result.reserve(keys.size()); for (int i : qAsConst(order)) { result.push_back(keys[i]); } return result; } QList HierarchicalKeyListModel::doAddKeys(const std::vector &keys) { Q_ASSERT(std::is_sorted(keys.begin(), keys.end(), _detail::ByFingerprint())); if (keys.empty()) { return QList(); } const std::vector oldKeys = mKeysByFingerprint; std::vector merged; merged.reserve(keys.size() + mKeysByFingerprint.size()); std::set_union(keys.begin(), keys.end(), mKeysByFingerprint.begin(), mKeysByFingerprint.end(), std::back_inserter(merged), _detail::ByFingerprint()); mKeysByFingerprint = merged; std::set > changedParents; Q_FOREACH (const Key &key, topological_sort(keys)) { // check to see whether this key is a parent for a previously parent-less group: const char *const fpr = key.primaryFingerprint(); if (!fpr || !*fpr) { continue; } const bool keyAlreadyExisted = qBinaryFind(oldKeys.begin(), oldKeys.end(), key, _detail::ByFingerprint()) != oldKeys.end(); const Map::iterator it = mKeysByNonExistingParent.find(fpr); const std::vector children = it != mKeysByNonExistingParent.end() ? it->second : std::vector(); if (it != mKeysByNonExistingParent.end()) { mKeysByNonExistingParent.erase(it); } // Step 1: For new keys, remove children from toplevel: if (!keyAlreadyExisted) { auto last = mTopLevels.begin(); auto lastFP = mKeysByFingerprint.begin(); for (const Key &k : qAsConst(children)) { last = qBinaryFind(last, mTopLevels.end(), k, _detail::ByFingerprint()); Q_ASSERT(last != mTopLevels.end()); const int row = std::distance(mTopLevels.begin(), last); lastFP = qBinaryFind(lastFP, mKeysByFingerprint.end(), k, _detail::ByFingerprint()); Q_ASSERT(lastFP != mKeysByFingerprint.end()); Q_EMIT rowAboutToBeMoved(QModelIndex(), row); beginRemoveRows(QModelIndex(), row, row); last = mTopLevels.erase(last); lastFP = mKeysByFingerprint.erase(lastFP); endRemoveRows(); } } // Step 2: add/update key const char *const issuer_fpr = cleanChainID(key); if (!issuer_fpr || !*issuer_fpr) // root or something... { addTopLevelKey(key); } else if (std::binary_search(mKeysByFingerprint.begin(), mKeysByFingerprint.end(), issuer_fpr, _detail::ByFingerprint())) // parent exists... { addKeyWithParent(issuer_fpr, key); } else // parent doesn't exist yet... { addKeyWithoutParent(issuer_fpr, key); } const QModelIndex key_idx = index(key); QModelIndex key_parent = key_idx.parent(); while (key_parent.isValid()) { changedParents.insert(doMapToKey(key_parent)); key_parent = key_parent.parent(); } // Step 3: Add children to new parent ( == key ) if (!keyAlreadyExisted && !children.empty()) { addKeys(children); const QModelIndex new_parent = index(key); // Q_EMIT the rowMoved() signals in reversed direction, so the // implementation can use a stack for mapping. for (int i = children.size() - 1; i >= 0; --i) { Q_EMIT rowMoved(new_parent, i); } } } //Q_EMIT dataChanged for all parents with new children. This triggers KeyListSortFilterProxyModel to //show a parent node if it just got children matching the proxy's filter for (const Key &i : qAsConst(changedParents)) { const QModelIndex idx = index(i); if (idx.isValid()) { Q_EMIT dataChanged(idx.sibling(idx.row(), 0), idx.sibling(idx.row(), NumColumns - 1)); } } return indexes(keys); } void HierarchicalKeyListModel::doRemoveKey(const Key &key) { const QModelIndex idx = index(key); if (!idx.isValid()) { return; } const char *const fpr = key.primaryFingerprint(); if (mKeysByExistingParent.find(fpr) != mKeysByExistingParent.end()) { //handle non-leave nodes: std::vector keys = mKeysByFingerprint; const std::vector::iterator it = qBinaryFind(keys.begin(), keys.end(), key, _detail::ByFingerprint()); if (it == keys.end()) { return; } keys.erase(it); // FIXME for simplicity, we just clear the model and re-add all keys minus the removed one. This is suboptimal, // but acceptable given that deletion of non-leave nodes is rather rare. clear(); addKeys(keys); return; } //handle leave nodes: const std::vector::iterator it = qBinaryFind(mKeysByFingerprint.begin(), mKeysByFingerprint.end(), key, _detail::ByFingerprint()); Q_ASSERT(it != mKeysByFingerprint.end()); Q_ASSERT(mKeysByNonExistingParent.find(fpr) == mKeysByNonExistingParent.end()); Q_ASSERT(mKeysByExistingParent.find(fpr) == mKeysByExistingParent.end()); beginRemoveRows(parent(idx), idx.row(), idx.row()); mKeysByFingerprint.erase(it); const char *const issuer_fpr = cleanChainID(key); const std::vector::iterator tlIt = qBinaryFind(mTopLevels.begin(), mTopLevels.end(), key, _detail::ByFingerprint()); if (tlIt != mTopLevels.end()) { mTopLevels.erase(tlIt); } if (issuer_fpr && *issuer_fpr) { const Map::iterator nexIt = mKeysByNonExistingParent.find(issuer_fpr); if (nexIt != mKeysByNonExistingParent.end()) { const std::vector::iterator eit = qBinaryFind(nexIt->second.begin(), nexIt->second.end(), key, _detail::ByFingerprint()); if (eit != nexIt->second.end()) { nexIt->second.erase(eit); } if (nexIt->second.empty()) { mKeysByNonExistingParent.erase(nexIt); } } const Map::iterator exIt = mKeysByExistingParent.find(issuer_fpr); if (exIt != mKeysByExistingParent.end()) { const std::vector::iterator eit = qBinaryFind(exIt->second.begin(), exIt->second.end(), key, _detail::ByFingerprint()); if (eit != exIt->second.end()) { exIt->second.erase(eit); } if (exIt->second.empty()) { mKeysByExistingParent.erase(exIt); } } } endRemoveRows(); } void AbstractKeyListModel::useKeyCache(bool value, bool secretOnly) { d->m_secretOnly = secretOnly; d->m_useKeyCache = value; if (value) { setKeys(d->m_secretOnly ? KeyCache::instance()->secretKeys() : KeyCache::instance()->keys()); } else { setKeys(std::vector()); } connect(KeyCache::instance().get(), &KeyCache::keysMayHaveChanged, this, [this] { if (d->m_useKeyCache) { setKeys(d->m_secretOnly ? KeyCache::instance()->secretKeys() : KeyCache::instance()->keys()); } }); } // static AbstractKeyListModel *AbstractKeyListModel::createFlatKeyListModel(QObject *p) { AbstractKeyListModel *const m = new FlatKeyListModel(p); #ifdef KLEO_MODEL_TEST new QAbstractItemModelTester(m, p); #endif return m; } // static AbstractKeyListModel *AbstractKeyListModel::createHierarchicalKeyListModel(QObject *p) { AbstractKeyListModel *const m = new HierarchicalKeyListModel(p); #ifdef KLEO_MODEL_TEST new QAbstractItemModelTester(m, p); #endif return m; } #include "keylistmodel.moc" /*! \fn AbstractKeyListModel::rowAboutToBeMoved( const QModelIndex & old_parent, int old_row ) Emitted before the removal of a row from that model. It will later be added to the model again, in response to which rowMoved() will be emitted. If multiple rows are moved in one go, multiple rowAboutToBeMoved() signals are emitted before the corresponding number of rowMoved() signals is emitted - in reverse order. This works around the absence of move semantics in QAbstractItemModel. Clients can maintain a stack to perform the QModelIndex-mapping themselves, or, e.g., to preserve the selection status of the row: \code std::vector mMovingRowWasSelected; // transient, used when rows are moved // ... void slotRowAboutToBeMoved( const QModelIndex & p, int row ) { mMovingRowWasSelected.push_back( selectionModel()->isSelected( model()->index( row, 0, p ) ) ); } void slotRowMoved( const QModelIndex & p, int row ) { const bool wasSelected = mMovingRowWasSelected.back(); mMovingRowWasSelected.pop_back(); if ( wasSelected ) selectionModel()->select( model()->index( row, 0, p ), Select|Rows ); } \endcode A similar mechanism could be used to preserve the current item during moves. */ /*! \fn AbstractKeyListModel::rowMoved( const QModelIndex & new_parent, int new_parent ) See rowAboutToBeMoved() */ diff --git a/src/models/keylistmodelinterface.h b/src/models/keylistmodelinterface.h index 9830d5af5..5eab35e35 100644 --- a/src/models/keylistmodelinterface.h +++ b/src/models/keylistmodelinterface.h @@ -1,66 +1,42 @@ /* -*- mode: c++; c-basic-offset:4 -*- models/keylistmodelinterface.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #ifndef __KLEOPATRA_MODELS_KEYLISTMODELINTERFACE_H__ #define __KLEOPATRA_MODELS_KEYLISTMODELINTERFACE_H__ #include #include namespace GpgME { class Key; } class QModelIndex; template class QList; namespace Kleo { class KLEO_EXPORT KeyListModelInterface { public: virtual ~KeyListModelInterface(); - static const int FingerprintRole = 0xF1; - static const int KeyRole = 0xF2; - - enum Columns { - PrettyName, - PrettyEMail, - ValidFrom, - ValidUntil, - TechnicalDetails, - ShortKeyID, - KeyID, - Fingerprint, - Issuer, - SerialNumber, - OwnerTrust, - Origin, - LastUpdate, - Validity, - Summary, // Short summary line - Remarks, // Additional remark notations - NumColumns, - Icon = PrettyName // which column shall the icon be displayed in? - }; - virtual GpgME::Key key(const QModelIndex &idx) const = 0; virtual std::vector keys(const QList &idxs) const = 0; virtual QModelIndex index(const GpgME::Key &key) const = 0; virtual QList indexes(const std::vector &keys) const = 0; }; } #endif /* __KLEOPATRA_MODELS_KEYLISTMODELINTERFACE_H__ */ diff --git a/src/models/keylistsortfilterproxymodel.cpp b/src/models/keylistsortfilterproxymodel.cpp index c17fb41e6..37c1cde6f 100644 --- a/src/models/keylistsortfilterproxymodel.cpp +++ b/src/models/keylistsortfilterproxymodel.cpp @@ -1,214 +1,215 @@ /* -*- mode: c++; c-basic-offset:4 -*- models/keylistsortfilterproxymodel.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2007 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include "keylistsortfilterproxymodel.h" +#include "keylist.h" #include "keylistmodel.h" #include "kleo/keyfilter.h" #include "kleo/stl_util.h" #include #include #include #if GPGMEPP_VERSION >= 0x10E00 // 1.14.0 # define GPGME_HAS_REMARKS #endif using namespace Kleo; using namespace GpgME; AbstractKeyListSortFilterProxyModel::AbstractKeyListSortFilterProxyModel(QObject *p) : QSortFilterProxyModel(p), KeyListModelInterface() { init(); } AbstractKeyListSortFilterProxyModel::AbstractKeyListSortFilterProxyModel(const AbstractKeyListSortFilterProxyModel &other) : QSortFilterProxyModel(), KeyListModelInterface() { Q_UNUSED(other) init(); } void AbstractKeyListSortFilterProxyModel::init() { setDynamicSortFilter(true); setSortRole(Qt::EditRole); // EditRole can be expected to be in a less formatted way, better for sorting setFilterRole(Qt::DisplayRole); setFilterCaseSensitivity(Qt::CaseInsensitive); } AbstractKeyListSortFilterProxyModel::~AbstractKeyListSortFilterProxyModel() {} Key AbstractKeyListSortFilterProxyModel::key(const QModelIndex &idx) const { const KeyListModelInterface *const klmi = dynamic_cast(sourceModel()); if (!klmi) { static Key null; return null; } return klmi->key(mapToSource(idx)); } std::vector AbstractKeyListSortFilterProxyModel::keys(const QList &indexes) const { const KeyListModelInterface *const klmi = dynamic_cast(sourceModel()); if (!klmi) { return std::vector(); } QList mapped; mapped.reserve(indexes.size()); std::transform(indexes.begin(), indexes.end(), std::back_inserter(mapped), [this](const QModelIndex &idx) { return mapToSource(idx); }); return klmi->keys(mapped); } QModelIndex AbstractKeyListSortFilterProxyModel::index(const Key &key) const { if (const KeyListModelInterface *const klmi = dynamic_cast(sourceModel())) { return mapFromSource(klmi->index(key)); } else { return {}; } } QList AbstractKeyListSortFilterProxyModel::indexes(const std::vector &keys) const { if (const KeyListModelInterface *const klmi = dynamic_cast(sourceModel())) { const QList source = klmi->indexes(keys); QList mapped; mapped.reserve(source.size()); std::transform(source.begin(), source.end(), std::back_inserter(mapped), [this](const QModelIndex &idx) { return mapFromSource(idx); }); return mapped; } else { return QList(); } } class KeyListSortFilterProxyModel::Private { friend class ::Kleo::KeyListSortFilterProxyModel; public: explicit Private() : keyFilter() {} ~Private() {} private: std::shared_ptr keyFilter; }; KeyListSortFilterProxyModel::KeyListSortFilterProxyModel(QObject *p) : AbstractKeyListSortFilterProxyModel(p), d(new Private) { } KeyListSortFilterProxyModel::KeyListSortFilterProxyModel(const KeyListSortFilterProxyModel &other) : AbstractKeyListSortFilterProxyModel(other), d(new Private(*other.d)) { } KeyListSortFilterProxyModel::~KeyListSortFilterProxyModel() {} KeyListSortFilterProxyModel *KeyListSortFilterProxyModel::clone() const { return new KeyListSortFilterProxyModel(*this); } std::shared_ptr KeyListSortFilterProxyModel::keyFilter() const { return d->keyFilter; } void KeyListSortFilterProxyModel::setKeyFilter(const std::shared_ptr &kf) { if (kf == d->keyFilter) { return; } d->keyFilter = kf; invalidate(); } bool KeyListSortFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { // // 0. Keep parents of matching children: // const QModelIndex index = sourceModel()->index(source_row, 0, source_parent); for (int i = 0, end = sourceModel()->rowCount(index); i != end; ++i) if (filterAcceptsRow(i, index)) { return true; } // // 1. Check filterRegExp // const int role = filterRole(); const int col = filterKeyColumn(); const QRegExp rx = filterRegExp(); - const QModelIndex nameIndex = sourceModel()->index(source_row, PrettyName, source_parent); + const QModelIndex nameIndex = sourceModel()->index(source_row, KeyList::PrettyName, source_parent); const KeyListModelInterface *const klm = dynamic_cast(sourceModel()); Q_ASSERT(klm); const Key key = klm->key(nameIndex); if (col) { const QModelIndex colIdx = sourceModel()->index(source_row, col, source_parent); const QString content = colIdx.data(role).toString(); if (!content.contains(rx)) { return false; } } else { // By default match against the full uid data (name / email / comment / dn) bool match = false; for (const auto &uid: key.userIDs()) { const auto id = QString::fromUtf8(uid.id()); if (id.contains(rx)) { match = true; break; } #ifdef GPGME_HAS_REMARKS // Also match against remarks (search tags) const auto alm = dynamic_cast(sourceModel()); if (alm) { - const auto remarks = alm->data(alm->index(key, KeyListModelInterface::Remarks)); + const auto remarks = alm->data(alm->index(key, KeyList::Remarks)); if (!remarks.isNull() && remarks.toString().contains(rx)) { match = true; break; } } #endif } if (!match) { return false; } } // // 2. Check that key filters match (if any are defined) // if (d->keyFilter) { // avoid artifacts when no filters are defined return d->keyFilter->matches(key, KeyFilter::Filtering); } // 3. match by default: return true; } diff --git a/src/ui/keyselectioncombo.cpp b/src/ui/keyselectioncombo.cpp index 946ed6232..633db3793 100644 --- a/src/ui/keyselectioncombo.cpp +++ b/src/ui/keyselectioncombo.cpp @@ -1,538 +1,541 @@ /* 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 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, Kleo::KeyListModelInterface::KeyRole).value(); - const auto rightKey = sourceModel()->data(right, Kleo::KeyListModelInterface::KeyRole).value(); + 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, Kleo::KeyListModelInterface::KeyRole).value(); + 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, Kleo::KeyListModelInterface::KeyRole).value(); + 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() const { 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); } q->setCurrentKey(defaultKey); } Kleo::AbstractKeyListModel *model = nullptr; Kleo::KeyListSortFilterProxyModel *sortFilterProxy = nullptr; ProxyModel *proxyModel = nullptr; std::shared_ptr cache; QMap defaultKeys; bool wasEnabled = false; bool useWasEnabled = false; bool secretOnly; QString mPerfectMatchMbox; 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 Kleo::KeyListSortFilterProxyModel(this); d->sortFilterProxy->setSourceModel(d->model); d->proxyModel = new ProxyModel(this); d->proxyModel->setSourceModel(d->sortFilterProxy); setModel(d->proxyModel); connect(this, static_cast(&KeySelectionCombo::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)); } else { Q_EMIT currentKeyChanged(currentKey()); } } }); d->cache = Kleo::KeyCache::mutableInstance(); 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); 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->cache->initialized()) { refreshKeys(); } else { d->model->useKeyCache(true, d->secretOnly); 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(Kleo::KeyListModelInterface::KeyRole).value(); + return currentData(KeyList::KeyRole).value(); } void Kleo::KeySelectionCombo::setCurrentKey(const GpgME::Key &key) { - const int idx = findData(QVariant::fromValue(key), Kleo::KeyListModelInterface::KeyRole, Qt::MatchExactly); + const int idx = findData(QVariant::fromValue(key), KeyList::KeyRole, Qt::MatchExactly); if (idx > -1) { setCurrentIndex(idx); } else { d->selectPerfectIdMatch(); } 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 return; } - const int idx = findData(fingerprint, Kleo::KeyListModelInterface::FingerprintRole, Qt::MatchExactly); + 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 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"