fix: misc UI updates and fixes
This commit is contained in:
@@ -48,7 +48,7 @@ const Toolbar: React.FC = () => {
|
||||
barWidthMultiplier, setBarWidthMultiplier,
|
||||
isLooping, toggleLoop,
|
||||
canUndo, canRedo, undoDescription, redoDescription, undo, redo,
|
||||
toggleChatBox, toggleSettings, toggleKGOnePanel, toggleListEventPanel, showKGOnePanel, showListEventPanel, showChatBox, cleanupProjectState, toggleMetronome, isMetronomeEnabled,
|
||||
toggleChatBox, toggleSettings, toggleKGOnePanel, toggleListEventPanel, showKGOnePanel, showListEventPanel, showChatBox, showSettings, cleanupProjectState, toggleMetronome, isMetronomeEnabled,
|
||||
isRecording, startRecording, stopRecording,
|
||||
// Piano roll state/actions
|
||||
showPianoRoll, setShowPianoRoll, activeRegionId, setActiveRegionId,
|
||||
@@ -1065,7 +1065,13 @@ const Toolbar: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<button title="Import" onClick={handleImportProject}><FaUpload /></button>
|
||||
<button title="Settings" onClick={handleSettingsClick}><FaCog /></button>
|
||||
<button
|
||||
title="Settings"
|
||||
className={`tool-button ${showSettings ? 'active' : ''}`}
|
||||
onClick={handleSettingsClick}
|
||||
>
|
||||
<FaCog />
|
||||
</button>
|
||||
<div className="toolbar-separator"></div>
|
||||
<button title="Undo" onClick={handleUndoClick}><FaUndo /></button>
|
||||
<button title="Redo" onClick={handleRedoClick}><FaRedo /></button>
|
||||
|
||||
@@ -180,6 +180,14 @@
|
||||
--toolbar-button-bg: #d6d6d6;
|
||||
}
|
||||
|
||||
.piano-roll-toolbar button:focus,
|
||||
.piano-roll-toolbar button:focus-visible,
|
||||
.piano-roll-toolbar .tool-button:focus,
|
||||
.piano-roll-toolbar .tool-button:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Override pointer-events for piano roll toolbar sections */
|
||||
.piano-roll-toolbar .toolbar-left,
|
||||
.piano-roll-toolbar .toolbar-right {
|
||||
@@ -310,8 +318,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.piano-roll-main-section {
|
||||
@@ -319,7 +326,7 @@
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
min-width: calc(var(--max-number-of-bars) * var(--region-grid-bar-width) + var(--region-piano-key-width));
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.piano-roll-main-section.with-automation {
|
||||
@@ -329,34 +336,12 @@
|
||||
.piano-roll-note-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: visible;
|
||||
}
|
||||
|
||||
.piano-roll-body-shell {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.piano-roll-keys-viewport {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 15;
|
||||
width: var(--region-piano-key-width);
|
||||
flex: 0 0 var(--region-piano-key-width);
|
||||
overflow: hidden;
|
||||
border-right: 1px solid #3a3a3a;
|
||||
background-color: #252525;
|
||||
}
|
||||
|
||||
.piano-roll-keys-offset {
|
||||
will-change: transform;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.piano-roll-automation-section {
|
||||
flex: 0 0 50%;
|
||||
min-height: 0;
|
||||
min-width: calc(var(--max-number-of-bars) * var(--region-grid-bar-width) + var(--region-piano-key-width));
|
||||
border-top: 1px solid #3a3a3a;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -416,7 +401,11 @@
|
||||
width: var(--region-piano-key-width);
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
border-right: 1px solid #3a3a3a;
|
||||
background-color: #252525;
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
.piano-grid-container {
|
||||
@@ -523,6 +512,13 @@
|
||||
min-width: calc(var(--max-number-of-bars) * var(--region-grid-bar-width) + var(--region-piano-key-width));
|
||||
}
|
||||
|
||||
.piano-roll-automation-scroll-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
will-change: transform;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.piano-roll-automation-track::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -540,6 +536,7 @@
|
||||
left: 0;
|
||||
width: var(--region-piano-key-width);
|
||||
background: linear-gradient(180deg, #343434 0%, #2e2e2e 100%);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.piano-roll-automation-grid {
|
||||
@@ -561,7 +558,7 @@
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 10px;
|
||||
z-index: 1;
|
||||
z-index: 3;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
|
||||
@@ -671,7 +671,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
const handleZoomChange = useCallback((nextZoom: number) => {
|
||||
if (nextZoom === pianoRollZoom) return;
|
||||
|
||||
const container = pianoRollContentRef.current;
|
||||
const container = pianoRollNoteScrollRef.current;
|
||||
pendingZoomAnchorBeatRef.current = null;
|
||||
if (container) {
|
||||
const keysWidth = parseInt(
|
||||
@@ -742,7 +742,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
|
||||
// Detect manual horizontal scroll during playback
|
||||
useEffect(() => {
|
||||
const container = pianoRollContentRef.current;
|
||||
const container = pianoRollNoteScrollRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const handleScroll = () => {
|
||||
@@ -761,7 +761,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
useEffect(() => {
|
||||
if (!isPlaying || !autoScrollEnabled) return;
|
||||
|
||||
const container = pianoRollContentRef.current;
|
||||
const container = pianoRollNoteScrollRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const beatWidth = parseInt(
|
||||
@@ -787,7 +787,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
useEffect(() => {
|
||||
if (pianoRollScrollRequest === null) return;
|
||||
|
||||
const container = pianoRollContentRef.current;
|
||||
const container = pianoRollNoteScrollRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const beatWidth = parseInt(
|
||||
@@ -820,7 +820,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
}
|
||||
|
||||
const anchorBeat = pendingZoomAnchorBeatRef.current;
|
||||
const container = pianoRollContentRef.current;
|
||||
const container = pianoRollNoteScrollRef.current;
|
||||
if (anchorBeat !== null && container) {
|
||||
const keysWidth = parseInt(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--region-piano-key-width')
|
||||
@@ -845,7 +845,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
|
||||
// Scroll horizontally to the active region's starting bar
|
||||
useEffect(() => {
|
||||
if (pianoRollContentRef.current && activeRegion) {
|
||||
if (pianoRollNoteScrollRef.current && activeRegion) {
|
||||
// Get the starting beat of the region
|
||||
const startBeat = activeRegion.getStartFromBeat();
|
||||
|
||||
@@ -866,7 +866,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
const scrollPosition = barNumber * barWidth;
|
||||
|
||||
// Scroll to the calculated position
|
||||
pianoRollContentRef.current.scrollLeft = Math.max(0, scrollPosition);
|
||||
pianoRollNoteScrollRef.current.scrollLeft = Math.max(0, scrollPosition);
|
||||
}
|
||||
}, [activeRegion, timeSignature]);
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ interface PianoRollAutomationLaneProps {
|
||||
timeSignature: { numerator: number; denominator: number };
|
||||
bpm?: number;
|
||||
redrawVersion?: number;
|
||||
horizontalScrollLeft?: number;
|
||||
onHorizontalWheel?: (delta: number) => void;
|
||||
}
|
||||
|
||||
const AUTOMATION_COLOR = '#87CEFA';
|
||||
@@ -38,6 +40,8 @@ const PianoRollAutomationLane: React.FC<PianoRollAutomationLaneProps> = ({
|
||||
maxBars,
|
||||
timeSignature,
|
||||
redrawVersion = 0,
|
||||
horizontalScrollLeft = 0,
|
||||
onHorizontalWheel,
|
||||
}) => {
|
||||
const laneRef = useRef<HTMLDivElement | null>(null);
|
||||
const [laneHeight, setLaneHeight] = useState(MIN_LANE_HEIGHT);
|
||||
@@ -66,6 +70,28 @@ const PianoRollAutomationLane: React.FC<PianoRollAutomationLaneProps> = ({
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const element = laneRef.current;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleWheel = (event: WheelEvent) => {
|
||||
const horizontalDelta = Math.abs(event.deltaX) > 0 ? event.deltaX : (event.shiftKey ? event.deltaY : 0);
|
||||
if (horizontalDelta === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
onHorizontalWheel?.(horizontalDelta);
|
||||
};
|
||||
|
||||
element.addEventListener('wheel', handleWheel, { passive: false });
|
||||
return () => {
|
||||
element.removeEventListener('wheel', handleWheel);
|
||||
};
|
||||
}, [onHorizontalWheel]);
|
||||
|
||||
const points = useMemo<AutomationPoint[]>(() => {
|
||||
if (!activeRegion) {
|
||||
return [];
|
||||
@@ -160,62 +186,67 @@ const PianoRollAutomationLane: React.FC<PianoRollAutomationLaneProps> = ({
|
||||
>
|
||||
<div className="piano-roll-automation-track" style={{ width: totalWidth }}>
|
||||
<div className="piano-roll-automation-gutter" />
|
||||
<div className="piano-roll-automation-grid" />
|
||||
<div className="piano-roll-automation-lane-label">{laneLabel}</div>
|
||||
<svg
|
||||
className="piano-roll-automation-svg"
|
||||
width="100%"
|
||||
height={laneHeight}
|
||||
viewBox={`0 0 ${beatWidth * totalBeats + keyWidth} ${laneHeight}`}
|
||||
preserveAspectRatio="none"
|
||||
<div
|
||||
className="piano-roll-automation-scroll-layer"
|
||||
style={{ transform: `translateX(-${horizontalScrollLeft}px)` }}
|
||||
>
|
||||
{interpolationMode === 'linear' && points.length > 0 && (
|
||||
<polyline
|
||||
className="piano-roll-automation-line"
|
||||
fill="none"
|
||||
stroke={AUTOMATION_COLOR}
|
||||
strokeWidth="2"
|
||||
points={polylinePoints}
|
||||
/>
|
||||
)}
|
||||
{interpolationMode === 'step' && stepSegments.map(segment => (
|
||||
<line
|
||||
key={segment.id}
|
||||
className="piano-roll-automation-line"
|
||||
x1={segment.x1}
|
||||
y1={segment.y1}
|
||||
x2={segment.x2}
|
||||
y2={segment.y1}
|
||||
stroke={AUTOMATION_COLOR}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
))}
|
||||
{svgPoints.map(point => {
|
||||
const labelY = Math.max(14, Math.min(laneHeight - 6, point.y - 10));
|
||||
<div className="piano-roll-automation-grid" />
|
||||
<svg
|
||||
className="piano-roll-automation-svg"
|
||||
width="100%"
|
||||
height={laneHeight}
|
||||
viewBox={`0 0 ${beatWidth * totalBeats + keyWidth} ${laneHeight}`}
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
{interpolationMode === 'linear' && points.length > 0 && (
|
||||
<polyline
|
||||
className="piano-roll-automation-line"
|
||||
fill="none"
|
||||
stroke={AUTOMATION_COLOR}
|
||||
strokeWidth="2"
|
||||
points={polylinePoints}
|
||||
/>
|
||||
)}
|
||||
{interpolationMode === 'step' && stepSegments.map(segment => (
|
||||
<line
|
||||
key={segment.id}
|
||||
className="piano-roll-automation-line"
|
||||
x1={segment.x1}
|
||||
y1={segment.y1}
|
||||
x2={segment.x2}
|
||||
y2={segment.y1}
|
||||
stroke={AUTOMATION_COLOR}
|
||||
strokeWidth="2"
|
||||
/>
|
||||
))}
|
||||
{svgPoints.map(point => {
|
||||
const labelY = Math.max(14, Math.min(laneHeight - 6, point.y - 10));
|
||||
|
||||
return (
|
||||
<g key={point.id}>
|
||||
<circle
|
||||
className="piano-roll-automation-point"
|
||||
cx={point.x}
|
||||
cy={point.y}
|
||||
r="5"
|
||||
fill={AUTOMATION_COLOR}
|
||||
stroke="#1d2428"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<text
|
||||
className="piano-roll-automation-value"
|
||||
x={point.x + 6}
|
||||
y={labelY}
|
||||
fill={AUTOMATION_COLOR}
|
||||
>
|
||||
{point.label}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
return (
|
||||
<g key={point.id}>
|
||||
<circle
|
||||
className="piano-roll-automation-point"
|
||||
cx={point.x}
|
||||
cy={point.y}
|
||||
r="5"
|
||||
fill={AUTOMATION_COLOR}
|
||||
stroke="#1d2428"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<text
|
||||
className="piano-roll-automation-value"
|
||||
x={point.x + 6}
|
||||
y={labelY}
|
||||
fill={AUTOMATION_COLOR}
|
||||
>
|
||||
{point.label}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
</div>
|
||||
{points.length === 0 && (
|
||||
<div className="piano-roll-automation-empty-state">
|
||||
No {laneLabel} events in this region
|
||||
|
||||
@@ -76,7 +76,7 @@ const PianoRollContent: React.FC<PianoRollContentProps> = ({
|
||||
const isSpectrogram = mode === 'spectrogram';
|
||||
const showAutomationLane = automationEnabled && !isSpectrogram;
|
||||
const [spectrogramLoading, setSpectrogramLoading] = useState(false);
|
||||
const [noteScrollTop, setNoteScrollTop] = useState(0);
|
||||
const [noteScrollLeft, setNoteScrollLeft] = useState(0);
|
||||
const handleSpectrogramLoadingChange = useCallback((loading: boolean) => {
|
||||
setSpectrogramLoading(loading);
|
||||
}, []);
|
||||
@@ -87,6 +87,15 @@ const PianoRollContent: React.FC<PianoRollContentProps> = ({
|
||||
// Recording state
|
||||
const isRecording = useProjectStore(s => s.isRecording);
|
||||
const recordingNotes = useProjectStore(s => s.recordingNotes);
|
||||
const forwardAutomationHorizontalWheel = useCallback((delta: number) => {
|
||||
const container = noteScrollRef.current;
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
container.scrollLeft += delta;
|
||||
setNoteScrollLeft(container.scrollLeft);
|
||||
}, [noteScrollRef]);
|
||||
|
||||
// Use the note operations hook for resize and drag functionality
|
||||
const {
|
||||
@@ -290,23 +299,14 @@ const PianoRollContent: React.FC<PianoRollContentProps> = ({
|
||||
data-testid={showAutomationLane ? 'piano-roll-content-split' : 'piano-roll-content-single'}
|
||||
>
|
||||
<div className={`piano-roll-main-section ${showAutomationLane ? 'with-automation' : ''}`}>
|
||||
<PianoGridHeader maxBars={maxBars} timeSignature={timeSignature} />
|
||||
|
||||
<div className="piano-roll-body-shell">
|
||||
<div className="piano-roll-keys-viewport">
|
||||
<div
|
||||
className="piano-roll-keys-offset"
|
||||
style={{ transform: `translateY(-${noteScrollTop}px)` }}
|
||||
>
|
||||
<PianoKeys activeRegion={activeRegion} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="piano-roll-note-scroll"
|
||||
ref={noteScrollRef}
|
||||
onScroll={(event) => setNoteScrollTop(event.currentTarget.scrollTop)}
|
||||
>
|
||||
<div className="piano-roll-body">
|
||||
<div
|
||||
className="piano-roll-note-scroll"
|
||||
ref={noteScrollRef}
|
||||
onScroll={(event) => setNoteScrollLeft(event.currentTarget.scrollLeft)}
|
||||
>
|
||||
<PianoGridHeader maxBars={maxBars} timeSignature={timeSignature} />
|
||||
<div className="piano-roll-body">
|
||||
<PianoKeys activeRegion={activeRegion} />
|
||||
<PianoGrid
|
||||
gridRef={pianoGridRef}
|
||||
onDoubleClick={isSpectrogram ? () => {} : handleGridDoubleClick}
|
||||
@@ -331,7 +331,6 @@ const PianoRollContent: React.FC<PianoRollContentProps> = ({
|
||||
{memoizedNotes}
|
||||
{!isSpectrogram && recordingNoteOverlays}
|
||||
</PianoGrid>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -344,6 +343,8 @@ const PianoRollContent: React.FC<PianoRollContentProps> = ({
|
||||
timeSignature={timeSignature}
|
||||
bpm={bpm}
|
||||
redrawVersion={automationRedrawVersion}
|
||||
horizontalScrollLeft={noteScrollLeft}
|
||||
onHorizontalWheel={forwardAutomationHorizontalWheel}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user