From 2a86d766cadcd5eaf6955f4390271328951ca912 Mon Sep 17 00:00:00 2001 From: hanez Date: Fri, 6 Nov 2026 02:20:33 +0100 Subject: [PATCH] feat(settings): persist application defaults and workspace on exit (phase 100) --- Makefile | 4 ++ docs/MIDI_WORKFLOWS.md | 15 ++++++ docs/ROADMAP.md | 90 ++++++++++++++++++++++++++++++++++ include/xtc/xtc_settings.h | 1 + src/gui/main_window.cpp | 19 +++++-- src/gui/main_window.hpp | 2 + src/settings/settings.c | 4 +- tests/integration/test_gui.cpp | 19 +++++++ 8 files changed, 150 insertions(+), 4 deletions(-) create mode 100644 docs/MIDI_WORKFLOWS.md diff --git a/Makefile b/Makefile index 39c4457..09a37ac 100644 --- a/Makefile +++ b/Makefile @@ -596,3 +596,7 @@ test: test-creative test-gui-creative: $(BUILD_DIR)/test_gui XTC_TEST_CREATIVE_ONLY=1 xvfb-run -a -s "-screen 0 1280x900x24" ./$(BUILD_DIR)/test_gui -include $(BUILD_DIR)/tests/unit/test_creative.d + +.PHONY: test-gui-midi-workflows +test-gui-midi-workflows: $(BUILD_DIR)/test_gui + XTC_TEST_MIDI_WORKFLOWS_ONLY=1 xvfb-run -a -s "-screen 0 1440x960x24" ./$(BUILD_DIR)/test_gui diff --git a/docs/MIDI_WORKFLOWS.md b/docs/MIDI_WORKFLOWS.md new file mode 100644 index 0000000..9fe3841 --- /dev/null +++ b/docs/MIDI_WORKFLOWS.md @@ -0,0 +1,15 @@ +# Application configuration and MIDI workflows + +`~/.xtc.conf` is the application's base configuration. It is read before creating +its first project and backend. Missing keys use compiled defaults. Preferences +Apply writes every tab atomically with owner-only permissions; clean exit also +collects workspace, snap/zoom, loop, piano filters and note defaults. A missing +file is created on Apply or clean exit. Invalid files produce a visible startup +error and are preserved until an explicit successful Apply replaces them. A save +failure keeps the window open for retry. Project contents, plugin states and +session clipboards are project/session data, not application preferences. + +All persistent preference fields are listed by the shared C schema and available +in Preferences. Existing project tempo/controller maps remain project data; +Preferences project defaults apply to New. No configuration I/O runs in an audio +callback. Tests and screenshot captures use isolated configuration paths. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index aa77c31..a3788c2 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -2220,6 +2220,96 @@ A/B snapshots copy supported metadata, not opaque plugin states or DSP histories Crossfades use existing linear envelopes; strumming is a source-note edit. Project format remains 17, readable from versions 1–17; no dependencies were added. +## Phase 100 — Complete Application Configuration Persistence + +### Goal +Load ~/.xtc.conf before creating projects or audio backends; save all application preferences and workspace defaults on Apply and clean exit. Preserve malformed files until explicitly replaced. + +### Tasks and acceptance criteria +- [x] Implement and document the workflow with bounded validation and safe ownership. +- [x] Pass meaningful core and GUI checks, including errors and persistence or undo. +- [x] Commit this phase separately after validation. + +### Implementation status +Implemented: ~/.xtc.conf is loaded before project/backend creation and saved atomically on Preferences Apply and clean exit. Workspace position, inspector visibility, snap/zoom, loop and note defaults are collected. Invalid startup files are preserved until explicit Apply. Configuration roundtrip/permissions tests, focused startup/exit/malformed-file GUI checks and full GUI suite passed. + +--- + +## Phase 101 — MIDI Device Input and Output Backend + +### Goal +Expose JACK MIDI source/destination discovery, routing, input filtering and external MIDI output with bounded callback work and panic safety. + +### Tasks and acceptance criteria +- [ ] Implement and document the workflow with bounded validation and safe ownership. +- [ ] Pass meaningful core and GUI checks, including errors and persistence or undo. +- [ ] Commit this phase separately after validation. + +### Implementation status +Planned. + +--- + +## Phase 102 — MIDI Preferences and Device Defaults + +### Goal +Provide device dropdowns and refresh in Preferences, persistent default input/output ports, live channel filters and MIDI thru; retain editing and controller defaults in ~/.xtc.conf. + +### Tasks and acceptance criteria +- [ ] Implement and document the workflow with bounded validation and safe ownership. +- [ ] Pass meaningful core and GUI checks, including errors and persistence or undo. +- [ ] Commit this phase separately after validation. + +### Implementation status +Planned. + +--- + +## Phase 103 — Export Selected Clips as Standard MIDI Files + +### Goal +Export the audible part of a selected MIDI clip as an atomic Standard MIDI File with note timing, channels, velocities and project tempo. + +### Tasks and acceptance criteria +- [ ] Implement and document the workflow with bounded validation and safe ownership. +- [ ] Pass meaningful core and GUI checks, including errors and persistence or undo. +- [ ] Commit this phase separately after validation. + +### Implementation status +Planned. + +--- + +## Phase 104 — Import Standard MIDI Files + +### Goal +Import type 0/1 PPQN MIDI notes onto the selected track or a new track, with safe parsing, explicit limits, tempo policy and one undoable prepared adoption. + +### Tasks and acceptance criteria +- [ ] Implement and document the workflow with bounded validation and safe ownership. +- [ ] Pass meaningful core and GUI checks, including errors and persistence or undo. +- [ ] Commit this phase separately after validation. + +### Implementation status +Planned. + +--- + +## Phase 105 — MIDI Workflow Screenshots and Qualification + +### Goal +Capture current MIDI preferences, file workflows and imported notes, refresh the gallery, and qualify configuration, MIDI I/O and file workflows together. + +### Tasks and acceptance criteria +- [ ] Implement and document the workflow with bounded validation and safe ownership. +- [ ] Pass meaningful core and GUI checks, including errors and persistence or undo. +- [ ] Commit this phase separately after validation. + +### Implementation status +Planned. + +--- + ## Recommended Execution Order Reorder the phases when the audit reveals a blocking dependency. Do not mark a phase complete merely because code exists: its acceptance criteria must be demonstrated by builds, tests, or documented manual verification. diff --git a/include/xtc/xtc_settings.h b/include/xtc/xtc_settings.h index 692d45f..8284a6c 100644 --- a/include/xtc/xtc_settings.h +++ b/include/xtc/xtc_settings.h @@ -10,6 +10,7 @@ extern "C" { typedef struct { char client_name[64], clap_paths[2048], vst2_paths[2048], vst3_paths[2048], ladspa_paths[2048]; int64_t max_frames, channels, transition_ms, offline_rate, recording_seconds; + int64_t main_x, main_y, inspector_visible; int64_t width, height, piano_width, piano_height, refresh_ms; int64_t transport_detached,mixer_detached,sequencer_detached,graph_detached,piano_detached; int64_t sequencer_x,sequencer_y,sequencer_width,sequencer_height; diff --git a/src/gui/main_window.cpp b/src/gui/main_window.cpp index c787b63..9b426f6 100644 --- a/src/gui/main_window.cpp +++ b/src/gui/main_window.cpp @@ -110,6 +110,7 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration) else if(xtc_settings_path(config_path,sizeof(config_path))==XTC_OK) configuration_=config_path; xtc_result config_result=configuration_.empty() ? XTC_ERR_NOT_FOUND : xtc_settings_load(&settings_,configuration_.c_str()); + configuration_valid_=config_result==XTC_OK || config_result==XTC_ERR_NOT_FOUND; xtc_project *initial = nullptr; xtc_result result = xtc_project_create(&initial); if (result != XTC_OK) @@ -572,7 +573,7 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration) size_range(1220, 720); callback(close_callback, this); apply_ui_preferences(); - show_track_inspector(false); + show_track_inspector(settings_.inspector_visible!=0); open_backend(); if(config_result!=XTC_OK && config_result!=XTC_ERR_NOT_FOUND) report_error("Preferences file invalid; using defaults",config_result); Fl::add_timeout(static_cast(settings_.refresh_ms)/1000,timer_callback,this); @@ -1122,6 +1123,10 @@ bool MainWindow::close_window() backend_ = nullptr; ready_ = false; running_ = false; + if(!configuration_.empty() && configuration_valid_) { + xtc_result saved=save_preferences(); + if(saved!=XTC_OK) {report_error("Could not save application settings; window kept open for retry",saved);return false;} + } closing_ = true; Fl::remove_timeout(timer_callback, this); for(auto &panel:editors_)if(panel.window)panel.window->hide(); @@ -2473,10 +2478,12 @@ void MainWindow::collect_preferences() settings_.transport_detached=transport_detached_;settings_.mixer_detached=mixer_detached_; if(transport_window_) {settings_.transport_x=transport_window_->x();settings_.transport_y=transport_window_->y();settings_.transport_width=transport_window_->w();settings_.transport_height=transport_window_->h();} if(mixer_window_) {settings_.mixer_x=mixer_window_->x();settings_.mixer_y=mixer_window_->y();settings_.mixer_width=mixer_window_->w();settings_.mixer_height=mixer_window_->h();} + settings_.main_x=x();settings_.main_y=y();settings_.inspector_visible=sidebar_->visible(); settings_.width=w();settings_.height=h();settings_.sequencer_snap=static_cast(sequencer_->grid());settings_.sequencer_zoom=sequencer_->zoom_level(); if(piano_) { settings_.piano_width=piano_window_->w();settings_.piano_height=piano_window_->h();settings_.piano_snap=static_cast(piano_->grid()); settings_.filter_channel=piano_->filter_channel();settings_.filter_low=piano_->filter_low();settings_.filter_high=piano_->filter_high(); + settings_.note_ticks=static_cast(piano_->new_note_ticks);settings_.note_velocity=piano_->new_note_velocity;settings_.note_channel=piano_->new_note_channel; } } void MainWindow::apply_ui_preferences() @@ -2490,7 +2497,8 @@ void MainWindow::apply_ui_preferences() if(transport_window_)restore_panel(transport_window_.get(),static_cast(settings_.transport_x),static_cast(settings_.transport_y),static_cast(settings_.transport_width),static_cast(settings_.transport_height),400,130); if(mixer_window_)restore_panel(mixer_window_.get(),static_cast(settings_.mixer_x),static_cast(settings_.mixer_y),static_cast(settings_.mixer_width),static_cast(settings_.mixer_height),600,420); follow_->value(static_cast(settings_.follow_playhead)); - resize(x(),y(),static_cast(settings_.width),static_cast(settings_.height)); + restore_panel(this,static_cast(settings_.main_x),static_cast(settings_.main_y),static_cast(settings_.width),static_cast(settings_.height),1220,720); + show_track_inspector(settings_.inspector_visible!=0); sequencer_->grid(static_cast(settings_.sequencer_snap));sequencer_->set_zoom(static_cast(settings_.sequencer_zoom)); sequencer_->new_clip_ticks=static_cast(settings_.clip_ticks); sequencer_->loop_region(static_cast(settings_.loop_start),static_cast(settings_.loop_end),settings_.loop_enabled!=0); @@ -2509,7 +2517,12 @@ xtc_result MainWindow::apply_preferences(const xtc_settings &candidate) if(r==XTC_OK)r=configure_loop(project_.get(),candidate); if(r==XTC_OK)r=xtc_settings_save(&candidate,configuration_.c_str()); if(r!=XTC_OK) {if(ready_)(void)configure_loop(project_.get(),settings_);refresh_status();return r;} - settings_=candidate;apply_ui_preferences();refresh_status();return XTC_OK; + settings_=candidate;configuration_valid_=true;apply_ui_preferences();refresh_status();return XTC_OK; +} +xtc_result MainWindow::save_preferences() +{ + if(configuration_.empty() || !configuration_valid_)return XTC_ERR_INVALID_STATE; + collect_preferences();return xtc_settings_save(&settings_,configuration_.c_str()); } void MainWindow::show_preferences() { diff --git a/src/gui/main_window.hpp b/src/gui/main_window.hpp index bd6858f..8d1dd56 100644 --- a/src/gui/main_window.hpp +++ b/src/gui/main_window.hpp @@ -66,6 +66,7 @@ class MainWindow : public Fl_Double_Window { const xtc_settings &settings() const { return settings_; } xtc_result apply_preferences(const xtc_settings &settings); void show_preferences(); + xtc_result save_preferences(); xtc_result set_loop(uint64_t start,uint64_t end,bool enabled); Fl_Widget *velocity_lane() const { return velocity_lane_; } /* Control-thread plugin commands, transactional model adoption. */ @@ -247,6 +248,7 @@ class MainWindow : public Fl_Double_Window { bool transport_detached_=false, mixer_detached_=false; xtc_settings settings_{}; std::string configuration_; + bool configuration_valid_=true; Fl_Check_Button *loop_button_=nullptr; xtc_result configure_loop(const xtc_project *project,const xtc_settings &settings); void apply_ui_preferences(); diff --git a/src/settings/settings.c b/src/settings/settings.c index 293c70c..014e7bc 100644 --- a/src/settings/settings.c +++ b/src/settings/settings.c @@ -21,6 +21,8 @@ static const xtc_setting_info schema[] = { STR("plugins",vst2_paths,"VST2 search paths (colon-separated)"), STR("plugins",vst3_paths,"VST3 search paths (colon-separated)"), STR("plugins",ladspa_paths,"LADSPA search paths (colon-separated)"), + NUM("interface",main_x,"Main window X",-32768,32768), NUM("interface",main_y,"Main window Y",-32768,32768), + NUM("interface",inspector_visible,"Track inspector visible: 0 or 1",0,1), NUM("interface",width,"Main window width",1220,3840), NUM("interface",height,"Main window height",720,2160), NUM("interface",piano_width,"Piano window width",800,3840), NUM("interface",piano_height,"Piano window height",520,2160), NUM("interface",transport_detached,"Floating transport: 0 or 1",0,1), @@ -175,7 +177,7 @@ xtc_result xtc_settings_save(const xtc_settings *s,const char *path) n=snprintf(temporary,sizeof(temporary),"%s.tmp-XXXXXX",path); if(n<0 || (size_t)n>=sizeof(temporary)) return XTC_ERR_CAPACITY; fd=mkstemp(temporary); if(fd<0) return XTC_ERR_IO; f=fdopen(fd,"wb"); if(!f) {close(fd);unlink(temporary);return XTC_ERR_IO;} - r=XTC_OK; if(fputs("; xtc application preferences. Saved by Preferences / Apply.\n",f)<0) r=XTC_ERR_IO; + r=XTC_OK; if(fputs("; xtc application preferences. Saved on Preferences / Apply and clean exit.\n",f)<0) r=XTC_ERR_IO; for(i=0;iproject(),track,&chain)==XTC_OK && chain.count==1);unlink("/tmp/xtc-inserts-gui.xtc"); window.do_callback();puts("Track inserts GUI adoption/undo/rollback/reload passed.");return failures ? 1 : 0; } +int midi_workflow_tests() +{ + char dir[]="/tmp/xtc-midi-settings-XXXXXX";CHECK(mkdtemp(dir));std::string config=std::string(dir)+"/.xtc.conf"; + reset();{ + MainWindow window(false,config.c_str());window.show();auto *s=timeline(&window); + s->grid(480);window.show_track_inspector(true);window.resize(20,30,1300,820); + CHECK(window.set_loop(960,3840,true)==XTC_OK);window.do_callback();CHECK(!window.shown()); + } + xtc_settings saved{};CHECK(xtc_settings_load(&saved,config.c_str())==XTC_OK && saved.sequencer_snap==480 && saved.inspector_visible && saved.loop_start==960 && saved.width==1300); + reset();{ + MainWindow window(false,config.c_str());CHECK(window.settings().sequencer_snap==480 && window.settings().inspector_visible && window.settings().loop_enabled); + window.do_callback(); + } + {FILE *f=fopen(config.c_str(),"wb");CHECK(f);if(f){fputs("[invalid]\n",f);fclose(f);}} + reset();{MainWindow window(false,config.c_str());CHECK(window.save_preferences()==XTC_ERR_INVALID_STATE);window.do_callback();} + CHECK(xtc_settings_load(&saved,config.c_str())==XTC_ERR_FORMAT); + unlink(config.c_str());rmdir(dir);puts("MIDI workflow/configuration GUI checks passed.");return failures ? 1 : 0; +} int main(int argc, char **argv) { + if(std::getenv("XTC_TEST_MIDI_WORKFLOWS_ONLY"))return midi_workflow_tests(); if(std::getenv("XTC_TEST_CREATIVE_ONLY"))return creative_workflow_tests(); if(std::getenv("XTC_TEST_TRACK_FX_ONLY"))return track_insert_tests(); if(std::getenv("XTC_TEST_SAMPLE_EDITOR_ONLY"))return sample_editor_tests();