Let several text stages share one named look
A picture with a title, a caption and a credit meant typing the same font, size and colour into every text stage, and changing the look meant typing them all again. A project now holds named styles: a stage follows one, and editing it moves every stage that follows it at once. The front-ends learn nothing effect-specific. A ParamSpec can be marked .styled(), saying a style carries it rather than the stage, and Effect::styleKind() names the pool it comes from -- so styleSpecs(kind) is to a style what params() is to a stage, and a second text-ish effect would slot into the styles that already exist. The look is what a style carries; the words and the placement stay on the stage, which is what lets two stages share a typography in two different corners. stageValues() resolves one in a single place, and a stage keeps a full set of values of its own beside the style, so switching it to Custom, dropping the style, or opening the project in a build that does not know it renders exactly the same picture. A style moves stages together; it never holds the only copy of anything. The command line builds no styles and passes none, so it renders what it always did. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+325
-11
@@ -6,6 +6,7 @@
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
#include "core/canvas.hpp"
|
||||
#include "core/chain.hpp"
|
||||
@@ -360,6 +361,7 @@ void EffectorWindow::buildParamWidgets() {
|
||||
while (auto* child = paramGrid_.get_first_child()) paramGrid_.remove(*child);
|
||||
paramWidgets_.clear();
|
||||
ownedWidgets_.clear();
|
||||
styleDrop_ = nullptr;
|
||||
|
||||
if (stages_.empty()) {
|
||||
paramHeading_.set_text("No effects");
|
||||
@@ -372,17 +374,49 @@ void EffectorWindow::buildParamWidgets() {
|
||||
(stage.enabled ? " settings" : " settings (off)"));
|
||||
|
||||
int row = 0;
|
||||
// The style the stage follows, when its effect takes one at all: the
|
||||
// params it carries are then shown and edited on the style rather than on
|
||||
// the stage, which is what makes one change move every stage using it.
|
||||
const Style* style = selectedStyle();
|
||||
if (!stage.effect->styleKind().empty()) {
|
||||
auto label = std::make_unique<Gtk::Label>("Style");
|
||||
label->set_xalign(0.0F);
|
||||
label->set_tooltip_text(
|
||||
"A named set of formatting shared by several stages: the font, the colour and the "
|
||||
"box, but never the words or the placement.");
|
||||
auto widget = buildStyleRow(stage);
|
||||
widget->set_hexpand(true);
|
||||
paramGrid_.attach(*label, 0, row);
|
||||
paramGrid_.attach(*widget, 1, row);
|
||||
ownedWidgets_.push_back(std::move(label));
|
||||
ownedWidgets_.push_back(std::move(widget));
|
||||
++row;
|
||||
}
|
||||
|
||||
for (const auto& spec : stage.effect->params()) {
|
||||
// A styled param is read from the style while one is followed, so the
|
||||
// widgets show what the stage will actually render with.
|
||||
const bool shared = style != nullptr && spec.inStyle;
|
||||
const ParamValues& values = shared ? style->values : stage.values;
|
||||
|
||||
auto label = std::make_unique<Gtk::Label>(spec.label);
|
||||
label->set_xalign(0.0F);
|
||||
if (!spec.help.empty()) label->set_tooltip_text(spec.help);
|
||||
std::string help = spec.help;
|
||||
if (shared) {
|
||||
label->set_markup(Glib::Markup::escape_text(spec.label) +
|
||||
" <span alpha='55%'>(style)</span>");
|
||||
if (!help.empty()) help += "\n\n";
|
||||
help += "Part of the style \"" + style->name +
|
||||
"\": changing it changes every stage that follows it.";
|
||||
}
|
||||
if (!help.empty()) label->set_tooltip_text(help);
|
||||
|
||||
std::unique_ptr<Gtk::Widget> widget;
|
||||
switch (spec.type) {
|
||||
case ParamSpec::Type::Bool: {
|
||||
auto toggle = std::make_unique<Gtk::Switch>();
|
||||
toggle->set_halign(Gtk::Align::START);
|
||||
toggle->set_active(paramBool(stage.values, spec.key, spec.defaultValue != 0.0));
|
||||
toggle->set_active(paramBool(values, spec.key, spec.defaultValue != 0.0));
|
||||
toggle->property_active().signal_changed().connect(
|
||||
sigc::mem_fun(*this, &EffectorWindow::onParamChanged));
|
||||
widget = std::move(toggle);
|
||||
@@ -395,9 +429,9 @@ void EffectorWindow::buildParamWidgets() {
|
||||
spin->set_increments(spec.step, spec.step * 10);
|
||||
spin->set_digits(spec.type == ParamSpec::Type::Int ? 0 : spec.digits);
|
||||
spin->set_value(spec.type == ParamSpec::Type::Int
|
||||
? static_cast<double>(paramInt(stage.values, spec.key,
|
||||
? static_cast<double>(paramInt(values, spec.key,
|
||||
static_cast<long>(spec.defaultValue)))
|
||||
: paramDouble(stage.values, spec.key, spec.defaultValue));
|
||||
: paramDouble(values, spec.key, spec.defaultValue));
|
||||
spin->signal_value_changed().connect(
|
||||
sigc::mem_fun(*this, &EffectorWindow::onParamChanged));
|
||||
widget = std::move(spin);
|
||||
@@ -409,7 +443,7 @@ void EffectorWindow::buildParamWidgets() {
|
||||
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_text(paramText(values, spec.key, ""));
|
||||
entry->set_placeholder_text("No image chosen");
|
||||
entry->signal_activate().connect(
|
||||
sigc::mem_fun(*this, &EffectorWindow::onParamChanged));
|
||||
@@ -427,7 +461,7 @@ void EffectorWindow::buildParamWidgets() {
|
||||
}
|
||||
case ParamSpec::Type::Text: {
|
||||
auto entry = std::make_unique<Gtk::Entry>();
|
||||
entry->set_text(paramText(stage.values, spec.key, spec.defaultChoice));
|
||||
entry->set_text(paramText(values, spec.key, spec.defaultChoice));
|
||||
// Re-rendering on every keystroke would start a render per
|
||||
// letter typed, so the text is taken when the entry is left or
|
||||
// Enter is pressed.
|
||||
@@ -444,7 +478,7 @@ void EffectorWindow::buildParamWidgets() {
|
||||
auto button = std::make_unique<Gtk::ColorDialogButton>(Gtk::ColorDialog::create());
|
||||
button->set_halign(Gtk::Align::START);
|
||||
std::uint8_t rgb[3];
|
||||
paramColor(stage.values, spec.key, spec.defaultChoice, rgb);
|
||||
paramColor(values, spec.key, spec.defaultChoice, rgb);
|
||||
Gdk::RGBA colour;
|
||||
colour.set_rgba(rgb[0] / 255.0, rgb[1] / 255.0, rgb[2] / 255.0, 1.0);
|
||||
button->set_rgba(colour);
|
||||
@@ -457,7 +491,7 @@ void EffectorWindow::buildParamWidgets() {
|
||||
std::vector<Glib::ustring> choices(spec.choices.begin(), spec.choices.end());
|
||||
auto dropdown = std::make_unique<Gtk::DropDown>();
|
||||
dropdown->set_model(Gtk::StringList::create(choices));
|
||||
const auto current = paramChoice(stage.values, spec.key, spec.defaultChoice);
|
||||
const auto current = paramChoice(values, spec.key, spec.defaultChoice);
|
||||
for (std::size_t i = 0; i < spec.choices.size(); ++i) {
|
||||
if (spec.choices[i] == current) dropdown->set_selected(i);
|
||||
}
|
||||
@@ -523,7 +557,282 @@ ParamValues EffectorWindow::paramValues() const {
|
||||
// whenever they change; the other stages keep the settings they were given.
|
||||
void EffectorWindow::storeParams() {
|
||||
if (stages_.empty()) return;
|
||||
stages_[static_cast<std::size_t>(selectedStage_)].values = paramValues();
|
||||
auto& stage = stages_[static_cast<std::size_t>(selectedStage_)];
|
||||
const ParamValues values = paramValues();
|
||||
stage.values = values;
|
||||
|
||||
// A param the style carries is written to the style too -- that is how the
|
||||
// look of every stage following it is changed from here. The stage keeps
|
||||
// its own copy of the same value, so leaving the style, or losing it, does
|
||||
// not move the picture.
|
||||
if (Style* style = selectedStyle()) {
|
||||
for (const auto& spec : stage.effect->params()) {
|
||||
if (!spec.inStyle) continue;
|
||||
const auto found = values.find(spec.key);
|
||||
if (found != values.end()) style->values[spec.key] = found->second;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------- text styles ---
|
||||
|
||||
std::vector<std::string> EffectorWindow::styleNames(const std::string& kind) const {
|
||||
std::vector<std::string> names;
|
||||
for (const auto& style : styles_) {
|
||||
if (style.kind == kind) names.push_back(style.name);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
Style* EffectorWindow::selectedStyle() {
|
||||
return const_cast<Style*>(std::as_const(*this).selectedStyle());
|
||||
}
|
||||
|
||||
const Style* EffectorWindow::selectedStyle() const {
|
||||
if (stages_.empty()) return nullptr;
|
||||
const auto& stage = stages_[static_cast<std::size_t>(selectedStage_)];
|
||||
return findStyle(styles_, stage.effect->styleKind(), stage.style);
|
||||
}
|
||||
|
||||
bool EffectorWindow::styleNameTaken(const std::string& kind, const std::string& name) const {
|
||||
return findStyle(styles_, kind, name) != nullptr;
|
||||
}
|
||||
|
||||
std::unique_ptr<Gtk::Widget> EffectorWindow::buildStyleRow(const ChainStage& stage) {
|
||||
auto box = std::make_unique<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 6);
|
||||
|
||||
const auto names = styleNames(stage.effect->styleKind());
|
||||
// "Custom" first so that the stage standing on its own is the plain case,
|
||||
// and "New style…" last so that the names in between keep their indices.
|
||||
std::vector<Glib::ustring> entries{"Custom"};
|
||||
for (const auto& name : names) entries.emplace_back(name);
|
||||
entries.emplace_back("New style…");
|
||||
|
||||
auto* drop = Gtk::make_managed<Gtk::DropDown>();
|
||||
drop->set_model(Gtk::StringList::create(entries));
|
||||
drop->set_hexpand(true);
|
||||
suspendStyleSelect_ = true;
|
||||
drop->set_selected(0);
|
||||
for (std::size_t i = 0; i < names.size(); ++i) {
|
||||
if (names[i] == stage.style) drop->set_selected(i + 1);
|
||||
}
|
||||
suspendStyleSelect_ = false;
|
||||
drop->property_selected().signal_changed().connect(
|
||||
sigc::mem_fun(*this, &EffectorWindow::onStyleSelected));
|
||||
box->append(*drop);
|
||||
styleDrop_ = drop;
|
||||
|
||||
// Renaming and removing only make sense for a style that is being followed.
|
||||
auto* rename = Gtk::make_managed<Gtk::Button>("Rename…");
|
||||
rename->set_sensitive(!stage.style.empty());
|
||||
rename->signal_clicked().connect(sigc::mem_fun(*this, &EffectorWindow::onRenameStyle));
|
||||
box->append(*rename);
|
||||
|
||||
auto* remove = Gtk::make_managed<Gtk::Button>("Remove");
|
||||
remove->set_sensitive(!stage.style.empty());
|
||||
remove->set_tooltip_text(
|
||||
"Drops the style. Every stage following it keeps the look it has and goes back to its own "
|
||||
"settings.");
|
||||
remove->signal_clicked().connect(sigc::mem_fun(*this, &EffectorWindow::onRemoveStyle));
|
||||
box->append(*remove);
|
||||
|
||||
return box;
|
||||
}
|
||||
|
||||
void EffectorWindow::onStyleSelected() {
|
||||
if (suspendStyleSelect_ || styleDrop_ == nullptr || stages_.empty()) return;
|
||||
const auto& stage = stages_[static_cast<std::size_t>(selectedStage_)];
|
||||
const auto names = styleNames(stage.effect->styleKind());
|
||||
const auto index = styleDrop_->get_selected();
|
||||
|
||||
if (index == 0) {
|
||||
adoptStyle("");
|
||||
} else if (index <= names.size()) {
|
||||
adoptStyle(names[index - 1]);
|
||||
} else {
|
||||
onNewStyle(); // the last entry
|
||||
}
|
||||
}
|
||||
|
||||
// Pointing the stage at a style, or at nothing. The style's values are copied
|
||||
// onto the stage as well, so the project file always says what the stage would
|
||||
// render if the style went away -- which is the same picture.
|
||||
void EffectorWindow::adoptStyle(const std::string& name) {
|
||||
if (stages_.empty()) return;
|
||||
auto& stage = stages_[static_cast<std::size_t>(selectedStage_)];
|
||||
stage.style = name;
|
||||
if (const Style* style = selectedStyle()) {
|
||||
for (const auto& spec : stage.effect->params()) {
|
||||
if (!spec.inStyle) continue;
|
||||
const auto found = style->values.find(spec.key);
|
||||
if (found != style->values.end()) stage.values[spec.key] = found->second;
|
||||
}
|
||||
}
|
||||
buildParamWidgets();
|
||||
requestRender();
|
||||
}
|
||||
|
||||
void EffectorWindow::onNewStyle() {
|
||||
if (stages_.empty()) return;
|
||||
const auto& stage = stages_[static_cast<std::size_t>(selectedStage_)];
|
||||
const std::string kind = stage.effect->styleKind();
|
||||
if (kind.empty()) return;
|
||||
|
||||
askStyleName("New style", "Style " + std::to_string(styleNames(kind).size() + 1),
|
||||
[this, kind](const std::string& name) {
|
||||
// Seeded from the stage it was made on, so the style
|
||||
// starts as the look that is already on screen.
|
||||
Style style;
|
||||
style.kind = kind;
|
||||
style.name = name;
|
||||
style.values = styleDefaults(kind);
|
||||
const auto& current = stages_[static_cast<std::size_t>(selectedStage_)];
|
||||
for (const auto& spec : current.effect->params()) {
|
||||
if (!spec.inStyle) continue;
|
||||
const auto found = current.values.find(spec.key);
|
||||
if (found != current.values.end()) style.values[spec.key] = found->second;
|
||||
}
|
||||
styles_.push_back(std::move(style));
|
||||
adoptStyle(name);
|
||||
});
|
||||
}
|
||||
|
||||
void EffectorWindow::onRenameStyle() {
|
||||
const Style* style = selectedStyle();
|
||||
if (style == nullptr) return;
|
||||
const std::string kind = style->kind;
|
||||
const std::string was = style->name;
|
||||
|
||||
askStyleName("Rename style", was, [this, kind, was](const std::string& name) {
|
||||
if (name == was) return;
|
||||
for (auto& candidate : styles_) {
|
||||
if (candidate.kind == kind && candidate.name == was) candidate.name = name;
|
||||
}
|
||||
// Every stage that followed the old name follows the new one: a rename
|
||||
// must not quietly cut a stage loose from its look.
|
||||
for (auto& stage : stages_) {
|
||||
if (stage.effect->styleKind() == kind && stage.style == was) stage.style = name;
|
||||
}
|
||||
buildParamWidgets();
|
||||
});
|
||||
}
|
||||
|
||||
void EffectorWindow::onRemoveStyle() {
|
||||
const Style* style = selectedStyle();
|
||||
if (style == nullptr) return;
|
||||
const std::string kind = style->kind;
|
||||
const std::string name = style->name;
|
||||
|
||||
// The stages keep the values they were rendering with, so dropping a style
|
||||
// changes nothing in the picture -- it only stops the stages moving
|
||||
// together.
|
||||
for (auto& stage : stages_) {
|
||||
if (stage.effect->styleKind() == kind && stage.style == name) stage.style.clear();
|
||||
}
|
||||
styles_.erase(std::remove_if(styles_.begin(), styles_.end(),
|
||||
[&](const Style& candidate) {
|
||||
return candidate.kind == kind && candidate.name == name;
|
||||
}),
|
||||
styles_.end());
|
||||
buildParamWidgets();
|
||||
requestRender();
|
||||
}
|
||||
|
||||
// Built from scratch each time it is needed, like the progress window, so the
|
||||
// entry it holds is a raw pointer that closeStyleWindow() nulls.
|
||||
void EffectorWindow::askStyleName(const std::string& title, const std::string& initial,
|
||||
const std::function<void(const std::string&)>& accept) {
|
||||
closeStyleWindow();
|
||||
if (stages_.empty()) return;
|
||||
const std::string kind = stages_[static_cast<std::size_t>(selectedStage_)].effect->styleKind();
|
||||
|
||||
styleWindow_ = std::make_shared<Gtk::Window>();
|
||||
styleWindow_->set_title(title);
|
||||
styleWindow_->set_transient_for(*this);
|
||||
styleWindow_->set_modal(true);
|
||||
styleWindow_->set_resizable(false);
|
||||
styleWindow_->set_default_size(360, -1);
|
||||
|
||||
auto* box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 12);
|
||||
box->set_margin(18);
|
||||
|
||||
auto* entry = Gtk::make_managed<Gtk::Entry>();
|
||||
entry->set_text(initial);
|
||||
entry->set_activates_default(false);
|
||||
box->append(*entry);
|
||||
styleNameEntry_ = entry;
|
||||
|
||||
auto* error = Gtk::make_managed<Gtk::Label>("");
|
||||
error->set_xalign(0.0F);
|
||||
error->add_css_class("dim-label");
|
||||
error->set_visible(false);
|
||||
box->append(*error);
|
||||
styleNameError_ = error;
|
||||
|
||||
auto* buttons = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 6);
|
||||
buttons->set_halign(Gtk::Align::END);
|
||||
|
||||
auto* cancel = Gtk::make_managed<Gtk::Button>("Cancel");
|
||||
cancel->signal_clicked().connect([this] {
|
||||
closeStyleWindow();
|
||||
// The dropdown may be sitting on "New style…"; put it back on what the
|
||||
// stage actually follows.
|
||||
buildParamWidgets();
|
||||
});
|
||||
buttons->append(*cancel);
|
||||
|
||||
auto* confirm = Gtk::make_managed<Gtk::Button>("OK");
|
||||
confirm->add_css_class("suggested-action");
|
||||
// A name that is empty or already in the pool is refused in place rather
|
||||
// than silently changed, so two styles can never share a name.
|
||||
const auto submit = [this, kind, accept] {
|
||||
if (styleNameEntry_ == nullptr) return;
|
||||
const std::string name = styleNameEntry_->get_text();
|
||||
const char* complaint = nullptr;
|
||||
if (name.empty()) {
|
||||
complaint = "A style needs a name.";
|
||||
} else if (styleNameTaken(kind, name)) {
|
||||
complaint = "There is already a style with that name.";
|
||||
}
|
||||
if (complaint != nullptr) {
|
||||
if (styleNameError_ != nullptr) {
|
||||
styleNameError_->set_text(complaint);
|
||||
styleNameError_->set_visible(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
closeStyleWindow();
|
||||
accept(name);
|
||||
};
|
||||
confirm->signal_clicked().connect(submit);
|
||||
entry->signal_activate().connect(submit);
|
||||
buttons->append(*confirm);
|
||||
box->append(*buttons);
|
||||
|
||||
styleWindow_->signal_close_request().connect(
|
||||
[this] {
|
||||
closeStyleWindow();
|
||||
buildParamWidgets();
|
||||
return true;
|
||||
},
|
||||
false);
|
||||
|
||||
styleWindow_->set_child(*box);
|
||||
styleWindow_->present();
|
||||
entry->grab_focus();
|
||||
}
|
||||
|
||||
void EffectorWindow::closeStyleWindow() {
|
||||
styleNameEntry_ = nullptr;
|
||||
styleNameError_ = nullptr;
|
||||
if (!styleWindow_) return;
|
||||
styleWindow_->set_hide_on_close(false);
|
||||
styleWindow_->hide();
|
||||
// Closing is asked for by the window's own buttons and by its close
|
||||
// request, so it is torn down once that signal has finished running rather
|
||||
// than underneath it.
|
||||
Glib::signal_idle().connect_once([window = styleWindow_] {});
|
||||
styleWindow_.reset();
|
||||
}
|
||||
|
||||
void EffectorWindow::applyInitial(const Options& initial) {
|
||||
@@ -1120,6 +1429,7 @@ void EffectorWindow::startRender() {
|
||||
|
||||
const auto sources = sources_;
|
||||
const auto stages = stages_; // a snapshot, like the sources
|
||||
const auto styles = styles_; // the same, so the worker reads nothing live
|
||||
const int width = static_cast<int>(widthSpin_.get_value());
|
||||
const int height = static_cast<int>(heightSpin_.get_value());
|
||||
const std::uint64_t seed = seed_;
|
||||
@@ -1127,13 +1437,13 @@ void EffectorWindow::startRender() {
|
||||
spinner_.start();
|
||||
recreateButton_.set_sensitive(false);
|
||||
|
||||
worker_ = std::thread([this, sources, stages, width, height, seed] {
|
||||
worker_ = std::thread([this, sources, stages, styles, width, height, seed] {
|
||||
Image image;
|
||||
std::string error;
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
try {
|
||||
Rng rng(seed);
|
||||
image = renderChain(stages, *sources, width, height, rng);
|
||||
image = renderChain(stages, *sources, width, height, rng, styles);
|
||||
} catch (const std::exception& exception) {
|
||||
error = exception.what();
|
||||
}
|
||||
@@ -1241,6 +1551,7 @@ Project EffectorWindow::currentProject() const {
|
||||
Project project;
|
||||
for (const auto& image : *sources_) project.sources.push_back(image.path());
|
||||
project.stages = stages_;
|
||||
project.styles = styles_;
|
||||
project.width = static_cast<int>(widthSpin_.get_value());
|
||||
project.height = static_cast<int>(heightSpin_.get_value());
|
||||
project.format = canvasFormatKey();
|
||||
@@ -1252,9 +1563,12 @@ Project EffectorWindow::currentProject() const {
|
||||
void EffectorWindow::applyProject(const Project& project, const std::string& path) {
|
||||
// One render at the end rather than one per piece of the project.
|
||||
suspendRender_ = true;
|
||||
closeStyleWindow(); // it names a style of the project being replaced
|
||||
clearSources();
|
||||
|
||||
stages_ = project.stages;
|
||||
// Before the rows and the params: both ask which style a stage follows.
|
||||
styles_ = project.styles;
|
||||
selectedStage_ = 0;
|
||||
rebuildChainRows();
|
||||
buildParamWidgets();
|
||||
|
||||
+33
-1
@@ -7,6 +7,7 @@
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
@@ -16,6 +17,7 @@
|
||||
#include "core/effect.hpp"
|
||||
#include "core/image.hpp"
|
||||
#include "core/project.hpp"
|
||||
#include "core/style.hpp"
|
||||
|
||||
namespace effector {
|
||||
|
||||
@@ -30,7 +32,29 @@ private:
|
||||
Gtk::Widget& buildPreview();
|
||||
void buildParamWidgets();
|
||||
ParamValues paramValues() const;
|
||||
void storeParams(); // widgets -> the selected stage's values
|
||||
void storeParams(); // widgets -> the selected stage, and its style
|
||||
|
||||
// The style row above the params of a stage whose effect takes one.
|
||||
std::unique_ptr<Gtk::Widget> buildStyleRow(const ChainStage& stage);
|
||||
// The names of the pool the selected stage draws from, in the order the
|
||||
// dropdown shows them.
|
||||
std::vector<std::string> styleNames(const std::string& kind) const;
|
||||
// The style the selected stage follows, or nullptr when it stands alone.
|
||||
Style* selectedStyle();
|
||||
const Style* selectedStyle() const;
|
||||
void onStyleSelected(); // the dropdown: adopt, drop, or make a new one
|
||||
void adoptStyle(const std::string& name);
|
||||
void onNewStyle();
|
||||
void onRenameStyle();
|
||||
void onRemoveStyle();
|
||||
// The little modal that asks for a style name, used by both New and
|
||||
// Rename. Built from scratch each time, like the progress window.
|
||||
void askStyleName(const std::string& title, const std::string& initial,
|
||||
const std::function<void(const std::string&)>& accept);
|
||||
void closeStyleWindow();
|
||||
// A name that is empty, or already used in the pool, is refused in place.
|
||||
bool styleNameTaken(const std::string& kind, const std::string& name) const;
|
||||
|
||||
void applyInitial(const Options& initial);
|
||||
|
||||
bool canRender() const; // a chain, and sources if the chain needs them
|
||||
@@ -114,6 +138,9 @@ private:
|
||||
// The cascade: stage 0 works on the sources, every later stage on the
|
||||
// output of the one before it.
|
||||
std::vector<ChainStage> stages_;
|
||||
// The named looks the stages share. Edited through the params of whichever
|
||||
// stage follows one, and saved with the project.
|
||||
StyleSet styles_;
|
||||
int selectedStage_ = 0;
|
||||
bool suspendStageSelect_ = false; // guards the rebuild of the chain rows
|
||||
Image result_;
|
||||
@@ -166,6 +193,11 @@ private:
|
||||
// Set while a format fills the spins, so their own handler does not queue
|
||||
// a second render on top of the one the format change asks for.
|
||||
bool suspendCanvasSync_ = false;
|
||||
Gtk::DropDown* styleDrop_ = nullptr; // null while the stage takes no style
|
||||
bool suspendStyleSelect_ = false; // guards filling the dropdown
|
||||
std::shared_ptr<Gtk::Window> styleWindow_;
|
||||
Gtk::Entry* styleNameEntry_ = nullptr; // nulled with the window
|
||||
Gtk::Label* styleNameError_ = nullptr;
|
||||
Gtk::Grid paramGrid_;
|
||||
Gtk::Label paramHeading_;
|
||||
Gtk::Entry seedEntry_;
|
||||
|
||||
Reference in New Issue
Block a user