File indexing completed on 2024-05-12 03:48:29

0001 /*
0002     File                 : NumberSpinBox.cpp
0003     Project              : LabPlot
0004     Description          : widget for setting numbers with a spinbox
0005     --------------------------------------------------------------------
0006     SPDX-FileCopyrightText: 2022 Martin Marmsoler <martin.marmsoler@gmail.com>
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 #include "NumberSpinBox.h"
0010 
0011 #include "backend/lib/macrosWarningStyle.h"
0012 
0013 #include <KLocalizedString>
0014 
0015 #include <QApplication>
0016 #include <QDebug>
0017 #include <QKeyEvent>
0018 #include <QLineEdit>
0019 #include <QLocale>
0020 #include <QString>
0021 #include <QStringView>
0022 
0023 #include <cmath>
0024 #include <limits>
0025 
0026 NumberSpinBox::NumberSpinBox(QWidget* parent)
0027     : QDoubleSpinBox(parent) {
0028     init(0, false);
0029 }
0030 
0031 NumberSpinBox::NumberSpinBox(double initValue, QWidget* parent)
0032     : QDoubleSpinBox(parent) {
0033     init(initValue, false);
0034 }
0035 
0036 NumberSpinBox::NumberSpinBox(double initValue, bool feedback, QWidget* parent)
0037     : QDoubleSpinBox(parent) {
0038     init(initValue, feedback);
0039 }
0040 
0041 void NumberSpinBox::init(double initValue, bool feedback) {
0042     setFocusPolicy(Qt::StrongFocus);
0043     setValue(initValue);
0044     m_feedback = feedback; // must be after setValue()!
0045     setInvalid(Errors::NoError);
0046     setDecimals(2);
0047     lineEdit()->setValidator(nullptr);
0048 }
0049 
0050 QString NumberSpinBox::errorToString(Errors e) {
0051     switch (e) {
0052     case Errors::Min:
0053         return i18n("Minimum allowed value: %1").arg(QString::number(minimum()));
0054     case Errors::Max:
0055         return i18n("Maximum allowed value: %1").arg(QString::number(maximum()));
0056     case Errors::Invalid:
0057         return i18n("The value does not represent a valid number");
0058     case Errors::NoNumber:
0059         return i18n("No number entered");
0060     case Errors::NoError:
0061         return {};
0062     }
0063     return i18n("Unhandled error");
0064 }
0065 
0066 void NumberSpinBox::keyPressEvent(QKeyEvent* event) {
0067     switch (event->key()) {
0068     case Qt::Key_Down:
0069         decreaseValue();
0070         return;
0071     case Qt::Key_Up:
0072         increaseValue();
0073         return;
0074     default: {
0075         if (lineEdit()->selectionLength() > 0) {
0076             int selectionStart = qMax(lineEdit()->selectionStart(), prefix().length());
0077             selectionStart = qMin(selectionStart, lineEdit()->text().length() - suffix().length());
0078 
0079             int selectionEnd = qMax(lineEdit()->selectionEnd(), prefix().length());
0080             selectionEnd = qMin(selectionEnd, lineEdit()->text().length() - suffix().length());
0081 
0082             lineEdit()->setSelection(selectionStart, selectionEnd - selectionStart);
0083         } else {
0084             int cursorPos = qMax(lineEdit()->cursorPosition(), prefix().length());
0085             cursorPos = qMin(cursorPos, lineEdit()->text().length() - suffix().length());
0086             lineEdit()->setCursorPosition(cursorPos);
0087         }
0088         QDoubleSpinBox::keyPressEvent(event);
0089         break;
0090     }
0091     }
0092     QString text = lineEdit()->text();
0093     double v;
0094     QString valueStr;
0095     Errors e = validate(text, v, valueStr);
0096     setInvalid(e);
0097     if (e == Errors::NoError && v != m_value && m_valueStr != valueStr) {
0098         m_valueStr = valueStr;
0099         m_value = v;
0100         valueChanged();
0101     }
0102 }
0103 
0104 void NumberSpinBox::wheelEvent(QWheelEvent* event) {
0105     if (m_strongFocus && !hasFocus())
0106         event->ignore();
0107     else
0108         QDoubleSpinBox::wheelEvent(event);
0109 }
0110 
0111 void NumberSpinBox::stepBy(int steps) {
0112     // used when scrolling
0113     Errors e = step(steps);
0114     if (e == Errors::Min || e == Errors::Max)
0115         setInvalid(Errors::NoError);
0116     else if (e == Errors::NoError) {
0117         setInvalid(e);
0118         valueChanged();
0119     } else
0120         setInvalid(e);
0121 }
0122 
0123 void NumberSpinBox::increaseValue() {
0124     stepBy(1);
0125 }
0126 
0127 void NumberSpinBox::decreaseValue() {
0128     stepBy(-1);
0129 }
0130 
0131 /*!
0132  * \brief NumberSpinBox::properties
0133  * Determine the properties of a numeric value represented in a string
0134  * \param v_str string representation of a numeric value
0135  * \param p properties of the value
0136  * \return
0137  */
0138 bool NumberSpinBox::properties(const QString& v_str, NumberProperties& p) const {
0139     const auto decimalpoint = locale().decimalPoint();
0140     p.fractionPos = v_str.indexOf(decimalpoint);
0141     p.exponentPos = v_str.indexOf(QLatin1Char('e'), p.fractionPos > 0 ? p.fractionPos : 0, Qt::CaseInsensitive);
0142     p.groupSeparators = v_str.indexOf(locale().groupSeparator()) != -1 ? true : false;
0143     const auto number_length = v_str.length();
0144 
0145     bool ok;
0146 
0147     if (v_str.at(0) == QLatin1Char('+') || v_str.at(0) == QLatin1Char('-'))
0148         p.integerSign = v_str.at(0);
0149 
0150     p.fraction = false;
0151     // integer properties
0152     if (p.fractionPos >= 0) {
0153         p.fraction = true;
0154         const auto integer_str = v_str.mid(!p.integerSign.isNull(), p.fractionPos - !p.integerSign.isNull());
0155         p.integer = locale().toInt(integer_str, &ok);
0156         if (!ok)
0157             return false;
0158         p.intergerDigits = integer_str.length() - p.groupSeparators;
0159 
0160         QString fraction_str;
0161         if (number_length - 1 > p.fractionPos) {
0162             int end = number_length;
0163             if (p.exponentPos > 0)
0164                 end = p.exponentPos;
0165             fraction_str = v_str.mid(p.fractionPos + 1, end - (p.fractionPos + 1));
0166         }
0167         p.fractionDigits = fraction_str.length();
0168     } else if (p.exponentPos > 0) {
0169         const auto integer_str = v_str.mid(!p.integerSign.isNull(), p.exponentPos - !p.integerSign.isNull());
0170         p.integer = locale().toInt(integer_str, &ok);
0171         if (!ok)
0172             return false;
0173         p.intergerDigits = integer_str.length() - p.groupSeparators;
0174     } else {
0175         const auto integer_str = v_str.mid(!p.integerSign.isNull(), number_length - !p.integerSign.isNull());
0176         p.integer = locale().toInt(integer_str, &ok);
0177         if (!ok)
0178             return false;
0179         p.intergerDigits = integer_str.length() - p.groupSeparators;
0180     }
0181 
0182     if (p.exponentPos > 0) {
0183         if (v_str.at(p.exponentPos + 1) == QLatin1Char('+') || v_str.at(p.exponentPos + 1) == QLatin1Char('-'))
0184             p.exponentSign = v_str.at(p.exponentPos + 1);
0185         const QString& e = v_str.mid(p.exponentPos + 1 + !p.exponentSign.isNull(), number_length - (p.exponentPos + 1 + !p.exponentSign.isNull()));
0186         p.exponentDigits = e.length();
0187         p.exponent = e.toInt(&ok);
0188         if (!ok)
0189             return false;
0190         p.exponentLetter = v_str.at(p.exponentPos);
0191     }
0192     return true;
0193 }
0194 
0195 /*!
0196  * \brief NumberSpinBox::createStringNumber
0197  * Create a string with integer, fraction and exponent part but with the constraint to match
0198  * the properties of \p p
0199  * \param integerFraction integer and fraction part of the numeric value
0200  * \param exponent exponent part of the numeric value
0201  * \param p value properties
0202  * \return
0203  */
0204 QString NumberSpinBox::createStringNumber(double integerFraction, int exponent, const NumberProperties& p) const {
0205     QString number;
0206     if (p.fraction) {
0207         number = locale().toString(integerFraction, 'f', p.fractionDigits);
0208         if (p.fractionDigits == 0)
0209             number.append(locale().decimalPoint());
0210     } else {
0211         if (p.groupSeparators)
0212             number = locale().toString(int(integerFraction));
0213         else
0214             number = QStringLiteral("%1").arg(int(integerFraction));
0215     }
0216 
0217     if (p.exponentLetter != QChar::Null) {
0218         const auto e = QStringLiteral("%L1").arg(exponent, p.exponentDigits + (p.exponentSign == QLatin1Char('-')), 10, QLatin1Char('0'));
0219         QString sign;
0220         if (exponent >= 0 && !p.exponentSign.isNull())
0221             sign = QLatin1Char('+');
0222         number += p.exponentLetter + sign + e;
0223     }
0224 
0225     if (p.integerSign == QLatin1Char('+'))
0226         number.prepend(QLatin1Char('+'));
0227 
0228     return number;
0229 }
0230 
0231 QString NumberSpinBox::strip(const QString& t) const {
0232     // Copied from QAbstractSpinBox.cpp
0233     QStringView text(t);
0234 
0235     int size = text.size();
0236     const QString p = prefix();
0237     const QString s = suffix();
0238     bool changed = false;
0239     int from = 0;
0240     if (p.size() && text.startsWith(p)) {
0241         from += p.size();
0242         size -= from;
0243         changed = true;
0244     }
0245     if (s.size() && text.endsWith(s)) {
0246         size -= s.size();
0247         changed = true;
0248     }
0249     if (changed)
0250         text = text.mid(from, size);
0251     text = text.trimmed();
0252     return text.toString();
0253 }
0254 
0255 QString NumberSpinBox::textFromValue(double value) const {
0256     Q_UNUSED(value);
0257     return m_valueStr;
0258 }
0259 
0260 /*!
0261  * \brief NumberSpinBox::valueFromText
0262  * Will be called when value() is called
0263  * \param text
0264  * \return
0265  */
0266 double NumberSpinBox::valueFromText(const QString& text) const {
0267     QString t = strip(text);
0268     double v = locale().toDouble(t);
0269     return v;
0270 }
0271 
0272 NumberSpinBox::Errors NumberSpinBox::step(int steps) {
0273     int cursorPos = lineEdit()->cursorPosition() - prefix().size();
0274     if (cursorPos < 0)
0275         cursorPos = 0;
0276     int end = lineEdit()->text().length() - suffix().size() - prefix().size();
0277     if (cursorPos > end)
0278         cursorPos = end;
0279 
0280     if (cursorPos == 0)
0281         return Errors::NoError;
0282 
0283     QString v_str = strip(lineEdit()->text());
0284 
0285     NumberProperties p;
0286     bool ok;
0287     const double origValue = locale().toDouble(v_str, &ok);
0288     if (!ok)
0289         return Errors::Invalid;
0290     if (!properties(v_str, p))
0291         return Errors::Invalid;
0292 
0293     const auto comma = p.fractionPos;
0294     const auto exponentialIndex = p.exponentPos;
0295 
0296     // cursor behind the integer sign
0297     // cursor behind the comma
0298     // cursor behind the exponent letter
0299     // cursor behind the exponent sign
0300     if ((cursorPos == 1 && !p.integerSign.isNull()) || (comma >= 0 && cursorPos - 1 == comma)
0301         || (exponentialIndex > 0 && (cursorPos - 1 == exponentialIndex || (cursorPos - 1 == exponentialIndex + 1 && !p.exponentSign.isNull()))))
0302         return Errors::NoError;
0303 
0304     bool before_comma = comma >= 0 && cursorPos - 1 < comma;
0305     bool before_exponent = exponentialIndex >= 0 && cursorPos - 1 < exponentialIndex;
0306 
0307     const auto& l = v_str.split(QLatin1Char('e'), Qt::KeepEmptyParts, Qt::CaseInsensitive);
0308 
0309     const auto& integerString = l.at(0);
0310     double integerFraction = locale().toDouble(integerString);
0311     int exponent = 0;
0312     if (l.length() > 1)
0313         exponent = l.at(1).toInt();
0314 
0315     double increase;
0316     if (before_comma || (comma == -1 && before_exponent) || (comma == -1 && exponentialIndex == -1)) {
0317         // integer
0318         int initial;
0319         if (comma >= 0)
0320             initial = comma;
0321         else if (exponentialIndex >= 0)
0322             initial = exponentialIndex;
0323         else
0324             initial = end;
0325         if (!p.groupSeparators)
0326             increase = steps * std::pow(10, initial - cursorPos);
0327         else {
0328             const auto groupSeparator = locale().groupSeparator();
0329             int separatorsCount = 0;
0330             int separator_pos = integerString.indexOf(groupSeparator);
0331             while (separator_pos != -1) {
0332                 if (separator_pos >= cursorPos)
0333                     separatorsCount++;
0334                 if (separator_pos + 1 < integerString.length())
0335                     separator_pos = integerString.indexOf(groupSeparator, separator_pos + 1);
0336                 else
0337                     break;
0338             }
0339             increase = steps * std::pow(10, initial - cursorPos - separatorsCount);
0340         }
0341 
0342         // from 0.1 with step -1 the desired result shall be -1.1 not -0.9
0343         if ((integerFraction > 0 && integerFraction + increase > 0) || (integerFraction < 0 && integerFraction + increase < 0))
0344             integerFraction += increase;
0345         else {
0346             int integer = static_cast<int>(integerFraction);
0347             integerFraction = integer + increase + (integer - integerFraction);
0348         }
0349     } else if (comma >= 0 && (exponentialIndex == -1 || before_exponent)) {
0350         // fraction
0351         increase = steps * std::pow(10, -(cursorPos - 1 - comma));
0352         integerFraction += increase;
0353     } else {
0354         // exponent
0355         increase = end - cursorPos;
0356         const auto calc = steps * std::pow(10, increase);
0357         exponent += calc;
0358 
0359         // double max value 1.7976931348623157E+308
0360         if (std::abs(exponent) > 307) {
0361             if (abs(integerFraction) > 1.7976931348623157)
0362                 exponent = exponent > 0 ? 307 : -307;
0363             else
0364                 exponent = exponent > 0 ? 308 : -308;
0365         }
0366     }
0367     if (integerFraction > std::numeric_limits<int>::max())
0368         integerFraction = std::numeric_limits<int>::max();
0369     else if (integerFraction < std::numeric_limits<int>::min())
0370         integerFraction = std::numeric_limits<int>::min();
0371 
0372     double v = integerFraction * std::pow(10, exponent);
0373 
0374     if (v > maximum())
0375         return Errors::Max;
0376 
0377     if (v < minimum())
0378         return Errors::Min;
0379 
0380     QString number = createStringNumber(integerFraction, exponent, p);
0381     setText(number);
0382 
0383     // Set cursor position
0384     auto newPos = number.length() - (end - cursorPos);
0385     if ((newPos == 0 && number.length() > 0))
0386         newPos = 1;
0387     if (newPos == 1 && !p.integerSign.isNull() && number.length() > 1 && origValue < 0 && v < 0)
0388         newPos = 2;
0389 
0390     lineEdit()->setCursorPosition(newPos + prefix().size());
0391 
0392     m_value = v;
0393     return Errors::NoError;
0394 }
0395 
0396 NumberSpinBox::Errors NumberSpinBox::validate(QString& input, double& value, QString& valueStr) const {
0397     valueStr = strip(input);
0398     if (valueStr.isEmpty())
0399         return Errors::NoNumber;
0400     NumberProperties p;
0401     bool ok;
0402     value = locale().toDouble(valueStr, &ok);
0403     if (!ok)
0404         return Errors::Invalid;
0405     if (!properties(valueStr, p))
0406         return Errors::Invalid;
0407 
0408     if (value > maximum())
0409         return Errors::Max;
0410 
0411     if (value < minimum())
0412         return Errors::Min;
0413     return Errors::NoError;
0414 }
0415 
0416 /*!
0417  * \brief NumberSpinBox::validate
0418  * Function which validates the user input. Reimplemented from QDoubleSpinBox
0419  * \param input
0420  * \param pos
0421  * \return
0422  */
0423 QValidator::State NumberSpinBox::validate(QString& input, int& pos) const {
0424     Q_UNUSED(pos);
0425     double value;
0426     QString valueStr;
0427     const auto e = validate(input, value, valueStr);
0428     return e == Errors::NoError ? QValidator::State::Acceptable : QValidator::State::Intermediate;
0429 }
0430 
0431 void NumberSpinBox::setText(const QString& text) {
0432     m_valueStr = text;
0433     lineEdit()->setText(prefix() + text + suffix());
0434 }
0435 
0436 bool NumberSpinBox::setValue(double v) {
0437     if (m_feedback && m_waitFeedback) {
0438         m_waitFeedback = false;
0439         if (!qFuzzyCompare(v, value())) {
0440             setInvalid(i18n("Invalid value entered. Valid value: %1", v));
0441             return false;
0442         }
0443         return true;
0444     }
0445 
0446     setText(locale().toString(v, 'g'));
0447     m_value = v;
0448     valueChanged();
0449     return true;
0450 }
0451 
0452 double NumberSpinBox::minimum() const {
0453     return m_minimum;
0454 }
0455 
0456 void NumberSpinBox::setMinimum(double min) {
0457     m_minimum = min;
0458 }
0459 
0460 double NumberSpinBox::maximum() const {
0461     return m_maximum;
0462 }
0463 
0464 void NumberSpinBox::setMaximum(double max) {
0465     m_maximum = max;
0466 }
0467 
0468 void NumberSpinBox::setFeedback(bool enable) {
0469     m_feedback = enable;
0470 }
0471 
0472 bool NumberSpinBox::feedback() {
0473     return m_feedback;
0474 }
0475 
0476 void NumberSpinBox::setStrongFocus(bool enable) {
0477     m_strongFocus = enable;
0478     if (enable)
0479         setFocusPolicy(Qt::StrongFocus);
0480     else
0481         setFocusPolicy(Qt::WheelFocus);
0482 }
0483 
0484 double NumberSpinBox::value() {
0485     return m_value;
0486 }
0487 
0488 void NumberSpinBox::setClearButtonEnabled(bool enable) {
0489     lineEdit()->setClearButtonEnabled(enable);
0490 }
0491 
0492 QAbstractSpinBox::StepEnabled NumberSpinBox::stepEnabled() const {
0493     return QAbstractSpinBox::StepEnabledFlag::StepUpEnabled | QAbstractSpinBox::StepEnabledFlag::StepDownEnabled; // for testing
0494 }
0495 
0496 void NumberSpinBox::valueChanged() {
0497     if (m_feedback)
0498         m_waitFeedback = true;
0499     Q_EMIT valueChanged(value());
0500     m_waitFeedback = false;
0501 }
0502 
0503 void NumberSpinBox::setInvalid(const QString& str) {
0504     if (!str.isEmpty())
0505         SET_WARNING_PALETTE
0506     else
0507         setPalette(qApp->palette());
0508     setToolTip(str);
0509 }
0510 
0511 void NumberSpinBox::setInvalid(Errors e) {
0512     setInvalid(errorToString(e));
0513 }