File indexing completed on 2024-05-12 04:44:26

0001 // SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com>
0002 // SPDX-License-Identifier: BSD-2-Clause OR MIT
0003 
0004 // First included header is the public header of the class we are testing;
0005 // this forces the header to be self-contained.
0006 #include "multispinbox.h"
0007 // Second, the private implementation.
0008 #include "multispinbox_p.h" // IWYU pragma: keep
0009 
0010 #include "constpropagatinguniquepointer.h"
0011 #include "multispinboxsection.h"
0012 #include <qabstractspinbox.h>
0013 #include <qaction.h>
0014 #include <qapplication.h>
0015 #include <qdebug.h>
0016 #include <qglobal.h>
0017 #include <qlabel.h>
0018 #include <qlineedit.h>
0019 #include <qlist.h>
0020 #include <qlocale.h>
0021 #include <qnamespace.h>
0022 #include <qobject.h>
0023 #include <qscopedpointer.h>
0024 #include <qsignalspy.h>
0025 #include <qsize.h>
0026 #include <qspinbox.h>
0027 #include <qstring.h>
0028 #include <qstringliteral.h>
0029 #include <qtest.h>
0030 #include <qtestcase.h>
0031 #include <qtestdata.h>
0032 #include <qtestkeyboard.h>
0033 #include <qvariant.h>
0034 #include <qwidget.h>
0035 
0036 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
0037 #include <qtmetamacros.h>
0038 #else
0039 #include <qobjectdefs.h>
0040 #endif
0041 
0042 // From Qt documentation:
0043 //     “Note: This function is not declared in any of Qt's header files. To
0044 //      use it in your application, declare the function prototype before
0045 //      calling it.”
0046 void qt_set_sequence_auto_mnemonic(bool b);
0047 
0048 static void snippet02()
0049 {
0050     //! [MultiSpinBox Basic example]
0051     PerceptualColor::MultiSpinBox *myHsvSpinBox = //
0052         new PerceptualColor::MultiSpinBox();
0053     PerceptualColor::MultiSpinBoxSection myConfiguration;
0054     QList<PerceptualColor::MultiSpinBoxSection> hsvConfigurations;
0055 
0056     myConfiguration.setDecimals(1);
0057 
0058     myConfiguration.setPrefix(QString());
0059     myConfiguration.setMinimum(0);
0060     myConfiguration.setWrapping(true);
0061     myConfiguration.setMaximum(360);
0062     myConfiguration.setSuffix(QStringLiteral(u"° "));
0063     hsvConfigurations.append(myConfiguration);
0064 
0065     myConfiguration.setPrefix(QStringLiteral(u" "));
0066     myConfiguration.setMinimum(0);
0067     myConfiguration.setMaximum(255);
0068     myConfiguration.setWrapping(false);
0069     myConfiguration.setSuffix(QStringLiteral(u" "));
0070     hsvConfigurations.append(myConfiguration);
0071 
0072     myConfiguration.setSuffix(QString());
0073     hsvConfigurations.append(myConfiguration);
0074 
0075     myHsvSpinBox->setSectionConfigurations(hsvConfigurations);
0076 
0077     myHsvSpinBox->setSectionValues(QList<double>{310, 200, 100});
0078     // Initial content is:  310,0°  200,0  100,0
0079     //! [MultiSpinBox Basic example]
0080     delete myHsvSpinBox;
0081 }
0082 
0083 class testSnippet02 : public PerceptualColor::MultiSpinBox
0084 {
0085     Q_OBJECT
0086 
0087     //! [MultiSpinBox Full-featured interface]
0088     Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged)
0089     Q_PROPERTY(int sectionCount READ sectionCount NOTIFY sectionCountChanged)
0090 
0091 public:
0092     void addSection(PerceptualColor::MultiSpinBoxSection newSection);
0093     void addSections(QList<PerceptualColor::MultiSpinBoxSection> newSections);
0094     void append(PerceptualColor::MultiSpinBoxSection newSection);
0095     void append(QList<PerceptualColor::MultiSpinBoxSection> newSections);
0096     QString cleanText(int index) const; // See also “cleanText”
0097     void clearSections();
0098     int currentIndex() const;
0099     PerceptualColor::MultiSpinBoxSection currentSection() const;
0100     PerceptualColor::MultiSpinBoxSection firstSection() const;
0101     void insertSection(int index, PerceptualColor::MultiSpinBoxSection newSection);
0102     void insertSection(int index, QList<PerceptualColor::MultiSpinBoxSection> newSections);
0103     PerceptualColor::MultiSpinBoxSection lastSection() const;
0104     void moveSection(int from, int to);
0105     void prependSection(PerceptualColor::MultiSpinBoxSection newSection);
0106     void prependSections(QList<PerceptualColor::MultiSpinBoxSection> newSections);
0107     void removeFirstSection();
0108     void removeLastSection();
0109     void removeSection(int index);
0110     void replaceSection(int index, PerceptualColor::MultiSpinBoxSection newSection);
0111     PerceptualColor::MultiSpinBoxSection sectionAt(int index) const;
0112     int sectionCount() const; // Somewhat redundant with MultiSpinBox::sections().count()
0113     QList<PerceptualColor::MultiSpinBoxSection> sectionConfigurations() const;
0114     QString sectionText(int index) const; // See also “cleanText”
0115     void setSelectedSection(int index); // A better name might be “selectSection”
0116     void setSectionConfigurations(const QList<PerceptualColor::MultiSpinBoxSection> &newSections);
0117     void swapSections(int i, int j);
0118 
0119     // What about these functions? They…
0120     // …are public in QDoubleSpinBox
0121     // …are protected in QSpinBox
0122     // …do not exist in QDateTimeEdit
0123     // …do not exist in QAbstractSpinBox:
0124     QString textFromValue(double value) const;
0125     double valueFromText(const QString &text) const;
0126 
0127 Q_SIGNALS:
0128     void currentIndexChanged(int newCurrentIndex);
0129     void sectionCountChanged(int newSectionCount);
0130 
0131     // The following signal in Qt 5.15 seems to not always be emitted
0132     // when actually text changes, but instead only if actually the value
0133     // changes. So if the text changes from  “0.1” to “0.10” this signal
0134     // is not emitted because the value itself did not change. This is
0135     // counter-intuitive because the behaviour does not correspond to the
0136     // name of the signal. It’s therefore better not no implement this
0137     // signal.
0138     void textChanged(const QString &newText);
0139 
0140     // The following signal is emitted always when the value changes.
0141     // If we separate the section configuration from the section value,
0142     // we could provide the new value of type QList<double> as an
0143     // argument of this signal and also declare a property corresponding
0144     // to this signal.
0145     void valueChanged();
0146 
0147 public Q_SLOTS:
0148     void setCurrentIndex(int newIndex);
0149     //! [MultiSpinBox Full-featured interface]
0150 };
0151 int testSnippet02::currentIndex() const
0152 {
0153     return 0;
0154 }
0155 void testSnippet02::setCurrentIndex(int newIndex)
0156 {
0157     Q_UNUSED(newIndex)
0158 }
0159 int testSnippet02::sectionCount() const
0160 {
0161     return 0;
0162 }
0163 
0164 namespace PerceptualColor
0165 {
0166 class TestMultiSpinBox : public QObject
0167 {
0168     Q_OBJECT
0169 
0170 public:
0171     explicit TestMultiSpinBox(QObject *parent = nullptr)
0172         : QObject(parent)
0173     {
0174     }
0175 
0176 private:
0177     QList<MultiSpinBoxSection> exampleConfigurations;
0178     static void voidMessageHandler(QtMsgType, const QMessageLogContext &, const QString &)
0179     {
0180         // dummy message handler that does not print messages
0181     }
0182 
0183 private Q_SLOTS:
0184     void initTestCase()
0185     {
0186         // Called before the first test function is executed
0187 
0188         // Make sure to have mnemonics (like Qt::ALT+Qt::Key_X for "E&xit")
0189         // enabled, also on platforms that disable it by default.
0190         qt_set_sequence_auto_mnemonic(true);
0191 
0192         // Provide example configuration
0193         MultiSpinBoxSection mySection;
0194         mySection.setDecimals(0);
0195         mySection.setMinimum(0);
0196         mySection.setMaximum(360);
0197         mySection.setPrefix(QString());
0198         mySection.setSuffix(QStringLiteral(u"°"));
0199         exampleConfigurations.append(mySection);
0200         mySection.setMaximum(100);
0201         mySection.setPrefix(QStringLiteral(u"  "));
0202         mySection.setSuffix(QStringLiteral(u"%"));
0203         exampleConfigurations.append(mySection);
0204         mySection.setMaximum(255);
0205         mySection.setPrefix(QStringLiteral(u"  "));
0206         mySection.setSuffix(QString());
0207         exampleConfigurations.append(mySection);
0208     }
0209 
0210     void cleanupTestCase()
0211     {
0212         // Called after the last test function was executed
0213     }
0214 
0215     void init()
0216     {
0217         // Called before each test function is executed
0218     }
0219 
0220     void cleanup()
0221     {
0222         // Called after every test function
0223     }
0224 
0225     void testDefaultValues()
0226     {
0227         // The default values should be the same as for QDoubleSpinBox
0228         MultiSpinBox myMulti;
0229         QDoubleSpinBox myDoubleSpinBox;
0230 
0231         // Test default section values
0232         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
0233 
0234         // Test default values of the configuration directly in the widget
0235         QCOMPARE(myMulti.sectionConfigurations().at(0).decimals(), //
0236                  myDoubleSpinBox.decimals());
0237         QCOMPARE(myMulti.sectionConfigurations().at(0).isWrapping(), //
0238                  myDoubleSpinBox.wrapping());
0239         QCOMPARE(myMulti.sectionConfigurations().at(0).maximum(), //
0240                  myDoubleSpinBox.maximum());
0241         QCOMPARE(myMulti.sectionConfigurations().at(0).minimum(), //
0242                  myDoubleSpinBox.minimum());
0243         QCOMPARE(myMulti.sectionConfigurations().at(0).prefix(), //
0244                  myDoubleSpinBox.prefix());
0245         QCOMPARE(myMulti.sectionConfigurations().at(0).singleStep(), //
0246                  myDoubleSpinBox.singleStep());
0247         QCOMPARE(myMulti.sectionConfigurations().at(0).suffix(), //
0248                  myDoubleSpinBox.suffix());
0249 
0250         // Whitebox tests
0251         QCOMPARE(myMulti.sectionValues(), QList<double>{0});
0252         QCOMPARE(myMulti.d_pointer->m_sectionValues, QList<double>{0});
0253         QCOMPARE(myMulti.d_pointer->m_currentIndex, 0);
0254     }
0255 
0256     void testConstructor()
0257     {
0258         // Test the the constructor does not crash
0259         PerceptualColor::MultiSpinBox myMulti;
0260         // Test basic constructor results
0261         QVERIFY2(myMulti.d_pointer->m_sectionConfigurations.length() > 0, //
0262                  "Make sure the default configuration has at least 1 section.");
0263     }
0264 
0265     void testInteraction()
0266     {
0267         QScopedPointer<PerceptualColor::MultiSpinBox> widget( //
0268             new PerceptualColor::MultiSpinBox());
0269         widget->setSectionConfigurations(exampleConfigurations);
0270         // Assert that the setup is okay.
0271         QCOMPARE(widget->lineEdit()->text(), QStringLiteral(u"0°  0%  0"));
0272         // Go to begin of the line edit
0273         for (int i = 0; i < 10; i++) {
0274             // NOTE Simply use 1 time Qt::Key_Home would be easier
0275             // than calling 10 times Qt::Key_Left, however Qt::Key_Home
0276             // does not work on MacOS.
0277             QTest::keyClick(widget.data(), Qt::Key_Left);
0278         }
0279         QCOMPARE(widget->lineEdit()->selectedText(), QString());
0280         QCOMPARE(widget->lineEdit()->text(), QStringLiteral(u"0°  0%  0"));
0281         QCOMPARE(widget->lineEdit()->cursorPosition(), 0);
0282         // Select the first “0”:
0283         QTest::keyClick(widget.data(), Qt::Key_Right, Qt::ShiftModifier, 0);
0284         // The content shouldn’t have changed.
0285         QCOMPARE(widget->lineEdit()->text(), QStringLiteral(u"0°  0%  0"));
0286         // The selection should contain “0”.
0287         QCOMPARE(widget->lineEdit()->selectedText(), QStringLiteral(u"0"));
0288         // Write “45”
0289         QTest::keyClicks(widget.data(), QStringLiteral(u"45"));
0290         QCOMPARE(widget->lineEdit()->text(), QStringLiteral(u"45°  0%  0"));
0291         // Select “45”
0292         QTest::keyClick(widget.data(), Qt::Key_Left, Qt::ShiftModifier, 0);
0293         QTest::keyClick(widget.data(), Qt::Key_Left, Qt::ShiftModifier, 0);
0294         // Copy to clipboard
0295         // TODO The following line that copies to clipboard
0296         // is surprisingly extremly slow.
0297         QTest::keyClick(widget.data(), Qt::Key_C, Qt::ControlModifier, 0);
0298         // Go to the left
0299         QTest::keyClick(widget.data(), Qt::Key_Left);
0300         QCOMPARE(widget->lineEdit()->text(), QStringLiteral(u"45°  0%  0"));
0301         // Go to second section
0302         QTest::keyClick(widget.data(), Qt::Key_Right);
0303         QCOMPARE(widget->lineEdit()->text(), QStringLiteral(u"45°  0%  0"));
0304         QTest::keyClick(widget.data(), Qt::Key_Right);
0305         QCOMPARE(widget->lineEdit()->text(), QStringLiteral(u"45°  0%  0"));
0306         QTest::keyClick(widget.data(), Qt::Key_Right);
0307         QCOMPARE(widget->lineEdit()->text(), QStringLiteral(u"45°  0%  0"));
0308         QTest::keyClick(widget.data(), Qt::Key_Right);
0309         QCOMPARE(widget->lineEdit()->text(), QStringLiteral(u"45°  0%  0"));
0310         QTest::keyClick(widget.data(), Qt::Key_Right);
0311         QCOMPARE(widget->lineEdit()->text(), QStringLiteral(u"45°  0%  0"));
0312         // Select second section:
0313         QTest::keyClick(widget.data(), Qt::Key_Right, Qt::ShiftModifier, 0);
0314         QCOMPARE(widget->lineEdit()->selectedText(), QStringLiteral(u"0"));
0315         // Take “45” from clipboard
0316         QTest::keyClick(widget.data(), Qt::Key_V, Qt::ControlModifier, 0);
0317         QCOMPARE(widget->lineEdit()->text(), QStringLiteral(u"45°  45%  0"));
0318         QTest::keyClick(widget.data(), Qt::Key_Right);
0319         QCOMPARE(widget->lineEdit()->text(), QStringLiteral(u"45°  45%  0"));
0320         QTest::keyClick(widget.data(), Qt::Key_Right);
0321         QCOMPARE(widget->lineEdit()->text(), QStringLiteral(u"45°  45%  0"));
0322         QTest::keyClick(widget.data(), Qt::Key_Right);
0323         QCOMPARE(widget->lineEdit()->text(), QStringLiteral(u"45°  45%  0"));
0324         QTest::keyClick(widget.data(), Qt::Key_Right);
0325         QCOMPARE(widget->lineEdit()->text(), QStringLiteral(u"45°  45%  0"));
0326     }
0327 
0328     void testCurrentSectionIndex()
0329     {
0330         MultiSpinBox test;
0331         // Test default index
0332         QCOMPARE(test.d_pointer->m_currentIndex, 0);
0333 
0334         // suppress warnings
0335         qInstallMessageHandler(voidMessageHandler);
0336         // Test if setting negative value is ignored
0337         QVERIFY_EXCEPTION_THROWN( //
0338             test.d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(-1), //
0339             int);
0340         QCOMPARE(test.d_pointer->m_currentIndex, 0);
0341         QVERIFY_EXCEPTION_THROWN( //
0342             test.d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(-100), //
0343             int);
0344         QCOMPARE(test.d_pointer->m_currentIndex, 0);
0345         // Test setting too high values is ignored
0346         QVERIFY_EXCEPTION_THROWN( //
0347             test.d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(100), //
0348             int);
0349         QCOMPARE(test.d_pointer->m_currentIndex, 0);
0350         // do not suppress warning for generating invalid QColor anymore
0351         qInstallMessageHandler(nullptr);
0352 
0353         // Test if correct sections are stored correctly
0354         QList<MultiSpinBoxSection> mySectionList;
0355         mySectionList.append(MultiSpinBoxSection());
0356         mySectionList.append(MultiSpinBoxSection());
0357         mySectionList.append(MultiSpinBoxSection());
0358         test.setSectionConfigurations(mySectionList);
0359         test.d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(2);
0360         QCOMPARE(test.d_pointer->m_currentIndex, 2);
0361     }
0362 
0363     void testSetConfiguration()
0364     {
0365         // Correct configurations should be applied as-is.
0366         QList<MultiSpinBoxSection> myConfigurations;
0367         myConfigurations.append(MultiSpinBoxSection());
0368         myConfigurations.append(MultiSpinBoxSection());
0369         myConfigurations.append(MultiSpinBoxSection());
0370         MultiSpinBox test;
0371         QCOMPARE(test.sectionConfigurations().count(), 1);
0372         QCOMPARE(test.d_pointer->m_currentIndex, 0);
0373         test.setSectionConfigurations(myConfigurations);
0374         QCOMPARE(test.sectionConfigurations().count(), 3);
0375         QCOMPARE(test.d_pointer->m_currentIndex, 0);
0376 
0377         // Empty configurations shall be ignored
0378         test.setSectionConfigurations(QList<MultiSpinBoxSection>());
0379         QCOMPARE(test.sectionConfigurations().count(), 3);
0380 
0381         // Invalid values should be adapted
0382         myConfigurations.clear();
0383         MultiSpinBoxSection myInvalidSection;
0384         myInvalidSection.setMinimum(50);
0385         myInvalidSection.setMaximum(30);
0386         myConfigurations.append(myInvalidSection);
0387         test.setSectionConfigurations(myConfigurations);
0388         QList<double> myValues;
0389         myValues.clear();
0390         myValues.append(40);
0391         test.setSectionValues(myValues);
0392         QVERIFY2(
0393             // condition
0394             test.d_pointer->m_sectionConfigurations.at(0).minimum() //
0395                 <= test.d_pointer->m_sectionConfigurations.at(0).maximum(),
0396             // comment
0397             "minimum <= maximum");
0398         QVERIFY2(
0399             // condition
0400             test.d_pointer->m_sectionConfigurations.at(0).minimum() //
0401                 <= test.d_pointer->m_sectionValues.at(0),
0402             // comment
0403             "minimum <= value");
0404         QVERIFY2(
0405             // condition
0406             test.d_pointer->m_sectionValues.at(0) //
0407                 <= test.d_pointer->m_sectionConfigurations.at(0).maximum(),
0408             // comment
0409             "value <= maximum");
0410 
0411         // Invalid values should be adapted
0412         myConfigurations.clear();
0413         myInvalidSection.setMinimum(-50);
0414         myInvalidSection.setMaximum(-70);
0415         myConfigurations.append(myInvalidSection);
0416         myValues.clear();
0417         myValues.append(-60);
0418         test.setSectionConfigurations(myConfigurations);
0419         QVERIFY2(
0420             // condition
0421             test.d_pointer->m_sectionConfigurations.at(0).minimum() //
0422                 <= test.d_pointer->m_sectionConfigurations.at(0).maximum(),
0423             // comment
0424             "minimum <= maximum");
0425         QVERIFY2(
0426             // condition
0427             test.d_pointer->m_sectionConfigurations.at(0).minimum() //
0428                 <= test.d_pointer->m_sectionValues.at(0),
0429             // comment
0430             "minimum <= value");
0431         QVERIFY2(
0432             // condition
0433             test.d_pointer->m_sectionValues.at(0) //
0434                 <= test.d_pointer->m_sectionConfigurations.at(0).maximum(),
0435             // comment
0436             "value <= maximum");
0437     }
0438 
0439     void testMinimalSizeHint()
0440     {
0441         PerceptualColor::MultiSpinBox myMulti;
0442         QCOMPARE(myMulti.minimumSizeHint(), myMulti.sizeHint());
0443         myMulti.setSectionConfigurations(exampleConfigurations);
0444         QCOMPARE(myMulti.minimumSizeHint(), myMulti.sizeHint());
0445     }
0446 
0447     void testSizeHint()
0448     {
0449         PerceptualColor::MultiSpinBox myMulti;
0450         // Example configuration with long prefix and suffix to make
0451         // sure being bigger than the default minimal widget size.
0452         QList<MultiSpinBoxSection> config;
0453         MultiSpinBoxSection section;
0454         section.setMinimum(1);
0455         section.setMaximum(9);
0456         section.setPrefix(QStringLiteral(u"abcdefghij"));
0457         section.setSuffix(QStringLiteral(u"abcdefghij"));
0458         config.append(section);
0459         myMulti.setSectionConfigurations(config);
0460         const int referenceWidth = myMulti.sizeHint().width();
0461 
0462         // Now test various configurations that should lead to bigger sizes…
0463 
0464         section.setMinimum(-1);
0465         section.setMaximum(9);
0466         section.setPrefix(QStringLiteral(u"abcdefghij"));
0467         section.setSuffix(QStringLiteral(u"abcdefghij"));
0468         config.clear();
0469         config.append(section);
0470         myMulti.setSectionConfigurations(config);
0471         QVERIFY(myMulti.sizeHint().width() > referenceWidth);
0472 
0473         section.setMinimum(1);
0474         section.setMaximum(19);
0475         section.setPrefix(QStringLiteral(u"abcdefghij"));
0476         section.setSuffix(QStringLiteral(u"abcdefghij"));
0477         config.clear();
0478         config.append(section);
0479         myMulti.setSectionConfigurations(config);
0480         QVERIFY(myMulti.sizeHint().width() > referenceWidth);
0481 
0482         section.setMinimum(-1);
0483         section.setMaximum(9);
0484         section.setPrefix(QStringLiteral(u"abcdefghijh"));
0485         section.setSuffix(QStringLiteral(u"abcdefghij"));
0486         config.clear();
0487         config.append(section);
0488         myMulti.setSectionConfigurations(config);
0489         QVERIFY(myMulti.sizeHint().width() > referenceWidth);
0490 
0491         section.setMinimum(-1);
0492         section.setMaximum(9);
0493         section.setPrefix(QStringLiteral(u"abcdefghij"));
0494         section.setSuffix(QStringLiteral(u"abcdefghijh"));
0495         config.clear();
0496         config.append(section);
0497         myMulti.setSectionConfigurations(config);
0498         QVERIFY(myMulti.sizeHint().width() > referenceWidth);
0499     }
0500 
0501     void testUpdatePrefixValueSuffixText()
0502     {
0503         PerceptualColor::MultiSpinBox myMulti;
0504         // Example configuration with long prefix and suffix to make
0505         // sure being bigger than the default minimal widget size.
0506         QList<MultiSpinBoxSection> myConfigurations;
0507         MultiSpinBoxSection myConfiguration;
0508         QList<double> myValues;
0509 
0510         myConfiguration.setDecimals(0);
0511         myConfiguration.setMinimum(1);
0512         myConfiguration.setMaximum(9);
0513         myConfiguration.setPrefix(QStringLiteral(u"abc"));
0514         myConfiguration.setSuffix(QStringLiteral(u"def"));
0515         myConfigurations.append(myConfiguration);
0516         myValues.append(8);
0517 
0518         myConfiguration.setMinimum(10);
0519         myConfiguration.setMaximum(90);
0520         myConfiguration.setPrefix(QStringLiteral(u"ghi"));
0521         myConfiguration.setSuffix(QStringLiteral(u"jkl"));
0522         myConfigurations.append(myConfiguration);
0523         myValues.append(80);
0524 
0525         myMulti.setSectionConfigurations(myConfigurations);
0526         myMulti.setSectionValues(myValues);
0527         myMulti.d_pointer->m_currentIndex = 1;
0528         myMulti.d_pointer->updatePrefixValueSuffixText();
0529         QCOMPARE(myMulti.d_pointer->m_textBeforeCurrentValue, //
0530                  QStringLiteral(u"abc8defghi"));
0531         QCOMPARE(myMulti.d_pointer->m_textOfCurrentValue, //
0532                  QStringLiteral(u"80"));
0533         QCOMPARE(myMulti.d_pointer->m_textAfterCurrentValue, //
0534                  QStringLiteral(u"jkl"));
0535     }
0536 
0537     void testSetCurrentSectionIndexWithoutSelectingText()
0538     {
0539         PerceptualColor::MultiSpinBox myMulti;
0540         QList<MultiSpinBoxSection> myConfigurations;
0541         MultiSpinBoxSection myConfiguration;
0542         QList<double> myValues;
0543 
0544         myConfiguration.setMinimum(1);
0545         myConfiguration.setMaximum(9);
0546         myConfiguration.setPrefix(QStringLiteral(u"abc"));
0547         myConfiguration.setSuffix(QStringLiteral(u"def"));
0548         myConfigurations.append(myConfiguration);
0549         myValues.append(8);
0550 
0551         myConfiguration.setMinimum(10);
0552         myConfiguration.setMaximum(90);
0553         myConfiguration.setPrefix(QStringLiteral(u"ghi"));
0554         myConfiguration.setSuffix(QStringLiteral(u"jkl"));
0555         myConfigurations.append(myConfiguration);
0556         myValues.append(80);
0557 
0558         myMulti.setSectionConfigurations(myConfigurations);
0559         myMulti.setSectionValues(myValues);
0560         myMulti.d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(1);
0561         QCOMPARE(myMulti.d_pointer->m_currentIndex, 1);
0562         QVERIFY2(!myMulti.lineEdit()->hasSelectedText(), //
0563                  "No text should be selected.");
0564     }
0565 
0566     void testSetCurrentSectionIndex()
0567     {
0568         PerceptualColor::MultiSpinBox myMulti;
0569         myMulti.setSectionConfigurations(exampleConfigurations);
0570         myMulti.d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(2);
0571         QCOMPARE(myMulti.d_pointer->m_currentIndex, 2);
0572         myMulti.d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(0);
0573         QCOMPARE(myMulti.d_pointer->m_currentIndex, 0);
0574         myMulti.d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(1);
0575         QCOMPARE(myMulti.d_pointer->m_currentIndex, 1);
0576         myMulti.d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(2);
0577         QCOMPARE(myMulti.d_pointer->m_currentIndex, 2);
0578         myMulti.d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(0);
0579         QCOMPARE(myMulti.d_pointer->m_currentIndex, 0);
0580 
0581         QList<MultiSpinBoxSection> myConfigurations;
0582         MultiSpinBoxSection myConfiguration;
0583         QList<double> myValues;
0584 
0585         myConfiguration.setMinimum(1);
0586         myConfiguration.setMaximum(9);
0587         myConfiguration.setPrefix(QStringLiteral(u"abc"));
0588         myConfiguration.setSuffix(QStringLiteral(u"def"));
0589         myConfigurations.append(myConfiguration);
0590         myValues.append(8);
0591 
0592         myConfiguration.setMinimum(10);
0593         myConfiguration.setMaximum(90);
0594         myConfiguration.setPrefix(QStringLiteral(u"ghi"));
0595         myConfiguration.setSuffix(QStringLiteral(u"jkl"));
0596         myConfigurations.append(myConfiguration);
0597         myValues.append(80);
0598 
0599         myMulti.setSectionConfigurations(myConfigurations);
0600         myMulti.setSectionValues(myValues);
0601 
0602         myMulti.d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(1);
0603         QCOMPARE(myMulti.d_pointer->m_currentIndex, 1);
0604         QVERIFY2(!myMulti.lineEdit()->hasSelectedText(),
0605                  "No text should be selected because invisible widgets "
0606                  "have no focus.");
0607     }
0608 
0609     void testStepEnabledSimple()
0610     {
0611         PerceptualColor::MultiSpinBox myMulti;
0612         QList<MultiSpinBoxSection> myConfigurations;
0613         MultiSpinBoxSection myConfiguration;
0614         QList<double> myValues;
0615 
0616         myConfigurations.clear();
0617         myConfiguration.setMinimum(1);
0618         myConfiguration.setMaximum(9);
0619         myConfiguration.setPrefix(QStringLiteral(u"abc"));
0620         myConfiguration.setSuffix(QStringLiteral(u"def"));
0621         myConfigurations.append(myConfiguration);
0622         myMulti.setSectionConfigurations(myConfigurations);
0623         myValues.clear();
0624         myValues.append(8);
0625         myMulti.setSectionValues(myValues);
0626         QAbstractSpinBox::StepEnabled flags = myMulti.stepEnabled();
0627         QVERIFY2(flags.testFlag(QAbstractSpinBox::StepUpEnabled), //
0628                  "Step up should be enabled");
0629         QVERIFY2(flags.testFlag(QAbstractSpinBox::StepDownEnabled), //
0630                  "Step down should be enabled");
0631 
0632         myValues.clear();
0633         myValues.append(9);
0634         myMulti.setSectionValues(myValues);
0635         flags = myMulti.stepEnabled();
0636         QVERIFY2(!flags.testFlag(QAbstractSpinBox::StepUpEnabled), //
0637                  "Step up should be disabled");
0638         QVERIFY2(flags.testFlag(QAbstractSpinBox::StepDownEnabled), //
0639                  "Step down should be enabled");
0640 
0641         myValues.clear();
0642         myValues.append(10);
0643         myMulti.setSectionValues(myValues);
0644         flags = myMulti.stepEnabled();
0645         QVERIFY2(!flags.testFlag(QAbstractSpinBox::StepUpEnabled), //
0646                  "Step up should be disabled");
0647         QVERIFY2(flags.testFlag(QAbstractSpinBox::StepDownEnabled), //
0648                  "Step down should be enabled");
0649 
0650         myValues.clear();
0651         myValues.append(1);
0652         myMulti.setSectionValues(myValues);
0653         flags = myMulti.stepEnabled();
0654         QVERIFY2(flags.testFlag(QAbstractSpinBox::StepUpEnabled), //
0655                  "Step up should be enabled");
0656         QVERIFY2(!flags.testFlag(QAbstractSpinBox::StepDownEnabled), //
0657                  "Step down should be disabled");
0658 
0659         myValues.clear();
0660         myValues.append(0);
0661         myMulti.setSectionValues(myValues);
0662         flags = myMulti.stepEnabled();
0663         QVERIFY2(flags.testFlag(QAbstractSpinBox::StepUpEnabled), //
0664                  "Step up should be enabled");
0665         QVERIFY2(!flags.testFlag(QAbstractSpinBox::StepDownEnabled), //
0666                  "Step down should be disabled");
0667 
0668         myValues.clear();
0669         myValues.append(-1);
0670         myMulti.setSectionValues(myValues);
0671         flags = myMulti.stepEnabled();
0672         QVERIFY2(flags.testFlag(QAbstractSpinBox::StepUpEnabled), //
0673                  "Step up should be enabled");
0674         QVERIFY2(!flags.testFlag(QAbstractSpinBox::StepDownEnabled), //
0675                  "Step down should be disabled");
0676     }
0677 
0678     void testStepEnabledAndSectionIndex_data()
0679     {
0680         QTest::addColumn<int>("cursorPosition");
0681         QTest::addColumn<int>("sectionIndex");
0682         QTest::addColumn<int>("minimum");
0683         QTest::addColumn<int>("value");
0684         QTest::addColumn<int>("maximum");
0685         QTest::addColumn<bool>("StepUpEnabled");
0686         QTest::addColumn<bool>("StepDownEnabled");
0687 
0688         QTest::newRow("0") << 0 << 0 << 0 << 0 << 360 << true << false;
0689         QTest::newRow("1") << 1 << 0 << 0 << 0 << 360 << true << false;
0690         QTest::newRow("2") << 2 << 0 << 0 << 0 << 360 << true << false;
0691         QTest::newRow("4") << 4 << 1 << 0 << 5 << 100 << true << true;
0692         QTest::newRow("5") << 5 << 1 << 0 << 5 << 100 << true << true;
0693         QTest::newRow("6") << 6 << 1 << 0 << 5 << 100 << true << true;
0694         QTest::newRow("8") << 8 << 2 << 0 << 0 << 255 << true << false;
0695         QTest::newRow("9") << 9 << 2 << 0 << 0 << 255 << true << false;
0696     }
0697 
0698     void testStepEnabledAndSectionIndex()
0699     {
0700         QScopedPointer<PerceptualColor::MultiSpinBox> widget( //
0701             new PerceptualColor::MultiSpinBox());
0702         QList<MultiSpinBoxSection> specialConfigurations = //
0703             exampleConfigurations;
0704         QList<double> myValues;
0705         while (myValues.count() < specialConfigurations.count()) {
0706             myValues.append(0);
0707         }
0708         const quint8 sampleSectionNumber = 1;
0709         const quint8 sampleValue = 5;
0710         widget->setSectionConfigurations(specialConfigurations);
0711         myValues[sampleSectionNumber] = sampleValue;
0712         widget->setSectionValues(myValues);
0713         widget->d_pointer->setCurrentIndexAndUpdateTextAndSelectValue( //
0714             sampleSectionNumber);
0715 
0716         // Assertions: Assert that the setup is okay.
0717         // Assert statements seem to be not always reliably within QTest.
0718         // Therefore we do some assertions here with QCOMPARE.
0719         QCOMPARE(widget->lineEdit()->text(), QStringLiteral(u"0°  5%  0"));
0720         QCOMPARE(widget->sectionValues().at(sampleSectionNumber), sampleValue);
0721         QAbstractSpinBox::StepEnabled flags;
0722 
0723         // Actual testing
0724         QFETCH(int, cursorPosition);
0725         QFETCH(int, sectionIndex);
0726         QFETCH(int, minimum);
0727         QFETCH(int, value);
0728         QFETCH(int, maximum);
0729         QFETCH(bool, StepUpEnabled);
0730         QFETCH(bool, StepDownEnabled);
0731         widget->lineEdit()->setCursorPosition(cursorPosition);
0732         if (widget->lineEdit()->text() != QStringLiteral(u"0°  5%  0")) {
0733             // Throw an exception instead of using an assert statement.
0734             // Assert statements seem to be not always reliably within QTest.
0735             throw 0;
0736         }
0737         flags = widget->stepEnabled();
0738         const auto &d = widget->d_pointer;
0739         QCOMPARE(d->m_currentIndex, sectionIndex);
0740         QCOMPARE(d->m_sectionConfigurations.at(d->m_currentIndex).minimum(), //
0741                  minimum);
0742         QCOMPARE(d->m_sectionValues.at(d->m_currentIndex), //
0743                  value);
0744         QCOMPARE(d->m_sectionConfigurations.at(d->m_currentIndex).maximum(), //
0745                  maximum);
0746         QCOMPARE(flags.testFlag(QAbstractSpinBox::StepUpEnabled), //
0747                  StepUpEnabled);
0748         QCOMPARE(flags.testFlag(QAbstractSpinBox::StepDownEnabled), //
0749                  StepDownEnabled);
0750     }
0751 
0752     void testConfiguration()
0753     {
0754         PerceptualColor::MultiSpinBox myMulti;
0755         QList<MultiSpinBoxSection> config;
0756         MultiSpinBoxSection section;
0757         section.setMinimum(1);
0758         section.setMaximum(9);
0759         section.setPrefix(QStringLiteral(u"abc"));
0760         section.setSuffix(QStringLiteral(u"def"));
0761         config.append(section);
0762         myMulti.setSectionConfigurations(config);
0763         QCOMPARE(myMulti.sectionConfigurations().count(), 1);
0764         QCOMPARE(myMulti.sectionConfigurations().at(0).minimum(), 1);
0765         QCOMPARE(myMulti.sectionConfigurations().at(0).maximum(), 9);
0766         QCOMPARE(myMulti.sectionConfigurations().at(0).prefix(), //
0767                  QStringLiteral(u"abc"));
0768         QCOMPARE(myMulti.sectionConfigurations().at(0).suffix(), //
0769                  QStringLiteral(u"def"));
0770     }
0771 
0772     void testFocusIntegrationForwardTab()
0773     {
0774         // Integration test for:
0775         // → MultiSpinBox::focusNextPrevChild()
0776         // → MultiSpinBox::focusInEvent()
0777         // → MultiSpinBox::focusOutEvent()
0778         QScopedPointer<QWidget> parentWidget(new QWidget());
0779         QSpinBox *widget1 = new QSpinBox(parentWidget.data());
0780         widget1->setFocusPolicy(Qt::FocusPolicy::StrongFocus);
0781         PerceptualColor::MultiSpinBox *widget2 = //
0782             new PerceptualColor::MultiSpinBox(parentWidget.data());
0783         widget2->setFocusPolicy(Qt::FocusPolicy::StrongFocus);
0784         widget2->setSectionConfigurations(exampleConfigurations);
0785         QSpinBox *widget3 = new QSpinBox(parentWidget.data());
0786         widget3->setFocusPolicy(Qt::FocusPolicy::StrongFocus);
0787         QLabel *label2 = new QLabel(QStringLiteral(u"&Test"), //
0788                                     parentWidget.data());
0789         label2->setBuddy(widget2);
0790         widget1->setFocus();
0791         parentWidget->setFocusPolicy(Qt::FocusPolicy::StrongFocus);
0792 
0793         // It is necessary to show the widget and make it active
0794         // to make focus and widget events working within unit tests.
0795         parentWidget->show();
0796         QApplication::setActiveWindow(parentWidget.data());
0797 
0798         // Assert that the setup is okay.
0799         if (!widget1->hasFocus()) {
0800             // Throw an exception instead of using an assert statement.
0801             // Assert statements seem to be not always reliably within QTest.
0802             throw 0;
0803         }
0804         if (widget2->hasFocus()) {
0805             // Throw an exception instead of using an assert statement.
0806             // Assert statements seem to be not always reliably within QTest.
0807             throw 0;
0808         }
0809         if (widget3->hasFocus()) {
0810             // Throw an exception instead of using an assert statement.
0811             // Assert statements seem to be not always reliably within QTest.
0812             throw 0;
0813         }
0814         if (QApplication::focusWidget() != widget1) {
0815             // Throw an exception instead of using an assert statement.
0816             // Assert statements seem to be not always reliably within QTest.
0817             throw 0;
0818         }
0819         if (widget2->d_pointer->m_sectionConfigurations.count() != 3) {
0820             // Throw an exception instead of using an assert statement.
0821             // Assert statements seem to be not always reliably within QTest.
0822             throw 0;
0823         }
0824 
0825         // Start actual testing
0826 
0827         // Apparently it isn’t possible to call simply the key click
0828         // on the parent widget. This code fails sometimes:
0829         // QTest::keyClick(parentWidget, Qt::Key::Key_Tab);
0830         // Therefore, we call QTest::keyClick() on
0831         // QApplication::focusWidget()
0832 
0833         // Move focus from widget1 to widget2/section0
0834         QTest::keyClick(QApplication::focusWidget(), Qt::Key::Key_Tab);
0835         QCOMPARE(QApplication::focusWidget(), widget2);
0836         QCOMPARE(widget2->d_pointer->m_currentIndex, 0);
0837         // Move focus from widget2/section0 to widget2/section1
0838         QTest::keyClick(QApplication::focusWidget(), Qt::Key::Key_Tab);
0839         QCOMPARE(QApplication::focusWidget(), widget2);
0840         QCOMPARE(widget2->d_pointer->m_currentIndex, 1);
0841         // Move focus from widget2/section1 to widget2/section2
0842         QTest::keyClick(QApplication::focusWidget(), Qt::Key::Key_Tab);
0843         QCOMPARE(QApplication::focusWidget(), widget2);
0844         QCOMPARE(widget2->d_pointer->m_currentIndex, 2);
0845         // Move focus from widget2/section2 to widget3
0846         QTest::keyClick(QApplication::focusWidget(), Qt::Key::Key_Tab);
0847         QCOMPARE(QApplication::focusWidget(), widget3);
0848         QCOMPARE(widget2->d_pointer->m_currentIndex, 0);
0849     }
0850 
0851     void testFocusIntegrationBackwardTab()
0852     {
0853         // Integration test for:
0854         // → MultiSpinBox::focusNextPrevChild()
0855         // → MultiSpinBox::focusInEvent()
0856         // → MultiSpinBox::focusOutEvent()
0857         QScopedPointer<QWidget> parentWidget(new QWidget());
0858         QSpinBox *widget1 = new QSpinBox(parentWidget.data());
0859         widget1->setFocusPolicy(Qt::FocusPolicy::StrongFocus);
0860         PerceptualColor::MultiSpinBox *widget2 = //
0861             new PerceptualColor::MultiSpinBox(parentWidget.data());
0862         widget2->setFocusPolicy(Qt::FocusPolicy::StrongFocus);
0863         widget2->setSectionConfigurations(exampleConfigurations);
0864         QSpinBox *widget3 = new QSpinBox(parentWidget.data());
0865         widget3->setFocusPolicy(Qt::FocusPolicy::StrongFocus);
0866         QLabel *label2 = new QLabel(QStringLiteral(u"&Test"), //
0867                                     parentWidget.data());
0868         label2->setBuddy(widget2);
0869         widget3->setFocus();
0870         parentWidget->setFocusPolicy(Qt::FocusPolicy::StrongFocus);
0871         parentWidget->show();
0872         // The following statement make focus and widget events working.
0873         QApplication::setActiveWindow(parentWidget.data());
0874         // Assert that the setup is okay.
0875         if (widget1->hasFocus()) {
0876             // Throw an exception instead of using an assert statement.
0877             // Assert statements seem to be not always reliably within QTest.
0878             throw 0;
0879         }
0880         if (widget2->hasFocus()) {
0881             // Throw an exception instead of using an assert statement.
0882             // Assert statements seem to be not always reliably within QTest.
0883             throw 0;
0884         }
0885         if (!widget3->hasFocus()) {
0886             // Throw an exception instead of using an assert statement.
0887             // Assert statements seem to be not always reliably within QTest.
0888             throw 0;
0889         }
0890         if (QApplication::focusWidget() != widget3) {
0891             // Throw an exception instead of using an assert statement.
0892             // Assert statements seem to be not always reliably within QTest.
0893             throw 0;
0894         }
0895         if (widget2->d_pointer->m_sectionConfigurations.count() != 3) {
0896             // Throw an exception instead of using an assert statement.
0897             // Assert statements seem to be not always reliably within QTest.
0898             throw 0;
0899         }
0900 
0901         // Start actual testing
0902         // Move focus from widget3 to widget2/section2
0903         QTest::keyClick(QApplication::focusWidget(), //
0904                         Qt::Key::Key_Tab, //
0905                         Qt::KeyboardModifier::ShiftModifier);
0906         QCOMPARE(QApplication::focusWidget(), widget2);
0907         QCOMPARE(widget2->d_pointer->m_currentIndex, 2);
0908         // Move focus from widget2/section2 to widget2/section1
0909         QTest::keyClick(QApplication::focusWidget(), //
0910                         Qt::Key::Key_Tab, //
0911                         Qt::KeyboardModifier::ShiftModifier);
0912         QCOMPARE(QApplication::focusWidget(), widget2);
0913         QCOMPARE(widget2->d_pointer->m_currentIndex, 1);
0914         // Move focus from widget2/section1 to widget2/section0
0915         QTest::keyClick(QApplication::focusWidget(), //
0916                         Qt::Key::Key_Tab, //
0917                         Qt::KeyboardModifier::ShiftModifier);
0918         QCOMPARE(QApplication::focusWidget(), widget2);
0919         QCOMPARE(widget2->d_pointer->m_currentIndex, 0);
0920         // Move focus from widget2/section0 to widget1
0921         QTest::keyClick(QApplication::focusWidget(), //
0922                         Qt::Key::Key_Tab, //
0923                         Qt::KeyboardModifier::ShiftModifier);
0924         QCOMPARE(QApplication::focusWidget(), widget1);
0925         QCOMPARE(widget2->d_pointer->m_currentIndex, 0);
0926     }
0927 
0928     void testFocusIntegrationIntegrationWithMnemonicBuddy()
0929     {
0930         // Integration test for:
0931         // → MultiSpinBox::focusNextPrevChild()
0932         // → MultiSpinBox::focusInEvent()
0933         // → MultiSpinBox::focusOutEvent()
0934         QScopedPointer<QWidget> parentWidget(new QWidget());
0935         QSpinBox *widget1 = new QSpinBox(parentWidget.data());
0936         widget1->setFocusPolicy(Qt::FocusPolicy::StrongFocus);
0937         PerceptualColor::MultiSpinBox *widget2 = //
0938             new PerceptualColor::MultiSpinBox(parentWidget.data());
0939         widget2->setFocusPolicy(Qt::FocusPolicy::StrongFocus);
0940         widget2->setSectionConfigurations(exampleConfigurations);
0941         widget2->d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(1);
0942         QSpinBox *widget3 = new QSpinBox(parentWidget.data());
0943         widget3->setFocusPolicy(Qt::FocusPolicy::StrongFocus);
0944         QLabel *label2 = new QLabel(QStringLiteral(u"&Test"), //
0945                                     parentWidget.data());
0946         label2->setBuddy(widget2);
0947         QLabel *label3 = new QLabel(QStringLiteral(u"&Other widget"), //
0948                                     parentWidget.data());
0949         label3->setBuddy(widget3);
0950         widget3->setFocus();
0951         parentWidget->setFocusPolicy(Qt::FocusPolicy::StrongFocus);
0952         parentWidget->show();
0953         // The following statement make focus and widget events working.
0954         QApplication::setActiveWindow(parentWidget.data());
0955         // Assert that the setup is okay.
0956         if (widget1->hasFocus()) {
0957             // Throw an exception instead of using an assert statement.
0958             // Assert statements seem to be not always reliably within QTest.
0959             throw 0;
0960         }
0961         if (widget2->hasFocus()) {
0962             // Throw an exception instead of using an assert statement.
0963             // Assert statements seem to be not always reliably within QTest.
0964             throw 0;
0965         }
0966         if (!widget3->hasFocus()) {
0967             // Throw an exception instead of using an assert statement.
0968             // Assert statements seem to be not always reliably within QTest.
0969             throw 0;
0970         }
0971         if (QApplication::focusWidget() != widget3) {
0972             // Throw an exception instead of using an assert statement.
0973             // Assert statements seem to be not always reliably within QTest.
0974             throw 0;
0975         }
0976         if (widget2->d_pointer->m_sectionConfigurations.count() != 3) {
0977             // Throw an exception instead of using an assert statement.
0978             // Assert statements seem to be not always reliably within QTest.
0979             throw 0;
0980         }
0981         if (widget2->d_pointer->m_currentIndex != 1) {
0982             // Throw an exception instead of using an assert statement.
0983             // Assert statements seem to be not always reliably within QTest.
0984             throw 0;
0985         }
0986 
0987         // Start actual testing
0988         // Move focus from widget3 to widget2/section0
0989         QTest::keyClick(QApplication::focusWidget(), //
0990                         Qt::Key::Key_T, //
0991                         Qt::KeyboardModifier::AltModifier);
0992         QCOMPARE(QApplication::focusWidget(), widget2);
0993         QCOMPARE(widget2->d_pointer->m_currentIndex, 0);
0994         // Move focus from widget2/section0 to widget2/section1
0995         QTest::keyClick(QApplication::focusWidget(), Qt::Key::Key_Tab);
0996         QCOMPARE(QApplication::focusWidget(), widget2);
0997         QCOMPARE(widget2->d_pointer->m_currentIndex, 1);
0998         // Move focus from widget2/section1 to widget3
0999         QTest::keyClick(QApplication::focusWidget(), //
1000                         Qt::Key::Key_O, //
1001                         Qt::KeyboardModifier::AltModifier);
1002         QCOMPARE(QApplication::focusWidget(), widget3);
1003         // Move focus from widget3 to widget2/section0
1004         // This has to move to section0 (even if before this event, the last
1005         // selected section of widget2 was NOT section0.
1006         QTest::keyClick(QApplication::focusWidget(), //
1007                         Qt::Key::Key_T, //
1008                         Qt::KeyboardModifier::AltModifier);
1009         QCOMPARE(QApplication::focusWidget(), widget2);
1010         QCOMPARE(widget2->d_pointer->m_currentIndex, 0);
1011     }
1012 
1013     void testFocusIntegrationFocusPolicy()
1014     {
1015         // Integration test for:
1016         // → MultiSpinBox::focusNextPrevChild()
1017         // → MultiSpinBox::focusInEvent()
1018         // → MultiSpinBox::focusOutEvent()
1019         QScopedPointer<QWidget> parentWidget(new QWidget());
1020         QSpinBox *widget1 = new QSpinBox(parentWidget.data());
1021         widget1->setFocusPolicy(Qt::FocusPolicy::StrongFocus);
1022         PerceptualColor::MultiSpinBox *widget2 = //
1023             new PerceptualColor::MultiSpinBox(parentWidget.data());
1024         widget2->setFocusPolicy(Qt::FocusPolicy::StrongFocus);
1025         widget2->setSectionConfigurations(exampleConfigurations);
1026         QSpinBox *widget3 = new QSpinBox(parentWidget.data());
1027         widget3->setFocusPolicy(Qt::FocusPolicy::StrongFocus);
1028         QLabel *label2 = new QLabel(QStringLiteral(u"&Test"), //
1029                                     parentWidget.data());
1030         label2->setBuddy(widget2);
1031         QLabel *label3 = new QLabel(QStringLiteral(u"&Other widget"), //
1032                                     parentWidget.data());
1033         label3->setBuddy(widget3);
1034         widget3->setFocus();
1035         parentWidget->setFocusPolicy(Qt::FocusPolicy::StrongFocus);
1036         parentWidget->show();
1037         // The following statement make focus and widget events working.
1038         QApplication::setActiveWindow(parentWidget.data());
1039         // Assert that the setup is okay.
1040         if (widget1->hasFocus()) {
1041             // Throw an exception instead of using an assert statement.
1042             // Assert statements seem to be not always reliably within QTest.
1043             throw 0;
1044         }
1045         if (widget2->hasFocus()) {
1046             // Throw an exception instead of using an assert statement.
1047             // Assert statements seem to be not always reliably within QTest.
1048             throw 0;
1049         }
1050         if (!widget3->hasFocus()) {
1051             // Throw an exception instead of using an assert statement.
1052             // Assert statements seem to be not always reliably within QTest.
1053             throw 0;
1054         }
1055         if (QApplication::focusWidget() != widget3) {
1056             // Throw an exception instead of using an assert statement.
1057             // Assert statements seem to be not always reliably within QTest.
1058             throw 0;
1059         }
1060         if (widget2->d_pointer->m_sectionConfigurations.count() != 3) {
1061             // Throw an exception instead of using an assert statement.
1062             // Assert statements seem to be not always reliably within QTest.
1063             throw 0;
1064         }
1065 
1066         // Start actual testing
1067         // Make sure that MultiSpinBox does not react on incoming tab focus
1068         // events if the current focus policy does not allow tab focus.
1069         widget2->setFocusPolicy(Qt::FocusPolicy::ClickFocus);
1070         widget1->setFocus();
1071         if (QApplication::focusWidget() != widget1) {
1072             // Throw an exception instead of using an assert statement.
1073             // Assert statements seem to be not always reliably within QTest.
1074             throw 0;
1075         }
1076         QTest::keyClick(QApplication::focusWidget(), Qt::Key::Key_Tab);
1077         QCOMPARE(QApplication::focusWidget(), widget3);
1078         widget2->setFocusPolicy(Qt::FocusPolicy::NoFocus);
1079         widget1->setFocus();
1080         if (QApplication::focusWidget() != widget1) {
1081             // Throw an exception instead of using an assert statement.
1082             // Assert statements seem to be not always reliably within QTest.
1083             throw 0;
1084         }
1085         QTest::keyClick(QApplication::focusWidget(), Qt::Key::Key_Tab);
1086         QCOMPARE(QApplication::focusWidget(), widget3);
1087     }
1088 
1089     void testStepBy()
1090     {
1091         QScopedPointer<PerceptualColor::MultiSpinBox> widget( //
1092             new PerceptualColor::MultiSpinBox());
1093         widget->setFocusPolicy(Qt::FocusPolicy::StrongFocus);
1094         widget->setSectionConfigurations(exampleConfigurations);
1095         widget->d_pointer->setCurrentIndexWithoutUpdatingText(0);
1096         widget->stepBy(13);
1097         QCOMPARE(widget->sectionValues().at(0), 13);
1098         widget->d_pointer->setCurrentIndexWithoutUpdatingText(1);
1099         widget->stepBy(130);
1100         QCOMPARE(widget->sectionValues().at(1), 100);
1101         widget->d_pointer->setCurrentIndexWithoutUpdatingText(2);
1102         widget->stepBy(-260);
1103         QCOMPARE(widget->sectionValues().at(2), 0);
1104     }
1105 
1106     void testStepUpDown()
1107     {
1108         QScopedPointer<PerceptualColor::MultiSpinBox> widget( //
1109             new PerceptualColor::MultiSpinBox());
1110         widget->setSectionConfigurations(exampleConfigurations);
1111         QCOMPARE(widget->sectionValues().at(0), 0);
1112         widget->stepUp();
1113         QCOMPARE(widget->sectionValues().at(0), 1);
1114         widget->stepUp();
1115         QCOMPARE(widget->sectionValues().at(0), 2);
1116         widget->stepDown();
1117         QCOMPARE(widget->sectionValues().at(0), 1);
1118         widget->stepDown();
1119         QCOMPARE(widget->sectionValues().at(0), 0);
1120         widget->stepDown();
1121         QCOMPARE(widget->sectionValues().at(0), 0);
1122     }
1123 
1124     void testUpdateValueFromText1()
1125     {
1126         QScopedPointer<PerceptualColor::MultiSpinBox> widget( //
1127             new PerceptualColor::MultiSpinBox());
1128         widget->setSectionConfigurations(exampleConfigurations);
1129         const quint8 sampleSectionNumber = 1;
1130         widget->d_pointer->setCurrentIndexAndUpdateTextAndSelectValue( //
1131             sampleSectionNumber);
1132         // Assert that the setup is okay.
1133         if (widget->lineEdit()->text() != QStringLiteral(u"0°  0%  0")) {
1134             // Throw an exception instead of using an assert statement.
1135             // Assert statements seem to be not always reliably within QTest.
1136             throw 0;
1137         }
1138         widget->d_pointer->updateCurrentValueFromText( //
1139             QStringLiteral(u"0°  9%  0"));
1140         QCOMPARE(widget->sectionValues().at(sampleSectionNumber), 9);
1141     }
1142 
1143     void testUpdateValueFromText2()
1144     {
1145         QScopedPointer<PerceptualColor::MultiSpinBox> widget( //
1146             new PerceptualColor::MultiSpinBox());
1147         QList<MultiSpinBoxSection> specialConfiguration = exampleConfigurations;
1148         const quint8 sampleSectionNumber = 1;
1149         const quint8 sampleValue = 5;
1150         widget->setSectionConfigurations(specialConfiguration);
1151         QList<double> myValues;
1152         while (myValues.count() < specialConfiguration.count()) {
1153             myValues.append(0);
1154         }
1155         myValues[sampleSectionNumber] = sampleValue;
1156         widget->setSectionValues(myValues);
1157         widget->d_pointer->setCurrentIndexAndUpdateTextAndSelectValue( //
1158             sampleSectionNumber);
1159         // Assert that the setup is okay.
1160         if (widget->lineEdit()->text() != QStringLiteral(u"0°  5%  0")) {
1161             // Throw an exception instead of using an assert statement.
1162             // Assert statements seem to be not always reliably within QTest.
1163             throw 0;
1164         }
1165         if (widget->sectionValues().at(sampleSectionNumber) != sampleValue) {
1166             // Throw an exception instead of using an assert statement.
1167             // Assert statements seem to be not always reliably within QTest.
1168             throw 0;
1169         }
1170         // suppress warnings
1171         qInstallMessageHandler(voidMessageHandler);
1172         // Execute the tested function (with an invalid argument)
1173         widget->d_pointer->updateCurrentValueFromText( //
1174             QStringLiteral(u"abcdef"));
1175         // do not suppress warning for generating invalid QColor anymore
1176         qInstallMessageHandler(nullptr);
1177         // The original value should not have changed.
1178         QCOMPARE(widget->sectionValues().at(sampleSectionNumber), sampleValue);
1179     }
1180 
1181     void testUpdateSectionFromCursorPosition()
1182     {
1183         // Setup
1184         QScopedPointer<PerceptualColor::MultiSpinBox> widget( //
1185             new PerceptualColor::MultiSpinBox());
1186         QList<MultiSpinBoxSection> specialConfiguration = exampleConfigurations;
1187         const quint8 sampleSectionNumber = 1;
1188         const quint8 sampleValue = 5;
1189         widget->setSectionConfigurations(specialConfiguration);
1190         QList<double> myValues;
1191         while (myValues.count() < specialConfiguration.count()) {
1192             myValues.append(0);
1193         }
1194         myValues[sampleSectionNumber] = sampleValue;
1195         widget->setSectionValues(myValues);
1196         widget->d_pointer->setCurrentIndexAndUpdateTextAndSelectValue( //
1197             sampleSectionNumber);
1198         // Assert that the setup is okay.
1199         if (widget->lineEdit()->text() != QStringLiteral(u"0°  5%  0")) {
1200             // Throw an exception instead of using an assert statement.
1201             // Assert statements seem to be not always reliably within QTest.
1202             throw 0;
1203         }
1204         if (widget->sectionValues().at(sampleSectionNumber) != sampleValue) {
1205             // Throw an exception instead of using an assert statement.
1206             // Assert statements seem to be not always reliably within QTest.
1207             throw 0;
1208         }
1209 
1210         // Do testing
1211         widget->lineEdit()->setCursorPosition(0);
1212         QCOMPARE(widget->d_pointer->m_currentIndex, 0);
1213         widget->lineEdit()->setCursorPosition(1);
1214         QCOMPARE(widget->d_pointer->m_currentIndex, 0);
1215         widget->lineEdit()->setCursorPosition(2);
1216         QCOMPARE(widget->d_pointer->m_currentIndex, 0);
1217         widget->lineEdit()->setCursorPosition(4);
1218         QCOMPARE(widget->d_pointer->m_currentIndex, 1);
1219         widget->lineEdit()->setCursorPosition(5);
1220         QCOMPARE(widget->d_pointer->m_currentIndex, 1);
1221         widget->lineEdit()->setCursorPosition(6);
1222         QCOMPARE(widget->d_pointer->m_currentIndex, 1);
1223         widget->lineEdit()->setCursorPosition(8);
1224         QCOMPARE(widget->d_pointer->m_currentIndex, 2);
1225         widget->lineEdit()->setCursorPosition(9);
1226         QCOMPARE(widget->d_pointer->m_currentIndex, 2);
1227     }
1228 
1229     void testInitialLineEditValue()
1230     {
1231         // Setup
1232         QScopedPointer<PerceptualColor::MultiSpinBox> widget( //
1233             new PerceptualColor::MultiSpinBox());
1234         QList<MultiSpinBoxSection> specialConfiguration = exampleConfigurations;
1235         const quint8 sampleSectionNumber = 1;
1236         const quint8 sampleValue = 5;
1237         widget->setSectionConfigurations(specialConfiguration);
1238         QList<double> myValues;
1239         while (myValues.count() < specialConfiguration.count()) {
1240             myValues.append(0);
1241         }
1242         myValues[sampleSectionNumber] = sampleValue;
1243         widget->setSectionValues(myValues);
1244         // Assert that the initial content of the line edit is okay
1245         QCOMPARE(widget->lineEdit()->text(), QStringLiteral(u"0°  5%  0"));
1246     }
1247 
1248     void testLocalizationAndInternationalization()
1249     {
1250         // Setup
1251         QScopedPointer<PerceptualColor::MultiSpinBox> widget( //
1252             new PerceptualColor::MultiSpinBox());
1253         QList<MultiSpinBoxSection> mySectionList;
1254         MultiSpinBoxSection mySection;
1255         mySection.setDecimals(1);
1256         mySection.setMinimum(0);
1257         mySection.setMaximum(100);
1258         mySectionList.append(mySection);
1259         widget->setSectionConfigurations(mySectionList);
1260         QList<double> myValues;
1261         myValues.append(50);
1262         widget->setSectionValues(myValues);
1263 
1264         // Begin testing
1265 
1266         widget->setLocale(QLocale::English);
1267         // Without calling update() or other functions, the new locale should
1268         // be applied on-the-fly.
1269         QCOMPARE(widget->lineEdit()->text(), QStringLiteral(u"50.0"));
1270 
1271         widget->setLocale(QLocale::German);
1272         // Without calling update() or other functions, the new locale should
1273         // be applied on-the-fly.
1274         QCOMPARE(widget->lineEdit()->text(), QStringLiteral(u"50,0"));
1275 
1276         widget->setLocale(QLocale::Bengali);
1277         // Without calling update() or other functions, the new locale should
1278         // be applied on-the-fly.
1279         QCOMPARE(widget->lineEdit()->text(), QStringLiteral(u"৫০.০"));
1280     }
1281 
1282     void testArrowKeys()
1283     {
1284         QScopedPointer<QWidget> parentWidget(new QWidget());
1285         PerceptualColor::MultiSpinBox *widget2 = //
1286             new PerceptualColor::MultiSpinBox(parentWidget.data());
1287         widget2->setFocusPolicy(Qt::FocusPolicy::StrongFocus);
1288         widget2->setSectionConfigurations(exampleConfigurations);
1289         widget2->setFocus();
1290         parentWidget->setFocusPolicy(Qt::FocusPolicy::StrongFocus);
1291         parentWidget->show();
1292         widget2->d_pointer->setCurrentIndexAndUpdateTextAndSelectValue(1);
1293         // The following statement make focus and widget events working.
1294         QApplication::setActiveWindow(parentWidget.data());
1295         // Assert that the setup is okay.
1296         if (!widget2->hasFocus()) {
1297             // Throw an exception instead of using an assert statement.
1298             // Assert statements seem to be not always reliably within QTest.
1299             throw 0;
1300         }
1301         if (QApplication::focusWidget() != widget2) {
1302             // Throw an exception instead of using an assert statement.
1303             // Assert statements seem to be not always reliably within QTest.
1304             throw 0;
1305         }
1306         if (widget2->d_pointer->m_sectionConfigurations.count() != 3) {
1307             // Throw an exception instead of using an assert statement.
1308             // Assert statements seem to be not always reliably within QTest.
1309             throw 0;
1310         }
1311         if (widget2->lineEdit()->text() != QStringLiteral(u"0°  0%  0")) {
1312             // Throw an exception instead of using an assert statement.
1313             // Assert statements seem to be not always reliably within QTest.
1314             throw 0;
1315         }
1316 
1317         // Start actual testing
1318         QTest::keyClick(QApplication::focusWidget(), Qt::Key::Key_Up);
1319         QCOMPARE(widget2->sectionValues().at(1), 1);
1320         QCOMPARE(widget2->lineEdit()->text(), QStringLiteral(u"0°  1%  0"));
1321     }
1322 
1323     void testSectionConfigurationDebug()
1324     {
1325         // suppress warnings
1326         qInstallMessageHandler(voidMessageHandler);
1327         // Test if QDebug support does not make a crash.
1328         qDebug() << MultiSpinBoxSection();
1329         // do not suppress warning for generating invalid QColor anymore
1330         qInstallMessageHandler(nullptr);
1331     }
1332 
1333     void testAddActionButton()
1334     {
1335         MultiSpinBox mySpinBox;
1336         int oldWidth = 0;
1337         QCOMPARE(mySpinBox.d_pointer->m_actionButtonCount, 0);
1338         oldWidth = mySpinBox.sizeHint().width();
1339         mySpinBox.addActionButton( //
1340             new QAction(QStringLiteral(u"test"), &mySpinBox), //
1341             QLineEdit::ActionPosition::TrailingPosition);
1342         QCOMPARE(mySpinBox.d_pointer->m_actionButtonCount, 1);
1343         QVERIFY2(mySpinBox.sizeHint().width() > oldWidth,
1344                  "Verify: After adding an action button, "
1345                  "the size hint has a bigger width than before.");
1346         oldWidth = mySpinBox.sizeHint().width();
1347         mySpinBox.addActionButton( //
1348             new QAction(QStringLiteral(u"test"), &mySpinBox), //
1349             QLineEdit::ActionPosition::TrailingPosition);
1350         QCOMPARE(mySpinBox.d_pointer->m_actionButtonCount, 2);
1351         QVERIFY2(mySpinBox.sizeHint().width() > oldWidth,
1352                  "Verify: After adding an action button, "
1353                  "the size hint has a bigger width than before.");
1354     }
1355 
1356     void testFixSectionValue_data()
1357     {
1358         QTest::addColumn<double>("value");
1359         QTest::addColumn<double>("expectedOnIsWrappigFalse");
1360         QTest::addColumn<double>("expectedOnIsWrappigTrue");
1361 
1362         QTest::newRow(" -5") << -05. << 000. << 355.;
1363         QTest::newRow("  0") << 000. << 000. << 000.;
1364         QTest::newRow("  5") << 005. << 005. << 005.;
1365         QTest::newRow("355") << 355. << 355. << 355.;
1366         QTest::newRow("360") << 360. << 360. << 000.;
1367         QTest::newRow("365") << 365. << 360. << 005.;
1368         QTest::newRow("715") << 715. << 360. << 355.;
1369         QTest::newRow("720") << 720. << 360. << 000.;
1370         QTest::newRow("725") << 725. << 360. << 005.;
1371     }
1372 
1373     void testFixSectionValue()
1374     {
1375         MultiSpinBox mySpinBox;
1376 
1377         QFETCH(double, value);
1378         QFETCH(double, expectedOnIsWrappigFalse);
1379         QFETCH(double, expectedOnIsWrappigTrue);
1380 
1381         MultiSpinBoxSection myConfiguration;
1382         myConfiguration.setMinimum(0);
1383         myConfiguration.setMaximum(360);
1384         myConfiguration.setWrapping(false);
1385         QList<MultiSpinBoxSection> myConfigurations;
1386         myConfigurations.append(myConfiguration);
1387         mySpinBox.setSectionConfigurations(myConfigurations);
1388         QList<double> myValues;
1389         myValues.append(value);
1390         mySpinBox.setSectionValues(myValues);
1391         QCOMPARE(mySpinBox.sectionValues().at(0), expectedOnIsWrappigFalse);
1392 
1393         myConfiguration.setWrapping(true);
1394         myConfigurations.clear();
1395         myConfigurations.append(myConfiguration);
1396         mySpinBox.setSectionConfigurations(myConfigurations);
1397         mySpinBox.setSectionValues(myValues);
1398         QCOMPARE(mySpinBox.sectionValues().at(0), expectedOnIsWrappigTrue);
1399     }
1400 
1401     void testFixedSectionOther_data()
1402     {
1403         QTest::addColumn<double>("value");
1404         QTest::addColumn<double>("expectedOnIsWrappigFalse");
1405         QTest::addColumn<double>("expectedOnIsWrappigTrue");
1406 
1407         QTest::newRow("-25") << -25. << -20. << 335.;
1408         QTest::newRow("-20") << -20. << -20. << -20.;
1409         QTest::newRow("-15") << -15. << -15. << -15.;
1410         QTest::newRow("335") << 335. << 335. << 335.;
1411         QTest::newRow("340") << 340. << 340. << -20.;
1412         QTest::newRow("345") << 345. << 340. << -15.;
1413         QTest::newRow("695") << 695. << 340. << 335.;
1414         QTest::newRow("700") << 700. << 340. << -20.;
1415         QTest::newRow("705") << 705. << 340. << -15.;
1416     }
1417 
1418     void testFixedSectionOther()
1419     {
1420         MultiSpinBox mySpinBox;
1421 
1422         QFETCH(double, value);
1423         QFETCH(double, expectedOnIsWrappigFalse);
1424         QFETCH(double, expectedOnIsWrappigTrue);
1425 
1426         MultiSpinBoxSection myConfiguration;
1427         myConfiguration.setMinimum(-20);
1428         myConfiguration.setMaximum(340);
1429         myConfiguration.setWrapping(false);
1430         QList<MultiSpinBoxSection> myConfigurations;
1431         myConfigurations.append(myConfiguration);
1432         mySpinBox.setSectionConfigurations(myConfigurations);
1433         QList<double> myValues;
1434         myValues.append(value);
1435         mySpinBox.setSectionValues(myValues);
1436         QCOMPARE(mySpinBox.sectionValues().at(0), expectedOnIsWrappigFalse);
1437 
1438         myConfiguration.setWrapping(true);
1439         myConfigurations.clear();
1440         myConfigurations.append(myConfiguration);
1441         mySpinBox.setSectionConfigurations(myConfigurations);
1442         mySpinBox.setSectionValues(myValues);
1443         QCOMPARE(mySpinBox.sectionValues().at(0), expectedOnIsWrappigTrue);
1444     }
1445 
1446     void testValuesSetterAndConfigurationsSetter()
1447     {
1448         // Both, sectionValues() and sectionConfigurations() have a count()
1449         // that has to be identical. The count of sectionConfigurations() is
1450         // mandatory. Make sure that different setters let the count()s
1451         // in a correct state. Our reference for default values is
1452         // QDoubleSpinBox.
1453         MultiSpinBox myMulti;
1454         QDoubleSpinBox myDoubleSpinBox;
1455         QList<MultiSpinBoxSection> myConfigurations;
1456         QList<double> myValues;
1457 
1458         // Section count should be 1 (by default):
1459         QCOMPARE(myMulti.sectionConfigurations().count(), 1);
1460         QCOMPARE(myMulti.sectionValues().count(), 1);
1461         // Control that sections has default value:
1462         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1463 
1464         // Raise the section count to 3:
1465         myConfigurations.append(MultiSpinBoxSection());
1466         myConfigurations.append(MultiSpinBoxSection());
1467         myConfigurations.append(MultiSpinBoxSection());
1468         myMulti.setSectionConfigurations(myConfigurations);
1469         // Control that all the new sections got the default value:
1470         QCOMPARE(myMulti.sectionValues().at(1), myDoubleSpinBox.value());
1471         QCOMPARE(myMulti.sectionValues().at(2), myDoubleSpinBox.value());
1472 
1473         // Put specific values into each of the 3 sections:
1474         myValues.clear();
1475         myValues.append(10);
1476         myValues.append(11);
1477         myValues.append(12);
1478         myValues.append(13); // Too many values for current configuration count
1479         myMulti.setSectionValues(myValues);
1480         // Assert that the values have been applied correctly
1481         QCOMPARE(myMulti.sectionValues().at(0), 10);
1482         QCOMPARE(myMulti.sectionValues().at(1), 11);
1483         QCOMPARE(myMulti.sectionValues().at(2), 12);
1484         // The last value has to be ignored (as there are not so many sections):
1485         QCOMPARE(myMulti.sectionConfigurations().count(), 3);
1486         QCOMPARE(myMulti.sectionValues().count(), 3);
1487 
1488         // Apply a configuration with less sections
1489         myConfigurations.removeLast();
1490         QCOMPARE(myConfigurations.count(), 2); // Assertion
1491         myMulti.setSectionConfigurations(myConfigurations);
1492         QCOMPARE(myMulti.sectionConfigurations().count(), 2);
1493         QCOMPARE(myMulti.sectionValues().count(), 2);
1494         // The values that survive should not be changed:
1495         QCOMPARE(myMulti.sectionValues().at(0), 10);
1496         QCOMPARE(myMulti.sectionValues().at(1), 11);
1497 
1498         // Set sectionValues that has not enough values
1499         QCOMPARE(myMulti.sectionConfigurations().count(), 2); // Assertion
1500         QCOMPARE(myMulti.sectionValues().count(), 2); // Assertion
1501         QCOMPARE(myMulti.sectionValues().at(0), 10); // Assertion
1502         QCOMPARE(myMulti.sectionValues().at(1), 11); // Assertion
1503         myValues.clear();
1504         myValues.append(20);
1505         // Apply a value list with only 1 value:
1506         myMulti.setSectionValues(myValues);
1507         QCOMPARE(myMulti.sectionValues().at(0), 20); // This values was applied
1508         // Section count has not been altered:
1509         QCOMPARE(myMulti.sectionConfigurations().count(), 2);
1510         QCOMPARE(myMulti.sectionValues().count(), 2);
1511         // The last section, that got no particular value assigned,
1512         // has been changed to the default value. (This behaviour
1513         // is not documented, so not part of the public API, but
1514         // is seems reasonable (and less confusing and more
1515         // predictable than just stay with the old value:
1516         QCOMPARE(myMulti.sectionValues().at(1), 0);
1517     }
1518 
1519     void testSectionValuesChangedSignalBasic()
1520     {
1521         // Initialize
1522         MultiSpinBox myMulti;
1523         MultiSpinBoxSection myConfig;
1524         QList<MultiSpinBoxSection> myConfigs;
1525         myConfigs.append(myConfig);
1526         myConfigs.append(myConfig);
1527         myMulti.setSectionConfigurations(myConfigs);
1528         myMulti.show();
1529         QSignalSpy spyMulti(&myMulti, //
1530                             &MultiSpinBox::sectionValuesChanged);
1531         QDoubleSpinBox myDouble;
1532         myDouble.show();
1533         QSignalSpy spyDouble( //
1534             &myDouble, //
1535             QOverload<double>::of(&QDoubleSpinBox::valueChanged));
1536 
1537         // Set a value different from the default
1538         myMulti.setSectionValues(QList<double>{2, 2});
1539         myDouble.setValue(2);
1540         QCOMPARE(spyMulti.count(), 1);
1541         QCOMPARE(spyMulti.count(), spyDouble.count());
1542 
1543         // Setting the same value again should not call again the signal
1544         myMulti.setSectionValues(QList<double>{2, 2});
1545         myDouble.setValue(2);
1546         QCOMPARE(spyMulti.count(), 1);
1547         QCOMPARE(spyMulti.count(), spyDouble.count());
1548 
1549         // Setting a value list which has only one different element triggers:
1550         myMulti.setSectionValues(QList<double>{2, 3});
1551         myDouble.setValue(3);
1552         QCOMPARE(spyMulti.count(), 2);
1553         QCOMPARE(spyMulti.count(), spyDouble.count());
1554     }
1555 
1556     void testSectionValuesChangedSignalKeyboardTracking()
1557     {
1558         // Initialize
1559         MultiSpinBox myMulti;
1560         myMulti.setSectionConfigurations(
1561             // Use only one section to allow to compare easily
1562             // with QDoubleSpinBox
1563             QList<MultiSpinBoxSection>{MultiSpinBoxSection()});
1564         myMulti.show();
1565         QSignalSpy spyMulti(&myMulti, //
1566                             &MultiSpinBox::sectionValuesChanged);
1567         QDoubleSpinBox myDouble;
1568         myDouble.show();
1569         QSignalSpy spyDouble( //
1570             &myDouble, //
1571             QOverload<double>::of(&QDoubleSpinBox::valueChanged));
1572 
1573         // Test with keyboard tracking
1574         myMulti.setKeyboardTracking(true);
1575         myDouble.setKeyboardTracking(true);
1576 
1577         // Get test data
1578         QApplication::setActiveWindow(&myMulti);
1579         QTest::keyClick(&myMulti, Qt::Key_Up); // Get text selection
1580         QTest::keyClick(&myMulti, Qt::Key::Key_5);
1581         QTest::keyClick(&myMulti, Qt::Key::Key_4);
1582         QCOMPARE(myMulti.sectionValues().at(0), 54); // Assertion
1583 
1584         // Get reference data
1585         QApplication::setActiveWindow(&myDouble);
1586         QTest::keyClick(&myDouble, Qt::Key_Up);
1587         QTest::keyClick(&myDouble, Qt::Key::Key_5);
1588         QTest::keyClick(&myDouble, Qt::Key::Key_4);
1589         QCOMPARE(myDouble.value(), 54); // Assertion
1590 
1591         // Test conformance of MultiSpinBox with QDoubleSpinBox’s behaviour
1592         QCOMPARE(spyMulti.count(), spyDouble.count());
1593         for (int i = 0; i < spyMulti.count(); ++i) {
1594             QCOMPARE(spyMulti // Get value of first section of MultiSpinBox…
1595                          .at(i) // Signal at position i
1596                          .at(0) // First argument of this signal
1597                          .value<QList<double>>() // Convert to original type
1598                          .at(0), // First section of the MultiSpinBox
1599                      spyDouble // Get value of QSpinBox…
1600                          .at(i) // Signal at position i
1601                          .at(0) // First argument of this signal
1602                          .toDouble() // Convert to original type
1603             );
1604         }
1605     }
1606 
1607     void testRoundingBehaviourCompliance()
1608     {
1609         // Test the compliance of the behaviour of this class with
1610         // the behaviour of QDoubleSpinBox
1611         MultiSpinBoxSection myConfig;
1612         myConfig.setDecimals(0);
1613         myConfig.setMinimum(5);
1614         myConfig.setMaximum(360);
1615         MultiSpinBox myMulti;
1616         myMulti.setSectionConfigurations(
1617             // Create on-the-fly a list with only one section
1618             QList<MultiSpinBoxSection>{myConfig});
1619         QDoubleSpinBox myDoubleSpinBox;
1620         myDoubleSpinBox.setDecimals(0);
1621         myDoubleSpinBox.setMinimum(5);
1622         myDoubleSpinBox.setMaximum(360);
1623 
1624         myMulti.setSectionValues(QList<double>{-1});
1625         myDoubleSpinBox.setValue(-1);
1626         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1627 
1628         myMulti.setSectionValues(QList<double>{0});
1629         myDoubleSpinBox.setValue(0);
1630         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1631 
1632         // Test with a value that rounds down and stays too small
1633         myMulti.setSectionValues(QList<double>{4.1});
1634         myDoubleSpinBox.setValue(4.1);
1635         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1636 
1637         // Test with a value that is too small, but rounds up to the minimum
1638         myMulti.setSectionValues(QList<double>{4.9}); // Rounds up to 5
1639         myDoubleSpinBox.setValue(4.9); // Rounds up to 5
1640         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1641 
1642         myMulti.setSectionValues(QList<double>{5});
1643         myDoubleSpinBox.setValue(5);
1644         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1645 
1646         // Test with a value that rounds down to the minimum
1647         myMulti.setSectionValues(QList<double>{5.1});
1648         myDoubleSpinBox.setValue(5.1);
1649         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1650 
1651         // Test with a value in the middle that rounds down
1652         myMulti.setSectionValues(QList<double>{72.1}); // Rounds up to 5
1653         myDoubleSpinBox.setValue(72.1);
1654         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1655 
1656         // Test with a value in the middle that rounds up
1657         myMulti.setSectionValues(QList<double>{72.9}); // Rounds up to 5
1658         myDoubleSpinBox.setValue(72.9);
1659         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1660 
1661         // Test with a value that is in range and rounds down
1662         myMulti.setSectionValues(QList<double>{359.1});
1663         myDoubleSpinBox.setValue(359.1);
1664         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1665 
1666         // Test with a value that rounds up to the maximum
1667         myMulti.setSectionValues(QList<double>{359.9});
1668         myDoubleSpinBox.setValue(359.9);
1669 
1670         // Test with maximum
1671         myMulti.setSectionValues(QList<double>{360});
1672         myDoubleSpinBox.setValue(360);
1673 
1674         // Test with value that rounds down to maximum
1675         myMulti.setSectionValues(QList<double>{360.1});
1676         myDoubleSpinBox.setValue(360.1);
1677         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1678 
1679         myMulti.setSectionValues(QList<double>{361});
1680         myDoubleSpinBox.setValue(361);
1681         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1682     }
1683 
1684     void testRoundingBehaviourComplianceWithRoundedRanges()
1685     {
1686         // Test the compliance of the behaviour of this class with
1687         // the behaviour of QDoubleSpinBox
1688         MultiSpinBoxSection myConfig;
1689         myConfig.setDecimals(0);
1690         myConfig.setMinimum(4.8);
1691         myConfig.setMaximum(360.2);
1692         MultiSpinBox myMulti;
1693         myMulti.setSectionConfigurations(
1694             // Create on-the-fly a list with only one section
1695             QList<MultiSpinBoxSection>{myConfig});
1696         QDoubleSpinBox myDoubleSpinBox;
1697         myDoubleSpinBox.setDecimals(0);
1698         myDoubleSpinBox.setMinimum(4.8);
1699         myDoubleSpinBox.setMaximum(360.2);
1700 
1701         myMulti.setSectionValues(QList<double>{-1});
1702         myDoubleSpinBox.setValue(-1);
1703         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1704 
1705         myMulti.setSectionValues(QList<double>{0});
1706         myDoubleSpinBox.setValue(0);
1707         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1708 
1709         // Test with a value that rounds down and stays too small
1710         myMulti.setSectionValues(QList<double>{4.1});
1711         myDoubleSpinBox.setValue(4.1);
1712         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1713 
1714         myMulti.setSectionValues(QList<double>{4.7});
1715         myDoubleSpinBox.setValue(4.7); // Rounds up to 5
1716         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1717 
1718         myMulti.setSectionValues(QList<double>{4.8});
1719         myDoubleSpinBox.setValue(4.8); // Rounds up to 5
1720         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1721 
1722         myMulti.setSectionValues(QList<double>{4.9});
1723         myDoubleSpinBox.setValue(4.9); // Rounds up to 5
1724         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1725 
1726         myMulti.setSectionValues(QList<double>{5});
1727         myDoubleSpinBox.setValue(5);
1728         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1729 
1730         myMulti.setSectionValues(QList<double>{5.1}); // Rounds up to 5
1731         myDoubleSpinBox.setValue(5.1); // Rounds up to 5
1732         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1733 
1734         myMulti.setSectionValues(QList<double>{72.1}); // Rounds up to 5
1735         myDoubleSpinBox.setValue(72.1);
1736         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1737 
1738         myMulti.setSectionValues(QList<double>{72.9}); // Rounds up to 5
1739         myDoubleSpinBox.setValue(72.9);
1740         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1741 
1742         myMulti.setSectionValues(QList<double>{359.1});
1743         myDoubleSpinBox.setValue(359.1);
1744         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1745 
1746         myMulti.setSectionValues(QList<double>{359.9});
1747         myDoubleSpinBox.setValue(359.9);
1748 
1749         myMulti.setSectionValues(QList<double>{360});
1750         myDoubleSpinBox.setValue(360);
1751 
1752         myMulti.setSectionValues(QList<double>{360.1});
1753         myDoubleSpinBox.setValue(360.1);
1754         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1755 
1756         myMulti.setSectionValues(QList<double>{360.2});
1757         myDoubleSpinBox.setValue(360.2);
1758         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1759 
1760         myMulti.setSectionValues(QList<double>{360.3});
1761         myDoubleSpinBox.setValue(360.3);
1762         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1763 
1764         myMulti.setSectionValues(QList<double>{360.9});
1765         myDoubleSpinBox.setValue(360.9);
1766         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1767 
1768         myMulti.setSectionValues(QList<double>{361});
1769         myDoubleSpinBox.setValue(361);
1770         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1771     }
1772 
1773     void testRoundingBehaviourCornerCases()
1774     {
1775         // Test the compliance of the behaviour of this class with
1776         // the behaviour of QDoubleSpinBox
1777         MultiSpinBoxSection myConfig;
1778         myConfig.setDecimals(0);
1779         myConfig.setMinimum(4.8);
1780         myConfig.setMaximum(359.8);
1781         MultiSpinBox myMulti;
1782         myMulti.setSectionConfigurations(
1783             // Create on-the-fly a list with only one section
1784             QList<MultiSpinBoxSection>{myConfig});
1785         QDoubleSpinBox myDoubleSpinBox;
1786         myDoubleSpinBox.setDecimals(0);
1787         myDoubleSpinBox.setMinimum(4.8);
1788         myDoubleSpinBox.setMaximum(359.8);
1789 
1790         myMulti.setSectionValues(QList<double>{359});
1791         myDoubleSpinBox.setValue(359);
1792         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1793 
1794         myMulti.setSectionValues(QList<double>{359.7});
1795         myDoubleSpinBox.setValue(359.7);
1796         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1797 
1798         myMulti.setSectionValues(QList<double>{359.8});
1799         myDoubleSpinBox.setValue(359.8);
1800         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1801 
1802         myMulti.setSectionValues(QList<double>{359.9});
1803         myDoubleSpinBox.setValue(359.9);
1804         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1805 
1806         myMulti.setSectionValues(QList<double>{360});
1807         myDoubleSpinBox.setValue(360);
1808         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1809     }
1810 
1811     void testRoundingAfterChangingDecimals()
1812     {
1813         // Test the compliance of the behaviour of this class with
1814         // the behaviour of QDoubleSpinBox
1815         QList<MultiSpinBoxSection> myConfigs
1816             // Initialize the list with one single, default section
1817             {MultiSpinBoxSection()};
1818         myConfigs[0].setDecimals(2);
1819         MultiSpinBox myMulti;
1820         myMulti.setSectionConfigurations(myConfigs);
1821         QDoubleSpinBox myDoubleSpinBox;
1822         myDoubleSpinBox.setDecimals(2);
1823         const double initialTestValue = 12.34;
1824         myMulti.setSectionValues(QList<double>{initialTestValue});
1825         myDoubleSpinBox.setValue(initialTestValue);
1826         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1827 
1828         myConfigs[0].setDecimals(1);
1829         myMulti.setSectionConfigurations(myConfigs);
1830         myDoubleSpinBox.setDecimals(1);
1831         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1832 
1833         myConfigs[0].setDecimals(0);
1834         myMulti.setSectionConfigurations(myConfigs);
1835         myDoubleSpinBox.setDecimals(0);
1836         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1837 
1838         myConfigs[0].setDecimals(3);
1839         myMulti.setSectionConfigurations(myConfigs);
1840         myDoubleSpinBox.setDecimals(3);
1841         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1842 
1843         myConfigs[0].setDecimals(-1);
1844         myMulti.setSectionConfigurations(myConfigs);
1845         myDoubleSpinBox.setDecimals(-1);
1846         QCOMPARE(myMulti.sectionValues().at(0), myDoubleSpinBox.value());
1847     }
1848 
1849     void testMaximumWrappingRounding_data()
1850     {
1851         QTest::addColumn<double>("value");
1852 
1853         QTest::newRow("-360.1") << -360.1;
1854         QTest::newRow("-360") << -360.0;
1855         QTest::newRow("-359.9") << -359.9;
1856         QTest::newRow("-0.1") << -0.1;
1857         QTest::newRow("0") << 0.0;
1858         QTest::newRow("0.1") << 0.1;
1859         QTest::newRow("359.9") << 359.9;
1860         QTest::newRow("360") << 360.0;
1861         QTest::newRow("360.1") << 360.1;
1862         QTest::newRow("719.9") << 719.9;
1863         QTest::newRow("720") << 720.0;
1864         QTest::newRow("720.1") << 720.1;
1865     }
1866 
1867     void testMaximumWrappingRounding()
1868     {
1869         // When using wrapping, the MultiSpinBox is supposed to never
1870         // show “360”, but instead “0”. This should also be true when
1871         // rounding applies. And when being a magnitude higher or lower.
1872 
1873         // Get data
1874         QFETCH(double, value);
1875 
1876         // Initialization
1877         MultiSpinBoxSection myConfig;
1878         myConfig.setDecimals(0);
1879         myConfig.setMinimum(0);
1880         myConfig.setMaximum(360);
1881         myConfig.setWrapping(true);
1882         MultiSpinBox mySpinBox;
1883         mySpinBox.setSectionConfigurations(
1884             // Create the QList on the fly…
1885             QList<MultiSpinBoxSection>{myConfig});
1886 
1887         mySpinBox.setSectionValues(
1888             // Create the QList on the fly…
1889             QList<double>{value});
1890         QCOMPARE(mySpinBox.text(), QStringLiteral("0"));
1891 
1892         mySpinBox.setSectionValues(
1893             // Create the QList on the fly…
1894             QList<double>{359.9});
1895         QCOMPARE(mySpinBox.text(), QStringLiteral("0"));
1896     }
1897 
1898     void testMetaTypeDeclaration()
1899     {
1900         QVariant test;
1901         // The next line should produce a compiler error if the
1902         // type is not declared to Qt’s Meta Object System.
1903         test.setValue(MultiSpinBoxSection());
1904     }
1905 
1906     void testMetaTypeDeclarationForPropertySectionValues()
1907     {
1908         // The data type QList<double> seems to be automatically declared
1909         // because it’s an instance of QList. This unit test controls this
1910         // assumption.
1911         QVariant test;
1912         // The next line should produce a compiler error if the
1913         // type is not declared to Qt’s Meta Object System.
1914         test.setValue(QList<double>());
1915     }
1916 
1917     void testSnippet02()
1918     {
1919         snippet02();
1920     }
1921 };
1922 
1923 } // namespace PerceptualColor
1924 
1925 QTEST_MAIN(PerceptualColor::TestMultiSpinBox)
1926 
1927 // The following “include” is necessary because we do not use a header file:
1928 #include "testmultispinbox.moc"