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 }