2026-11-06 00:56:27 +01:00
/* Optional documentation tool: real FLTK widgets and real backend, isolated data. */
# include "../src/gui/main_window.hpp"
2026-11-06 01:16:39 +01:00
# include "../src/gui/sample_editor.hpp"
2026-11-06 00:56:27 +01:00
# include "../src/gui/sequencer_widget.hpp"
# include "../src/gui/mixer_widget.hpp"
# include "../src/gui/piano_roll.hpp"
# include "../src/gui/plugin_dialog.hpp"
# include "../src/gui/plugin_library.hpp"
2026-11-06 01:50:25 +01:00
# include "../src/gui/track_inserts.hpp"
# include <FL/Fl_Menu_Button.H>
2026-11-06 00:56:27 +01:00
# include <FL/Fl.H>
# include <FL/Fl_Tabs.H>
# include <FL/Fl_Menu_Bar.H>
# include <FL/Fl_Button.H>
# include <FL/Fl_Choice.H>
# include <FL/Fl_Scroll.H>
# include <FL/x.H>
# include <chrono>
# include <cstdio>
# include <cstdlib>
# include <cstring>
# include <filesystem>
# include <stdexcept>
# include <functional>
# include <unistd.h>
namespace {
MainWindow * main_window ;
std : : string destination ;
void check ( xtc_result r ) { if ( r ! = XTC_OK ) throw std : : runtime_error ( xtc_result_string ( r ) ) ; }
void pump ( ) { auto end = std : : chrono : : steady_clock : : now ( ) + std : : chrono : : milliseconds ( 180 ) ; while ( std : : chrono : : steady_clock : : now ( ) < end ) Fl : : wait ( .01 ) ; }
template < class T > T * first ( Fl_Group & g ) { for ( int i = 0 ; i < g . children ( ) ; + + i ) { auto * c = g . child ( i ) ; if ( auto * t = dynamic_cast < T * > ( c ) ) return t ; if ( auto * n = dynamic_cast < Fl_Group * > ( c ) ) if ( auto * t = first < T > ( * n ) ) return t ; } return nullptr ; }
Fl_Widget * named ( Fl_Group & g , const char * label ) { for ( int i = 0 ; i < g . children ( ) ; + + i ) { auto * c = g . child ( i ) ; if ( c - > label ( ) & & ! strcmp ( c - > label ( ) , label ) ) return c ; if ( auto * n = dynamic_cast < Fl_Group * > ( c ) ) if ( auto * w = named ( * n , label ) ) return w ; } return nullptr ; }
void tab ( const char * label ) { auto * page = named ( * main_window , label ) ; if ( ! page ) throw std : : runtime_error ( label ) ; auto * tabs = dynamic_cast < Fl_Tabs * > ( page - > parent ( ) ) ; if ( ! tabs ) throw std : : runtime_error ( " Tab parent " ) ; tabs - > value ( page ) ; tabs - > redraw ( ) ; pump ( ) ; }
2026-11-06 01:16:39 +01:00
void capture ( const char * name ) { if ( std : : getenv ( " XTC_CAPTURE_SAMPLE_ONLY " ) & & std : : strncmp ( name , " sample- " , 7 ) ) return ; Fl : : flush ( ) ; pump ( ) ; std : : string command = " import -window root ' " + destination + " / " + name + " .png' " ; if ( system ( command . c_str ( ) ) ) throw std : : runtime_error ( " ImageMagick capture failed " ) ; std : : printf ( " Captured %s \n " , name ) ; std : : fflush ( stdout ) ; }
2026-11-06 00:56:27 +01:00
struct Modal { const char * name ; const char * page ; } ;
2026-11-06 01:16:39 +01:00
void modal_capture ( void * data ) { auto * job = static_cast < Modal * > ( data ) ; auto * w = Fl : : modal ( ) ; if ( ! w | | w = = main_window ) throw std : : runtime_error ( " Expected a modal dialog " ) ; if ( auto * editor = dynamic_cast < SampleEditor * > ( w ) ) { editor - > select ( 2400 , std : : min ( size_t ( 12000 ) , editor - > frames ( ) ) ) ; if ( ! strcmp ( job - > name , " sample-transients " ) ) { editor - > select ( 0 , editor - > frames ( ) ) ; check ( editor - > detect_attacks ( .05 , 2400 ) ) ; } }
if ( job - > page ) { auto * tabs = first < Fl_Tabs > ( * w ) ; auto * page = named ( * w , job - > page ) ; if ( ! tabs | | ! page ) throw std : : runtime_error ( job - > page ) ; tabs - > value ( page ) ; tabs - > redraw ( ) ; } capture ( job - > name ) ; auto * cancel = named ( * w , " Cancel " ) ; if ( ! cancel ) cancel = named ( * w , " Close " ) ; if ( ! cancel ) cancel = named ( * w , " OK " ) ; if ( cancel ) cancel - > do_callback ( ) ; else w - > do_callback ( ) ; }
2026-11-06 00:56:27 +01:00
void dialog ( const char * name , std : : function < void ( ) > action , const char * page = nullptr ) { Modal job { name , page } ; Fl : : add_timeout ( .3 , modal_capture , & job ) ; action ( ) ; Fl : : remove_timeout ( modal_capture , & job ) ; pump ( ) ; }
void menu ( const char * path ) { auto * bar = first < Fl_Menu_Bar > ( * main_window ) ; const auto * item = bar - > find_item ( path ) ; if ( ! item ) throw std : : runtime_error ( path ) ; bar - > picked ( item ) ; }
void piano_view ( ) {
check ( main_window - > open_piano_roll ( ) ) ;
auto * piano = main_window - > piano_roll ( ) ;
if ( auto * scroll = dynamic_cast < Fl_Scroll * > ( piano - > parent ( ) ) ) scroll - > scroll_to ( 0 , 50 * 16 ) ;
size_t count = 0 ; check ( xtc_project_note_count ( piano - > project ( ) , & count ) ) ;
for ( size_t i = 0 ; i < count ; + + i ) { xtc_note_info note { } ; check ( xtc_project_get_note ( piano - > project ( ) , i , & note ) ) ; if ( note . clip_id = = piano - > clip ( ) ) { piano - > select_note ( note . id ) ; break ; } }
piano - > redraw ( ) ; pump ( ) ;
}
void button ( const char * name ) { auto * b = named ( * main_window , name ) ; if ( ! b ) throw std : : runtime_error ( name ) ; b - > do_callback ( ) ; pump ( ) ; }
}
int main ( int argc , char * * argv ) {
if ( argc ! = 2 ) { std : : fprintf ( stderr , " Usage: capture_screenshots OUTPUT_DIRECTORY (absolute, no apostrophes) \n " ) ; return 1 ; }
destination = argv [ 1 ] ; if ( destination . empty ( ) | | destination [ 0 ] ! = ' / ' | | destination . find ( ' \' ' ) ! = std : : string : : npos ) return 1 ;
try {
std : : filesystem : : create_directories ( destination ) ;
char temp [ ] = " /tmp/xtc-screenshots-XXXXXX " ; if ( ! mkdtemp ( temp ) ) return 1 ; std : : string root = temp ;
struct Cleanup { std : : string path ; ~ Cleanup ( ) { std : : filesystem : : remove_all ( path ) ; } } cleanup { root } ;
xtc_project * p = nullptr ; check ( xtc_project_create ( & p ) ) ; check ( xtc_project_load ( p , " examples/projects/modern-session.xtc " ) ) ;
for ( auto pair : { std : : pair < uint64_t , const char * > { 0 , " Intro " } , { 3840 , " Verse " } , { 7680 , " Chorus " } , { 11520 , " Outro " } } ) check ( xtc_project_set_marker ( p , pair . first , pair . second ) ) ;
uint64_t bus ; check ( xtc_project_add_bus ( p , " Music bus " , & bus ) ) ; check ( xtc_project_route_track ( p , 0 , bus ) ) ;
xtc_automation_point a { 0 , 0 , XTC_AUTOMATION_GAIN , .65f , 0 } ; check ( xtc_project_set_automation ( p , & a ) ) ; a . tick = 3840 ; a . value = 1 ; check ( xtc_project_set_automation ( p , & a ) ) ; a . tick = 7680 ; a . value = .8f ; check ( xtc_project_set_automation ( p , & a ) ) ;
std : : string project = root + " /composition.xtc " ; check ( xtc_project_save ( p , project . c_str ( ) ) ) ; xtc_project_destroy ( p ) ;
MainWindow window ( false , " " ) ; main_window = & window ; window . resize ( 0 , 0 , 1400 , 900 ) ; window . show ( ) ; check ( window . open_project ( project . c_str ( ) ) ) ; pump ( ) ;
auto * s = first < SequencerWidget > ( window ) ; if ( ! s ) throw std : : runtime_error ( " Sequencer missing " ) ;
auto reset = [ & ] ( ) { window . reset_layout ( ) ; check ( window . open_project ( project . c_str ( ) ) ) ; window . resize ( 0 , 20 , 1400 , 900 ) ; tab ( " Sequencer " ) ; s - > select ( 0 , 0 ) ; s - > position ( 0 ) ; check ( window . set_loop ( 3840 , 7680 , true ) ) ; window . fit_song ( ) ; pump ( ) ; } ;
2026-11-06 01:50:25 +01:00
if ( std : : getenv ( " XTC_CAPTURE_HOST_VST3 " ) ) {
reset ( ) ; capture ( " compact-workspace " ) ;
auto * create = dynamic_cast < Fl_Menu_Button * > ( named ( window , " Create " ) ) ; if ( ! create ) throw std : : runtime_error ( " Missing Create dropdown " ) ;
Fl : : add_timeout ( .3 , [ ] ( void * ) { capture ( " create-track-menu " ) ; if ( system ( " xdotool key Escape " ) ) std : : abort ( ) ; } ) ;
create - > menu ( ) - > pulldown ( create - > x ( ) , create - > y ( ) , create - > w ( ) , create - > h ( ) , nullptr , create ) ;
const char * effects [ ] = { " ZamEQ2 " , " ZamAutoSat " , " ZamDelay " } ;
for ( const char * effect : effects ) { std : : string binary = std : : string ( " /usr/lib/vst3/ " ) + effect + " .vst3/Contents/x86_64-linux/ " + effect + " .so " ; check ( window . add_track_effect ( 0 , binary . c_str ( ) , 0 ) ) ; }
auto * inserts = window . track_inserts ( ) ; inserts - > select_track ( 0 ) ; tab ( " Track inserts " ) ; capture ( " track-inserts-vst3 " ) ;
dialog ( " track-insert-vst3-identity " , [ & ] { check ( inserts - > inspect_slot ( ) ) ; } ) ;
check ( inserts - > set_mix ( .65 , 1 ) ) ; capture ( " track-inserts-parallel " ) ;
window . do_callback ( ) ; std : : puts ( " Installed host VST3 captures completed. " ) ; return 0 ;
}
2026-11-06 01:16:39 +01:00
if ( std : : getenv ( " XTC_CAPTURE_SAMPLE_ONLY " ) ) {
reset ( ) ; char source [ 4096 ] ; if ( ! realpath ( " examples/projects/original-pluck.wav " , source ) ) throw std : : runtime_error ( " Missing sample WAV " ) ;
check ( window . create_track ( false ) ) ; check ( window . import_audio ( source ) ) ;
dialog ( " sample-editor " , [ & ] { check ( window . open_sample_editor ( ) ) ; } ) ; dialog ( " sample-transients " , [ & ] { check ( window . open_sample_editor ( ) ) ; } ) ;
window . do_callback ( ) ; std : : puts ( " Sample-editor captures completed. " ) ; return 0 ;
}
2026-11-06 00:56:27 +01:00
reset ( ) ; capture ( " overview " ) ; capture ( " sequencer-loop " ) ; capture ( " arrangement-markers " ) ;
s - > position ( 4800 ) ; check ( window . loop_section ( ) ) ; capture ( " section-navigation " ) ;
dialog ( " marker-edit " , [ & ] { s - > marker_dialog ( ) ; } ) ;
dialog ( " ripple-time " , [ & ] { s - > ripple_dialog ( false ) ; } ) ;
s - > position ( 7680 ) ; check ( s - > ripple_time ( 1920 , false ) ) ; capture ( " ripple-result " ) ; reset ( ) ;
check ( s - > duplicate_track ( " Keys layered " ) ) ; capture ( " track-duplication " ) ; reset ( ) ;
s - > position ( 1920 ) ; check ( s - > split_selection ( ) ) ; capture ( " midi-split " ) ;
s - > select_all_clips ( ) ; // Consolidation requires one track; reset to just the two halves.
s - > select ( 0 , 0 ) ; size_t tracks = 0 , clips = 0 ; xtc_project_get_counts ( s - > project ( ) , & tracks , & clips ) ;
for ( size_t i = 0 ; i < clips ; + + i ) { xtc_clip_info c { } ; xtc_project_get_clip ( s - > project ( ) , i , & c ) ; if ( c . track_id = = 0 & & c . start_tick = = 1920 ) s - > toggle_clip ( c . id ) ; }
check ( s - > merge_midi ( ) ) ; capture ( " midi-consolidation " ) ; reset ( ) ;
dialog ( " midi-humanize " , [ & ] { s - > humanize_dialog ( ) ; } ) ; dialog ( " midi-chord-builder " , [ & ] { s - > chord_dialog ( ) ; } ) ; dialog ( " midi-arpeggio " , [ & ] { s - > arpeggio_dialog ( ) ; } ) ;
check ( s - > make_chord ( { 4 , 7 } ) ) ; piano_view ( ) ; capture ( " midi-chords-result " ) ; check ( s - > arpeggiate ( 240 , 70 , false ) ) ; piano_view ( ) ; capture ( " midi-arpeggio-result " ) ; reset ( ) ;
piano_view ( ) ; capture ( " piano-roll-velocity " ) ; window . detach_piano ( false ) ; reset ( ) ;
dialog ( " commands " , [ & ] { window . show_commands ( ) ; } ) ;
tab ( " Mixer " ) ; capture ( " mixer-automation " ) ; button ( " Play " ) ; pump ( ) ; capture ( " mixer-playing " ) ; button ( " Stop " ) ;
window . resize ( 0 , 0 , 1220 , 720 ) ; pump ( ) ; capture ( " mixer-minimum-size " ) ; window . resize ( 0 , 0 , 1400 , 900 ) ;
for ( int i = 0 ; i < 12 ; + + i ) { check ( window . create_track ( false ) ) ; }
tab ( " Mixer " ) ; auto * scroll = first < Fl_Scroll > ( * window . mixer ( ) ) ; if ( scroll ) scroll - > scroll_to ( std : : max ( 0 , static_cast < int > ( window . mixer ( ) - > strip_count ( ) ) * 156 - scroll - > w ( ) ) , 0 ) ; pump ( ) ; capture ( " mixer-scrolled-session " ) ; reset ( ) ;
tab ( " Modular Graph " ) ; capture ( " modular-graph " ) ; reset ( ) ;
window . detach_transport ( true ) ; pump ( ) ; capture ( " transport-detached " ) ; window . detach_mixer ( true ) ; pump ( ) ; capture ( " workspace-detached " ) ; capture ( " mixer-detached " ) ; reset ( ) ;
window . detach_editor ( 0 , true ) ; pump ( ) ; capture ( " sequencer-detached " ) ; reset ( ) ; tab ( " Modular Graph " ) ; window . detach_editor ( 1 , true ) ; pump ( ) ; capture ( " graph-detached " ) ; reset ( ) ;
char wave [ 4096 ] ; if ( ! realpath ( " examples/projects/original-pluck.wav " , wave ) ) throw std : : runtime_error ( " Missing WAV " ) ; check ( window . create_track ( false ) ) ; check ( window . import_audio ( wave ) ) ; check ( s - > set_zoom ( 192 ) ) ; check ( s - > audio_properties ( .8f , 240 , 480 ) ) ; pump ( ) ; capture ( " sequencer-audio-waveform " ) ;
2026-11-06 01:16:39 +01:00
dialog ( " sample-editor " , [ & ] { check ( window . open_sample_editor ( ) ) ; } ) ; dialog ( " sample-transients " , [ & ] { check ( window . open_sample_editor ( ) ) ; } ) ;
2026-11-06 00:56:27 +01:00
dialog ( " audio-clip-properties " , [ & ] { s - > audio_properties_dialog ( ) ; } ) ; dialog ( " audio-normalization " , [ & ] { s - > normalize_dialog ( ) ; } ) ; check ( s - > normalize_audio ( .25f ) ) ; capture ( " audio-normalization-result " ) ;
dialog ( " project-collection " , [ & ] { menu ( " File/Collect project media... " ) ; } ) ; check ( window . collect_project ( ( root + " /portable " ) . c_str ( ) ) ) ;
dialog ( " loop-range " , [ & ] { button ( " Loop range... " ) ; } ) ; dialog ( " midi-controller-mapping " , [ & ] { menu ( " Audio/MIDI controller mapping " ) ; } ) ; dialog ( " about " , [ & ] { menu ( " Help/About " ) ; } ) ;
for ( const char * section : { " audio " , " plugins " , " interface " , " editing " , " project_defaults " , " transport " } ) { std : : string file = " preferences- " + std : : string ( section ) ; for ( char & c : file ) if ( c = = ' ' ) c = ' _ ' ; dialog ( file . c_str ( ) , [ & ] { window . show_preferences ( ) ; } , section ) ; }
xtc_settings settings = window . settings ( ) ; std : : string fixtures = std : : filesystem : : canonical ( argv [ 0 ] ) . parent_path ( ) . string ( ) ; snprintf ( settings . ladspa_paths , sizeof ( settings . ladspa_paths ) , " %s " , fixtures . c_str ( ) ) ; settings . clap_paths [ 0 ] = settings . vst2_paths [ 0 ] = settings . vst3_paths [ 0 ] = 0 ;
dialog ( " plugin-library " , [ & ] { std : : string path ; xtc_choose_plugin_file ( settings , path ) ; } ) ;
for ( auto entry : { std : : pair < const char * , const char * > { " plugin-descriptors " , " plugin-fixture.so " } , { " clap-instrument-descriptors " , " clap-synth-fixture.clap " } , { " vst2-instrument-descriptors " , " vst2-synth-fixture.so " } , { " vst3-instrument-descriptors " , " vst3-synth-fixture.so " } } ) { unsigned int index = 0 ; dialog ( entry . first , [ & ] { xtc_choose_plugin ( ( fixtures + " / " + entry . second ) . c_str ( ) , 48000 , index , strcmp ( entry . first , " plugin-descriptors " ) ! = 0 ) ; } ) ; }
for ( auto entry : { std : : pair < const char * , unsigned int > { " internal-delay " , 2 } , { " internal-reverb " , 3 } , { " plugin-controls " , 2 } } ) { xtc_plugin_spec spec { } , edited { } ; xtc_plugin_details details { } ; if ( ! strcmp ( entry . first , " plugin-controls " ) ) check ( xtc_plugin_probe ( ( fixtures + " /plugin-fixture.so " ) . c_str ( ) , 0 , 48000 , & spec ) ) ; else check ( xtc_plugin_probe_format ( XTC_PLUGIN_BUILTIN , XTC_BUILTIN_PATH , entry . second , 48000 , & spec ) ) ; check ( xtc_plugin_describe ( & spec , 48000 , & details ) ) ; dialog ( entry . first , [ & ] { xtc_edit_plugin ( spec , details , edited ) ; } ) ; }
reset ( ) ; auto * choice = s - > instrument_choice ( 0 ) ; if ( ! choice ) throw std : : runtime_error ( " Instrument menu missing " ) ; Fl : : add_timeout ( .3 , [ ] ( void * ) { capture ( " internal-instruments " ) ; if ( system ( " xdotool key Escape " ) ) std : : abort ( ) ; } ) ; choice - > menu ( ) - > pulldown ( choice - > x ( ) , choice - > y ( ) , choice - > w ( ) , choice - > h ( ) , choice - > mvalue ( ) , choice ) ;
window . do_callback ( ) ; std : : puts ( " All documentation captures completed. " ) ; return 0 ;
} catch ( const std : : exception & e ) { std : : fprintf ( stderr , " Screenshot capture failed: %s \n " , e . what ( ) ) ; return 1 ; }
}