diff --git a/src/dialogs/certifywidget.cpp b/src/dialogs/certifywidget.cpp index 3dc1bb842..77302900d 100644 --- a/src/dialogs/certifywidget.cpp +++ b/src/dialogs/certifywidget.cpp @@ -1,529 +1,551 @@ /* dialogs/certifywidget.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2019 g 10code GmbH SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "certifywidget.h" #include "kleopatra_debug.h" #include #include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if GPGMEPP_VERSION >= 0x10E00 // 1.14.0 # define GPGME_HAS_REMARKS #endif using namespace Kleo; namespace { // Maybe move this in its own file // based on code from StackOverflow class AnimatedExpander: public QWidget { Q_OBJECT public: explicit AnimatedExpander(const QString &title = QString(), - const int animationDuration = 300, QWidget *parent = nullptr); void setContentLayout(QLayout *contentLayout); private: QGridLayout mainLayout; QToolButton toggleButton; QFrame headerLine; QParallelAnimationGroup toggleAnimation; QScrollArea contentArea; int animationDuration{300}; }; -AnimatedExpander::AnimatedExpander(const QString &title, - const int animationDuration, QWidget *parent): - QWidget(parent), - animationDuration(animationDuration) +AnimatedExpander::AnimatedExpander(const QString &title, QWidget *parent): + QWidget(parent) { toggleButton.setStyleSheet(QStringLiteral("QToolButton { border: none; }")); toggleButton.setToolButtonStyle(Qt::ToolButtonTextBesideIcon); toggleButton.setArrowType(Qt::ArrowType::RightArrow); toggleButton.setText(title); toggleButton.setCheckable(true); toggleButton.setChecked(false); headerLine.setFrameShape(QFrame::HLine); headerLine.setFrameShadow(QFrame::Sunken); headerLine.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); contentArea.setStyleSheet(QStringLiteral("QScrollArea { border: none; }")); contentArea.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); // start out collapsed contentArea.setMaximumHeight(0); contentArea.setMinimumHeight(0); // let the entire widget grow and shrink with its content toggleAnimation.addAnimation(new QPropertyAnimation(this, "minimumHeight")); toggleAnimation.addAnimation(new QPropertyAnimation(this, "maximumHeight")); toggleAnimation.addAnimation(new QPropertyAnimation(&contentArea, "maximumHeight")); mainLayout.setVerticalSpacing(0); mainLayout.setContentsMargins(0, 0, 0, 0); int row = 0; mainLayout.addWidget(&toggleButton, row, 0, 1, 1, Qt::AlignLeft); mainLayout.addWidget(&headerLine, row++, 2, 1, 1); mainLayout.addWidget(&contentArea, row, 0, 1, 3); setLayout(&mainLayout); QObject::connect(&toggleButton, &QToolButton::clicked, [this](const bool checked) { toggleButton.setArrowType(checked ? Qt::ArrowType::DownArrow : Qt::ArrowType::RightArrow); toggleAnimation.setDirection(checked ? QAbstractAnimation::Forward : QAbstractAnimation::Backward); toggleAnimation.start(); }); } void AnimatedExpander::setContentLayout(QLayout *contentLayout) { delete contentArea.layout(); contentArea.setLayout(contentLayout); const auto collapsedHeight = sizeHint().height() - contentArea.maximumHeight(); auto contentHeight = contentLayout->sizeHint().height(); for (int i = 0; i < toggleAnimation.animationCount() - 1; ++i) { auto expanderAnimation = static_cast(toggleAnimation.animationAt(i)); expanderAnimation->setDuration(animationDuration); expanderAnimation->setStartValue(collapsedHeight); expanderAnimation->setEndValue(collapsedHeight + contentHeight); } auto contentAnimation = static_cast(toggleAnimation.animationAt(toggleAnimation.animationCount() - 1)); contentAnimation->setDuration(animationDuration); contentAnimation->setStartValue(0); contentAnimation->setEndValue(contentHeight); } class SecKeyFilter: public DefaultKeyFilter { public: SecKeyFilter() : DefaultKeyFilter() { setRevoked(DefaultKeyFilter::NotSet); setExpired(DefaultKeyFilter::NotSet); setHasSecret(DefaultKeyFilter::Set); setCanCertify(DefaultKeyFilter::Set); setIsOpenPGP(DefaultKeyFilter::Set); } }; class UserIDModel : public QStandardItemModel { Q_OBJECT public: enum Role { UserIDIndex = Qt::UserRole }; explicit UserIDModel(QObject *parent = nullptr) : QStandardItemModel(parent) {} GpgME::Key certificateToCertify() const { return m_key; } void setKey(const GpgME::Key &key) { m_key = key; clear(); const std::vector ids = key.userIDs(); int i = 0; for (const auto &uid: key.userIDs()) { if (uid.isRevoked() || uid.isInvalid()) { // Skip user ID's that cannot really be certified. i++; continue; } auto const item = new QStandardItem; item->setText(Formatting::prettyUserID(uid)); item->setData(i, UserIDIndex); item->setCheckable(true); item->setEditable(false); item->setCheckState(Qt::Checked); appendRow(item); i++; } } void setCheckedUserIDs(const std::vector &uids) { std::vector sorted = uids; std::sort(sorted.begin(), sorted.end()); for (int i = 0, end = rowCount(); i != end; ++i) { item(i)->setCheckState(std::binary_search(sorted.begin(), sorted.end(), i) ? Qt::Checked : Qt::Unchecked); } } std::vector checkedUserIDs() const { std::vector ids; for (int i = 0; i < rowCount(); ++i) { if (item(i)->checkState() == Qt::Checked) { ids.push_back(item(i)->data(UserIDIndex).toUInt()); } } qCDebug(KLEOPATRA_LOG) << "Checked uids are: " << ids; return ids; } private: GpgME::Key m_key; }; static bool uidEqual(const GpgME::UserID &lhs, const GpgME::UserID &rhs) { return qstrcmp(lhs.parent().primaryFingerprint(), rhs.parent().primaryFingerprint()) == 0 && qstrcmp(lhs.id(), rhs.id()) == 0; } +auto checkBoxSize(const QCheckBox *checkBox) +{ + QStyleOptionButton opt; + return checkBox->style()->sizeFromContents(QStyle::CT_CheckBox, &opt, QSize(), checkBox); +} + +auto createInfoButton(const QString &text, QWidget *parent) +{ + auto infoBtn = new QPushButton{parent}; + infoBtn->setIcon(QIcon::fromTheme(QStringLiteral("help-contextual"))); + infoBtn->setFlat(true); + + QObject::connect(infoBtn, &QPushButton::clicked, infoBtn, [infoBtn, text] () { + QToolTip::showText(infoBtn->mapToGlobal(QPoint()) + QPoint(infoBtn->width(), 0), + text, infoBtn, QRect(), 30000); + }); + + return infoBtn; +} + } class CertifyWidget::Private { public: Private(CertifyWidget *qq) : q{qq} , mFprLabel{new QLabel{q}} + , mSecKeySelect{new KeySelectionCombo{/* secretOnly= */true, q}} + , mExportCB{new QCheckBox{q}} + , mPublishCB{new QCheckBox{q}} + , mTagsLE{new QLineEdit{q}} , mTrustSignatureCB{new QCheckBox{q}} , mTrustSignatureDomainLE{new QLineEdit{q}} { - auto mainLay = new QVBoxLayout(q); + auto mainLay = new QVBoxLayout{q}; mainLay->addWidget(mFprLabel); - auto secKeyLay = new QHBoxLayout; + auto secKeyLay = new QHBoxLayout{q}; secKeyLay->addWidget(new QLabel(i18n("Certify with:"))); - mSecKeySelect = new KeySelectionCombo(true); mSecKeySelect->setKeyFilter(std::shared_ptr(new SecKeyFilter())); secKeyLay->addWidget(mSecKeySelect, 1); mainLay->addLayout(secKeyLay); - auto splitLine = new QFrame; - splitLine->setFrameShape(QFrame::HLine); - splitLine->setFrameShadow(QFrame::Sunken); - splitLine->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); + mainLay->addWidget(new KSeparator{Qt::Horizontal, q}); - mainLay->addWidget(splitLine); - - auto listView = new QListView; + auto listView = new QListView{q}; listView->setModel(&mUserIDModel); mainLay->addWidget(listView, 1); // Setup the advanced area - auto expander = new AnimatedExpander(i18n("Advanced")); + auto expander = new AnimatedExpander{i18n("Advanced"), q}; mainLay->addWidget(expander); - auto advLay = new QVBoxLayout; - - mExportCB = new QCheckBox(i18n("Certify for everyone to see (exportable)")); - mPublishCB = new QCheckBox(i18n("Publish on keyserver afterwards")); - auto publishLay = new QHBoxLayout; - publishLay->addSpacing(20); - publishLay->addWidget(mPublishCB); + auto advLay = new QVBoxLayout{q}; - mTagsLE = new QLineEdit; - mTagsLE->setPlaceholderText(i18n("Tags")); + mExportCB->setText(i18n("Certify for everyone to see (exportable)")); + advLay->addWidget(mExportCB); - auto infoBtn = new QPushButton; - infoBtn->setIcon(QIcon::fromTheme(QStringLiteral("help-contextual"))); - infoBtn->setFlat(true); + { + auto layout = new QHBoxLayout{q}; - connect(infoBtn, &QPushButton::clicked, q, [infoBtn] () { - const QString msg = i18n("You can use this to add additional info to a " - "certification.") + QStringLiteral("

