diff --git a/src/framework/uicomponents/qml/Muse/UiComponents/tests/CMakeLists.txt b/src/framework/uicomponents/qml/Muse/UiComponents/tests/CMakeLists.txt index 7fc0ce714c0f7..3995dd6207363 100644 --- a/src/framework/uicomponents/qml/Muse/UiComponents/tests/CMakeLists.txt +++ b/src/framework/uicomponents/qml/Muse/UiComponents/tests/CMakeLists.txt @@ -22,6 +22,7 @@ set(MODULE_TEST muse_uicomponents_qml_tests) set(MODULE_TEST_SRC ${CMAKE_CURRENT_LIST_DIR}/doubleinputvalidator_tests.cpp + ${CMAKE_CURRENT_LIST_DIR}/intinputvalidator_tests.cpp ) set(MODULE_TEST_LINK diff --git a/src/framework/uicomponents/qml/Muse/UiComponents/tests/intinputvalidator_tests.cpp b/src/framework/uicomponents/qml/Muse/UiComponents/tests/intinputvalidator_tests.cpp new file mode 100644 index 0000000000000..83ca41acd013f --- /dev/null +++ b/src/framework/uicomponents/qml/Muse/UiComponents/tests/intinputvalidator_tests.cpp @@ -0,0 +1,168 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2026 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include +#include + +#include "../validators/intinputvalidator.h" + +// Teach GoogleTest how to print QString so failure diffs are readable +// instead of a UTF-16 byte dump. +inline void PrintTo(const QString& s, std::ostream* os) +{ + *os << '"' << s.toStdString() << '"'; +} + +using namespace muse; +using namespace muse::uicomponents; + +namespace muse::uicomponents { +struct Input +{ + QString str; + QValidator::State expectedState; + QString fixedStr = {}; +}; + +class IntInputValidatorTests : public ::testing::Test, public QObject +{ +public: + void SetUp() override + { + m_validator = new IntInputValidator(nullptr); + } + + void TearDown() override + { + delete m_validator; + } + +protected: + IntInputValidator* m_validator = nullptr; + + void runInputTests(const std::vector& inputs) + { + int pos = 0; + for (const Input& input : inputs) { + QString inputCopy = input.str; + auto state = m_validator->validate(inputCopy, pos); + EXPECT_EQ(state, input.expectedState) << "validate(\"" << input.str.toStdString() << "\")"; + + if (QValidator::Invalid == input.expectedState) { + continue; + } + + QString fixInput = input.str; + m_validator->fixup(fixInput); + + QString expectedStr = input.fixedStr.isEmpty() ? input.str : input.fixedStr; + EXPECT_EQ(expectedStr, fixInput) << "fixup(\"" << input.str.toStdString() << "\")"; + } + } +}; + +TEST_F(IntInputValidatorTests, ValidateCommaLocale) { + QLocale prev = QLocale(); + QLocale::setDefault(QLocale("en_US")); + + m_validator->setTop(48000); + m_validator->setBottom(-48000); + + runInputTests({ + { "0", QValidator::Acceptable }, + { "1", QValidator::Acceptable }, + { "100", QValidator::Acceptable }, + { "1,000", QValidator::Acceptable }, + { "1000", QValidator::Acceptable, "1,000" }, + { "1,0000", QValidator::Acceptable, "10,000" }, + { "48,000", QValidator::Acceptable }, + { "48,001", QValidator::Invalid }, + { "-100", QValidator::Acceptable }, + { "-1,000", QValidator::Acceptable }, + { "-1000", QValidator::Acceptable, "-1,000" }, + { "-1,0000", QValidator::Acceptable, "-10,000" }, + { "-48,000", QValidator::Acceptable }, + { "-48,001", QValidator::Invalid }, + { "2147483647", QValidator::Invalid }, + { "-2147483648", QValidator::Invalid }, + { "abc", QValidator::Invalid }, + { "", QValidator::Intermediate, "0" } + }); + + m_validator->setTop(10); + m_validator->setBottom(1); + + runInputTests({ + { "0", QValidator::Invalid }, + { "", QValidator::Intermediate, "1" }, + { "1", QValidator::Acceptable } + }); + + m_validator->setTop(10); + m_validator->setBottom(5); + + runInputTests({ + { "1", QValidator::Intermediate, "5" }, + { "2", QValidator::Intermediate, "5" } + }); + + m_validator->setTop(-50); + m_validator->setBottom(-100); + + runInputTests({ + { "-5", QValidator::Intermediate, "-50" }, + { "-2", QValidator::Intermediate, "-50" }, + { "-75", QValidator::Acceptable }, + { "0", QValidator::Invalid } + }); + + QLocale::setDefault(prev); +} + +TEST_F(IntInputValidatorTests, ValidateDotLocale) { + QLocale prev = QLocale(); + QLocale::setDefault(QLocale("ro_RO")); + + m_validator->setTop(48000); + m_validator->setBottom(-48000); + + runInputTests({ + { "0", QValidator::Acceptable }, + { "1", QValidator::Acceptable }, + { "100", QValidator::Acceptable }, + { "1.000", QValidator::Acceptable }, + { "1000", QValidator::Acceptable, "1.000" }, + { "1.0000", QValidator::Acceptable, "10.000" }, + { "48.000", QValidator::Acceptable }, + { "48.001", QValidator::Invalid }, + { "-100", QValidator::Acceptable }, + { "-1.000", QValidator::Acceptable }, + { "-1000", QValidator::Acceptable, "-1.000" }, + { "-1.0000", QValidator::Acceptable, "-10.000" }, + { "-48.000", QValidator::Acceptable }, + { "-48.001", QValidator::Invalid }, + { "abc", QValidator::Invalid }, + { "", QValidator::Intermediate, "0" } + }); + + QLocale::setDefault(prev); +} +} diff --git a/src/framework/uicomponents/qml/Muse/UiComponents/validators/intinputvalidator.cpp b/src/framework/uicomponents/qml/Muse/UiComponents/validators/intinputvalidator.cpp index 4c5e52179aa2f..05c039514cb86 100644 --- a/src/framework/uicomponents/qml/Muse/UiComponents/validators/intinputvalidator.cpp +++ b/src/framework/uicomponents/qml/Muse/UiComponents/validators/intinputvalidator.cpp @@ -21,6 +21,7 @@ */ #include "intinputvalidator.h" #include "global/realfn.h" +#include using namespace muse::uicomponents; @@ -31,44 +32,84 @@ IntInputValidator::IntInputValidator(QObject* parent) void IntInputValidator::fixup(QString& string) const { + QLocale locale; + if (string.isEmpty() || string.endsWith("-")) { string.append("0"); } - if (string.toInt() == 0) { + + QString stripped = string; + stripped.remove(locale.groupSeparator()); + + bool ok = false; + int val = stripped.toInt(&ok); + + if (!ok) { string = "0"; + return; } - if (string.toInt() > m_top) { - string = QString::number(m_top); - } else if (string.toInt() < m_bottom) { - string = QString::number(m_bottom); + if (val > m_top) { + val = m_top; + } else if (val < m_bottom) { + val = m_bottom; } + + string = locale.toString(val); } QValidator::State IntInputValidator::validate(QString& inputStr, int& cursorPos) const { + QLocale locale; + QString groupSep = locale.groupSeparator(); + + if (inputStr.contains(groupSep)) { + bool ok = false; + locale.toInt(inputStr, &ok); + if (!ok) { + // Handle group separator in the wrong place + int sepsBefore = inputStr.left(cursorPos).count(groupSep); + inputStr.remove(groupSep); + cursorPos -= sepsBefore; + } + } + + QString digits = inputStr; + digits.remove(groupSep); + QValidator::State state = Invalid; const int maxAbsoluteValue = std::max(std::abs(m_top), std::abs(m_bottom)); const int maxNumberOfDigits = maxAbsoluteValue > 0 ? std::floor(std::log10(maxAbsoluteValue)) + 1 : 1; - if (inputStr.contains(QRegularExpression(QString("^\\-?\\d{1,%1}$").arg(maxNumberOfDigits)))) { - if ((maxNumberOfDigits >= 2 && inputStr.contains(QRegularExpression(QString("^\\-?0{2,%1}").arg(maxNumberOfDigits)))) - || (inputStr.startsWith("-") && muse::RealIsNull(inputStr.toDouble()))) { + if (digits.contains(QRegularExpression(QString("^\\-?\\d{1,%1}$").arg(maxNumberOfDigits)))) { + if ((maxNumberOfDigits >= 2 && digits.contains(QRegularExpression(QString("^\\-?0{2,%1}").arg(maxNumberOfDigits)))) + || (digits.startsWith("-") && muse::RealIsNull(digits.toDouble()))) { state = Intermediate; } else { state = Acceptable; } - } else if (inputStr.contains(QRegularExpression("^\\-?$"))) { - state = Intermediate; - } else { + } else if (digits.isEmpty()) { + return Intermediate; + } else if ((digits == "-")) { + if (m_bottom < 0) { + return Intermediate; + } + cursorPos = 0; return Invalid; } - if (inputStr.toInt() > m_top || inputStr.toInt() < m_bottom) { - state = Intermediate; + int val = digits.toInt(); + if (val > m_top) { + // A negative value above the maximum may still be an in-progress prefix + return val < 0 ? Intermediate : Invalid; + } + + if (val < m_bottom) { + // Symmetrically, a positive not 0 value below the minimum may still be an in-progress + return val > 0 ? Intermediate : Invalid; } return state;