diff --git a/src/crypto/gui/resultitemwidget.cpp b/src/crypto/gui/resultitemwidget.cpp index 510966124..39204cc53 100644 --- a/src/crypto/gui/resultitemwidget.cpp +++ b/src/crypto/gui/resultitemwidget.cpp @@ -1,350 +1,352 @@ /* -*- 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/htmllabel.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: 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; 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 QColor linkColor = txtColor; 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 HtmlLabel; overview->setWordWrap(true); overview->setHtml(d->m_result->overview()); overview->setStyleSheet(styleSheet); overview->setLinkColor(linkColor); + overview->setSelectTextOnFocus(true); setFocusPolicy(overview->focusPolicy()); setFocusProxy(overview); connect(overview, &QLabel::linkActivated, this, [this](const auto &link) { d->slotLinkActivated(link); }); vlay->addWidget(overview); layout->addLayout(vlay); 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, &QLabel::linkActivated, this, [this](const auto &link) { d->slotLinkActivated(link); }); actionLayout->addWidget(d->m_auditLogLabel); d->m_auditLogLabel->setStyleSheet(styleSheet); d->m_auditLogLabel->setLinkColor(linkColor); auto detailsLabel = new HtmlLabel; detailsLabel->setWordWrap(true); detailsLabel->setHtml(d->m_result->details()); detailsLabel->setStyleSheet(styleSheet); detailsLabel->setLinkColor(linkColor); + detailsLabel->setSelectTextOnFocus(true); connect(detailsLabel, &QLabel::linkActivated, this, [this](const auto &link) { d->slotLinkActivated(link); }); vlay->addWidget(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/dialogs/adduseriddialog.cpp b/src/dialogs/adduseriddialog.cpp index 84b383b24..38ebdd8a8 100644 --- a/src/dialogs/adduseriddialog.cpp +++ b/src/dialogs/adduseriddialog.cpp @@ -1,189 +1,190 @@ /* -*- mode: c++; c-basic-offset:4 -*- dialogs/adduseriddialog.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "adduseriddialog.h" #include "nameandemailwidget.h" #include "utils/accessibility.h" #include "utils/scrollarea.h" #include "view/htmllabel.h" #include #include #include #include #include #include #include #include #include "kleopatra_debug.h" using namespace Kleo; class AddUserIDDialog::Private { friend class ::Kleo::AddUserIDDialog; AddUserIDDialog *const q; struct { ScrollArea *scrollArea; NameAndEmailWidget *nameAndEmail; HtmlLabel *resultLabel; QDialogButtonBox *buttonBox; } ui; LabelHelper labelHelper; public: explicit Private(AddUserIDDialog *qq) : q{qq} { q->setWindowTitle(i18nc("title:window", "Add User ID")); const KConfigGroup config{KSharedConfig::openConfig(), "CertificateCreationWizard"}; const auto attrOrder = config.readEntry("OpenPGPAttributeOrder", QStringList{}); const auto nameIsRequired = attrOrder.contains(QLatin1String{"NAME!"}, Qt::CaseInsensitive); const auto emailIsRequired = attrOrder.contains(QLatin1String{"EMAIL!"}, Qt::CaseInsensitive); auto mainLayout = new QVBoxLayout{q}; { const auto infoText = nameIsRequired || emailIsRequired ? i18n("Enter a name and an email address to use for the user ID.") : i18n("Enter a name and/or an email address to use for the user ID."); auto label = new QLabel{infoText, q}; label->setWordWrap(true); mainLayout->addWidget(label); } mainLayout->addWidget(new KSeparator{Qt::Horizontal, q}); ui.scrollArea = new ScrollArea{q}; ui.scrollArea->setFocusPolicy(Qt::NoFocus); ui.scrollArea->setFrameStyle(QFrame::NoFrame); ui.scrollArea->setBackgroundRole(q->backgroundRole()); ui.scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); ui.scrollArea->setSizeAdjustPolicy(QScrollArea::AdjustToContents); auto scrollAreaLayout = qobject_cast(ui.scrollArea->widget()->layout()); scrollAreaLayout->setContentsMargins(0, 0, 0, 0); ui.nameAndEmail = new NameAndEmailWidget{q}; ui.nameAndEmail->layout()->setContentsMargins(0, 0, 0, 0); ui.nameAndEmail->setNameIsRequired(nameIsRequired); ui.nameAndEmail->setNameLabel(config.readEntry("NAME_label")); ui.nameAndEmail->setNameHint(config.readEntry("NAME_hint", config.readEntry("NAME_placeholder"))); ui.nameAndEmail->setNamePattern(config.readEntry("NAME_regex")); ui.nameAndEmail->setEmailIsRequired(emailIsRequired); ui.nameAndEmail->setEmailLabel(config.readEntry("EMAIL_label")); ui.nameAndEmail->setEmailHint(config.readEntry("EMAIL_hint", config.readEntry("EMAIL_placeholder"))); ui.nameAndEmail->setEmailPattern(config.readEntry("EMAIL_regex")); scrollAreaLayout->addWidget(ui.nameAndEmail); scrollAreaLayout->addWidget(new KSeparator{Qt::Horizontal, q}); { ui.resultLabel = new HtmlLabel{q}; ui.resultLabel->setWordWrap(true); ui.resultLabel->setFocusPolicy(Qt::ClickFocus); + ui.resultLabel->setSelectTextOnFocus(true); labelHelper.addLabel(ui.resultLabel); scrollAreaLayout->addWidget(ui.resultLabel); } scrollAreaLayout->addStretch(1); mainLayout->addWidget(ui.scrollArea); mainLayout->addWidget(new KSeparator{Qt::Horizontal, q}); ui.buttonBox = new QDialogButtonBox{QDialogButtonBox::Ok | QDialogButtonBox::Cancel, q}; mainLayout->addWidget(ui.buttonBox); connect(ui.nameAndEmail, &NameAndEmailWidget::userIDChanged, q, [this]() { updateResultLabel(); }); connect(ui.buttonBox, &QDialogButtonBox::accepted, q, [this]() { checkAccept(); }); connect(ui.buttonBox, &QDialogButtonBox::rejected, q, &QDialog::reject); updateResultLabel(); } private: void checkAccept() { QStringList errors; if (ui.nameAndEmail->userID().isEmpty() && !ui.nameAndEmail->nameIsRequired() && !ui.nameAndEmail->emailIsRequired()) { errors.push_back(i18n("Enter a name or an email address.")); } const auto nameError = ui.nameAndEmail->nameError(); if (!nameError.isEmpty()) { errors.push_back(nameError); } const auto emailError = ui.nameAndEmail->emailError(); if (!emailError.isEmpty()) { errors.push_back(emailError); } if (errors.size() > 1) { KMessageBox::errorList(q, i18n("There is a problem."), errors); } else if (!errors.empty()) { KMessageBox::error(q, errors.first()); } else { q->accept(); } } void updateResultLabel() { ui.resultLabel->setHtml(i18nc("@info", "
This is how the new user ID will be stored in the certificate:
" "
%1
", ui.nameAndEmail->userID().toHtmlEscaped())); } }; AddUserIDDialog::AddUserIDDialog(QWidget *parent, Qt::WindowFlags f) : QDialog{parent, f} , d(new Private{this}) { } AddUserIDDialog::~AddUserIDDialog() = default; void AddUserIDDialog::setName(const QString &name) { d->ui.nameAndEmail->setName(name); } QString AddUserIDDialog::name() const { return d->ui.nameAndEmail->name(); } void AddUserIDDialog::setEmail(const QString &email) { d->ui.nameAndEmail->setEmail(email); } QString AddUserIDDialog::email() const { return d->ui.nameAndEmail->email(); } QString AddUserIDDialog::userID() const { return d->ui.nameAndEmail->userID(); } diff --git a/src/view/htmllabel.cpp b/src/view/htmllabel.cpp index ef8f969f1..ee77b3d87 100644 --- a/src/view/htmllabel.cpp +++ b/src/view/htmllabel.cpp @@ -1,250 +1,258 @@ /* view/htmllabel.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "htmllabel.h" #include "utils/accessibility.h" #include #include #include #include #include using namespace Kleo; namespace { struct AnchorData { int start; int end; QString text; QString href; }; } class HtmlLabel::Private { HtmlLabel *q; public: Private(HtmlLabel *q) : q{q} {} void updateText(const QString &newText = {}); std::vector &anchors(); int anchorIndex(int start); void invalidateAnchorCache(); + bool mSelectTextOnFocus = false; bool mAnchorsValid = false; std::vector mAnchors; QColor linkColor; }; void HtmlLabel::Private::updateText(const QString &newText) { static const QString styleTemplate{QLatin1String{""}}; if (newText.isEmpty() && q->text().isEmpty()) { return; } const auto styleTag = styleTemplate.arg(linkColor.isValid() ? linkColor.name() : q->palette().link().color().name()); if (newText.isEmpty()) { q->setText(styleTag + q->text().mid(styleTag.size())); } else { q->setText(styleTag + newText); } invalidateAnchorCache(); } std::vector &HtmlLabel::Private::anchors() { if (mAnchorsValid) { return mAnchors; } mAnchors.clear(); QTextDocument doc; doc.setHtml(q->text()); // taken from QWidgetTextControl::setFocusToNextOrPreviousAnchor and QWidgetTextControl::findNextPrevAnchor for (QTextBlock block = doc.begin(); block.isValid(); block = block.next()) { QTextBlock::Iterator it = block.begin(); while (!it.atEnd()) { const QTextFragment fragment = it.fragment(); const QTextCharFormat fmt = fragment.charFormat(); if (fmt.isAnchor() && fmt.hasProperty(QTextFormat::AnchorHref)) { const int anchorStart = fragment.position(); const QString anchorHref = fmt.anchorHref(); int anchorEnd = -1; // find next non-anchor fragment for (; !it.atEnd(); ++it) { const QTextFragment fragment = it.fragment(); const QTextCharFormat fmt = fragment.charFormat(); if (!fmt.isAnchor() || fmt.anchorHref() != anchorHref) { anchorEnd = fragment.position(); break; } } if (anchorEnd == -1) { anchorEnd = block.position() + block.length() - 1; } QTextCursor cursor{&doc}; cursor.setPosition(anchorStart); cursor.setPosition(anchorEnd, QTextCursor::KeepAnchor); QString anchorText = cursor.selectedText(); mAnchors.push_back({anchorStart, anchorEnd, anchorText, anchorHref}); } else { ++it; } } } mAnchorsValid = true; return mAnchors; } int HtmlLabel::Private::anchorIndex(int start) { anchors(); // ensure that the anchor cache is valid auto it = std::find_if(std::cbegin(mAnchors), std::cend(mAnchors), [start](const auto &anchor) { return anchor.start == start; }); if (it != std::cend(mAnchors)) { return std::distance(std::cbegin(mAnchors), it); } return -1; } void HtmlLabel::Private::invalidateAnchorCache() { mAnchorsValid = false; } HtmlLabel::HtmlLabel(QWidget *parent) : HtmlLabel{{}, parent} { } HtmlLabel::HtmlLabel(const QString &html, QWidget *parent) : QLabel{parent} , d{new Private{this}} { setTextFormat(Qt::RichText); setTextInteractionFlags(Qt::TextBrowserInteraction); setHtml(html); } HtmlLabel::~HtmlLabel() = default; +void HtmlLabel::setSelectTextOnFocus(bool select) +{ + d->mSelectTextOnFocus = select; +} + void HtmlLabel::setHtml(const QString &html) { if (html.isEmpty()) { clear(); d->invalidateAnchorCache(); return; } d->updateText(html); } void HtmlLabel::setLinkColor(const QColor &color) { d->linkColor = color; d->updateText(); } int HtmlLabel::numberOfAnchors() const { return d->anchors().size(); } QString HtmlLabel::anchorText(int index) const { if (index >= 0 && index < numberOfAnchors()) { return d->anchors()[index].text; } return {}; } QString HtmlLabel::anchorHref(int index) const { if (index >= 0 && index < numberOfAnchors()) { return d->anchors()[index].href; } return {}; } void HtmlLabel::activateAnchor(int index) { // based on QWidgetTextControlPrivate::activateLinkUnderCursor if (index < 0 || index >= numberOfAnchors()) { return; } const auto &anchor = d->anchors()[index]; if (anchor.href.isEmpty()) { return; } if (hasFocus()) { // move cursor just before the anchor and clear the selection setSelection(anchor.start, 0); // focus the anchor focusNextPrevChild(true); } else { // clear the selection moving the cursor just after the anchor setSelection(anchor.end, 0); } if (openExternalLinks()) { QDesktopServices::openUrl(QUrl{anchor.href}); } else { Q_EMIT linkActivated(anchor.href); } } int HtmlLabel::selectedAnchor() const { return d->anchorIndex(selectionStart()); } void HtmlLabel::focusInEvent(QFocusEvent *ev) { QLabel::focusInEvent(ev); - // if the text label gets focus, then select its text; this is a workaround - // for missing focus indicators for labels in many Qt styles - const Qt::FocusReason reason = ev->reason(); - const auto isKeyboardFocusEvent = reason == Qt::TabFocusReason - || reason == Qt::BacktabFocusReason - || reason == Qt::ShortcutFocusReason; - if (!text().isEmpty() && isKeyboardFocusEvent) { - Kleo::selectLabelText(this); + if (d->mSelectTextOnFocus) { + // if the text label gets focus, then select its text; this is a workaround + // for missing focus indicators for labels in many Qt styles + const Qt::FocusReason reason = ev->reason(); + const auto isKeyboardFocusEvent = reason == Qt::TabFocusReason + || reason == Qt::BacktabFocusReason + || reason == Qt::ShortcutFocusReason; + if (!text().isEmpty() && isKeyboardFocusEvent) { + Kleo::selectLabelText(this); + } } } bool HtmlLabel::focusNextPrevChild(bool next) { const bool result = QLabel::focusNextPrevChild(next); if (hasFocus() && QAccessible::isActive()) { const int anchorIndex = selectedAnchor(); if (anchorIndex >= 0) { QAccessibleEvent focusEvent(this, QAccessible::Focus); focusEvent.setChild(anchorIndex); QAccessible::updateAccessibility(&focusEvent); } } return result; } diff --git a/src/view/htmllabel.h b/src/view/htmllabel.h index 0d373109e..9c16ae0c7 100644 --- a/src/view/htmllabel.h +++ b/src/view/htmllabel.h @@ -1,53 +1,55 @@ /* view/htmllabel.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include #include #include namespace Kleo { class HtmlLabel : public QLabel, public AnchorProvider { Q_OBJECT public: explicit HtmlLabel(QWidget *parent = nullptr); explicit HtmlLabel(const QString &html, QWidget *parent = nullptr); ~HtmlLabel() override; + void setSelectTextOnFocus(bool select); + void setHtml(const QString &html); void setLinkColor(const QColor &color); // AnchorProvider int numberOfAnchors() const override; QString anchorText(int index) const override; QString anchorHref(int index) const override; void activateAnchor(int index) override; int selectedAnchor() const override; protected: void focusInEvent(QFocusEvent *ev) override; bool focusNextPrevChild(bool next) override; private: using QLabel::setText; private: class Private; std::unique_ptr d; }; }