diff --git a/docs/CREATIVE_WORKFLOWS.md b/docs/CREATIVE_WORKFLOWS.md index 40fd9be..216d79d 100644 --- a/docs/CREATIVE_WORKFLOWS.md +++ b/docs/CREATIVE_WORKFLOWS.md @@ -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 independent instance with fresh processing history. Eight-slot capacity and 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. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index ce9c653..d3acd0c 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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. ### Tasks and acceptance criteria -- [ ] Implement the workflow with bounded validation, safe preparation and documented limits. -- [ ] Pass meaningful core and/or GUI checks, including error and undo behavior. -- [ ] Document results and commit this phase separately. +- [x] Implement the workflow with bounded validation, safe preparation and documented limits. +- [x] Pass meaningful core and/or GUI checks, including error and undo behavior. +- [x] Document results and commit this phase separately. ### 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. --- diff --git a/src/gui/main_window.cpp b/src/gui/main_window.cpp index 459e9b2..4161f5b 100644 --- a/src/gui/main_window.cpp +++ b/src/gui/main_window.cpp @@ -1405,6 +1405,7 @@ xtc_result MainWindow::install_project(xtc_project *candidate, bool reset_histor if (reset_history) { history_.swap(new_history); for(auto &snapshot:mix_snapshots_)snapshot.reset(); + if(track_inserts_)track_inserts_->clear_session(); } graph_widget_->reset_layout(); graph_widget_->set_graph(xtc_project_graph(candidate)); diff --git a/src/gui/track_inserts.hpp b/src/gui/track_inserts.hpp index a1f8a6f..f3d6dcd 100644 --- a/src/gui/track_inserts.hpp +++ b/src/gui/track_inserts.hpp @@ -26,6 +26,8 @@ class TrackInsertsPanel : public Fl_Group { xtc_graph *model_=nullptr; uint64_t track_=UINT64_MAX; xtc_track_chain cached_{}; + xtc_track_chain clipboard_{}; + bool clipboard_ready_=false; std::vector ids_; std::vector names_; 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); for(auto *input:{wet_,gain_})input->callback([](Fl_Widget *,void *c){auto *p=static_cast(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_->add("Copy chain",0,[](Fl_Widget *,void *c){auto *p=static_cast(c);p->result(p->copy_chain());},this); + actions_->add("Paste chain (replace)",0,[](Fl_Widget *,void *c){auto *p=static_cast(c);p->result(p->paste_chain());},this,FL_MENU_INACTIVE); actions_->add("Duplicate effect",0,[](Fl_Widget *,void *c){auto *p=static_cast(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); slots_=new Fl_Hold_Browser(x+16,y+126,230,h-142);slots_->format_char(0);slots_->callback([](Fl_Widget *,void *c){static_cast(c)->update_mix();},this); @@ -64,6 +68,20 @@ class TrackInsertsPanel : public Fl_Group { void select_track(uint64_t id) {track_=id;refresh(true);} const xtc_graph *graph() const {return model_;} 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_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); diff --git a/tests/integration/test_gui.cpp b/tests/integration/test_gui.cpp index 1ced5fc..d0a0c28 100644 --- a/tests/integration/test_gui.cpp +++ b/tests/integration/test_gui.cpp @@ -232,6 +232,14 @@ int creative_workflow_tests() 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); 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; } int track_insert_tests()