The canvas was two spin buttons, so "an Instagram story" or "A4 at 300 dpi" was a number the user had to work out. src/core/canvas.cpp now holds one table of standard sizes both front-ends read: a web format is a fixed pixel size, a print format a sheet in millimetres that becomes pixels at a dpi. Adding a format is a line in canvasFormats() — the dropdown, --format and --help all walk the table. The pixels stay the truth everywhere; renderChain never hears about formats. A format is the presentation of a canvas, carried alongside the pixels in Options and Project and empty for a custom size, and where one is named it decides the pixels — so a project saved as A4 reopens as A4 rather than as whatever it happened to measure. In the interface a format fills the width and height in and greys them out rather than hiding them: the size a sheet works out to is worth seeing. A print format also shows a DPI row, and Custom hands the spins back keeping the last size, which makes starting from a format and then tweaking it the ordinary path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
188 lines
7.0 KiB
C++
188 lines
7.0 KiB
C++
#pragma once
|
|
|
|
#include <gtkmm.h>
|
|
|
|
#include <atomic>
|
|
#include <cstdint>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <optional>
|
|
#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"
|
|
|
|
namespace effector {
|
|
|
|
class EffectorWindow : public Gtk::ApplicationWindow {
|
|
public:
|
|
explicit EffectorWindow(const Options& initial);
|
|
~EffectorWindow() override;
|
|
|
|
private:
|
|
void buildUi();
|
|
Gtk::Widget& buildSidebar();
|
|
Gtk::Widget& buildPreview();
|
|
void buildParamWidgets();
|
|
ParamValues paramValues() const;
|
|
void storeParams(); // widgets -> the selected stage's values
|
|
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 collapsed expander shows the count
|
|
|
|
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 index, int delta);
|
|
void rebuildChainRows();
|
|
void selectStage(int index);
|
|
void onStageSelected();
|
|
Gtk::Widget& buildMainMenu(); // the header menu: new, open, save project
|
|
Project currentProject() const;
|
|
void applyProject(const Project& project, const std::string& path);
|
|
void clearSources();
|
|
void onNewProject();
|
|
void onOpenProject();
|
|
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_;
|
|
int selectedStage_ = 0;
|
|
bool suspendStageSelect_ = false; // guards the rebuild of the chain rows
|
|
Image result_;
|
|
std::uint64_t seed_ = 0;
|
|
std::string projectPath_; // empty until the project has been saved once
|
|
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_;
|
|
Gtk::ProgressBar* loadBar_ = nullptr;
|
|
Gtk::Label* loadFileLabel_ = nullptr;
|
|
Gtk::Expander sourcesExpander_;
|
|
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::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
|