#include "Document/BrushStroke.h" #include "Document/EditorSession.h" #include "UI/ColorPaletteControls.h" #include "UI/FilterSheet.h" #include #include #include #include #include #include #include #include // Swift 1.3.3's Dither: the kind, its colours, its sheet. namespace { struct Shown { EditorSession session; QImage image; std::unique_ptr sheet; explicit Shown(int width = 40, int height = 30) { session.createDocument(width, height); image = BrushRaster::context(width, height, false); for (int y = 0; y < height; ++y) for (int x = 0; x < width; ++x) image.setPixelColor(x, y, QColor(x * 255 / std::max(1, width - 1), 120, 200)); session.insert(ImportedImage(image, image, QStringLiteral("Colour"))); session.beginFilter(FilterKind::dither); sheet = std::make_unique(session); sheet->show(); } const DitherSettings &dither() const { return session.filterEdit().value().settings.dither; } void set(const std::function &change) { FilterSettings settings = session.filterEdit().value().settings; change(settings.dither); session.updateFilter(settings, true); } template Widget &child(const char *name) const { auto *found = sheet->findChild(QString::fromLatin1(name)); if (!found) throw std::runtime_error(name); return *found; } // The rows shown, by their titles, sorted. QStringList shown() const { QStringList titles; for (const QLabel *label : sheet->findChildren()) { if (label->buddy() && !label->parentWidget()->isHidden() && label->isVisibleTo(sheet.get())) titles << label->text(); } for (const QCheckBox *box : sheet->findChildren()) { if (box->isVisibleTo(sheet.get()) && box->text() != QLatin1String("Preview")) titles << box->text(); } if (child("ditherDark").isVisibleTo(sheet.get())) titles << QStringLiteral("Swatches"); titles.sort(); return titles; } }; QStringList sorted(QStringList titles) { titles.sort(); return titles; } void pick(EditorSession &session, const PaletteColor &color) { PickerHSB hsb = session.colorPicker().value().hsb; hsb.setRGB(color); session.setColorPickerHSB(hsb); } } class DitherSheetTests : public QObject { Q_OBJECT private slots: void theKindSitsUnderBloomAndPreviewsWhole(); void itsCommitIsTheSettingsApplied(); void theTwoColoursHaveTheirPicker(); void theControlsFollowTheStyle(); void theMenusAndFieldsWriteTheSettings(); void theSheetPreviewsThePickersColour(); void eachNumberWritesItsOwnSetting(); void leftTheCharactersShowWhatWasKept(); void theSlidersReachTheirEnds(); void aCommitSaysItIsApplying(); void thePickerRefusesWhenItCannot(); void anAsciiCommitDrawsOnTheWorker(); void rowsKeepSwiftsOrderHelpAndScale(); }; void DitherSheetTests::theKindSitsUnderBloomAndPreviewsWhole() { QCOMPARE(rawValue(FilterKind::dither), QString("Dither")); const auto at = std::find(allFilterKinds.begin(), allFilterKinds.end(), FilterKind::dither); QVERIFY(at != allFilterKinds.end() && *(at - 1) == FilterKind::bloomGlow && *(at + 1) == FilterKind::tonalContrast); QVERIFY(!isAutomatic(FilterKind::dither) && !isImageAdjustment(FilterKind::dither)); // Past the preview limit, Dither still previews at full size. EditorSession session; session.createDocument(3000, 10); const QImage wide = BrushRaster::context(3000, 10, false); session.insert(ImportedImage(wide, wide, QStringLiteral("Wide"))); session.beginFilter(FilterKind::dither); QCOMPARE(session.filterEdit().value().previewScale, 1.0); QCOMPARE(session.filterEdit().value().previewSource.size(), QSize(3000, 10)); session.cancelFilter(); session.beginFilter(FilterKind::gaussianBlur); QVERIFY(session.filterEdit().value().previewScale < 1); } void DitherSheetTests::itsCommitIsTheSettingsApplied() { Shown shown; shown.set([](DitherSettings &dither) { dither.style = DitherStyle::bayer4; dither.pixelSize = 3; }); const DitherSettings settings = shown.dither(); bool done = false; shown.session.commitFilter([&done] { done = true; }); QTRY_VERIFY(done); QCOMPARE(shown.session.history.undoName(), QString("Dither")); QCOMPARE(shown.session.activeLayer().value().asset.value().image(), settings.apply(shown.image)); } void DitherSheetTests::theTwoColoursHaveTheirPicker() { Shown shown; shown.session.openDitherColorPicker(false); QCOMPARE(shown.session.colorPicker().value().target.title(), QString("Color Picker (Dither Dark Color)")); QVERIFY(shown.session.colorPicker().value().original == PaletteColor::black()); pick(shown.session, PaletteColor{0, 0, 1}); shown.session.previewDitherColor(); QVERIFY(shown.dither().dark == AdjustmentColor(0, 0, 1)); // Cancel puts it back; OK on the light keeps it. shown.session.closeColorPicker(false); QVERIFY(shown.dither().dark == AdjustmentColor(0, 0, 0)); shown.session.openDitherColorPicker(true); QCOMPARE(shown.session.colorPicker().value().target.title(), QString("Color Picker (Dither Light Color)")); pick(shown.session, PaletteColor{1, 1, 0}); shown.session.closeColorPicker(true); QVERIFY(shown.dither().light == AdjustmentColor(1, 1, 0) && shown.dither().dark == AdjustmentColor(0, 0, 0)); // The swatches open it; the panel's close takes it along. shown.set([](DitherSettings &dither) { dither.colors = DitherColors::twoColors; }); QTest::mouseClick(&shown.child("ditherDark"), Qt::LeftButton); QVERIFY(shown.session.colorPicker().has_value() && !shown.session.colorPicker().value().target.light); pick(shown.session, PaletteColor{1, 0, 0}); shown.session.previewDitherColor(); shown.session.cancelFilter(); QVERIFY(!shown.session.colorPicker() && !shown.session.filterEdit()); // Not over another filter. shown.session.beginFilter(FilterKind::gaussianBlur); shown.session.openDitherColorPicker(true); QVERIFY(!shown.session.colorPicker()); } void DitherSheetTests::theControlsFollowTheStyle() { Shown shown; const QStringList common{"Style", "Pixel Size", "Density", "Contrast", "Colors", "Pixel Shape"}; QCOMPARE(shown.shown(), sorted(common + QStringList{"Tones", "Diffusion"})); shown.set([](DitherSettings &dither) { dither.style = DitherStyle::bayer8; }); QCOMPARE(shown.shown(), sorted(common + QStringList{"Tones"})); shown.set([](DitherSettings &dither) { dither.style = DitherStyle::lines; }); QCOMPARE(shown.shown(), sorted(common + QStringList{"Cell Size", "Angle", "Light on Dark"})); // ASCII: Text Size, never chunky pixels or cells. shown.set([](DitherSettings &dither) { dither.style = DitherStyle::ascii; }); QCOMPARE(shown.shown(), sorted(QStringList{"Style", "Text Size", "Characters", "Density", "Contrast", "Colors", "Light on Dark"})); shown.set([](DitherSettings &dither) { dither.style = DitherStyle::patterns; }); QCOMPARE(shown.shown(), sorted(common + QStringList{"Light on Dark"})); // Pixel Shape needs chunky pixels; the swatches, Two Colors. shown.set([](DitherSettings &dither) { dither.pixelSize = 1; dither.colors = DitherColors::twoColors; }); QCOMPARE(shown.shown(), sorted(QStringList{"Style", "Pixel Size", "Density", "Contrast", "Colors", "Light on Dark", "Swatches"})); // Style's menu: ten looks in four groups. QComboBox &style = shown.child("ditherStyle"); QStringList items; for (int index = 0; index < style.count(); ++index) items << (style.itemText(index).isEmpty() ? QStringLiteral("—") : style.itemText(index)); QCOMPARE(items, (QStringList{"Atkinson (Classic Mac)", "Floyd–Steinberg", "—", "Bayer 2 × 2", "Bayer 4 × 4", "Bayer 8 × 8", "—", "Halftone Dots", "Halftone Lines", "Halftone Diamonds", "—", "Mac Patterns", "ASCII"})); QCOMPARE(style.currentText(), QString("Mac Patterns")); // Its quick preview never shows the busy line. QVERIFY(!shown.child("filterSpinner").isVisibleTo(shown.sheet.get())); } void DitherSheetTests::theMenusAndFieldsWriteTheSettings() { Shown shown; QComboBox &style = shown.child("ditherStyle"); const int ascii = style.findData(int(DitherStyle::ascii)); style.setCurrentIndex(ascii); emit style.activated(ascii); QVERIFY(shown.dither().style == DitherStyle::ascii); QComboBox &colors = shown.child("ditherColors"); colors.setCurrentIndex(2); emit colors.activated(2); QVERIFY(shown.dither().colors == DitherColors::original); QComboBox &shape = shown.child("ditherPixelShape"); shape.setCurrentIndex(1); emit shape.activated(1); QVERIFY(shown.dither().pixelShape == DitherPixelShape::dot); // Characters as typed, stored without line breaks. QLineEdit &characters = shown.child("ditherCharacters"); QCOMPARE(characters.text(), DitherSettings::defaultCharacters()); characters.setFocus(); characters.selectAll(); QTest::keyClicks(&characters, QStringLiteral("#o.")); QCOMPARE(shown.dither().characters, QString("#o.")); // Unfocused, it shows the settings as they stand. characters.clearFocus(); shown.set([](DitherSettings &dither) { dither.characters = QStringLiteral("xy"); }); QCOMPARE(characters.text(), QString("xy")); } void DitherSheetTests::theSheetPreviewsThePickersColour() { Shown shown; shown.set([](DitherSettings &dither) { dither.colors = DitherColors::twoColors; }); for (const bool light : {false, true}) { QTest::mouseClick(&shown.child(light ? "ditherLight" : "ditherDark"), Qt::LeftButton); QVERIFY(shown.session.colorPicker().has_value() && shown.session.colorPicker().value().target.light == light); // The sheet previews the working colour itself. pick(shown.session, PaletteColor{0, 1, 0}); QVERIFY((light ? shown.dither().light : shown.dither().dark) == AdjustmentColor(0, 1, 0)); shown.session.closeColorPicker(false); QVERIFY(shown.dither().dark == AdjustmentColor(0, 0, 0) && shown.dither().light == AdjustmentColor(1, 1, 1)); } // The filter's OK takes an open picker's colour along. QTest::mouseClick(&shown.child("ditherLight"), Qt::LeftButton); pick(shown.session, PaletteColor{1, 0, 0}); DitherSettings expected = shown.dither(); expected.light = AdjustmentColor(1, 0, 0); bool done = false; shown.session.commitFilter([&done] { done = true; }); QTRY_VERIFY(done); QVERIFY(!shown.session.colorPicker()); QCOMPARE(shown.session.activeLayer().value().asset.value().image(), expected.apply(shown.image)); } void DitherSheetTests::eachNumberWritesItsOwnSetting() { Shown shown; shown.set([](DitherSettings &dither) { dither.style = DitherStyle::lines; }); struct Row { const char *field; double DitherSettings::*setting; const char *typed; double expected; }; const Row rows[] = {{"pixelSizeField", &DitherSettings::pixelSize, "5", 5}, {"cellSizeField", &DitherSettings::cellSize, "12", 12}, {"angleField", &DitherSettings::angle, "-30", -30}, {"densityField", &DitherSettings::density, "25", 25}, {"contrastField", &DitherSettings::contrast, "-40", -40}}; for (const Row &row : rows) { const DitherSettings before = shown.dither(); QLineEdit &field = shown.child(row.field); field.setFocus(); field.selectAll(); QTest::keyClicks(&field, QString::fromLatin1(row.typed)); QTest::keyClick(&field, Qt::Key_Return); DitherSettings expected = before; expected.*row.setting = row.expected; QVERIFY2(shown.dither() == expected, row.field); } // Tones and Diffusion under a diffusion style. shown.set([](DitherSettings &dither) { dither.style = DitherStyle::floydSteinberg; }); for (const auto &[name, setting, value] : {std::tuple("tonesField", &DitherSettings::levels, 4.0), std::tuple("diffusionField", &DitherSettings::diffusion, 60.0)}) { QLineEdit &field = shown.child(name); field.setFocus(); field.selectAll(); QTest::keyClicks(&field, QString::number(value)); QTest::keyClick(&field, Qt::Key_Return); QCOMPARE(shown.dither().*setting, value); } // Light on Dark, under marks. shown.set([](DitherSettings &dither) { dither.style = DitherStyle::dots; }); QCheckBox *glow = nullptr; for (QCheckBox *box : shown.sheet->findChildren()) glow = box->text() == QLatin1String("Light on Dark") ? box : glow; QVERIFY(glow && glow->isChecked()); glow->click(); QVERIFY(!shown.dither().lightOnDark); // Text Size, under ASCII: whole pixels, its unit beside. shown.set([](DitherSettings &dither) { dither.style = DitherStyle::ascii; }); QLineEdit &text = shown.child("textSizeField"); text.setFocus(); text.selectAll(); QTest::keyClicks(&text, QStringLiteral("20.4")); QTest::keyClick(&text, Qt::Key_Return); QCOMPARE(shown.dither().textSize, 20.0); QCOMPARE(text.text(), QStringLiteral("20")); QStringList units; for (QLabel *label : text.parentWidget()->findChildren()) units << label->text(); QVERIFY2(units.contains(QStringLiteral("px")), qPrintable(units.join(u','))); } void DitherSheetTests::leftTheCharactersShowWhatWasKept() { Shown shown; QVERIFY(QTest::qWaitForWindowActive(shown.sheet.get())); shown.session.updateFilter(shown.session.filterEdit().value().settings, false); shown.set([](DitherSettings &dither) { dither.style = DitherStyle::ascii; }); shown.session.updateFilter(shown.session.filterEdit().value().settings, false); QLineEdit &characters = shown.child("ditherCharacters"); characters.setFocus(); QTRY_VERIFY(characters.hasFocus()); characters.selectAll(); QTest::keyClicks(&characters, QString(70, u'x')); // Kept to 64; the typing shows until left. QCOMPARE(shown.dither().characters, QString(64, u'x')); QCOMPARE(characters.text(), QString(70, u'x')); shown.child("densityField").setFocus(); QTRY_VERIFY(!characters.hasFocus()); QCOMPARE(characters.text(), QString(64, u'x')); QVERIFY(!shown.session.filterEdit().value().preview); } void DitherSheetTests::theSlidersReachTheirEnds() { Shown shown; shown.set([](DitherSettings &dither) { dither.style = DitherStyle::lines; }); const std::tuple rows[] = { {"pixelSizeSlider", &DitherSettings::pixelSize, 1, 32}, {"cellSizeSlider", &DitherSettings::cellSize, 4, 64}, {"angleSlider", &DitherSettings::angle, -90, 90}, {"densitySlider", &DitherSettings::density, -100, 100}, {"contrastSlider", &DitherSettings::contrast, -100, 100}}; for (const auto &[name, setting, low, high] : rows) { QSlider &slider = shown.child(name); slider.setValue(slider.maximum()); QCOMPARE(shown.dither().*setting, high); slider.setValue(slider.minimum()); QCOMPARE(shown.dither().*setting, low); } QCOMPARE(shown.child("pixelSizeSlider").parentWidget()->toolTip(), QString("Make each dithered pixel this many pixels across, for a chunky old-screen look")); shown.set([](DitherSettings &dither) { dither.style = DitherStyle::ascii; }); QSlider &text = shown.child("textSizeSlider"); text.setValue(text.maximum()); QCOMPARE(shown.dither().textSize, 64.0); text.setValue(text.minimum()); QCOMPARE(shown.dither().textSize, 6.0); // Quarter travel: 20.5, so 21; step 25: 7.45, so 7. text.setValue(text.maximum() / 4); QCOMPARE(shown.dither().textSize, 21.0); text.setValue(25); QCOMPARE(shown.dither().textSize, 7.0); shown.set([](DitherSettings &dither) { dither.style = DitherStyle::atkinson; }); for (const auto &[name, setting, low, high] : {std::tuple("tonesSlider", &DitherSettings::levels, 2.0, 8.0), std::tuple("diffusionSlider", &DitherSettings::diffusion, 0.0, 100.0)}) { QSlider &slider = shown.child(name); slider.setValue(slider.maximum()); QCOMPARE(shown.dither().*setting, high); slider.setValue(slider.minimum()); QCOMPARE(shown.dither().*setting, low); } } void DitherSheetTests::aCommitSaysItIsApplying() { // Quick previews never show the line; a commit does. Shown shown; bool done = false; shown.session.commitFilter([&done] { done = true; }); QVERIFY(shown.child("filterSpinner").isVisibleTo(shown.sheet.get())); QCOMPARE(shown.child("filterActivity").text(), QString("Applying…")); QTRY_VERIFY(done); } void DitherSheetTests::thePickerRefusesWhenItCannot() { Shown shown; // Busy, the palette rests. shown.session.setIsProjectBusy(true); shown.session.openDitherColorPicker(false); QVERIFY(!shown.session.colorPicker()); shown.session.setIsProjectBusy(false); // Another picker open stays so. shown.session.openDitherColorPicker(true); shown.session.openDitherColorPicker(false); QVERIFY(shown.session.colorPicker().value().target.light); shown.session.closeColorPicker(false); // A style change keeps an open picker on its colour. shown.session.openDitherColorPicker(false); shown.set([](DitherSettings &dither) { dither.style = DitherStyle::ascii; }); pick(shown.session, PaletteColor{1, 0, 1}); shown.session.previewDitherColor(); QVERIFY(shown.dither().dark == AdjustmentColor(1, 0, 1) && shown.dither().style == DitherStyle::ascii); shown.session.closeColorPicker(false); QVERIFY(shown.dither().dark == AdjustmentColor(0, 0, 0)); } void DitherSheetTests::anAsciiCommitDrawsOnTheWorker() { // Glyphs are drawn off the UI thread, in the commit. Shown shown(96, 48); shown.set([](DitherSettings &dither) { dither.style = DitherStyle::ascii; dither.cellSize = 12; dither.pixelSize = 1; }); const DitherSettings settings = shown.dither(); bool done = false; shown.session.commitFilter([&done] { done = true; }); QTRY_VERIFY(done); QCOMPARE(shown.session.activeLayer().value().asset.value().image(), settings.apply(shown.image)); } void DitherSheetTests::rowsKeepSwiftsOrderHelpAndScale() { Shown shown; // The shown rows, in the column's order, with their help. const auto rows = [&shown] { QStringList titles; QLayout *column = shown.sheet->layout(); for (int index = 0; index < column->count(); ++index) { QWidget *row = column->itemAt(index)->widget(); if (!row || row->isHidden()) continue; if (auto *box = qobject_cast(row); box && box->text() != QLatin1String("Preview")) titles << box->text() + u'|' + box->toolTip(); for (QLabel *label : row->findChildren(QString(), Qt::FindDirectChildrenOnly)) { if (label->buddy()) titles << label->text() + u'|' + row->toolTip(); } } return titles; }; QCOMPARE(rows(), (QStringList{"Style|", "Pixel Size|Make each dithered pixel this many pixels across, for a chunky old-screen look", "Tones|Tones per channel: 2 is pure black and white", "Diffusion|How much of each pixel's error spreads to its neighbors. Less gives flatter areas", "Density|More ink (darker) or less before dithering", "Contrast|", "Colors|", "Pixel Shape|Draw each chunky pixel as a solid square, or as a round dot like a dot-matrix screen"})); shown.set([](DitherSettings &dither) { dither.style = DitherStyle::ascii; }); QCOMPARE(rows().mid(1, 2), (QStringList{"Text Size|The height of each line of characters", "Characters|The characters to draw with, in any order: each spot gets the one whose ink best matches its tone"})); // Text Size, 6 to 64: half its travel is 35. QSlider &text = shown.child("textSizeSlider"); text.setValue(text.maximum() / 2); QCOMPARE(shown.dither().textSize, 35.0); QCOMPARE(rows().last(), QString("Light on Dark|Draw the marks for the light tones on the dark color, like a glowing screen")); // Linear: half the Pixel Size travel is 16.5, so 17. QSlider &pixels = shown.child("pixelSizeSlider"); pixels.setValue(pixels.maximum() / 2); QCOMPARE(shown.dither().pixelSize, 17.0); // Across Colors a picker keeps on; without an edit, none. shown.set([](DitherSettings &dither) { dither.colors = DitherColors::twoColors; }); shown.session.updateFilter(shown.session.filterEdit().value().settings, false); QTest::mouseClick(&shown.child("ditherDark"), Qt::LeftButton); FilterSettings original = shown.session.filterEdit().value().settings; original.dither.colors = DitherColors::original; shown.session.updateFilter(original, false); pick(shown.session, PaletteColor{0, 1, 1}); QVERIFY(shown.dither().dark == AdjustmentColor(0, 1, 1) && shown.dither().colors == DitherColors::original); QVERIFY(!shown.session.filterEdit().value().preview); shown.session.closeColorPicker(false); QVERIFY(shown.dither().dark == AdjustmentColor(0, 0, 0)); shown.session.cancelFilter(); shown.session.openDitherColorPicker(false); QVERIFY(!shown.session.colorPicker()); } QTEST_MAIN(DitherSheetTests) #include "DitherSheetTests.moc"