IMPROVE: PIANO ROLL TAB khi play thì playhead luôn nằm chính giữa piano roll

This commit is contained in:
2026-08-07 22:10:37 +07:00
parent 393df8fd7b
commit 8e3c652551
4 changed files with 120 additions and 21 deletions
+75 -15
View File
@@ -7675,6 +7675,29 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
});
}, [notes, ccMode, rollZoom, viewWidth, selectedNoteIds, renderBeatOffset, ccHeight, showCC]);
// Follow playhead: khi PLAY playhead luôn GIA view, notes trôi sang
// trái (scroll theo st.currentTime); khi STOP scroll v đu (playhead
// v trí đu piano roll) user 09:40. Dùng C isPlaying (main) LN
// st.isPlaying (piano roll play main isPlaying=false khi tab play bug
// 09:50: effect tưng đang stop luôn v đu).
React.useEffect(function() {
const wrapper = gridScrollRef.current;
if (!wrapper) return;
const playing = isPlaying || !!st.isPlaying;
if (!playing) {
if (wrapper.scrollLeft !== 0) { wrapper.scrollLeft = 0; setRenderTick(t => t + 1); }
return;
}
const beatSec = 60.0 / (parseInt(bpm) || 120);
const phBeat = (st.currentTime || 0) / beatSec;
const midX = Math.max(0, wrapper.clientWidth / 2);
const targetLeft = Math.max(0, phBeat * pixelsPerBeat - midX);
if (Math.abs(wrapper.scrollLeft - targetLeft) > 1) {
wrapper.scrollLeft = targetLeft;
}
setRenderTick(t => t + 1);
}, [isPlaying, st.isPlaying, st.currentTime]);
React.useEffect(() => {
const scrollToC3 = () => {
if (gridScrollRef.current) {
@@ -8317,6 +8340,21 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
const brushAutoScrollRef = React.useRef(null);
const rightClickDragRef = React.useRef({ active: false, startX: 0, startY: 0 });
const swallowContextMenuRef = React.useRef(false);
// Status hint đng (user 09:40): theo dõi Shift/Ctrl + mouse trong piano roll
const prKeyStateRef = React.useRef({ shift: false, ctrl: false });
const prMouseInRef = React.useRef(false);
React.useEffect(function() {
const updateHint = function() {
if (!window.__setPrHint || !prMouseInRef.current) return;
const ks = prKeyStateRef.current;
window.__setPrHint(ks.ctrl ? "Ctrl+drag: fast copy notes | Ctrl+click empty: marquee chọn notes" : (ks.shift ? "Shift+Click: select/unselect notes | Shift+Click note: thêm/bớt vào nhóm chọn" : "Scroll: Up/Down | Drag: Draw notes"));
};
const kd = function(e) { const ks = prKeyStateRef.current; if (e.shiftKey !== ks.shift || e.ctrlKey !== ks.ctrl) { ks.shift = e.shiftKey; ks.ctrl = e.ctrlKey; updateHint(); } };
const ku = function(e) { const ks = prKeyStateRef.current; if (e.shiftKey !== ks.shift || e.ctrlKey !== ks.ctrl) { ks.shift = e.shiftKey; ks.ctrl = e.ctrlKey; updateHint(); } };
window.addEventListener('keydown', kd);
window.addEventListener('keyup', ku);
return function() { window.removeEventListener('keydown', kd); window.removeEventListener('keyup', ku); };
}, []);
const findCCNoteIndex = (b, mouseY, ccH) => {
const snapped = getSnapBeat(b, snapValue);
@@ -9266,7 +9304,16 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
/* 3. MAIN PIANO ROLL GRID (KEYBOARD + TRACK COL + CANVAS) */
React.createElement("div", {
className: "flex-1 flex overflow-hidden min-h-0 relative"
className: "flex-1 flex overflow-hidden min-h-0 relative",
onMouseEnter: function() {
prMouseInRef.current = true;
// Status bar gi ý đng (user 09:40): trong piano roll base hint;
// gi Shift select/unselect; gi Ctrl fast copy
if (window.__setPrHint) {
window.__setPrHint(prKeyStateRef.current.ctrl ? "Ctrl+drag: fast copy notes | Ctrl+click empty: marquee chọn notes" : (prKeyStateRef.current.shift ? "Shift+Click: select/unselect notes | Shift+Click note: thêm/bớt vào nhóm chọn" : "Scroll: Up/Down | Drag: Draw notes"));
}
},
onMouseLeave: function() { prMouseInRef.current = false; if (window.__setPrHint) window.__setPrHint(null); }
}, /* Track column */ React.createElement("div", {
className: "w-[120px] bg-[#16161a] border-r border-zinc-800 shrink-0 flex flex-col z-10",
style: { height: KeybedPixelHeight + 'px' }
@@ -9279,19 +9326,30 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
var track = (activeTracks || []).find(function(t) { return t.id === m._trackId; });
var isActive = m._trackId === st.trackId && m.id === st.target_id;
var isPlayOn = activePlayTrackIds && activePlayTrackIds.indexOf(m._trackId) !== -1;
els.push(React.createElement("button", {
els.push(React.createElement("div", {
key: m._trackId,
className: "flex items-center gap-0.5 mx-1 my-[2px]"
}, React.createElement("button", {
onClick: function() {
// Click nút tên track ACTIVE ghost notes ca track đó thành MAIN
// notes đ chnh sa (user 09:40)
handleSwitchMidiItem(m.id);
},
className: "flex items-center justify-center flex-1 h-[26px] min-w-0 border border-zinc-600 rounded-md cursor-pointer outline-none " + (isActive ? 'bg-yellow-600 text-black font-bold' : 'bg-zinc-700 text-zinc-300 hover:bg-zinc-600')
}, React.createElement("span", {
className: "text-[13px] leading-none font-sans truncate px-1",
title: track ? track.name : m._trackName
}, track ? track.name : m._trackName)), React.createElement("button", {
onClick: function(e) {
e.stopPropagation();
var prevList = activePlayTrackIds || [];
var nextList = prevList.indexOf(m._trackId) !== -1 ? prevList.filter(function(id) { return id !== m._trackId; }) : prevList.concat([m._trackId]);
setActivePlayTrackIds(nextList);
if (onRealtimePlay) onRealtimePlay(nextList);
},
className: "flex items-center justify-center h-[20px] border border-zinc-600 rounded-md cursor-pointer outline-none mx-1 my-[2px] " + (isActive ? 'bg-yellow-600 text-black font-bold' : (isPlayOn ? 'bg-red-700 text-white font-semibold' : 'bg-zinc-700 text-zinc-300 hover:bg-zinc-600'))
}, React.createElement("span", {
className: "text-[14px] font-sans truncate px-1",
title: track ? track.name : m._trackName
}, track ? track.name : m._trackName)));
title: isPlayOn ? "Unmute — ghost track play cùng main notes" : "Mute (mặc định) — click để play ghost cùng main",
className: "w-6 h-[26px] shrink-0 border border-zinc-600 rounded-md cursor-pointer text-[11px] font-bold outline-none flex items-center justify-center " + (isPlayOn ? 'bg-green-700 text-white' : 'bg-zinc-800 text-zinc-500 hover:text-zinc-300')
}, isPlayOn ? "\u266A" : "M")));
});
return els;
}() : null)), React.createElement("div", {
@@ -14826,6 +14884,13 @@ const App = () => {
// Context Menu & Clipboard
const [contextMenu, setContextMenu] = useState(null); // { x, y, trackId }
// Gi ý đng PIANO ROLL trên status bar (user 09:40) piano roll set qua
// window.__setPrHint (mouse in/out + Shift/Ctrl state)
const [prHint, setPrHint] = useState(null);
React.useEffect(() => {
window.__setPrHint = (h) => setPrHint(h || null);
return () => { delete window.__setPrHint; };
}, []);
const clipboardRef = useRef(null); // { buffer, name, volume, color } for copy/paste
// Undo/Redo Engine (LOOP_EDITOR.md §4 + Global Extension)
@@ -28783,14 +28848,9 @@ STRICT CONSTRAINTS:
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "info",
className: "w-3 h-3 text-zinc-600"
})), " Scroll: Zoom"), /*#__PURE__*/React.createElement("span", null, "|"), /*#__PURE__*/React.createElement("span", {
className: "flex items-center gap-1"
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center shrink-0"
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "keyboard",
className: "w-3 h-3 text-zinc-600"
})), " Ctrl+Scroll: Playhead"))), contextMenu && (contextMenu.isSubTab ? /*#__PURE__*/React.createElement("div", {
})), prHint ? /*#__PURE__*/React.createElement("span", {
className: "text-cyan-400"
}, prHint) : " Scroll: Zoom"))), contextMenu && (contextMenu.isSubTab ? /*#__PURE__*/React.createElement("div", {
className: "fixed z-[60] bg-[#2a2a2a] border border-zinc-700 rounded-lg shadow-2xl py-1 w-64 overflow-y-auto",
style: {
left: Math.min(contextMenu.x, window.innerWidth - 260),
File diff suppressed because one or more lines are too long
+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=202608070935" defer></script>
<script src="/static/js/app.precompiled.js?v=202608070950" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {