Black & White, Color Balance and Invert adjustments (12.7b)

This commit is contained in:
雷电芽衣 committed 2026-09-27 16:00:06 -04:00
1 parent 7d47cc367a
commit 4291f4249a
33 files changed
+819 -52

No files matched your search

+13 -8
View File
@@ -18,14 +18,14 @@ NativeLayerList *focusedList()
// An entry's name: Hue/Saturation is newHueSaturationAdjustment.
QString adjustmentName(AdjustmentKind kind)
{
return QStringLiteral("new%1Adjustment").arg(rawValue(kind).remove(QLatin1Char(' ')).remove(QLatin1Char('/')));
return QStringLiteral("new%1Adjustment").arg(rawValue(kind).remove(QLatin1Char(' ')).remove(QLatin1Char('/')).remove(QLatin1Char('&')));
}
// A filter's entry from Swift's title: gaussianBlur.
QString filterName(FilterKind kind)
{
QString name = rawValue(kind);
name.remove(QLatin1Char(' '));
name.remove(QLatin1Char(' ')).remove(QLatin1Char('&'));
name[0] = name[0].toLower();
return name;
}
@@ -203,8 +203,8 @@ CompositorMenus::CompositorMenus(ProjectWorkspace &workspace, QMenuBar &bar, QWi
add(image, QStringLiteral("levels"), QStringLiteral("Levels…"), QKeySequence(Qt::CTRL | Qt::Key_L), [this] { session().beginLevels(); });
add(image, QStringLiteral("hueSaturation"), QStringLiteral("Hue/Saturation…"), QKeySequence(Qt::CTRL | Qt::Key_U),
[this] { session().beginHueSaturation(); });
for (const FilterKind kind : {FilterKind::exposure, FilterKind::gradientMap, FilterKind::grain})
add(image, filterName(kind), rawValue(kind) + QStringLiteral("…"), QKeySequence(), [this, kind] { session().beginFilter(kind); });
for (const FilterKind kind : {FilterKind::blackWhite, FilterKind::colorBalance, FilterKind::exposure, FilterKind::gradientMap, FilterKind::grain})
add(image, filterName(kind), rawValue(kind).replace(QLatin1Char('&'), QStringLiteral("&&")) + QStringLiteral("…"), QKeySequence(), [this, kind] { session().beginFilter(kind); });
add(image, QStringLiteral("invert"), QStringLiteral("Invert"), QKeySequence(Qt::CTRL | Qt::Key_I), [this] { session().invertPixels(); });
image->addSeparator();
add(image, QStringLiteral("canvasSize"), QStringLiteral("Canvas Size…"), QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_C), [this] { projects().canvasSize(); });
@@ -223,7 +223,9 @@ CompositorMenus::CompositorMenus(ProjectWorkspace &workspace, QMenuBar &bar, QWi
QMenu *adjustments = layer->addMenu(QStringLiteral("New Adjustment Layer"));
adjustments->menuAction()->setObjectName(QStringLiteral("newAdjustmentLayer"));
for (const AdjustmentKind kind : allAdjustmentKinds)
add(adjustments, adjustmentName(kind), rawValue(kind) + QStringLiteral("…"), QKeySequence(), [this, kind] { session().addAdjustment(kind); });
// A menu reads a lone ampersand as a mnemonic: doubled.
add(adjustments, adjustmentName(kind), rawValue(kind).replace(QLatin1Char('&'), QStringLiteral("&&")) + (isEditable(kind) ? QStringLiteral("…") : QString()), QKeySequence(),
[this, kind] { session().addAdjustment(kind); });
add(layer, QStringLiteral("editAdjustment"), QStringLiteral("Edit Adjustment…"), QKeySequence(),
[this] { session().setAdjustmentEditingID(session().activeLayerID()); });
layer->addSeparator();
@@ -323,10 +325,12 @@ void CompositorMenus::synchronize()
action(QStringLiteral("exportPNG"))->setEnabled(drawn && p.canStart());
action(QStringLiteral("exportJPEG"))->setEnabled(drawn && p.canStart());
action(QStringLiteral("closeProject"))->setEnabled(p.canStart());
// A step's ampersand, doubled: a lone one is a mnemonic.
const auto shown = [](QString name) { return name.replace(QLatin1Char('&'), QStringLiteral("&&")); };
// In a field, Undo is the field's and bare.
action(QStringLiteral("undo"))->setText(!typing && s.canUndo() ? QStringLiteral("Undo %1").arg(s.history.undoName()) : QStringLiteral("Undo"));
action(QStringLiteral("undo"))->setText(!typing && s.canUndo() ? QStringLiteral("Undo %1").arg(shown(s.history.undoName())) : QStringLiteral("Undo"));
action(QStringLiteral("undo"))->setEnabled(typing || s.canUndo());
action(QStringLiteral("redo"))->setText(!typing && s.canRedo() ? QStringLiteral("Redo %1").arg(s.history.redoName()) : QStringLiteral("Redo"));
action(QStringLiteral("redo"))->setText(!typing && s.canRedo() ? QStringLiteral("Redo %1").arg(shown(s.history.redoName())) : QStringLiteral("Redo"));
action(QStringLiteral("redo"))->setEnabled(typing || s.canRedo());
for (const char *name : {"fit", "actualPixels", "zoomIn", "zoomOut"})
action(QString::fromLatin1(name))->setEnabled(drawn);
@@ -364,7 +368,8 @@ void CompositorMenus::synchronize()
action(QStringLiteral("flipCanvasHorizontal"))->setEnabled(s.canEditLayers());
action(QStringLiteral("flipCanvasVertical"))->setEnabled(s.canEditLayers());
action(QStringLiteral("newAdjustmentLayer"))->setEnabled(s.canEditLayers());
action(QStringLiteral("editAdjustment"))->setEnabled(s.canEditLayers() && active && active->adjustment);
// Beyond Swift: Invert has no editor, so nothing to open.
action(QStringLiteral("editAdjustment"))->setEnabled(s.canEditLayers() && active && active->adjustment && isEditable(active->adjustment->kind));
action(QStringLiteral("transformLayer"))->setText(s.canTransformSelection() ? QStringLiteral("Transform Selection") : QStringLiteral("Transform Layer"));
action(QStringLiteral("transformLayer"))->setEnabled(s.canTransform() || s.canTransformSelection());
action(QStringLiteral("clippingMask"))->setText(active && active->maskSourceID ? QStringLiteral("Release Clipping Mask") : QStringLiteral("Create Clipping Mask"));
+64
View File
@@ -0,0 +1,64 @@
#include "UI/FilterSheet.h"
#include <QCheckBox>
#include <QLabel>
#include <QSlider>
#include <QVBoxLayout>
// Black & White's and Color Balance's rows, as Swift's FilterSheet.
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)
control(title, [field](FilterSettings &settings) -> double & { return settings.blackWhite.*field; }, BlackWhiteSettings::low,
BlackWhiteSettings::high, QStringLiteral("%"), 0, false);
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);
control(QStringLiteral("Saturation"), [](FilterSettings &settings) -> double & { return settings.blackWhite.tintSaturation; }, 0, 100,
QStringLiteral("%"), 0, false);
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; });
}
void FilterSheet::colorBalance()
{
const std::vector<std::pair<QString, std::array<double ColorBalanceSettings::*, 3>>> ranges{
{QStringLiteral("Shadows"),
{&ColorBalanceSettings::shadowCyanRed, &ColorBalanceSettings::shadowMagentaGreen, &ColorBalanceSettings::shadowYellowBlue}},
{QStringLiteral("Midtones"), {&ColorBalanceSettings::midCyanRed, &ColorBalanceSettings::midMagentaGreen, &ColorBalanceSettings::midYellowBlue}},
{QStringLiteral("Highlights"),
{&ColorBalanceSettings::highlightCyanRed, &ColorBalanceSettings::highlightMagentaGreen, &ColorBalanceSettings::highlightYellowBlue}}};
const std::array<QString, 3> pairs{QStringLiteral("Cyan / Red"), QStringLiteral("Magenta / Green"), QStringLiteral("Yellow / Blue")};
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);
}
flag(QStringLiteral("Preserve Luminosity"), QStringLiteral("Put each pixel's brightness back afterwards, so only the color moves"),
[](FilterSettings &settings) -> bool & { return settings.colorBalance.preserveLuminosity; });
}
void FilterSheet::flag(const QString &title, const QString &help, std::function<bool &(FilterSettings &)> key)
{
auto *box = new QCheckBox(title, this);
box->setToolTip(help);
connect(box, &QCheckBox::clicked, this, [this, key](bool on) { update([&key, on](FilterSettings &settings) { key(settings) = on; }); });
m_column->addWidget(box);
m_flags.emplace_back(box, std::move(key));
}
void FilterSheet::headline(const QString &title)
{
auto *label = new QLabel(title, this);
QFont font = label->font();
font.setPixelSize(13);
font.setWeight(QFont::DemiBold);
label->setFont(font);
m_column->addWidget(label);
}
+9 -3
View File
@@ -181,6 +181,8 @@ FilterSheet::FilterSheet(EditorSession &session, QWidget *parent)
GrainSettings::roughnessLow, GrainSettings::roughnessHigh, QString(), 0, false);
break;
case FilterKind::removeBackground: background(); break;
case FilterKind::blackWhite: blackWhite(); 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));
break;
@@ -356,7 +358,7 @@ void FilterSheet::background()
for (qsizetype index = 0; index < helps.size(); ++index) {
QWidget *advanced = m_controls[size_t(index)].slider->parentWidget();
advanced->setToolTip(helps[index]);
m_advanced.push_back(advanced);
m_shownWhen.emplace_back(advanced, [](const FilterSettings &settings) { return settings.backgroundQuality == BackgroundQuality::advanced; });
}
}
@@ -409,8 +411,12 @@ void FilterSheet::synchronize()
m_monochromatic->setChecked(edit.settings.monochromatic);
if (m_quality)
m_quality->button(int(edit.settings.backgroundQuality))->setChecked(true);
for (QWidget *advanced : m_advanced)
advanced->setVisible(edit.settings.backgroundQuality == BackgroundQuality::advanced);
for (auto &[box, key] : m_flags) {
FilterSettings copy = edit.settings;
box->setChecked(key(copy));
}
for (const auto &[row, shows] : m_shownWhen)
row->setVisible(shows(edit.settings));
m_preview->setChecked(edit.preview);
m_error->setText(edit.previewError.value_or(QString()));
m_error->setVisible(edit.previewError.has_value());
+10 -2
View File
@@ -55,6 +55,13 @@ private:
void noise();
// Remove Background's words, Quality, and Advanced's three rows.
void background();
// Black & White's six families and Tint; Color Balance's nine.
void blackWhite();
void colorBalance();
// Swift's Toggle over a flag, with its help.
void flag(const QString &title, const QString &help, std::function<bool &(FilterSettings &)> key);
// Swift's headline over a group of rows.
void headline(const QString &title);
// Swift's `settings` and `update`: the defaults once the edit goes.
FilterSettings settings() const;
void update(const std::function<void(FilterSettings &)> &change);
@@ -68,8 +75,9 @@ private:
QButtonGroup *m_distribution = nullptr;
QCheckBox *m_monochromatic = nullptr;
QButtonGroup *m_quality = nullptr;
// Shown while the quality is Advanced, as Swift's `if`.
std::vector<QWidget *> m_advanced;
std::vector<std::pair<QCheckBox *, std::function<bool &(FilterSettings &)>>> m_flags;
// Rows under Swift's `if`: Advanced's, Tint's.
std::vector<std::pair<QWidget *, std::function<bool(const FilterSettings &)>>> m_shownWhen;
QCheckBox *const m_preview;
QLabel *const m_error;
QLabel *const m_limited;
+45
View File
@@ -93,6 +93,45 @@ void halfFilledCircle(QPainter &painter)
painter.drawEllipse(QPointF(9, 9), 7, 7);
}
// Swift's circle.righthalf.filled: the other half, Invert's.
void rightHalfCircle(QPainter &painter)
{
QPainterPath half;
half.moveTo(9, 2);
half.arcTo(QRectF(2, 2, 14, 14), 90, -180);
half.closeSubpath();
painter.fillPath(half, painter.pen().color());
painter.drawEllipse(QPointF(9, 9), 7, 7);
}
// Swift's circle.filled.pattern.diagonalline.rectangle: Black & White's.
void hatchedCircle(QPainter &painter)
{
const QRectF frame(2, 4, 14, 10);
painter.drawRoundedRect(frame, 2, 2);
painter.save();
painter.setClipRect(frame);
for (double x = -6; x < 16; x += 3)
painter.drawLine(QPointF(x, 14), QPointF(x + 10, 4));
painter.restore();
painter.setBrush(painter.pen().color());
painter.drawEllipse(QPointF(9, 9), 3.2, 3.2);
}
// Swift's scale.3d: three axes from one corner, Color Balance's.
void axes(QPainter &painter)
{
const QPointF origin(7, 11);
const auto arrow = [&](QPointF tip, QPointF back1, QPointF back2) {
painter.drawLine(origin, tip);
painter.drawLine(tip, back1);
painter.drawLine(tip, back2);
};
arrow(QPointF(7, 2.5), QPointF(5.2, 4.5), QPointF(8.8, 4.5));
arrow(QPointF(16, 11), QPointF(14, 9.2), QPointF(14, 12.8));
arrow(QPointF(2.5, 15.5), QPointF(2.6, 12.9), QPointF(5.1, 15.4));
}
// Swift's slider.horizontal.3: three tracks, each with a knob.
void sliders(QPainter &painter)
{
@@ -201,6 +240,9 @@ void LayerIcons::paint(QPainter &painter, LayerIcon icon, QPointF origin, double
case LayerIcon::paintPalette: paintPalette(painter); break;
case LayerIcon::circleGrid: circleGrid(painter); break;
case LayerIcon::sparkles: sparkles(painter); break;
case LayerIcon::rightHalfCircle: rightHalfCircle(painter); break;
case LayerIcon::hatchedCircle: hatchedCircle(painter); break;
case LayerIcon::axes: axes(painter); break;
}
painter.restore();
}
@@ -224,6 +266,9 @@ LayerIcon LayerIcons::symbol(AdjustmentKind kind)
case AdjustmentKind::exposure: return LayerIcon::plusMinusCircle;
case AdjustmentKind::gradientMap: return LayerIcon::paintPalette;
case AdjustmentKind::grain: return LayerIcon::circleGrid;
case AdjustmentKind::invert: return LayerIcon::rightHalfCircle;
case AdjustmentKind::blackWhite: return LayerIcon::hatchedCircle;
case AdjustmentKind::colorBalance: return LayerIcon::axes;
}
throw std::logic_error("unknown adjustment kind");
}
+1 -1
View File
@@ -7,7 +7,7 @@
// The layers panel's icons, drawn: SF Symbols have no twin.
enum class LayerIcon {
eye, eyeSlash, chevronRight, chevronDown, folder, link, newLayer, newFolder, addMask, trash, layers, text,
halfFilledCircle, sliders, curvePath, plusMinusCircle, paintPalette, circleGrid, sparkles
halfFilledCircle, sliders, curvePath, plusMinusCircle, paintPalette, circleGrid, sparkles, rightHalfCircle, hatchedCircle, axes
};
namespace LayerIcons {
+1 -1
View File
@@ -93,7 +93,7 @@ LayersPanel::LayersPanel(EditorSession &session, QWidget *parent)
m_adjustments = footerButton(QStringLiteral("addAdjustment"), QStringLiteral("New adjustment layer"), QStringLiteral("New adjustment layer"), [] {});
auto *kinds = new QMenu(m_adjustments);
for (const AdjustmentKind kind : allAdjustmentKinds)
kinds->addAction(rawValue(kind), this, [this, kind] { m_session.addAdjustment(kind); });
kinds->addAction(rawValue(kind).replace(QLatin1Char('&'), QStringLiteral("&&")), this, [this, kind] { m_session.addAdjustment(kind); });
m_adjustments->setMenu(kinds);
m_adjustments->setPopupMode(QToolButton::InstantPopup);
footer->addWidget(m_adjustments);
+2 -1
View File
@@ -120,6 +120,7 @@ void LayerCell::configure(const ImageLayer &layer, bool enabled, int depth, bool
const QSizeF canvas = session.document() ? session.document()->size() : QSizeF(1, 1);
m_editableText = layer.liveText().has_value();
m_isAdjustment = layer.adjustment.has_value();
m_editableAdjustment = m_isAdjustment && isEditable(layer.adjustment->kind);
m_thumbnailSize = layer.isGroup || m_editableText || m_isAdjustment ? QSize(36, 36) : CanvasThumbnail::fittedSize(canvas, 36);
const ThumbnailKey key{layer.asset ? std::optional(layer.asset->identity()) : std::nullopt, layer.transform, canvas, ink, m_editableText};
if (m_layerID != layer.id || m_thumbnailKey != key) {
@@ -306,7 +307,7 @@ void LayerCell::mouseDoubleClickEvent(QMouseEvent *event)
session.editActiveText();
return;
}
if (m_isAdjustment) {
if (m_editableAdjustment) {
session.setAdjustmentEditingID(m_layerID);
return;
}
+6
View File
@@ -155,6 +155,12 @@ void NativeLayerList::focusList()
setFocus(Qt::MouseFocusReason);
}
// Closing hides it: a rename's focus-out commits, unheard here.
NativeLayerList::~NativeLayerList()
{
disconnect(&m_session, &EditorSession::changed, this, &NativeLayerList::update);
}
// Tab reaches the keys, not the focus chain, as Swift.
bool NativeLayerList::event(QEvent *event)
{
+3
View File
@@ -166,6 +166,8 @@ private:
bool m_isGroup = false;
bool m_editableText = false;
bool m_isAdjustment = false;
// Swift's isEditable: Invert's row renames instead.
bool m_editableAdjustment = false;
int m_indent = 0;
std::optional<QPoint> m_press;
// Alt at the press makes the drag a copy.
@@ -203,6 +205,7 @@ class NativeLayerList : public QScrollArea {
Q_OBJECT
public:
explicit NativeLayerList(EditorSession &session, QWidget *parent = nullptr);
~NativeLayerList() override;
// A dragged mask carries its layer's id under this type.
static const QString maskType;
// A row drag names its list, as Swift checks tables.