14.3: New Canvas in inches, cm or mm at 72 or 300 DPI, transparent, white or black, every setting a pill

This commit is contained in:
ZacharyZhang-NY committed 2026-10-11 03:02:29 -04:00
1 parent c4c6307d0e
commit f8c1332c29
12 files changed
+521 -38

No files matched your search

+125 -9
View File
@@ -11,8 +11,16 @@ namespace {
struct Sheet {
EditorSession session;
QList<QSize> created;
QList<double> resolutions;
QList<std::optional<QColor>> backgrounds;
int opened = 0;
NewCanvasSheet sheet{session, [this](int width, int height) { created << QSize(width, height); }, [this] { opened += 1; }};
NewCanvasSheet sheet{session,
[this](int width, int height, double resolution, std::optional<QColor> background) {
created << QSize(width, height);
resolutions << resolution;
backgrounds << background;
},
[this] { opened += 1; }};
QLineEdit &width = *sheet.findChild<QLineEdit *>("widthInput");
QLineEdit &height = *sheet.findChild<QLineEdit *>("heightInput");
QLabel &note = *sheet.findChild<QLabel *>("canvasNote");
@@ -71,6 +79,8 @@ private slots:
void presetsAreSwiftsGroups();
void aPresetFillsTheFieldsAndTheMatchIsChecked();
void theMoreButtonDrawsThreeDotsAndOpensTheMenu();
void thePillsStepThroughTheirChoices();
void printUnitsWriteTheSameCanvas();
};
void NewCanvasSheetTests::cleanup()
@@ -85,12 +95,13 @@ void NewCanvasSheetTests::itStartsAtFullHighDefinition()
QVERIFY(QTest::qWaitForWindowExposed(&fixture.sheet));
QCOMPARE(fixture.width.text(), QString("1920"));
QCOMPARE(fixture.height.text(), QString("1080"));
QCOMPARE(fixture.note.text(), QString("Transparent canvas · sRGB"));
QCOMPARE(fixture.note.foregroundRole(), QPalette::PlaceholderText);
QVERIFY(fixture.note.isHidden());
QVERIFY(fixture.create.isEnabled() && fixture.create.isDefault() && fixture.width.hasFocus());
QCOMPARE(fixture.sheet.width(), 500);
QTest::mouseClick(&fixture.create, Qt::LeftButton);
QCOMPARE(fixture.created, (QList<QSize>{QSize(1920, 1080)}));
QCOMPARE(fixture.resolutions, QList<double>{72});
QCOMPARE(fixture.backgrounds, QList<std::optional<QColor>>{std::nullopt});
}
void NewCanvasSheetTests::onlyWholeNumbersInRangeMayCreate()
@@ -110,7 +121,8 @@ void NewCanvasSheetTests::onlyWholeNumbersInRangeMayCreate()
QVERIFY(valid(" 640 ", "480"));
// Either field alone refuses, by button, Return and Enter.
const auto refused = [&] {
const bool warned = fixture.note.text() == "Enter whole numbers from 1 to 30,000 pixels." && fixture.note.foregroundRole() == QPalette::BrightText;
const bool warned = fixture.note.text() == "Enter whole numbers from 1 to 30,000 pixels." && fixture.note.foregroundRole() == QPalette::BrightText
&& !fixture.note.isHidden();
fixture.create.click();
QTest::keyClick(&fixture.width, Qt::Key_Return);
QTest::keyClick(&fixture.height, Qt::Key_Enter);
@@ -118,13 +130,16 @@ void NewCanvasSheetTests::onlyWholeNumbersInRangeMayCreate()
QTest::keyClick(&fixture.sheet, Qt::Key_Return);
return warned && fixture.created.isEmpty();
};
for (const QString &bad : {QString("0"), QString("30001"), QString(""), QString("12.5"), QString("-4"), QString("1e3"), QString("wide")}) {
// Swift's Double(text): whole numbers written any way pass.
QVERIFY(valid("1e3", "480.0") && valid("6,4e2", "480"));
for (const QString &bad : {QString("0"), QString("30001"), QString(""), QString("12.5"), QString("-4"), QString("3e4.5"), QString("wide"),
QString("inf"), QString("nan"), QString("\n640"), QString("640\n")}) {
QVERIFY2(!valid(bad, "480") && refused(), qPrintable(bad));
QVERIFY2(!valid("640", bad) && refused(), qPrintable(bad));
}
QVERIFY(fixture.created.isEmpty() && fixture.opened == 0);
QVERIFY(valid("640", "480"));
QCOMPARE(fixture.note.text(), QString("Transparent canvas · sRGB"));
QVERIFY(fixture.note.isHidden());
}
void NewCanvasSheetTests::returnInEitherFieldCreates()
@@ -211,7 +226,7 @@ void NewCanvasSheetTests::theFirstTabSkipsTheClipboardOnce()
QCOMPARE(first.width.text(), QString("1920"));
QVERIFY(!first.session.skipsInitialClipboardCanvasSize);
// The session's next sheet reads the clipboard again.
NewCanvasSheet later(first.session, [](int, int) {}, [] {});
NewCanvasSheet later(first.session, [](int, int, double, std::optional<QColor>) {}, [] {});
later.show();
QVERIFY(QTest::qWaitForWindowExposed(&later));
QCOMPARE(later.findChild<QLineEdit *>("widthInput")->text(), QString("640"));
@@ -290,12 +305,14 @@ void NewCanvasSheetTests::aPresetFillsTheFieldsAndTheMatchIsChecked()
fixture.entry("Studio Display").trigger();
QCOMPARE(fixture.width.text() + "x" + fixture.height.text(), QString("5120x2880"));
QCOMPARE(fixture.checked(), QString("Studio Display"));
// A size no preset has is Custom, matched as text.
// No preset has it: Custom. Presets match by pixels.
fixture.height.setText("2881");
QCOMPARE(fixture.checked(), QString("Custom"));
fixture.height.setText("2880");
QCOMPARE(fixture.checked(), QString("Studio Display"));
fixture.width.setText("05120");
fixture.width.setText("05120.0");
QCOMPARE(fixture.checked(), QString("Studio Display"));
fixture.width.setText("5121");
QCOMPARE(fixture.checked(), QString("Custom"));
fixture.width.setText("1080");
fixture.height.setText("608");
@@ -340,5 +357,104 @@ void NewCanvasSheetTests::theMoreButtonDrawsThreeDotsAndOpensTheMenu()
QVERIFY(shown);
}
void NewCanvasSheetTests::thePillsStepThroughTheirChoices()
{
Sheet fixture;
fixture.sheet.show();
QVERIFY(QTest::qWaitForWindowExposed(&fixture.sheet));
QAbstractButton &background = *fixture.sheet.findChild<QAbstractButton *>("canvasBackground");
QAbstractButton &unit = *fixture.sheet.findChild<QAbstractButton *>("canvasUnit");
QAbstractButton &resolution = *fixture.sheet.findChild<QAbstractButton *>("canvasResolution");
QCOMPARE(background.text(), QString("Transparent canvas"));
QCOMPARE(unit.text(), QString("Pixels"));
QCOMPARE(resolution.text(), QString("72 DPI"));
QCOMPARE(background.toolTip(), QString("Start see-through, or with a white or black Background layer. Click to switch."));
QCOMPARE(unit.toolTip(), QString("Units: pixels, inches, centimeters or millimeters. Click to switch."));
QCOMPARE(resolution.toolTip(), QString("Resolution: 72 for screens, 300 for print. Click to switch."));
// Each pill steps to the next and back round.
QTest::mouseClick(&background, Qt::LeftButton);
QCOMPARE(background.text(), QString("White canvas"));
fixture.create.click();
QTest::mouseClick(&background, Qt::LeftButton);
QCOMPARE(background.text(), QString("Black canvas"));
QTest::mouseClick(&resolution, Qt::LeftButton);
QCOMPARE(resolution.text(), QString("300 DPI"));
fixture.create.click();
QTest::mouseClick(&background, Qt::LeftButton);
QTest::mouseClick(&resolution, Qt::LeftButton);
QCOMPARE(background.text(), QString("Transparent canvas"));
QCOMPARE(resolution.text(), QString("72 DPI"));
fixture.create.click();
QCOMPARE(fixture.created, (QList<QSize>{QSize(1920, 1080), QSize(1920, 1080), QSize(1920, 1080)}));
QCOMPARE(fixture.resolutions, (QList<double>{72, 300, 72}));
QCOMPARE(fixture.backgrounds, (QList<std::optional<QColor>>{QColor(255, 255, 255), QColor(0, 0, 0), std::nullopt}));
// A pill is text alone; hovered, a capsule shows.
const QImage plain = background.grab().toImage();
QTest::mouseMove(&background);
QTRY_VERIFY(background.underMouse());
background.repaint();
QVERIFY(background.grab().toImage() != plain);
}
void NewCanvasSheetTests::printUnitsWriteTheSameCanvas()
{
Sheet fixture;
fixture.sheet.show();
QVERIFY(QTest::qWaitForWindowExposed(&fixture.sheet));
QAbstractButton &unit = *fixture.sheet.findChild<QAbstractButton *>("canvasUnit");
QAbstractButton &resolution = *fixture.sheet.findChild<QAbstractButton *>("canvasResolution");
QStringList labels;
for (const QLabel *label : fixture.sheet.findChildren<QLabel *>())
labels << label->text();
QCOMPARE(labels.count("px"), qsizetype(2));
unit.click();
QCOMPARE(unit.text(), QString("Inches"));
QCOMPARE(fixture.width.text(), QString("26.67"));
QCOMPARE(fixture.height.text(), QString("15"));
labels.clear();
for (const QLabel *label : fixture.sheet.findChildren<QLabel *>())
labels << label->text();
QCOMPARE(labels.count("in"), qsizetype(2));
QCOMPARE(fixture.checked(), QString("1080p"));
fixture.create.click();
// At 300 DPI the same inches make more pixels.
resolution.click();
QCOMPARE(resolution.toolTip(), QString("Resolution: 72 for screens, 300 for print. Click to switch. · 300 DPI: 8001 × 4500 pixels"));
QCOMPARE(fixture.checked(), QString("Custom"));
fixture.create.click();
QCOMPARE(fixture.created, (QList<QSize>{QSize(1920, 1080), QSize(8001, 4500)}));
// A preset is written in the unit in use.
fixture.entry("Instagram Square").trigger();
QCOMPARE(fixture.width.text(), QString("3.6"));
QCOMPARE(fixture.height.text(), QString("3.6"));
QCOMPARE(fixture.checked(), QString("Instagram Square"));
// With the height unreadable, a change of unit rewrites neither.
fixture.height.setText("tall");
unit.click();
QCOMPARE(fixture.width.text() + "x" + fixture.height.text(), QString("3.6xtall"));
unit.click();
unit.click();
unit.click();
QCOMPARE(unit.text(), QString("Inches"));
fixture.height.setText("3.6");
fixture.width.setText("200");
QVERIFY(!fixture.create.isEnabled() && !fixture.note.isHidden());
QCOMPARE(fixture.note.text(), QString("Enter a size up to 30,000 pixels at this DPI."));
// One side unreadable: a unit change rewrites neither.
unit.click();
QCOMPARE(unit.text(), QString("Centimeters"));
QCOMPARE(fixture.width.text(), QString("200"));
QCOMPARE(fixture.height.text(), QString("3.6"));
fixture.width.setText("30.48");
unit.click();
unit.click();
QCOMPARE(unit.text(), QString("Pixels"));
// The height's 3.6 was read in centimeters: 425 pixels.
QCOMPARE(fixture.width.text(), QString("3600"));
QCOMPARE(fixture.height.text(), QString("425"));
QCOMPARE(resolution.toolTip(), QString("Resolution: 72 for screens, 300 for print. Click to switch."));
QVERIFY(fixture.note.isHidden());
}
QTEST_MAIN(NewCanvasSheetTests)
#include "NewCanvasSheetTests.moc"
+103
View File
@@ -0,0 +1,103 @@
#include "UI/NewCanvasSheet.h"
#include <QtTest>
// Swift's NewCanvasUnitTests: units, DPI and the first layer.
class NewCanvasUnitTests : public QObject {
Q_OBJECT
private slots:
void printSizesBecomePixelsAtTheDPI();
void unitsRoundTripAndCycle();
void newProjectKeepsItsResolution();
void backgroundFillsTheFirstLayer();
};
void NewCanvasUnitTests::printSizesBecomePixelsAtTheDPI()
{
// US Letter at 300 DPI is 2550 × 3300.
QCOMPARE(pixels(NewCanvasUnit::inches, "8.5", 300), std::optional(2550));
QCOMPARE(pixels(NewCanvasUnit::inches, "11", 300), std::optional(3300));
QCOMPARE(pixels(NewCanvasUnit::millimeters, "210", 300), std::optional(2480));
QCOMPARE(pixels(NewCanvasUnit::centimeters, "2,54", 72), std::optional(72));
QCOMPARE(pixels(NewCanvasUnit::pixels, "1920", 300), std::optional(1920));
QCOMPARE(pixels(NewCanvasUnit::pixels, "19.5", 72), std::nullopt);
QCOMPARE(pixels(NewCanvasUnit::inches, "200", 300), std::nullopt);
QCOMPARE(pixels(NewCanvasUnit::inches, "0", 300), std::nullopt);
// Rounded to the nearest pixel, halves away from zero.
QCOMPARE(pixels(NewCanvasUnit::inches, "0.005", 300), std::optional(2));
QCOMPARE(pixels(NewCanvasUnit::inches, "0.0049", 300), std::optional(1));
QCOMPARE(pixels(NewCanvasUnit::inches, "0.0016", 300), std::nullopt);
QCOMPARE(pixels(NewCanvasUnit::inches, " 1\t", 72), std::optional(72));
QCOMPARE(pixels(NewCanvasUnit::inches, "\t\u00a01", 72), std::optional(72));
// Infinity and NaN fail the range and the whole number.
QCOMPARE(pixels(NewCanvasUnit::inches, "inf", 72), std::nullopt);
QCOMPARE(pixels(NewCanvasUnit::pixels, "nan", 72), std::nullopt);
QCOMPARE(pixels(NewCanvasUnit::pixels, "30000", 72), std::optional(30000));
QCOMPARE(pixels(NewCanvasUnit::pixels, "30001", 72), std::nullopt);
}
void NewCanvasUnitTests::unitsRoundTripAndCycle()
{
QCOMPARE(text(NewCanvasUnit::inches, 1920, 72), QString("26.67"));
QCOMPARE(text(NewCanvasUnit::inches, 2550, 300), QString("8.5"));
QCOMPARE(text(NewCanvasUnit::inches, 3000, 300), QString("10"));
QCOMPARE(text(NewCanvasUnit::millimeters, 2480, 300), QString("209.97"));
QCOMPARE(text(NewCanvasUnit::centimeters, 30000, 72), QString("1058.33"));
QCOMPARE(text(NewCanvasUnit::pixels, 30000, 72), QString("30000"));
for (NewCanvasUnit unit : {NewCanvasUnit::inches, NewCanvasUnit::centimeters, NewCanvasUnit::millimeters})
QCOMPARE(pixels(unit, text(unit, 2550, 300), 300), std::optional(2550));
QCOMPARE(next(NewCanvasUnit::pixels), NewCanvasUnit::inches);
QCOMPARE(next(NewCanvasUnit::inches), NewCanvasUnit::centimeters);
QCOMPARE(next(NewCanvasUnit::centimeters), NewCanvasUnit::millimeters);
QCOMPARE(next(NewCanvasUnit::millimeters), NewCanvasUnit::pixels);
QCOMPARE(rawValue(NewCanvasUnit::millimeters), QString("mm"));
QCOMPARE(name(NewCanvasUnit::centimeters), QString("Centimeters"));
}
void NewCanvasUnitTests::newProjectKeepsItsResolution()
{
EditorSession session;
session.createNewProject(2550, 3300, 300);
QCOMPARE(session.document().value().resolution, 300.0);
QCOMPARE(session.projectSnapshot().value().manifest.resolution, std::optional(300.0));
// A resolution out of 1 to 9,600 is 72.
for (double odd : {0.5, 9601.0, qQNaN()}) {
session.createNewProject(10, 10, odd);
QCOMPARE(session.document().value().resolution, 72.0);
}
session.createNewProject(10, 10, 9600);
QCOMPARE(session.document().value().resolution, 9600.0);
session.createNewProject(10, 10, 1);
QCOMPARE(session.document().value().resolution, 1.0);
}
void NewCanvasUnitTests::backgroundFillsTheFirstLayer()
{
QCOMPARE(next(NewCanvasBackground::transparent), NewCanvasBackground::white);
QCOMPARE(next(NewCanvasBackground::white), NewCanvasBackground::black);
QCOMPARE(next(NewCanvasBackground::black), NewCanvasBackground::transparent);
QCOMPARE(title(NewCanvasBackground::white), QString("White canvas"));
for (const auto &[background, value] : {std::pair(NewCanvasBackground::white, 255), std::pair(NewCanvasBackground::black, 0)}) {
EditorSession session;
session.createNewProject(40, 30, 72, color(background));
const ImageLayer layer = session.document().value().layers.front();
QCOMPARE(layer.name, QString("Background"));
QCOMPARE(session.activeLayerID(), std::optional(layer.id));
const QImage image = layer.asset.value().image();
QCOMPARE(image.size(), QSize(40, 30));
QCOMPARE(image.format(), QImage::Format_RGBA8888_Premultiplied);
QCOMPARE(image.pixelColor(0, 0), QColor(value, value, value));
QCOMPARE(image.pixelColor(39, 29), QColor(value, value, value));
QCOMPARE(layer.asset.value().thumbnail.pixelColor(0, 0), QColor(value, value, value));
}
// A thumbnail, not the whole picture, for the panel.
EditorSession large;
large.createNewProject(400, 300, 72, color(NewCanvasBackground::black));
QCOMPARE(large.document().value().layers.front().asset.value().thumbnail.size(), QSize(96, 72));
EditorSession session;
session.createNewProject(40, 30, 72, color(NewCanvasBackground::transparent));
QCOMPARE(session.document().value().layers.front().name, QString("Layer 1"));
QVERIFY(!session.document().value().layers.front().asset);
}
QTEST_MAIN(NewCanvasUnitTests)
#include "NewCanvasUnitTests.moc"
+1 -1
View File
@@ -235,7 +235,7 @@ void RemappedKeysTests::everySheetBindsSwiftsKeys()
const QSet<QString> zooms{"Ctrl+K", "Ctrl+Q", "Ctrl+0", "Ctrl+1", "Ctrl+=", "Ctrl+-"};
QCOMPARE(keys(JPEGExportSheet(ExportRaster{red, 72}, session, [](std::optional<QByteArray>) {})), zooms);
QCOMPARE(keys(ColorPickerSheet(session, [](bool) {})), both);
QCOMPARE(keys(NewCanvasSheet(session, [](int, int) {}, [] {})), QSet<QString>{"Ctrl+K"});
QCOMPARE(keys(NewCanvasSheet(session, [](int, int, double, std::optional<QColor>) {}, [] {})), QSet<QString>{"Ctrl+K"});
QCOMPARE(keys(TransformInspector(session)), both);
QCOMPARE(keys(SelectionAmountSheet(session, SelectionAmountOperation::expand)), both);
session.addEffect(LayerEffectKind::stroke);
+1
View File
@@ -157,6 +157,7 @@ compositor_test(ScrubbableSheetsTests)
compositor_test(ScrubbableCameraRawTests)
compositor_test(ScrubbableTransformTests)
compositor_test(NewCanvasSheetTests)
compositor_test(NewCanvasUnitTests)
compositor_test(OmarchyThemeTests)
compositor_test(ProjectControllerTests)
compositor_test(ExternalChangeTests)