feat(settings): persist application defaults and workspace on exit (phase 100)
This commit is contained in:
parent
bd0a63c06d
commit
2a86d766ca
8 changed files with 150 additions and 4 deletions
4
Makefile
4
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
|
||||
|
|
|
|||
15
docs/MIDI_WORKFLOWS.md
Normal file
15
docs/MIDI_WORKFLOWS.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<double>(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<int64_t>(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<int64_t>(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<int64_t>(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<int>(settings_.transport_x),static_cast<int>(settings_.transport_y),static_cast<int>(settings_.transport_width),static_cast<int>(settings_.transport_height),400,130);
|
||||
if(mixer_window_)restore_panel(mixer_window_.get(),static_cast<int>(settings_.mixer_x),static_cast<int>(settings_.mixer_y),static_cast<int>(settings_.mixer_width),static_cast<int>(settings_.mixer_height),600,420);
|
||||
follow_->value(static_cast<int>(settings_.follow_playhead));
|
||||
resize(x(),y(),static_cast<int>(settings_.width),static_cast<int>(settings_.height));
|
||||
restore_panel(this,static_cast<int>(settings_.main_x),static_cast<int>(settings_.main_y),static_cast<int>(settings_.width),static_cast<int>(settings_.height),1220,720);
|
||||
show_track_inspector(settings_.inspector_visible!=0);
|
||||
sequencer_->grid(static_cast<uint64_t>(settings_.sequencer_snap));sequencer_->set_zoom(static_cast<int>(settings_.sequencer_zoom));
|
||||
sequencer_->new_clip_ticks=static_cast<uint64_t>(settings_.clip_ticks);
|
||||
sequencer_->loop_region(static_cast<uint64_t>(settings_.loop_start),static_cast<uint64_t>(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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;i<COUNT && r==XTC_OK;++i) {
|
||||
char value[4096]; const char *p;
|
||||
if(strcmp(section,schema[i].section)) { section=schema[i].section; if(fprintf(f,"\n[%s]\n",section)<0) {r=XTC_ERR_IO;break;} }
|
||||
|
|
|
|||
|
|
@ -346,8 +346,27 @@ int track_insert_tests()
|
|||
CHECK(xtc_project_get_chain(s->project(),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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue