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