Black & White, Color Balance and Invert adjustments (12.7b)
This commit is contained in:
1 parent
7d47cc367a
commit
4291f4249a
33 files changed
+819
-52
No files matched your search
@@ -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"));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user