diff --git a/src/crypto/gui/certificatelineedit.cpp b/src/crypto/gui/certificatelineedit.cpp index 13755189b..848893a84 100644 --- a/src/crypto/gui/certificatelineedit.cpp +++ b/src/crypto/gui/certificatelineedit.cpp @@ -1,695 +1,695 @@ /* 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 "utils/accessibility.h" #include "view/errorlabel.h" #include #include #include #include #include #include "kleopatra_debug.h" #include #include #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: enum class Status { Empty, //< text is empty Success, //< a certificate or group is set None, //< entered text does not match any certificates or groups Ambiguous, //< entered text matches multiple certificates or groups }; 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 setAccessibleName(const QString &s); private: void updateKey(); void editChanged(); void editFinished(); void checkLocate(); void onLocateJobResult(QGpgME::Job *job, const QString &email, const KeyListResult &result, const std::vector &keys); void openDetailsDialog(); void setTextWithBlockedSignals(const QString &s); void showContextMenu(const QPoint &pos); QString errorMessage() const; QIcon statusIcon() const; QString statusToolTip() const; void updateStatusAction(); void updateErrorLabel(); void updateAccessibleNameAndDescription(); public: Status mStatus = Status::Empty; GpgME::Key mKey; KeyGroup mGroup; struct Ui { explicit Ui(QWidget *parent) : lineEdit{parent} , button{parent} , errorLabel{parent} {} QLineEdit lineEdit; QToolButton button; ErrorLabel errorLabel; } ui; private: QString mAccessibleName; KeyListSortFilterProxyModel *const mFilterModel; KeyListSortFilterProxyModel *const mCompleterFilterModel; QCompleter *mCompleter = nullptr; std::shared_ptr mFilter; bool mEditingInProgress = false; QAction *const mStatusAction; QAction *const mShowDetailsAction; QPointer mLocateJob; }; 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}} , mStatusAction{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(mStatusAction, 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")); ui.errorLabel.setVisible(false); auto vbox = new QVBoxLayout{q}; vbox->setContentsMargins(0, 0, 0, 0); auto l = new QHBoxLayout; l->setContentsMargins(0, 0, 0, 0); l->addWidget(&ui.lineEdit); l->addWidget(&ui.button); vbox->addLayout(l); vbox->addWidget(&ui.errorLabel); 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::keysMayHaveChanged, 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(mStatusAction, &QAction::triggered, q, [this]() { openDetailsDialog(); }); 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() { const bool editingStarted = !mEditingInProgress; mEditingInProgress = true; updateKey(); if (editingStarted) { Q_EMIT q->editingStarted(); } } void CertificateLineEdit::Private::editFinished() { // perform a first update with the "editing in progress" flag still set updateKey(); mEditingInProgress = false; checkLocate(); // perform another update with the "editing in progress" flag cleared // after a key locate may have been started; this makes sure that displaying // an error is delayed until the key locate job has finished updateKey(); } void CertificateLineEdit::Private::checkLocate() { if (mStatus != Status::None) { // try to locate key only if text matches no local certificates or groups return; } // Only check once per mailbox const auto mailText = ui.lineEdit.text().trimmed(); if (mailText.isEmpty() || s_lookedUpKeys.contains(mailText)) { return; } s_lookedUpKeys << mailText; if (mLocateJob) { mLocateJob->slotCancel(); mLocateJob.clear(); } auto job = QGpgME::openpgp()->locateKeysJob(); connect(job, &QGpgME::KeyListJob::result, q, [this, job, mailText](const KeyListResult &result, const std::vector &keys) { onLocateJobResult(job, mailText, result, keys); }); if (auto err = job->start({mailText}, /*secretOnly=*/false)) { qCDebug(KLEOPATRA_LOG) << __func__ << "Error: Starting" << job << "for" << mailText << "failed with" << err.asString(); } else { mLocateJob = job; qCDebug(KLEOPATRA_LOG) << __func__ << "Started" << job << "for" << mailText; } } void CertificateLineEdit::Private::onLocateJobResult(QGpgME::Job *job, const QString &email, const KeyListResult &result, const std::vector &keys) { if (mLocateJob != job) { qCDebug(KLEOPATRA_LOG) << __func__ << "Ignoring outdated finished" << job << "for" << email; return; } qCDebug(KLEOPATRA_LOG) << __func__ << job << "for" << email << "finished with" << result.error().asString() << "and keys" << keys; mLocateJob.clear(); if (!keys.empty() && !keys.front().isNull()) { KeyCache::mutableInstance()->insert(keys.front()); // inserting the key implicitly triggers an update } else { // explicitly trigger an update to display "no key" error updateKey(); } } void CertificateLineEdit::Private::updateKey() { static const _detail::ByFingerprint keysHaveSameFingerprint; const auto mailText = ui.lineEdit.text().trimmed(); auto newKey = Key(); auto newGroup = KeyGroup(); if (mailText.isEmpty()) { mStatus = Status::Empty; } else { - mFilterModel->setFilterFixedString(mailText); + mFilterModel->setFilterRegularExpression(QRegularExpression::escape(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()) { mStatus = Status::Ambiguous; } } 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()) { mStatus = Status::None; } } else { mStatus = Status::None; } } mKey = newKey; mGroup = newGroup; if (!mKey.isNull()) { /* FIXME: This needs to be solved by a multiple UID supporting model */ mStatus = Status::Success; ui.lineEdit.setToolTip(Formatting::toolTip(mKey, Formatting::ToolTipOption::AllOptions)); if (!mEditingInProgress) { setTextWithBlockedSignals(Formatting::summaryLine(mKey)); } } else if (!mGroup.isNull()) { mStatus = Status::Success; ui.lineEdit.setToolTip(Formatting::toolTip(mGroup, Formatting::ToolTipOption::AllOptions)); if (!mEditingInProgress) { setTextWithBlockedSignals(Formatting::summaryLine(mGroup)); } } else { ui.lineEdit.setToolTip({}); } mShowDetailsAction->setEnabled(mStatus == Status::Success); updateStatusAction(); updateErrorLabel(); Q_EMIT q->keyChanged(); } QString CertificateLineEdit::Private::errorMessage() const { switch (mStatus) { case Status::Empty: case Status::Success: return {}; case Status::None: return i18n("No matching certificates or groups found"); case Status::Ambiguous: return i18n("Multiple matching certificates or groups found"); default: qDebug(KLEOPATRA_LOG) << __func__ << "Invalid status:" << static_cast(mStatus); Q_ASSERT(!"Invalid status"); }; return {}; } QIcon CertificateLineEdit::Private::statusIcon() const { switch (mStatus) { case Status::Empty: return QIcon::fromTheme(QStringLiteral("emblem-unavailable")); case Status::Success: if (!mKey.isNull()) { return Formatting::iconForUid(mKey.userID(0)); } else if (!mGroup.isNull()) { return Formatting::validityIcon(mGroup); } else { qDebug(KLEOPATRA_LOG) << __func__ << "Success, but neither key nor group."; return {}; } case Status::None: case Status::Ambiguous: if (mEditingInProgress || mLocateJob) { return QIcon::fromTheme(QStringLiteral("emblem-question")); } else { return QIcon::fromTheme(QStringLiteral("emblem-error")); } default: qDebug(KLEOPATRA_LOG) << __func__ << "Invalid status:" << static_cast(mStatus); Q_ASSERT(!"Invalid status"); }; return {}; } QString CertificateLineEdit::Private::statusToolTip() const { switch (mStatus) { case Status::Empty: return {}; case Status::Success: if (!mKey.isNull()) { return Formatting::validity(mKey.userID(0)); } else if (!mGroup.isNull()) { return Formatting::validity(mGroup); } else { qDebug(KLEOPATRA_LOG) << __func__ << "Success, but neither key nor group."; return {}; } case Status::None: case Status::Ambiguous: return errorMessage(); default: qDebug(KLEOPATRA_LOG) << __func__ << "Invalid status:" << static_cast(mStatus); Q_ASSERT(!"Invalid status"); }; return {}; } void CertificateLineEdit::Private::updateStatusAction() { mStatusAction->setIcon(statusIcon()); mStatusAction->setToolTip(statusToolTip()); } namespace { QString decoratedError(const QString &text) { return text.isEmpty() ? QString() : i18nc("@info", "Error: %1", text); } } void CertificateLineEdit::Private::updateErrorLabel() { const auto currentErrorMessage = ui.errorLabel.text(); const auto newErrorMessage = decoratedError(errorMessage()); if (newErrorMessage == currentErrorMessage) { return; } if (currentErrorMessage.isEmpty() && (mEditingInProgress || mLocateJob)) { // delay showing the error message until editing is finished, so that we // do not annoy the user with an error message while they are still // entering the recipient; // on the other hand, we clear the error message immediately if it does // not apply anymore and we update the error message immediately if it // changed return; } ui.errorLabel.setVisible(!newErrorMessage.isEmpty()); ui.errorLabel.setText(newErrorMessage); updateAccessibleNameAndDescription(); } void CertificateLineEdit::Private::setAccessibleName(const QString &s) { mAccessibleName = s; updateAccessibleNameAndDescription(); } void CertificateLineEdit::Private::updateAccessibleNameAndDescription() { // fall back to default accessible name if accessible name wasn't set explicitly if (mAccessibleName.isEmpty()) { mAccessibleName = getAccessibleName(&ui.lineEdit); } const bool errorShown = ui.errorLabel.isVisible(); // Qt does not support "described-by" relations (like WCAG's "aria-describedby" relationship attribute); // emulate this by setting the error message as accessible description of the input field const auto description = errorShown ? ui.errorLabel.text() : QString{}; if (ui.lineEdit.accessibleDescription() != description) { ui.lineEdit.setAccessibleDescription(description); } // Qt does not support IA2's "invalid entry" state (like WCAG's "aria-invalid" state attribute); // screen readers say something like "invalid data" if this state is set; // emulate this by adding "invalid data" to the accessible name of the input field const auto name = errorShown ? mAccessibleName + QLatin1String{", "} + invalidEntryText() : mAccessibleName; if (ui.lineEdit.accessibleName() != name) { ui.lineEdit.setAccessibleName(name); } } 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().trimmed(); } 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 d->mStatus == Private::Status::Empty; } bool CertificateLineEdit::hasAcceptableInput() const { return d->mStatus == Private::Status::Empty || d->mStatus == Private::Status::Success; } 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); } void CertificateLineEdit::setAccessibleNameOfLineEdit(const QString &name) { d->setAccessibleName(name); } #include "certificatelineedit.moc" diff --git a/src/view/keytreeview.cpp b/src/view/keytreeview.cpp index 81e7ccbde..24dedd5ce 100644 --- a/src/view/keytreeview.cpp +++ b/src/view/keytreeview.cpp @@ -1,800 +1,800 @@ /* -*- mode: c++; c-basic-offset:4 -*- view/keytreeview.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2009 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "keytreeview.h" #include "searchbar.h" #include #include #include #include #include #include "utils/headerview.h" #include "utils/tags.h" #include #include #include #include #include "kleopatra_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define TAGS_COLUMN 13 using namespace Kleo; using namespace GpgME; Q_DECLARE_METATYPE(GpgME::Key) namespace { class TreeView : public QTreeView { public: explicit TreeView(QWidget *parent = nullptr) : QTreeView(parent) { header()->installEventFilter(this); } QSize minimumSizeHint() const override { const QSize min = QTreeView::minimumSizeHint(); return QSize(min.width(), min.height() + 5 * fontMetrics().height()); } protected: bool eventFilter(QObject *watched, QEvent *event) override { Q_UNUSED(watched) if (event->type() == QEvent::ContextMenu) { auto e = static_cast(event); if (!mHeaderPopup) { mHeaderPopup = new QMenu(this); mHeaderPopup->setTitle(i18n("View Columns")); for (int i = 0; i < model()->columnCount(); ++i) { QAction *tmp = mHeaderPopup->addAction(model()->headerData(i, Qt::Horizontal).toString()); tmp->setData(QVariant(i)); tmp->setCheckable(true); mColumnActions << tmp; } connect(mHeaderPopup, &QMenu::triggered, this, [this] (QAction *action) { const int col = action->data().toInt(); if ((col == TAGS_COLUMN) && action->isChecked()) { Tags::enableTags(); } if (action->isChecked()) { showColumn(col); } else { hideColumn(col); } auto tv = qobject_cast (parent()); if (tv) { tv->resizeColumns(); } }); } for (QAction *action : std::as_const(mColumnActions)) { const int column = action->data().toInt(); action->setChecked(!isColumnHidden(column)); } mHeaderPopup->popup(mapToGlobal(e->pos())); return true; } return false; } void keyPressEvent(QKeyEvent *event) override; QModelIndex moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers) override; private: bool mMoveCursorUpdatedView = false; QMenu *mHeaderPopup = nullptr; QList mColumnActions; }; void TreeView::keyPressEvent(QKeyEvent *event) { mMoveCursorUpdatedView = false; QTreeView::keyPressEvent(event); if (mMoveCursorUpdatedView) { event->accept(); } } QModelIndex TreeView::moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers) { // this code is based heavily on QTreeView::moveCursor() QModelIndex current = currentIndex(); if (!current.isValid()) { // let QTreeView handle invalid current index return QTreeView::moveCursor(cursorAction, modifiers); } if (isRightToLeft()) { if (cursorAction == MoveRight) { cursorAction = MoveLeft; } else if (cursorAction == MoveLeft) { cursorAction = MoveRight; } } switch (cursorAction) { case MoveLeft: { // HACK: call QTreeView::moveCursor with invalid cursor action to make it call the private executePostedLayout() (void) QTreeView::moveCursor(static_cast(-1), modifiers); int visualColumn = header()->visualIndex(current.column()) - 1; while (visualColumn >= 0 && isColumnHidden(header()->logicalIndex(visualColumn))) { visualColumn--; } int newColumn = header()->logicalIndex(visualColumn); QModelIndex next = current.sibling(current.row(), newColumn); if (next.isValid()) { return next; } //last restort: we change the scrollbar value QScrollBar *sb = horizontalScrollBar(); int oldValue = sb->value(); sb->setValue(sb->value() - sb->singleStep()); if (oldValue != sb->value()) { mMoveCursorUpdatedView = true; } updateGeometries(); viewport()->update(); break; } case MoveRight: { // HACK: call QTreeView::moveCursor with invalid cursor action to make it call the private executePostedLayout() (void) QTreeView::moveCursor(static_cast(-1), modifiers); int visualColumn = header()->visualIndex(current.column()) + 1; while (visualColumn < model()->columnCount(current.parent()) && isColumnHidden(header()->logicalIndex(visualColumn))) { visualColumn++; } const int newColumn = header()->logicalIndex(visualColumn); const QModelIndex next = current.sibling(current.row(), newColumn); if (next.isValid()) { return next; } //last restort: we change the scrollbar value QScrollBar *sb = horizontalScrollBar(); int oldValue = sb->value(); sb->setValue(sb->value() + sb->singleStep()); if (oldValue != sb->value()) { mMoveCursorUpdatedView = true; } updateGeometries(); viewport()->update(); break; } default: return QTreeView::moveCursor(cursorAction, modifiers); } return current; } const KeyListModelInterface * keyListModel(const QTreeView &view) { const KeyListModelInterface *const klmi = dynamic_cast(view.model()); Q_ASSERT(klmi); return klmi; } } // anon namespace KeyTreeView::KeyTreeView(QWidget *parent) : QWidget(parent), m_proxy(new KeyListSortFilterProxyModel(this)), m_additionalProxy(nullptr), m_view(new TreeView(this)), m_flatModel(nullptr), m_hierarchicalModel(nullptr), m_stringFilter(), m_keyFilter(), m_isHierarchical(true) { init(); } KeyTreeView::KeyTreeView(const KeyTreeView &other) : QWidget(nullptr), m_proxy(new KeyListSortFilterProxyModel(this)), m_additionalProxy(other.m_additionalProxy ? other.m_additionalProxy->clone() : nullptr), m_view(new TreeView(this)), m_flatModel(other.m_flatModel), m_hierarchicalModel(other.m_hierarchicalModel), m_stringFilter(other.m_stringFilter), m_keyFilter(other.m_keyFilter), m_group(other.m_group), m_isHierarchical(other.m_isHierarchical) { init(); setColumnSizes(other.columnSizes()); setSortColumn(other.sortColumn(), other.sortOrder()); } KeyTreeView::KeyTreeView(const QString &text, const std::shared_ptr &kf, AbstractKeyListSortFilterProxyModel *proxy, QWidget *parent, const KConfigGroup &group) : QWidget(parent), m_proxy(new KeyListSortFilterProxyModel(this)), m_additionalProxy(proxy), m_view(new TreeView(this)), m_flatModel(nullptr), m_hierarchicalModel(nullptr), m_stringFilter(text), m_keyFilter(kf), m_group(group), m_isHierarchical(true), m_onceResized(false) { init(); } void KeyTreeView::setColumnSizes(const std::vector &sizes) { if (sizes.empty()) { return; } Q_ASSERT(m_view); Q_ASSERT(m_view->header()); Q_ASSERT(qobject_cast(m_view->header()) == static_cast(m_view->header())); if (auto const hv = static_cast(m_view->header())) { hv->setSectionSizes(sizes); } } void KeyTreeView::setSortColumn(int sortColumn, Qt::SortOrder sortOrder) { Q_ASSERT(m_view); m_view->sortByColumn(sortColumn, sortOrder); } int KeyTreeView::sortColumn() const { Q_ASSERT(m_view); Q_ASSERT(m_view->header()); return m_view->header()->sortIndicatorSection(); } Qt::SortOrder KeyTreeView::sortOrder() const { Q_ASSERT(m_view); Q_ASSERT(m_view->header()); return m_view->header()->sortIndicatorOrder(); } std::vector KeyTreeView::columnSizes() const { Q_ASSERT(m_view); Q_ASSERT(m_view->header()); Q_ASSERT(qobject_cast(m_view->header()) == static_cast(m_view->header())); if (auto const hv = static_cast(m_view->header())) { return hv->sectionSizes(); } else { return std::vector(); } } void KeyTreeView::init() { KDAB_SET_OBJECT_NAME(m_proxy); KDAB_SET_OBJECT_NAME(m_view); if (m_group.isValid()) { // Reopen as non const KConfig *conf = m_group.config(); m_group = conf->group(m_group.name()); } if (m_additionalProxy && m_additionalProxy->objectName().isEmpty()) { KDAB_SET_OBJECT_NAME(m_additionalProxy); } QLayout *layout = new QVBoxLayout(this); KDAB_SET_OBJECT_NAME(layout); layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(m_view); auto headerView = new HeaderView(Qt::Horizontal); KDAB_SET_OBJECT_NAME(headerView); headerView->installEventFilter(m_view); headerView->setSectionsMovable(true); m_view->setHeader(headerView); m_view->setSelectionBehavior(QAbstractItemView::SelectRows); m_view->setSelectionMode(QAbstractItemView::ExtendedSelection); m_view->setAllColumnsShowFocus(false); m_view->setSortingEnabled(true); m_view->setAccessibleName(i18n("Certificates")); m_view->setAccessibleDescription(m_isHierarchical ? i18n("Hierarchical list of certificates") : i18n("List of certificates")); // we show details on double-click m_view->setExpandsOnDoubleClick(false); if (model()) { if (m_additionalProxy) { m_additionalProxy->setSourceModel(model()); } else { m_proxy->setSourceModel(model()); } } if (m_additionalProxy) { m_proxy->setSourceModel(m_additionalProxy); if (!m_additionalProxy->parent()) { m_additionalProxy->setParent(this); } } - m_proxy->setFilterFixedString(m_stringFilter); + m_proxy->setFilterRegularExpression(QRegularExpression::escape(m_stringFilter)); m_proxy->setKeyFilter(m_keyFilter); m_proxy->setSortCaseSensitivity(Qt::CaseInsensitive); auto rearangingModel = new KeyRearrangeColumnsProxyModel(this); rearangingModel->setSourceModel(m_proxy); rearangingModel->setSourceColumns(QVector() << KeyList::PrettyName << KeyList::PrettyEMail << KeyList::Validity << KeyList::ValidFrom << KeyList::ValidUntil << KeyList::TechnicalDetails << KeyList::KeyID << KeyList::Fingerprint << KeyList::OwnerTrust << KeyList::Origin << KeyList::LastUpdate << KeyList::Issuer << KeyList::SerialNumber // If a column is added before this TAGS_COLUMN define has to be updated accordingly << KeyList::Remarks ); m_view->setModel(rearangingModel); /* Handle expansion state */ if (m_group.isValid()) { m_expandedKeys = m_group.readEntry("Expanded", QStringList()); } connect(m_view, &QTreeView::expanded, this, [this] (const QModelIndex &index) { if (!index.isValid()) { return; } const auto &key = index.data(KeyList::KeyRole).value(); if (key.isNull()) { return; } const auto fpr = QString::fromLatin1(key.primaryFingerprint()); if (m_expandedKeys.contains(fpr)) { return; } m_expandedKeys << fpr; if (m_group.isValid()) { m_group.writeEntry("Expanded", m_expandedKeys); } }); connect(m_view, &QTreeView::collapsed, this, [this] (const QModelIndex &index) { if (!index.isValid()) { return; } const auto &key = index.data(KeyList::KeyRole).value(); if (key.isNull()) { return; } m_expandedKeys.removeAll(QString::fromLatin1(key.primaryFingerprint())); if (m_group.isValid()) { m_group.writeEntry("Expanded", m_expandedKeys); } }); connect(KeyCache::instance().get(), &KeyCache::keysMayHaveChanged, this, [this] () { /* We use a single shot timer here to ensure that the keysMayHaveChanged * handlers are all handled before we restore the expand state so that * the model is already populated. */ QTimer::singleShot(0, [this] () { restoreExpandState(); setUpTagKeys(); if (!m_onceResized) { m_onceResized = true; resizeColumns(); } }); }); resizeColumns(); if (m_group.isValid()) { restoreLayout(m_group); } } void KeyTreeView::restoreExpandState() { if (!KeyCache::instance()->initialized()) { qCWarning(KLEOPATRA_LOG) << "Restore expand state before keycache available. Aborting."; return; } for (const auto &fpr: std::as_const(m_expandedKeys)) { const KeyListModelInterface *const km = keyListModel(*m_view); if (!km) { qCWarning(KLEOPATRA_LOG) << "invalid model"; return; } const auto key = KeyCache::instance()->findByFingerprint(fpr.toLatin1().constData()); if (key.isNull()) { qCDebug(KLEOPATRA_LOG) << "Cannot find:" << fpr << "anymore in cache"; m_expandedKeys.removeAll(fpr); return; } const auto idx = km->index(key); if (!idx.isValid()) { qCDebug(KLEOPATRA_LOG) << "Cannot find:" << fpr << "anymore in model"; m_expandedKeys.removeAll(fpr); return; } m_view->expand(idx); } } void KeyTreeView::setUpTagKeys() { const auto tagKeys = Tags::tagKeys(); if (m_hierarchicalModel) { m_hierarchicalModel->setRemarkKeys(tagKeys); } if (m_flatModel) { m_flatModel->setRemarkKeys(tagKeys); } } void KeyTreeView::saveLayout(KConfigGroup &group) { QHeaderView *header = m_view->header(); QVariantList columnVisibility; QVariantList columnOrder; QVariantList columnWidths; const int headerCount = header->count(); columnVisibility.reserve(headerCount); columnWidths.reserve(headerCount); columnOrder.reserve(headerCount); for (int i = 0; i < headerCount; ++i) { columnVisibility << QVariant(!m_view->isColumnHidden(i)); columnWidths << QVariant(header->sectionSize(i)); columnOrder << QVariant(header->visualIndex(i)); } group.writeEntry("ColumnVisibility", columnVisibility); group.writeEntry("ColumnOrder", columnOrder); group.writeEntry("ColumnWidths", columnWidths); group.writeEntry("SortAscending", (int)header->sortIndicatorOrder()); if (header->isSortIndicatorShown()) { group.writeEntry("SortColumn", header->sortIndicatorSection()); } else { group.writeEntry("SortColumn", -1); } } void KeyTreeView::restoreLayout(const KConfigGroup &group) { QHeaderView *header = m_view->header(); QVariantList columnVisibility = group.readEntry("ColumnVisibility", QVariantList()); QVariantList columnOrder = group.readEntry("ColumnOrder", QVariantList()); QVariantList columnWidths = group.readEntry("ColumnWidths", QVariantList()); if (columnVisibility.isEmpty()) { // if config is empty then use default settings // The numbers have to be in line with the order in // setsSourceColumns above m_view->hideColumn(5); for (int i = 7; i < m_view->model()->columnCount(); ++i) { m_view->hideColumn(i); } if (KeyCache::instance()->initialized()) { QTimer::singleShot(0, this, &KeyTreeView::resizeColumns); } } else { for (int i = 0; i < header->count(); ++i) { if (i >= columnOrder.size() || i >= columnWidths.size() || i >= columnVisibility.size()) { // An additional column that was not around last time we saved. // We default to hidden. m_view->hideColumn(i); continue; } bool visible = columnVisibility[i].toBool(); int width = columnWidths[i].toInt(); int order = columnOrder[i].toInt(); header->resizeSection(i, width ? width : 100); header->moveSection(header->visualIndex(i), order); if ((i == TAGS_COLUMN) && visible) { Tags::enableTags(); } if (!visible) { m_view->hideColumn(i); } } m_onceResized = true; } int sortOrder = group.readEntry("SortAscending", (int)Qt::AscendingOrder); int sortColumn = group.readEntry("SortColumn", 0); if (sortColumn >= 0) { m_view->sortByColumn(sortColumn, (Qt::SortOrder)sortOrder); } } KeyTreeView::~KeyTreeView() { if (m_group.isValid()) { saveLayout(m_group); } } static QAbstractProxyModel *find_last_proxy(QAbstractProxyModel *pm) { Q_ASSERT(pm); while (auto const sm = qobject_cast(pm->sourceModel())) { pm = sm; } return pm; } void KeyTreeView::setFlatModel(AbstractKeyListModel *model) { if (model == m_flatModel) { return; } m_flatModel = model; if (!m_isHierarchical) // TODO: this fails when called after setHierarchicalView( false )... { find_last_proxy(m_proxy)->setSourceModel(model); } } void KeyTreeView::setHierarchicalModel(AbstractKeyListModel *model) { if (model == m_hierarchicalModel) { return; } m_hierarchicalModel = model; if (m_isHierarchical) { find_last_proxy(m_proxy)->setSourceModel(model); m_view->expandAll(); for (int column = 0; column < m_view->header()->count(); ++column) { m_view->header()->resizeSection(column, qMax(m_view->header()->sectionSize(column), m_view->header()->sectionSizeHint(column))); } } } void KeyTreeView::setStringFilter(const QString &filter) { if (filter == m_stringFilter) { return; } m_stringFilter = filter; - m_proxy->setFilterFixedString(filter); + m_proxy->setFilterRegularExpression(QRegularExpression::escape(filter)); Q_EMIT stringFilterChanged(filter); } void KeyTreeView::setKeyFilter(const std::shared_ptr &filter) { if (filter == m_keyFilter || (filter && m_keyFilter && filter->id() == m_keyFilter->id())) { return; } m_keyFilter = filter; m_proxy->setKeyFilter(filter); Q_EMIT keyFilterChanged(filter); } namespace { QItemSelection itemSelectionFromKeys(const std::vector &keys, const QTreeView &view) { const QModelIndexList indexes = keyListModel(view)->indexes(keys); return std::accumulate( indexes.cbegin(), indexes.cend(), QItemSelection(), [] (QItemSelection selection, const QModelIndex &index) { if (index.isValid()) { selection.merge(QItemSelection(index, index), QItemSelectionModel::Select); } return selection; }); } } void KeyTreeView::selectKeys(const std::vector &keys) { m_view->selectionModel()->select(itemSelectionFromKeys(keys, *m_view), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); } std::vector KeyTreeView::selectedKeys() const { return keyListModel(*m_view)->keys(m_view->selectionModel()->selectedRows()); } void KeyTreeView::setHierarchicalView(bool on) { if (on == m_isHierarchical) { return; } if (on && !hierarchicalModel()) { qCWarning(KLEOPATRA_LOG) << "hierarchical view requested, but no hierarchical model set"; return; } if (!on && !flatModel()) { qCWarning(KLEOPATRA_LOG) << "flat view requested, but no flat model set"; return; } const std::vector selectedKeys = this->selectedKeys(); const Key currentKey = keyListModel(*m_view)->key(m_view->currentIndex()); m_isHierarchical = on; find_last_proxy(m_proxy)->setSourceModel(model()); if (on) { m_view->expandAll(); } selectKeys(selectedKeys); if (!currentKey.isNull()) { const QModelIndex currentIndex = keyListModel(*m_view)->index(currentKey); if (currentIndex.isValid()) { m_view->selectionModel()->setCurrentIndex(currentIndex, QItemSelectionModel::NoUpdate); m_view->scrollTo(currentIndex); } } m_view->setAccessibleDescription(m_isHierarchical ? i18n("Hierarchical list of certificates") : i18n("List of certificates")); Q_EMIT hierarchicalChanged(on); } void KeyTreeView::setKeys(const std::vector &keys) { std::vector sorted = keys; _detail::sort_by_fpr(sorted); _detail::remove_duplicates_by_fpr(sorted); m_keys = sorted; if (m_flatModel) { m_flatModel->setKeys(sorted); } if (m_hierarchicalModel) { m_hierarchicalModel->setKeys(sorted); } } void KeyTreeView::addKeysImpl(const std::vector &keys, bool select) { if (keys.empty()) { return; } if (m_keys.empty()) { setKeys(keys); return; } std::vector sorted = keys; _detail::sort_by_fpr(sorted); _detail::remove_duplicates_by_fpr(sorted); std::vector newKeys = _detail::union_by_fpr(sorted, m_keys); m_keys.swap(newKeys); if (m_flatModel) { m_flatModel->addKeys(sorted); } if (m_hierarchicalModel) { m_hierarchicalModel->addKeys(sorted); } if (select) { selectKeys(sorted); } } void KeyTreeView::addKeysSelected(const std::vector &keys) { addKeysImpl(keys, true); } void KeyTreeView::addKeysUnselected(const std::vector &keys) { addKeysImpl(keys, false); } void KeyTreeView::removeKeys(const std::vector &keys) { if (keys.empty()) { return; } std::vector sorted = keys; _detail::sort_by_fpr(sorted); _detail::remove_duplicates_by_fpr(sorted); std::vector newKeys; newKeys.reserve(m_keys.size()); std::set_difference(m_keys.begin(), m_keys.end(), sorted.begin(), sorted.end(), std::back_inserter(newKeys), _detail::ByFingerprint()); m_keys.swap(newKeys); if (m_flatModel) { std::for_each(sorted.cbegin(), sorted.cend(), [this](const Key &key) { m_flatModel->removeKey(key); }); } if (m_hierarchicalModel) { std::for_each(sorted.cbegin(), sorted.cend(), [this](const Key &key) { m_hierarchicalModel->removeKey(key); }); } } void KeyTreeView::disconnectSearchBar() { for (const auto &connection : m_connections) { disconnect(connection); } m_connections.clear(); } bool KeyTreeView::connectSearchBar(const SearchBar *bar) { m_connections.reserve(4); m_connections.push_back(connect(this, &KeyTreeView::stringFilterChanged, bar, &SearchBar::setStringFilter)); m_connections.push_back(connect(bar, &SearchBar::stringFilterChanged, this, &KeyTreeView::setStringFilter)); m_connections.push_back(connect(this, &KeyTreeView::keyFilterChanged, bar, &SearchBar::setKeyFilter)); m_connections.push_back(connect(bar, &SearchBar::keyFilterChanged, this, &KeyTreeView::setKeyFilter)); return std::all_of(m_connections.cbegin(), m_connections.cend(), [](const QMetaObject::Connection &conn) { return conn; }); } void KeyTreeView::resizeColumns() { m_view->setColumnWidth(KeyList::PrettyName, 260); m_view->setColumnWidth(KeyList::PrettyEMail, 260); for (int i = 2; i < m_view->model()->columnCount(); ++i) { m_view->resizeColumnToContents(i); } }