") + - i18n("Tags created by anyone with full certification trust " - "are shown in the keylist and can be searched."); - QToolTip::showText(infoBtn->mapToGlobal(QPoint()) + QPoint(infoBtn->width(), 0), - msg, infoBtn, QRect(), 30000); - }); + mPublishCB->setText(i18n("Publish on keyserver afterwards")); + mPublishCB->setEnabled(mExportCB->isChecked()); - auto tagsLay = new QHBoxLayout; - tagsLay->addWidget(infoBtn); - tagsLay->addWidget(mTagsLE); + layout->addSpacing(checkBoxSize(mExportCB).width()); + layout->addWidget(mPublishCB); - advLay->addWidget(mExportCB); - advLay->addLayout(publishLay); - advLay->addLayout(tagsLay); + advLay->addLayout(layout); + } #ifndef GPGME_HAS_REMARKS - // Hide it if we do not have remark support mTagsLE->setVisible(false); - infoBtn->setVisible(false); +#else + { + auto tagsLay = new QHBoxLayout{q}; + + mTagsLE->setPlaceholderText(i18n("Tags")); + auto infoBtn = createInfoButton(i18n("You can use this to add additional info to a certification.") + + QStringLiteral("

") + + i18n("Tags created by anyone with full certification trust " + "are shown in the keylist and can be searched."), + q); + + tagsLay->addWidget(new QLabel{i18n("Tags:"), q}); + tagsLay->addWidget(mTagsLE, 1); + tagsLay->addWidget(infoBtn); + + advLay->addLayout(tagsLay); + } #endif #ifndef QGPGME_SUPPORTS_TRUST_SIGNATURES mTrustSignatureCB->setVisible(false); mTrustSignatureDomainLE->setVisible(false); -#endif +#else mTrustSignatureCB->setText(i18n("Certify as trusted introducer")); - mTrustSignatureDomainLE->setPlaceholderText(i18n("Domain")); - mTrustSignatureDomainLE->setEnabled(mTrustSignatureCB->isChecked()); - advLay->addWidget(mTrustSignatureCB); - advLay->addWidget(mTrustSignatureDomainLE); + { + auto layout = new QHBoxLayout{q}; - expander->setContentLayout(advLay); + mTrustSignatureDomainLE->setPlaceholderText(i18n("Domain")); + mTrustSignatureDomainLE->setEnabled(mTrustSignatureCB->isChecked()); + + layout->addSpacing(checkBoxSize(mTrustSignatureCB).width()); + layout->addWidget(mTrustSignatureDomainLE); + + advLay->addLayout(layout); + } +#endif - mPublishCB->setEnabled(false); + expander->setContentLayout(advLay); connect(&mUserIDModel, &QStandardItemModel::itemChanged, q, &CertifyWidget::changed); connect(mExportCB, &QCheckBox::toggled, [this] (bool on) { mPublishCB->setEnabled(on); }); connect(mSecKeySelect, &KeySelectionCombo::currentKeyChanged, [this] (const GpgME::Key &) { #ifdef GPGME_HAS_REMARKS updateTags(); #endif Q_EMIT q->changed(); }); connect(mTrustSignatureCB, &QCheckBox::toggled, q, [this] (bool on) { mTrustSignatureDomainLE->setEnabled(on); Q_EMIT q->changed(); }); connect(mTrustSignatureDomainLE, &QLineEdit::textChanged, q, &CertifyWidget::changed); loadConfig(); } ~Private() = default; void loadConfig() { const KConfigGroup conf(KSharedConfig::openConfig(), "CertifySettings"); mSecKeySelect->setDefaultKey(conf.readEntry("LastKey", QString())); mExportCB->setChecked(conf.readEntry("ExportCheckState", false)); mPublishCB->setChecked(conf.readEntry("PublishCheckState", false)); } void updateTags() { if (mTagsLE->isModified()) { return; } #ifdef GPGME_HAS_REMARKS GpgME::Key remarkKey = mSecKeySelect->currentKey(); if (!remarkKey.isNull()) { std::vector uidsWithRemark; QString remark; for (const auto &uid: mTarget.userIDs()) { GpgME::Error err; const char *c_remark = uid.remark(remarkKey, err); if (c_remark) { const QString candidate = QString::fromUtf8(c_remark); if (candidate != remark) { qCDebug(KLEOPATRA_LOG) << "Different remarks on user ids. Taking last."; remark = candidate; uidsWithRemark.clear(); } uidsWithRemark.push_back(uid); } } // Only select the user ids with the correct remark if (!remark.isEmpty()) { selectUserIDs(uidsWithRemark); } mTagsLE->setText(remark); } #endif } void setTarget(const GpgME::Key &key) { mFprLabel->setText(i18n("Fingerprint: %1", Formatting::prettyID(key.primaryFingerprint())) + QStringLiteral("
") + i18n("Only the fingerprint clearly identifies the key and its owner.")); mUserIDModel.setKey(key); mTarget = key; updateTags(); } GpgME::Key secKey() const { return mSecKeySelect->currentKey(); } void selectUserIDs(const std::vector &uids) { const auto all = mTarget.userIDs(); std::vector indexes; indexes.reserve(uids.size()); for (const auto &uid: uids) { const unsigned int idx = std::distance(all.cbegin(), std::find_if(all.cbegin(), all.cend(), [uid](const GpgME::UserID &other) { return uidEqual(uid, other); })); if (idx < all.size()) { indexes.push_back(idx); } } mUserIDModel.setCheckedUserIDs(indexes); } std::vector selectedUserIDs() const { return mUserIDModel.checkedUserIDs(); } bool exportableSelected() const { return mExportCB->isChecked(); } bool publishSelected() const { return mPublishCB->isChecked(); } QString tags() const { return mTagsLE->text().trimmed(); } GpgME::Key target() const { return mTarget; } bool isValid() const { static const QRegularExpression domainNameRegExp{QStringLiteral(R"(^\s*((xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}\s*$)"), QRegularExpression::CaseInsensitiveOption}; // do not accept null keys if (mTarget.isNull() || mSecKeySelect->currentKey().isNull()) { return false; } // do not accept empty list of user ids if (selectedUserIDs().empty()) { return false; } // do not accept if the keys are the same if (_detail::ByFingerprint()(mTarget, mSecKeySelect->currentKey())) { return false; } if (mTrustSignatureCB->isChecked() && !domainNameRegExp.match(mTrustSignatureDomainLE->text()).hasMatch()) { return false; } return true; } public: CertifyWidget *const q; QLabel *mFprLabel = nullptr; KeySelectionCombo *mSecKeySelect = nullptr; QCheckBox *mExportCB = nullptr; QCheckBox *mPublishCB = nullptr; QLineEdit *mTagsLE = nullptr; QCheckBox *mTrustSignatureCB = nullptr; QLineEdit *mTrustSignatureDomainLE = nullptr; UserIDModel mUserIDModel; GpgME::Key mTarget; }; CertifyWidget::CertifyWidget(QWidget *parent) : QWidget{parent} , d{std::make_unique(this)} { } Kleo::CertifyWidget::~CertifyWidget() = default; void CertifyWidget::setTarget(const GpgME::Key &key) { d->setTarget(key); } GpgME::Key CertifyWidget::target() const { return d->target(); } void CertifyWidget::selectUserIDs(const std::vector &uids) { d->selectUserIDs(uids); } std::vector CertifyWidget::selectedUserIDs() const { return d->selectedUserIDs(); } GpgME::Key CertifyWidget::secKey() const { return d->secKey(); } bool CertifyWidget::exportableSelected() const { return d->exportableSelected(); } QString CertifyWidget::tags() const { return d->tags(); } bool CertifyWidget::publishSelected() const { return d->publishSelected(); } bool CertifyWidget::trustSignatureSelected() const { return d->mTrustSignatureCB->isChecked(); } QString CertifyWidget::trustSignatureDomain() const { return d->mTrustSignatureDomainLE->text().trimmed(); } bool CertifyWidget::isValid() const { return d->isValid(); } // For UserID model #include "certifywidget.moc"