Scrub numbers by dragging their labels in the tool bars and layer opacity (12.12c1)
This commit is contained in:
1 parent
f335818e53
commit
5bc7889387
25 files changed
+1126
-32
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user