diff --git a/src/utils/accessibility.cpp b/src/utils/accessibility.cpp index 442fc5ca1..51234100d 100644 --- a/src/utils/accessibility.cpp +++ b/src/utils/accessibility.cpp @@ -1,33 +1,46 @@ /* utils/accessibility.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 #include "accessibility.h" #include #include #include -QString Kleo::getAccessibleName(QObject *object) +namespace +{ +QString getAccessibleText(QObject *object, QAccessible::Text t) { QString name; if (const auto *const iface = QAccessible::queryAccessibleInterface(object)) { - name = iface->text(QAccessible::Name); + name = iface->text(t); } return name; } +} + +QString Kleo::getAccessibleName(QObject *object) +{ + return getAccessibleText(object, QAccessible::Name); +} + +QString Kleo::getAccessibleDescription(QObject *object) +{ + return getAccessibleText(object, QAccessible::Description); +} QString Kleo::invalidEntryText() { return i18nc("text for screen readers to indicate that the associated object, " "such as a form field, has an error", "invalid entry"); } diff --git a/src/utils/accessibility.h b/src/utils/accessibility.h index c3a096661..54fcac62d 100644 --- a/src/utils/accessibility.h +++ b/src/utils/accessibility.h @@ -1,18 +1,19 @@ /* utils/accessibility.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 class QObject; class QString; namespace Kleo { QString getAccessibleName(QObject *object); + QString getAccessibleDescription(QObject *object); QString invalidEntryText(); } diff --git a/src/view/formtextinput.cpp b/src/view/formtextinput.cpp index 7c693193e..dad113d09 100644 --- a/src/view/formtextinput.cpp +++ b/src/view/formtextinput.cpp @@ -1,220 +1,237 @@ /* 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 Kleo::_detail { class FormTextInputBase::Private { FormTextInputBase *q; public: Private(FormTextInputBase *q) : q{q} , mErrorMessage{i18n("Error: The entered text is not valid.")} {} QString errorMessage() const; void updateError(); void updateAccessibleNameAndDescription(); QPointer mLabel; QPointer mWidget; QPointer mErrorLabel; QPointer mValidator; QString mAccessibleName; + QString mAccessibleDescription; QString mErrorMessage; bool mEditingInProgress = false; }; QString FormTextInputBase::Private::errorMessage() const { return q->hasAcceptableInput() ? QString{} : mErrorMessage; } void FormTextInputBase::Private::updateError() { if (!mErrorLabel) { return; } const auto currentErrorMessage = mErrorLabel->text(); const auto newErrorMessage = errorMessage(); 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 if accessible name wasn't set explicitly + // 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 setting the error message as accessible description of the input field - const auto description = errorShown ? mErrorLabel->text() : QString{}; + // 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 const auto name = errorShown ? mAccessibleName + QLatin1String{", "} + invalidEntryText() : mAccessibleName; 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::setValidator(const QValidator *validator) { d->mValidator = validator; } void FormTextInputBase::setErrorMessage(const QString &text) { if (text.isEmpty()) { d->mErrorMessage = i18n("Error: The entered text is not valid."); } else { d->mErrorMessage = 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::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 8236ab6b5..69810fa2a 100644 --- a/src/view/formtextinput.h +++ b/src/view/formtextinput.h @@ -1,177 +1,190 @@ /* 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. */ QLabel *label() const; /** * Returns the error label associated to the controlled widget. */ ErrorLabel *errorLabel() 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 \p text is empty, then the default * error message will be used. */ void setErrorMessage(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 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 hasAcceptableInput() const override; private: void connectWidget() override; }; template<> bool FormTextInput::hasAcceptableInput() const; template<> void FormTextInput::connectWidget(); }