feat(inserts): copy and paste chains between tracks (phase 92)

This commit is contained in:
Johannes Findeisen 2026-11-06 02:04:38 +01:00
commit 58f0dc51c5
5 changed files with 40 additions and 4 deletions

View file

@ -17,3 +17,12 @@ Select a slot, then **Rack actions → Duplicate effect**. The copy appears dire
after it, retaining controls, automation, wet amount and bypass. It is an after it, retaining controls, automation, wet amount and bypass. It is an
independent instance with fresh processing history. Eight-slot capacity and independent instance with fresh processing history. Eight-slot capacity and
plugin preparation errors preserve the old chain; Undo removes the copy. plugin preparation errors preserve the old chain; Undo removes the copy.
## Copy a chain between tracks
**Rack actions → Copy chain** stores an independent metadata snapshot. Select
another track and use **Paste chain (replace)** to replace its complete chain,
including output gain. Copying neither loads modules nor changes undo history;
pasting prepares the candidate and is undoable. The clipboard is internal to
this session and clears on successful New/Open. Preparation errors keep both
the destination and clipboard intact.

View file

@ -2088,12 +2088,12 @@ Implemented: bounded C99 metadata duplication and Rack actions/Duplicate effect
Copy and paste independent chain metadata between tracks, with explicit replacement, undo and document-lifecycle cleanup. Clipboard operations must not load modules. Copy and paste independent chain metadata between tracks, with explicit replacement, undo and document-lifecycle cleanup. Clipboard operations must not load modules.
### Tasks and acceptance criteria ### Tasks and acceptance criteria
- [ ] Implement the workflow with bounded validation, safe preparation and documented limits. - [x] Implement the workflow with bounded validation, safe preparation and documented limits.
- [ ] Pass meaningful core and/or GUI checks, including error and undo behavior. - [x] Pass meaningful core and/or GUI checks, including error and undo behavior.
- [ ] Document results and commit this phase separately. - [x] Document results and commit this phase separately.
### Implementation status ### Implementation status
Planned. Implemented: session-local independent chain copy/paste with explicit destination replacement, output-gain preservation and prepared undoable adoption. Paste readiness is visible in the menu and successful New/Open clears the clipboard. Focused GUI tests passed for independence, cross-track paste, undo, failed preparation and lifecycle cleanup.
--- ---

View file

