2026-10-26 22:51:25 +01:00
# include "main_window.hpp"
2026-10-26 23:55:35 +01:00
# include "graph_widget.hpp"
2026-10-27 00:10:45 +01:00
# include "sequencer_widget.hpp"
2026-10-27 00:53:41 +01:00
# include "piano_roll.hpp"
2026-10-27 01:06:57 +01:00
# include "mixer_widget.hpp"
2026-10-27 00:10:45 +01:00
# include <FL/Fl_Native_File_Chooser.H>
2026-10-26 23:40:28 +01:00
# include "xtc/xtc_version.h"
2026-10-26 22:51:25 +01:00
2026-10-26 23:40:28 +01:00
# include <FL/Fl.H>
2026-10-26 22:51:25 +01:00
# include <FL/Fl_Box.H>
2026-10-26 23:40:28 +01:00
# include <FL/Fl_Button.H>
# include <FL/Fl_Group.H>
2026-10-26 23:55:35 +01:00
# include <FL/Fl_Scroll.H>
2026-10-26 22:51:25 +01:00
# include <FL/Fl_Menu_Bar.H>
# include <FL/Fl_Tabs.H>
2026-10-26 23:40:28 +01:00
# include <FL/fl_ask.H>
# include <cstdio>
# include <cstring>
2026-10-26 23:55:35 +01:00
# include <stdexcept>
2026-10-26 22:51:25 +01:00
2026-10-26 23:40:28 +01:00
namespace {
void set_label ( Fl_Box * box , const char * text )
{
if ( box - > label ( ) = = nullptr | | std : : strcmp ( box - > label ( ) , text ) ! = 0 )
box - > copy_label ( text ) ;
}
2026-10-26 22:51:25 +01:00
}
2026-10-27 00:10:45 +01:00
MainWindow : : MainWindow ( bool prompt_unsaved )
: Fl_Double_Window ( 1000 , 680 , XTC_PROJECT_NAME " — modular audio workstation " ) ,
prompt_unsaved_ ( prompt_unsaved )
2026-10-26 22:51:25 +01:00
{
2026-10-27 00:10:45 +01:00
xtc_project * initial = nullptr ;
xtc_result result = xtc_project_create ( & initial ) ;
if ( result ! = XTC_OK ) throw std : : runtime_error ( xtc_result_string ( result ) ) ;
project_ . reset ( initial ) ;
2026-10-27 01:06:57 +01:00
xtc_history * history = nullptr ;
result = xtc_history_create ( initial , & history ) ;
if ( result ! = XTC_OK ) throw std : : runtime_error ( xtc_result_string ( result ) ) ;
history_ . reset ( history ) ;
2026-10-26 22:51:25 +01:00
begin ( ) ;
2026-10-26 23:40:28 +01:00
auto * menu = new Fl_Menu_Bar ( 0 , 0 , 1000 , 28 ) ;
2026-10-27 01:06:57 +01:00
menu - > add ( " Edit/Undo " , FL_CTRL + ' z ' , history_callback , this ) ;
menu - > add ( " Edit/Redo " , FL_CTRL + FL_SHIFT + ' z ' , history_callback , this ) ;
menu - > add ( " Edit/Redo alternate " , FL_CTRL + ' y ' , history_callback , this ) ;
2026-10-27 00:10:45 +01:00
menu - > add ( " File/New " , FL_CTRL + ' n ' , file_callback , this ) ;
menu - > add ( " File/Open " , FL_CTRL + ' o ' , file_callback , this ) ;
menu - > add ( " File/Save " , FL_CTRL + ' s ' , file_callback , this ) ;
menu - > add ( " File/Save As " , FL_CTRL + FL_SHIFT + ' s ' , file_callback , this ) ;
2026-10-27 00:31:03 +01:00
menu - > add ( " File/Import WAV " , FL_CTRL + ' i ' , audio_file_callback , this ) ;
menu - > add ( " File/Relink selected audio " , 0 , audio_file_callback , this ) ;
2026-10-27 00:53:41 +01:00
menu - > add ( " Audio/Panic " , FL_F + 9 , panic_callback , this ) ;
menu - > add ( " View/Piano roll " , FL_CTRL + ' 4 ' , piano_callback , this ) ;
2026-10-27 00:31:03 +01:00
menu - > add ( " Audio/Record WAV " , FL_SHIFT + FL_F + 5 , audio_file_callback , this ) ;
2026-10-26 23:40:28 +01:00
menu - > add ( " File/Quit " , FL_CTRL + ' q ' , close_callback , this ) ;
menu - > add ( " Audio/Start engine " , FL_F + 5 , play_callback , this ) ;
menu - > add ( " Audio/Stop engine " , FL_F + 8 , stop_callback , this ) ;
menu - > add ( " Audio/Reconnect " , FL_F + 6 , reconnect_callback , this ) ;
2026-10-26 22:51:25 +01:00
2026-10-26 23:40:28 +01:00
play_ = new Fl_Button ( 12 , 40 , 80 , 30 , " Play " ) ;
play_ - > callback ( play_callback , this ) ;
2026-10-27 00:31:03 +01:00
play_ - > tooltip ( " Start the audio engine (F5 or Space). Play audio clips and the modular graph. " ) ;
2026-10-26 23:40:28 +01:00
stop_ = new Fl_Button ( 100 , 40 , 80 , 30 , " Stop " ) ;
stop_ - > callback ( stop_callback , this ) ;
stop_ - > tooltip ( " Stop the audio engine (F8 or Space). " ) ;
auto * reconnect = new Fl_Button ( 188 , 40 , 110 , 30 , " Reconnect " ) ;
reconnect - > callback ( reconnect_callback , this ) ;
reconnect - > tooltip ( " Close and reopen JACK, leaving the engine stopped (F6). " ) ;
2026-10-27 00:31:03 +01:00
record_ = new Fl_Button ( 306 , 40 , 90 , 30 , " Record " ) ;
record_ - > callback ( audio_file_callback , this ) ;
record_ - > tooltip ( " Record JACK input to WAV; choose path, then Stop to finalize and add the take. " ) ;
2026-10-27 00:53:41 +01:00
auto * panic = new Fl_Button ( 404 , 40 , 70 , 30 , " Panic " ) ;
panic - > callback ( panic_callback , this ) ;
panic - > tooltip ( " Silence held instrument notes (F9). " ) ;
indicator_ = new Fl_Box ( 482 , 40 , 506 , 30 , " Audio: disconnected " ) ;
2026-10-26 23:40:28 +01:00
indicator_ - > align ( FL_ALIGN_LEFT | FL_ALIGN_INSIDE ) ;
indicator_ - > labelfont ( FL_BOLD ) ;
auto * hint = new Fl_Box ( 12 , 78 , 976 , 24 ,
2026-10-27 00:31:03 +01:00
" Edit signal flow in Modular Graph. No hardware ports are connected automatically; Import WAV clips or record JACK input; connect external ports explicitly. " ) ;
2026-10-26 23:40:28 +01:00
hint - > align ( FL_ALIGN_LEFT | FL_ALIGN_INSIDE ) ;
hint - > labelsize ( 12 ) ;
2026-10-26 22:51:25 +01:00
2026-10-26 23:40:28 +01:00
tabs_ = new Fl_Tabs ( 8 , 114 , 984 , 470 ) ;
tabs_ - > begin ( ) ;
auto * sequencer = new Fl_Group ( 8 , 140 , 984 , 444 , " Sequencer " ) ;
2026-10-26 22:51:25 +01:00
sequencer - > begin ( ) ;
2026-10-27 00:10:45 +01:00
auto * add_track = new Fl_Button ( 20 , 150 , 100 , 26 , " Add track " ) ;
auto * add_region = new Fl_Button ( 128 , 150 , 110 , 26 , " Add region " ) ;
auto * delete_region = new Fl_Button ( 246 , 150 , 130 , 26 , " Delete region/track " ) ;
auto * zoom_in = new Fl_Button ( 384 , 150 , 80 , 26 , " Zoom + " ) ;
auto * zoom_out = new Fl_Button ( 472 , 150 , 80 , 26 , " Zoom - " ) ;
2026-10-27 00:53:41 +01:00
auto * add_midi = new Fl_Button ( 560 , 150 , 110 , 26 , " Add MIDI clip " ) ;
auto * piano = new Fl_Button ( 678 , 150 , 100 , 26 , " Piano roll " ) ;
piano - > callback ( piano_callback , this ) ;
2026-10-27 00:10:45 +01:00
auto * timeline_hint = new Fl_Box ( 20 , 548 , 956 , 28 ,
2026-10-27 00:31:03 +01:00
" Import WAV (Ctrl+I) or Record. Drag body to move, edges to trim; ruler seeks while stopped. " ) ;
2026-10-27 00:10:45 +01:00
timeline_hint - > align ( FL_ALIGN_LEFT | FL_ALIGN_INSIDE ) ; timeline_hint - > labelsize ( 12 ) ;
auto * timeline_scroll = new Fl_Scroll ( 20 , 188 , 956 , 350 ) ;
sequencer_ = new SequencerWidget ( 20 , 188 , project_ . get ( ) , seek , this ) ;
timeline_scroll - > end ( ) ;
2026-10-27 00:31:03 +01:00
sequencer_ - > before_edit = before_arrangement_edit ;
2026-10-27 00:10:45 +01:00
sequencer_ - > callback ( [ ] ( Fl_Widget * , void * c ) { static_cast < MainWindow * > ( c ) - > mark_dirty ( ) ; } , this ) ;
add_track - > callback ( [ ] ( Fl_Widget * , void * c ) { static_cast < SequencerWidget * > ( c ) - > add_track ( ) ; } , sequencer_ ) ;
2026-10-27 00:53:41 +01:00
add_midi - > callback ( [ ] ( Fl_Widget * , void * c ) { static_cast < SequencerWidget * > ( c ) - > add_midi_clip ( ) ; } , sequencer_ ) ;
2026-10-27 00:10:45 +01:00
add_region - > callback ( [ ] ( Fl_Widget * , void * c ) { static_cast < SequencerWidget * > ( c ) - > add_region ( ) ; } , sequencer_ ) ;
delete_region - > callback ( [ ] ( Fl_Widget * , void * c ) { static_cast < SequencerWidget * > ( c ) - > delete_selection ( ) ; } , sequencer_ ) ;
zoom_in - > callback ( [ ] ( Fl_Widget * , void * c ) { static_cast < SequencerWidget * > ( c ) - > zoom ( 1 ) ; } , sequencer_ ) ;
zoom_out - > callback ( [ ] ( Fl_Widget * , void * c ) { static_cast < SequencerWidget * > ( c ) - > zoom ( - 1 ) ; } , sequencer_ ) ;
sequencer - > resizable ( timeline_scroll ) ;
2026-10-26 22:51:25 +01:00
sequencer - > end ( ) ;
2026-10-26 23:40:28 +01:00
auto * mixer = new Fl_Group ( 8 , 140 , 984 , 444 , " Mixer " ) ;
2026-10-26 22:51:25 +01:00
mixer - > begin ( ) ;
2026-10-27 01:06:57 +01:00
mixer_ = new MixerWidget ( 20 , 150 , 956 , 420 , project_ . get ( ) , before_arrangement_edit , this ) ;
mixer_ - > callback ( [ ] ( Fl_Widget * , void * c ) { static_cast < MainWindow * > ( c ) - > mark_dirty ( ) ; } , this ) ;
mixer - > resizable ( mixer_ ) ;
2026-10-26 22:51:25 +01:00
mixer - > end ( ) ;
2026-10-26 23:40:28 +01:00
auto * graph = new Fl_Group ( 8 , 140 , 984 , 444 , " Modular Graph " ) ;
2026-10-26 22:51:25 +01:00
graph - > begin ( ) ;
2026-10-26 23:55:35 +01:00
graph_state_ = new Fl_Box ( 20 , 182 , 956 , 24 , " Graph: offline " ) ;
graph_state_ - > align ( FL_ALIGN_LEFT | FL_ALIGN_INSIDE ) ;
graph_state_ - > labelsize ( 12 ) ;
auto * feedback = new Fl_Box ( 20 , 548 , 956 , 28 ,
" Add nodes. Drag output to input to connect; click a node or wire then Delete. Scroll to pan. " ) ;
feedback - > align ( FL_ALIGN_LEFT | FL_ALIGN_INSIDE | FL_ALIGN_WRAP ) ;
feedback - > labelsize ( 12 ) ;
auto * scroll = new Fl_Scroll ( 20 , 212 , 956 , 328 ) ;
2026-10-27 00:10:45 +01:00
graph_widget_ = new GraphWidget ( 20 , 212 , 2000 , 4000 , xtc_project_graph ( project_ . get ( ) ) , edit_graph , this , feedback ) ;
2026-10-26 23:55:35 +01:00
scroll - > end ( ) ;
auto * oscillator = new Fl_Button ( 20 , 150 , 130 , 26 , " Add oscillator " ) ;
oscillator - > callback ( [ ] ( Fl_Widget * , void * context ) {
( void ) static_cast < GraphWidget * > ( context ) - > add_node ( XTC_NODE_OSCILLATOR ) ;
} , graph_widget_ ) ;
auto * gain = new Fl_Button ( 158 , 150 , 110 , 26 , " Add gain " ) ;
gain - > callback ( [ ] ( Fl_Widget * , void * context ) {
( void ) static_cast < GraphWidget * > ( context ) - > add_node ( XTC_NODE_GAIN ) ;
} , graph_widget_ ) ;
auto * meter = new Fl_Button ( 276 , 150 , 110 , 26 , " Add meter " ) ;
meter - > callback ( [ ] ( Fl_Widget * , void * context ) {
( void ) static_cast < GraphWidget * > ( context ) - > add_node ( XTC_NODE_METER ) ;
} , graph_widget_ ) ;
auto * output = new Fl_Button ( 394 , 150 , 110 , 26 , " Add output " ) ;
output - > callback ( [ ] ( Fl_Widget * , void * context ) {
( void ) static_cast < GraphWidget * > ( context ) - > add_node ( XTC_NODE_OUTPUT ) ;
} , graph_widget_ ) ;
2026-10-27 00:31:03 +01:00
auto * audio = new Fl_Button ( 650 , 150 , 130 , 26 , " Add audio source " ) ;
audio - > callback ( [ ] ( Fl_Widget * , void * context ) {
( void ) static_cast < GraphWidget * > ( context ) - > add_node ( XTC_NODE_AUDIO ) ;
} , graph_widget_ ) ;
2026-10-27 00:53:41 +01:00
auto * instrument = new Fl_Button ( 788 , 150 , 130 , 26 , " Add instrument " ) ;
instrument - > callback ( [ ] ( Fl_Widget * , void * c ) { ( void ) static_cast < GraphWidget * > ( c ) - > add_node ( XTC_NODE_INSTRUMENT ) ; } , graph_widget_ ) ;
2026-10-26 23:55:35 +01:00
auto * remove = new Fl_Button ( 512 , 150 , 130 , 26 , " Delete selection " ) ;
remove - > callback ( [ ] ( Fl_Widget * , void * context ) {
( void ) static_cast < GraphWidget * > ( context ) - > delete_selected ( ) ;
} , graph_widget_ ) ;
graph - > resizable ( scroll ) ;
2026-10-26 22:51:25 +01:00
graph - > end ( ) ;
2026-10-26 23:40:28 +01:00
tabs_ - > end ( ) ;
tabs_ - > value ( sequencer ) ;
menu - > add ( " View/Sequencer " , FL_CTRL + ' 1 ' , view_callback , sequencer ) ;
menu - > add ( " View/Mixer " , FL_CTRL + ' 2 ' , view_callback , mixer ) ;
menu - > add ( " View/Modular Graph " , FL_CTRL + ' 3 ' , view_callback , graph ) ;
menu - > add ( " Help/About " , 0 , about_callback ) ;
2026-10-26 22:51:25 +01:00
2026-10-26 23:40:28 +01:00
status_ = new Fl_Box ( 12 , 594 , 976 , 26 , " JACK: not connected " ) ;
status_ - > align ( FL_ALIGN_LEFT | FL_ALIGN_INSIDE ) ;
status_ - > labelsize ( 12 ) ;
error_ = new Fl_Box ( 12 , 624 , 976 , 44 , " " ) ;
error_ - > align ( FL_ALIGN_LEFT | FL_ALIGN_INSIDE | FL_ALIGN_WRAP ) ;
error_ - > labelcolor ( fl_rgb_color ( 255 , 155 , 120 ) ) ;
error_ - > labelsize ( 12 ) ;
2026-10-26 22:51:25 +01:00
end ( ) ;
2026-10-26 23:40:28 +01:00
resizable ( tabs_ ) ;
size_range ( 800 , 540 ) ;
callback ( close_callback , this ) ;
open_backend ( ) ;
Fl : : add_timeout ( 0.1 , timer_callback , this ) ;
2026-10-26 22:51:25 +01:00
}
2026-10-26 23:40:28 +01:00
MainWindow : : ~ MainWindow ( )
2026-10-26 22:51:25 +01:00
{
2026-10-26 23:40:28 +01:00
Fl : : remove_timeout ( timer_callback , this ) ;
// Normal window/menu close already synchronized callbacks. This fallback
// covers exceptional event-loop exit. A failed close must retain its C
// callback context until process exit, rather than free active audio state.
if ( backend_ ! = nullptr & & xtc_backend_close ( backend_ ) ! = XTC_OK )
2026-10-27 00:31:03 +01:00
std : : fputs ( " Failed to close JACK during GUI destruction; backend and recording retained until process exit. \n " , stderr ) ;
else if ( recorder_ ) {
( void ) xtc_recorder_finish ( recorder_ ) ;
xtc_recorder_destroy ( recorder_ ) ;
}
2026-10-26 23:40:28 +01:00
}
void MainWindow : : report_error ( const char * operation , xtc_result result )
{
char text [ 384 ] ;
2026-10-27 00:10:45 +01:00
const char * hint = std : : strstr ( operation , " Project " ) ! = nullptr ? " Check the project path, file format and write permissions. " :
result = = XTC_ERR_UNSUPPORTED ? " Rebuild with JACK development support. " :
2026-10-26 23:40:28 +01:00
" Check the JACK-compatible server and use Reconnect to retry. " ;
std : : snprintf ( text , sizeof ( text ) , " %s: %s. %s " , operation , xtc_result_string ( result ) , hint ) ;
set_label ( error_ , text ) ;
}
2026-10-26 23:55:35 +01:00
xtc_result MainWindow : : edit_graph ( void * context , const xtc_graph_edit & edit , size_t * out_added_id )
{
auto * window = static_cast < MainWindow * > ( context ) ;
2026-10-27 00:31:03 +01:00
if ( window - > recorder_ ) return XTC_ERR_INVALID_STATE ;
2026-10-26 23:55:35 +01:00
xtc_graph * candidate = nullptr ;
size_t added_id = SIZE_MAX ;
2026-10-27 00:10:45 +01:00
xtc_result result = xtc_graph_apply_edit ( xtc_project_graph ( window - > project_ . get ( ) ) , & edit , & candidate , & added_id ) ;
2026-10-26 23:55:35 +01:00
if ( result ! = XTC_OK ) return result ;
2026-10-27 00:10:45 +01:00
xtc_project * replacement = nullptr ;
result = xtc_project_clone ( window - > project_ . get ( ) , & replacement ) ;
if ( result = = XTC_OK ) result = xtc_project_set_graph ( replacement , candidate ) ;
xtc_graph_destroy ( candidate ) ;
if ( result = = XTC_OK & & window - > ready_ )
result = xtc_backend_submit ( window - > backend_ , xtc_project_graph ( replacement ) ) ;
if ( result ! = XTC_OK ) { xtc_project_destroy ( replacement ) ; return result ; }
window - > project_ . reset ( replacement ) ;
window - > graph_widget_ - > set_graph ( xtc_project_graph ( replacement ) ) ;
window - > sequencer_ - > set_project ( replacement , false ) ;
window - > mark_dirty ( ) ;
2026-10-26 23:55:35 +01:00
if ( out_added_id ! = nullptr & & edit . kind = = XTC_EDIT_ADD_NODE ) * out_added_id = added_id ;
window - > refresh_status ( ) ;
return XTC_OK ;
}
2026-10-26 23:40:28 +01:00
void MainWindow : : open_backend ( )
{
2026-10-27 01:06:57 +01:00
xtc_backend_config config = { XTC_BACKEND_JACK , XTC_PROJECT_NAME , 65536U , 2U } ;
2026-10-26 23:40:28 +01:00
xtc_result result = xtc_backend_open ( & config , & backend_ ) ;
ready_ = result = = XTC_OK ;
running_ = false ;
if ( result ! = XTC_OK ) report_error ( " Open JACK failed " , result ) ;
2026-10-26 23:55:35 +01:00
else {
2026-10-27 00:10:45 +01:00
result = xtc_backend_submit ( backend_ , xtc_project_graph ( project_ . get ( ) ) ) ;
2026-10-26 23:55:35 +01:00
if ( result ! = XTC_OK ) {
ready_ = false ; // Handle is retained for close/reconnect; graph was not installed.
report_error ( " Submit graph failed " , result ) ;
} else set_label ( error_ , " " ) ;
}
2026-10-26 23:40:28 +01:00
refresh_status ( ) ;
}
void MainWindow : : start_engine ( )
{
if ( ! ready_ ) {
report_error ( " Audio is disconnected; use Reconnect " , XTC_ERR_INVALID_STATE ) ;
return ;
}
if ( running_ ) return ;
2026-10-27 00:31:03 +01:00
xtc_result result = xtc_backend_prepare_project ( backend_ , project_ . get ( ) ) ;
if ( result = = XTC_OK ) result = xtc_backend_start ( backend_ ) ;
2026-10-26 23:40:28 +01:00
if ( result ! = XTC_OK ) report_error ( " Start engine failed " , result ) ;
2026-10-27 00:31:03 +01:00
else {
set_label ( error_ , " " ) ;
const char * issue = xtc_backend_asset_issue ( backend_ ) ;
if ( issue [ 0 ] ) {
char message [ 640 ] ; std : : snprintf ( message , sizeof ( message ) , " Missing/unreadable audio (silent): %s. Select clip and Relink to repair. " , issue ) ;
set_label ( error_ , message ) ;
}
}
2026-10-26 23:40:28 +01:00
refresh_status ( ) ;
}
void MainWindow : : stop_engine ( )
{
if ( ! ready_ ) return ;
xtc_result result = xtc_backend_stop ( backend_ ) ;
if ( result ! = XTC_OK ) report_error ( " Stop engine failed " , result ) ;
2026-10-27 00:31:03 +01:00
else {
set_label ( error_ , " " ) ;
if ( recorder_ ) finish_recording ( ) ;
}
2026-10-26 23:40:28 +01:00
refresh_status ( ) ;
}
void MainWindow : : reconnect_engine ( )
{
2026-10-27 00:31:03 +01:00
if ( recorder_ ) { report_error ( " Stop recording before reconnecting " , XTC_ERR_INVALID_STATE ) ; return ; }
2026-10-26 23:40:28 +01:00
xtc_result result = xtc_backend_close ( backend_ ) ;
if ( result ! = XTC_OK ) {
report_error ( " Reconnect could not close JACK " , result ) ;
return ; // Preserve the existing handle for a later retry.
}
backend_ = nullptr ;
ready_ = false ;
running_ = false ;
open_backend ( ) ;
}
void MainWindow : : refresh_status ( )
{
2026-10-27 01:06:57 +01:00
if ( mixer_ ) mixer_ - > peaks ( 0 , 0 ) ;
2026-10-27 00:31:03 +01:00
bool can_play = false , can_record = false ;
2026-10-26 23:40:28 +01:00
if ( ! ready_ ) {
set_label ( indicator_ , " Audio: disconnected " ) ;
set_label ( status_ , " JACK: not connected — use Reconnect after starting the server " ) ;
indicator_ - > labelcolor ( fl_rgb_color ( 255 , 155 , 120 ) ) ;
2026-10-26 23:55:35 +01:00
set_label ( graph_state_ , " Graph: offline — edits stored locally; Reconnect to play " ) ;
2026-10-26 23:40:28 +01:00
} else {
xtc_backend_status status = { } ;
xtc_result result = xtc_backend_get_status ( backend_ , & status ) ;
if ( result ! = XTC_OK ) {
report_error ( " Read engine status failed " , result ) ;
set_label ( indicator_ , " Audio: status unavailable " ) ;
2026-10-26 23:55:35 +01:00
set_label ( graph_state_ , " Graph: engine status unavailable " ) ;
2026-10-26 23:40:28 +01:00
set_label ( status_ , " Unable to read JACK status — use Reconnect " ) ;
indicator_ - > labelcolor ( fl_rgb_color ( 255 , 155 , 120 ) ) ;
} else {
2026-10-27 00:10:45 +01:00
uint64_t tick = 0 ;
if ( xtc_project_sample_to_tick ( project_ . get ( ) , status . sample_position , status . sample_rate , & tick ) = = XTC_OK )
sequencer_ - > position ( tick ) ;
2026-10-26 23:40:28 +01:00
running_ = status . running ! = 0 ;
const bool fault = status . server_lost | | status . sample_rate_changed | | status . process_result ! = XTC_OK ;
can_play = ! running_ & & ! fault ;
2026-10-27 00:31:03 +01:00
can_record = ! fault & & recorder_ = = nullptr ;
2026-10-26 23:40:28 +01:00
const char * state = status . server_lost ? " Audio: server lost — Reconnect required " :
status . sample_rate_changed ? " Audio: sample rate changed — Reconnect required " :
status . process_result ! = XTC_OK ? " Audio: processing error " :
2026-10-27 00:31:03 +01:00
recorder_ ? " Audio: recording JACK input — Stop to finish " : running_ ? " Audio: running — modular graph " : " Audio: ready / stopped " ;
2026-10-26 23:40:28 +01:00
set_label ( indicator_ , state ) ;
indicator_ - > labelcolor ( fault ? fl_rgb_color ( 255 , 155 , 120 ) :
running_ ? fl_rgb_color ( 125 , 225 , 160 ) : fl_rgb_color ( 215 , 215 , 215 ) ) ;
2026-10-27 01:06:57 +01:00
mixer_ - > peaks ( status . peak_left , status . peak_right ) ;
2026-10-26 23:40:28 +01:00
char text [ 256 ] ;
2026-10-27 01:06:57 +01:00
std : : snprintf ( text , sizeof ( text ) , " JACK | %u Hz | %u frames | stereo | %u blocks | %u xruns | %s | %u asset issues | %u MIDI drops " ,
2026-10-26 23:40:28 +01:00
status . sample_rate , status . buffer_frames , status . processed_blocks , status . xruns ,
2026-10-27 00:53:41 +01:00
xtc_result_string ( status . process_result ) , status . asset_issues , status . midi_dropped ) ;
2026-10-26 23:40:28 +01:00
set_label ( status_ , text ) ;
2026-10-26 23:55:35 +01:00
if ( fault ) set_label ( graph_state_ , " Graph: engine unavailable — Reconnect required " ) ;
else if ( status . submitted_revision ! = status . active_revision )
set_label ( graph_state_ , running_ ? " Graph: queued — engine still processing previous graph " :
" Graph: prepared — starts on Play " ) ;
else set_label ( graph_state_ , running_ ? " Graph: active in engine " : " Graph: stopped " ) ;
2026-10-26 23:40:28 +01:00
}
xtc_backend_collect ( backend_ ) ;
}
if ( can_play ) play_ - > activate ( ) ; else play_ - > deactivate ( ) ;
2026-10-27 00:31:03 +01:00
if ( can_record ) record_ - > activate ( ) ; else record_ - > deactivate ( ) ;
if ( ready_ & & ( running_ | | recorder_ ) ) stop_ - > activate ( ) ; else stop_ - > deactivate ( ) ;
2026-10-26 23:40:28 +01:00
}
bool MainWindow : : close_window ( )
{
2026-10-27 00:31:03 +01:00
if ( recorder_ ) { report_error ( " Stop recording before closing " , XTC_ERR_INVALID_STATE ) ; return false ; }
2026-10-27 00:10:45 +01:00
if ( ! discard_changes ( ) ) return false ;
2026-10-26 23:40:28 +01:00
xtc_result result = xtc_backend_close ( backend_ ) ;
if ( result ! = XTC_OK ) {
report_error ( " Close JACK failed; window kept open for retry " , result ) ;
return false ;
}
backend_ = nullptr ;
ready_ = false ;
running_ = false ;
closing_ = true ;
Fl : : remove_timeout ( timer_callback , this ) ;
2026-10-27 00:53:41 +01:00
if ( piano_window_ ) piano_window_ - > hide ( ) ;
2026-10-26 23:40:28 +01:00
hide ( ) ;
return true ;
}
int MainWindow : : handle ( int event )
{
if ( event = = FL_SHORTCUT & & Fl : : event_key ( ) = = ' ' & &
( Fl : : event_state ( ) & ( FL_CTRL | FL_ALT | FL_META ) ) = = 0 ) {
if ( running_ ) stop_engine ( ) ; else start_engine ( ) ;
return 1 ;
}
return Fl_Double_Window : : handle ( event ) ;
}
void MainWindow : : play_callback ( Fl_Widget * , void * context )
{ static_cast < MainWindow * > ( context ) - > start_engine ( ) ; }
void MainWindow : : stop_callback ( Fl_Widget * , void * context )
{ static_cast < MainWindow * > ( context ) - > stop_engine ( ) ; }
void MainWindow : : reconnect_callback ( Fl_Widget * , void * context )
{ static_cast < MainWindow * > ( context ) - > reconnect_engine ( ) ; }
void MainWindow : : close_callback ( Fl_Widget * , void * context )
{ ( void ) static_cast < MainWindow * > ( context ) - > close_window ( ) ; }
void MainWindow : : view_callback ( Fl_Widget * widget , void * view )
{
auto * window = static_cast < MainWindow * > ( widget - > window ( ) ) ;
window - > tabs_ - > value ( static_cast < Fl_Widget * > ( view ) ) ;
window - > tabs_ - > redraw ( ) ;
}
void MainWindow : : about_callback ( Fl_Widget * , void * )
{
2026-10-27 01:06:57 +01:00
fl_message ( " %s %s \n Modular audio workstation \n Phase 8: stereo track mixer, automation, meters and undo/redo. " ,
2026-10-26 23:40:28 +01:00
XTC_PROJECT_NAME , XTC_VERSION_STRING ) ;
}
void MainWindow : : timer_callback ( void * context )
{
auto * window = static_cast < MainWindow * > ( context ) ;
2026-10-27 00:31:03 +01:00
if ( window - > recorder_ ) {
xtc_result r = xtc_recorder_drain ( window - > recorder_ ) ;
xtc_backend_status status { } ;
xtc_backend_get_status ( window - > backend_ , & status ) ;
if ( r ! = XTC_OK | | status . server_lost | | status . sample_rate_changed | | status . process_result ! = XTC_OK )
window - > stop_engine ( ) ;
}
2026-10-26 23:40:28 +01:00
window - > refresh_status ( ) ;
if ( ! window - > closing_ ) Fl : : repeat_timeout ( 0.1 , timer_callback , context ) ;
2026-10-26 22:51:25 +01:00
}
2026-10-27 00:10:45 +01:00
void MainWindow : : mark_dirty ( )
2026-10-27 00:53:41 +01:00
{
2026-10-27 01:06:57 +01:00
xtc_result history_result = xtc_history_record ( history_ . get ( ) , project_ . get ( ) ) ;
if ( history_result ! = XTC_OK ) report_error ( " Undo history could not retain edit " , history_result ) ;
mixer_ - > set_project ( project_ . get ( ) ) ;
2026-10-27 00:53:41 +01:00
graph_widget_ - > set_graph ( xtc_project_graph ( project_ . get ( ) ) ) ;
if ( piano_ ) piano_ - > set_project ( project_ . get ( ) , piano_ - > clip ( ) ) ;
dirty_ = true ; copy_label ( " xtc * — unsaved changes " ) ;
}
2026-10-27 00:10:45 +01:00
bool MainWindow : : discard_changes ( )
{
return ! dirty_ | | ! prompt_unsaved_ | |
fl_choice ( " Discard unsaved project changes? " , " Cancel " , " Discard " , nullptr ) = = 1 ;
}
xtc_result MainWindow : : seek ( void * context , uint64_t tick )
{
auto * window = static_cast < MainWindow * > ( context ) ;
if ( ! window - > ready_ ) return XTC_OK ;
xtc_backend_status status { } ;
xtc_result r = xtc_backend_get_status ( window - > backend_ , & status ) ;
uint64_t sample = 0 ;
if ( r = = XTC_OK ) r = xtc_project_tick_to_sample ( window - > project_ . get ( ) , tick , status . sample_rate , & sample ) ;
return r = = XTC_OK ? xtc_backend_seek ( window - > backend_ , sample ) : r ;
}
xtc_result MainWindow : : save_project ( const char * path )
{
2026-10-27 00:31:03 +01:00
if ( recorder_ ) return XTC_ERR_INVALID_STATE ;
2026-10-27 00:10:45 +01:00
if ( path = = nullptr ) return XTC_ERR_INVALID_ARGUMENT ;
std : : string saved_path ( path ) ;
xtc_result r = xtc_project_save ( project_ . get ( ) , path ) ;
if ( r = = XTC_OK ) { path_ = saved_path ; dirty_ = false ; copy_label ( path_ . c_str ( ) ) ; }
return r ;
}
2026-10-27 01:06:57 +01:00
xtc_result MainWindow : : install_project ( xtc_project * candidate , bool reset_history )
2026-10-27 00:10:45 +01:00
{
2026-10-27 00:31:03 +01:00
if ( recorder_ ) return XTC_ERR_INVALID_STATE ;
2026-10-27 01:06:57 +01:00
std : : unique_ptr < xtc_history , decltype ( & xtc_history_destroy ) > new_history ( nullptr , xtc_history_destroy ) ;
if ( reset_history ) {
xtc_history * h = nullptr ;
xtc_result result = xtc_history_create ( candidate , & h ) ;
if ( result ! = XTC_OK ) return result ;
new_history . reset ( h ) ;
}
2026-10-27 00:10:45 +01:00
xtc_result r = XTC_OK ;
if ( ready_ ) {
r = xtc_backend_stop ( backend_ ) ;
if ( r = = XTC_OK ) r = xtc_backend_seek ( backend_ , 0 ) ;
2026-10-27 00:31:03 +01:00
if ( r = = XTC_OK ) r = xtc_backend_prepare_project ( backend_ , candidate ) ;
2026-10-27 00:10:45 +01:00
}
2026-10-27 00:31:03 +01:00
xtc_audio_plan * offline = nullptr ;
if ( r = = XTC_OK & & ! ready_ ) r = xtc_audio_plan_create ( candidate , 48000 , & offline ) ;
2026-10-27 00:53:41 +01:00
xtc_midi_plan * offline_midi = nullptr ;
if ( r = = XTC_OK & & ! ready_ ) r = xtc_midi_plan_create ( candidate , 48000 , & offline_midi ) ;
xtc_midi_plan_destroy ( offline_midi ) ;
if ( r ! = XTC_OK ) { xtc_audio_plan_destroy ( offline ) ; return r ; }
2026-10-27 00:31:03 +01:00
std : : string issue = ready_ ? xtc_backend_asset_issue ( backend_ ) : " " ;
if ( offline ) {
const char * path = " " ;
xtc_audio_plan_issues ( offline , & path ) ; issue = path ;
xtc_audio_plan_destroy ( offline ) ;
}
2026-10-27 00:53:41 +01:00
if ( piano_window_ ) piano_window_ - > hide ( ) ;
if ( piano_ ) piano_ - > set_project ( candidate , UINT64_MAX ) ;
2026-10-27 00:10:45 +01:00
project_ . reset ( candidate ) ;
2026-10-27 01:06:57 +01:00
mixer_ - > set_project ( candidate ) ;
if ( reset_history ) history_ . swap ( new_history ) ;
2026-10-27 00:10:45 +01:00
graph_widget_ - > reset_layout ( ) ;
graph_widget_ - > set_graph ( xtc_project_graph ( candidate ) ) ;
sequencer_ - > set_project ( candidate ) ;
dirty_ = false ;
2026-10-27 00:31:03 +01:00
if ( ! issue . empty ( ) ) {
char text [ 640 ] ; std : : snprintf ( text , sizeof ( text ) , " Missing/unreadable audio (silent): %s. Select clip and Relink to repair. " , issue . c_str ( ) ) ;
set_label ( error_ , text ) ;
} else set_label ( error_ , " " ) ;
2026-10-27 00:10:45 +01:00
refresh_status ( ) ;
return XTC_OK ;
}
xtc_result MainWindow : : open_project ( const char * path )
{
if ( path = = nullptr ) return XTC_ERR_INVALID_ARGUMENT ;
std : : string loaded_path ( path ) ;
xtc_project * candidate = nullptr ;
xtc_result r = xtc_project_create ( & candidate ) ;
if ( r = = XTC_OK ) r = xtc_project_load ( candidate , path ) ;
if ( r = = XTC_OK ) r = install_project ( candidate ) ;
if ( r ! = XTC_OK ) xtc_project_destroy ( candidate ) ;
else { path_ = loaded_path ; copy_label ( path_ . c_str ( ) ) ; }
return r ;
}
void MainWindow : : file_callback ( Fl_Widget * widget , void * context )
{
auto * window = static_cast < MainWindow * > ( context ) ;
2026-10-27 00:31:03 +01:00
if ( window - > recorder_ ) { window - > report_error ( " Stop recording before file operations " , XTC_ERR_INVALID_STATE ) ; return ; }
2026-10-27 00:10:45 +01:00
auto * menu = static_cast < Fl_Menu_Bar * > ( widget ) ;
const char * action = menu - > mvalue ( ) - > label ( ) ;
xtc_result r = XTC_OK ;
if ( std : : strcmp ( action , " New " ) = = 0 ) {
if ( ! window - > discard_changes ( ) ) return ;
xtc_project * candidate = nullptr ;
r = xtc_project_create ( & candidate ) ;
if ( r = = XTC_OK ) r = window - > install_project ( candidate ) ;
if ( r ! = XTC_OK ) xtc_project_destroy ( candidate ) ;
else { window - > path_ . clear ( ) ; window - > copy_label ( " xtc — Untitled " ) ; }
} else {
bool open = std : : strcmp ( action , " Open " ) = = 0 ;
if ( open & & ! window - > discard_changes ( ) ) return ;
if ( ! open & & std : : strcmp ( action , " Save " ) = = 0 & & ! window - > path_ . empty ( ) )
r = window - > save_project ( window - > path_ . c_str ( ) ) ;
else {
Fl_Native_File_Chooser chooser ;
chooser . title ( open ? " Open project " : " Save project " ) ;
chooser . type ( open ? Fl_Native_File_Chooser : : BROWSE_FILE : Fl_Native_File_Chooser : : BROWSE_SAVE_FILE ) ;
chooser . filter ( " xtc projects *.xtc " ) ;
if ( ! open ) chooser . options ( Fl_Native_File_Chooser : : SAVEAS_CONFIRM ) ;
int result = chooser . show ( ) ;
if ( result = = 1 ) return ;
if ( result < 0 ) r = XTC_ERR_IO ;
else r = open ? window - > open_project ( chooser . filename ( ) ) : window - > save_project ( chooser . filename ( ) ) ;
}
}
if ( r ! = XTC_OK ) window - > report_error ( " Project operation failed " , r ) ;
}
2026-10-27 00:31:03 +01:00
xtc_result MainWindow : : before_arrangement_edit ( void * context )
{
auto * window = static_cast < MainWindow * > ( context ) ;
if ( window - > recorder_ ) return XTC_ERR_INVALID_STATE ;
if ( ! window - > ready_ ) return XTC_OK ;
xtc_result r = xtc_backend_stop ( window - > backend_ ) ;
window - > refresh_status ( ) ;
return r ;
}
xtc_result MainWindow : : import_audio ( const char * path )
{
xtc_result r = before_arrangement_edit ( this ) ;
if ( r ! = XTC_OK ) return r ;
xtc_project * candidate = nullptr ;
r = xtc_project_clone ( project_ . get ( ) , & candidate ) ;
uint64_t track = sequencer_ - > selected_track ( ) , clip = 0 ;
if ( r = = XTC_OK & & track = = UINT64_MAX ) r = xtc_project_add_track ( candidate , " Audio " , & track ) ;
if ( r = = XTC_OK ) r = xtc_project_import_audio ( candidate , track , path , sequencer_ - > position ( ) , & clip ) ;
if ( r = = XTC_OK & & ready_ ) r = xtc_backend_prepare_project ( backend_ , candidate ) ;
if ( r ! = XTC_OK ) { xtc_project_destroy ( candidate ) ; return r ; }
project_ . reset ( candidate ) ;
sequencer_ - > set_project ( candidate , false ) ;
graph_widget_ - > set_graph ( xtc_project_graph ( candidate ) ) ;
mark_dirty ( ) ; refresh_status ( ) ; return XTC_OK ;
}
xtc_result MainWindow : : record_audio ( const char * path )
{
if ( ! ready_ | | recorder_ ) return XTC_ERR_INVALID_STATE ;
xtc_result r = xtc_backend_stop ( backend_ ) ;
xtc_backend_status status { } ;
if ( r = = XTC_OK ) r = xtc_backend_get_status ( backend_ , & status ) ;
if ( r = = XTC_OK ) r = xtc_project_sample_to_tick ( project_ . get ( ) , status . sample_position , status . sample_rate , & recording_tick_ ) ;
if ( r = = XTC_OK ) r = xtc_backend_prepare_project ( backend_ , project_ . get ( ) ) ;
if ( r = = XTC_OK ) r = xtc_recorder_create ( path , status . sample_rate , static_cast < size_t > ( status . sample_rate ) * 2 , & recorder_ ) ;
if ( r ! = XTC_OK ) return r ;
recording_path_ = path ;
recording_track_ = sequencer_ - > selected_track ( ) ;
r = xtc_backend_set_recorder ( backend_ , recorder_ ) ;
if ( r = = XTC_OK ) r = xtc_backend_start ( backend_ ) ;
if ( r ! = XTC_OK ) {
( void ) xtc_backend_set_recorder ( backend_ , nullptr ) ;
xtc_recorder_destroy ( recorder_ ) ; recorder_ = nullptr ;
} else mark_dirty ( ) ;
refresh_status ( ) ; return r ;
}
void MainWindow : : finish_recording ( )
{
// Called only after successful backend_stop: no producer can reference ring.
( void ) xtc_backend_set_recorder ( backend_ , nullptr ) ;
uint64_t frames = 0 ;
( void ) xtc_recorder_status ( recorder_ , & frames ) ;
xtc_result r = xtc_recorder_finish ( recorder_ ) ;
xtc_recorder_destroy ( recorder_ ) ; recorder_ = nullptr ;
if ( r ! = XTC_OK ) {
char text [ 768 ] ; std : : snprintf ( text , sizeof ( text ) , " Recording failed (%s). Partial take, if finalized: %s. It was not added to the project. " ,
xtc_result_string ( r ) , recording_path_ . c_str ( ) ) ;
set_label ( error_ , text ) ; return ;
}
if ( ! frames ) { set_label ( error_ , " Empty recording saved; no clip added. " ) ; return ; }
xtc_project * candidate = nullptr ;
r = xtc_project_clone ( project_ . get ( ) , & candidate ) ;
uint64_t clip = 0 , track = recording_track_ ;
if ( r = = XTC_OK & & track = = UINT64_MAX ) r = xtc_project_add_track ( candidate , " Recording " , & track ) ;
if ( r = = XTC_OK ) r = xtc_project_import_audio ( candidate , track , recording_path_ . c_str ( ) , recording_tick_ , & clip ) ;
if ( r = = XTC_OK ) r = xtc_backend_prepare_project ( backend_ , candidate ) ;
if ( r ! = XTC_OK ) {
xtc_project_destroy ( candidate ) ;
char text [ 768 ] ; std : : snprintf ( text , sizeof ( text ) , " Take saved to %s; adding clip failed: %s. Import the WAV to recover. " ,
recording_path_ . c_str ( ) , xtc_result_string ( r ) ) ;
set_label ( error_ , text ) ; return ;
}
project_ . reset ( candidate ) ; sequencer_ - > set_project ( candidate , false ) ;
graph_widget_ - > set_graph ( xtc_project_graph ( candidate ) ) ; mark_dirty ( ) ;
}
void MainWindow : : audio_file_callback ( Fl_Widget * widget , void * context )
{
auto * window = static_cast < MainWindow * > ( context ) ;
auto * menu = dynamic_cast < Fl_Menu_Bar * > ( widget ) ;
const char * action = menu ? menu - > mvalue ( ) - > label ( ) : " Record WAV " ;
bool recording = std : : strcmp ( action , " Record WAV " ) = = 0 ;
bool relink = std : : strcmp ( action , " Relink selected audio " ) = = 0 ;
if ( window - > recorder_ ) { window - > report_error ( " Stop recording first " , XTC_ERR_INVALID_STATE ) ; return ; }
Fl_Native_File_Chooser chooser ;
chooser . title ( recording ? " Record JACK input to WAV " : relink ? " Relink selected clip " : " Import WAV " ) ;
chooser . type ( recording ? Fl_Native_File_Chooser : : BROWSE_SAVE_FILE : Fl_Native_File_Chooser : : BROWSE_FILE ) ;
chooser . filter ( " WAV audio \t *.wav " ) ;
if ( recording ) chooser . options ( Fl_Native_File_Chooser : : SAVEAS_CONFIRM ) ;
int chosen = chooser . show ( ) ;
if ( chosen = = 1 ) return ;
xtc_result r = chosen < 0 ? XTC_ERR_IO : XTC_OK ;
if ( r = = XTC_OK & & relink ) {
r = before_arrangement_edit ( window ) ;
if ( r = = XTC_OK ) r = xtc_project_set_clip_asset ( window - > project_ . get ( ) , window - > sequencer_ - > selected_clip ( ) , chooser . filename ( ) ) ;
if ( r = = XTC_OK ) { window - > mark_dirty ( ) ; window - > sequencer_ - > redraw ( ) ; }
} else if ( r = = XTC_OK ) r = recording ? window - > record_audio ( chooser . filename ( ) ) : window - > import_audio ( chooser . filename ( ) ) ;
if ( r ! = XTC_OK ) window - > report_error ( " Project audio operation failed " , r ) ;
}
2026-10-27 00:53:41 +01:00
xtc_result MainWindow : : open_piano_roll ( )
{
xtc_clip_info clip { } ;
if ( xtc_project_find_clip ( project_ . get ( ) , sequencer_ - > selected_clip ( ) , & clip ) ! = XTC_OK | | ! clip . midi )
return XTC_ERR_INVALID_STATE ;
if ( ! piano_window_ ) {
piano_window_ = std : : make_unique < Fl_Double_Window > ( 900 , 620 , " xtc — Piano roll " ) ;
piano_window_ - > begin ( ) ;
auto * hint = new Fl_Box ( 12 , 8 , 876 , 32 ,
" Source ticks: click draws; drag body moves/pitches, right edge resizes; right-click/Delete removes. Snap 1/16. " ) ;
hint - > align ( FL_ALIGN_LEFT | FL_ALIGN_INSIDE ) ; hint - > labelsize ( 12 ) ;
auto * scroll = new Fl_Scroll ( 12 , 48 , 876 , 560 ) ;
piano_ = new PianoRollWidget ( 12 , 48 , project_ . get ( ) , clip . id , before_arrangement_edit , this ) ;
piano_ - > callback ( [ ] ( Fl_Widget * , void * c ) { static_cast < MainWindow * > ( c ) - > mark_dirty ( ) ; } , this ) ;
scroll - > end ( ) ; piano_window_ - > end ( ) ; piano_window_ - > resizable ( scroll ) ;
scroll - > scroll_to ( 0 , 60 * 16 ) ;
} else piano_ - > set_project ( project_ . get ( ) , clip . id ) ;
piano_window_ - > show ( ) ; return XTC_OK ;
}
void MainWindow : : piano_callback ( Fl_Widget * , void * context )
{
auto * window = static_cast < MainWindow * > ( context ) ;
if ( window - > recorder_ ) { window - > report_error ( " Stop recording before editing MIDI " , XTC_ERR_INVALID_STATE ) ; return ; }
xtc_result r = window - > open_piano_roll ( ) ;
if ( r ! = XTC_OK ) set_label ( window - > error_ , " Select a MIDI clip, then open Piano roll. " ) ;
}
void MainWindow : : panic_callback ( Fl_Widget * , void * context )
{
auto * window = static_cast < MainWindow * > ( context ) ;
if ( ! window - > ready_ ) return ;
xtc_result r = xtc_backend_panic ( window - > backend_ ) ;
if ( r ! = XTC_OK ) window - > report_error ( " Panic failed " , r ) ;
}
2026-10-27 01:06:57 +01:00
xtc_result MainWindow : : undo ( bool redo )
{
if ( recorder_ ) return XTC_ERR_INVALID_STATE ;
xtc_project * candidate = nullptr ;
xtc_result r = xtc_history_checkout ( history_ . get ( ) , redo ? 1 : 0 , & candidate ) ;
if ( r = = XTC_OK ) r = install_project ( candidate , false ) ;
if ( r ! = XTC_OK ) { xtc_project_destroy ( candidate ) ; return r ; }
r = xtc_history_accept ( history_ . get ( ) , redo ? 1 : 0 ) ;
dirty_ = true ; copy_label ( " xtc * — unsaved changes " ) ;
return r ;
}
void MainWindow : : history_callback ( Fl_Widget * widget , void * context )
{
auto * window = static_cast < MainWindow * > ( context ) ;
auto * menu = static_cast < Fl_Menu_Bar * > ( widget ) ;
bool redo = std : : strncmp ( menu - > mvalue ( ) - > label ( ) , " Redo " , 4 ) = = 0 ;
xtc_result r = window - > undo ( redo ) ;
if ( r ! = XTC_OK & & r ! = XTC_ERR_NOT_FOUND ) window - > report_error ( " Undo/redo failed " , r ) ;
}