diff --git a/src/crypto/gui/certificatelineedit.cpp b/src/crypto/gui/certificatelineedit.cpp index a6af8aaed..255252c39 100644 --- a/src/crypto/gui/certificatelineedit.cpp +++ b/src/crypto/gui/certificatelineedit.cpp @@ -1,450 +1,501 @@ /* crypto/gui/certificatelineedit.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-FileCopyrightText: 2021, 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include "certificatelineedit.h" +#include "commands/detailscommand.h" +#include "dialogs/groupdetailsdialog.h" + #include #include #include #include #include "kleopatra_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include using namespace Kleo; using namespace GpgME; Q_DECLARE_METATYPE(GpgME::Key) Q_DECLARE_METATYPE(KeyGroup) static QStringList s_lookedUpKeys; namespace { class CompletionProxyModel : public KeyListSortFilterProxyModel { Q_OBJECT public: CompletionProxyModel(QObject *parent = nullptr) : KeyListSortFilterProxyModel(parent) { } int columnCount(const QModelIndex &parent = QModelIndex()) const override { Q_UNUSED(parent) // pretend that there is only one column to workaround a bug in // QAccessibleTable which provides the accessibility interface for the // completion pop-up return 1; } QVariant data(const QModelIndex &idx, int role) const override { if (!idx.isValid()) { return QVariant(); } switch (role) { case Qt::DecorationRole: { const auto key = KeyListSortFilterProxyModel::data(idx, KeyList::KeyRole).value(); if (!key.isNull()) { return Kleo::Formatting::iconForUid(key.userID(0)); } const auto group = KeyListSortFilterProxyModel::data(idx, KeyList::GroupRole).value(); if (!group.isNull()) { return QIcon::fromTheme(QStringLiteral("group")); } Q_ASSERT(!key.isNull() || !group.isNull()); return QVariant(); } default: return KeyListSortFilterProxyModel::data(index(idx.row(), KeyList::Summary), role); } } }; + +auto createSeparatorAction(QObject *parent) +{ + auto action = new QAction{parent}; + action->setSeparator(true); + return action; +} } // namespace class CertificateLineEdit::Private { CertificateLineEdit *q; public: explicit Private(CertificateLineEdit *qq, AbstractKeyListModel *model, KeyFilter *filter); QString text() const; void setKey(const GpgME::Key &key); void setGroup(const KeyGroup &group); void setKeyFilter(const std::shared_ptr &filter); void updateKey(); void editChanged(); void editFinished(); void checkLocate(); private: + void openDetailsDialog(); void setTextWithBlockedSignals(const QString &s); + void showContextMenu(const QPoint &pos); public: GpgME::Key mKey; KeyGroup mGroup; private: struct Ui { Ui(QWidget *parent) : lineEdit{parent} , button{parent} {} QLineEdit lineEdit; QToolButton button; } ui; KeyListSortFilterProxyModel *const mFilterModel; KeyListSortFilterProxyModel *const mCompleterFilterModel; QCompleter *mCompleter = nullptr; std::shared_ptr mFilter; bool mEditStarted = false; bool mEditFinished = false; QAction *const mLineAction; + QAction *const mShowDetailsAction; }; CertificateLineEdit::Private::Private(CertificateLineEdit *qq, AbstractKeyListModel *model, KeyFilter *filter) : q{qq} , ui{qq} , mFilterModel{new KeyListSortFilterProxyModel{qq}} , mCompleterFilterModel{new CompletionProxyModel{qq}} , mCompleter{new QCompleter{qq}} , mFilter{std::shared_ptr{filter}} , mLineAction{new QAction{qq}} + , mShowDetailsAction{new QAction{qq}} { ui.lineEdit.setPlaceholderText(i18n("Please enter a name or email address...")); ui.lineEdit.setClearButtonEnabled(true); + ui.lineEdit.setContextMenuPolicy(Qt::CustomContextMenu); ui.lineEdit.addAction(mLineAction, QLineEdit::LeadingPosition); mCompleterFilterModel->setKeyFilter(mFilter); mCompleterFilterModel->setSourceModel(model); mCompleter->setModel(mCompleterFilterModel); mCompleter->setFilterMode(Qt::MatchContains); mCompleter->setCaseSensitivity(Qt::CaseInsensitive); ui.lineEdit.setCompleter(mCompleter); ui.button.setIcon(QIcon::fromTheme(QStringLiteral("resource-group-new"))); ui.button.setToolTip(i18n("Show certificate list")); ui.button.setAccessibleName(i18n("Show certificate list")); auto l = new QHBoxLayout{q}; l->setContentsMargins(0, 0, 0, 0); l->addWidget(&ui.lineEdit); l->addWidget(&ui.button); q->setFocusPolicy(ui.lineEdit.focusPolicy()); q->setFocusProxy(&ui.lineEdit); + mShowDetailsAction->setIcon(QIcon::fromTheme(QStringLiteral("help-about"))); + mShowDetailsAction->setText(i18nc("@action:inmenu", "Show Details")); + mShowDetailsAction->setEnabled(false); + mFilterModel->setSourceModel(model); mFilterModel->setFilterKeyColumn(KeyList::Summary); if (filter) { mFilterModel->setKeyFilter(mFilter); } connect(KeyCache::instance().get(), &Kleo::KeyCache::keyListingDone, q, [this]() { updateKey(); }); connect(KeyCache::instance().get(), &Kleo::KeyCache::groupUpdated, q, [this](const KeyGroup &group) { if (!mGroup.isNull() && mGroup.source() == group.source() && mGroup.id() == group.id()) { setTextWithBlockedSignals(Formatting::summaryLine(group)); // queue the update to ensure that the model has been updated QMetaObject::invokeMethod(q, [this]() { updateKey(); }, Qt::QueuedConnection); } }); connect(KeyCache::instance().get(), &Kleo::KeyCache::groupRemoved, q, [this](const KeyGroup &group) { if (!mGroup.isNull() && mGroup.source() == group.source() && mGroup.id() == group.id()) { mGroup = KeyGroup(); QSignalBlocker blocky{&ui.lineEdit}; ui.lineEdit.clear(); // queue the update to ensure that the model has been updated QMetaObject::invokeMethod(q, [this]() { updateKey(); }, Qt::QueuedConnection); } }); connect(&ui.lineEdit, &QLineEdit::editingFinished, q, [this]() { // queue the call of editFinished() to ensure that QCompleter::activated is handled first QMetaObject::invokeMethod(q, [this]() { editFinished(); }, Qt::QueuedConnection); }); connect(&ui.lineEdit, &QLineEdit::textChanged, q, [this]() { editChanged(); }); + connect(&ui.lineEdit, &QLineEdit::customContextMenuRequested, + q, [this](const QPoint &pos) { showContextMenu(pos); }); connect(mLineAction, &QAction::triggered, q, &CertificateLineEdit::dialogRequested); + connect(mShowDetailsAction, &QAction::triggered, + q, [this]() { openDetailsDialog(); }); connect(&ui.button, &QToolButton::clicked, q, &CertificateLineEdit::certificateSelectionRequested); connect(mCompleter, qOverload(&QCompleter::activated), q, [this] (const QModelIndex &index) { Key key = mCompleter->completionModel()->data(index, KeyList::KeyRole).value(); auto group = mCompleter->completionModel()->data(index, KeyList::GroupRole).value(); if (!key.isNull()) { q->setKey(key); } else if (!group.isNull()) { q->setGroup(group); } else { qCDebug(KLEOPATRA_LOG) << "Activated item is neither key nor group"; } }); updateKey(); } +void CertificateLineEdit::Private::openDetailsDialog() +{ + if (!q->key().isNull()) { + auto cmd = new Commands::DetailsCommand{q->key(), nullptr}; + cmd->setParentWidget(q); + cmd->start(); + } else if (!q->group().isNull()) { + auto dlg = new Dialogs::GroupDetailsDialog{q}; + dlg->setAttribute(Qt::WA_DeleteOnClose); + dlg->setGroup(q->group()); + dlg->show(); + } +} + void CertificateLineEdit::Private::setTextWithBlockedSignals(const QString &s) { QSignalBlocker blocky{&ui.lineEdit}; ui.lineEdit.setText(s); } +void CertificateLineEdit::Private::showContextMenu(const QPoint &pos) +{ + if (QMenu *menu = ui.lineEdit.createStandardContextMenu()) { + auto *const firstStandardAction = menu->actions().value(0); + menu->insertActions(firstStandardAction, + {mShowDetailsAction, createSeparatorAction(menu)}); + menu->setAttribute(Qt::WA_DeleteOnClose); + menu->popup(ui.lineEdit.mapToGlobal(pos)); + } +} + CertificateLineEdit::CertificateLineEdit(AbstractKeyListModel *model, KeyFilter *filter, QWidget *parent) : QWidget{parent} , d{new Private{this, model, filter}} { /* Take ownership of the model to prevent double deletion when the * filter models are deleted */ model->setParent(parent ? parent : this); } CertificateLineEdit::~CertificateLineEdit() = default; void CertificateLineEdit::Private::editChanged() { mEditFinished = false; updateKey(); if (!mEditStarted) { Q_EMIT q->editingStarted(); mEditStarted = true; } } void CertificateLineEdit::Private::editFinished() { mEditStarted = false; mEditFinished = true; updateKey(); if (!q->key().isNull()) { setTextWithBlockedSignals(Formatting::summaryLine(q->key())); } else if (!q->group().isNull()) { setTextWithBlockedSignals(Formatting::summaryLine(q->group())); } else { checkLocate(); } } void CertificateLineEdit::Private::checkLocate() { if (!q->key().isNull() || !q->group().isNull()) { // Already have a key or group return; } // Only check once per mailbox const auto mailText = ui.lineEdit.text(); if (s_lookedUpKeys.contains(mailText)) { return; } s_lookedUpKeys << mailText; qCDebug(KLEOPATRA_LOG) << "Lookup job for" << mailText; QGpgME::KeyForMailboxJob *job = QGpgME::openpgp()->keyForMailboxJob(); job->start(mailText); } void CertificateLineEdit::Private::updateKey() { static const _detail::ByFingerprint keysHaveSameFingerprint; const auto mailText = ui.lineEdit.text(); auto newKey = Key(); auto newGroup = KeyGroup(); if (mailText.isEmpty()) { mLineAction->setIcon(QIcon::fromTheme(QStringLiteral("resource-group-new"))); mLineAction->setToolTip(i18n("Open selection dialog.")); ui.lineEdit.setToolTip({}); } else { mFilterModel->setFilterFixedString(mailText); if (mFilterModel->rowCount() > 1) { // keep current key or group if they still match if (!mKey.isNull()) { for (int row = 0; row < mFilterModel->rowCount(); ++row) { const QModelIndex index = mFilterModel->index(row, 0); Key key = mFilterModel->key(index); if (!key.isNull() && keysHaveSameFingerprint(key, mKey)) { newKey = mKey; break; } } } else if (!mGroup.isNull()) { newGroup = mGroup; for (int row = 0; row < mFilterModel->rowCount(); ++row) { const QModelIndex index = mFilterModel->index(row, 0); KeyGroup group = mFilterModel->group(index); if (!group.isNull() && group.source() == mGroup.source() && group.id() == mGroup.id()) { newGroup = mGroup; break; } } } if (newKey.isNull() && newGroup.isNull()) { if (mEditFinished) { mLineAction->setIcon(QIcon::fromTheme(QStringLiteral("question"))); mLineAction->setToolTip(i18n("Multiple matching certificates found")); ui.lineEdit.setToolTip(i18n("Multiple matching certificates found")); } else { mLineAction->setIcon(QIcon::fromTheme(QStringLiteral("resource-group-new"))); mLineAction->setToolTip(i18n("Open selection dialog.")); ui.lineEdit.setToolTip({}); } } } else if (mFilterModel->rowCount() == 1) { const auto index = mFilterModel->index(0, 0); newKey = mFilterModel->data(index, KeyList::KeyRole).value(); newGroup = mFilterModel->data(index, KeyList::GroupRole).value(); Q_ASSERT(!newKey.isNull() || !newGroup.isNull()); if (newKey.isNull() && newGroup.isNull()) { mLineAction->setIcon(QIcon::fromTheme(QStringLiteral("emblem-error"))); mLineAction->setToolTip(i18n("No matching certificates found.
Click to import a certificate.")); ui.lineEdit.setToolTip(i18n("No matching certificates found")); } } else { mLineAction->setIcon(QIcon::fromTheme(QStringLiteral("emblem-error"))); mLineAction->setToolTip(i18n("No matching certificates found.
Click to import a certificate.")); ui.lineEdit.setToolTip(i18n("No matching certificates found")); } } mKey = newKey; mGroup = newGroup; if (!mKey.isNull()) { /* FIXME: This needs to be solved by a multiple UID supporting model */ mLineAction->setIcon(Formatting::iconForUid(mKey.userID(0))); mLineAction->setToolTip(Formatting::validity(mKey.userID(0)) + QLatin1String("
") + i18n("Click for details.")); ui.lineEdit.setToolTip(Formatting::toolTip(mKey, Formatting::ToolTipOption::AllOptions)); } else if (!mGroup.isNull()) { mLineAction->setIcon(Formatting::validityIcon(mGroup)); mLineAction->setToolTip(Formatting::validity(mGroup) + QLatin1String("
") + i18n("Click for details.")); ui.lineEdit.setToolTip(Formatting::toolTip(mGroup, Formatting::ToolTipOption::AllOptions)); } + mShowDetailsAction->setEnabled(!mKey.isNull() || !mGroup.isNull()); + Q_EMIT q->keyChanged(); if (mailText.isEmpty()) { Q_EMIT q->wantsRemoval(q); } } Key CertificateLineEdit::key() const { if (isEnabled()) { return d->mKey; } else { return Key(); } } KeyGroup CertificateLineEdit::group() const { if (isEnabled()) { return d->mGroup; } else { return KeyGroup(); } } QString CertificateLineEdit::Private::text() const { return ui.lineEdit.text(); } QString CertificateLineEdit::text() const { return d->text(); } void CertificateLineEdit::Private::setKey(const Key &key) { mKey = key; mGroup = KeyGroup(); qCDebug(KLEOPATRA_LOG) << "Setting Key. " << Formatting::summaryLine(key); setTextWithBlockedSignals(Formatting::summaryLine(key)); updateKey(); } void CertificateLineEdit::setKey(const Key &key) { d->setKey(key); } void CertificateLineEdit::Private::setGroup(const KeyGroup &group) { mGroup = group; mKey = Key(); const QString summary = Formatting::summaryLine(group); qCDebug(KLEOPATRA_LOG) << "Setting KeyGroup. " << summary; setTextWithBlockedSignals(summary); updateKey(); } void CertificateLineEdit::setGroup(const KeyGroup &group) { d->setGroup(group); } bool CertificateLineEdit::isEmpty() const { return text().isEmpty(); } void CertificateLineEdit::Private::setKeyFilter(const std::shared_ptr &filter) { mFilter = filter; mFilterModel->setKeyFilter(filter); mCompleterFilterModel->setKeyFilter(mFilter); updateKey(); } void CertificateLineEdit::setKeyFilter(const std::shared_ptr &filter) { d->setKeyFilter(filter); } #include "certificatelineedit.moc"