= ({ isVisible }) => {
+ setAddEventType(value as AddEventType)}
+ label="Note"
+ buttonClassName="list-event-type-button"
+ showValueAsLabel
+ />
@@ -780,6 +833,15 @@ const ListEventPanel: React.FC = ({ isVisible }) => {
label="Qua. Len."
buttonClassName="list-event-quant-button"
/>
+
diff --git a/src/core/commands/index.ts b/src/core/commands/index.ts
index 9817ec7..5dc6515 100644
--- a/src/core/commands/index.ts
+++ b/src/core/commands/index.ts
@@ -30,6 +30,7 @@ export { MergeMidiRegionsCommand } from './region/MergeMidiRegionsCommand';
// Note commands
export { CreateNoteCommand } from './note/CreateNoteCommand';
+export { DeleteMidiEventsCommand } from './note/DeleteMidiEventsCommand';
export { DeleteNotesCommand, DeleteNoteCommand } from './note/DeleteNotesCommand';
export { ResizeNotesCommand } from './note/ResizeNotesCommand';
export { MoveNotesCommand } from './note/MoveNotesCommand';
diff --git a/src/core/commands/note/CreateMidiEventsCommand.ts b/src/core/commands/note/CreateMidiEventsCommand.ts
index a9185eb..43c42da 100644
--- a/src/core/commands/note/CreateMidiEventsCommand.ts
+++ b/src/core/commands/note/CreateMidiEventsCommand.ts
@@ -114,6 +114,10 @@ export class CreateMidiEventsCommand extends KGCommand {
return this.createdNotes;
}
+ public getCreatedPitchBends(): Array<{ pitchBend: KGMidiPitchBend; regionId: string }> {
+ return this.createdPitchBends;
+ }
+
public getCreatedNoteIds(): string[] {
return this.noteCreationData.map(data => data.noteId!);
}
diff --git a/src/core/commands/note/DeleteMidiEventsCommand.test.ts b/src/core/commands/note/DeleteMidiEventsCommand.test.ts
new file mode 100644
index 0000000..f19140f
--- /dev/null
+++ b/src/core/commands/note/DeleteMidiEventsCommand.test.ts
@@ -0,0 +1,77 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { DeleteMidiEventsCommand } from './DeleteMidiEventsCommand';
+import { KGCore } from '../../KGCore';
+import { KGMidiNote } from '../../midi/KGMidiNote';
+import { KGMidiPitchBend } from '../../midi/KGMidiPitchBend';
+import { createMockMidiRegion, createMockMidiTrack, createMockProject } from '../../../test/utils/mock-data';
+
+vi.mock('../../KGCore', () => ({
+ KGCore: {
+ instance: vi.fn()
+ }
+}));
+
+interface MockCore {
+ getCurrentProject: ReturnType;
+ getSelectedItems: ReturnType;
+ removeSelectedItem: ReturnType;
+}
+
+describe('DeleteMidiEventsCommand', () => {
+ let mockCore: MockCore;
+ let note: KGMidiNote;
+ let pitchBend: KGMidiPitchBend;
+ let region: ReturnType;
+
+ beforeEach(() => {
+ note = new KGMidiNote('note-1', 1, 2, 60, 100);
+ pitchBend = new KGMidiPitchBend('bend-1', 1.5, 12288);
+
+ region = createMockMidiRegion({
+ id: 'region-1',
+ trackId: 'track-1',
+ trackIndex: 0,
+ notes: [note],
+ pitchBends: [pitchBend],
+ });
+
+ const track = createMockMidiTrack({
+ id: 1,
+ regions: [region],
+ });
+
+ const project = createMockProject({
+ tracks: [track],
+ });
+
+ mockCore = {
+ getCurrentProject: vi.fn().mockReturnValue(project),
+ getSelectedItems: vi.fn(() => [note, pitchBend]),
+ removeSelectedItem: vi.fn(),
+ };
+
+ vi.mocked(KGCore.instance).mockReturnValue(mockCore as unknown as KGCore);
+ });
+
+ it('deletes selected notes and pitch bends together', () => {
+ const command = new DeleteMidiEventsCommand(['note-1'], ['bend-1']);
+
+ command.execute();
+
+ expect(region.getNotes()).toHaveLength(0);
+ expect(region.getPitchBends()).toHaveLength(0);
+ expect(mockCore.removeSelectedItem).toHaveBeenCalledTimes(2);
+ });
+
+ it('restores deleted notes and pitch bends on undo', () => {
+ const command = new DeleteMidiEventsCommand(['note-1'], ['bend-1']);
+
+ command.execute();
+ command.undo();
+
+ expect(region.getNotes()).toHaveLength(1);
+ expect(region.getNotes()[0].getId()).toBe('note-1');
+ expect(region.getPitchBends()).toHaveLength(1);
+ expect(region.getPitchBends()[0].getId()).toBe('bend-1');
+ });
+});
diff --git a/src/core/commands/note/DeleteMidiEventsCommand.ts b/src/core/commands/note/DeleteMidiEventsCommand.ts
new file mode 100644
index 0000000..30f5a4c
--- /dev/null
+++ b/src/core/commands/note/DeleteMidiEventsCommand.ts
@@ -0,0 +1,153 @@
+import { KGCommand } from '../KGCommand';
+import { KGCore } from '../../KGCore';
+import { KGMidiNote } from '../../midi/KGMidiNote';
+import { KGMidiPitchBend } from '../../midi/KGMidiPitchBend';
+import { KGMidiRegion } from '../../region/KGMidiRegion';
+
+interface DeletedNoteData {
+ note: KGMidiNote;
+ regionId: string;
+ originalIndex: number;
+}
+
+interface DeletedPitchBendData {
+ pitchBend: KGMidiPitchBend;
+ regionId: string;
+ originalIndex: number;
+}
+
+export class DeleteMidiEventsCommand extends KGCommand {
+ private noteIds: string[];
+ private pitchBendIds: string[];
+ private deletedNoteData: DeletedNoteData[] = [];
+ private deletedPitchBendData: DeletedPitchBendData[] = [];
+
+ constructor(noteIds: string[] = [], pitchBendIds: string[] = []) {
+ super();
+ this.noteIds = noteIds;
+ this.pitchBendIds = pitchBendIds;
+ }
+
+ execute(): void {
+ const core = KGCore.instance();
+ const tracks = core.getCurrentProject().getTracks();
+
+ this.deletedNoteData = [];
+ this.deletedPitchBendData = [];
+
+ for (const noteId of this.noteIds) {
+ for (const track of tracks) {
+ for (const region of track.getRegions()) {
+ if (!(region instanceof KGMidiRegion)) continue;
+
+ const noteIndex = region.getNotes().findIndex(note => note.getId() === noteId);
+ if (noteIndex === -1) continue;
+
+ this.deletedNoteData.push({
+ note: region.getNotes()[noteIndex],
+ regionId: region.getId(),
+ originalIndex: noteIndex,
+ });
+ break;
+ }
+ }
+ }
+
+ for (const pitchBendId of this.pitchBendIds) {
+ for (const track of tracks) {
+ for (const region of track.getRegions()) {
+ if (!(region instanceof KGMidiRegion)) continue;
+
+ const pitchBendIndex = region.getPitchBends().findIndex(pitchBend => pitchBend.getId() === pitchBendId);
+ if (pitchBendIndex === -1) continue;
+
+ this.deletedPitchBendData.push({
+ pitchBend: region.getPitchBends()[pitchBendIndex],
+ regionId: region.getId(),
+ originalIndex: pitchBendIndex,
+ });
+ break;
+ }
+ }
+ }
+
+ if (this.deletedNoteData.length === 0 && this.deletedPitchBendData.length === 0) {
+ throw new Error('No MIDI events found to delete');
+ }
+
+ this.deletedNoteData.sort((a, b) => b.originalIndex - a.originalIndex);
+ this.deletedPitchBendData.sort((a, b) => b.originalIndex - a.originalIndex);
+
+ for (const data of this.deletedNoteData) {
+ const region = this.resolveRegion(tracks, data.regionId);
+ region.removeNote(data.note.getId());
+
+ const selectedNote = core.getSelectedItems().find(item => item instanceof KGMidiNote && item.getId() === data.note.getId());
+ if (selectedNote) {
+ core.removeSelectedItem(selectedNote);
+ }
+ }
+
+ for (const data of this.deletedPitchBendData) {
+ const region = this.resolveRegion(tracks, data.regionId);
+ region.removePitchBend(data.pitchBend.getId());
+
+ const selectedPitchBend = core.getSelectedItems().find(
+ item => item instanceof KGMidiPitchBend && item.getId() === data.pitchBend.getId()
+ );
+ if (selectedPitchBend) {
+ core.removeSelectedItem(selectedPitchBend);
+ }
+ }
+ }
+
+ undo(): void {
+ const tracks = KGCore.instance().getCurrentProject().getTracks();
+
+ for (const data of [...this.deletedNoteData].sort((a, b) => a.originalIndex - b.originalIndex)) {
+ const region = this.resolveRegion(tracks, data.regionId);
+ const notes = region.getNotes();
+ if (data.originalIndex >= 0 && data.originalIndex <= notes.length) {
+ notes.splice(data.originalIndex, 0, data.note);
+ region.setNotes(notes);
+ } else {
+ region.addNote(data.note);
+ }
+ }
+
+ for (const data of [...this.deletedPitchBendData].sort((a, b) => a.originalIndex - b.originalIndex)) {
+ const region = this.resolveRegion(tracks, data.regionId);
+ const pitchBends = region.getPitchBends();
+ if (data.originalIndex >= 0 && data.originalIndex <= pitchBends.length) {
+ pitchBends.splice(data.originalIndex, 0, data.pitchBend);
+ region.setPitchBends(pitchBends);
+ } else {
+ region.addPitchBend(data.pitchBend);
+ }
+ }
+ }
+
+ getDescription(): string {
+ const noteCount = this.noteIds.length;
+ const pitchBendCount = this.pitchBendIds.length;
+
+ if (noteCount > 0 && pitchBendCount > 0) {
+ return `Delete ${noteCount} note${noteCount === 1 ? '' : 's'} and ${pitchBendCount} pitch bend${pitchBendCount === 1 ? '' : 's'}`;
+ }
+ if (pitchBendCount > 0) {
+ return pitchBendCount === 1 ? 'Delete pitch bend' : `Delete ${pitchBendCount} pitch bends`;
+ }
+ return noteCount === 1 ? 'Delete note' : `Delete ${noteCount} notes`;
+ }
+
+ private resolveRegion(tracks: Array<{ getRegions(): unknown[] }>, regionId: string): KGMidiRegion {
+ for (const track of tracks) {
+ const region = track.getRegions().find(candidate => candidate instanceof KGMidiRegion && candidate.getId() === regionId);
+ if (region instanceof KGMidiRegion) {
+ return region;
+ }
+ }
+
+ throw new Error(`MIDI region with ID ${regionId} not found`);
+ }
+}