#include "Document/BrushStroke.h" #include "Document/EditorSession.h" #include "UI/CameraRawColorControls.h" #include "UI/LayerIcons.h" #include #include #include #include #include #include #include #include // Swift's Curve and Color Mixer groups. namespace { // The knob's middle, where a double click resets. QPoint knob(const CameraRawSlider &slider) { QStyleOptionSlider option; option.initFrom(&slider); option.orientation = Qt::Horizontal; option.minimum = slider.minimum(); option.maximum = slider.maximum(); option.sliderPosition = slider.sliderPosition(); option.sliderValue = slider.value(); return slider.style()->subControlRect(QStyle::CC_Slider, &option, QStyle::SC_SliderHandle, &slider).center(); } template struct Group { EditorSession session; std::unique_ptr controls; Group() { session.createDocument(8, 8); QImage image = BrushRaster::context(8, 8, false); image.fill(QColor(200, 120, 60)); session.insert(ImportedImage(image, image, QStringLiteral("Warm"))); session.beginFilter(FilterKind::cameraRaw); controls = std::make_unique(session); controls->resize(374, 600); controls->show(); } template Widget &child(const QString &name) const { auto *found = controls->template findChild(name); if (!found) throw std::runtime_error(name.toStdString()); return *found; } const CameraRawSettings &raw() const { return session.filterEdit().value().settings.cameraRaw; } const CameraRawPanel &panel() const { return session.filterEdit().value().rawPanel; } void set(const std::function &change) { FilterSettings settings = session.filterEdit().value().settings; change(settings.cameraRaw); session.updateFilter(settings, true); } }; void press(QWidget &widget, QPoint at) { QTest::mousePress(&widget, Qt::LeftButton, {}, at); } void drag(QWidget &widget, QPoint to) { QMouseEvent move(QEvent::MouseMove, to, widget.mapToGlobal(to), Qt::NoButton, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(&widget, &move); } } class CameraRawCurveMixerTests : public QObject { Q_OBJECT private slots: void theParametricPageMovesRegionsAndDividers(); void thePointPageAddsMovesAndRemovesPoints(); void presetsAndRefineSaturation(); void targetedAdjustmentArmsOneAtATime(); void hslRowsWriteTheirTabsFamilies(); void theColorPageEditsTheChosenFamily(); void pointColorsArePickedChosenAndShifted(); }; void CameraRawCurveMixerTests::theParametricPageMovesRegionsAndDividers() { Group group; QVERIFY(group.child(QStringLiteral("curvePage0")).isChecked() && !group.child(QStringLiteral("curveSelectedPoint")).isVisible()); for (const auto &[name, member] : {std::pair("curveHighlights", &CameraRawCurveSettings::highlights), std::pair("curveLights", &CameraRawCurveSettings::lights), std::pair("curveDarks", &CameraRawCurveSettings::darks), std::pair("curveShadows", &CameraRawCurveSettings::shadows)}) { auto &slider = group.child(QString::fromLatin1(name) + QStringLiteral("Slider")); slider.setValue(800); QCOMPARE(group.raw().curve.*member, 60.0); // A double click on the knob resets it. QTest::mouseDClick(&slider, Qt::LeftButton, {}, knob(slider)); QCOMPARE(group.raw().curve.*member, 0.0); } // A drag moves the nearest divider, within 2 and 98. auto &graph = group.child(QStringLiteral("cameraRawCurveGraph")); press(graph, QPoint(int(graph.width() * 0.3), 75)); drag(graph, QPoint(int(graph.width() * 0.35), 75)); QVERIFY(std::abs(group.raw().curve.shadowSplit - 35) < 1 && group.raw().curve.darkSplit == 50); drag(graph, QPoint(graph.width() + 40, 75)); QVERIFY(group.raw().curve.lightSplit == 98 || group.raw().curve.shadowSplit == 98 || group.raw().curve.darkSplit == 98); QTest::mouseRelease(&graph, Qt::LeftButton, {}, QPoint(graph.width() + 40, 75)); // A double click here leaves the point curve's points. group.set([](CameraRawSettings &settings) { settings.curve.rgb = {{0, 0}, {0.5, 0.5}, {1, 1}}; }); QTest::mouseDClick(&graph, Qt::LeftButton, {}, QPoint(graph.width() / 2, graph.height() / 2)); QCOMPARE(group.raw().curve.rgb.size(), size_t(3)); } void CameraRawCurveMixerTests::thePointPageAddsMovesAndRemovesPoints() { Group group; group.child(QStringLiteral("curvePage1")).click(); QVERIFY(group.panel().curvePage == CameraRawCurvePage::point); group.child(QStringLiteral("pointChannel1")).click(); QVERIFY(group.panel().pointChannel == CameraRawPointChannel::red); auto &graph = group.child(QStringLiteral("cameraRawCurveGraph")); const int w = graph.width(), h = graph.height(); // A still click adds a point where it lands. QTest::mouseClick(&graph, Qt::LeftButton, {}, QPoint(w / 2, h / 4)); QCOMPARE(group.raw().curve.red.size(), size_t(3)); QVERIFY(std::abs(group.raw().curve.red[1].x - 0.5) < 0.01 && std::abs(group.raw().curve.red[1].y - 0.75) < 0.01); QVERIFY(group.raw().curve.rgb == CameraRawCurveSettings::linear()); QCOMPARE(group.child(QStringLiteral("curveSelectedPoint")).text(), QString("In %1 Out %2").arg(std::lround(group.raw().curve.red[1].x * 255)) .arg(std::lround(group.raw().curve.red[1].y * 255))); // A drag moves the nearest inner point. press(graph, QPoint(w / 2, h / 4)); drag(graph, QPoint(int(w * 0.6), h / 2)); QTest::mouseRelease(&graph, Qt::LeftButton, {}, QPoint(int(w * 0.6), h / 2)); QCOMPARE(group.raw().curve.red.size(), size_t(3)); QVERIFY(std::abs(group.raw().curve.red[1].x - 0.6) < 0.01 && std::abs(group.raw().curve.red[1].y - 0.5) < 0.01); // A double click far off removes nothing; near, it does. QTest::mouseDClick(&graph, Qt::LeftButton, {}, QPoint(int(w * 0.1), h / 2)); QCOMPARE(group.raw().curve.red.size(), size_t(3)); const size_t before = group.raw().curve.red.size(); QTest::mouseDClick(&graph, Qt::LeftButton, {}, QPoint(int(w * 0.6), h / 2)); QCOMPARE(group.raw().curve.red.size(), before - 1); } void CameraRawCurveMixerTests::presetsAndRefineSaturation() { Group group; group.child(QStringLiteral("curvePage1")).click(); auto &preset = group.child(QStringLiteral("curvePreset")); QCOMPARE(preset.currentText(), QString("Linear")); emit preset.activated(3); QVERIFY(group.raw().curve.rgb == CameraRawCurveSettings::strongContrast()); QCOMPARE(preset.currentText(), QString("Strong Contrast")); emit preset.activated(2); QVERIFY(group.raw().curve.rgb == CameraRawCurveSettings::mediumContrast()); emit preset.activated(1); QVERIFY(group.raw().curve.rgb == CameraRawCurveSettings::linear()); group.set([](CameraRawSettings &settings) { settings.curve.rgb = {{0, 0}, {0.4, 0.3}, {1, 1}}; }); QCOMPARE(preset.currentText(), QString("Custom")); // Custom changes nothing; Refine Saturation shows for RGB alone. emit preset.activated(0); QCOMPARE(group.raw().curve.rgb.size(), size_t(3)); auto &refine = group.child(QStringLiteral("refineSaturationSlider")); QVERIFY(refine.isVisible()); refine.setValue(750); QCOMPARE(group.raw().curve.refineSaturation, 50.0); group.child(QStringLiteral("pointChannel2")).click(); QVERIFY(!refine.isVisible()); } void CameraRawCurveMixerTests::targetedAdjustmentArmsOneAtATime() { Group curve; CameraRawMixerControls mixer(curve.session); auto &curveTarget = curve.child(QStringLiteral("curveTargeted")); auto *mixerTarget = mixer.findChild(QStringLiteral("mixerTargeted")); curveTarget.click(); QVERIFY(curve.panel().targetsCurve && curveTarget.isChecked()); mixerTarget->click(); QVERIFY(curve.panel().targetsMixer && !curve.panel().targetsCurve && !curveTarget.isChecked() && mixerTarget->isChecked()); mixerTarget->click(); QVERIFY(!curve.panel().targetsMixer); mixerTarget->click(); curveTarget.click(); QVERIFY(curve.panel().targetsCurve && !curve.panel().targetsMixer); // Both carry Swift's scope glyph. const qreal ratio = curveTarget.devicePixelRatioF(); const QImage scope = LayerIcons::pixmap(LayerIcon::scope, 14, curveTarget.palette().color(QPalette::ButtonText), ratio).toImage(); QVERIFY(curveTarget.icon().pixmap(QSize(14, 14), ratio).toImage() == scope && mixerTarget->icon().pixmap(QSize(14, 14), ratio).toImage() == scope); } void CameraRawCurveMixerTests::hslRowsWriteTheirTabsFamilies() { Group group; QVERIFY(group.child(QStringLiteral("mixerPage0")).isChecked()); for (int tab = 0; tab < 3; ++tab) { group.child(QStringLiteral("mixerTab%1").arg(tab)).click(); for (int index = 0; index < 8; ++index) { const QString name = CameraRawMixerSettings::names.at(size_t(index)); auto &slider = group.child(QStringLiteral("family") + name + QStringLiteral("Slider")); QCOMPARE(slider.toolTip(), QStringLiteral("%1 of %2.").arg(tab == 0 ? "Hue" : tab == 1 ? "Saturation" : "Luminance", name)); slider.setValue(600 + index * 10); const std::array &family = tab == 0 ? group.raw().mixer.hue : tab == 1 ? group.raw().mixer.saturation : group.raw().mixer.luminance; QVERIFY(std::abs(family.at(size_t(index)) - (20 + index * 2)) < 1e-9); } } // Each tab's rows show its own track: saturation starts gray. const QImage drawn = group.child(QStringLiteral("familyGreensSlider")).grab().toImage(); group.child(QStringLiteral("mixerTab1")).click(); const QImage gray = group.child(QStringLiteral("familyGreensSlider")).grab().toImage(); QVERIFY(drawn != gray); const QColor start = gray.pixelColor(10, gray.height() / 2); QVERIFY(std::abs(start.red() - start.green()) < 12); } void CameraRawCurveMixerTests::theColorPageEditsTheChosenFamily() { Group group; group.child(QStringLiteral("mixerPage1")).click(); QVERIFY(group.panel().mixerPage == CameraRawMixerPage::color); group.child(QStringLiteral("mixerSwatch5")).click(); QCOMPARE(group.panel().mixerSwatch, 5); group.child(QStringLiteral("colorLuminanceSlider")).setValue(250); QCOMPARE(group.raw().mixer.luminance[5], -50.0); QCOMPARE(group.raw().mixer.luminance[4], 0.0); group.child(QStringLiteral("colorHueSlider")).setValue(750); QCOMPARE(group.raw().mixer.hue[5], 50.0); QVERIFY(!group.controls->findChild(QStringLiteral("colorHueField"))); } void CameraRawCurveMixerTests::pointColorsArePickedChosenAndShifted() { Group group; group.child(QStringLiteral("mixerPage2")).click(); group.child(QStringLiteral("pointColorSampler")).click(); QVERIFY(group.panel().samplesPointColor); QVERIFY(!group.child(QStringLiteral("visualizeRange")).isVisible()); group.set([](CameraRawSettings &settings) { settings.mixer.points = {CameraRawPointColor{.hue = 30, .saturation = 1, .luminance = 0.5}, CameraRawPointColor{.hue = 200, .saturation = 0.5, .luminance = 0.5}}; }); QVERIFY(group.child(QStringLiteral("pickedColor1")).isVisible()); group.child(QStringLiteral("pickedColor1")).click(); QCOMPARE(group.panel().pointIndex, 1); group.child(QStringLiteral("hueShiftSlider")).setValue(750); QCOMPARE(group.raw().mixer.points[1].hueShift, 50.0); QCOMPARE(group.raw().mixer.points[0].hueShift, 0.0); auto &hueRange = group.child(QStringLiteral("hueRangeSlider")); hueRange.setValue(1000); QCOMPARE(group.raw().mixer.points[1].hueRange, 180.0); QTest::mouseDClick(&hueRange, Qt::LeftButton, {}, knob(hueRange)); QCOMPARE(group.raw().mixer.points[1].hueRange, 30.0); auto &range = group.child(QStringLiteral("saturationRangeSlider")); QTest::mouseDClick(&range, Qt::LeftButton, {}, knob(range)); group.child(QStringLiteral("saturationRangeSlider")).setValue(0); QCOMPARE(group.raw().mixer.points[1].saturationRange, 0.05); group.child(QStringLiteral("visualizeRange")).click(); QVERIFY(group.raw().mixer.points[1].visualize && !group.raw().mixer.points[0].visualize); group.child(QStringLiteral("visualizeRange")).click(); QVERIFY(!group.raw().mixer.points[1].visualize); } QTEST_MAIN(CameraRawCurveMixerTests) #include "CameraRawCurveMixerTests.moc"