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

+10 -1
View File
@@ -75,15 +75,21 @@ void EditorSession::openAdjustmentEditor(QUuid id, const LayerAdjustment &origin
m_hueSaturation.emplace(layer.id, made.asset.value(), std::nullopt, layer.transform);
m_hueSaturation->settings = original.resolvedHSV();
break;
case AdjustmentKind::invert:
throw std::logic_error("Invert has no editor to open");
case AdjustmentKind::curves:
case AdjustmentKind::exposure:
case AdjustmentKind::gradientMap:
case AdjustmentKind::grain: {
case AdjustmentKind::grain:
case AdjustmentKind::blackWhite:
case AdjustmentKind::colorBalance: {
FilterSettings settings;
settings.curves = original.curves;
settings.exposure = original.exposure();
settings.gradientMap = original.gradientMap();
settings.grain = original.grain();
settings.blackWhite = original.blackWhite();
settings.colorBalance = original.colorBalance();
m_filterEdit.emplace(filterKind(original.kind).value(), layer, std::nullopt, settings, std::nullopt);
break;
}
@@ -113,6 +119,9 @@ LayerAdjustment EditorSession::editedAdjustment() const
case AdjustmentKind::exposure: value.setExposure(m_filterEdit.value().settings.exposure); break;
case AdjustmentKind::gradientMap: value.setGradientMap(m_filterEdit.value().settings.gradientMap); break;
case AdjustmentKind::grain: value.setGrain(m_filterEdit.value().settings.grain); break;
case AdjustmentKind::blackWhite: value.setBlackWhite(m_filterEdit.value().settings.blackWhite); break;
case AdjustmentKind::colorBalance: value.setColorBalance(m_filterEdit.value().settings.colorBalance); break;
case AdjustmentKind::invert: throw std::logic_error("Invert has no editor to read");
}
return value;
}
+5
View File
@@ -121,6 +121,9 @@ void EditorSession::renderFilterPreview()
return Filtered{std::nullopt, QString::fromUtf8(error.what()), job.settings};
} catch (const ExportError &error) {
return Filtered{std::nullopt, QString::fromUtf8(error.what()), job.settings};
} catch (const ProjectError &error) {
// Settings past their bounds: Black & White, Color Balance.
return Filtered{std::nullopt, QString::fromUtf8(error.what()), job.settings};
}
}));
}
@@ -228,6 +231,8 @@ void EditorSession::commitFilter(std::function<void()> done)
return FilterMade{std::nullopt, std::nullopt, QString::fromUtf8(error.what())};
} catch (const ExportError &error) {
return FilterMade{std::nullopt, std::nullopt, QString::fromUtf8(error.what())};
} catch (const ProjectError &error) {
return FilterMade{std::nullopt, std::nullopt, QString::fromUtf8(error.what())};
}
}));
}
+6 -1
View File
@@ -30,6 +30,8 @@ QString rawValue(FilterKind kind)
case FilterKind::exposure: return QStringLiteral("Exposure");
case FilterKind::gradientMap: return QStringLiteral("Gradient Map");
case FilterKind::grain: return QStringLiteral("Grain");
case FilterKind::blackWhite: return QStringLiteral("Black & White");
case FilterKind::colorBalance: return QStringLiteral("Color Balance");
}
throw std::logic_error("unknown filter kind");
}
@@ -46,7 +48,8 @@ QString rawValue(BackgroundQuality quality)
bool isImageAdjustment(FilterKind kind)
{
return kind == FilterKind::curves || kind == FilterKind::exposure || kind == FilterKind::gradientMap || kind == FilterKind::grain;
return kind == FilterKind::curves || kind == FilterKind::exposure || kind == FilterKind::gradientMap || kind == FilterKind::grain
|| kind == FilterKind::blackWhite || kind == FilterKind::colorBalance;
}
FilterSettings FilterSettings::normalized() const
@@ -113,6 +116,8 @@ QImage PixelFilter::run(const FilterJob &job)
case FilterKind::curves: image = settings.curves.apply(job.image); break;
case FilterKind::exposure: image = settings.exposure.apply(job.image); break;
case FilterKind::gradientMap: image = settings.gradientMap.apply(job.image); break;
case FilterKind::blackWhite: image = settings.blackWhite.apply(job.image); break;
case FilterKind::colorBalance: image = settings.colorBalance.apply(job.image); break;
// Grain sits in layer pixels; the job's seed patterns it.
case FilterKind::grain: image = settings.grain.apply(job.image, QPointF(), 1 / job.scale, job.seed); break;
case FilterKind::removeBackground: image = SubjectRemoval::run(job.image, settings); break;
+4 -2
View File
@@ -14,7 +14,7 @@
struct ImageLayer;
// The Filter and Image menus' filters.
enum class FilterKind { gaussianBlur, motionBlur, addNoise, lensCorrection, removeBackground, contentAwareFill, curves, exposure, gradientMap, grain };
enum class FilterKind { gaussianBlur, motionBlur, addNoise, lensCorrection, removeBackground, contentAwareFill, curves, exposure, gradientMap, grain, blackWhite, colorBalance };
// The menu's title, Swift's rawValue.
QString rawValue(FilterKind kind);
// Made without settings: its preview is its result.
@@ -25,7 +25,7 @@ bool isImageAdjustment(FilterKind kind);
inline constexpr std::array allFilterKinds{FilterKind::gaussianBlur, FilterKind::motionBlur, FilterKind::addNoise,
FilterKind::lensCorrection, FilterKind::removeBackground, FilterKind::contentAwareFill,
FilterKind::curves, FilterKind::exposure, FilterKind::gradientMap,
FilterKind::grain};
FilterKind::grain, FilterKind::blackWhite, FilterKind::colorBalance};
// Remove Background's mask: the model's alone, or refined.
enum class BackgroundQuality { basic, advanced };
@@ -50,6 +50,8 @@ struct FilterSettings {
ExposureSettings exposure{};
GradientMapSettings gradientMap{};
GrainSettings grain{};
BlackWhiteSettings blackWhite{};
ColorBalanceSettings colorBalance{};
BackgroundQuality backgroundQuality = BackgroundQuality::basic;
// How far, in pixels, the mask is pulled onto edges.
double refineEdges = 12;
+55
View File
@@ -2,6 +2,7 @@
#include "Document/BrushStroke.h"
#include "IO/ProjectStore.h"
#include <QPainter>
#include <algorithm>
#include <array>
#include <cmath>
extern "C" {
@@ -131,3 +132,57 @@ QImage GrainSettings::apply(const QImage &image, QPointF origin, double unitsPer
adjust_grain(pixels, size_t(width), size_t(height), size_t(stride), amount, size, roughness, pattern, origin.x(), origin.y(), unitsPerPixel);
});
}
bool BlackWhiteSettings::isValid() const
{
const std::array weights{reds, yellows, greens, cyans, blues, magentas};
return std::all_of(weights.begin(), weights.end(), [](double weight) { return weight >= low && weight <= high; })
&& tintHue >= 0 && tintHue <= 360 && tintSaturation >= 0 && tintSaturation <= 100;
}
QImage BlackWhiteSettings::apply(const QImage &image) const
{
if (!isValid())
throw ProjectError(ProjectError::Kind::invalid);
// The kernel's order: red, yellow, green, cyan, blue, magenta.
const std::array<float, 6> weights{float(reds / 100), float(yellows / 100), float(greens / 100),
float(cyans / 100), float(blues / 100), float(magentas / 100)};
return ImageAdjustmentPixels::run(image, [&](uchar *pixels, int width, int height, qsizetype stride) {
adjust_black_white(pixels, size_t(width), size_t(height), size_t(stride), weights.data(), tint ? 1 : 0, tintHue, tintSaturation / 100);
});
}
namespace {
std::array<double, 9> amounts(const ColorBalanceSettings &settings)
{
return {settings.shadowCyanRed, settings.shadowMagentaGreen, settings.shadowYellowBlue, settings.midCyanRed, settings.midMagentaGreen,
settings.midYellowBlue, settings.highlightCyanRed, settings.highlightMagentaGreen, settings.highlightYellowBlue};
}
}
bool ColorBalanceSettings::isValid() const
{
const std::array<double, 9> all = amounts(*this);
return std::all_of(all.begin(), all.end(), [](double amount) { return amount >= low && amount <= high; });
}
bool ColorBalanceSettings::isIdentity() const
{
const std::array<double, 9> all = amounts(*this);
return std::all_of(all.begin(), all.end(), [](double amount) { return amount == 0; });
}
QImage ColorBalanceSettings::apply(const QImage &image) const
{
if (!isValid())
throw ProjectError(ProjectError::Kind::invalid);
if (isIdentity())
return image;
std::array<float, 9> ranges;
const std::array<double, 9> all = amounts(*this);
std::transform(all.begin(), all.end(), ranges.begin(), [](double amount) { return float(amount / 100); });
return ImageAdjustmentPixels::run(image, [&](uchar *pixels, int width, int height, qsizetype stride) {
adjust_color_balance(pixels, size_t(width), size_t(height), size_t(stride), ranges.data(), ranges.data() + 3, ranges.data() + 6,
preserveLuminosity ? 1 : 0);
});
}
+39
View File
@@ -74,3 +74,42 @@ struct GrainSettings {
QImage apply(const QImage &image, QPointF origin = QPointF(), double unitsPerPixel = 1, std::optional<quint32> seed = std::nullopt) const;
friend bool operator==(const GrainSettings &, const GrainSettings &) = default;
};
// Photoshop's Black & White: how bright each colour family turns.
struct BlackWhiteSettings {
static constexpr double low = -200, high = 300;
// Photoshop's defaults, a percent each.
double reds = 40;
double yellows = 60;
double greens = 40;
double cyans = 60;
double blues = 20;
double magentas = 80;
// Colours the result, keeping its tones: sepia, cyanotype.
bool tint = false;
double tintHue = 40;
double tintSaturation = 20;
bool isValid() const;
QImage apply(const QImage &image) const;
friend bool operator==(const BlackWhiteSettings &, const BlackWhiteSettings &) = default;
};
// Color Balance: each pair shifted in shadows, midtones and highlights.
struct ColorBalanceSettings {
static constexpr double low = -100, high = 100;
double shadowCyanRed = 0;
double shadowMagentaGreen = 0;
double shadowYellowBlue = 0;
double midCyanRed = 0;
double midMagentaGreen = 0;
double midYellowBlue = 0;
double highlightCyanRed = 0;
double highlightMagentaGreen = 0;
double highlightYellowBlue = 0;
// Puts each pixel's brightness back, so only colour moves.
bool preserveLuminosity = true;
bool isValid() const;
bool isIdentity() const;
QImage apply(const QImage &image) const;
friend bool operator==(const ColorBalanceSettings &, const ColorBalanceSettings &) = default;
};
+42 -3
View File
@@ -1,5 +1,6 @@
#include "Document/LayerAdjustment.h"
#include "Document/EditorSession.h"
#include "Document/PixelInvert.h"
#include <QRandomGenerator>
#include <cmath>
@@ -12,6 +13,9 @@ QString rawValue(AdjustmentKind kind)
case AdjustmentKind::exposure: return QStringLiteral("Exposure");
case AdjustmentKind::gradientMap: return QStringLiteral("Gradient Map");
case AdjustmentKind::grain: return QStringLiteral("Grain");
case AdjustmentKind::invert: return QStringLiteral("Invert");
case AdjustmentKind::blackWhite: return QStringLiteral("Black & White");
case AdjustmentKind::colorBalance: return QStringLiteral("Color Balance");
}
throw std::logic_error("unknown adjustment kind");
}
@@ -32,12 +36,21 @@ std::optional<FilterKind> filterKind(AdjustmentKind kind)
case AdjustmentKind::exposure: return FilterKind::exposure;
case AdjustmentKind::gradientMap: return FilterKind::gradientMap;
case AdjustmentKind::grain: return FilterKind::grain;
case AdjustmentKind::blackWhite: return FilterKind::blackWhite;
case AdjustmentKind::colorBalance: return FilterKind::colorBalance;
// Panels of their own; Invert has nothing to set.
case AdjustmentKind::hsv:
case AdjustmentKind::levels: return std::nullopt;
case AdjustmentKind::levels:
case AdjustmentKind::invert: return std::nullopt;
}
throw std::logic_error("unknown adjustment kind");
}
bool isEditable(AdjustmentKind kind)
{
return kind != AdjustmentKind::invert;
}
HueSaturationSettings LayerAdjustment::resolvedHSV() const
{
return hsvSettings.value_or(HueSaturationSettings(hue, saturation, lightness, colorize));
@@ -73,6 +86,26 @@ void LayerAdjustment::setGrain(const GrainSettings &value)
grainSettings = value;
}
BlackWhiteSettings LayerAdjustment::blackWhite() const
{
return blackWhiteSettings.value_or(BlackWhiteSettings());
}
void LayerAdjustment::setBlackWhite(const BlackWhiteSettings &value)
{
blackWhiteSettings = value;
}
ColorBalanceSettings LayerAdjustment::colorBalance() const
{
return colorBalanceSettings.value_or(ColorBalanceSettings());
}
void LayerAdjustment::setColorBalance(const ColorBalanceSettings &value)
{
colorBalanceSettings = value;
}
// Bounds refuse what is no number: no isFinite terms.
bool LayerAdjustment::isValid() const
{
@@ -96,7 +129,8 @@ bool LayerAdjustment::isValid() const
if (!(range == range.normalized()))
return false;
}
return curves.isValid() && exposure().isValid() && gradientMap().isValid() && grain().isValid();
return curves.isValid() && exposure().isValid() && gradientMap().isValid() && grain().isValid() && blackWhite().isValid()
&& colorBalance().isValid();
}
QImage LayerAdjustment::apply(const QImage &image, std::optional<QRectF> region) const
@@ -108,6 +142,9 @@ QImage LayerAdjustment::apply(const QImage &image, std::optional<QRectF> region)
case AdjustmentKind::curves: return curves.apply(image);
case AdjustmentKind::exposure: return exposure().apply(image);
case AdjustmentKind::gradientMap: return gradientMap().apply(image);
case AdjustmentKind::blackWhite: return blackWhite().apply(image);
case AdjustmentKind::colorBalance: return colorBalance().apply(image);
case AdjustmentKind::invert: return PixelInvert::run(PixelInvert::Job{image, false, QTransform(), std::nullopt});
case AdjustmentKind::grain: {
// Grain sits in the document, however the image is cut.
const QRectF area = region.value_or(QRectF(0, 0, image.width(), image.height()));
@@ -143,7 +180,9 @@ void EditorSession::addAdjustment(AdjustmentKind kind)
m_collapsedGroupIDs.remove(*layer.parentID);
setActiveLayerID(layer.id);
endEdit();
setAdjustmentEditingID(layer.id);
// Invert has nothing to set: it applies, no editor opens.
if (isEditable(kind))
setAdjustmentEditingID(layer.id);
}
void EditorSession::updateAdjustment(QUuid id, const LayerAdjustment &value)
+12 -3
View File
@@ -10,15 +10,18 @@
#include <optional>
// An adjustment layer's kind, in the Layer menu's order.
enum class AdjustmentKind { hsv, levels, curves, exposure, gradientMap, grain };
enum class AdjustmentKind { hsv, levels, curves, exposure, gradientMap, grain, invert, blackWhite, colorBalance };
inline constexpr std::array allAdjustmentKinds{AdjustmentKind::hsv, AdjustmentKind::levels, AdjustmentKind::curves,
AdjustmentKind::exposure, AdjustmentKind::gradientMap, AdjustmentKind::grain};
AdjustmentKind::exposure, AdjustmentKind::gradientMap, AdjustmentKind::grain,
AdjustmentKind::invert, AdjustmentKind::blackWhite, AdjustmentKind::colorBalance};
// Swift's rawValue: the new layer's name and the manifest's word.
QString rawValue(AdjustmentKind kind);
// The kind a manifest names, or none.
std::optional<AdjustmentKind> adjustmentKind(const QString &text);
// The filter panel that edits it; Levels and Hue/Saturation: none.
// The filter panel that edits it; Levels, Hue/Saturation, Invert: none.
std::optional<FilterKind> filterKind(AdjustmentKind kind);
// Every kind but Invert opens an editor.
bool isEditable(AdjustmentKind kind);
// An adjustment layer's settings; its kind reads only its own.
struct LayerAdjustment {
@@ -35,6 +38,8 @@ struct LayerAdjustment {
std::optional<ExposureSettings> exposureSettings = std::nullopt;
std::optional<GradientMapSettings> gradientMapSettings = std::nullopt;
std::optional<GrainSettings> grainSettings = std::nullopt;
std::optional<BlackWhiteSettings> blackWhiteSettings = std::nullopt;
std::optional<ColorBalanceSettings> colorBalanceSettings = std::nullopt;
HueSaturationSettings resolvedHSV() const;
// Swift's computed properties over the optional settings.
ExposureSettings exposure() const;
@@ -43,6 +48,10 @@ struct LayerAdjustment {
void setGradientMap(const GradientMapSettings &value);
GrainSettings grain() const;
void setGrain(const GrainSettings &value);
BlackWhiteSettings blackWhite() const;
void setBlackWhite(const BlackWhiteSettings &value);
ColorBalanceSettings colorBalance() const;
void setColorBalance(const ColorBalanceSettings &value);
bool isValid() const;
// `region`: the document `image` covers, whole pixels by default.
QImage apply(const QImage &image, std::optional<QRectF> region = std::nullopt) const;
+37 -1
View File
@@ -129,6 +129,25 @@ GrainSettings grain(const QJsonValue &value)
.seed = quint32(seed)};
}
BlackWhiteSettings blackWhite(const QJsonValue &value)
{
const QJsonObject object = value.toObject();
return {.reds = number(object.value("reds")), .yellows = number(object.value("yellows")), .greens = number(object.value("greens")),
.cyans = number(object.value("cyans")), .blues = number(object.value("blues")), .magentas = number(object.value("magentas")),
.tint = boolean(object.value("tint")), .tintHue = number(object.value("tintHue")),
.tintSaturation = number(object.value("tintSaturation"))};
}
ColorBalanceSettings colorBalance(const QJsonValue &value)
{
const QJsonObject object = value.toObject();
return {.shadowCyanRed = number(object.value("shadowCyanRed")), .shadowMagentaGreen = number(object.value("shadowMagentaGreen")),
.shadowYellowBlue = number(object.value("shadowYellowBlue")), .midCyanRed = number(object.value("midCyanRed")),
.midMagentaGreen = number(object.value("midMagentaGreen")), .midYellowBlue = number(object.value("midYellowBlue")),
.highlightCyanRed = number(object.value("highlightCyanRed")), .highlightMagentaGreen = number(object.value("highlightMagentaGreen")),
.highlightYellowBlue = number(object.value("highlightYellowBlue")), .preserveLuminosity = boolean(object.value("preserveLuminosity"))};
}
QJsonObject encoded(const AdjustmentColor &colour)
{
return {{"red", colour.red}, {"green", colour.green}, {"blue", colour.blue}};
@@ -184,7 +203,9 @@ LayerAdjustment ManifestJson::adjustment(const QJsonValue &value)
.curves = curves(object.value("curves")),
.exposureSettings = optional(object, "exposureSettings", exposure),
.gradientMapSettings = optional(object, "gradientMapSettings", gradientMap),
.grainSettings = optional(object, "grainSettings", grain)};
.grainSettings = optional(object, "grainSettings", grain),
.blackWhiteSettings = optional(object, "blackWhiteSettings", blackWhite),
.colorBalanceSettings = optional(object, "colorBalanceSettings", colorBalance)};
}
QJsonObject ManifestJson::encoded(const LayerAdjustment &adjustment)
@@ -212,5 +233,20 @@ QJsonObject ManifestJson::encoded(const LayerAdjustment &adjustment)
object.insert("grainSettings", QJsonObject{{"amount", grain.amount}, {"size", grain.size}, {"roughness", grain.roughness},
{"seed", qint64(grain.seed)}});
}
if (adjustment.blackWhiteSettings) {
const BlackWhiteSettings &gray = *adjustment.blackWhiteSettings;
object.insert("blackWhiteSettings", QJsonObject{{"reds", gray.reds}, {"yellows", gray.yellows}, {"greens", gray.greens}, {"cyans", gray.cyans},
{"blues", gray.blues}, {"magentas", gray.magentas}, {"tint", gray.tint},
{"tintHue", gray.tintHue}, {"tintSaturation", gray.tintSaturation}});
}
if (adjustment.colorBalanceSettings) {
const ColorBalanceSettings &balance = *adjustment.colorBalanceSettings;
object.insert("colorBalanceSettings",
QJsonObject{{"shadowCyanRed", balance.shadowCyanRed}, {"shadowMagentaGreen", balance.shadowMagentaGreen},
{"shadowYellowBlue", balance.shadowYellowBlue}, {"midCyanRed", balance.midCyanRed},
{"midMagentaGreen", balance.midMagentaGreen}, {"midYellowBlue", balance.midYellowBlue},
{"highlightCyanRed", balance.highlightCyanRed}, {"highlightMagentaGreen", balance.highlightMagentaGreen},
{"highlightYellowBlue", balance.highlightYellowBlue}, {"preserveLuminosity", balance.preserveLuminosity}});
}
return object;
}
+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.