Scrub numbers by dragging their labels in the tool bars and layer opacity (12.12c1)

This commit is contained in:
雷电芽衣 committed 2026-09-29 15:06:49 -04:00
1 parent f335818e53
commit 5bc7889387
25 files changed
+1126 -32

No files matched your search

+282
View File
@@ -0,0 +1,282 @@
#include "UI/NumericScrub.h"
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QtTest>
// Swift's NumericScrub: a label dragged sideways sets its value.
namespace {
// Counts the moves that pass the scrub, which runs first.
struct MoveCounter : QObject {
int moves = 0;
bool eventFilter(QObject *, QEvent *event) override
{
moves += event->type() == QEvent::MouseMove;
return false;
}
};
struct Probe {
QWidget window;
QLabel *label = new QLabel(QStringLiteral("Size"), &window);
QLineEdit *other = new QLineEdit(&window);
double value = 10;
QStringList log;
MoveCounter counter;
explicit Probe(double sensitivity = 1, double low = 0, double high = 100, std::optional<double> step = std::nullopt)
{
auto *row = new QHBoxLayout(&window);
row->addWidget(label);
row->addWidget(other);
window.resize(300, 40);
label->installEventFilter(&counter);
new NumericScrub(label, {.sensitivity = sensitivity, .low = low, .high = high, .step = step, .value = [this] { return value; },
.set = [this](double set) { value = set; log << QString::number(set); },
.onStart = [this] { log << "began"; }, .onEnd = [this] { log << "ended"; }});
window.show();
if (!QTest::qWaitForWindowExposed(&window))
throw std::runtime_error("the probe never showed");
}
};
// A mouse event at a widget's point, sent directly.
bool send(QWidget *widget, QEvent::Type type, QPointF at, Qt::MouseButton button, Qt::MouseButtons buttons)
{
QMouseEvent event(type, at, widget->mapToGlobal(at), button, buttons, Qt::NoModifier);
QApplication::sendEvent(widget, &event);
return event.isAccepted();
}
void press(QWidget *widget, QPointF at, Qt::MouseButton button = Qt::LeftButton)
{
send(widget, QEvent::MouseButtonPress, at, button, button);
}
void move(QWidget *widget, QPointF at, Qt::MouseButtons buttons = Qt::LeftButton)
{
send(widget, QEvent::MouseMove, at, Qt::NoButton, buttons);
}
void release(QWidget *widget, QPointF at, Qt::MouseButton button = Qt::LeftButton)
{
send(widget, QEvent::MouseButtonRelease, at, button, Qt::NoButton);
}
}
class NumericScrubTests : public QObject {
Q_OBJECT
private slots:
void theLabelShowsTheSidewaysCursor();
void aDragFromThePressSetsTheValueOnceItMoves();
void stepsSnapAndTheRangeClamps();
void aClickAloneChangesNothing();
void onlyTheLeftButtonScrubs();
void aLostReleaseEndsTheDrag();
void theDragOutlivesThePointerLeavingTheLabel();
void aDisabledLabelScrubsNothing();
void anyMoveOfAPointBeginsTheDrag();
void aDoubleClicksSecondPressScrubs();
};
void NumericScrubTests::theLabelShowsTheSidewaysCursor()
{
Probe probe;
QCOMPARE(probe.label->cursor().shape(), Qt::SizeHorCursor);
QVERIFY(probe.label->testAttribute(Qt::WA_SetCursor));
QVERIFY(!probe.window.testAttribute(Qt::WA_SetCursor));
// Tracked, so a move after a lost release arrives.
QVERIFY(probe.label->hasMouseTracking());
}
void NumericScrubTests::aDragFromThePressSetsTheValueOnceItMoves()
{
Probe probe(0.5);
press(probe.label, QPointF(10, 5));
// Unmoved, Swift's gesture has not begun.
move(probe.label, QPointF(10, 5));
QVERIFY(probe.log.isEmpty());
move(probe.label, QPointF(14, 5));
QCOMPARE(probe.log, QStringList({"began", "12"}));
// Only the sideways part counts; the start stays fixed.
move(probe.label, QPointF(40, 30));
QCOMPARE(probe.value, 25.0);
move(probe.label, QPointF(9, 5));
QCOMPARE(probe.value, 9.5);
QCOMPARE(probe.log.count("began"), 1);
release(probe.label, QPointF(9, 5));
QCOMPARE(probe.log.last(), QString("ended"));
QCOMPARE(probe.log.count("ended"), 1);
// The next drag starts from where the last one left.
press(probe.label, QPointF(10, 5));
move(probe.label, QPointF(30, 5));
QCOMPARE(probe.value, 19.5);
release(probe.label, QPointF(30, 5));
QCOMPARE(probe.log.count("began"), 2);
QCOMPARE(probe.log.count("ended"), 2);
}
void NumericScrubTests::stepsSnapAndTheRangeClamps()
{
Probe probe(0.3, 5, 12, 1);
press(probe.label, QPointF(100, 5));
move(probe.label, QPointF(105, 5));
// 10 + 1.5 rounds away from zero, as Swift's `rounded()`.
QCOMPARE(probe.value, 12.0);
move(probe.label, QPointF(104, 5));
QCOMPARE(probe.value, 11.0);
move(probe.label, QPointF(200, 5));
QCOMPARE(probe.value, 12.0);
move(probe.label, QPointF(0, 5));
QCOMPARE(probe.value, 5.0);
release(probe.label, QPointF(0, 5));
// A step that is no step leaves the value unsnapped.
Probe free(0.3, 0, 100, 0);
press(free.label, QPointF(100, 5));
move(free.label, QPointF(105, 5));
QCOMPARE(free.value, 11.5);
release(free.label, QPointF(105, 5));
// Halves below zero round away from it, as Swift's.
Probe negative(0.5, -100, 100, 1);
press(negative.label, QPointF(100, 5));
move(negative.label, QPointF(77, 5));
QCOMPARE(negative.value, -2.0);
release(negative.label, QPointF(77, 5));
// A fractional step snaps to its multiples.
Probe tenths(0.07, 0, 100, 0.25);
press(tenths.label, QPointF(100, 5));
move(tenths.label, QPointF(105, 5));
QCOMPARE(tenths.value, 10.25);
release(tenths.label, QPointF(105, 5));
}
void NumericScrubTests::aClickAloneChangesNothing()
{
Probe probe;
QVERIFY(send(probe.label, QEvent::MouseButtonPress, QPointF(10, 5), Qt::LeftButton, Qt::LeftButton));
release(probe.label, QPointF(10, 5));
QVERIFY(probe.log.isEmpty());
// Moves without a press scrub nothing.
move(probe.label, QPointF(40, 5));
QVERIFY(probe.log.isEmpty());
QCOMPARE(probe.value, 10.0);
}
void NumericScrubTests::onlyTheLeftButtonScrubs()
{
Probe probe;
// Unpressed, moves go on to the label untouched.
send(probe.label, QEvent::MouseMove, QPointF(40, 5), Qt::NoButton, Qt::NoButton);
QCOMPARE(probe.counter.moves, 1);
// The label lets other presses go on to its parent.
QVERIFY(!send(probe.label, QEvent::MouseButtonPress, QPointF(10, 5), Qt::RightButton, Qt::RightButton));
move(probe.label, QPointF(40, 5), Qt::RightButton);
release(probe.label, QPointF(40, 5), Qt::RightButton);
QVERIFY(probe.log.isEmpty());
// Another button's release leaves a left drag going.
press(probe.label, QPointF(10, 5));
move(probe.label, QPointF(20, 5));
send(probe.label, QEvent::MouseButtonRelease, QPointF(20, 5), Qt::RightButton, Qt::LeftButton);
send(probe.other, QEvent::MouseButtonRelease, QPointF(5, 5), Qt::RightButton, Qt::LeftButton);
move(probe.label, QPointF(30, 5));
QCOMPARE(probe.log, QStringList({"began", "20", "30"}));
// The drag's own moves stop at the scrub.
QCOMPARE(probe.counter.moves, 2);
release(probe.label, QPointF(30, 5));
QCOMPARE(probe.log.last(), QString("ended"));
}
void NumericScrubTests::aLostReleaseEndsTheDrag()
{
Probe probe;
press(probe.label, QPointF(10, 5));
move(probe.label, QPointF(20, 5));
// A move without the button: the release went elsewhere.
move(probe.label, QPointF(60, 5), Qt::NoButton);
QCOMPARE(probe.log, QStringList({"began", "20", "ended"}));
move(probe.label, QPointF(70, 5));
QCOMPARE(probe.value, 20.0);
// A release a popup takes ends it, seen application-wide.
press(probe.label, QPointF(10, 5));
move(probe.label, QPointF(15, 5));
release(probe.other, QPointF(5, 5));
QCOMPARE(probe.log.mid(3), QStringList({"began", "25", "ended"}));
move(probe.label, QPointF(70, 5));
QCOMPARE(probe.value, 25.0);
// Another widget's release before any move ends only the press.
press(probe.label, QPointF(10, 5));
release(probe.other, QPointF(5, 5));
move(probe.label, QPointF(70, 5));
QCOMPARE(probe.log.size(), 6);
// A move holding another button alone lost the left release.
press(probe.label, QPointF(10, 5));
move(probe.label, QPointF(12, 5));
move(probe.label, QPointF(30, 5), Qt::RightButton);
QCOMPARE(probe.log.mid(6), QStringList({"began", "27", "ended"}));
}
void NumericScrubTests::theDragOutlivesThePointerLeavingTheLabel()
{
Probe probe;
QWindow *window = probe.window.windowHandle();
const QPoint start = probe.label->mapTo(&probe.window, QPoint(5, probe.label->height() / 2));
QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, start);
QTest::mouseMove(window, start + QPoint(3, 0));
// Past the label and the field, out of the window.
QTest::mouseMove(window, start + QPoint(200, 0));
QTest::mouseMove(window, start + QPoint(400, 0));
QCOMPARE(probe.value, 100.0);
QTest::mouseMove(window, start + QPoint(30, 0));
QCOMPARE(probe.value, 40.0);
QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, start + QPoint(30, 0));
QCOMPARE(probe.log, QStringList({"began", "13", "100", "100", "40", "ended"}));
QCOMPARE(probe.value, 40.0);
}
void NumericScrubTests::aDisabledLabelScrubsNothing()
{
Probe probe;
probe.label->setEnabled(false);
QWindow *window = probe.window.windowHandle();
const QPoint start = probe.label->mapTo(&probe.window, QPoint(5, probe.label->height() / 2));
QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, start);
QTest::mouseMove(window, start + QPoint(30, 0));
QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, start + QPoint(30, 0));
QVERIFY(probe.log.isEmpty());
QCOMPARE(probe.value, 10.0);
}
void NumericScrubTests::anyMoveOfAPointBeginsTheDrag()
{
Probe probe;
press(probe.label, QPointF(10, 5));
// Under a point, Swift's gesture has not begun.
move(probe.label, QPointF(10.6, 5));
QVERIFY(probe.log.isEmpty());
move(probe.label, QPointF(11, 5));
QCOMPARE(probe.log, QStringList({"began", "11"}));
release(probe.label, QPointF(11, 5));
// Straight down begins too, and sets the start again.
press(probe.label, QPointF(10, 5));
move(probe.label, QPointF(10, 8));
QCOMPARE(probe.log.mid(3), QStringList({"began", "11"}));
release(probe.label, QPointF(10, 8));
}
void NumericScrubTests::aDoubleClicksSecondPressScrubs()
{
Probe probe;
press(probe.label, QPointF(10, 5));
release(probe.label, QPointF(10, 5));
send(probe.label, QEvent::MouseButtonDblClick, QPointF(10, 5), Qt::LeftButton, Qt::LeftButton);
move(probe.label, QPointF(30, 5));
release(probe.label, QPointF(30, 5));
QCOMPARE(probe.log, QStringList({"began", "30", "ended"}));
// A disabled label's double click scrubs nothing either.
probe.label->setEnabled(false);
send(probe.label, QEvent::MouseButtonDblClick, QPointF(10, 5), Qt::LeftButton, Qt::LeftButton);
move(probe.label, QPointF(60, 5));
QCOMPARE(probe.value, 30.0);
}
QTEST_MAIN(NumericScrubTests)
#include "NumericScrubTests.moc"
+76
View File
@@ -0,0 +1,76 @@
#pragma once
#include "SelectionFixtures.h"
#include "SessionFixtures.h"
#include <QLabel>
#include <QLineEdit>
#include <QtTest>
// Shared by the scrubbable label tests: finding, dragging, typing.
// The scrubbing labels saying `text`, left to right.
inline QLabel &scrubbed(QWidget &root, const QString &text, int index = 0)
{
std::vector<QLabel *> found;
for (QLabel *label : root.findChildren<QLabel *>())
if (label->text() == text && label->testAttribute(Qt::WA_SetCursor) && label->cursor().shape() == Qt::SizeHorCursor)
found.push_back(label);
std::sort(found.begin(), found.end(), [&root](QLabel *a, QLabel *b) { return a->mapTo(&root, QPoint()).x() < b->mapTo(&root, QPoint()).x(); });
if (size_t(index) >= found.size())
throw std::runtime_error("no scrubbing label " + text.toStdString());
return *found[size_t(index)];
}
inline void send(QWidget &widget, QEvent::Type type, QPointF at, Qt::MouseButton button, Qt::MouseButtons buttons)
{
QMouseEvent event(type, at, widget.mapToGlobal(at), button, buttons, Qt::NoModifier);
QApplication::sendEvent(&widget, &event);
}
// Presses, drags `dx` points sideways, and lets go.
inline void drag(QWidget &label, double dx, bool release = true)
{
send(label, QEvent::MouseButtonPress, QPointF(2, 2), Qt::LeftButton, Qt::LeftButton);
send(label, QEvent::MouseMove, QPointF(2 + dx, 2), Qt::NoButton, Qt::LeftButton);
if (release)
send(label, QEvent::MouseButtonRelease, QPointF(2 + dx, 2), Qt::LeftButton, Qt::NoButton);
}
// Types 7, which applies, then scrubs, holding the press.
inline QString scrubOverTyping(QLineEdit &field, QWidget &label, double dx)
{
field.setFocus();
field.selectAll();
QTest::keyClicks(&field, QStringLiteral("7"));
if (!field.hasFocus())
throw std::runtime_error("the field lost its focus");
drag(label, dx, false);
const QString during = field.text();
send(label, QEvent::MouseButtonRelease, QPointF(2 + dx, 2), Qt::LeftButton, Qt::NoButton);
if (field.text() != during || !field.hasFocus())
throw std::runtime_error("the release changed the field");
return during;
}
// Shown in an active window, so fields take the focus.
inline void showActive(QWidget &widget)
{
widget.show();
widget.activateWindow();
if (!QTest::qWaitForWindowActive(&widget))
throw std::runtime_error("the bar never became active");
}
template <typename Widget> inline Widget &find(QWidget &root, const char *name)
{
auto *found = root.findChild<Widget *>(QString::fromUtf8(name));
if (!found)
throw std::runtime_error(name);
return *found;
}
inline ImportedImage pixels(int width, int height)
{
QImage image(width, height, QImage::Format_RGBA8888_Premultiplied);
image.fill(Qt::white);
return ImportedImage(image, image, "Pixels");
}
+281
View File
@@ -0,0 +1,281 @@
#include "ScrubFixtures.h"
#include "UI/BrushControls.h"
#include "UI/GradientControls.h"
#include "UI/LassoControls.h"
#include "UI/ShapeControls.h"
#include "UI/TypeControls.h"
#include <QPushButton>
// Swift's scrubbable labels and units in the tool bars.
class ScrubbableLabelsTests : public QObject {
Q_OBJECT
private slots:
void theBrushsLabelsScrubItsTip();
void theGradientsOpacityScrubs();
void theSelectionBarsLabelsAndUnitsScrubWholeNumbers();
void theAmountSheetsTitleScrubsItsInput();
void theShapesWidthAndRadiusScrub();
void theTypeBarScrubsWholeSizesTrackingAndLeading();
void aScrubShowsItsValueInAFocusedField();
void featherScrubsWithoutASelection();
};
void ScrubbableLabelsTests::theBrushsLabelsScrubItsTip()
{
EditorSession session;
session.createDocument(100, 100, true);
session.selectTool(NavigationTool::brush);
BrushControls bar(session);
bar.show();
drag(scrubbed(bar, "Size"), 25);
QCOMPARE(session.brushSettings().diameter, 65.0);
QCOMPARE(find<QLineEdit>(bar, "brushSize").text(), QString("65"));
drag(scrubbed(bar, "Size"), -5000);
QCOMPARE(session.brushSettings().diameter, 1.0);
drag(scrubbed(bar, "Size"), 5000);
QCOMPARE(session.brushSettings().diameter, 2000.0);
// Unstepped, a fraction of a point counts.
drag(scrubbed(bar, "Size"), -1.5);
QCOMPARE(session.brushSettings().diameter, 1998.5);
drag(scrubbed(bar, "Hardness"), -30);
QVERIFY(qAbs(session.brushSettings().hardness - 0.7) < 1e-12);
drag(scrubbed(bar, "Hardness"), 90);
QCOMPARE(session.brushSettings().hardness, 1.0);
drag(scrubbed(bar, "Hardness"), -900);
QCOMPARE(session.brushSettings().hardness, 0.0);
drag(scrubbed(bar, "Opacity"), -50);
QVERIFY(qAbs(session.brushSettings().opacity - 0.5) < 1e-12);
drag(scrubbed(bar, "Opacity"), -500);
QCOMPARE(session.brushSettings().opacity, 0.01);
drag(scrubbed(bar, "Opacity"), 500);
QCOMPARE(session.brushSettings().opacity, 1.0);
drag(scrubbed(bar, "Smoothing"), 30);
QCOMPARE(session.brushSettings().smoothing, 30.0);
drag(scrubbed(bar, "Smoothing"), 300);
QCOMPARE(session.brushSettings().smoothing, 100.0);
drag(scrubbed(bar, "Smoothing"), -300);
QCOMPARE(session.brushSettings().smoothing, 0.0);
// The Smear's Strength is the same label, retitled.
session.selectTool(NavigationTool::blur);
const double strength = session.brushSettings().opacity;
drag(scrubbed(bar, "Strength"), -10);
QVERIFY(qAbs(session.brushSettings().opacity - (strength - 0.1)) < 1e-12);
}
void ScrubbableLabelsTests::theGradientsOpacityScrubs()
{
EditorSession session;
session.createDocument(100, 100, true);
session.selectTool(NavigationTool::gradient);
GradientControls bar(session);
bar.show();
// Unstepped: a point and a half is 1.5%.
drag(scrubbed(bar, "Opacity"), -1.5);
QVERIFY(qAbs(session.gradientSettings().opacity - 0.985) < 1e-12);
drag(scrubbed(bar, "Opacity"), 1.5);
drag(scrubbed(bar, "Opacity"), -40);
QVERIFY(qAbs(session.gradientSettings().opacity - 0.6) < 1e-12);
drag(scrubbed(bar, "Opacity"), -400);
QCOMPARE(session.gradientSettings().opacity, 0.01);
drag(scrubbed(bar, "Opacity"), 400);
QCOMPARE(session.gradientSettings().opacity, 1.0);
}
void ScrubbableLabelsTests::theSelectionBarsLabelsAndUnitsScrubWholeNumbers()
{
std::unique_ptr<EditorSession> session = selectionSession();
LassoControls bar(*session);
bar.resize(1600, bar.height());
bar.show();
// Without a selection the amounts rest, units and all.
QLabel &expand = scrubbed(bar, "px", 0);
QVERIFY(!expand.isEnabled());
QVERIFY(!scrubbed(bar, "px", 1).isEnabled());
drag(scrubbed(bar, "px", 1), 9);
QCOMPARE(session->selectionContractAmount(), 1);
lasso(*session, square(10, 10, 20));
QVERIFY(expand.isEnabled());
drag(expand, 7);
QCOMPARE(session->selectionExpandAmount(), 8);
drag(expand, 900);
QCOMPARE(session->selectionExpandAmount(), 500);
drag(expand, -900);
QCOMPARE(session->selectionExpandAmount(), 1);
drag(scrubbed(bar, "px", 1), 4);
QCOMPARE(session->selectionContractAmount(), 5);
drag(scrubbed(bar, "px", 1), 900);
QCOMPARE(session->selectionContractAmount(), 500);
drag(scrubbed(bar, "px", 1), -900);
QCOMPARE(session->selectionContractAmount(), 1);
drag(scrubbed(bar, "px", 2), 3);
QCOMPARE(session->selectionFeatherAmount(), 5);
drag(scrubbed(bar, "px", 2), 900);
QCOMPARE(session->selectionFeatherAmount(), 250);
drag(scrubbed(bar, "px", 2), -900);
QCOMPARE(session->selectionFeatherAmount(), 1);
QCOMPARE(find<QLineEdit>(bar, "selectionFeatherAmount").text(), QString("1"));
session->selectTool(NavigationTool::wand);
// Whole numbers round, where the setter would truncate.
drag(scrubbed(bar, "Tolerance"), 1.6);
QCOMPARE(session->wandSettings().tolerance, 34);
drag(scrubbed(bar, "Tolerance"), 8);
QCOMPARE(session->wandSettings().tolerance, 42);
drag(scrubbed(bar, "Tolerance"), 400);
QCOMPARE(session->wandSettings().tolerance, 255);
drag(scrubbed(bar, "Tolerance"), -400);
QCOMPARE(session->wandSettings().tolerance, 0);
session->setWandMode(WandMode::object);
drag(scrubbed(bar, "Edge"), -1.5);
QCOMPARE(session->objectSelectionSettings().edgeOffset, -2);
drag(scrubbed(bar, "Edge"), -2);
QCOMPARE(session->objectSelectionSettings().edgeOffset, -4);
drag(scrubbed(bar, "Edge"), 50);
QCOMPARE(session->objectSelectionSettings().edgeOffset, 10);
drag(scrubbed(bar, "Edge"), -50);
QCOMPARE(session->objectSelectionSettings().edgeOffset, -10);
}
void ScrubbableLabelsTests::theAmountSheetsTitleScrubsItsInput()
{
std::unique_ptr<EditorSession> session = selectionSession();
SelectionAmountSheet sheet(*session, SelectionAmountOperation::feather);
sheet.show();
QLineEdit &input = find<QLineEdit>(sheet, "amountField");
// Rounded to whole pixels, not truncated.
drag(scrubbed(sheet, "Amount"), 1.6);
QCOMPARE(input.text(), QString("4"));
input.setText(QStringLiteral("2"));
drag(scrubbed(sheet, "Amount"), 10);
QCOMPARE(input.text(), QString("12"));
drag(scrubbed(sheet, "Amount"), 900);
QCOMPARE(input.text(), QString("250"));
// Unreadable input starts the drag from one.
input.setText(QStringLiteral("abc"));
drag(scrubbed(sheet, "Amount"), 4);
QCOMPARE(input.text(), QString("5"));
drag(scrubbed(sheet, "Amount"), -40);
QCOMPARE(input.text(), QString("1"));
SelectionAmountSheet expand(*session, SelectionAmountOperation::expand);
drag(scrubbed(expand, "Amount"), 900);
QCOMPARE(find<QLineEdit>(expand, "amountField").text(), QString("500"));
}
void ScrubbableLabelsTests::theShapesWidthAndRadiusScrub()
{
EditorSession session;
session.createDocument(100, 100, true);
session.selectTool(NavigationTool::shape);
session.setShapeKind(ShapeKind::line);
ShapeControls bar(session);
bar.show();
drag(scrubbed(bar, "Width"), 6.5);
QCOMPARE(session.shapeLineWidth(), 10.5);
drag(scrubbed(bar, "Width"), 9000);
QCOMPARE(session.shapeLineWidth(), 5000.0);
drag(scrubbed(bar, "Width"), -9000);
QCOMPARE(session.shapeLineWidth(), 1.0);
session.setShapeKind(ShapeKind::rectangle);
drag(scrubbed(bar, "Radius"), 12.25);
QCOMPARE(session.shapeCornerRadius(), 12.25);
drag(scrubbed(bar, "Radius"), 9000);
QCOMPARE(session.shapeCornerRadius(), 5000.0);
drag(scrubbed(bar, "Radius"), -9000);
QCOMPARE(session.shapeCornerRadius(), 0.0);
}
void ScrubbableLabelsTests::theTypeBarScrubsWholeSizesTrackingAndLeading()
{
EditorSession session;
session.createDocument(400, 300, true);
session.selectTool(NavigationTool::type);
TypeControls bar(session);
bar.show();
session.changeTextStyle([](LayerTextStyle &style) { style.fontSize = 24.4; });
// Dragged sizes land on whole numbers, Swift's step of one.
drag(scrubbed(bar, "px"), 3);
QCOMPARE(session.currentTextStyle().fontSize, 27.0);
drag(scrubbed(bar, "px"), 5000);
QCOMPARE(session.currentTextStyle().fontSize, 2000.0);
drag(scrubbed(bar, "px"), -5000);
QCOMPARE(session.currentTextStyle().fontSize, 1.0);
session.changeTextStyle([](LayerTextStyle &style) { style.tracking = 2.6; });
drag(scrubbed(bar, "Tracking"), -5);
QCOMPARE(session.currentTextStyle().tracking, -2.0);
drag(scrubbed(bar, "Tracking"), -500);
QCOMPARE(session.currentTextStyle().tracking, -100.0);
drag(scrubbed(bar, "Tracking"), 5000);
QCOMPARE(session.currentTextStyle().tracking, 1000.0);
// Auto is zero, so a drag starts there.
drag(scrubbed(bar, "Leading"), 12);
QCOMPARE(session.currentTextStyle().leading, 12.0);
drag(scrubbed(bar, "Leading"), -50);
QCOMPARE(session.currentTextStyle().leading, 0.0);
drag(scrubbed(bar, "Leading"), 9000);
QCOMPARE(session.currentTextStyle().leading, 5000.0);
}
void ScrubbableLabelsTests::aScrubShowsItsValueInAFocusedField()
{
EditorSession session;
session.createDocument(100, 100, true);
session.selectTool(NavigationTool::brush);
{
BrushControls bar(session);
showActive(bar);
QCOMPARE(scrubOverTyping(find<QLineEdit>(bar, "brushSize"), scrubbed(bar, "Size"), 25), QString("32"));
QCOMPARE(scrubOverTyping(find<QLineEdit>(bar, "brushHardness"), scrubbed(bar, "Hardness"), 30), QString("37"));
QCOMPARE(scrubOverTyping(find<QLineEdit>(bar, "brushOpacity"), scrubbed(bar, "Opacity"), 40), QString("47"));
QCOMPARE(scrubOverTyping(find<QLineEdit>(bar, "brushSmoothing"), scrubbed(bar, "Smoothing"), 12), QString("19"));
}
session.selectTool(NavigationTool::gradient);
{
GradientControls bar(session);
showActive(bar);
QCOMPARE(scrubOverTyping(find<QLineEdit>(bar, "gradientOpacity"), scrubbed(bar, "Opacity"), 25), QString("32"));
}
session.selectTool(NavigationTool::shape);
session.setShapeKind(ShapeKind::line);
{
ShapeControls bar(session);
showActive(bar);
QCOMPARE(scrubOverTyping(find<QLineEdit>(bar, "shapeWidth"), scrubbed(bar, "Width"), 6), QString("13"));
}
session.selectTool(NavigationTool::wand);
{
LassoControls bar(session);
bar.resize(1600, bar.height());
showActive(bar);
QCOMPARE(scrubOverTyping(find<QLineEdit>(bar, "wandTolerance"), scrubbed(bar, "Tolerance"), 10), QString("17"));
QCOMPARE(scrubOverTyping(find<QLineEdit>(bar, "selectionFeatherAmount"), scrubbed(bar, "px", 2), 3), QString("10"));
}
session.selectTool(NavigationTool::type);
{
TypeControls bar(session);
showActive(bar);
QCOMPARE(scrubOverTyping(find<QLineEdit>(bar, "typeTracking"), scrubbed(bar, "Tracking"), 4), QString("11"));
QCOMPARE(scrubOverTyping(find<QLineEdit>(bar, "typeLeading"), scrubbed(bar, "Leading"), 30), QString("37"));
QCOMPARE(scrubOverTyping(find<QLineEdit>(bar, "typeSize"), scrubbed(bar, "px"), -2), QString("5"));
}
}
void ScrubbableLabelsTests::featherScrubsWithoutASelection()
{
std::unique_ptr<EditorSession> session = selectionSession();
LassoControls bar(*session);
bar.resize(1600, bar.height());
bar.show();
// Swift rests Feather's button alone; its amount stays.
drag(scrubbed(bar, "px", 2), 7);
QCOMPARE(session->selectionFeatherAmount(), 9);
QVERIFY(find<QLineEdit>(bar, "selectionFeatherAmount").isEnabled());
QVERIFY(!find<QLineEdit>(bar, "selectionExpandAmount").isEnabled());
QVERIFY(!find<QLineEdit>(bar, "selectionContractAmount").isEnabled());
for (QPushButton *button : bar.findChildren<QPushButton *>())
if (button->text() == "Expand" || button->text() == "Contract" || button->text() == "Feather")
QVERIFY(!button->isEnabled());
drag(scrubbed(bar, "px", 0), 7);
QCOMPARE(session->selectionExpandAmount(), 1);
}
QTEST_MAIN(ScrubbableLabelsTests)
#include "ScrubbableLabelsTests.moc"
+213
View File
@@ -0,0 +1,213 @@
#include "ScrubFixtures.h"
#include "UI/LayerAppearanceControls.h"
#include "UI/NavigationToolHeader.h"
#include "UI/TransformInspector.h"
// Swift's scrubbable Move bar, zoom unit and layer opacity.
class ScrubbableTransformTests : public QObject {
Q_OBJECT
private slots:
void theTransformLabelsScrubWholeNumbersOverTyping();
void scaleScrubsDownToATenth();
void theZoomUnitScrubsThePercentage();
void theLayerOpacityScrubIsOneStep();
void anOpacityScrubEndsWithItsLayer();
void aTransformScrubEndsWithItsLayer();
};
void ScrubbableTransformTests::theTransformLabelsScrubWholeNumbersOverTyping()
{
EditorSession session;
session.createDocument(200, 100);
session.insert(pixels(40, 20));
session.selectTool(NavigationTool::move);
session.setLocksTransformRatio(false);
TransformInspector bar(session);
bar.show();
bar.activateWindow();
QVERIFY(QTest::qWaitForWindowActive(&bar));
const QPointF origin = session.activeLayer().value().transform.origin;
// The first drag begins the edit, as typing does.
drag(scrubbed(bar, "X"), 7);
QCOMPARE(session.transformEdit().value().draft.origin.x(), std::round(origin.x()) + 7);
// A scrub writes over what is typed in its field.
QLineEdit &width = find<QLineEdit>(bar, "transformW");
width.setFocus();
width.selectAll();
QTest::keyClicks(&width, QStringLiteral("3"));
drag(scrubbed(bar, "W"), 10);
QCOMPARE(session.transformEdit().value().draft.size, QSizeF(13, 20));
QCOMPARE(width.text(), QString("13"));
drag(scrubbed(bar, "W"), -100);
QCOMPARE(session.transformEdit().value().draft.size.width(), 1.0);
drag(scrubbed(bar, "H"), 40000);
QCOMPARE(session.transformEdit().value().draft.size.height(), 30000.0);
drag(scrubbed(bar, "°"), 30);
QCOMPARE(session.transformEdit().value().draft.rotation, 30.0);
drag(scrubbed(bar, "°"), -900);
QCOMPARE(session.transformEdit().value().draft.rotation, -0.0);
// Unfocused, the field shows what the session keeps.
QCOMPARE(find<QLineEdit>(bar, "transform°").text(), QString("0"));
QCOMPARE(width.text(), QString("1"));
drag(scrubbed(bar, "Y"), -90000);
QCOMPARE(session.transformEdit().value().draft.origin.y(), -30000.0);
// A fractional start lands on a whole number.
QLineEdit &x = find<QLineEdit>(bar, "transformX");
x.setFocus();
x.selectAll();
QTest::keyClicks(&x, QStringLiteral("10.4"));
QCOMPARE(session.transformEdit().value().draft.origin.x(), 10.4);
drag(scrubbed(bar, "X"), 3);
QCOMPARE(session.transformEdit().value().draft.origin.x(), 13.0);
QCOMPARE(x.text(), QString("13"));
// Every range reaches its ends exactly.
drag(scrubbed(bar, "X"), -90000);
QCOMPARE(session.transformEdit().value().draft.origin.x(), -30000.0);
drag(scrubbed(bar, "X"), 90000);
QCOMPARE(session.transformEdit().value().draft.origin.x(), 30000.0);
drag(scrubbed(bar, "Y"), 90000);
QCOMPARE(session.transformEdit().value().draft.origin.y(), 30000.0);
drag(scrubbed(bar, "W"), 90000);
QCOMPARE(session.transformEdit().value().draft.size.width(), 30000.0);
drag(scrubbed(bar, "H"), -90000);
QCOMPARE(session.transformEdit().value().draft.size.height(), 1.0);
// 360 reaches the setter, whose remainder leaves none.
drag(scrubbed(bar, "°"), 9000);
QCOMPARE(session.transformEdit().value().draft.rotation, 0.0);
drag(scrubbed(bar, "°"), 359);
QCOMPARE(session.transformEdit().value().draft.rotation, 359.0);
// Scale snaps whole percents too.
session.cancelTransform();
drag(scrubbed(bar, "Scale"), 5);
QCOMPARE(session.transformEdit().value().draft.size, QSizeF(42, 21));
session.cancelTransform();
drag(scrubbed(bar, "Scale"), 90000);
QCOMPARE(session.transformEdit().value().draft.size, QSizeF(12000, 6000));
QCOMPARE(find<QLineEdit>(bar, "transformScale").text(), QString("30000"));
// While distorted the numbers rest, labels included.
session.cancelTransform();
session.beginTransform();
session.beginDistort();
QVERIFY(!scrubbed(bar, "X").isEnabled());
}
// Scale's floor is a tenth of a percent.
void ScrubbableTransformTests::scaleScrubsDownToATenth()
{
EditorSession session;
session.createDocument(4000, 2000);
session.insert(pixels(2000, 1000));
session.selectTool(NavigationTool::move);
TransformInspector bar(session);
bar.show();
drag(scrubbed(bar, "Scale"), -90000);
QCOMPARE(session.transformEdit().value().draft.size, QSizeF(2, 1));
QCOMPARE(find<QLineEdit>(bar, "transformScale").text(), QString("0.10"));
}
void ScrubbableTransformTests::theZoomUnitScrubsThePercentage()
{
EditorSession session;
session.createDocument(800, 600);
session.selectTool(NavigationTool::zoom);
NavigationToolHeader bar(session);
bar.show();
session.zoom(1);
drag(scrubbed(bar, "%"), 25.5);
QVERIFY(qAbs(session.viewport.zoom() - 1.255) < 1e-9);
QCOMPARE(find<QLineEdit>(bar, "zoomPercentage").text(), QString("125.5"));
drag(scrubbed(bar, "%"), -900);
QVERIFY(qAbs(session.viewport.zoom() - 0.001) < 1e-9);
drag(scrubbed(bar, "%"), 9000);
QVERIFY(qAbs(session.viewport.zoom() - 32) < 1e-9);
}
void ScrubbableTransformTests::theLayerOpacityScrubIsOneStep()
{
EditorSession session;
LayerAppearanceControls controls(session);
session.createDocument(4, 4);
session.insert(pixels(2, 2));
controls.show();
QLabel &opacity = scrubbed(controls, "Opacity");
// One drag is one opacity edit, open until the release.
drag(opacity, -30, false);
QVERIFY(qAbs(session.activeLayer().value().opacity - 0.7) < 1e-12);
QVERIFY(!session.canUndo());
send(opacity, QEvent::MouseMove, QPoint(-18, 2), Qt::NoButton, Qt::LeftButton);
QVERIFY(qAbs(session.activeLayer().value().opacity - 0.8) < 1e-12);
send(opacity, QEvent::MouseButtonRelease, QPoint(-18, 2), Qt::LeftButton, Qt::NoButton);
QVERIFY(session.canUndo());
QCOMPARE(find<QLineEdit>(controls, "opacityPercent").text(), QString("80"));
// One undo takes the whole drag back; the layer stays.
session.undo();
QCOMPARE(session.activeLayer().value().opacity, 1.0);
QCOMPARE(session.document().value().layers.size(), size_t(1));
drag(opacity, -30.5);
QVERIFY(qAbs(session.activeLayer().value().opacity - 0.695) < 1e-12);
drag(opacity, -500);
QCOMPARE(session.activeLayer().value().opacity, 0.0);
drag(opacity, 500);
QCOMPARE(session.activeLayer().value().opacity, 1.0);
}
void ScrubbableTransformTests::anOpacityScrubEndsWithItsLayer()
{
EditorSession session;
LayerAppearanceControls controls(session);
session.createDocument(4, 4);
session.insert(pixels(2, 2));
const QUuid lower = session.activeLayerID().value();
session.setLayerOpacity(0.3);
session.insert(pixels(2, 2));
const QUuid upper = session.activeLayerID().value();
controls.show();
QLabel &opacity = scrubbed(controls, "Opacity");
drag(opacity, -10, false);
QVERIFY(qAbs(layerWith(session, upper).opacity - 0.9) < 1e-12);
// Another layer ends the drag; held moves change nothing.
session.selectLayer(lower);
send(opacity, QEvent::MouseMove, QPointF(-18, 2), Qt::NoButton, Qt::LeftButton);
send(opacity, QEvent::MouseButtonRelease, QPointF(-18, 2), Qt::LeftButton, Qt::NoButton);
QCOMPARE(layerWith(session, lower).opacity, 0.3);
QVERIFY(qAbs(layerWith(session, upper).opacity - 0.9) < 1e-12);
QVERIFY(session.canUndo());
session.undo();
QCOMPARE(layerWith(session, upper).opacity, 1.0);
QCOMPARE(layerWith(session, lower).opacity, 0.3);
// A fresh press on the new layer scrubs it.
session.selectLayer(lower);
drag(opacity, 20);
QVERIFY(qAbs(layerWith(session, lower).opacity - 0.5) < 1e-12);
}
void ScrubbableTransformTests::aTransformScrubEndsWithItsLayer()
{
EditorSession session;
session.createDocument(200, 100);
session.insert(pixels(40, 20));
const QUuid first = session.activeLayerID().value();
session.insert(pixels(40, 20));
const QUuid second = session.activeLayerID().value();
session.selectTool(NavigationTool::move);
TransformInspector bar(session);
bar.show();
const double x = layerWith(session, second).transform.origin.x();
QLabel &label = scrubbed(bar, "X");
drag(label, 10, false);
QCOMPARE(session.transformEdit().value().draft.origin.x(), std::round(x) + 10);
session.commitTransform();
// Another layer ends the drag; held moves open no edit.
session.selectLayer(first);
send(label, QEvent::MouseMove, QPointF(22, 2), Qt::NoButton, Qt::LeftButton);
send(label, QEvent::MouseButtonRelease, QPointF(22, 2), Qt::LeftButton, Qt::NoButton);
QVERIFY(!session.transformEdit().has_value());
QCOMPARE(layerWith(session, first).transform.origin.x(), x);
QCOMPARE(layerWith(session, second).transform.origin.x(), std::round(x) + 10);
// A fresh press scrubs the new layer.
drag(label, 5);
QCOMPARE(session.transformEdit().value().draft.origin.x(), std::round(x) + 5);
}
QTEST_MAIN(ScrubbableTransformTests)
#include "ScrubbableTransformTests.moc"
+2 -1
View File
@@ -120,7 +120,8 @@ void SelectionAmountSheetTests::theBarFeathersByItsAmount()
QVERIFY(QTest::qWaitForWindowExposed(&controls));
auto &feather = find<QPushButton>(controls, "featherSelection");
auto &amount = find<QLineEdit>(controls, "selectionFeatherAmount");
QVERIFY(!feather.isEnabled() && !amount.isEnabled());
// Swift rests the button alone; the amount stays editable.
QVERIFY(!feather.isEnabled() && amount.isEnabled());
QCOMPARE(amount.text(), QString("2"));
lasso(*session, square(20, 20, 40));
QVERIFY(feather.isEnabled() && amount.isEnabled());
+3
View File
@@ -144,6 +144,9 @@ compositor_test(NativeLayerListTests)
compositor_test(LayerContextMenuTests)
compositor_test(LayerContextMenuEdgeTests)
compositor_test(NavigationToolHeaderTests)
compositor_test(NumericScrubTests)
compositor_test(ScrubbableLabelsTests)
compositor_test(ScrubbableTransformTests)
compositor_test(NewCanvasSheetTests)
compositor_test(OmarchyThemeTests)
compositor_test(ProjectControllerTests)