#include "CameraRawRowTable.h" #include "Document/BrushStroke.h" #include "UI/CameraRawColorControls.h" #include "UI/FilterSheet.h" #include #include #include #include // The groups' drawing, pages and gestures, and the shipped panel. namespace { 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, 900); controls->show(); if (!QTest::qWaitForWindowExposed(controls.get())) throw std::runtime_error("the controls never showed"); } template Widget &child(const QString &name) const { return *controls->template findChild(name); } const CameraRawSettings &raw() const { return session.filterEdit().value().settings.cameraRaw; } void set(const std::function &change) { FilterSettings settings = session.filterEdit().value().settings; change(settings.cameraRaw); session.updateFilter(settings, true); } void arm(const std::function &change) { CameraRawPanel panel = session.filterEdit().value().rawPanel; change(panel); session.setCameraRawPanel(panel); } }; bool near(QColor seen, QColor wanted, int tolerance = 4) { return std::abs(seen.red() - wanted.red()) <= tolerance && std::abs(seen.green() - wanted.green()) <= tolerance && std::abs(seen.blue() - wanted.blue()) <= tolerance; } QColor over(QColor top, double alpha, QColor below) { return QColor(int(std::lround(top.red() * alpha + below.red() * (1 - alpha))), int(std::lround(top.green() * alpha + below.green() * (1 - alpha))), int(std::lround(top.blue() * alpha + below.blue() * (1 - alpha)))); } // A real double click's four events, through the window. void doubleClick(QWidget &widget, QPoint at) { QTest::mouseDClick(widget.window()->windowHandle(), Qt::LeftButton, {}, widget.mapTo(widget.window(), at)); } } class CameraRawGroupDrawTests : public QObject { Q_OBJECT private slots: void theCurvePagesShowTheirOwnControls(); void theCurveGraphDrawsItsGround(); void endPointsStayAndARealDoubleClickRemoves(); void theCurveGroupWorksInsideTheShippedSheet(); void swatchesAndTracksFollowTheirFamilies(); void theWheelDrawsAndTakesItsOwnCircle(); void aTypedFieldKeepsItsTypingThroughChanges(); }; void CameraRawGroupDrawTests::theCurvePagesShowTheirOwnControls() { Group group; auto &channel = group.child(QStringLiteral("pointChannel0")); auto &amounts = group.child(QStringLiteral("curveLightsSlider")); auto &preset = group.child(QStringLiteral("curvePreset")); QVERIFY(!channel.isVisible() && amounts.isVisible() && !preset.isVisible()); QCOMPARE(group.child(QStringLiteral("curvePage0")).toolTip(), QString("Parametric lifts tonal regions. Point places anchors on the curve.")); group.arm([](CameraRawPanel &panel) { panel.curvePage = CameraRawCurvePage::point; }); QVERIFY(channel.isVisible() && !amounts.isVisible() && preset.isVisible()); QStringList presets; for (int index = 0; index < preset.count(); ++index) presets << preset.itemText(index); QCOMPARE(presets, (QStringList{"Custom", "Linear", "Medium Contrast", "Strong Contrast"})); QCOMPARE(preset.toolTip(), QString("Replaces this curve with a straight line or a contrast curve.")); QCOMPARE(channel.toolTip(), QString("RGB changes brightness. Red, green, and blue also shift the color.")); QCOMPARE(group.child(QStringLiteral("cameraRawCurveGraph")).toolTip(), QString("Drag a point. Click the curve to add one. Double-click a point to remove it.")); QCOMPARE(group.child(QStringLiteral("curveSelectedPoint")).toolTip(), QString("Input and output of the selected curve point.")); QCOMPARE(group.child(QStringLiteral("curveTargeted")).toolTip(), QString("Drag on the picture to move the curve for the tone under the pointer.")); } void CameraRawGroupDrawTests::theCurveGraphDrawsItsGround() { Group group; auto &graph = group.child(QStringLiteral("cameraRawCurveGraph")); QCOMPARE(graph.height(), 150); const QColor window = graph.palette().color(QPalette::Window); const QColor ground = over(Qt::black, 0.35, window); QImage drawn = graph.grab().toImage(); const int w = drawn.width(), h = drawn.height(); // Away from every line: black at 35% over the window. QVERIFY2(near(drawn.pixelColor(w * 3 / 4, h * 3 / 4), ground), qPrintable(drawn.pixelColor(w * 3 / 4, h * 3 / 4).name())); // Each divider rises 8 points, 3 wide, from the bottom. for (const double split : {25.0, 50.0, 75.0}) { QVERIFY(near(drawn.pixelColor(int(split / 100 * w), h - 3), Qt::white, 30)); QVERIFY(near(drawn.pixelColor(int(split / 100 * w), h - 12), ground)); } group.arm([](CameraRawPanel &panel) { panel.curvePage = CameraRawCurvePage::point; }); group.set([](CameraRawSettings &settings) { settings.curve.rgb = {{0, 0}, {0.5, 0.25}, {1, 1}}; }); drawn = graph.grab().toImage(); // A dot 8 across; the diagonal at 25% white. QVERIFY(near(drawn.pixelColor(w / 2, int(h * 0.75)), Qt::white, 10)); QVERIFY(near(drawn.pixelColor(w / 2 + 3, int(h * 0.75)), Qt::white, 60)); QVERIFY(near(drawn.pixelColor(w / 4, h - int(h * 0.25)), over(Qt::white, 0.25, ground), 40)); } void CameraRawGroupDrawTests::endPointsStayAndARealDoubleClickRemoves() { Group group; group.arm([](CameraRawPanel &panel) { panel.curvePage = CameraRawCurvePage::point; }); auto &graph = group.child(QStringLiteral("cameraRawCurveGraph")); const int w = graph.width(), h = graph.height(); // With no inner point a drag moves nothing. QTest::mousePress(&graph, Qt::LeftButton, {}, QPoint(2, h - 2)); QMouseEvent move(QEvent::MouseMove, QPointF(40, 40), graph.mapToGlobal(QPointF(40, 40)), Qt::NoButton, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(&graph, &move); QTest::mouseRelease(&graph, Qt::LeftButton, {}, QPoint(40, 40)); QVERIFY(group.raw().curve.rgb == CameraRawCurveSettings::linear()); // An inner point is held within 0.02 and 0.98. group.set([](CameraRawSettings &settings) { settings.curve.rgb = {{0, 0}, {0.5, 0.5}, {1, 1}}; }); QTest::mousePress(&graph, Qt::LeftButton, {}, QPoint(w / 2, h / 2)); QMouseEvent far(QEvent::MouseMove, QPointF(-50, h / 2), graph.mapToGlobal(QPointF(-50, h / 2)), Qt::NoButton, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(&graph, &far); QTest::mouseRelease(&graph, Qt::LeftButton, {}, QPoint(-50, h / 2)); QCOMPARE(group.raw().curve.rgb.size(), size_t(3)); QVERIFY(std::abs(group.raw().curve.rgb[1].x - 0.02) < 1e-9 && group.raw().curve.rgb.front().x == 0 && group.raw().curve.rgb.back().x == 1); // A real double click: press, release, double click, release. group.set([](CameraRawSettings &settings) { settings.curve.rgb = {{0, 0}, {0.5, 0.5}, {1, 1}}; }); doubleClick(graph, QPoint(w / 2, h / 2)); QCOMPARE(group.raw().curve.rgb.size(), size_t(2)); } void CameraRawGroupDrawTests::theCurveGroupWorksInsideTheShippedSheet() { EditorSession session; 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); FilterSheet sheet(session); sheet.resize(440, 1400); sheet.show(); QVERIFY(QTest::qWaitForWindowExposed(&sheet)); sheet.findChild(QStringLiteral("CurveSection"))->click(); CameraRawPanel panel = session.filterEdit().value().rawPanel; panel.curvePage = CameraRawCurvePage::point; session.setCameraRawPanel(panel); auto *graph = sheet.findChild(QStringLiteral("cameraRawCurveGraph")); QVERIFY(graph && graph->isVisible()); QVERIFY(!graph->grab().isNull()); const QPoint at(graph->width() / 2, graph->height() / 4); QTest::mouseClick(graph->window()->windowHandle(), Qt::LeftButton, {}, graph->mapTo(graph->window(), at)); QCOMPARE(session.filterEdit().value().settings.cameraRaw.curve.rgb.size(), size_t(3)); } void CameraRawGroupDrawTests::swatchesAndTracksFollowTheirFamilies() { Group group; group.arm([](CameraRawPanel &panel) { panel.mixerPage = CameraRawMixerPage::color; panel.mixerSwatch = 5; }); // Each swatch: its centre at 0.8 and 0.9, chosen ringed. for (int index = 0; index < 8; ++index) { auto &swatch = group.child(QStringLiteral("mixerSwatch%1").arg(index)); const QImage drawn = swatch.grab().toImage(); const QColor wanted = QColor::fromHsvF(float(CameraRawMixerSettings::centers.at(size_t(index)) / 360), 0.8f, 0.9f); QVERIFY2(near(drawn.pixelColor(drawn.width() / 2, drawn.height() / 2), wanted), qPrintable(QString::number(index))); QCOMPARE(near(drawn.pixelColor(drawn.width() / 2, 2), Qt::white, 40), index == 5); QCOMPARE(swatch.toolTip(), QStringLiteral("Edit %1.").arg(CameraRawMixerSettings::names.at(size_t(index)))); } // The Color page's hue row spans Blues, 50 each side. const auto end = [&group](const char *name) { const QImage drawn = group.child(QString::fromLatin1(name)).grab().toImage(); return drawn.pixelColor(drawn.width() - 6, drawn.height() / 2); }; QVERIFY2(std::abs(end("colorHueSlider").hsvHue() - 290) < 8, qPrintable(end("colorHueSlider").name())); // A point picked anew with another hue rebuilds its tracks. group.arm([](CameraRawPanel &panel) { panel.mixerPage = CameraRawMixerPage::point; panel.pointIndex = 0; }); group.set([](CameraRawSettings &settings) { settings.mixer.points = {CameraRawPointColor{.hue = 30, .saturation = 1, .luminance = 0.5}}; }); QVERIFY2(std::abs(end("hueShiftSlider").hsvHue() - 80) < 8, qPrintable(end("hueShiftSlider").name())); group.set([](CameraRawSettings &settings) { settings.mixer.points = {CameraRawPointColor{.hue = 200, .saturation = 1, .luminance = 0.5}}; }); QVERIFY2(std::abs(end("hueShiftSlider").hsvHue() - 250) < 8, qPrintable(end("hueShiftSlider").name())); const QImage picked = group.child(QStringLiteral("pickedColor0")).grab().toImage(); QVERIFY(near(picked.pixelColor(picked.width() / 2, picked.height() / 2), QColor::fromHsvF(200.0f / 360, 1, 0.5f))); QVERIFY(near(picked.pixelColor(picked.width() / 2, 2), Qt::white, 40)); } void CameraRawGroupDrawTests::theWheelDrawsAndTakesItsOwnCircle() { Group group; auto *shadows = group.controls->findChild(QStringLiteral("gradeWheelShadows")); auto *midtones = group.controls->findChild(QStringLiteral("gradeWheelMidtones")); // Swift's HStack: 30 points between the three columns. QCOMPARE(midtones->x() - (shadows->x() + shadows->width()), 30); QWidget *wheel = nullptr; for (QWidget *child : shadows->findChildren()) if (child->size() == QSize(86, 86)) wheel = child; group.set([](CameraRawSettings &settings) { settings.grading.shadows = {90, 50, 0}; }); const QImage drawn = wheel->grab().toImage(); const QColor window = wheel->palette().color(QPalette::Window); // Red at 85% on the right; the dot sits above. QVERIFY2(near(drawn.pixelColor(80, 43), over(Qt::red, 0.85, window), 12), qPrintable(drawn.pixelColor(80, 43).name())); QVERIFY(near(drawn.pixelColor(43, 43 - 18), Qt::white, 10)); QVERIFY(!near(drawn.pixelColor(43, 43 + 18), Qt::white, 60)); // A double click in a corner resets nothing. QTest::mouseDClick(wheel, Qt::LeftButton, {}, QPoint(2, 2)); QCOMPARE(group.raw().grading.shadows.saturation, 50.0); QTest::mouseDClick(wheel, Qt::LeftButton, {}, QPoint(43, 43)); QCOMPARE(group.raw().grading.shadows.saturation, 0.0); QCOMPARE(wheel->toolTip(), QString("Drag to set hue and saturation. Double-click to reset this wheel.")); } void CameraRawGroupDrawTests::aTypedFieldKeepsItsTypingThroughChanges() { Group group; auto *field = group.controls->findChild(QStringLiteral("curveLightsField")); field->setFocus(); QTRY_VERIFY(field->hasFocus()); field->selectAll(); QTest::keyClicks(field, QStringLiteral("42")); group.set([](CameraRawSettings &settings) { settings.curve.darks = 10; }); QCOMPARE(field->text(), QString("42")); QTest::keyClick(field, Qt::Key_Return); QCOMPARE(group.raw().curve.lights, 42.0); } QTEST_MAIN(CameraRawGroupDrawTests) #include "CameraRawGroupDrawTests.moc"