IMPROVE: PIANO ROLL TAB khi play thì playhead luôn nằm chính giữa piano roll
This commit is contained in:
+75
-15
@@ -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 ở GIỮA 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) LẪN
|
||||
// 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 gợi ý độ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 của track đó thành MAIN
|
||||
// notes để chỉnh sửa (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 }
|
||||
// Gợi ý độ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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user