Outer Glow, brush Smoothing and the rest of Compositor 1.2.0

This commit is contained in:
雷电芽衣 committed 2026-09-27 07:00:12 -04:00
1 parent 2cd0d1b86d
commit 374ba8ec73
32 files changed
+624 -47

No files matched your search

+2
View File
@@ -31,6 +31,8 @@ struct BrushSettings {
double blue = 0;
// Caps the whole stroke: overlapping dabs never exceed it.
double opacity = 1;
// 0–100: the brush trails the pointer on this string.
double smoothing = 0;
// Erase clears the layer's pixels instead of painting on them.
bool erasing = false;
// Spot healing takes nearby pixels instead of the colour.
+27 -2
View File
@@ -111,6 +111,8 @@ void EditorSession::beginBrush(QPointF point)
m_brushStroke->clone = clone;
m_brushStroke->isBlur = m_tool == NavigationTool::blur;
m_brushStroke->append(point);
m_brushAnchor = point;
m_brushPointer = point;
m_lastBrushPoint = LastBrushPoint{point, layer.id, m_isMaskSelected};
++m_brushRevision;
resumeFileRequests();
@@ -135,9 +137,13 @@ void EditorSession::continueBrush(QPointF point)
}
if (!m_brushStroke)
return;
m_brushPointer = point;
const std::optional<QPointF> painted = smoothed(point);
if (!painted)
return;
try {
m_brushStroke->append(point);
m_lastBrushPoint->point = point;
m_brushStroke->append(*painted);
m_lastBrushPoint->point = *painted;
++m_brushRevision;
notify();
} catch (const ProjectError &error) {
@@ -149,6 +155,20 @@ void EditorSession::continueBrush(QPointF point)
}
}
// Photoshop's string: the brush moves once pulled taut.
std::optional<QPointF> EditorSession::smoothed(QPointF point)
{
if (m_tool != NavigationTool::brush || !(m_brushSettings.smoothing > 0) || !m_brushAnchor)
return point;
const double radius = m_brushSettings.smoothing / std::max(0.01, viewport.zoom());
const QPointF delta = point - *m_brushAnchor;
const double distance = std::hypot(delta.x(), delta.y());
if (!(distance > radius))
return std::nullopt;
m_brushAnchor = *m_brushAnchor + delta * ((distance - radius) / distance);
return m_brushAnchor;
}
// Where a Shift-click's line starts, on the same target.
std::optional<QPointF> EditorSession::shiftLineStart() const
{
@@ -161,6 +181,8 @@ void EditorSession::cancelBrush()
{
m_warpStroke.reset();
m_brushStroke.reset();
m_brushAnchor.reset();
m_brushPointer.reset();
++m_brushRevision;
resumeFileRequests();
notify();
@@ -180,6 +202,9 @@ bool EditorSession::finishBrushImmediately()
if (m_isProjectBusy)
return false;
try {
// Smoothing leaves the brush short; the stroke ends there.
if (m_brushPointer && m_brushAnchor && *m_brushPointer != *m_brushAnchor && m_tool == NavigationTool::brush && m_brushSettings.smoothing > 0)
m_brushStroke->append(*m_brushPointer);
m_brushStroke->flush();
if (m_brushStroke->settings.healing)
m_brushStroke->heal();
+2
View File
@@ -22,3 +22,5 @@ public:
private:
void finishRasterCommit();
// Where Smoothing lets the brush go; none while slack.
std::optional<QPointF> smoothed(QPointF point);
+3
View File
@@ -427,6 +427,9 @@ private:
// Clone Stamp and the Smear keep their own soft tips.
std::map<int, ParkedTip> m_parkedBrushTips{{1, {40, 0, 1}}, {2, {40, 0, 1}}};
std::optional<LastBrushPoint> m_lastBrushPoint;
// Smoothing's brush, and the pointer it trails.
std::optional<QPointF> m_brushAnchor;
std::optional<QPointF> m_brushPointer;
int m_brushRevision = 0;
std::optional<PendingOpacityDigit> m_pendingOpacityDigit;
std::optional<CommittingRaster> m_committingRaster;
+2
View File
@@ -104,6 +104,8 @@ double LayerEffectsRenderer::margin(const LayerEffects &all)
margin = std::max(margin, effects.stroke->size);
if (effects.shadow)
margin = std::max(margin, effects.shadow->distance + effects.shadow->blur * 3);
if (effects.outerGlow)
margin = std::max(margin, effects.outerGlow->size * 3);
return std::ceil(margin) + 2;
}
+5
View File
@@ -10,6 +10,7 @@ void take(LayerEffects &into, const LayerEffects &from, LayerEffectKind kind)
case LayerEffectKind::shadow: into.shadow = from.shadow; return;
case LayerEffectKind::colorOverlay: into.colorOverlay = from.colorOverlay; return;
case LayerEffectKind::innerShadow: into.innerShadow = from.innerShadow; return;
case LayerEffectKind::outerGlow: into.outerGlow = from.outerGlow; return;
}
throw std::logic_error("unknown effect kind");
}
@@ -70,6 +71,10 @@ void EditorSession::addEffect(LayerEffectKind kind)
if (!effects.innerShadow)
effects.innerShadow = InnerShadowEffect();
break;
case LayerEffectKind::outerGlow:
if (!effects.outerGlow)
effects.outerGlow = OuterGlowEffect();
break;
}
setEffects(effects, id, QStringLiteral("Add ") + rawValue(kind));
selectEffect(kind, id, true);
+14 -2
View File
@@ -56,6 +56,11 @@ bool InnerShadowEffect::isValid() const
return shadowIsValid(angle, distance, blur, opacity) && colourIsValid(red, green, blue);
}
bool OuterGlowEffect::isValid() const
{
return within(size, 0, 500) && within(opacity, 0, 1) && colourIsValid(red, green, blue);
}
QString rawValue(LayerEffectKind kind)
{
switch (kind) {
@@ -63,6 +68,7 @@ QString rawValue(LayerEffectKind kind)
case LayerEffectKind::shadow: return QStringLiteral("Drop Shadow");
case LayerEffectKind::colorOverlay: return QStringLiteral("Color Overlay");
case LayerEffectKind::innerShadow: return QStringLiteral("Inner Shadow");
case LayerEffectKind::outerGlow: return QStringLiteral("Outer Glow");
}
throw std::logic_error("unknown effect kind");
}
@@ -70,7 +76,7 @@ QString rawValue(LayerEffectKind kind)
bool LayerEffects::isValid() const
{
return (!stroke || stroke->isValid()) && (!shadow || shadow->isValid()) && (!colorOverlay || colorOverlay->isValid())
&& (!innerShadow || innerShadow->isValid());
&& (!innerShadow || innerShadow->isValid()) && (!outerGlow || outerGlow->isValid());
}
std::vector<LayerEffectKind> LayerEffects::kinds() const
@@ -90,6 +96,7 @@ bool LayerEffects::contains(LayerEffectKind kind) const
case LayerEffectKind::shadow: return shadow.has_value();
case LayerEffectKind::colorOverlay: return colorOverlay.has_value();
case LayerEffectKind::innerShadow: return innerShadow.has_value();
case LayerEffectKind::outerGlow: return outerGlow.has_value();
}
throw std::logic_error("unknown effect kind");
}
@@ -101,6 +108,7 @@ bool LayerEffects::isEnabled(LayerEffectKind kind) const
case LayerEffectKind::shadow: return shadow && shadow->isEnabled();
case LayerEffectKind::colorOverlay: return colorOverlay && colorOverlay->isEnabled();
case LayerEffectKind::innerShadow: return innerShadow && innerShadow->isEnabled();
case LayerEffectKind::outerGlow: return outerGlow && outerGlow->isEnabled();
}
throw std::logic_error("unknown effect kind");
}
@@ -112,6 +120,7 @@ std::optional<PaletteColor> LayerEffects::color(LayerEffectKind kind) const
case LayerEffectKind::shadow: return shadow ? std::optional(shadow->color()) : std::nullopt;
case LayerEffectKind::colorOverlay: return colorOverlay ? std::optional(colorOverlay->color()) : std::nullopt;
case LayerEffectKind::innerShadow: return innerShadow ? std::optional(innerShadow->color()) : std::nullopt;
case LayerEffectKind::outerGlow: return outerGlow ? std::optional(outerGlow->color()) : std::nullopt;
}
throw std::logic_error("unknown effect kind");
}
@@ -146,6 +155,7 @@ void LayerEffects::setColor(const PaletteColor &color, LayerEffectKind kind)
case LayerEffectKind::shadow: paint(shadow, color); return;
case LayerEffectKind::colorOverlay: paint(colorOverlay, color); return;
case LayerEffectKind::innerShadow: paint(innerShadow, color); return;
case LayerEffectKind::outerGlow: paint(outerGlow, color); return;
}
throw std::logic_error("unknown effect kind");
}
@@ -157,6 +167,7 @@ void LayerEffects::remove(LayerEffectKind kind)
case LayerEffectKind::shadow: shadow.reset(); return;
case LayerEffectKind::colorOverlay: colorOverlay.reset(); return;
case LayerEffectKind::innerShadow: innerShadow.reset(); return;
case LayerEffectKind::outerGlow: outerGlow.reset(); return;
}
throw std::logic_error("unknown effect kind");
}
@@ -168,11 +179,12 @@ void LayerEffects::setEnabled(bool enabled, LayerEffectKind kind)
case LayerEffectKind::shadow: enable(shadow, enabled); return;
case LayerEffectKind::colorOverlay: enable(colorOverlay, enabled); return;
case LayerEffectKind::innerShadow: enable(innerShadow, enabled); return;
case LayerEffectKind::outerGlow: enable(outerGlow, enabled); return;
}
throw std::logic_error("unknown effect kind");
}
LayerEffects LayerEffects::visible() const
{
return LayerEffects{shown(stroke), shown(shadow), shown(colorOverlay), shown(innerShadow)};
return LayerEffects{shown(stroke), shown(shadow), shown(colorOverlay), shown(innerShadow), shown(outerGlow)};
}
+18 -3
View File
@@ -74,10 +74,24 @@ struct InnerShadowEffect {
friend bool operator==(const InnerShadowEffect &, const InnerShadowEffect &) = default;
};
// A soft glow round the outside of the layer.
struct OuterGlowEffect {
std::optional<bool> enabled = std::nullopt;
double size = 20;
double red = 1;
double green = 1;
double blue = 1;
double opacity = 0.75;
bool isEnabled() const { return enabled.value_or(true); }
PaletteColor color() const { return {red, green, blue}; }
bool isValid() const;
friend bool operator==(const OuterGlowEffect &, const OuterGlowEffect &) = default;
};
// Swift's raw values are the panel's and the menu's words.
enum class LayerEffectKind { stroke, shadow, colorOverlay, innerShadow };
enum class LayerEffectKind { stroke, shadow, colorOverlay, innerShadow, outerGlow };
inline constexpr std::array allLayerEffectKinds{LayerEffectKind::stroke, LayerEffectKind::shadow, LayerEffectKind::colorOverlay,
LayerEffectKind::innerShadow};
LayerEffectKind::innerShadow, LayerEffectKind::outerGlow};
QString rawValue(LayerEffectKind kind);
// What a layer draws round itself; its pixels stay untouched.
@@ -86,7 +100,8 @@ struct LayerEffects {
std::optional<ShadowEffect> shadow = std::nullopt;
std::optional<ColorOverlayEffect> colorOverlay = std::nullopt;
std::optional<InnerShadowEffect> innerShadow = std::nullopt;
bool isEmpty() const { return !stroke && !shadow && !colorOverlay && !innerShadow; }
std::optional<OuterGlowEffect> outerGlow = std::nullopt;
bool isEmpty() const { return !stroke && !shadow && !colorOverlay && !innerShadow && !outerGlow; }
bool isValid() const;
std::vector<LayerEffectKind> kinds() const;
bool contains(LayerEffectKind kind) const;
+16 -1
View File
@@ -39,6 +39,13 @@ InnerShadowEffect innerShadow(const QJsonValue &value)
number(keys.value("red")), number(keys.value("green")), number(keys.value("blue")), number(keys.value("opacity"))};
}
OuterGlowEffect outerGlow(const QJsonValue &value)
{
const QJsonObject keys = object(value);
return {optional(keys, "enabled", boolean), number(keys.value("size")), number(keys.value("red")), number(keys.value("green")),
number(keys.value("blue")), number(keys.value("opacity"))};
}
void colour(QJsonObject &keys, double red, double green, double blue)
{
keys.insert("red", red);
@@ -51,7 +58,7 @@ LayerEffects ManifestJson::effects(const QJsonValue &value)
{
const QJsonObject keys = object(value);
return {optional(keys, "stroke", stroke), optional(keys, "shadow", shadow), optional(keys, "colorOverlay", colorOverlay),
optional(keys, "innerShadow", innerShadow)};
optional(keys, "innerShadow", innerShadow), optional(keys, "outerGlow", outerGlow)};
}
QJsonObject ManifestJson::encoded(const LayerEffects &effects)
@@ -89,5 +96,13 @@ QJsonObject ManifestJson::encoded(const LayerEffects &effects)
object.insert("enabled", *inner.enabled);
keys.insert("innerShadow", object);
}
if (effects.outerGlow) {
const OuterGlowEffect &glow = *effects.outerGlow;
QJsonObject object{{"size", glow.size}, {"opacity", glow.opacity}};
colour(object, glow.red, glow.green, glow.blue);
if (glow.enabled)
object.insert("enabled", *glow.enabled);
keys.insert("outerGlow", object);
}
return keys;
}
+5 -4
View File
@@ -80,9 +80,8 @@ std::optional<EffectsPreviewCache::Result> EffectsPreviewCache::preview(const Im
if (m_entries.contains(layer.id)) {
const Entry &old = m_entries.at(layer.id);
old.request.cancelled->store(true);
// Kept while the settings change on the same pixels.
if (old.request.image.identity() == request.image.identity() && old.request.maskSource == maskSource
&& old.request.effects.kinds() == effects.kinds())
// Kept on the same pixels, an effect hidden or not.
if (old.request.image.identity() == request.image.identity() && old.request.maskSource == maskSource)
previous = old.result;
}
if (!previous && m_seeds.contains(layer.id))
@@ -151,11 +150,13 @@ std::optional<EffectsPreviewCache::Result> EffectsPreviewCache::render(const Req
effects.shadow->distance *= factor;
effects.shadow->blur *= factor;
}
// Swift forgets the inner shadow, which then shows too wide.
// Swift forgets the inner shadow and glow: too wide.
if (effects.innerShadow) {
effects.innerShadow->distance *= factor;
effects.innerShadow->blur *= factor;
}
if (effects.outerGlow)
effects.outerGlow->size *= factor;
const LayerEffectsRenderer::Rendered rendered = LayerEffectsRenderer::render(pixels, mask, effects);
return Result{rendered.image, rendered.inset, std::nullopt};
} catch (const ExportError &error) {
+12 -1
View File
@@ -197,13 +197,22 @@ QImage LayerEffectsKernel::render(const QImage &pixels, const LayerEffects &effe
for (qint64 index = 0; index < count; ++index)
inside[index] = std::clamp(shape[index] * (1.f - inside[index]), 0.f, 1.f);
}
const std::optional<OuterGlowEffect> glow = effects.outerGlow && effects.outerGlow->isEnabled() && effects.outerGlow->size > 0 && effects.outerGlow->opacity > 0
? effects.outerGlow : std::nullopt;
Buffer glowing;
if (glow) {
// The shape softened every way, as Metal's glow buffer.
glowing = buffer(count);
std::copy(shape.get(), shape.get() + count, glowing.get());
blurred(glowing.get(), buffer(count).get(), width, height, float(glow->size / 2));
}
const std::optional<ColorOverlayEffect> overlay = effects.colorOverlay && effects.colorOverlay->isEnabled() && effects.colorOverlay->opacity > 0
? effects.colorOverlay : std::nullopt;
QImage result = BrushRaster::context(width, height, false);
// Taken once: scanLine() detaches, which workers must not race.
uchar *const bits = result.bits();
const qsizetype stride = result.bytesPerLine();
// Shadow, outside stroke, pixels, overlay, inner shadow, inside stroke.
// Shadow, glow, outside stroke, pixels, overlay, inner shadow, inside stroke.
bands(height, [&](int, int y) {
const uchar *in = source.constScanLine(y);
uchar *out = bits + y * stride;
@@ -212,6 +221,8 @@ QImage LayerEffectsKernel::render(const QImage &pixels, const LayerEffects &effe
float colour[3] = {0, 0, 0}, alpha = 0;
if (shadow)
over(colour, alpha, shadow->color(), std::clamp(cast[index] * float(shadow->opacity), 0.f, 1.f));
if (glow)
over(colour, alpha, glow->color(), std::clamp(glowing[index] * (1.f - shape[index]) * float(glow->opacity), 0.f, 1.f));
const float strokeCoverage = stroke ? std::clamp(ring[index] * float(stroke->opacity), 0.f, 1.f) : 0.f;
if (stroke && !stroke->inside)
over(colour, alpha, stroke->color(), strokeCoverage);
+2
View File
@@ -52,6 +52,8 @@ double LayerEffectsSurface::reach() const
// Swift forgets the inner shadow, whose blur reaches as far.
if (m_effects.innerShadow)
reach = std::max(reach, m_effects.innerShadow->distance + m_effects.innerShadow->blur * 3 + 2);
if (m_effects.outerGlow)
reach = std::max(reach, m_effects.outerGlow->size * 3 + 2);
return reach;
}
+14 -2
View File
@@ -52,6 +52,11 @@ BrushControls::BrushControls(EditorSession &session, QWidget *parent)
[this](int value) { changeBrush(m_session, [value](BrushSettings &brush) { brush.opacity = value / 1000.0; }); })),
m_opacity(new SelectionAmountField(session, 1, 100, [this] { return m_session.brushSettings().opacity * 100; },
[this](double percent) { changeBrush(m_session, [percent](BrushSettings &brush) { brush.opacity = percent / 100; }); }, this)),
m_smoothingLabel(label(QStringLiteral("Smoothing"), this)),
m_smoothingSlider(slider(QStringLiteral("brushSmoothingSlider"), 0, 1000,
[this](int value) { changeBrush(m_session, [value](BrushSettings &brush) { brush.smoothing = value / 10.0; }); })),
m_smoothing(new SelectionAmountField(session, 0, 100, [this] { return m_session.brushSettings().smoothing; },
[this](double value) { changeBrush(m_session, [value](BrushSettings &brush) { brush.smoothing = value; }); }, this)),
m_paintLabel(label(QStringLiteral("Paint"), this)), m_maskPaint(new QComboBox(this)), m_colourLabel(label(QStringLiteral("Color"), this)),
m_colour(new SwatchButton([&session] { return session.foregroundColor(); }, 4, 1, 1, this)),
m_cloneNote(label(QStringLiteral("Alt-click to set the source"), this)), m_mask(label(QStringLiteral("Mask"), this))
@@ -73,6 +78,9 @@ BrushControls::BrushControls(EditorSession &session, QWidget *parent)
m_opacity->setObjectName(QStringLiteral("brushOpacity"));
m_opacity->setFixedWidth(42);
m_opacity->setToolTip(QStringLiteral("Press 1–9 for 10–90%, 0 for 100%"));
m_smoothing->setObjectName(QStringLiteral("brushSmoothing"));
m_smoothing->setFixedWidth(42);
m_smoothing->setToolTip(QStringLiteral("The brush trails the pointer on a string this long, so a shaky hand still draws a smooth line"));
m_maskPaint->setObjectName(QStringLiteral("maskPaint"));
m_maskPaint->setFixedWidth(180);
m_maskPaint->addItem(QStringLiteral("Black · Hide"), false);
@@ -95,7 +103,7 @@ BrushControls::BrushControls(EditorSession &session, QWidget *parent)
widgets.insert(widgets.end(), {m_aligned, m_thisLayer, m_allLayers});
widgets.insert(widgets.end(), {label(QStringLiteral("Size"), this), m_size, label(QStringLiteral("px"), this), label(QStringLiteral("Hardness"), this),
m_hardnessSlider, m_hardness, label(QStringLiteral("%"), this), m_opacityLabel, m_opacitySlider, m_opacity,
label(QStringLiteral("%"), this), m_paintLabel, m_maskPaint, m_colourLabel, m_colour});
label(QStringLiteral("%"), this), m_smoothingLabel, m_smoothingSlider, m_smoothing, m_paintLabel, m_maskPaint, m_colourLabel, m_colour});
for (QWidget *widget : widgets)
row->insertWidget(row->count() - 1, widget);
row->addWidget(m_cloneNote);
@@ -156,10 +164,14 @@ void BrushControls::synchronize()
m_size->sync(int(std::lround(brush.diameter)));
m_hardness->sync(int(std::lround(brush.hardness * 100)));
m_opacity->sync(int(std::lround(brush.opacity * 100)));
for (QWidget *widget : std::initializer_list<QWidget *>{m_smoothingLabel, m_smoothingSlider, m_smoothing})
widget->setVisible(tool == NavigationTool::brush);
m_smoothing->sync(int(std::lround(brush.smoothing)));
// The sliders show the session's numbers without writing back.
const QSignalBlocker hardness(m_hardnessSlider), opacity(m_opacitySlider);
const QSignalBlocker hardness(m_hardnessSlider), opacity(m_opacitySlider), smoothing(m_smoothingSlider);
m_hardnessSlider->setValue(int(std::lround(brush.hardness * 1000)));
m_opacitySlider->setValue(int(std::lround(brush.opacity * 1000)));
m_smoothingSlider->setValue(int(std::lround(brush.smoothing * 10)));
const bool mask = m_session.isMaskSelected();
m_paintLabel->setVisible(mask);
m_maskPaint->setVisible(mask);
+4
View File
@@ -44,6 +44,10 @@ private:
QLabel *const m_opacityLabel;
QSlider *const m_opacitySlider;
SelectionAmountField *const m_opacity;
// Paint and Erase only; other tools have their own feel.
QLabel *const m_smoothingLabel;
QSlider *const m_smoothingSlider;
SelectionAmountField *const m_smoothing;
QLabel *const m_paintLabel;
QComboBox *const m_maskPaint;
// The foreground colour, the rail swatch's own picker.
+13 -2
View File
@@ -1,10 +1,20 @@
#include "UI/CompositorMenus.h"
#include "Logging.h"
#include "UI/KeyboardShortcuts.h"
#include "UI/NativeLayerList.h"
#include <QApplication>
#include <QMenu>
namespace {
// The layer list holding the keys, as Swift's table.
NativeLayerList *focusedList()
{
for (QWidget *widget = QApplication::focusWidget(); widget; widget = widget->parentWidget())
if (auto *list = qobject_cast<NativeLayerList *>(widget))
return list;
return nullptr;
}
// An entry's name: Hue/Saturation is newHueSaturationAdjustment.
QString adjustmentName(AdjustmentKind kind)
{
@@ -157,12 +167,14 @@ CompositorMenus::CompositorMenus(ProjectWorkspace &workspace, QMenuBar &bar, QWi
add(view, QStringLiteral("clearGuides"), QStringLiteral("Clear Guides"), QKeySequence(), [this] { session().clearGuides(); });
QMenu *select = bar.addMenu(QStringLiteral("&Select"));
// Text fields keep their own Select All.
// The focus takes Select All first, as Swift's responder chain.
add(select, QStringLiteral("selectAll"), QStringLiteral("All"), QKeySequence(Qt::CTRL | Qt::Key_A), [this] {
if (m_field)
m_field->selectAll();
else if (InlineTextEditor *text = typedText())
text->selectAll();
else if (NativeLayerList *list = focusedList())
list->selectAllRows();
else
session().selectAll();
});
@@ -332,7 +344,6 @@ void CompositorMenus::synchronize()
action(QStringLiteral("transformControls"))->setChecked(s.showsTransformControls());
action(QStringLiteral("transformControls"))->setEnabled(s.tool() == NavigationTool::move && s.document().has_value());
const bool selected = s.selection().has_value();
action(QStringLiteral("selectAll"))->setEnabled(drawn);
action(QStringLiteral("deselect"))->setEnabled(selected && s.canEditSelection());
action(QStringLiteral("inverse"))->setEnabled(selected && s.canEditSelection());
action(QStringLiteral("layerPixels"))->setEnabled(active && active->asset && s.canEditSelection());
+7
View File
@@ -85,6 +85,13 @@ EffectsSheet::EffectsSheet(EditorSession &session, LayerEffectKind kind, QWidget
slider(QStringLiteral("Blur"), [](const LayerEffects &e) { return e.innerShadow ? std::optional(e.innerShadow->blur) : std::nullopt; },
[](LayerEffects &e, double value) { e.innerShadow->blur = value; }, 0, 100, QStringLiteral("px"), 500);
break;
case LayerEffectKind::outerGlow:
header(QStringLiteral("Outer Glow"), false);
slider(QStringLiteral("Size"), [](const LayerEffects &e) { return e.outerGlow ? std::optional(e.outerGlow->size) : std::nullopt; },
[](LayerEffects &e, double value) { e.outerGlow->size = value; }, 0, 100, QStringLiteral("px"), 500);
slider(QStringLiteral("Opacity"), [](const LayerEffects &e) { return percent(e.outerGlow); },
[](LayerEffects &e, double value) { e.outerGlow->opacity = value / 100; }, 0, 100, QStringLiteral("%"));
break;
}
auto *cancel = new QPushButton(QStringLiteral("Cancel"), this);
cancel->setObjectName(QStringLiteral("effectsCancel"));
+10
View File
@@ -255,3 +255,13 @@ void NativeLayerList::changeEvent(QEvent *event)
if (event->type() == QEvent::PaletteChange)
QMetaObject::invokeMethod(this, &NativeLayerList::update, Qt::QueuedConnection);
}
void NativeLayerList::selectAllRows()
{
if (m_rows.empty())
return;
QSet<QUuid> ids;
for (const ImageLayer &row : m_rows)
ids.insert(row.id);
m_session.selectLayers(ids, m_rows.front().id);
}
+2
View File
@@ -212,6 +212,8 @@ public:
QString dragToken() const { return m_dragToken; }
EditorSession &session() const { return m_session; }
// The table's Select All: every row, the top one primary.
void selectAllRows();
void update();
std::vector<LayerCell *> cells() const { return m_cells; }
// The row under a point of this list, or -1.