Move bar: held Ctrl and Shift flip Auto Select and the lock, values apply as one step, resize handles snap (13.13)
This commit is contained in:
1 parent
765e071c0c
commit
b50657bcdb
17 files changed
+703
-78
No files matched your search
@@ -132,6 +132,7 @@ add_library(compositor_core STATIC
|
||||
src/UI/NativeLayerList+Menu.cpp
|
||||
src/UI/NavigationToolHeader.cpp
|
||||
src/UI/NumericScrub.cpp
|
||||
src/UI/HeldModifiers.cpp
|
||||
src/UI/TransformInspector.cpp
|
||||
src/UI/EffectsSheet.cpp
|
||||
src/UI/CameraRawColorControls.cpp
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
public:
|
||||
SnapGuides transformSnapTargets(const QSet<QUuid> &moving) const;
|
||||
LayerTransform snappedMove(const LayerTransform &draft, const QSet<QUuid> &moving, double tolerance);
|
||||
// A resize handle's pointer, nudged so moved edges snap.
|
||||
QPointF snappedResizePoint(QPointF point, const TransformDrag &drag, bool proportional, const QSet<QUuid> &moving,
|
||||
double tolerance, const std::function<LayerTransform(QPointF)> &update);
|
||||
SnapGuides cropSnapTargets() const;
|
||||
// A drawn corner, each axis to a target within reach.
|
||||
QPointF snappedPoint(QPointF point, double tolerance);
|
||||
|
||||
@@ -168,6 +168,58 @@ LayerTransform EditorSession::snappedMove(const LayerTransform &draft, const QSe
|
||||
return snapped;
|
||||
}
|
||||
|
||||
// A resize handle's point, nudged onto targets; upright only.
|
||||
QPointF EditorSession::snappedResizePoint(QPointF point, const TransformDrag &drag, bool proportional, const QSet<QUuid> &moving,
|
||||
double tolerance, const std::function<LayerTransform(QPointF)> &update)
|
||||
{
|
||||
if (!m_snappingEnabled || drag.mode.kind != TransformDrag::Kind::resize || drag.original.radians() != 0) {
|
||||
snapGuides = {};
|
||||
return point;
|
||||
}
|
||||
const QPointF handle = LayerTransform::handles[size_t(drag.mode.index)];
|
||||
const SnapGuides targets = transformSnapTargets(moving);
|
||||
// The dragged handle's place tells its edge apart.
|
||||
const QPointF at = drag.original.point(handle) + point - drag.start;
|
||||
const auto edge = [at](const LayerTransform &transform, bool horizontal) {
|
||||
const QRectF box(transform.origin, transform.size);
|
||||
if (horizontal)
|
||||
return std::abs(box.left() - at.x()) <= std::abs(box.right() - at.x()) ? box.left() : box.right();
|
||||
return std::abs(box.top() - at.y()) <= std::abs(box.bottom() - at.y()) ? box.top() : box.bottom();
|
||||
};
|
||||
struct Snap {
|
||||
bool horizontal;
|
||||
double target;
|
||||
};
|
||||
const LayerTransform draft = update(point);
|
||||
std::vector<Snap> snaps;
|
||||
if (handle.x() != 0.5)
|
||||
if (const std::optional<double> x = nearest(edge(draft, true), targets.xs, tolerance))
|
||||
snaps.push_back({true, *x});
|
||||
if (handle.y() != 0.5)
|
||||
if (const std::optional<double> y = nearest(edge(draft, false), targets.ys, tolerance))
|
||||
snaps.push_back({false, *y});
|
||||
// Proportional, the nearer edge snaps; the ratio moves the other.
|
||||
if (proportional && snaps.size() == 2) {
|
||||
const auto distance = [&](const Snap &snap) { return std::abs(snap.target - edge(draft, snap.horizontal)); };
|
||||
snaps = {distance(snaps[1]) < distance(snaps[0]) ? snaps[1] : snaps[0]};
|
||||
}
|
||||
// Edges follow the pointer linearly: one pixel measures the step.
|
||||
QPointF result = point;
|
||||
for (const Snap &snap : snaps) {
|
||||
const double before = edge(update(result), snap.horizontal);
|
||||
const QPointF nudged = result + (snap.horizontal ? QPointF(1, 0) : QPointF(0, 1));
|
||||
const double perPixel = edge(update(nudged), snap.horizontal) - before;
|
||||
if (std::abs(perPixel) <= 0.01)
|
||||
continue;
|
||||
const double shift = (snap.target - before) / perPixel;
|
||||
result += snap.horizontal ? QPointF(shift, 0) : QPointF(0, shift);
|
||||
}
|
||||
snapGuides = {};
|
||||
for (const Snap &snap : snaps)
|
||||
(snap.horizontal ? snapGuides.xs : snapGuides.ys).push_back(snap.target);
|
||||
return result;
|
||||
}
|
||||
|
||||
// View > Snap To's targets, without the centres.
|
||||
SnapGuides EditorSession::cropSnapTargets() const
|
||||
{
|
||||
|
||||
@@ -59,7 +59,7 @@ std::optional<LayerTransform> EditorSession::groupTransformBox() const
|
||||
return LayerTransform{.origin = low, .size = {std::max(1.0, high.x() - low.x()), std::max(1.0, high.y() - low.y())}};
|
||||
}
|
||||
|
||||
void EditorSession::beginTransform(bool persistent)
|
||||
void EditorSession::beginTransform(bool persistent, bool fromFields)
|
||||
{
|
||||
cancelCrop();
|
||||
const std::optional<ImageLayer> layer = activeLayer();
|
||||
@@ -73,11 +73,11 @@ void EditorSession::beginTransform(bool persistent)
|
||||
QHash<QUuid, LayerTransform> originals;
|
||||
for (const ImageLayer &member : groupTransformMembers())
|
||||
originals.insert(member.id, member.transform);
|
||||
m_transformEdit = TransformEdit{layer->id, *box, TransformGroup{*box, originals}, false, persistent, std::nullopt, nullptr};
|
||||
m_transformEdit = TransformEdit{layer->id, *box, TransformGroup{*box, originals}, false, persistent, std::nullopt, nullptr, fromFields};
|
||||
} else {
|
||||
// Linked, layer and mask move together as the layer.
|
||||
const bool maskAlone = transformTargetsMask();
|
||||
m_transformEdit = TransformEdit{layer->id, maskAlone ? layer->maskTransform() : layer->transform, std::nullopt, maskAlone, persistent, std::nullopt, nullptr};
|
||||
m_transformEdit = TransformEdit{layer->id, maskAlone ? layer->maskTransform() : layer->transform, std::nullopt, maskAlone, persistent, std::nullopt, nullptr, fromFields};
|
||||
}
|
||||
notify();
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ public:
|
||||
bool transformsAsGroup() const;
|
||||
std::vector<ImageLayer> groupTransformMembers() const;
|
||||
std::optional<LayerTransform> groupTransformBox() const;
|
||||
void beginTransform(bool persistent = true);
|
||||
void beginTransform(bool persistent = true, bool fromFields = false);
|
||||
// Alt-drag: the selection's copies move, one step.
|
||||
void beginDuplicateTransform();
|
||||
// The Move tool's settings (TransformInspector, Ctrl+H).
|
||||
|
||||
@@ -71,6 +71,8 @@ struct TransformEdit {
|
||||
std::optional<Corners> corners;
|
||||
// Ctrl+T on selected pixels: they float, then merge back.
|
||||
std::shared_ptr<const FloatingTransform> floating;
|
||||
// The Move bar's fields: applied once they are done.
|
||||
bool fromFields = false;
|
||||
};
|
||||
|
||||
// Where a move has just snapped: guides while it lasts.
|
||||
|
||||
@@ -137,7 +137,7 @@ QCursor CanvasView::rotationCursor(double ratio)
|
||||
return QCursor(pixmap, 12, 12);
|
||||
}
|
||||
|
||||
// The layer a press off the handles drags; Ctrl picks.
|
||||
// What a press off the handles drags; Ctrl flips picking.
|
||||
std::optional<CanvasView::PressTarget> CanvasView::transformPressLayer(QPointF pixel, Qt::KeyboardModifiers modifiers) const
|
||||
{
|
||||
const std::optional<CanvasDocument> &document = m_session.document();
|
||||
@@ -154,21 +154,23 @@ std::optional<CanvasView::PressTarget> CanvasView::transformPressLayer(QPointF p
|
||||
active = std::nullopt;
|
||||
const bool picks = !m_session.transformEdit();
|
||||
const bool control = modifiers.testFlag(Qt::ControlModifier);
|
||||
if (control && picks && underPointer)
|
||||
const bool autoSelect = m_session.transformAutoSelect() != control;
|
||||
// Ctrl+Shift adds the layer under the pointer either way.
|
||||
if (control && (modifiers.testFlag(Qt::ShiftModifier) || !m_session.transformAutoSelect()) && picks && underPointer)
|
||||
return PressTarget{*underPointer, true};
|
||||
// A group's box drags them all; outside, unless auto-select picks.
|
||||
if (m_session.transformsAsGroup() && m_session.activeLayerID()) {
|
||||
const std::optional<LayerTransform> box = m_session.transformEdit() ? std::optional(m_session.transformEdit()->draft) : m_session.groupTransformBox();
|
||||
if ((box && box->contains(pixel)) || !(picks && m_session.transformAutoSelect()) || !underPointer)
|
||||
if ((box && box->contains(pixel)) || !(picks && autoSelect) || !underPointer)
|
||||
return PressTarget{*m_session.activeLayerID(), false};
|
||||
}
|
||||
if (active && m_session.editedTransform(*active).contains(pixel)) {
|
||||
// Auto Select prefers a layer above, as over a background.
|
||||
if (picks && m_session.transformAutoSelect() && underPointer && above(rendered, *underPointer, active->id))
|
||||
if (picks && autoSelect && underPointer && above(rendered, *underPointer, active->id))
|
||||
return PressTarget{*underPointer, true};
|
||||
return PressTarget{active->id, false};
|
||||
}
|
||||
if (picks && (m_session.transformAutoSelect() || control) && underPointer)
|
||||
if (picks && autoSelect && underPointer)
|
||||
return PressTarget{*underPointer, true};
|
||||
if (active)
|
||||
return PressTarget{active->id, false};
|
||||
@@ -230,6 +232,9 @@ void CanvasView::beginTransformDrag(QPointF point, Qt::KeyboardModifiers modifie
|
||||
mode = TransformDrag::Mode{TransformDrag::Kind::move};
|
||||
}
|
||||
m_duplicatesTransformOnDrag = mode->kind == TransformDrag::Kind::move && modifiers.testFlag(Qt::AltModifier);
|
||||
// A value the bar's fields were changing applies first.
|
||||
if (m_session.transformEdit() && m_session.transformEdit()->fromFields)
|
||||
m_session.commitTransform();
|
||||
if (!m_session.transformEdit())
|
||||
m_session.beginTransform(false);
|
||||
if (!m_session.transformEdit())
|
||||
@@ -265,21 +270,28 @@ void CanvasView::dragTransform(QPointF point, Qt::KeyboardModifiers modifiers)
|
||||
synchronizeDisplay();
|
||||
return;
|
||||
}
|
||||
// Drags land on whole pixels and degrees; typing stays exact.
|
||||
LayerTransform draft = m_transformDrag->updated(pixel, m_session.locksTransformRatio(), modifiers.testFlag(Qt::ShiftModifier),
|
||||
modifiers.testFlag(Qt::AltModifier)).rounded();
|
||||
const bool shift = modifiers.testFlag(Qt::ShiftModifier), option = modifiers.testFlag(Qt::AltModifier);
|
||||
const std::optional<TransformEdit> &edit = m_session.transformEdit();
|
||||
// A move snaps to canvas and layers; Ctrl frees it.
|
||||
if (m_transformDrag->mode.kind == TransformDrag::Kind::move && !modifiers.testFlag(Qt::ControlModifier) && edit) {
|
||||
QSet<QUuid> moving;
|
||||
if (edit->group) {
|
||||
for (auto original = edit->group->originals.constBegin(); original != edit->group->originals.constEnd(); ++original)
|
||||
moving.insert(original.key());
|
||||
} else {
|
||||
moving.insert(edit->layerID);
|
||||
}
|
||||
draft = m_session.snappedMove(draft, moving, TransformSnap::distance / std::max(m_session.viewport.pointsPerPixel(), 0.0001));
|
||||
QSet<QUuid> moving;
|
||||
if (edit && edit->group) {
|
||||
for (auto original = edit->group->originals.constBegin(); original != edit->group->originals.constEnd(); ++original)
|
||||
moving.insert(original.key());
|
||||
} else if (edit) {
|
||||
moving.insert(edit->layerID);
|
||||
}
|
||||
const double tolerance = TransformSnap::distance / std::max(m_session.viewport.pointsPerPixel(), 0.0001);
|
||||
const TransformDrag &drag = *m_transformDrag;
|
||||
// Moves and resized edges snap, not turns; Ctrl frees both.
|
||||
QPointF target = pixel;
|
||||
if (drag.mode.kind == TransformDrag::Kind::resize && !modifiers.testFlag(Qt::ControlModifier)) {
|
||||
target = m_session.snappedResizePoint(pixel, drag, m_session.locksTransformRatio() != shift, moving, tolerance, [&](QPointF to) {
|
||||
return drag.updated(to, m_session.locksTransformRatio(), shift, option);
|
||||
});
|
||||
}
|
||||
// Drags land on whole pixels and degrees; typing stays exact.
|
||||
LayerTransform draft = drag.updated(target, m_session.locksTransformRatio(), shift, option).rounded();
|
||||
if (drag.mode.kind == TransformDrag::Kind::move && !modifiers.testFlag(Qt::ControlModifier) && edit)
|
||||
draft = m_session.snappedMove(draft, moving, tolerance);
|
||||
m_session.previewTransform(draft);
|
||||
synchronizeDisplay();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
#include "UI/HeldModifiers.h"
|
||||
#include <QApplication>
|
||||
#include <QKeyEvent>
|
||||
#include <QMenu>
|
||||
#include <QWidget>
|
||||
|
||||
HeldModifiers &HeldModifiers::shared()
|
||||
{
|
||||
static HeldModifiers *held = new HeldModifiers;
|
||||
return *held;
|
||||
}
|
||||
|
||||
HeldModifiers::HeldModifiers() : QObject(qApp)
|
||||
{
|
||||
qApp->installEventFilter(this);
|
||||
// A key let go behind another app is never seen.
|
||||
connect(qApp, &QGuiApplication::applicationStateChanged, this, [this](Qt::ApplicationState state) {
|
||||
update(state == Qt::ApplicationActive ? QGuiApplication::queryKeyboardModifiers() : Qt::NoModifier);
|
||||
});
|
||||
}
|
||||
|
||||
void HeldModifiers::update(Qt::KeyboardModifiers raw)
|
||||
{
|
||||
// An NSText first responder: a widget taking typed text.
|
||||
const QWidget *focus = QApplication::focusWidget();
|
||||
const bool typing = focus && focus->testAttribute(Qt::WA_InputMethodEnabled);
|
||||
const Qt::KeyboardModifiers held =
|
||||
typing ? Qt::NoModifier : raw & (Qt::ControlModifier | Qt::ShiftModifier | Qt::AltModifier | Qt::MetaModifier);
|
||||
if (held == m_flags)
|
||||
return;
|
||||
m_flags = held;
|
||||
emit changed();
|
||||
}
|
||||
|
||||
bool HeldModifiers::eventFilter(QObject *watched, QEvent *event)
|
||||
{
|
||||
const QEvent::Type type = event->type();
|
||||
if (type == QEvent::KeyPress || type == QEvent::KeyRelease) {
|
||||
const auto *key = static_cast<QKeyEvent *>(event);
|
||||
Qt::KeyboardModifiers raw = key->modifiers();
|
||||
// A modifier's own event may not count it yet.
|
||||
const Qt::KeyboardModifier own = key->key() == Qt::Key_Control ? Qt::ControlModifier
|
||||
: key->key() == Qt::Key_Shift ? Qt::ShiftModifier
|
||||
: key->key() == Qt::Key_Alt ? Qt::AltModifier
|
||||
: key->key() == Qt::Key_Meta ? Qt::MetaModifier
|
||||
: Qt::NoModifier;
|
||||
raw.setFlag(own, type == QEvent::KeyPress);
|
||||
update(raw);
|
||||
} else if (type == QEvent::MouseButtonPress || type == QEvent::MouseButtonRelease) {
|
||||
if (static_cast<QMouseEvent *>(event)->button() == Qt::LeftButton)
|
||||
update(static_cast<QMouseEvent *>(event)->modifiers());
|
||||
} else if (type == QEvent::Hide && qobject_cast<QMenu *>(watched)) {
|
||||
// Nor is one let go while a menu is open.
|
||||
update(QGuiApplication::queryKeyboardModifiers());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
|
||||
// Swift's HeldModifiers: keys held now, for the Move bar.
|
||||
class HeldModifiers : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
static HeldModifiers &shared();
|
||||
Qt::KeyboardModifiers flags() const { return m_flags; }
|
||||
// Keys held while typing in a field count as none.
|
||||
void update(Qt::KeyboardModifiers raw);
|
||||
|
||||
signals:
|
||||
void changed();
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
|
||||
private:
|
||||
HeldModifiers();
|
||||
|
||||
Qt::KeyboardModifiers m_flags;
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "UI/TransformInspector.h"
|
||||
#include "UI/HeldModifiers.h"
|
||||
#include "UI/KeyboardShortcuts.h"
|
||||
#include "UI/LayerIcons.h"
|
||||
#include "UI/NumericScrub.h"
|
||||
@@ -8,8 +9,8 @@
|
||||
#include <cmath>
|
||||
|
||||
TransformValueField::TransformValueField(const QString &label, const QString &suffix, double low, double high, EditorSession &session,
|
||||
std::function<void(double)> change, QWidget *parent)
|
||||
: QWidget(parent), field(new QLineEdit(this)), m_session(session), m_change(std::move(change))
|
||||
std::function<void()> finish, std::function<void(double)> change, QWidget *parent)
|
||||
: QWidget(parent), field(new QLineEdit(this)), m_session(session), m_finish(std::move(finish)), m_change(std::move(change))
|
||||
{
|
||||
auto *row = new QHBoxLayout(this);
|
||||
row->setContentsMargins(0, 0, 0, 0);
|
||||
@@ -21,7 +22,7 @@ TransformValueField::TransformValueField(const QString &label, const QString &su
|
||||
m_scrub = new NumericScrub(name, {.sensitivity = 1, .low = low, .high = high, .step = 1, .value = [this] { return m_value; }, .set = [this](double value) {
|
||||
field->setText(formatted(value));
|
||||
m_change(value);
|
||||
}});
|
||||
}, .onEnd = m_finish});
|
||||
row->addWidget(name);
|
||||
field->setObjectName(QStringLiteral("transform") + label);
|
||||
field->setAccessibleName(label);
|
||||
@@ -64,15 +65,22 @@ bool TransformValueField::eventFilter(QObject *watched, QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::FocusOut) {
|
||||
const Qt::FocusReason reason = static_cast<QFocusEvent *>(event)->reason();
|
||||
m_borrowed = reason == Qt::MenuBarFocusReason || reason == Qt::PopupFocusReason;
|
||||
if (!m_borrowed)
|
||||
// Another window in front keeps the field, as AppKit does.
|
||||
m_borrowed = reason == Qt::MenuBarFocusReason || reason == Qt::PopupFocusReason || reason == Qt::ActiveWindowFocusReason;
|
||||
if (!m_borrowed) {
|
||||
m_finish();
|
||||
field->setText(formatted(m_value));
|
||||
}
|
||||
}
|
||||
if (event->type() != QEvent::KeyPress)
|
||||
return QWidget::eventFilter(watched, event);
|
||||
const auto *key = static_cast<QKeyEvent *>(event);
|
||||
// Return and Escape hand the canvas the keys.
|
||||
if (key->key() == Qt::Key_Return || key->key() == Qt::Key_Enter || key->key() == Qt::Key_Escape) {
|
||||
// Escape throws the fields' own change away, as Swift's Cancel.
|
||||
const std::optional<TransformEdit> &edit = m_session.transformEdit();
|
||||
if (key->key() == Qt::Key_Escape && edit && edit->fromFields)
|
||||
m_session.cancelTransform();
|
||||
field->clearFocus();
|
||||
m_session.requestCanvasFocus();
|
||||
return true;
|
||||
@@ -90,22 +98,25 @@ bool TransformValueField::eventFilter(QObject *watched, QEvent *event)
|
||||
TransformInspector::TransformInspector(EditorSession &session, QWidget *parent)
|
||||
: ToolHeaderBar(QStringLiteral("Transform"), parent), m_session(session), m_autoSelect(new QCheckBox(QStringLiteral("Auto Select"), this)),
|
||||
m_showControls(new QCheckBox(QStringLiteral("Show Controls"), this)), m_fields(new QScrollArea(this)),
|
||||
m_x(new TransformValueField(QStringLiteral("X"), QString(), -30000, 30000, session, [this](double number) { change([number](LayerTransform &value) { value.origin.setX(number); }); })),
|
||||
m_y(new TransformValueField(QStringLiteral("Y"), QString(), -30000, 30000, session, [this](double number) { change([number](LayerTransform &value) { value.origin.setY(number); }); })),
|
||||
m_width(new TransformValueField(QStringLiteral("W"), QString(), 1, 30000, session, [this](double number) { resizeBox(number, true); })),
|
||||
m_height(new TransformValueField(QStringLiteral("H"), QString(), 1, 30000, session, [this](double number) { resizeBox(number, false); })),
|
||||
m_x(new TransformValueField(QStringLiteral("X"), QString(), -30000, 30000, session, [this] { finish(); }, [this](double number) { change([number](LayerTransform &value) { value.origin.setX(number); }); })),
|
||||
m_y(new TransformValueField(QStringLiteral("Y"), QString(), -30000, 30000, session, [this] { finish(); }, [this](double number) { change([number](LayerTransform &value) { value.origin.setY(number); }); })),
|
||||
m_width(new TransformValueField(QStringLiteral("W"), QString(), 1, 30000, session, [this] { finish(); }, [this](double number) { resizeBox(number, true); })),
|
||||
m_height(new TransformValueField(QStringLiteral("H"), QString(), 1, 30000, session, [this] { finish(); }, [this](double number) { resizeBox(number, false); })),
|
||||
m_lock(new QToolButton(this)),
|
||||
m_scale(new TransformValueField(QStringLiteral("Scale"), QStringLiteral("%"), 0.1, 30000, session, [this](double number) {
|
||||
m_scale(new TransformValueField(QStringLiteral("Scale"), QStringLiteral("%"), 0.1, 30000, session, [this] { finish(); }, [this](double number) {
|
||||
change([&](LayerTransform &value) { value = value.scaled(number, pixelSize()); });
|
||||
})),
|
||||
m_rotation(new TransformValueField(QStringLiteral("°"), QString(), -360, 360, session, [this](double number) {
|
||||
m_rotation(new TransformValueField(QStringLiteral("°"), QString(), -360, 360, session, [this] { finish(); }, [this](double number) {
|
||||
change([number](LayerTransform &value) { value.rotation = std::fmod(number, 360.0); });
|
||||
})),
|
||||
m_sampling(new QComboBox(this)), m_cancel(new QPushButton(QStringLiteral("Cancel"), this)), m_apply(new QPushButton(QStringLiteral("Apply"), this))
|
||||
{
|
||||
m_autoSelect->setObjectName(QStringLiteral("transformAutoSelect"));
|
||||
m_autoSelect->setToolTip(QStringLiteral("Select layers by clicking the canvas. When off, hold Ctrl to select a layer."));
|
||||
connect(m_autoSelect, &QCheckBox::toggled, this, [this](bool picks) { m_session.setTransformAutoSelect(picks); });
|
||||
m_autoSelect->setToolTip(QStringLiteral("Select layers by clicking the canvas. Hold Ctrl to turn it the other way while you click."));
|
||||
// Held Ctrl flips Auto Select, and the box shows it.
|
||||
connect(m_autoSelect, &QCheckBox::toggled, this, [this](bool picks) {
|
||||
m_session.setTransformAutoSelect(picks != HeldModifiers::shared().flags().testFlag(Qt::ControlModifier));
|
||||
});
|
||||
m_showControls->setObjectName(QStringLiteral("showTransformControls"));
|
||||
m_showControls->setToolTip(QStringLiteral("Show the transform box and handles (Ctrl+H). When hidden, drag anywhere to move the layer."));
|
||||
connect(m_showControls, &QCheckBox::toggled, this, [this](bool shows) { m_session.setShowsTransformControls(shows); });
|
||||
@@ -122,9 +133,12 @@ TransformInspector::TransformInspector(EditorSession &session, QWidget *parent)
|
||||
m_lock->setObjectName(QStringLiteral("locksTransformRatio"));
|
||||
m_lock->setCheckable(true);
|
||||
m_lock->setAutoRaise(true);
|
||||
m_lock->setToolTip(QStringLiteral("Lock aspect ratio"));
|
||||
m_lock->setToolTip(QStringLiteral("Lock aspect ratio. Hold Shift while dragging a handle to turn it the other way."));
|
||||
applyLockIcon();
|
||||
connect(m_lock, &QToolButton::toggled, this, [this](bool locks) { m_session.setLocksTransformRatio(locks); });
|
||||
// Held Shift flips the lock, and the button shows it.
|
||||
connect(m_lock, &QToolButton::toggled, this, [this](bool locks) {
|
||||
m_session.setLocksTransformRatio(locks != HeldModifiers::shared().flags().testFlag(Qt::ShiftModifier));
|
||||
});
|
||||
fields->addWidget(m_lock);
|
||||
m_scale->setFixedWidth(110);
|
||||
m_scale->setToolTip(QStringLiteral("Scale width and height together, about the center"));
|
||||
@@ -168,6 +182,7 @@ TransformInspector::TransformInspector(EditorSession &session, QWidget *parent)
|
||||
// The scroll view takes the spare room, as SwiftUI's does.
|
||||
row->setStretch(row->count() - 1, 0);
|
||||
connect(&m_session, &EditorSession::changed, this, &TransformInspector::synchronize);
|
||||
connect(&HeldModifiers::shared(), &HeldModifiers::changed, this, &TransformInspector::synchronize);
|
||||
synchronize();
|
||||
}
|
||||
|
||||
@@ -216,10 +231,11 @@ QSizeF TransformInspector::pixelSize() const
|
||||
return active ? active->size() : value().size;
|
||||
}
|
||||
|
||||
// Shown at once, applied without Apply; Ctrl+T's edit takes it.
|
||||
void TransformInspector::change(const std::function<void(LayerTransform &)> &update)
|
||||
{
|
||||
if (!m_session.transformEdit())
|
||||
m_session.beginTransform();
|
||||
m_session.beginTransform(false, true);
|
||||
if (!m_session.transformEdit())
|
||||
return;
|
||||
LayerTransform next = m_session.transformEdit()->draft;
|
||||
@@ -227,6 +243,12 @@ void TransformInspector::change(const std::function<void(LayerTransform &)> &upd
|
||||
m_session.previewTransform(next);
|
||||
}
|
||||
|
||||
void TransformInspector::finish()
|
||||
{
|
||||
if (m_session.transformEdit() && m_session.transformEdit()->fromFields)
|
||||
m_session.commitTransform();
|
||||
}
|
||||
|
||||
void TransformInspector::resizeBox(double number, bool width)
|
||||
{
|
||||
// A side under a pixel makes a box previewTransform refuses.
|
||||
@@ -258,9 +280,10 @@ void TransformInspector::synchronize()
|
||||
const QSignalBlocker blocker(button);
|
||||
button->setChecked(checked);
|
||||
};
|
||||
set(m_autoSelect, m_session.transformAutoSelect());
|
||||
const Qt::KeyboardModifiers held = HeldModifiers::shared().flags();
|
||||
set(m_autoSelect, m_session.transformAutoSelect() != held.testFlag(Qt::ControlModifier));
|
||||
set(m_showControls, m_session.showsTransformControls());
|
||||
set(m_lock, m_session.locksTransformRatio());
|
||||
set(m_lock, m_session.locksTransformRatio() != held.testFlag(Qt::ShiftModifier));
|
||||
const LayerTransform shown = value();
|
||||
m_x->sync(shown.origin.x());
|
||||
m_y->sync(shown.origin.y());
|
||||
|
||||
@@ -17,7 +17,7 @@ class TransformValueField : public QWidget {
|
||||
public:
|
||||
// The label scrubs within low and high, in whole steps.
|
||||
TransformValueField(const QString &label, const QString &suffix, double low, double high, EditorSession &session,
|
||||
std::function<void(double)> change, QWidget *parent = nullptr);
|
||||
std::function<void()> finish, std::function<void(double)> change, QWidget *parent = nullptr);
|
||||
|
||||
// Shown when the field is not being edited.
|
||||
void sync(double value);
|
||||
@@ -33,6 +33,8 @@ protected:
|
||||
|
||||
private:
|
||||
EditorSession &m_session;
|
||||
// Done with its value: a scrub let go, or left.
|
||||
const std::function<void()> m_finish;
|
||||
const std::function<void(double)> m_change;
|
||||
NumericScrub *m_scrub = nullptr;
|
||||
double m_value = 0;
|
||||
@@ -56,6 +58,8 @@ private:
|
||||
LayerTransform value() const;
|
||||
QSizeF pixelSize() const;
|
||||
void change(const std::function<void(LayerTransform &)> &update);
|
||||
// What the fields changed applies, one step.
|
||||
void finish();
|
||||
// Swift's `resize`; the name would hide QWidget's.
|
||||
void resizeBox(double number, bool width);
|
||||
void synchronize();
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
#include "CanvasFixtures.h"
|
||||
|
||||
// Swift's resize snapping on the canvas: handles, Ctrl, Shift.
|
||||
namespace {
|
||||
// The document fills the canvas: view points are pixels.
|
||||
struct Canvas : Shown {
|
||||
QUuid red;
|
||||
Canvas() : Shown(QSize(400, 300), QSize(400, 300))
|
||||
{
|
||||
settle();
|
||||
session.zoom(1);
|
||||
session.insert(filled(100, 100, qRgba(255, 0, 0, 255), "Red"));
|
||||
red = session.activeLayerID().value();
|
||||
session.selectTool(NavigationTool::move);
|
||||
canvas->synchronizeDisplay();
|
||||
if (session.viewport.viewPoint(QPointF(0, 0), documentSize()) != QPointF(0, 0))
|
||||
throw std::runtime_error("the document does not fill the canvas");
|
||||
}
|
||||
QPointF origin() const { return layerWith(session, red).transform.origin; }
|
||||
void press(QPointF at, Qt::KeyboardModifiers modifiers = Qt::NoModifier) { QTest::mousePress(canvas, Qt::LeftButton, modifiers, at.toPoint()); }
|
||||
void move(QPointF to, Qt::KeyboardModifiers modifiers = Qt::NoModifier)
|
||||
{
|
||||
QMouseEvent event(QEvent::MouseMove, to, to, canvas->mapToGlobal(to.toPoint()), Qt::NoButton, Qt::LeftButton, modifiers);
|
||||
QApplication::sendEvent(canvas, &event);
|
||||
}
|
||||
void release(QPointF at, Qt::KeyboardModifiers modifiers = Qt::NoModifier) { QTest::mouseRelease(canvas, Qt::LeftButton, modifiers, at.toPoint()); }
|
||||
void drag(QPointF from, QPointF to, Qt::KeyboardModifiers modifiers = Qt::NoModifier)
|
||||
{
|
||||
press(from, modifiers);
|
||||
move((from + to) / 2, modifiers);
|
||||
move(to, modifiers);
|
||||
release(to, modifiers);
|
||||
}
|
||||
void click(QPointF at, Qt::KeyboardModifiers modifiers = Qt::NoModifier)
|
||||
{
|
||||
press(at, modifiers);
|
||||
release(at, modifiers);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class ResizeSnapCanvasTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void resizedEdgesSnapUnlessCtrl();
|
||||
};
|
||||
|
||||
void ResizeSnapCanvasTests::resizedEdgesSnapUnlessCtrl()
|
||||
{
|
||||
Canvas shown;
|
||||
EditorSession &session = shown.session;
|
||||
session.setLocksTransformRatio(false);
|
||||
// The right edge, from 250 to four short of 400.
|
||||
shown.press(QPointF(250, 150));
|
||||
shown.move(QPointF(396, 150));
|
||||
QCOMPARE(session.transformEdit().value().draft, (LayerTransform{.origin = {150, 100}, .size = {250, 100}}));
|
||||
QCOMPARE(session.snapGuides, (SnapGuides{{400}, {}}));
|
||||
// Ctrl mid-drag frees it.
|
||||
shown.move(QPointF(396, 150), Qt::ControlModifier);
|
||||
QCOMPARE(session.transformEdit().value().draft.size, QSizeF(246, 100));
|
||||
shown.release(QPointF(396, 150), Qt::ControlModifier);
|
||||
QCOMPARE(layerWith(session, shown.red).transform.size, QSizeF(246, 100));
|
||||
// Snapping off, nothing snaps.
|
||||
session.undo();
|
||||
session.setSnappingEnabled(false);
|
||||
shown.drag(QPointF(250, 150), QPointF(396, 150));
|
||||
QCOMPARE(layerWith(session, shown.red).transform.size, QSizeF(246, 100));
|
||||
QCOMPARE(session.snapGuides, SnapGuides{});
|
||||
// Locked, Shift frees the lock: a corner snaps both edges.
|
||||
session.undo();
|
||||
session.setSnappingEnabled(true);
|
||||
session.setLocksTransformRatio(true);
|
||||
shown.press(QPointF(250, 200));
|
||||
shown.move(QPointF(396, 296), Qt::ShiftModifier);
|
||||
QCOMPARE(session.transformEdit().value().draft, (LayerTransform{.origin = {150, 100}, .size = {250, 200}}));
|
||||
QCOMPARE(session.snapGuides, (SnapGuides{{400}, {300}}));
|
||||
// Kept proportional, only the nearer edge snaps.
|
||||
shown.move(QPointF(397, 349));
|
||||
QCOMPARE(session.transformEdit().value().draft.size, QSizeF(250, 250));
|
||||
QCOMPARE(session.snapGuides, (SnapGuides{{400}, {}}));
|
||||
shown.release(QPointF(397, 349));
|
||||
}
|
||||
|
||||
QTEST_MAIN(ResizeSnapCanvasTests)
|
||||
#include "ResizeSnapCanvasTests.moc"
|
||||
@@ -0,0 +1,157 @@
|
||||
#include "SessionFixtures.h"
|
||||
#include <QtTest>
|
||||
|
||||
// Swift's ResizeSnapTests: a resized layer's edges snap, as moves do.
|
||||
namespace {
|
||||
struct Resized {
|
||||
EditorSession session;
|
||||
QUuid layer;
|
||||
|
||||
// 300 by 200: the layer at 10; another at 150.
|
||||
Resized()
|
||||
{
|
||||
session.createDocument(300, 200);
|
||||
place(QPointF(150, 150), QSizeF(40, 40));
|
||||
place(QPointF(10, 10), QSizeF(100, 100));
|
||||
layer = session.activeLayerID().value();
|
||||
}
|
||||
void place(QPointF origin, QSizeF size)
|
||||
{
|
||||
session.insert(ImportedImage(QImage(20, 20, QImage::Format_RGBA8888_Premultiplied), QImage(), "Layer"));
|
||||
session.beginTransform();
|
||||
session.previewTransform(LayerTransform{.origin = origin, .size = size});
|
||||
session.commitTransform();
|
||||
}
|
||||
LayerTransform resize(int handle, QPointF start, QPointF point, bool lockRatio)
|
||||
{
|
||||
const TransformDrag drag{layerWith(session, layer).transform, start, {TransformDrag::Kind::resize, handle}, std::nullopt};
|
||||
const QPointF snapped = session.snappedResizePoint(point, drag, lockRatio, {layer}, 5, [&](QPointF to) {
|
||||
return drag.updated(to, lockRatio, false);
|
||||
});
|
||||
return drag.updated(snapped, lockRatio, false).rounded();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class ResizeSnapTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void aSideHandleSnapsItsEdge();
|
||||
void aProportionalCornerSnapsItsNearerEdgeAndKeepsTheRatio();
|
||||
void aTurnedLayerDoesntSnap();
|
||||
void eachEdgeSnapsOnItsOwn();
|
||||
void theDraggedSideAloneSnaps();
|
||||
void snappingOffOrAnotherDragLeavesThePoint();
|
||||
};
|
||||
|
||||
void ResizeSnapTests::aSideHandleSnapsItsEdge()
|
||||
{
|
||||
Resized resized;
|
||||
QCOMPARE(LayerTransform::handles[3], QPointF(1, 0.5));
|
||||
// The right edge, three short of the other layer's left.
|
||||
const LayerTransform result = resized.resize(3, QPointF(110, 60), QPointF(147, 60), false);
|
||||
QCOMPARE(result.origin.x() + result.size.width(), 150.0);
|
||||
QCOMPARE(result.size.height(), 100.0);
|
||||
QCOMPARE(resized.session.snapGuides, (SnapGuides{{150}, {}}));
|
||||
// Out of reach, it does not.
|
||||
const LayerTransform free = resized.resize(3, QPointF(110, 60), QPointF(130, 60), false);
|
||||
QCOMPARE(free.origin.x() + free.size.width(), 130.0);
|
||||
QCOMPARE(resized.session.snapGuides, SnapGuides{});
|
||||
// The left handle snaps the left edge, to the canvas.
|
||||
const LayerTransform left = resized.resize(7, QPointF(10, 60), QPointF(3, 60), false);
|
||||
QCOMPARE(left.origin.x(), 0.0);
|
||||
QCOMPARE(left.origin.x() + left.size.width(), 110.0);
|
||||
}
|
||||
|
||||
void ResizeSnapTests::aProportionalCornerSnapsItsNearerEdgeAndKeepsTheRatio()
|
||||
{
|
||||
Resized resized;
|
||||
// Bottom right toward 146, 148: the bottom edge snaps.
|
||||
const LayerTransform result = resized.resize(4, QPointF(110, 110), QPointF(146, 148), true);
|
||||
QCOMPARE(result.origin.y() + result.size.height(), 150.0);
|
||||
QVERIFY(std::abs(result.size.width() - result.size.height()) <= 1);
|
||||
// A tie goes to the right edge, the first.
|
||||
QCOMPARE(resized.session.snapGuides, (SnapGuides{{150}, {}}));
|
||||
// A top at 148 lies nearer the bottom: it snaps.
|
||||
resized.place(QPointF(260, 148), QSizeF(10, 10));
|
||||
resized.session.selectLayer(resized.layer);
|
||||
const LayerTransform bottom = resized.resize(4, QPointF(110, 110), QPointF(147, 147), true);
|
||||
QCOMPARE(bottom, (LayerTransform{.origin = {10, 10}, .size = {138, 138}}));
|
||||
QCOMPARE(resized.session.snapGuides, (SnapGuides{{}, {148}}));
|
||||
// Free, both edges snap, each to its nearest.
|
||||
const LayerTransform free = resized.resize(4, QPointF(110, 110), QPointF(147, 147), false);
|
||||
QCOMPARE(free, (LayerTransform{.origin = {10, 10}, .size = {140, 138}}));
|
||||
}
|
||||
|
||||
void ResizeSnapTests::aTurnedLayerDoesntSnap()
|
||||
{
|
||||
Resized resized;
|
||||
LayerTransform turned = layerWith(resized.session, resized.layer).transform;
|
||||
turned.rotation = 20;
|
||||
const TransformDrag drag{turned, QPointF(110, 60), {TransformDrag::Kind::resize, 3}, std::nullopt};
|
||||
// Wherever the handle goes, the pointer stays put.
|
||||
for (double x = 100; x <= 200; x += 0.5) {
|
||||
resized.session.snapGuides = SnapGuides{{1}, {2}};
|
||||
const QPointF point(x, 60);
|
||||
QCOMPARE(resized.session.snappedResizePoint(point, drag, false, {resized.layer}, 5, [&](QPointF to) { return drag.updated(to, false, false); }),
|
||||
point);
|
||||
QCOMPARE(resized.session.snapGuides, SnapGuides{});
|
||||
}
|
||||
}
|
||||
|
||||
void ResizeSnapTests::eachEdgeSnapsOnItsOwn()
|
||||
{
|
||||
Resized resized;
|
||||
// Free, a corner snaps both edges, each to its line.
|
||||
const LayerTransform result = resized.resize(4, QPointF(110, 110), QPointF(146, 147), false);
|
||||
QCOMPARE(result, (LayerTransform{.origin = {10, 10}, .size = {140, 140}}));
|
||||
QCOMPARE(resized.session.snapGuides, (SnapGuides{{150}, {150}}));
|
||||
// The top left corner moves the top and left edges.
|
||||
const LayerTransform corner = resized.resize(0, QPointF(10, 10), QPointF(2, 4), false);
|
||||
QCOMPARE(corner, (LayerTransform{.origin = {0, 0}, .size = {110, 110}}));
|
||||
// An edge no step moves keeps the pointer; guide shown.
|
||||
const TransformDrag drag{layerWith(resized.session, resized.layer).transform, QPointF(110, 60), {TransformDrag::Kind::resize, 3}, std::nullopt};
|
||||
const QPointF still = resized.session.snappedResizePoint(QPointF(147, 60), drag, false, {resized.layer}, 5, [&](QPointF) {
|
||||
return LayerTransform{.origin = {10, 10}, .size = {138, 100}};
|
||||
});
|
||||
QCOMPARE(still, QPointF(147, 60));
|
||||
QCOMPARE(resized.session.snapGuides, (SnapGuides{{150}, {}}));
|
||||
}
|
||||
|
||||
// The dragged side tells its edge, wherever the press began.
|
||||
void ResizeSnapTests::theDraggedSideAloneSnaps()
|
||||
{
|
||||
Resized resized;
|
||||
// Pressed left of the layer, the right edge still moves.
|
||||
const LayerTransform far = resized.resize(3, QPointF(-50, 60), QPointF(-13, 60), false);
|
||||
QCOMPARE(far.origin.x() + far.size.width(), 150.0);
|
||||
QCOMPARE(far.origin.x(), 10.0);
|
||||
// A top on a line: side drags never snap it.
|
||||
resized.session.beginTransform();
|
||||
resized.session.previewTransform(LayerTransform{.origin = {10, 98}, .size = {100, 60}});
|
||||
resized.session.commitTransform();
|
||||
resized.resize(3, QPointF(110, 128), QPointF(147, 128), false);
|
||||
QCOMPARE(resized.session.snapGuides, (SnapGuides{{150}, {}}));
|
||||
// Nor a bottom handle the left edge, near the canvas's.
|
||||
resized.session.beginTransform();
|
||||
resized.session.previewTransform(LayerTransform{.origin = {2, 10}, .size = {100, 100}});
|
||||
resized.session.commitTransform();
|
||||
const LayerTransform down = resized.resize(5, QPointF(52, 110), QPointF(52, 148), false);
|
||||
QCOMPARE(down, (LayerTransform{.origin = {2, 10}, .size = {100, 140}}));
|
||||
QCOMPARE(resized.session.snapGuides, (SnapGuides{{}, {150}}));
|
||||
}
|
||||
|
||||
void ResizeSnapTests::snappingOffOrAnotherDragLeavesThePoint()
|
||||
{
|
||||
Resized resized;
|
||||
resized.session.setSnappingEnabled(false);
|
||||
const LayerTransform result = resized.resize(3, QPointF(110, 60), QPointF(147, 60), false);
|
||||
QCOMPARE(result.origin.x() + result.size.width(), 147.0);
|
||||
resized.session.setSnappingEnabled(true);
|
||||
const TransformDrag move{layerWith(resized.session, resized.layer).transform, QPointF(110, 60), {TransformDrag::Kind::move, 3}, std::nullopt};
|
||||
QCOMPARE(resized.session.snappedResizePoint(QPointF(147, 60), move, false, {resized.layer}, 5, [&](QPointF to) { return move.updated(to, false, false); }),
|
||||
QPointF(147, 60));
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(ResizeSnapTests)
|
||||
#include "ResizeSnapTests.moc"
|
||||
@@ -27,62 +27,72 @@ void ScrubbableTransformTests::theTransformLabelsScrubWholeNumbersOverTyping()
|
||||
bar.activateWindow();
|
||||
QVERIFY(QTest::qWaitForWindowActive(&bar));
|
||||
const QPointF origin = session.activeLayer().value().transform.origin;
|
||||
// The first drag begins the edit, as typing does.
|
||||
const int base = session.history.undoCount();
|
||||
const auto shown = [&] { return session.activeLayer().value().transform; };
|
||||
// Each drag applies when let go, one step.
|
||||
drag(scrubbed(bar, "X"), 7);
|
||||
QCOMPARE(session.transformEdit().value().draft.origin.x(), std::round(origin.x()) + 7);
|
||||
QVERIFY(!session.transformEdit().has_value());
|
||||
QCOMPARE(shown().origin.x(), std::round(origin.x()) + 7);
|
||||
QCOMPARE(session.history.undoCount(), base + 1);
|
||||
QCOMPARE(session.history.undoName(), QString("Transform Layer"));
|
||||
// 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(shown().size, QSizeF(13, 20));
|
||||
QCOMPARE(width.text(), QString("13"));
|
||||
drag(scrubbed(bar, "W"), -100);
|
||||
QCOMPARE(session.transformEdit().value().draft.size.width(), 1.0);
|
||||
QCOMPARE(shown().size.width(), 1.0);
|
||||
drag(scrubbed(bar, "H"), 40000);
|
||||
QCOMPARE(session.transformEdit().value().draft.size.height(), 30000.0);
|
||||
QCOMPARE(shown().size.height(), 30000.0);
|
||||
drag(scrubbed(bar, "°"), 30);
|
||||
QCOMPARE(session.transformEdit().value().draft.rotation, 30.0);
|
||||
QCOMPARE(shown().rotation, 30.0);
|
||||
drag(scrubbed(bar, "°"), -900);
|
||||
QCOMPARE(session.transformEdit().value().draft.rotation, -0.0);
|
||||
QCOMPARE(shown().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);
|
||||
QCOMPARE(shown().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"));
|
||||
QVERIFY(session.transformEdit().value().fromFields);
|
||||
QCOMPARE(session.transformEdit().value().draft.origin.x(), 10.4);
|
||||
drag(scrubbed(bar, "X"), 3);
|
||||
QCOMPARE(session.transformEdit().value().draft.origin.x(), 13.0);
|
||||
QCOMPARE(shown().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);
|
||||
QCOMPARE(shown().origin.x(), -30000.0);
|
||||
drag(scrubbed(bar, "X"), 90000);
|
||||
QCOMPARE(session.transformEdit().value().draft.origin.x(), 30000.0);
|
||||
QCOMPARE(shown().origin.x(), 30000.0);
|
||||
drag(scrubbed(bar, "Y"), 90000);
|
||||
QCOMPARE(session.transformEdit().value().draft.origin.y(), 30000.0);
|
||||
QCOMPARE(shown().origin.y(), 30000.0);
|
||||
drag(scrubbed(bar, "W"), 90000);
|
||||
QCOMPARE(session.transformEdit().value().draft.size.width(), 30000.0);
|
||||
QCOMPARE(shown().size.width(), 30000.0);
|
||||
drag(scrubbed(bar, "H"), -90000);
|
||||
QCOMPARE(session.transformEdit().value().draft.size.height(), 1.0);
|
||||
QCOMPARE(shown().size.height(), 1.0);
|
||||
// 360 reaches the setter, whose remainder leaves none.
|
||||
drag(scrubbed(bar, "°"), 9000);
|
||||
QCOMPARE(session.transformEdit().value().draft.rotation, 0.0);
|
||||
QCOMPARE(shown().rotation, 0.0);
|
||||
drag(scrubbed(bar, "°"), 359);
|
||||
QCOMPARE(session.transformEdit().value().draft.rotation, 359.0);
|
||||
QCOMPARE(shown().rotation, 359.0);
|
||||
// Scale snaps whole percents too.
|
||||
session.cancelTransform();
|
||||
const auto restart = [&] {
|
||||
while (session.history.undoCount() > base)
|
||||
session.undo();
|
||||
};
|
||||
restart();
|
||||
drag(scrubbed(bar, "Scale"), 5);
|
||||
QCOMPARE(session.transformEdit().value().draft.size, QSizeF(42, 21));
|
||||
session.cancelTransform();
|
||||
QCOMPARE(shown().size, QSizeF(42, 21));
|
||||
restart();
|
||||
drag(scrubbed(bar, "Scale"), 90000);
|
||||
QCOMPARE(session.transformEdit().value().draft.size, QSizeF(12000, 6000));
|
||||
QCOMPARE(shown().size, QSizeF(12000, 6000));
|
||||
QCOMPARE(find<QLineEdit>(bar, "transformScale").text(), QString("30000"));
|
||||
// While distorted the numbers rest, labels included.
|
||||
session.cancelTransform();
|
||||
@@ -101,7 +111,7 @@ void ScrubbableTransformTests::scaleScrubsDownToATenth()
|
||||
TransformInspector bar(session);
|
||||
bar.show();
|
||||
drag(scrubbed(bar, "Scale"), -90000);
|
||||
QCOMPARE(session.transformEdit().value().draft.size, QSizeF(2, 1));
|
||||
QCOMPARE(session.activeLayer().value().transform.size, QSizeF(2, 1));
|
||||
QCOMPARE(find<QLineEdit>(bar, "transformScale").text(), QString("0.10"));
|
||||
}
|
||||
|
||||
@@ -204,9 +214,10 @@ void ScrubbableTransformTests::aTransformScrubEndsWithItsLayer()
|
||||
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.
|
||||
// A fresh press scrubs the new layer, applied on release.
|
||||
drag(label, 5);
|
||||
QCOMPARE(session.transformEdit().value().draft.origin.x(), std::round(x) + 5);
|
||||
QVERIFY(!session.transformEdit().has_value());
|
||||
QCOMPARE(layerWith(session, first).transform.origin.x(), std::round(x) + 5);
|
||||
}
|
||||
|
||||
QTEST_MAIN(ScrubbableTransformTests)
|
||||
|
||||
@@ -216,6 +216,8 @@ compositor_test(TransformOverlayTests)
|
||||
compositor_test(TransformPressTests)
|
||||
compositor_test(TransformInspectorTests)
|
||||
compositor_test(TransformSnapTests)
|
||||
compositor_test(ResizeSnapTests)
|
||||
compositor_test(ResizeSnapCanvasTests)
|
||||
compositor_test(CursorTests)
|
||||
compositor_test(DistortCanvasTests)
|
||||
compositor_test(DistortMaskTests)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "SessionFixtures.h"
|
||||
#include "UI/HeldModifiers.h"
|
||||
#include "UI/LayerIcons.h"
|
||||
#include "UI/TransformInspector.h"
|
||||
#include <QtTest>
|
||||
@@ -57,6 +58,7 @@ private slots:
|
||||
void aMenuBorrowsTheFocusAndTheBarsKeysApplyOrCancel();
|
||||
void theLockIconFollowsThePalette();
|
||||
void theFieldsTakeTheSpareRoom();
|
||||
void heldKeysShowFlippedInTheBar();
|
||||
};
|
||||
|
||||
void TransformInspectorTests::theFieldsShowTheLayerAndTypingPreviews()
|
||||
@@ -73,12 +75,18 @@ void TransformInspectorTests::theFieldsShowTheLayerAndTypingPreviews()
|
||||
QCOMPARE(bar.field("°").text(), QString("0"));
|
||||
QCOMPARE(bar.control<QComboBox>("transformSampling").currentText(), QString("High quality"));
|
||||
QCOMPARE(bar.field("X").accessibleName(), QString("X"));
|
||||
// Typing begins a Ctrl+T edit and previews it.
|
||||
// Typing previews in an edit of the fields' own.
|
||||
const int base = bar.session.history.undoCount();
|
||||
bar.type("X", "90");
|
||||
QVERIFY(bar.session.transformEdit().value().persistent);
|
||||
QVERIFY(!bar.session.transformEdit().value().persistent);
|
||||
QVERIFY(bar.session.transformEdit().value().fromFields);
|
||||
QCOMPARE(bar.draft().origin, QPointF(90, 40));
|
||||
QCOMPARE(layerWith(bar.session, bar.layer).transform.origin, QPointF(80, 40));
|
||||
// Leaving the field applies it, one step.
|
||||
bar.type("Y", "12.5");
|
||||
QCOMPARE(layerWith(bar.session, bar.layer).transform.origin, QPointF(90, 40));
|
||||
QCOMPARE(bar.session.history.undoCount(), base + 1);
|
||||
QCOMPARE(bar.session.history.undoName(), QString("Transform Layer"));
|
||||
QCOMPARE(bar.draft().origin, QPointF(90, 12.5));
|
||||
// Width locked carries the height; unlocked, alone.
|
||||
bar.type("W", "80");
|
||||
@@ -114,7 +122,7 @@ void TransformInspectorTests::theFieldsShowTheLayerAndTypingPreviews()
|
||||
QVERIFY(bar.draft().flipX);
|
||||
bar.control<QPushButton>("flipVertical").click();
|
||||
QVERIFY(bar.draft().flipY);
|
||||
// Apply commits the edit as one step; Cancel drops it.
|
||||
// Apply commits the open edit; Cancel drops it.
|
||||
const LayerTransform drafted = bar.draft();
|
||||
bar.control<QPushButton>("applyTransform").click();
|
||||
QVERIFY(!bar.session.transformEdit().has_value());
|
||||
@@ -141,12 +149,16 @@ void TransformInspectorTests::returnEscapeAndArrowsInAField()
|
||||
QTest::keyClick(&x, Qt::Key_Down, Qt::ShiftModifier);
|
||||
QCOMPARE(bar.draft().origin.x(), 71.0);
|
||||
QCOMPARE(x.text(), QString("71"));
|
||||
// Return hands the canvas the keys; the edit waits there.
|
||||
// Return applies and hands the canvas the keys.
|
||||
const int request = bar.session.canvasFocusRequest();
|
||||
const int steps = bar.session.history.undoCount();
|
||||
QTest::keyClick(&x, Qt::Key_Return);
|
||||
QVERIFY(!x.hasFocus());
|
||||
QCOMPARE(bar.session.canvasFocusRequest(), request + 1);
|
||||
QVERIFY(bar.session.transformEdit().has_value());
|
||||
QVERIFY(!bar.session.transformEdit().has_value());
|
||||
QCOMPARE(layerWith(bar.session, bar.layer).transform.origin.x(), 71.0);
|
||||
QCOMPARE(bar.session.history.undoCount(), steps + 1);
|
||||
// Escape throws the fields' change away.
|
||||
x.setFocus();
|
||||
QTest::keyClicks(&x, "2");
|
||||
QCOMPARE(x.text(), QString("712"));
|
||||
@@ -154,13 +166,28 @@ void TransformInspectorTests::returnEscapeAndArrowsInAField()
|
||||
QTest::keyClick(&x, Qt::Key_Escape);
|
||||
QVERIFY(!x.hasFocus());
|
||||
QCOMPARE(bar.session.canvasFocusRequest(), request + 2);
|
||||
QCOMPARE(bar.draft().origin.x(), 712.0);
|
||||
QVERIFY(!bar.session.transformEdit().has_value());
|
||||
QCOMPARE(layerWith(bar.session, bar.layer).transform.origin.x(), 71.0);
|
||||
QCOMPARE(x.text(), QString("71"));
|
||||
QCOMPARE(bar.session.history.undoCount(), steps + 1);
|
||||
// Ctrl+T's edit takes typing and waits through Return and Escape.
|
||||
bar.session.beginTransform();
|
||||
x.setFocus();
|
||||
QTest::keyClicks(&x, "5");
|
||||
QCOMPARE(bar.draft().origin.x(), 715.0);
|
||||
QVERIFY(!bar.session.transformEdit().value().fromFields);
|
||||
QTest::keyClick(&x, Qt::Key_Return);
|
||||
QCOMPARE(bar.draft().origin.x(), 715.0);
|
||||
x.setFocus();
|
||||
QTest::keyClick(&x, Qt::Key_Escape);
|
||||
QCOMPARE(bar.draft().origin.x(), 715.0);
|
||||
QCOMPARE(layerWith(bar.session, bar.layer).transform.origin.x(), 71.0);
|
||||
// While a field is typed in, the numbers wait outside.
|
||||
x.setFocus();
|
||||
LayerTransform moved = bar.draft();
|
||||
moved.origin.setX(30);
|
||||
bar.session.previewTransform(moved);
|
||||
QCOMPARE(x.text(), QString("712"));
|
||||
QCOMPARE(x.text(), QString("715"));
|
||||
QCOMPARE(bar.field("Y").text(), QString("40"));
|
||||
x.clearFocus();
|
||||
QCOMPARE(x.text(), QString("30"));
|
||||
@@ -172,7 +199,9 @@ void TransformInspectorTests::settingsButtonsAndTheTitleFollowTheSession()
|
||||
QCheckBox &autoSelect = bar.control<QCheckBox>("transformAutoSelect");
|
||||
QCheckBox &controls = bar.control<QCheckBox>("showTransformControls");
|
||||
QVERIFY(!autoSelect.isChecked() && controls.isChecked());
|
||||
QCOMPARE(autoSelect.toolTip(), QString("Select layers by clicking the canvas. When off, hold Ctrl to select a layer."));
|
||||
QCOMPARE(autoSelect.toolTip(), QString("Select layers by clicking the canvas. Hold Ctrl to turn it the other way while you click."));
|
||||
QCOMPARE(bar.control<QToolButton>("locksTransformRatio").toolTip(),
|
||||
QString("Lock aspect ratio. Hold Shift while dragging a handle to turn it the other way."));
|
||||
autoSelect.click();
|
||||
QVERIFY(bar.session.transformAutoSelect());
|
||||
controls.click();
|
||||
@@ -258,14 +287,25 @@ void TransformInspectorTests::aMenuBorrowsTheFocusAndTheBarsKeysApplyOrCancel()
|
||||
apply.setFocus(Qt::PopupFocusReason);
|
||||
QCOMPARE(x.text(), QString("12.5"));
|
||||
x.setFocus();
|
||||
// Another window in front keeps it too.
|
||||
apply.setFocus(Qt::ActiveWindowFocusReason);
|
||||
QCOMPARE(x.text(), QString("12.5"));
|
||||
QVERIFY(bar.session.transformEdit().has_value());
|
||||
x.setFocus();
|
||||
apply.setFocus(Qt::TabFocusReason);
|
||||
QCOMPARE(x.text(), QString("12.50"));
|
||||
// Return on Apply applies; Escape on a toggle cancels.
|
||||
apply.setFocus();
|
||||
QVERIFY(apply.hasFocus());
|
||||
QTest::keyClick(&apply, Qt::Key_Return);
|
||||
QVERIFY(!bar.session.transformEdit().has_value());
|
||||
QCOMPARE(layerWith(bar.session, bar.layer).transform.origin.x(), 12.5);
|
||||
// Return on Apply applies Ctrl+T's; Escape on a toggle cancels.
|
||||
bar.session.beginTransform();
|
||||
bar.type("X", "14");
|
||||
apply.setFocus();
|
||||
QVERIFY(apply.hasFocus());
|
||||
QVERIFY(bar.session.transformEdit().has_value());
|
||||
QTest::keyClick(&apply, Qt::Key_Return);
|
||||
QVERIFY(!bar.session.transformEdit().has_value());
|
||||
QCOMPARE(layerWith(bar.session, bar.layer).transform.origin.x(), 14.0);
|
||||
bar.session.beginTransform();
|
||||
bar.type("Y", "7");
|
||||
QCOMPARE(bar.draft().origin.y(), 7.0);
|
||||
QCheckBox &controls = bar.control<QCheckBox>("showTransformControls");
|
||||
@@ -306,5 +346,74 @@ void TransformInspectorTests::theFieldsTakeTheSpareRoom()
|
||||
QCOMPARE(flip.width(), flip.sizeHint().width());
|
||||
}
|
||||
|
||||
// Swift's HeldModifiers: Ctrl flips Auto Select, Shift the lock.
|
||||
void TransformInspectorTests::heldKeysShowFlippedInTheBar()
|
||||
{
|
||||
Bar bar;
|
||||
QVERIFY(bar.active);
|
||||
QCheckBox &autoSelect = bar.control<QCheckBox>("transformAutoSelect");
|
||||
QToolButton &lock = bar.control<QToolButton>("locksTransformRatio");
|
||||
HeldModifiers &held = HeldModifiers::shared();
|
||||
autoSelect.setFocus();
|
||||
QVERIFY(!autoSelect.isChecked() && lock.isChecked());
|
||||
// A modifier's own press counts it, whatever the event says.
|
||||
QTest::keyPress(&autoSelect, Qt::Key_Control, Qt::NoModifier);
|
||||
QCOMPARE(held.flags(), Qt::KeyboardModifiers(Qt::ControlModifier));
|
||||
QVERIFY(autoSelect.isChecked());
|
||||
QVERIFY(!bar.session.transformAutoSelect());
|
||||
// Only a change is announced.
|
||||
QSignalSpy announced(&held, &HeldModifiers::changed);
|
||||
const auto press = [&](Qt::KeyboardModifiers modifiers) {
|
||||
QKeyEvent event(QEvent::KeyPress, Qt::Key_A, modifiers);
|
||||
QApplication::sendEvent(&autoSelect, &event);
|
||||
};
|
||||
press(Qt::ControlModifier);
|
||||
QCOMPARE(announced.count(), 0);
|
||||
press(Qt::ControlModifier | Qt::ShiftModifier);
|
||||
QCOMPARE(announced.count(), 1);
|
||||
press(Qt::ControlModifier);
|
||||
QCOMPARE(announced.count(), 2);
|
||||
// A click while held writes the other way.
|
||||
autoSelect.click();
|
||||
QVERIFY(bar.session.transformAutoSelect());
|
||||
QVERIFY(!autoSelect.isChecked());
|
||||
QTest::keyRelease(&autoSelect, Qt::Key_Control, Qt::ControlModifier);
|
||||
QCOMPARE(held.flags(), Qt::KeyboardModifiers());
|
||||
QVERIFY(autoSelect.isChecked());
|
||||
// Shift flips the lock likewise; Alt and Meta count, unshown.
|
||||
QTest::keyPress(&autoSelect, Qt::Key_Shift, Qt::AltModifier | Qt::MetaModifier | Qt::KeypadModifier);
|
||||
QCOMPARE(held.flags(), Qt::ShiftModifier | Qt::AltModifier | Qt::MetaModifier);
|
||||
QVERIFY(!lock.isChecked());
|
||||
QVERIFY(autoSelect.isChecked());
|
||||
lock.click();
|
||||
QVERIFY(!bar.session.locksTransformRatio());
|
||||
QVERIFY(lock.isChecked());
|
||||
QTest::keyRelease(&autoSelect, Qt::Key_Shift, Qt::ShiftModifier);
|
||||
QVERIFY(!lock.isChecked());
|
||||
QTest::keyRelease(&autoSelect, Qt::Key_Alt, Qt::AltModifier);
|
||||
QTest::keyRelease(&autoSelect, Qt::Key_Meta, Qt::MetaModifier);
|
||||
QCOMPARE(held.flags(), Qt::KeyboardModifiers());
|
||||
// Left presses and releases read modifiers; others do not.
|
||||
QTest::mousePress(bar.header.get(), Qt::LeftButton, Qt::ControlModifier, QPoint(2, 2));
|
||||
QCOMPARE(held.flags(), Qt::KeyboardModifiers(Qt::ControlModifier));
|
||||
QTest::mouseRelease(bar.header.get(), Qt::RightButton, Qt::NoModifier, QPoint(2, 2));
|
||||
QCOMPARE(held.flags(), Qt::KeyboardModifiers(Qt::ControlModifier));
|
||||
QTest::mouseRelease(bar.header.get(), Qt::LeftButton, Qt::NoModifier, QPoint(2, 2));
|
||||
QCOMPARE(held.flags(), Qt::KeyboardModifiers());
|
||||
// Another app in front lets every key go.
|
||||
QTest::keyPress(&autoSelect, Qt::Key_Shift, Qt::ShiftModifier);
|
||||
QVERIFY(lock.isChecked());
|
||||
emit qApp->applicationStateChanged(Qt::ApplicationInactive);
|
||||
QCOMPARE(held.flags(), Qt::KeyboardModifiers());
|
||||
QVERIFY(!lock.isChecked());
|
||||
// Typing in a field, keys count as none.
|
||||
QLineEdit &x = bar.field("X");
|
||||
x.setFocus();
|
||||
QTest::keyPress(&x, Qt::Key_Control, Qt::ControlModifier);
|
||||
QCOMPARE(held.flags(), Qt::KeyboardModifiers());
|
||||
QVERIFY(autoSelect.isChecked() == bar.session.transformAutoSelect());
|
||||
QTest::keyRelease(&x, Qt::Key_Control, Qt::NoModifier);
|
||||
}
|
||||
|
||||
QTEST_MAIN(TransformInspectorTests)
|
||||
#include "TransformInspectorTests.moc"
|
||||
@@ -51,6 +51,10 @@ private slots:
|
||||
void escapeMidDragRestoresAndAPersistentEditWaitsForReturn();
|
||||
void losingTheKeysOrTheWheelMidDrag();
|
||||
void aSelectionDragsAsOneBox();
|
||||
void ctrlFlipsAutoSelectWhileHeld_data();
|
||||
void ctrlFlipsAutoSelectWhileHeld();
|
||||
void ctrlShiftClickAddsALayerWithAutoSelectOn();
|
||||
void aCanvasDragAppliesTheFieldsEditFirst();
|
||||
};
|
||||
|
||||
void TransformPressTests::draggingOutsideTheLayerMovesIt()
|
||||
@@ -120,12 +124,13 @@ void TransformPressTests::handlesResizeAndTheGripRotates()
|
||||
// An edge, away from its handle, resizes too.
|
||||
shown.drag(QPointF(300, 170), QPointF(320, 170));
|
||||
QCOMPARE(layerWith(shown.session, shown.red).transform.size, QSizeF(170, 100));
|
||||
// Resizing never snaps: five past the middle stays five past.
|
||||
// The dragged edge snaps: five past the middle lands there.
|
||||
shown.drag(QPointF(320, 170), QPointF(205, 170));
|
||||
QCOMPARE(layerWith(shown.session, shown.red).transform, (LayerTransform{.origin = {150, 100}, .size = {55, 100}}));
|
||||
QCOMPARE(layerWith(shown.session, shown.red).transform, (LayerTransform{.origin = {150, 100}, .size = {50, 100}}));
|
||||
// The grip: a quarter turn about the middle.
|
||||
const QPointF center = layerWith(shown.session, shown.red).transform.center();
|
||||
const QPointF grip = center + QPointF(0, -78);
|
||||
QVERIFY(center == QPointF(175, 150));
|
||||
shown.drag(grip, center + QPointF(78, 0));
|
||||
transform = layerWith(shown.session, shown.red).transform;
|
||||
QCOMPARE(transform.rotation, 90.0);
|
||||
@@ -381,5 +386,84 @@ void TransformPressTests::aSelectionDragsAsOneBox()
|
||||
QCOMPARE(session.selectedLayerIDs(), (QSet<QUuid>{shown.red, blue}));
|
||||
}
|
||||
|
||||
// A full-canvas blue layer under the red, and active.
|
||||
static QUuid blueBelow(Canvas &shown)
|
||||
{
|
||||
shown.session.insert(filled(400, 300, qRgba(0, 0, 255, 255), "Blue"));
|
||||
const QUuid blue = shown.session.activeLayerID().value();
|
||||
shown.session.reorderLayers({1}, 0);
|
||||
if (shown.session.document().value().layers[0].id != blue || shown.session.activeLayerID() != blue)
|
||||
throw std::runtime_error("the blue layer is not active at the bottom");
|
||||
shown.canvas->synchronizeDisplay();
|
||||
return blue;
|
||||
}
|
||||
|
||||
void TransformPressTests::ctrlFlipsAutoSelectWhileHeld_data()
|
||||
{
|
||||
QTest::addColumn<bool>("autoSelect");
|
||||
QTest::newRow("off") << false;
|
||||
QTest::newRow("on") << true;
|
||||
}
|
||||
|
||||
// Swift's commandFlipsAutoSelectWhileHeld: Ctrl turns it the other way.
|
||||
void TransformPressTests::ctrlFlipsAutoSelectWhileHeld()
|
||||
{
|
||||
QFETCH(bool, autoSelect);
|
||||
Canvas shown;
|
||||
const QUuid blue = blueBelow(shown);
|
||||
shown.session.setTransformAutoSelect(autoSelect);
|
||||
shown.drag(QPointF(210, 170), QPointF(230, 180), Qt::ControlModifier);
|
||||
const QUuid moved = autoSelect ? blue : shown.red;
|
||||
QCOMPARE(shown.session.activeLayerID(), std::optional(moved));
|
||||
QCOMPARE(layerWith(shown.session, moved).transform.origin, autoSelect ? QPointF(20, 10) : QPointF(170, 110));
|
||||
QCOMPARE(layerWith(shown.session, autoSelect ? shown.red : blue).transform.origin, autoSelect ? QPointF(150, 100) : QPointF(0, 0));
|
||||
// Without Ctrl it picks as set.
|
||||
shown.session.undo();
|
||||
shown.session.selectLayer(blue);
|
||||
shown.drag(QPointF(210, 170), QPointF(230, 180));
|
||||
QCOMPARE(shown.session.activeLayerID(), std::optional(autoSelect ? shown.red : blue));
|
||||
}
|
||||
|
||||
void TransformPressTests::ctrlShiftClickAddsALayerWithAutoSelectOn()
|
||||
{
|
||||
Canvas shown;
|
||||
const QUuid blue = blueBelow(shown);
|
||||
shown.session.setTransformAutoSelect(true);
|
||||
shown.click(QPointF(210, 170), Qt::ControlModifier | Qt::ShiftModifier);
|
||||
QCOMPARE(shown.session.selectedLayerIDs(), (QSet<QUuid>{shown.red, blue}));
|
||||
// Past the active layer's box, Ctrl still keeps it.
|
||||
shown.session.insert(filled(50, 50, qRgba(0, 255, 0, 255), "Green"), QPointF(330, 20));
|
||||
shown.session.selectLayer(shown.red);
|
||||
shown.drag(QPointF(340, 30), QPointF(353, 30), Qt::ControlModifier);
|
||||
QCOMPARE(shown.session.activeLayerID(), std::optional(shown.red));
|
||||
QCOMPARE(shown.origin(), QPointF(163, 100));
|
||||
}
|
||||
|
||||
// The bar's pending value applies first, its own step.
|
||||
void TransformPressTests::aCanvasDragAppliesTheFieldsEditFirst()
|
||||
{
|
||||
Canvas shown;
|
||||
EditorSession &session = shown.session;
|
||||
const int steps = session.history.undoCount();
|
||||
session.beginTransform(false, true);
|
||||
LayerTransform flipped = session.transformEdit().value().draft;
|
||||
flipped.flipX = true;
|
||||
session.previewTransform(flipped);
|
||||
shown.drag(QPointF(20, 20), QPointF(40, 33));
|
||||
QVERIFY(!session.transformEdit().has_value());
|
||||
QCOMPARE(session.history.undoCount(), steps + 2);
|
||||
QCOMPARE(shown.origin(), QPointF(170, 113));
|
||||
QVERIFY(layerWith(session, shown.red).transform.flipX);
|
||||
session.undo();
|
||||
QCOMPARE(shown.origin(), QPointF(150, 100));
|
||||
QVERIFY(layerWith(session, shown.red).transform.flipX);
|
||||
// A Ctrl+T edit takes the drag into itself.
|
||||
session.undo();
|
||||
session.beginTransform();
|
||||
shown.drag(QPointF(20, 20), QPointF(40, 33));
|
||||
QCOMPARE(session.transformEdit().value().draft.origin, QPointF(170, 113));
|
||||
QCOMPARE(session.history.undoCount(), steps);
|
||||
}
|
||||
|
||||
QTEST_MAIN(TransformPressTests)
|
||||
#include "TransformPressTests.moc"
|
||||
Reference in new issue
Block a user