File indexing completed on 2024-06-23 05:13:49
0001 /* crypto/gui/certificatelineedit.cpp 0002 0003 This file is part of Kleopatra, the KDE keymanager 0004 SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik 0005 SPDX-FileContributor: Intevation GmbH 0006 SPDX-FileCopyrightText: 2021, 2022 g10 Code GmbH 0007 SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de> 0008 0009 SPDX-License-Identifier: GPL-2.0-or-later 0010 */ 0011 0012 #include "certificatelineedit.h" 0013 0014 #include "commands/detailscommand.h" 0015 #include "dialogs/groupdetailsdialog.h" 0016 #include "utils/accessibility.h" 0017 #include "view/errorlabel.h" 0018 0019 #include <QAccessible> 0020 #include <QAction> 0021 #include <QCompleter> 0022 #include <QPushButton> 0023 #include <QSignalBlocker> 0024 0025 #include "kleopatra_debug.h" 0026 0027 #include <Libkleo/Debug> 0028 #include <Libkleo/Formatting> 0029 #include <Libkleo/KeyCache> 0030 #include <Libkleo/KeyFilter> 0031 #include <Libkleo/KeyGroup> 0032 #include <Libkleo/KeyList> 0033 #include <Libkleo/KeyListModel> 0034 #include <Libkleo/KeyListSortFilterProxyModel> 0035 0036 #include <KLocalizedString> 0037 0038 #include <gpgme++/key.h> 0039 #include <gpgme++/keylistresult.h> 0040 0041 #include <QGpgME/KeyListJob> 0042 #include <QGpgME/Protocol> 0043 0044 #include <QHBoxLayout> 0045 #include <QLineEdit> 0046 #include <QMenu> 0047 #include <QToolButton> 0048 0049 using namespace Kleo; 0050 using namespace GpgME; 0051 0052 Q_DECLARE_METATYPE(GpgME::Key) 0053 Q_DECLARE_METATYPE(KeyGroup) 0054 0055 static QStringList s_lookedUpKeys; 0056 0057 namespace 0058 { 0059 class CompletionProxyModel : public KeyListSortFilterProxyModel 0060 { 0061 Q_OBJECT 0062 0063 public: 0064 CompletionProxyModel(QObject *parent = nullptr) 0065 : KeyListSortFilterProxyModel(parent) 0066 { 0067 } 0068 0069 int columnCount(const QModelIndex &parent = QModelIndex()) const override 0070 { 0071 Q_UNUSED(parent) 0072 // pretend that there is only one column to workaround a bug in 0073 // QAccessibleTable which provides the accessibility interface for the 0074 // completion pop-up 0075 return 1; 0076 } 0077 0078 QVariant data(const QModelIndex &idx, int role) const override 0079 { 0080 if (!idx.isValid()) { 0081 return QVariant(); 0082 } 0083 0084 switch (role) { 0085 case Qt::DecorationRole: { 0086 const auto key = KeyListSortFilterProxyModel::data(idx, KeyList::KeyRole).value<GpgME::Key>(); 0087 if (!key.isNull()) { 0088 return Kleo::Formatting::iconForUid(key.userID(0)); 0089 } 0090 0091 const auto group = KeyListSortFilterProxyModel::data(idx, KeyList::GroupRole).value<KeyGroup>(); 0092 if (!group.isNull()) { 0093 return QIcon::fromTheme(QStringLiteral("group")); 0094 } 0095 0096 Q_ASSERT(!key.isNull() || !group.isNull()); 0097 return QVariant(); 0098 } 0099 default: 0100 return KeyListSortFilterProxyModel::data(index(idx.row(), KeyList::Summary), role); 0101 } 0102 } 0103 0104 private: 0105 bool lessThan(const QModelIndex &left, const QModelIndex &right) const override 0106 { 0107 const auto leftKey = sourceModel()->data(left, KeyList::KeyRole).value<GpgME::Key>(); 0108 const auto leftGroup = leftKey.isNull() ? sourceModel()->data(left, KeyList::GroupRole).value<KeyGroup>() : KeyGroup{}; 0109 const auto rightKey = sourceModel()->data(right, KeyList::KeyRole).value<GpgME::Key>(); 0110 const auto rightGroup = rightKey.isNull() ? sourceModel()->data(right, KeyList::GroupRole).value<KeyGroup>() : KeyGroup{}; 0111 0112 // shouldn't happen, but still put null entries at the end 0113 if (leftKey.isNull() && leftGroup.isNull()) { 0114 return false; 0115 } 0116 if (rightKey.isNull() && rightGroup.isNull()) { 0117 return true; 0118 } 0119 0120 // first sort by the displayed name and/or email address 0121 const auto leftNameAndOrEmail = leftGroup.isNull() ? Formatting::nameAndEmailForSummaryLine(leftKey) : leftGroup.name(); 0122 const auto rightNameAndOrEmail = rightGroup.isNull() ? Formatting::nameAndEmailForSummaryLine(rightKey) : rightGroup.name(); 0123 const int cmp = QString::localeAwareCompare(leftNameAndOrEmail, rightNameAndOrEmail); 0124 if (cmp) { 0125 return cmp < 0; 0126 } 0127 // then sort groups before certificates 0128 if (!leftGroup.isNull() && !rightKey.isNull()) { 0129 return true; // left is group, right is certificate 0130 } 0131 if (!leftKey.isNull() && !rightGroup.isNull()) { 0132 return false; // left is certificate, right is group 0133 } 0134 0135 // if both are groups (with identical names) sort them by their ID 0136 if (!leftGroup.isNull() && !rightGroup.isNull()) { 0137 return leftGroup.id() < rightGroup.id(); 0138 } 0139 0140 // sort certificates with same name/email by validity and creation time 0141 const auto lUid = leftKey.userID(0); 0142 const auto rUid = rightKey.userID(0); 0143 if (lUid.validity() != rUid.validity()) { 0144 return lUid.validity() > rUid.validity(); 0145 } 0146 0147 /* Both have the same validity, check which one is newer. */ 0148 time_t leftTime = 0; 0149 for (const GpgME::Subkey &s : leftKey.subkeys()) { 0150 if (s.isBad()) { 0151 continue; 0152 } 0153 if (s.creationTime() > leftTime) { 0154 leftTime = s.creationTime(); 0155 } 0156 } 0157 time_t rightTime = 0; 0158 for (const GpgME::Subkey &s : rightKey.subkeys()) { 0159 if (s.isBad()) { 0160 continue; 0161 } 0162 if (s.creationTime() > rightTime) { 0163 rightTime = s.creationTime(); 0164 } 0165 } 0166 if (rightTime != leftTime) { 0167 return leftTime > rightTime; 0168 } 0169 0170 // as final resort we compare the fingerprints 0171 return strcmp(leftKey.primaryFingerprint(), rightKey.primaryFingerprint()) < 0; 0172 } 0173 }; 0174 0175 auto createSeparatorAction(QObject *parent) 0176 { 0177 auto action = new QAction{parent}; 0178 action->setSeparator(true); 0179 return action; 0180 } 0181 } // namespace 0182 0183 class CertificateLineEdit::Private 0184 { 0185 CertificateLineEdit *q; 0186 0187 public: 0188 enum class Status { 0189 Empty, //< text is empty 0190 Success, //< a certificate or group is set 0191 None, //< entered text does not match any certificates or groups 0192 Ambiguous, //< entered text matches multiple certificates or groups 0193 }; 0194 enum class CursorPositioning { 0195 MoveToEnd, 0196 KeepPosition, 0197 MoveToStart, 0198 Default = MoveToEnd, 0199 }; 0200 0201 explicit Private(CertificateLineEdit *qq, AbstractKeyListModel *model, KeyUsage::Flags usage, KeyFilter *filter); 0202 0203 QString text() const; 0204 0205 void setKey(const GpgME::Key &key); 0206 0207 void setGroup(const KeyGroup &group); 0208 0209 void setKeyFilter(const std::shared_ptr<KeyFilter> &filter); 0210 0211 void setAccessibleName(const QString &s); 0212 0213 private: 0214 void updateKey(CursorPositioning positioning); 0215 void editChanged(); 0216 void editFinished(); 0217 void checkLocate(); 0218 void onLocateJobResult(QGpgME::Job *job, const QString &email, const KeyListResult &result, const std::vector<GpgME::Key> &keys); 0219 void openDetailsDialog(); 0220 void setTextWithBlockedSignals(const QString &s, CursorPositioning positioning); 0221 void showContextMenu(const QPoint &pos); 0222 QString errorMessage() const; 0223 QIcon statusIcon() const; 0224 QString statusToolTip() const; 0225 void updateStatusAction(); 0226 void updateErrorLabel(); 0227 void updateAccessibleNameAndDescription(); 0228 0229 public: 0230 Status mStatus = Status::Empty; 0231 bool mEditingInProgress = false; 0232 GpgME::Key mKey; 0233 KeyGroup mGroup; 0234 0235 struct Ui { 0236 explicit Ui(QWidget *parent) 0237 : lineEdit{parent} 0238 , button{parent} 0239 , errorLabel{parent} 0240 { 0241 } 0242 0243 QLineEdit lineEdit; 0244 QToolButton button; 0245 ErrorLabel errorLabel; 0246 } ui; 0247 0248 private: 0249 QString mAccessibleName; 0250 KeyListSortFilterProxyModel *const mFilterModel; 0251 CompletionProxyModel *const mCompleterFilterModel; 0252 QCompleter *mCompleter = nullptr; 0253 std::shared_ptr<KeyFilter> mFilter; 0254 QAction *const mStatusAction; 0255 QAction *const mShowDetailsAction; 0256 QPointer<QGpgME::Job> mLocateJob; 0257 Formatting::IconProvider mIconProvider; 0258 }; 0259 0260 CertificateLineEdit::Private::Private(CertificateLineEdit *qq, AbstractKeyListModel *model, KeyUsage::Flags usage, KeyFilter *filter) 0261 : q{qq} 0262 , ui{qq} 0263 , mFilterModel{new KeyListSortFilterProxyModel{qq}} 0264 , mCompleterFilterModel{new CompletionProxyModel{qq}} 0265 , mCompleter{new QCompleter{qq}} 0266 , mFilter{std::shared_ptr<KeyFilter>{filter}} 0267 , mStatusAction{new QAction{qq}} 0268 , mShowDetailsAction{new QAction{qq}} 0269 , mIconProvider{usage} 0270 { 0271 ui.lineEdit.setPlaceholderText(i18n("Please enter a name or email address...")); 0272 ui.lineEdit.setClearButtonEnabled(true); 0273 ui.lineEdit.setContextMenuPolicy(Qt::CustomContextMenu); 0274 ui.lineEdit.addAction(mStatusAction, QLineEdit::LeadingPosition); 0275 0276 mCompleterFilterModel->setKeyFilter(mFilter); 0277 mCompleterFilterModel->setSourceModel(model); 0278 // initialize dynamic sorting 0279 mCompleterFilterModel->sort(0); 0280 mCompleter->setModel(mCompleterFilterModel); 0281 mCompleter->setFilterMode(Qt::MatchContains); 0282 mCompleter->setCaseSensitivity(Qt::CaseInsensitive); 0283 ui.lineEdit.setCompleter(mCompleter); 0284 0285 ui.button.setIcon(QIcon::fromTheme(QStringLiteral("resource-group-new"))); 0286 ui.button.setToolTip(i18n("Show certificate list")); 0287 ui.button.setAccessibleName(i18n("Show certificate list")); 0288 0289 ui.errorLabel.setVisible(false); 0290 0291 auto vbox = new QVBoxLayout{q}; 0292 vbox->setContentsMargins(0, 0, 0, 0); 0293 0294 auto l = new QHBoxLayout; 0295 l->setContentsMargins(0, 0, 0, 0); 0296 l->addWidget(&ui.lineEdit); 0297 l->addWidget(&ui.button); 0298 0299 vbox->addLayout(l); 0300 vbox->addWidget(&ui.errorLabel); 0301 0302 q->setFocusPolicy(ui.lineEdit.focusPolicy()); 0303 q->setFocusProxy(&ui.lineEdit); 0304 0305 mShowDetailsAction->setIcon(QIcon::fromTheme(QStringLiteral("help-about"))); 0306 mShowDetailsAction->setText(i18nc("@action:inmenu", "Show Details")); 0307 mShowDetailsAction->setEnabled(false); 0308 0309 mFilterModel->setSourceModel(model); 0310 mFilterModel->setFilterKeyColumn(KeyList::Summary); 0311 if (filter) { 0312 mFilterModel->setKeyFilter(mFilter); 0313 } 0314 0315 connect(KeyCache::instance().get(), &Kleo::KeyCache::keysMayHaveChanged, q, [this]() { 0316 updateKey(CursorPositioning::KeepPosition); 0317 }); 0318 connect(KeyCache::instance().get(), &Kleo::KeyCache::groupUpdated, q, [this](const KeyGroup &group) { 0319 if (!mGroup.isNull() && mGroup.source() == group.source() && mGroup.id() == group.id()) { 0320 setTextWithBlockedSignals(Formatting::summaryLine(group), CursorPositioning::KeepPosition); 0321 // queue the update to ensure that the model has been updated 0322 QMetaObject::invokeMethod( 0323 q, 0324 [this]() { 0325 updateKey(CursorPositioning::KeepPosition); 0326 }, 0327 Qt::QueuedConnection); 0328 } 0329 }); 0330 connect(KeyCache::instance().get(), &Kleo::KeyCache::groupRemoved, q, [this](const KeyGroup &group) { 0331 if (!mGroup.isNull() && mGroup.source() == group.source() && mGroup.id() == group.id()) { 0332 mGroup = KeyGroup(); 0333 QSignalBlocker blocky{&ui.lineEdit}; 0334 ui.lineEdit.clear(); 0335 // queue the update to ensure that the model has been updated 0336 QMetaObject::invokeMethod( 0337 q, 0338 [this]() { 0339 updateKey(CursorPositioning::KeepPosition); 0340 }, 0341 Qt::QueuedConnection); 0342 } 0343 }); 0344 connect(&ui.lineEdit, &QLineEdit::editingFinished, q, [this]() { 0345 // queue the call of editFinished() to ensure that QCompleter::activated is handled first 0346 QMetaObject::invokeMethod( 0347 q, 0348 [this]() { 0349 editFinished(); 0350 }, 0351 Qt::QueuedConnection); 0352 }); 0353 connect(&ui.lineEdit, &QLineEdit::textChanged, q, [this]() { 0354 editChanged(); 0355 }); 0356 connect(&ui.lineEdit, &QLineEdit::customContextMenuRequested, q, [this](const QPoint &pos) { 0357 showContextMenu(pos); 0358 }); 0359 connect(mStatusAction, &QAction::triggered, q, [this]() { 0360 openDetailsDialog(); 0361 }); 0362 connect(mShowDetailsAction, &QAction::triggered, q, [this]() { 0363 openDetailsDialog(); 0364 }); 0365 connect(&ui.button, &QToolButton::clicked, q, &CertificateLineEdit::certificateSelectionRequested); 0366 connect(mCompleter, qOverload<const QModelIndex &>(&QCompleter::activated), q, [this](const QModelIndex &index) { 0367 Key key = mCompleter->completionModel()->data(index, KeyList::KeyRole).value<Key>(); 0368 auto group = mCompleter->completionModel()->data(index, KeyList::GroupRole).value<KeyGroup>(); 0369 if (!key.isNull()) { 0370 q->setKey(key); 0371 } else if (!group.isNull()) { 0372 q->setGroup(group); 0373 } else { 0374 qCDebug(KLEOPATRA_LOG) << "Activated item is neither key nor group"; 0375 } 0376 // queue the call of editFinished() to ensure that QLineEdit finished its own work 0377 QMetaObject::invokeMethod( 0378 q, 0379 [this]() { 0380 editFinished(); 0381 }, 0382 Qt::QueuedConnection); 0383 }); 0384 updateKey(CursorPositioning::Default); 0385 } 0386 0387 void CertificateLineEdit::Private::openDetailsDialog() 0388 { 0389 if (!q->key().isNull()) { 0390 auto cmd = new Commands::DetailsCommand{q->key()}; 0391 cmd->setParentWidget(q); 0392 cmd->start(); 0393 } else if (!q->group().isNull()) { 0394 auto dlg = new Dialogs::GroupDetailsDialog{q}; 0395 dlg->setAttribute(Qt::WA_DeleteOnClose); 0396 dlg->setGroup(q->group()); 0397 dlg->show(); 0398 } 0399 } 0400 0401 void CertificateLineEdit::Private::setTextWithBlockedSignals(const QString &s, CursorPositioning positioning) 0402 { 0403 QSignalBlocker blocky{&ui.lineEdit}; 0404 const auto cursorPos = ui.lineEdit.cursorPosition(); 0405 ui.lineEdit.setText(s); 0406 switch (positioning) { 0407 case CursorPositioning::KeepPosition: 0408 ui.lineEdit.setCursorPosition(cursorPos); 0409 break; 0410 case CursorPositioning::MoveToStart: 0411 ui.lineEdit.setCursorPosition(0); 0412 break; 0413 case CursorPositioning::MoveToEnd: 0414 default:; // setText() already moved the cursor to the end of the line 0415 }; 0416 } 0417 0418 void CertificateLineEdit::Private::showContextMenu(const QPoint &pos) 0419 { 0420 if (QMenu *menu = ui.lineEdit.createStandardContextMenu()) { 0421 auto *const firstStandardAction = menu->actions().value(0); 0422 menu->insertActions(firstStandardAction, {mShowDetailsAction, createSeparatorAction(menu)}); 0423 menu->setAttribute(Qt::WA_DeleteOnClose); 0424 menu->popup(ui.lineEdit.mapToGlobal(pos)); 0425 } 0426 } 0427 0428 CertificateLineEdit::CertificateLineEdit(AbstractKeyListModel *model, KeyUsage::Flags usage, KeyFilter *filter, QWidget *parent) 0429 : QWidget{parent} 0430 , d{new Private{this, model, usage, filter}} 0431 { 0432 /* Take ownership of the model to prevent double deletion when the 0433 * filter models are deleted */ 0434 model->setParent(parent ? parent : this); 0435 } 0436 0437 CertificateLineEdit::~CertificateLineEdit() = default; 0438 0439 void CertificateLineEdit::Private::editChanged() 0440 { 0441 const bool editingStarted = !mEditingInProgress; 0442 mEditingInProgress = true; 0443 updateKey(CursorPositioning::Default); 0444 if (editingStarted) { 0445 Q_EMIT q->editingStarted(); 0446 } 0447 if (q->isEmpty()) { 0448 Q_EMIT q->cleared(); 0449 } 0450 } 0451 0452 void CertificateLineEdit::Private::editFinished() 0453 { 0454 // perform a first update with the "editing in progress" flag still set 0455 updateKey(CursorPositioning::MoveToStart); 0456 mEditingInProgress = false; 0457 checkLocate(); 0458 // perform another update with the "editing in progress" flag cleared 0459 // after a key locate may have been started; this makes sure that displaying 0460 // an error is delayed until the key locate job has finished 0461 updateKey(CursorPositioning::MoveToStart); 0462 } 0463 0464 void CertificateLineEdit::Private::checkLocate() 0465 { 0466 if (mStatus != Status::None) { 0467 // try to locate key only if text matches no local certificates or groups 0468 return; 0469 } 0470 0471 // Only check once per mailbox 0472 const auto mailText = ui.lineEdit.text().trimmed(); 0473 if (mailText.isEmpty() || s_lookedUpKeys.contains(mailText)) { 0474 return; 0475 } 0476 s_lookedUpKeys << mailText; 0477 if (mLocateJob) { 0478 mLocateJob->slotCancel(); 0479 mLocateJob.clear(); 0480 } 0481 auto job = QGpgME::openpgp()->locateKeysJob(); 0482 connect(job, &QGpgME::KeyListJob::result, q, [this, job, mailText](const KeyListResult &result, const std::vector<GpgME::Key> &keys) { 0483 onLocateJobResult(job, mailText, result, keys); 0484 }); 0485 if (auto err = job->start({mailText}, /*secretOnly=*/false)) { 0486 qCDebug(KLEOPATRA_LOG) << __func__ << "Error: Starting" << job << "for" << mailText << "failed with" << Formatting::errorAsString(err); 0487 } else { 0488 mLocateJob = job; 0489 qCDebug(KLEOPATRA_LOG) << __func__ << "Started" << job << "for" << mailText; 0490 } 0491 } 0492 0493 void CertificateLineEdit::Private::onLocateJobResult(QGpgME::Job *job, const QString &email, const KeyListResult &result, const std::vector<GpgME::Key> &keys) 0494 { 0495 if (mLocateJob != job) { 0496 qCDebug(KLEOPATRA_LOG) << __func__ << "Ignoring outdated finished" << job << "for" << email; 0497 return; 0498 } 0499 qCDebug(KLEOPATRA_LOG) << __func__ << job << "for" << email << "finished with" << Formatting::errorAsString(result.error()) << "and keys" << keys; 0500 mLocateJob.clear(); 0501 if (!keys.empty() && !keys.front().isNull()) { 0502 KeyCache::mutableInstance()->insert(keys.front()); 0503 // inserting the key implicitly triggers an update 0504 } else { 0505 // explicitly trigger an update to display "no key" error 0506 updateKey(CursorPositioning::MoveToStart); 0507 } 0508 } 0509 0510 void CertificateLineEdit::Private::updateKey(CursorPositioning positioning) 0511 { 0512 static const _detail::ByFingerprint<std::equal_to> keysHaveSameFingerprint; 0513 0514 const auto mailText = ui.lineEdit.text().trimmed(); 0515 auto newKey = Key(); 0516 auto newGroup = KeyGroup(); 0517 if (mailText.isEmpty()) { 0518 mStatus = Status::Empty; 0519 } else { 0520 mFilterModel->setFilterRegularExpression(QRegularExpression::escape(mailText)); 0521 if (mFilterModel->rowCount() > 1) { 0522 // keep current key or group if they still match 0523 if (!mKey.isNull()) { 0524 for (int row = 0; row < mFilterModel->rowCount(); ++row) { 0525 const QModelIndex index = mFilterModel->index(row, 0); 0526 Key key = mFilterModel->key(index); 0527 if (!key.isNull() && keysHaveSameFingerprint(key, mKey)) { 0528 newKey = mKey; 0529 break; 0530 } 0531 } 0532 } else if (!mGroup.isNull()) { 0533 newGroup = mGroup; 0534 for (int row = 0; row < mFilterModel->rowCount(); ++row) { 0535 const QModelIndex index = mFilterModel->index(row, 0); 0536 KeyGroup group = mFilterModel->group(index); 0537 if (!group.isNull() && group.source() == mGroup.source() && group.id() == mGroup.id()) { 0538 newGroup = mGroup; 0539 break; 0540 } 0541 } 0542 } 0543 if (newKey.isNull() && newGroup.isNull()) { 0544 mStatus = Status::Ambiguous; 0545 } 0546 } else if (mFilterModel->rowCount() == 1) { 0547 const auto index = mFilterModel->index(0, 0); 0548 newKey = mFilterModel->data(index, KeyList::KeyRole).value<Key>(); 0549 newGroup = mFilterModel->data(index, KeyList::GroupRole).value<KeyGroup>(); 0550 Q_ASSERT(!newKey.isNull() || !newGroup.isNull()); 0551 if (newKey.isNull() && newGroup.isNull()) { 0552 mStatus = Status::None; 0553 } 0554 } else { 0555 mStatus = Status::None; 0556 } 0557 } 0558 mKey = newKey; 0559 mGroup = newGroup; 0560 0561 if (!mKey.isNull()) { 0562 /* FIXME: This needs to be solved by a multiple UID supporting model */ 0563 mStatus = Status::Success; 0564 ui.lineEdit.setToolTip(Formatting::toolTip(mKey, Formatting::ToolTipOption::AllOptions)); 0565 if (!mEditingInProgress) { 0566 setTextWithBlockedSignals(Formatting::summaryLine(mKey), positioning); 0567 } 0568 } else if (!mGroup.isNull()) { 0569 mStatus = Status::Success; 0570 ui.lineEdit.setToolTip(Formatting::toolTip(mGroup, Formatting::ToolTipOption::AllOptions)); 0571 if (!mEditingInProgress) { 0572 setTextWithBlockedSignals(Formatting::summaryLine(mGroup), positioning); 0573 } 0574 } else { 0575 ui.lineEdit.setToolTip({}); 0576 } 0577 0578 mShowDetailsAction->setEnabled(mStatus == Status::Success); 0579 updateStatusAction(); 0580 updateErrorLabel(); 0581 0582 Q_EMIT q->keyChanged(); 0583 } 0584 0585 QString CertificateLineEdit::Private::errorMessage() const 0586 { 0587 switch (mStatus) { 0588 case Status::Empty: 0589 case Status::Success: 0590 return {}; 0591 case Status::None: 0592 return i18n("No matching certificates or groups found"); 0593 case Status::Ambiguous: 0594 return i18n("Multiple matching certificates or groups found"); 0595 default: 0596 qDebug(KLEOPATRA_LOG) << __func__ << "Invalid status:" << static_cast<int>(mStatus); 0597 Q_ASSERT(!"Invalid status"); 0598 }; 0599 return {}; 0600 } 0601 0602 QIcon CertificateLineEdit::Private::statusIcon() const 0603 { 0604 switch (mStatus) { 0605 case Status::Empty: 0606 return QIcon::fromTheme(QStringLiteral("emblem-unavailable")); 0607 case Status::Success: 0608 if (!mKey.isNull()) { 0609 return mIconProvider.icon(mKey); 0610 } else if (!mGroup.isNull()) { 0611 return mIconProvider.icon(mGroup); 0612 } else { 0613 qDebug(KLEOPATRA_LOG) << __func__ << "Success, but neither key nor group."; 0614 return {}; 0615 } 0616 case Status::None: 0617 case Status::Ambiguous: 0618 if (mEditingInProgress || mLocateJob) { 0619 return QIcon::fromTheme(QStringLiteral("emblem-question")); 0620 } else { 0621 return QIcon::fromTheme(QStringLiteral("emblem-error")); 0622 } 0623 default: 0624 qDebug(KLEOPATRA_LOG) << __func__ << "Invalid status:" << static_cast<int>(mStatus); 0625 Q_ASSERT(!"Invalid status"); 0626 }; 0627 return {}; 0628 } 0629 0630 QString CertificateLineEdit::Private::statusToolTip() const 0631 { 0632 switch (mStatus) { 0633 case Status::Empty: 0634 return {}; 0635 case Status::Success: 0636 if (!mKey.isNull()) { 0637 return Formatting::validity(mKey.userID(0)); 0638 } else if (!mGroup.isNull()) { 0639 return Formatting::validity(mGroup); 0640 } else { 0641 qDebug(KLEOPATRA_LOG) << __func__ << "Success, but neither key nor group."; 0642 return {}; 0643 } 0644 case Status::None: 0645 case Status::Ambiguous: 0646 return errorMessage(); 0647 default: 0648 qDebug(KLEOPATRA_LOG) << __func__ << "Invalid status:" << static_cast<int>(mStatus); 0649 Q_ASSERT(!"Invalid status"); 0650 }; 0651 return {}; 0652 } 0653 0654 void CertificateLineEdit::Private::updateStatusAction() 0655 { 0656 mStatusAction->setIcon(statusIcon()); 0657 mStatusAction->setToolTip(statusToolTip()); 0658 } 0659 0660 namespace 0661 { 0662 QString decoratedError(const QString &text) 0663 { 0664 return text.isEmpty() ? QString() : i18nc("@info", "Error: %1", text); 0665 } 0666 } 0667 0668 void CertificateLineEdit::Private::updateErrorLabel() 0669 { 0670 const auto currentErrorMessage = ui.errorLabel.text(); 0671 const auto newErrorMessage = decoratedError(errorMessage()); 0672 if (newErrorMessage == currentErrorMessage) { 0673 return; 0674 } 0675 if (currentErrorMessage.isEmpty() && (mEditingInProgress || mLocateJob)) { 0676 // delay showing the error message until editing is finished, so that we 0677 // do not annoy the user with an error message while they are still 0678 // entering the recipient; 0679 // on the other hand, we clear the error message immediately if it does 0680 // not apply anymore and we update the error message immediately if it 0681 // changed 0682 return; 0683 } 0684 ui.errorLabel.setVisible(!newErrorMessage.isEmpty()); 0685 ui.errorLabel.setText(newErrorMessage); 0686 updateAccessibleNameAndDescription(); 0687 } 0688 0689 void CertificateLineEdit::Private::setAccessibleName(const QString &s) 0690 { 0691 mAccessibleName = s; 0692 updateAccessibleNameAndDescription(); 0693 } 0694 0695 void CertificateLineEdit::Private::updateAccessibleNameAndDescription() 0696 { 0697 // fall back to default accessible name if accessible name wasn't set explicitly 0698 if (mAccessibleName.isEmpty()) { 0699 mAccessibleName = getAccessibleName(&ui.lineEdit); 0700 } 0701 const bool errorShown = ui.errorLabel.isVisible(); 0702 0703 // Qt does not support "described-by" relations (like WCAG's "aria-describedby" relationship attribute); 0704 // emulate this by setting the error message as accessible description of the input field 0705 const auto description = errorShown ? ui.errorLabel.text() : QString{}; 0706 if (ui.lineEdit.accessibleDescription() != description) { 0707 ui.lineEdit.setAccessibleDescription(description); 0708 } 0709 0710 // Qt does not support IA2's "invalid entry" state (like WCAG's "aria-invalid" state attribute); 0711 // screen readers say something like "invalid data" if this state is set; 0712 // emulate this by adding "invalid data" to the accessible name of the input field 0713 const auto name = errorShown ? mAccessibleName + QLatin1StringView{", "} + invalidEntryText() // 0714 : mAccessibleName; 0715 if (ui.lineEdit.accessibleName() != name) { 0716 ui.lineEdit.setAccessibleName(name); 0717 } 0718 } 0719 0720 Key CertificateLineEdit::key() const 0721 { 0722 if (isEnabled()) { 0723 return d->mKey; 0724 } else { 0725 return Key(); 0726 } 0727 } 0728 0729 KeyGroup CertificateLineEdit::group() const 0730 { 0731 if (isEnabled()) { 0732 return d->mGroup; 0733 } else { 0734 return KeyGroup(); 0735 } 0736 } 0737 0738 QString CertificateLineEdit::Private::text() const 0739 { 0740 return ui.lineEdit.text().trimmed(); 0741 } 0742 0743 QString CertificateLineEdit::text() const 0744 { 0745 return d->text(); 0746 } 0747 0748 void CertificateLineEdit::Private::setKey(const Key &key) 0749 { 0750 mKey = key; 0751 mGroup = KeyGroup(); 0752 qCDebug(KLEOPATRA_LOG) << "Setting Key. " << Formatting::summaryLine(key); 0753 // position cursor, so that that the start of the summary is visible 0754 setTextWithBlockedSignals(Formatting::summaryLine(key), CursorPositioning::MoveToStart); 0755 updateKey(CursorPositioning::MoveToStart); 0756 } 0757 0758 void CertificateLineEdit::setKey(const Key &key) 0759 { 0760 d->setKey(key); 0761 } 0762 0763 void CertificateLineEdit::Private::setGroup(const KeyGroup &group) 0764 { 0765 mGroup = group; 0766 mKey = Key(); 0767 const QString summary = Formatting::summaryLine(group); 0768 qCDebug(KLEOPATRA_LOG) << "Setting KeyGroup. " << summary; 0769 // position cursor, so that that the start of the summary is visible 0770 setTextWithBlockedSignals(summary, CursorPositioning::MoveToStart); 0771 updateKey(CursorPositioning::MoveToStart); 0772 } 0773 0774 void CertificateLineEdit::setGroup(const KeyGroup &group) 0775 { 0776 d->setGroup(group); 0777 } 0778 0779 bool CertificateLineEdit::isEmpty() const 0780 { 0781 return d->mStatus == Private::Status::Empty; 0782 } 0783 0784 bool CertificateLineEdit::isEditingInProgress() const 0785 { 0786 return d->mEditingInProgress; 0787 } 0788 0789 bool CertificateLineEdit::hasAcceptableInput() const 0790 { 0791 return d->mStatus == Private::Status::Empty // 0792 || d->mStatus == Private::Status::Success; 0793 } 0794 0795 void CertificateLineEdit::Private::setKeyFilter(const std::shared_ptr<KeyFilter> &filter) 0796 { 0797 mFilter = filter; 0798 mFilterModel->setKeyFilter(filter); 0799 mCompleterFilterModel->setKeyFilter(mFilter); 0800 updateKey(CursorPositioning::Default); 0801 } 0802 0803 void CertificateLineEdit::setKeyFilter(const std::shared_ptr<KeyFilter> &filter) 0804 { 0805 d->setKeyFilter(filter); 0806 } 0807 0808 void CertificateLineEdit::setAccessibleNameOfLineEdit(const QString &name) 0809 { 0810 d->setAccessibleName(name); 0811 } 0812 0813 #include "certificatelineedit.moc" 0814 0815 #include "moc_certificatelineedit.cpp"