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:
+32
-2
@@ -7762,8 +7762,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ctrl+click: toggle selection (multi-select)
|
// Shift+click: toggle selection (multi-select) — user 09:30 (trước đây là
|
||||||
if (e.ctrlKey && !e.altKey && !e.shiftKey) {
|
// Ctrl+click — Ctrl giờ dành cho COPY-drag)
|
||||||
|
if (e.shiftKey && !e.altKey && !e.ctrlKey) {
|
||||||
if (clickedNoteIdx !== -1) {
|
if (clickedNoteIdx !== -1) {
|
||||||
const clickedNote = notes[clickedNoteIdx];
|
const clickedNote = notes[clickedNoteIdx];
|
||||||
if (selectedNoteIds.includes(clickedNote.id)) {
|
if (selectedNoteIds.includes(clickedNote.id)) {
|
||||||
@@ -7772,6 +7773,35 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
setSelectedNoteIds(prev => [...prev, clickedNote.id]);
|
setSelectedNoteIds(prev => [...prev, clickedNote.id]);
|
||||||
}
|
}
|
||||||
return;
|
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 {
|
} else {
|
||||||
// Ctrl+click on a dim (same-track) MIDI note → select it and focus its MIDI item
|
// Ctrl+click on a dim (same-track) MIDI note → select it and focus its MIDI item
|
||||||
var ctrlGhostHit = null;
|
var ctrlGhostHit = null;
|
||||||
|
|||||||
@@ -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
|
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
|
// 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
|
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(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à
|
||||||
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
|
// 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
|
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
|
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
|
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
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
|
<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/promptTemplateManager.js?v=202607281039"></script>
|
||||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></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">
|
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
@@ -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).
|
- **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).
|
- **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.
|
- **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).
|
||||||
|
|||||||
Reference in New Issue
Block a user