feat: Ctrl+click deselect + global empty-area sweep

- Ctrl+click on selected item: deselect that item only
- Ctrl+click on empty space (via sweep start): deselect all items
- Ctrl+drag on empty space BETWEEN or BELOW tracks: creates
  sweep overlay scanning ALL tracks for intersecting items
- onDeselectItem handler + prop wired through WaveformLane
- Global onMouseDown on tracks container div for empty-area sweep
This commit is contained in:
2026-07-27 21:59:33 +07:00
parent 85ad22ee4f
commit e055e8b5c8
2 changed files with 46 additions and 9 deletions
+39 -3
View File
@@ -387,6 +387,7 @@ const WaveformLane = ({
currentTime, currentTime,
getLocalAnchor, getLocalAnchor,
onClearLocalSelection, onClearLocalSelection,
onDeselectItem,
onSetSelectionMode, onSetSelectionMode,
onSetSelectionStart, onSetSelectionStart,
onSetSelectionEnd, onSetSelectionEnd,
@@ -1079,7 +1080,11 @@ const WaveformLane = ({
if (hitItem && !e.altKey && !e.shiftKey) { if (hitItem && !e.altKey && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (onSectionItemDragStart) onSectionItemDragStart(track.id, hitItem.type, hitItem.id, time - hitItem.start, e.ctrlKey); if (e.ctrlKey && selectedItemIds && selectedItemIds.has(hitItem.id)) {
if (onDeselectItem) onDeselectItem(hitItem.id);
} else {
if (onSectionItemDragStart) onSectionItemDragStart(track.id, hitItem.type, hitItem.id, time - hitItem.start, e.ctrlKey);
}
return; return;
} }
@@ -1134,13 +1139,20 @@ const WaveformLane = ({
if (clickedClip && (e.altKey || e.ctrlKey)) { if (clickedClip && (e.altKey || e.ctrlKey)) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (e.ctrlKey && selectedItemIds) {
var clipCanonicalId = clickedClip.id === 'default' ? 'default_' + track.id : clickedClip.id;
if (selectedItemIds.has(clipCanonicalId)) {
if (onDeselectItem) onDeselectItem(clipCanonicalId);
return;
}
}
if (onClipDragStart) { if (onClipDragStart) {
onClipDragStart(track.id, clickedClip.id, time - clickedClip.startTime, e.ctrlKey); onClipDragStart(track.id, clickedClip.id, time - clickedClip.startTime, e.ctrlKey);
} }
return; return;
} }
// Ctrl+Click on empty space: start sweep select // Ctrl+Click on empty space: sweep select (deselect all via onSweepSelectStart)
if (e.ctrlKey && !clickedClip && !hitItem) { if (e.ctrlKey && !clickedClip && !hitItem) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@@ -11620,6 +11632,15 @@ const App = () => {
}; };
}, [zoom]); }, [zoom]);
// Deselect single item
const handleDeselectItem = itemId => {
setSelectedItemIds(prev => {
var next = new Set(prev);
next.delete(itemId);
return next;
});
};
// Sweep Select // Sweep Select
const handleSweepSelectStart = (trackId, startTime) => { const handleSweepSelectStart = (trackId, startTime) => {
isSweepingRef.current = true; isSweepingRef.current = true;
@@ -11902,7 +11923,9 @@ const App = () => {
const handleMouseUp = () => { const handleMouseUp = () => {
if (!isSweepingRef.current) return; if (!isSweepingRef.current) return;
isSweepingRef.current = false; isSweepingRef.current = false;
sweepTrackIdRef.current = null;
const sweep = sweepSelectRef.current; const sweep = sweepSelectRef.current;
sweepSelectRef.current = null;
if (sweep) { if (sweep) {
const start = Math.min(sweep.startTime, sweep.endTime); const start = Math.min(sweep.startTime, sweep.endTime);
const end = Math.max(sweep.startTime, sweep.endTime); const end = Math.max(sweep.startTime, sweep.endTime);
@@ -16456,7 +16479,19 @@ const App = () => {
borderRight: '1px solid #f59e0b' borderRight: '1px solid #f59e0b'
} }
})), /*#__PURE__*/React.createElement("div", { })), /*#__PURE__*/React.createElement("div", {
className: "flex-1 flex flex-col divide-y divide-[#141414] relative bg-[#111111] min-h-full" className: "flex-1 flex flex-col divide-y divide-[#141414] relative bg-[#111111] min-h-full",
onMouseDown: e => {
if (e.ctrlKey && !e.shiftKey && !e.altKey && e.button === 0) {
const wrapper = timelineWrapperRef.current;
if (wrapper) {
const rect = wrapper.getBoundingClientRect();
const sl = wrapper.scrollLeft;
const raw = Math.max(0, (e.clientX - rect.left + sl) / zoom - leadInMargin);
const time = snapValue !== 'free' ? snapTime(raw, snapValue, bpm) : raw;
handleSweepSelectStart(null, time);
}
}
}
}, activeTracks.map((track, idx) => { }, activeTracks.map((track, idx) => {
const isSelected = selectedTrackId === track.id; const isSelected = selectedTrackId === track.id;
const autoHeight = track.height || (track.isArmed ? 164 : 140); const autoHeight = track.height || (track.isArmed ? 164 : 140);
@@ -16486,6 +16521,7 @@ const App = () => {
markers: track.markers, markers: track.markers,
onTrackLaneMouseDown: handleTrackLaneMouseDown, onTrackLaneMouseDown: handleTrackLaneMouseDown,
onSweepSelectStart: handleSweepSelectStart, onSweepSelectStart: handleSweepSelectStart,
onDeselectItem: handleDeselectItem,
onContextMenu: handleContextMenu, onContextMenu: handleContextMenu,
onClipDragStart: handleClipDragStart, onClipDragStart: handleClipDragStart,
onClipStretchStart: handleClipStretchStart, onClipStretchStart: handleClipStretchStart,
File diff suppressed because one or more lines are too long