Camera Raw panel docked beside the canvas: scope, Light, Color, Effects, canvas tools (12.8b1)

This commit is contained in:
雷电芽衣 committed 2026-09-28 00:22:02 -04:00
1 parent 9d012d325a
commit 56af936a7b
26 files changed
+1794 -34

No files matched your search

+163
View File
@@ -0,0 +1,163 @@
#include "UI/CameraRawControls.h"
#include "Rendering/EyedropperIcon.h"
#include <QComboBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QToolButton>
#include <QVBoxLayout>
namespace {
using Key = std::function<double &(CameraRawSettings &)>;
Key field(double CameraRawSettings::*member)
{
return [member](CameraRawSettings &settings) -> double & { return settings.*member; };
}
constexpr CameraRawClipping highlights = CameraRawClipping::highlights, shadows = CameraRawClipping::shadows;
// Swift's labelled Picker: its title, then the menu.
QComboBox *picker(const QString &title, const QStringList &choices, const QString &help, QVBoxLayout *column)
{
auto *box = new QWidget;
auto *label = new QLabel(title, box);
auto *menu = new QComboBox(box);
menu->addItems(choices);
menu->setToolTip(help);
label->setBuddy(menu);
auto *row = new QHBoxLayout(box);
row->setContentsMargins(0, 0, 0, 0);
row->addWidget(label);
row->addWidget(menu);
row->addStretch(1);
column->addWidget(box);
return menu;
}
// Swift's `.padding(.leading, 16)` around a group of rows.
QVBoxLayout *indented(QVBoxLayout *column)
{
auto *box = new QWidget;
auto *rows = new QVBoxLayout(box);
rows->setContentsMargins(16, 0, 0, 0);
rows->setSpacing(8);
column->addWidget(box);
return rows;
}
}
void CameraRawControls::light(QVBoxLayout *column)
{
slider(column, QStringLiteral("exposure"), QStringLiteral("Exposure"), field(&CameraRawSettings::exposure), CameraRawSettings::exposureLow, CameraRawSettings::exposureHigh, 2,
highlights, QStringLiteral("Brightens or darkens the whole picture, in stops of light. Hold Alt to see clipped highlights."));
slider(column, QStringLiteral("contrast"), QStringLiteral("Contrast"), field(&CameraRawSettings::contrast), -100, 100, 0, std::nullopt,
QStringLiteral("Makes light and dark tones more or less different, mostly around the middle."));
slider(column, QStringLiteral("highlights"), QStringLiteral("Highlights"), field(&CameraRawSettings::highlights), -100, 100, 0, highlights,
QStringLiteral("Adjusts the bright parts of the picture. Hold Alt to see clipped highlights."));
slider(column, QStringLiteral("shadows"), QStringLiteral("Shadows"), field(&CameraRawSettings::shadows), -100, 100, 0, shadows,
QStringLiteral("Adjusts the dark parts of the picture. Hold Alt to see clipped shadows."));
slider(column, QStringLiteral("whites"), QStringLiteral("Whites"), field(&CameraRawSettings::whites), -100, 100, 0, highlights,
QStringLiteral("Sets the brightest point. Hold Alt to see clipped highlights."));
slider(column, QStringLiteral("blacks"), QStringLiteral("Blacks"), field(&CameraRawSettings::blacks), -100, 100, 0, shadows,
QStringLiteral("Sets the darkest point. Hold Alt to see clipped shadows."));
}
void CameraRawControls::color(QVBoxLayout *column)
{
const QString help = QStringLiteral("Auto balances the average color. Custom follows Temperature and Tint.");
auto *box = new QWidget;
auto *label = new QLabel(QStringLiteral("White Balance"), box);
label->setMinimumWidth(labelWidth);
label->setToolTip(help);
m_whiteBalance = new QComboBox(box);
m_whiteBalance->setObjectName(QStringLiteral("whiteBalance"));
m_whiteBalance->addItems({QStringLiteral("Custom"), QStringLiteral("Auto")});
m_whiteBalance->setToolTip(help);
label->setBuddy(m_whiteBalance);
connect(m_whiteBalance, &QComboBox::activated, this, [this](int index) {
if (CameraRawWhiteBalance(index) == CameraRawWhiteBalance::automatic) {
m_session.applyCameraRawAutoWhiteBalance();
return;
}
update([](CameraRawSettings &settings) { settings.whiteBalance = CameraRawWhiteBalance::custom; });
});
m_whiteBalanceSampler = new QToolButton(box);
m_whiteBalanceSampler->setObjectName(QStringLiteral("whiteBalanceSelector"));
m_whiteBalanceSampler->setAutoRaise(true);
m_whiteBalanceSampler->setToolTip(QStringLiteral("Click a pixel that should be neutral."));
m_whiteBalanceSampler->setAccessibleName(QStringLiteral("White Balance Selector"));
connect(m_whiteBalanceSampler, &QToolButton::clicked, this,
[this] { panel([](CameraRawPanel &raw) { raw.samplesWhiteBalance = !raw.samplesWhiteBalance; }); });
auto *row = new QHBoxLayout(box);
row->setContentsMargins(0, 0, 0, 0);
row->setSpacing(10);
row->addWidget(label);
row->addWidget(m_whiteBalance, 1);
row->addWidget(m_whiteBalanceSampler);
column->addWidget(box);
m_whiteBalanceHint = new QLabel(QStringLiteral("Click the original layer. Click the eyedropper again to stop."));
m_whiteBalanceHint->setObjectName(QStringLiteral("whiteBalanceHint"));
m_whiteBalanceHint->setWordWrap(true);
m_whiteBalanceHint->setForegroundRole(QPalette::PlaceholderText);
QFont caption = m_whiteBalanceHint->font();
caption.setPixelSize(10);
m_whiteBalanceHint->setFont(caption);
column->addWidget(m_whiteBalanceHint);
using Kind = CameraRawSliderTrack::Kind;
slider(column, QStringLiteral("temperature"), QStringLiteral("Temperature"), field(&CameraRawSettings::temperature), -100, 100, 0, std::nullopt,
QStringLiteral("Shifts the picture from blue to yellow."), {Kind::temperature});
slider(column, QStringLiteral("tint"), QStringLiteral("Tint"), field(&CameraRawSettings::tint), -100, 100, 0, std::nullopt,
QStringLiteral("Shifts the picture from green to mauve."), {Kind::tint});
slider(column, QStringLiteral("vibrance"), QStringLiteral("Vibrance"), field(&CameraRawSettings::vibrance), -100, 100, 0, std::nullopt,
QStringLiteral("Strengthens quiet colors more than colors that are already strong, and protects skin tones."), {Kind::chroma});
slider(column, QStringLiteral("saturation"), QStringLiteral("Saturation"), field(&CameraRawSettings::saturation), -100, 100, 0, std::nullopt,
QStringLiteral("Strengthens or weakens every color by the same amount."), {Kind::chroma});
}
void CameraRawControls::effects(QVBoxLayout *column)
{
slider(column, QStringLiteral("texture"), QStringLiteral("Texture"), field(&CameraRawSettings::texture), -100, 100, 0, std::nullopt, QStringLiteral("Adds or softens small detail."));
slider(column, QStringLiteral("clarity"), QStringLiteral("Clarity"), field(&CameraRawSettings::clarity), -100, 100, 0, std::nullopt,
QStringLiteral("Adds or softens contrast along broader shapes."));
slider(column, QStringLiteral("dehaze"), QStringLiteral("Dehaze"), field(&CameraRawSettings::dehaze), -100, 100, 0, std::nullopt,
QStringLiteral("Clears haze when raised, and adds haze when lowered."));
subheadline(QStringLiteral("Glow"), column);
slider(column, QStringLiteral("glow"), QStringLiteral("Glow"), field(&CameraRawSettings::glow), 0, 100, 0, std::nullopt, QStringLiteral("Spreads a glow from the bright areas."));
m_glowStyle = picker(QStringLiteral("Style"), {QStringLiteral("Diffusion"), QStringLiteral("Bloom"), QStringLiteral("Halation")},
QStringLiteral("Diffusion is soft and wide, Bloom is tighter, and Halation is a red fringe."), column);
m_glowStyle->setObjectName(QStringLiteral("glowStyle"));
connect(m_glowStyle, &QComboBox::activated, this,
[this](int index) { update([index](CameraRawSettings &settings) { settings.glowStyle = CameraRawGlowStyle(index); }); });
QVBoxLayout *glow = indented(column);
slider(glow, QStringLiteral("glowRange"), QStringLiteral("Range"), field(&CameraRawSettings::glowRange), -100, 100, 0, std::nullopt,
QStringLiteral("Chooses how bright an area must be to glow. Has no effect until Glow is raised."));
slider(glow, QStringLiteral("glowSpread"), QStringLiteral("Spread"), field(&CameraRawSettings::glowSpread), -100, 100, 0, std::nullopt,
QStringLiteral("Sets how far the glow reaches. Has no effect until Glow is raised."));
slider(glow, QStringLiteral("glowWarmth"), QStringLiteral("Warmth"), field(&CameraRawSettings::glowWarmth), -100, 100, 0, std::nullopt,
QStringLiteral("Shifts the glow from cool to warm. Halation stays red. Has no effect until Glow is raised."));
subheadline(QStringLiteral("Vignette"), column);
slider(column, QStringLiteral("vignetteAmount"), QStringLiteral("Amount"), field(&CameraRawSettings::vignetteAmount), -100, 100, 0, std::nullopt,
QStringLiteral("Darkens or lightens the edges. The center does not change."));
m_vignetteStyle = picker(QStringLiteral("Style"), {QStringLiteral("Highlight Priority"), QStringLiteral("Color Priority"), QStringLiteral("Paint Overlay")},
QStringLiteral("Highlight Priority protects bright edges. Color Priority also reduces color. Paint Overlay covers the edges evenly."),
column);
m_vignetteStyle->setObjectName(QStringLiteral("vignetteStyle"));
connect(m_vignetteStyle, &QComboBox::activated, this,
[this](int index) { update([index](CameraRawSettings &settings) { settings.vignetteStyle = CameraRawVignetteStyle(index); }); });
QVBoxLayout *vignette = indented(column);
slider(vignette, QStringLiteral("vignetteMidpoint"), QStringLiteral("Midpoint"), field(&CameraRawSettings::vignetteMidpoint), 0, 100, 0, std::nullopt,
QStringLiteral("Sets where the vignette begins, from the center outward."), {}, 50);
slider(vignette, QStringLiteral("vignetteRoundness"), QStringLiteral("Roundness"), field(&CameraRawSettings::vignetteRoundness), -100, 100, 0, std::nullopt,
QStringLiteral("Makes the vignette rounder or more square."));
slider(vignette, QStringLiteral("vignetteFeather"), QStringLiteral("Feather"), field(&CameraRawSettings::vignetteFeather), 0, 100, 0, std::nullopt,
QStringLiteral("Softens the edge of the vignette."), {}, 50);
slider(vignette, QStringLiteral("vignetteHighlights"), QStringLiteral("Highlights"), field(&CameraRawSettings::vignetteHighlights), 0, 100, 0, std::nullopt,
QStringLiteral("Protects bright pixels while a dark vignette is applied. Used by Highlight Priority."));
subheadline(QStringLiteral("Grain"), column);
slider(column, QStringLiteral("grainAmount"), QStringLiteral("Amount"), field(&CameraRawSettings::grainAmount), 0, 100, 0, std::nullopt,
QStringLiteral("Adds film grain, strongest in the middle tones."));
slider(column, QStringLiteral("grainSize"), QStringLiteral("Size"), field(&CameraRawSettings::grainSize), 0, 100, 0, std::nullopt, QStringLiteral("Makes the grain coarser or finer."),
{}, 25);
slider(column, QStringLiteral("grainRoughness"), QStringLiteral("Roughness"), field(&CameraRawSettings::grainRoughness), 0, 100, 0, std::nullopt,
QStringLiteral("Makes the grain smoother or more uneven."), {}, 50);
}
+445
View File
@@ -0,0 +1,445 @@
#include "UI/CameraRawControls.h"
#include "UI/ColorPickerSheet.h"
#include "UI/LayerIcons.h"
#include "Rendering/EyedropperIcon.h"
#include <QApplication>
#include <QComboBox>
#include <QContextMenuEvent>
#include <QHBoxLayout>
#include <QKeyEvent>
#include <QLabel>
#include <QMenu>
#include <QPainter>
#include <QPainterPath>
#include <QScrollArea>
#include <QToolButton>
#include <QVBoxLayout>
namespace {
QString title(int section)
{
static const std::array<QString, 10> titles{QStringLiteral("Light"), QStringLiteral("Color"), QStringLiteral("Effects"),
QStringLiteral("Curve"), QStringLiteral("Color Mixer"), QStringLiteral("Color Grading"),
QStringLiteral("Detail"), QStringLiteral("Optics"), QStringLiteral("Geometry"),
QStringLiteral("Calibration")};
return titles.at(size_t(section));
}
// The group's eye in CameraRawGroups, by section.
bool &shows(CameraRawGroups &groups, int section)
{
const std::array fields{&CameraRawGroups::light, &CameraRawGroups::color, &CameraRawGroups::effects, &CameraRawGroups::curve,
&CameraRawGroups::mixer, &CameraRawGroups::grading, &CameraRawGroups::detail, &CameraRawGroups::optics,
&CameraRawGroups::geometry, &CameraRawGroups::calibration};
return groups.*fields.at(size_t(section));
}
bool adjusts(const CameraRawSettings &raw, int section)
{
switch (section) {
case 0: return raw.adjustsLight();
case 1: return raw.adjustsColor();
case 2: return raw.adjustsEffects();
case 3: return raw.curve.adjusts();
case 4: return raw.mixer.adjusts();
case 5: return raw.grading.adjusts();
case 6: return raw.detail.adjusts();
case 7: return raw.optics.adjusts();
case 8: return raw.geometry.adjusts();
case 9: return raw.calibration.adjusts();
}
throw std::logic_error("unknown Camera Raw section");
}
// 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;
}
// Swift's graph: the three ribbons, or the vectorscope's cells.
class ScopeGraph : public QWidget {
public:
ScopeGraph(EditorSession &session, QWidget *parent) : QWidget(parent), m_session(session) { setFixedHeight(110); }
protected:
void paintEvent(QPaintEvent *) override
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
QPainterPath frame;
frame.addRoundedRect(QRectF(rect()), 4, 4);
painter.setClipPath(frame);
painter.fillRect(rect(), QColor::fromRgbF(0, 0, 0, 0.35f));
const std::optional<FilterEdit> &edit = m_session.filterEdit();
if (!edit || !edit->rawPanel.scope)
return;
const CameraRawScope &scope = *edit->rawPanel.scope;
const QSizeF size = QSizeF(this->size());
if (edit->rawPanel.scopeMode == CameraRawScopeMode::histogram) {
const double peak = scope.peak();
if (!(peak > 0))
return;
// SwiftUI's red, green and blue, as the Levels histogram.
ribbon(painter, scope.red, QColor(255, 66, 69), peak, size);
ribbon(painter, scope.green, QColor(48, 209, 88), peak, size);
ribbon(painter, scope.blue, QColor(0, 145, 255), peak, size);
return;
}
const double peak = *std::max_element(scope.vectorscope.begin(), scope.vectorscope.end());
if (!(peak > 0))
return;
const double cell = size.width() / CameraRawScope::scopeSide;
for (size_t index = 0; index < scope.vectorscope.size(); ++index) {
if (!(scope.vectorscope[index] > 0))
continue;
const int column = int(index) % CameraRawScope::scopeSide, row = int(index) / CameraRawScope::scopeSide;
const double amount = std::min(1.0, scope.vectorscope[index] / peak);
painter.fillRect(QRectF(column * cell, size.height() - (row + 1) * cell, cell + 0.2, cell + 0.2),
QColor::fromRgbF(1, 1, 1, float(0.15 + 0.85 * amount)));
}
}
void contextMenuEvent(QContextMenuEvent *event) override
{
QMenu menu(this);
menu.addAction(QStringLiteral("Histogram"), this, [this] { mode(CameraRawScopeMode::histogram); });
menu.addAction(QStringLiteral("Vectorscope"), this, [this] { mode(CameraRawScopeMode::vectorscope); });
menu.exec(event->globalPos());
}
private:
static void ribbon(QPainter &painter, const std::array<double, 256> &bins, QColor colour, double peak, QSizeF size)
{
QPainterPath path(QPointF(0, size.height()));
for (size_t index = 0; index < bins.size(); ++index) {
const double height = size.height() * std::min(1.0, std::max(0.0, bins[index] / peak));
path.lineTo(double(index) * size.width() / double(bins.size()), size.height() - height);
}
path.lineTo(size.width(), size.height());
path.closeSubpath();
colour.setAlphaF(0.55f);
painter.fillPath(path, colour);
}
void mode(CameraRawScopeMode mode)
{
if (!m_session.filterEdit())
return;
CameraRawPanel panel = m_session.filterEdit()->rawPanel;
panel.scopeMode = mode;
m_session.setCameraRawPanel(panel);
}
EditorSession &m_session;
};
// Swift's clip button: a small triangle, lit when on.
class ClipButton : public QToolButton {
public:
ClipButton(bool shadows, QWidget *parent) : QToolButton(parent), m_shadows(shadows)
{
setFixedSize(14, 14);
setAutoRaise(true);
setObjectName(shadows ? QStringLiteral("shadowClipping") : QStringLiteral("highlightClipping"));
setToolTip(shadows ? QStringLiteral("Show clipped shadows in blue on the preview.") : QStringLiteral("Show clipped highlights in red on the preview."));
setAccessibleName(shadows ? QStringLiteral("Shadow Clipping Indicator") : QStringLiteral("Highlight Clipping Indicator"));
}
bool on = false;
protected:
void paintEvent(QPaintEvent *) override
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.setPen(Qt::NoPen);
painter.setBrush(on ? (m_shadows ? QColor(0, 145, 255) : QColor(255, 66, 69)) : QColor::fromRgbF(1, 1, 1, 0.55f));
painter.drawPolygon(QPolygonF({QPointF(7, 3), QPointF(12, 11), QPointF(2, 11)}));
}
private:
const bool m_shadows;
};
}
CameraRawControls::CameraRawControls(EditorSession &session, QWidget *parent)
: QWidget(parent), m_session(session), m_scope(new ScopeGraph(session, this)), m_readout(new QLabel(this))
{
m_scope->setObjectName(QStringLiteral("cameraRawScope"));
m_readout->setObjectName(QStringLiteral("cameraRawReadout"));
m_readout->setToolTip(QStringLiteral("Red, green, and blue of the pixel under the pointer."));
m_readout->setForegroundRole(QPalette::PlaceholderText);
QFont caption = m_readout->font();
caption.setPixelSize(10);
m_readout->setFont(caption);
auto *clips = new QHBoxLayout(m_scope);
clips->setContentsMargins(4, 4, 4, 4);
for (const bool shadows : {true, false}) {
auto *button = new ClipButton(shadows, m_scope);
connect(button, &QToolButton::clicked, this, [this, shadows] {
panel([shadows](CameraRawPanel &raw) {
bool &shown = shadows ? raw.showsShadowClipping : raw.showsHighlightClipping;
shown = !shown;
});
m_session.updateFilter(m_session.filterEdit().value().settings, m_session.filterEdit()->preview);
});
clips->addWidget(button);
if (shadows)
clips->addStretch(1);
}
auto *scopeColumn = new QVBoxLayout;
scopeColumn->setSpacing(4);
scopeColumn->addWidget(m_scope);
scopeColumn->addWidget(m_readout);
auto *content = new QWidget;
auto *groups = new QVBoxLayout(content);
groups->setContentsMargins(0, 0, 0, 0);
groups->setSpacing(12);
light(group(Section::light, groups));
color(group(Section::color, groups));
effects(group(Section::effects, groups));
// Curve to Calibration fill their groups with 12.8b2.
for (const Section section : {Section::curve, Section::colorMixer, Section::colorGrading, Section::detail, Section::optics, Section::geometry,
Section::calibration})
group(section, groups);
groups->addStretch(1);
auto *scroll = new QScrollArea(this);
scroll->setObjectName(QStringLiteral("cameraRawGroups"));
scroll->setWidgetResizable(true);
scroll->setFrameShape(QFrame::NoFrame);
scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scroll->setWidget(content);
auto *column = new QVBoxLayout(this);
column->setContentsMargins(0, 0, 0, 0);
column->setSpacing(10);
column->addLayout(scopeColumn);
column->addWidget(scroll, 1);
qApp->installEventFilter(this);
connect(&m_session, &EditorSession::changed, this, &CameraRawControls::synchronize);
synchronize();
}
QVBoxLayout *CameraRawControls::group(Section section, QVBoxLayout *column)
{
const QString name = title(int(section));
auto *box = new QWidget;
auto *header = new QHBoxLayout;
header->setSpacing(6);
auto *disclosure = new QToolButton(box);
disclosure->setObjectName(QString(name).remove(QLatin1Char(' ')) + QStringLiteral("Section"));
disclosure->setText(name);
disclosure->setAccessibleName(name);
disclosure->setAutoRaise(true);
disclosure->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
QFont headline = disclosure->font();
headline.setPixelSize(13);
headline.setWeight(QFont::DemiBold);
disclosure->setFont(headline);
auto *eye = new QToolButton(box);
eye->setObjectName(QString(name).remove(QLatin1Char(' ')) + QStringLiteral("Eye"));
eye->setAutoRaise(true);
connect(eye, &QToolButton::clicked, this, [this, section] { toggleShown(section); });
header->addWidget(disclosure);
header->addStretch(1);
header->addWidget(eye);
auto *body = new QWidget(box);
auto *rows = new QVBoxLayout(body);
rows->setContentsMargins(18, 0, 0, 0);
rows->setSpacing(8);
// Light and Color open first, Swift's `expanded`.
body->setVisible(section == Section::light || section == Section::color);
connect(disclosure, &QToolButton::clicked, this, [this, body] {
body->setVisible(body->isHidden());
synchronize();
});
auto *stack = new QVBoxLayout(box);
stack->setContentsMargins(0, 0, 0, 0);
stack->setSpacing(8);
stack->addLayout(header);
stack->addWidget(body);
column->addWidget(box);
m_groups.push_back(Group{section, disclosure, eye, body});
return rows;
}
void CameraRawControls::slider(QVBoxLayout *column, const QString &name, const QString &title, std::function<double &(CameraRawSettings &)> key,
double low, double high, int decimals, std::optional<CameraRawClipping> clipping, const QString &help,
CameraRawSliderTrack track, double reset)
{
auto *box = new QWidget;
auto *label = new QLabel(title, box);
label->setMinimumWidth(labelWidth);
label->setToolTip(help);
const double step = std::pow(10.0, decimals);
auto *slider = new CameraRawSlider(
low, high, track, help, [this, key, step, clipping](double value) { assign(key, std::round(value * step) / step, clipping); },
[this, key, reset] { assign(key, reset, std::nullopt); }, box);
slider->setObjectName(name + QStringLiteral("Slider"));
const size_t index = m_rows.size();
auto *field = new PickerField([this, index] {
PickerField &edited = *m_rows[index].field;
bool number = false;
const double typed = edited.locale().toDouble(edited.text(), &number);
if (edited.isModified() && number && std::isfinite(typed))
assign(m_rows[index].key, typed, std::nullopt);
edited.setModified(false);
synchronize();
}, nullptr, box);
field->setObjectName(name + QStringLiteral("Field"));
field->setAccessibleName(title);
field->setPlaceholderText(title);
field->setToolTip(help);
field->setAlignment(Qt::AlignRight);
field->setFixedWidth(56);
// Swift's double click on the title resets the slider.
label->setProperty("resetRow", qulonglong(index));
label->setProperty("resetValue", reset);
auto *row = new QHBoxLayout(box);
row->setContentsMargins(0, 0, 0, 0);
row->setSpacing(10);
row->addWidget(label);
row->addWidget(slider, 1);
row->addWidget(field);
box->setAccessibleName(title);
column->addWidget(box);
m_rows.push_back(Row{std::move(key), decimals, slider, field});
}
QLabel *CameraRawControls::subheadline(const QString &title, QVBoxLayout *column)
{
auto *label = new QLabel(title);
QFont font = label->font();
font.setPixelSize(11);
label->setFont(font);
column->addWidget(label);
return label;
}
// Swift's assign: Alt on a Light slider shows its clipping.
void CameraRawControls::assign(const std::function<double &(CameraRawSettings &)> &key, double value, std::optional<CameraRawClipping> clipping)
{
if (!m_session.filterEdit())
return;
const bool showClipping = clipping && QGuiApplication::keyboardModifiers().testFlag(Qt::AltModifier);
panel([&](CameraRawPanel &raw) { raw.clipping = showClipping ? clipping : std::nullopt; });
update([&](CameraRawSettings &settings) {
double &field = key(settings);
field = value;
if (&field == &settings.temperature || &field == &settings.tint)
settings.whiteBalance = CameraRawWhiteBalance::custom;
});
}
void CameraRawControls::update(const std::function<void(CameraRawSettings &)> &change)
{
FilterSettings settings = m_session.filterEdit() ? m_session.filterEdit()->settings : FilterSettings();
change(settings.cameraRaw);
m_session.updateFilter(settings, m_session.filterEdit() ? m_session.filterEdit()->preview : true);
}
void CameraRawControls::panel(const std::function<void(CameraRawPanel &)> &change)
{
if (!m_session.filterEdit())
return;
CameraRawPanel raw = m_session.filterEdit()->rawPanel;
change(raw);
m_session.setCameraRawPanel(raw);
}
void CameraRawControls::toggleShown(Section section)
{
if (!m_session.filterEdit())
return;
panel([section](CameraRawPanel &raw) {
bool &shown = shows(raw.shows, int(section));
shown = !shown;
});
m_session.updateFilter(m_session.filterEdit()->settings, m_session.filterEdit()->preview);
}
CameraRawSettings CameraRawControls::raw() const
{
return m_session.filterEdit() ? m_session.filterEdit()->settings.cameraRaw : CameraRawSettings();
}
bool CameraRawControls::eventFilter(QObject *watched, QEvent *event)
{
if (event->type() == QEvent::MouseButtonDblClick && watched->property("resetRow").isValid()) {
const Row &row = m_rows.at(size_t(watched->property("resetRow").toULongLong()));
assign(row.key, watched->property("resetValue").toDouble(), std::nullopt);
return true;
}
// Alt let go ends the clipping and sharpen-mask views.
if (event->type() == QEvent::KeyRelease && static_cast<QKeyEvent *>(event)->key() == Qt::Key_Alt && !static_cast<QKeyEvent *>(event)->isAutoRepeat()
&& m_session.filterEdit()) {
const CameraRawPanel &raw = m_session.filterEdit()->rawPanel;
if (raw.clipping || raw.sharpenMask) {
panel([](CameraRawPanel &shown) {
shown.clipping = std::nullopt;
shown.sharpenMask = false;
});
m_session.updateFilter(m_session.filterEdit()->settings, m_session.filterEdit()->preview);
}
}
return QWidget::eventFilter(watched, event);
}
void CameraRawControls::synchronize()
{
const CameraRawSettings settings = raw();
const std::optional<FilterEdit> &edit = m_session.filterEdit();
const qreal ratio = devicePixelRatioF();
const QColor ink = palette().color(QPalette::WindowText), secondary = palette().color(QPalette::PlaceholderText);
for (const Group &group : m_groups) {
group.disclosure->setIcon(QIcon(LayerIcons::pixmap(!group.body->isHidden() ? LayerIcon::chevronDown : LayerIcon::chevronRight, 12, ink, ratio)));
const bool adjusted = adjusts(settings, int(group.section));
group.eye->setVisible(adjusted);
CameraRawGroups groups = edit ? edit->rawPanel.shows : CameraRawGroups();
const bool shown = shows(groups, int(group.section));
const QString name = title(int(group.section));
group.eye->setIcon(QIcon(LayerIcons::pixmap(shown ? LayerIcon::eye : LayerIcon::eyeSlash, 14, ink, ratio)));
group.eye->setToolTip((shown ? QStringLiteral("Hide %1 in the preview") : QStringLiteral("Show %1 in the preview")).arg(name));
group.eye->setAccessibleName((shown ? QStringLiteral("Hide %1") : QStringLiteral("Show %1")).arg(name));
}
for (Row &row : m_rows) {
CameraRawSettings copy = settings;
const double value = row.key(copy);
row.slider->display(value);
if (!(row.field->hasFocus() && row.field->isModified()))
row.field->setText(shown(value, row.decimals, row.field->locale()));
}
const std::optional<std::array<int, 3>> readout = edit ? edit->rawPanel.readout : std::nullopt;
m_readout->setText(readout ? QStringLiteral("R %1 G %2 B %3").arg((*readout)[0]).arg((*readout)[1]).arg((*readout)[2])
: QStringLiteral("R — G — B —"));
const bool histogram = !edit || edit->rawPanel.scopeMode == CameraRawScopeMode::histogram;
m_scope->setToolTip(histogram ? QStringLiteral("Tones from black on the left to white on the right: blacks, shadows, midtones, highlights, whites. "
"Right-click to show the vectorscope.")
: QStringLiteral("Hue around the wheel, saturation outward from the center. Right-click to show the histogram."));
m_scope->setAccessibleName(histogram ? QStringLiteral("RGB histogram") : QStringLiteral("Vectorscope"));
for (auto *clip : m_scope->findChildren<ClipButton *>()) {
const bool shadows = clip->objectName() == QLatin1String("shadowClipping");
clip->on = edit && (shadows ? edit->rawPanel.showsShadowClipping : edit->rawPanel.showsHighlightClipping);
clip->update();
}
m_scope->update();
if (m_whiteBalance) {
const QSignalBlocker quiet(m_whiteBalance);
m_whiteBalance->setCurrentIndex(int(settings.whiteBalance));
const bool sampling = edit && edit->rawPanel.samplesWhiteBalance;
m_whiteBalanceSampler->setIcon(EyedropperIcon::icon(sampling ? palette().color(QPalette::Highlight) : secondary, ratio));
m_whiteBalanceHint->setVisible(sampling);
}
if (m_glowStyle) {
const QSignalBlocker glow(m_glowStyle), vignette(m_vignetteStyle);
m_glowStyle->setCurrentIndex(int(settings.glowStyle));
m_vignetteStyle->setCurrentIndex(int(settings.vignetteStyle));
}
}
+63
View File
@@ -0,0 +1,63 @@
#pragma once
#include "Document/EditorSession.h"
#include "UI/CameraRawSlider.h"
#include <QWidget>
class PickerField;
class QComboBox;
class QLabel;
class QToolButton;
class QVBoxLayout;
// Swift's CameraRawControls: the scope, then the ten groups.
class CameraRawControls : public QWidget {
Q_OBJECT
public:
explicit CameraRawControls(EditorSession &session, QWidget *parent = nullptr);
// Shows the edit as it now stands.
void synchronize();
static constexpr int labelWidth = 96;
protected:
// Swift's option monitor: Alt let go ends the views.
bool eventFilter(QObject *watched, QEvent *event) override;
private:
enum class Section { light, color, effects, curve, colorMixer, colorGrading, detail, optics, geometry, calibration };
struct Group {
Section section;
QToolButton *disclosure;
QToolButton *eye;
QWidget *body;
};
struct Row {
std::function<double &(CameraRawSettings &)> key;
int decimals;
CameraRawSlider *slider;
PickerField *field;
};
QVBoxLayout *group(Section section, QVBoxLayout *column);
// Swift's slider: a title, the slider and an exact field.
void slider(QVBoxLayout *column, const QString &name, const QString &title, std::function<double &(CameraRawSettings &)> key, double low, double high, int decimals,
std::optional<CameraRawClipping> clipping, const QString &help, CameraRawSliderTrack track = {}, double reset = 0);
QLabel *subheadline(const QString &title, QVBoxLayout *column);
void light(QVBoxLayout *column);
void color(QVBoxLayout *column);
void effects(QVBoxLayout *column);
void assign(const std::function<double &(CameraRawSettings &)> &key, double value, std::optional<CameraRawClipping> clipping);
void update(const std::function<void(CameraRawSettings &)> &change);
void panel(const std::function<void(CameraRawPanel &)> &change);
void toggleShown(Section section);
CameraRawSettings raw() const;
EditorSession &m_session;
QWidget *const m_scope;
QLabel *const m_readout;
std::vector<Group> m_groups;
std::vector<Row> m_rows;
QComboBox *m_whiteBalance = nullptr;
QToolButton *m_whiteBalanceSampler = nullptr;
QLabel *m_whiteBalanceHint = nullptr;
QComboBox *m_glowStyle = nullptr;
QComboBox *m_vignetteStyle = nullptr;
};
+99
View File
@@ -0,0 +1,99 @@
#include "UI/CameraRawSlider.h"
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QStyleOptionSlider>
#include <cmath>
namespace {
// Thousandths of the range, as the filter sheet's sliders.
constexpr int travel = 1000;
QColor hsb(double degrees, double saturation, double brightness)
{
double turns = degrees / 360;
turns -= std::floor(turns);
return QColor::fromHsvF(float(turns), float(saturation), float(brightness));
}
}
std::optional<std::pair<QColor, 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));
}
throw std::logic_error("unknown slider track");
}
CameraRawSlider::CameraRawSlider(double low, double high, CameraRawSliderTrack track, const QString &help, std::function<void(double)> change,
std::function<void()> reset, QWidget *parent)
: QSlider(Qt::Horizontal, parent), m_low(low), m_high(high), m_track(track), m_change(std::move(change)), m_reset(std::move(reset))
{
setRange(0, travel);
setMinimumWidth(60);
setFixedHeight(22);
setToolTip(help);
setAccessibleName(help);
connect(this, &QSlider::valueChanged, this, [this] { m_change(shown()); });
}
void CameraRawSlider::display(double value)
{
// Swift's isTrackingValue: a drag keeps its own knob.
if (isSliderDown())
return;
const QSignalBlocker quiet(this);
setValue(int(std::lround((value - m_low) / (m_high - m_low) * travel)));
}
double CameraRawSlider::shown() const
{
return m_low + (m_high - m_low) * value() / travel;
}
bool CameraRawSlider::isOnKnob(QPoint point) const
{
QStyleOptionSlider option;
initStyleOption(&option);
return style()->subControlRect(QStyle::CC_Slider, &option, QStyle::SC_SliderHandle, this).adjusted(-2, -2, 2, 2).contains(point);
}
// 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();
if (!colors) {
QSlider::paintEvent(event);
return;
}
QStyleOptionSlider option;
initStyleOption(&option);
const QRect groove = style()->subControlRect(QStyle::CC_Slider, &option, QStyle::SC_SliderGroove, this);
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
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);
QPainterPath shape;
shape.addRoundedRect(bar, height / 2, height / 2);
painter.fillPath(shape, gradient);
option.subControls = QStyle::SC_SliderHandle;
style()->drawComplexControl(QStyle::CC_Slider, &option, &painter, this);
}
void CameraRawSlider::mouseDoubleClickEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton && isOnKnob(event->position().toPoint())) {
m_reset();
return;
}
QSlider::mouseDoubleClickEvent(event);
}
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include <QColor>
#include <QSlider>
#include <functional>
#include <optional>
#include <utility>
// Swift's CameraRawSliderTrack: a coloured track, or the system's.
struct CameraRawSliderTrack {
enum class Kind { plain, temperature, tint, chroma, hue, saturation, luminance };
Kind kind = Kind::plain;
// A family's centre, for hue, saturation and luminance.
double degrees = 0;
// Left and right colours; none keeps the system track.
std::optional<std::pair<QColor, QColor>> colors() const;
};
// Swift's CameraRawSlider: a double click on the knob resets.
class CameraRawSlider : public QSlider {
Q_OBJECT
public:
CameraRawSlider(double low, double high, CameraRawSliderTrack track, const QString &help, std::function<void(double)> change,
std::function<void()> reset, QWidget *parent = nullptr);
// Shows a value, unless a drag holds the knob.
void display(double value);
double shown() const;
bool isOnKnob(QPoint point) const;
protected:
void paintEvent(QPaintEvent *event) override;
void mouseDoubleClickEvent(QMouseEvent *event) override;
private:
const double m_low;
const double m_high;
const CameraRawSliderTrack m_track;
const std::function<void(double)> m_change;
const std::function<void()> m_reset;
};
+5 -3
View File
@@ -2,7 +2,9 @@
#include "UI/KeyboardShortcuts.h"
#include "UI/ColorPaletteControls.h"
#include "UI/ColorPickerSheet.h"
#include "UI/CameraRawControls.h"
#include "UI/CurvesControls.h"
#include "UI/FloatingPanel.h"
#include <QButtonGroup>
#include <QCheckBox>
#include <QFrame>
@@ -147,7 +149,8 @@ FilterSheet::FilterSheet(EditorSession &session, QWidget *parent)
m_spinner(new QProgressBar(this)), m_activity(words(QString(), true, this)), m_cancel(new QPushButton(QStringLiteral("Cancel"), this)),
m_ok(new QPushButton(QStringLiteral("OK"), this))
{
setFixedWidth(380);
const bool cameraRaw = m_session.filterEdit() && m_session.filterEdit()->kind == FilterKind::cameraRaw;
setFixedWidth(cameraRaw ? FloatingPanel::dockedWidth : 380);
m_column->setContentsMargins(24, 24, 24, 24);
m_column->setSpacing(16);
const auto key = [](double FilterSettings::*field) { return [field](FilterSettings &settings) -> double & { return settings.*field; }; };
@@ -182,8 +185,7 @@ FilterSheet::FilterSheet(EditorSession &session, QWidget *parent)
break;
case FilterKind::removeBackground: background(); break;
case FilterKind::blackWhite: blackWhite(); break;
// CameraRawControls arrive with the panel (12.8b).
case FilterKind::cameraRaw: break;
case FilterKind::cameraRaw: m_column->addWidget(new CameraRawControls(m_session, this), 1); break;
case FilterKind::colorBalance: colorBalance(); break;
case FilterKind::contentAwareFill:
m_column->addWidget(words(QStringLiteral("Fill the selection using surrounding pixels from this layer."), false, this));
+63 -9
View File
@@ -4,6 +4,9 @@
#include <QCloseEvent>
#include <QDialog>
#include <QHash>
#include <QLabel>
#include <QStyle>
#include <QToolButton>
#include <QVBoxLayout>
namespace {
@@ -39,7 +42,8 @@ protected:
void moveEvent(QMoveEvent *event) override
{
QDialog::moveEvent(event);
if (isVisible())
// A docked frame is the window's: never remembered.
if (isVisible() && isWindow())
m_owner.remember();
}
@@ -52,22 +56,22 @@ FloatingPanel::FloatingPanel(const QString &name, QWidget &owner) : m_name(name)
FloatingPanel::~FloatingPanel()
{
delete m_panel;
delete m_docked;
}
void FloatingPanel::show(const QString &title, QWidget *content)
void FloatingPanel::show(const QString &title, QWidget *content, QWidget *dock)
{
if (dock) {
showDocked(title, content, *dock);
return;
}
if (!m_panel) {
m_panel = new PanelWindow(*this, m_owner.window());
m_panel->setObjectName(m_name);
}
const bool wasVisible = m_panel->isVisible();
m_panel->setWindowTitle(title);
// Later: a signal from the old content may still run.
if (m_content) {
m_content->hide();
m_content->deleteLater();
}
m_content = content;
place(content);
m_panel->layout()->addWidget(content);
// Shown now: layouts show late children from the event loop.
content->show();
@@ -86,16 +90,61 @@ void FloatingPanel::show(const QString &title, QWidget *content)
remember();
}
// Wayland lets no client place a window: a slot instead.
void FloatingPanel::showDocked(const QString &title, QWidget *content, QWidget &dock)
{
if (!m_docked) {
m_docked = new PanelWindow(*this, &dock);
m_docked->setWindowFlags(Qt::Widget);
m_docked->setObjectName(m_name);
m_docked->layout()->setSizeConstraint(QLayout::SetDefaultConstraint);
auto *header = new QWidget(m_docked);
auto *row = new QHBoxLayout(header);
row->setContentsMargins(12, 8, 8, 0);
auto *heading = new QLabel(header);
heading->setObjectName(QStringLiteral("dockedTitle"));
auto *closer = new QToolButton(header);
closer->setObjectName(QStringLiteral("dockedClose"));
closer->setIcon(closer->style()->standardIcon(QStyle::SP_TitleBarCloseButton));
closer->setAccessibleName(QStringLiteral("Close"));
closer->setAutoRaise(true);
QObject::connect(closer, &QToolButton::clicked, m_docked, &QWidget::close);
row->addWidget(heading, 1);
row->addWidget(closer);
m_docked->layout()->addWidget(header);
dock.layout()->addWidget(m_docked);
}
m_docked->findChild<QLabel *>(QStringLiteral("dockedTitle"))->setText(title);
place(content);
m_docked->layout()->addWidget(content);
content->show();
m_docked->show();
dock.show();
m_docked->setFocus();
}
// Later: a signal from the old content may still run.
void FloatingPanel::place(QWidget *content)
{
if (m_content) {
m_content->hide();
m_content->deleteLater();
}
m_content = content;
}
// Moves were remembered as they came; hide sends no close.
void FloatingPanel::close()
{
if (m_panel)
m_panel->hide();
if (m_docked)
m_docked->parentWidget()->hide();
}
bool FloatingPanel::isVisible() const
{
return m_panel && m_panel->isVisible();
return (m_panel && m_panel->isVisible()) || (m_docked && m_docked->isVisible());
}
void FloatingPanel::refocus(const QString &name)
@@ -103,6 +152,11 @@ void FloatingPanel::refocus(const QString &name)
for (QWidget *widget : QApplication::topLevelWidgets()) {
if (widget->objectName() == name && widget->isVisible())
widget->activateWindow();
// A docked panel takes the keys inside its window.
for (QDialog *docked : widget->findChildren<QDialog *>(name)) {
if (docked->isVisible() && !docked->isWindow())
docked->setFocus();
}
}
}
+7 -2
View File
@@ -13,13 +13,15 @@ public:
~FloatingPanel();
FloatingPanel(const FloatingPanel &) = delete;
FloatingPanel &operator=(const FloatingPanel &) = delete;
// New content and title; a shown panel keeps its place.
void show(const QString &title, QWidget *content);
// New content and title; a dock slot takes it docked.
void show(const QString &title, QWidget *content, QWidget *dock = nullptr);
// Hides the panel without reporting a close.
void close();
bool isVisible() const;
// Its close button and Escape report here, as Cancel.
std::function<void()> onClose;
// Swift's docked width: Camera Raw's grading wheels fit it.
static constexpr int dockedWidth = 440;
// Hands the keys back to a shown panel, after clicks.
static void refocus(const QString &name);
@@ -27,9 +29,12 @@ private:
friend class PanelWindow;
void remember();
void closed();
void place(QWidget *content);
void showDocked(const QString &title, QWidget *content, QWidget &dock);
const QString m_name;
QWidget &m_owner;
QPointer<QDialog> m_panel;
QPointer<QWidget> m_docked;
QPointer<QWidget> m_content;
};
+1 -15
View File
@@ -213,20 +213,6 @@ void dragHandle(LevelRange &range, bool output, size_t index, double tone)
range.gamma = std::log(std::min(0.999, std::max(0.001, (x - range.black) / (range.white - range.black)))) / std::log(0.5);
}
// The eyedropper glyph in one ink, 14 points.
QIcon eyedropper(const QColor &ink, double ratio)
{
QPixmap pixmap(QSize(14, 14) * ratio);
pixmap.setDevicePixelRatio(ratio);
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
painter.setRenderHint(QPainter::Antialiasing);
painter.scale(14.0 / 18, 14.0 / 18);
painter.setPen(QPen(ink, 1.5, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
EyedropperIcon::paint(painter);
return QIcon(pixmap);
}
// Swift's HStack: spread by Spacers, or packed to the left.
QHBoxLayout *row(std::initializer_list<QWidget *> widgets, bool spread)
{
@@ -411,7 +397,7 @@ void LevelsSheet::synchronize()
for (size_t index = 0; index < 3; ++index) {
const bool chosen = edit->sampleMode == allLevelsSamples[index];
m_samples[index]->setChecked(chosen);
m_samples[index]->setIcon(eyedropper(palette().color(chosen ? QPalette::Highlight : QPalette::PlaceholderText), devicePixelRatioF()));
m_samples[index]->setIcon(EyedropperIcon::icon(palette().color(chosen ? QPalette::Highlight : QPalette::PlaceholderText), devicePixelRatioF()));
m_autos[index]->setEnabled(edit->histogramReady);
}
m_sampleHint->setVisible(edit->sampleMode.has_value());