File indexing completed on 2024-05-12 16:02:28

0001 /* This file is part of the KDE project
0002  *
0003  * SPDX-FileCopyrightText: 2016 Laurent Valentin Jospin <laurent.valentin@famillejospin.ch>
0004  * SPDX-FileCopyrightText: 2021 Deif Lou <ginoba@gmail.com>
0005  *
0006  * SPDX-License-Identifier: GPL-2.0-or-later
0007  */
0008 
0009 #ifndef KISPARSESPINBOXPRIVATE_H
0010 #define KISPARSESPINBOXPRIVATE_H
0011 
0012 #include <QTimer>
0013 #include <QVariantAnimation>
0014 #include <QValidator>
0015 #include <QLineEdit>
0016 #include <QIcon>
0017 #include <QFile>
0018 #include <QPainter>
0019 #include <QApplication>
0020 #include <QStyleOptionSpinBox>
0021 #include <QEvent>
0022 #include <QKeyEvent>
0023 #include <QMouseEvent>
0024 #include <QResizeEvent>
0025 
0026 #include <cmath>
0027 #include <utility>
0028 #include <type_traits>
0029 
0030 #include <kis_painting_tweaks.h>
0031 #include <kis_num_parser.h>
0032 #include <kis_algebra_2d.h>
0033 
0034 template <typename SpinBoxTypeTP, typename BaseSpinBoxTypeTP>
0035 class Q_DECL_HIDDEN KisParseSpinBoxPrivate : public QObject
0036 {
0037 public:
0038     using SpinBoxType = SpinBoxTypeTP;
0039     using BaseSpinBoxType = BaseSpinBoxTypeTP;
0040     using ValueType = decltype(std::declval<SpinBoxType>().value());
0041     
0042     KisParseSpinBoxPrivate(SpinBoxType *q)
0043         : m_q(q)
0044         , m_lineEdit(m_q->lineEdit())
0045     {
0046         m_q->installEventFilter(this);
0047 
0048         m_lineEdit->setAutoFillBackground(false);
0049         m_lineEdit->installEventFilter(this);
0050         connect(m_lineEdit, &QLineEdit::selectionChanged, this, &KisParseSpinBoxPrivate::fixupSelection);
0051         connect(m_lineEdit, &QLineEdit::cursorPositionChanged, this, &KisParseSpinBoxPrivate::fixupCursorPosition);
0052 
0053         m_timerShowWarning.setSingleShot(true);
0054         connect(&m_timerShowWarning, &QTimer::timeout, this, QOverload<>::of(&KisParseSpinBoxPrivate::showWarning));
0055         if (m_warningIcon.isNull() && QFile(":/./16_light_warning.svg").exists()) {
0056             m_warningIcon = QIcon(":/./16_light_warning.svg");
0057         }
0058         m_warningAnimation.setStartValue(0.0);
0059         m_warningAnimation.setEndValue(1.0);
0060         m_warningAnimation.setEasingCurve(QEasingCurve(QEasingCurve::InOutCubic));
0061         connect(&m_warningAnimation, &QVariantAnimation::valueChanged, m_lineEdit, QOverload<>::of(&QLineEdit::update));
0062     }
0063     
0064     void stepBy(int steps)
0065     {
0066         if (steps == 0) {
0067             return;
0068         }
0069         // Use the reimplementation os setValue in this function so that we can
0070         // clear the expression
0071         setValue(m_q->value() + static_cast<ValueType>(steps) * m_q->singleStep(), true);
0072         m_q->selectAll();
0073     }
0074 
0075     void setValue(ValueType value, bool overwriteExpression = false)
0076     {
0077         // The expression should be always cleared if the user is not
0078         // actively editing the text
0079         if (!m_q->hasFocus() || m_lineEdit->isReadOnly()) {
0080             overwriteExpression = true;
0081         }
0082         // Clear the expression so that the line edit shows just the
0083         // current value with prefix and suffix
0084         if (overwriteExpression) {
0085             m_lastExpressionParsed = QString();
0086         }
0087         // Prevent setting the new value if it is equal to the current one.
0088         // That will maintain the current expression and warning status.
0089         // If the value is different or the expression should overwritten then
0090         // the value is set and the warning status is cleared
0091         if (value != m_q->value() || overwriteExpression) {
0092             m_q->BaseSpinBoxType::setValue(value);
0093             if (!m_isLastValid) {
0094                 m_isLastValid = true;
0095                 hideWarning();
0096                 emit m_q->noMoreParsingError();
0097             }
0098         }
0099     }
0100 
0101     bool isLastValid() const
0102     {
0103         return m_isLastValid;
0104     }
0105 
0106     QString veryCleanText() const
0107     {
0108         return m_q->cleanText();
0109     }
0110 
0111     QValidator::State validate(QString&, int&) const
0112     {
0113         // We want the user to be able to write any kind of expression.
0114         // If it produces a valid value or not is decided in "valueFromText"
0115         return QValidator::Acceptable;
0116     }
0117 
0118     // Helper function to evaluate a math expression string into an int
0119     template <typename U = SpinBoxTypeTP, typename = typename std::enable_if<std::is_same<ValueType, int>::value, U>::type>
0120     int parseMathExpression(const QString &text, bool *ok) const
0121     {
0122         return KisNumericParser::parseIntegerMathExpr(text, ok);
0123     }
0124 
0125     // Helper function to evaluate a math expression string into a double
0126     template <typename U = SpinBoxTypeTP, typename = typename std::enable_if<std::is_same<ValueType, double>::value, U>::type>
0127     double parseMathExpression(const QString &text, bool *ok) const
0128     {
0129         double value = KisNumericParser::parseSimpleMathExpr(text, ok);
0130         if(qIsNaN(value) || qIsInf(value)){
0131             *ok = false;
0132         }
0133         return value;
0134     }
0135 
0136     ValueType valueFromText(const QString &text) const
0137     {
0138         // Always hide the warning when the text changes
0139         hideWarning();
0140         // Get the expression, removing the prefix and suffix
0141         m_lastExpressionParsed = text;
0142         if (m_lastExpressionParsed.endsWith(m_q->suffix())) {
0143             m_lastExpressionParsed.remove(m_lastExpressionParsed.size() - m_q->suffix().size(), m_q->suffix().size());
0144         }
0145         if(m_lastExpressionParsed.startsWith(m_q->prefix())){
0146             m_lastExpressionParsed.remove(0, m_q->prefix().size());
0147         }
0148         // Parse
0149         bool ok;
0150         ValueType value = parseMathExpression(m_lastExpressionParsed, &ok);
0151         // Validate
0152         if (!ok) {
0153             m_isLastValid = false;
0154             value = m_q->value();
0155             showWarning(showWarningInterval);
0156             emit m_q->errorWhileParsing(text);
0157         } else {
0158             if (!m_isLastValid) {
0159                 m_isLastValid = true;
0160                 emit m_q->noMoreParsingError();
0161             }
0162         }
0163         return value;
0164     }
0165 
0166     QString textFromValue(ValueType value) const
0167     {
0168         // If the last expression parsed is not null then the user actively
0169         // changed the text, so that expression is returned regardless of the
0170         // actual value
0171         if (!m_lastExpressionParsed.isNull()) {
0172             return m_lastExpressionParsed;
0173         }
0174         // Otherwise we transform the passed value to a string using the 
0175         // method from the base class and return that
0176         return m_q->BaseSpinBoxType::textFromValue(value);
0177     }
0178 
0179     // Fix the selection so that the start and the end are in the value text
0180     // and not in the prefix or suffix. This makes those unselectable
0181     void fixupSelection()
0182     {
0183         // If there's no selection just do nothing
0184         if (m_lineEdit->selectedText().isEmpty()) {
0185             return;
0186         }
0187         const int suffixStart = m_q->text().length() - m_q->suffix().length();
0188         const int newStart = qBound(m_q->prefix().length(), m_lineEdit->selectionStart(), suffixStart);
0189         const int newEnd = qBound(m_q->prefix().length(), m_lineEdit->selectionStart() + m_lineEdit->selectedText().length(), suffixStart);
0190         if (m_lineEdit->cursorPosition() == m_lineEdit->selectionStart()) {
0191             m_lineEdit->setSelection(newEnd, -(newEnd - newStart));
0192         } else {
0193             m_lineEdit->setSelection(newStart, newEnd - newStart);
0194         }
0195     }
0196 
0197     // Fix the cursor position so that it is in the value text
0198     // and not in the prefix or suffix.
0199     void fixupCursorPosition(int oldPos, int newPos)
0200     {
0201         Q_UNUSED(oldPos);
0202         if (newPos < m_q->prefix().length()) {
0203             m_lineEdit->setCursorPosition(m_q->prefix().length());
0204         } else {
0205             const int suffixStart = m_q->text().length() - m_q->suffix().length();
0206             if (newPos > suffixStart) {
0207                 m_lineEdit->setCursorPosition(suffixStart);
0208             }
0209         }
0210     }
0211 
0212     // Immediately show the warning overlay and icon
0213     void showWarning() const
0214     {
0215         if (m_isWarningActive && m_warningAnimation.state() == QVariantAnimation::Running) {
0216             return;
0217         }
0218         m_timerShowWarning.stop();
0219         m_warningAnimation.stop();
0220         m_isWarningActive = true;
0221         if (!m_warningIcon.isNull()) {
0222             QFontMetricsF fm(m_lineEdit->font());
0223 #if QT_VERSION >= QT_VERSION_CHECK(5,11,0)
0224             const qreal textWidth = fm.horizontalAdvance(m_lineEdit->text());
0225 #else
0226             const qreal textWidth = fm.width(m_lineEdit->text());
0227 #endif
0228             const int minimumWidth =
0229                 static_cast<int>(
0230                     std::ceil(
0231                         textWidth + (m_q->alignment() == Qt::AlignCenter ? 2.0 : 1.0) * widthOfWarningIconArea + 4
0232                     )
0233                 );
0234             if (m_lineEdit->width() >= minimumWidth) {
0235                 m_showWarningIcon = true;
0236             } else {
0237                 m_showWarningIcon = false;
0238             }
0239         }
0240         // scale the animation duration in case the animation is in the middle
0241         const int animationDuration =
0242             static_cast<int>(std::round((1.0 - m_warningAnimation.currentValue().toReal()) * warningAnimationDuration));
0243         m_warningAnimation.setStartValue(m_warningAnimation.currentValue());
0244         m_warningAnimation.setEndValue(1.0);
0245         m_warningAnimation.setDuration(animationDuration);
0246         m_warningAnimation.start();
0247     }
0248 
0249     // Show the warning after a specific amount of time
0250     void showWarning(int delay) const
0251     {
0252         if (delay > 0) {
0253             if (!m_isWarningActive || m_warningAnimation.state() != QVariantAnimation::Running) {
0254                 m_timerShowWarning.start(delay);
0255             }
0256             return;
0257         }
0258         // If "delay" is not greater that 0 then the warning will be
0259         // immediately shown
0260         showWarning();
0261     }
0262 
0263     void hideWarning() const
0264     {
0265         m_timerShowWarning.stop();
0266         m_warningAnimation.stop();
0267         m_isWarningActive = false;
0268         // scale the animation duration in case the animation is in the middle
0269         const int animationDuration =
0270             static_cast<int>(std::round(m_warningAnimation.currentValue().toReal() * warningAnimationDuration));
0271         m_warningAnimation.setStartValue(m_warningAnimation.currentValue());
0272         m_warningAnimation.setEndValue(0.0);
0273         m_warningAnimation.setDuration(animationDuration);
0274         m_warningAnimation.start();
0275     }
0276 
0277     bool qResizeEvent(QResizeEvent*)
0278     {
0279         // When resizing the spinbox, perform style specific positioning
0280         // of the lineedit
0281 
0282         // Get the default rect for the lineedit widget
0283         QStyleOptionSpinBox spinBoxOptions;
0284         m_q->initStyleOption(&spinBoxOptions);
0285         QRect rect = m_q->style()->subControlRect(QStyle::CC_SpinBox, &spinBoxOptions, QStyle::SC_SpinBoxEditField);
0286         // Offset the rect to make it take all the available space inside the
0287         // spinbox, without overlapping the buttons
0288         QString style = qApp->property(currentUnderlyingStyleNameProperty).toString().toLower();
0289         if (style == "breeze") {
0290             rect.adjust(-4, -4, 0, 4);
0291         } else if (style == "fusion") {
0292             rect.adjust(-2, -1, 2, 1);
0293         }
0294         // Set the rect
0295         m_lineEdit->setGeometry(rect);
0296 
0297         return true;
0298     }
0299 
0300     bool qStyleChangeEvent(QEvent*)
0301     {
0302         // Fire a resize event so that the line edit geometry is updated.
0303         // For some reason (stylesheet set in the app) setting the geometry
0304         // using qstyle to get a rect has no effect here, as if the style is
0305         // not updated yet... 
0306         qApp->postEvent(m_q, new QResizeEvent(m_q->size(), m_q->size()));
0307         return false;
0308     }
0309 
0310     bool qKeyPressEvent(QKeyEvent *e)
0311     {
0312         switch (e->key()) {
0313             case Qt::Key_Enter:
0314             case Qt::Key_Return:
0315                 if (!isLastValid()) {
0316                     // Immediately show the warning if the expression is not valid
0317                     showWarning();
0318                     return true;
0319                 } else {
0320                     // Set the value forcing the expression to be overwritten.
0321                     // This will make an expression like "2*4" automatically be changed
0322                     // to "8" when enter/return key is pressed
0323                     setValue(m_q->value(), true);
0324                 }
0325                 break;
0326             // Prevent deleting the last character of the prefix and the first
0327             // one of the suffix. This solves some issue that apprears when the
0328             // prefix ends with a space or the suffix starts with a space. For
0329             // example, if the prefix is "size: " and the value 50, deleting
0330             // the space will join the string "size:" with "50" to form
0331             // "size:50", and since that string does not start with the prefix,
0332             // it will be treated as the new entered value. Then, prepending
0333             // the prefix will display the text "size: size:50".
0334             case Qt::Key_Backspace:
0335                 if (m_lineEdit->selectedText().length() == 0 && m_lineEdit->cursorPosition() == m_q->prefix().length()) {
0336                     return true;
0337                 }
0338                 break;
0339             case Qt::Key_Delete:
0340                 if (m_lineEdit->selectedText().length() == 0 && m_lineEdit->cursorPosition() == m_q->text().length() - m_q->suffix().length()) {
0341                     return true;
0342                 }
0343                 break;
0344             default:
0345                 break;
0346         }
0347         return false;
0348     }
0349 
0350     bool qFocusOutEvent(QFocusEvent*)
0351     {
0352         if (!isLastValid()) {
0353             // Immediately show the warning if the expression is not valid
0354             showWarning();
0355         } else {
0356             // Set the value forcing the expression to be overwritten.
0357             // This will make an expression like "2*4" automatically be changed
0358             // to "8" when the spinbox looses focus 
0359             setValue(m_q->value(), true);
0360         }
0361         return false;
0362     }
0363 
0364     bool lineEditPaintEvent(QPaintEvent*)
0365     {
0366         QPainter painter(m_lineEdit);
0367         painter.setRenderHint(QPainter::Antialiasing, true);
0368         QPalette pal = m_lineEdit->palette();
0369         // the overlay color, a red warning color when there is an error
0370         QColor color(255, 48, 0, 0);
0371         constexpr int maxOpacity = 160;
0372         QColor textColor;
0373         const qreal warningAnimationPos = m_warningAnimation.currentValue().toReal();
0374         // compute colors
0375         if (m_warningAnimation.state() == QVariantAnimation::Running) {
0376             color.setAlpha(static_cast<int>(std::round(KisAlgebra2D::lerp(0.0, static_cast<double>(maxOpacity), warningAnimationPos))));
0377             textColor = KisPaintingTweaks::blendColors(m_q->palette().text().color(), Qt::white, 1.0 - warningAnimationPos);
0378         } else {
0379             if (m_isWarningActive) {
0380                 color.setAlpha(maxOpacity);
0381                 textColor = Qt::white;
0382             } else {
0383                 textColor = m_q->palette().text().color();
0384             }
0385         }
0386         // Paint the overlay
0387         const QRect rect = m_lineEdit->rect();
0388         painter.setBrush(color);
0389         painter.setPen(Qt::NoPen);
0390         QString style = qApp->property(currentUnderlyingStyleNameProperty).toString().toLower();
0391         if (style == "fusion") {
0392             painter.drawRoundedRect(rect, 1, 1);
0393         } else {
0394             painter.drawRoundedRect(rect, 0, 0);
0395         }
0396         // Paint the warning icon
0397         if (m_showWarningIcon) {
0398             constexpr qreal warningIconMargin = 4.0;
0399             const qreal warningIconSize = widthOfWarningIconArea - 2.0 * warningIconMargin;
0400             if (m_warningAnimation.state() == QVariantAnimation::Running) {
0401                 qreal warningIconPos =
0402                     KisAlgebra2D::lerp(
0403                         m_lineEdit->alignment() & Qt::AlignRight ? -warningIconMargin : rect.width() - warningIconSize + warningIconMargin,
0404                         m_lineEdit->alignment() & Qt::AlignRight ? warningIconMargin : rect.width() - warningIconSize - warningIconMargin,
0405                         warningAnimationPos
0406                     );
0407                 painter.setOpacity(warningAnimationPos);
0408                 painter.drawPixmap(
0409                     warningIconPos, (static_cast<qreal>(rect.height()) - warningIconSize) / 2.0,
0410                     m_warningIcon.pixmap(warningIconSize, warningIconSize)
0411                 );
0412             } else if (m_isWarningActive) {
0413                 painter.drawPixmap(
0414                     m_lineEdit->alignment() & Qt::AlignRight ? warningIconMargin : rect.width() - warningIconSize - warningIconMargin,
0415                     (static_cast<qreal>(rect.height()) - warningIconSize) / 2.0,
0416                     m_warningIcon.pixmap(warningIconSize, warningIconSize)
0417                 );
0418             }
0419         }
0420         // Set the text color
0421         pal.setBrush(QPalette::Text, textColor);
0422         // Make sure the background of the line edit is transparent so that
0423         // the base class paint event only draws the text
0424         pal.setBrush(QPalette::Base, Qt::transparent);
0425         pal.setBrush(QPalette::Button, Qt::transparent);
0426         m_lineEdit->setPalette(pal);
0427         return false;
0428     }
0429 
0430     bool lineEditMouseDoubleClickEvent(QMouseEvent *e)
0431     {
0432         if (!m_q->isEnabled() || m_lineEdit->isReadOnly()) {
0433             return false;
0434         }
0435         // If we double click anywhere with the left button then select all the value text
0436         if (e->button() == Qt::LeftButton) {
0437             m_q->selectAll();
0438             return true;
0439         }
0440         return false;
0441     }
0442 
0443     bool eventFilter(QObject *o, QEvent *e) override
0444     {
0445         if (!o || !e) {
0446             return false;
0447         }
0448         if (o == m_q) {
0449             switch (e->type()) {
0450                 case QEvent::Resize: return qResizeEvent(static_cast<QResizeEvent*>(e));
0451                 case QEvent::StyleChange: return qStyleChangeEvent(e);
0452                 case QEvent::KeyPress: return qKeyPressEvent(static_cast<QKeyEvent*>(e));
0453                 case QEvent::FocusOut: return qFocusOutEvent(static_cast<QFocusEvent*>(e));
0454                 default: break;
0455             }
0456         } else if (o == m_lineEdit) {
0457             switch (e->type()) {
0458                 case QEvent::Paint: return lineEditPaintEvent(static_cast<QPaintEvent*>(e));
0459                 case QEvent::MouseButtonDblClick: return lineEditMouseDoubleClickEvent(static_cast<QMouseEvent*>(e));
0460                 default: break;
0461             }
0462         }
0463         return false;
0464     }
0465 
0466 private:
0467     // Amount of time that has to pass after a keypress to show
0468     // the warning, in milliseconds
0469     static constexpr int showWarningInterval{2000};
0470     // The width of the warning icon
0471     static constexpr double widthOfWarningIconArea{24.0};
0472     // The animation duration
0473     static constexpr double warningAnimationDuration{250.0};
0474 
0475     SpinBoxType *m_q;
0476     QLineEdit *m_lineEdit;
0477     mutable QString m_lastExpressionParsed;
0478     mutable bool m_isLastValid{true};
0479     mutable bool m_isWarningActive{false};
0480     mutable QTimer m_timerShowWarning;
0481     mutable bool m_showWarningIcon{false};
0482     mutable QVariantAnimation m_warningAnimation;
0483     static QIcon m_warningIcon;
0484 };
0485 
0486 template <typename SpinBoxTypeTP, typename BaseSpinBoxTypeTP>
0487 QIcon KisParseSpinBoxPrivate<SpinBoxTypeTP, BaseSpinBoxTypeTP>::m_warningIcon;
0488 
0489 #endif // KISPARSESPINBOXPRIVATE_H