feat(piano-roll): context menu flip, track column, session duration
- Context menu flips upward near viewport bottom edge - Track column (120px) left of keybed: checkboxes per track to filter ghost notes; active track highlighted - Session mode: canvas extends to full session duration - Filtered ghost layers via ghostTrackFilter state (Set of track IDs) - Fixed bar label seek in session mode (was using wrong offset)
This commit is contained in:
+82
-14
@@ -4595,7 +4595,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
const [rollBeats, setRollBeats] = React.useState(Math.max(noteMaxBeat + 16, 64));
|
const [rollBeats, setRollBeats] = React.useState(Math.max(noteMaxBeat + 16, 64));
|
||||||
const rollBeatsRef = React.useRef(rollBeats);
|
const rollBeatsRef = React.useRef(rollBeats);
|
||||||
rollBeatsRef.current = rollBeats;
|
rollBeatsRef.current = rollBeats;
|
||||||
const totalBeats = Math.max(rollBeats, noteMaxBeat + 16, 64); // at least 64 beats (16 bars) for scrolling
|
const totalBeats = sessionSyncMode ? Math.max(rollBeats, sessionLengthBars * 4 + 16, 64) : Math.max(rollBeats, noteMaxBeat + 16, 64);
|
||||||
const drawWidth = totalBeats * pixelsPerBeat;
|
const drawWidth = totalBeats * pixelsPerBeat;
|
||||||
const [rollViewWidth, setRollViewWidth] = React.useState(800);
|
const [rollViewWidth, setRollViewWidth] = React.useState(800);
|
||||||
const viewWidth = Math.max(drawWidth, rollViewWidth);
|
const viewWidth = Math.max(drawWidth, rollViewWidth);
|
||||||
@@ -4640,6 +4640,24 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
const sessionStartBar = sessionSyncMode && activeTargetItem
|
const sessionStartBar = sessionSyncMode && activeTargetItem
|
||||||
? (activeTargetItem.startTime / secondsPerBar) : 0;
|
? (activeTargetItem.startTime / secondsPerBar) : 0;
|
||||||
|
|
||||||
|
const [ghostTrackFilter, setGhostTrackFilter] = React.useState(null);
|
||||||
|
|
||||||
|
const sessionLengthBars = React.useMemo(function() {
|
||||||
|
var maxSec = 0;
|
||||||
|
(activeTracks || []).forEach(function(tr) {
|
||||||
|
(tr.midiItems || []).forEach(function(m) {
|
||||||
|
var end = (m.startTime || 0) + (m.duration || 4);
|
||||||
|
if (end > maxSec) maxSec = end;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
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 = React.useCallback(function(itemId) {
|
const handleSwitchMidiItem = React.useCallback(function(itemId) {
|
||||||
if (itemId === st.target_id) return;
|
if (itemId === st.target_id) return;
|
||||||
if (st.isDirty) {
|
if (st.isDirty) {
|
||||||
@@ -4892,8 +4910,8 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Layer 2: Ghost Notes (background reference from other tracks)
|
// Layer 2: Ghost Notes (background reference from other tracks)
|
||||||
if (showGhostNotes && sessionSyncMode && ghostLayers.length > 0) {
|
if (showGhostNotes && sessionSyncMode && filteredGhostLayers.length > 0) {
|
||||||
ghostLayers.forEach(function(layer) {
|
filteredGhostLayers.forEach(function(layer) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.globalAlpha = 0.25;
|
ctx.globalAlpha = 0.25;
|
||||||
ctx.fillStyle = layer.track_color || '#888';
|
ctx.fillStyle = layer.track_color || '#888';
|
||||||
@@ -4985,7 +5003,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [notes, snapVal, rollZoom, selectedNoteIds, selectionMarquee, st.currentTime, bpm, viewWidth, viewBeats, recordingState, recTempMidiNotes, showGhostNotes, sessionSyncMode, ghostLayers]);
|
}, [notes, snapVal, rollZoom, selectedNoteIds, selectionMarquee, st.currentTime, bpm, viewWidth, viewBeats, recordingState, recTempMidiNotes, showGhostNotes, sessionSyncMode, filteredGhostLayers]);
|
||||||
|
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
const canvas = ccCanvasRef.current;
|
const canvas = ccCanvasRef.current;
|
||||||
@@ -5774,24 +5792,28 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
setScaleMenuPos({ x: origin.x, y: origin.y, parentKey });
|
setScaleMenuPos({ x: origin.x, y: origin.y, parentKey });
|
||||||
});
|
});
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
const subItems = [];
|
const subs = [];
|
||||||
Object.keys(val).forEach(subKey => {
|
Object.keys(val).forEach(subKey => {
|
||||||
subItems.push(React.createElement("div", {
|
subs.push(React.createElement("div", {
|
||||||
key: subKey,
|
key: subKey,
|
||||||
onClick: () => { setSelectedScale(val[subKey]); closeMenu(); },
|
onClick: () => { setSelectedScale(val[subKey]); closeMenu(); },
|
||||||
className: "px-3 py-1.5 text-xs cursor-pointer hover:bg-amber-600 hover:text-white whitespace-nowrap " + (isSameScale(val[subKey], selectedScale) ? "bg-amber-800/40 text-amber-300" : "text-zinc-300")
|
className: "px-3 py-1.5 text-xs cursor-pointer hover:bg-amber-600 hover:text-white whitespace-nowrap " + (isSameScale(val[subKey], selectedScale) ? "bg-amber-800/40 text-amber-300" : "text-zinc-300")
|
||||||
}, subKey));
|
}, subKey));
|
||||||
});
|
});
|
||||||
|
var subH = subs.length * 30 + 16;
|
||||||
|
var subTop = (origin.y + subH + 20 > window.innerHeight) ? (origin.y - subH) : origin.y;
|
||||||
subMenu = React.createElement("div", {
|
subMenu = React.createElement("div", {
|
||||||
style: { position: "fixed", left: origin.x + 150, top: origin.y, zIndex: 10000 },
|
style: { position: "fixed", left: origin.x + 150, top: subTop, zIndex: 10000 },
|
||||||
className: "bg-[#2a2a2a] border border-zinc-600 rounded shadow-xl py-1 min-w-[120px]"
|
className: "bg-[#2a2a2a] border border-zinc-600 rounded shadow-xl py-1 min-w-[120px]"
|
||||||
}, ...subItems);
|
}, ...subs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
var menuH = items.length * 30 + 16;
|
||||||
|
var menuTop = (origin.y + menuH + 20 > window.innerHeight) ? Math.max(10, origin.y - menuH) : origin.y;
|
||||||
return React.createElement(React.Fragment, null,
|
return React.createElement(React.Fragment, null,
|
||||||
React.createElement("div", {
|
React.createElement("div", {
|
||||||
style: { position: "fixed", left: origin.x, top: origin.y, zIndex: 9999 },
|
style: { position: "fixed", left: origin.x, top: menuTop, zIndex: 9999 },
|
||||||
className: "bg-[#2a2a2a] border border-zinc-600 rounded shadow-xl py-1 min-w-[140px]"
|
className: "bg-[#2a2a2a] border border-zinc-600 rounded shadow-xl py-1 min-w-[140px]"
|
||||||
}, ...items),
|
}, ...items),
|
||||||
subMenu
|
subMenu
|
||||||
@@ -5816,7 +5838,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
|
|||||||
className: "pl-1 border-l border-zinc-700 h-full select-none cursor-pointer hover:bg-zinc-800/30",
|
className: "pl-1 border-l border-zinc-700 h-full select-none cursor-pointer hover:bg-zinc-800/30",
|
||||||
onClick: (e) => {
|
onClick: (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const barTime = displayBar * 4 * beatSec;
|
const barTime = bar * 4 * beatSec;
|
||||||
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: barTime } : s));
|
setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, currentTime: barTime } : s));
|
||||||
}
|
}
|
||||||
}, `Bar ${displayBar}`)
|
}, `Bar ${displayBar}`)
|
||||||
@@ -5972,10 +5994,14 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
|
|||||||
className: "w-3 h-3"
|
className: "w-3 h-3"
|
||||||
}), "Đóng"))),
|
}), "Đóng"))),
|
||||||
|
|
||||||
/* 2. BAR RULER (ĐÃ SỬA LẠI ĐÓNG NGOẶC ĐÚNG TẠI ĐÂY) */
|
/* 2. BAR RULER */
|
||||||
React.createElement("div", {
|
React.createElement("div", {
|
||||||
className: "h-6 bg-[#1a1a1f] border-b border-zinc-900 flex shrink-0"
|
className: "h-6 bg-[#1a1a1f] border-b border-zinc-900 flex shrink-0"
|
||||||
}, React.createElement("div", {
|
}, React.createElement("div", {
|
||||||
|
className: "w-[120px] bg-[#1e1e22] border-r border-zinc-800 shrink-0 flex items-end"
|
||||||
|
}, React.createElement("span", {
|
||||||
|
className: "text-[8px] text-zinc-600 font-mono px-1.5 pb-0.5 uppercase tracking-wider"
|
||||||
|
}, "Tracks")), React.createElement("div", {
|
||||||
className: "w-[60px] bg-[#222222] border-r border-zinc-900 shrink-0"
|
className: "w-[60px] bg-[#222222] border-r border-zinc-900 shrink-0"
|
||||||
}), React.createElement("div", {
|
}), React.createElement("div", {
|
||||||
ref: rulerScrollRef,
|
ref: rulerScrollRef,
|
||||||
@@ -6095,18 +6121,60 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
|
|||||||
}
|
}
|
||||||
}))))),
|
}))))),
|
||||||
|
|
||||||
/* 3. MAIN PIANO ROLL GRID (KEYBOARD + CANVAS) */
|
/* 3. MAIN PIANO ROLL GRID (KEYBOARD + TRACK COL + CANVAS) */
|
||||||
React.createElement("div", {
|
React.createElement("div", {
|
||||||
className: "flex-1 flex overflow-hidden min-h-0 relative"
|
className: "flex-1 flex overflow-hidden min-h-0 relative"
|
||||||
}, React.createElement("div", {
|
}, React.createElement("div", {
|
||||||
ref: keybedRef,
|
ref: keybedRef,
|
||||||
onScroll: handleKeybedScroll,
|
onScroll: handleKeybedScroll,
|
||||||
className: "w-[60px] overflow-y-auto flex flex-col border-r border-zinc-900 shrink-0",
|
className: "flex overflow-y-auto shrink-0 flex-row",
|
||||||
style: {
|
style: {
|
||||||
scrollbarWidth: 'none',
|
scrollbarWidth: 'none',
|
||||||
msOverflowStyle: 'none'
|
msOverflowStyle: 'none'
|
||||||
}
|
}
|
||||||
}, renderKeybed()), React.createElement("div", {
|
}, /* Track column */ React.createElement("div", {
|
||||||
|
className: "w-[120px] bg-[#16161a] border-r border-zinc-800 shrink-0 flex flex-col",
|
||||||
|
style: { height: KeybedPixelHeight + 'px', overflow: 'hidden' }
|
||||||
|
}, (allMidiItems.length > 0 ? function() {
|
||||||
|
var seenTracks = {};
|
||||||
|
var els = [];
|
||||||
|
allMidiItems.forEach(function(m) {
|
||||||
|
if (seenTracks[m._trackId]) return;
|
||||||
|
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; });
|
||||||
|
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() {
|
||||||
|
if (isActive) return;
|
||||||
|
setGhostTrackFilter(function(prev) {
|
||||||
|
var s = prev === null ? new Set() : new Set(prev);
|
||||||
|
if (ghostEnabled) 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'),
|
||||||
|
title: track ? track.name : m._trackName
|
||||||
|
}, track ? track.name : m._trackName)));
|
||||||
|
});
|
||||||
|
return els;
|
||||||
|
}() : null)), React.createElement("div", {
|
||||||
|
className: "w-[60px] flex flex-col border-r border-zinc-900 shrink-0",
|
||||||
|
style: {
|
||||||
|
scrollbarWidth: 'none',
|
||||||
|
msOverflowStyle: 'none'
|
||||||
|
}
|
||||||
|
}, renderKeybed())), React.createElement("div", {
|
||||||
ref: gridScrollRef,
|
ref: gridScrollRef,
|
||||||
onScroll: handleScroll,
|
onScroll: handleScroll,
|
||||||
className: "flex-1 overflow-auto bg-[#141414] min-w-0"
|
className: "flex-1 overflow-auto bg-[#141414] min-w-0"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user