fix: dropdown switch keeps ghost notes from other tracks only; track column toggles MIDI play

- handleSwitchMidiItem no longer saves/sets previous item as ghost
- Removed ghostTrackFilter/filteredGhostLayers
- Track column shows clickable track names that toggle play state
- activePlayTrackIds Set controls which tracks' MIDI are playable
This commit is contained in:
2026-07-27 17:40:31 +07:00
parent 91ddc3cb6f
commit b9113f8a06
2 changed files with 38 additions and 25 deletions
+34 -21
View File
@@ -4598,7 +4598,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
const [showGhostNotes, setShowGhostNotes] = React.useState(true);
const [sessionSyncMode, setSessionSyncMode] = React.useState(true);
const [ghostTrackFilter, setGhostTrackFilter] = React.useState(null);
const [activePlayTrackIds, setActivePlayTrackIds] = React.useState(null);
const allMidiItems = React.useMemo(() => {
const result = [];
@@ -4638,10 +4638,29 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
return Math.ceil((maxSec || 4) / secondsPerBar);
}, [activeTracks, secondsPerBar]);
const filteredGhostLayers = React.useMemo(function() {
if (ghostTrackFilter === null || !ghostLayers.length) return ghostLayers;
return ghostLayers.filter(function(l) { return ghostTrackFilter.has(l.track_id); });
}, [ghostLayers, ghostTrackFilter]);
const handleSwitchMidiItem = function(itemId) {
if (itemId === st.target_id) return;
var match = allMidiItems.find(function(m) { return m.id === itemId; });
if (!match) return;
var trk = (activeTracks || []).find(function(t) { return t.id === match._trackId; });
setSubTabs(function(prev) {
return prev.map(function(s) {
if (s.id !== st.id) return s;
return Object.assign({}, s, {
trackId: match._trackId,
target_id: match.id,
label: 'Piano Roll: ' + (match.name || 'MIDI'),
notes: match.notes || [],
duration: match.duration || 4,
instrumentProgram: trk ? trk.instrumentProgram : undefined,
instrumentName: trk ? trk.instrumentName : undefined,
note_selection: [],
currentTime: 0
});
});
});
setSelectedNoteIds([]);
};
const totalBeats = sessionSyncMode ? Math.max(rollBeats, sessionLengthBars * 4 + 16, 64) : Math.max(rollBeats, noteMaxBeat + 16, 64);
const drawWidth = totalBeats * pixelsPerBeat;
@@ -4880,8 +4899,8 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
}
// Layer 2: Ghost Notes (background reference from other tracks)
if (showGhostNotes && sessionSyncMode && filteredGhostLayers.length > 0) {
filteredGhostLayers.forEach(function(layer) {
if (showGhostNotes && sessionSyncMode && ghostLayers.length > 0) {
ghostLayers.forEach(function(layer) {
ctx.save();
ctx.globalAlpha = 0.25;
ctx.fillStyle = layer.track_color || '#888';
@@ -4973,7 +4992,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
ctx.stroke();
}
}
}, [notes, snapVal, rollZoom, selectedNoteIds, selectionMarquee, st.currentTime, bpm, viewWidth, viewBeats, recordingState, recTempMidiNotes, showGhostNotes, sessionSyncMode, filteredGhostLayers]);
}, [notes, snapVal, rollZoom, selectedNoteIds, selectionMarquee, st.currentTime, bpm, viewWidth, viewBeats, recordingState, recTempMidiNotes, showGhostNotes, sessionSyncMode, ghostLayers]);
React.useLayoutEffect(() => {
const canvas = ccCanvasRef.current;
@@ -6113,27 +6132,21 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
seenTracks[m._trackId] = true;
var track = (activeTracks || []).find(function(t) { return t.id === m._trackId; });
var isActive = m._trackId === st.trackId && m.id === st.target_id;
var ghostEnabled = ghostTrackFilter === null || ghostTrackFilter.has(m._trackId);
var hasGhost = ghostLayers.some(function(l) { return l.track_id === m._trackId; });
var isPlayOn = activePlayTrackIds === null || activePlayTrackIds.has(m._trackId);
els.push(React.createElement("div", {
key: m._trackId,
className: "flex items-center gap-1 px-1 h-[18px] border-b border-zinc-900 " + (isActive ? 'bg-yellow-900/20' : '')
}, React.createElement("input", {
type: "checkbox",
checked: ghostEnabled,
disabled: !hasGhost || isActive,
onChange: function() {
onClick: function() {
if (isActive) return;
setGhostTrackFilter(function(prev) {
setActivePlayTrackIds(function(prev) {
var s = prev === null ? new Set() : new Set(prev);
if (ghostEnabled) s.delete(m._trackId);
if (isPlayOn) s.delete(m._trackId);
else s.add(m._trackId);
return s.size === 0 ? null : s;
});
},
className: "w-2.5 h-2.5 accent-purple-500 cursor-pointer shrink-0"
}), React.createElement("span", {
className: "text-[9px] font-mono truncate " + (isActive ? 'text-yellow-400 font-bold' : hasGhost ? (ghostEnabled ? 'text-zinc-300' : 'text-zinc-600') : 'text-zinc-700'),
className: "flex items-center px-1.5 h-[18px] border-b border-zinc-900 cursor-pointer " + (isActive ? 'bg-yellow-900/20' : (isPlayOn ? 'bg-zinc-800/40' : 'bg-zinc-900/60'))
}, React.createElement("span", {
className: "text-[9px] font-mono truncate " + (isActive ? 'text-yellow-400 font-bold' : (isPlayOn ? 'text-zinc-300' : 'text-zinc-600')),
title: track ? track.name : m._trackName
}, track ? track.name : m._trackName)));
});
File diff suppressed because one or more lines are too long