Files
OmaPhoto/tests/GuideCanvasTests.cpp

420 lines
20 KiB
C++

#include "CanvasFixtures.h"
#include "ContentView.h"
#include "UI/CanvasRulers.h"
#include "UI/LayersPanel.h"
#include <QNativeGestureEvent>
// Swift 1.1.7 on the canvas: grid, guides, rulers, drags.
namespace {
// A 400 by 300 document filling the canvas at 100%.
struct GuideCanvas : Shown {
GuideCanvas() : Shown(QSize(400, 300), QSize(400, 300))
{
settle();
session.zoom(1);
session.setShowsTransformControls(false);
QImage image(100, 60, QImage::Format_RGBA8888_Premultiplied);
image.fill(Qt::red);
session.insert(ImportedImage(image, image, QStringLiteral("Red")));
session.selectTool(NavigationTool::move);
canvas->synchronizeDisplay();
}
QPointF at(double x, double y) const { return session.viewport.viewPoint(QPointF(x, y), QSizeF(400, 300)); }
QColor pixel(QPointF point) { return canvas->grab().toImage().pixelColor(point.toPoint()); }
void press(QPointF point) { QTest::mousePress(canvas, Qt::LeftButton, Qt::NoModifier, point.toPoint()); }
void drag(QPointF point) { ::drag(*canvas, point); }
void release(QPointF point) { QTest::mouseRelease(canvas, Qt::LeftButton, Qt::NoModifier, point.toPoint()); }
};
void hover(QWidget &widget, QPointF at)
{
QMouseEvent event(QEvent::MouseMove, at, at, widget.mapToGlobal(at), Qt::NoButton, Qt::NoButton, Qt::NoModifier);
QApplication::sendEvent(&widget, &event);
}
struct Painted : QObject {
int &count;
explicit Painted(int &count) : count(count) {}
bool eventFilter(QObject *, QEvent *event) override
{
count += event->type() == QEvent::Paint;
return false;
}
};
// A hairline on a whole point spans two rows.
bool cyan(const QColor &colour)
{
return colour.green() - colour.red() > 80 && colour.blue() - colour.red() > 80;
}
}
class GuideCanvasTests : public QObject {
Q_OBJECT
private slots:
void moveToolDragsAGuideAndRulerDropDeletesIt();
void rulerStepUsesNicePixelIntervals();
void guidesAndTheGridDrawOverTheCanvas();
void theGridDrawsItsSettings();
void theCursorKeysAndLostReleasesEndADrag();
void theRulersFrameTheCanvasAndDragOutGuides();
};
void GuideCanvasTests::moveToolDragsAGuideAndRulerDropDeletesIt()
{
GuideCanvas shown;
const CanvasGuide guide{QUuid::createUuid(), CanvasGuide::Axis::vertical, 40};
shown.session.addGuide(guide);
shown.session.setShowsRulers(true);
shown.press(shown.at(40, 20));
shown.drag(shown.at(70, 20));
QCOMPARE(shown.session.guideDrag().value().position, 70.0);
shown.release(shown.at(70, 20));
QCOMPARE(shown.session.document().value().guides.front().position, 70.0);
QVERIFY(!shown.session.guideDrag());
QCOMPARE(shown.session.history.undoName(), QString("Move Guide"));
// The layer under the guide never moved.
QCOMPARE(shown.session.activeLayer().value().origin(), QPointF(150, 120));
shown.press(shown.at(70, 20));
shown.release(shown.at(-10, 20));
QVERIFY2(shown.session.document().value().guides.empty(), "dropping on the ruler deletes the guide");
// So does the top ruler, for a horizontal guide.
shown.session.addGuide({QUuid::createUuid(), CanvasGuide::Axis::horizontal, 200});
shown.press(shown.at(300, 200));
shown.release(shown.at(300, -5));
QVERIFY(shown.session.document().value().guides.empty());
shown.session.undo();
shown.session.undo();
// Without rulers a drop out there deletes nothing.
shown.session.undo();
shown.session.setShowsRulers(false);
shown.press(shown.at(70, 20));
shown.release(shown.at(-10, 20));
QCOMPARE(shown.session.document().value().guides.front().position, 70.0);
}
void GuideCanvasTests::rulerStepUsesNicePixelIntervals()
{
QCOMPARE(CanvasRulerView::majorStep(1), 100.0);
QCOMPARE(CanvasRulerView::majorStep(8), 10.0);
QCOMPARE(CanvasRulerView::majorStep(0.7), 100.0);
QCOMPARE(CanvasRulerView::majorStep(0.0001), 50'000.0);
QCOMPARE(CanvasRulerView::label(0), QString("0"));
QCOMPARE(CanvasRulerView::label(250), QString("250"));
QCOMPARE(CanvasRulerView::label(-0.4), QString("0"));
QCOMPARE(CanvasRulerView::label(-99.6), QString("-100"));
}
void GuideCanvasTests::guidesAndTheGridDrawOverTheCanvas()
{
GuideCanvas shown;
// A shown flag or a new guide repaints the canvas.
QTest::qWait(50);
int paints = 0;
Painted counter(paints);
shown.canvas->installEventFilter(&counter);
shown.session.setShowsGrid(true);
shown.canvas->synchronizeDisplay();
QTRY_VERIFY(paints > 0);
shown.session.setShowsGrid(false);
shown.canvas->synchronizeDisplay();
paints = 0;
shown.session.addGuide({QUuid::createUuid(), CanvasGuide::Axis::vertical, 390});
shown.canvas->synchronizeDisplay();
QTRY_VERIFY(paints > 0);
shown.canvas->removeEventFilter(&counter);
// A guide spans the view in cyan; hidden, it goes.
shown.session.addGuide({QUuid::createUuid(), CanvasGuide::Axis::horizontal, 20});
shown.canvas->synchronizeDisplay();
const QPointF line = shown.at(10, 20);
QVERIFY2(cyan(shown.pixel(line)), qPrintable(shown.pixel(line).name()));
QVERIFY(cyan(shown.pixel(line + QPointF(350, 0))));
shown.session.addGuide({QUuid::createUuid(), CanvasGuide::Axis::vertical, 300});
QVERIFY(cyan(shown.pixel(shown.at(300, 290))));
// A hairline: two columns at most show it at 100%.
const QImage hair = shown.canvas->grab().toImage();
int columns = 0;
for (int x = int(shown.at(300, 0).x()) - 5; x <= int(shown.at(300, 0).x()) + 5; ++x) {
const QColor seen = hair.pixelColor(x, int(shown.at(0, 290).y()));
columns += seen.green() - seen.red() > 30;
}
QVERIFY2(columns >= 1 && columns <= 2, qPrintable(QString::number(columns)));
shown.session.setShowsGuides(false);
QVERIFY(!cyan(shown.pixel(line)));
// The grid's majors every 64 pixels, subdivisions dotted between.
const QColor plain = shown.pixel(shown.at(64.5, 10.5));
shown.session.setShowsGrid(true);
const QColor major = shown.pixel(shown.at(64, 10) + QPointF(0.2, 0));
QVERIFY2(major != plain, qPrintable(major.name() + plain.name()));
// The major ink: gray 0.7 at 45%, over two columns.
const QImage majors = shown.canvas->grab().toImage();
const int column = int(shown.at(64, 0).x()), rowAt = int(shown.at(0, 10).y());
const double spread = majors.pixelColor(column - 1, rowAt).red() + majors.pixelColor(column, rowAt).red() - 2.0 * plain.red();
QVERIFY2(std::abs(spread - 0.45 * (0.7 * 255 - plain.red())) <= 4, qPrintable(QString::number(spread)));
// The subdivision at 32 is dotted: some rows inked.
const QImage grid = shown.canvas->grab().toImage();
int inked = 0;
for (int y = 0; y < 30; ++y)
inked += grid.pixelColor(shown.at(32, y).toPoint()) != grid.pixelColor(shown.at(36, y).toPoint());
QVERIFY2(inked > 3 && inked < 27, qPrintable(QString::number(inked)));
// At 25% the subdivisions are closer than four points: none.
shown.session.zoom(0.25);
shown.canvas->synchronizeDisplay();
const QImage quarter = shown.canvas->grab().toImage();
QCOMPARE(quarter.pixelColor(shown.at(8, 150).toPoint()), quarter.pixelColor(shown.at(12, 150).toPoint()));
// The major at 64 shows on a column it straddles.
const int major64 = int(std::floor(shown.at(64, 150).x()));
const QColor between = quarter.pixelColor(shown.at(40, 150).toPoint());
QVERIFY(quarter.pixelColor(major64, 150) != between || quarter.pixelColor(major64 - 1, 150) != between);
}
// Swift 1.3.5's Grid Settings: spacing, colour, opacity, style.
void GuideCanvasTests::theGridDrawsItsSettings()
{
GuideCanvas shown;
const QImage bare = shown.canvas->grab().toImage();
shown.session.setShowsGrid(true);
// What a line adds to its two columns, by channel.
const auto ink = [&](double x, int y, int channel) {
const QImage drawn = shown.canvas->grab().toImage();
int sum = 0;
for (int column = int(shown.at(x, 0).x()) - 1; column <= int(shown.at(x, 0).x()); ++column) {
const QPoint at(column, int(shown.at(0, y).y()));
const QRgb seen = drawn.pixel(at), was = bare.pixel(at);
sum += channel == 0 ? qRed(seen) - qRed(was) : qGreen(seen) - qGreen(was);
}
return sum;
};
// Rows 1 to 35 along a line: # inked.
const auto pattern = [&](double x) {
QString rows;
for (int y = 1; y <= 35; ++y)
rows += std::abs(ink(x, y, 1)) > 2 ? QLatin1Char('#') : QLatin1Char('.');
return rows;
};
const auto rowsInked = [&](double x) {
int rows = 0;
// Row 0 holds the top major: rows 1 to 35.
for (int y = 1; y <= 35; ++y)
rows += std::abs(ink(x, y, 1)) > 2;
return rows;
};
// Majors at 100, dotted halves at 50, none at 64.
int paints = 0;
Painted counter(paints);
QTest::qWait(50);
shown.canvas->installEventFilter(&counter);
shown.session.setLayoutGrid(LayoutGrid(100, 2));
shown.canvas->synchronizeDisplay();
QTRY_VERIFY(paints > 0);
QVERIFY(rowsInked(100) == 35 && rowsInked(64) == 0);
// Dotted subdivisions: one point on, two off, flat ends.
QCOMPARE(pattern(50), QString("..#..#..#..#..#..#..#..#..#..#..#.."));
// Subdivisions at 28/45 of the majors' ink.
int faint = 0;
for (int y = 1; y <= 35; ++y)
faint = std::max(faint, std::abs(ink(50, y, 1)));
QVERIFY2(std::abs(faint - ink(100, 20, 1) * 28 / 45) <= 3, qPrintable(QString::number(faint)));
// Closer than four points, subdivisions go: 64 in 32.
shown.session.setLayoutGrid(LayoutGrid(64, 32));
QVERIFY(rowsInked(2) == 0 && rowsInked(64) == 35);
// Exactly four points apart, they still show: 64 in 16.
shown.session.setLayoutGrid(LayoutGrid(64, 16));
QVERIFY(rowsInked(4) > 0);
shown.session.setLayoutGrid(LayoutGrid(100, 2));
// Cyan at full opacity: green rises, red falls.
paints = 0;
GridAppearance cyan;
cyan.preset = GridAppearance::Preset::cyan;
cyan.opacity = 100;
shown.session.setGridAppearance(cyan);
shown.canvas->synchronizeDisplay();
QTRY_VERIFY(paints > 0);
shown.canvas->removeEventFilter(&counter);
QVERIFY2(ink(100, 20, 1) >= 0 && ink(100, 20, 0) < -60, qPrintable(QString::number(ink(100, 20, 0))));
// At 1% the line barely shows.
cyan.opacity = 1;
shown.session.setGridAppearance(cyan);
QVERIFY(std::abs(ink(100, 20, 0)) < 6 && std::abs(ink(100, 20, 0)) > 0);
// Dashes four on, three off; dots one on, two off.
cyan.opacity = 100;
cyan.style = GridAppearance::Style::dashedLines;
shown.session.setGridAppearance(cyan);
QCOMPARE(pattern(100), QString("###...####...####...####...####...#"));
cyan.style = GridAppearance::Style::dots;
shown.session.setGridAppearance(cyan);
QCOMPARE(pattern(100), QString("..#..#..#..#..#..#..#..#..#..#..#.."));
}
void GuideCanvasTests::theCursorKeysAndLostReleasesEndADrag()
{
GuideCanvas shown;
shown.session.addGuide({QUuid::createUuid(), CanvasGuide::Axis::vertical, 40});
shown.session.addGuide({QUuid::createUuid(), CanvasGuide::Axis::horizontal, 250});
hover(*shown.canvas, shown.at(41, 20));
QCOMPARE(shown.canvas->cursor().shape(), Qt::SizeHorCursor);
hover(*shown.canvas, shown.at(60, 251));
QCOMPARE(shown.canvas->cursor().shape(), Qt::SizeVerCursor);
shown.session.setLocksGuides(true);
hover(*shown.canvas, shown.at(60, 251));
QVERIFY(shown.canvas->cursor().shape() != Qt::SizeVerCursor);
shown.session.setLocksGuides(false);
// Escape puts the guide back; the cursor lets go.
shown.press(shown.at(40, 20));
shown.drag(shown.at(90, 20));
QCOMPARE(shown.canvas->cursor().shape(), Qt::SizeHorCursor);
// A guide drag takes Escape before a transform edit.
QTest::keyClick(shown.canvas, Qt::Key_Escape);
shown.release(shown.at(90, 20));
shown.session.beginTransform();
QVERIFY(shown.session.transformEdit());
shown.press(shown.at(40, 20));
QVERIFY(shown.session.guideDrag());
QTest::keyClick(shown.canvas, Qt::Key_Escape);
QVERIFY(!shown.session.guideDrag() && shown.session.transformEdit());
shown.release(shown.at(40, 20));
shown.session.cancelTransform();
shown.press(shown.at(40, 20));
shown.drag(shown.at(90, 20));
// A wheel or pinch rests while a guide is dragged.
const CanvasViewport before = shown.session.viewport;
QWheelEvent wheel(shown.at(90, 20), shown.canvas->mapToGlobal(shown.at(90, 20)), QPoint(0, 30), QPoint(0, 0), Qt::LeftButton,
Qt::NoModifier, Qt::NoScrollPhase, false);
QApplication::sendEvent(shown.canvas, &wheel);
QNativeGestureEvent pinch(Qt::ZoomNativeGesture, QPointingDevice::primaryPointingDevice(), 2, shown.at(90, 20), shown.at(90, 20),
shown.canvas->mapToGlobal(shown.at(90, 20)), 0.5, {}, {});
QApplication::sendEvent(shown.canvas, &pinch);
QCOMPARE(shown.session.viewport, before);
QTest::keyClick(shown.canvas, Qt::Key_Escape);
QVERIFY(!shown.session.guideDrag());
QCOMPARE(shown.session.document().value().guides.front().position, 40.0);
hover(*shown.canvas, shown.at(200, 150));
QVERIFY(shown.canvas->cursor().shape() != Qt::SizeHorCursor);
shown.release(shown.at(90, 20));
QCOMPARE(shown.session.document().value().guides.front().position, 40.0);
// A lost release: a buttonless move lands it.
shown.press(shown.at(40, 20));
shown.drag(shown.at(95, 20));
hover(*shown.canvas, shown.at(95, 20));
QVERIFY(!shown.session.guideDrag());
QCOMPARE(shown.session.document().value().guides.front().position, 95.0);
// The drag's cursor let go with it.
hover(*shown.canvas, shown.at(300, 50));
QVERIFY(shown.canvas->cursor().shape() != Qt::SizeHorCursor);
// So does a lost focus.
shown.press(shown.at(95, 20));
shown.drag(shown.at(120, 20));
shown.canvas->clearFocus();
QVERIFY(!shown.session.guideDrag());
QCOMPARE(shown.session.document().value().guides.front().position, 120.0);
// A rename in progress leaves the guides alone.
shown.session.setRenamingLayerID(shown.session.activeLayerID());
shown.press(shown.at(120, 20));
QVERIFY(!shown.session.guideDrag());
// No drag began: a wheel still pans.
const CanvasViewport resting = shown.session.viewport;
QApplication::sendEvent(shown.canvas, &wheel);
QVERIFY(!(shown.session.viewport == resting));
shown.release(shown.at(120, 20));
shown.session.setRenamingLayerID(std::nullopt);
// Away from guides a press moves the layer, as before.
shown.press(shown.at(200, 150));
shown.drag(shown.at(230, 150));
shown.release(shown.at(230, 150));
QCOMPARE(shown.session.activeLayer().value().origin(), QPointF(180, 120));
}
void GuideCanvasTests::theRulersFrameTheCanvasAndDragOutGuides()
{
EditorSession session;
ContentView view(session);
view.resize(900, 600);
view.show();
QVERIFY(QTest::qWaitForWindowExposed(&view));
const QList<CanvasRulerView *> rulers = view.findChildren<CanvasRulerView *>();
QCOMPARE(rulers.size(), 2);
CanvasRulerView *top = rulers[0]->accessibleName() == "Horizontal ruler" ? rulers[0] : rulers[1];
CanvasRulerView *side = top == rulers[0] ? rulers[1] : rulers[0];
QCOMPARE(side->accessibleName(), QString("Vertical ruler"));
session.setShowsRulers(true);
QVERIFY(!top->isVisible());
session.createDocument(400, 300);
QTRY_VERIFY(top->isVisible() && side->isVisible());
QVERIFY(view.findChild<CanvasRulerCorner *>()->isVisible());
QVERIFY(top->height() == CanvasRuler::thickness && side->width() == CanvasRuler::thickness);
QVERIFY(top->cursor().shape() == Qt::SizeVerCursor && side->cursor().shape() == Qt::SizeHorCursor);
CanvasView &canvas = *view.findChild<CanvasView *>();
// The top ruler sits above the canvas, the side left.
QCOMPARE(top->mapTo(&view, QPoint(0, top->height())).y(), canvas.mapTo(&view, QPoint(0, 0)).y());
QCOMPARE(side->mapTo(&view, QPoint(side->width(), 0)).x(), canvas.mapTo(&view, QPoint(0, 0)).x());
QTRY_COMPARE(session.viewport.viewSize, QSizeF(canvas.size()));
session.zoom(1);
// The corner's diagonal, and rulers that follow the view.
const QImage corner = view.findChild<CanvasRulerCorner *>()->grab().toImage();
QVERIFY(corner.pixelColor(9, 9).lightness() > corner.pixelColor(3, 3).lightness() + 10);
int paints = 0;
Painted counter(paints);
top->installEventFilter(&counter);
session.zoom(2);
QTRY_VERIFY(paints > 0);
top->removeEventFilter(&counter);
session.zoom(1);
// A press on a ruler hands the canvas the keys.
view.layersPanel().setFocus();
QTRY_VERIFY(!canvas.hasFocus());
QTest::mousePress(side, Qt::LeftButton, Qt::NoModifier, QPoint(9, 100));
QVERIFY(canvas.hasFocus());
QTest::mouseRelease(side, Qt::LeftButton, Qt::NoModifier, QPoint(9, 100));
// A drag from the top ruler makes a guide.
const QPointF target = session.viewport.viewPoint(QPointF(0, 150), QSizeF(400, 300));
QTest::mousePress(top, Qt::LeftButton, Qt::NoModifier, QPoint(50, 9));
QVERIFY(session.guideDrag() && session.guideDrag().value().axis == CanvasGuide::Axis::horizontal);
const QPoint inRuler = top->mapFromGlobal(canvas.mapToGlobal(target.toPoint()));
drag(*top, QPointF(inRuler));
QCOMPARE(session.guideDrag().value().position, 150.0);
QTest::mouseRelease(top, Qt::LeftButton, Qt::NoModifier, inRuler);
QCOMPARE(session.document().value().guides.size(), size_t(1));
QCOMPARE(session.document().value().guides.front().position, 150.0);
QCOMPARE(session.history.undoName(), QString("New Guide"));
// Dropped back on the ruler, no guide is made.
QTest::mousePress(side, Qt::LeftButton, Qt::NoModifier, QPoint(9, 100));
QTest::mouseRelease(side, Qt::LeftButton, Qt::NoModifier, QPoint(9, 100));
QCOMPARE(session.document().value().guides.size(), size_t(1));
// Locked guides take no drag; rulers follow the flag.
session.setLocksGuides(true);
QTest::mousePress(side, Qt::LeftButton, Qt::NoModifier, QPoint(9, 100));
QVERIFY(!session.guideDrag());
QTest::mouseRelease(side, Qt::LeftButton, Qt::NoModifier, QPoint(9, 100));
session.setShowsRulers(false);
QVERIFY(!top->isVisible() && !side->isVisible());
// The ruler ticks: a labelled major every 100 pixels.
session.setShowsRulers(true);
const QImage strip = top->grab().toImage();
const int zero = top->mapFromGlobal(canvas.mapToGlobal(session.viewport.viewPoint(QPointF(0, 0), QSizeF(400, 300)).toPoint())).x();
const int row = CanvasRuler::thickness - 2;
// A half-point hairline lands on one of two columns.
QVERIFY(std::max(strip.pixelColor(zero, row).lightness(), strip.pixelColor(zero - 1, row).lightness()) > 120);
// Majors reach eight points up, halves five, minors three.
const auto lit = [&](int x, int y) { return std::max(strip.pixelColor(x, y).lightness(), strip.pixelColor(x - 1, y).lightness()) > 120; };
QVERIFY(lit(zero + 50, CanvasRuler::thickness - 5) && !lit(zero + 50, CanvasRuler::thickness - 6));
// A label two points past the major, in gray 0.78.
int brightest = 0;
for (int y = 0; y < 10; ++y) {
for (int x = zero + 2; x < zero + 12; ++x)
brightest = std::max(brightest, strip.pixelColor(x, y).lightness());
}
// Gray 0.78 at partial coverage stays well below white.
QVERIFY2(brightest > 150 && brightest < 180, qPrintable(QString::number(brightest)));
int between = 0;
for (int y = 0; y < 8; ++y) {
for (int x = zero + 30; x < zero + 45; ++x)
between = std::max(between, strip.pixelColor(x, y).lightness());
}
QVERIFY(between < 80);
QVERIFY(lit(zero, CanvasRuler::thickness - 7) && lit(zero + 10, row) && !lit(zero + 10, CanvasRuler::thickness - 5));
QVERIFY(strip.pixelColor(zero + 5, CanvasRuler::thickness - 2).lightness() < 80);
}
QTEST_MAIN(GuideCanvasTests)
#include "GuideCanvasTests.moc"