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;