Outer Glow, brush Smoothing and the rest of Compositor 1.2.0
This commit is contained in:
1 parent
2cd0d1b86d
commit
374ba8ec73
32 files changed
+624
-47
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -22,3 +22,5 @@ public:
|
||||
|
||||
private:
|
||||
void finishRasterCommit();
|
||||
// Where Smoothing lets the brush go; none while slack.
|
||||
std::optional<QPointF> smoothed(QPointF point);
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user