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

+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));
}
}