diff --git a/src/crypto/gui/resultitemwidget.cpp b/src/crypto/gui/resultitemwidget.cpp index bc70ce568..68caca404 100644 --- a/src/crypto/gui/resultitemwidget.cpp +++ b/src/crypto/gui/resultitemwidget.cpp @@ -1,351 +1,353 @@ /* -*- mode: c++; c-basic-offset:4 -*- crypto/gui/resultitemwidget.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2016 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "resultitemwidget.h" #include "utils/auditlog.h" #include "commands/command.h" #include "commands/importcertificatefromfilecommand.h" #include "commands/lookupcertificatescommand.h" #include "crypto/decryptverifytask.h" #include "view/urllabel.h" #include #include #include #include #include #include #include #include "kleopatra_debug.h" #include #include #include #include #include #include using namespace Kleo; using namespace Kleo::Crypto; using namespace Kleo::Crypto::Gui; namespace { // TODO move out of here static QColor colorForVisualCode(Task::Result::VisualCode code) { switch (code) { case Task::Result::AllGood: return KColorScheme(QPalette::Active, KColorScheme::View).background(KColorScheme::PositiveBackground).color(); case Task::Result::NeutralError: case Task::Result::Warning: return KColorScheme(QPalette::Active, KColorScheme::View).background(KColorScheme::NormalBackground).color(); case Task::Result::Danger: return KColorScheme(QPalette::Active, KColorScheme::View).background(KColorScheme::NegativeBackground).color(); case Task::Result::NeutralSuccess: default: return QColor(0x00, 0x80, 0xFF); // light blue } } static QColor txtColorForVisualCode(Task::Result::VisualCode code) { switch (code) { case Task::Result::AllGood: return KColorScheme(QPalette::Active, KColorScheme::View).foreground(KColorScheme::PositiveText).color(); case Task::Result::NeutralError: case Task::Result::Warning: return KColorScheme(QPalette::Active, KColorScheme::View).foreground(KColorScheme::NormalText).color(); case Task::Result::Danger: return KColorScheme(QPalette::Active, KColorScheme::View).foreground(KColorScheme::NegativeText).color(); case Task::Result::NeutralSuccess: default: return QColor(0xFF, 0xFF, 0xFF); // white } } } class ResultItemWidget::Private { ResultItemWidget *const q; public: explicit Private(const std::shared_ptr &result, ResultItemWidget *qq) : q(qq), m_result(result) { Q_ASSERT(m_result); } void slotLinkActivated(const QString &); void updateShowDetailsLabel(); void addKeyImportButton(QBoxLayout *lay, bool search); void addIgnoreMDCButton(QBoxLayout *lay); void oneImportFinished(); const std::shared_ptr m_result; QLabel *m_detailsLabel = nullptr; UrlLabel *m_auditLogLabel = nullptr; QPushButton *m_closeButton = nullptr; bool m_importCanceled = false; }; void ResultItemWidget::Private::oneImportFinished() { if (m_importCanceled) { return; } if (m_result->parentTask()) { m_result->parentTask()->start(); } q->setVisible(false); } void ResultItemWidget::Private::addIgnoreMDCButton(QBoxLayout *lay) { if (!m_result || !lay) { return; } const auto dvResult = dynamic_cast(m_result.get()); if (!dvResult) { return; } const auto decResult = dvResult->decryptionResult(); if (decResult.isNull() || !decResult.error() || !decResult.isLegacyCipherNoMDC()) { return; } auto btn = new QPushButton(i18n("Force decryption")); btn->setFixedSize(btn->sizeHint()); connect (btn, &QPushButton::clicked, q, [this] () { if (m_result->parentTask()) { const auto dvTask = dynamic_cast(m_result->parentTask().data()); dvTask->setIgnoreMDCError(true); dvTask->start(); q->setVisible(false); } else { qCWarning(KLEOPATRA_LOG) << "Failed to get parent task"; } }); lay->addWidget(btn); } void ResultItemWidget::Private::addKeyImportButton(QBoxLayout *lay, bool search) { if (!m_result || !lay) { return; } const auto dvResult = dynamic_cast(m_result.get()); if (!dvResult) { return; } const auto verifyResult = dvResult->verificationResult(); if (verifyResult.isNull()) { return; } for (const auto &sig: verifyResult.signatures()) { if (!(sig.summary() & GpgME::Signature::KeyMissing)) { continue; } auto btn = new QPushButton; QString suffix; const auto keyid = QLatin1String(sig.fingerprint()); if (verifyResult.numSignatures() > 1) { suffix = QLatin1Char(' ') + keyid; } btn = new QPushButton(search ? i18nc("1 is optional keyid. No space is intended as it can be empty.", "Search%1", suffix) : i18nc("1 is optional keyid. No space is intended as it can be empty.", "Import%1", suffix)); if (search) { btn->setIcon(QIcon::fromTheme(QStringLiteral("edit-find"))); connect (btn, &QPushButton::clicked, q, [this, btn, keyid] () { btn->setEnabled(false); m_importCanceled = false; auto cmd = new Kleo::Commands::LookupCertificatesCommand(keyid, nullptr); connect(cmd, &Kleo::Commands::LookupCertificatesCommand::canceled, q, [this]() { m_importCanceled = true; }); connect(cmd, &Kleo::Commands::LookupCertificatesCommand::finished, q, [this, btn]() { btn->setEnabled(true); oneImportFinished(); }); cmd->setParentWidget(q); cmd->start(); }); } else { btn->setIcon(QIcon::fromTheme(QStringLiteral("view-certificate-import"))); connect (btn, &QPushButton::clicked, q, [this, btn] () { btn->setEnabled(false); m_importCanceled = false; auto cmd = new Kleo::ImportCertificateFromFileCommand(); connect(cmd, &Kleo::ImportCertificateFromFileCommand::canceled, q, [this]() { m_importCanceled = true; }); connect(cmd, &Kleo::ImportCertificateFromFileCommand::finished, q, [this, btn]() { btn->setEnabled(true); oneImportFinished(); }); cmd->setParentWidget(q); cmd->start(); }); } btn->setFixedSize(btn->sizeHint()); lay->addWidget(btn); } } static QUrl auditlog_url_template() { QUrl url(QStringLiteral("kleoresultitem://showauditlog")); return url; } void ResultItemWidget::Private::updateShowDetailsLabel() { const auto auditLogUrl = m_result->auditLog().asUrl(auditlog_url_template()); const auto auditLogLinkText = m_result->hasError() ? i18n("Diagnostics") : i18nc("The Audit Log is a detailed error log from the gnupg backend", "Show Audit Log"); m_auditLogLabel->setUrl(auditLogUrl, auditLogLinkText); m_auditLogLabel->setVisible(!auditLogUrl.isEmpty()); } ResultItemWidget::ResultItemWidget(const std::shared_ptr &result, QWidget *parent, Qt::WindowFlags flags) : QWidget(parent, flags), d(new Private(result, this)) { const QColor color = colorForVisualCode(d->m_result->code()); const QColor txtColor = txtColorForVisualCode(d->m_result->code()); const QString styleSheet = QStringLiteral("QFrame,QLabel { background-color: %1; margin: 0px; }" "QFrame#resultFrame{ border-color: %2; border-style: solid; border-radius: 3px; border-width: 1px }" "QLabel { color: %3; padding: 5px; border-radius: 3px }").arg(color.name()).arg(color.darker(150).name()).arg(txtColor.name()); auto topLayout = new QVBoxLayout(this); auto frame = new QFrame; frame->setObjectName(QStringLiteral("resultFrame")); frame->setStyleSheet(styleSheet); topLayout->addWidget(frame); auto layout = new QHBoxLayout(frame); auto vlay = new QVBoxLayout(); auto overview = new QLabel; overview->setWordWrap(true); overview->setTextFormat(Qt::RichText); overview->setText(d->m_result->overview()); overview->setFocusPolicy(Qt::StrongFocus); overview->setStyleSheet(styleSheet); + setFocusPolicy(overview->focusPolicy()); + setFocusProxy(overview); connect(overview, SIGNAL(linkActivated(QString)), this, SLOT(slotLinkActivated(QString))); vlay->addWidget(overview); layout->addLayout(vlay); const QString details = d->m_result->details(); auto actionLayout = new QVBoxLayout; layout->addLayout(actionLayout); d->addKeyImportButton(actionLayout, false); // TODO: Only show if auto-key-retrieve is not set. d->addKeyImportButton(actionLayout, true); d->addIgnoreMDCButton(actionLayout); d->m_auditLogLabel = new UrlLabel; connect(d->m_auditLogLabel, &UrlLabel::linkActivated, this, [this](const auto &link) { d->slotLinkActivated(link); }); actionLayout->addWidget(d->m_auditLogLabel); d->m_auditLogLabel->setStyleSheet(styleSheet); d->m_detailsLabel = new QLabel; d->m_detailsLabel->setWordWrap(true); d->m_detailsLabel->setTextFormat(Qt::RichText); d->m_detailsLabel->setText(details); d->m_detailsLabel->setFocusPolicy(Qt::StrongFocus); d->m_detailsLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); d->m_detailsLabel->setStyleSheet(styleSheet); connect(d->m_detailsLabel, SIGNAL(linkActivated(QString)), this, SLOT(slotLinkActivated(QString))); vlay->addWidget(d->m_detailsLabel); d->m_closeButton = new QPushButton; KGuiItem::assign(d->m_closeButton, KStandardGuiItem::close()); d->m_closeButton->setFixedSize(d->m_closeButton->sizeHint()); connect(d->m_closeButton, &QAbstractButton::clicked, this, &ResultItemWidget::closeButtonClicked); actionLayout->addWidget(d->m_closeButton); d->m_closeButton->setVisible(false); layout->setStretch(0, 1); actionLayout->addStretch(-1); vlay->addStretch(-1); d->updateShowDetailsLabel(); setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Maximum); } ResultItemWidget::~ResultItemWidget() { } void ResultItemWidget::showCloseButton(bool show) { d->m_closeButton->setVisible(show); } bool ResultItemWidget::hasErrorResult() const { return d->m_result->hasError(); } void ResultItemWidget::Private::slotLinkActivated(const QString &link) { Q_ASSERT(m_result); qCDebug(KLEOPATRA_LOG) << "Link activated: " << link; if (link.startsWith(QLatin1String("key:"))) { auto split = link.split(QLatin1Char(':')); auto fpr = split.value(1); if (split.size() == 2 && isFingerprint(fpr)) { /* There might be a security consideration here if somehow * a short keyid is used in a link and it collides with another. * So we additionally check that it really is a fingerprint. */ auto cmd = Command::commandForQuery(fpr); cmd->setParentWId(q->effectiveWinId()); cmd->start(); } else { qCWarning(KLEOPATRA_LOG) << "key link invalid " << link; } return; } const QUrl url(link); if (url.host() == QLatin1String("showauditlog")) { q->showAuditLog(); return; } qCWarning(KLEOPATRA_LOG) << "Unexpected link scheme: " << link; } void ResultItemWidget::showAuditLog() { MessageBox::auditLog(parentWidget(), d->m_result->auditLog().text()); } #include "moc_resultitemwidget.cpp" diff --git a/src/crypto/gui/resultlistwidget.cpp b/src/crypto/gui/resultlistwidget.cpp index 79f2c02e4..b4f2f8f0b 100644 --- a/src/crypto/gui/resultlistwidget.cpp +++ b/src/crypto/gui/resultlistwidget.cpp @@ -1,201 +1,210 @@ /* -*- mode: c++; c-basic-offset:4 -*- crypto/gui/resultlistwidget.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "resultlistwidget.h" #include "emailoperationspreferences.h" #include +#include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace Kleo::Crypto; using namespace Kleo::Crypto::Gui; class ResultListWidget::Private { ResultListWidget *const q; public: explicit Private(ResultListWidget *qq); void result(const std::shared_ptr &result); void started(const std::shared_ptr &task); void allTasksDone(); void addResultWidget(ResultItemWidget *widget); void resizeIfStandalone(); std::vector< std::shared_ptr > m_collections; bool m_standaloneMode = false; int m_lastErrorItemIndex = 0; ScrollArea *m_scrollArea = nullptr; QPushButton *m_closeButton = nullptr; QVBoxLayout *m_layout = nullptr; QLabel *m_progressLabel = nullptr; }; ResultListWidget::Private::Private(ResultListWidget *qq) : q(qq), m_collections() { m_layout = new QVBoxLayout(q); m_layout->setContentsMargins(0, 0, 0, 0); m_layout->setSpacing(0); m_scrollArea = new ScrollArea; m_scrollArea->setFocusPolicy(Qt::NoFocus); auto scrollAreaLayout = qobject_cast(m_scrollArea->widget()->layout()); Q_ASSERT(scrollAreaLayout); scrollAreaLayout->setContentsMargins(0, 0, 0, 0); scrollAreaLayout->setSpacing(2); scrollAreaLayout->addStretch(); m_layout->addWidget(m_scrollArea); m_progressLabel = new QLabel; m_progressLabel->setWordWrap(true); m_layout->addWidget(m_progressLabel); m_progressLabel->setVisible(false); m_closeButton = new QPushButton; KGuiItem::assign(m_closeButton, KStandardGuiItem::close()); q->connect(m_closeButton, &QPushButton::clicked, q, &ResultListWidget::close); m_layout->addWidget(m_closeButton); m_closeButton->setVisible(false); m_closeButton->setEnabled(false); } ResultListWidget::ResultListWidget(QWidget *parent, Qt::WindowFlags f) : QWidget(parent, f), d(new Private(this)) { } ResultListWidget::~ResultListWidget() { if (!d->m_standaloneMode) { return; } EMailOperationsPreferences prefs; prefs.setDecryptVerifyPopupGeometry(geometry()); prefs.save(); } void ResultListWidget::Private::resizeIfStandalone() { if (m_standaloneMode) { q->resize(q->size().expandedTo(q->sizeHint())); } } void ResultListWidget::Private::addResultWidget(ResultItemWidget *widget) { Q_ASSERT(widget); Q_ASSERT(std::any_of(m_collections.cbegin(), m_collections.cend(), [](const std::shared_ptr &t) { return !t->isEmpty(); })); Q_ASSERT(m_scrollArea); Q_ASSERT(m_scrollArea->widget()); - Q_ASSERT(qobject_cast(m_scrollArea->widget()->layout())); - QBoxLayout &blay = *static_cast(m_scrollArea->widget()->layout()); - blay.insertWidget(widget->hasErrorResult() ? m_lastErrorItemIndex++ : (blay.count() - 1), widget); + auto scrollAreaLayout = qobject_cast(m_scrollArea->widget()->layout()); + Q_ASSERT(scrollAreaLayout); + // insert new widget after last widget showing error or before the trailing stretch + const auto insertIndex = widget->hasErrorResult() ? m_lastErrorItemIndex++ : scrollAreaLayout->count() - 1; + scrollAreaLayout->insertWidget(insertIndex, widget); + if (insertIndex == 0) { + forceSetTabOrder(m_scrollArea->widget(), widget); + } else { + auto previousResultWidget = qobject_cast(scrollAreaLayout->itemAt(insertIndex - 1)->widget()); + QWidget::setTabOrder(previousResultWidget, widget); + } widget->show(); resizeIfStandalone(); } void ResultListWidget::Private::allTasksDone() { if (!q->isComplete()) { return; } m_progressLabel->setVisible(false); resizeIfStandalone(); Q_EMIT q->completeChanged(); } void ResultListWidget::Private::result(const std::shared_ptr &result) { Q_ASSERT(result); Q_ASSERT(std::any_of(m_collections.cbegin(), m_collections.cend(), [](const std::shared_ptr &t) { return !t->isEmpty(); })); auto wid = new ResultItemWidget(result); q->connect(wid, &ResultItemWidget::linkActivated, q, &ResultListWidget::linkActivated); q->connect(wid, &ResultItemWidget::closeButtonClicked, q, &ResultListWidget::close); addResultWidget(wid); } bool ResultListWidget::isComplete() const { return std::all_of(d->m_collections.cbegin(), d->m_collections.cend(), std::mem_fn(&TaskCollection::allTasksCompleted)); } unsigned int ResultListWidget::totalNumberOfTasks() const { return kdtools::accumulate_transform(d->m_collections.cbegin(), d->m_collections.cend(), std::mem_fn(&TaskCollection::size), 0U); } unsigned int ResultListWidget::numberOfCompletedTasks() const { return kdtools::accumulate_transform(d->m_collections.cbegin(), d->m_collections.cend(), std::mem_fn(&TaskCollection::numberOfCompletedTasks), 0U); } void ResultListWidget::setTaskCollection(const std::shared_ptr &coll) { //clear(); ### PENDING(marc) implement addTaskCollection(coll); } void ResultListWidget::addTaskCollection(const std::shared_ptr &coll) { Q_ASSERT(coll); Q_ASSERT(!coll->isEmpty()); d->m_collections.push_back(coll); connect(coll.get(), SIGNAL(result(std::shared_ptr)), this, SLOT(result(std::shared_ptr))); connect(coll.get(), SIGNAL(started(std::shared_ptr)), this, SLOT(started(std::shared_ptr))); connect(coll.get(), SIGNAL(done()), this, SLOT(allTasksDone())); setStandaloneMode(d->m_standaloneMode); } void ResultListWidget::Private::started(const std::shared_ptr &task) { Q_ASSERT(task); Q_ASSERT(m_progressLabel); m_progressLabel->setText(i18nc("number, operation description", "Operation %1: %2", q->numberOfCompletedTasks() + 1, task->label())); resizeIfStandalone(); } void ResultListWidget::setStandaloneMode(bool standalone) { d->m_standaloneMode = standalone; if (totalNumberOfTasks() == 0) { return; } d->m_closeButton->setVisible(standalone); d->m_closeButton->setEnabled(standalone); d->m_progressLabel->setVisible(standalone); } #include "moc_resultlistwidget.cpp" diff --git a/src/utils/gui-helper.cpp b/src/utils/gui-helper.cpp index 99de8c719..c70763feb 100644 --- a/src/utils/gui-helper.cpp +++ b/src/utils/gui-helper.cpp @@ -1,83 +1,107 @@ /* crypto/gui/signencryptemailconflictdialog.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2017 Intevation GmbH SPDX-License-Identifier: GPL-2.0-or-later */ #include "gui-helper.h" #include #ifdef Q_OS_WIN #include #endif /* This is a Hack to workaround the fact that Foregrounding a Window is so restricted that it AllowSetForegroundWindow does not always work e.g. when the ForegroundWindow timeout has not expired. This Hack is semi official but may stop working in future windows versions. This is similar to what pinentry-qt does on Windows. */ #ifdef Q_OS_WIN WINBOOL SetForegroundWindowEx(HWND hWnd) { //Attach foreground window thread to our thread const DWORD ForeGroundID = GetWindowThreadProcessId(::GetForegroundWindow(), NULL); const DWORD CurrentID = GetCurrentThreadId(); WINBOOL retval; AttachThreadInput(ForeGroundID, CurrentID, TRUE); //Do our stuff here HWND hLastActivePopupWnd = GetLastActivePopup(hWnd); retval = SetForegroundWindow(hLastActivePopupWnd); //Detach the attached thread AttachThreadInput(ForeGroundID, CurrentID, FALSE); return retval; }// End SetForegroundWindowEx #endif void Kleo::aggressive_raise(QWidget *w, bool stayOnTop) { /* Maybe Qt will become aggressive enough one day that * this is enough on windows too*/ w->raise(); w->setWindowState(Qt::WindowActive); w->activateWindow(); #ifdef Q_OS_WIN HWND wid = (HWND)w->effectiveWinId(); /* In the meantime we do our own attention grabbing */ if (!SetForegroundWindow(wid) && !SetForegroundWindowEx(wid)) { OutputDebugStringA("SetForegroundWindow (ex) failed"); /* Yet another fallback which will not work on some * versions and is not recommended by msdn */ if (!ShowWindow(wid, SW_SHOWNORMAL)) { OutputDebugStringA("ShowWindow failed."); } } /* Even if SetForgeoundWindow / SetForegroundWinowEx don't fail * we sometimes are still not in the foreground. So we try yet * another hack by using SetWindowPos */ if (!SetWindowPos(wid, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW)) { OutputDebugStringA("SetWindowPos failed."); } /* sometimes we want to stay on top even if the user * changes focus because we are _aggressive_ and otherwise * Outlook might show the "Help I'm unresponsive so I must have * crashed" Popup if the user clicks into Outlook while a dialog * from us is active. */ else if (!stayOnTop) { // Without moving back to NOTOPMOST we just stay on top. // Even if the user changes focus. SetWindowPos(wid, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); } #else Q_UNUSED(stayOnTop) #endif } +void Kleo::forceSetTabOrder(QWidget *first, QWidget *second) +{ + if (!first || !second || first == second) { + return; + } + // temporarily change the focus policy of the two widgets to something + // other than Qt::NoFocus because QWidget::setTabOrder() does nothing + // if either widget has focus policy Qt::NoFocus + const auto firstFocusPolicy = first->focusPolicy(); + const auto secondFocusPolicy = second->focusPolicy(); + if (firstFocusPolicy == Qt::NoFocus) { + first->setFocusPolicy(Qt::StrongFocus); + } + if (secondFocusPolicy == Qt::NoFocus) { + second->setFocusPolicy(Qt::StrongFocus); + } + QWidget::setTabOrder(first, second); + if (first->focusPolicy() != firstFocusPolicy) { + first->setFocusPolicy(firstFocusPolicy); + } + if (second->focusPolicy() != secondFocusPolicy) { + second->setFocusPolicy(secondFocusPolicy); + } +} diff --git a/src/utils/gui-helper.h b/src/utils/gui-helper.h index 89c2bb528..43236a203 100644 --- a/src/utils/gui-helper.h +++ b/src/utils/gui-helper.h @@ -1,39 +1,51 @@ /* -*- mode: c++; c-basic-offset:4 -*- utils/gui-helper.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2009 Klarälvdalens Datakonsult AB SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include class QWidget; namespace Kleo { static inline void really_check(QAbstractButton &b, bool on) { const bool excl = b.autoExclusive(); b.setAutoExclusive(false); b.setChecked(on); b.setAutoExclusive(excl); } static inline bool xconnect(const QObject *a, const char *signal, const QObject *b, const char *slot) { return QObject::connect(a, signal, b, slot) && QObject::connect(b, signal, a, slot); } /** Aggressively raise a window to foreground. May be platform * specific. */ void aggressive_raise(QWidget *w, bool stayOnTop); -} +/** + * Puts the second widget after the first widget in the focus order. + * + * In contrast to QWidget::setTabOrder(), this function also changes the + * focus order if the first widget or the second widget has focus policy + * Qt::NoFocus. + * + * Note: After calling this function all widgets in the focus proxy chain + * of the first widget have focus policy Qt::NoFocus if the first widget + * has this focus policy. Correspondingly, for the second widget. + */ +void forceSetTabOrder(QWidget *first, QWidget *second); +}