feat(gui): compact track creation and make the inspector optional (phase 80)

This commit is contained in:
Johannes Findeisen 2026-11-06 01:33:16 +01:00
commit e9d783146f
4 changed files with 198 additions and 41 deletions

View file

@ -1874,6 +1874,156 @@ See [AUDIO_EDITOR.md](AUDIO_EDITOR.md) for precise workflows and limits.
---
## Phase 80 — Compact Creation and Optional Track Inspector
### Goal
Replace the large creation sidebar with a sequencer Create dropdown. Keep all creation actions and shortcuts, and provide an optional track inspector to reclaim horizontal editing space.
### Tasks and acceptance criteria
- [x] Implement the described workflow with validated errors and safe ownership.
- [x] Pass relevant core and/or GUI tests.
- [x] Document behavior and limitations; commit this phase separately.
### Implementation status
Implemented: sequencer Create menu retains empty/audio/MIDI track creation, WAV import, instrument loading and MIDI editing. The inspector is hidden by default and available under View/Track inspector, reclaiming 204 pixels. Core baseline and full Xvfb GUI suite passed, including menu callbacks, creation/undo, layout sizes and floating panels. Inspector visibility is session-local.
---
## Phase 81 — Persistent Track Insert Chains
### Goal
Add a bounded C99 per-track insert model with validation, clone/history/duplicate support and backward-compatible project persistence. Loading metadata must not load plugin binaries.
### Tasks and acceptance criteria
- [ ] Implement the described workflow with validated errors and safe ownership.
- [ ] Pass relevant core and/or GUI tests.
- [ ] Document behavior and limitations; commit this phase separately.
### Implementation status
Planned.
---
## Phase 82 — Prepared Track Insert Processing
### Goal
Compile track inserts into serial modular graphs before track gain/pan. Cover audio and MIDI rendering, export, bypass, cleanup and callback safety; explicitly reject unsupported routing.
### Tasks and acceptance criteria
- [ ] Implement the described workflow with validated errors and safe ownership.
- [ ] Pass relevant core and/or GUI tests.
- [ ] Document behavior and limitations; commit this phase separately.
### Implementation status
Planned.
---
## Phase 83 — Track Inserts in the Modular Workspace
### Goal
Offer a selected-track insert view alongside the global graph, with clear signal order and transactional Add effect actions.
### Tasks and acceptance criteria
- [ ] Implement the described workflow with validated errors and safe ownership.
- [ ] Pass relevant core and/or GUI tests.
- [ ] Document behavior and limitations; commit this phase separately.
### Implementation status
Planned.
---
## Phase 84 — Searchable Effect Discovery
### Goal
Make LADSPA, VST2, VST3 and CLAP effects easy to find and add, including Linux VST3 bundles, format filters and clear compatibility errors.
### Tasks and acceptance criteria
- [ ] Implement the described workflow with validated errors and safe ownership.
- [ ] Pass relevant core and/or GUI tests.
- [ ] Document behavior and limitations; commit this phase separately.
### Implementation status
Planned.
---
## Phase 85 — Insert Chain Editing
### Goal
Expose move up/down, remove and bypass with one undo step per action and safe prepared-plan adoption.
### Tasks and acceptance criteria
- [ ] Implement the described workflow with validated errors and safe ownership.
- [ ] Pass relevant core and/or GUI tests.
- [ ] Document behavior and limitations; commit this phase separately.
### Implementation status
Planned.
---
## Phase 86 — Insert Inspection and Controls
### Goal
Show selected effect identity and supported generic controls. Provide reset where supported and clearly identify unavailable native editors/parameters.
### Tasks and acceptance criteria
- [ ] Implement the described workflow with validated errors and safe ownership.
- [ ] Pass relevant core and/or GUI tests.
- [ ] Document behavior and limitations; commit this phase separately.
### Implementation status
Planned.
---
## Phase 87 — Reusable Effect Chain Presets
### Goal
Save and load portable chain metadata with bounded validation, transactional replacement and explicit missing-plugin errors at preparation.
### Tasks and acceptance criteria
- [ ] Implement the described workflow with validated errors and safe ownership.
- [ ] Pass relevant core and/or GUI tests.
- [ ] Document behavior and limitations; commit this phase separately.
### Implementation status
Planned.
---
## Phase 88 — Parallel Insert Mix and Chain Output
### Goal
Expose per-slot wet/dry and chain output gain with finite bounded values and render tests for endpoints and ordering.
### Tasks and acceptance criteria
- [ ] Implement the described workflow with validated errors and safe ownership.
- [ ] Pass relevant core and/or GUI tests.
- [ ] Document behavior and limitations; commit this phase separately.
### Implementation status
Planned.
---
## Phase 89 — Host VST3 Gallery and Integrated Qualification
### Goal
Load compatible installed host VST3 effects in real track chains, capture screenshots, update usage documentation and qualify integration, persistence and real-time boundaries.
### Tasks and acceptance criteria
- [ ] Implement the described workflow with validated errors and safe ownership.
- [ ] Pass relevant core and/or GUI tests.
- [ ] Document behavior and limitations; commit this phase separately.
### 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.

View file

@ -24,6 +24,7 @@
#include <FL/Fl_Group.H>
#include <FL/Fl_Scroll.H>
#include <FL/Fl_Menu_Bar.H>
#include <FL/Fl_Menu_Button.H>
#include <FL/Fl_Input_.H>
#include <FL/Fl_Tabs.H>
#include <FL/fl_ask.H>
@ -291,7 +292,7 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
editors_[0].owner=this;editors_[0].index=0;editors_[0].page=sequencer;
editors_[0].content=new Fl_Group(216,158,1056,546);
editors_[0].content->begin();
auto *add_track = new Fl_Button(228, 168, 100, 26, "Add track");
auto *add_track = new Fl_Menu_Button(228, 168, 100, 26, "Create");
auto *add_region = new Fl_Button(336, 168, 110, 26, "Add region");
auto *delete_region = new Fl_Button(454, 168, 130, 26, "Delete region/track");
auto *zoom_in = new Fl_Button(592, 168, 80, 26, "Zoom +");
@ -373,11 +374,6 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
static_cast<MainWindow *>(c)->mark_dirty();
},
this);
add_track->callback(
[](Fl_Widget *, void *c) {
static_cast<SequencerWidget *>(c)->add_track();
},
sequencer_);
add_midi->callback(
[](Fl_Widget *, void *c) {
static_cast<SequencerWidget *>(c)->add_midi_clip();
@ -510,36 +506,33 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
auto *sidebar = new Fl_Group(12,126,192,578);
sidebar_=sidebar;
sidebar->box(FL_BORDER_BOX);
auto *create_title = new Fl_Box(24,140,168,24,"CREATE TRACKS");
create_title->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
auto *midi_track = new Fl_Button(24,176,168,34,"New MIDI track");
midi_track->tooltip("Create a track and one-bar MIDI clip at the cursor; undo as one edit.");
midi_track->callback([](Fl_Widget *, void *c) { auto *w = static_cast<MainWindow *>(c);
xtc_result r = w->create_track(true); if (r != XTC_OK) w->report_error("Create MIDI track",r); },this);
auto *audio_track = new Fl_Button(24,218,168,34,"New audio track");
audio_track->tooltip("Create an empty audio track, ready for WAV import or recording.");
audio_track->callback([](Fl_Widget *, void *c) { auto *w = static_cast<MainWindow *>(c);
xtc_result r = w->create_track(false); if (r != XTC_OK) w->report_error("Create audio track",r); },this);
menu->add("Track/New MIDI track",FL_CTRL+FL_SHIFT+'m',midi_track->callback(),this);
menu->add("Track/New audio track",FL_CTRL+FL_SHIFT+'a',audio_track->callback(),this);
auto *import = new Fl_Button(24,260,168,34,"Import WAV");
import->callback(audio_file_callback,this);
auto *load = new Fl_Button(24,302,168,34,"Load instrument plugin");
load->callback(instrument_callback,this);
auto *editor = new Fl_Button(24,344,168,34,"Edit MIDI notes");
editor->callback(piano_callback,this);
auto *selection_title = new Fl_Box(24,404,168,24,"TRACK INSPECTOR");
auto midi_create = [](Fl_Widget *, void *c) { auto *w=static_cast<MainWindow *>(c);
xtc_result r=w->create_track(true); if(r!=XTC_OK)w->report_error("Create MIDI track",r); };
auto audio_create = [](Fl_Widget *, void *c) { auto *w=static_cast<MainWindow *>(c);
xtc_result r=w->create_track(false); if(r!=XTC_OK)w->report_error("Create audio track",r); };
add_track->add("Empty track",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->add_track();},sequencer_);
add_track->add("New MIDI track",0,midi_create,this);
add_track->add("New audio track",0,audio_create,this);
add_track->add("Import WAV",0,audio_file_callback,this);
add_track->add("Load instrument plugin",0,instrument_callback,this);
add_track->add("Edit MIDI notes",0,piano_callback,this);
menu->add("Track/New MIDI track",FL_CTRL+FL_SHIFT+'m',midi_create,this);
menu->add("Track/New audio track",FL_CTRL+FL_SHIFT+'a',audio_create,this);
menu->add("View/Track inspector",0,[](Fl_Widget *,void *c){
auto *w=static_cast<MainWindow *>(c);w->show_track_inspector(!w->sidebar_->visible());
},this,FL_MENU_TOGGLE);
auto *selection_title = new Fl_Box(24,140,168,24,"TRACK INSPECTOR");
selection_title->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
selection_info_ = new Fl_Box(24,438,168,82,"Select a track to inspect it.");
selection_info_ = new Fl_Box(24,174,168,82,"Select a track to inspect it.");
selection_info_->align(FL_ALIGN_TOP_LEFT | FL_ALIGN_INSIDE | FL_ALIGN_WRAP);
inspector_gain_ = new Fl_Value_Input(78, 528, 114, 26, "Gain");
inspector_gain_ = new Fl_Value_Input(78, 264, 114, 26, "Gain");
inspector_gain_->bounds(0, 4);
inspector_gain_->step(.01);
inspector_pan_ = new Fl_Value_Input(78, 562, 114, 26, "Pan");
inspector_pan_ = new Fl_Value_Input(78, 298, 114, 26, "Pan");
inspector_pan_->bounds(-1, 1);
inspector_pan_->step(.01);
inspector_mute_ = new Fl_Check_Button(24, 596, 78, 24, "Mute");
inspector_solo_ = new Fl_Check_Button(108, 596, 84, 24, "Solo");
inspector_mute_ = new Fl_Check_Button(24, 332, 78, 24, "Mute");
inspector_solo_ = new Fl_Check_Button(108, 332, 84, 24, "Solo");
for (auto *control : {static_cast<Fl_Widget *>(inspector_gain_), static_cast<Fl_Widget *>(inspector_pan_),
static_cast<Fl_Widget *>(inspector_mute_), static_cast<Fl_Widget *>(inspector_solo_)}) {
control->when(FL_WHEN_RELEASE);
@ -548,7 +541,7 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
inspector_gain_->tooltip(
"Selected track linear gain: 0 silence, 1 unity, 4 maximum. Automation overrides static gain.");
inspector_pan_->tooltip("Selected track pan: -1 left, 0 center, +1 right.");
auto *tip = new Fl_Box(24, 632, 168, 52, "Ctrl+P Commands\nCtrl+1/2/3 Views\nCtrl+0 Fit song");
auto *tip = new Fl_Box(24, 368, 168, 52, "Ctrl+P Commands\nCtrl+1/2/3 Views\nCtrl+0 Fit song");
tip->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
sidebar->end();
status_ = new Fl_Box(12,714,1256,26,"JACK: not connected");
@ -557,12 +550,11 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
error_->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE | FL_ALIGN_WRAP);
xtc_ui::style(this);
color(xtc_ui::background());
create_title->labelcolor(xtc_ui::muted()); selection_title->labelcolor(xtc_ui::muted());
selection_title->labelcolor(xtc_ui::muted());
tip->labelcolor(xtc_ui::muted()); hint->labelcolor(xtc_ui::muted());
transport_position_->box(xtc_ui::flat); transport_position_->color(xtc_ui::background());
transport_position_->labelfont(FL_COURIER_BOLD); transport_position_->labelsize(18);
tempo_display_->labelcolor(xtc_ui::muted());
midi_track->color(fl_rgb_color(45,91,86));
play_->labelcolor(xtc_ui::accent()); record_->labelcolor(fl_rgb_color(245,132,142));
error_->labelcolor(fl_rgb_color(255,166,139));
end();
@ -570,6 +562,7 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
size_range(1220, 720);
callback(close_callback, this);
apply_ui_preferences();
show_track_inspector(false);
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);
@ -636,6 +629,16 @@ void MainWindow::reset_layout()
settings_.piano_width=900;settings_.piano_height=620;settings_.track_header_width=180;
sequencer_->header_width(180);tabs_->value(editors_[0].page);apply_ui_preferences();
}
void MainWindow::show_track_inspector(bool visible)
{
const int left=visible ? 216 : 12;
if(visible)sidebar_->show();else sidebar_->hide();
tabs_->resize(left,tabs_->y(),w()-left-8,tabs_->h());
auto *menu=dynamic_cast<Fl_Menu_Bar *>(child(0));
if(menu) { auto *item=const_cast<Fl_Menu_Item *>(menu->find_item("View/Track inspector"));
if(item) { if(visible)item->set();else item->clear(); } }
init_sizes();redraw();
}
void MainWindow::resize(int x,int y,int width,int height)
{
Fl_Double_Window::resize(x,y,width,height);

View file

@ -31,6 +31,7 @@ class Fl_Check_Button;
class MainWindow : public Fl_Double_Window {
public:
void show_track_inspector(bool visible);
/* One undoable candidate edit. MIDI also adds a one-bar clip at the cursor.
* Preparation stops/resets transport; recording and failed candidates retain
* the current document. No engine/model changes happen in audio callbacks. */

View file

@ -21,6 +21,7 @@
#include <FL/Fl_Button.H>
#include <FL/Fl_Group.H>
#include <FL/Fl_Menu_Bar.H>
#include <FL/Fl_Menu_Button.H>
#include <FL/Fl_Tabs.H>
#include <chrono>
#include <cstdio>
@ -342,7 +343,7 @@ int main(int argc, char **argv)
CHECK(sequence != nullptr);
char directory[] = "/tmp/xtc-gui-XXXXXX"; CHECK(mkdtemp(directory) != nullptr);
std::string filename = std::string(directory) + "/song.xtc";
press(find(&window, "Add track")); press(find(&window, "Add region"));
auto *creation=dynamic_cast<Fl_Menu_Button *>(find(&window,"Create")); CHECK(creation); if(creation)creation->picked(creation->find_item("Empty track")); press(find(&window, "Add region"));
size_t track_count, clip_count;
CHECK(xtc_project_get_counts(sequence->project(), &track_count, &clip_count) == XTC_OK && track_count == 1 && clip_count == 1);
xtc_clip_info region{};
@ -957,10 +958,12 @@ int main(int argc, char **argv)
MainWindow window(false,""); window.show();
auto *sequence = timeline(&window);
CHECK(sequence != nullptr);
auto *midi = find(&window,"New MIDI track");
auto *audio = find(&window,"New audio track");
CHECK(midi && audio && find(&window,"Import WAV") && find(&window,"Edit MIDI notes"));
press(midi);
auto *create=dynamic_cast<Fl_Menu_Button *>(find(&window,"Create"));
CHECK(create && create->find_item("New MIDI track") && create->find_item("Import WAV"));
CHECK(sequence->x()<100);
window.show_track_inspector(true);CHECK(sequence->x()>200);
window.show_track_inspector(false);CHECK(sequence->x()<100);
create->picked(create->find_item("New MIDI track"));
size_t tracks = 0, clips = 0;
xtc_project_get_counts(sequence->project(),&tracks,&clips);
CHECK(tracks == 1 && clips == 1);
@ -973,7 +976,7 @@ int main(int argc, char **argv)
CHECK(window.undo(true) == XTC_OK);
CHECK(sequence->select(track,clip)); pump();
CHECK(contains(&window,"MIDI track"));
press(audio);
create->picked(create->find_item("New audio track"));
xtc_project_get_counts(sequence->project(),&tracks,&clips); CHECK(tracks == 2 && clips == 1);
const xtc_project *before = sequence->project();
reserve_result = XTC_ERR_OUT_OF_MEMORY;
@ -984,9 +987,9 @@ int main(int argc, char **argv)
// Exercise the supported minimum and a larger workspace with real layout.
for (auto size : {std::pair<int,int>{1220,720},{1600,1000}}) {
window.resize(window.x(),window.y(),size.first,size.second); pump();
CHECK(midi->x() >= 0 && midi->x()+midi->w() <= sequence->parent()->x());
CHECK(create->x() >= 0 && create->x()+create->w() <= window.w());
CHECK(sequence->parent()->x()+sequence->parent()->w() <= window.w());
CHECK(find(&window,"Play")->visible_r() && midi->visible_r());
CHECK(find(&window,"Play")->visible_r() && create->visible_r());
}
CHECK(window.save_project("/tmp/xtc-workspace.xtc") == XTC_OK);
CHECK(window.new_project() == XTC_OK && window.open_project("/tmp/xtc-workspace.xtc") == XTC_OK);