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:
+57
-8
@@ -388,6 +388,8 @@ const WaveformLane = ({
|
|||||||
getLocalAnchor,
|
getLocalAnchor,
|
||||||
onClearLocalSelection,
|
onClearLocalSelection,
|
||||||
onDeselectItem,
|
onDeselectItem,
|
||||||
|
onAddToSelection,
|
||||||
|
onSetPendingDrag,
|
||||||
onSetSelectionMode,
|
onSetSelectionMode,
|
||||||
onSetSelectionStart,
|
onSetSelectionStart,
|
||||||
onSetSelectionEnd,
|
onSetSelectionEnd,
|
||||||
@@ -1080,10 +1082,18 @@ const WaveformLane = ({
|
|||||||
if (hitItem && !e.altKey && !e.shiftKey) {
|
if (hitItem && !e.altKey && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
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);
|
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 {
|
} 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;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1139,15 +1149,18 @@ 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) {
|
if (e.ctrlKey) {
|
||||||
|
// Ctrl+Click: toggle selection, store pending drag
|
||||||
var clipCanonicalId = clickedClip.id === 'default' ? 'default_' + track.id : clickedClip.id;
|
var clipCanonicalId = clickedClip.id === 'default' ? 'default_' + track.id : clickedClip.id;
|
||||||
if (selectedItemIds.has(clipCanonicalId)) {
|
if (selectedItemIds && selectedItemIds.has(clipCanonicalId)) {
|
||||||
if (onDeselectItem) onDeselectItem(clipCanonicalId);
|
if (onDeselectItem) onDeselectItem(clipCanonicalId);
|
||||||
return;
|
} else if (onAddToSelection) {
|
||||||
|
onAddToSelection(clipCanonicalId);
|
||||||
}
|
}
|
||||||
}
|
if (onSetPendingDrag) onSetPendingDrag(track.id, 'clip', clipCanonicalId, time - clickedClip.startTime, e.nativeEvent || e);
|
||||||
if (onClipDragStart) {
|
} else {
|
||||||
onClipDragStart(track.id, clickedClip.id, time - clickedClip.startTime, e.ctrlKey);
|
// Alt+Click: move immediately
|
||||||
|
if (onClipDragStart) onClipDragStart(track.id, clickedClip.id, time - clickedClip.startTime, false);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -6934,6 +6947,7 @@ const App = () => {
|
|||||||
const sweepStartRef = useRef(0);
|
const sweepStartRef = useRef(0);
|
||||||
const sweepTrackIdRef = useRef(null);
|
const sweepTrackIdRef = useRef(null);
|
||||||
const sweepSelectRef = useRef(null);
|
const sweepSelectRef = useRef(null);
|
||||||
|
const pendingDragRef = useRef(null); // { trackId, itemType, itemId, clickOffset, startX, startY }
|
||||||
const [localSelectionTrackId, setLocalSelectionTrackId] = useState(null);
|
const [localSelectionTrackId, setLocalSelectionTrackId] = useState(null);
|
||||||
const [localSelectionStart, setLocalSelectionStart] = useState(null);
|
const [localSelectionStart, setLocalSelectionStart] = useState(null);
|
||||||
const [localSelectionEnd, setLocalSelectionEnd] = 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 ──
|
// ── Sweep Select ──
|
||||||
const handleSweepSelectStart = (trackId, startTime) => {
|
const handleSweepSelectStart = (trackId, startTime) => {
|
||||||
isSweepingRef.current = true;
|
isSweepingRef.current = true;
|
||||||
@@ -11841,6 +11867,27 @@ const App = () => {
|
|||||||
};
|
};
|
||||||
}, [zoom, activeTab, sessionTabs]);
|
}, [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 ──
|
// ── Document-level mousemove/mouseup for Section/MIDI item resize ──
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleMouseMove = e => {
|
const handleMouseMove = e => {
|
||||||
@@ -16522,6 +16569,8 @@ const App = () => {
|
|||||||
onTrackLaneMouseDown: handleTrackLaneMouseDown,
|
onTrackLaneMouseDown: handleTrackLaneMouseDown,
|
||||||
onSweepSelectStart: handleSweepSelectStart,
|
onSweepSelectStart: handleSweepSelectStart,
|
||||||
onDeselectItem: handleDeselectItem,
|
onDeselectItem: handleDeselectItem,
|
||||||
|
onAddToSelection: handleAddToSelection,
|
||||||
|
onSetPendingDrag: handleSetPendingDrag,
|
||||||
onContextMenu: handleContextMenu,
|
onContextMenu: handleContextMenu,
|
||||||
onClipDragStart: handleClipDragStart,
|
onClipDragStart: handleClipDragStart,
|
||||||
onClipStretchStart: handleClipStretchStart,
|
onClipStretchStart: handleClipStretchStart,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user