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 }