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

+16 -1
View File
@@ -2,6 +2,7 @@
#include "Document/EditorSession.h"
#include "UI/ColorPaletteControls.h"
#include "UI/LassoControls.h"
#include "UI/NumericScrub.h"
#include <QButtonGroup>
#include <QCheckBox>
#include <QComboBox>
@@ -95,13 +96,27 @@ BrushControls::BrushControls(EditorSession &session, QWidget *parent)
m_colour->setToolTip(QStringLiteral("Foreground color"));
m_colour->setAccessibleName(QStringLiteral("Foreground color"));
connect(m_colour, &QAbstractButton::clicked, this, [this] { m_session.openColorPicker(false); });
// Swift's scrubbable labels: a drag sideways sets the value.
QLabel *size = label(QStringLiteral("Size"), this);
QLabel *hardness = label(QStringLiteral("Hardness"), this);
const auto scrub = [this](QLabel *name, SelectionAmountField *shown, double sensitivity, double low, double high, double BrushSettings::*key) {
new NumericScrub(name, {.sensitivity = sensitivity, .low = low, .high = high, .step = std::nullopt,
.value = [this, key] { return m_session.brushSettings().*key; }, .set = [this, shown, key](double value) {
changeBrush(m_session, [&](BrushSettings &brush) { brush.*key = value; });
shown->showValue();
}});
};
scrub(size, m_size, 1, 1, 2000, &BrushSettings::diameter);
scrub(hardness, m_hardness, 0.01, 0, 1, &BrushSettings::hardness);
scrub(m_opacityLabel, m_opacity, 0.01, 0.01, 1, &BrushSettings::opacity);
scrub(m_smoothingLabel, m_smoothing, 1, 0, 100, &BrushSettings::smoothing);
std::vector<QWidget *> widgets{m_paint, m_erase};
for (QAbstractButton *mode : m_blurModes->buttons())
widgets.push_back(mode);
for (QAbstractButton *type : m_healingTypes->buttons())
widgets.push_back(type);
widgets.insert(widgets.end(), {m_aligned, m_thisLayer, m_allLayers});
widgets.insert(widgets.end(), {label(QStringLiteral("Size"), this), m_size, label(QStringLiteral("px"), this), label(QStringLiteral("Hardness"), this),
widgets.insert(widgets.end(), {size, m_size, label(QStringLiteral("px"), this), hardness,
m_hardnessSlider, m_hardness, label(QStringLiteral("%"), this), m_opacityLabel, m_opacitySlider, m_opacity,
label(QStringLiteral("%"), this), m_smoothingLabel, m_smoothingSlider, m_smoothing, m_paintLabel, m_maskPaint, m_colourLabel, m_colour});
for (QWidget *widget : widgets)
+9 -1
View File
@@ -1,6 +1,7 @@
#include "UI/GradientControls.h"
#include "Document/EditorSession.h"
#include "UI/LassoControls.h"
#include "UI/NumericScrub.h"
#include <QButtonGroup>
#include <QCheckBox>
#include <QComboBox>
@@ -87,7 +88,14 @@ GradientControls::GradientControls(EditorSession &session, QWidget *parent)
m_apply->setObjectName(QStringLiteral("gradientApply"));
connect(m_cancel, &QPushButton::clicked, this, [this] { m_session.cancelGradient(); });
connect(m_apply, &QPushButton::clicked, this, [this] { m_session.commitGradient(); });
for (QWidget *widget : std::initializer_list<QWidget *>{m_linear, m_radial, m_swatch, m_style, m_reverse, new QLabel(QStringLiteral("Opacity"), this),
auto *opacity = new QLabel(QStringLiteral("Opacity"), this);
new NumericScrub(opacity, {.sensitivity = 0.01, .low = 0.01, .high = 1, .step = std::nullopt,
.value = [this] { return m_session.gradientSettings().opacity; },
.set = [this](double value) {
changeGradient(m_session, [value](GradientSettings &gradient) { gradient.opacity = value; });
m_opacity->showValue();
}});
for (QWidget *widget : std::initializer_list<QWidget *>{m_linear, m_radial, m_swatch, m_style, m_reverse, opacity,
m_opacitySlider, m_opacity, new QLabel(QStringLiteral("%"), this)})
row->insertWidget(row->count() - 1, widget);
for (QWidget *widget : std::initializer_list<QWidget *>{m_mask, m_cancel, m_apply})
+31 -7
View File
@@ -2,6 +2,7 @@
#include "UI/KeyboardShortcuts.h"
#include <QBoxLayout>
#include <QFrame>
#include "UI/NumericScrub.h"
#include <QLabel>
#include <QSignalBlocker>
#include <QRegularExpressionValidator>
@@ -36,7 +37,7 @@ void SelectionAmountField::sync(int amount)
setText(QString::number(amount));
}
void SelectionAmountField::apply()
void SelectionAmountField::showValue()
{
setText(QString::number(m_amount));
}
@@ -45,7 +46,7 @@ void SelectionAmountField::keyPressEvent(QKeyEvent *event)
{
// Return and Escape hand the canvas the keys.
if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter || event->key() == Qt::Key_Escape) {
apply();
showValue();
clearFocus();
m_session.requestCanvasFocus();
return;
@@ -64,7 +65,7 @@ void SelectionAmountField::focusOutEvent(QFocusEvent *event)
{
m_borrowed = event->reason() == Qt::MenuBarFocusReason || event->reason() == Qt::PopupFocusReason;
if (!m_borrowed)
apply();
showValue();
QLineEdit::focusOutEvent(event);
}
@@ -115,12 +116,15 @@ LassoControls::LassoControls(EditorSession &session, QWidget *parent)
m_antialias(new QCheckBox(QStringLiteral("Anti-alias"), this)), m_expand(new QPushButton(QStringLiteral("Expand"), this)),
m_expandAmount(new SelectionAmountField(session, 1, 500, [this] { return double(m_session.selectionExpandAmount()); },
[this](double amount) { m_session.setSelectionExpandAmount(int(std::lround(amount))); }, this)),
m_expandUnit(new QLabel(QStringLiteral("px"), this)),
m_contract(new QPushButton(QStringLiteral("Contract"), this)),
m_contractAmount(new SelectionAmountField(session, 1, 500, [this] { return double(m_session.selectionContractAmount()); },
[this](double amount) { m_session.setSelectionContractAmount(int(std::lround(amount))); }, this)),
m_contractUnit(new QLabel(QStringLiteral("px"), this)),
m_feather(new QPushButton(QStringLiteral("Feather"), this)),
m_featherAmount(new SelectionAmountField(session, 1, 250, [this] { return double(m_session.selectionFeatherAmount()); },
[this](double amount) { m_session.setSelectionFeatherAmount(int(std::lround(amount))); }, this)),
m_featherUnit(new QLabel(QStringLiteral("px"), this)),
m_empty(new QLabel(QStringLiteral("Empty selection"), this)), m_deselect(new QPushButton(QStringLiteral("Deselect"), this))
{
m_toleranceLabel->setFont(ToolHeaderStyle::controlFont());
@@ -169,13 +173,31 @@ LassoControls::LassoControls(EditorSession &session, QWidget *parent)
m_empty->setForegroundRole(QPalette::PlaceholderText);
m_deselect->setObjectName(QStringLiteral("deselect"));
connect(m_deselect, &QPushButton::clicked, this, [this] { m_session.deselect(); });
// Swift's Int scrub rounds: a step of one.
const auto scrub = [](QWidget *label, SelectionAmountField *shown, double low, double high, std::function<double()> value,
std::function<void(double)> set) {
new NumericScrub(label, {.sensitivity = 1, .low = low, .high = high, .step = 1, .value = std::move(value),
.set = [shown, set = std::move(set)](double amount) {
set(amount);
shown->showValue();
}});
};
scrub(m_toleranceLabel, m_tolerance, 0, 255, [this] { return double(m_session.wandSettings().tolerance); },
[this](double tolerance) { changeWand([tolerance](WandSettings &wand) { wand.tolerance = int(tolerance); }); });
scrub(m_edgeLabel, m_edge, -10, 10, [this] { return double(m_session.objectSelectionSettings().edgeOffset); },
[this](double edge) { changeObject([edge](ObjectSelectionSettings &object) { object.edgeOffset = int(edge); }); });
scrub(m_expandUnit, m_expandAmount, 1, 500, [this] { return double(m_session.selectionExpandAmount()); },
[this](double amount) { m_session.setSelectionExpandAmount(int(amount)); });
scrub(m_contractUnit, m_contractAmount, 1, 500, [this] { return double(m_session.selectionContractAmount()); },
[this](double amount) { m_session.setSelectionContractAmount(int(amount)); });
scrub(m_featherUnit, m_featherAmount, 1, 250, [this] { return double(m_session.selectionFeatherAmount()); },
[this](double amount) { m_session.setSelectionFeatherAmount(int(amount)); });
// Swift's `unitSuffix`: a unit sits by each field.
for (QWidget *widget : std::initializer_list<QWidget *>{m_rectangle, m_ellipse, m_wandMode, m_objectMode, m_freehand, m_polygonal, m_replace, m_add,
m_subtract, m_toleranceLabel, m_tolerance, m_sampleSize, m_thisLayer, m_allLayers, m_contiguous,
m_objectThisLayer, m_objectAllLayers, m_edgeLabel, m_edge, m_edgeUnit, m_antialias,
divider, m_expand, m_expandAmount, new QLabel(QStringLiteral("px"), this), m_contract, m_contractAmount,
new QLabel(QStringLiteral("px"), this), m_feather, m_featherAmount,
new QLabel(QStringLiteral("px"), this)})
divider, m_expand, m_expandAmount, m_expandUnit, m_contract, m_contractAmount,
m_contractUnit, m_feather, m_featherAmount, m_featherUnit})
row->insertWidget(row->count() - 1, widget);
row->addWidget(m_empty);
row->addWidget(m_deselect);
@@ -257,7 +279,7 @@ void LassoControls::synchronize()
if (m_antialias->isChecked() != m_session.selectionAntialiased())
m_antialias->setChecked(m_session.selectionAntialiased());
const bool modifies = m_session.canModifySelection();
for (QWidget *widget : std::initializer_list<QWidget *>{m_expand, m_expandAmount, m_contract, m_contractAmount, m_feather, m_featherAmount})
for (QWidget *widget : std::initializer_list<QWidget *>{m_expand, m_expandAmount, m_expandUnit, m_contract, m_contractAmount, m_contractUnit, m_feather})
widget->setEnabled(modifies);
m_expandAmount->sync(m_session.selectionExpandAmount());
m_contractAmount->sync(m_session.selectionContractAmount());
@@ -286,6 +308,8 @@ SelectionAmountSheet::SelectionAmountSheet(EditorSession &session, SelectionAmou
auto *title = new QLabel(QStringLiteral("Amount"), this);
title->setMinimumWidth(60);
title->setBuddy(m_slider);
new NumericScrub(title, {.sensitivity = 1, .low = 1, .high = double(m_maximum), .step = 1, .value = [this] { return double(amount().value_or(1)); },
.set = [this](double value) { m_input->setText(QString::number(int(value))); }});
m_slider->setObjectName(QStringLiteral("amountSlider"));
m_slider->setRange(1, m_maximum);
m_input->setObjectName(QStringLiteral("amountField"));
+6 -2
View File
@@ -18,14 +18,14 @@ public:
SelectionAmountField(EditorSession &session, int low, int high, std::function<double()> value, std::function<void(double)> change,
QWidget *parent = nullptr);
void sync(int amount);
// The session's number, over any typing: a step or scrub.
void showValue();
protected:
void keyPressEvent(QKeyEvent *event) override;
void focusOutEvent(QFocusEvent *event) override;
private:
void apply();
EditorSession &m_session;
const int m_low;
const int m_high;
@@ -80,10 +80,14 @@ private:
QCheckBox *const m_antialias;
QPushButton *const m_expand;
SelectionAmountField *const m_expandAmount;
// The units scrub their amounts, Swift's `unitSuffix`.
QLabel *const m_expandUnit;
QPushButton *const m_contract;
SelectionAmountField *const m_contractAmount;
QLabel *const m_contractUnit;
QPushButton *const m_feather;
SelectionAmountField *const m_featherAmount;
QLabel *const m_featherUnit;
QLabel *const m_empty;
QPushButton *const m_deselect;
};
+13 -1
View File
@@ -1,5 +1,6 @@
#include "UI/LayerAppearanceControls.h"
#include "UI/BlendModePicker.h"
#include "UI/NumericScrub.h"
#include <QApplication>
#include <QHBoxLayout>
#include <QKeyEvent>
@@ -34,7 +35,16 @@ LayerAppearanceControls::LayerAppearanceControls(EditorSession &session, QWidget
column->addLayout(blend);
auto *opacity = new QHBoxLayout;
opacity->setSpacing(6);
opacity->addWidget(caption(QStringLiteral("Opacity"), this));
QLabel *name = caption(QStringLiteral("Opacity"), this);
// A scrub is one opacity drag, as the slider's.
m_scrub = new NumericScrub(name, {.sensitivity = 1, .low = 0, .high = 100, .step = std::nullopt,
.value = [this] {
const std::optional<ImageLayer> active = m_session.activeLayer();
return (active ? active->opacity : 1) * 100;
},
.set = [this](double percent) { step(percent); }, .onStart = [this] { m_session.beginOpacityEdit(); },
.onEnd = [this] { m_session.finishOpacityEdit(); }});
opacity->addWidget(name);
// A thousandth a step; a drag is one undo step.
m_slider->setRange(0, 1000);
m_slider->setObjectName(QStringLiteral("opacitySlider"));
@@ -87,6 +97,8 @@ void LayerAppearanceControls::synchronize()
if (m_layerID != m_session.activeLayerID()) {
// Leaving applies to the old layer only; it is gone.
m_percentage->clearFocus();
// Swift's `.id` drops a scrub under way.
m_scrub->end();
m_layerID = m_session.activeLayerID();
sync();
} else if (!m_percentage->hasFocus()) {
+3
View File
@@ -5,6 +5,7 @@
#include <QWidget>
class BlendModePicker;
class NumericScrub;
class QLabel;
// The active layer's blend mode and opacity.
@@ -29,6 +30,8 @@ private:
QLabel *m_blendCaption = nullptr;
QSlider *const m_slider;
QLineEdit *const m_percentage;
// The Opacity label's scrub, ended when another layer is active.
NumericScrub *m_scrub = nullptr;
// The layer the controls were last made for.
std::optional<QUuid> m_layerID;
bool m_syncing = false;
+4
View File
@@ -1,4 +1,5 @@
#include "UI/NavigationToolHeader.h"
#include "UI/NumericScrub.h"
#include <QKeyEvent>
#include <QRegularExpression>
#include <cmath>
@@ -12,6 +13,9 @@ NavigationToolHeader::NavigationToolHeader(EditorSession &session, QWidget *pare
m_zoom->setFixedWidth(72);
m_zoom->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
m_zoom->installEventFilter(this);
// Swift's scrubbable unit: a drag sideways zooms.
new NumericScrub(m_unit, {.sensitivity = 1, .low = 0.1, .high = 3200, .step = std::nullopt,
.value = [this] { return m_session.viewport.zoom() * 100; }, .set = [this](double percent) { step(percent); }});
// The unit sits tight against its field: one value.
auto *value = new QHBoxLayout;
value->setSpacing(2);
+76
View File
@@ -0,0 +1,76 @@
#include "UI/NumericScrub.h"
#include <QApplication>
#include <QMouseEvent>
#include <QWidget>
#include <algorithm>
#include <cmath>
NumericScrub::NumericScrub(QWidget *label, Options options) : QObject(label), m_label(label), m_options(std::move(options))
{
// A disabled label shows its parent's cursor, as Qt does.
label->setCursor(Qt::SizeHorCursor);
// Buttonless moves must arrive to end a lost release.
label->setMouseTracking(true);
label->installEventFilter(this);
}
bool NumericScrub::eventFilter(QObject *watched, QEvent *event)
{
const QEvent::Type type = event->type();
// A double click's second press is a press too.
const bool press = type == QEvent::MouseButtonPress || type == QEvent::MouseButtonDblClick;
if (!press && type != QEvent::MouseMove && type != QEvent::MouseButtonRelease)
return false;
const auto *mouse = static_cast<QMouseEvent *>(event);
if (watched != m_label) {
// A popup opened mid-drag takes the release.
if (type == QEvent::MouseButtonRelease && mouse->button() == Qt::LeftButton)
end();
return false;
}
if (press) {
// Filters see a disabled label's presses before Qt drops them.
if (mouse->button() != Qt::LeftButton || !m_label->isEnabled())
return false;
// The label holds the grab: its own points stay exact.
m_press = mouse->position();
qApp->installEventFilter(this);
return true;
}
if (!m_press)
return false;
if (type == QEvent::MouseButtonRelease) {
if (mouse->button() == Qt::LeftButton)
end();
return true;
}
// Qt can lose a release: a buttonless move ends.
if (!mouse->buttons().testFlag(Qt::LeftButton)) {
end();
return true;
}
const QPointF translation = mouse->position() - *m_press;
if (!m_start) {
if (std::hypot(translation.x(), translation.y()) < 1)
return true;
m_start = m_options.value();
m_options.onStart();
}
double proposed = *m_start + translation.x() * m_options.sensitivity;
if (m_options.step && *m_options.step > 0)
proposed = std::round(proposed / *m_options.step) * *m_options.step;
m_options.set(std::min(m_options.high, std::max(m_options.low, proposed)));
return true;
}
void NumericScrub::end()
{
if (!m_press)
return;
qApp->removeEventFilter(this);
m_press.reset();
if (m_start) {
m_start.reset();
m_options.onEnd();
}
}
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include <QObject>
#include <QPointF>
#include <functional>
#include <optional>
class QWidget;
// Swift's NumericScrub: a label dragged sideways changes its value.
class NumericScrub : public QObject {
public:
struct Options {
double sensitivity;
double low;
double high;
// Dragged values snap to multiples of this, when set.
std::optional<double> step;
std::function<double()> value;
std::function<void(double)> set;
std::function<void()> onStart = [] {};
std::function<void()> onEnd = [] {};
};
// Lives as the label's child and filters its events.
NumericScrub(QWidget *label, Options options);
// Ends a drag as a release would.
void end();
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
private:
QWidget *const m_label;
const Options m_options;
// The press, until the release; Swift's gesture begins there.
std::optional<QPointF> m_press;
// Swift's startValue: set once the drag moves a point.
std::optional<double> m_start;
};
+15 -4
View File
@@ -16,10 +16,14 @@ ShapeControls::ShapeControls(EditorSession &session, QWidget *parent)
m_widthSlider(new QSlider(Qt::Horizontal, this)),
m_widthField(new SelectionAmountField(session, 1, 5000, [this] { return m_session.shapeLineWidth(); },
[this](double width) { m_session.setShapeLineWidth(width); }, this)),
m_width(amount(QStringLiteral("Width"), m_widthSlider, m_widthField)), m_radiusSlider(new QSlider(Qt::Horizontal, this)),
m_width(amount(QStringLiteral("Width"), m_widthSlider, m_widthField,
{.sensitivity = 1, .low = 1, .high = 5000, .step = std::nullopt, .value = [this] { return m_session.shapeLineWidth(); },
.set = [this](double width) { m_session.setShapeLineWidth(width); }})), m_radiusSlider(new QSlider(Qt::Horizontal, this)),
m_radiusField(new SelectionAmountField(session, 0, 5000, [this] { return m_session.shapeCornerRadius(); },
[this](double radius) { m_session.setShapeCornerRadius(radius); }, this)),
m_radius(amount(QStringLiteral("Radius"), m_radiusSlider, m_radiusField)),
m_radius(amount(QStringLiteral("Radius"), m_radiusSlider, m_radiusField,
{.sensitivity = 1, .low = 0, .high = 5000, .step = std::nullopt, .value = [this] { return m_session.shapeCornerRadius(); },
.set = [this](double radius) { m_session.setShapeCornerRadius(radius); }})),
m_fill(new SwatchButton([&session] { return session.foregroundColor(); }, 3, 0, 0.5, this))
{
m_widthSlider->setObjectName(QStringLiteral("shapeWidthSlider"));
@@ -65,7 +69,7 @@ QToolButton *ShapeControls::kind(ShapeKind value)
return button;
}
QWidget *ShapeControls::amount(const QString &name, QSlider *slider, SelectionAmountField *field)
QWidget *ShapeControls::amount(const QString &name, QSlider *slider, SelectionAmountField *field, NumericScrub::Options scrub)
{
auto *group = new QWidget(this);
auto *layout = new QHBoxLayout(group);
@@ -73,7 +77,14 @@ QWidget *ShapeControls::amount(const QString &name, QSlider *slider, SelectionAm
layout->setSpacing(6);
slider->setFixedWidth(100);
field->setFixedWidth(48);
for (QWidget *widget : std::initializer_list<QWidget *>{new QLabel(name, group), slider, field, new QLabel(QStringLiteral("px"), group)})
auto *label = new QLabel(name, group);
// A scrub shows its value over the field's typing.
scrub.set = [field, set = std::move(scrub.set)](double value) {
set(value);
field->showValue();
};
new NumericScrub(label, std::move(scrub));
for (QWidget *widget : std::initializer_list<QWidget *>{label, slider, field, new QLabel(QStringLiteral("px"), group)})
layout->addWidget(widget);
return group;
}
+2 -1
View File
@@ -1,5 +1,6 @@
#pragma once
#include "Document/ShapeTool.h"
#include "UI/NumericScrub.h"
#include "UI/ToolHeaderStyle.h"
#include <array>
@@ -19,7 +20,7 @@ public:
private:
QToolButton *kind(ShapeKind value);
// A label, slider, field and "px", as Swift's HStack.
QWidget *amount(const QString &name, QSlider *slider, SelectionAmountField *field);
QWidget *amount(const QString &name, QSlider *slider, SelectionAmountField *field, NumericScrub::Options scrub);
void synchronize();
EditorSession &m_session;
+21 -8
View File
@@ -1,13 +1,14 @@
#include "UI/TransformInspector.h"
#include "UI/KeyboardShortcuts.h"
#include "UI/LayerIcons.h"
#include "UI/NumericScrub.h"
#include <QEvent>
#include <QKeyEvent>
#include <QLabel>
#include <cmath>
TransformValueField::TransformValueField(const QString &label, const QString &suffix, EditorSession &session, std::function<void(double)> change,
QWidget *parent)
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))
{
auto *row = new QHBoxLayout(this);
@@ -16,6 +17,11 @@ TransformValueField::TransformValueField(const QString &label, const QString &su
auto *name = new QLabel(label, this);
name->setFont(ToolHeaderStyle::controlFont());
name->setForegroundRole(QPalette::PlaceholderText);
// A scrub writes over typing; an unfocused field then follows.
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);
}});
row->addWidget(name);
field->setObjectName(QStringLiteral("transform") + label);
field->setAccessibleName(label);
@@ -44,6 +50,11 @@ void TransformValueField::sync(double value)
field->setText(formatted(value));
}
void TransformValueField::endScrub()
{
m_scrub->end();
}
QString TransformValueField::formatted(double value)
{
return std::abs(value - std::round(value)) < 0.005 ? QString::number(std::lround(value)) : QString::number(value, 'f', 2);
@@ -79,15 +90,15 @@ 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(), session, [this](double number) { change([number](LayerTransform &value) { value.origin.setX(number); }); })),
m_y(new TransformValueField(QStringLiteral("Y"), QString(), session, [this](double number) { change([number](LayerTransform &value) { value.origin.setY(number); }); })),
m_width(new TransformValueField(QStringLiteral("W"), QString(), session, [this](double number) { resizeBox(number, true); })),
m_height(new TransformValueField(QStringLiteral("H"), QString(), session, [this](double number) { resizeBox(number, false); })),
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_lock(new QToolButton(this)),
m_scale(new TransformValueField(QStringLiteral("Scale"), QStringLiteral("%"), session, [this](double number) {
m_scale(new TransformValueField(QStringLiteral("Scale"), QStringLiteral("%"), 0.1, 30000, session, [this](double number) {
change([&](LayerTransform &value) { value = value.scaled(number, pixelSize()); });
})),
m_rotation(new TransformValueField(QStringLiteral("°"), QString(), session, [this](double number) {
m_rotation(new TransformValueField(QStringLiteral("°"), QString(), -360, 360, session, [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))
@@ -239,6 +250,8 @@ void TransformInspector::synchronize()
m_layerID = m_session.activeLayerID();
if (QWidget *focused = focusWidget(); focused && isAncestorOf(focused))
focused->clearFocus();
for (TransformValueField *field : {m_x, m_y, m_width, m_height, m_scale, m_rotation})
field->endScrub();
}
title->setText(m_session.transformTargetsMask() ? QStringLiteral("Transform Mask") : QStringLiteral("Transform"));
const auto set = [](QAbstractButton *button, bool checked) {
+8 -2
View File
@@ -9,15 +9,20 @@
#include <QToolButton>
#include <functional>
class NumericScrub;
// A labelled number, typed at once, stepped by arrows.
class TransformValueField : public QWidget {
Q_OBJECT
public:
TransformValueField(const QString &label, const QString &suffix, EditorSession &session, std::function<void(double)> change,
QWidget *parent = nullptr);
// 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);
// Shown when the field is not being edited.
void sync(double value);
// Swift's `.id` drops a scrub under way with its layer.
void endScrub();
// No idle zeros on a whole number, two decimals otherwise.
static QString formatted(double value);
@@ -29,6 +34,7 @@ protected:
private:
EditorSession &m_session;
const std::function<void(double)> m_change;
NumericScrub *m_scrub = nullptr;
double m_value = 0;
// A menu or popup borrows the focus: the typing stays.
bool m_borrowed = false;
+19 -2
View File
@@ -8,6 +8,7 @@
#include <QComboBox>
#include <QFocusEvent>
#include <QHBoxLayout>
#include "UI/NumericScrub.h"
#include <QLabel>
#include <QPainter>
#include <QPushButton>
@@ -61,6 +62,11 @@ void TextStyleField::sync()
setText(m_shown());
}
void TextStyleField::showValue()
{
setText(m_shown());
}
void TextStyleField::keyPressEvent(QKeyEvent *event)
{
// Swift's bar keeps the focus: Return shows the number, selected.
@@ -211,8 +217,19 @@ TypeControls::TypeControls(EditorSession &session, QWidget *parent)
aligned->setSpacing(2);
for (QToolButton *button : m_alignments)
aligned->addWidget(button);
for (QWidget *widget : std::initializer_list<QWidget *>{m_font, m_size, new QLabel(QStringLiteral("px")), m_colour, alignments, new QLabel(QStringLiteral("Tracking")),
m_tracking, new QLabel(QStringLiteral("Leading")), m_leading})
// Swift's scrubbable size unit, tracking and leading: whole numbers.
const auto scrub = [this](const QString &text, TextStyleField *shown, double low, double high, double LayerTextStyle::*key) {
auto *label = new QLabel(text);
new NumericScrub(label, {.sensitivity = 1, .low = low, .high = high, .step = 1,
.value = [this, key] { return m_session.currentTextStyle().*key; }, .set = [this, shown, key](double value) {
m_session.changeTextStyle([&](LayerTextStyle &style) { style.*key = value; });
shown->showValue();
}});
return label;
};
for (QWidget *widget : std::initializer_list<QWidget *>{m_font, m_size, scrub(QStringLiteral("px"), m_size, 1, 2000, &LayerTextStyle::fontSize), m_colour, alignments,
scrub(QStringLiteral("Tracking"), m_tracking, -100, 1000, &LayerTextStyle::tracking), m_tracking,
scrub(QStringLiteral("Leading"), m_leading, 0, 5000, &LayerTextStyle::leading), m_leading})
fields->addWidget(widget);
// Swift's scroll content keeps its size at the leading edge.
fields->addStretch(1);
+2
View File
@@ -20,6 +20,8 @@ public:
QWidget *parent);
// Shows the session's number unless being edited.
void sync();
// The session's number, over any typing: a scrub.
void showValue();
protected:
void keyPressEvent(QKeyEvent *event) override;