Release / version (push) Canceled after 0s
Release / deb (ubuntu:24.04, noble) (push) Canceled after 0s
Release / deb (ubuntu:26.04, resolute) (push) Canceled after 0s
Release / rpm (push) Canceled after 0s
Release / arch (push) Canceled after 0s
Release / publish (push) Canceled after 0s
339 lines
15 KiB
C++
339 lines
15 KiB
C++
#include "MenuFixtures.h"
|
|
#include "SelectionCanvasFixtures.h"
|
|
#include "UI/FloatingPanel.h"
|
|
#include "UI/HueSaturationSheet.h"
|
|
#include <QCheckBox>
|
|
#include <QLineEdit>
|
|
#include <QPushButton>
|
|
|
|
// Hue/Saturation in its panel and on the canvas.
|
|
namespace {
|
|
// Red left, green right, filling the view.
|
|
struct Halves : Canvas {
|
|
Halves()
|
|
{
|
|
QImage image(400, 300, QImage::Format_RGBA8888_Premultiplied);
|
|
image.fill(Qt::red);
|
|
for (int y = 0; y < 300; ++y) {
|
|
for (int x = 200; x < 400; ++x)
|
|
image.setPixelColor(x, y, Qt::green);
|
|
}
|
|
session.insert(ImportedImage(image, image, QStringLiteral("Halves")));
|
|
}
|
|
QColor shown(QPoint at)
|
|
{
|
|
canvas->synchronizeDisplay();
|
|
return canvas->grab().toImage().pixelColor(at);
|
|
}
|
|
const HueSaturationSettings &settings() const { return session.hueSaturation().value().settings; }
|
|
RangeAdjustment adjustment(ColorRange range) const
|
|
{
|
|
return settings().adjustments.contains(range) ? settings().adjustments.at(range) : RangeAdjustment();
|
|
}
|
|
};
|
|
|
|
QWidget *adjustmentPanel()
|
|
{
|
|
for (QWidget *widget : QApplication::topLevelWidgets()) {
|
|
if (widget->objectName() == QStringLiteral("adjustmentPanel") && widget->isVisible())
|
|
return widget;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// A window with one red layer, shown and active.
|
|
struct Editor : Bar {
|
|
Editor()
|
|
{
|
|
window.show();
|
|
if (!QTest::qWaitForWindowActive(&window))
|
|
throw std::runtime_error("the window never became active");
|
|
session().createDocument(4, 4, true);
|
|
QImage red(4, 4, QImage::Format_RGBA8888_Premultiplied);
|
|
red.fill(Qt::red);
|
|
session().insert(ImportedImage(red, red, QStringLiteral("Red")));
|
|
}
|
|
};
|
|
}
|
|
|
|
class HueSaturationCanvasTests : public QObject {
|
|
Q_OBJECT
|
|
private slots:
|
|
void thePreviewStandsInForTheLayer();
|
|
void anEyedropperClickSetsTheBandAndHandsThePanelTheKeys();
|
|
void colourPickingOutranksTheRangeEyedropperOnlyWhenAsked();
|
|
void targetingDragsSaturationOrWithCtrlTheHue();
|
|
void targetingTakesPressesAndDoubleClicksFirst();
|
|
void thePanelFollowsTheEdit();
|
|
void returnInAFieldClampsAndCommits();
|
|
void returnFromAnyButtonGoesToOK();
|
|
};
|
|
|
|
void HueSaturationCanvasTests::thePreviewStandsInForTheLayer()
|
|
{
|
|
Halves halves;
|
|
halves.session.beginHueSaturation();
|
|
halves.session.updateHueSaturation(HueSaturationSettings(120), true);
|
|
QTRY_VERIFY(halves.session.hueSaturation().value().previewImage(halves.session.activeLayerID().value()));
|
|
QCOMPARE(halves.shown(QPoint(100, 150)), QColor(Qt::green));
|
|
QCOMPARE(halves.shown(QPoint(300, 150)), QColor(Qt::blue));
|
|
// Off, the layer's own pixels show again.
|
|
halves.session.updateHueSaturation(HueSaturationSettings(120), false);
|
|
QCOMPARE(halves.shown(QPoint(100, 150)), QColor(Qt::red));
|
|
halves.session.updateHueSaturation(HueSaturationSettings(120), true);
|
|
QTRY_VERIFY(halves.session.hueSaturation().value().previewImage(halves.session.activeLayerID().value()));
|
|
halves.session.cancelHueSaturation();
|
|
QCOMPARE(halves.shown(QPoint(300, 150)), QColor(Qt::green));
|
|
}
|
|
|
|
void HueSaturationCanvasTests::anEyedropperClickSetsTheBandAndHandsThePanelTheKeys()
|
|
{
|
|
Halves halves;
|
|
halves.session.beginHueSaturation();
|
|
halves.session.updateHueSaturation(HueSaturationSettings(0, 0, 0, false, ColorRange::yellows), false);
|
|
FloatingPanel panel(QStringLiteral("adjustmentPanel"), halves.window);
|
|
panel.show(QStringLiteral("Hue/Saturation"), new HueSaturationSheet(halves.session));
|
|
QTRY_COMPARE(QApplication::activeWindow(), adjustmentPanel());
|
|
const QCursor eyedropper = CanvasView::eyedropperCursor(halves.canvas->devicePixelRatio());
|
|
QVERIFY(!halves.shows(eyedropper));
|
|
halves.session.setHueSampleMode(HueSampleMode::replace);
|
|
halves.canvas->synchronizeDisplay();
|
|
QVERIFY(halves.shows(eyedropper));
|
|
halves.window.activateWindow();
|
|
QTRY_COMPARE(QApplication::activeWindow(), &halves.window);
|
|
// Green: Yellows centred on 120.
|
|
halves.click(QPointF(300, 150));
|
|
QCOMPARE(halves.settings().band(), (HueBand{75, 105, 135, 165}));
|
|
QTRY_COMPARE(QApplication::activeWindow(), adjustmentPanel());
|
|
// With Space held the press pans instead.
|
|
const QSizeF pan = halves.session.viewport.pan;
|
|
QTest::keyPress(halves.canvas, Qt::Key_Space);
|
|
halves.drag(QPointF(100, 150), QPointF(110, 160));
|
|
QTest::keyRelease(halves.canvas, Qt::Key_Space);
|
|
QCOMPARE(halves.session.viewport.pan, pan + QSizeF(10, 10));
|
|
QCOMPARE(halves.settings().band(), (HueBand{75, 105, 135, 165}));
|
|
// Put away, the tool's own cursor returns.
|
|
halves.session.setHueSampleMode(std::nullopt);
|
|
halves.canvas->synchronizeDisplay();
|
|
QVERIFY(!halves.shows(eyedropper));
|
|
halves.session.cancelHueSaturation();
|
|
}
|
|
|
|
void HueSaturationCanvasTests::colourPickingOutranksTheRangeEyedropperOnlyWhenAsked()
|
|
{
|
|
Halves halves;
|
|
halves.session.selectTool(NavigationTool::eyedropper);
|
|
halves.session.beginHueSaturation();
|
|
halves.session.updateHueSaturation(HueSaturationSettings(0, 0, 0, false, ColorRange::yellows), false);
|
|
// Unarmed, the Eyedropper takes the colour.
|
|
halves.click(QPointF(300, 150));
|
|
QCOMPARE(halves.session.foregroundColor().hex(), QString("00FF00"));
|
|
QCOMPARE(halves.settings().band(), defaultBand(ColorRange::yellows));
|
|
// Armed, the range's eyedropper takes the click.
|
|
halves.session.setHueSampleMode(HueSampleMode::replace);
|
|
halves.click(QPointF(100, 150));
|
|
QCOMPARE(halves.session.foregroundColor().hex(), QString("00FF00"));
|
|
QCOMPARE(halves.settings().band(), (HueBand{315, 345, 15, 45}));
|
|
// An open colour picker outranks both.
|
|
halves.session.openColorPicker(false);
|
|
halves.click(QPointF(300, 150));
|
|
QCOMPARE(halves.session.colorPicker().value().color().hex(), QString("00FF00"));
|
|
QCOMPARE(halves.settings().band(), (HueBand{315, 345, 15, 45}));
|
|
halves.session.closeColorPicker(false);
|
|
// Armed targeting leaves a colour drag its sampling.
|
|
halves.session.setHueSampleMode(std::nullopt);
|
|
halves.session.setHueTargeting(true);
|
|
halves.canvas->synchronizeDisplay();
|
|
halves.press(QPointF(100, 150));
|
|
halves.move(QPointF(300, 150));
|
|
QCOMPARE(halves.session.foregroundColor().hex(), QString("00FF00"));
|
|
halves.release(QPointF(300, 150));
|
|
halves.session.cancelHueSaturation();
|
|
}
|
|
|
|
void HueSaturationCanvasTests::targetingDragsSaturationOrWithCtrlTheHue()
|
|
{
|
|
Halves halves;
|
|
halves.session.beginHueSaturation();
|
|
halves.session.setHueTargeting(true);
|
|
halves.canvas->synchronizeDisplay();
|
|
QCOMPARE(halves.canvas->cursor().shape(), Qt::SizeHorCursor);
|
|
// Red picks Reds; right raises a unit per two points.
|
|
halves.press(QPointF(100, 150));
|
|
QCOMPARE(halves.settings().range, ColorRange::reds);
|
|
halves.move(QPointF(140, 150));
|
|
QCOMPARE(halves.adjustment(ColorRange::reds), (RangeAdjustment{0, 20, 0}));
|
|
// Ctrl drags the hue, from the drag's start.
|
|
halves.move(QPointF(160, 150), Qt::ControlModifier);
|
|
QCOMPARE(halves.adjustment(ColorRange::reds), (RangeAdjustment{30, 20, 0}));
|
|
halves.release(QPointF(160, 150));
|
|
QCOMPARE(halves.canvas->cursor().shape(), Qt::SizeHorCursor);
|
|
// Alt mid-drag shows the Brush's eyedropper till the next drag.
|
|
halves.session.selectTool(NavigationTool::brush);
|
|
halves.canvas->synchronizeDisplay();
|
|
halves.press(QPointF(100, 150));
|
|
QTest::keyPress(halves.canvas, Qt::Key_Alt);
|
|
QVERIFY(halves.shows(CanvasView::eyedropperCursor(halves.canvas->devicePixelRatio())));
|
|
halves.move(QPointF(120, 150), Qt::AltModifier);
|
|
QCOMPARE(halves.canvas->cursor().shape(), Qt::SizeHorCursor);
|
|
QCOMPARE(halves.adjustment(ColorRange::reds), (RangeAdjustment{30, 30, 0}));
|
|
halves.release(QPointF(120, 150), Qt::AltModifier);
|
|
QTest::keyRelease(halves.canvas, Qt::Key_Alt);
|
|
halves.session.selectTool(NavigationTool::marquee);
|
|
// Ended, moves change nothing.
|
|
halves.move(QPointF(300, 150));
|
|
QCOMPARE(halves.adjustment(ColorRange::reds), (RangeAdjustment{30, 30, 0}));
|
|
// A buttonless move ends a drag whose release was lost.
|
|
halves.press(QPointF(300, 150));
|
|
QCOMPARE(halves.settings().range, ColorRange::greens);
|
|
halves.hover(QPointF(320, 150));
|
|
halves.move(QPointF(360, 150));
|
|
QCOMPARE(halves.adjustment(ColorRange::greens), RangeAdjustment());
|
|
halves.release(QPointF(360, 150));
|
|
// With Space held the press pans; Colorize refuses.
|
|
const QSizeF pan = halves.session.viewport.pan;
|
|
QTest::keyPress(halves.canvas, Qt::Key_Space);
|
|
halves.drag(QPointF(100, 150), QPointF(90, 150));
|
|
QTest::keyRelease(halves.canvas, Qt::Key_Space);
|
|
QCOMPARE(halves.session.viewport.pan, pan + QSizeF(-10, 0));
|
|
QCOMPARE(halves.settings().range, ColorRange::greens);
|
|
halves.session.updateHueSaturation(HueSaturationSettings::colorizeStart(), false);
|
|
halves.drag(QPointF(100, 150), QPointF(140, 150));
|
|
QVERIFY(halves.settings() == HueSaturationSettings::colorizeStart());
|
|
// Put away, the tool's cursor returns.
|
|
halves.session.setHueTargeting(false);
|
|
halves.canvas->synchronizeDisplay();
|
|
QVERIFY(halves.canvas->cursor().shape() != Qt::SizeHorCursor);
|
|
halves.session.cancelHueSaturation();
|
|
}
|
|
|
|
void HueSaturationCanvasTests::targetingTakesPressesAndDoubleClicksFirst()
|
|
{
|
|
Halves halves;
|
|
// Refused under Colorize, a press still reaches no tool.
|
|
halves.session.selectTool(NavigationTool::zoom);
|
|
halves.session.beginHueSaturation();
|
|
halves.session.updateHueSaturation(HueSaturationSettings::colorizeStart(), false);
|
|
halves.session.setHueTargeting(true);
|
|
halves.canvas->synchronizeDisplay();
|
|
halves.click(QPointF(100, 150));
|
|
QCOMPARE(halves.session.viewport.zoom(), 1.0);
|
|
halves.session.cancelHueSaturation();
|
|
// A polygonal outline begun before stays under a double click.
|
|
halves.session.selectTool(NavigationTool::lasso);
|
|
halves.session.setLassoKind(LassoKind::polygonal);
|
|
halves.canvas->synchronizeDisplay();
|
|
halves.click(QPointF(20, 20));
|
|
halves.click(QPointF(120, 20));
|
|
QVERIFY(halves.session.lassoDraft());
|
|
halves.session.beginHueSaturation();
|
|
halves.session.setHueTargeting(true);
|
|
QTest::mouseDClick(halves.canvas, Qt::LeftButton, Qt::NoModifier, QPoint(100, 150));
|
|
QCOMPARE(halves.settings().range, ColorRange::reds);
|
|
QVERIFY(halves.session.lassoDraft());
|
|
halves.release(QPointF(100, 150));
|
|
halves.session.cancelHueSaturation();
|
|
halves.session.cancelLasso();
|
|
// Open text keeps Hue/Saturation closed since 1.3.2.
|
|
halves.session.selectTool(NavigationTool::type);
|
|
halves.session.beginText(QPointF(300, 50), true);
|
|
QVERIFY(halves.session.textDraft());
|
|
halves.session.beginHueSaturation();
|
|
QVERIFY(!halves.session.hueSaturation());
|
|
halves.session.cancelText();
|
|
// The Type tool's double click targets, not its words.
|
|
halves.session.beginHueSaturation();
|
|
halves.session.setHueTargeting(true);
|
|
halves.canvas->synchronizeDisplay();
|
|
QTest::mouseDClick(halves.canvas, Qt::LeftButton, Qt::NoModifier, QPoint(300, 150));
|
|
QCOMPARE(halves.settings().range, ColorRange::greens);
|
|
halves.release(QPointF(300, 150));
|
|
QVERIFY(!halves.session.textDraft());
|
|
halves.session.cancelHueSaturation();
|
|
}
|
|
|
|
void HueSaturationCanvasTests::thePanelFollowsTheEdit()
|
|
{
|
|
Editor editor;
|
|
QVERIFY(!adjustmentPanel());
|
|
editor.session().beginHueSaturation();
|
|
QWidget *panel = adjustmentPanel();
|
|
QVERIFY(panel);
|
|
QCOMPARE(panel->windowTitle(), QString("Hue/Saturation"));
|
|
HueSaturationSheet *sheet = panel->findChild<HueSaturationSheet *>();
|
|
QVERIFY(sheet->isVisible());
|
|
// A change keeps the sheet; a new edit makes one.
|
|
editor.session().updateHueSaturation(HueSaturationSettings(90), false);
|
|
QVERIFY(sheet->isVisible());
|
|
editor.session().cancelHueSaturation();
|
|
QVERIFY(!panel->isVisible());
|
|
editor.session().beginHueSaturation();
|
|
QVERIFY(panel->isVisible());
|
|
QTRY_COMPARE(panel->findChildren<HueSaturationSheet *>().size(), 1);
|
|
// Its close button and Escape cancel.
|
|
panel->close();
|
|
QVERIFY(!editor.session().hueSaturation());
|
|
editor.session().beginHueSaturation();
|
|
QTRY_COMPARE(QApplication::activeWindow(), panel);
|
|
QTest::keyClick(panel, Qt::Key_Escape);
|
|
QVERIFY(!editor.session().hueSaturation() && !panel->isVisible());
|
|
// Escape over typing: the field lets go after the edit.
|
|
editor.session().beginHueSaturation();
|
|
QTRY_COMPARE(QApplication::activeWindow(), panel);
|
|
QLineEdit &hue = *panel->findChild<QLineEdit *>(QStringLiteral("hueField"));
|
|
hue.setFocus();
|
|
QTRY_VERIFY(hue.hasFocus());
|
|
QTest::keyClicks(&hue, QStringLiteral("5"));
|
|
QTest::keyClick(&hue, Qt::Key_Escape);
|
|
QTRY_VERIFY(!panel->isVisible());
|
|
QVERIFY(!editor.session().hueSaturation());
|
|
}
|
|
|
|
void HueSaturationCanvasTests::returnInAFieldClampsAndCommits()
|
|
{
|
|
Editor editor;
|
|
editor.session().beginHueSaturation();
|
|
QWidget *panel = adjustmentPanel();
|
|
QTRY_COMPARE(QApplication::activeWindow(), panel);
|
|
QLineEdit &lightness = *panel->findChild<QLineEdit *>(QStringLiteral("lightnessField"));
|
|
lightness.setFocus();
|
|
QTRY_VERIFY(lightness.hasFocus());
|
|
lightness.selectAll();
|
|
QTest::keyClicks(&lightness, QStringLiteral("-500"));
|
|
QTest::keyClick(&lightness, Qt::Key_Return);
|
|
QTRY_VERIFY(!editor.session().hueSaturation());
|
|
QCOMPARE(editor.session().history.undoName(), QString("Hue/Saturation"));
|
|
QCOMPARE(editor.session().activeLayer().value().asset.value().image().pixelColor(0, 0), QColor(Qt::black));
|
|
QVERIFY(!panel->isVisible());
|
|
}
|
|
|
|
void HueSaturationCanvasTests::returnFromAnyButtonGoesToOK()
|
|
{
|
|
Editor editor;
|
|
// Swift's default action: no other control takes Return.
|
|
for (const char *text : {"Reset", "Cancel", "Colorize", "Preview"}) {
|
|
// Window managers hand the keys back once a panel closes.
|
|
editor.window.activateWindow();
|
|
QTRY_COMPARE(QApplication::activeWindow(), &editor.window);
|
|
editor.session().beginHueSaturation();
|
|
editor.session().updateHueSaturation(HueSaturationSettings(120), false);
|
|
QWidget *panel = adjustmentPanel();
|
|
QTRY_COMPARE(QApplication::activeWindow(), panel);
|
|
QAbstractButton *button = nullptr;
|
|
for (QAbstractButton *found : panel->findChildren<QAbstractButton *>()) {
|
|
if (found->text() == QString::fromUtf8(text) && found->isVisible())
|
|
button = found;
|
|
}
|
|
QTRY_VERIFY(button && button->isEnabled());
|
|
button->setFocus();
|
|
QTRY_VERIFY(button->hasFocus());
|
|
const int steps = editor.session().history.undoCount();
|
|
QTest::keyClick(button, Qt::Key_Return);
|
|
QTRY_VERIFY2(!editor.session().hueSaturation(), text);
|
|
QCOMPARE(editor.session().history.undoCount(), steps + 1);
|
|
}
|
|
}
|
|
|
|
QTEST_MAIN(HueSaturationCanvasTests)
|
|
#include "HueSaturationCanvasTests.moc"
|