#include "Document/BrushStroke.h" #include "Document/EditorSession.h" #include "UI/CameraRawColorControls.h" #include #include #include #include // Swift 1.4.5: the Camera Raw curve graph's one drag. namespace { struct Graph { EditorSession session; std::unique_ptr controls; QWidget *graph = nullptr; explicit Graph(bool point = false) { 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(); graph = controls->findChild(QStringLiteral("cameraRawCurveGraph")); if (point) controls->findChild(QStringLiteral("curvePage1"))->click(); } const CameraRawCurveSettings &curve() const { return session.filterEdit().value().settings.cameraRaw.curve; } void set(const std::function &change) { FilterSettings settings = session.filterEdit().value().settings; change(settings.cameraRaw.curve); session.updateFilter(settings, true); } // A curve position in the graph's own points. QPoint at(double x, double y) const { return QPoint(int(std::lround(x * graph->width())), int(std::lround((1 - y) * graph->height()))); } void press(QPoint where, Qt::MouseButton button = Qt::LeftButton) const { QTest::mousePress(graph, button, {}, where); } void move(QPoint where, Qt::MouseButtons buttons = Qt::LeftButton) const { QMouseEvent event(QEvent::MouseMove, where, graph->mapToGlobal(where), Qt::NoButton, buttons, Qt::NoModifier); QApplication::sendEvent(graph, &event); } void release(QPoint where) const { QTest::mouseRelease(graph, Qt::LeftButton, {}, where); } QLabel &readout() const { return *controls->findChild(QStringLiteral("curveSelectedPoint")); } QColor dot(double x, double y) const { return graph->grab().toImage().pixelColor(at(x, y)); } }; } class CameraRawCurveGraphTests : public QObject { Q_OBJECT private slots: void aRegionDragLiftsOrLowersThePressedRegion(); void theRegionIsPickedOnceByItsTone(); void dividersMoveInTheBottomStripAndStayOrdered(); void aPointIsPickedByDistanceAndKept(); void aPressElsewhereAddsAndDragsAPoint(); void addingIsRefusedNearAnEdgeAPointOrSixteen(); void theChosenPointIsLitAndReadOut(); void aNewChannelOrPresetDropsTheChoice(); void aDoubleClickRemovesAnInnerPointAndTheChoice(); void aDoubleClickReachesFourHundredthsByX(); void aDoubleClickInTheStripMovesADivider(); void onlyTheLeftButtonDragsAndAPressStartsAfresh(); void theGraphSaysWhatADragDoes(); }; void CameraRawCurveGraphTests::aRegionDragLiftsOrLowersThePressedRegion() { Graph graph; const int h = graph.graph->height(); graph.set([](CameraRawCurveSettings &curve) { curve.lights = 10; }); // Up lifts: a quarter height adds 50. graph.press(QPoint(int(graph.graph->width() * 0.6), 100)); graph.move(QPoint(int(graph.graph->width() * 0.6), 100 - h / 4)); QCOMPARE(graph.curve().lights, std::round(10 + double(h / 4) / h * 200)); // Down lowers, clamped at −100, rounded to whole. graph.move(QPoint(int(graph.graph->width() * 0.6), 100 + h)); QCOMPARE(graph.curve().lights, -100.0); graph.move(QPoint(10, 100 + 3)); QCOMPARE(graph.curve().lights, std::round(10 - 3.0 / h * 200)); graph.release(QPoint(10, 103)); QVERIFY(graph.curve().shadows == 0 && graph.curve().darks == 0 && graph.curve().highlights == 0); // Far up, +100 at most. graph.press(QPoint(int(graph.graph->width() * 0.9), 120)); graph.move(QPoint(int(graph.graph->width() * 0.9), 120 - 4 * h)); QCOMPARE(graph.curve().highlights, 100.0); } void CameraRawCurveGraphTests::theRegionIsPickedOnceByItsTone() { Graph graph; const int w = graph.graph->width(); graph.set([](CameraRawCurveSettings &curve) { curve.shadowSplit = 20; }); // A tone on a divider joins the region above. const std::pair regions[] = { {0.1, &CameraRawCurveSettings::shadows}, {0.2, &CameraRawCurveSettings::darks}, {0.5, &CameraRawCurveSettings::lights}, {0.75, &CameraRawCurveSettings::highlights}}; for (const auto &[tone, key] : regions) { const CameraRawCurveSettings before = graph.curve(); graph.press(QPoint(int(std::lround(tone * w)), 60)); graph.move(QPoint(int(std::lround(tone * w)), 50)); graph.release(QPoint(int(std::lround(tone * w)), 50)); CameraRawCurveSettings expected = before; expected.*key = std::round(10.0 / graph.graph->height() * 200); QVERIFY2(graph.curve() == expected, qPrintable(QString::number(tone))); } // At tone 50 exactly, each divider hands it upward. QCOMPARE(w % 2, 0); const std::tuple, double CameraRawCurveSettings::*> onDividers[] = { {{50, 60, 80}, &CameraRawCurveSettings::darks}, {{20, 50, 80}, &CameraRawCurveSettings::lights}, {{20, 30, 50}, &CameraRawCurveSettings::highlights}}; for (const auto &[splits, key] : onDividers) { graph.set([&splits](CameraRawCurveSettings &curve) { curve = CameraRawCurveSettings(); curve.shadowSplit = splits[0]; curve.darkSplit = splits[1]; curve.lightSplit = splits[2]; }); graph.press(QPoint(w / 2, 60)); graph.move(QPoint(w / 2, 50)); graph.release(QPoint(w / 2, 50)); QVERIFY(graph.curve().*key > 0); } // Kept through the drag: across another region, the same one. graph.set([](CameraRawCurveSettings &curve) { curve.shadows = curve.darks = curve.lights = curve.highlights = 0; }); graph.press(QPoint(int(0.1 * w), 60)); graph.move(QPoint(int(0.9 * w), 40)); QVERIFY(graph.curve().shadows > 0 && graph.curve().highlights == 0); } void CameraRawCurveGraphTests::dividersMoveInTheBottomStripAndStayOrdered() { Graph graph; const int w = graph.graph->width(), h = graph.graph->height(); // The strip is the bottom 18 points. graph.press(QPoint(int(0.5 * w), h - 18)); graph.move(QPoint(int(0.6 * w), h - 30)); graph.release(QPoint(int(0.6 * w), h - 30)); QCOMPARE(graph.curve().darkSplit, 50.0); QVERIFY(graph.curve().lights > 0); graph.set([](CameraRawCurveSettings &curve) { curve.lights = 0; }); // Each divider, the nearest to the press, between its neighbours. const auto dragged = [&](double from, double to) { graph.press(QPoint(int(std::lround(from * w)), h - 17)); graph.move(QPoint(int(std::lround(to * w)), 10)); graph.release(QPoint(int(std::lround(to * w)), 10)); }; dragged(0.3, 0.9); QCOMPARE(graph.curve().shadowSplit, 48.0); // Past 2 and 98 the settings' own limits hold. dragged(0.47, 0.0); QCOMPARE(graph.curve().shadowSplit, 5.0); dragged(0.55, 0.9); QCOMPARE(graph.curve().darkSplit, 73.0); dragged(0.73, 0.01); QCOMPARE(graph.curve().darkSplit, 7.0); dragged(0.74, 0.0); QCOMPARE(graph.curve().lightSplit, 9.0); dragged(0.095, 1.2); QCOMPARE(graph.curve().lightSplit, 98.0); QVERIFY(graph.curve().shadows == 0 && graph.curve().darks == 0 && graph.curve().lights == 0 && graph.curve().highlights == 0); // At tone 50 exactly, between 40 and 60: the first. QCOMPARE(w % 2, 0); graph.set([](CameraRawCurveSettings &curve) { curve.shadowSplit = 40; curve.darkSplit = 60; curve.lightSplit = 80; }); graph.press(QPoint(w / 2, h - 5)); graph.move(QPoint(int(0.45 * w), h - 5)); QVERIFY(std::abs(graph.curve().shadowSplit - 45) < 0.5 && graph.curve().darkSplit == 60); } void CameraRawCurveGraphTests::aPointIsPickedByDistanceAndKept() { Graph graph(true); graph.set([](CameraRawCurveSettings &curve) { curve.rgb = {{0, 0}, {0.3, 0.3}, {0.36, 0.5}, {1, 1}}; }); // Nearer by distance, not by x: the second. graph.press(graph.at(0.33, 0.48)); graph.move(graph.at(0.33, 0.6)); QCOMPARE(graph.curve().rgb.size(), size_t(4)); QVERIFY(std::abs(graph.curve().rgb[2].y - 0.6) < 0.01 && graph.curve().rgb[1] == (CurvePoint{0.3, 0.3})); // Kept as it passes x 0.99, held before the end. graph.move(graph.at(1.2, 0.2)); QVERIFY(std::abs(graph.curve().rgb[2].x - 0.99) < 1e-6 && graph.curve().rgb[1] == (CurvePoint{0.3, 0.3})); // Fix beyond Swift: onto its neighbour it stays, still dragged. graph.move(graph.at(0.1, 0.2)); QCOMPARE(graph.curve().rgb.size(), size_t(4)); QVERIFY(std::abs(graph.curve().rgb[2].x - 0.31) < 1e-6 && graph.curve().rgb[1] == (CurvePoint{0.3, 0.3})); graph.move(graph.at(0.6, 0.7)); QCOMPARE(graph.curve().rgb.size(), size_t(4)); QVERIFY(std::abs(graph.curve().rgb[2].x - 0.6) < 0.003 && graph.curve().rgb[3] == (CurvePoint{1, 1})); graph.release(graph.at(0.6, 0.7)); // Onto the next point: a hundredth short; both stay. graph.set([](CameraRawCurveSettings &curve) { curve.rgb = {{0, 0}, {0.3, 0.3}, {0.5, 0.5}, {1, 1}}; }); graph.press(graph.at(0.3, 0.3)); graph.move(graph.at(0.8, 0.3)); graph.release(graph.at(0.8, 0.3)); QCOMPARE(graph.curve().rgb.size(), size_t(4)); QVERIFY(std::abs(graph.curve().rgb[1].x - 0.49) < 1e-6 && graph.curve().rgb[2] == (CurvePoint{0.5, 0.5})); // 0.05 away takes the point; 0.06 away adds one. graph.set([](CameraRawCurveSettings &curve) { curve.rgb = {{0, 0}, {0.5, 0.5}, {1, 1}}; }); graph.press(graph.at(0.53, 0.54)); graph.release(graph.at(0.53, 0.54)); QCOMPARE(graph.curve().rgb.size(), size_t(3)); QVERIFY(std::abs(graph.curve().rgb[1].x - 0.53) < 0.003); graph.set([](CameraRawCurveSettings &curve) { curve.rgb = {{0, 0}, {0.5, 0.5}, {1, 1}}; }); graph.press(graph.at(0.54, 0.545)); graph.release(graph.at(0.54, 0.545)); QCOMPARE(graph.curve().rgb.size(), size_t(4)); QVERIFY(graph.curve().rgb[1] == (CurvePoint{0.5, 0.5})); } void CameraRawCurveGraphTests::aPressElsewhereAddsAndDragsAPoint() { Graph graph(true); graph.press(graph.at(0.25, 0.6)); QCOMPARE(graph.curve().rgb.size(), size_t(3)); QVERIFY(std::abs(graph.curve().rgb[1].x - 0.25) < 0.01 && std::abs(graph.curve().rgb[1].y - 0.6) < 0.01); // The same press drags it on, in order. graph.move(graph.at(0.4, 0.7)); QVERIFY(std::abs(graph.curve().rgb[1].x - 0.4) < 0.01 && std::abs(graph.curve().rgb[1].y - 0.7) < 0.01); graph.release(graph.at(0.4, 0.7)); // Added below, sorted in at its x. graph.press(graph.at(0.8, 0.95)); graph.move(graph.at(0.7, 1.4)); graph.release(graph.at(0.7, 1.4)); QCOMPARE(graph.curve().rgb.size(), size_t(4)); QVERIFY(std::abs(graph.curve().rgb[2].x - 0.7) < 0.01 && graph.curve().rgb[2].y == 1); QVERIFY(std::abs(graph.curve().rgb[1].x - 0.4) < 0.01); // The ends move their output alone. graph.press(graph.at(1, 1)); graph.move(graph.at(0.6, 0.8)); graph.release(graph.at(0.6, 0.8)); QVERIFY(graph.curve().rgb.back().x == 1 && std::abs(graph.curve().rgb.back().y - 0.8) < 0.01); graph.press(graph.at(0, 0)); graph.move(graph.at(0.3, -0.5)); graph.release(graph.at(0.3, -0.5)); QVERIFY(graph.curve().rgb.front() == (CurvePoint{0, 0})); } void CameraRawCurveGraphTests::addingIsRefusedNearAnEdgeAPointOrSixteen() { Graph graph(true); // Too near an end's x, or a point's. const int w = graph.graph->width(); graph.press(QPoint(2, 20)); graph.move(QPoint(40, 30)); graph.release(QPoint(40, 30)); graph.press(QPoint(w - 2, 140)); graph.release(QPoint(w - 2, 140)); QVERIFY(graph.curve().rgb == CameraRawCurveSettings::linear()); graph.set([](CameraRawCurveSettings &curve) { curve.rgb = {{0, 0}, {0.5, 0.2}, {1, 1}}; }); graph.press(graph.at(0.505, 0.9)); graph.move(graph.at(0.6, 0.9)); graph.release(graph.at(0.6, 0.9)); QVERIFY(graph.curve().rgb == (std::vector{{0, 0}, {0.5, 0.2}, {1, 1}})); QVERIFY(!graph.readout().isVisible()); // Sixteen points take no seventeenth. graph.set([](CameraRawCurveSettings &curve) { curve.rgb = {{0, 0}}; for (int index = 1; index < 15; ++index) curve.rgb.push_back({index * 0.06, index * 0.06}); curve.rgb.push_back({1, 1}); }); QCOMPARE(graph.curve().rgb.size(), size_t(16)); graph.press(graph.at(0.93, 0.2)); graph.release(graph.at(0.93, 0.2)); QCOMPARE(graph.curve().rgb.size(), size_t(16)); // Fifteen take a sixteenth. graph.set([](CameraRawCurveSettings &curve) { curve.rgb.erase(curve.rgb.begin() + 1); }); graph.press(graph.at(0.93, 0.2)); graph.release(graph.at(0.93, 0.2)); QCOMPARE(graph.curve().rgb.size(), size_t(16)); } void CameraRawCurveGraphTests::theChosenPointIsLitAndReadOut() { Graph graph(true); graph.set([](CameraRawCurveSettings &curve) { curve.rgb = {{0, 0}, {0.3, 0.4}, {0.7, 0.6}, {1, 1}}; }); const QColor accent = graph.graph->palette().color(QPalette::Highlight); QCOMPARE(graph.dot(0.3, 0.4), QColor(Qt::white)); QVERIFY(!graph.readout().isVisible()); const auto shown = [&graph](size_t index) { const CurvePoint point = graph.curve().rgb[index]; return QStringLiteral("In %1 Out %2").arg(std::lround(point.x * 255)).arg(std::lround(point.y * 255)); }; // The press takes the point to the pointer. graph.press(graph.at(0.3, 0.4)); QVERIFY(std::abs(graph.curve().rgb[1].x - 0.3) < 0.003 && std::abs(graph.curve().rgb[1].y - 0.4) < 0.007); QCOMPARE(graph.dot(0.3, 0.4), accent); QCOMPARE(graph.dot(0.7, 0.6), QColor(Qt::white)); QCOMPARE(graph.readout().text(), shown(1)); // The readout follows the drag. graph.move(graph.at(0.4, 0.5)); QCOMPARE(graph.readout().text(), shown(1)); QVERIFY(graph.readout().text().startsWith("In 102 Out 12")); graph.release(graph.at(0.4, 0.5)); // An end point is chosen too. graph.press(graph.at(1, 1)); graph.release(graph.at(1, 1)); QCOMPARE(graph.readout().text(), QString("In 255 Out 255")); QCOMPARE(graph.graph->grab().toImage().pixelColor(graph.graph->width() - 2, 2), accent); // A new point is chosen as it is made. graph.press(graph.at(0.85, 0.2)); graph.release(graph.at(0.85, 0.2)); QCOMPARE(graph.readout().text(), shown(3)); QVERIFY(std::abs(graph.curve().rgb[3].x - 0.85) < 0.003); QCOMPARE(graph.dot(0.85, 0.2), accent); // A choice past the points changed elsewhere hides. graph.press(graph.at(1, 1)); graph.release(graph.at(1, 1)); graph.set([](CameraRawCurveSettings &curve) { curve.rgb = CameraRawCurveSettings::linear(); }); QVERIFY(!graph.readout().isVisible()); } void CameraRawCurveGraphTests::aNewChannelOrPresetDropsTheChoice() { Graph graph(true); graph.set([](CameraRawCurveSettings &curve) { curve.rgb = curve.red = {{0, 0}, {0.3, 0.4}, {1, 1}}; }); graph.press(graph.at(0.3, 0.4)); QVERIFY(graph.readout().isVisible()); // Another channel: no choice, and the held drag lets go. graph.controls->findChild(QStringLiteral("pointChannel1"))->click(); QVERIFY(!graph.readout().isVisible()); QCOMPARE(graph.dot(0.3, 0.4), QColor(Qt::white)); graph.move(graph.at(0.3, 0.9)); QVERIFY(graph.curve().red == (std::vector{{0, 0}, {0.3, 0.4}, {1, 1}})); graph.release(graph.at(0.3, 0.9)); // A preset's points drop it too; Custom keeps it. graph.press(graph.at(0.3, 0.4)); graph.release(graph.at(0.3, 0.4)); auto &preset = *graph.controls->findChild(QStringLiteral("curvePreset")); emit preset.activated(0); graph.set([](CameraRawCurveSettings &curve) { curve.refineSaturation = 10; }); QVERIFY(graph.readout().isVisible()); emit preset.activated(2); QVERIFY(graph.curve().red == CameraRawCurveSettings::mediumContrast()); QVERIFY(!graph.readout().isVisible()); QCOMPARE(graph.dot(0.25, 0.18), QColor(Qt::white)); } void CameraRawCurveGraphTests::aDoubleClickRemovesAnInnerPointAndTheChoice() { Graph graph(true); graph.set([](CameraRawCurveSettings &curve) { curve.rgb = {{0, 0}, {0.5, 0.5}, {1, 1}}; }); QTest::mouseDClick(graph.graph->window()->windowHandle(), Qt::LeftButton, {}, graph.graph->mapTo(graph.graph->window(), graph.at(0.5, 0.5))); QVERIFY(graph.curve().rgb == CameraRawCurveSettings::linear()); QVERIFY(!graph.readout().isVisible()); // On empty ground: one click adds, two remove. QTest::mouseDClick(graph.graph->window()->windowHandle(), Qt::LeftButton, {}, graph.graph->mapTo(graph.graph->window(), graph.at(0.4, 0.8))); QVERIFY(graph.curve().rgb == CameraRawCurveSettings::linear()); // A move after it drags nothing. graph.set([](CameraRawCurveSettings &curve) { curve.rgb = {{0, 0}, {0.3, 0.3}, {0.6, 0.6}, {1, 1}}; }); QTest::mouseDClick(graph.graph, Qt::LeftButton, {}, graph.at(0.3, 0.3)); graph.move(graph.at(0.5, 0.9)); QVERIFY(graph.curve().rgb == (std::vector{{0, 0}, {0.6, 0.6}, {1, 1}})); // An end point stays, though chosen. QTest::mouseDClick(graph.graph, Qt::LeftButton, {}, graph.at(1, 1)); QCOMPARE(graph.curve().rgb.size(), size_t(3)); QCOMPARE(graph.readout().text(), QString("In 255 Out 255")); } void CameraRawCurveGraphTests::aDoubleClickReachesFourHundredthsByX() { Graph graph(true); // Beside the start nothing is added; 0.037 removes, 0.042 not. QVERIFY(3.0 / graph.graph->width() < 0.01); for (const auto &[inner, removed] : {std::pair(0.045, true), std::pair(0.05, false)}) { graph.set([inner](CameraRawCurveSettings &curve) { curve.rgb = {{0, 0}, {inner, 0.9}, {1, 1}}; }); QTest::mouseDClick(graph.graph->window()->windowHandle(), Qt::LeftButton, {}, graph.graph->mapTo(graph.graph->window(), QPoint(3, 140))); QCOMPARE(graph.curve().rgb.size(), removed ? size_t(2) : size_t(3)); } } void CameraRawCurveGraphTests::aDoubleClickInTheStripMovesADivider() { Graph graph; const int w = graph.graph->width(), h = graph.graph->height(); graph.set([](CameraRawCurveSettings &curve) { curve.rgb = {{0, 0}, {0.5, 0.5}, {1, 1}}; }); // Both presses take the nearest divider there; no point goes. QTest::mouseDClick(graph.graph->window()->windowHandle(), Qt::LeftButton, {}, graph.graph->mapTo(graph.graph->window(), QPoint(int(0.3 * w), h - 5))); QVERIFY(std::abs(graph.curve().shadowSplit - 30) < 0.5); QVERIFY(graph.curve().darkSplit == 50 && graph.curve().lightSplit == 75); QCOMPARE(graph.curve().rgb.size(), size_t(3)); QVERIFY(graph.curve().shadows == 0 && graph.curve().darks == 0); } void CameraRawCurveGraphTests::onlyTheLeftButtonDragsAndAPressStartsAfresh() { Graph graph(true); graph.press(graph.at(0.5, 0.8), Qt::RightButton); graph.move(graph.at(0.5, 0.2), Qt::RightButton); QVERIFY(graph.curve().rgb == CameraRawCurveSettings::linear()); graph.set([](CameraRawCurveSettings &curve) { curve.rgb = {{0, 0}, {0.5, 0.5}, {1, 1}}; }); QTest::mouseDClick(graph.graph, Qt::RightButton, {}, graph.at(0.5, 0.5)); QVERIFY(graph.curve().rgb == (std::vector{{0, 0}, {0.5, 0.5}, {1, 1}})); // A move without the left button drags nothing. graph.press(graph.at(0.5, 0.5)); graph.move(graph.at(0.5, 0.9), Qt::RightButton); QVERIFY(std::abs(graph.curve().rgb[1].y - 0.5) < 0.01); graph.release(graph.at(0.5, 0.5)); // A lost release: the next press picks its own target. graph.set([](CameraRawCurveSettings &curve) { curve.rgb = {{0, 0}, {0.3, 0.3}, {0.7, 0.7}, {1, 1}}; }); graph.press(graph.at(0.3, 0.3)); graph.press(graph.at(0.7, 0.7)); graph.move(graph.at(0.7, 0.9)); QVERIFY(std::abs(graph.curve().rgb[1].y - 0.3) < 0.01 && std::abs(graph.curve().rgb[2].y - 0.9) < 0.01); // A release lets go: moves then change nothing. graph.release(graph.at(0.7, 0.9)); const std::vector held = graph.curve().rgb; graph.move(graph.at(0.5, 0.1)); QVERIFY(graph.curve().rgb == held); // Parametric too: a release ends the region. graph.controls->findChild(QStringLiteral("curvePage0"))->click(); graph.press(QPoint(30, 80)); graph.release(QPoint(30, 80)); graph.move(QPoint(30, 10)); QCOMPARE(graph.curve().shadows, 0.0); } void CameraRawCurveGraphTests::theGraphSaysWhatADragDoes() { Graph graph; QCOMPARE(graph.graph->toolTip(), QString("Drag up or down to lift or lower those tones. Drag a divider along the bottom to change which tones each region covers.")); graph.controls->findChild(QStringLiteral("curvePage1"))->click(); QCOMPARE(graph.graph->toolTip(), QString("Drag a point. Click to add one. Double-click a point to remove it.")); } QTEST_MAIN(CameraRawCurveGraphTests) #include "CameraRawCurveGraphTests.moc"