@ -1405,6 +1405,7 @@ xtc_result MainWindow::install_project(xtc_project *candidate, bool reset_histor
if (reset_history) { if (reset_history) {
history_.swap(new_history); history_.swap(new_history);
for(auto &snapshot:mix_snapshots_)snapshot.reset(); for(auto &snapshot:mix_snapshots_)snapshot.reset();
if(track_inserts_)track_inserts_->clear_session();
} }
graph_widget_->reset_layout(); graph_widget_->reset_layout();
graph_widget_->set_graph(xtc_project_graph(candidate)); graph_widget_->set_graph(xtc_project_graph(candidate));

View file

@ -26,6 +26,8 @@ class TrackInsertsPanel : public Fl_Group {
xtc_graph *model_=nullptr; xtc_graph *model_=nullptr;
uint64_t track_=UINT64_MAX; uint64_t track_=UINT64_MAX;
xtc_track_chain cached_{}; xtc_track_chain cached_{};
xtc_track_chain clipboard_{};
bool clipboard_ready_=false;
std::vector<uint64_t> ids_; std::vector<uint64_t> ids_;
std::vector<std::string> names_; std::vector<std::string> names_;
void result(xtc_result r) { if(r!=XTC_OK)owner_->report_error("Track inserts",r);refresh(true); } void result(xtc_result r) { if(r!=XTC_OK)owner_->report_error("Track inserts",r);refresh(true); }
@ -51,6 +53,8 @@ class TrackInsertsPanel : public Fl_Group {
gain_=new Fl_Value_Input(x+360,y+52,100,26,"Chain gain");gain_->bounds(0,4);gain_->step(.01);gain_->when(FL_WHEN_RELEASE); gain_=new Fl_Value_Input(x+360,y+52,100,26,"Chain gain");gain_->bounds(0,4);gain_->step(.01);gain_->when(FL_WHEN_RELEASE);
for(auto *input:{wet_,gain_})input->callback([](Fl_Widget *,void *c){auto *p=static_cast<TrackInsertsPanel *>(c);p->result(p->set_mix(p->wet_->value(),p->gain_->value()));},this); for(auto *input:{wet_,gain_})input->callback([](Fl_Widget *,void *c){auto *p=static_cast<TrackInsertsPanel *>(c);p->result(p->set_mix(p->wet_->value(),p->gain_->value()));},this);
actions_=new Fl_Menu_Button(x+486,y+52,150,26,"Rack actions"); actions_=new Fl_Menu_Button(x+486,y+52,150,26,"Rack actions");
actions_->add("Copy chain",0,[](Fl_Widget *,void *c){auto *p=static_cast<TrackInsertsPanel *>(c);p->result(p->copy_chain());},this);
actions_->add("Paste chain (replace)",0,[](Fl_Widget *,void *c){auto *p=static_cast<TrackInsertsPanel *>(c);p->result(p->paste_chain());},this,FL_MENU_INACTIVE);
actions_->add("Duplicate effect",0,[](Fl_Widget *,void *c){auto *p=static_cast<TrackInsertsPanel *>(c);p->result(p->duplicate_slot());},this); actions_->add("Duplicate effect",0,[](Fl_Widget *,void *c){auto *p=static_cast<TrackInsertsPanel *>(c);p->result(p->duplicate_slot());},this);
message_=new Fl_Box(x+16,y+84,w-32,30,"Track input > inserts > output gain > track fader > bus/master");message_->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE); message_=new Fl_Box(x+16,y+84,w-32,30,"Track input > inserts > output gain > track fader > bus/master");message_->align(FL_ALIGN_LEFT|FL_ALIGN_INSIDE);
slots_=new Fl_Hold_Browser(x+16,y+126,230,h-142);slots_->format_char(0);slots_->callback([](Fl_Widget *,void *c){static_cast<TrackInsertsPanel *>(c)->update_mix();},this); slots_=new Fl_Hold_Browser(x+16,y+126,230,h-142);slots_->format_char(0);slots_->callback([](Fl_Widget *,void *c){static_cast<TrackInsertsPanel *>(c)->update_mix();},this);
@ -64,6 +68,20 @@ class TrackInsertsPanel : public Fl_Group {
void select_track(uint64_t id) {track_=id;refresh(true);} void select_track(uint64_t id) {track_=id;refresh(true);}
const xtc_graph *graph() const {return model_;} const xtc_graph *graph() const {return model_;}
void add(const char *file,unsigned int index) { result(owner_->add_track_effect(track_,file,index)); } void add(const char *file,unsigned int index) { result(owner_->add_track_effect(track_,file,index)); }
void clear_session() {clipboard_={};clipboard_ready_=false;update_actions();}
bool clipboard_ready() const {return clipboard_ready_;}
void update_actions() {
int index=actions_->find_index("Paste chain (replace)");
if(index>=0)actions_->mode(index,clipboard_ready_ ? 0 : FL_MENU_INACTIVE);
}
xtc_result copy_chain() {
xtc_track_chain chain{};auto r=xtc_project_get_chain(owner_->project_.get(),track_,&chain);
if(r==XTC_OK){clipboard_=chain;clipboard_ready_=true;update_actions();}return r;
}
xtc_result paste_chain() {
if(!clipboard_ready_)return XTC_ERR_NOT_FOUND;
return owner_->set_track_chain(track_,clipboard_);
}
xtc_result duplicate_slot() { xtc_result duplicate_slot() {
xtc_track_chain chain{};auto r=xtc_project_get_chain(owner_->project_.get(),track_,&chain);size_t index=selected_slot(); xtc_track_chain chain{};auto r=xtc_project_get_chain(owner_->project_.get(),track_,&chain);size_t index=selected_slot();
if(r==XTC_OK)r=xtc_track_chain_duplicate(&chain,index,&chain); if(r==XTC_OK)r=xtc_track_chain_duplicate(&chain,index,&chain);

View file

@ -232,6 +232,14 @@ int creative_workflow_tests()
CHECK(panel->duplicate_slot()==XTC_OK && panel->selected_slot()==1); CHECK(panel->duplicate_slot()==XTC_OK && panel->selected_slot()==1);
xtc_track_chain chain{};CHECK(xtc_project_get_chain(s->project(),first_track,&chain)==XTC_OK && chain.count==2); xtc_track_chain chain{};CHECK(xtc_project_get_chain(s->project(),first_track,&chain)==XTC_OK && chain.count==2);
CHECK(window.undo()==XTC_OK);panel->refresh(true);CHECK(xtc_project_get_chain(s->project(),first_track,&chain)==XTC_OK && chain.count==1); CHECK(window.undo()==XTC_OK);panel->refresh(true);CHECK(xtc_project_get_chain(s->project(),first_track,&chain)==XTC_OK && chain.count==1);
CHECK(!panel->clipboard_ready() && panel->paste_chain()==XTC_ERR_NOT_FOUND);
CHECK(panel->copy_chain()==XTC_OK && panel->clipboard_ready());
CHECK(panel->set_mix(.2,.6)==XTC_OK);
panel->select_track(second_track);CHECK(panel->paste_chain()==XTC_OK);
CHECK(xtc_project_get_chain(s->project(),second_track,&chain)==XTC_OK && chain.count==1 && chain.output_gain==1 && chain.slots[0].wet==1);
CHECK(window.undo()==XTC_OK);panel->refresh(true);CHECK(xtc_project_get_chain(s->project(),second_track,&chain)==XTC_OK && chain.count==0);
submit_result=XTC_ERR_INVALID_STATE;auto *unchanged=s->project();CHECK(panel->paste_chain()==XTC_ERR_INVALID_STATE && s->project()==unchanged);submit_result=XTC_OK;
CHECK(window.new_project()==XTC_OK && !panel->clipboard_ready() && panel->paste_chain()==XTC_ERR_NOT_FOUND);
window.do_callback();puts("Creative workflow GUI checks passed.");return failures ? 1 : 0; window.do_callback();puts("Creative workflow GUI checks passed.");return failures ? 1 : 0;
} }
int track_insert_tests() int track_insert_tests()