feat: plan modern editing phases and add arrangement multi-selection
This commit is contained in:
parent
85291d81ff
commit
c5c7df1d9d
6 changed files with 226 additions and 2 deletions
9
docs/MODERN_EDITING.md
Normal file
9
docs/MODERN_EDITING.md
Normal file
|
|
@ -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.
|
||||
165
docs/ROADMAP.md
165
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.
|
||||
|
|
|
|||
|
|
@ -320,6 +320,7 @@ MainWindow::MainWindow(bool prompt_unsaved,const char *configuration)
|
|||
(void)static_cast<MainWindow *>(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<SequencerWidget *>(c)->select_all_clips();},sequencer_);
|
||||
menu->add("Edit/Duplicate clip",FL_CTRL+'d',[](Fl_Widget *, void *c) { (void)static_cast<SequencerWidget *>(c)->duplicate_selection(); },sequencer_);
|
||||
menu->add("Edit/Split audio at cursor", 0, [](Fl_Widget *, void *c) { (void)static_cast<SequencerWidget *>(c)->split_selection(); }, sequencer_);
|
||||
menu->add("Edit/Audio clip gain and fades", 0, [](Fl_Widget *, void *c) { static_cast<SequencerWidget *>(c)->audio_properties_dialog(); }, sequencer_);
|
||||
|
|
|
|||
|
|
@ -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<int>(c.length_tick*static_cast<unsigned int>(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<size_t>(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<uint64_t> SequencerWidget::selected_clips() const
|
||||
{
|
||||
std::vector<uint64_t> 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<clips;++i){xtc_clip_info c{};xtc_project_get_clip(project_,i,&c);selection_.push_back(c.id);}
|
||||
if(!selection_.empty())clip_=selection_.back();
|
||||
redraw();
|
||||
}
|
||||
xtc_result SequencerWidget::grid(uint64_t ticks)
|
||||
{
|
||||
if (xtc_ui::snap_index(ticks) < 0) return XTC_ERR_INVALID_ARGUMENT;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ class SequencerWidget : public Fl_Group {
|
|||
Fl_Choice *instrument_choice(uint64_t track) const;
|
||||
void refresh_instruments();
|
||||
bool select(uint64_t track, uint64_t clip = UINT64_MAX);
|
||||
bool toggle_clip(uint64_t clip);
|
||||
void select_all_clips();
|
||||
bool clip_selected(uint64_t clip) const;
|
||||
std::vector<uint64_t> 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<xtc_waveform, decltype(&xtc_waveform_destroy)> data{nullptr, xtc_waveform_destroy};
|
||||
};
|
||||
std::vector<Wave> waves_;
|
||||
std::vector<uint64_t> selection_;
|
||||
void draw_waveform(const xtc_clip_info &clip, int left, int top);
|
||||
xtc_project *project_;
|
||||
Seek seek_;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue