feat: allow user to multi-select regions.
This commit is contained in:
@@ -4,12 +4,13 @@ import { createPortal } from 'react-dom';
|
|||||||
import { useProjectStore } from '../stores/projectStore';
|
import { useProjectStore } from '../stores/projectStore';
|
||||||
import { KGCore } from '../core/KGCore';
|
import { KGCore } from '../core/KGCore';
|
||||||
import { KGTrack } from '../core/track/KGTrack';
|
import { KGTrack } from '../core/track/KGTrack';
|
||||||
|
import { KGRegion } from '../core/region/KGRegion';
|
||||||
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 TrackInfoPanel from './track/TrackInfoPanel';
|
import TrackInfoPanel from './track/TrackInfoPanel';
|
||||||
import TrackGridPanel from './track/TrackGridPanel';
|
import TrackGridPanel from './track/TrackGridPanel';
|
||||||
import PianoRoll from './piano-roll/PianoRoll';
|
import PianoRoll from './piano-roll/PianoRoll';
|
||||||
import type { RegionUI } from './interfaces';
|
import type { RegionClickOptions, RegionUI } from './interfaces';
|
||||||
import { DEBUG_MODE, BAR_NUMBERS_CONSTANTS, TOOLBAR_CONSTANTS } from '../constants';
|
import { DEBUG_MODE, BAR_NUMBERS_CONSTANTS, TOOLBAR_CONSTANTS } from '../constants';
|
||||||
import { useRegionOperations } from '../hooks/useRegionOperations';
|
import { useRegionOperations } from '../hooks/useRegionOperations';
|
||||||
import { regionDeleteManager } from '../util/regionDeleteUtil';
|
import { regionDeleteManager } from '../util/regionDeleteUtil';
|
||||||
@@ -38,6 +39,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
setAutoScrollEnabled,
|
setAutoScrollEnabled,
|
||||||
clearAllSelections,
|
clearAllSelections,
|
||||||
setSelectedTrack,
|
setSelectedTrack,
|
||||||
|
selectedRegionIds,
|
||||||
showPianoRoll,
|
showPianoRoll,
|
||||||
activeRegionId,
|
activeRegionId,
|
||||||
setShowPianoRoll,
|
setShowPianoRoll,
|
||||||
@@ -349,7 +351,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
const updatedRegions = [...prevRegions, regionUI];
|
const updatedRegions = [...prevRegions, regionUI];
|
||||||
|
|
||||||
// Select the region using the updated regions array
|
// Select the region using the updated regions array
|
||||||
selectRegion(regionUI.id, updatedRegions);
|
selectRegion(regionUI.id, { shiftKey: false }, updatedRegions);
|
||||||
|
|
||||||
// Manually trigger selection sync to ensure UI updates immediately
|
// Manually trigger selection sync to ensure UI updates immediately
|
||||||
const { syncSelectionFromCore } = useProjectStore.getState();
|
const { syncSelectionFromCore } = useProjectStore.getState();
|
||||||
@@ -384,7 +386,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
|
|
||||||
setRegions(prev => {
|
setRegions(prev => {
|
||||||
const updated = [...prev, regionUI];
|
const updated = [...prev, regionUI];
|
||||||
selectRegion(regionUI.id, updated);
|
selectRegion(regionUI.id, { shiftKey: false }, updated);
|
||||||
return updated;
|
return updated;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -518,10 +520,11 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to select a region (clears previous selections)
|
// Helper function to select a region (clears previous selections)
|
||||||
const selectRegion = (regionId: string, regionsToSearch?: RegionUI[]) => {
|
const selectRegion = (
|
||||||
// Clear any existing selections using store method
|
regionId: string,
|
||||||
clearAllSelections();
|
options: RegionClickOptions = { shiftKey: false },
|
||||||
|
regionsToSearch?: RegionUI[]
|
||||||
|
) => {
|
||||||
// Find the region in the UI state (use provided regions or current state)
|
// Find the region in the UI state (use provided regions or current state)
|
||||||
const regionsToUse = regionsToSearch || regions;
|
const regionsToUse = regionsToSearch || regions;
|
||||||
const region = regionsToUse.find(r => r.id === regionId);
|
const region = regionsToUse.find(r => r.id === regionId);
|
||||||
@@ -542,39 +545,81 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find the region in the track's model
|
// Find the region in the track's model
|
||||||
const trackRegions = track.getRegions();
|
const coreRegion = track.getRegions().find(r => r.getId() === regionId);
|
||||||
const midiRegion = trackRegions.find(r => r.getId() === regionId) as KGMidiRegion | undefined;
|
|
||||||
|
|
||||||
if (!midiRegion) {
|
if (!coreRegion) {
|
||||||
if (DEBUG_MODE.MAIN_CONTENT) {
|
if (DEBUG_MODE.MAIN_CONTENT) {
|
||||||
console.log(`MIDI region not found in track model: ${regionId}`);
|
console.log(`Region not found in track model: ${regionId}`);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the region to KGCore's selection
|
|
||||||
const core = KGCore.instance();
|
const core = KGCore.instance();
|
||||||
core.addSelectedItem(midiRegion);
|
const orderedSelection = options.shiftKey
|
||||||
|
? (selectedRegionIds.includes(regionId)
|
||||||
|
? selectedRegionIds.filter(id => id !== regionId)
|
||||||
|
: [...selectedRegionIds, regionId])
|
||||||
|
: [regionId];
|
||||||
|
|
||||||
// Update the region's internal selection state
|
tracks.forEach(projectTrack => {
|
||||||
midiRegion.select();
|
projectTrack.getRegions().forEach(projectRegion => projectRegion.deselect());
|
||||||
|
});
|
||||||
|
|
||||||
// Set the selected region (this might be redundant now, but keeping for compatibility)
|
clearAllSelections();
|
||||||
setSelectedRegionId(regionId);
|
|
||||||
|
const selectedRegions: KGRegion[] = orderedSelection
|
||||||
|
.map(selectedId => {
|
||||||
|
for (const projectTrack of tracks) {
|
||||||
|
const selectedRegion = projectTrack.getRegions().find(r => r.getId() === selectedId);
|
||||||
|
if (selectedRegion) {
|
||||||
|
selectedRegion.select();
|
||||||
|
return selectedRegion as KGRegion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter((selectedRegion): selectedRegion is KGRegion => selectedRegion !== null);
|
||||||
|
|
||||||
|
if (selectedRegions.length > 0) {
|
||||||
|
core.addSelectedItems(selectedRegions);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastSelectedRegionId = selectedRegions.length > 0
|
||||||
|
? selectedRegions[selectedRegions.length - 1].getId()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
setSelectedRegionId(lastSelectedRegionId);
|
||||||
|
|
||||||
if (DEBUG_MODE.MAIN_CONTENT) {
|
if (DEBUG_MODE.MAIN_CONTENT) {
|
||||||
console.log(`Selected region: ${regionId} (added to KGCore selection)`);
|
console.log(`Selected regions: ${selectedRegions.map(selectedRegion => selectedRegion.getId()).join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!showPianoRoll) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lastSelectedRegionId) {
|
||||||
|
setShowPianoRoll(false);
|
||||||
|
setActiveRegionId(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastSelectedRegion = selectedRegions[selectedRegions.length - 1];
|
||||||
|
if (lastSelectedRegion instanceof KGAudioRegion) {
|
||||||
|
openSpectrogramViewer(lastSelectedRegionId);
|
||||||
|
} else if (lastSelectedRegion instanceof KGMidiRegion) {
|
||||||
|
openMidiPianoRoll(lastSelectedRegionId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle region single click: selection only (no piano roll opening)
|
// Handle region single click: selection only (no piano roll opening)
|
||||||
const handleRegionClick = (regionId: string) => {
|
const handleRegionClick = (regionId: string, options: RegionClickOptions = { shiftKey: false }) => {
|
||||||
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}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select the region
|
// Select the region
|
||||||
selectRegion(regionId);
|
selectRegion(regionId, options);
|
||||||
|
|
||||||
// Also select the containing track
|
// Also select the containing track
|
||||||
const region = regions.find(r => r.id === regionId);
|
const region = regions.find(r => r.id === regionId);
|
||||||
@@ -583,15 +628,6 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
if (!track) return;
|
if (!track) return;
|
||||||
setSelectedTrack(track.getId().toString());
|
setSelectedTrack(track.getId().toString());
|
||||||
|
|
||||||
// If the piano roll window is already open, follow the selected region's type
|
|
||||||
if (showPianoRoll) {
|
|
||||||
const coreRegion = track.getRegions().find(r => r.getId() === regionId);
|
|
||||||
if (coreRegion?.getCurrentType() === 'KGAudioRegion') {
|
|
||||||
openSpectrogramViewer(regionId);
|
|
||||||
} else if (coreRegion?.getCurrentType() === 'KGMidiRegion') {
|
|
||||||
openMidiPianoRoll(regionId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle explicit pencil action: select region and open piano roll
|
// Handle explicit pencil action: select region and open piano roll
|
||||||
@@ -610,7 +646,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reuse selection logic
|
// Reuse selection logic
|
||||||
handleRegionClick(regionId);
|
handleRegionClick(regionId, { shiftKey: false });
|
||||||
|
|
||||||
// Activate and show piano roll in midi-edit mode
|
// Activate and show piano roll in midi-edit mode
|
||||||
openMidiPianoRoll(regionId);
|
openMidiPianoRoll(regionId);
|
||||||
@@ -618,7 +654,7 @@ const MainContent: React.FC<MainContentProps> = ({
|
|||||||
|
|
||||||
// Handle spectrogram viewer open
|
// Handle spectrogram viewer open
|
||||||
const handleOpenSpectrogram = (regionId: string) => {
|
const handleOpenSpectrogram = (regionId: string) => {
|
||||||
handleRegionClick(regionId);
|
handleRegionClick(regionId, { shiftKey: false });
|
||||||
openSpectrogramViewer(regionId);
|
openSpectrogramViewer(regionId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ const Toolbar: React.FC = () => {
|
|||||||
|
|
||||||
// Export options
|
// Export options
|
||||||
const exportOptions = ["Export to KGStudio file", "Export to MIDI file", "Export to WAV", "Export to MP3"];
|
const exportOptions = ["Export to KGStudio file", "Export to MIDI file", "Export to WAV", "Export to MP3"];
|
||||||
|
const lastSelectedRegionId = selectedRegionIds[selectedRegionIds.length - 1] ?? null;
|
||||||
|
|
||||||
const handleProjectNameClick = async () => {
|
const handleProjectNameClick = async () => {
|
||||||
const newName = await showPrompt("Enter project name:", projectName);
|
const newName = await showPrompt("Enter project name:", projectName);
|
||||||
@@ -730,7 +731,7 @@ const Toolbar: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const regionId = selectedRegionIds[0];
|
const regionId = lastSelectedRegionId;
|
||||||
const tracks = KGCore.instance().getCurrentProject().getTracks();
|
const tracks = KGCore.instance().getCurrentProject().getTracks();
|
||||||
let targetRegion = null;
|
let targetRegion = null;
|
||||||
for (const track of tracks) {
|
for (const track of tracks) {
|
||||||
@@ -845,7 +846,7 @@ const Toolbar: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Prefer current active region; otherwise, first selected region
|
// Prefer current active region; otherwise, first selected region
|
||||||
const candidateRegionId = activeRegionId || (selectedRegionIds && selectedRegionIds.length > 0 ? selectedRegionIds[0] : null);
|
const candidateRegionId = activeRegionId || lastSelectedRegionId;
|
||||||
|
|
||||||
if (!candidateRegionId) {
|
if (!candidateRegionId) {
|
||||||
if (DEBUG_MODE.TOOLBAR) {
|
if (DEBUG_MODE.TOOLBAR) {
|
||||||
@@ -877,7 +878,7 @@ const Toolbar: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Require an active or selected MIDI region
|
// Require an active or selected MIDI region
|
||||||
const candidateId = activeRegionId ?? (selectedRegionIds[0] ?? null);
|
const candidateId = activeRegionId ?? lastSelectedRegionId;
|
||||||
if (!candidateId) {
|
if (!candidateId) {
|
||||||
await showAlert("Please open a MIDI region in the Piano Roll before starting recording.");
|
await showAlert("Please open a MIDI region in the Piano Roll before starting recording.");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ export interface RegionUI {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RegionClickOptions {
|
||||||
|
shiftKey: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
// Define resize action types
|
// Define resize action types
|
||||||
export type ResizeAction = 'none' | 'start' | 'end';
|
export type ResizeAction = 'none' | 'start' | 'end';
|
||||||
|
|
||||||
@@ -39,4 +43,4 @@ export interface RegionDragState {
|
|||||||
initialY: number;
|
initialY: number;
|
||||||
initialBarNumber: number;
|
initialBarNumber: number;
|
||||||
initialTrackIndex: number;
|
initialTrackIndex: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,10 @@
|
|||||||
border-color: #ffffff;
|
border-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.track-region.selected-secondary {
|
||||||
|
border-color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
.track-region.dragging {
|
.track-region.dragging {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||||
@@ -89,6 +93,10 @@
|
|||||||
border-color: #ffffff;
|
border-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.track-region.audio-region.selected-secondary {
|
||||||
|
border-color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
.track-region.audio-region .region-header {
|
.track-region.audio-region .region-header {
|
||||||
background-color: #4a8b5a;
|
background-color: #4a8b5a;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,12 +68,25 @@ 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');
|
expect(onClick).toHaveBeenCalledWith('midi-1', { shiftKey: 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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('passes shift-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, shiftKey: true });
|
||||||
|
fireEvent.mouseUp(document, { clientX: 100, clientY: 100, shiftKey: true });
|
||||||
|
|
||||||
|
expect(onClick).toHaveBeenCalledWith('midi-1', { shiftKey: true });
|
||||||
|
});
|
||||||
|
|
||||||
it('starts a drag after crossing the movement threshold', () => {
|
it('starts a drag after crossing the movement threshold', () => {
|
||||||
const onClick = vi.fn();
|
const onClick = vi.fn();
|
||||||
const onDragStart = vi.fn();
|
const onDragStart = vi.fn();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useState, useRef, useEffect } from 'react';
|
|||||||
import './Region.css';
|
import './Region.css';
|
||||||
import { FaPencilAlt, FaPlus } from 'react-icons/fa';
|
import { FaPencilAlt, FaPlus } from 'react-icons/fa';
|
||||||
import { MdGraphicEq, MdSwapHoriz } from 'react-icons/md';
|
import { MdGraphicEq, MdSwapHoriz } from 'react-icons/md';
|
||||||
import type { ResizeAction } from '../interfaces';
|
import type { RegionClickOptions, ResizeAction } from '../interfaces';
|
||||||
import { REGION_CONSTANTS, DEBUG_MODE } from '../../constants';
|
import { REGION_CONSTANTS, DEBUG_MODE } from '../../constants';
|
||||||
import { KGMidiRegion } from '../../core/region/KGMidiRegion';
|
import { KGMidiRegion } from '../../core/region/KGMidiRegion';
|
||||||
import { KGAudioRegion } from '../../core/region/KGAudioRegion';
|
import { KGAudioRegion } from '../../core/region/KGAudioRegion';
|
||||||
@@ -27,7 +27,7 @@ interface RegionItemProps {
|
|||||||
onDrag?: (regionId: string, deltaX: number, deltaY: number) => void;
|
onDrag?: (regionId: string, deltaX: number, deltaY: number) => void;
|
||||||
onDragEnd?: (regionId: string) => void;
|
onDragEnd?: (regionId: string) => void;
|
||||||
// Click prop
|
// Click prop
|
||||||
onClick?: (regionId: string) => void;
|
onClick?: (regionId: string, options: RegionClickOptions) => void;
|
||||||
// Explicit open piano roll action from header pencil icon
|
// Explicit open piano roll action from header pencil icon
|
||||||
onOpenPianoRoll?: (regionId: string) => void;
|
onOpenPianoRoll?: (regionId: string) => void;
|
||||||
// Open spectrogram viewer for audio regions
|
// Open spectrogram viewer for audio regions
|
||||||
@@ -70,6 +70,7 @@ const RegionItem: React.FC<RegionItemProps> = ({
|
|||||||
// Get selection state and time signature from store
|
// Get selection state and time signature from store
|
||||||
const { selectedRegionIds, timeSignature, bpm } = useProjectStore();
|
const { selectedRegionIds, timeSignature, bpm } = useProjectStore();
|
||||||
const isSelected = selectedRegionIds.includes(id);
|
const isSelected = selectedRegionIds.includes(id);
|
||||||
|
const isPrimarySelected = isSelected && selectedRegionIds[selectedRegionIds.length - 1] === id;
|
||||||
const [cursor, setCursor] = useState<string>('pointer');
|
const [cursor, setCursor] = useState<string>('pointer');
|
||||||
const [resizeEdge, setResizeEdge] = useState<ResizeAction>('none');
|
const [resizeEdge, setResizeEdge] = useState<ResizeAction>('none');
|
||||||
const [isResizing, setIsResizing] = useState(false);
|
const [isResizing, setIsResizing] = useState(false);
|
||||||
@@ -408,7 +409,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);
|
onClick(id, { shiftKey: e.shiftKey });
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -535,7 +536,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);
|
onClick(id, { shiftKey: e.shiftKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
isPendingDragRef.current = false;
|
isPendingDragRef.current = false;
|
||||||
@@ -602,7 +603,7 @@ const RegionItem: React.FC<RegionItemProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={id}
|
key={id}
|
||||||
className={`track-region ${isDragging ? 'dragging' : ''} ${isSelected ? 'selected' : ''} ${audioRegion ? 'audio-region' : ''}`}
|
className={`track-region ${isDragging ? 'dragging' : ''} ${isSelected ? (isPrimarySelected ? 'selected' : 'selected-secondary') : ''} ${audioRegion ? 'audio-region' : ''}`}
|
||||||
style={{ ...style, cursor, ...(isFineDragging ? { transform: `translateX(${fineTranslateX}px)`, zIndex: 100 } : {}) }}
|
style={{ ...style, cursor, ...(isFineDragging ? { transform: `translateX(${fineTranslateX}px)`, zIndex: 100 } : {}) }}
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
@@ -634,7 +635,7 @@ const RegionItem: React.FC<RegionItemProps> = ({
|
|||||||
if (onOpenPianoRoll) {
|
if (onOpenPianoRoll) {
|
||||||
onOpenPianoRoll(id);
|
onOpenPianoRoll(id);
|
||||||
} else if (onClick) {
|
} else if (onClick) {
|
||||||
onClick(id);
|
onClick(id, { shiftKey: e.shiftKey });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
aria-label="Open piano roll"
|
aria-label="Open piano roll"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { KGMidiRegion } from '../../core/region/KGMidiRegion';
|
|||||||
import { KGAudioRegion } from '../../core/region/KGAudioRegion';
|
import { KGAudioRegion } from '../../core/region/KGAudioRegion';
|
||||||
import { KGAudioInterface } from '../../core/audio-interface/KGAudioInterface';
|
import { KGAudioInterface } from '../../core/audio-interface/KGAudioInterface';
|
||||||
import RegionItem from './RegionItem';
|
import RegionItem from './RegionItem';
|
||||||
import type { RegionUI, ResizeAction } from '../interfaces';
|
import type { RegionClickOptions, 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 { isModifierKeyPressed } from '../../util/osUtil';
|
||||||
@@ -25,7 +25,7 @@ interface TrackGridItemProps {
|
|||||||
onRegionDrag?: (regionId: string, newBarNumber: number, newTrackIndex: number) => void;
|
onRegionDrag?: (regionId: string, newBarNumber: number, newTrackIndex: number) => void;
|
||||||
onRegionDragEnd?: (regionId: string, finalBarNumber: number, finalTrackIndex: number) => void;
|
onRegionDragEnd?: (regionId: string, finalBarNumber: number, finalTrackIndex: number) => void;
|
||||||
onRegionFineMoveEnd?: (regionId: string, deltaInBars: number) => void;
|
onRegionFineMoveEnd?: (regionId: string, deltaInBars: number) => void;
|
||||||
onRegionClick?: (regionId: string) => void;
|
onRegionClick?: (regionId: string, options: RegionClickOptions) => void;
|
||||||
onOpenPianoRoll?: (regionId: string) => void;
|
onOpenPianoRoll?: (regionId: string) => void;
|
||||||
onOpenSpectrogram?: (regionId: string) => void;
|
onOpenSpectrogram?: (regionId: string) => void;
|
||||||
showHybridButtonForAudio?: boolean;
|
showHybridButtonForAudio?: boolean;
|
||||||
@@ -516,13 +516,13 @@ const TrackGridItem: React.FC<TrackGridItemProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Handle region click
|
// Handle region click
|
||||||
const handleRegionClick = (regionId: string) => {
|
const handleRegionClick = (regionId: string, options: RegionClickOptions) => {
|
||||||
if (DEBUG_MODE.TRACK_GRID_ITEM) {
|
if (DEBUG_MODE.TRACK_GRID_ITEM) {
|
||||||
console.log(`Region clicked: ${regionId}`);
|
console.log(`Region clicked: ${regionId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onRegionClick) {
|
if (onRegionClick) {
|
||||||
onRegionClick(regionId);
|
onRegionClick(regionId, options);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -589,7 +589,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);
|
onRegionClick(regionId, { shiftKey: false });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onOpenSpectrogram={audioRegion ? (regionId) => {
|
onOpenSpectrogram={audioRegion ? (regionId) => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { KGTrack, TrackType } from '../../core/track/KGTrack';
|
|||||||
import { KGMidiRegion } from '../../core/region/KGMidiRegion';
|
import { KGMidiRegion } from '../../core/region/KGMidiRegion';
|
||||||
import TrackGridItem from './TrackGridItem';
|
import TrackGridItem from './TrackGridItem';
|
||||||
import { Playhead, FileImportModal } from '../common';
|
import { Playhead, FileImportModal } from '../common';
|
||||||
import type { RegionUI } from '../interfaces';
|
import type { RegionClickOptions, RegionUI } from '../interfaces';
|
||||||
import { DEBUG_MODE, REGION_CONSTANTS } from '../../constants';
|
import { DEBUG_MODE, REGION_CONSTANTS } from '../../constants';
|
||||||
import { KGMainContentState } from '../../core/state/KGMainContentState';
|
import { KGMainContentState } from '../../core/state/KGMainContentState';
|
||||||
import { isModifierKeyPressed } from '../../util/osUtil';
|
import { isModifierKeyPressed } from '../../util/osUtil';
|
||||||
@@ -28,7 +28,7 @@ interface TrackGridPanelProps {
|
|||||||
projectName: string;
|
projectName: string;
|
||||||
onRegionCreated: (trackIndex: number, region: RegionUI, midiRegion: KGMidiRegion) => void;
|
onRegionCreated: (trackIndex: number, region: RegionUI, midiRegion: KGMidiRegion) => void;
|
||||||
onRegionUpdated?: (regionId: string, updates: Partial<RegionUI>, expectedModelUpdates?: { startBeat: number, length: number }) => void;
|
onRegionUpdated?: (regionId: string, updates: Partial<RegionUI>, expectedModelUpdates?: { startBeat: number, length: number }) => void;
|
||||||
onRegionClick?: (regionId: string) => void;
|
onRegionClick?: (regionId: string, options: RegionClickOptions) => void;
|
||||||
onOpenPianoRoll?: (regionId: string) => void;
|
onOpenPianoRoll?: (regionId: string) => void;
|
||||||
onOpenSpectrogram?: (regionId: string) => void;
|
onOpenSpectrogram?: (regionId: string) => void;
|
||||||
showHybridButtonForAudio?: boolean;
|
showHybridButtonForAudio?: boolean;
|
||||||
@@ -512,7 +512,7 @@ const TrackGridPanel: React.FC<TrackGridPanelProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Handle region click
|
// Handle region click
|
||||||
const handleRegionClick = (regionId: string) => {
|
const handleRegionClick = (regionId: string, options: RegionClickOptions) => {
|
||||||
if (DEBUG_MODE.TRACK_GRID_PANEL) {
|
if (DEBUG_MODE.TRACK_GRID_PANEL) {
|
||||||
console.log(`Region clicked in panel: ${regionId}`);
|
console.log(`Region clicked in panel: ${regionId}`);
|
||||||
}
|
}
|
||||||
@@ -535,7 +535,7 @@ const TrackGridPanel: React.FC<TrackGridPanelProps> = ({
|
|||||||
|
|
||||||
// Notify parent about the click
|
// Notify parent about the click
|
||||||
if (onRegionClick) {
|
if (onRegionClick) {
|
||||||
onRegionClick(regionId);
|
onRegionClick(regionId, options);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -450,11 +450,14 @@ export class KGCore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public addSelectedItem(item: Selectable): void {
|
public addSelectedItem(item: Selectable): void {
|
||||||
|
this.selectedItems = this.selectedItems.filter(i => i.getId() !== item.getId());
|
||||||
this.selectedItems.push(item);
|
this.selectedItems.push(item);
|
||||||
this.notifySelectionChanged();
|
this.notifySelectionChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
public addSelectedItems(items: Selectable[]): void {
|
public addSelectedItems(items: Selectable[]): void {
|
||||||
|
const seenIds = new Set(items.map(item => item.getId()));
|
||||||
|
this.selectedItems = this.selectedItems.filter(item => !seenIds.has(item.getId()));
|
||||||
this.selectedItems.push(...items);
|
this.selectedItems.push(...items);
|
||||||
this.notifySelectionChanged();
|
this.notifySelectionChanged();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { showAlert } from '../util/dialogUtil';
|
|||||||
*/
|
*/
|
||||||
export const useGlobalKeyboardHandler = () => {
|
export const useGlobalKeyboardHandler = () => {
|
||||||
const { undo, redo, setStatus, isPlaying, startPlaying, stopTransport, toggleLoop, projectName, savedProjectName, setSavedProjectName, setProjectName, isRecording, startRecording, stopRecording, activeRegionId, selectedRegionIds, setActiveRegionId, setShowPianoRoll, showPianoRoll, openMidiPianoRoll, openSpectrogramViewer } = useProjectStore();
|
const { undo, redo, setStatus, isPlaying, startPlaying, stopTransport, toggleLoop, projectName, savedProjectName, setSavedProjectName, setProjectName, isRecording, startRecording, stopRecording, activeRegionId, selectedRegionIds, setActiveRegionId, setShowPianoRoll, showPianoRoll, openMidiPianoRoll, openSpectrogramViewer } = useProjectStore();
|
||||||
|
const lastSelectedRegionId = selectedRegionIds[selectedRegionIds.length - 1] ?? null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
@@ -166,7 +167,7 @@ export const useGlobalKeyboardHandler = () => {
|
|||||||
setStatus('Recording stopped — notes committed');
|
setStatus('Recording stopped — notes committed');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const candidateId = activeRegionId ?? (selectedRegionIds[0] ?? null);
|
const candidateId = activeRegionId ?? lastSelectedRegionId;
|
||||||
if (!candidateId) {
|
if (!candidateId) {
|
||||||
setStatus('Select a MIDI region before recording');
|
setStatus('Select a MIDI region before recording');
|
||||||
return;
|
return;
|
||||||
@@ -201,7 +202,7 @@ export const useGlobalKeyboardHandler = () => {
|
|||||||
setShowPianoRoll(false);
|
setShowPianoRoll(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const candidateId = activeRegionId ?? (selectedRegionIds[0] ?? null);
|
const candidateId = activeRegionId ?? lastSelectedRegionId;
|
||||||
if (!candidateId) {
|
if (!candidateId) {
|
||||||
void showAlert('Please select a region to open the editor.');
|
void showAlert('Please select a region to open the editor.');
|
||||||
return;
|
return;
|
||||||
@@ -264,7 +265,7 @@ export const useGlobalKeyboardHandler = () => {
|
|||||||
startRecording,
|
startRecording,
|
||||||
stopRecording,
|
stopRecording,
|
||||||
activeRegionId,
|
activeRegionId,
|
||||||
selectedRegionIds,
|
lastSelectedRegionId,
|
||||||
setActiveRegionId,
|
setActiveRegionId,
|
||||||
setShowPianoRoll,
|
setShowPianoRoll,
|
||||||
showPianoRoll,
|
showPianoRoll,
|
||||||
|
|||||||
Reference in New Issue
Block a user