File indexing completed on 2024-06-16 04:56:14

0001 /*  view/formtextinput.cpp
0002 
0003     This file is part of Kleopatra, the KDE keymanager
0004     SPDX-FileCopyrightText: 2022 g10 Code GmbH
0005     SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 #include "formtextinput.h"
0011 
0012 #include "errorlabel.h"
0013 #include "utils/accessibility.h"
0014 
0015 #include <KLocalizedString>
0016 
0017 #include <QLabel>
0018 #include <QLineEdit>
0019 #include <QPointer>
0020 #include <QValidator>
0021 
0022 #include "kleopatra_debug.h"
0023 
0024 namespace
0025 {
0026 auto defaultValueRequiredErrorMessage()
0027 {
0028     return i18n("Error: Enter a value.");
0029 }
0030 
0031 auto defaultInvalidEntryErrorMessage()
0032 {
0033     return i18n("Error: Enter a value in the correct format.");
0034 }
0035 }
0036 
0037 namespace Kleo::_detail
0038 {
0039 
0040 class FormTextInputBase::Private
0041 {
0042     FormTextInputBase *q;
0043 
0044 public:
0045     enum Error {
0046         EntryOK,
0047         EntryMissing, // a required entry is missing
0048         InvalidEntry // the validator doesn't accept the entry
0049     };
0050 
0051     Private(FormTextInputBase *q)
0052         : q{q}
0053         , mValueRequiredErrorMessage{defaultValueRequiredErrorMessage()}
0054         , mInvalidEntryErrorMessage{defaultInvalidEntryErrorMessage()}
0055     {
0056     }
0057 
0058     QString annotatedIfRequired(const QString &text) const;
0059     void updateLabel();
0060     void setLabelText(const QString &text, const QString &accessibleName);
0061     void setHint(const QString &text, const QString &accessibleDescription);
0062     QString errorMessage(Error error) const;
0063     QString accessibleErrorMessage(Error error) const;
0064     void updateError();
0065     QString accessibleDescription() const;
0066     void updateAccessibleNameAndDescription();
0067 
0068     QPointer<QLabel> mLabel;
0069     QPointer<QLabel> mHintLabel;
0070     QPointer<QWidget> mWidget;
0071     QPointer<ErrorLabel> mErrorLabel;
0072     std::shared_ptr<QValidator> mValidator;
0073     QString mLabelText;
0074     QString mAccessibleName;
0075     QString mValueRequiredErrorMessage;
0076     QString mAccessibleValueRequiredErrorMessage;
0077     QString mInvalidEntryErrorMessage;
0078     QString mAccessibleInvalidEntryErrorMessage;
0079     Error mError = EntryOK;
0080     bool mRequired = false;
0081     bool mEditingInProgress = false;
0082 };
0083 
0084 QString FormTextInputBase::Private::annotatedIfRequired(const QString &text) const
0085 {
0086     return mRequired ? i18nc("@label label text (required)", "%1 (required)", text) //
0087                      : text;
0088 }
0089 
0090 void FormTextInputBase::Private::updateLabel()
0091 {
0092     if (mLabel) {
0093         mLabel->setText(annotatedIfRequired(mLabelText));
0094     }
0095 }
0096 
0097 void FormTextInputBase::Private::setLabelText(const QString &text, const QString &accessibleName)
0098 {
0099     mLabelText = text;
0100     mAccessibleName = accessibleName.isEmpty() ? text : accessibleName;
0101     updateLabel();
0102     updateAccessibleNameAndDescription();
0103 }
0104 
0105 void FormTextInputBase::Private::setHint(const QString &text, const QString &accessibleDescription)
0106 {
0107     if (!mHintLabel) {
0108         return;
0109     }
0110     mHintLabel->setVisible(!text.isEmpty());
0111     mHintLabel->setText(text);
0112     mHintLabel->setAccessibleName(accessibleDescription.isEmpty() ? text : accessibleDescription);
0113     updateAccessibleNameAndDescription();
0114 }
0115 
0116 namespace
0117 {
0118 QString decoratedError(const QString &text)
0119 {
0120     return text.isEmpty() ? QString() : i18nc("@info", "Error: %1", text);
0121 }
0122 }
0123 
0124 QString FormTextInputBase::Private::errorMessage(Error error) const
0125 {
0126     switch (error) {
0127     case EntryOK:
0128         return {};
0129     case EntryMissing:
0130         return mValueRequiredErrorMessage;
0131     case InvalidEntry:
0132         return mInvalidEntryErrorMessage;
0133     }
0134     return {};
0135 }
0136 
0137 QString FormTextInputBase::Private::accessibleErrorMessage(Error error) const
0138 {
0139     switch (error) {
0140     case EntryOK:
0141         return {};
0142     case EntryMissing:
0143         return mAccessibleValueRequiredErrorMessage;
0144     case InvalidEntry:
0145         return mAccessibleInvalidEntryErrorMessage;
0146     }
0147     return {};
0148 }
0149 
0150 void FormTextInputBase::Private::updateError()
0151 {
0152     if (!mErrorLabel) {
0153         return;
0154     }
0155 
0156     if (mRequired && !q->hasValue()) {
0157         mError = EntryMissing;
0158     } else if (!q->hasAcceptableInput()) {
0159         mError = InvalidEntry;
0160     } else {
0161         mError = EntryOK;
0162     }
0163 
0164     const auto currentErrorMessage = mErrorLabel->text();
0165     const auto newErrorMessage = decoratedError(errorMessage(mError));
0166     if (newErrorMessage == currentErrorMessage) {
0167         return;
0168     }
0169     if (currentErrorMessage.isEmpty() && mEditingInProgress) {
0170         // delay showing the error message until editing is finished, so that we
0171         // do not annoy the user with an error message while they are still
0172         // entering the recipient;
0173         // on the other hand, we clear the error message immediately if it does
0174         // not apply anymore and we update the error message immediately if it
0175         // changed
0176         return;
0177     }
0178     mErrorLabel->setVisible(!newErrorMessage.isEmpty());
0179     mErrorLabel->setText(newErrorMessage);
0180     mErrorLabel->setAccessibleName(decoratedError(accessibleErrorMessage(mError)));
0181     updateAccessibleNameAndDescription();
0182 }
0183 
0184 QString FormTextInputBase::Private::accessibleDescription() const
0185 {
0186     QString description;
0187     if (mHintLabel) {
0188         // get the explicitly set accessible hint text
0189         description = mHintLabel->accessibleName();
0190     }
0191     if (description.isEmpty()) {
0192         // fall back to the default accessible description of the input widget
0193         description = getAccessibleDescription(mWidget);
0194     }
0195     return description;
0196 }
0197 
0198 void FormTextInputBase::Private::updateAccessibleNameAndDescription()
0199 {
0200     // fall back to default accessible name if accessible name wasn't set explicitly
0201     if (mAccessibleName.isEmpty()) {
0202         mAccessibleName = getAccessibleName(mWidget);
0203     }
0204     const bool errorShown = mErrorLabel && mErrorLabel->isVisible();
0205 
0206     // Qt does not support "described-by" relations (like WCAG's "aria-describedby" relationship attribute);
0207     // emulate this by setting the hint text and, if the error is shown, the error message as accessible
0208     // description of the input field
0209     const auto description = errorShown ? accessibleDescription() + QLatin1StringView{" "} + mErrorLabel->accessibleName() //
0210                                         : accessibleDescription();
0211     if (mWidget && mWidget->accessibleDescription() != description) {
0212         mWidget->setAccessibleDescription(description);
0213     }
0214 
0215     // Qt does not support IA2's "invalid entry" state (like WCAG's "aria-invalid" state attribute);
0216     // screen readers say something like "invalid entry" if this state is set;
0217     // emulate this by adding "invalid entry" to the accessible name of the input field
0218     // and its label
0219     QString name = annotatedIfRequired(mAccessibleName);
0220     if (errorShown) {
0221         name += QLatin1StringView{", "} + invalidEntryText();
0222     };
0223     if (mLabel && mLabel->accessibleName() != name) {
0224         mLabel->setAccessibleName(name);
0225     }
0226     if (mWidget && mWidget->accessibleName() != name) {
0227         mWidget->setAccessibleName(name);
0228     }
0229 }
0230 
0231 FormTextInputBase::FormTextInputBase()
0232     : d{new Private{this}}
0233 {
0234 }
0235 
0236 FormTextInputBase::~FormTextInputBase() = default;
0237 
0238 QWidget *FormTextInputBase::widget() const
0239 {
0240     return d->mWidget;
0241 }
0242 
0243 QLabel *FormTextInputBase::label() const
0244 {
0245     return d->mLabel;
0246 }
0247 
0248 QLabel *FormTextInputBase::hintLabel() const
0249 {
0250     return d->mHintLabel;
0251 }
0252 
0253 ErrorLabel *FormTextInputBase::errorLabel() const
0254 {
0255     return d->mErrorLabel;
0256 }
0257 
0258 void FormTextInputBase::setLabelText(const QString &text, const QString &accessibleName)
0259 {
0260     d->setLabelText(text, accessibleName);
0261 }
0262 
0263 void FormTextInputBase::setHint(const QString &text, const QString &accessibleDescription)
0264 {
0265     d->setHint(text, accessibleDescription);
0266 }
0267 
0268 void FormTextInputBase::setIsRequired(bool required)
0269 {
0270     d->mRequired = required;
0271     d->updateLabel();
0272     d->updateAccessibleNameAndDescription();
0273 }
0274 
0275 bool FormTextInputBase::isRequired() const
0276 {
0277     return d->mRequired;
0278 }
0279 
0280 void FormTextInputBase::setValidator(const std::shared_ptr<QValidator> &validator)
0281 {
0282     Q_ASSERT(!validator || !validator->parent());
0283 
0284     d->mValidator = validator;
0285 }
0286 
0287 void FormTextInputBase::setValueRequiredErrorMessage(const QString &text, const QString &accessibleText)
0288 {
0289     if (text.isEmpty()) {
0290         d->mValueRequiredErrorMessage = defaultValueRequiredErrorMessage();
0291     } else {
0292         d->mValueRequiredErrorMessage = text;
0293     }
0294     if (accessibleText.isEmpty()) {
0295         d->mAccessibleValueRequiredErrorMessage = d->mValueRequiredErrorMessage;
0296     } else {
0297         d->mAccessibleValueRequiredErrorMessage = accessibleText;
0298     }
0299 }
0300 
0301 void FormTextInputBase::setInvalidEntryErrorMessage(const QString &text, const QString &accessibleText)
0302 {
0303     if (text.isEmpty()) {
0304         d->mInvalidEntryErrorMessage = defaultInvalidEntryErrorMessage();
0305     } else {
0306         d->mInvalidEntryErrorMessage = text;
0307     }
0308     if (accessibleText.isEmpty()) {
0309         d->mAccessibleInvalidEntryErrorMessage = d->mInvalidEntryErrorMessage;
0310     } else {
0311         d->mAccessibleInvalidEntryErrorMessage = accessibleText;
0312     }
0313 }
0314 
0315 void FormTextInputBase::setToolTip(const QString &toolTip)
0316 {
0317     if (d->mLabel) {
0318         d->mLabel->setToolTip(toolTip);
0319     }
0320     if (d->mWidget) {
0321         d->mWidget->setToolTip(toolTip);
0322     }
0323 }
0324 
0325 void FormTextInputBase::setWidget(QWidget *widget)
0326 {
0327     auto parent = widget ? widget->parentWidget() : nullptr;
0328     d->mWidget = widget;
0329     d->mLabel = new QLabel{parent};
0330     d->mLabel->setTextFormat(Qt::PlainText);
0331     d->mLabel->setWordWrap(true);
0332     QFont font = d->mLabel->font();
0333     font.setBold(true);
0334     d->mLabel->setFont(font);
0335     d->mLabel->setBuddy(d->mWidget);
0336     d->mHintLabel = new QLabel{parent};
0337     d->mHintLabel->setWordWrap(true);
0338     d->mHintLabel->setTextFormat(Qt::PlainText);
0339     // set widget as buddy of hint label, so that the label isn't considered unrelated
0340     d->mHintLabel->setBuddy(d->mWidget);
0341     d->mHintLabel->setVisible(false);
0342     d->mErrorLabel = new ErrorLabel{parent};
0343     d->mErrorLabel->setWordWrap(true);
0344     d->mErrorLabel->setTextFormat(Qt::PlainText);
0345     // set widget as buddy of error label, so that the label isn't considered unrelated
0346     d->mErrorLabel->setBuddy(d->mWidget);
0347     d->mErrorLabel->setVisible(false);
0348     connectWidget();
0349 }
0350 
0351 void FormTextInputBase::setEnabled(bool enabled)
0352 {
0353     if (d->mLabel) {
0354         d->mLabel->setEnabled(enabled);
0355     }
0356     if (d->mWidget) {
0357         d->mWidget->setEnabled(enabled);
0358     }
0359     if (d->mErrorLabel) {
0360         d->mErrorLabel->setVisible(enabled && !d->mErrorLabel->text().isEmpty());
0361     }
0362 }
0363 
0364 QString FormTextInputBase::currentError() const
0365 {
0366     if (d->mError) {
0367         return d->errorMessage(d->mError);
0368     }
0369     return {};
0370 }
0371 
0372 bool FormTextInputBase::validate(const QString &text, int pos) const
0373 {
0374     QString textCopy = text;
0375     if (d->mValidator && d->mValidator->validate(textCopy, pos) != QValidator::Acceptable) {
0376         return false;
0377     }
0378     return true;
0379 }
0380 
0381 void FormTextInputBase::onTextChanged()
0382 {
0383     d->mEditingInProgress = true;
0384     d->updateError();
0385 }
0386 
0387 void FormTextInputBase::onEditingFinished()
0388 {
0389     d->mEditingInProgress = false;
0390     d->updateError();
0391 }
0392 
0393 }
0394 
0395 template<>
0396 bool Kleo::FormTextInput<QLineEdit>::hasValue() const
0397 {
0398     const auto w = widget();
0399     return w && !w->text().trimmed().isEmpty();
0400 }
0401 
0402 template<>
0403 bool Kleo::FormTextInput<QLineEdit>::hasAcceptableInput() const
0404 {
0405     const auto w = widget();
0406     return w && validate(w->text(), w->cursorPosition());
0407 }
0408 
0409 template<>
0410 void Kleo::FormTextInput<QLineEdit>::connectWidget()
0411 {
0412     const auto w = widget();
0413     QObject::connect(w, &QLineEdit::editingFinished, w, [this]() {
0414         onEditingFinished();
0415     });
0416     QObject::connect(w, &QLineEdit::textChanged, w, [this]() {
0417         onTextChanged();
0418     });
0419 }