#include "UI/CameraRawControls.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 #include #include #include #include #include #include #include #include #include #include #include namespace { QString title(int section) { static const std::array 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 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 &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 &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)); group(Section::curve, groups)->addWidget(new CameraRawCurveControls(session)); group(Section::colorMixer, groups)->addWidget(new CameraRawMixerControls(session)); group(Section::colorGrading, groups)->addWidget(new CameraRawGradingControls(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 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 key, double low, double high, int decimals, std::optional 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}, [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 &key, double value, std::optional 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 &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 &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(event)->key() == Qt::Key_Alt && !static_cast(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 &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> 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()) { 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)); } }