#pragma once #include #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" #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 buildStyleRow(const ChainStage& stage); // The names of the pool the selected stage draws from, in the order the // dropdown shows them. std::vector 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& 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& 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 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& 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_; // 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 stageLabels_; Image result_; std::uint64_t seed_ = 0; std::string projectPath_; // empty until the project has been saved once Glib::RefPtr 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 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_; // Opened from the menu on first use, then shown again rather than rebuilt. std::unique_ptr changelogWindow_; std::unique_ptr 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 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 paramWidgets_; std::vector> ownedWidgets_; }; } // namespace effector