xtc/src/gui/main_window.hpp

445 lines
18 KiB
C++

#ifndef XTC_MAIN_WINDOW_HPP
#define XTC_MAIN_WINDOW_HPP
#include <FL/Fl_Double_Window.H>
#include <memory>
#include <string>
#include "xtc/xtc_project.h"
#include "xtc/xtc_backend.h"
#include "xtc/xtc_mixer.h"
#include "xtc/xtc_plugin.h"
#include "xtc/xtc_control.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 set_control_map(unsigned int channel, const unsigned int controllers[3]);
xtc_result add_plugin(const char *path, unsigned int index, size_t *id = nullptr);
xtc_result set_plugin_automation(size_t id, const xtc_plugin_point &point, bool remove = false);
xtc_result replace_plugin(size_t id, const char *path, unsigned int index);
xtc_result reset_plugin(size_t id, unsigned int port = UINT32_MAX);
xtc_result insert_plugin(const xtc_connection_info &wire, 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<xtc_history, decltype(&xtc_history_destroy)> 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<xtc_project, decltype(&xtc_project_destroy)> project_{nullptr, xtc_project_destroy};
std::unique_ptr<Fl_Double_Window> 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 control_map_callback(Fl_Widget *widget, void *context);
static void plugin_callback(Fl_Widget *widget, void *context);
xtc_result apply_plugin_spec(size_t id, const xtc_plugin_spec &spec, bool replace = false);
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