feat: allow user to multi-select regions.
This commit is contained in:
@@ -24,6 +24,10 @@
|
||||
border-color: #ffffff;
|
||||
}
|
||||
|
||||
.track-region.selected-secondary {
|
||||
border-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.track-region.dragging {
|
||||
opacity: 0.8;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||
@@ -89,6 +93,10 @@
|
||||
border-color: #ffffff;
|
||||
}
|
||||
|
||||
.track-region.audio-region.selected-secondary {
|
||||
border-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.track-region.audio-region .region-header {
|
||||
background-color: #4a8b5a;
|
||||
}
|
||||
|
||||
@@ -68,12 +68,25 @@ describe('RegionItem', () => {
|
||||
fireEvent.mouseMove(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(onDrag).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', () => {
|
||||
const onClick = vi.fn();
|
||||
const onDragStart = vi.fn();
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useRef, useEffect } from 'react';
|
||||
import './Region.css';
|
||||
import { FaPencilAlt, FaPlus } from 'react-icons/fa';
|
||||
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 { KGMidiRegion } from '../../core/region/KGMidiRegion';
|
||||
import { KGAudioRegion } from '../../core/region/KGAudioRegion';
|
||||
@@ -27,7 +27,7 @@ interface RegionItemProps {
|
||||
onDrag?: (regionId: string, deltaX: number, deltaY: number) => void;
|
||||
onDragEnd?: (regionId: string) => void;
|
||||
// Click prop
|
||||
onClick?: (regionId: string) => void;
|
||||
onClick?: (regionId: string, options: RegionClickOptions) => void;
|
||||
// Explicit open piano roll action from header pencil icon
|
||||
onOpenPianoRoll?: (regionId: string) => void;
|
||||
// Open spectrogram viewer for audio regions
|
||||
@@ -70,6 +70,7 @@ const RegionItem: React.FC<RegionItemProps> = ({
|
||||
// Get selection state and time signature from store
|
||||
const { selectedRegionIds, timeSignature, bpm } = useProjectStore();
|
||||
const isSelected = selectedRegionIds.includes(id);
|
||||
const isPrimarySelected = isSelected && selectedRegionIds[selectedRegionIds.length - 1] === id;
|
||||
const [cursor, setCursor] = useState<string>('pointer');
|
||||
const [resizeEdge, setResizeEdge] = useState<ResizeAction>('none');
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
@@ -408,7 +409,7 @@ const RegionItem: React.FC<RegionItemProps> = ({
|
||||
if (DEBUG_MODE.REGION_ITEM) {
|
||||
console.log(`REGION CLICKED (pencil mode): regionId=${id}`);
|
||||
}
|
||||
onClick(id);
|
||||
onClick(id, { shiftKey: e.shiftKey });
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -535,7 +536,7 @@ const RegionItem: React.FC<RegionItemProps> = ({
|
||||
if (DEBUG_MODE.REGION_ITEM) {
|
||||
console.log(`REGION CLICKED: regionId=${id}`);
|
||||
}
|
||||
onClick(id);
|
||||
onClick(id, { shiftKey: e.shiftKey });
|
||||
}
|
||||
|
||||
isPendingDragRef.current = false;
|
||||
@@ -602,7 +603,7 @@ const RegionItem: React.FC<RegionItemProps> = ({
|
||||
return (
|
||||
<div
|
||||
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 } : {}) }}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
@@ -634,7 +635,7 @@ const RegionItem: React.FC<RegionItemProps> = ({
|
||||
if (onOpenPianoRoll) {
|
||||
onOpenPianoRoll(id);
|
||||
} else if (onClick) {
|
||||
onClick(id);
|
||||
onClick(id, { shiftKey: e.shiftKey });
|
||||
}
|
||||
}}
|
||||
aria-label="Open piano roll"
|
||||
|
||||
@@ -4,7 +4,7 @@ import { KGMidiRegion } from '../../core/region/KGMidiRegion';
|
||||
import { KGAudioRegion } from '../../core/region/KGAudioRegion';
|
||||
import { KGAudioInterface } from '../../core/audio-interface/KGAudioInterface';
|
||||
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 { KGMainContentState } from '../../core/state/KGMainContentState';
|
||||
import { isModifierKeyPressed } from '../../util/osUtil';
|
||||
@@ -25,7 +25,7 @@ interface TrackGridItemProps {
|
||||
onRegionDrag?: (regionId: string, newBarNumber: number, newTrackIndex: number) => void;
|
||||
onRegionDragEnd?: (regionId: string, finalBarNumber: number, finalTrackIndex: number) => void;
|
||||
onRegionFineMoveEnd?: (regionId: string, deltaInBars: number) => void;
|
||||
onRegionClick?: (regionId: string) => void;
|
||||
onRegionClick?: (regionId: string, options: RegionClickOptions) => void;
|
||||
onOpenPianoRoll?: (regionId: string) => void;
|
||||
onOpenSpectrogram?: (regionId: string) => void;
|
||||
showHybridButtonForAudio?: boolean;
|
||||
@@ -516,13 +516,13 @@ const TrackGridItem: React.FC<TrackGridItemProps> = ({
|
||||
};
|
||||
|
||||
// Handle region click
|
||||
const handleRegionClick = (regionId: string) => {
|
||||
const handleRegionClick = (regionId: string, options: RegionClickOptions) => {
|
||||
if (DEBUG_MODE.TRACK_GRID_ITEM) {
|
||||
console.log(`Region clicked: ${regionId}`);
|
||||
}
|
||||
|
||||
if (onRegionClick) {
|
||||
onRegionClick(regionId);
|
||||
onRegionClick(regionId, options);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -589,7 +589,7 @@ const TrackGridItem: React.FC<TrackGridItemProps> = ({
|
||||
onOpenPianoRoll(regionId);
|
||||
} else if (onRegionClick) {
|
||||
// Fallback to legacy behavior
|
||||
onRegionClick(regionId);
|
||||
onRegionClick(regionId, { shiftKey: false });
|
||||
}
|
||||
}}
|
||||
onOpenSpectrogram={audioRegion ? (regionId) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { KGTrack, TrackType } from '../../core/track/KGTrack';
|
||||
import { KGMidiRegion } from '../../core/region/KGMidiRegion';
|
||||
import TrackGridItem from './TrackGridItem';
|
||||
import { Playhead, FileImportModal } from '../common';
|
||||
import type { RegionUI } from '../interfaces';
|
||||
import type { RegionClickOptions, RegionUI } from '../interfaces';
|
||||
import { DEBUG_MODE, REGION_CONSTANTS } from '../../constants';
|
||||
import { KGMainContentState } from '../../core/state/KGMainContentState';
|
||||
import { isModifierKeyPressed } from '../../util/osUtil';
|
||||
@@ -28,7 +28,7 @@ interface TrackGridPanelProps {
|
||||
projectName: string;
|
||||
onRegionCreated: (trackIndex: number, region: RegionUI, midiRegion: KGMidiRegion) => 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;
|
||||
onOpenSpectrogram?: (regionId: string) => void;
|
||||
showHybridButtonForAudio?: boolean;
|
||||
@@ -512,7 +512,7 @@ const TrackGridPanel: React.FC<TrackGridPanelProps> = ({
|
||||
};
|
||||
|
||||
// Handle region click
|
||||
const handleRegionClick = (regionId: string) => {
|
||||
const handleRegionClick = (regionId: string, options: RegionClickOptions) => {
|
||||
if (DEBUG_MODE.TRACK_GRID_PANEL) {
|
||||
console.log(`Region clicked in panel: ${regionId}`);
|
||||
}
|
||||
@@ -535,7 +535,7 @@ const TrackGridPanel: React.FC<TrackGridPanelProps> = ({
|
||||
|
||||
// Notify parent about the click
|
||||
if (onRegionClick) {
|
||||
onRegionClick(regionId);
|
||||
onRegionClick(regionId, options);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user