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>
258 lines
10 KiB
C++
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
|