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>
180 lines
8.4 KiB
C++
180 lines
8.4 KiB
C++
#include "ColorPaletteFixtures.h"
|
|
#include "ContentView.h"
|
|
#include <QScrollArea>
|
|
|
|
// Swift's ColorPaletteControls under the rail.
|
|
class ColorPaletteControlsTests : public QObject {
|
|
Q_OBJECT
|
|
private slots:
|
|
void swatchesShowThePaletteAndOpenThePicker();
|
|
void swapAndDefaultsFollowTheirButtons();
|
|
void aMaskChoosesBlackOrWhite();
|
|
void aMaskEndsTheChoiceAndThePicker();
|
|
void aBusyProjectRestsThePalette();
|
|
void theRailHoldsThePaletteBelowItsTools();
|
|
};
|
|
|
|
void ColorPaletteControlsTests::swatchesShowThePaletteAndOpenThePicker()
|
|
{
|
|
Palette shown;
|
|
QAbstractButton &foreground = shown.button("foregroundSwatch"), &background = shown.button("backgroundSwatch");
|
|
QCOMPARE(shown.controls->size(), QSize(42, 42));
|
|
QCOMPARE(foreground.geometry(), QRect(3, 3, 24, 24));
|
|
QCOMPARE(background.geometry(), QRect(15, 15, 24, 24));
|
|
QCOMPARE(shown.button("swapColors").geometry(), QRect(30, 0, 12, 12));
|
|
QCOMPARE(shown.button("defaultColors").geometry(), QRect(2, 30, 12, 12));
|
|
QCOMPARE(middle(foreground), QColor(Qt::black));
|
|
QCOMPARE(middle(background), QColor(Qt::white));
|
|
// A white ring inside a black rim.
|
|
QVERIFY(foreground.grab().toImage().pixelColor(12, 1).lightness() > 200);
|
|
QCOMPARE(background.grab().toImage().pixelColor(12, 0).lightness(), 0);
|
|
QCOMPARE(foreground.toolTip(), QString("Foreground color"));
|
|
QCOMPARE(background.toolTip(), QString("Background color"));
|
|
// Each colour repaints its whole swatch; the two overlap.
|
|
QCoreApplication::processEvents();
|
|
PaintSpy front(foreground), back(background);
|
|
shown.session.setForegroundColor(PaletteColor{0, 0, 1});
|
|
QTRY_COMPARE(front.painted, foreground.rect());
|
|
shown.session.setBackgroundColor(PaletteColor{0, 0, 1});
|
|
QTRY_COMPARE(back.painted, background.rect());
|
|
shown.session.resetPaletteColors();
|
|
// A swatch opens the picker; the other takes it over.
|
|
QTest::mouseClick(&foreground, Qt::LeftButton);
|
|
QWidget *window = panel();
|
|
QVERIFY(window);
|
|
QCOMPARE(window->windowTitle(), QString("Color Picker (Foreground Color)"));
|
|
QTest::mouseClick(&background, Qt::LeftButton);
|
|
QCOMPARE(panel(), window);
|
|
QCOMPARE(window->windowTitle(), QString("Color Picker (Background Color)"));
|
|
QCOMPARE(shown.session.colorPicker().value().original, PaletteColor::white());
|
|
shown.session.setColorPickerHSB(PickerHSB(0, 1, 1));
|
|
QTest::mouseClick(&inPanel<QPushButton>("pickerCancel"), Qt::LeftButton);
|
|
QVERIFY(!shown.session.colorPicker());
|
|
QVERIFY(!panel());
|
|
QCOMPARE(shown.session.backgroundColor(), PaletteColor::white());
|
|
QTest::mouseClick(&foreground, Qt::LeftButton);
|
|
shown.session.setColorPickerHSB(PickerHSB(120, 1, 1));
|
|
QTest::mouseClick(&inPanel<QPushButton>("pickerOK"), Qt::LeftButton);
|
|
QVERIFY(!panel());
|
|
QCOMPARE(shown.session.foregroundColor(), (PaletteColor{0, 1, 0}));
|
|
QCOMPARE(middle(foreground), QColor(Qt::green));
|
|
}
|
|
|
|
void ColorPaletteControlsTests::swapAndDefaultsFollowTheirButtons()
|
|
{
|
|
Palette shown;
|
|
QAbstractButton &swap = shown.button("swapColors"), &defaults = shown.button("defaultColors");
|
|
QCOMPARE(swap.toolTip(), QString("Swap foreground and background (X)"));
|
|
QCOMPARE(swap.accessibleName(), QString("Swap colors"));
|
|
QCOMPARE(defaults.toolTip(), QString("Default colors (D)"));
|
|
QCOMPARE(defaults.accessibleName(), QString("Default colors"));
|
|
shown.session.setForegroundColor(PaletteColor{1, 0, 0});
|
|
QTest::mouseClick(&swap, Qt::LeftButton);
|
|
QCOMPARE(shown.session.foregroundColor(), PaletteColor::white());
|
|
QCOMPARE(shown.session.backgroundColor(), (PaletteColor{1, 0, 0}));
|
|
QCOMPARE(middle(shown.button("backgroundSwatch")), QColor(Qt::red));
|
|
QTest::mouseClick(&defaults, Qt::LeftButton);
|
|
QCOMPARE(shown.session.foregroundColor(), PaletteColor::black());
|
|
QCOMPARE(shown.session.backgroundColor(), PaletteColor::white());
|
|
// The glyphs take the secondary ink, dimmed when resting.
|
|
QPalette inks = shown.controls->palette();
|
|
inks.setColor(QPalette::Active, QPalette::PlaceholderText, Qt::red);
|
|
inks.setColor(QPalette::Inactive, QPalette::PlaceholderText, Qt::red);
|
|
inks.setColor(QPalette::Disabled, QPalette::PlaceholderText, Qt::blue);
|
|
shown.controls->setPalette(inks);
|
|
const auto ink = [](QWidget &glyph, QPoint at) { return glyph.grab().toImage().pixelColor(at).hsvHue(); };
|
|
// The swap's diagonal, the ring and the ring's head.
|
|
for (const auto &[glyph, at] : {std::pair(&swap, QPoint(4, 4)), std::pair(&defaults, QPoint(9, 6)), std::pair(&defaults, QPoint(7, 1))})
|
|
QCOMPARE(ink(*glyph, at), 0);
|
|
// Turned: the head's corner inked; a flat tip's spot bare.
|
|
QCOMPARE(ink(swap, QPoint(4, 2)), 0);
|
|
QCOMPARE(ink(swap, QPoint(1, 6)), -1);
|
|
shown.session.setIsProjectBusy(true);
|
|
QCOMPARE(ink(swap, QPoint(4, 4)), 240);
|
|
QCOMPARE(ink(defaults, QPoint(9, 6)), 240);
|
|
}
|
|
|
|
void ColorPaletteControlsTests::aMaskChoosesBlackOrWhite()
|
|
{
|
|
Palette shown;
|
|
shown.session.addMask();
|
|
QVERIFY(shown.session.isMaskSelected());
|
|
for (const auto &[name, heading, choice] : {std::tuple("foregroundSwatch", "Mask foreground", "White · Reveal"),
|
|
std::tuple("backgroundSwatch", "Mask background", "Black · Hide")}) {
|
|
QTest::mouseClick(&shown.button(name), Qt::LeftButton);
|
|
QWidget *popup = QApplication::activePopupWidget();
|
|
QVERIFY(popup);
|
|
QCOMPARE(popup->objectName(), QString("maskColorChoice"));
|
|
// Under the swatches; offscreen adds a frame of two.
|
|
QVERIFY((popup->pos() - shown.controls->mapToGlobal(QPoint(0, shown.controls->height()))).manhattanLength() <= 4);
|
|
QCOMPARE(popup->findChild<QLabel *>()->text(), QString::fromUtf8(heading));
|
|
QPushButton *button = nullptr;
|
|
for (QPushButton *each : popup->findChildren<QPushButton *>()) {
|
|
if (each->text() == QString::fromUtf8(choice))
|
|
button = each;
|
|
}
|
|
QVERIFY(button);
|
|
QTest::mouseClick(button, Qt::LeftButton);
|
|
QTRY_VERIFY(!QApplication::activePopupWidget());
|
|
QVERIFY(shown.session.maskPaintWhite());
|
|
QVERIFY(!shown.session.colorPicker());
|
|
}
|
|
QCOMPARE(shown.session.paletteColor(false), PaletteColor::white());
|
|
QCOMPARE(shown.session.paletteColor(true), PaletteColor::black());
|
|
QCOMPARE(middle(shown.button("foregroundSwatch")), QColor(Qt::white));
|
|
}
|
|
|
|
void ColorPaletteControlsTests::aMaskEndsTheChoiceAndThePicker()
|
|
{
|
|
Palette shown;
|
|
shown.session.addMask();
|
|
const QUuid id = shown.session.activeLayerID().value();
|
|
shown.session.selectLayerTarget(id, false);
|
|
// A mask chosen cancels the open picker.
|
|
QTest::mouseClick(&shown.button("foregroundSwatch"), Qt::LeftButton);
|
|
shown.session.setColorPickerHSB(PickerHSB(0, 1, 1));
|
|
shown.session.selectLayerTarget(id, true);
|
|
QVERIFY(!shown.session.colorPicker());
|
|
QVERIFY(!panel());
|
|
QCOMPARE(shown.session.foregroundColor(), PaletteColor::black());
|
|
// The pixels chosen close the mask's choice.
|
|
QTest::mouseClick(&shown.button("foregroundSwatch"), Qt::LeftButton);
|
|
QVERIFY(QApplication::activePopupWidget());
|
|
shown.session.selectLayerTarget(id, false);
|
|
QTRY_VERIFY(!QApplication::activePopupWidget());
|
|
}
|
|
|
|
void ColorPaletteControlsTests::aBusyProjectRestsThePalette()
|
|
{
|
|
Palette shown;
|
|
shown.session.setIsProjectBusy(true);
|
|
QVERIFY(!shown.controls->isEnabled());
|
|
shown.session.setIsProjectBusy(false);
|
|
QVERIFY(shown.controls->isEnabled());
|
|
}
|
|
|
|
void ColorPaletteControlsTests::theRailHoldsThePaletteBelowItsTools()
|
|
{
|
|
EditorSession session;
|
|
session.createDocument(400, 300, true);
|
|
ContentView view(session);
|
|
view.show();
|
|
QVERIFY(QTest::qWaitForWindowExposed(&view));
|
|
auto *palette = view.findChild<ColorPaletteControls *>();
|
|
QVERIFY(palette);
|
|
QWidget *rail = view.findChild<QScrollArea *>(QStringLiteral("toolRail"))->widget();
|
|
QRect last;
|
|
for (const QToolButton *tool : rail->findChildren<QToolButton *>()) {
|
|
if (tool->parentWidget() == rail && tool->geometry().bottom() > last.bottom())
|
|
last = tool->geometry();
|
|
}
|
|
// Swift's column: 36 across, the swatches 18 below the tools.
|
|
const QRect swatch = palette->findChild<QAbstractButton *>(QStringLiteral("foregroundSwatch"))->geometry().translated(palette->pos());
|
|
QCOMPARE(last.left(), 10);
|
|
QCOMPARE(swatch.left(), 10);
|
|
QCOMPARE(swatch.top() - (last.bottom() + 1), 18);
|
|
}
|
|
|
|
QTEST_MAIN(ColorPaletteControlsTests)
|
|
#include "ColorPaletteControlsTests.moc"
|