Commit df5f8bb4 authored by Ingo Klöcker's avatar Ingo Klöcker
Browse files

Add class wrapping a text input used in a form-like dialog

This class is supposed to simplify the management and the error handling
of text input widgets like QLineEdit or QTextEdit. In particular, it
takes care of making error reporting accessible.

GnuPG-bug-id: 5916
parent 4e7c5381
......@@ -108,6 +108,7 @@ set(_kleopatra_SRCS
${_kleopatra_extra_SRCS}
view/errorlabel.cpp
view/formtextinput.cpp
view/htmllabel.cpp
view/keylistcontroller.cpp
view/keytreeview.cpp
......
/* view/formtextinput.cpp
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2022 g10 Code GmbH
SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "formtextinput.h"
#include "errorlabel.h"
#include "utils/accessibility.h"
#include <KLocalizedString>
#include <QLabel>
#include <QLineEdit>
#include <QPointer>
#include <QValidator>
#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<QLabel> mLabel;
QPointer<QWidget> mWidget;
QPointer<ErrorLabel> mErrorLabel;
QPointer<const QValidator> mValidator;
QString mAccessibleName;
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
if (mAccessibleName.isEmpty()) {
mAccessibleName = getAccessibleName(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{};
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::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<QLineEdit>::hasAcceptableInput() const
{
const auto w = widget();
return w && validate(w->text(), w->cursorPosition());
}
template<>
void Kleo::FormTextInput<QLineEdit>::connectWidget()
{
const auto w = widget();
QObject::connect(w, &QLineEdit::editingFinished,
w, [this]() { onEditingFinished(); });
QObject::connect(w, &QLineEdit::textChanged,
w, [this]() { onTextChanged(); });
}
/* view/formtextinput.h
This file is part of Kleopatra, the KDE keymanager
SPDX-FileCopyrightText: 2022 g10 Code GmbH
SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <memory>
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);
/**
* 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<Private> 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 Widget>
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<FormTextInput> self{new FormTextInput};
self->setWidget(new Widget{parent});
return self;
}
/**
* Returns the controlled widget.
*/
Widget *widget() const
{
return static_cast<Widget *>(FormTextInputBase::widget());
}
bool hasAcceptableInput() const override;
private:
void connectWidget() override;
};
template<>
bool FormTextInput<QLineEdit>::hasAcceptableInput() const;
template<>
void FormTextInput<QLineEdit>::connectWidget();
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment