Libkleo

formtextinput.cpp
1/*
2 This file is part of libkleopatra
3 SPDX-FileCopyrightText: 2022 g10 Code GmbH
4 SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
5
6 SPDX-License-Identifier: GPL-2.0-or-later
7*/
8
9#include "formtextinput_p.h"
10
11#include "ui/errorlabel.h"
12
13#include <KLocalizedString>
14
15#include <QAccessible>
16#include <QLabel>
17#include <QLineEdit>
18#include <QPointer>
19#include <QValidator>
20
21namespace
22{
23auto defaultValueRequiredErrorMessage()
24{
25 return i18n("Error: Enter a value.");
26}
27
28auto defaultInvalidEntryErrorMessage()
29{
30 return i18n("Error: Enter a value in the correct format.");
31}
32
33QString getAccessibleText(QWidget *widget, QAccessible::Text t)
34{
35 QString name;
36 if (const auto *const iface = QAccessible::queryAccessibleInterface(widget)) {
37 name = iface->text(t);
38 }
39 return name;
40}
41}
42
43namespace Kleo::_detail
44{
45
46class FormTextInputBase::Private
47{
48 FormTextInputBase *q;
49
50public:
51 enum Error {
52 EntryOK,
53 EntryMissing, // a required entry is missing
54 InvalidEntry // the validator doesn't accept the entry
55 };
56
57 Private(FormTextInputBase *q)
58 : q{q}
59 , mValueRequiredErrorMessage{defaultValueRequiredErrorMessage()}
60 , mInvalidEntryErrorMessage{defaultInvalidEntryErrorMessage()}
61 {
62 }
63
64 QString annotatedIfRequired(const QString &text) const;
65 void updateLabel();
66 void setLabelText(const QString &text, const QString &accessibleName);
67 void setHint(const QString &text, const QString &accessibleDescription);
68 QString errorMessage(Error error) const;
69 QString accessibleErrorMessage(Error error) const;
70 void updateError();
71 QString accessibleDescription() const;
72 void updateAccessibleNameAndDescription();
73
74 QPointer<QLabel> mLabel;
75 QPointer<QLabel> mHintLabel;
76 QPointer<QWidget> mWidget;
77 QPointer<ErrorLabel> mErrorLabel;
78 std::shared_ptr<QValidator> mValidator;
79 QString mLabelText;
80 QString mAccessibleName;
81 QString mValueRequiredErrorMessage;
82 QString mAccessibleValueRequiredErrorMessage;
83 QString mInvalidEntryErrorMessage;
84 QString mAccessibleInvalidEntryErrorMessage;
85 Error mError = EntryOK;
86 bool mRequired = false;
87 bool mEditingInProgress = false;
88};
89
90QString FormTextInputBase::Private::annotatedIfRequired(const QString &text) const
91{
92 return mRequired ? i18nc("@label label text (required)", "%1 (required)", text) //
93 : text;
94}
95
96void FormTextInputBase::Private::updateLabel()
97{
98 if (mLabel) {
99 mLabel->setText(annotatedIfRequired(mLabelText));
100 }
101}
102
103void FormTextInputBase::Private::setLabelText(const QString &text, const QString &accessibleName)
104{
105 mLabelText = text;
106 mAccessibleName = accessibleName.isEmpty() ? text : accessibleName;
107 updateLabel();
108 updateAccessibleNameAndDescription();
109}
110
111void FormTextInputBase::Private::setHint(const QString &text, const QString &accessibleDescription)
112{
113 if (!mHintLabel) {
114 return;
115 }
116 mHintLabel->setVisible(!text.isEmpty());
117 mHintLabel->setText(text);
118 mHintLabel->setAccessibleName(accessibleDescription.isEmpty() ? text : accessibleDescription);
119 updateAccessibleNameAndDescription();
120}
121
122namespace
123{
124QString decoratedError(const QString &text)
125{
126 return text.isEmpty() ? QString() : i18nc("@info", "Error: %1", text);
127}
128}
129
130QString FormTextInputBase::Private::errorMessage(Error error) const
131{
132 switch (error) {
133 case EntryOK:
134 return {};
135 case EntryMissing:
136 return mValueRequiredErrorMessage;
137 case InvalidEntry:
138 return mInvalidEntryErrorMessage;
139 }
140 return {};
141}
142
143QString FormTextInputBase::Private::accessibleErrorMessage(Error error) const
144{
145 switch (error) {
146 case EntryOK:
147 return {};
148 case EntryMissing:
149 return mAccessibleValueRequiredErrorMessage;
150 case InvalidEntry:
151 return mAccessibleInvalidEntryErrorMessage;
152 }
153 return {};
154}
155
156void FormTextInputBase::Private::updateError()
157{
158 if (!mErrorLabel) {
159 return;
160 }
161
162 if (mRequired && !q->hasValue()) {
163 mError = EntryMissing;
164 } else if (!q->hasAcceptableInput()) {
165 mError = InvalidEntry;
166 } else {
167 mError = EntryOK;
168 }
169
170 const auto currentErrorMessage = mErrorLabel->text();
171 const auto newErrorMessage = decoratedError(errorMessage(mError));
172 if (newErrorMessage == currentErrorMessage) {
173 return;
174 }
175 if (currentErrorMessage.isEmpty() && mEditingInProgress) {
176 // delay showing the error message until editing is finished, so that we
177 // do not annoy the user with an error message while they are still
178 // entering the recipient;
179 // on the other hand, we clear the error message immediately if it does
180 // not apply anymore and we update the error message immediately if it
181 // changed
182 return;
183 }
184 mErrorLabel->setVisible(!newErrorMessage.isEmpty());
185 mErrorLabel->setText(newErrorMessage);
186 mErrorLabel->setAccessibleName(decoratedError(accessibleErrorMessage(mError)));
187 updateAccessibleNameAndDescription();
188}
189
190QString FormTextInputBase::Private::accessibleDescription() const
191{
192 QString description;
193 if (mHintLabel) {
194 // get the explicitly set accessible hint text
195 description = mHintLabel->accessibleName();
196 }
197 if (description.isEmpty()) {
198 // fall back to the default accessible description of the input widget
199 description = getAccessibleText(mWidget, QAccessible::Description);
200 }
201 return description;
202}
203
204void FormTextInputBase::Private::updateAccessibleNameAndDescription()
205{
206 // fall back to default accessible name if accessible name wasn't set explicitly
207 if (mAccessibleName.isEmpty()) {
208 mAccessibleName = getAccessibleText(mWidget, QAccessible::Name);
209 }
210 const bool errorShown = mErrorLabel && mErrorLabel->isVisible();
211
212 // Qt does not support "described-by" relations (like WCAG's "aria-describedby" relationship attribute);
213 // emulate this by setting the hint text and, if the error is shown, the error message as accessible
214 // description of the input field
215 const auto description = errorShown ? accessibleDescription() + QLatin1StringView{" "} + mErrorLabel->accessibleName() //
216 : accessibleDescription();
217 if (mWidget && mWidget->accessibleDescription() != description) {
218 mWidget->setAccessibleDescription(description);
219 }
220
221 // Qt does not support IA2's "invalid entry" state (like WCAG's "aria-invalid" state attribute);
222 // screen readers say something like "invalid entry" if this state is set;
223 // emulate this by adding "invalid entry" to the accessible name of the input field
224 // and its label
225 QString name = annotatedIfRequired(mAccessibleName);
226 if (errorShown) {
227 name += QLatin1StringView{", "}
228 + i18nc("text for screen readers to indicate that the associated object, "
229 "such as a form field, has an error",
230 "invalid entry");
231 }
232 if (mLabel && mLabel->accessibleName() != name) {
233 mLabel->setAccessibleName(name);
234 }
235 if (mWidget && mWidget->accessibleName() != name) {
236 mWidget->setAccessibleName(name);
237 }
238}
239
240FormTextInputBase::FormTextInputBase()
241 : d{new Private{this}}
242{
243}
244
245FormTextInputBase::~FormTextInputBase() = default;
246
247QWidget *FormTextInputBase::widget() const
248{
249 return d->mWidget;
250}
251
252QLabel *FormTextInputBase::label() const
253{
254 return d->mLabel;
255}
256
257QLabel *FormTextInputBase::hintLabel() const
258{
259 return d->mHintLabel;
260}
261
262ErrorLabel *FormTextInputBase::errorLabel() const
263{
264 return d->mErrorLabel;
265}
266
267void FormTextInputBase::setLabelText(const QString &text, const QString &accessibleName)
268{
269 d->setLabelText(text, accessibleName);
270}
271
272void FormTextInputBase::setHint(const QString &text, const QString &accessibleDescription)
273{
274 d->setHint(text, accessibleDescription);
275}
276
277void FormTextInputBase::setIsRequired(bool required)
278{
279 d->mRequired = required;
280 d->updateLabel();
281 d->updateAccessibleNameAndDescription();
282}
283
284bool FormTextInputBase::isRequired() const
285{
286 return d->mRequired;
287}
288
289void FormTextInputBase::setValidator(const std::shared_ptr<QValidator> &validator)
290{
291 Q_ASSERT(!validator || !validator->parent());
292
293 d->mValidator = validator;
294}
295
296void FormTextInputBase::setValueRequiredErrorMessage(const QString &text, const QString &accessibleText)
297{
298 if (text.isEmpty()) {
299 d->mValueRequiredErrorMessage = defaultValueRequiredErrorMessage();
300 } else {
301 d->mValueRequiredErrorMessage = text;
302 }
303 if (accessibleText.isEmpty()) {
304 d->mAccessibleValueRequiredErrorMessage = d->mValueRequiredErrorMessage;
305 } else {
306 d->mAccessibleValueRequiredErrorMessage = accessibleText;
307 }
308}
309
310void FormTextInputBase::setInvalidEntryErrorMessage(const QString &text, const QString &accessibleText)
311{
312 if (text.isEmpty()) {
313 d->mInvalidEntryErrorMessage = defaultInvalidEntryErrorMessage();
314 } else {
315 d->mInvalidEntryErrorMessage = text;
316 }
317 if (accessibleText.isEmpty()) {
318 d->mAccessibleInvalidEntryErrorMessage = d->mInvalidEntryErrorMessage;
319 } else {
320 d->mAccessibleInvalidEntryErrorMessage = accessibleText;
321 }
322}
323
324void FormTextInputBase::setToolTip(const QString &toolTip)
325{
326 if (d->mLabel) {
327 d->mLabel->setToolTip(toolTip);
328 }
329 if (d->mWidget) {
330 d->mWidget->setToolTip(toolTip);
331 }
332}
333
334void FormTextInputBase::setWidget(QWidget *widget)
335{
336 auto parent = widget ? widget->parentWidget() : nullptr;
337 d->mWidget = widget;
338 d->mLabel = new QLabel{parent};
339 d->mLabel->setTextFormat(Qt::PlainText);
340 d->mLabel->setWordWrap(true);
341 QFont font = d->mLabel->font();
342 font.setBold(true);
343 d->mLabel->setFont(font);
344 d->mLabel->setBuddy(d->mWidget);
345 d->mHintLabel = new QLabel{parent};
346 d->mHintLabel->setWordWrap(true);
347 d->mHintLabel->setTextFormat(Qt::PlainText);
348 // set widget as buddy of hint label, so that the label isn't considered unrelated
349 d->mHintLabel->setBuddy(d->mWidget);
350 d->mHintLabel->setVisible(false);
351 d->mErrorLabel = new ErrorLabel{parent};
352 d->mErrorLabel->setWordWrap(true);
353 d->mErrorLabel->setTextFormat(Qt::PlainText);
354 // set widget as buddy of error label, so that the label isn't considered unrelated
355 d->mErrorLabel->setBuddy(d->mWidget);
356 d->mErrorLabel->setVisible(false);
357 connectWidget();
358}
359
360void FormTextInputBase::setEnabled(bool enabled)
361{
362 if (d->mLabel) {
363 d->mLabel->setEnabled(enabled);
364 }
365 if (d->mWidget) {
366 d->mWidget->setEnabled(enabled);
367 }
368 if (d->mErrorLabel) {
369 d->mErrorLabel->setVisible(enabled && !d->mErrorLabel->text().isEmpty());
370 }
371}
372
373QString FormTextInputBase::currentError() const
374{
375 if (d->mError) {
376 return d->errorMessage(d->mError);
377 }
378 return {};
379}
380
381bool FormTextInputBase::validate(const QString &text, int pos) const
382{
383 QString textCopy = text;
384 if (d->mValidator && d->mValidator->validate(textCopy, pos) != QValidator::Acceptable) {
385 return false;
386 }
387 return true;
388}
389
390void FormTextInputBase::onTextChanged()
391{
392 d->mEditingInProgress = true;
393 d->updateError();
394}
395
396void FormTextInputBase::onEditingFinished()
397{
398 d->mEditingInProgress = false;
399 d->updateError();
400}
401
402}
403
404template<>
405bool Kleo::FormTextInput<QLineEdit>::hasValue() const
406{
407 const auto w = widget();
408 return w && !w->text().trimmed().isEmpty();
409}
410
411template<>
412bool Kleo::FormTextInput<QLineEdit>::hasAcceptableInput() const
413{
414 const auto w = widget();
415 return w && validate(w->text(), w->cursorPosition());
416}
417
418template<>
419void Kleo::FormTextInput<QLineEdit>::connectWidget()
420{
421 const auto w = widget();
423 onEditingFinished();
424 });
426 onTextChanged();
427 });
428}
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
KCALUTILS_EXPORT QString errorMessage(const KCalendarCore::Exception &exception)
QAccessibleInterface * queryAccessibleInterface(QObject *object)
void setBold(bool enable)
void setTextFormat(Qt::TextFormat)
void setWordWrap(bool on)
void editingFinished()
void textChanged(const QString &text)
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool isEmpty() const const
PlainText
QWidget * parentWidget() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:09:14 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.