Add an image overlay effect
Lays a single picture from disk over its input: fitted, filled, stretched, left at its own size or tiled, centred or pinned to the top left corner, nudged by an offset and mixed in by its opacity. The file it needs is a new kind of param, Path, so the interface shows a chooser and the CLI takes a file name, both without knowing an overlay exists. Projects write it relative to their own folder, as they do a source, and Image::load can now hand back the alpha channel instead of flattening it onto white, so a cut-out PNG keeps its cut-out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -110,6 +110,9 @@ Options parseOptions(int argc, char** argv) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ParamSpec::Type::Path:
|
||||
options.params[spec->key] = requireValue(argc, argv, i, arg);
|
||||
break;
|
||||
case ParamSpec::Type::Choice: {
|
||||
const std::string text = requireValue(argc, argv, i, arg);
|
||||
bool valid = false;
|
||||
|
||||
@@ -16,6 +16,7 @@ ParamValues Effect::defaults() const {
|
||||
values[spec.key] = spec.defaultValue != 0.0;
|
||||
break;
|
||||
case ParamSpec::Type::Choice:
|
||||
case ParamSpec::Type::Path:
|
||||
values[spec.key] = spec.defaultChoice;
|
||||
break;
|
||||
}
|
||||
|
||||
+13
-1
@@ -43,7 +43,7 @@ Image::Image(int width, int height)
|
||||
pixels_(static_cast<std::size_t>(std::max(width, 0)) * 3 *
|
||||
static_cast<std::size_t>(std::max(height, 0))) {}
|
||||
|
||||
Image Image::load(const std::string& path) {
|
||||
Image Image::load(const std::string& path, std::vector<std::uint8_t>* alphaOut) {
|
||||
GError* error = nullptr;
|
||||
PixbufPtr pixbuf = wrap(gdk_pixbuf_new_from_file(path.c_str(), &error));
|
||||
if (!pixbuf) throw std::runtime_error(takeError(error));
|
||||
@@ -59,11 +59,23 @@ Image Image::load(const std::string& path) {
|
||||
const guint8* src = gdk_pixbuf_get_pixels(pixbuf.get());
|
||||
|
||||
Image image(width, height);
|
||||
if (alphaOut != nullptr) {
|
||||
alphaOut->assign(static_cast<std::size_t>(std::max(width, 0)) *
|
||||
static_cast<std::size_t>(std::max(height, 0)),
|
||||
255);
|
||||
}
|
||||
for (int y = 0; y < height; ++y) {
|
||||
const guint8* in = src + static_cast<std::size_t>(y) * srcStride;
|
||||
std::uint8_t* out = image.row(y);
|
||||
if (channels == 3) {
|
||||
std::memcpy(out, in, static_cast<std::size_t>(width) * 3);
|
||||
} else if (alphaOut != nullptr) {
|
||||
// Kept apart rather than flattened: the caller does the compositing.
|
||||
std::uint8_t* alpha = alphaOut->data() + static_cast<std::size_t>(y) * width;
|
||||
for (int x = 0; x < width; ++x) {
|
||||
for (int c = 0; c < 3; ++c) out[x * 3 + c] = in[x * channels + c];
|
||||
alpha[x] = in[x * channels + 3];
|
||||
}
|
||||
} else {
|
||||
// Flatten alpha onto white, matching what a viewer would show.
|
||||
for (int x = 0; x < width; ++x) {
|
||||
|
||||
+5
-1
@@ -34,7 +34,11 @@ public:
|
||||
|
||||
// Decodes via GdkPixbuf (PNG, JPEG, WebP, TIFF, BMP, ...), applying the
|
||||
// embedded EXIF orientation. Throws std::runtime_error on failure.
|
||||
static Image load(const std::string& path);
|
||||
// Transparency is flattened onto white, as a viewer would show it, unless
|
||||
// `alphaOut` is given: it then receives one byte per pixel (255 throughout
|
||||
// for a file without an alpha channel) and the colours are left untouched,
|
||||
// which is what an effect compositing the image needs.
|
||||
static Image load(const std::string& path, std::vector<std::uint8_t>* alphaOut = nullptr);
|
||||
|
||||
// Encodes by file extension; png is used when the extension is unknown.
|
||||
void save(const std::string& path) const;
|
||||
|
||||
@@ -63,6 +63,15 @@ ParamSpec ParamSpec::makeChoice(std::string key, std::string label, std::string
|
||||
return spec;
|
||||
}
|
||||
|
||||
ParamSpec ParamSpec::makePath(std::string key, std::string label, std::string help) {
|
||||
ParamSpec spec;
|
||||
spec.type = Type::Path;
|
||||
spec.key = std::move(key);
|
||||
spec.label = std::move(label);
|
||||
spec.help = std::move(help);
|
||||
return spec;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
const ParamValue* find(const ParamValues& values, const std::string& key) {
|
||||
@@ -99,6 +108,11 @@ bool paramBool(const ParamValues& values, const std::string& key, bool fallback)
|
||||
|
||||
std::string paramChoice(const ParamValues& values, const std::string& key,
|
||||
const std::string& fallback) {
|
||||
return paramText(values, key, fallback);
|
||||
}
|
||||
|
||||
std::string paramText(const ParamValues& values, const std::string& key,
|
||||
const std::string& fallback) {
|
||||
if (const auto* value = find(values, key)) {
|
||||
if (const auto* v = std::get_if<std::string>(value)) return *v;
|
||||
}
|
||||
|
||||
+7
-1
@@ -10,7 +10,7 @@ namespace effector {
|
||||
// Declarative description of one effect knob. The GUI turns these into widgets
|
||||
// and the CLI turns them into flags, so neither knows about any effect.
|
||||
struct ParamSpec {
|
||||
enum class Type { Int, Double, Bool, Choice };
|
||||
enum class Type { Int, Double, Bool, Choice, Path };
|
||||
|
||||
Type type = Type::Int;
|
||||
std::string key;
|
||||
@@ -35,6 +35,10 @@ struct ParamSpec {
|
||||
std::string help = {});
|
||||
static ParamSpec makeChoice(std::string key, std::string label, std::string defaultChoice,
|
||||
std::vector<std::string> choices, std::string help = {});
|
||||
// A path to a file on disk, empty by default: a chooser in the interface, a
|
||||
// flag taking any string on the command line, written relative to the
|
||||
// project folder so that a folder still opens after it is moved.
|
||||
static ParamSpec makePath(std::string key, std::string label, std::string help = {});
|
||||
};
|
||||
|
||||
using ParamValue = std::variant<long, double, bool, std::string>;
|
||||
@@ -45,5 +49,7 @@ double paramDouble(const ParamValues& values, const std::string& key, double fal
|
||||
bool paramBool(const ParamValues& values, const std::string& key, bool fallback);
|
||||
std::string paramChoice(const ParamValues& values, const std::string& key,
|
||||
const std::string& fallback);
|
||||
std::string paramText(const ParamValues& values, const std::string& key,
|
||||
const std::string& fallback);
|
||||
|
||||
} // namespace effector
|
||||
|
||||
@@ -348,6 +348,13 @@ std::string projectToJson(const Project& project, const std::string& directory)
|
||||
case ParamSpec::Type::Choice:
|
||||
out << quote(paramChoice(stage.values, spec.key, spec.defaultChoice));
|
||||
break;
|
||||
case ParamSpec::Type::Path: {
|
||||
// Written relative to the project folder, exactly as the
|
||||
// sources are, so a folder still opens once it is moved.
|
||||
const auto path = paramText(stage.values, spec.key, "");
|
||||
out << quote(path.empty() ? path : relativeTo(path, directory));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
out << (specs.empty() ? "}" : "\n }") << "\n }";
|
||||
@@ -418,6 +425,12 @@ Project projectFromJson(const std::string& text, const std::string& directory) {
|
||||
case ParamSpec::Type::Double:
|
||||
if (value->kind == Json::Kind::Number) stage.values[spec.key] = value->number;
|
||||
break;
|
||||
case ParamSpec::Type::Path:
|
||||
if (value->kind == Json::Kind::String) {
|
||||
stage.values[spec.key] =
|
||||
value->text.empty() ? value->text : resolve(value->text, directory);
|
||||
}
|
||||
break;
|
||||
case ParamSpec::Type::Choice:
|
||||
for (const auto& choice : spec.choices) {
|
||||
if (value->kind == Json::Kind::String && value->text == choice) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "effects/blur.hpp"
|
||||
#include "effects/grain.hpp"
|
||||
#include "effects/monochrome.hpp"
|
||||
#include "effects/overlay.hpp"
|
||||
#include "effects/scanlines.hpp"
|
||||
|
||||
namespace effector {
|
||||
@@ -14,6 +15,7 @@ const std::vector<EffectPtr>& effects() {
|
||||
std::make_shared<BlurEffect>(),
|
||||
std::make_shared<GrainEffect>(),
|
||||
std::make_shared<MonochromeEffect>(),
|
||||
std::make_shared<OverlayEffect>(),
|
||||
};
|
||||
return registry;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
#include "effects/overlay.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
#include "core/parallel.hpp"
|
||||
#include "effects/source_fill.hpp"
|
||||
|
||||
namespace effector {
|
||||
|
||||
const std::vector<ParamSpec>& OverlayEffect::params() const {
|
||||
static const std::vector<ParamSpec> specs = {
|
||||
ParamSpec::makePath("overlay_image", "Image",
|
||||
"The picture laid over the input: a logo, a texture, a frame."),
|
||||
ParamSpec::makeChoice("overlay_fit", "Fitting", "fit",
|
||||
{"fit", "fill", "stretch", "original", "tile"},
|
||||
"fit keeps the proportions and shows the whole image; fill keeps "
|
||||
"them and covers the canvas, cropping what does not fit; stretch "
|
||||
"distorts it to the canvas exactly; original leaves it at its own "
|
||||
"size; tile repeats it at its own size over the whole canvas."),
|
||||
ParamSpec::makeDouble("overlay_scale", "Scale", 1.0, 0.05, 8.0,
|
||||
"Resizes the fitted image (the tile, for tiling) by this factor.",
|
||||
0.05),
|
||||
ParamSpec::makeBool("overlay_center", "Centre", true,
|
||||
"Places the image in the middle of the canvas rather than at its top "
|
||||
"left corner; the offset is counted from there either way."),
|
||||
ParamSpec::makeInt("overlay_offset_x", "Offset X", 0, -20000, 20000,
|
||||
"Moves the image right by this many pixels (left when negative)."),
|
||||
ParamSpec::makeInt("overlay_offset_y", "Offset Y", 0, -20000, 20000,
|
||||
"Moves the image down by this many pixels (up when negative)."),
|
||||
ParamSpec::makeDouble("overlay_opacity", "Opacity", 1.0, 0.0, 1.0,
|
||||
"How strongly the image is mixed over the input; its own "
|
||||
"transparency is taken into account as well."),
|
||||
};
|
||||
return specs;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Where the overlay lands on the canvas, in canvas pixels.
|
||||
struct Placement {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int width = 1;
|
||||
int height = 1;
|
||||
};
|
||||
|
||||
Placement placeOverlay(const std::string& fit, double scale, bool centre, int offsetX, int offsetY,
|
||||
int srcWidth, int srcHeight, int width, int height) {
|
||||
double drawWidth = srcWidth;
|
||||
double drawHeight = srcHeight;
|
||||
if (fit == "stretch") {
|
||||
drawWidth = width;
|
||||
drawHeight = height;
|
||||
} else if (fit == "fit" || fit == "fill") {
|
||||
const double byWidth = static_cast<double>(width) / srcWidth;
|
||||
const double byHeight = static_cast<double>(height) / srcHeight;
|
||||
const double factor = fit == "fit" ? std::min(byWidth, byHeight) : std::max(byWidth, byHeight);
|
||||
drawWidth = srcWidth * factor;
|
||||
drawHeight = srcHeight * factor;
|
||||
}
|
||||
// "original" and "tile" keep the file's own size; the scale below applies
|
||||
// to every mode, so a fitted image can still be nudged a little larger.
|
||||
|
||||
Placement placement;
|
||||
placement.width = std::max(1, static_cast<int>(std::lround(drawWidth * scale)));
|
||||
placement.height = std::max(1, static_cast<int>(std::lround(drawHeight * scale)));
|
||||
placement.x = centre ? (width - placement.width) / 2 : 0;
|
||||
placement.y = centre ? (height - placement.height) / 2 : 0;
|
||||
placement.x += offsetX;
|
||||
placement.y += offsetY;
|
||||
return placement;
|
||||
}
|
||||
|
||||
int wrapIndex(int value, int limit) {
|
||||
const int wrapped = value % limit;
|
||||
return wrapped < 0 ? wrapped + limit : wrapped;
|
||||
}
|
||||
|
||||
// Bilinear sample of the overlay at a point in its own pixel coordinates,
|
||||
// clamped at the edges or wrapped around when the image is being tiled.
|
||||
void sampleOverlay(const Image& src, const std::uint8_t* alpha, double u, double v, bool wrap,
|
||||
int rgb[3], int& outAlpha) {
|
||||
const int w = src.width();
|
||||
const int h = src.height();
|
||||
const double fu = std::floor(u);
|
||||
const double fv = std::floor(v);
|
||||
const int u0 = static_cast<int>(fu);
|
||||
const int v0 = static_cast<int>(fv);
|
||||
const double du = u - fu;
|
||||
const double dv = v - fv;
|
||||
|
||||
int xs[2];
|
||||
int ys[2];
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
xs[i] = wrap ? wrapIndex(u0 + i, w) : std::clamp(u0 + i, 0, w - 1);
|
||||
ys[i] = wrap ? wrapIndex(v0 + i, h) : std::clamp(v0 + i, 0, h - 1);
|
||||
}
|
||||
|
||||
double accum[3] = {0.0, 0.0, 0.0};
|
||||
double accumAlpha = 0.0;
|
||||
for (int j = 0; j < 2; ++j) {
|
||||
const double wy = j == 0 ? 1.0 - dv : dv;
|
||||
const std::uint8_t* row = src.row(ys[j]);
|
||||
const std::uint8_t* alphaRow = alpha + static_cast<std::size_t>(ys[j]) * w;
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
const double weight = wy * (i == 0 ? 1.0 - du : du);
|
||||
const std::uint8_t* px = row + static_cast<std::size_t>(xs[i]) * 3;
|
||||
const double alphaWeight = weight * alphaRow[xs[i]];
|
||||
// Colour weighted by alpha: a transparent pixel in a PNG is usually
|
||||
// black, and averaging it in unweighted draws a dark fringe around
|
||||
// everything the image cuts out.
|
||||
for (int c = 0; c < 3; ++c) accum[c] += px[c] * alphaWeight;
|
||||
accumAlpha += alphaWeight;
|
||||
}
|
||||
}
|
||||
outAlpha = std::clamp(static_cast<int>(accumAlpha + 0.5), 0, 255);
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
rgb[c] = accumAlpha <= 0.0 ? 0
|
||||
: std::clamp(static_cast<int>(accum[c] / accumAlpha + 0.5), 0,
|
||||
255);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void OverlayEffect::loadOverlay(const std::string& path, Image& image,
|
||||
std::vector<std::uint8_t>& alpha) const {
|
||||
std::lock_guard<std::mutex> lock(cacheMutex_);
|
||||
if (cachedPath_ != path || cached_.empty()) {
|
||||
std::vector<std::uint8_t> loadedAlpha;
|
||||
Image loaded;
|
||||
try {
|
||||
loaded = Image::load(path, &loadedAlpha);
|
||||
} catch (const std::exception& error) {
|
||||
throw EffectError("Cannot read the overlay image '" + path + "': " + error.what());
|
||||
}
|
||||
if (loaded.empty()) throw EffectError("The overlay image '" + path + "' is empty.");
|
||||
cached_ = std::move(loaded);
|
||||
cachedAlpha_ = std::move(loadedAlpha);
|
||||
cachedPath_ = path;
|
||||
}
|
||||
image = cached_;
|
||||
alpha = cachedAlpha_;
|
||||
}
|
||||
|
||||
Image OverlayEffect::render(const std::vector<Image>& sources, int width, int height, Rng& rng,
|
||||
const ParamValues& values) const {
|
||||
if (width < 1 || height < 1) throw EffectError("Canvas size must be at least 1x1 pixels.");
|
||||
|
||||
Image canvas(width, height);
|
||||
if (!sources.empty()) {
|
||||
// Drawn before any threading, so that a seed always gives the same image.
|
||||
const std::size_t index = rng.below(sources.size() - 1);
|
||||
const Image& src = sources[index];
|
||||
const int sx = static_cast<int>(rng.inRange(0, std::max(0, src.width() - width)));
|
||||
const int sy = static_cast<int>(rng.inRange(0, std::max(0, src.height() - height)));
|
||||
fillFromSource(canvas, src, sx, sy);
|
||||
}
|
||||
|
||||
const std::string path = paramText(values, "overlay_image", "");
|
||||
const double opacity = std::clamp(paramDouble(values, "overlay_opacity", 1.0), 0.0, 1.0);
|
||||
// No image chosen yet is the state the interface starts in, not an error:
|
||||
// the stage simply passes its input through.
|
||||
if (path.empty() || opacity <= 0.0) return canvas;
|
||||
|
||||
Image overlay;
|
||||
std::vector<std::uint8_t> alpha;
|
||||
loadOverlay(path, overlay, alpha);
|
||||
|
||||
const std::string fit = paramChoice(values, "overlay_fit", "fit");
|
||||
const Placement place = placeOverlay(
|
||||
fit, std::clamp(paramDouble(values, "overlay_scale", 1.0), 0.05, 8.0),
|
||||
paramBool(values, "overlay_center", true),
|
||||
static_cast<int>(paramInt(values, "overlay_offset_x", 0)),
|
||||
static_cast<int>(paramInt(values, "overlay_offset_y", 0)), overlay.width(),
|
||||
overlay.height(), width, height);
|
||||
|
||||
const bool tiling = fit == "tile";
|
||||
// Source pixels per canvas pixel, the same both ways for every mode but
|
||||
// stretch, where the two axes are scaled apart on purpose.
|
||||
const double stepX = static_cast<double>(overlay.width()) / place.width;
|
||||
const double stepY = static_cast<double>(overlay.height()) / place.height;
|
||||
|
||||
const int firstRow = tiling ? 0 : std::max(0, place.y);
|
||||
const int lastRow = tiling ? height : std::min(height, place.y + place.height);
|
||||
const int firstCol = tiling ? 0 : std::max(0, place.x);
|
||||
const int lastCol = tiling ? width : std::min(width, place.x + place.width);
|
||||
if (firstRow >= lastRow || firstCol >= lastCol) return canvas; // entirely off the canvas
|
||||
|
||||
const int mix = static_cast<int>(opacity * 255.0 + 0.5);
|
||||
const std::uint8_t* alphaData = alpha.data();
|
||||
|
||||
parallelFor(static_cast<std::size_t>(lastRow - firstRow), [&](std::size_t begin,
|
||||
std::size_t end) {
|
||||
int rgb[3];
|
||||
int pixelAlpha = 255;
|
||||
for (std::size_t i = begin; i < end; ++i) {
|
||||
const int y = firstRow + static_cast<int>(i);
|
||||
const double v = (y - place.y + 0.5) * stepY - 0.5;
|
||||
std::uint8_t* out = canvas.row(y);
|
||||
for (int x = firstCol; x < lastCol; ++x) {
|
||||
const double u = (x - place.x + 0.5) * stepX - 0.5;
|
||||
sampleOverlay(overlay, alphaData, u, v, tiling, rgb, pixelAlpha);
|
||||
|
||||
const int weight = pixelAlpha * mix / 255;
|
||||
if (weight == 0) continue;
|
||||
std::uint8_t* px = out + static_cast<std::size_t>(x) * 3;
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
px[c] = static_cast<std::uint8_t>(px[c] + (rgb[c] - px[c]) * weight / 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
} // namespace effector
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/effect.hpp"
|
||||
|
||||
namespace effector {
|
||||
|
||||
// Lays one picture from disk over its input: a logo, a texture, a frame. The
|
||||
// overlay is fitted to the canvas in the chosen way, placed from the top left
|
||||
// or from the centre, nudged by an offset and mixed in by its opacity. Like the
|
||||
// blur it needs no source images of its own, so it can lead a chain; given
|
||||
// sources it fills the canvas from a randomly chosen one first.
|
||||
class OverlayEffect : public Effect {
|
||||
public:
|
||||
std::string name() const override { return "overlay"; }
|
||||
std::string title() const override { return "Image overlay"; }
|
||||
std::string description() const override {
|
||||
return "Lays a single image from disk over its input, fitted to the canvas, centred or "
|
||||
"not, nudged by an offset and mixed in by its opacity.";
|
||||
}
|
||||
const std::vector<ParamSpec>& params() const override;
|
||||
bool needsSources() const override { return false; }
|
||||
|
||||
Image render(const std::vector<Image>& sources, int width, int height, Rng& rng,
|
||||
const ParamValues& values) const override;
|
||||
|
||||
private:
|
||||
// The same file is re-read on every param change otherwise, and a render is
|
||||
// a keystroke away in the interface. Renders can overlap, so it is guarded.
|
||||
// Returns the decoded file and its alpha, reading it only when the path
|
||||
// differs from the one already held.
|
||||
void loadOverlay(const std::string& path, Image& image,
|
||||
std::vector<std::uint8_t>& alpha) const;
|
||||
|
||||
mutable std::mutex cacheMutex_;
|
||||
mutable std::string cachedPath_;
|
||||
mutable Image cached_;
|
||||
mutable std::vector<std::uint8_t> cachedAlpha_;
|
||||
};
|
||||
|
||||
} // namespace effector
|
||||
+59
-1
@@ -293,6 +293,28 @@ void EffectorWindow::buildParamWidgets() {
|
||||
widget = std::move(spin);
|
||||
break;
|
||||
}
|
||||
case ParamSpec::Type::Path: {
|
||||
// An entry holding the path, with a chooser beside it; the
|
||||
// entry is what paramValues() reads back.
|
||||
auto box = std::make_unique<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 6);
|
||||
auto* entry = Gtk::make_managed<Gtk::Entry>();
|
||||
entry->set_hexpand(true);
|
||||
entry->set_text(paramText(stage.values, spec.key, ""));
|
||||
entry->set_placeholder_text("No image chosen");
|
||||
entry->signal_activate().connect(
|
||||
sigc::mem_fun(*this, &EffectorWindow::onParamChanged));
|
||||
auto* choose = Gtk::make_managed<Gtk::Button>("Choose…");
|
||||
const int index = selectedStage_;
|
||||
const std::string key = spec.key;
|
||||
const std::string title = spec.label;
|
||||
choose->signal_clicked().connect(
|
||||
[this, index, key, title] { onChoosePath(index, key, title); });
|
||||
box->append(*entry);
|
||||
box->append(*choose);
|
||||
paramWidgets_.push_back({spec, entry});
|
||||
widget = std::move(box);
|
||||
break;
|
||||
}
|
||||
case ParamSpec::Type::Choice: {
|
||||
std::vector<Glib::ustring> choices(spec.choices.begin(), spec.choices.end());
|
||||
auto dropdown = std::make_unique<Gtk::DropDown>();
|
||||
@@ -311,7 +333,9 @@ void EffectorWindow::buildParamWidgets() {
|
||||
widget->set_hexpand(true);
|
||||
paramGrid_.attach(*label, 0, row);
|
||||
paramGrid_.attach(*widget, 1, row);
|
||||
paramWidgets_.push_back({spec, widget.get()});
|
||||
// A path attaches a box but reads back from the entry inside it, which
|
||||
// it has registered itself.
|
||||
if (spec.type != ParamSpec::Type::Path) paramWidgets_.push_back({spec, widget.get()});
|
||||
ownedWidgets_.push_back(std::move(label));
|
||||
ownedWidgets_.push_back(std::move(widget));
|
||||
++row;
|
||||
@@ -333,6 +357,10 @@ ParamValues EffectorWindow::paramValues() const {
|
||||
case ParamSpec::Type::Double:
|
||||
values[entry.spec.key] = static_cast<Gtk::SpinButton*>(entry.widget)->get_value();
|
||||
break;
|
||||
case ParamSpec::Type::Path:
|
||||
values[entry.spec.key] =
|
||||
std::string(static_cast<Gtk::Entry*>(entry.widget)->get_text());
|
||||
break;
|
||||
case ParamSpec::Type::Choice: {
|
||||
const auto index = static_cast<Gtk::DropDown*>(entry.widget)->get_selected();
|
||||
if (index < entry.spec.choices.size()) values[entry.spec.key] = entry.spec.choices[index];
|
||||
@@ -809,6 +837,36 @@ void EffectorWindow::onParamChanged() {
|
||||
requestRender();
|
||||
}
|
||||
|
||||
void EffectorWindow::onChoosePath(int stage, const std::string& key, const std::string& label) {
|
||||
auto dialog = Gtk::FileDialog::create();
|
||||
dialog->set_title("Select the " + label);
|
||||
|
||||
auto filter = Gtk::FileFilter::create();
|
||||
filter->set_name("Images");
|
||||
for (const char* pattern : kImagePatterns) filter->add_pattern(pattern);
|
||||
auto filters = Gio::ListStore<Gtk::FileFilter>::create();
|
||||
filters->append(filter);
|
||||
dialog->set_filters(filters);
|
||||
dialog->set_default_filter(filter);
|
||||
|
||||
dialog->open(*this, [this, dialog, stage, key](const Glib::RefPtr<Gio::AsyncResult>& result) {
|
||||
std::string path;
|
||||
try {
|
||||
const auto file = dialog->open_finish(result);
|
||||
if (file) path = file->get_path();
|
||||
} catch (const Glib::Error&) {
|
||||
return; // cancelled
|
||||
}
|
||||
if (path.empty() || stage < 0 || static_cast<std::size_t>(stage) >= stages_.size()) return;
|
||||
// The widgets may be showing another stage by now: store what they hold
|
||||
// first, then write the path into the stage that asked for it.
|
||||
storeParams();
|
||||
stages_[static_cast<std::size_t>(stage)].values[key] = path;
|
||||
if (stage == selectedStage_) buildParamWidgets();
|
||||
requestRender();
|
||||
});
|
||||
}
|
||||
|
||||
void EffectorWindow::onSeedEntered() {
|
||||
try {
|
||||
seed_ = std::stoull(seedEntry_.get_text());
|
||||
|
||||
@@ -75,6 +75,10 @@ private:
|
||||
void updateTitle();
|
||||
|
||||
void onParamChanged();
|
||||
// The chooser behind a Path param. The chosen file goes straight into the
|
||||
// stage's values rather than into the widget, which may well have been
|
||||
// rebuilt by the time the dialog comes back.
|
||||
void onChoosePath(int stage, const std::string& key, const std::string& label);
|
||||
void onSeedEntered();
|
||||
void onRecreate();
|
||||
void onSave();
|
||||
|
||||
Reference in New Issue
Block a user