#pragma once #include #include #include #include #include #include #include #include #include #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& result, const Glib::RefPtr& dialog); void addPaths(const std::vector& 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& result, const Glib::RefPtr& dialog); void onSaveProject(); // falls back to Save As while the project is untitled void onSaveProjectAs(); void onSaveProjectChosen(const Glib::RefPtr& result, const Glib::RefPtr& 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& result, const Glib::RefPtr& 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; template void mutateSources(Mutate&& mutate); void showError(const std::string& message); void clearError(); // Model std::shared_ptr sources_ = std::make_shared(); std::vector effects_; // The cascade: stage 0 works on the sources, every later stage on the // output of the one before it. std::vector 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 loadCancel_{false}; std::atomic loadDone_{0}; std::size_t loadTotal_ = 0; std::mutex loadMutex_; std::string loadCurrent_; std::vector loadedImages_; std::vector loadFailures_; std::vector 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 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 paramWidgets_; std::vector> ownedWidgets_; }; } // namespace effector