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 {
+17
View File
@@ -2881,3 +2881,20 @@
- **FIX (app.jsx keydown 7106 — Ctrl+V):** `pasteBeat = st.currentTime / (60/bpm)` (playhead → beat) — `offset = pasteBeat - minStart(clipboard)` — dán với `start_beat = n.start_beat + offset` (note đầu tiên nằm đúng playhead; clamp ≥ 0).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070925), `wiki.md`. Rebuild precompiled (build PASS — pasteBeat ×2).
- **Ghi chú/Test:** hard refresh → PIANO ROLL — di chuyển playhead → Ctrl+V → notes dán bắt đầu đúng vị trí playhead.
### [2026-08-07 09:30] Task: PIANO ROLL — Shift+click toggle chọn; Ctrl+click+drag COPY nhanh; drag move pitch
- **Yêu cầu user:** (1) Ctrl+Click → Shift+Click để thêm/bớt note vào nhóm chọn; (2) Ctrl+Click giữ + drag → copy nhanh nhóm notes/note đến vị trí mới (cùng pitch — drag đổi vị trí/pitch); (3) drag note/nhóm → vị trí + pitch khác.
- **Trạng thái:** (3) ĐÃ CÓ (7895-7922 — mode 'move' — deltaBeat + deltaPitch).
- **FIX (app.jsx PianoRollTabEditor handleGridMouseDown):**
(1) toggle selection: `e.ctrlKey``e.shiftKey` (Shift+click thêm/bớt note).
(2) Ctrl+click TRÊN NOTE → clone notes (nhóm chọn nếu note trong nhóm, ngược lại note đơn) + setDraggedNote mode 'move' (selectedNotesOffset + clickedOriginalStartBeat) → kéo clones đến vị trí/pitch mới (gốc giữ) — mousemove move có sẵn.
(3) Ctrl+click empty → marquee (giữ); Ctrl+Shift+click → split (giữ).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070930), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → PIANO ROLL — Shift+click thêm/bớt note chọn; Ctrl+giữ+click note → kéo → copy nhóm đến vị trí mới; drag note (không modifier) → move vị trí + pitch.
### [2026-08-07 09:35] Task: FIX Ctrl+click+drag copy — notes không bám vị trí con trỏ
- **Báo cáo user:** ctrl-click-drag — notes không được copy ngay tại vị trí con trỏ chuột.
- **Nguyên nhân:** copy-drag set `startOffsetBeat: beat` (vị trí chuột) — move mousemove tính `deltaBeat = snap(beatMouse - startOffsetBeat) - refOrigStart` → delta bị lệch (Δ=0 → -noteStart → clones nhảy về beat 0 / không bám chuột). Move thường dùng `startOffsetBeat = beat - clickedNote.start_beat` (offset trong note).
- **FIX (app.jsx):** copy-drag → `startOffsetBeat: beat - clickedNote.start_beat` (giống move thường) → kéo → delta = snap(Δ) → clones bám chuột (vị trí + pitch).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070935), `wiki.md`. Rebuild precompiled (build PASS — 2 chỗ đồng bộ).
- **Ghi chú/Test:** hard refresh → PIANO ROLL — Ctrl+click note → kéo → các bản copy theo đúng vị trí con trỏ (beat + pitch).