Files
OmaPhoto/src/UI/CameraRawControls.cpp
T

409 lines
18 KiB
C++

#include "UI/CameraRawControls.h"
#include "UI/ColorPickerSheet.h"
#include "UI/CameraRawColorControls.h"
#include "UI/CameraRawDetailOpticsControls.h"
#include "UI/CameraRawGeometryCalibrationControls.h"
#include "UI/CameraRawRow.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("Color Grading"),
QStringLiteral("Effects"), QStringLiteral("Curve"), QStringLiteral("Color Mixer"),
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::grading, &CameraRawGroups::effects,
&CameraRawGroups::curve, &CameraRawGroups::mixer, &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.grading.adjusts();
case 3: return raw.adjustsEffects();
case 4: return raw.curve.adjusts();
case 5: return raw.mixer.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 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));
group(Section::colorGrading, groups)->addWidget(new CameraRawGradingControls(session));
effects(group(Section::effects, groups));
group(Section::curve, groups)->addWidget(new CameraRawCurveControls(session));
group(Section::colorMixer, groups)->addWidget(new CameraRawMixerControls(session));
group(Section::detail, groups)->addWidget(new CameraRawDetailControls(session));
group(Section::optics, groups)->addWidget(new CameraRawOpticsControls(session));
group(Section::geometry, groups)->addWidget(new CameraRawGeometryControls(session));
group(Section::calibration, groups)->addWidget(new CameraRawCalibrationControls(session));
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, Color and Color Grading open first, Swift's `expanded`.
body->setVisible(section == Section::light || section == Section::color || section == Section::colorGrading);
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)
{
const double step = std::pow(10.0, decimals);
auto *row = new CameraRawRow(
{.name = name, .title = title, .help = help, .low = low, .high = high, .decimals = decimals, .track = track, .titleResets = true, .scrub = 1 / step},
[this, key] {
CameraRawSettings settings = raw();
return key(settings);
},
[this, key, step, clipping](double value) { assign(key, std::round(value * step) / step, clipping); },
[this, key](double typed) { assign(key, typed, std::nullopt); }, [this, key, reset] { assign(key, reset, std::nullopt); });
column->addWidget(row);
m_rows.push_back(row);
}
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)
{
// 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 (CameraRawRow *row : m_rows)
row->synchronize();
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"));
// Found as QToolButtons: newer Qt finds only Q_OBJECT classes.
for (const bool shadows : {true, false}) {
auto *clip = static_cast<ClipButton *>(m_scope->findChild<QToolButton *>(shadows ? QStringLiteral("shadowClipping") : QStringLiteral("highlightClipping")));
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));
}
}
CameraRawControls::~CameraRawControls()
{
releaseFocus(*this);
}