feat: ctrl+click dim note selects it and focuses its MIDI item
This commit is contained in:
+27
-3
@@ -6280,10 +6280,11 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
var isSameTrackLayer = layer.isSameTrack;
|
||||
layer.notes.forEach(function(note) {
|
||||
var isFocusedNote = isSameTrackLayer && note.item_id === focusedItemId;
|
||||
var isGhostSelected = selectedNoteIds.indexOf(note.id) !== -1;
|
||||
if (isSameTrackLayer) {
|
||||
ctx.globalAlpha = isFocusedNote ? 0.7 : 0.3;
|
||||
ctx.fillStyle = isFocusedNote ? 'rgba(253, 224, 71, 0.7)' : 'rgba(251, 191, 36, 0.3)';
|
||||
ctx.strokeStyle = isFocusedNote ? '#fde047' : 'rgba(245, 158, 11, 0.6)';
|
||||
ctx.fillStyle = isGhostSelected ? 'rgba(96, 165, 250, 0.65)' : (isFocusedNote ? 'rgba(253, 224, 71, 0.7)' : 'rgba(251, 191, 36, 0.3)');
|
||||
ctx.strokeStyle = isGhostSelected ? '#60a5fa' : (isFocusedNote ? '#fde047' : 'rgba(245, 158, 11, 0.6)');
|
||||
} else {
|
||||
ctx.globalAlpha = 0.25;
|
||||
ctx.fillStyle = layer.track_color || '#888';
|
||||
@@ -6297,7 +6298,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
var w = Math.max(2, (snapEnd - snapStart) * pixelsPerBeat);
|
||||
var h = NoteHeight - 1;
|
||||
ctx.fillRect(x, y, w, h);
|
||||
if (isSameTrackLayer && isFocusedNote) ctx.strokeRect(x + 0.5, y + 0.5, w - 1, h - 1);
|
||||
if (isSameTrackLayer && (isFocusedNote || isGhostSelected)) ctx.strokeRect(x + 0.5, y + 0.5, w - 1, h - 1);
|
||||
});
|
||||
ctx.restore();
|
||||
});
|
||||
@@ -6527,6 +6528,29 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
||||
}
|
||||
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);
|
||||
|
||||
@@ -180,7 +180,7 @@ const scrollBeat=mx/pixelsPerBeat-renderBeatOffset;const clickedNote=notes.find(
|
||||
const container=gridScrollRef.current;if(container)container.scrollLeft+=e.deltaY;}return;}if(e.altKey){e.preventDefault();const scrollDelta=e.deltaY;const beatSec=60.0/(parseInt(bpm)||120);const step=scrollDelta<0?-0.25:0.25;const currentBeat=(st.currentTime||0)/beatSec;const maxBeats=totalBeats;const newBeat=Math.max(0,Math.min(maxBeats,currentBeat+step));const newTime=newBeat*beatSec;setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:newTime}:s));if(window.SonicSF){const ctx=getAudioContext();const playing=notes.filter(n=>currentBeat<n.start_beat&&newBeat>=n.start_beat);var pvTrk=activeTracks.find(function(t){return t.id===st.trackId;});var pvCh=window.SonicPianoRoll?window.SonicPianoRoll.getTrackMidiChannel(pvTrk,activeTracks):pvTrk?pvTrk.midiChannel:0;playing.forEach(n=>{window.SonicSF.playNote(n.pitch,(n.velocity||0.8)*127,200,ctx.currentTime,st.instrumentProgram,null,pvCh,pvTrk?pvTrk.synth_engine:undefined);});}}};const canvas=canvasRef.current;if(canvas){canvas.addEventListener('wheel',handleCanvasWheel,{passive:false});}return()=>{if(canvas){canvas.removeEventListener('wheel',handleCanvasWheel);}};},[notes,st.currentTime,pixelsPerBeat,st.id,totalBeats,bpm]);React.useLayoutEffect(()=>{const canvas=canvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;const h=128*NoteHeight;canvas.width=viewWidth*dpr;canvas.height=h*dpr;ctx.scale(dpr,dpr);// Draw background rows
|
||||
for(let pitch=0;pitch<128;pitch++){const y=(127-pitch)*NoteHeight;const isBlack=[1,3,6,8,10].includes(pitch%12);ctx.fillStyle=isBlack?'#1a1a1e':'#25252a';ctx.fillRect(0,y,viewWidth,NoteHeight);ctx.strokeStyle='#2d2d35';ctx.lineWidth=0.5;ctx.beginPath();ctx.moveTo(0,y+NoteHeight);ctx.lineTo(viewWidth,y+NoteHeight);ctx.stroke();}// Draw snap lines
|
||||
let snapBeats=0.25;if(snapValue==='1')snapBeats=1.0;else if(snapValue==='1/2')snapBeats=0.5;else if(snapValue==='1/4')snapBeats=0.25;else if(snapValue==='1/8')snapBeats=0.125;else if(snapValue==='1/16')snapBeats=0.0625;else if(snapValue==='4')snapBeats=4.0;else if(snapValue==='1/32')snapBeats=0.03125;for(let beat=0;beat<=viewBeats;beat+=snapBeats){const x=beat*pixelsPerBeat;if(x>viewWidth)break;const isBar=beat%timeSigNum===0;ctx.strokeStyle=isBar?'#444450':'#2d2d35';ctx.lineWidth=isBar?1.2:0.6;ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,h);ctx.stroke();}var focusedItemId=focusItemId||st.target_id;if(selectedNoteIds&&selectedNoteIds.length>0){focusedItemId=st.target_id;}else if(st.isPlaying&&st.currentTime!=null&&activeTracks){var curSec=st.currentTime||0;var fidxTrk=activeTracks.find(function(t){return t.id===st.trackId;});var itemsArr=fidxTrk?(fidxTrk.midiItems||[]):[];for(var fi=0;fi<itemsArr.length;fi++){var fit=itemsArr[fi];var fStart=fit.startTime||0;var fEnd=fStart+(fit.duration||4);if(curSec>=fStart&&curSec<fEnd){focusedItemId=fit.id;break;}}}// Layer 2: Ghost Notes (background reference from other tracks)
|
||||
if(showGhostNotes&&sessionSyncMode&&ghostLayers.length>0){ghostLayers.forEach(function(layer){ctx.save();var isSameTrackLayer=layer.isSameTrack;layer.notes.forEach(function(note){var isFocusedNote=isSameTrackLayer&¬e.item_id===focusedItemId;if(isSameTrackLayer){ctx.globalAlpha=isFocusedNote?0.7:0.3;ctx.fillStyle=isFocusedNote?'rgba(253, 224, 71, 0.7)':'rgba(251, 191, 36, 0.3)';ctx.strokeStyle=isFocusedNote?'#fde047':'rgba(245, 158, 11, 0.6)';}else{ctx.globalAlpha=0.25;ctx.fillStyle=layer.track_color||'#888';ctx.strokeStyle=layer.track_color||'#888';}var snapStart=snapValue!=='free'?getSnapBeat(note.relative_start_beat,snapValue):note.relative_start_beat;var rawEnd=note.relative_start_beat+note.duration_beats;var snapEnd=snapValue!=='free'?getSnapBeat(rawEnd,snapValue):rawEnd;var x=(renderBeatOffset+snapStart)*pixelsPerBeat;var y=(127-note.pitch)*NoteHeight;var w=Math.max(2,(snapEnd-snapStart)*pixelsPerBeat);var h=NoteHeight-1;ctx.fillRect(x,y,w,h);if(isSameTrackLayer&&isFocusedNote)ctx.strokeRect(x+0.5,y+0.5,w-1,h-1);});ctx.restore();});}// Layer 3: Active notes with velocity layer representation
|
||||
if(showGhostNotes&&sessionSyncMode&&ghostLayers.length>0){ghostLayers.forEach(function(layer){ctx.save();var isSameTrackLayer=layer.isSameTrack;layer.notes.forEach(function(note){var isFocusedNote=isSameTrackLayer&¬e.item_id===focusedItemId;var isGhostSelected=selectedNoteIds.indexOf(note.id)!==-1;if(isSameTrackLayer){ctx.globalAlpha=isFocusedNote?0.7:0.3;ctx.fillStyle=isGhostSelected?'rgba(96, 165, 250, 0.65)':(isFocusedNote?'rgba(253, 224, 71, 0.7)':'rgba(251, 191, 36, 0.3)');ctx.strokeStyle=isGhostSelected?'#60a5fa':(isFocusedNote?'#fde047':'rgba(245, 158, 11, 0.6)');}else{ctx.globalAlpha=0.25;ctx.fillStyle=layer.track_color||'#888';ctx.strokeStyle=layer.track_color||'#888';}var snapStart=snapValue!=='free'?getSnapBeat(note.relative_start_beat,snapValue):note.relative_start_beat;var rawEnd=note.relative_start_beat+note.duration_beats;var snapEnd=snapValue!=='free'?getSnapBeat(rawEnd,snapValue):rawEnd;var x=(renderBeatOffset+snapStart)*pixelsPerBeat;var y=(127-note.pitch)*NoteHeight;var w=Math.max(2,(snapEnd-snapStart)*pixelsPerBeat);var h=NoteHeight-1;ctx.fillRect(x,y,w,h);if(isSameTrackLayer&&(isFocusedNote||isGhostSelected))ctx.strokeRect(x+0.5,y+0.5,w-1,h-1);});ctx.restore();});}// Layer 3: Active notes with velocity layer representation
|
||||
notes.forEach(note=>{const snapStart=snapValue!=='free'?getSnapBeat(note.start_beat,snapValue):note.start_beat;const rawEnd=note.start_beat+note.duration_beats;const snapEnd=snapValue!=='free'?getSnapBeat(rawEnd,snapValue):rawEnd;const x=(renderBeatOffset+snapStart)*pixelsPerBeat;const y=(127-note.pitch)*NoteHeight;const w=Math.max(2,(snapEnd-snapStart)*pixelsPerBeat);const isSelected=selectedNoteIds.includes(note.id);// Draw background of note
|
||||
const playingNow=st.isPlaying;const mainFocused=focusedItemId===st.target_id;ctx.fillStyle=isSelected?'rgba(96, 165, 250, 0.6)':(mainFocused?(playingNow?'rgba(254, 240, 138, 0.75)':'rgba(253, 224, 71, 0.6)'):'rgba(120, 100, 40, 0.35)');ctx.strokeStyle=isSelected?'#60a5fa':(mainFocused?(playingNow?'#fef08a':'#fde047'):'#8a7504');ctx.lineWidth=isSelected?1.5:(mainFocused?1.2:0.8);ctx.fillRect(x+1,y+1,w-2,NoteHeight-2);ctx.strokeRect(x+1,y+1,w-2,NoteHeight-2);// Draw velocity layer (solid yellow/blue bar inside, proportional to velocity)
|
||||
const vel=note.velocity!==undefined?note.velocity:0.8;const velW=Math.max(2,(w-2)*vel);ctx.fillStyle=isSelected?'#60a5fa':(mainFocused?(playingNow?'#fde047':'#facc15'):'#7a6a10');ctx.fillRect(x+1,y+1,velW,NoteHeight-2);});// Draw real-time recording notes
|
||||
@@ -192,7 +192,7 @@ if(e.button===2){e.preventDefault();const clickedNote=notes.find(n=>{return pitc
|
||||
// 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 clCh=window.SonicPianoRoll?window.SonicPianoRoll.getTrackMidiChannel(clTrk,activeTracks):clTrk?clTrk.midiChannel:0;window.SonicSF.playNote(notes[clickedNoteIdx].pitch,100,300,ctx.currentTime,st.instrumentProgram,null,clCh,clTrk?clTrk.synth_engine:undefined);}}// 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 empty space: start selection marquee
|
||||
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{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
|
||||
pushToUndo(notes);const clones=notes.filter(n=>selectedNoteIds.includes(n.id)).map(n=>({...JSON.parse(JSON.stringify(n)),id:'note_'+Date.now()+Math.random().toString(36).substr(2,8)}));if(clones.length>0){setNotes(prev=>[...prev,...clones]);const cloneIds=clones.map(c=>c.id);setSelectedNoteIds(cloneIds);notesBeforeDragRef.current=JSON.parse(JSON.stringify(notes));const cloneOffsets=clones.map(n=>({id:n.id,originalStartBeat:n.start_beat,originalPitch:n.pitch}));setDraggedNote({mode:'move',idx:-1,startOffsetBeat:beat,startOffsetPitch:pitch,selectedNotesOffset:cloneOffsets});showToast('Đã nhân bản '+clones.length+' nốt!','info');}}return;}// Hovered resize edge (Alt+resize for scaling)
|
||||
|
||||
Reference in New Issue
Block a user