feat: edit arrangement selections atomically with grouped undo
This commit is contained in:
parent
c5c7df1d9d
commit
7379545ced
10 changed files with 139 additions and 25 deletions
11
Makefile
11
Makefile
|
|
@ -27,7 +27,7 @@ CXXFLAGS ?= -O2 -g
|
|||
CXXFLAGS += -std=c++17 -Wall -Wextra -Wpedantic -Iinclude
|
||||
LDFLAGS ?=
|
||||
|
||||
CORE_SOURCES := src/audio/export.c src/plugin/builtin.c src/settings/settings.c src/control/model.c src/audio/waveform.c src/plugin/ladspa.c src/plugin/host.c src/plugin/modern.c src/native/processor.c src/audio/recovery.c src/dsp/ramp.c src/native/native.c src/mixer/model.c src/mixer/render.c src/project/history.c src/midi/model.c src/midi/instrument.c src/audio/wave.c src/audio/arrangement.c src/audio/recorder.c src/core/xtc.c src/project/project.c src/persistence/project_file.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c src/engine/realtime.c src/backend/jack_backend.c
|
||||
CORE_SOURCES := src/project/editing.c src/audio/export.c src/plugin/builtin.c src/settings/settings.c src/control/model.c src/audio/waveform.c src/plugin/ladspa.c src/plugin/host.c src/plugin/modern.c src/native/processor.c src/audio/recovery.c src/dsp/ramp.c src/native/native.c src/mixer/model.c src/mixer/render.c src/project/history.c src/midi/model.c src/midi/instrument.c src/audio/wave.c src/audio/arrangement.c src/audio/recorder.c src/core/xtc.c src/project/project.c src/persistence/project_file.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c src/engine/realtime.c src/backend/jack_backend.c
|
||||
JACK_AVAILABLE := $(shell $(PKG_CONFIG) --exists jack 2>/dev/null && echo yes)
|
||||
ifeq ($(JACK_AVAILABLE),yes)
|
||||
JACK_CFLAGS := $(shell $(PKG_CONFIG) --cflags jack)
|
||||
|
|
@ -104,7 +104,7 @@ $(BUILD_DIR)/test_native: $(BUILD_DIR)/tests/unit/test_native.o $(BUILD_DIR)/lib
|
|||
$(BUILD_DIR)/native-gain: $(BUILD_DIR)/examples/native_gain.o $(BUILD_DIR)/libxtc.a
|
||||
$(CC) $(LDFLAGS) -o $@ $^ -lm -ldl
|
||||
|
||||
test: $(BUILD_DIR)/test_native $(BUILD_DIR)/native-gain $(BUILD_DIR)/test_mixer $(BUILD_DIR)/test_midi $(BUILD_DIR)/test_audio $(BUILD_DIR)/test_project $(BUILD_DIR)/test_graph_edits $(BACKEND_TEST) $(BUILD_DIR)/test_realtime $(BUILD_DIR)/test_core $(BUILD_DIR)/test_offline $(BUILD_DIR)/xtc-check
|
||||
test: test-modern-editing $(BUILD_DIR)/test_native $(BUILD_DIR)/native-gain $(BUILD_DIR)/test_mixer $(BUILD_DIR)/test_midi $(BUILD_DIR)/test_audio $(BUILD_DIR)/test_project $(BUILD_DIR)/test_graph_edits $(BACKEND_TEST) $(BUILD_DIR)/test_realtime $(BUILD_DIR)/test_core $(BUILD_DIR)/test_offline $(BUILD_DIR)/xtc-check
|
||||
$(if $(BACKEND_TEST),./$(BACKEND_TEST),@echo "JACK fault tests skipped: JACK development files unavailable")
|
||||
./$(BUILD_DIR)/test_native
|
||||
./$(BUILD_DIR)/native-gain
|
||||
|
|
@ -534,3 +534,10 @@ $(BUILD_DIR)/builtin-preview: $(BUILD_DIR)/examples/builtin_preview.o $(BUILD_DI
|
|||
builtin-preview: $(BUILD_DIR)/builtin-preview
|
||||
./$(BUILD_DIR)/builtin-preview "$(abspath $(BUILD_DIR))/builtin"
|
||||
-include $(BUILD_DIR)/examples/builtin_preview.d
|
||||
|
||||
$(BUILD_DIR)/test_modern_editing: tests/unit/test_modern_editing.c $(BUILD_DIR)/libxtc.a
|
||||
$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) -o $@ $< $(BUILD_DIR)/libxtc.a -lm -ldl
|
||||
|
||||
.PHONY: test-modern-editing
|
||||
test-modern-editing: $(BUILD_DIR)/test_modern_editing
|
||||
./$(BUILD_DIR)/test_modern_editing
|
||||
|
|
|
|||
|
|
@ -7,3 +7,7 @@ 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.
|
||||
|
||||
## Phase 48
|
||||
|
||||
Use Left/Right in the sequencer (or Edit/Nudge selection) to move the selected phrase by the current snap step. Ctrl+D duplicates after its complete bounding span; Delete removes selected clips. Edits are atomic and undoable. Mouse dragging remains a single-clip gesture.
|
||||
|
|
|
|||
|
|
@ -1333,12 +1333,12 @@ Implemented: additive Shift-click selection, Ctrl+A/Select all and selected clip
|
|||
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.
|
||||
- [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
|
||||
Planned.
|
||||
Implemented: atomic C99 batch move/duplicate/delete APIs and GUI selection commands. One published edit preserves relative spacing, fresh duplicate IDs and single undo history entries. Core and GUI tests pass, including negative-position rollback and invalid selections.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
19
include/xtc/xtc_editing.h
Normal file
19
include/xtc/xtc_editing.h
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#ifndef XTC_EDITING_H
|
||||
#define XTC_EDITING_H
|
||||
#include "xtc_midi.h"
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
/* Control thread only. Nonempty unique stable IDs, at most MAX_CLIPS.
|
||||
* All operations clone/validate before publication: errors preserve the project,
|
||||
* ID counters and optional count-element output. No media loading or DSP work.
|
||||
* MOVE/DUPLICATE amount is a signed tick displacement, bounded by MAX_TICK.
|
||||
* DUPLICATE retains tracks, source ranges, notes and media with fresh IDs.
|
||||
* out_ids receives resulting IDs (removed IDs for DELETE). May be NULL. */
|
||||
typedef enum { XTC_CLIPS_MOVE, XTC_CLIPS_DUPLICATE, XTC_CLIPS_DELETE } xtc_clip_operation;
|
||||
xtc_result xtc_project_transform_clips(xtc_project *project,const uint64_t *ids,size_t count,
|
||||
xtc_clip_operation operation,int64_t amount,uint64_t *out_ids);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
|
@ -320,6 +320,8 @@ 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/Nudge selection left",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->nudge_selection(-1);},sequencer_);
|
||||
menu->add("Edit/Nudge selection right",0,[](Fl_Widget *,void *c){static_cast<SequencerWidget *>(c)->nudge_selection(1);},sequencer_);
|
||||
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_);
|
||||
|
|
|
|||
|
|
@ -209,19 +209,11 @@ xtc_result SequencerWidget::add_midi_clip()
|
|||
*/
|
||||
xtc_result SequencerWidget::delete_selection()
|
||||
{
|
||||
if (before_edit) {
|
||||
xtc_result r = before_edit(context_);
|
||||
if (r != XTC_OK) {
|
||||
changed(r);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
xtc_result r =
|
||||
clip_ != UINT64_MAX ? xtc_project_remove_clip(project_, clip_) : xtc_project_remove_track(project_, track_);
|
||||
if (r == XTC_OK)
|
||||
clip_ = track_ = UINT64_MAX;
|
||||
changed(r);
|
||||
return r;
|
||||
if(!selected_clips().empty())return transform_selection(XTC_CLIPS_DELETE,0);
|
||||
xtc_result r=before_edit ? before_edit(context_) : XTC_OK;
|
||||
if(r==XTC_OK)r=xtc_project_remove_track(project_,track_);
|
||||
if(r==XTC_OK)track_=clip_=UINT64_MAX;
|
||||
changed(r);return r;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -497,6 +489,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_ALT|FL_SHIFT) && (Fl::event_key()==FL_Left || Fl::event_key()==FL_Right)) {nudge_selection(Fl::event_key()==FL_Left ? -1 : 1);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) {
|
||||
|
|
@ -548,14 +541,27 @@ xtc_result SequencerWidget::grid(uint64_t ticks)
|
|||
if (xtc_ui::snap_index(ticks) < 0) return XTC_ERR_INVALID_ARGUMENT;
|
||||
grid_ = ticks; redraw(); return XTC_OK;
|
||||
}
|
||||
xtc_result SequencerWidget::transform_selection(xtc_clip_operation op,int64_t amount)
|
||||
{
|
||||
auto ids=selected_clips();std::vector<uint64_t> output(ids.size());
|
||||
xtc_result r=ids.empty() ? XTC_ERR_NOT_FOUND : XTC_OK;
|
||||
if(r==XTC_OK && before_edit)r=before_edit(context_);
|
||||
if(r==XTC_OK)r=xtc_project_transform_clips(project_,ids.data(),ids.size(),op,amount,output.data());
|
||||
if(r==XTC_OK) {
|
||||
selection_=op==XTC_CLIPS_DELETE ? std::vector<uint64_t>{} : output;
|
||||
clip_=selection_.empty() ? UINT64_MAX : selection_.back();
|
||||
}
|
||||
changed(r);return r;
|
||||
}
|
||||
xtc_result SequencerWidget::nudge_selection(int direction)
|
||||
{
|
||||
return transform_selection(XTC_CLIPS_MOVE,direction<0 ? -static_cast<int64_t>(grid_) : static_cast<int64_t>(grid_));
|
||||
}
|
||||
xtc_result SequencerWidget::duplicate_selection()
|
||||
{
|
||||
xtc_clip_info c{}; uint64_t id;
|
||||
xtc_result r = xtc_project_find_clip(project_,clip_,&c);
|
||||
if (r == XTC_OK && before_edit) r = before_edit(context_);
|
||||
if (r == XTC_OK) r = xtc_project_duplicate_clip(project_,clip_,c.track_id,c.start_tick+c.length_tick,&id);
|
||||
if (r == XTC_OK) clip_ = id;
|
||||
changed(r); return r;
|
||||
auto ids=selected_clips();uint64_t begin=XTC_PROJECT_MAX_TICK,end=0;
|
||||
for(auto id:ids){xtc_clip_info c{};xtc_project_find_clip(project_,id,&c);begin=std::min(begin,c.start_tick);end=std::max(end,c.start_tick+c.length_tick);}
|
||||
return transform_selection(XTC_CLIPS_DUPLICATE,static_cast<int64_t>(end)-static_cast<int64_t>(begin));
|
||||
}
|
||||
xtc_result SequencerWidget::rename_selection(const char *name)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include <FL/Fl_Group.H>
|
||||
#include <FL/Fl_Choice.H>
|
||||
#include "xtc/xtc_plugin.h"
|
||||
#include "xtc/xtc_editing.h"
|
||||
#include <array>
|
||||
#include "xtc/xtc_audio.h"
|
||||
#include <memory>
|
||||
|
|
@ -36,6 +37,8 @@ class SequencerWidget : public Fl_Group {
|
|||
void select_all_clips();
|
||||
bool clip_selected(uint64_t clip) const;
|
||||
std::vector<uint64_t> selected_clips() const;
|
||||
xtc_result transform_selection(xtc_clip_operation operation,int64_t amount);
|
||||
xtc_result nudge_selection(int direction);
|
||||
xtc_result duplicate_selection();
|
||||
xtc_result split_selection();
|
||||
xtc_result audio_properties(float gain, uint64_t fade_in, uint64_t fade_out);
|
||||
|
|
|
|||
40
src/project/editing.c
Normal file
40
src/project/editing.c
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
#include "project_internal.h"
|
||||
#include "xtc/xtc_editing.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static xtc_result selection_valid(const xtc_project *p,const uint64_t *ids,size_t count)
|
||||
{
|
||||
size_t i,j;xtc_clip_info c;
|
||||
if(!p || !ids || !count || count>XTC_PROJECT_MAX_CLIPS)return XTC_ERR_INVALID_ARGUMENT;
|
||||
for(i=0;i<count;++i) {
|
||||
if(xtc_project_find_clip(p,ids[i],&c)!=XTC_OK)return XTC_ERR_NOT_FOUND;
|
||||
for(j=0;j<i;++j)if(ids[i]==ids[j])return XTC_ERR_INVALID_ARGUMENT;
|
||||
}
|
||||
return XTC_OK;
|
||||
}
|
||||
static void publish(xtc_project *p,xtc_project *copy)
|
||||
{
|
||||
xtc_graph *old=p->graph;*p=*copy;copy->graph=old;
|
||||
}
|
||||
xtc_result xtc_project_transform_clips(xtc_project *p,const uint64_t *ids,size_t count,
|
||||
xtc_clip_operation operation,int64_t amount,uint64_t *out_ids)
|
||||
{
|
||||
xtc_project *copy=NULL;size_t i;uint64_t output[XTC_PROJECT_MAX_CLIPS];
|
||||
xtc_result r=selection_valid(p,ids,count);
|
||||
if(r!=XTC_OK)return r;
|
||||
if(operation<XTC_CLIPS_MOVE || operation>XTC_CLIPS_DELETE ||
|
||||
amount<-(int64_t)XTC_PROJECT_MAX_TICK || amount>(int64_t)XTC_PROJECT_MAX_TICK)return XTC_ERR_INVALID_ARGUMENT;
|
||||
r=xtc_project_clone(p,©);
|
||||
for(i=0;r==XTC_OK && i<count;++i) {
|
||||
xtc_clip_info c;int64_t start;
|
||||
r=xtc_project_find_clip(copy,ids[i],&c);if(r!=XTC_OK)break;
|
||||
start=(int64_t)c.start_tick+amount;output[i]=ids[i];
|
||||
if(operation==XTC_CLIPS_DELETE)r=xtc_project_remove_clip(copy,ids[i]);
|
||||
else if(start<0 || start>(int64_t)XTC_PROJECT_MAX_TICK)r=XTC_ERR_INVALID_ARGUMENT;
|
||||
else if(operation==XTC_CLIPS_DUPLICATE)r=xtc_project_duplicate_clip(copy,ids[i],c.track_id,(uint64_t)start,output+i);
|
||||
else r=xtc_project_edit_clip(copy,ids[i],c.track_id,(uint64_t)start,c.length_tick,c.offset_tick);
|
||||
}
|
||||
if(r==XTC_OK){publish(p,copy);if(out_ids)memcpy(out_ids,output,count*sizeof(*output));}
|
||||
xtc_project_destroy(copy);return r;
|
||||
}
|
||||
|
|
@ -1315,6 +1315,11 @@ int main(int argc, char **argv)
|
|||
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);
|
||||
CHECK(s->nudge_selection(1)==XTC_OK);xtc_clip_info moved{};
|
||||
CHECK(xtc_project_find_clip(s->project(),first,&moved)==XTC_OK && moved.start_tick==240);
|
||||
CHECK(s->duplicate_selection()==XTC_OK && s->selected_clips().size()==2);
|
||||
size_t tc=0,cc=0;xtc_project_get_counts(s->project(),&tc,&cc);CHECK(cc==4);
|
||||
CHECK(s->delete_selection()==XTC_OK);xtc_project_get_counts(s->project(),&tc,&cc);CHECK(cc==2);
|
||||
window.do_callback();
|
||||
}
|
||||
if (failures != 0) return 1;
|
||||
|
|
|
|||
28
tests/unit/test_modern_editing.c
Normal file
28
tests/unit/test_modern_editing.c
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#include "xtc/xtc_editing.h"
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
|
||||
int main(void)
|
||||
{
|
||||
xtc_project *p=NULL;uint64_t track,a,b,n,ids[2],out[2]={999,999};xtc_clip_info c;
|
||||
size_t tracks,clips,notes;
|
||||
assert(xtc_project_create(&p)==XTC_OK);
|
||||
assert(xtc_project_add_track(p,"Keys",&track)==XTC_OK);
|
||||
assert(xtc_project_add_midi_clip(p,track,960,1920,&a)==XTC_OK);
|
||||
assert(xtc_project_add_note(p,a,0,240,60,100,0,&n)==XTC_OK);
|
||||
assert(xtc_project_add_clip(p,track,"Audio",3840,960,&b)==XTC_OK);
|
||||
ids[0]=a;ids[1]=b;
|
||||
assert(xtc_project_transform_clips(p,ids,2,XTC_CLIPS_MOVE,-1920,out)==XTC_ERR_INVALID_ARGUMENT);
|
||||
assert(out[0]==999 && xtc_project_find_clip(p,b,&c)==XTC_OK && c.start_tick==3840);
|
||||
assert(xtc_project_transform_clips(p,ids,2,XTC_CLIPS_MOVE,240,out)==XTC_OK);
|
||||
assert(xtc_project_find_clip(p,a,&c)==XTC_OK && c.start_tick==1200);
|
||||
assert(xtc_project_transform_clips(p,ids,2,XTC_CLIPS_DUPLICATE,4800,out)==XTC_OK);
|
||||
assert(out[0]!=a && out[1]!=b);
|
||||
assert(xtc_project_note_count(p,¬es)==XTC_OK && notes==2);
|
||||
assert(xtc_project_transform_clips(p,out,2,XTC_CLIPS_DELETE,0,NULL)==XTC_OK);
|
||||
assert(xtc_project_get_counts(p,&tracks,&clips)==XTC_OK && clips==2);
|
||||
ids[1]=a;assert(xtc_project_transform_clips(p,ids,2,XTC_CLIPS_DELETE,0,NULL)==XTC_ERR_INVALID_ARGUMENT);
|
||||
ids[1]=UINT64_MAX;assert(xtc_project_transform_clips(p,ids,2,XTC_CLIPS_MOVE,1,NULL)==XTC_ERR_NOT_FOUND);
|
||||
assert(xtc_project_transform_clips(p,ids,0,XTC_CLIPS_MOVE,1,NULL)==XTC_ERR_INVALID_ARGUMENT);
|
||||
xtc_project_destroy(p);puts("Modern editing tests passed.");return 0;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue