#ifndef XTC_MAIN_WINDOW_HPP #define XTC_MAIN_WINDOW_HPP #include #include #include #include "xtc/xtc_project.h" #include "xtc/xtc_backend.h" #include "xtc/xtc_mixer.h" #include "xtc/xtc_plugin.h" class GraphWidget; class MixerWidget; class PianoRollWidget; class SequencerWidget; class Fl_Box; class Fl_Button; class Fl_Tabs; class MainWindow : public Fl_Double_Window { public: /* Control-thread plugin commands, transactional model adoption. */ xtc_result add_plugin(const char *path, unsigned int index, size_t *id = nullptr); xtc_result set_plugin_control(size_t id, unsigned int port, float value); /** * @brief Constructs the window, its widgets, and the initial project. * * Creates an empty project and undo history, opens the audio backend, and * schedules the periodic status timer. Throws std::runtime_error when the * initial project or history cannot be created. * * @param prompt_unsaved Whether open, new, and close actions ask before * discarding unsaved changes. */ explicit MainWindow(bool prompt_unsaved = true); /** * @brief Removes the status timer and releases backend and recording state. * * A backend that fails to close is intentionally retained with its recorder * until process exit so active callbacks never observe freed state. */ ~MainWindow() override; /** * @brief Deleted copy constructor; a window uniquely owns its project and backend. */ MainWindow(const MainWindow &) = delete; /** * @brief Deleted copy assignment; a window uniquely owns its project and backend. */ MainWindow &operator=(const MainWindow &) = delete; using Fl_Double_Window::show; /** * @brief Handles FLTK events for the window. * * Space without Ctrl/Alt/Meta toggles the audio engine and is consumed. * * @param event FLTK event code, such as FL_SHORTCUT. * @return 1 when Space was consumed; otherwise the Fl_Double_Window result. */ int handle(int event) override; /** * @brief Applies one undo or redo step from the project history. * * The selected snapshot is installed without resetting history and then * accepted, leaving the window marked dirty. * * @param redo True to redo, false to undo. * @return XTC_OK on success, XTC_ERR_INVALID_STATE while recording, * XTC_ERR_NOT_FOUND when no step is available, or another error from * checkout, install, or accept. */ xtc_result undo(bool redo = false); /** * @brief Returns the mixer tab widget owned by the window. * * @return Borrowed mixer widget, or nullptr before construction completes. */ MixerWidget *mixer() const { return mixer_; } /** * @brief Shows the piano roll for the selected MIDI clip. * * The piano-roll window is created on first use and reused afterwards. * * @return XTC_OK on success, or XTC_ERR_INVALID_STATE when the selected clip * does not exist or is not MIDI. */ xtc_result open_piano_roll(); /** * @brief Returns the piano-roll editor widget. * * @return Borrowed widget, or nullptr until open_piano_roll() first succeeds. */ PianoRollWidget *piano_roll() const { return piano_; } /** * @brief Imports a WAV file as an audio clip in a cloned project. * * Stops the engine first, creates an "Audio" track when none is selected, * and places the clip at the sequencer cursor. The clone replaces the live * project only after every step succeeds. * * @param path Path to the WAV file to import. * @return XTC_OK on success, or an error from the engine stop, clone, import, * or backend preparation. */ xtc_result import_audio(const char *path); /** * @brief Starts recording JACK input to a WAV file. * * Requires a connected backend and no active recording. The engine is * stopped, the current position and selected track are remembered, and a * stereo recorder at the engine sample rate is attached before starting. * Failure leaves the window without a recorder. * * @param path Destination WAV path chosen by the user. * @return XTC_OK when recording started, XTC_ERR_INVALID_STATE when * disconnected or already recording, or another backend or recorder * error. */ xtc_result record_audio(const char *path); /** * @brief Saves the current project to a path and updates the window title. * * On success the path is remembered and the unsaved marker is cleared. * * @param path Destination project path; nullptr is invalid. * @return XTC_OK on success, XTC_ERR_INVALID_ARGUMENT for nullptr, * XTC_ERR_INVALID_STATE while recording, or the save error. */ xtc_result save_project(const char *path); /** * @brief Loads a project from a path and makes it the current project. * * The file is loaded into a candidate project and installed only on success. * * @param path Source project path; nullptr is invalid. * @return XTC_OK on success, XTC_ERR_INVALID_ARGUMENT for nullptr, or an * error from loading or installing the project. */ xtc_result open_project(const char *path); /** Creates a fresh untitled project; the File menu checks unsaved changes first. * On failure the current document remains owned by the window. */ xtc_result new_project(); /** Requests a playback fade/stop; the timer completes deactivation after * callback acknowledgement. Recording must use immediate Stop instead. * Returns request acceptance or the backend failure. */ xtc_result pause_playback(); /** Stops playback and seeks to zero. Refuses during recording. A failed * stop/seek is reported without moving the displayed cursor to zero. */ xtc_result return_to_start(); private: MixerWidget *mixer_ = nullptr; std::unique_ptr history_{nullptr, xtc_history_destroy}; /** * @brief FLTK menu callback for the Undo and Redo menu items. * * The selected menu label starting with "Redo" chooses redo; all other * labels undo. Failures other than XTC_ERR_NOT_FOUND are shown in the window. * * @param widget Fl_Menu_Bar whose selected item triggered the callback. * @param context MainWindow * owning the menu and history. */ static void history_callback(Fl_Widget * widget, void * context); std::unique_ptr project_{nullptr, xtc_project_destroy}; std::unique_ptr piano_window_; PianoRollWidget *piano_ = nullptr; /** * @brief FLTK callback for the Piano roll menu item and button. * * Refuses to open the editor while recording and asks the user to select a * MIDI clip when open_piano_roll() fails. * * @param widget Unused triggering widget. * @param context MainWindow * owning the piano roll. */ static void piano_callback(Fl_Widget * widget, void *context); /** * @brief FLTK callback for the Panic menu item and button. * * When the backend is connected, sends a panic request that silences held * instrument notes and reports failures. * * @param widget Unused triggering widget. * @param context MainWindow * owning the backend. */ static void panic_callback(Fl_Widget * widget, void *context); SequencerWidget *sequencer_ = nullptr; xtc_recorder *recorder_ = nullptr; std::string recording_path_; uint64_t recording_tick_ = 0, recording_track_ = UINT64_MAX; /** * @brief Finalizes the active recording and adds the take to the project. * * Must run only after the engine has stopped, so no audio callback can * reference the recorder ring. The recorder is detached, finished, and * destroyed. A failed finalize or empty take is reported and not added. If * the subsequent import fails, the saved WAV path is reported for recovery. */ void finish_recording(); /** * @brief Hook called by arrangement widgets before mutating the project. * * Refuses edits while recording. When connected, stops the engine so the * mutation cannot race the audio thread, then refreshes the status display. * * @param context MainWindow * passed as the widget owner context. * @return XTC_OK when disconnected or the stop succeeded, * XTC_ERR_INVALID_STATE while recording, or the backend stop error. */ static xtc_result before_arrangement_edit(void *context); /** * @brief FLTK callback for the audio import, record, and relink commands. * * The triggering widget supplies the menu label; the Record button uses the * "Record WAV" default. A native file chooser selects the file, after which * the selected clip is relinked, recording starts, or a WAV is imported. * Failures are reported in the window. * * @param widget Triggering Fl_Menu_Bar or Fl_Button; menu labels select the * action. * @param context MainWindow * owning the project and sequencer. */ static void audio_file_callback(Fl_Widget * widget, void *context); bool dirty_ = false, prompt_unsaved_ = true; std::string path_; /** * @brief Asks whether unsaved changes may be discarded. * * @return True when the project is clean, prompting is disabled, or the user * chose Discard; false when the user cancels. */ bool discard_changes(); /** * @brief Records the current project in history and marks the window dirty. * * Dependent widgets are refreshed with the current project and graph, and * the title gains the unsaved marker. A history failure is reported. */ void mark_dirty(); /** * @brief Installs a candidate project as the active project. * * Rejected while recording. Optionally builds fresh undo history, stops and * prepares the connected backend, or validates offline audio and MIDI plans * when disconnected. On success the window takes ownership of candidate, * rebinds widgets, resets the graph layout, clears the dirty flag, and shows * any missing-asset warning. On failure ownership stays with the caller. * * @param candidate Project to install; must be non-null. * @param reset_history True to discard existing undo/redo history and start * from candidate. * @return XTC_OK on success, XTC_ERR_INVALID_STATE while recording, or an * error from history creation, the backend, or offline planning. */ xtc_result install_project(xtc_project *candidate, bool reset_history = true); /** * @brief Hook used by the sequencer to seek the audio engine. * * Does nothing when the backend is disconnected. Otherwise reads the engine * sample rate, converts the tick position to samples, and seeks the backend. * * @param context MainWindow * passed as the sequencer owner context. * @param tick Target timeline position in project ticks. * @return XTC_OK when disconnected or the seek succeeded, or the status, * conversion, or backend seek error. */ static xtc_result seek(void *context, uint64_t tick); /** * @brief FLTK menu callback for the File/New, Open, Save, and Save As items. * * Reads the action from the selected menu label, confirms discarding unsaved * changes for New and Open, and uses a native file chooser unless Save can * reuse the remembered path. Refuses file operations while recording and * reports failures in the window. * * @param widget Triggering Fl_Menu_Bar whose selected label names the action. * @param context MainWindow * owning the project and file path. */ static void plugin_callback(Fl_Widget *widget, void *context); xtc_result install_plugin_graph(xtc_graph *graph); static void file_callback(Fl_Widget * widget, void *context); GraphWidget *graph_widget_ = nullptr; Fl_Box *graph_state_ = nullptr; xtc_backend *backend_ = nullptr; std::string project_issue_; bool ready_ = false; bool running_ = false; bool closing_ = false; Fl_Button *record_ = nullptr; Fl_Button *play_ = nullptr; Fl_Button *stop_ = nullptr; Fl_Button *pause_ = nullptr; Fl_Button *home_ = nullptr; Fl_Box *indicator_ = nullptr; Fl_Box *status_ = nullptr; Fl_Box *error_ = nullptr; Fl_Tabs *tabs_ = nullptr; /** * @brief Edit handler passed to the modular graph widget. * * Applies the edit to a candidate graph, clones the project around it, and * submits the new graph to the engine when connected. The live project is * replaced only after every step succeeds. * * @param context MainWindow * passed as the graph widget owner context. * @param edit Graph edit to apply. * @param out_added_id Optional output receiving the new node ID when the edit * adds a node; untouched otherwise or when nullptr. * @return XTC_OK on success, XTC_ERR_INVALID_STATE while recording, or an * error from the graph, project, or backend operation. */ static xtc_result edit_graph(void *context, const xtc_graph_edit &edit, size_t *out_added_id); /** * @brief Opens the JACK backend and submits the current graph. * * Uses the default backend configuration and leaves the engine stopped. * Failure is reported and leaves the backend disconnected; a handle that * opened but failed to accept the graph is retained for reconnect. */ void open_backend(); /** * @brief Starts the audio engine with the current project. * * Does nothing when already running. Reports an error when disconnected. * Otherwise prepares the project and starts the engine. Missing or * unreadable audio is reported as a warning after a successful start. */ void start_engine(); /** * @brief Requests a graceful playback stop, or immediately finalizes recording. * * Does nothing when disconnected. Failures are reported in the window. */ void stop_engine(); /** * @brief Closes and reopens the JACK backend, leaving the engine stopped. * * Refuses to run while recording. If closing fails the existing handle is * retained for a later retry and the failure is reported. */ void reconnect_engine(); /** * @brief Polls the backend and updates labels, meters, and button states. * * When disconnected, shows offline text and disables transport controls. * Otherwise updates the sequencer position, mixer peaks, status line, and * error indicators from the latest engine status, then collects backend * work. */ void refresh_status(); /** * @brief Attempts to close the window after discarding unsaved changes. * * Refuses while recording. On success closes the backend, removes the status * timer, hides the piano-roll window, and hides this window. * * @return True when the window closed; false when recording is active, the * user cancels discarding, or the backend cannot close. */ bool close_window(); /** * @brief Shows an operation failure and a context-sensitive hint. * * @param operation Human-readable operation name; names containing * "Project" select a file and permission hint. * @param result Failing result code whose name is included in the message. */ void report_error(const char *operation, xtc_result result); /** * @brief FLTK callback for the Play menu item and button. * * @param widget Unused triggering widget. * @param context MainWindow * whose engine is started. */ static void play_callback(Fl_Widget * widget, void *context); /** * @brief FLTK callback for the Stop menu item and button. * * @param widget Unused triggering widget. * @param context MainWindow * whose engine is stopped. */ static void stop_callback(Fl_Widget * widget, void *context); /** * @brief FLTK callback for the Reconnect menu item and button. * * @param widget Unused triggering widget. * @param context MainWindow * whose backend is reopened. */ static void reconnect_callback(Fl_Widget * widget, void *context); /** * @brief FLTK callback for the window close box, File/Quit, and close events. * * @param widget Unused triggering widget or window. * @param context MainWindow * whose close is requested; a refused close * simply leaves the window open. */ static void close_callback(Fl_Widget * widget, void *context); /** * @brief FLTK menu callback that selects a tab page in the main window. * * @param widget Triggering menu item; window() must return the MainWindow. * @param view Fl_Widget * tab page to make current, not the MainWindow. */ static void view_callback(Fl_Widget *widget, void *view); /** * @brief FLTK help callback that shows the application name and version. * * @param widget Unused triggering widget. * @param context Unused callback context. */ static void about_callback(Fl_Widget * widget, void * context); /** * @brief FLTK timeout callback that refreshes status and supervises recording. * * While recording, drains the recorder and stops the engine if draining * fails or the backend reports a lost server, sample-rate change, or * processing error. Reschedules itself every 0.1 seconds until closing. * * @param context MainWindow * registered with Fl::add_timeout(). */ static void timer_callback(void *context); }; #endif