diff --git a/src/dialogs/adduseriddialog.cpp b/src/dialogs/adduseriddialog.cpp index 98b98842f..f23546312 100644 --- a/src/dialogs/adduseriddialog.cpp +++ b/src/dialogs/adduseriddialog.cpp @@ -1,255 +1,253 @@ /* -*- 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 "utils/accessibility.h" #include "view/errorlabel.h" #include "view/formtextinput.h" #include "view/htmllabel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "kleopatra_debug.h" using namespace Kleo; namespace { QString buildUserId(const QString &name, const QString &email) { if (name.isEmpty()) { return email; } else if (email.isEmpty()) { return name; } else { return QStringLiteral("%1 <%2>").arg(name, email); } } } class AddUserIDDialog::Private { friend class ::Kleo::AddUserIDDialog; AddUserIDDialog *const q; struct { std::unique_ptr> nameInput; std::unique_ptr> emailInput; 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."); mainLayout->addWidget(new QLabel{infoText, q}); auto gridLayout = new QGridLayout; int row = -1; { ui.nameInput = FormTextInput::create(q); - ui.nameInput->label()->setText(i18nc("@label", "Name:")); - ui.nameInput->setAccessibleName(i18nc("@label without colon for assistive tools", "Name")); + ui.nameInput->setLabelText(i18nc("@label", "Name")); ui.nameInput->setIsRequired(nameIsRequired); ui.nameInput->setValueRequiredErrorMessage(i18n("Error: The name is required.")); const auto regexp = config.readEntry("NAME_regex"); if (regexp.isEmpty()) { ui.nameInput->setValidator(Validation::simpleName(Validation::Optional, q)); ui.nameInput->setToolTip(xi18n( "The name must not contain any of the following characters: <, >, @.")); ui.nameInput->setAccessibleDescription(i18nc("text for screen readers", "The name must not contain any of the following characters: less-than sign, greater-than sign, at sign.")); ui.nameInput->setInvalidEntryErrorMessage(i18n("Error: The entered name contains invalid characters.")); } else { ui.nameInput->setValidator(Validation::simpleName(regexp, Validation::Optional, q)); ui.nameInput->setToolTip(xi18n( "The name must not contain any of the following characters: <, >, @. " "Additionally, the name must follow the rules set by your organization.")); ui.nameInput->setAccessibleDescription(i18nc("text for screen readers", "The name must not contain any of the following characters: less-than sign, greater-than sign, at sign. " "Additionally, the name must follow the rules set by your organization.")); ui.nameInput->setInvalidEntryErrorMessage(i18n( "Error: The entered name contains invalid characters " "or it does not follow your organization's rules.")); } row++; gridLayout->addWidget(ui.nameInput->label(), row, 0, 1, 1); gridLayout->addWidget(ui.nameInput->widget(), row, 1, 1, 1); row++; gridLayout->addWidget(ui.nameInput->errorLabel(), row, 0, 1, 2); } connect(ui.nameInput->widget(), &QLineEdit::textChanged, q, [this]() { updateResultLabel(); }); { ui.emailInput = FormTextInput::create(q); - ui.emailInput->label()->setText(i18nc("@label", "Email:")); - ui.emailInput->setAccessibleName(i18nc("@label without colon for assistive tools", "Email")); + ui.emailInput->setLabelText(i18nc("@label", "Email address")); ui.emailInput->setIsRequired(emailIsRequired); ui.emailInput->setValueRequiredErrorMessage(i18n("Error: The email address is required.")); const auto regexp = config.readEntry(QLatin1String("EMAIL_regex")); if (regexp.isEmpty()) { ui.emailInput->setValidator(Validation::email(Validation::Optional, q)); ui.emailInput->setInvalidEntryErrorMessage(i18n("Error: The entered email address is not valid.")); } else { ui.emailInput->setValidator(Validation::email(regexp, Validation::Optional, q)); ui.emailInput->setToolTip(xi18n( "If an email address is given, then it has to satisfy the rules set by your organization.")); ui.emailInput->setInvalidEntryErrorMessage(i18n( "Error: The entered email address is not valid " "or it does not follow your organization's rules.")); } row++; gridLayout->addWidget(ui.emailInput->label(), row, 0, 1, 1); gridLayout->addWidget(ui.emailInput->widget(), row, 1, 1, 1); row++; gridLayout->addWidget(ui.emailInput->errorLabel(), row, 0, 1, 2); } connect(ui.emailInput->widget(), &QLineEdit::textChanged, q, [this]() { updateResultLabel(); }); mainLayout->addLayout(gridLayout); mainLayout->addWidget(new KSeparator{Qt::Horizontal, q}); { ui.resultLabel = new HtmlLabel{q}; ui.resultLabel->setFocusPolicy(Qt::ClickFocus); labelHelper.addLabel(ui.resultLabel); mainLayout->addWidget(ui.resultLabel); } mainLayout->addWidget(new KSeparator{Qt::Horizontal, q}); mainLayout->addStretch(1); ui.buttonBox = new QDialogButtonBox{QDialogButtonBox::Ok | QDialogButtonBox::Cancel, q}; mainLayout->addWidget(ui.buttonBox); connect(ui.buttonBox, &QDialogButtonBox::accepted, q, [this]() { checkAccept(); }); connect(ui.buttonBox, &QDialogButtonBox::rejected, q, &QDialog::reject); updateResultLabel(); } QString name() const { return ui.nameInput->widget()->text().trimmed(); } QString email() const { return ui.emailInput->widget()->text().trimmed(); } private: void checkAccept() { QStringList errors; if (ui.resultLabel->text().isEmpty() && !ui.nameInput->isRequired() && !ui.emailInput->isRequired()) { errors.push_back(i18n("Name and email address cannot both be empty.")); } if (ui.nameInput->isRequired() && !ui.nameInput->hasValue()) { errors.push_back(i18n("A name is required.")); } else if (!ui.nameInput->hasAcceptableInput()) { errors.push_back(i18n("The entered name is not valid.")); } if (ui.emailInput->isRequired() && !ui.emailInput->hasValue()) { errors.push_back(i18n("An email address is required.")); } else if (!ui.emailInput->hasAcceptableInput()) { errors.push_back(i18n("The entered email address is not valid.")); } if (errors.size() > 1) { KMessageBox::errorList(q, i18n("Sorry, the entered data is not acceptable."), errors); } else if (!errors.empty()) { KMessageBox::sorry(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
", buildUserId(name(), email()).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.nameInput->widget()->setText(name); } QString AddUserIDDialog::name() const { return d->name(); } void AddUserIDDialog::setEmail(const QString &email) { d->ui.emailInput->widget()->setText(email); } QString AddUserIDDialog::email() const { return d->email(); } QString AddUserIDDialog::userID() const { return d->ui.resultLabel->text(); } diff --git a/src/view/formtextinput.cpp b/src/view/formtextinput.cpp index 946e77bab..8df658703 100644 --- a/src/view/formtextinput.cpp +++ b/src/view/formtextinput.cpp @@ -1,309 +1,326 @@ /* view/formtextinput.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include "formtextinput.h" #include "errorlabel.h" #include "utils/accessibility.h" #include #include #include #include #include #include "kleopatra_debug.h" namespace { auto defaultValueRequiredErrorMessage() { return i18n("Error: A value is required."); } auto defaultInvalidEntryErrorMessage() { return i18n("Error: The entered text is not valid."); } } namespace Kleo::_detail { class FormTextInputBase::Private { FormTextInputBase *q; public: enum Error { EntryOK, EntryMissing, // a required entry is missing InvalidEntry // the validator doesn't accept the entry }; Private(FormTextInputBase *q) : q{q} , mValueRequiredErrorMessage{defaultValueRequiredErrorMessage()} , mInvalidEntryErrorMessage{defaultInvalidEntryErrorMessage()} {} + void updateLabel(); QString errorMessage(Error error) const; void updateError(); void updateAccessibleNameAndDescription(); QPointer mLabel; QPointer mWidget; QPointer mErrorLabel; QPointer mValidator; + QString mLabelText; QString mAccessibleName; QString mAccessibleDescription; QString mValueRequiredErrorMessage; QString mInvalidEntryErrorMessage; Error mError = EntryOK; bool mRequired = false; bool mEditingInProgress = false; }; +void FormTextInputBase::Private::updateLabel() +{ + if (!mLabel) { + return; + } + const auto text = mRequired + ? i18nc("@label label text (required)", "%1 (required)", mLabelText) + : mLabelText; + mLabel->setText(text); +} + QString FormTextInputBase::Private::errorMessage(Error error) const { switch (error) { case EntryOK: return {}; case EntryMissing: return mValueRequiredErrorMessage; case InvalidEntry: return mInvalidEntryErrorMessage; } return {}; } void FormTextInputBase::Private::updateError() { if (!mErrorLabel) { return; } if (mRequired && !q->hasValue()) { mError = EntryMissing; } else if (!q->hasAcceptableInput()) { mError = InvalidEntry; } else { mError = EntryOK; } const auto currentErrorMessage = mErrorLabel->text(); const auto newErrorMessage = errorMessage(mError); if (newErrorMessage == currentErrorMessage) { return; } if (currentErrorMessage.isEmpty() && mEditingInProgress) { // 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; } mErrorLabel->setVisible(!newErrorMessage.isEmpty()); mErrorLabel->setText(newErrorMessage); updateAccessibleNameAndDescription(); } void FormTextInputBase::Private::updateAccessibleNameAndDescription() { // fall back to default accessible name/description if accessible name/description wasn't set explicitly if (mAccessibleName.isEmpty()) { mAccessibleName = getAccessibleName(mWidget); } if (mAccessibleDescription.isEmpty()) { mAccessibleDescription = getAccessibleDescription(mWidget); } const bool errorShown = mErrorLabel && mErrorLabel->isVisible(); // Qt does not support "described-by" relations (like WCAG's "aria-describedby" relationship attribute); // emulate this by adding the error message to the accessible description of the input field const auto description = errorShown ? mAccessibleDescription + QLatin1String{" "} + mErrorLabel->text() : mAccessibleDescription; if (mWidget && mWidget->accessibleDescription() != description) { mWidget->setAccessibleDescription(description); } // Qt does not support IA2's "invalid entry" state (like WCAG's "aria-invalid" state attribute); // screen readers say something like "invalid entry" if this state is set; // emulate this by adding "invalid entry" to the accessible name of the input field // and its label QString name = mAccessibleName; - if (mRequired) { - name += QLatin1String{", "} + requiredText(); - } if (errorShown) { name += QLatin1String{", "} + invalidEntryText(); }; if (mLabel && mLabel->accessibleName() != name) { mLabel->setAccessibleName(name); } if (mWidget && mWidget->accessibleName() != name) { mWidget->setAccessibleName(name); } } FormTextInputBase::FormTextInputBase() : d{new Private{this}} { } FormTextInputBase::~FormTextInputBase() = default; QWidget *FormTextInputBase::widget() const { return d->mWidget; } QLabel *FormTextInputBase::label() const { return d->mLabel; } ErrorLabel *FormTextInputBase::errorLabel() const { return d->mErrorLabel; } +void FormTextInputBase::setLabelText(const QString &text) +{ + d->mLabelText = text; + d->updateLabel(); +} + void FormTextInputBase::setIsRequired(bool required) { d->mRequired = required; + d->updateLabel(); } bool FormTextInputBase::isRequired() const { return d->mRequired; } void FormTextInputBase::setValidator(const QValidator *validator) { d->mValidator = validator; } void FormTextInputBase::setValueRequiredErrorMessage(const QString &text) { if (text.isEmpty()) { d->mValueRequiredErrorMessage = defaultValueRequiredErrorMessage(); } else { d->mValueRequiredErrorMessage = text; } } void FormTextInputBase::setInvalidEntryErrorMessage(const QString &text) { if (text.isEmpty()) { d->mInvalidEntryErrorMessage = defaultInvalidEntryErrorMessage(); } else { d->mInvalidEntryErrorMessage = text; } } void FormTextInputBase::setToolTip(const QString &toolTip) { if (d->mLabel) { d->mLabel->setToolTip(toolTip); } if (d->mWidget) { d->mWidget->setToolTip(toolTip); } } void FormTextInputBase::setAccessibleName(const QString &name) { d->mAccessibleName = name; d->updateAccessibleNameAndDescription(); } void FormTextInputBase::setAccessibleDescription(const QString &description) { d->mAccessibleDescription = description; d->updateAccessibleNameAndDescription(); } void FormTextInputBase::setWidget(QWidget *widget) { auto parent = widget ? widget->parentWidget() : nullptr; d->mWidget = widget; d->mLabel = new QLabel{parent}; d->mErrorLabel = new ErrorLabel{parent}; if (d->mLabel) { d->mLabel->setBuddy(d->mWidget); } if (d->mErrorLabel) { d->mErrorLabel->setVisible(false); } connectWidget(); } void FormTextInputBase::setEnabled(bool enabled) { if (d->mLabel) { d->mLabel->setEnabled(enabled); } if (d->mWidget) { d->mWidget->setEnabled(enabled); } if (d->mErrorLabel) { d->mErrorLabel->setVisible(enabled && !d->mErrorLabel->text().isEmpty()); } } bool FormTextInputBase::validate(const QString &text, int pos) const { QString textCopy = text; if (d->mValidator && d->mValidator->validate(textCopy, pos) != QValidator::Acceptable) { return false; } return true; } void FormTextInputBase::onTextChanged() { d->mEditingInProgress = true; d->updateError(); } void FormTextInputBase::onEditingFinished() { d->mEditingInProgress = false; d->updateError(); } } template<> bool Kleo::FormTextInput::hasValue() const { const auto w = widget(); return w && !w->text().trimmed().isEmpty(); } template<> bool Kleo::FormTextInput::hasAcceptableInput() const { const auto w = widget(); return w && validate(w->text(), w->cursorPosition()); } template<> void Kleo::FormTextInput::connectWidget() { const auto w = widget(); QObject::connect(w, &QLineEdit::editingFinished, w, [this]() { onEditingFinished(); }); QObject::connect(w, &QLineEdit::textChanged, w, [this]() { onTextChanged(); }); } diff --git a/src/view/formtextinput.h b/src/view/formtextinput.h index 1a75a65bb..4e90de377 100644 --- a/src/view/formtextinput.h +++ b/src/view/formtextinput.h @@ -1,220 +1,228 @@ /* view/formtextinput.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include class QLabel; class QLineEdit; class QString; class QValidator; class QWidget; namespace Kleo { class ErrorLabel; namespace _detail { class FormTextInputBase { protected: FormTextInputBase(); public: virtual ~FormTextInputBase(); FormTextInputBase(const FormTextInputBase&) = delete; FormTextInputBase& operator=(const FormTextInputBase&) = delete; FormTextInputBase(FormTextInputBase&&) = delete; FormTextInputBase& operator=(FormTextInputBase&&) = delete; /** - * Returns the label associated to the controlled widget. + * Returns the label associated to the controlled widget. Use it to add + * the label to a layout, but do not use it to set properties of the label + * for which this class provides setters. */ QLabel *label() const; /** * Returns the error label associated to the controlled widget. */ ErrorLabel *errorLabel() const; + /** + * Sets the text of the label. Do not add a colon at the end of the text. + * If input is required, then the label is marked appropriately. + */ + void setLabelText(const QString &text); + /** * Marks this input field as required. */ void setIsRequired(bool required); /** * Returns \c true, if this field needs to be filled out. */ bool isRequired() const; /** * Sets the validator to use for validating the input. * * Note: If you wrap a QLineEdit, then do not set a validator (or an input mask) * on it because this will break the correct displaying of the error message. */ void setValidator(const QValidator *validator); /** * Sets the error message to display if a value is required for this input field, * but if no value has been entered. If \p text is empty, then a default * message will be used. */ void setValueRequiredErrorMessage(const QString &text); /** * Sets the error message to display if the entered value is not accepted * by the validator. If \p text is empty, then a default message will be used. */ void setInvalidEntryErrorMessage(const QString &text); /** * Sets the tool tip of the controlled widget and its associated label. */ void setToolTip(const QString &toolTip); /** * Sets the accessible name of the controlled widget. Use this function * instead of setting the accessible name directly on the controlled widget. */ void setAccessibleName(const QString &name); /** * Sets the accessible description of the controlled widget. Use this * function instead of setting the accessible description directly on * the controlled widget. */ void setAccessibleDescription(const QString &description); /** * Enables or disables the controlled widget and its associated label. * If the widget is disables, then the error label is hidden. Otherwise, * the error label is shown if there is an error. */ void setEnabled(bool enabled); /** * Returns \c true, if the input has a value. This function is used to * check required input fields for non-empty user input. * Needs to be implemented for concrete widget classes. * \sa validate */ virtual bool hasValue() const = 0; /** * Returns \c true, if the input satisfies the validator. * Needs to be implemented for concrete widget classes. * \sa validate */ virtual bool hasAcceptableInput() const = 0; protected: /** * Connects the slots \ref onTextChanged and \ref onEditingFinished to the * corresponding signal of the controlled widget. * Needs to be implemented for concrete widget classes. */ virtual void connectWidget() = 0; /** * Sets the controlled widget and creates the associated labels. */ void setWidget(QWidget *widget); /** * Returns the controlled widget. */ QWidget *widget() const; /** * Validates \p text with the validator. Should be used when implementing * \ref hasAcceptableInput. */ bool validate(const QString &text, int pos) const; /** * This slot needs to be connected to a signal of the controlled widget * that is emitted when the text changes like \ref QLineEdit::textChanged. * \sa connectWidget */ void onTextChanged(); /** * This slot needs to be connected to a signal of the controlled widget * that is emitted when the widget loses focus (or some user interaction * signals that they want to commit the entered text) like * \ref QLineEdit::editingFinished. * \sa connectWidget */ void onEditingFinished(); private: class Private; const std::unique_ptr d; }; } /** * FormTextInput is a class for simplifying the management of text input widgets * like QLineEdit or QTextEdit with associated label and error message for usage * in form-like dialogs. * * Usage hints: * * If you wrap a QLineEdit, then do not set a validator (or an input mask) * on it. Instead set the validator on this class. * If you set a validator on the QLineEdit, then showing the error message * when editing is finished does not work because QLineEdit doesn't emit the * editingFinished() signal if the input is not acceptable. */ template class FormTextInput : public _detail::FormTextInputBase { /** * Use \ref create to create a new instance. */ FormTextInput() = default; public: /** * Creates a new instance of this class with a new instance of \p Widget. */ static auto create(QWidget *parent) { std::unique_ptr self{new FormTextInput}; self->setWidget(new Widget{parent}); return self; } /** * Returns the controlled widget. */ Widget *widget() const { return static_cast(FormTextInputBase::widget()); } bool hasValue() const override; bool hasAcceptableInput() const override; private: void connectWidget() override; }; template<> bool FormTextInput::hasValue() const; template<> bool FormTextInput::hasAcceptableInput() const; template<> void FormTextInput::connectWidget(); }