#include "UI/ColorPickerSheet+Dialog.h" #include "UI/ColorPickerSheet.h" #include "UI/GridSettingsSheet.h" #include #include #include #include #include #include #include #include // Swift 1.3.5's GridSettingsSheet: the controls, previews and answers. class GridSettingsSheetTests : public QObject { Q_OBJECT private slots: // A user's English locale: Qt 6.10's C locale groups nothing. void initTestCase() { QLocale::setDefault(QLocale(QLocale::English, QLocale::UnitedStates)); } void theSheetLaysOutAsSwifts(); void eachChangePreviewsTheGrid(); void aPickedColourBecomesCustom(); void restoreDefaultsOkAndCancel(); void theRowsHoldSwiftsControls(); void aSliderOrScrubReplacesTyping(); }; namespace { // Drags a scrubbable title `points` to the right. void scrub(QLabel &title, int points) { QTest::mousePress(&title, Qt::LeftButton, Qt::NoModifier, QPoint(2, 2)); QMouseEvent move(QEvent::MouseMove, QPointF(2 + points, 2), title.mapToGlobal(QPointF(2 + points, 2)), Qt::NoButton, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(&title, &move); QTest::mouseRelease(&title, Qt::LeftButton, Qt::NoModifier, QPoint(2 + points, 2)); } struct Sheet { EditorSession session; std::vector previews; std::optional> answer; std::unique_ptr sheet; explicit Sheet(LayoutGrid grid = LayoutGrid(), GridAppearance appearance = GridAppearance()) : sheet(std::make_unique( session, grid, appearance, [this](const GridSettingsSheet::Settings &settings) { previews.push_back(settings); }, [this](std::optional settings) { answer = settings; })) { sheet->show(); if (!QTest::qWaitForWindowActive(sheet.get())) throw std::runtime_error("the sheet never became active"); } template Widget &find(const char *name) { Widget *found = sheet->findChild(QString::fromLatin1(name)); if (!found) throw std::runtime_error(std::string("no widget named ") + name); return *found; } // Typed, then Return: the field commits. void type(const char *name, const QString &text) { auto &field = find(name); field.setFocus(); field.selectAll(); QTest::keyClicks(&field, text); QTest::keyClick(&field, Qt::Key_Return); } QString note() { return find("gridNote").text(); } const GridSettingsSheet::Settings &last() const { return previews.back(); } }; } void GridSettingsSheetTests::theSheetLaysOutAsSwifts() { Sheet shown; auto *column = qobject_cast(shown.sheet->layout()); QVERIFY(column && shown.sheet->width() == 360 && column->spacing() == 18 && column->contentsMargins() == QMargins(24, 24, 24, 24)); auto *heading = qobject_cast(column->itemAt(0)->widget()); QVERIFY(heading->text() == "Grid" && heading->font().pixelSize() == 17 && heading->font().bold()); // Titles 110 wide, each naming its control. const auto rowOf = [column](int index) { return qobject_cast(column->itemAt(index)->layout()); }; const QStringList titles{"Color", "Style", "Opacity"}; for (int index = 0; index < 3; ++index) { auto *title = qobject_cast(rowOf(index + 1)->itemAt(0)->widget()); QVERIFY(title->text() == titles[index] && title->width() == 110 && title->buddy()); } auto &preset = shown.find("gridPreset"); QCOMPARE(preset.count(), 10); QVERIFY(preset.itemText(0) == "Light Gray" && preset.itemText(9) == "Custom" && preset.currentText() == "Light Gray"); auto &swatch = shown.find("gridColor"); QVERIFY(swatch.accessibleName() == "Grid Color" && swatch.toolTip() == "Choose a custom grid color" && rowOf(1)->itemAt(2)->widget() == &swatch); auto &style = shown.find("gridStyle"); QVERIFY(style.count() == 3 && style.itemText(1) == "Dashed Lines" && style.currentText() == "Lines"); auto &opacity = shown.find("gridOpacity"); QVERIFY(opacity.text() == "45" && opacity.width() == 48 && opacity.alignment() == Qt::AlignRight && opacity.accessibleName() == "Opacity"); QVERIFY(shown.find("gridOpacitySlider").minimum() == 1 && shown.find("gridOpacitySlider").maximum() == 100); QCOMPARE(shown.find("gridOpacitySlider").value(), 45); auto *rule = qobject_cast(column->itemAt(4)->widget()); QVERIFY(rule && rule->frameShape() == QFrame::HLine); QVERIFY(qobject_cast(rowOf(5)->itemAt(0)->widget())->text() == "Gridline every" && shown.find("gridSpacing").text() == "64"); QCOMPARE(qobject_cast(rowOf(5)->itemAt(2)->widget())->text(), QString("pixels")); QVERIFY(qobject_cast(rowOf(6)->itemAt(0)->widget())->text() == "Subdivisions" && shown.find("gridSubdivisions").text() == "8"); QCOMPARE(shown.note(), QString("A subdivision every 8 pixels.")); QCOMPARE(shown.find("gridNote").foregroundRole(), QPalette::PlaceholderText); auto *buttons = rowOf(8); QVERIFY(qobject_cast(buttons->itemAt(0)->widget())->text() == "Cancel" && qobject_cast(buttons->itemAt(1)->widget())->text() == "Restore Defaults"); QVERIFY(buttons->itemAt(2)->spacerItem() && shown.find("gridOK").isDefault()); QVERIFY(shown.previews.empty()); } void GridSettingsSheetTests::eachChangePreviewsTheGrid() { Sheet shown; auto &preset = shown.find("gridPreset"); preset.setCurrentIndex(7); emit preset.activated(7); QVERIFY(shown.previews.size() == 1 && shown.last().second.preset == GridAppearance::Preset::cyan && shown.last().first == LayoutGrid()); shown.find("gridStyle").setCurrentIndex(2); emit shown.find("gridStyle").activated(2); QCOMPARE(shown.last().second.style, GridAppearance::Style::dots); // Opacity: the slider, the field (clamped), its arrows. shown.find("gridOpacitySlider").setValue(80); QCOMPARE(shown.last().second.opacity, 80); shown.type("gridOpacity", "150"); QVERIFY(shown.last().second.opacity == 100 && shown.find("gridOpacity").text() == "100"); QTest::keyClick(&shown.find("gridOpacity"), Qt::Key_Down, Qt::ShiftModifier); QCOMPARE(shown.last().second.opacity, 90); // The grid previews only while valid; the note says why. shown.type("gridSpacing", "100"); QVERIFY(shown.last().first == LayoutGrid(100, 8)); QCOMPARE(shown.note(), QString("A subdivision every 12.5 pixels.")); shown.type("gridSubdivisions", "3"); QCOMPARE(shown.note(), QString("A subdivision every 33.33 pixels.")); const size_t seen = shown.previews.size(); shown.type("gridSpacing", "1"); QVERIFY(shown.previews.size() == seen && !shown.find("gridOK").isEnabled()); QCOMPARE(shown.note(), QString("Use gridlines every 2–4,096 pixels and 1–64 subdivisions, no more than the pixels between gridlines.")); QCOMPARE(shown.find("gridNote").foregroundRole(), QPalette::BrightText); shown.type("gridSpacing", "2"); QCOMPARE(shown.note().left(5), QString("Use g")); shown.type("gridSubdivisions", "2"); QVERIFY(shown.last().first == LayoutGrid(2, 2) && shown.find("gridOK").isEnabled()); // Text that is no number goes back. shown.type("gridSpacing", "lots"); QCOMPARE(shown.find("gridSpacing").text(), QString("2")); // The titles scrub: opacity a point per half. auto &title = *qobject_cast(qobject_cast(shown.sheet->layout()->itemAt(3)->layout())->itemAt(0)->widget()); QTest::mousePress(&title, Qt::LeftButton, Qt::NoModifier, QPoint(2, 2)); QMouseEvent move(QEvent::MouseMove, QPointF(12, 2), title.mapToGlobal(QPointF(12, 2)), Qt::NoButton, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(&title, &move); QTest::mouseRelease(&title, Qt::LeftButton, Qt::NoModifier, QPoint(12, 2)); QCOMPARE(shown.last().second.opacity, 95); } void GridSettingsSheetTests::aPickedColourBecomesCustom() { GridAppearance cyan; cyan.preset = GridAppearance::Preset::cyan; Sheet shown(LayoutGrid(), cyan); auto &swatch = shown.find("gridColor"); swatch.click(); QCOMPARE(shown.session.colorPicker().value().target.title(), QString("Color Picker (Grid Color)")); QCOMPARE(shown.session.colorPicker().value().original, (PaletteColor{0, 1, 1})); // Its opening colour keeps the preset; another makes Custom. shown.session.previewDialogColor(); QVERIFY(shown.previews.empty()); const PaletteColor teal{0, 128 / 255.0, 128 / 255.0}; shown.session.setColorPickerHSB(PickerHSB(teal)); QVERIFY(shown.last().second.preset == GridAppearance::Preset::custom && shown.last().second.customColor == teal); QCOMPARE(shown.find("gridPreset").currentText(), QString("Custom")); // Picking the first preset's colour again returns to it. shown.session.setColorPickerHSB(PickerHSB(PaletteColor{0, 1, 1})); QVERIFY(shown.last().second.preset == GridAppearance::Preset::cyan && shown.last().second.customColor == teal); shown.session.closeColorPicker(false); // Beyond Swift: Light Gray's 0.7, no byte, still stays. auto &preset = shown.find("gridPreset"); preset.setCurrentIndex(0); emit preset.activated(0); swatch.click(); shown.session.closeColorPicker(true); QCOMPARE(shown.last().second.preset, GridAppearance::Preset::lightGray); swatch.click(); shown.session.setColorPickerHSB(PickerHSB(teal)); shown.session.setColorPickerHSB(PickerHSB(PaletteColor{0.7, 0.7, 0.7})); QCOMPARE(shown.last().second.preset, GridAppearance::Preset::lightGray); shown.session.closeColorPicker(false); // The picker's Cancel hands back 0.7 itself: Light Gray stays. const size_t seen = shown.previews.size(); swatch.click(); shown.session.closeColorPicker(false); QCOMPARE(shown.previews.size(), seen); swatch.click(); shown.session.setColorPickerHSB(PickerHSB(teal)); shown.session.closeColorPicker(false); QCOMPARE(shown.last().second.preset, GridAppearance::Preset::lightGray); // A preset chosen from the menu ends a pick. swatch.click(); shown.session.setColorPickerHSB(PickerHSB(teal)); preset.setCurrentIndex(8); emit preset.activated(8); shown.session.setColorPickerHSB(PickerHSB(PaletteColor{0.7, 0.7, 0.7})); QVERIFY(shown.last().second.preset == GridAppearance::Preset::custom && shown.last().second.customColor == (PaletteColor{0.7, 0.7, 0.7}).quantized()); shown.session.closeColorPicker(false); // Restore Defaults ends a pick too. preset.setCurrentIndex(7); emit preset.activated(7); swatch.click(); shown.session.setColorPickerHSB(PickerHSB(teal)); shown.find("gridDefaults").click(); shown.session.setColorPickerHSB(PickerHSB(PaletteColor{0, 1, 1})); QVERIFY(shown.last().second.preset == GridAppearance::Preset::custom && shown.last().second.customColor == (PaletteColor{0, 1, 1})); // A changed preset repaints the swatch. shown.session.closeColorPicker(false); QCoreApplication::processEvents(); int paints = 0; struct Counter : QObject { int &count; explicit Counter(int &count) : count(count) {} bool eventFilter(QObject *, QEvent *event) override { count += event->type() == QEvent::Paint; return false; } } counter(paints); swatch.installEventFilter(&counter); preset.setCurrentIndex(5); emit preset.activated(5); QTRY_VERIFY(paints > 0); swatch.removeEventFilter(&counter); shown.sheet.reset(); QVERIFY(!shown.session.colorPicker()); } void GridSettingsSheetTests::restoreDefaultsOkAndCancel() { GridAppearance look{GridAppearance::Preset::custom, PaletteColor{0, 0.5, 0.5}, GridAppearance::Style::dots, 70}; Sheet shown(LayoutGrid(100, 4), look); QVERIFY(shown.find("gridPreset").currentText() == "Custom" && shown.find("gridSpacing").text() == "100"); QVERIFY(shown.find("gridStyle").currentText() == "Dots" && shown.find("gridOpacity").text() == "70"); shown.find("gridDefaults").click(); const GridSettingsSheet::Settings restored = shown.last(); QVERIFY(restored.first == LayoutGrid() && restored.second.preset == GridAppearance::Preset::lightGray && restored.second.style == GridAppearance::Style::lines); QVERIFY(restored.second.opacity == 45 && restored.second.customColor == look.customColor); QVERIFY(shown.find("gridSpacing").text() == "64" && shown.find("gridStyle").currentText() == "Lines"); // OK closes an open picker and answers what is shown. shown.find("gridColor").click(); shown.find("gridOK").click(); QVERIFY(!shown.session.colorPicker() && shown.answer.value().value() == restored); Sheet cancelled; cancelled.find("gridColor").click(); cancelled.find("gridCancel").click(); QVERIFY(!cancelled.session.colorPicker() && !cancelled.answer.value()); } void GridSettingsSheetTests::theRowsHoldSwiftsControls() { Sheet shown; auto *column = qobject_cast(shown.sheet->layout()); const auto rowOf = [column](int index) { return qobject_cast(column->itemAt(index)->layout()); }; for (const int index : {1, 2, 3, 5, 6}) QCOMPARE(rowOf(index)->spacing(), 8); // Colour and Style: title, menu (and swatch), then room. QVERIFY(rowOf(1)->count() == 4 && rowOf(1)->itemAt(3)->spacerItem()); QVERIFY(rowOf(2)->count() == 3 && rowOf(2)->itemAt(1)->widget() == &shown.find("gridStyle") && rowOf(2)->itemAt(2)->spacerItem()); // Opacity: the slider stretches; % sits two after. QVERIFY(rowOf(3)->itemAt(1)->widget() == &shown.find("gridOpacitySlider") && rowOf(3)->stretch(1) == 1); auto *suffixed = qobject_cast(rowOf(3)->itemAt(2)->layout()); QVERIFY(suffixed && suffixed->spacing() == 2 && suffixed->itemAt(0)->widget() == &shown.find("gridOpacity")); QCOMPARE(qobject_cast(suffixed->itemAt(1)->widget())->text(), QString("%")); QCOMPARE(qobject_cast(column->itemAt(4)->widget())->foregroundRole(), QPalette::Mid); // The fields stretch, named by their titles. auto &spacing = shown.find("gridSpacing"); auto &subdivisions = shown.find("gridSubdivisions"); QVERIFY(qobject_cast(rowOf(5)->itemAt(0)->widget())->buddy() == &spacing && rowOf(5)->stretch(1) == 1); QVERIFY(qobject_cast(rowOf(6)->itemAt(0)->widget())->buddy() == &subdivisions && rowOf(6)->stretch(1) == 1); QCOMPARE(qobject_cast(rowOf(5)->itemAt(2)->widget())->foregroundRole(), QPalette::PlaceholderText); QVERIFY(spacing.placeholderText() == "Gridline every" && subdivisions.placeholderText() == "Subdivisions" && shown.find("gridOpacity").placeholderText() == "Opacity"); auto ¬e = shown.find("gridNote"); QVERIFY(note.wordWrap() && note.font().pixelSize() == 12); QCOMPARE(rowOf(8)->itemAt(3)->widget(), &shown.find("gridOK")); // In the controller's dialog only OK takes Return. QDialog dialog; shown.sheet->setParent(&dialog); QVERIFY(!shown.find("gridCancel").autoDefault() && !shown.find("gridDefaults").autoDefault()); QVERIFY(shown.find("gridOK").autoDefault()); shown.sheet->setParent(nullptr); } void GridSettingsSheetTests::aSliderOrScrubReplacesTyping() { Sheet shown; auto &opacity = shown.find("gridOpacity"); opacity.setFocus(); opacity.selectAll(); QTest::keyClicks(&opacity, QStringLiteral("77")); shown.find("gridOpacitySlider").setValue(60); QCOMPARE(opacity.text(), QString("60")); // Each title scrubs, and the note follows. auto *column = qobject_cast(shown.sheet->layout()); const auto titleOf = [column](int index) { return qobject_cast(qobject_cast(column->itemAt(index)->layout())->itemAt(0)->widget()); }; auto &spacing = shown.find("gridSpacing"); spacing.setFocus(); spacing.selectAll(); QTest::keyClicks(&spacing, QStringLiteral("99")); scrub(*titleOf(5), 10); QVERIFY(spacing.text() == "74" && shown.last().first == LayoutGrid(74, 8)); QCOMPARE(shown.note(), QString("A subdivision every 9.25 pixels.")); auto &subdivisions = shown.find("gridSubdivisions"); subdivisions.setFocus(); subdivisions.selectAll(); QTest::keyClicks(&subdivisions, QStringLiteral("33")); scrub(*titleOf(6), 10); QVERIFY(subdivisions.text() == "10" && shown.last().first == LayoutGrid(74, 10)); opacity.setFocus(); opacity.selectAll(); QTest::keyClicks(&opacity, QStringLiteral("33")); scrub(*titleOf(3), 10); QVERIFY(opacity.text() == "65" && shown.last().second.opacity == 65); } QTEST_MAIN(GridSettingsSheetTests) #include "GridSettingsSheetTests.moc"