feat: added highlighted chord guide on piano grid
This commit is contained in:
@@ -3,7 +3,7 @@ import type { MutableRefObject } from 'react';
|
||||
import { Playhead } from '../common';
|
||||
import SelectionBox from './SelectionBox';
|
||||
import { isModifierKeyPressed } from '../../util/osUtil';
|
||||
import { generatePianoGridBackground } from '../../util/scaleUtil';
|
||||
import { generatePianoGridBackground, getMatchingChordsForPitch } from '../../util/scaleUtil';
|
||||
import type { KeySignature } from '../../core/KGProject';
|
||||
|
||||
interface PianoGridProps {
|
||||
@@ -22,6 +22,7 @@ interface PianoGridProps {
|
||||
regionStartBeat?: number;
|
||||
selectedMode: string;
|
||||
keySignature: KeySignature;
|
||||
chordGuide: string;
|
||||
}
|
||||
|
||||
interface CursorPosition {
|
||||
@@ -41,10 +42,12 @@ const PianoGrid: React.FC<PianoGridProps> = ({
|
||||
selectionBox,
|
||||
regionStartBeat = 0,
|
||||
selectedMode,
|
||||
keySignature
|
||||
keySignature,
|
||||
chordGuide
|
||||
}) => {
|
||||
const [cursorPosition, setCursorPosition] = useState<CursorPosition | null>(null);
|
||||
const [isModifierPressed, setIsModifierPressed] = useState(false);
|
||||
const [selectedChordIndex, setSelectedChordIndex] = useState(0);
|
||||
|
||||
// Generate background with scale highlighting - only regenerate when mode or key changes
|
||||
const backgroundImage = useMemo(() => {
|
||||
@@ -125,6 +128,67 @@ const PianoGrid: React.FC<PianoGridProps> = ({
|
||||
setCursorPosition(null);
|
||||
};
|
||||
|
||||
// Get all matching chords for the current hover position
|
||||
const matchingChords = useMemo(() => {
|
||||
if (!cursorPosition) return [];
|
||||
|
||||
// If chord guide is disabled, return empty array
|
||||
if (chordGuide === 'N') return [];
|
||||
|
||||
// Use the utility function to get matching chords
|
||||
const functionType = chordGuide as 'T' | 'S' | 'D';
|
||||
return getMatchingChordsForPitch(cursorPosition.pitch, keySignature, selectedMode, functionType);
|
||||
}, [cursorPosition, chordGuide, keySignature, selectedMode]);
|
||||
|
||||
// Calculate chord highlights based on selected chord index
|
||||
const chordHighlights = useMemo(() => {
|
||||
if (matchingChords.length === 0) return [];
|
||||
|
||||
// Use the selected chord index (wrap around if needed)
|
||||
const chordIndex = selectedChordIndex % matchingChords.length;
|
||||
const matchedChordPitches = matchingChords[chordIndex];
|
||||
|
||||
// Convert pitch classes to actual pitches in the same octave as cursor
|
||||
const highlights: Array<{ pitch: number; beat: number }> = [];
|
||||
const cursorOctave = Math.floor(cursorPosition!.pitch / 12);
|
||||
|
||||
// For each pitch class in the chord, create highlights
|
||||
for (const pitchClass of matchedChordPitches) {
|
||||
// Calculate the actual pitch in the cursor's octave
|
||||
const actualPitch = cursorOctave * 12 + pitchClass;
|
||||
|
||||
// Ensure the pitch is in valid MIDI range (0-127)
|
||||
if (actualPitch >= 0 && actualPitch <= 127) {
|
||||
highlights.push({ pitch: actualPitch, beat: cursorPosition!.beat });
|
||||
}
|
||||
}
|
||||
|
||||
return highlights;
|
||||
}, [cursorPosition, matchingChords, selectedChordIndex]);
|
||||
|
||||
// Reset selected chord index when cursor moves to a different pitch or beat
|
||||
useEffect(() => {
|
||||
setSelectedChordIndex(0);
|
||||
}, [cursorPosition?.pitch, cursorPosition?.beat]);
|
||||
|
||||
// Expose switchChord function via window for hotkey handler
|
||||
useEffect(() => {
|
||||
const switchChord = () => {
|
||||
if (matchingChords.length > 1) {
|
||||
setSelectedChordIndex(prev => (prev + 1) % matchingChords.length);
|
||||
}
|
||||
};
|
||||
|
||||
// Store the function on window object so PianoRoll can call it
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(window as any).__pianoGridSwitchChord = switchChord;
|
||||
|
||||
return () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
delete (window as any).__pianoGridSwitchChord;
|
||||
};
|
||||
}, [matchingChords.length]);
|
||||
|
||||
return (
|
||||
<div className="piano-grid-container">
|
||||
<div
|
||||
@@ -150,13 +214,33 @@ const PianoGrid: React.FC<PianoGridProps> = ({
|
||||
/>
|
||||
|
||||
{/* Vertical beat column highlight */}
|
||||
<div
|
||||
<div
|
||||
className="piano-grid-beat-highlight"
|
||||
style={{
|
||||
left: cursorPosition.beat * (parseInt(getComputedStyle(document.documentElement).getPropertyValue('--region-grid-beat-width')) || 40),
|
||||
width: parseInt(getComputedStyle(document.documentElement).getPropertyValue('--region-grid-beat-width')) || 40
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Chord highlights - render red boxes for each note in the matched chord */}
|
||||
{chordHighlights.map((highlight, index) => {
|
||||
const noteHeight = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--region-piano-key-height')) || 20;
|
||||
const beatWidth = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--region-grid-beat-width')) || 40;
|
||||
const yPosition = (107 - highlight.pitch) * noteHeight; // B7 = 107, reverse for display
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`chord-highlight-${index}`}
|
||||
className="piano-grid-chord-highlight"
|
||||
style={{
|
||||
top: yPosition,
|
||||
left: highlight.beat * beatWidth,
|
||||
width: beatWidth,
|
||||
height: noteHeight
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -677,6 +677,19 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
// Handle piano roll hotkeys
|
||||
const configManager = ConfigManager.instance();
|
||||
if (configManager.getIsInitialized()) {
|
||||
// Chord guide switch hotkey
|
||||
const switch_key = configManager.get('hotkeys.piano_roll.switch') as string;
|
||||
if (event.key && event.key.toLowerCase() === switch_key.toLowerCase()) {
|
||||
// Call the switchChord function exposed by PianoGrid
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const switchChord = (window as any).__pianoGridSwitchChord;
|
||||
if (typeof switchChord === 'function') {
|
||||
switchChord();
|
||||
event.preventDefault();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Snapping hotkeys
|
||||
const snap_none_key = configManager.get('hotkeys.piano_roll.snap_none') as string;
|
||||
const snap_1_4_key = configManager.get('hotkeys.piano_roll.snap_1_4') as string;
|
||||
@@ -851,6 +864,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
onSetDeleteNotesTrigger={handleSetDeleteNotesTrigger}
|
||||
selectedMode={selectedMode}
|
||||
keySignature={keySignature}
|
||||
chordGuide={chordGuide}
|
||||
/>
|
||||
|
||||
<div
|
||||
|
||||
@@ -24,6 +24,7 @@ interface PianoRollContentProps {
|
||||
onSetDeleteNotesTrigger?: (deleteFn: () => boolean) => void;
|
||||
selectedMode: string;
|
||||
keySignature: KeySignature;
|
||||
chordGuide: string;
|
||||
}
|
||||
|
||||
const PianoRollContent: React.FC<PianoRollContentProps> = ({
|
||||
@@ -37,7 +38,8 @@ const PianoRollContent: React.FC<PianoRollContentProps> = ({
|
||||
onSetNoteUpdateTrigger,
|
||||
onSetDeleteNotesTrigger,
|
||||
selectedMode,
|
||||
keySignature
|
||||
keySignature,
|
||||
chordGuide
|
||||
}) => {
|
||||
// Get KGCore instance
|
||||
const core = KGCore.instance();
|
||||
@@ -234,6 +236,7 @@ const PianoRollContent: React.FC<PianoRollContentProps> = ({
|
||||
regionStartBeat={activeRegion?.getStartFromBeat() || 0}
|
||||
selectedMode={selectedMode}
|
||||
keySignature={keySignature}
|
||||
chordGuide={chordGuide}
|
||||
>
|
||||
{memoizedNotes}
|
||||
</PianoGrid>
|
||||
|
||||
Reference in New Issue
Block a user