#include "ContentView.h" #include #include #include // The status bar: numbers, colour space, and each tool's words. namespace { template Widget &find(QWidget &root, const char *name) { Widget *found = root.findChild(QString::fromLatin1(name)); if (!found) throw std::runtime_error(std::string("no widget named ") + name); return *found; } } class ContentViewStatusTests : public QObject { Q_OBJECT private slots: void theStatusBarSaysWhatIsGoingOn(); void everyToolHasItsHint(); void zoomPrintsAsSwiftPrintsIt(); }; void ContentViewStatusTests::theStatusBarSaysWhatIsGoingOn() { EditorSession session; ContentView view(session); view.show(); auto &zoom = find(view, "zoomStatus"), &size = find(view, "canvasDimensions"); auto &colour = find(view, "colourStatus"), &activity = find(view, "activityStatus"); auto &spinner = *view.findChild(); QVERIFY(!zoom.isVisible() && !size.isVisible() && !spinner.isVisible()); QCOMPARE(colour.text(), QString("Ready when you are")); session.createDocument(30000, 12345); QVERIFY(zoom.isVisible() && size.isVisible()); QCOMPARE(size.text(), QString("30,000 × 12,345 px")); QCOMPARE(colour.text(), QString("sRGB · Transparent")); QCOMPARE(zoom.width(), 62); // Importing shows at once; busy only after its quarter second. session.setIsImporting(true); QVERIFY(spinner.isVisible()); QCOMPARE(activity.text(), QString("Importing images…")); session.setIsProjectBusy(true); QCOMPARE(activity.text(), QString("Importing images…")); QTRY_COMPARE(activity.text(), QString("Working…")); QVERIFY(spinner.isVisible()); session.setIsProjectBusy(false); session.setIsImporting(false); QVERIFY(!spinner.isVisible()); QCOMPARE(activity.text(), ContentView::hint(session.tool())); // The Marquee's and the Lasso's words follow their kinds. session.selectTool(NavigationTool::marquee); session.setMarqueeKind(LassoKind::ellipse); QCOMPARE(activity.text(), ContentView::hint(NavigationTool::marquee, ToolIconKind::ellipse)); QVERIFY(activity.text().startsWith("Drag an ellipse")); session.selectTool(NavigationTool::lasso); session.setLassoKind(LassoKind::polygonal); QCOMPARE(activity.text(), ContentView::hint(NavigationTool::lasso, ToolIconKind::polygonal)); QVERIFY(activity.text().startsWith("Click corners")); session.setLassoKind(LassoKind::freehand); QCOMPARE(activity.text(), ContentView::hint(NavigationTool::lasso)); // The brush's words follow its mode. session.selectTool(NavigationTool::brush); session.setBrushMode(BrushToolMode::erase); QCOMPARE(activity.text(), QString("Drag to erase · [ ] size · Shift-[ ] hardness · 1–0 opacity · Escape cancel · Space to pan")); session.setBrushMode(BrushToolMode::paint); QCOMPARE(activity.text(), ContentView::hint(NavigationTool::brush)); // The Smear's words follow its mode. session.selectTool(NavigationTool::blur); session.setBlurMode(BlurToolMode::blur); QCOMPARE(activity.text(), QString("Drag to soften · [ ] size · Shift-[ ] hardness · 1–0 strength · Space to pan")); session.setBlurMode(BlurToolMode::smudge); QCOMPARE(activity.text(), QString("Drag to smudge · [ ] size · Shift-[ ] hardness · 1–0 strength · Space to pan")); QCOMPARE(ContentView::hint(NavigationTool::brush, ToolIconKind::plain, BlurToolMode::smudge), ContentView::hint(NavigationTool::brush)); session.setBlurMode(BlurToolMode::liquify); QCOMPARE(activity.text(), ContentView::hint(NavigationTool::blur)); // Busy alone turns the spinner as well. session.setIsProjectBusy(true); QTRY_COMPARE(activity.text(), QString("Working…")); QVERIFY(spinner.isVisible()); session.setIsProjectBusy(false); QVERIFY(!spinner.isVisible()); QCOMPARE(view.findChild("statusBar")->height(), 30); // At the narrowest the hint gives way, not the numbers. session.selectTool(NavigationTool::marquee); view.resize(800, 520); QTest::qWait(30); QVERIFY(size.width() >= size.sizeHint().width() && colour.width() >= colour.sizeHint().width()); QVERIFY(activity.width() < activity.sizeHint().width()); QVERIFY(activity.geometry().right() <= view.width() - 18); // The hint keeps its dim tone under any palette. QPalette loud = activity.palette(); loud.setColor(QPalette::PlaceholderText, QColor(200, 20, 20)); loud.setColor(QPalette::WindowText, QColor(20, 20, 200)); activity.setPalette(loud); const QImage tinted = activity.grab().toImage(); int red = 0, blue = 0; for (int y = 0; y < tinted.height(); ++y) { for (int x = 0; x < tinted.width(); ++x) { const QColor pixel = tinted.pixelColor(x, y); red += pixel.red() > 150 && pixel.blue() < 100; blue += pixel.blue() > 150 && pixel.red() < 100; } } QVERIFY2(red > 100 && blue == 0, qPrintable(QString("%1 %2").arg(red).arg(blue))); activity.setPalette(QPalette()); // What it paints there is the hint ending in dots. const auto painted = [&](const QString &words) { QImage image(activity.size(), QImage::Format_RGB32); image.fill(activity.palette().color(QPalette::Window)); QPainter painter(&image); painter.setFont(activity.font()); painter.setPen(activity.palette().color(QPalette::PlaceholderText)); painter.drawText(image.rect(), Qt::AlignRight | Qt::AlignVCenter, words); return image; }; // Tones differ a little between paint paths; letters do not. const auto apart = [&](const QImage &one, const QImage &other) { int count = 0; for (int y = 0; y < one.height(); ++y) { for (int x = 0; x < one.width(); ++x) count += std::abs(qRed(one.pixel(x, y)) - qRed(other.pixel(x, y))) > 64; } return count; }; const QString cut = activity.fontMetrics().elidedText(activity.text(), Qt::ElideRight, activity.width()); QVERIFY(cut.endsWith(QChar(0x2026)) && cut != activity.text()); const QImage narrow = activity.grab().toImage().convertToFormat(QImage::Format_RGB32); QVERIFY2(apart(narrow, painted(cut)) < 20 && apart(narrow, painted(activity.text())) > 200, qPrintable(QString("%1 %2").arg(apart(narrow, painted(cut))).arg(apart(narrow, painted(activity.text()))))); view.resize(2000, 520); QTest::qWait(30); QVERIFY(activity.width() >= activity.sizeHint().width()); QVERIFY(apart(activity.grab().toImage().convertToFormat(QImage::Format_RGB32), painted(activity.text())) < 20); } void ContentViewStatusTests::everyToolHasItsHint() { const std::pair hints[] = { {NavigationTool::move, "Drag to move · Handles to resize · Circle to rotate · 1–0 layer opacity · Space to pan"}, {NavigationTool::marquee, "Drag a rectangle · Shift add · Alt subtract · Shift again mid-drag square · Drag inside to move · Ctrl-drag moves pixels · Delete clears · Ctrl+D deselect"}, {NavigationTool::lasso, "Drag to select · Drag inside to move · Shift add · Alt subtract · Delete clears · Alt+Backspace/Ctrl+Backspace fill · Ctrl+D deselect"}, {NavigationTool::wand, "Click to select similar colors · Tab for Object · Shift add · Alt subtract · Drag inside to move · Ctrl-drag moves pixels · Delete clears · Ctrl+D deselect"}, {NavigationTool::crop, "Drag to crop · Enter apply · Escape cancel · Space to pan"}, {NavigationTool::brush, "Drag to paint · [ ] size · Shift-[ ] hardness · 1–0 opacity · Escape cancel · Space to pan"}, {NavigationTool::spotHealing, "Drag over blemishes to heal · [ ] size · Shift-[ ] hardness · Escape cancel · Space to pan"}, {NavigationTool::cloneStamp, "Alt-click to set the source · Drag to clone · [ ] size · Shift-[ ] hardness · 1–0 opacity · Space to pan"}, {NavigationTool::blur, "Drag to push pixels · [ ] size · Shift-[ ] hardness · 1–0 strength · Space to pan"}, {NavigationTool::gradient, "Drag to draw · Drag ends to adjust · Shift 45° · 1–0 opacity · Enter apply · Escape cancel"}, {NavigationTool::shape, "Drag to draw a shape on a new layer · Shift square · Alt from center · Shift-U or Tab for the next shape · Escape cancel · Space to pan"}, {NavigationTool::type, "Drag a text box · Click text to edit · Drag box handles to resize · Ctrl+Return finish · Escape cancel"}, {NavigationTool::eyedropper, "Click to zoom in · Alt-click to zoom out · Drag right or left to zoom smoothly · Space to pan"}, {NavigationTool::hand, "Drag to pan · Pinch to zoom"}, {NavigationTool::zoom, "Click to zoom in · Alt-click to zoom out · Drag right or left to zoom smoothly · Space to pan"}, {NavigationTool::idle, "No tool selected · Press a tool's key to pick one · Space to pan"}, }; // Another tool's kind leaves a tool's words alone. QCOMPARE(ContentView::hint(NavigationTool::move, ToolIconKind::eraser), ContentView::hint(NavigationTool::move)); QCOMPARE(ContentView::hint(NavigationTool::lasso, ToolIconKind::ellipse), ContentView::hint(NavigationTool::lasso)); QCOMPARE(ContentView::hint(NavigationTool::marquee, ToolIconKind::polygonal), ContentView::hint(NavigationTool::marquee)); QCOMPARE(ContentView::hint(NavigationTool::lasso, ToolIconKind::object), ContentView::hint(NavigationTool::lasso)); EditorSession session; session.createDocument(8, 8); ContentView view(session); for (const auto &[tool, words] : hints) { session.selectTool(tool); QCOMPARE(find(view, "activityStatus").text(), QString::fromUtf8(words)); } // Magic's words follow its mode. session.selectTool(NavigationTool::wand); session.setWandMode(WandMode::object); QCOMPARE(find(view, "activityStatus").text(), QString("Click an object to select its outline · Tab for Wand · Shift add · Alt subtract · Drag inside to move · Ctrl-drag moves pixels · Delete clears · Ctrl+D deselect")); // The Shape's Shift follows its kind. session.selectTool(NavigationTool::shape); session.setShapeKind(ShapeKind::ellipse); QCOMPARE(find(view, "activityStatus").text(), QString("Drag to draw a shape on a new layer · Shift circle · Alt from center · Shift-U or Tab for the next shape · Escape cancel · Space to pan")); session.setShapeKind(ShapeKind::line); QVERIFY(find(view, "activityStatus").text().contains(QString("· Shift 45° ·"))); QCOMPARE(ContentView::hint(NavigationTool::move, ToolIconKind::plain, BlurToolMode::liquify, ShapeKind::line), ContentView::hint(NavigationTool::move)); } void ContentViewStatusTests::zoomPrintsAsSwiftPrintsIt() { QCOMPARE(ContentView::percent(1), QString("100%")); QCOMPARE(ContentView::percent(0.125), QString("12.5%")); QCOMPARE(ContentView::percent(1.0 / 3), QString("33.3%")); QCOMPARE(ContentView::percent(0.001), QString("0.1%")); QCOMPARE(ContentView::percent(16), QString("1,600%")); QCOMPARE(ContentView::percent(32), QString("3,200%")); QCOMPARE(ContentView::percent(0.9996), QString("100%")); } QTEST_MAIN(ContentViewStatusTests) #include "ContentViewStatusTests.moc"