Coloured slider tracks with double-click reset, aligned filter titles; entries release focus before their owners go (12.14c)
This commit is contained in:
1 parent
2183348aa6
commit
2fa8d9921d
39 files changed
+1387
-311
No files matched your search
@@ -1,4 +1,5 @@
|
||||
#include "Rendering/EyedropperIcon.h"
|
||||
#include "UI/ColorPickerSheet.h"
|
||||
#include "UI/CameraRawColorControls.h"
|
||||
#include "UI/LayerIcons.h"
|
||||
#include <QAbstractButton>
|
||||
@@ -315,3 +316,8 @@ void CameraRawMixerControls::synchronize()
|
||||
m_target->setChecked(shown.targetsMixer);
|
||||
m_target->setIcon(QIcon(LayerIcons::pixmap(LayerIcon::scope, 14, palette().color(QPalette::ButtonText), devicePixelRatioF())));
|
||||
}
|
||||
|
||||
CameraRawMixerControls::~CameraRawMixerControls()
|
||||
{
|
||||
releaseFocus(*this);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "UI/CameraRawColorControls.h"
|
||||
#include "UI/ColorPickerSheet.h"
|
||||
#include "UI/CameraRawRow.h"
|
||||
#include "UI/LayerIcons.h"
|
||||
#include <QButtonGroup>
|
||||
@@ -353,3 +354,8 @@ void CameraRawCurveControls::synchronize()
|
||||
m_target->setChecked(edit && edit->rawPanel.targetsCurve);
|
||||
m_target->setIcon(QIcon(LayerIcons::pixmap(LayerIcon::scope, 14, palette().color(QPalette::ButtonText), devicePixelRatioF())));
|
||||
}
|
||||
|
||||
CameraRawCurveControls::~CameraRawCurveControls()
|
||||
{
|
||||
releaseFocus(*this);
|
||||
}
|
||||
@@ -19,6 +19,7 @@ class CameraRawCurveControls : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit CameraRawCurveControls(EditorSession &session, QWidget *parent = nullptr);
|
||||
~CameraRawCurveControls() override;
|
||||
void synchronize();
|
||||
|
||||
private:
|
||||
@@ -48,6 +49,7 @@ class CameraRawMixerControls : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit CameraRawMixerControls(EditorSession &session, QWidget *parent = nullptr);
|
||||
~CameraRawMixerControls() override;
|
||||
void synchronize();
|
||||
|
||||
private:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "UI/CameraRawControls.h"
|
||||
#include "UI/ColorPickerSheet.h"
|
||||
#include "UI/CameraRawColorControls.h"
|
||||
#include "UI/CameraRawDetailOpticsControls.h"
|
||||
#include "UI/CameraRawGeometryCalibrationControls.h"
|
||||
@@ -400,3 +401,8 @@ void CameraRawControls::synchronize()
|
||||
m_vignetteStyle->setCurrentIndex(int(settings.vignetteStyle));
|
||||
}
|
||||
}
|
||||
|
||||
CameraRawControls::~CameraRawControls()
|
||||
{
|
||||
releaseFocus(*this);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ class CameraRawControls : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit CameraRawControls(EditorSession &session, QWidget *parent = nullptr);
|
||||
~CameraRawControls() override;
|
||||
// Shows the edit as it now stands.
|
||||
void synchronize();
|
||||
static constexpr int labelWidth = 96;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "UI/CameraRawDetailOpticsControls.h"
|
||||
#include "UI/ColorPickerSheet.h"
|
||||
#include "Rendering/EyedropperIcon.h"
|
||||
#include "UI/CameraRawControls.h"
|
||||
#include <QCheckBox>
|
||||
@@ -252,3 +253,13 @@ void CameraRawOpticsControls::synchronize()
|
||||
for (const auto &[slider, key] : m_hueSliders)
|
||||
slider->display(optics.*key);
|
||||
}
|
||||
|
||||
CameraRawDetailControls::~CameraRawDetailControls()
|
||||
{
|
||||
releaseFocus(*this);
|
||||
}
|
||||
|
||||
CameraRawOpticsControls::~CameraRawOpticsControls()
|
||||
{
|
||||
releaseFocus(*this);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ class CameraRawDetailControls : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit CameraRawDetailControls(EditorSession &session, QWidget *parent = nullptr);
|
||||
~CameraRawDetailControls() override;
|
||||
void synchronize();
|
||||
|
||||
private:
|
||||
@@ -32,6 +33,7 @@ class CameraRawOpticsControls : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit CameraRawOpticsControls(EditorSession &session, QWidget *parent = nullptr);
|
||||
~CameraRawOpticsControls() override;
|
||||
void synchronize();
|
||||
|
||||
private:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "UI/CameraRawGeometryCalibrationControls.h"
|
||||
#include "UI/ColorPickerSheet.h"
|
||||
#include "UI/CameraRawColorControls.h"
|
||||
#include "UI/CameraRawControls.h"
|
||||
#include "UI/LayerIcons.h"
|
||||
@@ -213,3 +214,13 @@ void CameraRawCalibrationControls::synchronize()
|
||||
for (CameraRawRow *row : m_rows)
|
||||
row->synchronize();
|
||||
}
|
||||
|
||||
CameraRawGeometryControls::~CameraRawGeometryControls()
|
||||
{
|
||||
releaseFocus(*this);
|
||||
}
|
||||
|
||||
CameraRawCalibrationControls::~CameraRawCalibrationControls()
|
||||
{
|
||||
releaseFocus(*this);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ class CameraRawGeometryControls : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit CameraRawGeometryControls(EditorSession &session, QWidget *parent = nullptr);
|
||||
~CameraRawGeometryControls() override;
|
||||
void synchronize();
|
||||
|
||||
private:
|
||||
@@ -35,6 +36,7 @@ class CameraRawCalibrationControls : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit CameraRawCalibrationControls(EditorSession &session, QWidget *parent = nullptr);
|
||||
~CameraRawCalibrationControls() override;
|
||||
void synchronize();
|
||||
|
||||
private:
|
||||
|
||||
@@ -89,3 +89,8 @@ bool CameraRawRow::eventFilter(QObject *watched, QEvent *event)
|
||||
}
|
||||
return QWidget::eventFilter(watched, event);
|
||||
}
|
||||
|
||||
CameraRawRow::~CameraRawRow()
|
||||
{
|
||||
releaseFocus(*this);
|
||||
}
|
||||
@@ -32,6 +32,7 @@ public:
|
||||
// `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,
|
||||
std::function<void()> reset, QWidget *parent = nullptr);
|
||||
~CameraRawRow() override;
|
||||
// Shows the value; a focused field keeps its typing.
|
||||
void synchronize();
|
||||
|
||||
|
||||
+27
-10
@@ -17,16 +17,24 @@ QColor hsb(double degrees, double saturation, double brightness)
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::pair<QColor, QColor>> CameraRawSliderTrack::colors() const
|
||||
std::optional<std::vector<QColor>> CameraRawSliderTrack::colors() const
|
||||
{
|
||||
switch (kind) {
|
||||
case Kind::plain: return std::nullopt;
|
||||
case Kind::temperature: return std::pair(QColor::fromRgbF(0.22f, 0.46f, 0.95f), QColor::fromRgbF(0.98f, 0.82f, 0.18f));
|
||||
case Kind::tint: return std::pair(QColor::fromRgbF(0.28f, 0.70f, 0.34f), QColor::fromRgbF(0.70f, 0.40f, 0.64f));
|
||||
case Kind::chroma: return std::pair(QColor::fromRgbF(0.62f, 0.62f, 0.64f), QColor::fromRgbF(0.86f, 0.18f, 0.20f));
|
||||
case Kind::hue: return std::pair(hsb(degrees - 50, 0.85, 0.9), hsb(degrees + 50, 0.85, 0.9));
|
||||
case Kind::saturation: return std::pair(QColor::fromRgbF(0.55f, 0.55f, 0.56f), hsb(degrees, 0.9, 0.9));
|
||||
case Kind::luminance: return std::pair(hsb(degrees, 0.55, 0.18), hsb(degrees, 0.35, 0.95));
|
||||
case Kind::temperature: return std::vector{QColor::fromRgbF(0.22f, 0.46f, 0.95f), QColor::fromRgbF(0.98f, 0.82f, 0.18f)};
|
||||
case Kind::tint: return std::vector{QColor::fromRgbF(0.28f, 0.70f, 0.34f), QColor::fromRgbF(0.70f, 0.40f, 0.64f)};
|
||||
case Kind::chroma: return std::vector{QColor::fromRgbF(0.62f, 0.62f, 0.64f), QColor::fromRgbF(0.86f, 0.18f, 0.20f)};
|
||||
case Kind::hue: return std::vector{hsb(degrees - 50, 0.85, 0.9), hsb(degrees + 50, 0.85, 0.9)};
|
||||
case Kind::saturation: return std::vector{QColor::fromRgbF(0.55f, 0.55f, 0.56f), hsb(degrees, 0.9, 0.9)};
|
||||
case Kind::luminance: return std::vector{hsb(degrees, 0.55, 0.18), hsb(degrees, 0.35, 0.95)};
|
||||
case Kind::opposing: return std::vector{from, to};
|
||||
case Kind::spectrum: {
|
||||
// Every 30 degrees round the circle, that hue centred.
|
||||
std::vector<QColor> circle;
|
||||
for (int offset = -180; offset <= 180; offset += 30)
|
||||
circle.push_back(hsb(degrees + offset, 0.85, 0.9));
|
||||
return circle;
|
||||
}
|
||||
}
|
||||
throw std::logic_error("unknown slider track");
|
||||
}
|
||||
@@ -52,6 +60,15 @@ void CameraRawSlider::display(double value)
|
||||
setValue(int(std::lround((value - m_low) / (m_high - m_low) * travel)));
|
||||
}
|
||||
|
||||
void CameraRawSlider::reshape(double low, double high, CameraRawSliderTrack track, double value)
|
||||
{
|
||||
m_low = low;
|
||||
m_high = high;
|
||||
m_track = track;
|
||||
display(value);
|
||||
update();
|
||||
}
|
||||
|
||||
double CameraRawSlider::shown() const
|
||||
{
|
||||
return m_low + (m_high - m_low) * value() / travel;
|
||||
@@ -67,7 +84,7 @@ bool CameraRawSlider::isOnKnob(QPoint point) const
|
||||
// Swift's GradientSliderCell: the whole track shows the colours.
|
||||
void CameraRawSlider::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
const std::optional<std::pair<QColor, QColor>> colors = m_track.colors();
|
||||
const std::optional<std::vector<QColor>> colors = m_track.colors();
|
||||
if (!colors) {
|
||||
QSlider::paintEvent(event);
|
||||
return;
|
||||
@@ -80,8 +97,8 @@ void CameraRawSlider::paintEvent(QPaintEvent *event)
|
||||
const double height = 4;
|
||||
const QRectF bar(groove.left(), groove.center().y() + 0.5 - height / 2, groove.width(), height);
|
||||
QLinearGradient gradient(bar.topLeft(), bar.topRight());
|
||||
gradient.setColorAt(0, colors->first);
|
||||
gradient.setColorAt(1, colors->second);
|
||||
for (size_t index = 0; index < colors->size(); ++index)
|
||||
gradient.setColorAt(double(index) / double(colors->size() - 1), colors->at(index));
|
||||
QPainterPath shape;
|
||||
shape.addRoundedRect(bar, height / 2, height / 2);
|
||||
painter.fillPath(shape, gradient);
|
||||
|
||||
@@ -4,15 +4,19 @@
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
// Swift's CameraRawSliderTrack: a coloured track, or the system's.
|
||||
struct CameraRawSliderTrack {
|
||||
enum class Kind { plain, temperature, tint, chroma, hue, saturation, luminance };
|
||||
enum class Kind { plain, temperature, tint, chroma, hue, saturation, luminance, opposing, spectrum };
|
||||
Kind kind = Kind::plain;
|
||||
// A family's centre, for hue, saturation and luminance.
|
||||
// A family's centre, for hue, saturation, luminance and spectrum.
|
||||
double degrees = 0;
|
||||
// Left and right colours; none keeps the system track.
|
||||
std::optional<std::pair<QColor, QColor>> colors() const;
|
||||
// Opposing's two ends, as Color Balance's Cyan / Red.
|
||||
QColor from = {};
|
||||
QColor to = {};
|
||||
// Left-to-right colours, evenly spaced; none keeps the system track.
|
||||
std::optional<std::vector<QColor>> colors() const;
|
||||
};
|
||||
|
||||
// Swift's CameraRawSlider: a double click on the knob resets.
|
||||
@@ -23,7 +27,10 @@ public:
|
||||
std::function<void()> reset, QWidget *parent = nullptr);
|
||||
// Shows a value, unless a drag holds the knob.
|
||||
void display(double value);
|
||||
// Swift's updateNSView: new bounds and track, the value shown again.
|
||||
void reshape(double low, double high, CameraRawSliderTrack track, double value);
|
||||
double shown() const;
|
||||
const CameraRawSliderTrack &track() const { return m_track; }
|
||||
bool isOnKnob(QPoint point) const;
|
||||
|
||||
protected:
|
||||
@@ -31,9 +38,9 @@ protected:
|
||||
void mouseDoubleClickEvent(QMouseEvent *event) override;
|
||||
|
||||
private:
|
||||
const double m_low;
|
||||
const double m_high;
|
||||
const CameraRawSliderTrack m_track;
|
||||
double m_low;
|
||||
double m_high;
|
||||
CameraRawSliderTrack m_track;
|
||||
const std::function<void(double)> m_change;
|
||||
const std::function<void()> m_reset;
|
||||
};
|
||||
@@ -343,3 +343,8 @@ void CanvasSizeSheet::synchronize()
|
||||
m_customRow->setVisible(m_extension->currentIndex() == 5);
|
||||
m_ok->setEnabled(valid);
|
||||
}
|
||||
|
||||
CanvasSizeSheet::~CanvasSizeSheet()
|
||||
{
|
||||
releaseFocus(*this);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ class CanvasSizeSheet : public QWidget {
|
||||
public:
|
||||
CanvasSizeSheet(const CanvasDocument &document, PaletteColor foreground, PaletteColor background,
|
||||
std::function<void(std::optional<CanvasSizeOptions>)> finish, QWidget *parent = nullptr);
|
||||
~CanvasSizeSheet() override;
|
||||
|
||||
private:
|
||||
PickerField *dimension(bool widthAxis);
|
||||
|
||||
@@ -151,6 +151,13 @@ void PickerField::keyPressEvent(QKeyEvent *event)
|
||||
QLineEdit::keyPressEvent(event);
|
||||
}
|
||||
|
||||
void releaseFocus(QWidget &owner)
|
||||
{
|
||||
// Destroyed focused, it would commit into freed members.
|
||||
if (QWidget *focus = owner.focusWidget(); focus && focus->hasFocus())
|
||||
focus->clearFocus();
|
||||
}
|
||||
|
||||
void PickerField::focusOutEvent(QFocusEvent *event)
|
||||
{
|
||||
// A menu or popup borrows the focus: the typing stays.
|
||||
@@ -376,3 +383,8 @@ void ColorPickerPanelController::refocus()
|
||||
{
|
||||
FloatingPanel::refocus(identifier());
|
||||
}
|
||||
|
||||
ColorPickerSheet::~ColorPickerSheet()
|
||||
{
|
||||
releaseFocus(*this);
|
||||
}
|
||||
@@ -22,11 +22,15 @@ private:
|
||||
const std::function<void(int)> m_step;
|
||||
};
|
||||
|
||||
// An owner's destructor: its focused entry commits while members live.
|
||||
void releaseFocus(QWidget &owner);
|
||||
|
||||
// Swift's ColorPickerSheet: field, hue strip, preview, entry.
|
||||
class ColorPickerSheet : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
ColorPickerSheet(EditorSession &session, std::function<void(bool)> finish, QWidget *parent = nullptr);
|
||||
~ColorPickerSheet() override;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
||||
@@ -282,3 +282,8 @@ void EffectsSheet::synchronize()
|
||||
control.field->setText(text);
|
||||
}
|
||||
}
|
||||
|
||||
EffectsSheet::~EffectsSheet()
|
||||
{
|
||||
releaseFocus(*this);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ class EffectsSheet : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
EffectsSheet(EditorSession &session, LayerEffectKind kind, QWidget *parent = nullptr);
|
||||
~EffectsSheet() override;
|
||||
|
||||
private:
|
||||
// Swift's slider: a title, a slider, a whole-number field.
|
||||
|
||||
@@ -8,19 +8,23 @@
|
||||
void FilterSheet::blackWhite()
|
||||
{
|
||||
// Each slider: how bright that family of colours becomes.
|
||||
const std::vector<std::pair<QString, double BlackWhiteSettings::*>> families{
|
||||
{QStringLiteral("Reds"), &BlackWhiteSettings::reds}, {QStringLiteral("Yellows"), &BlackWhiteSettings::yellows},
|
||||
{QStringLiteral("Greens"), &BlackWhiteSettings::greens}, {QStringLiteral("Cyans"), &BlackWhiteSettings::cyans},
|
||||
{QStringLiteral("Blues"), &BlackWhiteSettings::blues}, {QStringLiteral("Magentas"), &BlackWhiteSettings::magentas}};
|
||||
for (const auto &[title, field] : families)
|
||||
using Kind = CameraRawSliderTrack::Kind;
|
||||
const std::vector<std::tuple<QString, double BlackWhiteSettings::*, double>> families{
|
||||
{QStringLiteral("Reds"), &BlackWhiteSettings::reds, 0}, {QStringLiteral("Yellows"), &BlackWhiteSettings::yellows, 60},
|
||||
{QStringLiteral("Greens"), &BlackWhiteSettings::greens, 120}, {QStringLiteral("Cyans"), &BlackWhiteSettings::cyans, 180},
|
||||
{QStringLiteral("Blues"), &BlackWhiteSettings::blues, 240}, {QStringLiteral("Magentas"), &BlackWhiteSettings::magentas, 300}};
|
||||
for (const auto &[title, field, degrees] : families)
|
||||
control(title, [field](FilterSettings &settings) -> double & { return settings.blackWhite.*field; }, BlackWhiteSettings::low,
|
||||
BlackWhiteSettings::high, QStringLiteral("%"), 0, false);
|
||||
BlackWhiteSettings::high, QStringLiteral("%"), 0, false,
|
||||
[degrees](const FilterSettings &) { return CameraRawSliderTrack{Kind::luminance, degrees}; });
|
||||
flag(QStringLiteral("Tint"), QStringLiteral("Color the result while keeping its tones, for a sepia or a cyanotype"),
|
||||
[](FilterSettings &settings) -> bool & { return settings.blackWhite.tint; });
|
||||
control(QStringLiteral("Hue"), [](FilterSettings &settings) -> double & { return settings.blackWhite.tintHue; }, 0, 360, QStringLiteral("°"), 0,
|
||||
false);
|
||||
false, [](const FilterSettings &) { return CameraRawSliderTrack{}; });
|
||||
// Saturation runs from gray to the tint's own hue.
|
||||
control(QStringLiteral("Saturation"), [](FilterSettings &settings) -> double & { return settings.blackWhite.tintSaturation; }, 0, 100,
|
||||
QStringLiteral("%"), 0, false);
|
||||
QStringLiteral("%"), 0, false,
|
||||
[](const FilterSettings &settings) { return CameraRawSliderTrack{Kind::saturation, settings.blackWhite.tintHue}; });
|
||||
for (size_t index = m_controls.size() - 2; index < m_controls.size(); ++index)
|
||||
m_shownWhen.emplace_back(m_controls[index].slider->parentWidget(), [](const FilterSettings &settings) { return settings.blackWhite.tint; });
|
||||
}
|
||||
@@ -34,11 +38,12 @@ void FilterSheet::colorBalance()
|
||||
{QStringLiteral("Highlights"),
|
||||
{&ColorBalanceSettings::highlightCyanRed, &ColorBalanceSettings::highlightMagentaGreen, &ColorBalanceSettings::highlightYellowBlue}}};
|
||||
const std::array<QString, 3> pairs{QStringLiteral("Cyan / Red"), QStringLiteral("Magenta / Green"), QStringLiteral("Yellow / Blue")};
|
||||
static constexpr std::array tracks{&FilterSheet::cyanRedTrack, &FilterSheet::magentaGreenTrack, &FilterSheet::yellowBlueTrack};
|
||||
for (const auto &[range, fields] : ranges) {
|
||||
headline(range);
|
||||
for (size_t pair = 0; pair < 3; ++pair)
|
||||
control(pairs[pair], [field = fields[pair]](FilterSettings &settings) -> double & { return settings.colorBalance.*field; },
|
||||
ColorBalanceSettings::low, ColorBalanceSettings::high, QString(), 0, false);
|
||||
ColorBalanceSettings::low, ColorBalanceSettings::high, QString(), 0, false, [pair](const FilterSettings &) { return tracks[pair](); });
|
||||
}
|
||||
flag(QStringLiteral("Preserve Luminosity"), QStringLiteral("Put each pixel's brightness back afterwards, so only the color moves"),
|
||||
[](FilterSettings &settings) -> bool & { return settings.colorBalance.preserveLuminosity; });
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
#include "UI/FilterSheet.h"
|
||||
#include "UI/ColorPickerSheet.h"
|
||||
#include "UI/NumericScrub.h"
|
||||
#include <QEvent>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLocale>
|
||||
#include <QMouseEvent>
|
||||
#include <QSlider>
|
||||
#include <QVBoxLayout>
|
||||
#include <cmath>
|
||||
|
||||
// Swift's `control`: title, slider, exact field; coloured tracks reset.
|
||||
namespace {
|
||||
// Swift's `.number`: at most `decimals` places, none trailing.
|
||||
QString shown(double value, int decimals, QLocale locale)
|
||||
{
|
||||
locale.setNumberOptions(QLocale::OmitGroupSeparator);
|
||||
QString text = locale.toString(value, 'f', decimals);
|
||||
if (decimals == 0)
|
||||
return text;
|
||||
while (text.endsWith(locale.zeroDigit()))
|
||||
text.chop(locale.zeroDigit().size());
|
||||
if (text.endsWith(locale.decimalPoint()))
|
||||
text.chop(locale.decimalPoint().size());
|
||||
return text;
|
||||
}
|
||||
|
||||
CameraRawSliderTrack opposing(QColor from, QColor to)
|
||||
{
|
||||
return {.kind = CameraRawSliderTrack::Kind::opposing, .from = from, .to = to};
|
||||
}
|
||||
}
|
||||
|
||||
FilterSettings FilterSheet::resetting(const std::function<double &(FilterSettings &)> &key, FilterSettings settings)
|
||||
{
|
||||
FilterSettings defaults;
|
||||
key(settings) = key(defaults);
|
||||
return settings;
|
||||
}
|
||||
|
||||
CameraRawSliderTrack FilterSheet::cyanRedTrack()
|
||||
{
|
||||
return opposing(QColor::fromRgbF(0.10f, 0.72f, 0.80f), QColor::fromRgbF(0.86f, 0.18f, 0.20f));
|
||||
}
|
||||
|
||||
CameraRawSliderTrack FilterSheet::magentaGreenTrack()
|
||||
{
|
||||
return opposing(QColor::fromRgbF(0.80f, 0.22f, 0.70f), QColor::fromRgbF(0.24f, 0.70f, 0.30f));
|
||||
}
|
||||
|
||||
CameraRawSliderTrack FilterSheet::yellowBlueTrack()
|
||||
{
|
||||
return opposing(QColor::fromRgbF(0.95f, 0.82f, 0.18f), QColor::fromRgbF(0.22f, 0.40f, 0.92f));
|
||||
}
|
||||
|
||||
void FilterSheet::control(const QString &title, std::function<double &(FilterSettings &)> key, double low, double high, const QString &unit,
|
||||
int decimals, bool logarithmic, std::function<CameraRawSliderTrack(const FilterSettings &)> track)
|
||||
{
|
||||
if (track && logarithmic)
|
||||
throw std::logic_error("a coloured track is linear");
|
||||
QString name = title;
|
||||
name.remove(QLatin1Char(' '));
|
||||
name[0] = name[0].toLower();
|
||||
const size_t index = m_controls.size();
|
||||
auto *box = new QWidget(this);
|
||||
auto *label = new QLabel(title, box);
|
||||
// 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();
|
||||
}});
|
||||
// Swift rounds what the slider sets to the field's decimals.
|
||||
const auto slide = [this, index](double value) {
|
||||
const Control &at = m_controls[index];
|
||||
const double step = std::pow(10.0, at.decimals), rounded = std::round(value * step) / step;
|
||||
// Unchanged, the thumb stays, so arrow steps add up.
|
||||
FilterSettings current = settings();
|
||||
if (at.key(current) == rounded)
|
||||
return;
|
||||
update([&at, rounded](FilterSettings &settings) { at.key(settings) = rounded; });
|
||||
// Its value replaces the field's typing.
|
||||
at.field->setModified(false);
|
||||
synchronize();
|
||||
};
|
||||
QSlider *slider = nullptr;
|
||||
if (track) {
|
||||
slider = new CameraRawSlider(low, high, track(settings()), title + QStringLiteral(". Double-click to reset."), slide, [this, index] { reset(index); },
|
||||
box);
|
||||
// After the scrub's filter, so this one runs first.
|
||||
label->installEventFilter(this);
|
||||
} else {
|
||||
slider = new QSlider(Qt::Horizontal, box);
|
||||
slider->setRange(0, travel);
|
||||
connect(slider, &QSlider::valueChanged, this, [this, index, slide](int position) {
|
||||
const Control &at = m_controls[index];
|
||||
const double from = at.logarithmic ? std::log(at.low) : at.low, to = at.logarithmic ? std::log(at.high) : at.high;
|
||||
const double value = from + (to - from) * position / travel;
|
||||
slide(at.logarithmic ? std::exp(value) : value);
|
||||
});
|
||||
}
|
||||
slider->setObjectName(name + QStringLiteral("Slider"));
|
||||
label->setBuddy(slider);
|
||||
// A readable number applies as typed; normalizing clamps it.
|
||||
auto *field = new PickerField([this, index] {
|
||||
PickerField &edited = *m_controls[index].field;
|
||||
bool number = false;
|
||||
const double typed = edited.locale().toDouble(edited.text(), &number);
|
||||
if (edited.isModified() && number && std::isfinite(typed))
|
||||
update([this, index, typed](FilterSettings &settings) { m_controls[index].key(settings) = typed; });
|
||||
edited.setModified(false);
|
||||
synchronize();
|
||||
}, nullptr, box);
|
||||
field->setObjectName(name + QStringLiteral("Field"));
|
||||
field->setAccessibleName(title);
|
||||
field->setPlaceholderText(title);
|
||||
field->setAlignment(Qt::AlignRight);
|
||||
field->setFixedWidth(56);
|
||||
auto *suffixed = new QHBoxLayout;
|
||||
suffixed->setSpacing(2);
|
||||
suffixed->addWidget(field);
|
||||
suffixed->addWidget(new QLabel(unit, box));
|
||||
auto *row = new QHBoxLayout(box);
|
||||
row->setContentsMargins(0, 0, 0, 0);
|
||||
row->setSpacing(10);
|
||||
row->addWidget(label);
|
||||
row->addWidget(slider, 1);
|
||||
row->addLayout(suffixed);
|
||||
m_column->addWidget(box);
|
||||
m_controls.push_back(Control{std::move(key), low, high, decimals, logarithmic, slider, field, label, std::move(track)});
|
||||
}
|
||||
|
||||
void FilterSheet::showControl(Control &control, const FilterSettings &settings)
|
||||
{
|
||||
FilterSettings copy = settings;
|
||||
const double value = control.key(copy);
|
||||
if (control.track) {
|
||||
// Swift's updateNSView: the track follows the settings.
|
||||
static_cast<CameraRawSlider *>(control.slider)->reshape(control.low, control.high, control.track(settings), value);
|
||||
} else {
|
||||
const double from = control.logarithmic ? std::log(control.low) : control.low, to = control.logarithmic ? std::log(control.high) : control.high;
|
||||
const double at = control.logarithmic ? std::log(value) : value;
|
||||
// The slider shows the session's number without writing back.
|
||||
const QSignalBlocker quiet(control.slider);
|
||||
control.slider->setValue(int(std::lround((at - from) / (to - from) * travel)));
|
||||
}
|
||||
// A field being typed in keeps its typing.
|
||||
const QString text = shown(value, control.decimals, control.field->locale());
|
||||
if (!control.field->isModified() && control.field->text() != text)
|
||||
control.field->setText(text);
|
||||
}
|
||||
|
||||
void FilterSheet::alignTitles()
|
||||
{
|
||||
int widest = 60;
|
||||
for (const Control &control : m_controls) {
|
||||
if (!control.slider->parentWidget()->isHidden())
|
||||
widest = std::max(widest, control.title->sizeHint().width());
|
||||
}
|
||||
for (const Control &control : m_controls)
|
||||
control.title->setFixedWidth(widest);
|
||||
}
|
||||
|
||||
void FilterSheet::reset(size_t index)
|
||||
{
|
||||
update([this, index](FilterSettings &settings) { settings = resetting(m_controls[index].key, settings); });
|
||||
m_controls[index].field->setModified(false);
|
||||
synchronize();
|
||||
}
|
||||
|
||||
// Swift's double click on a coloured slider's title resets it.
|
||||
bool FilterSheet::eventFilter(QObject *watched, QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::MouseButtonDblClick && static_cast<QMouseEvent *>(event)->button() == Qt::LeftButton) {
|
||||
for (size_t index = 0; index < m_controls.size(); ++index) {
|
||||
if (m_controls[index].title == watched && m_controls[index].track && m_controls[index].title->isEnabled())
|
||||
reset(index);
|
||||
}
|
||||
}
|
||||
// The scrub still takes the second press, as Swift's drag.
|
||||
return QWidget::eventFilter(watched, event);
|
||||
}
|
||||
+8
-102
@@ -21,9 +21,6 @@
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
// A slider's travel: thousandths of its range.
|
||||
constexpr int travel = 1000;
|
||||
|
||||
QLabel *words(const QString &text, bool callout, QWidget *parent)
|
||||
{
|
||||
auto *label = new QLabel(text, parent);
|
||||
@@ -39,20 +36,6 @@ QLabel *words(const QString &text, bool callout, QWidget *parent)
|
||||
return label;
|
||||
}
|
||||
|
||||
// Swift's `.number`: at most `decimals` places, none trailing.
|
||||
QString shown(double value, int decimals, QLocale locale)
|
||||
{
|
||||
locale.setNumberOptions(QLocale::OmitGroupSeparator);
|
||||
QString text = locale.toString(value, 'f', decimals);
|
||||
if (decimals == 0)
|
||||
return text;
|
||||
while (text.endsWith(locale.zeroDigit()))
|
||||
text.chop(locale.zeroDigit().size());
|
||||
if (text.endsWith(locale.decimalPoint()))
|
||||
text.chop(locale.decimalPoint().size());
|
||||
return text;
|
||||
}
|
||||
|
||||
QColor colour(const AdjustmentColor &value)
|
||||
{
|
||||
return QColor::fromRgbF(float(value.red), float(value.green), float(value.blue));
|
||||
@@ -247,76 +230,6 @@ FilterSheet::FilterSheet(EditorSession &session, QWidget *parent)
|
||||
synchronize();
|
||||
}
|
||||
|
||||
void FilterSheet::control(const QString &title, std::function<double &(FilterSettings &)> key, double low, double high, const QString &unit,
|
||||
int decimals, bool logarithmic)
|
||||
{
|
||||
QString name = title;
|
||||
name.remove(QLatin1Char(' '));
|
||||
name[0] = name[0].toLower();
|
||||
const size_t index = m_controls.size();
|
||||
auto *box = new QWidget(this);
|
||||
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);
|
||||
label->setBuddy(slider);
|
||||
// Swift rounds what the slider sets to the field's decimals.
|
||||
connect(slider, &QSlider::valueChanged, this, [this, index](int position) {
|
||||
const Control &at = m_controls[index];
|
||||
const double from = at.logarithmic ? std::log(at.low) : at.low, to = at.logarithmic ? std::log(at.high) : at.high;
|
||||
const double value = from + (to - from) * position / travel, step = std::pow(10.0, at.decimals);
|
||||
const double rounded = std::round((at.logarithmic ? std::exp(value) : value) * step) / step;
|
||||
// Unchanged, the thumb stays, so arrow steps add up.
|
||||
FilterSettings current = settings();
|
||||
if (at.key(current) == rounded)
|
||||
return;
|
||||
update([&at, rounded](FilterSettings &settings) { at.key(settings) = rounded; });
|
||||
// Its value replaces the field's typing.
|
||||
at.field->setModified(false);
|
||||
synchronize();
|
||||
});
|
||||
// A readable number applies as typed; normalizing clamps it.
|
||||
auto *field = new PickerField([this, index] {
|
||||
PickerField &edited = *m_controls[index].field;
|
||||
bool number = false;
|
||||
const double typed = edited.locale().toDouble(edited.text(), &number);
|
||||
if (edited.isModified() && number && std::isfinite(typed))
|
||||
update([this, index, typed](FilterSettings &settings) { m_controls[index].key(settings) = typed; });
|
||||
edited.setModified(false);
|
||||
synchronize();
|
||||
}, nullptr, box);
|
||||
field->setObjectName(name + QStringLiteral("Field"));
|
||||
field->setAccessibleName(title);
|
||||
field->setPlaceholderText(title);
|
||||
field->setAlignment(Qt::AlignRight);
|
||||
field->setFixedWidth(56);
|
||||
auto *suffixed = new QHBoxLayout;
|
||||
suffixed->setSpacing(2);
|
||||
suffixed->addWidget(field);
|
||||
suffixed->addWidget(new QLabel(unit, box));
|
||||
auto *row = new QHBoxLayout(box);
|
||||
row->setContentsMargins(0, 0, 0, 0);
|
||||
row->setSpacing(10);
|
||||
row->addWidget(label);
|
||||
row->addWidget(slider, 1);
|
||||
row->addLayout(suffixed);
|
||||
m_column->addWidget(box);
|
||||
m_controls.push_back(Control{std::move(key), low, high, decimals, logarithmic, slider, field});
|
||||
}
|
||||
|
||||
void FilterSheet::noise()
|
||||
{
|
||||
auto *box = new QWidget(this);
|
||||
@@ -408,21 +321,8 @@ void FilterSheet::synchronize()
|
||||
if (m_vignetteSwatch)
|
||||
m_vignetteSwatch->update();
|
||||
const FilterEdit &edit = m_session.filterEdit().value();
|
||||
for (Control &control : m_controls) {
|
||||
FilterSettings copy = edit.settings;
|
||||
const double value = control.key(copy);
|
||||
const double from = control.logarithmic ? std::log(control.low) : control.low, to = control.logarithmic ? std::log(control.high) : control.high;
|
||||
const double at = control.logarithmic ? std::log(value) : value;
|
||||
{
|
||||
// The slider shows the session's number without writing back.
|
||||
const QSignalBlocker quiet(control.slider);
|
||||
control.slider->setValue(int(std::lround((at - from) / (to - from) * travel)));
|
||||
}
|
||||
// A field being typed in keeps its typing.
|
||||
const QString text = shown(value, control.decimals, control.field->locale());
|
||||
if (!control.field->isModified() && control.field->text() != text)
|
||||
control.field->setText(text);
|
||||
}
|
||||
for (Control &control : m_controls)
|
||||
showControl(control, edit.settings);
|
||||
if (m_curves)
|
||||
m_curves->synchronize();
|
||||
if (m_gradientMap)
|
||||
@@ -439,6 +339,7 @@ void FilterSheet::synchronize()
|
||||
}
|
||||
for (const auto &[row, shows] : m_shownWhen)
|
||||
row->setVisible(shows(edit.settings));
|
||||
alignTitles();
|
||||
m_preview->setChecked(edit.preview);
|
||||
m_error->setText(edit.previewError.value_or(QString()));
|
||||
m_error->setVisible(edit.previewError.has_value());
|
||||
@@ -452,3 +353,8 @@ void FilterSheet::synchronize()
|
||||
m_ok->setEnabled(!(isAutomatic(edit.kind) && (edit.preparing || edit.previewError)));
|
||||
setEnabled(!edit.committing);
|
||||
}
|
||||
|
||||
FilterSheet::~FilterSheet()
|
||||
{
|
||||
releaseFocus(*this);
|
||||
}
|
||||
+21
-1
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include "Document/EditorSession.h"
|
||||
#include "UI/CameraRawSlider.h"
|
||||
#include <QWidget>
|
||||
|
||||
class CurvesControls;
|
||||
@@ -38,6 +39,16 @@ class FilterSheet : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit FilterSheet(EditorSession &session, QWidget *parent = nullptr);
|
||||
~FilterSheet() override;
|
||||
// Swift's resetting: that setting back to the filter's default.
|
||||
static FilterSettings resetting(const std::function<double &(FilterSettings &)> &key, FilterSettings settings);
|
||||
// Color Balance's tracks, each colour to its opposite.
|
||||
static CameraRawSliderTrack cyanRedTrack();
|
||||
static CameraRawSliderTrack magentaGreenTrack();
|
||||
static CameraRawSliderTrack yellowBlueTrack();
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
|
||||
private:
|
||||
// Swift's control: a slider, an exact field and its unit.
|
||||
@@ -49,9 +60,18 @@ private:
|
||||
bool logarithmic;
|
||||
QSlider *slider;
|
||||
PickerField *field;
|
||||
QLabel *title;
|
||||
// A coloured track, as Camera Raw's; a double click resets.
|
||||
std::function<CameraRawSliderTrack(const FilterSettings &)> track;
|
||||
};
|
||||
// A slider's travel: thousandths of its range.
|
||||
static constexpr int travel = 1000;
|
||||
void control(const QString &title, std::function<double &(FilterSettings &)> key, double low, double high, const QString &unit,
|
||||
int decimals, bool logarithmic);
|
||||
int decimals, bool logarithmic, std::function<CameraRawSliderTrack(const FilterSettings &)> track = {});
|
||||
void showControl(Control &control, const FilterSettings &settings);
|
||||
// Swift's LabelWidthKey: every shown title as wide as the widest.
|
||||
void alignTitles();
|
||||
void reset(size_t index);
|
||||
void noise();
|
||||
// Vignette's colour and rows, Bloom / Glow's, Tonal Contrast's.
|
||||
void finishing(FilterKind kind);
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
#include "UI/HueSaturationSheet.h"
|
||||
#include <QLabel>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QVBoxLayout>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
// Swift's SpectrumEditor: the bars, their handles and the readout.
|
||||
namespace {
|
||||
// Swift's 72 hue slices, as they are or adjusted.
|
||||
class Spectrum : public QWidget {
|
||||
public:
|
||||
Spectrum(std::function<HueSaturationSettings()> value, bool after, QWidget *parent) : QWidget(parent), m_value(std::move(value)), m_after(after)
|
||||
{
|
||||
setFixedHeight(16);
|
||||
}
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override
|
||||
{
|
||||
constexpr int slices = 72;
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
QPainterPath rounded;
|
||||
rounded.addRoundedRect(QRectF(rect()), 3, 3);
|
||||
painter.setClipPath(rounded);
|
||||
const HueSaturationSettings settings = m_value();
|
||||
const double width = double(this->width()) / slices;
|
||||
for (int slice = 0; slice < slices; ++slice) {
|
||||
const double hue = double(slice) / slices * 360;
|
||||
const double shown = m_after ? HueSaturationFilter::shiftedHue(hue, settings) : hue;
|
||||
painter.fillRect(QRectF(slice * width, 0, width + 0.5, height()), QColor::fromHsvF(float(shown / 360), 1, 1));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
const std::function<HueSaturationSettings()> m_value;
|
||||
const bool m_after;
|
||||
};
|
||||
|
||||
// Swift's handles: shoulders as blocks, the core's ends as bars.
|
||||
class SpectrumHandles : public QWidget {
|
||||
public:
|
||||
SpectrumHandles(std::function<HueSaturationSettings()> value, std::function<void(double, double, bool)> drag, QWidget *parent)
|
||||
: QWidget(parent), m_value(std::move(value)), m_drag(std::move(drag))
|
||||
{
|
||||
setFixedHeight(12);
|
||||
}
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
const std::array<double, 4> handles = m_value().band().handles();
|
||||
for (size_t index = 0; index < handles.size(); ++index) {
|
||||
const double x = handles[index] / 360 * width();
|
||||
const bool inner = index == 1 || index == 2;
|
||||
painter.fillRect(inner ? QRectF(x - 1, 0, 2, height()) : QRectF(x - 3.5, height() / 2.0 - 2.5, 7, 5), palette().color(QPalette::WindowText));
|
||||
}
|
||||
}
|
||||
void mousePressEvent(QMouseEvent *event) override
|
||||
{
|
||||
if (event->button() == Qt::LeftButton)
|
||||
m_drag(event->position().x(), width(), true);
|
||||
}
|
||||
void mouseMoveEvent(QMouseEvent *event) override
|
||||
{
|
||||
if (event->buttons().testFlag(Qt::LeftButton))
|
||||
m_drag(event->position().x(), width(), false);
|
||||
}
|
||||
|
||||
private:
|
||||
const std::function<HueSaturationSettings()> m_value;
|
||||
const std::function<void(double, double, bool)> m_drag;
|
||||
};
|
||||
|
||||
// The nearest handle round the circle; ties take the first.
|
||||
int nearestHandle(const HueBand &band, double degrees)
|
||||
{
|
||||
const std::array<double, 4> handles = band.handles();
|
||||
std::array<double, 4> distances{};
|
||||
for (size_t index = 0; index < handles.size(); ++index) {
|
||||
const double gap = std::fmod(std::abs(handles[index] - degrees), 360);
|
||||
distances[index] = std::min(gap, 360 - gap);
|
||||
}
|
||||
return int(std::min_element(distances.begin(), distances.end()) - distances.begin());
|
||||
}
|
||||
}
|
||||
|
||||
SpectrumEditor::SpectrumEditor(std::function<HueSaturationSettings()> value, std::function<void(const HueSaturationSettings &)> change, QWidget *parent)
|
||||
: QWidget(parent), m_value(std::move(value)), m_change(std::move(change)),
|
||||
m_handles(new SpectrumHandles(m_value, [this](double x, double width, bool pressed) { drag(x, width, pressed); }, this)),
|
||||
m_after(new Spectrum(m_value, true, this)), m_readout(new QLabel(this))
|
||||
{
|
||||
auto *before = new Spectrum(m_value, false, this);
|
||||
before->setObjectName(QStringLiteral("spectrumBefore"));
|
||||
m_handles->setObjectName(QStringLiteral("spectrumHandles"));
|
||||
m_after->setObjectName(QStringLiteral("spectrumAfter"));
|
||||
m_readout->setObjectName(QStringLiteral("spectrumReadout"));
|
||||
// Swift's .caption in the secondary ink.
|
||||
QFont small = m_readout->font();
|
||||
small.setPixelSize(10);
|
||||
m_readout->setFont(small);
|
||||
m_readout->setForegroundRole(QPalette::PlaceholderText);
|
||||
auto *column = new QVBoxLayout(this);
|
||||
column->setContentsMargins(0, 0, 0, 0);
|
||||
column->setSpacing(5);
|
||||
column->addWidget(before);
|
||||
column->addWidget(m_handles);
|
||||
column->addWidget(m_after);
|
||||
column->addWidget(m_readout, 0, Qt::AlignHCenter);
|
||||
synchronize();
|
||||
}
|
||||
|
||||
void SpectrumEditor::synchronize()
|
||||
{
|
||||
QStringList degrees;
|
||||
for (const double handle : m_value().band().handles())
|
||||
degrees << QStringLiteral("%1°").arg(std::lround(handle));
|
||||
m_readout->setText(degrees.join(QStringLiteral(" ")));
|
||||
m_handles->update();
|
||||
m_after->update();
|
||||
}
|
||||
|
||||
void SpectrumEditor::drag(double x, double width, bool pressed)
|
||||
{
|
||||
// A press starts afresh: Qt can lose a release.
|
||||
if (pressed)
|
||||
m_dragging.reset();
|
||||
const double degrees = std::clamp(x, 0.0, width) / width * 360;
|
||||
HueSaturationSettings settings = m_value();
|
||||
HueBand band = settings.band();
|
||||
const int index = m_dragging.value_or(nearestHandle(band, degrees));
|
||||
m_dragging = index;
|
||||
band.setHandle(index, degrees);
|
||||
settings.setBand(band);
|
||||
m_change(settings);
|
||||
}
|
||||
+64
-147
@@ -12,7 +12,6 @@
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QPushButton>
|
||||
#include <QSlider>
|
||||
#include <QToolButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <algorithm>
|
||||
@@ -30,9 +29,6 @@ constexpr std::array<Row, 3> rows{{{"Hue", "°", &HueSaturationSettings::hue, &H
|
||||
{"Saturation", "", &HueSaturationSettings::saturation, &HueSaturationSettings::setSaturation},
|
||||
{"Lightness", "", &HueSaturationSettings::lightness, &HueSaturationSettings::setLightness}}};
|
||||
|
||||
// Sliders move in hundredths, as Swift's run freely.
|
||||
constexpr double sliderScale = 100;
|
||||
|
||||
// Swift's ranges: Colorize sets hue and saturation outright.
|
||||
std::pair<double, double> bounds(size_t index, bool colorize)
|
||||
{
|
||||
@@ -43,92 +39,29 @@ std::pair<double, double> bounds(size_t index, bool colorize)
|
||||
return {-100.0, 100.0};
|
||||
}
|
||||
|
||||
// Swift's tracks: hue circle, gray to colour, black to white.
|
||||
CameraRawSliderTrack track(size_t index, const HueSaturationSettings &settings)
|
||||
{
|
||||
using Kind = CameraRawSliderTrack::Kind;
|
||||
// The middle of the chosen range; Master centres on red.
|
||||
const auto found = std::find(colorRanges.begin(), colorRanges.end(), settings.range);
|
||||
const double rangeHue = found == colorRanges.end() ? 0 : double(found - colorRanges.begin()) * 60;
|
||||
if (index == 0)
|
||||
return {Kind::spectrum, settings.colorize ? 180 : rangeHue};
|
||||
if (index == 1) {
|
||||
if (settings.colorize)
|
||||
return {Kind::saturation, settings.hue()};
|
||||
return settings.range == ColorRange::master ? CameraRawSliderTrack{Kind::chroma} : CameraRawSliderTrack{Kind::saturation, rangeHue};
|
||||
}
|
||||
return {.kind = Kind::opposing, .from = Qt::black, .to = Qt::white};
|
||||
}
|
||||
|
||||
// Swift's `current`: the open edit's settings, else the defaults.
|
||||
HueSaturationSettings current(const EditorSession &session)
|
||||
{
|
||||
return session.hueSaturation() ? session.hueSaturation()->settings : HueSaturationSettings();
|
||||
}
|
||||
|
||||
// Swift's 72 hue slices, as they are or adjusted.
|
||||
class Spectrum : public QWidget {
|
||||
public:
|
||||
Spectrum(std::function<HueSaturationSettings()> value, bool after, QWidget *parent) : QWidget(parent), m_value(std::move(value)), m_after(after)
|
||||
{
|
||||
setFixedHeight(16);
|
||||
}
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override
|
||||
{
|
||||
constexpr int slices = 72;
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
QPainterPath rounded;
|
||||
rounded.addRoundedRect(QRectF(rect()), 3, 3);
|
||||
painter.setClipPath(rounded);
|
||||
const HueSaturationSettings settings = m_value();
|
||||
const double width = double(this->width()) / slices;
|
||||
for (int slice = 0; slice < slices; ++slice) {
|
||||
const double hue = double(slice) / slices * 360;
|
||||
const double shown = m_after ? HueSaturationFilter::shiftedHue(hue, settings) : hue;
|
||||
painter.fillRect(QRectF(slice * width, 0, width + 0.5, height()), QColor::fromHsvF(float(shown / 360), 1, 1));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
const std::function<HueSaturationSettings()> m_value;
|
||||
const bool m_after;
|
||||
};
|
||||
|
||||
// Swift's handles: shoulders as blocks, the core's ends as bars.
|
||||
class SpectrumHandles : public QWidget {
|
||||
public:
|
||||
SpectrumHandles(std::function<HueSaturationSettings()> value, std::function<void(double, double, bool)> drag, QWidget *parent)
|
||||
: QWidget(parent), m_value(std::move(value)), m_drag(std::move(drag))
|
||||
{
|
||||
setFixedHeight(12);
|
||||
}
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *) override
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
const std::array<double, 4> handles = m_value().band().handles();
|
||||
for (size_t index = 0; index < handles.size(); ++index) {
|
||||
const double x = handles[index] / 360 * width();
|
||||
const bool inner = index == 1 || index == 2;
|
||||
painter.fillRect(inner ? QRectF(x - 1, 0, 2, height()) : QRectF(x - 3.5, height() / 2.0 - 2.5, 7, 5), palette().color(QPalette::WindowText));
|
||||
}
|
||||
}
|
||||
void mousePressEvent(QMouseEvent *event) override
|
||||
{
|
||||
if (event->button() == Qt::LeftButton)
|
||||
m_drag(event->position().x(), width(), true);
|
||||
}
|
||||
void mouseMoveEvent(QMouseEvent *event) override
|
||||
{
|
||||
if (event->buttons().testFlag(Qt::LeftButton))
|
||||
m_drag(event->position().x(), width(), false);
|
||||
}
|
||||
|
||||
private:
|
||||
const std::function<HueSaturationSettings()> m_value;
|
||||
const std::function<void(double, double, bool)> m_drag;
|
||||
};
|
||||
|
||||
// The nearest handle round the circle; ties take the first.
|
||||
int nearestHandle(const HueBand &band, double degrees)
|
||||
{
|
||||
const std::array<double, 4> handles = band.handles();
|
||||
std::array<double, 4> distances{};
|
||||
for (size_t index = 0; index < handles.size(); ++index) {
|
||||
const double gap = std::fmod(std::abs(handles[index] - degrees), 360);
|
||||
distances[index] = std::min(gap, 360 - gap);
|
||||
}
|
||||
return int(std::min_element(distances.begin(), distances.end()) - distances.begin());
|
||||
}
|
||||
|
||||
// Swift's plain sampling buttons: a glyph, tinted when chosen.
|
||||
class SampleButton : public QToolButton {
|
||||
public:
|
||||
@@ -216,56 +149,6 @@ QFrame *line(QFrame::Shape shape, QWidget *parent)
|
||||
}
|
||||
}
|
||||
|
||||
SpectrumEditor::SpectrumEditor(std::function<HueSaturationSettings()> value, std::function<void(const HueSaturationSettings &)> change, QWidget *parent)
|
||||
: QWidget(parent), m_value(std::move(value)), m_change(std::move(change)),
|
||||
m_handles(new SpectrumHandles(m_value, [this](double x, double width, bool pressed) { drag(x, width, pressed); }, this)),
|
||||
m_after(new Spectrum(m_value, true, this)), m_readout(new QLabel(this))
|
||||
{
|
||||
auto *before = new Spectrum(m_value, false, this);
|
||||
before->setObjectName(QStringLiteral("spectrumBefore"));
|
||||
m_handles->setObjectName(QStringLiteral("spectrumHandles"));
|
||||
m_after->setObjectName(QStringLiteral("spectrumAfter"));
|
||||
m_readout->setObjectName(QStringLiteral("spectrumReadout"));
|
||||
// Swift's .caption in the secondary ink.
|
||||
QFont small = m_readout->font();
|
||||
small.setPixelSize(10);
|
||||
m_readout->setFont(small);
|
||||
m_readout->setForegroundRole(QPalette::PlaceholderText);
|
||||
auto *column = new QVBoxLayout(this);
|
||||
column->setContentsMargins(0, 0, 0, 0);
|
||||
column->setSpacing(5);
|
||||
column->addWidget(before);
|
||||
column->addWidget(m_handles);
|
||||
column->addWidget(m_after);
|
||||
column->addWidget(m_readout, 0, Qt::AlignHCenter);
|
||||
synchronize();
|
||||
}
|
||||
|
||||
void SpectrumEditor::synchronize()
|
||||
{
|
||||
QStringList degrees;
|
||||
for (const double handle : m_value().band().handles())
|
||||
degrees << QStringLiteral("%1°").arg(std::lround(handle));
|
||||
m_readout->setText(degrees.join(QStringLiteral(" ")));
|
||||
m_handles->update();
|
||||
m_after->update();
|
||||
}
|
||||
|
||||
void SpectrumEditor::drag(double x, double width, bool pressed)
|
||||
{
|
||||
// A press starts afresh: Qt can lose a release.
|
||||
if (pressed)
|
||||
m_dragging.reset();
|
||||
const double degrees = std::clamp(x, 0.0, width) / width * 360;
|
||||
HueSaturationSettings settings = m_value();
|
||||
HueBand band = settings.band();
|
||||
const int index = m_dragging.value_or(nearestHandle(band, degrees));
|
||||
m_dragging = index;
|
||||
band.setHandle(index, degrees);
|
||||
settings.setBand(band);
|
||||
m_change(settings);
|
||||
}
|
||||
|
||||
HueSaturationSheet::HueSaturationSheet(EditorSession &session, QWidget *parent)
|
||||
: QWidget(parent), m_session(session), m_range(new QComboBox(this)),
|
||||
m_samples{sampleButton(HueSampleMode::replace, this), sampleButton(HueSampleMode::add, this), sampleButton(HueSampleMode::remove, this)},
|
||||
@@ -382,15 +265,25 @@ QWidget *HueSaturationSheet::row(size_t index)
|
||||
m_fields[index]->setModified(false);
|
||||
synchronize();
|
||||
}});
|
||||
auto *slider = new QSlider(Qt::Horizontal, box);
|
||||
const auto [low, high] = bounds(index, current(m_session).colorize);
|
||||
auto *slider = new CameraRawSlider(
|
||||
low, high, track(index, current(m_session)), title + QStringLiteral(". Double-click to reset."),
|
||||
[this, index](double value) {
|
||||
// Swift sets whole numbers; unchanged, the knob keeps its travel.
|
||||
const double rounded = std::round(value);
|
||||
if ((current(m_session).*rows[index].value)() == rounded)
|
||||
return;
|
||||
change([&](HueSaturationSettings &settings) { (settings.*rows[index].set)(rounded); });
|
||||
// Its value replaces the field's typing.
|
||||
m_fields[index]->setModified(false);
|
||||
synchronize();
|
||||
},
|
||||
[this, index] { reset(index); }, box);
|
||||
slider->setObjectName(title.toLower() + QStringLiteral("Slider"));
|
||||
label->setBuddy(slider);
|
||||
connect(slider, &QSlider::valueChanged, this, [this, index](int value) {
|
||||
change([&](HueSaturationSettings &settings) { (settings.*rows[index].set)(value / sliderScale); });
|
||||
// Its value replaces the field's typing.
|
||||
m_fields[index]->setModified(false);
|
||||
synchronize();
|
||||
});
|
||||
// After the scrub's filter, so this one runs first.
|
||||
label->installEventFilter(this);
|
||||
m_titles[index] = label;
|
||||
// A readable number applies as typed, as Swift's value binding.
|
||||
auto *field = new PickerField([this, index] {
|
||||
PickerField &edited = *m_fields[index];
|
||||
@@ -426,6 +319,29 @@ QWidget *HueSaturationSheet::row(size_t index)
|
||||
return box;
|
||||
}
|
||||
|
||||
void HueSaturationSheet::reset(size_t index)
|
||||
{
|
||||
change([index](HueSaturationSettings &settings) {
|
||||
const HueSaturationSettings start = settings.colorize ? HueSaturationSettings::colorizeStart() : HueSaturationSettings();
|
||||
(settings.*rows[index].set)((start.*rows[index].value)());
|
||||
});
|
||||
m_fields[index]->setModified(false);
|
||||
synchronize();
|
||||
}
|
||||
|
||||
// Swift's double click on a title resets its slider.
|
||||
bool HueSaturationSheet::eventFilter(QObject *watched, QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::MouseButtonDblClick && static_cast<QMouseEvent *>(event)->button() == Qt::LeftButton) {
|
||||
for (size_t index = 0; index < m_titles.size(); ++index) {
|
||||
if (m_titles[index] == watched && m_titles[index]->isEnabled())
|
||||
reset(index);
|
||||
}
|
||||
}
|
||||
// The scrub still takes the second press, as Swift's drag.
|
||||
return QWidget::eventFilter(watched, event);
|
||||
}
|
||||
|
||||
void HueSaturationSheet::change(const std::function<void(HueSaturationSettings &)> &edit)
|
||||
{
|
||||
HueSaturationSettings settings = current(m_session);
|
||||
@@ -454,12 +370,8 @@ void HueSaturationSheet::synchronize()
|
||||
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.
|
||||
const QSignalBlocker quiet(m_sliders[index]);
|
||||
m_sliders[index]->setRange(int(low * sliderScale), int(high * sliderScale));
|
||||
m_sliders[index]->setValue(int(std::lround(std::clamp(value, low, high) * sliderScale)));
|
||||
}
|
||||
// Swift's updateNSView: range and track follow the settings.
|
||||
m_sliders[index]->reshape(low, high, track(index, settings), std::clamp(value, low, high));
|
||||
// Ungrouped digits; a field being typed in keeps its typing.
|
||||
QLocale locale = m_fields[index]->locale();
|
||||
locale.setNumberOptions(QLocale::OmitGroupSeparator);
|
||||
@@ -476,3 +388,8 @@ void HueSaturationSheet::synchronize()
|
||||
// An adjustment layer's editor ignores the selection.
|
||||
m_limited->setVisible(!m_session.adjustmentOriginal() && m_session.selection());
|
||||
}
|
||||
|
||||
HueSaturationSheet::~HueSaturationSheet()
|
||||
{
|
||||
releaseFocus(*this);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include "Document/EditorSession.h"
|
||||
#include "UI/CameraRawSlider.h"
|
||||
#include <QWidget>
|
||||
#include <array>
|
||||
#include <functional>
|
||||
@@ -40,12 +41,18 @@ class HueSaturationSheet : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit HueSaturationSheet(EditorSession &session, QWidget *parent = nullptr);
|
||||
~HueSaturationSheet() override;
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
|
||||
private:
|
||||
// Swift's settings binding: changed, then handed to the session.
|
||||
void change(const std::function<void(HueSaturationSettings &)> &edit);
|
||||
// Swift's slider row: title, slider, field and unit.
|
||||
QWidget *row(size_t index);
|
||||
// Swift's resetValues: that row back to Colorize's start or none.
|
||||
void reset(size_t index);
|
||||
void synchronize();
|
||||
|
||||
EditorSession &m_session;
|
||||
@@ -54,7 +61,8 @@ private:
|
||||
QWidget *const m_divider;
|
||||
QToolButton *const m_targeting;
|
||||
// Hue, Saturation, Lightness; the rows make them.
|
||||
std::array<QSlider *, 3> m_sliders{};
|
||||
std::array<CameraRawSlider *, 3> m_sliders{};
|
||||
std::array<QLabel *, 3> m_titles{};
|
||||
std::array<PickerField *, 3> m_fields{};
|
||||
// Each title's scrub; Colorize changes its range.
|
||||
std::array<NumericScrub *, 3> m_scrubs{};
|
||||
|
||||
@@ -343,3 +343,8 @@ void ImageSizeSheet::synchronize()
|
||||
m_result->setForegroundRole(isValid ? QPalette::PlaceholderText : QPalette::BrightText);
|
||||
m_resize->setEnabled(isValid);
|
||||
}
|
||||
|
||||
ImageSizeSheet::~ImageSizeSheet()
|
||||
{
|
||||
releaseFocus(*this);
|
||||
}
|
||||
@@ -17,6 +17,7 @@ class ImageSizeSheet : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
ImageSizeSheet(const CanvasDocument &document, std::function<void(std::optional<ImageSizeOptions>)> finish, QWidget *parent = nullptr);
|
||||
~ImageSizeSheet() override;
|
||||
|
||||
private:
|
||||
bool valid() const;
|
||||
|
||||
@@ -429,3 +429,8 @@ void LevelsSheet::synchronize()
|
||||
m_spinner->setVisible(edit->committing);
|
||||
setEnabled(!edit->committing);
|
||||
}
|
||||
|
||||
LevelsSheet::~LevelsSheet()
|
||||
{
|
||||
releaseFocus(*this);
|
||||
}
|
||||
@@ -15,6 +15,7 @@ class LevelsSheet : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit LevelsSheet(EditorSession &session, QWidget *parent = nullptr);
|
||||
~LevelsSheet() override;
|
||||
|
||||
protected:
|
||||
// A new theme recolours the eyedroppers' ink.
|
||||
|
||||
Reference in new issue
Block a user