FIX: cho phép shift+click để chọn notes, drag để di chuyển các notes, ctrl+drag để copy nhanh các notes

This commit is contained in:
2026-08-07 21:48:14 +07:00
parent 688a7a57e2
commit 393df8fd7b
4 changed files with 55 additions and 5 deletions
+32 -2
View File
@@ -7762,8 +7762,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
}
}
// Ctrl+click: toggle selection (multi-select)
if (e.ctrlKey && !e.altKey && !e.shiftKey) {
// Shift+click: toggle selection (multi-select) user 09:30 (trưc đây là
// Ctrl+click Ctrl gi dành cho COPY-drag)
if (e.shiftKey && !e.altKey && !e.ctrlKey) {
if (clickedNoteIdx !== -1) {
const clickedNote = notes[clickedNoteIdx];
if (selectedNoteIds.includes(clickedNote.id)) {
@@ -7772,6 +7773,35 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
setSelectedNoteIds(prev => [...prev, clickedNote.id]);
}
return;
}
}
// Ctrl+click NOTE + drag COPY nhanh nhóm notes/note đến v trí mi
// (cùng pitch ban đu; drag đi v trí + pitch) user 09:30
if (e.ctrlKey && !e.altKey && !e.shiftKey) {
if (clickedNoteIdx !== -1) {
const clickedNote = notes[clickedNoteIdx];
const groupIds = (selectedNoteIds.includes(clickedNote.id) && selectedNoteIds.length > 1)
? selectedNoteIds : [clickedNote.id];
const src = notes.filter(n => groupIds.includes(n.id));
if (!src.length) return;
pushToUndo(notes);
notesBeforeDragRef.current = JSON.parse(JSON.stringify(notes));
const clones = src.map(n => ({ ...JSON.parse(JSON.stringify(n)), id: 'note_' + Date.now() + Math.random().toString(36).substr(2, 8) }));
setNotes(prev => [...prev, ...clones]);
const cloneIds = clones.map(c => c.id);
setSelectedNoteIds(cloneIds);
const cloneOffsets = clones.map(n => ({ id: n.id, originalStartBeat: n.start_beat, originalPitch: n.pitch }));
setDraggedNote({
mode: 'move',
idx: -1,
startOffsetBeat: beat - clickedNote.start_beat,
startOffsetPitch: pitch,
selectedNotesOffset: cloneOffsets,
clickedOriginalStartBeat: clickedNote.start_beat
});
showToast('Kéo để copy ' + clones.length + ' nốt.', 'info');
return;
} else {
// Ctrl+click on a dim (same-track) MIDI note select it and focus its MIDI item
var ctrlGhostHit = null;
+5 -2
View File
@@ -435,8 +435,11 @@ React.useEffect(function(){setFocusItemId(st.target_id);},[st.id,st.target_id]);
if(e.button===2){e.preventDefault();const clickedNote=notes.find(n=>{return pitch===n.pitch&&beat>=n.start_beat&&beat<n.start_beat+n.duration_beats;});if(clickedNote){pushToUndo(notes);setNotes(prev=>prev.filter(n=>n.id!==clickedNote.id));setSelectedNoteIds(prev=>prev.filter(id=>id!==clickedNote.id));swallowContextMenuRef.current=true;showToast('Đã xóa nốt!','info');}else{rightClickDragRef.current={active:true,startX:e.clientX,startY:e.clientY};}return;}if(e.button!==0)return;// Only handle left click
// Check if clicking on an existing note
const clickedNoteIdx=notes.findIndex(n=>{return pitch===n.pitch&&beat>=n.start_beat&&beat<n.start_beat+n.duration_beats;});// Click on note → play note with SoundFont
if(clickedNoteIdx!==-1&&!e.ctrlKey&&!e.shiftKey&&!e.altKey){if(window.SonicSF){const ctx=getAudioContext();var clTrk=activeTracks.find(function(t){return t.id===st.trackId;});var clCtx=resolveTrackInstrumentCtx(clTrk,activeTracks);ensureSonicInstrument(clCtx);window.SonicSF.playNote(notes[clickedNoteIdx].pitch,100,300,ctx.currentTime,clCtx.program,null,clCtx.ch,clCtx.synthEngine);}}// Ctrl+click: toggle selection (multi-select)
if(e.ctrlKey&&!e.altKey&&!e.shiftKey){if(clickedNoteIdx!==-1){const clickedNote=notes[clickedNoteIdx];if(selectedNoteIds.includes(clickedNote.id)){setSelectedNoteIds(prev=>prev.filter(id=>id!==clickedNote.id));}else{setSelectedNoteIds(prev=>[...prev,clickedNote.id]);}return;}else{// Ctrl+click on a dim (same-track) MIDI note → select it and focus its MIDI item
if(clickedNoteIdx!==-1&&!e.ctrlKey&&!e.shiftKey&&!e.altKey){if(window.SonicSF){const ctx=getAudioContext();var clTrk=activeTracks.find(function(t){return t.id===st.trackId;});var clCtx=resolveTrackInstrumentCtx(clTrk,activeTracks);ensureSonicInstrument(clCtx);window.SonicSF.playNote(notes[clickedNoteIdx].pitch,100,300,ctx.currentTime,clCtx.program,null,clCtx.ch,clCtx.synthEngine);}}// Shift+click: toggle selection (multi-select) — user 09:30 (trước đây là
// Ctrl+click — Ctrl giờ dành cho COPY-drag)
if(e.shiftKey&&!e.altKey&&!e.ctrlKey){if(clickedNoteIdx!==-1){const clickedNote=notes[clickedNoteIdx];if(selectedNoteIds.includes(clickedNote.id)){setSelectedNoteIds(prev=>prev.filter(id=>id!==clickedNote.id));}else{setSelectedNoteIds(prev=>[...prev,clickedNote.id]);}return;}}// Ctrl+click NOTE + drag → COPY nhanh nhóm notes/note đến vị trí mới
// (cùng pitch ban đầu; drag đổi vị trí + pitch) — user 09:30
if(e.ctrlKey&&!e.altKey&&!e.shiftKey){if(clickedNoteIdx!==-1){const clickedNote=notes[clickedNoteIdx];const groupIds=selectedNoteIds.includes(clickedNote.id)&&selectedNoteIds.length>1?selectedNoteIds:[clickedNote.id];const src=notes.filter(n=>groupIds.includes(n.id));if(!src.length)return;pushToUndo(notes);notesBeforeDragRef.current=JSON.parse(JSON.stringify(notes));const clones=src.map(n=>({...JSON.parse(JSON.stringify(n)),id:'note_'+Date.now()+Math.random().toString(36).substr(2,8)}));setNotes(prev=>[...prev,...clones]);const cloneIds=clones.map(c=>c.id);setSelectedNoteIds(cloneIds);const cloneOffsets=clones.map(n=>({id:n.id,originalStartBeat:n.start_beat,originalPitch:n.pitch}));setDraggedNote({mode:'move',idx:-1,startOffsetBeat:beat-clickedNote.start_beat,startOffsetPitch:pitch,selectedNotesOffset:cloneOffsets,clickedOriginalStartBeat:clickedNote.start_beat});showToast('Kéo để copy '+clones.length+' nốt.','info');return;}else{// Ctrl+click on a dim (same-track) MIDI note → select it and focus its MIDI item
var ctrlGhostHit=null;for(var cgi=0;cgi<ghostLayers.length;cgi++){var cgl=ghostLayers[cgi];if(!cgl.isSameTrack||!cgl.notes)continue;for(var cgn=0;cgn<cgl.notes.length;cgn++){var cgnote=cgl.notes[cgn];if(pitch===cgnote.pitch&&beat>=cgnote.relative_start_beat&&beat<cgnote.relative_start_beat+(cgnote.duration_beats||1)){ctrlGhostHit=cgnote;break;}}if(ctrlGhostHit)break;}if(ctrlGhostHit){setFocusItemId(ctrlGhostHit.item_id||st.target_id);if(selectedNoteIds.includes(ctrlGhostHit.id)){setSelectedNoteIds(prev=>prev.filter(id=>id!==ctrlGhostHit.id));}else{setSelectedNoteIds(prev=>[...prev,ctrlGhostHit.id]);}return;}// Ctrl+click on empty space: start selection marquee
setSelectedNoteIds([]);const snapStart=getSnapBeat(beat,snapValue);setSelectionMarquee({startBeat:snapStart,startPitch:pitch,currentBeat:snapStart,currentPitch:pitch});return;}}// Ctrl+Shift+click on note → split at click position
if(e.ctrlKey&&e.shiftKey){if(clickedNoteIdx!==-1){const target=notes[clickedNoteIdx];const splitBeat=getSnapBeat(beat,snapValue);if(splitBeat>target.start_beat+0.03125&&splitBeat<target.start_beat+target.duration_beats-0.03125){pushToUndo(notes);const noteA={...JSON.parse(JSON.stringify(target)),id:'note_'+Date.now()+Math.random().toString(36).substr(2,8),duration_beats:splitBeat-target.start_beat};const noteB={...JSON.parse(JSON.stringify(target)),id:'note_'+Date.now()+Math.random().toString(36).substr(2,8),start_beat:splitBeat,duration_beats:target.start_beat+target.duration_beats-splitBeat};setNotes(prev=>{const idx=prev.findIndex(n=>n.id===target.id);if(idx===-1)return prev;const result=[...prev];result.splice(idx,1,noteA);result.splice(idx+1,0,noteB);return result;});setSelectedNoteIds([noteA.id,noteB.id]);showToast('Đã tách nốt!','info');}}else{// Ctrl+Shift+click on empty space → duplicate selected + clicked notes
+1 -1
View File
@@ -24,7 +24,7 @@
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/app.precompiled.js?v=202608070925" defer></script>
<script src="/static/js/app.precompiled.js?v=202608070935" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {