feat: enhance multi-region selection feature to support both shift and cmd/ctrl
This commit is contained in:
@@ -340,10 +340,6 @@
|
|||||||
transition: box-shadow 0.1s ease, border-color 0.1s ease;
|
transition: box-shadow 0.1s ease, border-color 0.1s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.global-marker-region:hover {
|
|
||||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.45);
|
|
||||||
}
|
|
||||||
|
|
||||||
.global-marker-region.selected {
|
.global-marker-region.selected {
|
||||||
border-color: #ffffff;
|
border-color: #ffffff;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||||
import MainContent from './MainContent';
|
import MainContent from './MainContent';
|
||||||
import { KGMidiRegion } from '../core/region/KGMidiRegion';
|
import { KGMidiRegion } from '../core/region/KGMidiRegion';
|
||||||
import { KGAudioRegion } from '../core/region/KGAudioRegion';
|
import { KGAudioRegion } from '../core/region/KGAudioRegion';
|
||||||
import { KGChordRegion } from '../core/region/KGChordRegion';
|
import { KGChordRegion } from '../core/region/KGChordRegion';
|
||||||
|
import { KGTempoRegion } from '../core/region/KGTempoRegion';
|
||||||
import { KGAudioTrack } from '../core/track/KGAudioTrack';
|
import { KGAudioTrack } from '../core/track/KGAudioTrack';
|
||||||
import { createDefaultGlobalTracks, GlobalTrackType } from '../core/global-track';
|
import { createDefaultGlobalTracks, GlobalTrackType } from '../core/global-track';
|
||||||
import { createMockMidiTrack } from '../test/utils/mock-data';
|
import { createMockMidiTrack } from '../test/utils/mock-data';
|
||||||
@@ -71,7 +72,7 @@ const storeState = {
|
|||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
type StoreSelector = (...args: [typeof storeState]) => unknown;
|
type StoreSelector = (...args: [typeof storeState]) => unknown;
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
type RegionClickHandler = (...args: [string, { shiftKey: boolean }]) => void;
|
type RegionClickHandler = (...args: [string, { shiftKey: boolean; metaKey: boolean; ctrlKey: boolean }]) => void;
|
||||||
|
|
||||||
const finishTrackCreateDialogClose = () => {
|
const finishTrackCreateDialogClose = () => {
|
||||||
const overlay = document.querySelector('.dialog-overlay');
|
const overlay = document.querySelector('.dialog-overlay');
|
||||||
@@ -116,15 +117,27 @@ vi.mock('./track/TrackInfoPanel', () => ({
|
|||||||
vi.mock('./track/TrackGridPanel', () => ({
|
vi.mock('./track/TrackGridPanel', () => ({
|
||||||
default: ({ onRegionClick }: { onRegionClick?: RegionClickHandler }) => (
|
default: ({ onRegionClick }: { onRegionClick?: RegionClickHandler }) => (
|
||||||
<>
|
<>
|
||||||
<button type="button" onClick={() => onRegionClick?.('region-1', { shiftKey: false })}>
|
<button type="button" onClick={() => onRegionClick?.('region-1', { shiftKey: false, metaKey: false, ctrlKey: false })}>
|
||||||
select-midi-region
|
select-midi-region
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={() => onRegionClick?.('region-2', { shiftKey: false })}>
|
<button type="button" onClick={() => onRegionClick?.('region-2', { shiftKey: false, metaKey: false, ctrlKey: false })}>
|
||||||
select-second-midi-region
|
select-second-midi-region
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={() => onRegionClick?.('audio-1', { shiftKey: false })}>
|
<button type="button" onClick={() => onRegionClick?.('region-2', { shiftKey: true, metaKey: false, ctrlKey: false })}>
|
||||||
|
shift-select-second-midi-region
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => onRegionClick?.('region-2', { shiftKey: false, metaKey: true, ctrlKey: false })}>
|
||||||
|
meta-select-second-midi-region
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => onRegionClick?.('region-2', { shiftKey: true, metaKey: true, ctrlKey: false })}>
|
||||||
|
shift-meta-select-second-midi-region
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => onRegionClick?.('audio-1', { shiftKey: false, metaKey: false, ctrlKey: false })}>
|
||||||
select-audio-region
|
select-audio-region
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" onClick={() => onRegionClick?.('audio-1', { shiftKey: false, metaKey: true, ctrlKey: false })}>
|
||||||
|
meta-select-audio-region
|
||||||
|
</button>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
@@ -139,6 +152,9 @@ vi.mock('./piano-roll/PianoRoll', () => ({
|
|||||||
|
|
||||||
describe('MainContent', () => {
|
describe('MainContent', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
midiTrack.setRegions([midiRegion, anotherMidiRegion]);
|
||||||
|
audioTrack.setRegions([audioRegion]);
|
||||||
|
storeState.tracks = [midiTrack, audioTrack];
|
||||||
storeState.globalTracks = createDefaultGlobalTracks();
|
storeState.globalTracks = createDefaultGlobalTracks();
|
||||||
storeState.selectedRegionIds = [];
|
storeState.selectedRegionIds = [];
|
||||||
storeState.activeRegionId = null;
|
storeState.activeRegionId = null;
|
||||||
@@ -207,6 +223,77 @@ describe('MainContent', () => {
|
|||||||
expect(storeState.openSpectrogramViewer).toHaveBeenCalledWith('audio-1');
|
expect(storeState.openSpectrogramViewer).toHaveBeenCalledWith('audio-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('cmd-click adds a second regular region without range fill', () => {
|
||||||
|
render(<MainContent />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'select-midi-region' }));
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'meta-select-second-midi-region' }));
|
||||||
|
|
||||||
|
expect(storeState.selectedRegionIds).toEqual(['region-1', 'region-2']);
|
||||||
|
expect(storeState.activeRegionId).toBe('region-2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cmd-click on an already selected regular region removes it', () => {
|
||||||
|
storeState.selectedRegionIds = ['region-1', 'region-2'];
|
||||||
|
|
||||||
|
render(<MainContent />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'meta-select-second-midi-region' }));
|
||||||
|
|
||||||
|
expect(storeState.selectedRegionIds).toEqual(['region-1']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shift-click on the same track selects the full in-between range and keeps the clicked region primary', () => {
|
||||||
|
const middleMidiRegion = new KGMidiRegion('region-1b', '1', 0, 'Region 1B', 4, 4);
|
||||||
|
midiTrack.setRegions([midiRegion, middleMidiRegion, anotherMidiRegion]);
|
||||||
|
|
||||||
|
render(<MainContent />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'select-midi-region' }));
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'shift-select-second-midi-region' }));
|
||||||
|
|
||||||
|
expect(storeState.selectedRegionIds).toEqual(['region-1', 'region-1b', 'region-2']);
|
||||||
|
|
||||||
|
midiTrack.setRegions([midiRegion, anotherMidiRegion]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shift-click with no same-track anchor falls back to single selection', () => {
|
||||||
|
render(<MainContent />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'shift-select-second-midi-region' }));
|
||||||
|
|
||||||
|
expect(storeState.selectedRegionIds).toEqual(['region-2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shift-click preserves already selected regions on other regular tracks', () => {
|
||||||
|
const middleMidiRegion = new KGMidiRegion('region-1b', '1', 0, 'Region 1B', 4, 4);
|
||||||
|
midiTrack.setRegions([midiRegion, middleMidiRegion, anotherMidiRegion]);
|
||||||
|
|
||||||
|
render(<MainContent />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'select-midi-region' }));
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'meta-select-audio-region' }));
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'shift-select-second-midi-region' }));
|
||||||
|
|
||||||
|
expect(storeState.selectedRegionIds).toEqual(['audio-1', 'region-1', 'region-1b', 'region-2']);
|
||||||
|
|
||||||
|
midiTrack.setRegions([midiRegion, anotherMidiRegion]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shift-plus-cmd-click uses additive-toggle behavior instead of range selection', () => {
|
||||||
|
const middleMidiRegion = new KGMidiRegion('region-1b', '1', 0, 'Region 1B', 4, 4);
|
||||||
|
midiTrack.setRegions([midiRegion, middleMidiRegion, anotherMidiRegion]);
|
||||||
|
|
||||||
|
render(<MainContent />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'select-midi-region' }));
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'shift-meta-select-second-midi-region' }));
|
||||||
|
|
||||||
|
expect(storeState.selectedRegionIds).toEqual(['region-1', 'region-2']);
|
||||||
|
|
||||||
|
midiTrack.setRegions([midiRegion, anotherMidiRegion]);
|
||||||
|
});
|
||||||
|
|
||||||
it('explicit close still clears piano roll visibility and active region', () => {
|
it('explicit close still clears piano roll visibility and active region', () => {
|
||||||
storeState.showPianoRoll = true;
|
storeState.showPianoRoll = true;
|
||||||
storeState.activeRegionId = 'region-1';
|
storeState.activeRegionId = 'region-1';
|
||||||
@@ -352,6 +439,123 @@ describe('MainContent', () => {
|
|||||||
expect((executeCommandMock.mock.calls[1][0] as { startBeat?: number }).startBeat).toBe(5);
|
expect((executeCommandMock.mock.calls[1][0] as { startBeat?: number }).startBeat).toBe(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('selecting a global region clears regular-region selection', () => {
|
||||||
|
const globalTracks = createDefaultGlobalTracks();
|
||||||
|
const chordTrack = globalTracks.find(track => track.getType() === GlobalTrackType.Chord);
|
||||||
|
chordTrack?.setRegions([
|
||||||
|
new KGChordRegion('chord-1', chordTrack.getId(), chordTrack.getTrackIndex(), 'Am', 0, 4),
|
||||||
|
]);
|
||||||
|
storeState.globalTracks = globalTracks;
|
||||||
|
|
||||||
|
render(<MainContent />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'select-midi-region' }));
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
|
||||||
|
fireEvent.click(screen.getByText('Am'));
|
||||||
|
|
||||||
|
expect(storeState.selectedRegionIds).toEqual(['chord-1']);
|
||||||
|
|
||||||
|
storeState.globalTracks = createDefaultGlobalTracks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shift-click across globals ranges only within the same global lane and keeps the clicked region primary', () => {
|
||||||
|
const globalTracks = createDefaultGlobalTracks();
|
||||||
|
const chordTrack = globalTracks.find(track => track.getType() === GlobalTrackType.Chord);
|
||||||
|
chordTrack?.setRegions([
|
||||||
|
new KGChordRegion('chord-1', chordTrack.getId(), chordTrack.getTrackIndex(), 'Am', 0, 4),
|
||||||
|
new KGChordRegion('chord-2', chordTrack.getId(), chordTrack.getTrackIndex(), 'F', 4, 4),
|
||||||
|
new KGChordRegion('chord-3', chordTrack.getId(), chordTrack.getTrackIndex(), 'G', 8, 4),
|
||||||
|
]);
|
||||||
|
storeState.globalTracks = globalTracks;
|
||||||
|
|
||||||
|
render(<MainContent />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
|
||||||
|
fireEvent.click(screen.getByText('Am'));
|
||||||
|
fireEvent.click(screen.getByText('G'), { shiftKey: true });
|
||||||
|
|
||||||
|
expect(storeState.selectedRegionIds).toEqual(['chord-1', 'chord-2', 'chord-3']);
|
||||||
|
|
||||||
|
storeState.globalTracks = createDefaultGlobalTracks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shift-clicking from the first chord region to the last chord region selects the full chord range', () => {
|
||||||
|
const globalTracks = createDefaultGlobalTracks();
|
||||||
|
const chordTrack = globalTracks.find(track => track.getType() === GlobalTrackType.Chord);
|
||||||
|
chordTrack?.setRegions([
|
||||||
|
new KGChordRegion('chord-1', chordTrack.getId(), chordTrack.getTrackIndex(), 'Am', 0, 4),
|
||||||
|
new KGChordRegion('chord-2', chordTrack.getId(), chordTrack.getTrackIndex(), 'F', 4, 4),
|
||||||
|
new KGChordRegion('chord-3', chordTrack.getId(), chordTrack.getTrackIndex(), 'G', 8, 4),
|
||||||
|
]);
|
||||||
|
storeState.globalTracks = globalTracks;
|
||||||
|
|
||||||
|
render(<MainContent />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
|
||||||
|
|
||||||
|
const firstChord = screen.getByText('Am');
|
||||||
|
const lastChord = screen.getByText('G');
|
||||||
|
|
||||||
|
fireEvent.mouseDown(firstChord, { clientX: 10, clientY: 10, button: 0 });
|
||||||
|
fireEvent.mouseUp(window, { clientX: 10, clientY: 10 });
|
||||||
|
fireEvent.click(firstChord);
|
||||||
|
|
||||||
|
fireEvent.mouseDown(lastChord, { clientX: 10, clientY: 10, button: 0, shiftKey: true });
|
||||||
|
fireEvent.mouseUp(window, { clientX: 10, clientY: 10, shiftKey: true });
|
||||||
|
fireEvent.click(lastChord, { shiftKey: true });
|
||||||
|
|
||||||
|
expect(storeState.selectedRegionIds).toEqual(['chord-1', 'chord-2', 'chord-3']);
|
||||||
|
|
||||||
|
storeState.globalTracks = createDefaultGlobalTracks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cmd-clicking a region in a different global lane clears the previous global selection', () => {
|
||||||
|
const globalTracks = createDefaultGlobalTracks();
|
||||||
|
const chordTrack = globalTracks.find(track => track.getType() === GlobalTrackType.Chord);
|
||||||
|
const tempoTrack = globalTracks.find(track => track.getType() === GlobalTrackType.Tempo);
|
||||||
|
chordTrack?.setRegions([
|
||||||
|
new KGChordRegion('chord-1', chordTrack.getId(), chordTrack.getTrackIndex(), 'Am', 0, 4),
|
||||||
|
]);
|
||||||
|
tempoTrack?.setRegions([
|
||||||
|
new KGTempoRegion('tempo-1', tempoTrack.getId(), tempoTrack.getTrackIndex(), 128, 0, 4, 4),
|
||||||
|
]);
|
||||||
|
storeState.globalTracks = globalTracks;
|
||||||
|
|
||||||
|
render(<MainContent />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
|
||||||
|
fireEvent.click(screen.getByText('Am'));
|
||||||
|
fireEvent.click(screen.getByText('128 BPM'), { metaKey: true });
|
||||||
|
|
||||||
|
expect(storeState.selectedRegionIds).toEqual(['tempo-1']);
|
||||||
|
|
||||||
|
storeState.globalTracks = createDefaultGlobalTracks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shift-clicking a region in a different global lane clears the previous global selection', () => {
|
||||||
|
const globalTracks = createDefaultGlobalTracks();
|
||||||
|
const chordTrack = globalTracks.find(track => track.getType() === GlobalTrackType.Chord);
|
||||||
|
const tempoTrack = globalTracks.find(track => track.getType() === GlobalTrackType.Tempo);
|
||||||
|
chordTrack?.setRegions([
|
||||||
|
new KGChordRegion('chord-1', chordTrack.getId(), chordTrack.getTrackIndex(), 'Am', 0, 4),
|
||||||
|
]);
|
||||||
|
tempoTrack?.setRegions([
|
||||||
|
new KGTempoRegion('tempo-1', tempoTrack.getId(), tempoTrack.getTrackIndex(), 128, 0, 4, 4),
|
||||||
|
new KGTempoRegion('tempo-2', tempoTrack.getId(), tempoTrack.getTrackIndex(), 140, 4, 4, 4),
|
||||||
|
]);
|
||||||
|
storeState.globalTracks = globalTracks;
|
||||||
|
|
||||||
|
render(<MainContent />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
|
||||||
|
fireEvent.click(screen.getByText('Am'));
|
||||||
|
fireEvent.click(screen.getByText('140 BPM'), { shiftKey: true });
|
||||||
|
|
||||||
|
expect(storeState.selectedRegionIds).toEqual(['tempo-2']);
|
||||||
|
|
||||||
|
storeState.globalTracks = createDefaultGlobalTracks();
|
||||||
|
});
|
||||||
|
|
||||||
it('uses split-insert chord command when the playhead is inside an existing chord region', () => {
|
it('uses split-insert chord command when the playhead is inside an existing chord region', () => {
|
||||||
const globalTracks = createDefaultGlobalTracks();
|
const globalTracks = createDefaultGlobalTracks();
|
||||||
const chordTrack = globalTracks.find(track => track.getType() === GlobalTrackType.Chord);
|
const chordTrack = globalTracks.find(track => track.getType() === GlobalTrackType.Chord);
|
||||||
@@ -439,4 +643,46 @@ describe('MainContent', () => {
|
|||||||
|
|
||||||
expect(executeCommandMock).toHaveBeenCalledTimes(1);
|
expect(executeCommandMock).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('creates new marker regions with a one-bar default length', () => {
|
||||||
|
render(<MainContent />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Add Marker global track item' }));
|
||||||
|
|
||||||
|
expect(executeCommandMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect((executeCommandMock.mock.calls[0][0] as { preferredLength?: number }).preferredLength).toBe(
|
||||||
|
storeState.timeSignature.numerator
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps a double-clicked global region editor visible past the sticky left panel', async () => {
|
||||||
|
const globalTracks = createDefaultGlobalTracks();
|
||||||
|
const chordTrack = globalTracks.find(track => track.getType() === GlobalTrackType.Chord);
|
||||||
|
chordTrack?.setRegions([
|
||||||
|
new KGChordRegion('chord-1', chordTrack.getId(), chordTrack.getTrackIndex(), 'Am', 40, 16),
|
||||||
|
]);
|
||||||
|
storeState.globalTracks = globalTracks;
|
||||||
|
|
||||||
|
const requestAnimationFrameSpy = vi.spyOn(window, 'requestAnimationFrame').mockImplementation((callback: FrameRequestCallback) => {
|
||||||
|
callback(0);
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { container } = render(<MainContent />);
|
||||||
|
const mainContent = container.querySelector('.main-content') as HTMLDivElement;
|
||||||
|
Object.defineProperty(mainContent, 'scrollWidth', { configurable: true, value: 4000 });
|
||||||
|
Object.defineProperty(mainContent, 'clientWidth', { configurable: true, value: 900 });
|
||||||
|
mainContent.scrollLeft = 500;
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
|
||||||
|
fireEvent.doubleClick(screen.getByText('Am'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mainContent.scrollLeft).toBe(188);
|
||||||
|
});
|
||||||
|
|
||||||
|
requestAnimationFrameSpy.mockRestore();
|
||||||
|
storeState.globalTracks = createDefaultGlobalTracks();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+273
-32
@@ -70,6 +70,12 @@ const GLOBAL_TRACKS: GlobalTrackDefinition[] = [
|
|||||||
{ id: 'chord', label: 'Chord' },
|
{ id: 'chord', label: 'Chord' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const DEFAULT_REGION_CLICK_OPTIONS: RegionClickOptions = {
|
||||||
|
shiftKey: false,
|
||||||
|
metaKey: false,
|
||||||
|
ctrlKey: false,
|
||||||
|
};
|
||||||
|
|
||||||
const MainContent: React.FC<MainContentProps> = ({
|
const MainContent: React.FC<MainContentProps> = ({
|
||||||
onTrackClick = () => { } // Default to empty function if not provided
|
onTrackClick = () => { } // Default to empty function if not provided
|
||||||
}) => {
|
}) => {
|
||||||
@@ -417,6 +423,57 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
useProjectStore.setState({ mainContentScrollRequest: null });
|
useProjectStore.setState({ mainContentScrollRequest: null });
|
||||||
}, [mainContentScrollRequest, timeSignature]);
|
}, [mainContentScrollRequest, timeSignature]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const editingRegionId = editingGlobalRegionId
|
||||||
|
?? editingTempoRegionId
|
||||||
|
?? editingKeySignatureRegionId
|
||||||
|
?? editingChordRegionId;
|
||||||
|
if (!editingRegionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = mainContentRef.current;
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const editingRegion = findProjectRegionById(editingRegionId);
|
||||||
|
if (!(editingRegion instanceof KGGlobalRegion)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const barWidth = TOOLBAR_CONSTANTS.BASE_BAR_WIDTH * barWidthMultiplier;
|
||||||
|
const leftInset = (parseInt(
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue('--track-info-panel-width'),
|
||||||
|
10
|
||||||
|
) || 200) + 12;
|
||||||
|
const regionStartBeat = editingRegion instanceof KGTempoRegion || editingRegion instanceof KGKeySignatureRegion
|
||||||
|
? editingRegion.getStartBar() * timeSignature.numerator
|
||||||
|
: editingRegion.getStartFromBeat();
|
||||||
|
const regionStartPixel = (regionStartBeat / timeSignature.numerator) * barWidth;
|
||||||
|
const minimumVisiblePixel = container.scrollLeft + leftInset;
|
||||||
|
|
||||||
|
if (regionStartPixel >= minimumVisiblePixel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetScrollLeft = Math.max(0, regionStartPixel - leftInset);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (!mainContentRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mainContentRef.current.scrollLeft = targetScrollLeft;
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
barWidthMultiplier,
|
||||||
|
editingChordRegionId,
|
||||||
|
editingGlobalRegionId,
|
||||||
|
editingKeySignatureRegionId,
|
||||||
|
editingTempoRegionId,
|
||||||
|
findProjectRegionById,
|
||||||
|
timeSignature.numerator,
|
||||||
|
]);
|
||||||
|
|
||||||
// Effect to verify track updates
|
// Effect to verify track updates
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check for pending updates
|
// Check for pending updates
|
||||||
@@ -514,7 +571,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
if (!regionExists) return;
|
if (!regionExists) return;
|
||||||
|
|
||||||
pendingAutoSelectionRegionIdRef.current = null;
|
pendingAutoSelectionRegionIdRef.current = null;
|
||||||
selectRegion(pendingRegionId, { shiftKey: false }, regions);
|
selectRegion(pendingRegionId, DEFAULT_REGION_CLICK_OPTIONS, regions);
|
||||||
}, [regions]);
|
}, [regions]);
|
||||||
|
|
||||||
// Handle track name edit
|
// Handle track name edit
|
||||||
@@ -783,9 +840,137 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isAdditiveSelection = (options: RegionClickOptions) => options.metaKey || options.ctrlKey;
|
||||||
|
|
||||||
|
const getOrderedTrackRegionIds = useCallback((trackId: string) => (
|
||||||
|
regions
|
||||||
|
.filter(region => region.trackId === trackId)
|
||||||
|
.sort((left, right) => {
|
||||||
|
if (left.barNumber !== right.barNumber) {
|
||||||
|
return left.barNumber - right.barNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (left.length !== right.length) {
|
||||||
|
return left.length - right.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return left.id.localeCompare(right.id);
|
||||||
|
})
|
||||||
|
.map(region => region.id)
|
||||||
|
), [regions]);
|
||||||
|
|
||||||
|
const getOrderedGlobalRegionIds = useCallback((regionType: GlobalTrackDefinition['id']) => (
|
||||||
|
globalTracks
|
||||||
|
.flatMap(globalTrack => globalTrack.getRegions())
|
||||||
|
.filter((region): region is KGGlobalRegion => {
|
||||||
|
if (regionType === 'tempo') {
|
||||||
|
return region instanceof KGTempoRegion;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (regionType === 'signature') {
|
||||||
|
return region instanceof KGKeySignatureRegion;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (regionType === 'chord') {
|
||||||
|
return region instanceof KGChordRegion;
|
||||||
|
}
|
||||||
|
|
||||||
|
return region instanceof KGMarkerRegion;
|
||||||
|
})
|
||||||
|
.sort((left, right) => {
|
||||||
|
const leftStart = left instanceof KGTempoRegion || left instanceof KGKeySignatureRegion
|
||||||
|
? left.getStartBar()
|
||||||
|
: Math.round(left.getStartFromBeat() / timeSignature.numerator);
|
||||||
|
const rightStart = right instanceof KGTempoRegion || right instanceof KGKeySignatureRegion
|
||||||
|
? right.getStartBar()
|
||||||
|
: Math.round(right.getStartFromBeat() / timeSignature.numerator);
|
||||||
|
|
||||||
|
if (leftStart !== rightStart) {
|
||||||
|
return leftStart - rightStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
const leftLength = left instanceof KGTempoRegion || left instanceof KGKeySignatureRegion
|
||||||
|
? left.getLengthBars()
|
||||||
|
: left.getLength();
|
||||||
|
const rightLength = right instanceof KGTempoRegion || right instanceof KGKeySignatureRegion
|
||||||
|
? right.getLengthBars()
|
||||||
|
: right.getLength();
|
||||||
|
|
||||||
|
if (leftLength !== rightLength) {
|
||||||
|
return leftLength - rightLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
return left.getId().localeCompare(right.getId());
|
||||||
|
})
|
||||||
|
.map(region => region.getId())
|
||||||
|
), [globalTracks, timeSignature.numerator]);
|
||||||
|
|
||||||
|
const appendPrimarySelection = (orderedIds: string[], primaryRegionId: string) => {
|
||||||
|
const deduped = orderedIds.filter(id => id !== primaryRegionId);
|
||||||
|
return [...deduped, primaryRegionId];
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildSameTrackRangeSelection = (
|
||||||
|
existingRegularSelectionIds: string[],
|
||||||
|
anchorRegionId: string,
|
||||||
|
targetRegionId: string,
|
||||||
|
orderedTrackRegionIds: string[]
|
||||||
|
) => {
|
||||||
|
const anchorIndex = orderedTrackRegionIds.indexOf(anchorRegionId);
|
||||||
|
const targetIndex = orderedTrackRegionIds.indexOf(targetRegionId);
|
||||||
|
if (anchorIndex === -1 || targetIndex === -1) {
|
||||||
|
return [targetRegionId];
|
||||||
|
}
|
||||||
|
|
||||||
|
const [startIndex, endIndex] = anchorIndex <= targetIndex
|
||||||
|
? [anchorIndex, targetIndex]
|
||||||
|
: [targetIndex, anchorIndex];
|
||||||
|
const rangeIds = orderedTrackRegionIds.slice(startIndex, endIndex + 1);
|
||||||
|
const rangeIdSet = new Set(rangeIds);
|
||||||
|
const preservedOtherTrackIds = existingRegularSelectionIds.filter(selectedId => !rangeIdSet.has(selectedId));
|
||||||
|
return appendPrimarySelection([...preservedOtherTrackIds, ...rangeIds], targetRegionId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildSameLaneRangeSelection = (
|
||||||
|
existingGlobalSelectionIds: string[],
|
||||||
|
anchorRegionId: string,
|
||||||
|
targetRegionId: string,
|
||||||
|
orderedLaneRegionIds: string[]
|
||||||
|
) => {
|
||||||
|
const anchorIndex = orderedLaneRegionIds.indexOf(anchorRegionId);
|
||||||
|
const targetIndex = orderedLaneRegionIds.indexOf(targetRegionId);
|
||||||
|
if (anchorIndex === -1 || targetIndex === -1) {
|
||||||
|
return [targetRegionId];
|
||||||
|
}
|
||||||
|
|
||||||
|
const [startIndex, endIndex] = anchorIndex <= targetIndex
|
||||||
|
? [anchorIndex, targetIndex]
|
||||||
|
: [targetIndex, anchorIndex];
|
||||||
|
const rangeIds = orderedLaneRegionIds.slice(startIndex, endIndex + 1);
|
||||||
|
const laneIdSet = new Set(orderedLaneRegionIds);
|
||||||
|
const preservedOtherLaneIds = existingGlobalSelectionIds.filter(selectedId => !laneIdSet.has(selectedId));
|
||||||
|
return appendPrimarySelection([...preservedOtherLaneIds, ...rangeIds], targetRegionId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGlobalRegionLaneType = (region: KGGlobalRegion): GlobalTrackDefinition['id'] => {
|
||||||
|
if (region instanceof KGTempoRegion) {
|
||||||
|
return 'tempo';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (region instanceof KGKeySignatureRegion) {
|
||||||
|
return 'signature';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (region instanceof KGChordRegion) {
|
||||||
|
return 'chord';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'marker';
|
||||||
|
};
|
||||||
|
|
||||||
const selectRegion = (
|
const selectRegion = (
|
||||||
regionId: string,
|
regionId: string,
|
||||||
options: RegionClickOptions = { shiftKey: false },
|
options: RegionClickOptions = DEFAULT_REGION_CLICK_OPTIONS,
|
||||||
regionsToSearch?: RegionUI[]
|
regionsToSearch?: RegionUI[]
|
||||||
) => {
|
) => {
|
||||||
const regionsToUse = regionsToSearch || regions;
|
const regionsToUse = regionsToSearch || regions;
|
||||||
@@ -814,35 +999,91 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const regularSelectedRegionIds = selectedRegionIds.filter(selectedId => !isGlobalRegionId(selectedId));
|
const regularSelectedRegionIds = selectedRegionIds.filter(selectedId => !isGlobalRegionId(selectedId));
|
||||||
const orderedSelection = options.shiftKey
|
const additiveSelection = isAdditiveSelection(options);
|
||||||
? (selectedRegionIds.includes(regionId)
|
let orderedSelection: string[];
|
||||||
|
|
||||||
|
if (additiveSelection) {
|
||||||
|
orderedSelection = regularSelectedRegionIds.includes(regionId)
|
||||||
? regularSelectedRegionIds.filter(id => id !== regionId)
|
? regularSelectedRegionIds.filter(id => id !== regionId)
|
||||||
: [...regularSelectedRegionIds, regionId])
|
: appendPrimarySelection([...regularSelectedRegionIds, regionId], regionId);
|
||||||
: [regionId];
|
} else if (options.shiftKey) {
|
||||||
|
const sameTrackSelectedIds = regularSelectedRegionIds.filter(selectedId => {
|
||||||
|
const selectedRegion = regionsToUse.find(candidate => candidate.id === selectedId);
|
||||||
|
return selectedRegion?.trackId === region.trackId;
|
||||||
|
});
|
||||||
|
const anchorRegionId = [...sameTrackSelectedIds].reverse().find(selectedId => selectedId !== regionId) ?? null;
|
||||||
|
|
||||||
|
if (!anchorRegionId) {
|
||||||
|
orderedSelection = [regionId];
|
||||||
|
} else {
|
||||||
|
orderedSelection = buildSameTrackRangeSelection(
|
||||||
|
regularSelectedRegionIds,
|
||||||
|
anchorRegionId,
|
||||||
|
regionId,
|
||||||
|
getOrderedTrackRegionIds(region.trackId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
orderedSelection = [regionId];
|
||||||
|
}
|
||||||
|
|
||||||
applyRegionSelection(orderedSelection);
|
applyRegionSelection(orderedSelection);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectGlobalRegion = (regionId: string, options: RegionClickOptions = { shiftKey: false }) => {
|
const selectGlobalRegion = (regionId: string, options: RegionClickOptions = DEFAULT_REGION_CLICK_OPTIONS) => {
|
||||||
const globalSelectedRegionIds = selectedRegionIds.filter(selectedId => isGlobalRegionId(selectedId));
|
const globalSelectedRegionIds = selectedRegionIds.filter(selectedId => isGlobalRegionId(selectedId));
|
||||||
const orderedSelection = options.shiftKey
|
const globalRegion = findProjectRegionById(regionId);
|
||||||
? (globalSelectedRegionIds.includes(regionId)
|
if (!(globalRegion instanceof KGGlobalRegion)) {
|
||||||
? globalSelectedRegionIds.filter(id => id !== regionId)
|
return;
|
||||||
: [...globalSelectedRegionIds, regionId])
|
}
|
||||||
: [regionId];
|
|
||||||
|
const targetLaneType = getGlobalRegionLaneType(globalRegion);
|
||||||
|
const sameLaneSelectedRegionIds = globalSelectedRegionIds.filter(selectedId => {
|
||||||
|
const selectedRegion = findProjectRegionById(selectedId);
|
||||||
|
return selectedRegion instanceof KGGlobalRegion && getGlobalRegionLaneType(selectedRegion) === targetLaneType;
|
||||||
|
});
|
||||||
|
|
||||||
|
const additiveSelection = isAdditiveSelection(options);
|
||||||
|
let orderedSelection: string[];
|
||||||
|
|
||||||
|
if (additiveSelection) {
|
||||||
|
orderedSelection = sameLaneSelectedRegionIds.includes(regionId)
|
||||||
|
? sameLaneSelectedRegionIds.filter(id => id !== regionId)
|
||||||
|
: appendPrimarySelection([...sameLaneSelectedRegionIds, regionId], regionId);
|
||||||
|
} else if (options.shiftKey) {
|
||||||
|
const orderedLaneRegionIds = getOrderedGlobalRegionIds(targetLaneType);
|
||||||
|
|
||||||
|
const laneRegionIdSet = new Set(orderedLaneRegionIds);
|
||||||
|
const sameLaneSelectedIds = sameLaneSelectedRegionIds.filter(selectedId => laneRegionIdSet.has(selectedId));
|
||||||
|
const anchorRegionId = [...sameLaneSelectedIds].reverse().find(selectedId => selectedId !== regionId) ?? null;
|
||||||
|
|
||||||
|
if (!anchorRegionId) {
|
||||||
|
orderedSelection = [regionId];
|
||||||
|
} else {
|
||||||
|
orderedSelection = buildSameLaneRangeSelection(
|
||||||
|
sameLaneSelectedRegionIds,
|
||||||
|
anchorRegionId,
|
||||||
|
regionId,
|
||||||
|
orderedLaneRegionIds
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
orderedSelection = [regionId];
|
||||||
|
}
|
||||||
|
|
||||||
applyRegionSelection(orderedSelection);
|
applyRegionSelection(orderedSelection);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRegionLassoSelection = (regionIds: string[], options: RegionClickOptions = { shiftKey: false }) => {
|
const handleRegionLassoSelection = (regionIds: string[], options: RegionClickOptions = DEFAULT_REGION_CLICK_OPTIONS) => {
|
||||||
const orderedRegionIds = regionIds.filter(regionId => regions.some(region => region.id === regionId));
|
const orderedRegionIds = regionIds.filter(regionId => regions.some(region => region.id === regionId));
|
||||||
const regularSelectedRegionIds = selectedRegionIds.filter(selectedId => !isGlobalRegionId(selectedId));
|
const regularSelectedRegionIds = selectedRegionIds.filter(selectedId => !isGlobalRegionId(selectedId));
|
||||||
const orderedSelection = options.shiftKey
|
const additiveSelection = isAdditiveSelection(options);
|
||||||
|
const orderedSelection = additiveSelection
|
||||||
? orderedRegionIds.reduce<string[]>((nextSelection, regionId) => {
|
? orderedRegionIds.reduce<string[]>((nextSelection, regionId) => {
|
||||||
if (nextSelection.includes(regionId)) {
|
if (nextSelection.includes(regionId)) {
|
||||||
return nextSelection.filter(id => id !== regionId);
|
return nextSelection.filter(id => id !== regionId);
|
||||||
}
|
}
|
||||||
return [...nextSelection, regionId];
|
return appendPrimarySelection([...nextSelection, regionId], regionId);
|
||||||
}, [...regularSelectedRegionIds])
|
}, [...regularSelectedRegionIds])
|
||||||
: orderedRegionIds;
|
: orderedRegionIds;
|
||||||
|
|
||||||
@@ -859,7 +1100,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRegionLassoSelection([], { shiftKey: false });
|
handleRegionLassoSelection([], DEFAULT_REGION_CLICK_OPTIONS);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRegionLassoCommit = () => {
|
const handleRegionLassoCommit = () => {
|
||||||
@@ -867,7 +1108,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Handle region single click: selection only (no piano roll opening)
|
// Handle region single click: selection only (no piano roll opening)
|
||||||
const handleRegionClick = (regionId: string, options: RegionClickOptions = { shiftKey: false }) => {
|
const handleRegionClick = (regionId: string, options: RegionClickOptions = DEFAULT_REGION_CLICK_OPTIONS) => {
|
||||||
if (DEBUG_MODE.MAIN_CONTENT) {
|
if (DEBUG_MODE.MAIN_CONTENT) {
|
||||||
console.log(`Region clicked in MainContent (selection only): ${regionId}`);
|
console.log(`Region clicked in MainContent (selection only): ${regionId}`);
|
||||||
}
|
}
|
||||||
@@ -900,7 +1141,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reuse selection logic
|
// Reuse selection logic
|
||||||
handleRegionClick(regionId, { shiftKey: false });
|
handleRegionClick(regionId, DEFAULT_REGION_CLICK_OPTIONS);
|
||||||
|
|
||||||
// Activate and show piano roll in midi-edit mode
|
// Activate and show piano roll in midi-edit mode
|
||||||
openMidiPianoRoll(regionId);
|
openMidiPianoRoll(regionId);
|
||||||
@@ -908,7 +1149,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
|
|
||||||
// Handle spectrogram viewer open
|
// Handle spectrogram viewer open
|
||||||
const handleOpenSpectrogram = (regionId: string) => {
|
const handleOpenSpectrogram = (regionId: string) => {
|
||||||
handleRegionClick(regionId, { shiftKey: false });
|
handleRegionClick(regionId, DEFAULT_REGION_CLICK_OPTIONS);
|
||||||
openSpectrogramViewer(regionId);
|
openSpectrogramViewer(regionId);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1002,7 +1243,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
const normalizedStartBeat = Math.max(0, Math.round(requestedStartBeat));
|
const normalizedStartBeat = Math.max(0, Math.round(requestedStartBeat));
|
||||||
const occupiedRegion = markerRegions.find(region => region.getStartFromBeat() === normalizedStartBeat);
|
const occupiedRegion = markerRegions.find(region => region.getStartFromBeat() === normalizedStartBeat);
|
||||||
if (occupiedRegion) {
|
if (occupiedRegion) {
|
||||||
selectGlobalRegion(occupiedRegion.getId(), { shiftKey: false });
|
selectGlobalRegion(occupiedRegion.getId(), DEFAULT_REGION_CLICK_OPTIONS);
|
||||||
beginEditingGlobalRegion(occupiedRegion.getId());
|
beginEditingGlobalRegion(occupiedRegion.getId());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1010,7 +1251,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
try {
|
try {
|
||||||
const command = new CreateGlobalMarkerRegionCommand(
|
const command = new CreateGlobalMarkerRegionCommand(
|
||||||
normalizedStartBeat,
|
normalizedStartBeat,
|
||||||
8 * timeSignature.numerator,
|
timeSignature.numerator,
|
||||||
DEFAULT_MARKER_REGION_NAME
|
DEFAULT_MARKER_REGION_NAME
|
||||||
);
|
);
|
||||||
KGCore.instance().executeCommand(command);
|
KGCore.instance().executeCommand(command);
|
||||||
@@ -1021,7 +1262,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
selectGlobalRegion(createdRegion.getId(), { shiftKey: false });
|
selectGlobalRegion(createdRegion.getId(), DEFAULT_REGION_CLICK_OPTIONS);
|
||||||
setEditingGlobalRegionId(createdRegion.getId());
|
setEditingGlobalRegionId(createdRegion.getId());
|
||||||
setEditingGlobalRegionText(createdRegion.getName());
|
setEditingGlobalRegionText(createdRegion.getName());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1057,7 +1298,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
const normalizedStartBar = Math.max(0, Math.min(requestedStartBar, maxBars - 1));
|
const normalizedStartBar = Math.max(0, Math.min(requestedStartBar, maxBars - 1));
|
||||||
const existingRegionAtStart = signatureRegions.find(region => region.getStartBar() === normalizedStartBar);
|
const existingRegionAtStart = signatureRegions.find(region => region.getStartBar() === normalizedStartBar);
|
||||||
if (existingRegionAtStart) {
|
if (existingRegionAtStart) {
|
||||||
selectGlobalRegion(existingRegionAtStart.getId(), { shiftKey: false });
|
selectGlobalRegion(existingRegionAtStart.getId(), DEFAULT_REGION_CLICK_OPTIONS);
|
||||||
beginEditingKeySignatureRegion(existingRegionAtStart.getId());
|
beginEditingKeySignatureRegion(existingRegionAtStart.getId());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1072,7 +1313,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
selectGlobalRegion(createdRegion.getId(), { shiftKey: false });
|
selectGlobalRegion(createdRegion.getId(), DEFAULT_REGION_CLICK_OPTIONS);
|
||||||
setEditingKeySignatureRegionId(createdRegion.getId());
|
setEditingKeySignatureRegionId(createdRegion.getId());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating key signature region:', error);
|
console.error('Error creating key signature region:', error);
|
||||||
@@ -1142,7 +1383,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
const normalizedStartBar = Math.max(0, Math.min(requestedStartBar, maxBars - 1));
|
const normalizedStartBar = Math.max(0, Math.min(requestedStartBar, maxBars - 1));
|
||||||
const existingRegionAtStart = tempoRegions.find(region => region.getStartBar() === normalizedStartBar);
|
const existingRegionAtStart = tempoRegions.find(region => region.getStartBar() === normalizedStartBar);
|
||||||
if (existingRegionAtStart) {
|
if (existingRegionAtStart) {
|
||||||
selectGlobalRegion(existingRegionAtStart.getId(), { shiftKey: false });
|
selectGlobalRegion(existingRegionAtStart.getId(), DEFAULT_REGION_CLICK_OPTIONS);
|
||||||
beginEditingTempoRegion(existingRegionAtStart.getId());
|
beginEditingTempoRegion(existingRegionAtStart.getId());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1158,7 +1399,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
selectGlobalRegion(createdRegion.getId(), { shiftKey: false });
|
selectGlobalRegion(createdRegion.getId(), DEFAULT_REGION_CLICK_OPTIONS);
|
||||||
setEditingTempoRegionId(createdRegion.getId());
|
setEditingTempoRegionId(createdRegion.getId());
|
||||||
setEditingTempoText(createdRegion.getBpm().toString());
|
setEditingTempoText(createdRegion.getBpm().toString());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1189,7 +1430,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
&& normalizedStartBeat < region.getStartFromBeat() + region.getLength()
|
&& normalizedStartBeat < region.getStartFromBeat() + region.getLength()
|
||||||
));
|
));
|
||||||
if (occupiedRegion) {
|
if (occupiedRegion) {
|
||||||
selectGlobalRegion(occupiedRegion.getId(), { shiftKey: false });
|
selectGlobalRegion(occupiedRegion.getId(), DEFAULT_REGION_CLICK_OPTIONS);
|
||||||
beginEditingChordRegion(occupiedRegion.getId());
|
beginEditingChordRegion(occupiedRegion.getId());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1204,7 +1445,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
selectGlobalRegion(createdRegion.getId(), { shiftKey: false });
|
selectGlobalRegion(createdRegion.getId(), DEFAULT_REGION_CLICK_OPTIONS);
|
||||||
setEditingChordRegionId(createdRegion.getId());
|
setEditingChordRegionId(createdRegion.getId());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating chord region:', error);
|
console.error('Error creating chord region:', error);
|
||||||
@@ -1229,19 +1470,19 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
const createdRegion = command.getCreatedRegion();
|
const createdRegion = command.getCreatedRegion();
|
||||||
if (!createdRegion) {
|
if (!createdRegion) {
|
||||||
if (occupiedRegion) {
|
if (occupiedRegion) {
|
||||||
selectGlobalRegion(occupiedRegion.getId(), { shiftKey: false });
|
selectGlobalRegion(occupiedRegion.getId(), DEFAULT_REGION_CLICK_OPTIONS);
|
||||||
beginEditingChordRegion(occupiedRegion.getId());
|
beginEditingChordRegion(occupiedRegion.getId());
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
selectGlobalRegion(createdRegion.getId(), { shiftKey: false });
|
selectGlobalRegion(createdRegion.getId(), DEFAULT_REGION_CLICK_OPTIONS);
|
||||||
setEditingChordRegionId(createdRegion.getId());
|
setEditingChordRegionId(createdRegion.getId());
|
||||||
return createdRegion;
|
return createdRegion;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating chord region at exact beat:', error);
|
console.error('Error creating chord region at exact beat:', error);
|
||||||
if (occupiedRegion) {
|
if (occupiedRegion) {
|
||||||
selectGlobalRegion(occupiedRegion.getId(), { shiftKey: false });
|
selectGlobalRegion(occupiedRegion.getId(), DEFAULT_REGION_CLICK_OPTIONS);
|
||||||
beginEditingChordRegion(occupiedRegion.getId());
|
beginEditingChordRegion(occupiedRegion.getId());
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -1278,7 +1519,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
.find(region => region.getStartFromBeat() < currentStartBeat);
|
.find(region => region.getStartFromBeat() < currentStartBeat);
|
||||||
|
|
||||||
if (targetRegion) {
|
if (targetRegion) {
|
||||||
selectGlobalRegion(targetRegion.getId(), { shiftKey: false });
|
selectGlobalRegion(targetRegion.getId(), DEFAULT_REGION_CLICK_OPTIONS);
|
||||||
setEditingChordRegionId(targetRegion.getId());
|
setEditingChordRegionId(targetRegion.getId());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ describe('GlobalChordLane', () => {
|
|||||||
onResizeRegion={vi.fn()}
|
onResizeRegion={vi.fn()}
|
||||||
onChangeChord={vi.fn()}
|
onChangeChord={vi.fn()}
|
||||||
onOpenPopup={onOpenPopup}
|
onOpenPopup={onOpenPopup}
|
||||||
|
onTabNavigate={vi.fn()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -54,6 +55,7 @@ describe('GlobalChordLane', () => {
|
|||||||
onResizeRegion={vi.fn()}
|
onResizeRegion={vi.fn()}
|
||||||
onChangeChord={vi.fn()}
|
onChangeChord={vi.fn()}
|
||||||
onOpenPopup={vi.fn()}
|
onOpenPopup={vi.fn()}
|
||||||
|
onTabNavigate={vi.fn()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -95,6 +97,7 @@ describe('GlobalChordLane', () => {
|
|||||||
onResizeRegion={vi.fn()}
|
onResizeRegion={vi.fn()}
|
||||||
onChangeChord={vi.fn()}
|
onChangeChord={vi.fn()}
|
||||||
onOpenPopup={vi.fn()}
|
onOpenPopup={vi.fn()}
|
||||||
|
onTabNavigate={vi.fn()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -117,4 +120,31 @@ describe('GlobalChordLane', () => {
|
|||||||
|
|
||||||
expect(onMoveRegion).toHaveBeenCalledWith('chord-1', 2);
|
expect(onMoveRegion).toHaveBeenCalledWith('chord-1', 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('passes modifier state through region selection clicks', () => {
|
||||||
|
const onSelectRegion = vi.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<GlobalChordLane
|
||||||
|
chordRegions={[baseRegion]}
|
||||||
|
maxBars={8}
|
||||||
|
barWidthMultiplier={1}
|
||||||
|
timeSignature={{ numerator: 4, denominator: 4 }}
|
||||||
|
selectedRegionIds={[]}
|
||||||
|
popupRegionId={null}
|
||||||
|
onClosePopup={vi.fn()}
|
||||||
|
onSelectRegion={onSelectRegion}
|
||||||
|
onCreateAtBeat={vi.fn()}
|
||||||
|
onMoveRegion={vi.fn()}
|
||||||
|
onResizeRegion={vi.fn()}
|
||||||
|
onChangeChord={vi.fn()}
|
||||||
|
onOpenPopup={vi.fn()}
|
||||||
|
onTabNavigate={vi.fn()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('Cmaj7'), { metaKey: true, shiftKey: true });
|
||||||
|
|
||||||
|
expect(onSelectRegion).toHaveBeenCalledWith('chord-1', { shiftKey: true, metaKey: true, ctrlKey: false });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ type ResizeEdge = 'start' | 'end' | null;
|
|||||||
|
|
||||||
const REGION_EDGE_HITBOX_PX = 8;
|
const REGION_EDGE_HITBOX_PX = 8;
|
||||||
const DRAG_THRESHOLD_PX = 4;
|
const DRAG_THRESHOLD_PX = 4;
|
||||||
|
const getRegionClickOptions = (event: Pick<MouseEvent | React.MouseEvent, 'shiftKey' | 'metaKey' | 'ctrlKey'>): RegionClickOptions => ({
|
||||||
|
shiftKey: event.shiftKey,
|
||||||
|
metaKey: event.metaKey,
|
||||||
|
ctrlKey: event.ctrlKey,
|
||||||
|
});
|
||||||
|
|
||||||
const GlobalChordLane: React.FC<GlobalChordLaneProps> = ({
|
const GlobalChordLane: React.FC<GlobalChordLaneProps> = ({
|
||||||
chordRegions,
|
chordRegions,
|
||||||
@@ -48,6 +53,7 @@ const GlobalChordLane: React.FC<GlobalChordLaneProps> = ({
|
|||||||
const [previewBeats, setPreviewBeats] = useState<Record<string, { startBeat: number; length: number }>>({});
|
const [previewBeats, setPreviewBeats] = useState<Record<string, { startBeat: number; length: number }>>({});
|
||||||
const [hoverEdges, setHoverEdges] = useState<Record<string, ResizeEdge>>({});
|
const [hoverEdges, setHoverEdges] = useState<Record<string, ResizeEdge>>({});
|
||||||
const [isModifierPressed, setIsModifierPressed] = useState(false);
|
const [isModifierPressed, setIsModifierPressed] = useState(false);
|
||||||
|
const suppressClickSelectionRef = useRef(false);
|
||||||
const interactionRef = useRef<{
|
const interactionRef = useRef<{
|
||||||
mode: 'drag' | 'resize' | null;
|
mode: 'drag' | 'resize' | null;
|
||||||
regionId: string;
|
regionId: string;
|
||||||
@@ -193,7 +199,8 @@ const GlobalChordLane: React.FC<GlobalChordLaneProps> = ({
|
|||||||
|
|
||||||
if (!interaction.moved) {
|
if (!interaction.moved) {
|
||||||
setPreviewBeats({});
|
setPreviewBeats({});
|
||||||
onSelectRegion(interaction.regionId, { shiftKey: event.shiftKey });
|
suppressClickSelectionRef.current = true;
|
||||||
|
onSelectRegion(interaction.regionId, getRegionClickOptions(event));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,7 +290,7 @@ const GlobalChordLane: React.FC<GlobalChordLaneProps> = ({
|
|||||||
onDoubleClick={(event) => {
|
onDoubleClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
onSelectRegion(region.getId(), { shiftKey: false });
|
onSelectRegion(region.getId(), { shiftKey: false, metaKey: false, ctrlKey: false });
|
||||||
onOpenPopup(region.getId());
|
onOpenPopup(region.getId());
|
||||||
}}
|
}}
|
||||||
onMouseMove={(event) => {
|
onMouseMove={(event) => {
|
||||||
@@ -334,7 +341,11 @@ const GlobalChordLane: React.FC<GlobalChordLaneProps> = ({
|
|||||||
}}
|
}}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
onSelectRegion(region.getId(), { shiftKey: event.shiftKey });
|
if (suppressClickSelectionRef.current) {
|
||||||
|
suppressClickSelectionRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onSelectRegion(region.getId(), getRegionClickOptions(event));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="global-marker-label">{region.getSymbol()}</span>
|
<span className="global-marker-label">{region.getSymbol()}</span>
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ type ResizeEdge = 'start' | 'end' | null;
|
|||||||
|
|
||||||
const REGION_EDGE_HITBOX_PX = 8;
|
const REGION_EDGE_HITBOX_PX = 8;
|
||||||
const DRAG_THRESHOLD_PX = 4;
|
const DRAG_THRESHOLD_PX = 4;
|
||||||
|
const getRegionClickOptions = (event: Pick<MouseEvent | React.MouseEvent, 'shiftKey' | 'metaKey' | 'ctrlKey'>): RegionClickOptions => ({
|
||||||
|
shiftKey: event.shiftKey,
|
||||||
|
metaKey: event.metaKey,
|
||||||
|
ctrlKey: event.ctrlKey,
|
||||||
|
});
|
||||||
|
|
||||||
const GlobalKeySignatureLane: React.FC<GlobalKeySignatureLaneProps> = ({
|
const GlobalKeySignatureLane: React.FC<GlobalKeySignatureLaneProps> = ({
|
||||||
signatureRegions,
|
signatureRegions,
|
||||||
@@ -45,6 +50,7 @@ const GlobalKeySignatureLane: React.FC<GlobalKeySignatureLaneProps> = ({
|
|||||||
const [previewBars, setPreviewBars] = useState<Record<string, { startBar: number; lengthBars: number }>>({});
|
const [previewBars, setPreviewBars] = useState<Record<string, { startBar: number; lengthBars: number }>>({});
|
||||||
const [hoverEdges, setHoverEdges] = useState<Record<string, ResizeEdge>>({});
|
const [hoverEdges, setHoverEdges] = useState<Record<string, ResizeEdge>>({});
|
||||||
const [isModifierPressed, setIsModifierPressed] = useState(false);
|
const [isModifierPressed, setIsModifierPressed] = useState(false);
|
||||||
|
const suppressClickSelectionRef = useRef(false);
|
||||||
const interactionRef = useRef<{
|
const interactionRef = useRef<{
|
||||||
mode: 'resize' | null;
|
mode: 'resize' | null;
|
||||||
regionId: string;
|
regionId: string;
|
||||||
@@ -223,7 +229,8 @@ const GlobalKeySignatureLane: React.FC<GlobalKeySignatureLaneProps> = ({
|
|||||||
setPreviewBars({});
|
setPreviewBars({});
|
||||||
|
|
||||||
if (!shouldResize) {
|
if (!shouldResize) {
|
||||||
onSelectRegion(interaction.regionId, { shiftKey: event.shiftKey });
|
suppressClickSelectionRef.current = true;
|
||||||
|
onSelectRegion(interaction.regionId, getRegionClickOptions(event));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,7 +333,11 @@ const GlobalKeySignatureLane: React.FC<GlobalKeySignatureLaneProps> = ({
|
|||||||
}}
|
}}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
onSelectRegion(region.getId(), { shiftKey: event.shiftKey });
|
if (suppressClickSelectionRef.current) {
|
||||||
|
suppressClickSelectionRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onSelectRegion(region.getId(), getRegionClickOptions(event));
|
||||||
}}
|
}}
|
||||||
onDoubleClick={(event) => {
|
onDoubleClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ type ResizeEdge = 'start' | 'end' | null;
|
|||||||
|
|
||||||
const REGION_EDGE_HITBOX_PX = 8;
|
const REGION_EDGE_HITBOX_PX = 8;
|
||||||
const DRAG_THRESHOLD_PX = 4;
|
const DRAG_THRESHOLD_PX = 4;
|
||||||
|
const getRegionClickOptions = (event: Pick<MouseEvent | React.MouseEvent, 'shiftKey' | 'metaKey' | 'ctrlKey'>): RegionClickOptions => ({
|
||||||
|
shiftKey: event.shiftKey,
|
||||||
|
metaKey: event.metaKey,
|
||||||
|
ctrlKey: event.ctrlKey,
|
||||||
|
});
|
||||||
|
|
||||||
const GlobalMarkerLane: React.FC<GlobalMarkerLaneProps> = ({
|
const GlobalMarkerLane: React.FC<GlobalMarkerLaneProps> = ({
|
||||||
markerRegions,
|
markerRegions,
|
||||||
@@ -186,7 +191,7 @@ const GlobalMarkerLane: React.FC<GlobalMarkerLaneProps> = ({
|
|||||||
|
|
||||||
if (!interaction.moved) {
|
if (!interaction.moved) {
|
||||||
setPreviewBeats({});
|
setPreviewBeats({});
|
||||||
onSelectRegion(interaction.regionId, { shiftKey: event.shiftKey });
|
onSelectRegion(interaction.regionId, getRegionClickOptions(event));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,7 +268,7 @@ const GlobalMarkerLane: React.FC<GlobalMarkerLaneProps> = ({
|
|||||||
}}
|
}}
|
||||||
onDoubleClick={(event) => {
|
onDoubleClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
onSelectRegion(region.getId(), { shiftKey: false });
|
onSelectRegion(region.getId(), { shiftKey: false, metaKey: false, ctrlKey: false });
|
||||||
onBeginEdit(region.getId());
|
onBeginEdit(region.getId());
|
||||||
}}
|
}}
|
||||||
onMouseMove={(event) => {
|
onMouseMove={(event) => {
|
||||||
|
|||||||
@@ -120,4 +120,30 @@ describe('GlobalTempoLane', () => {
|
|||||||
|
|
||||||
expect(container.querySelector('.global-marker-region.global-tempo-region')).toBeInTheDocument();
|
expect(container.querySelector('.global-marker-region.global-tempo-region')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('passes modifier state through region selection clicks', () => {
|
||||||
|
const onSelectRegion = vi.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<GlobalTempoLane
|
||||||
|
tempoRegions={[baseRegion]}
|
||||||
|
maxBars={8}
|
||||||
|
barWidthMultiplier={1}
|
||||||
|
selectedRegionIds={[]}
|
||||||
|
editingRegionId={null}
|
||||||
|
editingText=""
|
||||||
|
onEditingTextChange={vi.fn()}
|
||||||
|
onCommitEdit={vi.fn()}
|
||||||
|
onCancelEdit={vi.fn()}
|
||||||
|
onBeginEdit={vi.fn()}
|
||||||
|
onSelectRegion={onSelectRegion}
|
||||||
|
onCreateAtBar={vi.fn()}
|
||||||
|
onResizeRegion={vi.fn()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByText('128 BPM'), { ctrlKey: true });
|
||||||
|
|
||||||
|
expect(onSelectRegion).toHaveBeenCalledWith('tempo-1', { shiftKey: false, metaKey: false, ctrlKey: true });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ type ResizeEdge = 'start' | 'end' | null;
|
|||||||
|
|
||||||
const REGION_EDGE_HITBOX_PX = 8;
|
const REGION_EDGE_HITBOX_PX = 8;
|
||||||
const DRAG_THRESHOLD_PX = 4;
|
const DRAG_THRESHOLD_PX = 4;
|
||||||
|
const getRegionClickOptions = (event: Pick<MouseEvent | React.MouseEvent, 'shiftKey' | 'metaKey' | 'ctrlKey'>): RegionClickOptions => ({
|
||||||
|
shiftKey: event.shiftKey,
|
||||||
|
metaKey: event.metaKey,
|
||||||
|
ctrlKey: event.ctrlKey,
|
||||||
|
});
|
||||||
|
|
||||||
const GlobalTempoLane: React.FC<GlobalTempoLaneProps> = ({
|
const GlobalTempoLane: React.FC<GlobalTempoLaneProps> = ({
|
||||||
tempoRegions,
|
tempoRegions,
|
||||||
@@ -44,6 +49,7 @@ const GlobalTempoLane: React.FC<GlobalTempoLaneProps> = ({
|
|||||||
const [previewBars, setPreviewBars] = useState<Record<string, { startBar: number; lengthBars: number }>>({});
|
const [previewBars, setPreviewBars] = useState<Record<string, { startBar: number; lengthBars: number }>>({});
|
||||||
const [hoverEdges, setHoverEdges] = useState<Record<string, ResizeEdge>>({});
|
const [hoverEdges, setHoverEdges] = useState<Record<string, ResizeEdge>>({});
|
||||||
const [isModifierPressed, setIsModifierPressed] = useState(false);
|
const [isModifierPressed, setIsModifierPressed] = useState(false);
|
||||||
|
const suppressClickSelectionRef = useRef(false);
|
||||||
const interactionRef = useRef<{
|
const interactionRef = useRef<{
|
||||||
mode: 'resize' | null;
|
mode: 'resize' | null;
|
||||||
regionId: string;
|
regionId: string;
|
||||||
@@ -222,7 +228,8 @@ const GlobalTempoLane: React.FC<GlobalTempoLaneProps> = ({
|
|||||||
setPreviewBars({});
|
setPreviewBars({});
|
||||||
|
|
||||||
if (!shouldResize) {
|
if (!shouldResize) {
|
||||||
onSelectRegion(interaction.regionId, { shiftKey: event.shiftKey });
|
suppressClickSelectionRef.current = true;
|
||||||
|
onSelectRegion(interaction.regionId, getRegionClickOptions(event));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,12 +335,16 @@ const GlobalTempoLane: React.FC<GlobalTempoLaneProps> = ({
|
|||||||
}}
|
}}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
onSelectRegion(region.getId(), { shiftKey: event.shiftKey });
|
if (suppressClickSelectionRef.current) {
|
||||||
|
suppressClickSelectionRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onSelectRegion(region.getId(), getRegionClickOptions(event));
|
||||||
}}
|
}}
|
||||||
onDoubleClick={(event) => {
|
onDoubleClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
onSelectRegion(region.getId(), { shiftKey: false });
|
onSelectRegion(region.getId(), { shiftKey: false, metaKey: false, ctrlKey: false });
|
||||||
onBeginEdit(region.getId());
|
onBeginEdit(region.getId());
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export interface RegionPreviewContentStyle {
|
|||||||
|
|
||||||
export interface RegionClickOptions {
|
export interface RegionClickOptions {
|
||||||
shiftKey: boolean;
|
shiftKey: boolean;
|
||||||
|
metaKey: boolean;
|
||||||
|
ctrlKey: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define resize action types
|
// Define resize action types
|
||||||
|
|||||||
@@ -15,10 +15,6 @@
|
|||||||
will-change: transform;
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-region:hover {
|
|
||||||
box-shadow: 0 0 0 1px #7a9bba;
|
|
||||||
}
|
|
||||||
|
|
||||||
.track-region.selected {
|
.track-region.selected {
|
||||||
/* box-shadow: 0 0 0 2px #ffffff, 0 0 8px rgba(255, 255, 255, 0.3); */ /* let's only apply a border */
|
/* box-shadow: 0 0 0 2px #ffffff, 0 0 8px rgba(255, 255, 255, 0.3); */ /* let's only apply a border */
|
||||||
border-color: #ffffff;
|
border-color: #ffffff;
|
||||||
@@ -99,10 +95,6 @@
|
|||||||
border-color: #4a8b5a;
|
border-color: #4a8b5a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-region.audio-region:hover {
|
|
||||||
box-shadow: 0 0 0 1px #6aab7a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.track-region.audio-region.selected {
|
.track-region.audio-region.selected {
|
||||||
border-color: #ffffff;
|
border-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ describe('RegionItem', () => {
|
|||||||
fireEvent.mouseMove(document, { clientX: 102, clientY: 102 });
|
fireEvent.mouseMove(document, { clientX: 102, clientY: 102 });
|
||||||
fireEvent.mouseUp(document, { clientX: 102, clientY: 102 });
|
fireEvent.mouseUp(document, { clientX: 102, clientY: 102 });
|
||||||
|
|
||||||
expect(onClick).toHaveBeenCalledWith('midi-1', { shiftKey: false });
|
expect(onClick).toHaveBeenCalledWith('midi-1', { shiftKey: false, metaKey: false, ctrlKey: false });
|
||||||
expect(onDragStart).not.toHaveBeenCalled();
|
expect(onDragStart).not.toHaveBeenCalled();
|
||||||
expect(onDrag).not.toHaveBeenCalled();
|
expect(onDrag).not.toHaveBeenCalled();
|
||||||
expect(onDragEnd).not.toHaveBeenCalled();
|
expect(onDragEnd).not.toHaveBeenCalled();
|
||||||
@@ -88,7 +88,20 @@ describe('RegionItem', () => {
|
|||||||
fireEvent.mouseDown(region!, { clientX: 100, clientY: 100, shiftKey: true });
|
fireEvent.mouseDown(region!, { clientX: 100, clientY: 100, shiftKey: true });
|
||||||
fireEvent.mouseUp(document, { clientX: 100, clientY: 100, shiftKey: true });
|
fireEvent.mouseUp(document, { clientX: 100, clientY: 100, shiftKey: true });
|
||||||
|
|
||||||
expect(onClick).toHaveBeenCalledWith('midi-1', { shiftKey: true });
|
expect(onClick).toHaveBeenCalledWith('midi-1', { shiftKey: true, metaKey: false, ctrlKey: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes cmd-click state through the region click callback', () => {
|
||||||
|
const onClick = vi.fn();
|
||||||
|
const { container } = renderRegion({ onClick });
|
||||||
|
const region = container.querySelector('.track-region');
|
||||||
|
|
||||||
|
expect(region).toBeTruthy();
|
||||||
|
|
||||||
|
fireEvent.mouseDown(region!, { clientX: 100, clientY: 100, metaKey: true });
|
||||||
|
fireEvent.mouseUp(document, { clientX: 100, clientY: 100, metaKey: true });
|
||||||
|
|
||||||
|
expect(onClick).toHaveBeenCalledWith('midi-1', { shiftKey: false, metaKey: true, ctrlKey: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('starts a drag after crossing the movement threshold', () => {
|
it('starts a drag after crossing the movement threshold', () => {
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ import { KGCore } from '../../core/KGCore';
|
|||||||
import { beatRangeToSeconds } from '../../util/globalTrackUtil';
|
import { beatRangeToSeconds } from '../../util/globalTrackUtil';
|
||||||
|
|
||||||
const DRAG_START_THRESHOLD_PX = 4;
|
const DRAG_START_THRESHOLD_PX = 4;
|
||||||
|
const getRegionClickOptions = (event: Pick<React.MouseEvent, 'shiftKey' | 'metaKey' | 'ctrlKey'>): RegionClickOptions => ({
|
||||||
|
shiftKey: event.shiftKey,
|
||||||
|
metaKey: event.metaKey,
|
||||||
|
ctrlKey: event.ctrlKey,
|
||||||
|
});
|
||||||
|
|
||||||
interface RegionItemProps {
|
interface RegionItemProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -481,7 +486,7 @@ const RegionItem: React.FC<RegionItemProps> = ({
|
|||||||
if (DEBUG_MODE.REGION_ITEM) {
|
if (DEBUG_MODE.REGION_ITEM) {
|
||||||
console.log(`REGION CLICKED (pencil mode): regionId=${id}`);
|
console.log(`REGION CLICKED (pencil mode): regionId=${id}`);
|
||||||
}
|
}
|
||||||
onClick(id, { shiftKey: e.shiftKey });
|
onClick(id, getRegionClickOptions(e));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -608,7 +613,7 @@ const RegionItem: React.FC<RegionItemProps> = ({
|
|||||||
if (DEBUG_MODE.REGION_ITEM) {
|
if (DEBUG_MODE.REGION_ITEM) {
|
||||||
console.log(`REGION CLICKED: regionId=${id}`);
|
console.log(`REGION CLICKED: regionId=${id}`);
|
||||||
}
|
}
|
||||||
onClick(id, { shiftKey: e.shiftKey });
|
onClick(id, getRegionClickOptions(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
isPendingDragRef.current = false;
|
isPendingDragRef.current = false;
|
||||||
@@ -708,7 +713,7 @@ const RegionItem: React.FC<RegionItemProps> = ({
|
|||||||
if (onOpenPianoRoll) {
|
if (onOpenPianoRoll) {
|
||||||
onOpenPianoRoll(id);
|
onOpenPianoRoll(id);
|
||||||
} else if (onClick) {
|
} else if (onClick) {
|
||||||
onClick(id, { shiftKey: e.shiftKey });
|
onClick(id, getRegionClickOptions(e));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
aria-label="Open piano roll"
|
aria-label="Open piano roll"
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import TrackAutomationLane from './TrackAutomationLane';
|
|||||||
import type { RegionClickOptions, RegionPreviewContentStyle, RegionUI, ResizeAction } from '../interfaces';
|
import type { RegionClickOptions, RegionPreviewContentStyle, RegionUI, ResizeAction } from '../interfaces';
|
||||||
import { REGION_CONSTANTS, DEBUG_MODE } from '../../constants';
|
import { REGION_CONSTANTS, DEBUG_MODE } from '../../constants';
|
||||||
import { KGMainContentState } from '../../core/state/KGMainContentState';
|
import { KGMainContentState } from '../../core/state/KGMainContentState';
|
||||||
import { isModifierKeyPressed } from '../../util/osUtil';
|
|
||||||
import { useProjectStore } from '../../stores/projectStore';
|
import { useProjectStore } from '../../stores/projectStore';
|
||||||
|
|
||||||
interface RegionResizePreviewBaseline {
|
interface RegionResizePreviewBaseline {
|
||||||
@@ -200,28 +199,23 @@ const TrackGridItem: React.FC<TrackGridItemProps> = ({
|
|||||||
};
|
};
|
||||||
}, [gridContainerRef]);
|
}, [gridContainerRef]);
|
||||||
|
|
||||||
// Track modifier key state for cursor feedback
|
// Track tool state for cursor feedback.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const syncCursorState = () => {
|
||||||
if (isModifierKeyPressed(e)) {
|
setIsModifierPressed(KGMainContentState.instance().getActiveTool() === 'pencil');
|
||||||
setIsModifierPressed(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyUp = (e: KeyboardEvent) => {
|
|
||||||
if (!isModifierKeyPressed(e)) {
|
|
||||||
setIsModifierPressed(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add global event listeners
|
// Add global event listeners
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', syncCursorState);
|
||||||
window.addEventListener('keyup', handleKeyUp);
|
window.addEventListener('keyup', syncCursorState);
|
||||||
|
window.addEventListener('focus', syncCursorState);
|
||||||
|
syncCursorState();
|
||||||
|
|
||||||
// Cleanup listeners on unmount
|
// Cleanup listeners on unmount
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', handleKeyDown);
|
window.removeEventListener('keydown', syncCursorState);
|
||||||
window.removeEventListener('keyup', handleKeyUp);
|
window.removeEventListener('keyup', syncCursorState);
|
||||||
|
window.removeEventListener('focus', syncCursorState);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -785,7 +779,7 @@ const TrackGridItem: React.FC<TrackGridItemProps> = ({
|
|||||||
onOpenPianoRoll(regionId);
|
onOpenPianoRoll(regionId);
|
||||||
} else if (onRegionClick) {
|
} else if (onRegionClick) {
|
||||||
// Fallback to legacy behavior
|
// Fallback to legacy behavior
|
||||||
onRegionClick(regionId, { shiftKey: false });
|
onRegionClick(regionId, { shiftKey: false, metaKey: false, ctrlKey: false });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onOpenSpectrogram={audioRegion ? (regionId) => {
|
onOpenSpectrogram={audioRegion ? (regionId) => {
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import { fireEvent, render } from '@testing-library/react';
|
|||||||
import TrackGridPanel from './TrackGridPanel';
|
import TrackGridPanel from './TrackGridPanel';
|
||||||
import { createMockMidiRegion, createMockMidiTrack } from '../../test/utils/mock-data';
|
import { createMockMidiRegion, createMockMidiTrack } from '../../test/utils/mock-data';
|
||||||
|
|
||||||
|
const executeCommandMock = vi.fn();
|
||||||
|
const getCreatedRegionMock = vi.fn();
|
||||||
|
|
||||||
vi.mock('../../stores/projectStore', () => ({
|
vi.mock('../../stores/projectStore', () => ({
|
||||||
useProjectStore: (selector?: (state: {
|
useProjectStore: (selector?: (state: {
|
||||||
selectedRegionIds: string[],
|
selectedRegionIds: string[],
|
||||||
@@ -26,6 +29,36 @@ vi.mock('../common', () => ({
|
|||||||
FileImportModal: () => null,
|
FileImportModal: () => null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../core/KGCore', () => ({
|
||||||
|
KGCore: {
|
||||||
|
instance: () => ({
|
||||||
|
executeCommand: executeCommandMock,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../util/miscUtil', () => ({
|
||||||
|
generateNewRegionName: () => 'New Region',
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../core/commands', async () => {
|
||||||
|
const actual = await vi.importActual<typeof import('../../core/commands')>('../../core/commands');
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
CreateRegionCommand: {
|
||||||
|
fromBarCoordinates: vi.fn((trackId: string, trackIndex: number, barNumber: number) => ({
|
||||||
|
getCreatedRegion: () => getCreatedRegionMock() ?? createMockMidiRegion({
|
||||||
|
id: 'created-region',
|
||||||
|
trackId,
|
||||||
|
trackIndex,
|
||||||
|
startFromBeat: (barNumber - 1) * 4,
|
||||||
|
length: 4,
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe('TrackGridPanel lasso selection', () => {
|
describe('TrackGridPanel lasso selection', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
Object.defineProperty(HTMLCanvasElement.prototype, 'getContext', {
|
Object.defineProperty(HTMLCanvasElement.prototype, 'getContext', {
|
||||||
@@ -57,6 +90,7 @@ describe('TrackGridPanel lasso selection', () => {
|
|||||||
trackB.setTrackIndex(1);
|
trackB.setTrackIndex(1);
|
||||||
|
|
||||||
const onRegionLassoSelection = vi.fn();
|
const onRegionLassoSelection = vi.fn();
|
||||||
|
const onRegionCreated = vi.fn();
|
||||||
|
|
||||||
const view = render(
|
const view = render(
|
||||||
<TrackGridPanel
|
<TrackGridPanel
|
||||||
@@ -71,7 +105,7 @@ describe('TrackGridPanel lasso selection', () => {
|
|||||||
dragOverTrackIndex={null}
|
dragOverTrackIndex={null}
|
||||||
selectedRegionId={null}
|
selectedRegionId={null}
|
||||||
projectName="Test"
|
projectName="Test"
|
||||||
onRegionCreated={vi.fn()}
|
onRegionCreated={onRegionCreated}
|
||||||
onRegionLassoSelection={onRegionLassoSelection}
|
onRegionLassoSelection={onRegionLassoSelection}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -90,11 +124,13 @@ describe('TrackGridPanel lasso selection', () => {
|
|||||||
toJSON: () => ({}),
|
toJSON: () => ({}),
|
||||||
});
|
});
|
||||||
|
|
||||||
return { ...view, onRegionLassoSelection };
|
return { ...view, onRegionLassoSelection, onRegionCreated };
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
|
executeCommandMock.mockReset();
|
||||||
|
getCreatedRegionMock.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('selects intersecting regions across multiple track rows', () => {
|
it('selects intersecting regions across multiple track rows', () => {
|
||||||
@@ -105,7 +141,7 @@ describe('TrackGridPanel lasso selection', () => {
|
|||||||
fireEvent.mouseMove(document, { clientX: 130, clientY: 200 });
|
fireEvent.mouseMove(document, { clientX: 130, clientY: 200 });
|
||||||
fireEvent.mouseUp(document, { clientX: 130, clientY: 200 });
|
fireEvent.mouseUp(document, { clientX: 130, clientY: 200 });
|
||||||
|
|
||||||
expect(onRegionLassoSelection).toHaveBeenCalledWith(['region-a', 'region-b'], { shiftKey: false });
|
expect(onRegionLassoSelection).toHaveBeenCalledWith(['region-a', 'region-b'], { shiftKey: false, metaKey: false, ctrlKey: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('moves the release-point region to the end of the lasso selection order', () => {
|
it('moves the release-point region to the end of the lasso selection order', () => {
|
||||||
@@ -116,7 +152,7 @@ describe('TrackGridPanel lasso selection', () => {
|
|||||||
fireEvent.mouseMove(document, { clientX: 20, clientY: 20 });
|
fireEvent.mouseMove(document, { clientX: 20, clientY: 20 });
|
||||||
fireEvent.mouseUp(document, { clientX: 20, clientY: 20 });
|
fireEvent.mouseUp(document, { clientX: 20, clientY: 20 });
|
||||||
|
|
||||||
expect(onRegionLassoSelection).toHaveBeenCalledWith(['region-b', 'region-a'], { shiftKey: false });
|
expect(onRegionLassoSelection).toHaveBeenCalledWith(['region-b', 'region-a'], { shiftKey: false, metaKey: false, ctrlKey: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses the closest intersected region as primary when release is outside all regions', () => {
|
it('uses the closest intersected region as primary when release is outside all regions', () => {
|
||||||
@@ -127,7 +163,7 @@ describe('TrackGridPanel lasso selection', () => {
|
|||||||
fireEvent.mouseMove(document, { clientX: 150, clientY: 170 });
|
fireEvent.mouseMove(document, { clientX: 150, clientY: 170 });
|
||||||
fireEvent.mouseUp(document, { clientX: 150, clientY: 170 });
|
fireEvent.mouseUp(document, { clientX: 150, clientY: 170 });
|
||||||
|
|
||||||
expect(onRegionLassoSelection).toHaveBeenCalledWith(['region-a', 'region-b'], { shiftKey: false });
|
expect(onRegionLassoSelection).toHaveBeenCalledWith(['region-a', 'region-b'], { shiftKey: false, metaKey: false, ctrlKey: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clears selection on a plain empty-space click', () => {
|
it('clears selection on a plain empty-space click', () => {
|
||||||
@@ -137,6 +173,36 @@ describe('TrackGridPanel lasso selection', () => {
|
|||||||
fireEvent.mouseDown(firstTrackGrid, { clientX: 10, clientY: 10, button: 0 });
|
fireEvent.mouseDown(firstTrackGrid, { clientX: 10, clientY: 10, button: 0 });
|
||||||
fireEvent.mouseUp(document, { clientX: 11, clientY: 11 });
|
fireEvent.mouseUp(document, { clientX: 11, clientY: 11 });
|
||||||
|
|
||||||
expect(onRegionLassoSelection).toHaveBeenCalledWith([], { shiftKey: false });
|
expect(onRegionLassoSelection).toHaveBeenCalledWith([], { shiftKey: false, metaKey: false, ctrlKey: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not create a region when ctrl-clicking an existing region', () => {
|
||||||
|
const { container, onRegionCreated } = renderPanel();
|
||||||
|
const region = container.querySelector('[data-region-id="region-a"]') as HTMLDivElement;
|
||||||
|
|
||||||
|
fireEvent.mouseDown(region, { clientX: 20, clientY: 20, button: 0, ctrlKey: true });
|
||||||
|
fireEvent.mouseUp(document, { clientX: 20, clientY: 20, ctrlKey: true });
|
||||||
|
fireEvent.click(region, { ctrlKey: true });
|
||||||
|
|
||||||
|
expect(onRegionCreated).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates a region when ctrl-clicking empty track space', async () => {
|
||||||
|
const { container, onRegionCreated } = renderPanel();
|
||||||
|
const firstTrackGrid = container.querySelector('[data-test-id="track-grid-1"]') as HTMLDivElement;
|
||||||
|
getCreatedRegionMock.mockReturnValue(createMockMidiRegion({
|
||||||
|
id: 'created-region',
|
||||||
|
trackId: '1',
|
||||||
|
trackIndex: 0,
|
||||||
|
startFromBeat: 12,
|
||||||
|
length: 4,
|
||||||
|
name: 'New Region',
|
||||||
|
}));
|
||||||
|
|
||||||
|
fireEvent.click(firstTrackGrid, { clientX: 140, clientY: 20, ctrlKey: true });
|
||||||
|
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(onRegionCreated).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ import * as Tone from 'tone';
|
|||||||
import { useProjectStore } from '../../stores/projectStore';
|
import { useProjectStore } from '../../stores/projectStore';
|
||||||
import { getAudioRegionDisplayLengthBeats } from '../../util/globalTrackUtil';
|
import { getAudioRegionDisplayLengthBeats } from '../../util/globalTrackUtil';
|
||||||
|
|
||||||
|
const getRegionClickOptions = (event: Pick<MouseEvent | React.MouseEvent, 'shiftKey' | 'metaKey' | 'ctrlKey'>): RegionClickOptions => ({
|
||||||
|
shiftKey: event.shiftKey,
|
||||||
|
metaKey: event.metaKey,
|
||||||
|
ctrlKey: event.ctrlKey,
|
||||||
|
});
|
||||||
|
|
||||||
interface TrackGridPanelProps {
|
interface TrackGridPanelProps {
|
||||||
tracks: KGTrack[];
|
tracks: KGTrack[];
|
||||||
regions: RegionUI[];
|
regions: RegionUI[];
|
||||||
@@ -130,7 +136,7 @@ const TrackGridPanel: React.FC<TrackGridPanelProps> = ({
|
|||||||
|
|
||||||
if (isClick) {
|
if (isClick) {
|
||||||
if (!isLassoShiftPressedRef.current) {
|
if (!isLassoShiftPressedRef.current) {
|
||||||
onRegionLassoSelection?.([], { shiftKey: false });
|
onRegionLassoSelection?.([], getRegionClickOptions({ shiftKey: false, metaKey: false, ctrlKey: false }));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const containerWidth = gridContainerRef.current.clientWidth;
|
const containerWidth = gridContainerRef.current.clientWidth;
|
||||||
@@ -194,7 +200,11 @@ const TrackGridPanel: React.FC<TrackGridPanelProps> = ({
|
|||||||
intersectedRegionIds.push(primaryRegionId);
|
intersectedRegionIds.push(primaryRegionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
onRegionLassoSelection?.(intersectedRegionIds, { shiftKey: isLassoShiftPressedRef.current });
|
onRegionLassoSelection?.(intersectedRegionIds, {
|
||||||
|
shiftKey: isLassoShiftPressedRef.current,
|
||||||
|
metaKey: false,
|
||||||
|
ctrlKey: false,
|
||||||
|
});
|
||||||
onRegionLassoCommit?.();
|
onRegionLassoCommit?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,12 +400,23 @@ const TrackGridPanel: React.FC<TrackGridPanelProps> = ({
|
|||||||
createRegionAtPosition(e, trackIndex);
|
createRegionAtPosition(e, trackIndex);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle single click on track grid for pencil mode or modifier+click
|
// Handle single click on track grid for pencil mode or modifier-click on empty space.
|
||||||
const handleTrackGridClick = (e: React.MouseEvent<HTMLDivElement>, trackIndex: number) => {
|
const handleTrackGridClick = (e: React.MouseEvent<HTMLDivElement>, trackIndex: number) => {
|
||||||
// Create region on single click in pencil mode OR when modifier key is pressed
|
if (!(e.target instanceof HTMLElement)) {
|
||||||
if (KGMainContentState.instance().getActiveTool() === 'pencil' || isModifierKeyPressed(e)) {
|
return;
|
||||||
createRegionAtPosition(e, trackIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clickedInsideRegion = e.target.closest('.track-region');
|
||||||
|
if (clickedInsideRegion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPencilMode = KGMainContentState.instance().getActiveTool() === 'pencil';
|
||||||
|
if (!isPencilMode && !isModifierKeyPressed(e)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
createRegionAtPosition(e, trackIndex);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle region resize during drag
|
// Handle region resize during drag
|
||||||
|
|||||||
Reference in New Issue
Block a user