Krita Source Code Documentation
Loading...
Searching...
No Matches
kis_parse_spin_box_p.h
Go to the documentation of this file.
1/* This file is part of the KDE project
2 *
3 * SPDX-FileCopyrightText: 2016 Laurent Valentin Jospin <laurent.valentin@famillejospin.ch>
4 * SPDX-FileCopyrightText: 2021 Deif Lou <ginoba@gmail.com>
5 *
6 * SPDX-License-Identifier: GPL-2.0-or-later
7 */
8
9#ifndef KISPARSESPINBOXPRIVATE_H
10#define KISPARSESPINBOXPRIVATE_H
11
12#include <QTimer>
13#include <QVariantAnimation>
14#include <QValidator>
15#include <QLineEdit>
16#include <QIcon>
17#include <QFile>
18#include <QPainter>
19#include <QApplication>
20#include <QStyleOptionSpinBox>
21#include <QEvent>
22#include <QKeyEvent>
23#include <QMouseEvent>
24#include <QResizeEvent>
25
26#include <cmath>
27#include <utility>
28#include <type_traits>
29
30#include <kis_painting_tweaks.h>
31#include <kis_num_parser.h>
32#include <kis_algebra_2d.h>
33
34template <typename SpinBoxTypeTP, typename BaseSpinBoxTypeTP>
35class Q_DECL_HIDDEN KisParseSpinBoxPrivate : public QObject
36{
37public:
38 using SpinBoxType = SpinBoxTypeTP;
39 using BaseSpinBoxType = BaseSpinBoxTypeTP;
40 using ValueType = decltype(std::declval<SpinBoxType>().value());
41
43 : m_q(q)
44 , m_lineEdit(m_q->lineEdit())
45 {
46 m_q->installEventFilter(this);
47
48 m_lineEdit->setAutoFillBackground(false);
49 m_lineEdit->installEventFilter(this);
50 connect(m_lineEdit, &QLineEdit::selectionChanged, this, &KisParseSpinBoxPrivate::fixupSelection);
51 connect(m_lineEdit, &QLineEdit::cursorPositionChanged, this, &KisParseSpinBoxPrivate::fixupCursorPosition);
52
53 m_timerShowWarning.setSingleShot(true);
54 connect(&m_timerShowWarning, &QTimer::timeout, this, QOverload<>::of(&KisParseSpinBoxPrivate::showWarning));
55 if (m_warningIcon.isNull() && QFile(":/./16_light_warning.svg").exists()) {
56 m_warningIcon = QIcon(":/./16_light_warning.svg");
57 }
58 m_warningAnimation.setStartValue(0.0);
59 m_warningAnimation.setEndValue(1.0);
60 m_warningAnimation.setEasingCurve(QEasingCurve(QEasingCurve::InOutCubic));
61 connect(&m_warningAnimation, &QVariantAnimation::valueChanged, m_lineEdit, QOverload<>::of(&QLineEdit::update));
62 }
63
64 void stepBy(int steps)
65 {
66 if (steps == 0) {
67 return;
68 }
69 // Use the reimplementation os setValue in this function so that we can
70 // clear the expression
71 setValue(m_q->value() + static_cast<ValueType>(steps) * m_q->singleStep(), true);
72 m_q->selectAll();
73 }
74
75 void setValue(ValueType value, bool overwriteExpression = false)
76 {
77 // The expression should be always cleared if the user is not
78 // actively editing the text
79 if (!m_q->hasFocus() || m_lineEdit->isReadOnly()) {
80 overwriteExpression = true;
81 }
82 // Clear the expression so that the line edit shows just the
83 // current value with prefix and suffix
84 if (overwriteExpression) {
85 m_lastExpressionParsed = QString();
86 }
87 // Prevent setting the new value if it is equal to the current one.
88 // That will maintain the current expression and warning status.
89 // If the value is different or the expression should overwritten then
90 // the value is set and the warning status is cleared
91 if (value != m_q->value() || overwriteExpression) {
92 m_q->BaseSpinBoxType::setValue(value);
93 if (!m_isLastValid) {
94 m_isLastValid = true;
95 hideWarning();
96 Q_EMIT m_q->noMoreParsingError();
97 }
98 }
99 }
100
101 bool isLastValid() const
102 {
103 return m_isLastValid;
104 }
105
106 QString veryCleanText() const
107 {
108 return m_q->cleanText();
109 }
110
111 QValidator::State validate(QString&, int&) const
112 {
113 // We want the user to be able to write any kind of expression.
114 // If it produces a valid value or not is decided in "valueFromText"
115 return QValidator::Acceptable;
116 }
117
118 // Helper function to evaluate a math expression string into an int
119 template <typename U = SpinBoxTypeTP, typename = typename std::enable_if<std::is_same<ValueType, int>::value, U>::type>
120 int parseMathExpression(const QString &text, bool *ok) const
121 {
123 }
124
125 // Helper function to evaluate a math expression string into a double
126 template <typename U = SpinBoxTypeTP, typename = typename std::enable_if<std::is_same<ValueType, double>::value, U>::type>
127 double parseMathExpression(const QString &text, bool *ok) const
128 {
130 if(qIsNaN(value) || qIsInf(value)){
131 *ok = false;
132 }
133 return value;
134 }
135
136 ValueType valueFromText(const QString &text) const
137 {
138 // Always hide the warning when the text changes
139 hideWarning();
140 // Get the expression, removing the prefix and suffix
141 m_lastExpressionParsed = text;
142 if (m_lastExpressionParsed.endsWith(m_q->suffix())) {
143 m_lastExpressionParsed.remove(m_lastExpressionParsed.size() - m_q->suffix().size(), m_q->suffix().size());
144 }
145 if(m_lastExpressionParsed.startsWith(m_q->prefix())){
146 m_lastExpressionParsed.remove(0, m_q->prefix().size());
147 }
148 // Parse
149 bool ok;
150 ValueType value = parseMathExpression(m_lastExpressionParsed, &ok);
151 // Validate
152 if (!ok) {
153 m_isLastValid = false;
154 value = m_q->value();
155 showWarning(showWarningInterval);
156 Q_EMIT m_q->errorWhileParsing(text);
157 } else {
158 if (!m_isLastValid) {
159 m_isLastValid = true;
160 Q_EMIT m_q->noMoreParsingError();
161 }
162 }
163 return value;
164 }
165
167 {
168 // If the last expression parsed is not null then the user actively
169 // changed the text, so that expression is returned regardless of the
170 // actual value
171 if (!m_lastExpressionParsed.isNull()) {
172 return m_lastExpressionParsed;
173 }
174 // Otherwise we transform the passed value to a string using the
175 // method from the base class and return that
176 return m_q->BaseSpinBoxType::textFromValue(value);
177 }
178
179 // Fix the selection so that the start and the end are in the value text
180 // and not in the prefix or suffix. This makes those unselectable
182 {
183 // If there's no selection just do nothing
184 if (m_lineEdit->selectedText().isEmpty()) {
185 return;
186 }
187 const int suffixStart = m_q->text().length() - m_q->suffix().length();
188 const int newStart = qBound(m_q->prefix().length(), m_lineEdit->selectionStart(), suffixStart);
189 const int newEnd = qBound(m_q->prefix().length(), m_lineEdit->selectionStart() + m_lineEdit->selectedText().length(), suffixStart);
190 if (m_lineEdit->cursorPosition() == m_lineEdit->selectionStart()) {
191 m_lineEdit->setSelection(newEnd, -(newEnd - newStart));
192 } else {
193 m_lineEdit->setSelection(newStart, newEnd - newStart);
194 }
195 }
196
197 // Fix the cursor position so that it is in the value text
198 // and not in the prefix or suffix.
199 void fixupCursorPosition(int oldPos, int newPos)
200 {
201 Q_UNUSED(oldPos);
202 if (newPos < m_q->prefix().length()) {
203 m_lineEdit->setCursorPosition(m_q->prefix().length());
204 } else {
205 const int suffixStart = m_q->text().length() - m_q->suffix().length();
206 if (newPos > suffixStart) {
207 m_lineEdit->setCursorPosition(suffixStart);
208 }
209 }
210 }
211
212 // Immediately show the warning overlay and icon
213 void showWarning() const
214 {
215 if (m_isWarningActive && m_warningAnimation.state() == QVariantAnimation::Running) {
216 return;
217 }
218 m_timerShowWarning.stop();
219 m_warningAnimation.stop();
220 m_isWarningActive = true;
221 if (!m_warningIcon.isNull()) {
222 QFontMetricsF fm(m_lineEdit->font());
223#if QT_VERSION >= QT_VERSION_CHECK(5,11,0)
224 const qreal textWidth = fm.horizontalAdvance(m_lineEdit->text());
225#else
226 const qreal textWidth = fm.width(m_lineEdit->text());
227#endif
228 const int minimumWidth =
229 static_cast<int>(
230 std::ceil(
231 textWidth + (m_q->alignment() == Qt::AlignCenter ? 2.0 : 1.0) * widthOfWarningIconArea + 4
232 )
233 );
234 if (m_lineEdit->width() >= minimumWidth) {
235 m_showWarningIcon = true;
236 } else {
237 m_showWarningIcon = false;
238 }
239 }
240 // scale the animation duration in case the animation is in the middle
241 const int animationDuration =
242 static_cast<int>(std::round((1.0 - m_warningAnimation.currentValue().toReal()) * warningAnimationDuration));
243 m_warningAnimation.setStartValue(m_warningAnimation.currentValue());
244 m_warningAnimation.setEndValue(1.0);
245 m_warningAnimation.setDuration(animationDuration);
246 m_warningAnimation.start();
247 }
248
249 // Show the warning after a specific amount of time
250 void showWarning(int delay) const
251 {
252 if (delay > 0) {
253 if (!m_isWarningActive || m_warningAnimation.state() != QVariantAnimation::Running) {
254 m_timerShowWarning.start(delay);
255 }
256 return;
257 }
258 // If "delay" is not greater that 0 then the warning will be
259 // immediately shown
260 showWarning();
261 }
262
263 void hideWarning() const
264 {
265 m_timerShowWarning.stop();
266 m_warningAnimation.stop();
267 m_isWarningActive = false;
268 // scale the animation duration in case the animation is in the middle
269 const int animationDuration =
270 static_cast<int>(std::round(m_warningAnimation.currentValue().toReal() * warningAnimationDuration));
271 m_warningAnimation.setStartValue(m_warningAnimation.currentValue());
272 m_warningAnimation.setEndValue(0.0);
273 m_warningAnimation.setDuration(animationDuration);
274 m_warningAnimation.start();
275 }
276
277 bool qResizeEvent(QResizeEvent*)
278 {
279 // When resizing the spinbox, perform style specific positioning
280 // of the lineedit
281
282 // Get the default rect for the lineedit widget
283 QStyleOptionSpinBox spinBoxOptions;
284 m_q->initStyleOption(&spinBoxOptions);
285 QRect rect = m_q->style()->subControlRect(QStyle::CC_SpinBox, &spinBoxOptions, QStyle::SC_SpinBoxEditField);
286 // Offset the rect to make it take all the available space inside the
287 // spinbox, without overlapping the buttons
288 QString style = qApp->property(currentUnderlyingStyleNameProperty).toString().toLower();
289 if (style == "breeze") {
290 rect.adjust(-4, -4, 0, 4);
291 } else if (style == "fusion") {
292 rect.adjust(-2, -1, 2, 1);
293 }
294 // Set the rect
295 m_lineEdit->setGeometry(rect);
296
297 return true;
298 }
299
300 bool qStyleChangeEvent(QEvent*)
301 {
302 // Fire a resize event so that the line edit geometry is updated.
303 // For some reason (stylesheet set in the app) setting the geometry
304 // using qstyle to get a rect has no effect here, as if the style is
305 // not updated yet...
306 qApp->postEvent(m_q, new QResizeEvent(m_q->size(), m_q->size()));
307 return false;
308 }
309
310 bool qKeyPressEvent(QKeyEvent *e)
311 {
312 switch (e->key()) {
313 case Qt::Key_Enter:
314 case Qt::Key_Return:
315 if (!isLastValid()) {
316 // Immediately show the warning if the expression is not valid
317 showWarning();
318 return true;
319 } else {
320 // Set the value forcing the expression to be overwritten.
321 // This will make an expression like "2*4" automatically be changed
322 // to "8" when enter/return key is pressed
323 setValue(m_q->value(), true);
324 }
325 break;
326 // Prevent deleting the last character of the prefix and the first
327 // one of the suffix. This solves some issue that appears when the
328 // prefix ends with a space or the suffix starts with a space. For
329 // example, if the prefix is "size: " and the value 50, deleting
330 // the space will join the string "size:" with "50" to form
331 // "size:50", and since that string does not start with the prefix,
332 // it will be treated as the new entered value. Then, prepending
333 // the prefix will display the text "size: size:50".
334 case Qt::Key_Backspace:
335 if (m_lineEdit->selectedText().length() == 0 && m_lineEdit->cursorPosition() == m_q->prefix().length()) {
336 return true;
337 }
338 break;
339 case Qt::Key_Delete:
340 if (m_lineEdit->selectedText().length() == 0 && m_lineEdit->cursorPosition() == m_q->text().length() - m_q->suffix().length()) {
341 return true;
342 }
343 break;
344 default:
345 break;
346 }
347 return false;
348 }
349
350 bool qFocusOutEvent(QFocusEvent*)
351 {
352 if (!isLastValid()) {
353 // Immediately show the warning if the expression is not valid
354 showWarning();
355 } else {
356 // Set the value forcing the expression to be overwritten.
357 // This will make an expression like "2*4" automatically be changed
358 // to "8" when the spinbox looses focus
359 setValue(m_q->value(), true);
360 }
361 return false;
362 }
363
364 bool lineEditPaintEvent(QPaintEvent*)
365 {
366 QPainter painter(m_lineEdit);
367 painter.setRenderHint(QPainter::Antialiasing, true);
368 QPalette pal = m_lineEdit->palette();
369 // the overlay color, a red warning color when there is an error
370 QColor color(255, 48, 0, 0);
371 constexpr int maxOpacity = 160;
372 QColor textColor;
373 const qreal warningAnimationPos = m_warningAnimation.currentValue().toReal();
374 // compute colors
375 if (m_warningAnimation.state() == QVariantAnimation::Running) {
376 color.setAlpha(static_cast<int>(std::round(KisAlgebra2D::lerp(0.0, static_cast<double>(maxOpacity), warningAnimationPos))));
377 textColor = KisPaintingTweaks::blendColors(m_q->palette().text().color(), Qt::white, 1.0 - warningAnimationPos);
378 } else {
379 if (m_isWarningActive) {
380 color.setAlpha(maxOpacity);
381 textColor = Qt::white;
382 } else {
383 textColor = m_q->palette().text().color();
384 }
385 }
386 // Paint the overlay
387 const QRect rect = m_lineEdit->rect();
388 painter.setBrush(color);
389 painter.setPen(Qt::NoPen);
390 QString style = qApp->property(currentUnderlyingStyleNameProperty).toString().toLower();
391 if (style == "fusion") {
392 painter.drawRoundedRect(rect, 1, 1);
393 } else {
394 painter.drawRoundedRect(rect, 0, 0);
395 }
396 // Paint the warning icon
397 if (m_showWarningIcon) {
398 constexpr qreal warningIconMargin = 4.0;
399 const qreal warningIconSize = widthOfWarningIconArea - 2.0 * warningIconMargin;
400 if (m_warningAnimation.state() == QVariantAnimation::Running) {
401 qreal warningIconPos =
403 m_lineEdit->alignment() & Qt::AlignRight ? -warningIconMargin : rect.width() - warningIconSize + warningIconMargin,
404 m_lineEdit->alignment() & Qt::AlignRight ? warningIconMargin : rect.width() - warningIconSize - warningIconMargin,
405 warningAnimationPos
406 );
407 painter.setOpacity(warningAnimationPos);
408 painter.drawPixmap(
409 warningIconPos, (static_cast<qreal>(rect.height()) - warningIconSize) / 2.0,
410 m_warningIcon.pixmap(warningIconSize, warningIconSize)
411 );
412 } else if (m_isWarningActive) {
413 painter.drawPixmap(
414 m_lineEdit->alignment() & Qt::AlignRight ? warningIconMargin : rect.width() - warningIconSize - warningIconMargin,
415 (static_cast<qreal>(rect.height()) - warningIconSize) / 2.0,
416 m_warningIcon.pixmap(warningIconSize, warningIconSize)
417 );
418 }
419 }
420 // Set the text color
421 pal.setBrush(QPalette::Text, textColor);
422 // Make sure the background of the line edit is transparent so that
423 // the base class paint event only draws the text
424 pal.setBrush(QPalette::Base, Qt::transparent);
425 pal.setBrush(QPalette::Button, Qt::transparent);
426 m_lineEdit->setPalette(pal);
427 return false;
428 }
429
430 bool lineEditMouseDoubleClickEvent(QMouseEvent *e)
431 {
432 if (!m_q->isEnabled() || m_lineEdit->isReadOnly()) {
433 return false;
434 }
435 // If we double click anywhere with the left button then select all the value text
436 if (e->button() == Qt::LeftButton) {
437 m_q->selectAll();
438 return true;
439 }
440 return false;
441 }
442
443 bool eventFilter(QObject *o, QEvent *e) override
444 {
445 if (!o || !e) {
446 return false;
447 }
448 if (o == m_q) {
449 switch (e->type()) {
450 case QEvent::Resize: return qResizeEvent(static_cast<QResizeEvent*>(e));
451 case QEvent::StyleChange: return qStyleChangeEvent(e);
452 case QEvent::KeyPress: return qKeyPressEvent(static_cast<QKeyEvent*>(e));
453 case QEvent::FocusOut: return qFocusOutEvent(static_cast<QFocusEvent*>(e));
454 default: break;
455 }
456 } else if (o == m_lineEdit) {
457 switch (e->type()) {
458 case QEvent::Paint: return lineEditPaintEvent(static_cast<QPaintEvent*>(e));
459 case QEvent::MouseButtonDblClick: return lineEditMouseDoubleClickEvent(static_cast<QMouseEvent*>(e));
460 default: break;
461 }
462 }
463 return false;
464 }
465
466private:
467 // Amount of time that has to pass after a keypress to show
468 // the warning, in milliseconds
469 static constexpr int showWarningInterval{2000};
470 // The width of the warning icon
471 static constexpr double widthOfWarningIconArea{24.0};
472 // The animation duration
473 static constexpr double warningAnimationDuration{250.0};
474
476 QLineEdit *m_lineEdit;
477 mutable QString m_lastExpressionParsed;
478 mutable bool m_isLastValid{true};
479 mutable bool m_isWarningActive{false};
480 mutable QTimer m_timerShowWarning;
481 mutable bool m_showWarningIcon{false};
482 mutable QVariantAnimation m_warningAnimation;
483 static QIcon m_warningIcon;
484};
485
486template <typename SpinBoxTypeTP, typename BaseSpinBoxTypeTP>
488
489#endif // KISPARSESPINBOXPRIVATE_H
qreal length(const QPointF &vec)
Definition Ellipse.cc:82
float value(const T *src, size_t ch)
connect(this, SIGNAL(optionsChanged()), this, SLOT(saveOptions()))
void fixupCursorPosition(int oldPos, int newPos)
void setValue(ValueType value, bool overwriteExpression=false)
QValidator::State validate(QString &, int &) const
void showWarning(int delay) const
QVariantAnimation m_warningAnimation
double parseMathExpression(const QString &text, bool *ok) const
BaseSpinBoxTypeTP BaseSpinBoxType
bool qKeyPressEvent(QKeyEvent *e)
bool lineEditPaintEvent(QPaintEvent *)
bool eventFilter(QObject *o, QEvent *e) override
QString textFromValue(ValueType value) const
decltype(std::declval< SpinBoxType >().value()) ValueType
int parseMathExpression(const QString &text, bool *ok) const
ValueType valueFromText(const QString &text) const
bool qResizeEvent(QResizeEvent *)
KisParseSpinBoxPrivate(SpinBoxType *q)
bool lineEditMouseDoubleClickEvent(QMouseEvent *e)
bool qFocusOutEvent(QFocusEvent *)
constexpr const char * currentUnderlyingStyleNameProperty
Definition kis_global.h:116
Point lerp(const Point &pt1, const Point &pt2, qreal t)
int parseIntegerMathExpr(QString const &expr, bool *noProblem)
parse an expression to an int.
double parseSimpleMathExpr(const QString &expr, bool *noProblem)
parse an expression to a double.
QColor blendColors(const QColor &c1, const QColor &c2, qreal r1)