Files
effector/src/gui/window.hpp
T
SiandClaude Opus 5.5 b9f905e04c Give Effector a version and a changelog, shown from the menu
The version is the project() line of CMakeLists.txt and nowhere else:
CMake writes it, with the text of CHANGELOG.md, into a generated
core/version.hpp, so the number and the release notes the binary shows
are the ones it was built from. effector --version (-V) prints it, and
the header-bar menu gains a section with Changelog, a window laying the
Markdown out as headings and bullets, and About Effector.

The core tests hold the newest changelog entry to the current version,
so a bump cannot go in without its notes. Releases are tagged vX.Y.Z;
this is 0.5.0, and the changelog reconstructs 0.1.0-0.4.0 from the
history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 10:14:43 +02:00

258 lines
10 KiB
C++

#pragma once
#include <gtkmm.h>
#include <atomic>
#include <cstdint>
#include <memory>
#include <mutex>
#include <optional>
#include <functional>
#include <string>
#include <thread>
#include <vector>
#include "cli/options.hpp"
#include "core/chain.hpp"
#include "core/effect.hpp"
#include "core/image.hpp"
#include "core/project.hpp"
#include "core/style.hpp"
namespace effector {
class EffectorWindow : public Gtk::ApplicationWindow {
public:
explicit EffectorWindow(const Options& initial);
~EffectorWindow() override;
private:
void buildUi();
Gtk::Widget& buildSidebar();
Gtk::Widget& buildChainTab();
Gtk::Widget& buildSetupTab();
Gtk::Widget& buildPreview();
void buildParamWidgets();
ParamValues paramValues() const;
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
std::string missingText() const; // what is still missing before a render
void showPlaceholder(); // drops the preview and says what is missing
void requestRender(); // coalesced: restarts the short debounce timer
void startRender(); // starts a worker, or marks the result stale
void onAddImages();
void onImagesChosen(const Glib::RefPtr<Gio::AsyncResult>& result,
const Glib::RefPtr<Gtk::FileDialog>& dialog);
void addPaths(const std::vector<std::string>& paths); // decodes in the background
void onLoadProgress(); // dispatcher: one file finished decoding
void onLoadFinished(); // dispatcher: the batch is done or was cancelled
void showLoadWindow();
void closeLoadWindow();
void cancelLoad();
void appendSourceRow(const std::string& path);
void updateSourcesLabel(); // the heading shows the count
// The whole sources section only shows while the chain asks for sources.
void updateSourcesVisible();
Gtk::Widget& buildCanvasSection();
// "" while "Custom" is picked, otherwise the format's key.
std::string canvasFormatKey() const;
void setCanvasFormat(const std::string& key, int dpi); // dropdown and dpi
// A format owns the pixel sizes, so the spins follow it and are greyed
// out; "Custom" hands them back to the user untouched.
void syncCanvasWidgets();
void removeSource(Gtk::ListBoxRow* row);
Gtk::Widget& buildChainSection();
Gtk::Widget& buildAddMenu(); // the menu of effects that can be appended
void appendStage(const EffectPtr& effect);
void removeStage(int index);
void moveStage(int from, int to); // `to` is the index it ends up at
// Dragging a row: the slot between rows it would drop into, and the line
// that shows it.
int chainDropSlot(double y) const;
void showChainDropSlot(int slot);
// The check on a chain row: the stage stays where it is, and is skipped.
void setStageEnabled(int index, bool enabled);
void rebuildChainRows();
// Rests the split under the chain's rows, until the user drags it.
void scheduleChainSplitFit();
void fitChainSplit();
void refitChainSplit(); // the double-click on the handle
void onChainSplitMoved();
std::string stageLabel(std::size_t index) const;
void selectStage(int index);
void onStageSelected();
Gtk::Widget& buildMainMenu(); // the header menu: new, open, save project
void showChangelog();
void showAbout();
Project currentProject() const;
void applyProject(const Project& project, const std::string& path);
void clearSources();
void onNewProject();
void onOpenProject();
void openProject(const std::string& path);
// Open Recent: the projects this program opened or saved, newest first,
// kept in the desktop's shared list of recently used files.
void rememberProject(const std::string& path);
void rebuildRecentMenu();
void onProjectChosen(const Glib::RefPtr<Gio::AsyncResult>& result,
const Glib::RefPtr<Gtk::FileDialog>& dialog);
void onSaveProject(); // falls back to Save As while the project is untitled
void onSaveProjectAs();
void onSaveProjectChosen(const Glib::RefPtr<Gio::AsyncResult>& result,
const Glib::RefPtr<Gtk::FileDialog>& dialog);
void writeProject(const std::string& path);
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();
void onSaveChosen(const Glib::RefPtr<Gio::AsyncResult>& result,
const Glib::RefPtr<Gtk::FileDialog>& dialog);
void onRenderFinished(); // runs on the main thread via the dispatcher
// Sources are handed to the worker as an immutable snapshot, so adding or
// removing an image while a render is in flight cannot race with it.
using SourceList = std::vector<Image>;
template <typename Mutate>
void mutateSources(Mutate&& mutate);
void showError(const std::string& message);
void clearError();
// Model
std::shared_ptr<const SourceList> sources_ = std::make_shared<SourceList>();
std::vector<EffectPtr> effects_;
// 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
// The split between the chain and the params, and where it was last put
// to fit the rows: any other position the user reached by dragging.
Gtk::Paned* chainSplit_ = nullptr;
int fittedChainSplit_ = -1;
bool chainSplitDragged_ = false;
bool suspendSplitTrack_ = false;
sigc::connection chainSplitFit_;
// The name label of each chain row, renamed in place as params change.
std::vector<Gtk::Label*> stageLabels_;
Image result_;
std::uint64_t seed_ = 0;
std::string projectPath_; // empty until the project has been saved once
Glib::RefPtr<Gio::Menu> recentMenu_;
sigc::connection recentChanged_; // the recent list outlives the window
bool suspendRender_ = true;
// Source loading. Decoding a batch of large photographs takes seconds, so
// it runs off the main thread and reports into a small progress window.
std::thread loader_;
Glib::Dispatcher loadProgress_;
Glib::Dispatcher loadFinished_;
std::atomic<bool> loadCancel_{false};
std::atomic<std::size_t> loadDone_{0};
std::size_t loadTotal_ = 0;
std::mutex loadMutex_;
std::string loadCurrent_;
std::vector<Image> loadedImages_;
std::vector<LoadFailure> loadFailures_;
std::vector<std::string> loadQueue_; // chosen while another batch was loading
bool loading_ = false;
bool dropLoadedImages_ = false; // the batch in flight belongs to a replaced project
sigc::connection loadWindowDelay_;
// Render job
Glib::Dispatcher renderDone_;
std::thread worker_;
std::mutex resultMutex_;
bool rendering_ = false;
bool staleResult_ = false;
sigc::connection debounce_;
Image pendingImage_;
std::string pendingError_;
double pendingMillis_ = 0.0;
// Widgets
Gtk::Label banner_;
Gtk::Button* addButton_ = nullptr;
// Built fresh with the progress window, and null whenever it is closed.
std::unique_ptr<Gtk::Window> loadWindow_;
// Opened from the menu on first use, then shown again rather than rebuilt.
std::unique_ptr<Gtk::Window> changelogWindow_;
std::unique_ptr<Gtk::AboutDialog> aboutDialog_;
Gtk::ProgressBar* loadBar_ = nullptr;
Gtk::Label* loadFileLabel_ = nullptr;
Gtk::Box sourcesSection_; // hidden while the chain needs no sources
Gtk::Label sourcesHeading_;
Gtk::ListBox sourceList_;
Gtk::DropDown formatDrop_;
Gtk::SpinButton widthSpin_;
Gtk::SpinButton heightSpin_;
Gtk::SpinButton dpiSpin_;
Gtk::Label* dpiLabel_ = nullptr; // hidden along with the dpi spin
// 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_;
Gtk::ListBox chainList_;
Gtk::MenuButton addEffectButton_;
Gtk::MenuButton menuButton_;
Gtk::Button recreateButton_{"Recreate"};
Gtk::Button saveButton_{"Save As…"};
Gtk::Picture picture_;
Gtk::Stack previewStack_;
Gtk::Label placeholder_;
Gtk::Spinner spinner_;
Gtk::Label status_;
struct ParamWidget {
ParamSpec spec;
Gtk::Widget* widget = nullptr;
};
std::vector<ParamWidget> paramWidgets_;
std::vector<std::unique_ptr<Gtk::Widget>> ownedWidgets_;
};
} // namespace effector