refactor: Ctrl+click toggles selection, Ctrl+click+drag copies group

Changed behavior:
- Ctrl+Click on item (no drag): toggle selection (add/remove)
- Ctrl+Click+Drag on item (movement > 5px): copy selected group
- Alt+Click: move item immediately
- No modifier + click selected item: move group

Added pendingDragRef + useEffect to detect mousemove threshold
before starting copy-drag. onAddToSelection + onSetPendingDrag
props wired through WaveformLane.
This commit is contained in:
2026-07-27 22:14:01 +07:00
parent e055e8b5c8
commit c99527e687
2 changed files with 73 additions and 16 deletions
+57 -8
View File
@@ -388,6 +388,8 @@ const WaveformLane = ({
getLocalAnchor,
onClearLocalSelection,
onDeselectItem,
onAddToSelection,
onSetPendingDrag,
onSetSelectionMode,
onSetSelectionStart,
onSetSelectionEnd,
@@ -1080,10 +1082,18 @@ const WaveformLane = ({
if (hitItem && !e.altKey && !e.shiftKey) {
e.preventDefault();
e.stopPropagation();
if (e.ctrlKey && selectedItemIds && selectedItemIds.has(hitItem.id)) {
if (e.ctrlKey) {
// Ctrl+Click: toggle selection only, never start drag immediately
if (selectedItemIds && selectedItemIds.has(hitItem.id)) {
if (onDeselectItem) onDeselectItem(hitItem.id);
} else if (onAddToSelection) {
onAddToSelection(hitItem.id);
}
// Store pending drag so mousemove can start copy-drag
if (onSetPendingDrag) onSetPendingDrag(track.id, hitItem.type, hitItem.id, time - hitItem.start, e.nativeEvent || e);
} else {
if (onSectionItemDragStart) onSectionItemDragStart(track.id, hitItem.type, hitItem.id, time - hitItem.start, e.ctrlKey);
// No modifier: start drag immediately (single or multi move)
if (onSectionItemDragStart) onSectionItemDragStart(track.id, hitItem.type, hitItem.id, time - hitItem.start, false);
}
return;
}
@@ -1139,15 +1149,18 @@ const WaveformLane = ({
if (clickedClip && (e.altKey || e.ctrlKey)) {
e.preventDefault();
e.stopPropagation();
if (e.ctrlKey && selectedItemIds) {
if (e.ctrlKey) {
// Ctrl+Click: toggle selection, store pending drag
var clipCanonicalId = clickedClip.id === 'default' ? 'default_' + track.id : clickedClip.id;
if (selectedItemIds.has(clipCanonicalId)) {
if (selectedItemIds && selectedItemIds.has(clipCanonicalId)) {
if (onDeselectItem) onDeselectItem(clipCanonicalId);
return;
} else if (onAddToSelection) {
onAddToSelection(clipCanonicalId);
}
}
if (onClipDragStart) {
onClipDragStart(track.id, clickedClip.id, time - clickedClip.startTime, e.ctrlKey);
if (onSetPendingDrag) onSetPendingDrag(track.id, 'clip', clipCanonicalId, time - clickedClip.startTime, e.nativeEvent || e);
} else {
// Alt+Click: move immediately
if (onClipDragStart) onClipDragStart(track.id, clickedClip.id, time - clickedClip.startTime, false);
}
return;
}
@@ -6934,6 +6947,7 @@ const App = () => {
const sweepStartRef = useRef(0);
const sweepTrackIdRef = useRef(null);
const sweepSelectRef = useRef(null);
const pendingDragRef = useRef(null); // { trackId, itemType, itemId, clickOffset, startX, startY }
const [localSelectionTrackId, setLocalSelectionTrackId] = useState(null);
const [localSelectionStart, setLocalSelectionStart] = useState(null);
const [localSelectionEnd, setLocalSelectionEnd] = useState(null);
@@ -11641,6 +11655,18 @@ const App = () => {
});
};
// Add item to selectedItemIds
const handleAddToSelection = itemId => {
setSelectedItemIds(prev => {
var next = new Set(prev);
next.add(itemId);
return next;
});
};
const handleSetPendingDrag = (trackId, itemType, itemId, clickOffset, e) => {
pendingDragRef.current = { trackId, itemType, itemId, clickOffset, startX: e.clientX, startY: e.clientY };
};
// Sweep Select
const handleSweepSelectStart = (trackId, startTime) => {
isSweepingRef.current = true;
@@ -11841,6 +11867,27 @@ const App = () => {
};
}, [zoom, activeTab, sessionTabs]);
// Pending drag: Ctrl+click toggles selection; mousemove > threshold starts copy-drag
useEffect(() => {
const handleMouseMove = e => {
var pd = pendingDragRef.current;
if (!pd) return;
var dx = e.clientX - pd.startX;
if (Math.abs(dx) > 5) {
pendingDragRef.current = null;
handleSectionItemDragStart(pd.trackId, pd.itemType, pd.itemId, pd.clickOffset, true);
}
};
document.addEventListener('mousemove', handleMouseMove);
var handleMouseUp = function() { pendingDragRef.current = null; };
document.addEventListener('mouseup', handleMouseUp);
return function() {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
pendingDragRef.current = null;
};
}, []);
// Document-level mousemove/mouseup for Section/MIDI item resize
useEffect(() => {
const handleMouseMove = e => {
@@ -16522,6 +16569,8 @@ const App = () => {
onTrackLaneMouseDown: handleTrackLaneMouseDown,
onSweepSelectStart: handleSweepSelectStart,
onDeselectItem: handleDeselectItem,
onAddToSelection: handleAddToSelection,
onSetPendingDrag: handleSetPendingDrag,
onContextMenu: handleContextMenu,
onClipDragStart: handleClipDragStart,
onClipStretchStart: handleClipStretchStart,
File diff suppressed because one or more lines are too long