Files
OmaPhoto/tests/NavigatorTests.cpp
T

438 lines
20 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "Document/EditorSession.h"
#include "Rendering/NavigatorGeometry.h"
#include "SessionFixtures.h"
#include "UI/NavigatorMinimap.h"
#include <QSettings>
#include <QStandardPaths>
#include <QtTest>
// Swift 1.4.9's Navigator: the geometry, the picture, the minimap.
class NavigatorTests : public QObject {
Q_OBJECT
private slots:
void initTestCase();
void documentFitsTheBoxCentered();
void visibleRectFollowsZoomAndPan();
void centeringKeepsZoomAndStopsFollowingFit();
void pictureFitsTheLongerSideAndShowsTheComposite();
void smallDocumentsAreNotEnlarged();
void minimapFitsTheDocument();
void hugeDocumentsWithAdjustmentsStillShow();
void blursShrinkWithThePicture();
void selectionChangesDoNotRedrawThePicture();
void placedMasksMotionAndEachAxisScale();
void editsInProgressStayOut();
void blursTakeTheWidthsFactor();
void theMinimapShowsFrom300PercentWhenTurnedOn();
void thePictureRedrawsOnceEditsSettle();
void aClickOrDragMovesTheView();
void theViewsBoxIsDrawnInTheAccent();
};
namespace {
ImportedImage red(int width, int height)
{
QImage image(width, height, QImage::Format_RGBA8888_Premultiplied);
image.fill(Qt::red);
return ImportedImage(image, image, QStringLiteral("Red"));
}
// A 400 by 300 document, viewed 800 by 600.
CanvasViewport viewport()
{
CanvasViewport viewport;
viewport.resize(QSizeF(800, 600), 1, QSizeF(400, 300));
return viewport;
}
QColor at(const QImage &image, int x, int y)
{
return image.pixelColor(x, y);
}
// A red layer of `size` at the origin.
QUuid placeRed(EditorSession &session, QSize pixels, QSizeF size)
{
session.insert(red(pixels.width(), pixels.height()));
const QUuid id = session.activeLayerID().value();
rewrite(session, [&](ProjectSnapshot &snapshot) {
record(snapshot, id).transform.origin = QPointF(0, 0);
record(snapshot, id).transform.size = size;
});
session.selectLayer(id);
return id;
}
struct Shown {
EditorSession session;
NavigatorMinimap minimap{session};
Shown()
{
NavigatorMinimap::setVisibleSetting(true);
session.createDocument(400, 300);
placeRed(session, QSize(400, 300), QSizeF(400, 300));
session.viewport.resize(QSizeF(800, 600), 1, QSizeF(400, 300));
session.zoom(4, session.viewport.center());
}
};
}
void NavigatorTests::initTestCase()
{
QStandardPaths::setTestModeEnabled(true);
QSettings().clear();
}
void NavigatorTests::documentFitsTheBoxCentered()
{
const NavigatorGeometry geometry{QSizeF(400, 200), QSizeF(200, 200)};
QCOMPARE(geometry.imageRect(), QRectF(0, 50, 200, 100));
QCOMPARE(geometry.thumbnailRect(QRectF(100, 50, 200, 100)), QRectF(50, 75, 100, 50));
QCOMPARE(geometry.documentPoint(QPointF(100, 100)), QPointF(200, 100));
// Points outside the picture land on the document's edge.
QCOMPARE(geometry.documentPoint(QPointF(-20, 199)), QPointF(0, 200));
QCOMPARE((NavigatorGeometry{QSizeF(0, 0), QSizeF(10, 10)}.imageRect()), QRectF());
QCOMPARE((NavigatorGeometry{QSizeF(0, 0), QSizeF(10, 10)}.documentPoint(QPointF(5, 5))), QPointF());
QCOMPARE((NavigatorGeometry{QSizeF(10, 10), QSizeF(0, 10)}.imageRect()), QRectF());
QCOMPARE((NavigatorGeometry{QSizeF(10, 0), QSizeF(10, 10)}.imageRect()), QRectF());
QCOMPARE((NavigatorGeometry{QSizeF(0, 10), QSizeF(10, 10)}.thumbnailRect(QRectF(1, 1, 2, 2))), QRectF());
QCOMPARE((NavigatorGeometry{QSizeF(0, 10), QSizeF(10, 10)}.documentPoint(QPointF(5, 5))), QPointF());
}
void NavigatorTests::visibleRectFollowsZoomAndPan()
{
const QSizeF size(400, 300);
CanvasViewport view = viewport();
view.setZoom(4, view.center(), size);
const QRectF visible = view.visibleDocumentRect(size);
QVERIFY(std::abs(visible.width() - 200) < 0.001 && std::abs(visible.height() - 150) < 0.001);
QVERIFY(std::abs(visible.center().x() - 200) < 0.001 && std::abs(visible.center().y() - 150) < 0.001);
// Panned by 40 points: 10 pixels across at 4×.
view.translate(QSizeF(40, -80));
QVERIFY(std::abs(view.visibleDocumentRect(size).center().x() - 190) < 0.001 && std::abs(view.visibleDocumentRect(size).center().y() - 170) < 0.001);
}
void NavigatorTests::centeringKeepsZoomAndStopsFollowingFit()
{
const QSizeF size(400, 300);
CanvasViewport view = viewport();
const double zoom = view.zoom();
view.centerView(QPointF(50, 60), size);
const QRectF visible = view.visibleDocumentRect(size);
QVERIFY(std::abs(visible.center().x() - 50) < 0.001 && std::abs(visible.center().y() - 60) < 0.001);
QCOMPARE(view.zoom(), zoom);
QVERIFY(!view.followsFit());
// At a backing scale of two, points per pixel halve.
CanvasViewport sharp;
sharp.resize(QSizeF(800, 600), 2, size);
sharp.setZoom(4, sharp.center(), size);
sharp.centerView(QPointF(50, 60), size);
QVERIFY(std::abs(sharp.visibleDocumentRect(size).center().x() - 50) < 0.001 && std::abs(sharp.visibleDocumentRect(size).center().y() - 60) < 0.001);
QCOMPARE(sharp.zoom(), 4.0);
// A later window resize keeps the view where it was.
view.resize(QSizeF(1000, 700), 1, size);
QCOMPARE(view.zoom(), zoom);
}
void NavigatorTests::pictureFitsTheLongerSideAndShowsTheComposite()
{
EditorSession session;
QVERIFY(!session.navigatorImage(100));
session.createDocument(400, 200);
QVERIFY(!session.navigatorImage(0));
placeRed(session, QSize(200, 200), QSizeF(200, 200));
const QImage picture = session.navigatorImage(100).value();
QCOMPARE(picture.size(), QSize(100, 50));
QCOMPARE(at(picture, 25, 25), QColor(255, 0, 0));
QCOMPARE(at(picture, 75, 25).alpha(), 0);
// Adjustment layers apply, as on the canvas.
session.addAdjustment(AdjustmentKind::invert);
QCOMPARE(at(session.navigatorImage(100).value(), 25, 25), QColor(0, 255, 255));
}
void NavigatorTests::smallDocumentsAreNotEnlarged()
{
EditorSession session;
session.createDocument(40, 30);
QCOMPARE(session.navigatorImage(900).value().size(), QSize(40, 30));
// Each side rounded, a pixel at least.
session.createDocument(1000, 3);
QCOMPARE(session.navigatorImage(100).value().size(), QSize(100, 1));
session.createDocument(1000, 15);
QCOMPARE(session.navigatorImage(100).value().size(), QSize(100, 2));
}
void NavigatorTests::minimapFitsTheDocument()
{
QCOMPARE(NavigatorMinimap::sizeFor(QSizeF(4000, 3000)), QSizeF(120, 90));
QCOMPARE(NavigatorMinimap::sizeFor(QSizeF(6000, 2000)), QSizeF(126, 42));
QCOMPARE(NavigatorMinimap::sizeFor(QSizeF(1000, 999)), QSizeF(90, 90));
// 125.7 rounds up.
QCOMPARE(NavigatorMinimap::sizeFor(QSizeF(1000, 716)), QSizeF(126, 90));
QCOMPARE(NavigatorMinimap::sizeFor(QSizeF(0, 0)), QSizeF(0, 0));
}
// Past one surface, drawn at the picture's size, it shows.
void NavigatorTests::hugeDocumentsWithAdjustmentsStillShow()
{
EditorSession session;
session.createDocument(20'000, 12'000);
placeRed(session, QSize(200, 120), QSizeF(20'000, 12'000));
session.addAdjustment(AdjustmentKind::invert);
const QImage picture = session.navigatorImage(100).value();
QCOMPARE(picture.size(), QSize(100, 60));
QCOMPARE(at(picture, 50, 30), QColor(0, 255, 255));
}
void NavigatorTests::blursShrinkWithThePicture()
{
EditorSession session;
session.createDocument(2000, 2000);
placeRed(session, QSize(100, 200), QSizeF(1000, 2000));
session.addAdjustment(AdjustmentKind::gaussianBlur);
session.setAdjustmentEditingID(std::nullopt);
const QUuid blur = session.activeLayerID().value();
LayerAdjustment adjustment = layerWith(session, blur).adjustment.value();
adjustment.blurRadius = 100;
session.updateAdjustment(blur, adjustment);
const QImage picture = session.navigatorImage(100).value();
// Five radii from every edge at the picture's scale: solid.
QVERIFY2(at(picture, 25, 50).alpha() >= 250, qPrintable(QString::number(at(picture, 25, 50).alpha())));
// Unshrunk, a radius of 100 would reach the edge here.
QVERIFY(at(picture, 49, 50).alpha() > 0 && at(picture, 49, 50).alpha() < 250);
}
void NavigatorTests::selectionChangesDoNotRedrawThePicture()
{
EditorSession session;
session.createDocument(100, 100);
placeRed(session, QSize(10, 10), QSizeF(10, 10));
const NavigatorMinimap::RenderKey key(session.document(), 0);
session.selectAll();
QVERIFY(NavigatorMinimap::RenderKey(session.document(), 0) == key);
QVERIFY(!(NavigatorMinimap::RenderKey(session.document(), 1) == key));
std::optional<CanvasDocument> hidden = session.document();
hidden->layers.back().isVisible = false;
QVERIFY(!(NavigatorMinimap::RenderKey(hidden, 0) == key));
std::optional<CanvasDocument> other = session.document();
other->id = QUuid::createUuid();
QVERIFY(!(NavigatorMinimap::RenderKey(other, 0) == key));
}
void NavigatorTests::placedMasksMotionAndEachAxisScale()
{
// A placed mask shrinks with its layer: white, then black.
EditorSession session;
session.createDocument(400, 200);
const QUuid id = placeRed(session, QSize(300, 200), QSizeF(300, 200));
QImage halves(4, 1, QImage::Format_Grayscale8);
const uchar values[] = {255, 255, 0, 0};
std::copy_n(values, 4, halves.scanLine(0));
rewrite(session, [&](ProjectSnapshot &snapshot) {
setMask(snapshot, id, LayerMask::assetFrom(halves));
record(snapshot, id).maskPlacement = LayerTransform{.origin = QPointF(0, 0), .size = QSizeF(200, 200)};
});
const QImage masked = session.navigatorImage(100).value();
// At a quarter: white to 25, black to 50.
QVERIFY(at(masked, 15, 25).alpha() == 255 && at(masked, 40, 25).alpha() == 0);
// A layer at 100, 40; its mask at 80, 20.
const QUuid moved = placeRed(session, QSize(120, 120), QSizeF(120, 120));
// Four mask pixels: white to its middle, black after.
QImage half(4, 1, QImage::Format_Grayscale8);
std::copy_n(values, 4, half.scanLine(0));
rewrite(session, [&](ProjectSnapshot &snapshot) {
record(snapshot, id).isVisible = false;
record(snapshot, moved).transform.origin = QPointF(100, 40);
setMask(snapshot, moved, LayerMask::assetFrom(half));
record(snapshot, moved).maskPlacement = LayerTransform{.origin = QPointF(80, 20), .size = QSizeF(160, 160)};
});
const QImage away = session.navigatorImage(100).value();
// Spans 25–55 and 10–40; black from 45.
QVERIFY(at(away, 24, 30).alpha() == 0 && at(away, 26, 30).alpha() == 255 && at(away, 33, 30).alpha() == 255);
QVERIFY(at(away, 30, 9).alpha() == 0 && at(away, 30, 11).alpha() == 255 && at(away, 30, 38).alpha() == 255 && at(away, 30, 41).alpha() == 0);
QVERIFY(at(away, 46, 30).alpha() == 0 && at(away, 53, 30).alpha() == 0);
// Each side alone: 1000 by 15 is 100 by 2.
session.createDocument(1000, 15);
placeRed(session, QSize(1000, 15), QSizeF(1000, 15));
// The halving softens the last row; half would show half.
const int bottom = at(session.navigatorImage(100).value(), 50, 1).alpha();
QVERIFY2(bottom > 200, qPrintable(QString::number(bottom)));
// A streak measured in document pixels shrinks too.
session.createDocument(2000, 2000);
placeRed(session, QSize(100, 200), QSizeF(1000, 2000));
session.addAdjustment(AdjustmentKind::motionBlur);
session.setAdjustmentEditingID(std::nullopt);
const QUuid streak = session.activeLayerID().value();
LayerAdjustment adjustment = layerWith(session, streak).adjustment.value();
adjustment.motionAngle = 0;
adjustment.motionDistance = 100;
session.updateAdjustment(streak, adjustment);
const QImage picture = session.navigatorImage(100).value();
QVERIFY2(at(picture, 25, 50).alpha() >= 250, qPrintable(QString::number(at(picture, 25, 50).alpha())));
// Five pixels of streak soften the edge; none would not.
QVERIFY2(at(picture, 49, 50).alpha() < 250 && at(picture, 51, 50).alpha() > 0, qPrintable(QString::number(at(picture, 49, 50).alpha())));
}
// Swift scales both blurs by the width's factor alone.
void NavigatorTests::blursTakeTheWidthsFactor()
{
const auto edge = [](AdjustmentKind kind, QSize document, double amount, QPoint probe) {
EditorSession session;
session.createDocument(document.width(), document.height());
// Red on top, or on the left when wide.
const QSize half = document.width() > document.height() ? QSize(document.width() / 2, document.height()) : QSize(document.width(), document.height() / 2);
placeRed(session, half, QSizeF(half));
session.addAdjustment(kind);
session.setAdjustmentEditingID(std::nullopt);
const QUuid id = session.activeLayerID().value();
LayerAdjustment adjustment = layerWith(session, id).adjustment.value();
if (kind == AdjustmentKind::motionBlur)
adjustment.motionAngle = 90, adjustment.motionDistance = amount;
else
adjustment.blurRadius = amount;
session.updateAdjustment(id, adjustment);
return session.navigatorImage(100).value().pixelColor(probe).alpha();
};
// 3 by 1000 becomes 1 by 100: a 33-pixel streak.
const int streak = edge(AdjustmentKind::motionBlur, QSize(3, 1000), 100, QPoint(0, 55));
QVERIFY2(streak > 30, qPrintable(QString::number(streak)));
// 1000 by 25 becomes 100 by 3: 19, not 15.
const int blur = edge(AdjustmentKind::gaussianBlur, QSize(1000, 25), 100, QPoint(45, 1));
QVERIFY2(blur >= 18, qPrintable(QString::number(blur)));
}
// Swift draws a bare session: a pending transform stays out.
void NavigatorTests::editsInProgressStayOut()
{
EditorSession session;
session.createDocument(400, 200);
placeRed(session, QSize(200, 200), QSizeF(200, 200));
session.selectTool(NavigationTool::move);
session.beginTransform();
LayerTransform moved = session.transformEdit().value().draft;
moved.origin = QPointF(200, 0);
session.previewTransform(moved);
const QImage picture = session.navigatorImage(100).value();
QVERIFY(at(picture, 25, 25).alpha() == 255 && at(picture, 75, 25).alpha() == 0);
}
void NavigatorTests::theMinimapShowsFrom300PercentWhenTurnedOn()
{
NavigatorMinimap::setVisibleSetting(false);
QCOMPARE(QSettings().value(QLatin1String(NavigatorMinimap::visibleKey)).toBool(), false);
Shown shown;
QVERIFY(!shown.minimap.isHidden() && shown.minimap.size() == QSize(120, 90));
QCOMPARE(shown.minimap.picture().size(), QSize(300, 225));
QCOMPARE(shown.minimap.accessibleName(), QString("Navigator"));
QCOMPARE(shown.minimap.accessibleDescription(), QString("Click or drag to move the view"));
shown.session.zoom(2.9, shown.session.viewport.center());
QVERIFY(shown.minimap.isHidden() && shown.minimap.picture().isNull());
shown.session.zoom(3, shown.session.viewport.center());
QVERIFY(!shown.minimap.isHidden() && !shown.minimap.picture().isNull());
shown.session.setCanvasOnly(true);
QVERIFY(shown.minimap.isHidden());
shown.session.setCanvasOnly(false);
NavigatorMinimap::setVisibleSetting(false);
shown.session.notify();
QVERIFY(shown.minimap.isHidden());
NavigatorMinimap::setVisibleSetting(true);
shown.session.notify();
QVERIFY(!shown.minimap.isHidden());
shown.session.clearProject();
QVERIFY(shown.minimap.isHidden());
}
void NavigatorTests::thePictureRedrawsOnceEditsSettle()
{
Shown shown;
QTimer &wait = *shown.minimap.findChild<QTimer *>(QStringLiteral("navigatorWait"));
QVERIFY(wait.interval() == 250 && wait.isSingleShot() && wait.timerType() == Qt::PreciseTimer && !wait.isActive());
const QImage first = shown.minimap.picture();
QCOMPARE(at(first, 150, 100), QColor(255, 0, 0));
// A selection redraws nothing.
shown.session.selectAll();
QVERIFY(!wait.isActive());
// An edit waits a moment, then redraws once.
shown.session.addAdjustment(AdjustmentKind::invert);
QVERIFY(wait.isActive());
QCOMPARE(shown.minimap.picture().cacheKey(), first.cacheKey());
QTRY_COMPARE(at(shown.minimap.picture(), 150, 100), QColor(0, 255, 255));
QVERIFY(!wait.isActive());
// A new edit midway waits its whole 250 ms anew.
shown.session.setLayerOpacity(0.8);
QTest::qWait(130);
shown.session.setLayerOpacity(0.6);
QVERIFY(wait.remainingTime() > 200);
QTRY_VERIFY(!wait.isActive());
// The Invert at 60% over red: 102, 153, 153.
const QColor mixed = at(shown.minimap.picture(), 150, 100);
QVERIFY2(std::abs(mixed.red() - 102) <= 1 && std::abs(mixed.green() - 153) <= 1, qPrintable(mixed.name()));
// Hidden mid-wait, the wait goes with the picture.
shown.session.setLayerOpacity(0.5);
QVERIFY(wait.isActive());
shown.session.zoom(2, shown.session.viewport.center());
QVERIFY(!wait.isActive() && shown.minimap.picture().isNull());
}
void NavigatorTests::aClickOrDragMovesTheView()
{
Shown shown;
const QSizeF size(400, 300);
QSignalSpy changes(&shown.session, &EditorSession::changed);
QTest::mousePress(&shown.minimap, Qt::LeftButton, Qt::NoModifier, QPoint(30, 30));
QCOMPARE(changes.count(), 1);
QRectF visible = shown.session.viewport.visibleDocumentRect(size);
QVERIFY(std::abs(visible.center().x() - 100) < 0.001 && std::abs(visible.center().y() - 100) < 0.001);
QCOMPARE(shown.session.viewport.zoom(), 4.0);
// A drag past the picture holds to the document's edge.
QTest::mouseMove(&shown.minimap, QPoint(119, 89));
QMouseEvent move(QEvent::MouseMove, QPointF(200, 200), QPointF(200, 200), Qt::NoButton, Qt::LeftButton, Qt::NoModifier);
QApplication::sendEvent(&shown.minimap, &move);
visible = shown.session.viewport.visibleDocumentRect(size);
QVERIFY(std::abs(visible.center().x() - 400) < 0.001 && std::abs(visible.center().y() - 300) < 0.001);
// Without the left button nothing moves.
QMouseEvent hover(QEvent::MouseMove, QPointF(60, 45), QPointF(60, 45), Qt::NoButton, Qt::RightButton, Qt::NoModifier);
QApplication::sendEvent(&shown.minimap, &hover);
QTest::mousePress(&shown.minimap, Qt::RightButton, Qt::NoModifier, QPoint(60, 45));
QVERIFY(std::abs(shown.session.viewport.visibleDocumentRect(size).center().x() - 400) < 0.001);
// Over the canvas's cross, the arrow.
QWidget canvas;
canvas.setCursor(Qt::CrossCursor);
NavigatorMinimap inside(shown.session, &canvas);
QCOMPARE(inside.cursor().shape(), Qt::ArrowCursor);
}
void NavigatorTests::theViewsBoxIsDrawnInTheAccent()
{
Shown shown;
QPalette palette = shown.minimap.palette();
palette.setColor(QPalette::Active, QPalette::Highlight, QColor(0, 200, 0));
shown.minimap.setPalette(palette);
const QImage drawn = shown.minimap.grab().toImage();
// The view's middle 200 × 150 pixels: 30 to 90.
QCOMPARE(at(drawn, 30, 45), QColor(0, 200, 0));
QCOMPARE(at(drawn, 31, 45), QColor(0, 200, 0));
QCOMPARE(at(drawn, 89, 45), QColor(0, 200, 0));
QCOMPARE(at(drawn, 60, 23), QColor(0, 200, 0));
QCOMPARE(at(drawn, 60, 45), QColor(255, 0, 0));
QCOMPARE(at(drawn, 28, 45), QColor(255, 0, 0));
// The rim: white at 25% over red at the edge.
QCOMPARE(at(drawn, 0, 45), QColor(255, 64, 64));
// Clear pixels show the minimap's own gray 0.11.
rewrite(shown.session, [](ProjectSnapshot &snapshot) { snapshot.manifest.layers.back().transform.size = QSizeF(200, 300); });
shown.session.zoom(4, shown.session.viewport.center());
QTRY_COMPARE(at(shown.minimap.grab().toImage(), 110, 80), QColor(28, 28, 28));
// A view smaller than three points still shows three.
shown.session.viewport.resize(QSizeF(40, 30), 1, QSizeF(400, 300));
shown.session.zoom(32, shown.session.viewport.center());
const QRectF view = shown.session.viewport.visibleDocumentRect(QSizeF(400, 300));
QVERIFY(view.width() * 0.3 < 1);
const int left = int(std::floor(view.left() * 0.3)), row = int(view.center().y() * 0.3);
const QImage tiny = shown.minimap.grab().toImage();
QVERIFY(at(tiny, left + 1, row).green() > 100 && at(tiny, left + 1, row).red() < 60);
QCOMPARE(at(tiny, left - 2, row), QColor(255, 0, 0));
}
QTEST_MAIN(NavigatorTests)
#include "NavigatorTests.moc"