diff --git a/docs/MODERN_EDITING.md b/docs/MODERN_EDITING.md new file mode 100644 index 0000000..3428bbd --- /dev/null +++ b/docs/MODERN_EDITING.md @@ -0,0 +1,9 @@ +# Modern editing workflows + +These control-thread edits preserve project format 15. Session selections, +clipboard and comparison snapshots are not saved in projects. Audio callbacks +continue to consume prepared plans; edits use the existing stop/history workflow. + +## Phase 47 + +Shift-click clips to toggle selection; Ctrl+A in the sequencer or Edit/Select all clips selects every clip. Plain clicking replaces the selection. Selection is session UI state; no marquee selection yet. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 84d753e..78e92ff 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1312,6 +1312,171 @@ BUILTIN_DEVICES.md. Application project format is now 15, reading versions 1–1 --- +## Phase 47 — Arrangement Multi-selection + +### Goal +Select clips across tracks with additive selection and Select all; preserve stable IDs across refresh and docking. + +### Tasks and acceptance criteria +- [x] Provide the control/model behavior and accessible GUI commands. +- [x] Validate bounds and failure rollback; preserve existing project compatibility and callback safety. +- [x] Test the new workflow and document scope and limitations. + +### Implementation status +Implemented: additive Shift-click selection, Ctrl+A/Select all and selected clip outlines across tracks. Stable IDs survive refresh and panel reparenting. Core build/tests and GUI selection/docking tests pass. + +--- + +## Phase 48 — Transactional Group Editing + +### Goal +Move, duplicate and delete selected clips atomically, preserving relative positions and a single undo step. + +### Tasks and acceptance criteria +- [ ] Provide the control/model behavior and accessible GUI commands. +- [ ] Validate bounds and failure rollback; preserve existing project compatibility and callback safety. +- [ ] Test the new workflow and document scope and limitations. + +### Implementation status +Planned. + +--- + +## Phase 49 — Arrangement Clipboard + +### Goal +Copy selected clips into an independent session clipboard and paste at the cursor with fresh IDs, notes and media references. + +### Tasks and acceptance criteria +- [ ] Provide the control/model behavior and accessible GUI commands. +- [ ] Validate bounds and failure rollback; preserve existing project compatibility and callback safety. +- [ ] Test the new workflow and document scope and limitations. + +### Implementation status +Planned. + +--- + +## Phase 50 — Repeat Arrangement Patterns + +### Goal +Repeat a selected phrase by its complete bounding span with bounded copy counts and all-or-nothing capacity validation. + +### Tasks and acceptance criteria +- [ ] Provide the control/model behavior and accessible GUI commands. +- [ ] Validate bounds and failure rollback; preserve existing project compatibility and callback safety. +- [ ] Test the new workflow and document scope and limitations. + +### Implementation status +Planned. + +--- + +## Phase 51 — Align and Slip Editing + +### Goal +Align selected clip starts to the cursor and slip source offsets without moving timeline boundaries. + +### Tasks and acceptance criteria +- [ ] Provide the control/model behavior and accessible GUI commands. +- [ ] Validate bounds and failure rollback; preserve existing project compatibility and callback safety. +- [ ] Test the new workflow and document scope and limitations. + +### Implementation status +Planned. + +--- + +## Phase 52 — MIDI Swing Quantization + +### Goal +Apply bounded swing and quantize strength to source note starts with deterministic rounding and source-bound checks. + +### Tasks and acceptance criteria +- [ ] Provide the control/model behavior and accessible GUI commands. +- [ ] Validate bounds and failure rollback; preserve existing project compatibility and callback safety. +- [ ] Test the new workflow and document scope and limitations. + +### Implementation status +Planned. + +--- + +## Phase 53 — MIDI Legato and Gate Tools + +### Goal +Set note gates or extend notes to the next onset on the same channel, preserving IDs and validating all notes before publication. + +### Tasks and acceptance criteria +- [ ] Provide the control/model behavior and accessible GUI commands. +- [ ] Validate bounds and failure rollback; preserve existing project compatibility and callback safety. +- [ ] Test the new workflow and document scope and limitations. + +### Implementation status +Planned. + +--- + +## Phase 54 — MIDI Scale Correction + +### Goal +Snap notes to a chosen root and chromatic, major or minor scale with deterministic nearest-pitch ties. + +### Tasks and acceptance criteria +- [ ] Provide the control/model behavior and accessible GUI commands. +- [ ] Validate bounds and failure rollback; preserve existing project compatibility and callback safety. +- [ ] Test the new workflow and document scope and limitations. + +### Implementation status +Planned. + +--- + +## Phase 55 — MIDI Velocity Shaping + +### Goal +Apply bounded velocity scaling and offsets to selected MIDI clips, with explicit clipping to valid MIDI velocities. + +### Tasks and acceptance criteria +- [ ] Provide the control/model behavior and accessible GUI commands. +- [ ] Validate bounds and failure rollback; preserve existing project compatibility and callback safety. +- [ ] Test the new workflow and document scope and limitations. + +### Implementation status +Planned. + +--- + +## Phase 56 — Batch Audio Fade Tools + +### Goal +Apply symmetric boundary fades or remove fades on selected audio clips, preserving clip gain, media and timing. + +### Tasks and acceptance criteria +- [ ] Provide the control/model behavior and accessible GUI commands. +- [ ] Validate bounds and failure rollback; preserve existing project compatibility and callback safety. +- [ ] Test the new workflow and document scope and limitations. + +### Implementation status +Planned. + +--- + +## Phase 57 — Mixer A/B Comparison + +### Goal +Capture and recall two session mixer snapshots by stable track ID, with transactional validation, undo and explicit automation limitations. + +### Tasks and acceptance criteria +- [ ] Provide the control/model behavior and accessible GUI commands. +- [ ] Validate bounds and failure rollback; preserve existing project compatibility and callback safety. +- [ ] Test the new workflow and document scope and limitations. + +### 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/src/gui/main_window.cpp b/src/gui/main_window.cpp index 00b5235..6fabaf8 100644 --- a/src/gui/main_window.cpp +++ b/src/gui/main_window.cpp @@ -320,6 +320,7 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration) (void)static_cast(c)->sequencer_->grid(xtc_ui::snap_ticks[value]); },this); Fl_Group::current(parent); + menu->add("Edit/Select all clips",0,[](Fl_Widget *,void *c){static_cast(c)->select_all_clips();},sequencer_); menu->add("Edit/Duplicate clip",FL_CTRL+'d',[](Fl_Widget *, void *c) { (void)static_cast(c)->duplicate_selection(); },sequencer_); menu->add("Edit/Split audio at cursor", 0, [](Fl_Widget *, void *c) { (void)static_cast(c)->split_selection(); }, sequencer_); menu->add("Edit/Audio clip gain and fades", 0, [](Fl_Widget *, void *c) { static_cast(c)->audio_properties_dialog(); }, sequencer_); diff --git a/src/gui/sequencer_widget.cpp b/src/gui/sequencer_widget.cpp index 63f435c..ef2a268 100644 --- a/src/gui/sequencer_widget.cpp +++ b/src/gui/sequencer_widget.cpp @@ -50,6 +50,7 @@ bool SequencerWidget::select(uint64_t track, uint64_t clip) xtc_clip_info info{}; if (xtc_project_find_clip(project_,clip,&info) != XTC_OK || info.track_id != track) return false; } + selection_.clear(); track_ = track; clip_ = clip; mode_ = 0; redraw(); return true; } @@ -59,6 +60,7 @@ void SequencerWidget::set_project(xtc_project *project, bool reset) project_ = project; if (reset) { waves_.clear(); + selection_.clear(); track_ = clip_ = UINT64_MAX; cursor_ = 0; mode_ = 0; @@ -281,9 +283,9 @@ void SequencerWidget::draw() int right = left+std::max(4,static_cast(c.length_tick*static_cast(scale_)/960U)); int a = std::max(left,cx), b = std::min(right,cx+cw); if (b <= a) continue; - fl_color(fl_color_average(color,xtc_ui::background(),c.id == clip_ ? .75f : .45f)); + fl_color(fl_color_average(color,xtc_ui::background(),clip_selected(c.id) ? .75f : .45f)); fl_rectf(a,py+5,b-a,46); - fl_color(c.id == clip_ ? xtc_ui::text() : color); fl_rect(a,py+5,b-a,46); + fl_color(clip_selected(c.id) ? xtc_ui::text() : color); fl_rect(a,py+5,b-a,46); fl_push_clip(a+1,py+6,std::max(0,b-a-2),44); if (c.midi) { /* Real note positions, pitch and duration; no placeholder waveform. */ @@ -405,6 +407,7 @@ int SequencerWidget::handle(int event) return 1; xtc_track_info t{}; xtc_project_get_track(project_, static_cast(row), &t); + uint64_t previous_clip=clip_; track_ = t.id; clip_ = UINT64_MAX; for (size_t j = clips; j > 0 && Fl::event_x()>=header_left()+header_width_; --j) { @@ -420,6 +423,10 @@ int SequencerWidget::handle(int event) break; } } + if (Fl::event_state(FL_SHIFT) && clip_ != UINT64_MAX) { + uint64_t hit=clip_;clip_=previous_clip;toggle_clip(hit);mode_=0;return 1; + } + selection_.clear(); if (Fl::event_button() == FL_LEFT_MOUSE && Fl::event_clicks() && clip_ != UINT64_MAX && original_.midi) { mode_ = 0; if (open_clip) open_clip(context_); @@ -490,6 +497,7 @@ int SequencerWidget::handle(int event) if (Fl::event_key() == 's') { split_selection(); return 1; } if (Fl::event_key() == 'a') { audio_properties_dialog(); return 1; } } + if (event == FL_KEYDOWN && Fl::event_state(FL_CTRL) && Fl::event_key() == 'a') {select_all_clips();return 1;} if (event == FL_KEYDOWN && Fl::event_state(FL_CTRL) && Fl::event_key() == 'd') { duplicate_selection(); return 1; } if (event == FL_KEYDOWN && Fl::event_key() == FL_F+2) { const char *name = fl_input("Name (1..63 UTF-8 bytes)",""); if (name) rename_selection(name); return 1; @@ -509,6 +517,32 @@ int SequencerWidget::handle(int event) return Fl_Group::handle(event); } +std::vector SequencerWidget::selected_clips() const +{ + std::vector ids;xtc_clip_info c{}; + for(auto id:selection_)if(xtc_project_find_clip(project_,id,&c)==XTC_OK)ids.push_back(id); + if(ids.empty() && clip_!=UINT64_MAX && xtc_project_find_clip(project_,clip_,&c)==XTC_OK)ids.push_back(clip_); + return ids; +} +bool SequencerWidget::clip_selected(uint64_t id) const +{ + return selection_.empty() ? id==clip_ : std::find(selection_.begin(),selection_.end(),id)!=selection_.end(); +} +bool SequencerWidget::toggle_clip(uint64_t id) +{ + xtc_clip_info c{};if(xtc_project_find_clip(project_,id,&c)!=XTC_OK)return false; + if(selection_.empty() && clip_!=UINT64_MAX)selection_.push_back(clip_); + auto found=std::find(selection_.begin(),selection_.end(),id); + if(found==selection_.end())selection_.push_back(id);else selection_.erase(found); + clip_=selection_.empty() ? UINT64_MAX : selection_.back();track_=c.track_id;redraw();return true; +} +void SequencerWidget::select_all_clips() +{ + selection_.clear();size_t tracks=0,clips=0;xtc_project_get_counts(project_,&tracks,&clips); + for(size_t i=0;i selected_clips() const; xtc_result duplicate_selection(); xtc_result split_selection(); xtc_result audio_properties(float gain, uint64_t fade_in, uint64_t fade_out); @@ -168,6 +172,7 @@ class SequencerWidget : public Fl_Group { std::unique_ptr data{nullptr, xtc_waveform_destroy}; }; std::vector waves_; + std::vector selection_; void draw_waveform(const xtc_clip_info &clip, int left, int top); xtc_project *project_; Seek seek_; diff --git a/tests/integration/test_gui.cpp b/tests/integration/test_gui.cpp index 61b33c6..a02c7bd 100644 --- a/tests/integration/test_gui.cpp +++ b/tests/integration/test_gui.cpp @@ -1307,6 +1307,16 @@ int main(int argc, char **argv) } unlink(path.c_str()); } + reset(); + { + MainWindow window(false,"");window.show();CHECK(window.create_track(true)==XTC_OK); + auto *s=timeline(&window);uint64_t first=s->selected_clip();CHECK(s->duplicate_selection()==XTC_OK); + uint64_t second=s->selected_clip();CHECK(s->toggle_clip(first));CHECK(s->selected_clips().size()==2); + CHECK(s->clip_selected(first) && s->clip_selected(second));CHECK(!s->toggle_clip(UINT64_MAX)); + window.detach_editor(0,true);CHECK(s->selected_clips().size()==2);window.detach_editor(0,false); + CHECK(s->toggle_clip(first) && !s->clip_selected(first));s->select_all_clips();CHECK(s->selected_clips().size()==2); + window.do_callback(); + } if (failures != 0) return 1; std::printf("GUI controls/errors/views/timers/cleanup passed: %u opens, %u closes.\n", opens, closes); return 0;