Scrub numbers by dragging their titles in the sheets and panels (12.12c2)
This commit is contained in:
1 parent
5bc7889387
commit
aa253d9715
25 files changed
+830
-26
No files matched your search
@@ -162,7 +162,7 @@ void CameraRawMixerControls::rebuildFamilies(int tab)
|
||||
};
|
||||
row(m_families,
|
||||
{.name = QStringLiteral("family") + name, .title = name, .help = QStringLiteral("%1 of %2.").arg(tabs.at(size_t(tab)), name),
|
||||
.track = familyTrack(index, tab), .titleWidth = 78, .fixedTitle = true, .fieldWidth = 48},
|
||||
.track = familyTrack(index, tab), .titleWidth = 78, .fixedTitle = true, .fieldWidth = 48, .scrub = 1},
|
||||
[family, index](const CameraRawSettings &settings) {
|
||||
CameraRawSettings copy = settings;
|
||||
return family(copy).at(size_t(index));
|
||||
|
||||
@@ -219,7 +219,7 @@ CameraRawCurveControls::CameraRawCurveControls(EditorSession &session, QWidget *
|
||||
amounts->setContentsMargins(0, 0, 0, 0);
|
||||
amounts->setSpacing(8);
|
||||
const auto amount = [this, amounts](const QString &name, const QString &title, double CameraRawCurveSettings::*key, const QString &help) {
|
||||
auto *row = new CameraRawRow({.name = name, .title = title, .help = help, .titleWidth = 88, .fixedTitle = true, .fieldWidth = 48},
|
||||
auto *row = new CameraRawRow({.name = name, .title = title, .help = help, .titleWidth = 88, .fixedTitle = true, .fieldWidth = 48, .scrub = 1},
|
||||
[this, key] { return raw().curve.*key; },
|
||||
[this, key](double value) { update([key, value](CameraRawSettings &settings) { settings.curve.*key = value; }); },
|
||||
[this, key](double value) { update([key, value](CameraRawSettings &settings) { settings.curve.*key = value; }); },
|
||||
@@ -258,7 +258,7 @@ CameraRawCurveControls::CameraRawCurveControls(EditorSession &session, QWidget *
|
||||
});
|
||||
m_refine = new CameraRawRow({.name = QStringLiteral("refineSaturation"), .title = QStringLiteral("Refine Saturation"),
|
||||
.help = QStringLiteral("How much the RGB curve also changes color strength. Zero keeps it to brightness."), .titleWidth = 88,
|
||||
.fixedTitle = true, .fieldWidth = 48},
|
||||
.fixedTitle = true, .fieldWidth = 48, .scrub = 1},
|
||||
[this] { return raw().curve.refineSaturation; },
|
||||
[this](double value) { update([value](CameraRawSettings &settings) { settings.curve.refineSaturation = value; }); },
|
||||
[this](double value) { update([value](CameraRawSettings &settings) { settings.curve.refineSaturation = value; }); },
|
||||
|
||||
@@ -268,7 +268,7 @@ void CameraRawControls::slider(QVBoxLayout *column, const QString &name, const Q
|
||||
{
|
||||
const double step = std::pow(10.0, decimals);
|
||||
auto *row = new CameraRawRow(
|
||||
{.name = name, .title = title, .help = help, .low = low, .high = high, .decimals = decimals, .track = track, .titleResets = true},
|
||||
{.name = name, .title = title, .help = help, .low = low, .high = high, .decimals = decimals, .track = track, .titleResets = true, .scrub = 1 / step},
|
||||
[this, key] {
|
||||
CameraRawSettings settings = raw();
|
||||
return key(settings);
|
||||
|
||||
@@ -91,7 +91,7 @@ CameraRawDetailControls::CameraRawDetailControls(EditorSession &session, QWidget
|
||||
void CameraRawDetailControls::row(QVBoxLayout *column, const QString &name, const QString &title, double CameraRawDetailSettings::*key,
|
||||
double high, double reset, bool maskingPreview, const QString &help)
|
||||
{
|
||||
auto *row = new CameraRawRow({.name = name, .title = title, .help = help, .low = 0, .high = high, .titleWidth = CameraRawControls::labelWidth},
|
||||
auto *row = new CameraRawRow({.name = name, .title = title, .help = help, .low = 0, .high = high, .titleWidth = CameraRawControls::labelWidth, .scrub = 1},
|
||||
[this, key] { return raw(m_session).detail.*key; },
|
||||
[this, key, maskingPreview](double value) { assign(key, std::round(value), maskingPreview); },
|
||||
[this, key](double value) { assign(key, value, false); }, [this, key, reset] { assign(key, reset, false); });
|
||||
@@ -194,7 +194,7 @@ void CameraRawOpticsControls::row(QVBoxLayout *column, const QString &name, cons
|
||||
double high, double reset, const QString &help)
|
||||
{
|
||||
const auto set = [this, key](double value) { update([key, value](CameraRawOpticsSettings &optics) { optics.*key = value; }); };
|
||||
auto *row = new CameraRawRow({.name = name, .title = title, .help = help, .low = low, .high = high, .titleWidth = CameraRawControls::labelWidth},
|
||||
auto *row = new CameraRawRow({.name = name, .title = title, .help = help, .low = low, .high = high, .titleWidth = CameraRawControls::labelWidth, .scrub = 1},
|
||||
[this, key] { return raw(m_session).optics.*key; },
|
||||
[set, low](double value) { set(low < 0 ? value : std::round(value)); }, set, [set, reset] { set(reset); });
|
||||
column->addWidget(row);
|
||||
|
||||
@@ -55,7 +55,7 @@ void write(EditorSession &session, const std::function<void(CameraRawSettings &)
|
||||
CameraRawRow *rounded(const QString &name, const QString &title, const QString &help, double low, double high, std::function<double()> value,
|
||||
std::function<void(double)> set, QWidget *parent)
|
||||
{
|
||||
return new CameraRawRow({.name = name, .title = title, .help = help, .low = low, .high = high, .titleWidth = CameraRawControls::labelWidth},
|
||||
return new CameraRawRow({.name = name, .title = title, .help = help, .low = low, .high = high, .titleWidth = CameraRawControls::labelWidth, .scrub = 1},
|
||||
std::move(value), [set](double number) { set(std::round(number)); }, set, [set] { set(0); }, parent);
|
||||
}
|
||||
}
|
||||
|
||||
+13
-1
@@ -1,5 +1,6 @@
|
||||
#include "UI/CameraRawRow.h"
|
||||
#include "UI/ColorPickerSheet.h"
|
||||
#include "UI/NumericScrub.h"
|
||||
#include <QEvent>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLocale>
|
||||
@@ -38,6 +39,16 @@ CameraRawRow::CameraRawRow(Spec spec, std::function<double()> value, std::functi
|
||||
row->setSpacing(10);
|
||||
row->addWidget(m_title);
|
||||
row->addWidget(m_slider, 1);
|
||||
// Before the reset's filter, which runs first and passes on.
|
||||
if (m_spec.scrub) {
|
||||
new NumericScrub(m_title, {.sensitivity = *m_spec.scrub, .low = m_spec.low, .high = m_spec.high, .step = std::nullopt, .value = m_value,
|
||||
.set = [this, type](double value) {
|
||||
type(value);
|
||||
if (m_field)
|
||||
m_field->setModified(false);
|
||||
synchronize();
|
||||
}});
|
||||
}
|
||||
if (m_spec.fieldWidth > 0) {
|
||||
m_field = new PickerField([this, type = std::move(type)] {
|
||||
bool number = false;
|
||||
@@ -73,7 +84,8 @@ bool CameraRawRow::eventFilter(QObject *watched, QEvent *event)
|
||||
{
|
||||
if (watched == m_title && event->type() == QEvent::MouseButtonDblClick) {
|
||||
m_reset();
|
||||
return true;
|
||||
// A scrub still takes the second press, as Swift's drag.
|
||||
return !m_spec.scrub;
|
||||
}
|
||||
return QWidget::eventFilter(watched, event);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "UI/CameraRawSlider.h"
|
||||
#include <QWidget>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
|
||||
class PickerField;
|
||||
class QLabel;
|
||||
@@ -25,6 +26,8 @@ public:
|
||||
int fieldWidth = 56;
|
||||
// A double click on the title resets it.
|
||||
bool titleResets = false;
|
||||
// Swift's scrubbable title: a point's worth, through `type`.
|
||||
std::optional<double> scrub = std::nullopt;
|
||||
};
|
||||
// `slide` takes the slider's own value, `type` a typed number.
|
||||
CameraRawRow(Spec spec, std::function<double()> value, std::function<void(double)> slide, std::function<void(double)> type,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "UI/ByteCounts.h"
|
||||
#include "UI/ColorPaletteControls.h"
|
||||
#include "UI/ColorPickerSheet.h"
|
||||
#include "UI/NumericScrub.h"
|
||||
#include <QButtonGroup>
|
||||
#include <QCheckBox>
|
||||
#include <QColorDialog>
|
||||
@@ -22,6 +23,38 @@ const std::array<QString, 9> anchorNames = {QStringLiteral("Top left"), QStr
|
||||
QStringLiteral("Middle left"), QStringLiteral("Center"), QStringLiteral("Middle right"),
|
||||
QStringLiteral("Bottom left"), QStringLiteral("Bottom center"), QStringLiteral("Bottom right")};
|
||||
|
||||
// Swift's scrubRange: the pixel limits, shown in the draft's unit.
|
||||
std::pair<double, double> scrubRange(const CanvasSizeDraft &draft, bool widthAxis)
|
||||
{
|
||||
const double original = double(widthAxis ? draft.originalWidth : draft.originalHeight);
|
||||
const double other = double(widthAxis ? draft.originalHeight : draft.originalWidth);
|
||||
const double lower = draft.locked ? std::max(1.0, original / other) : 1.0;
|
||||
const double upper = draft.locked ? std::min(30000.0, 30000 * original / other) : 30000.0;
|
||||
const auto displayed = [&](double pixels) {
|
||||
const double difference = pixels - (draft.relative ? original : 0);
|
||||
switch (draft.unit) {
|
||||
case CanvasUnit::pixels: return difference;
|
||||
case CanvasUnit::percent: return difference / original * 100;
|
||||
case CanvasUnit::inches: return difference / draft.resolution;
|
||||
case CanvasUnit::centimeters: return difference / draft.resolution * 2.54;
|
||||
}
|
||||
throw std::logic_error("no such unit");
|
||||
};
|
||||
return {displayed(lower), displayed(upper)};
|
||||
}
|
||||
|
||||
// Swift's scrubSensitivity: a point is a pixel in any unit.
|
||||
double scrubSensitivity(const CanvasSizeDraft &draft, bool widthAxis)
|
||||
{
|
||||
switch (draft.unit) {
|
||||
case CanvasUnit::pixels: return 1;
|
||||
case CanvasUnit::percent: return 100 / double(widthAxis ? draft.originalWidth : draft.originalHeight);
|
||||
case CanvasUnit::inches: return 1 / draft.resolution;
|
||||
case CanvasUnit::centimeters: return 2.54 / draft.resolution;
|
||||
}
|
||||
throw std::logic_error("no such unit");
|
||||
}
|
||||
|
||||
QLabel *text(const QString &words, int pixels, QFont::Weight weight, QPalette::ColorRole role, QWidget *parent)
|
||||
{
|
||||
auto *label = new QLabel(words, parent);
|
||||
@@ -117,9 +150,12 @@ CanvasSizeSheet::CanvasSizeSheet(const CanvasDocument &document, PaletteColor fo
|
||||
});
|
||||
fields->addWidget(unitsLabel, 0, 0);
|
||||
fields->addWidget(units, 0, 1);
|
||||
fields->addWidget(new QLabel(QStringLiteral("Width"), this), 1, 0);
|
||||
auto *width = new QLabel(QStringLiteral("Width"), this);
|
||||
auto *height = new QLabel(QStringLiteral("Height"), this);
|
||||
m_scrubs = {scrub(width, true), scrub(height, false)};
|
||||
fields->addWidget(width, 1, 0);
|
||||
fields->addWidget(m_width, 1, 1);
|
||||
fields->addWidget(new QLabel(QStringLiteral("Height"), this), 2, 0);
|
||||
fields->addWidget(height, 2, 0);
|
||||
fields->addWidget(m_height, 2, 1);
|
||||
column->addLayout(fields);
|
||||
|
||||
@@ -240,6 +276,17 @@ PickerField *CanvasSizeSheet::dimension(bool widthAxis)
|
||||
return field;
|
||||
}
|
||||
|
||||
NumericScrub *CanvasSizeSheet::scrub(QLabel *title, bool widthAxis)
|
||||
{
|
||||
// Dragged sizes snap to whole units; synchronize sets the limits.
|
||||
return new NumericScrub(title, {.sensitivity = 1, .low = 0, .high = 0, .step = 1, .value = [this, widthAxis] { return m_draft.displayed(widthAxis); },
|
||||
.set = [this, widthAxis](double value) {
|
||||
m_draft.set(value, widthAxis);
|
||||
(widthAxis ? m_width : m_height)->setModified(false);
|
||||
synchronize();
|
||||
}});
|
||||
}
|
||||
|
||||
std::optional<CanvasExtensionColor> CanvasSizeSheet::fill() const
|
||||
{
|
||||
const auto colour = [](PaletteColor value) { return CanvasExtensionColor{value.red, value.green, value.blue}; };
|
||||
@@ -272,6 +319,8 @@ void CanvasSizeSheet::synchronize()
|
||||
{
|
||||
for (const auto &[field, widthAxis] : {std::pair(m_width, true), std::pair(m_height, false)}) {
|
||||
// A field being typed in keeps its typing.
|
||||
const auto [low, high] = scrubRange(m_draft, widthAxis);
|
||||
m_scrubs[widthAxis ? 0 : 1]->reshape(scrubSensitivity(m_draft, widthAxis), low, high);
|
||||
const QString number = shown(m_draft.displayed(widthAxis), field->locale());
|
||||
if (!field->isModified() && field->text() != number)
|
||||
field->setText(number);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <array>
|
||||
#include <functional>
|
||||
|
||||
class NumericScrub;
|
||||
class PickerField;
|
||||
class QButtonGroup;
|
||||
class QComboBox;
|
||||
@@ -22,6 +23,8 @@ public:
|
||||
|
||||
private:
|
||||
PickerField *dimension(bool widthAxis);
|
||||
// Swift's scrubbable Width or Height title.
|
||||
NumericScrub *scrub(QLabel *title, bool widthAxis);
|
||||
std::optional<CanvasExtensionColor> fill() const;
|
||||
void pickCustomColor();
|
||||
void synchronize();
|
||||
@@ -34,6 +37,8 @@ private:
|
||||
PaletteColor m_custom = PaletteColor::white();
|
||||
PickerField *const m_width;
|
||||
PickerField *const m_height;
|
||||
// Their limits follow the unit, Relative and Lock.
|
||||
std::array<NumericScrub *, 2> m_scrubs{};
|
||||
QLabel *const m_note;
|
||||
QButtonGroup *const m_anchors;
|
||||
QLabel *const m_anchorName;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "UI/ColorPickerSheet.h"
|
||||
#include "UI/NumericScrub.h"
|
||||
#include "UI/KeyboardShortcuts.h"
|
||||
#include <QFontDatabase>
|
||||
#include <QGridLayout>
|
||||
@@ -205,7 +206,19 @@ ColorPickerSheet::ColorPickerSheet(EditorSession &session, std::function<void(bo
|
||||
fields->setHorizontalSpacing(8);
|
||||
fields->setVerticalSpacing(6);
|
||||
for (int index = 0; index < 3; ++index) {
|
||||
fields->addWidget(label(QString(QStringLiteral("RGB").at(index)), this), index, 0);
|
||||
QLabel *name = label(QString(QStringLiteral("RGB").at(index)), this);
|
||||
// Swift's scrubbable channel: whole levels, replacing typing.
|
||||
new NumericScrub(name, {.sensitivity = 1, .low = 0, .high = 255, .step = 1,
|
||||
.value = [this, index] {
|
||||
const PaletteColor color = m_session.colorPicker().value().color();
|
||||
return std::round((index == 0 ? color.red : index == 1 ? color.green : color.blue) * 255);
|
||||
},
|
||||
.set = [this, index](double value) {
|
||||
setChannel(index, value);
|
||||
m_channels[size_t(index)]->setModified(false);
|
||||
synchronize();
|
||||
}});
|
||||
fields->addWidget(name, index, 0);
|
||||
fields->addWidget(m_channels[size_t(index)], index, 1, Qt::AlignLeft);
|
||||
}
|
||||
m_hex->setObjectName(QStringLiteral("hex"));
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "UI/ColorPickerSheet.h"
|
||||
#include <QButtonGroup>
|
||||
#include <QHBoxLayout>
|
||||
#include "UI/NumericScrub.h"
|
||||
#include <QLabel>
|
||||
#include <QLocale>
|
||||
#include <QPushButton>
|
||||
@@ -195,6 +196,15 @@ void EffectsSheet::slider(const QString &title, std::function<std::optional<doub
|
||||
slider->setRange(0, travel);
|
||||
slider->setFixedWidth(130);
|
||||
label->setBuddy(slider);
|
||||
// Swift's scrubbable title, over the range typing reaches.
|
||||
new NumericScrub(label, {.sensitivity = 1, .low = low, .high = typedHigh.value_or(high), .step = std::nullopt,
|
||||
.value = [this, index] { return m_sliders[index].value(m_session.editingEffects()).value_or(m_sliders[index].low); },
|
||||
.set = [this, index](double amount) {
|
||||
const Slider &at = m_sliders[index];
|
||||
apply(at, amount);
|
||||
at.field->setModified(false);
|
||||
synchronize();
|
||||
}});
|
||||
connect(slider, &QSlider::valueChanged, this, [this, index](int position) {
|
||||
const Slider &at = m_sliders[index];
|
||||
apply(at, at.low + (at.high - at.low) * position / travel);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "UI/FilterSheet.h"
|
||||
#include "UI/NumericScrub.h"
|
||||
#include "UI/KeyboardShortcuts.h"
|
||||
#include "UI/ColorPaletteControls.h"
|
||||
#include "UI/ColorPickerSheet.h"
|
||||
@@ -257,6 +258,17 @@ void FilterSheet::control(const QString &title, std::function<double &(FilterSet
|
||||
auto *label = new QLabel(title, box);
|
||||
label->setMinimumWidth(60);
|
||||
label->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
|
||||
// Swift's scrubbable title: a point is the field's last decimal.
|
||||
new NumericScrub(label, {.sensitivity = 1 / std::pow(10.0, decimals), .low = low, .high = high, .step = std::nullopt,
|
||||
.value = [this, index] {
|
||||
FilterSettings current = settings();
|
||||
return m_controls[index].key(current);
|
||||
},
|
||||
.set = [this, index](double value) {
|
||||
update([this, index, value](FilterSettings &settings) { m_controls[index].key(settings) = value; });
|
||||
m_controls[index].field->setModified(false);
|
||||
synchronize();
|
||||
}});
|
||||
auto *slider = new QSlider(Qt::Horizontal, box);
|
||||
slider->setObjectName(name + QStringLiteral("Slider"));
|
||||
slider->setRange(0, travel);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "UI/KeyboardShortcuts.h"
|
||||
#include "Rendering/EyedropperIcon.h"
|
||||
#include "UI/ColorPickerSheet.h"
|
||||
#include "UI/NumericScrub.h"
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QFrame>
|
||||
@@ -373,6 +374,14 @@ QWidget *HueSaturationSheet::row(size_t index)
|
||||
layout->setSpacing(10);
|
||||
auto *label = new QLabel(title, box);
|
||||
label->setFixedWidth(76);
|
||||
// Swift's scrubbable title; synchronize sets its range.
|
||||
m_scrubs[index] = new NumericScrub(label, {.sensitivity = 1, .low = 0, .high = 0, .step = std::nullopt,
|
||||
.value = [this, index] { return (current(m_session).*rows[index].value)(); },
|
||||
.set = [this, index](double value) {
|
||||
change([&](HueSaturationSettings &settings) { (settings.*rows[index].set)(value); });
|
||||
m_fields[index]->setModified(false);
|
||||
synchronize();
|
||||
}});
|
||||
auto *slider = new QSlider(Qt::Horizontal, box);
|
||||
slider->setObjectName(title.toLower() + QStringLiteral("Slider"));
|
||||
label->setBuddy(slider);
|
||||
@@ -443,6 +452,7 @@ void HueSaturationSheet::synchronize()
|
||||
m_targeting->setChecked(m_session.hueTargeting());
|
||||
for (size_t index = 0; index < rows.size(); ++index) {
|
||||
const auto [low, high] = bounds(index, settings.colorize);
|
||||
m_scrubs[index]->reshape(1, low, high);
|
||||
const double value = (settings.*rows[index].value)();
|
||||
{
|
||||
// The sliders show the session's numbers without writing back.
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
|
||||
class NumericScrub;
|
||||
class PickerField;
|
||||
class QCheckBox;
|
||||
class QComboBox;
|
||||
@@ -55,6 +56,8 @@ private:
|
||||
// Hue, Saturation, Lightness; the rows make them.
|
||||
std::array<QSlider *, 3> m_sliders{};
|
||||
std::array<PickerField *, 3> m_fields{};
|
||||
// Each title's scrub; Colorize changes its range.
|
||||
std::array<NumericScrub *, 3> m_scrubs{};
|
||||
const std::array<QWidget *, 3> m_rows;
|
||||
SpectrumEditor *const m_spectrum;
|
||||
QCheckBox *const m_invert;
|
||||
|
||||
+77
-11
@@ -2,6 +2,7 @@
|
||||
#include "Document/DocumentLimits.h"
|
||||
#include "UI/KeyboardShortcuts.h"
|
||||
#include "UI/ColorPickerSheet.h"
|
||||
#include "UI/NumericScrub.h"
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QGridLayout>
|
||||
@@ -53,14 +54,8 @@ ImageSizeSheet::ImageSizeSheet(const CanvasDocument &document, std::function<voi
|
||||
m_resolutionField(new PickerField([this] {
|
||||
bool number = false;
|
||||
const double typed = m_resolutionField->locale().toDouble(m_resolutionField->text(), &number);
|
||||
if (m_resolutionField->isModified() && number) {
|
||||
// Swift's onChange: a physical size keeps, its pixels scale.
|
||||
const double old = std::exchange(m_resolution, typed);
|
||||
if (m_resample && physical(m_unit) && old > 0 && typed > 0 && std::isfinite(typed)) {
|
||||
m_width *= typed / old;
|
||||
m_height *= typed / old;
|
||||
}
|
||||
}
|
||||
if (m_resolutionField->isModified() && number)
|
||||
setResolution(typed);
|
||||
m_resolutionField->setModified(false);
|
||||
synchronize();
|
||||
}, nullptr, this)),
|
||||
@@ -91,9 +86,11 @@ ImageSizeSheet::ImageSizeSheet(const CanvasDocument &document, std::function<voi
|
||||
});
|
||||
fields->addWidget(units, 0, 0);
|
||||
fields->addWidget(m_units, 0, 1);
|
||||
fields->addWidget(new QLabel(QStringLiteral("Width"), this), 1, 0);
|
||||
m_titles = {new QLabel(QStringLiteral("Width"), this), new QLabel(QStringLiteral("Height"), this)};
|
||||
m_scrubs = {scrub(m_titles[0], true), scrub(m_titles[1], false)};
|
||||
fields->addWidget(m_titles[0], 1, 0);
|
||||
fields->addWidget(m_widthField, 1, 1);
|
||||
fields->addWidget(new QLabel(QStringLiteral("Height"), this), 2, 0);
|
||||
fields->addWidget(m_titles[1], 2, 0);
|
||||
fields->addWidget(m_heightField, 2, 1);
|
||||
column->addLayout(fields);
|
||||
|
||||
@@ -104,7 +101,15 @@ ImageSizeSheet::ImageSizeSheet(const CanvasDocument &document, std::function<voi
|
||||
});
|
||||
column->addWidget(m_lock);
|
||||
auto *resolution = new QHBoxLayout;
|
||||
resolution->addWidget(new QLabel(QStringLiteral("Resolution"), this));
|
||||
auto *resolutionTitle = new QLabel(QStringLiteral("Resolution"), this);
|
||||
// Swift's scrubbable Resolution: whole pixels per inch.
|
||||
new NumericScrub(resolutionTitle, {.sensitivity = 1, .low = 1, .high = 9600, .step = 1, .value = [this] { return m_resolution; },
|
||||
.set = [this](double value) {
|
||||
setResolution(value);
|
||||
m_resolutionField->setModified(false);
|
||||
synchronize();
|
||||
}});
|
||||
resolution->addWidget(resolutionTitle);
|
||||
m_resolutionField->setObjectName(QStringLiteral("imageResolution"));
|
||||
m_resolutionField->setPlaceholderText(QStringLiteral("Resolution"));
|
||||
m_resolutionField->setAccessibleName(QStringLiteral("Resolution"));
|
||||
@@ -223,6 +228,61 @@ void ImageSizeSheet::setDimension(double value, bool isWidth)
|
||||
}
|
||||
}
|
||||
|
||||
void ImageSizeSheet::setResolution(double value)
|
||||
{
|
||||
const double old = std::exchange(m_resolution, value);
|
||||
if (m_resample && physical(m_unit) && old > 0 && value > 0 && std::isfinite(value)) {
|
||||
m_width *= value / old;
|
||||
m_height *= value / old;
|
||||
}
|
||||
}
|
||||
|
||||
NumericScrub *ImageSizeSheet::scrub(QLabel *title, bool isWidth)
|
||||
{
|
||||
// Whole units; synchronize sets the limits and the gate.
|
||||
return new NumericScrub(title, {.sensitivity = 0, .low = 0, .high = 0, .step = 1,
|
||||
.value = [this, isWidth] { return display(isWidth ? m_width : m_height, isWidth ? m_originalWidth : m_originalHeight); },
|
||||
.set = [this, isWidth](double value) {
|
||||
setDimension(value, isWidth);
|
||||
(isWidth ? m_widthField : m_heightField)->setModified(false);
|
||||
synchronize();
|
||||
}});
|
||||
}
|
||||
|
||||
bool ImageSizeSheet::canScrubDimensions() const
|
||||
{
|
||||
return !physical(m_unit) || (std::isfinite(m_resolution) && m_resolution > 0);
|
||||
}
|
||||
|
||||
// Swift's scrubRange: pixel limits, or print sizes without resampling.
|
||||
std::pair<double, double> ImageSizeSheet::scrubRange(bool isWidth) const
|
||||
{
|
||||
const double pixels = isWidth ? m_width : m_height, other = isWidth ? m_height : m_width;
|
||||
if (!m_resample) {
|
||||
const double multiplier = m_unit == QLatin1String("Centimeters") ? 2.54 : 1;
|
||||
return {pixels * multiplier / 9600, pixels * multiplier};
|
||||
}
|
||||
const double minimum = m_locked ? std::max(1.0, pixels / other) : 1.0;
|
||||
const double dimensionLimit = m_locked ? std::min(30000.0, 30000 * pixels / other) : 30000.0;
|
||||
const double areaLimit = m_locked ? std::sqrt(100'000'000 * pixels / other) : 100'000'000 / other;
|
||||
const double maximum = std::max(minimum, std::min(dimensionLimit, areaLimit));
|
||||
const qint64 original = isWidth ? m_originalWidth : m_originalHeight;
|
||||
return {display(minimum, original), display(maximum, original)};
|
||||
}
|
||||
|
||||
double ImageSizeSheet::scrubSensitivity(bool isWidth) const
|
||||
{
|
||||
if (!m_resample)
|
||||
return m_unit == QLatin1String("Centimeters") ? 0.0254 : 0.01;
|
||||
if (m_unit == QLatin1String("Percent"))
|
||||
return 100 / double(isWidth ? m_originalWidth : m_originalHeight);
|
||||
if (m_unit == QLatin1String("Inches"))
|
||||
return 1 / m_resolution;
|
||||
if (m_unit == QLatin1String("Centimeters"))
|
||||
return 2.54 / m_resolution;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Swift's onChange: without resampling the pixels stay the document's.
|
||||
void ImageSizeSheet::setResample(bool enabled)
|
||||
{
|
||||
@@ -252,6 +312,12 @@ void ImageSizeSheet::synchronize()
|
||||
}
|
||||
m_units->setCurrentText(m_unit);
|
||||
for (const auto &[field, isWidth] : {std::pair(m_widthField, true), std::pair(m_heightField, false)}) {
|
||||
const bool scrubs = canScrubDimensions();
|
||||
m_titles[isWidth ? 0 : 1]->setEnabled(scrubs);
|
||||
if (scrubs) {
|
||||
const auto [low, high] = scrubRange(isWidth);
|
||||
m_scrubs[isWidth ? 0 : 1]->reshape(scrubSensitivity(isWidth), low, high);
|
||||
}
|
||||
// A field being typed in keeps its typing.
|
||||
const QString number = shown(display(isWidth ? m_width : m_height, isWidth ? m_originalWidth : m_originalHeight), field->locale());
|
||||
if (!field->isModified() && field->text() != number)
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
#include "Document/EditorSession+Model.h"
|
||||
#include "IO/ImageResizer.h"
|
||||
#include <QWidget>
|
||||
#include <array>
|
||||
#include <functional>
|
||||
|
||||
class NumericScrub;
|
||||
class PickerField;
|
||||
class QCheckBox;
|
||||
class QComboBox;
|
||||
@@ -21,6 +23,13 @@ private:
|
||||
double display(double pixels, qint64 original) const;
|
||||
PickerField *dimension(bool isWidth);
|
||||
void setDimension(double value, bool isWidth);
|
||||
// Swift's onChange: a physical size keeps, its pixels scale.
|
||||
void setResolution(double value);
|
||||
// Swift's scrubbable Width or Height title and its limits.
|
||||
NumericScrub *scrub(QLabel *title, bool isWidth);
|
||||
bool canScrubDimensions() const;
|
||||
std::pair<double, double> scrubRange(bool isWidth) const;
|
||||
double scrubSensitivity(bool isWidth) const;
|
||||
void setResample(bool enabled);
|
||||
void synchronize();
|
||||
|
||||
@@ -37,6 +46,8 @@ private:
|
||||
QComboBox *const m_units;
|
||||
PickerField *const m_widthField;
|
||||
PickerField *const m_heightField;
|
||||
std::array<QLabel *, 2> m_titles{};
|
||||
std::array<NumericScrub *, 2> m_scrubs{};
|
||||
QCheckBox *const m_lock;
|
||||
PickerField *const m_resolutionField;
|
||||
QWidget *const m_samplingRow;
|
||||
|
||||
+19
-1
@@ -1,4 +1,5 @@
|
||||
#include "UI/LevelsSheet.h"
|
||||
#include "UI/NumericScrub.h"
|
||||
#include "UI/KeyboardShortcuts.h"
|
||||
#include "Rendering/EyedropperIcon.h"
|
||||
#include <QEvent>
|
||||
@@ -327,7 +328,24 @@ PickerField *LevelsSheet::field(size_t index)
|
||||
auto *layout = new QVBoxLayout(box);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(5);
|
||||
layout->addWidget(caption(name, box));
|
||||
QLabel *title = caption(name, box);
|
||||
layout->addWidget(title);
|
||||
// Swift's scrubbable caption, over gamma's or a tone's range.
|
||||
const bool gamma = entries[index].decimals > 0;
|
||||
new NumericScrub(title, {.sensitivity = gamma ? 0.01 : 1, .low = gamma ? 0.1 : 0, .high = gamma ? 9.99 : 255, .step = std::nullopt,
|
||||
.value = [this, index] {
|
||||
const std::optional<LevelsEdit> &edit = m_session.levels();
|
||||
return edit ? edit->settings.current().*entries[index].value : 0.0;
|
||||
},
|
||||
.set = [this, index](double value) {
|
||||
change([&](LevelsSettings &settings) {
|
||||
LevelRange range = settings.current();
|
||||
range.*entries[index].value = value;
|
||||
settings.setCurrent(range);
|
||||
});
|
||||
m_fields[index]->setModified(false);
|
||||
synchronize();
|
||||
}});
|
||||
// A number in the user's locale applies; other text reverts.
|
||||
auto *entry = new PickerField([this, index] {
|
||||
PickerField &edited = *m_fields[index];
|
||||
|
||||
@@ -63,6 +63,13 @@ bool NumericScrub::eventFilter(QObject *watched, QEvent *event)
|
||||
return true;
|
||||
}
|
||||
|
||||
void NumericScrub::reshape(double sensitivity, double low, double high)
|
||||
{
|
||||
m_options.sensitivity = sensitivity;
|
||||
m_options.low = low;
|
||||
m_options.high = high;
|
||||
}
|
||||
|
||||
void NumericScrub::end()
|
||||
{
|
||||
if (!m_press)
|
||||
|
||||
@@ -25,13 +25,15 @@ public:
|
||||
NumericScrub(QWidget *label, Options options);
|
||||
// Ends a drag as a release would.
|
||||
void end();
|
||||
// New limits, as SwiftUI's next render hands the modifier.
|
||||
void reshape(double sensitivity, double low, double high);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
|
||||
private:
|
||||
QWidget *const m_label;
|
||||
const Options m_options;
|
||||
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.
|
||||
|
||||
Reference in new issue
Block a user