Files
OmaPhoto/tests/ColorPickerSheetTests.cpp
雷电芽衣andClaude c864304695
Release / deb (push) Canceled after 0s
Release / rpm (push) Canceled after 0s
Release / arch (push) Canceled after 0s
Release / publish (push) Canceled after 0s
OmaPhoto 1.0.0: the Linux port of Compositor
A layered image editor for Linux and Omarchy, ported from Compositor
(github.com/robbietilton/Compositor), whose Swift source is its
specification. It reads and writes Compositor's .comp projects.

Packages for Ubuntu, Fedora and Arch come from the release workflow;
scripts/install/ holds one install script per distribution family,
and flake.nix serves NixOS.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-26 00:44:52 -04:00

263 lines
13 KiB
C++

#include "ColorPaletteFixtures.h"
#include "UI/ColorPickerSheet.h"
#include <QAccessible>
#include <QMenu>
// Swift's ColorPickerSheet: field, strip, preview, entries, keys.
class ColorPickerSheetTests : public QObject {
Q_OBJECT
private slots:
void theFieldAndStripSetTheWorkingColour();
void theFieldStripAndPreviewDrawAsSwifts();
void theEntriesTypeAndStep();
void entriesKeepTheirBoundsAndTheirTyping();
void returnAppliesWhereEscapeAndClosingCancel();
void everyControlCarriesSwiftsLabel();
};
void ColorPickerSheetTests::theFieldAndStripSetTheWorkingColour()
{
Palette shown;
openForeground(shown);
QWidget &field = inPanel<QWidget>("saturationBrightness"), &strip = inPanel<QWidget>("hueStrip");
QCOMPARE(field.size(), QSize(256, 256));
QCOMPARE(strip.size(), QSize(34, 256));
QTest::mousePress(&field, Qt::LeftButton, Qt::NoModifier, QPoint(300, -10));
QTest::mouseRelease(&field, Qt::LeftButton, Qt::NoModifier, QPoint(300, -10));
QCOMPARE(shown.session.colorPicker().value().color().hex(), QString("FF0000"));
QTest::mousePress(&strip, Qt::LeftButton, Qt::NoModifier, QPoint(17, 128));
QCOMPARE(shown.session.colorPicker().value().color().hex(), QString("00FFFF"));
QMouseEvent drag(QEvent::MouseMove, QPointF(17, 400), QPointF(17, 400), Qt::NoButton, Qt::LeftButton, Qt::NoModifier);
QApplication::sendEvent(&strip, &drag);
QCOMPARE(shown.session.colorPicker().value().hsb.hue, 0.0);
QTest::mouseRelease(&strip, Qt::LeftButton, Qt::NoModifier, QPoint(17, 400));
// The other buttons pick nothing, as Swift's drag.
QTest::mousePress(&strip, Qt::RightButton, Qt::NoModifier, QPoint(17, 128));
QMouseEvent right(QEvent::MouseMove, QPointF(17, 64), QPointF(17, 64), Qt::NoButton, Qt::RightButton, Qt::NoModifier);
QApplication::sendEvent(&strip, &right);
QTest::mouseRelease(&strip, Qt::RightButton, Qt::NoModifier, QPoint(17, 64));
QTest::mouseClick(&field, Qt::RightButton, Qt::NoModifier, QPoint(0, 255));
QCOMPARE(shown.session.colorPicker().value().color().hex(), QString("FF0000"));
// Half down the field: half as bright.
QTest::mouseClick(&field, Qt::LeftButton, Qt::NoModifier, QPoint(256, 128));
QCOMPARE(shown.session.colorPicker().value().color().hex(), QString("800000"));
QCOMPARE(middle(inPanel<QWidget>("newColor")), QColor(128, 0, 0));
// A pick, field or strip, replaces a channel's typing.
QLineEdit &red = inPanel<QLineEdit>("r"), &green = inPanel<QLineEdit>("g");
type(red, QStringLiteral("12"));
QTest::mouseClick(&field, Qt::LeftButton, Qt::NoModifier, QPoint(256, 0));
QVERIFY(red.hasFocus() && red.text() == "255" && !red.isModified());
type(red, QStringLiteral("12"));
QTest::mouseClick(&strip, Qt::LeftButton, Qt::NoModifier, QPoint(17, 128));
QVERIFY(red.text() == "0" && !red.isModified());
green.setFocus();
QCOMPARE(shown.session.colorPicker().value().color().hex(), QString("00FFFF"));
}
void ColorPickerSheetTests::theFieldStripAndPreviewDrawAsSwifts()
{
Palette shown;
openForeground(shown);
// A colour from elsewhere repaints the field, strip and preview.
QWidget &sheet = *panel()->findChild<ColorPickerSheet *>(), &stripWidget = inPanel<QWidget>("hueStrip");
PaintSpy fieldPaints(inPanel<QWidget>("saturationBrightness")), stripPaints(stripWidget), previewPaints(inPanel<QWidget>("newColor")),
sheetPaints(sheet);
shown.session.setColorPickerHSB(PickerHSB(0, 0.5, 0.25));
QTRY_VERIFY(fieldPaints.count > 0 && stripPaints.count > 0 && previewPaints.count > 0);
// The arrows' room too: five points past either end.
QTRY_VERIFY(sheetPaints.painted.contains(stripWidget.geometry().adjusted(0, -5, 0, 5)));
// White to the hue across, to black down, framed.
const QImage field = inPanel<QWidget>("saturationBrightness").grab().toImage();
QVERIFY(field.pixelColor(2, 2).lightness() > 245);
const QColor hue = field.pixelColor(253, 2);
QVERIFY2(hue.red() > 245 && hue.green() < 12 && hue.blue() < 12, qPrintable(hue.name()));
QVERIFY(field.pixelColor(128, 253).lightness() < 10);
QVERIFY(field.pixelColor(128, 0).lightness() < field.pixelColor(128, 2).lightness());
// The marker: white ring, black ring, at the colour.
QVERIFY(field.pixelColor(133, 192).lightness() > 200);
QVERIFY(field.pixelColor(134, 192).lightness() < field.pixelColor(138, 192).lightness());
// Red, magenta, cyan, red down the strip; arrows mark hues.
shown.session.setColorPickerHSB(PickerHSB(90, 1, 1));
const QImage strip = inPanel<QWidget>("hueStrip").grab().toImage();
for (const auto &[y, turn] : {std::pair(2, 0), std::pair(43, 300), std::pair(128, 180), std::pair(253, 0)}) {
const QColor shade = strip.pixelColor(17, y);
const int off = std::abs(shade.hsvHue() - turn) % 360;
QVERIFY2(std::min(off, 360 - off) <= 8 && shade.hsvSaturation() == 255, qPrintable(shade.name()));
}
// The sheet draws the arrows, past the strip's ends too.
const QColor ink = panel()->palette().color(QPalette::WindowText);
const auto arrows = [&sheet, &stripWidget](QPoint at) { return sheet.grab().toImage().pixelColor(stripWidget.geometry().topLeft() + at); };
QCOMPARE(arrows(QPoint(2, 192)), ink);
QCOMPARE(arrows(QPoint(31, 192)), ink);
QVERIFY(arrows(QPoint(2, 64)) != ink);
// Antialiased: an edge pixel a quarter covered is neither.
const QColor edge = arrows(QPoint(2, 188));
QVERIFY2(edge != ink && edge != arrows(QPoint(2, 64)), qPrintable(edge.name()));
QVERIFY(strip.pixelColor(7, 192) != strip.pixelColor(8, 192));
// The preview is the new colour, rimmed in 60% black.
const QImage preview = inPanel<QWidget>("newColor").grab().toImage();
QCOMPARE(preview.pixelColor(32, 32), QColor(128, 255, 0));
const QColor rim = preview.pixelColor(32, 0);
QVERIFY2(std::abs(rim.red() - 51) <= 2 && std::abs(rim.green() - 102) <= 2, qPrintable(rim.name()));
// At either end the arrows reach past the strip.
for (const auto &[turn, y] : {std::pair(360.0, -3), std::pair(0.0, 258)}) {
shown.session.setColorPickerHSB(PickerHSB(turn, 1, 1));
QCOMPARE(arrows(QPoint(1, y)), ink);
QCOMPARE(arrows(QPoint(32, y)), ink);
}
}
void ColorPickerSheetTests::theEntriesTypeAndStep()
{
Palette shown;
openForeground(shown);
QLineEdit &red = inPanel<QLineEdit>("r"), &green = inPanel<QLineEdit>("g"), &hex = inPanel<QLineEdit>("hex");
QCOMPARE(red.text(), QString("0"));
QCOMPARE(hex.text(), QString("000000"));
// Past 255 is 255; leaving applies it.
type(red, QStringLiteral("300"));
green.setFocus();
QCOMPARE(shown.session.colorPicker().value().color().hex(), QString("FF0000"));
QCOMPARE(red.text(), QString("255"));
QCOMPARE(hex.text(), QString("FF0000"));
// The arrows step: one, ten with Shift.
QTest::keyClick(&green, Qt::Key_Up);
QTest::keyClick(&green, Qt::Key_Up, Qt::ShiftModifier);
QTest::keyClick(&green, Qt::Key_Down);
QCOMPARE(green.text(), QString("10"));
QCOMPARE(shown.session.colorPicker().value().color().hex(), QString("FF0A00"));
// Empty or wild text puts the colour's own back.
type(green, QString());
hex.setFocus();
QCOMPARE(green.text(), QString("10"));
type(hex, QStringLiteral("zzz"));
red.setFocus();
QCOMPARE(hex.text(), QString("FF0A00"));
type(hex, QStringLiteral("#00f"));
red.setFocus();
QCOMPARE(shown.session.colorPicker().value().color().hex(), QString("0000FF"));
QCOMPARE(red.text(), QString("0"));
// Typing stays while the colour changes elsewhere.
type(red, QStringLiteral("12"));
shown.session.setColorPickerHSB(PickerHSB(120, 1, 1));
QCOMPARE(red.text(), QString("12"));
QCOMPARE(hex.text(), QString("00FF00"));
type(hex, QStringLiteral("12"));
shown.session.setColorPickerHSB(PickerHSB(240, 1, 1));
QCOMPARE(hex.text(), QString("12"));
// A step after typing shows the stepped number.
type(red, QStringLiteral("12"));
QTest::keyClick(&red, Qt::Key_Up);
QCOMPARE(red.text(), QString("1"));
// Blue takes its typing too.
QLineEdit &blue = inPanel<QLineEdit>("b");
type(blue, QStringLiteral("200"));
red.setFocus();
QCOMPARE(shown.session.colorPicker().value().color().hex(), QString("0100C8"));
}
void ColorPickerSheetTests::entriesKeepTheirBoundsAndTheirTyping()
{
Palette shown;
openForeground(shown);
QLineEdit &red = inPanel<QLineEdit>("r"), &green = inPanel<QLineEdit>("g"), &hex = inPanel<QLineEdit>("hex");
// More digits than a number holds read as the top.
type(red, QStringLiteral("99999999999999999999"));
green.setFocus();
QCOMPARE(red.text(), QString("255"));
QTest::keyClick(&red, Qt::Key_Up);
QCOMPARE(red.text(), QString("255"));
QTest::keyClick(&green, Qt::Key_Down, Qt::ShiftModifier);
QCOMPARE(green.text(), QString("0"));
// A menu or popup borrowing the focus keeps the typing.
type(green, QStringLiteral("200"));
for (const Qt::FocusReason reason : {Qt::PopupFocusReason, Qt::MenuBarFocusReason}) {
QFocusEvent borrowed(QEvent::FocusOut, reason);
QApplication::sendEvent(&green, &borrowed);
QCOMPARE(green.text(), QString("200"));
QCOMPARE(shown.session.colorPicker().value().color().hex(), QString("FF0000"));
}
red.setFocus();
QCOMPARE(shown.session.colorPicker().value().color().hex(), QString("FFC800"));
// A menu taking the focus keeps the typing through changes.
for (QLineEdit *entry : {&green, &hex}) {
type(*entry, QStringLiteral("99"));
QMenu menu;
menu.addAction(QStringLiteral("Nothing"));
menu.popup(entry->mapToGlobal(QPoint(0, 0)));
QTRY_VERIFY(QApplication::activePopupWidget());
shown.session.setColorPickerHSB(PickerHSB(240, 1, 1));
QCOMPARE(entry->text(), QString("99"));
menu.close();
}
// Letters never reach a channel.
type(red, QStringLiteral("ab"));
QCOMPARE(red.text(), QString());
green.setFocus();
QCOMPARE(red.text(), QString("0"));
// An entry left untouched leaves the working colour exact.
const PickerHSB exact(10.3, 0.5, 0.5);
shown.session.setColorPickerHSB(exact);
green.setFocus();
red.setFocus();
QVERIFY(shown.session.colorPicker().value().hsb == exact);
// The hex commits on each leave, as Swift's: 8 bits.
hex.setFocus();
red.setFocus();
QVERIFY(!(shown.session.colorPicker().value().hsb == exact));
QCOMPARE(shown.session.colorPicker().value().color(), PickerHSB(exact).rgb().quantized());
}
void ColorPickerSheetTests::returnAppliesWhereEscapeAndClosingCancel()
{
Palette shown;
openForeground(shown);
type(inPanel<QLineEdit>("r"), QStringLiteral("128"));
QTest::keyClick(&inPanel<QLineEdit>("r"), Qt::Key_Return);
QVERIFY(!panel());
QCOMPARE(shown.session.foregroundColor().hex(), QString("800000"));
openForeground(shown);
type(inPanel<QLineEdit>("hex"), QStringLiteral("00ff00"));
QTest::keyClick(&inPanel<QLineEdit>("hex"), Qt::Key_Escape);
QVERIFY(!panel());
QVERIFY(!shown.session.colorPicker());
QCOMPARE(shown.session.foregroundColor().hex(), QString("800000"));
QTest::mouseClick(&shown.button("backgroundSwatch"), Qt::LeftButton);
shown.session.setColorPickerHSB(PickerHSB(240, 1, 1));
panel()->close();
QVERIFY(!shown.session.colorPicker());
QCOMPARE(shown.session.backgroundColor(), PaletteColor::white());
// Return on a focused Cancel still means OK, the default.
openForeground(shown);
shown.session.setColorPickerHSB(PickerHSB(120, 1, 1));
QPushButton &cancel = inPanel<QPushButton>("pickerCancel");
cancel.setFocus();
QTest::keyClick(&cancel, Qt::Key_Return);
QVERIFY(!panel());
QCOMPARE(shown.session.foregroundColor(), (PaletteColor{0, 1, 0}));
}
void ColorPickerSheetTests::everyControlCarriesSwiftsLabel()
{
Palette shown;
openForeground(shown);
// What a screen reader reads, as Swift labels it.
const auto text = [](const char *name, QAccessible::Text kind) { return QAccessible::queryAccessibleInterface(&inPanel<QWidget>(name))->text(kind); };
for (const auto &[name, label] : {std::pair("saturationBrightness", "Saturation and brightness"), std::pair("hueStrip", "Hue"),
std::pair("newColor", "New color"), std::pair("r", "Red"), std::pair("g", "Green"), std::pair("b", "Blue"),
std::pair("hex", "Hex color")})
QCOMPARE(text(name, QAccessible::Name), QString(label));
// Swift's titles show while an entry is empty.
for (const auto &[name, title] : {std::pair("r", "R"), std::pair("g", "G"), std::pair("b", "B"), std::pair("hex", "Hex")})
QCOMPARE(inPanel<QLineEdit>(name).placeholderText(), QString(title));
// The hue's whole degrees, rounded, follow the colour.
QCOMPARE(text("hueStrip", QAccessible::Description), QString("0 degrees"));
shown.session.setColorPickerHSB(PickerHSB(120.5, 1, 1));
QCOMPARE(text("hueStrip", QAccessible::Description), QString("121 degrees"));
shown.session.setColorPickerHSB(PickerHSB(359.4, 1, 1));
QCOMPARE(text("hueStrip", QAccessible::Description), QString("359 degrees"));
}
QTEST_MAIN(ColorPickerSheetTests)
#include "ColorPickerSheetTests.moc"