Select › Color Range: sampled colours, Fuzziness, Invert, a live preview and the panel (13.3b)
This commit is contained in:
1 parent
462fe805b1
commit
0b7eb6cb6f
29 files changed
+1535
-71
No files matched your search
@@ -0,0 +1,211 @@
|
||||
#include "AcceptanceFixtures.h"
|
||||
#include "SelectionCanvasFixtures.h"
|
||||
#include "UI/ColorPickerSheet.h"
|
||||
#include "UI/ColorRangeSheet.h"
|
||||
#include "UI/SampleButton.h"
|
||||
#include <QCheckBox>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QSlider>
|
||||
|
||||
// Swift 1.3.4's Color Range: canvas and panel.
|
||||
namespace {
|
||||
// Red left, blue right, filling the canvas.
|
||||
struct Ranged : Canvas {
|
||||
Ranged()
|
||||
{
|
||||
QImage image(400, 300, QImage::Format_RGBA8888_Premultiplied);
|
||||
image.fill(QColor(200, 30, 30));
|
||||
for (int y = 0; y < 300; ++y)
|
||||
for (int x = 200; x < 400; ++x)
|
||||
image.setPixelColor(x, y, QColor(30, 40, 210));
|
||||
session.insert(ImportedImage(image, image, QStringLiteral("Halves")));
|
||||
session.beginColorRange();
|
||||
canvas->synchronizeDisplay();
|
||||
}
|
||||
const ColorRangeEdit &edit() const { return session.colorRange().value(); }
|
||||
};
|
||||
|
||||
QWidget *panel()
|
||||
{
|
||||
for (QWidget *widget : QApplication::topLevelWidgets())
|
||||
if (widget->objectName() == QStringLiteral("colorRangePanel") && widget->isVisible())
|
||||
return widget;
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
class ColorRangeCanvasTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void initTestCase() { QStandardPaths::setTestModeEnabled(true); }
|
||||
void aClickTakesTheColourShiftAddsAltTakesAway();
|
||||
void theEyedropperSaysWhatAClickWillDo();
|
||||
void theSheetDrivesTheEdit();
|
||||
void thePanelFollowsTheEditAndTheMenu();
|
||||
};
|
||||
|
||||
void ColorRangeCanvasTests::aClickTakesTheColourShiftAddsAltTakesAway()
|
||||
{
|
||||
Ranged shown;
|
||||
shown.click(QPointF(50, 50));
|
||||
QCOMPARE(shown.edit().include, (std::vector<uchar>{200, 30, 30}));
|
||||
shown.click(QPointF(300, 50), Qt::ShiftModifier);
|
||||
QCOMPARE(shown.edit().include, (std::vector<uchar>{200, 30, 30, 30, 40, 210}));
|
||||
shown.click(QPointF(50, 50), Qt::AltModifier);
|
||||
QCOMPARE(shown.edit().exclude, (std::vector<uchar>{200, 30, 30}));
|
||||
// Alt with Shift takes away too, as Swift's order reads.
|
||||
shown.click(QPointF(300, 50), Qt::AltModifier | Qt::ShiftModifier);
|
||||
QCOMPARE(shown.edit().exclude, (std::vector<uchar>{200, 30, 30, 30, 40, 210}));
|
||||
// With Space held the press pans instead.
|
||||
const QSizeF pan = shown.session.viewport.pan;
|
||||
QTest::keyPress(shown.canvas, Qt::Key_Space);
|
||||
shown.drag(QPointF(100, 100), QPointF(110, 120));
|
||||
QTest::keyRelease(shown.canvas, Qt::Key_Space);
|
||||
QCOMPARE(shown.session.viewport.pan, pan + QSizeF(10, 20));
|
||||
QCOMPARE(shown.edit().exclude.size(), size_t(6));
|
||||
// The match lands on the canvas as the selection.
|
||||
shown.click(QPointF(300, 50));
|
||||
QTRY_VERIFY(shown.session.selection() && shown.coverage(300, 50) == 255);
|
||||
QCOMPARE(shown.coverage(50, 50), 0);
|
||||
// Under the Eyedropper the click is the range's alone.
|
||||
shown.session.cancelColorRange();
|
||||
shown.session.selectTool(NavigationTool::eyedropper);
|
||||
shown.session.beginColorRange();
|
||||
const PaletteColor foreground = shown.session.foregroundColor();
|
||||
shown.click(QPointF(50, 50));
|
||||
QVERIFY(shown.session.foregroundColor() == foreground && shown.edit().include == (std::vector<uchar>{200, 30, 30}));
|
||||
}
|
||||
|
||||
void ColorRangeCanvasTests::theEyedropperSaysWhatAClickWillDo()
|
||||
{
|
||||
Ranged shown;
|
||||
const double ratio = shown.canvas->devicePixelRatio();
|
||||
const QCursor plain = CanvasView::eyedropperCursor(ratio), add = CanvasView::eyedropperCursor(ratio, HueSampleMode::add),
|
||||
remove = CanvasView::eyedropperCursor(ratio, HueSampleMode::remove);
|
||||
QVERIFY(shown.shows(plain));
|
||||
// A disc bottom right: white, a black sign across.
|
||||
const QImage badged = image(add);
|
||||
const auto gray = [ratio](const QCursor &cursor, QPoint at) { return qGray(image(cursor).pixel(at * ratio)); };
|
||||
QVERIFY(gray(add, QPoint(20, 16)) > 230 && gray(add, QPoint(18, 18)) < 80 && gray(remove, QPoint(18, 18)) < 80);
|
||||
// A plus has its upright; a minus has none.
|
||||
QVERIFY(gray(add, QPoint(18, 15)) < 80 && gray(remove, QPoint(18, 15)) > 230);
|
||||
// The bar spans the disc; a black rim rings it.
|
||||
QVERIFY(gray(add, QPoint(16, 18)) < 80 && gray(remove, QPoint(20, 18)) < 80 && gray(remove, QPoint(18, 23)) < 80);
|
||||
QCOMPARE(image(plain).pixelColor(QPoint(20, 16) * ratio).alpha(), 0);
|
||||
// Held keys show it; the chosen eyedropper too.
|
||||
QTest::keyPress(shown.canvas, Qt::Key_Shift);
|
||||
QCOMPARE(shown.edit().held, std::optional(HueSampleMode::add));
|
||||
QVERIFY(shown.shows(add));
|
||||
QTest::keyPress(shown.canvas, Qt::Key_Alt, Qt::ShiftModifier);
|
||||
QCOMPARE(shown.edit().held, std::optional(HueSampleMode::remove));
|
||||
QVERIFY(shown.shows(remove));
|
||||
// QTest would let go of Shift too: Alt alone goes.
|
||||
QKeyEvent alt(QEvent::KeyRelease, Qt::Key_Alt, Qt::ShiftModifier);
|
||||
QApplication::sendEvent(shown.canvas, &alt);
|
||||
QCOMPARE(shown.edit().held, std::optional(HueSampleMode::add));
|
||||
QTest::keyRelease(shown.canvas, Qt::Key_Shift);
|
||||
QCOMPARE(shown.edit().held, std::optional<HueSampleMode>());
|
||||
QVERIFY(shown.shows(plain));
|
||||
shown.session.setColorRangeSampleMode(HueSampleMode::remove);
|
||||
shown.canvas->synchronizeDisplay();
|
||||
QVERIFY(shown.shows(remove));
|
||||
// Closed, the tool's own cursor returns.
|
||||
shown.session.cancelColorRange();
|
||||
shown.canvas->synchronizeDisplay();
|
||||
QVERIFY(!shown.shows(plain) && !shown.shows(remove));
|
||||
}
|
||||
|
||||
void ColorRangeCanvasTests::theSheetDrivesTheEdit()
|
||||
{
|
||||
Ranged shown;
|
||||
ColorRangeSheet sheet(shown.session);
|
||||
sheet.show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(&sheet));
|
||||
const auto dropper = [&sheet](const char *mode) { return sheet.findChild<SampleButton *>(QStringLiteral("colorRange") + QString::fromLatin1(mode)); };
|
||||
QVERIFY(dropper("Sample")->isChecked() && !dropper("Add")->isChecked());
|
||||
QCOMPARE(dropper("Add")->toolTip(), QStringLiteral("Click the image to add that color to the selection"));
|
||||
QCOMPARE(dropper("Remove")->accessibleName(), QStringLiteral("Remove color"));
|
||||
dropper("Add")->click();
|
||||
QVERIFY(shown.edit().sampleMode == HueSampleMode::add && dropper("Add")->isChecked() && !dropper("Sample")->isChecked());
|
||||
// Held keys light the eyedropper a click will use.
|
||||
shown.session.setColorRangeHeld(HueSampleMode::remove);
|
||||
QVERIFY(dropper("Remove")->isChecked() && !dropper("Add")->isChecked());
|
||||
shown.session.setColorRangeHeld(std::nullopt);
|
||||
QLabel *caption = sheet.findChild<QLabel *>(QStringLiteral("colorRangeCaption"));
|
||||
QCOMPARE(caption->text(), QStringLiteral("Click the image to pick the color to select."));
|
||||
shown.session.sampleColorRange(QPointF(50, 50), false, false);
|
||||
QCOMPARE(caption->text(), QStringLiteral("Shift-click adds a color, Alt-click takes one away."));
|
||||
// Fuzziness: the slider, the field, the title's scrub.
|
||||
QSlider *slider = sheet.findChild<QSlider *>(QStringLiteral("fuzzinessSlider"));
|
||||
QVERIFY(slider->minimum() == 0 && slider->maximum() == 200 && slider->value() == 40);
|
||||
slider->setValue(120);
|
||||
QCOMPARE(shown.edit().fuzziness, 120.0);
|
||||
PickerField *field = sheet.findChild<PickerField *>(QStringLiteral("fuzzinessField"));
|
||||
QCOMPARE(field->text(), QStringLiteral("120"));
|
||||
field->setFocus();
|
||||
field->selectAll();
|
||||
QTest::keyClicks(field, QStringLiteral("33.6"));
|
||||
QTest::keyClick(field, Qt::Key_Return);
|
||||
QVERIFY(shown.edit().fuzziness == 34 && slider->value() == 34);
|
||||
QLabel *title = nullptr;
|
||||
for (QLabel *label : sheet.findChildren<QLabel *>())
|
||||
title = label->text() == QStringLiteral("Fuzziness") ? label : title;
|
||||
QVERIFY(title && title->buddy() == slider);
|
||||
QTest::mousePress(title, Qt::LeftButton, Qt::NoModifier, QPoint(2, 2));
|
||||
QMouseEvent move(QEvent::MouseMove, QPointF(22, 2), title->mapToGlobal(QPointF(22, 2)), Qt::NoButton, Qt::LeftButton, Qt::NoModifier);
|
||||
QApplication::sendEvent(title, &move);
|
||||
QTest::mouseRelease(title, Qt::LeftButton, Qt::NoModifier, QPoint(22, 2));
|
||||
QCOMPARE(shown.edit().fuzziness, 54.0);
|
||||
// Invert, and an error shown in the warning colour.
|
||||
QCheckBox *invert = sheet.findChild<QCheckBox *>(QStringLiteral("colorRangeInvert"));
|
||||
invert->click();
|
||||
QVERIFY(shown.edit().invert);
|
||||
QLabel *error = sheet.findChild<QLabel *>(QStringLiteral("colorRangeError"));
|
||||
QVERIFY(error->isHidden() && error->foregroundRole() == QPalette::BrightText);
|
||||
// OK keeps the match as a step; the edit closes.
|
||||
QTRY_VERIFY(!shown.edit().preview.isNull());
|
||||
QPushButton *ok = nullptr;
|
||||
for (QPushButton *button : sheet.findChildren<QPushButton *>())
|
||||
ok = button->text() == QStringLiteral("OK") ? button : ok;
|
||||
QVERIFY(ok && ok->isDefault());
|
||||
ok->click();
|
||||
QVERIFY(!shown.session.colorRange());
|
||||
QCOMPARE(shown.session.history.undoName(), QStringLiteral("Color Range"));
|
||||
}
|
||||
|
||||
void ColorRangeCanvasTests::thePanelFollowsTheEditAndTheMenu()
|
||||
{
|
||||
App app;
|
||||
QVERIFY(scene(400, 300).save(app.path("scene.png")));
|
||||
app.session().createDocument(400, 300);
|
||||
app.importFile(app.path("scene.png"));
|
||||
QAction &entry = app.action("colorRange");
|
||||
QCOMPARE(entry.text(), QStringLiteral("Color Range…"));
|
||||
QVERIFY(entry.isEnabled());
|
||||
entry.trigger();
|
||||
QVERIFY(app.session().colorRange());
|
||||
QTRY_VERIFY(panel());
|
||||
QCOMPARE(panel()->windowTitle(), QStringLiteral("Color Range"));
|
||||
QVERIFY(panel()->findChild<ColorRangeSheet *>());
|
||||
QVERIFY(!entry.isEnabled());
|
||||
// Escape is Cancel: the panel and the edit go.
|
||||
QTRY_COMPARE(QApplication::activeWindow(), panel());
|
||||
QTest::keyClick(panel(), Qt::Key_Escape);
|
||||
QTRY_VERIFY(!panel());
|
||||
QVERIFY(!app.session().colorRange() && entry.isEnabled());
|
||||
// Reopened: a canvas click samples; the panel keeps the keys.
|
||||
entry.trigger();
|
||||
QTRY_VERIFY(panel());
|
||||
app.window.activateWindow();
|
||||
QTRY_COMPARE(QApplication::activeWindow(), &app.window);
|
||||
app.press(QPointF(200, 150));
|
||||
app.release(QPointF(200, 150));
|
||||
QVERIFY(app.session().colorRange().value().hasColors());
|
||||
QTRY_COMPARE(QApplication::activeWindow(), panel());
|
||||
app.session().commitColorRange();
|
||||
QTRY_VERIFY(!panel());
|
||||
}
|
||||
|
||||
QTEST_MAIN(ColorRangeCanvasTests)
|
||||
#include "ColorRangeCanvasTests.moc"
|
||||
@@ -0,0 +1,233 @@
|
||||
#include "Document/EditorSession.h"
|
||||
#include "UI/ColorPickerSheet.h"
|
||||
#include "UI/ColorRangeSheet.h"
|
||||
#include "UI/SampleButton.h"
|
||||
#include <QCheckBox>
|
||||
#include <QDialog>
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QSlider>
|
||||
#include <QVBoxLayout>
|
||||
#include <QtTest>
|
||||
|
||||
// Swift 1.3.4's ColorRangeSheet: its layout, field, preview, words.
|
||||
namespace {
|
||||
// Red left, blue right; the sheet over an open edit.
|
||||
struct Sheet {
|
||||
EditorSession session;
|
||||
std::unique_ptr<ColorRangeSheet> sheet;
|
||||
Sheet(QSize size = QSize(400, 300))
|
||||
{
|
||||
QImage image(size, QImage::Format_RGBA8888_Premultiplied);
|
||||
image.fill(QColor(200, 30, 30));
|
||||
for (int y = 0; y < size.height(); ++y)
|
||||
for (int x = size.width() / 2; x < size.width(); ++x)
|
||||
image.setPixelColor(x, y, QColor(30, 40, 210));
|
||||
session.createDocument(size.width(), size.height());
|
||||
session.insert(ImportedImage(image, image, QStringLiteral("Halves")));
|
||||
session.beginColorRange();
|
||||
sheet = std::make_unique<ColorRangeSheet>(session);
|
||||
sheet->show();
|
||||
if (!QTest::qWaitForWindowActive(sheet.get()))
|
||||
throw std::runtime_error("the sheet never became active");
|
||||
}
|
||||
const ColorRangeEdit &edit() const { return session.colorRange().value(); }
|
||||
template <typename Widget> Widget &child(const char *name) const
|
||||
{
|
||||
Widget *found = sheet->findChild<Widget *>(QString::fromLatin1(name));
|
||||
if (!found)
|
||||
throw std::runtime_error(std::string("no child ") + name);
|
||||
return *found;
|
||||
}
|
||||
// A click on the left half, and its match landing.
|
||||
void sampleRed()
|
||||
{
|
||||
QSignalSpy changed(&session, &EditorSession::changed);
|
||||
session.sampleColorRange(QPointF(10, 10), false, false);
|
||||
QVERIFY(QTest::qWaitFor([&changed] { return changed.count() >= 2; }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class ColorRangeSheetTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void theSheetLaysOutAsSwifts();
|
||||
void theFieldTakesTypingAsSwiftsBinding();
|
||||
void thePreviewPaintsTheMatch();
|
||||
void thePreviewShrinksSmoothly();
|
||||
void invertAndErrorsFollowTheEdit();
|
||||
};
|
||||
|
||||
void ColorRangeSheetTests::theSheetLaysOutAsSwifts()
|
||||
{
|
||||
Sheet shown;
|
||||
auto *column = qobject_cast<QVBoxLayout *>(shown.sheet->layout());
|
||||
QVERIFY(column && shown.sheet->width() == 340 && column->spacing() == 16);
|
||||
QCOMPARE(column->contentsMargins(), QMargins(24, 24, 24, 24));
|
||||
// The eyedroppers in Swift's order, 6 apart, then a stretch.
|
||||
auto *droppers = qobject_cast<QHBoxLayout *>(column->itemAt(0)->layout());
|
||||
QVERIFY(droppers && droppers->spacing() == 6 && droppers->count() == 4 && droppers->itemAt(3)->spacerItem());
|
||||
QCOMPARE(droppers->itemAt(0)->widget(), &shown.child<SampleButton>("colorRangeSample"));
|
||||
QCOMPARE(droppers->itemAt(2)->widget(), &shown.child<SampleButton>("colorRangeRemove"));
|
||||
// The preview, centred, the canvas's shape within 292 by 200.
|
||||
QWidget *preview = column->itemAt(1)->widget();
|
||||
QVERIFY(preview && column->itemAt(1)->alignment() == Qt::AlignHCenter);
|
||||
QCOMPARE(preview->size(), QSize(267, 200));
|
||||
QCOMPARE(preview->mapTo(shown.sheet.get(), QPoint()).x(), (340 - 267) / 2);
|
||||
// The caption: wrapped, callout-sized, secondary.
|
||||
auto &caption = shown.child<QLabel>("colorRangeCaption");
|
||||
QVERIFY(column->itemAt(2)->widget() == &caption && caption.wordWrap() && caption.font().pixelSize() == 12);
|
||||
QCOMPARE(caption.foregroundRole(), QPalette::PlaceholderText);
|
||||
// Fuzziness: title, slider stretching, a 48-point field.
|
||||
QWidget *row = column->itemAt(3)->widget();
|
||||
QCOMPARE(row->toolTip(), QStringLiteral("How far a color may be from the picked ones and still be selected"));
|
||||
auto *fuzziness = qobject_cast<QHBoxLayout *>(row->layout());
|
||||
QVERIFY(fuzziness->spacing() == 10 && fuzziness->contentsMargins() == QMargins() && fuzziness->count() == 3);
|
||||
QCOMPARE(qobject_cast<QLabel *>(fuzziness->itemAt(0)->widget())->text(), QStringLiteral("Fuzziness"));
|
||||
QVERIFY(fuzziness->itemAt(1)->widget() == &shown.child<QSlider>("fuzzinessSlider") && fuzziness->stretch(1) == 1);
|
||||
auto &field = shown.child<PickerField>("fuzzinessField");
|
||||
QVERIFY(fuzziness->itemAt(2)->widget() == &field && field.width() == 48 && field.alignment() == Qt::AlignRight);
|
||||
QVERIFY(field.accessibleName() == QStringLiteral("Fuzziness") && field.placeholderText() == QStringLiteral("Fuzziness"));
|
||||
// Invert, the error, a rule, then Cancel and OK apart.
|
||||
auto &invert = shown.child<QCheckBox>("colorRangeInvert");
|
||||
QVERIFY(column->itemAt(4)->widget() == &invert);
|
||||
QCOMPARE(invert.toolTip(), QStringLiteral("Select everything except those colors, such as all but a green screen"));
|
||||
auto &error = shown.child<QLabel>("colorRangeError");
|
||||
QVERIFY(column->itemAt(5)->widget() == &error && error.wordWrap());
|
||||
auto *rule = qobject_cast<QFrame *>(column->itemAt(6)->widget());
|
||||
QVERIFY(rule && rule->frameShape() == QFrame::HLine && rule->foregroundRole() == QPalette::Mid);
|
||||
auto *buttons = qobject_cast<QHBoxLayout *>(column->itemAt(7)->layout());
|
||||
QVERIFY(buttons && buttons->count() == 3 && buttons->itemAt(1)->spacerItem());
|
||||
auto *cancel = qobject_cast<QPushButton *>(buttons->itemAt(0)->widget());
|
||||
auto *ok = qobject_cast<QPushButton *>(buttons->itemAt(2)->widget());
|
||||
QVERIFY(cancel->text() == QStringLiteral("Cancel") && ok->text() == QStringLiteral("OK") && ok->isDefault());
|
||||
// In a panel, a dialog, only OK takes Return.
|
||||
QDialog panel;
|
||||
shown.sheet->setParent(&panel);
|
||||
QVERIFY(!cancel->autoDefault() && ok->autoDefault());
|
||||
shown.sheet->setParent(nullptr);
|
||||
// Cancel puts the old selection back and closes the edit.
|
||||
cancel->click();
|
||||
QVERIFY(!shown.session.colorRange());
|
||||
}
|
||||
|
||||
void ColorRangeSheetTests::theFieldTakesTypingAsSwiftsBinding()
|
||||
{
|
||||
Sheet shown;
|
||||
auto &field = shown.child<PickerField>("fuzzinessField");
|
||||
auto &slider = shown.child<QSlider>("fuzzinessSlider");
|
||||
QVERIFY(field.text() == QStringLiteral("40") && slider.value() == 40);
|
||||
// Typed, then Return: whole numbers, shown whole.
|
||||
field.setFocus();
|
||||
QTRY_VERIFY(field.hasFocus());
|
||||
field.selectAll();
|
||||
QTest::keyClicks(&field, QStringLiteral("40.4"));
|
||||
QTest::keyClick(&field, Qt::Key_Return);
|
||||
QVERIFY(shown.edit().fuzziness == 40 && field.text() == QStringLiteral("40"));
|
||||
// The slider replaces typing, as the title's scrub does.
|
||||
field.selectAll();
|
||||
QTest::keyClicks(&field, QStringLiteral("77"));
|
||||
slider.setValue(90);
|
||||
QVERIFY(shown.edit().fuzziness == 90 && field.text() == QStringLiteral("90"));
|
||||
field.selectAll();
|
||||
QTest::keyClicks(&field, QStringLiteral("77"));
|
||||
QLabel *title = qobject_cast<QLabel *>(qobject_cast<QHBoxLayout *>(field.parentWidget()->layout())->itemAt(0)->widget());
|
||||
QTest::mousePress(title, Qt::LeftButton, Qt::NoModifier, QPoint(2, 2));
|
||||
QMouseEvent move(QEvent::MouseMove, QPointF(12, 2), title->mapToGlobal(QPointF(12, 2)), Qt::NoButton, Qt::LeftButton, Qt::NoModifier);
|
||||
QApplication::sendEvent(title, &move);
|
||||
QTest::mouseRelease(title, Qt::LeftButton, Qt::NoModifier, QPoint(12, 2));
|
||||
QVERIFY(shown.edit().fuzziness == 100 && field.text() == QStringLiteral("100"));
|
||||
// Text that is no number goes back.
|
||||
field.selectAll();
|
||||
QTest::keyClicks(&field, QStringLiteral("lots"));
|
||||
QTest::keyClick(&field, Qt::Key_Return);
|
||||
QVERIFY(shown.edit().fuzziness == 100 && field.text() == QStringLiteral("100"));
|
||||
// The sheet going commits what its field holds.
|
||||
field.selectAll();
|
||||
QTest::keyClicks(&field, QStringLiteral("123"));
|
||||
shown.sheet.reset();
|
||||
QCOMPARE(shown.edit().fuzziness, 123.0);
|
||||
}
|
||||
|
||||
void ColorRangeSheetTests::thePreviewPaintsTheMatch()
|
||||
{
|
||||
Sheet shown;
|
||||
QWidget *preview = shown.sheet->layout()->itemAt(1)->widget();
|
||||
// Black before a colour, inside a faint white rim.
|
||||
QImage drawn = preview->grab().toImage();
|
||||
QCOMPARE(drawn.pixelColor(100, 100), QColor(Qt::black));
|
||||
QCOMPARE(qGray(drawn.pixel(0, 100)), 51);
|
||||
// A match repaints it: white over the red half.
|
||||
int painted = 0;
|
||||
struct Counter : QObject {
|
||||
int &count;
|
||||
explicit Counter(int &count) : count(count) {}
|
||||
bool eventFilter(QObject *, QEvent *event) override
|
||||
{
|
||||
count += event->type() == QEvent::Paint;
|
||||
return false;
|
||||
}
|
||||
} counter(painted);
|
||||
preview->installEventFilter(&counter);
|
||||
shown.sampleRed();
|
||||
QTRY_VERIFY(painted > 0);
|
||||
drawn = preview->grab().toImage();
|
||||
QVERIFY(qGray(drawn.pixel(60, 100)) > 240 && qGray(drawn.pixel(200, 100)) < 15);
|
||||
}
|
||||
|
||||
void ColorRangeSheetTests::thePreviewShrinksSmoothly()
|
||||
{
|
||||
// Red then stripes a pixel wide: the preview halves them.
|
||||
QImage image(584, 400, QImage::Format_RGBA8888_Premultiplied);
|
||||
for (int y = 0; y < 400; ++y)
|
||||
for (int x = 0; x < 584; ++x)
|
||||
image.setPixelColor(x, y, x < 40 || x % 2 ? QColor(200, 30, 30) : QColor(30, 40, 210));
|
||||
EditorSession session;
|
||||
session.createDocument(584, 400);
|
||||
session.insert(ImportedImage(image, image, QStringLiteral("Stripes")));
|
||||
session.beginColorRange();
|
||||
ColorRangeSheet sheet(session);
|
||||
QWidget *preview = sheet.layout()->itemAt(1)->widget();
|
||||
QCOMPARE(preview->size(), QSize(292, 200));
|
||||
QSignalSpy changed(&session, &EditorSession::changed);
|
||||
session.sampleColorRange(QPointF(10, 10), false, false);
|
||||
QVERIFY(QTest::qWaitFor([&changed] { return changed.count() >= 2; }));
|
||||
const QImage drawn = preview->grab().toImage();
|
||||
QVERIFY(qGray(drawn.pixel(10, 100)) > 240);
|
||||
QVERIFY(std::abs(qGray(drawn.pixel(150, 100)) - 128) < 20);
|
||||
}
|
||||
|
||||
void ColorRangeSheetTests::invertAndErrorsFollowTheEdit()
|
||||
{
|
||||
Sheet shown;
|
||||
auto &invert = shown.child<QCheckBox>("colorRangeInvert");
|
||||
shown.session.setColorRangeInvert(true);
|
||||
QVERIFY(invert.isChecked());
|
||||
shown.session.setColorRangeInvert(false);
|
||||
QVERIFY(!invert.isChecked());
|
||||
// A checkerboard past the edge limit: the error shows.
|
||||
QImage board(2100, 2100, QImage::Format_RGBA8888_Premultiplied);
|
||||
for (int y = 0; y < board.height(); ++y) {
|
||||
auto *row = reinterpret_cast<quint32 *>(board.scanLine(y));
|
||||
for (int x = 0; x < board.width(); ++x)
|
||||
row[x] = (x + y) % 2 && (x > 2 || y > 2) ? 0xFFFFFFFF : 0xFF000000;
|
||||
}
|
||||
EditorSession session;
|
||||
session.createDocument(2100, 2100);
|
||||
session.insert(ImportedImage(board, board, QStringLiteral("Board")));
|
||||
session.beginColorRange();
|
||||
ColorRangeSheet sheet(session);
|
||||
auto *error = sheet.findChild<QLabel *>(QStringLiteral("colorRangeError"));
|
||||
QVERIFY(error->isHidden());
|
||||
session.setColorRangeFuzziness(0);
|
||||
session.sampleColorRange(QPointF(1, 1), false, false);
|
||||
QTRY_VERIFY_WITH_TIMEOUT(!error->isHidden(), 30000);
|
||||
QCOMPARE(error->text(), QString::fromUtf8(MagicWandError(MagicWandError::Kind::tooDetailed).what()));
|
||||
QCOMPARE(error->foregroundRole(), QPalette::BrightText);
|
||||
}
|
||||
|
||||
QTEST_MAIN(ColorRangeSheetTests)
|
||||
#include "ColorRangeSheetTests.moc"
|
||||
@@ -0,0 +1,403 @@
|
||||
#include "Document/EditorSession.h"
|
||||
#include "SelectionFixtures.h"
|
||||
#include <QFutureWatcher>
|
||||
#include <QtTest>
|
||||
|
||||
extern "C" {
|
||||
#include "WandPixels.h"
|
||||
}
|
||||
|
||||
// Swift 1.3.4's Color Range: model, kernel, session.
|
||||
namespace {
|
||||
// Red, a band of blue, and a clear corner.
|
||||
QImage picture()
|
||||
{
|
||||
QImage image(40, 20, QImage::Format_RGBA8888_Premultiplied);
|
||||
image.fill(QColor(200, 30, 30));
|
||||
for (int y = 0; y < 20; ++y)
|
||||
for (int x = 20; x < 30; ++x)
|
||||
image.setPixelColor(x, y, QColor(30, 40, 210));
|
||||
for (int y = 0; y < 5; ++y)
|
||||
for (int x = 35; x < 40; ++x)
|
||||
image.setPixelColor(x, y, Qt::transparent);
|
||||
return image;
|
||||
}
|
||||
|
||||
struct Opened {
|
||||
EditorSession session;
|
||||
Opened()
|
||||
{
|
||||
session.createDocument(40, 20);
|
||||
session.insert(ImportedImage(picture(), picture(), QStringLiteral("Picture")));
|
||||
session.beginColorRange();
|
||||
}
|
||||
const ColorRangeEdit &edit() const { return session.colorRange().value(); }
|
||||
// A change, then its match landing: a second signal.
|
||||
void land(const std::function<void()> &change)
|
||||
{
|
||||
QSignalSpy changed(&session, &EditorSession::changed);
|
||||
change();
|
||||
QVERIFY(QTest::qWaitFor([&changed] { return changed.count() >= 2; }));
|
||||
}
|
||||
void sample(QPointF point, bool shift = false, bool option = false)
|
||||
{
|
||||
land([&] { session.sampleColorRange(point, shift, option); });
|
||||
}
|
||||
int covered(int x, int y) const { return coverage(session, x, y); }
|
||||
};
|
||||
|
||||
std::vector<uchar> rgb(int r, int g, int b)
|
||||
{
|
||||
return {uchar(r), uchar(g), uchar(b)};
|
||||
}
|
||||
|
||||
// A 2100 checkerboard, black in its corner: past the limit.
|
||||
QImage checkerboard()
|
||||
{
|
||||
QImage board(2100, 2100, QImage::Format_RGBA8888_Premultiplied);
|
||||
for (int y = 0; y < board.height(); ++y) {
|
||||
auto *row = reinterpret_cast<quint32 *>(board.scanLine(y));
|
||||
for (int x = 0; x < board.width(); ++x)
|
||||
row[x] = (x + y) % 2 && (x > 2 || y > 2) ? 0xFFFFFFFF : 0xFF000000;
|
||||
}
|
||||
return board;
|
||||
}
|
||||
|
||||
// The session's workers: its own watchers and one a change.
|
||||
int watchers(const EditorSession &session)
|
||||
{
|
||||
return int(session.findChildren<QFutureWatcherBase *>(Qt::FindDirectChildrenOnly).size());
|
||||
}
|
||||
}
|
||||
|
||||
class ColorRangeTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void theKernelMatchesWithinFuzziness();
|
||||
void aClickPicksTheAverageAroundIt();
|
||||
void addTakeAwayAndStartOver();
|
||||
void fuzzinessInvertAndThePreview();
|
||||
void okIsOneStepAndCancelPutsBack();
|
||||
void theGatesWaitForThePanel();
|
||||
void everyChangeIsAnnounced();
|
||||
void anOutlineTooDetailedIsShownAndKeptOut();
|
||||
void anErrorKeepsTheLastMatch();
|
||||
void onlyTheNewestMatchLands();
|
||||
void thePreviewTruncatesAsSwifts();
|
||||
};
|
||||
|
||||
void ColorRangeTests::theKernelMatchesWithinFuzziness()
|
||||
{
|
||||
// Four pixels: red, red off by 10, blue, clear.
|
||||
const std::array<uchar, 16> pixels{200, 30, 30, 255, 210, 30, 30, 255, 30, 40, 210, 255, 0, 0, 0, 0};
|
||||
std::array<uchar, 4> mask{};
|
||||
const std::vector<uchar> red = rgb(200, 30, 30);
|
||||
QCOMPARE(color_range_mask(pixels.data(), 4, 1, 16, red.data(), 1, nullptr, 0, 9, 0, mask.data()), 1L);
|
||||
QCOMPARE(mask, (std::array<uchar, 4>{255, 0, 0, 0}));
|
||||
QCOMPARE(color_range_mask(pixels.data(), 4, 1, 16, red.data(), 1, nullptr, 0, 10, 0, mask.data()), 2L);
|
||||
QCOMPARE(mask, (std::array<uchar, 4>{255, 255, 0, 0}));
|
||||
// Taken away wins; inverted, clear pixels are selected too.
|
||||
const std::vector<uchar> off = rgb(210, 30, 30);
|
||||
QCOMPARE(color_range_mask(pixels.data(), 4, 1, 16, red.data(), 1, off.data(), 1, 10, 0, mask.data()), 0L);
|
||||
QCOMPARE(color_range_mask(pixels.data(), 4, 1, 16, red.data(), 1, nullptr, 0, 10, 1, mask.data()), 2L);
|
||||
QCOMPARE(mask, (std::array<uchar, 4>{0, 0, 255, 255}));
|
||||
// Premultiplied pixels are read straight: half clear red matches red.
|
||||
const std::array<uchar, 4> half{100, 15, 15, 128};
|
||||
std::array<uchar, 1> one{};
|
||||
QCOMPARE(color_range_mask(half.data(), 1, 1, 4, red.data(), 1, nullptr, 0, 1, 0, one.data()), 1L);
|
||||
}
|
||||
|
||||
void ColorRangeTests::aClickPicksTheAverageAroundIt()
|
||||
{
|
||||
Opened opened;
|
||||
QVERIFY(!opened.edit().hasColors() && opened.edit().preview.isNull());
|
||||
opened.sample(QPointF(5.7, 10.2));
|
||||
QCOMPARE(opened.edit().include, rgb(200, 30, 30));
|
||||
// On the seam: three red and six blue, averaged.
|
||||
opened.sample(QPointF(20, 10));
|
||||
QCOMPARE(opened.edit().include, rgb((3 * 200 + 6 * 30 + 4) / 9, (3 * 30 + 6 * 40 + 4) / 9, (3 * 30 + 6 * 210 + 4) / 9));
|
||||
// At the clear corner's edge only coloured pixels count.
|
||||
opened.sample(QPointF(34, 2));
|
||||
QCOMPARE(opened.edit().include, rgb(200, 30, 30));
|
||||
// The click's pixel is floored: 19.6 reads round 19.
|
||||
opened.sample(QPointF(19.6, 10));
|
||||
QCOMPARE(opened.edit().include, rgb((6 * 200 + 3 * 30 + 4) / 9, (6 * 30 + 3 * 40 + 4) / 9, (6 * 30 + 3 * 210 + 4) / 9));
|
||||
// In the clear corner, past the edge, no number: nothing.
|
||||
for (const QPointF point : {QPointF(38, 2), QPointF(-1, 3), QPointF(40, 3), QPointF(qQNaN(), 3)}) {
|
||||
const int generation = opened.edit().generation;
|
||||
opened.session.sampleColorRange(point, false, false);
|
||||
QCOMPARE(opened.edit().generation, generation);
|
||||
}
|
||||
// Three rows, one of each colour: all three count.
|
||||
QImage rows(3, 3, QImage::Format_RGBA8888_Premultiplied);
|
||||
for (int x = 0; x < 3; ++x) {
|
||||
rows.setPixelColor(x, 0, QColor(90, 0, 0));
|
||||
rows.setPixelColor(x, 1, QColor(0, 90, 0));
|
||||
rows.setPixelColor(x, 2, QColor(0, 0, 90));
|
||||
}
|
||||
EditorSession session;
|
||||
session.createDocument(3, 3);
|
||||
session.insert(ImportedImage(rows, rows, QStringLiteral("Rows")));
|
||||
session.beginColorRange();
|
||||
session.sampleColorRange(QPointF(1, 1), false, false);
|
||||
QCOMPARE(session.colorRange().value().include, rgb(30, 30, 30));
|
||||
}
|
||||
|
||||
void ColorRangeTests::addTakeAwayAndStartOver()
|
||||
{
|
||||
Opened opened;
|
||||
opened.sample(QPointF(5, 5));
|
||||
QCOMPARE(opened.covered(5, 5), 255);
|
||||
QCOMPARE(opened.covered(25, 5), 0);
|
||||
QVERIFY(opened.session.selection().value().antialiased);
|
||||
// The match takes the session's antialiasing.
|
||||
opened.session.setSelectionAntialiased(false);
|
||||
opened.sample(QPointF(6, 5));
|
||||
QVERIFY(!opened.session.selection().value().antialiased);
|
||||
// Shift adds blue; Alt takes red away.
|
||||
opened.sample(QPointF(25, 5), true, false);
|
||||
QCOMPARE(opened.edit().include, (std::vector<uchar>{200, 30, 30, 30, 40, 210}));
|
||||
QCOMPARE(opened.covered(25, 5), 255);
|
||||
opened.sample(QPointF(5, 5), true, true);
|
||||
QCOMPARE(opened.edit().exclude, rgb(200, 30, 30));
|
||||
QVERIFY(opened.covered(5, 5) == 0 && opened.covered(25, 5) == 255);
|
||||
// The chosen eyedroppers act without keys; replace starts over.
|
||||
opened.session.setColorRangeSampleMode(HueSampleMode::remove);
|
||||
opened.sample(QPointF(25, 5));
|
||||
QCOMPARE(opened.edit().exclude, (std::vector<uchar>{200, 30, 30, 30, 40, 210}));
|
||||
QVERIFY(!opened.session.selection());
|
||||
opened.session.setColorRangeSampleMode(HueSampleMode::replace);
|
||||
opened.sample(QPointF(25, 5));
|
||||
QVERIFY(opened.edit().include == rgb(30, 40, 210) && opened.edit().exclude.empty());
|
||||
QCOMPARE(opened.covered(25, 5), 255);
|
||||
// Held keys outrank the chosen eyedropper.
|
||||
opened.session.setColorRangeHeld(HueSampleMode::add);
|
||||
QCOMPARE(opened.edit().effectiveMode(), HueSampleMode::add);
|
||||
}
|
||||
|
||||
void ColorRangeTests::fuzzinessInvertAndThePreview()
|
||||
{
|
||||
Opened opened;
|
||||
opened.sample(QPointF(5, 5));
|
||||
// The preview: the mask at twice the panel's fit.
|
||||
const double scale = std::min(292.0 / 40, 200.0 / 20) * 2;
|
||||
QCOMPARE(opened.edit().preview.size(), QSize(int(40 * scale), int(20 * scale)));
|
||||
QCOMPARE(qGray(opened.edit().preview.pixel(10, 10)), 255);
|
||||
QCOMPARE(qGray(opened.edit().preview.pixel(int(25 * scale), 10)), 0);
|
||||
// Fuzziness: whole, 0 to 200; wide, blue joins.
|
||||
opened.land([&] { opened.session.setColorRangeFuzziness(250.4); });
|
||||
QCOMPARE(opened.edit().fuzziness, 200.0);
|
||||
QCOMPARE(opened.covered(25, 5), 255);
|
||||
opened.session.setColorRangeFuzziness(-3);
|
||||
QCOMPARE(opened.edit().fuzziness, 0.0);
|
||||
opened.session.setColorRangeFuzziness(12.6);
|
||||
QCOMPARE(opened.edit().fuzziness, 13.0);
|
||||
// Invert: all but red, the clear corner included.
|
||||
opened.land([&] { opened.session.setColorRangeInvert(true); });
|
||||
QVERIFY(opened.covered(5, 5) == 0 && opened.covered(25, 5) == 255 && opened.covered(38, 2) == 255);
|
||||
// No colours: the old selection and no preview.
|
||||
Opened empty;
|
||||
empty.session.updateColorRange();
|
||||
QVERIFY(!empty.session.selection() && empty.edit().preview.isNull());
|
||||
}
|
||||
|
||||
void ColorRangeTests::okIsOneStepAndCancelPutsBack()
|
||||
{
|
||||
Opened opened;
|
||||
QPainterPath corner;
|
||||
corner.addRect(0, 0, 4, 4);
|
||||
const DocumentSelection before{corner, true};
|
||||
opened.session.cancelColorRange();
|
||||
opened.session.setSelection(before, QStringLiteral("Before"));
|
||||
opened.session.beginColorRange();
|
||||
QCOMPARE(opened.edit().original.value().path, before.path);
|
||||
opened.sample(QPointF(25, 5));
|
||||
QCOMPARE(opened.covered(1, 1), 0);
|
||||
// Cancel: the selection there was, no step.
|
||||
const int steps = opened.session.history.undoCount();
|
||||
opened.session.cancelColorRange();
|
||||
QVERIFY(!opened.session.colorRange() && opened.covered(1, 1) == 255 && opened.covered(25, 5) == 0);
|
||||
QCOMPARE(opened.session.history.undoCount(), steps);
|
||||
// OK: one step, named; undo puts the old back.
|
||||
opened.session.beginColorRange();
|
||||
opened.sample(QPointF(25, 5));
|
||||
opened.session.commitColorRange();
|
||||
QCOMPARE(opened.session.history.undoCount(), steps + 1);
|
||||
QCOMPARE(opened.session.history.undoName(), QStringLiteral("Color Range"));
|
||||
QCOMPARE(opened.covered(25, 5), 255);
|
||||
opened.session.undo();
|
||||
QVERIFY(opened.covered(1, 1) == 255 && opened.covered(25, 5) == 0);
|
||||
// OK with no colour keeps the old selection, no step.
|
||||
opened.session.beginColorRange();
|
||||
opened.session.commitColorRange();
|
||||
QCOMPARE(opened.session.history.undoCount(), steps);
|
||||
QCOMPARE(opened.covered(1, 1), 255);
|
||||
// OK on a match of nothing deselects.
|
||||
opened.session.beginColorRange();
|
||||
opened.sample(QPointF(5, 5));
|
||||
opened.sample(QPointF(5, 5), false, true);
|
||||
QVERIFY(!opened.session.selection());
|
||||
opened.session.commitColorRange();
|
||||
QVERIFY(!opened.session.selection());
|
||||
QCOMPARE(opened.session.history.undoName(), QStringLiteral("Deselect"));
|
||||
}
|
||||
|
||||
void ColorRangeTests::theGatesWaitForThePanel()
|
||||
{
|
||||
Opened opened;
|
||||
QVERIFY(!opened.session.canEditLayers() && !opened.session.canUseHistory() && !opened.session.canStartProjectOperation());
|
||||
QVERIFY(!opened.session.canSelectColorRange());
|
||||
// A second begin keeps the open edit.
|
||||
const QUuid id = opened.edit().id;
|
||||
opened.session.beginColorRange();
|
||||
QCOMPARE(opened.edit().id, id);
|
||||
opened.session.cancelColorRange();
|
||||
QVERIFY(opened.session.canEditLayers() && opened.session.canUseHistory() && opened.session.canStartProjectOperation());
|
||||
QVERIFY(opened.session.canSelectColorRange());
|
||||
// A request waiting on the panel runs once it closes.
|
||||
opened.session.beginColorRange();
|
||||
bool ran = false;
|
||||
opened.session.waitForFileRequest([&ran] { ran = true; });
|
||||
QTest::qWait(10);
|
||||
QVERIFY(!ran);
|
||||
opened.session.cancelColorRange();
|
||||
QTRY_VERIFY(ran);
|
||||
// OK frees one too.
|
||||
opened.session.beginColorRange();
|
||||
ran = false;
|
||||
opened.session.waitForFileRequest([&ran] { ran = true; });
|
||||
QTest::qWait(10);
|
||||
QVERIFY(!ran);
|
||||
opened.session.commitColorRange();
|
||||
QTRY_VERIFY(ran);
|
||||
// Without a document there is nothing to open.
|
||||
EditorSession empty;
|
||||
QVERIFY(!empty.canSelectColorRange());
|
||||
empty.beginColorRange();
|
||||
QVERIFY(!empty.colorRange());
|
||||
}
|
||||
|
||||
void ColorRangeTests::everyChangeIsAnnounced()
|
||||
{
|
||||
Opened opened;
|
||||
QSignalSpy changed(&opened.session, &EditorSession::changed);
|
||||
// Before a colour, Fuzziness and Invert still say so.
|
||||
opened.session.setColorRangeFuzziness(50);
|
||||
QCOMPARE(changed.count(), 1);
|
||||
opened.session.setColorRangeInvert(true);
|
||||
QCOMPARE(changed.count(), 2);
|
||||
opened.session.setColorRangeInvert(false);
|
||||
// Each eyedropper and held key; an unchanged one is silent.
|
||||
changed.clear();
|
||||
opened.session.setColorRangeSampleMode(HueSampleMode::add);
|
||||
opened.session.setColorRangeSampleMode(HueSampleMode::add);
|
||||
opened.session.setColorRangeHeld(HueSampleMode::remove);
|
||||
opened.session.setColorRangeHeld(HueSampleMode::remove);
|
||||
opened.session.setColorRangeFuzziness(50);
|
||||
QCOMPARE(changed.count(), 2);
|
||||
// A click says so now, and as its match lands.
|
||||
opened.session.setColorRangeHeld(std::nullopt);
|
||||
opened.session.setColorRangeSampleMode(HueSampleMode::replace);
|
||||
changed.clear();
|
||||
opened.session.sampleColorRange(QPointF(5, 5), false, false);
|
||||
QCOMPARE(changed.count(), 1);
|
||||
QTRY_COMPARE(changed.count(), 2);
|
||||
QCOMPARE(opened.covered(5, 5), 255);
|
||||
}
|
||||
|
||||
void ColorRangeTests::anOutlineTooDetailedIsShownAndKeptOut()
|
||||
{
|
||||
const QImage board = checkerboard();
|
||||
EditorSession session;
|
||||
session.createDocument(2100, 2100);
|
||||
session.insert(ImportedImage(board, board, QStringLiteral("Board")));
|
||||
session.beginColorRange();
|
||||
session.setColorRangeFuzziness(0);
|
||||
session.sampleColorRange(QPointF(1, 1), false, false);
|
||||
QTRY_VERIFY_WITH_TIMEOUT(session.colorRange().value().error, 30000);
|
||||
QCOMPARE(session.colorRange().value().error.value(), QString::fromUtf8(MagicWandError(MagicWandError::Kind::tooDetailed).what()));
|
||||
// The preview still shows; the selection stays as it was.
|
||||
QVERIFY(!session.colorRange().value().preview.isNull() && !session.selection());
|
||||
const int steps = session.history.undoCount();
|
||||
session.commitColorRange();
|
||||
QVERIFY(!session.selection() && !session.colorRange());
|
||||
QCOMPARE(session.history.undoCount(), steps);
|
||||
}
|
||||
|
||||
void ColorRangeTests::anErrorKeepsTheLastMatch()
|
||||
{
|
||||
// A red band on top; a black block amid checks.
|
||||
QImage board = checkerboard();
|
||||
for (int y = 0; y < 10; ++y)
|
||||
for (int x = 0; x < board.width(); ++x)
|
||||
board.setPixelColor(x, y, QColor(200, 30, 30));
|
||||
for (int y = 100; y < 103; ++y)
|
||||
for (int x = 100; x < 103; ++x)
|
||||
board.setPixelColor(x, y, Qt::black);
|
||||
EditorSession session;
|
||||
session.createDocument(2100, 2100);
|
||||
session.insert(ImportedImage(board, board, QStringLiteral("Board")));
|
||||
session.beginColorRange();
|
||||
session.setColorRangeFuzziness(0);
|
||||
session.sampleColorRange(QPointF(10, 4), false, false);
|
||||
QTRY_VERIFY_WITH_TIMEOUT(session.selection(), 30000);
|
||||
QVERIFY(coverage(session, 10, 4) == 255 && coverage(session, 500, 500) == 0);
|
||||
// Black matches every dark check: too detailed, the band stays.
|
||||
session.sampleColorRange(QPointF(101, 101), false, false);
|
||||
QTRY_VERIFY_WITH_TIMEOUT(session.colorRange().value().error, 30000);
|
||||
QVERIFY(coverage(session, 10, 4) == 255 && coverage(session, 500, 500) == 0);
|
||||
// OK after an error: the old selection, no step.
|
||||
const int steps = session.history.undoCount();
|
||||
session.commitColorRange();
|
||||
QVERIFY(!session.selection() && session.history.undoCount() == steps);
|
||||
}
|
||||
|
||||
void ColorRangeTests::onlyTheNewestMatchLands()
|
||||
{
|
||||
EditorSession session;
|
||||
session.createDocument(2100, 2100);
|
||||
const QImage board = checkerboard();
|
||||
session.insert(ImportedImage(board, board, QStringLiteral("Board")));
|
||||
const int idle = watchers(session);
|
||||
session.beginColorRange();
|
||||
session.setColorRangeFuzziness(0);
|
||||
// A slow trace, then a gray that matches nothing, quickly.
|
||||
session.sampleColorRange(QPointF(1, 1), false, false);
|
||||
session.sampleColorRange(QPointF(3, 3), false, false);
|
||||
QTRY_COMPARE_WITH_TIMEOUT(watchers(session), idle, 30000);
|
||||
QVERIFY(!session.colorRange().value().error && !session.colorRange().value().preview.isNull());
|
||||
// A closed edit's trace lands on no later edit.
|
||||
session.cancelColorRange();
|
||||
session.beginColorRange();
|
||||
session.sampleColorRange(QPointF(1, 1), false, false);
|
||||
session.cancelColorRange();
|
||||
session.beginColorRange();
|
||||
// Both at generation one: the edit's id tells them apart.
|
||||
session.sampleColorRange(QPointF(3, 3), false, false);
|
||||
QCOMPARE(session.colorRange().value().generation, 1);
|
||||
QTRY_COMPARE_WITH_TIMEOUT(watchers(session), idle, 30000);
|
||||
QVERIFY(!session.colorRange().value().error);
|
||||
// Nor on another document.
|
||||
Opened opened;
|
||||
const int quiet = watchers(opened.session);
|
||||
opened.session.sampleColorRange(QPointF(5, 5), false, false);
|
||||
opened.session.createDocument(40, 20);
|
||||
QTRY_COMPARE_WITH_TIMEOUT(watchers(opened.session), quiet, 30000);
|
||||
QVERIFY(!opened.session.selection() && opened.edit().preview.isNull());
|
||||
}
|
||||
|
||||
void ColorRangeTests::thePreviewTruncatesAsSwifts()
|
||||
{
|
||||
EditorSession session;
|
||||
session.createDocument(400, 300);
|
||||
QImage red(400, 300, QImage::Format_RGBA8888_Premultiplied);
|
||||
red.fill(QColor(200, 30, 30));
|
||||
session.insert(ImportedImage(red, red, QStringLiteral("Red")));
|
||||
session.beginColorRange();
|
||||
session.sampleColorRange(QPointF(5, 5), false, false);
|
||||
QTRY_VERIFY(!session.colorRange().value().preview.isNull());
|
||||
QCOMPARE(session.colorRange().value().preview.size(), QSize(533, 400));
|
||||
}
|
||||
|
||||
QTEST_MAIN(ColorRangeTests)
|
||||
#include "ColorRangeTests.moc"
|
||||
@@ -34,7 +34,7 @@ void CompositorMenusTests::everyEntryHasSwiftsShortcutWithCtrlForCommand()
|
||||
{"layerViaCopy", "Ctrl+J"}, {"cut", "Ctrl+X"}, {"copy", "Ctrl+C"}, {"copyMerged", "Ctrl+Shift+C"}, {"paste", "Ctrl+V"}, {"keyboardShortcuts", ""}, {"fillForeground", "Alt+Backspace"}, {"fillBackground", "Ctrl+Backspace"}, {"clearSelectionPixels", ""}, {"contentAwareFill", "Shift+Backspace"}, {"clippingMask", "Ctrl+Alt+G"}, {"groupLayers", "Ctrl+G"},
|
||||
{"moveOutOfFolder", ""}, {"newBlankLayer", "Ctrl+Shift+N"}, {"renameLayer", ""}, {"layerVisibility", ""}, {"moveLayerUp", "Ctrl+]"}, {"moveLayerDown", "Ctrl+["},
|
||||
{"mergeLayers", "Ctrl+E"}, {"flipHorizontal", ""}, {"flipVertical", ""}, {"deleteLayer", ""},
|
||||
{"selectAll", "Ctrl+A"}, {"deselect", "Ctrl+D"}, {"inverse", "Ctrl+Shift+I"}, {"layerPixels", ""}, {"subject", "Ctrl+Alt+A"}, {"maskBlackAreas", ""},
|
||||
{"selectAll", "Ctrl+A"}, {"deselect", "Ctrl+D"}, {"inverse", "Ctrl+Shift+I"}, {"layerPixels", ""}, {"subject", "Ctrl+Alt+A"}, {"colorRange", ""}, {"maskBlackAreas", ""},
|
||||
{"expandSelection", ""}, {"contractSelection", ""}, {"featherSelection", ""}, {"curves", "Ctrl+M"}, {"levels", "Ctrl+L"}, {"hueSaturation", "Ctrl+U"}, {"blackWhite", ""}, {"colorBalance", ""}, {"exposure", ""}, {"gradientMap", ""}, {"grain", ""}, {"invert", "Ctrl+I"},
|
||||
{"canvasSize", "Ctrl+Alt+C"}, {"imageSize", "Ctrl+Alt+I"}, {"trim", ""}, {"flipCanvasHorizontal", ""}, {"flipCanvasVertical", ""},
|
||||
{"gaussianBlur", ""}, {"motionBlur", ""}, {"addNoise", ""}, {"lensCorrection", ""}, {"removeBackground", ""}, {"newAdjustmentLayer", ""}, {"editAdjustment", ""},
|
||||
@@ -53,7 +53,10 @@ void CompositorMenusTests::everyEntryHasSwiftsShortcutWithCtrlForCommand()
|
||||
QCOMPARE(menus[0]->actions().size(), 12);
|
||||
QCOMPARE(menus[1]->actions().size(), 13);
|
||||
QCOMPARE(menus[2]->actions().size(), 16);
|
||||
QCOMPARE(menus[3]->actions().size(), 10);
|
||||
QCOMPARE(menus[3]->actions().size(), 11);
|
||||
// Swift 1.3.4's Color Range… follows Subject.
|
||||
const QList<QAction *> select = menus[3]->actions();
|
||||
QCOMPARE(select.indexOf(&bar.action("colorRange")), select.indexOf(&bar.action("subject")) + 1);
|
||||
QCOMPARE(menus[4]->actions().size(), 16);
|
||||
// Swift's Trim… follows Image Size….
|
||||
const QList<QAction *> image = menus[4]->actions();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "InlineTextFixtures.h"
|
||||
#include "UI/CanvasSizeSheet.h"
|
||||
#include "UI/ColorPickerSheet.h"
|
||||
#include "UI/ColorRangeSheet.h"
|
||||
#include "UI/EffectsSheet.h"
|
||||
#include "UI/FilterSheet.h"
|
||||
#include "UI/HueSaturationSheet.h"
|
||||
@@ -245,6 +246,9 @@ void RemappedKeysTests::everySheetBindsSwiftsKeys()
|
||||
session.beginFilter(FilterKind::gaussianBlur);
|
||||
QCOMPARE(keys(FilterSheet(session)), both);
|
||||
session.cancelFilter();
|
||||
session.beginColorRange();
|
||||
QCOMPARE(keys(ColorRangeSheet(session)), both);
|
||||
session.cancelColorRange();
|
||||
}
|
||||
|
||||
void RemappedKeysTests::aKeyLeftAloneReachesTheParent()
|
||||
|
||||
@@ -175,6 +175,11 @@ compositor_test(TextColorEditorTests)
|
||||
compositor_test(TextEditingGatesTests)
|
||||
compositor_test(TextFontRunsTests)
|
||||
compositor_test(TextFontControlsTests)
|
||||
compositor_test(ColorRangeTests)
|
||||
compositor_test(ColorRangeCanvasTests)
|
||||
compositor_test(ColorRangeSheetTests)
|
||||
# It calls the colour-range kernel itself.
|
||||
target_include_directories(ColorRangeTests PRIVATE ${OMAPHOTO_KERNELS})
|
||||
compositor_test(DitherTests)
|
||||
compositor_test(DitherSheetTests)
|
||||
compositor_test(DitherKernelTests)
|
||||
|
||||
Reference in new issue
Block a user