import { useCallback, useEffect, useRef, useState } from 'react'; import { KGCore } from '../core/KGCore'; import { KGGlobalTrack } from '../core/global-track'; import { KGTrack } from '../core/track/KGTrack'; import { KGRegion } from '../core/region/KGRegion'; import { KGGlobalRegion } from '../core/region/KGGlobalRegion'; import { KGChordRegion } from '../core/region/KGChordRegion'; import { KGKeySignatureRegion } from '../core/region/KGKeySignatureRegion'; import { KGTempoRegion } from '../core/region/KGTempoRegion'; import { KGMidiRegion } from '../core/region/KGMidiRegion'; import { KGAudioRegion } from '../core/region/KGAudioRegion'; import { KGMarkerRegion } from '../core/region/KGMarkerRegion'; import type { RegionClickOptions, RegionUI } from '../components/interfaces'; import { DEBUG_MODE } from '../constants'; import { useProjectStore } from '../stores/projectStore'; import { useRegionOperations } from './useRegionOperations'; import { getAudioRegionDisplayLengthBeats } from '../util/globalTrackUtil'; const DEFAULT_REGION_CLICK_OPTIONS: RegionClickOptions = { shiftKey: false, metaKey: false, ctrlKey: false, }; interface UseMainContentRegionsParams { tracks: KGTrack[]; globalTracks: KGGlobalTrack[]; timeSignature: { numerator: number; denominator: number }; selectedRegionIds: string[]; clearAllSelections: () => void; setSelectedTrack: (trackId: string) => void; showPianoRoll: boolean; activeRegionId: string | null; setShowPianoRoll: (show: boolean) => void; setActiveRegionId: (regionId: string | null) => void; openMidiPianoRoll: (regionId: string) => void; openSpectrogramViewer: (regionId: string) => void; openHybridMode: (midiRegionId: string, audioRegionId: string) => void; pianoRollMode: 'midi-edit' | 'spectrogram' | 'hybrid'; updateTrack: (track: KGTrack) => void; maxBars: number; } interface MainContentSelectionState { selectedRegionId: string | null; findProjectRegionById: (regionId: string) => KGRegion | null; isGlobalRegionId: (regionId: string) => boolean; selectGlobalRegion: (regionId: string, options?: RegionClickOptions) => void; } export interface UseMainContentRegionsResult { regions: RegionUI[]; deleteSelectedRegions: () => boolean; selection: MainContentSelectionState; handleRegionCreated: (trackIndex: number, regionUI: RegionUI, midiRegion: KGMidiRegion) => void; handleExternalDropComplete: (trackIndex: number, regionUI: RegionUI) => void; handleRegionUpdated: ( regionId: string, updates: Partial, expectedModelUpdates?: { startBeat: number; length: number } ) => void; handleRegionClick: (regionId: string, options?: RegionClickOptions) => void; handleRegionLassoSelection: (regionIds: string[], options?: RegionClickOptions) => void; handleRegionLassoCommit: () => void; handleEmptyMainContentClick: (event: React.MouseEvent) => void; handleOpenPianoRoll: (regionId: string) => void; handleOpenSpectrogram: (regionId: string) => void; handleOpenHybrid: (regionId: string) => void; } export function useMainContentRegions({ tracks, globalTracks, timeSignature, selectedRegionIds, clearAllSelections, setSelectedTrack, showPianoRoll, activeRegionId, setShowPianoRoll, setActiveRegionId, openMidiPianoRoll, openSpectrogramViewer, openHybridMode, pianoRollMode, updateTrack, maxBars, }: UseMainContentRegionsParams): UseMainContentRegionsResult { const [regions, setRegions] = useState([]); const [selectedRegionId, setSelectedRegionId] = useState(null); const pendingUpdates = useRef>(new Map()); const preventEmptyMainContentDeselectRef = useRef(false); const pendingAutoSelectionRegionIdRef = useRef(null); const { deleteSelectedRegions } = useRegionOperations({ tracks, updateTrack, setRegions, selectedRegionId, setSelectedRegionId, showPianoRoll, setShowPianoRoll, activeRegionId, setActiveRegionId, }); const findProjectRegionById = useCallback((regionId: string): KGRegion | null => { for (const track of tracks) { const region = track.getRegions().find(candidate => candidate.getId() === regionId); if (region) { return region; } } for (const globalTrack of globalTracks) { const region = globalTrack.getRegions().find(candidate => candidate.getId() === regionId); if (region) { return region; } } return null; }, [globalTracks, tracks]); const isGlobalRegionId = useCallback((regionId: string) => { const region = findProjectRegionById(regionId); return region instanceof KGGlobalRegion; }, [findProjectRegionById]); useEffect(() => { if (pendingUpdates.current.size === 0) { return; } const updates = new Map(pendingUpdates.current); pendingUpdates.current.clear(); updates.forEach(update => { const { trackId, regionId, startBeat, length } = update; const track = tracks.find(candidate => candidate.getId().toString() === trackId); if (!track) { return; } const region = track.getRegions().find(candidate => candidate.getId() === regionId); if (region && DEBUG_MODE.MAIN_CONTENT) { console.log(`Verification - Region ${regionId} in track ${trackId}:`); console.log(` Expected: startBeat=${startBeat}, length=${length}`); console.log(` Actual: startBeat=${region.getStartFromBeat()}, length=${region.getLength()}, trackId=${region.getTrackId()}, trackIndex=${region.getTrackIndex()}`); const success = region.getStartFromBeat() === startBeat && region.getLength() === length && region.getTrackId() === trackId; console.log(` Update successful: ${success}`); } }); }, [tracks]); useEffect(() => { const updatedRegions: RegionUI[] = []; tracks.forEach(track => { const trackId = track.getId().toString(); const trackIndex = track.getTrackIndex(); track.getRegions().forEach(region => { if (region instanceof KGMidiRegion || region instanceof KGAudioRegion) { const beatsPerBar = timeSignature.numerator; const barNumber = (region.getStartFromBeat() / beatsPerBar) + 1; const lengthBeats = region instanceof KGAudioRegion ? getAudioRegionDisplayLengthBeats(KGCore.instance().getCurrentProject(), region) : region.getLength(); const length = lengthBeats / beatsPerBar; updatedRegions.push({ id: region.getId(), trackId, trackIndex, barNumber, length, name: region.getName(), }); } }); }); setRegions(updatedRegions); }, [globalTracks, timeSignature, tracks]); useEffect(() => { if (!showPianoRoll || !activeRegionId) { return; } const activeRegionStillExists = tracks.some(track => track.getRegions().some(region => region.getId() === activeRegionId) ); if (!activeRegionStillExists) { setShowPianoRoll(false); setActiveRegionId(null); } }, [tracks, showPianoRoll, activeRegionId, setShowPianoRoll, setActiveRegionId]); const applyRegionSelection = useCallback((orderedSelectionIds: string[]) => { const core = KGCore.instance(); const allRegions = [ ...tracks.flatMap(projectTrack => projectTrack.getRegions()), ...globalTracks.flatMap(globalTrack => globalTrack.getRegions()), ]; allRegions.forEach(projectRegion => projectRegion.deselect()); clearAllSelections(); const selectedRegions: KGRegion[] = orderedSelectionIds .map(selectedId => allRegions.find(region => region.getId() === selectedId) ?? null) .filter((selectedRegion): selectedRegion is KGRegion => selectedRegion !== null); selectedRegions.forEach(selectedRegion => selectedRegion.select()); if (selectedRegions.length > 0) { core.addSelectedItems(selectedRegions); } const lastSelectedRegion = selectedRegions.length > 0 ? selectedRegions[selectedRegions.length - 1] : null; const lastSelectedRegionId = lastSelectedRegion?.getId() ?? null; setSelectedRegionId(lastSelectedRegionId); if (lastSelectedRegionId && lastSelectedRegion && !(lastSelectedRegion instanceof KGGlobalRegion)) { setActiveRegionId(lastSelectedRegionId); } if (DEBUG_MODE.MAIN_CONTENT) { console.log(`Selected regions: ${selectedRegions.map(selectedRegion => selectedRegion.getId()).join(', ')}`); } if (!showPianoRoll || !lastSelectedRegionId || !lastSelectedRegion || lastSelectedRegion instanceof KGGlobalRegion) { return; } if (lastSelectedRegion instanceof KGAudioRegion) { openSpectrogramViewer(lastSelectedRegionId); } else if (lastSelectedRegion instanceof KGMidiRegion) { openMidiPianoRoll(lastSelectedRegionId); } }, [ clearAllSelections, globalTracks, openMidiPianoRoll, openSpectrogramViewer, setActiveRegionId, showPianoRoll, tracks, ]); const isAdditiveSelection = useCallback((options: RegionClickOptions) => options.metaKey || options.ctrlKey, []); const getOrderedTrackRegionIds = useCallback((trackId: string) => ( regions .filter(region => region.trackId === trackId) .sort((left, right) => { if (left.barNumber !== right.barNumber) { return left.barNumber - right.barNumber; } if (left.length !== right.length) { return left.length - right.length; } return left.id.localeCompare(right.id); }) .map(region => region.id) ), [regions]); const getOrderedGlobalRegionIds = useCallback((regionType: 'marker' | 'tempo' | 'signature' | 'chord') => ( globalTracks .flatMap(globalTrack => globalTrack.getRegions()) .filter((region): region is KGGlobalRegion => { if (regionType === 'tempo') { return region instanceof KGTempoRegion; } if (regionType === 'signature') { return region instanceof KGKeySignatureRegion; } if (regionType === 'chord') { return region instanceof KGChordRegion; } return region instanceof KGMarkerRegion; }) .sort((left, right) => { const leftStart = left instanceof KGTempoRegion || left instanceof KGKeySignatureRegion ? left.getStartBar() : Math.round(left.getStartFromBeat() / timeSignature.numerator); const rightStart = right instanceof KGTempoRegion || right instanceof KGKeySignatureRegion ? right.getStartBar() : Math.round(right.getStartFromBeat() / timeSignature.numerator); if (leftStart !== rightStart) { return leftStart - rightStart; } const leftLength = left instanceof KGTempoRegion || left instanceof KGKeySignatureRegion ? left.getLengthBars() : left.getLength(); const rightLength = right instanceof KGTempoRegion || right instanceof KGKeySignatureRegion ? right.getLengthBars() : right.getLength(); if (leftLength !== rightLength) { return leftLength - rightLength; } return left.getId().localeCompare(right.getId()); }) .map(region => region.getId()) ), [globalTracks, timeSignature.numerator]); const appendPrimarySelection = useCallback((orderedIds: string[], primaryRegionId: string) => { const deduped = orderedIds.filter(id => id !== primaryRegionId); return [...deduped, primaryRegionId]; }, []); const buildSameTrackRangeSelection = useCallback(( existingRegularSelectionIds: string[], anchorRegionId: string, targetRegionId: string, orderedTrackRegionIds: string[] ) => { const anchorIndex = orderedTrackRegionIds.indexOf(anchorRegionId); const targetIndex = orderedTrackRegionIds.indexOf(targetRegionId); if (anchorIndex === -1 || targetIndex === -1) { return [targetRegionId]; } const [startIndex, endIndex] = anchorIndex <= targetIndex ? [anchorIndex, targetIndex] : [targetIndex, anchorIndex]; const rangeIds = orderedTrackRegionIds.slice(startIndex, endIndex + 1); const rangeIdSet = new Set(rangeIds); const preservedOtherTrackIds = existingRegularSelectionIds.filter(selectedId => !rangeIdSet.has(selectedId)); return appendPrimarySelection([...preservedOtherTrackIds, ...rangeIds], targetRegionId); }, [appendPrimarySelection]); const buildSameLaneRangeSelection = useCallback(( existingGlobalSelectionIds: string[], anchorRegionId: string, targetRegionId: string, orderedLaneRegionIds: string[] ) => { const anchorIndex = orderedLaneRegionIds.indexOf(anchorRegionId); const targetIndex = orderedLaneRegionIds.indexOf(targetRegionId); if (anchorIndex === -1 || targetIndex === -1) { return [targetRegionId]; } const [startIndex, endIndex] = anchorIndex <= targetIndex ? [anchorIndex, targetIndex] : [targetIndex, anchorIndex]; const rangeIds = orderedLaneRegionIds.slice(startIndex, endIndex + 1); const laneIdSet = new Set(orderedLaneRegionIds); const preservedOtherLaneIds = existingGlobalSelectionIds.filter(selectedId => !laneIdSet.has(selectedId)); return appendPrimarySelection([...preservedOtherLaneIds, ...rangeIds], targetRegionId); }, [appendPrimarySelection]); const getGlobalRegionLaneType = useCallback((region: KGGlobalRegion): 'marker' | 'tempo' | 'signature' | 'chord' => { if (region instanceof KGTempoRegion) { return 'tempo'; } if (region instanceof KGKeySignatureRegion) { return 'signature'; } if (region instanceof KGChordRegion) { return 'chord'; } return 'marker'; }, []); const selectRegion = useCallback(( regionId: string, options: RegionClickOptions = DEFAULT_REGION_CLICK_OPTIONS, regionsToSearch?: RegionUI[] ) => { const regionsToUse = regionsToSearch || regions; const region = regionsToUse.find(candidate => candidate.id === regionId); if (!region) { if (DEBUG_MODE.MAIN_CONTENT) { console.log(`Region not found in UI state: ${regionId}`); } return; } const track = tracks.find(candidate => candidate.getId().toString() === region.trackId); if (!track) { if (DEBUG_MODE.MAIN_CONTENT) { console.log(`Track not found for region: ${regionId}`); } return; } const coreRegion = track.getRegions().find(candidate => candidate.getId() === regionId); if (!coreRegion) { if (DEBUG_MODE.MAIN_CONTENT) { console.log(`Region not found in track model: ${regionId}`); } return; } const regularSelectedRegionIds = selectedRegionIds.filter(selectedId => !isGlobalRegionId(selectedId)); const additiveSelection = isAdditiveSelection(options); let orderedSelection: string[]; if (additiveSelection) { orderedSelection = regularSelectedRegionIds.includes(regionId) ? regularSelectedRegionIds.filter(id => id !== regionId) : appendPrimarySelection([...regularSelectedRegionIds, regionId], regionId); } else if (options.shiftKey) { const sameTrackSelectedIds = regularSelectedRegionIds.filter(selectedId => { const selectedRegion = regionsToUse.find(candidate => candidate.id === selectedId); return selectedRegion?.trackId === region.trackId; }); const anchorRegionId = [...sameTrackSelectedIds].reverse().find(selectedId => selectedId !== regionId) ?? null; if (!anchorRegionId) { orderedSelection = [regionId]; } else { orderedSelection = buildSameTrackRangeSelection( regularSelectedRegionIds, anchorRegionId, regionId, getOrderedTrackRegionIds(region.trackId) ); } } else { orderedSelection = [regionId]; } applyRegionSelection(orderedSelection); }, [ appendPrimarySelection, applyRegionSelection, buildSameTrackRangeSelection, getOrderedTrackRegionIds, isAdditiveSelection, isGlobalRegionId, regions, selectedRegionIds, tracks, ]); const selectGlobalRegion = useCallback((regionId: string, options: RegionClickOptions = DEFAULT_REGION_CLICK_OPTIONS) => { const globalSelectedRegionIds = selectedRegionIds.filter(selectedId => isGlobalRegionId(selectedId)); const globalRegion = findProjectRegionById(regionId); if (!(globalRegion instanceof KGGlobalRegion)) { return; } const targetLaneType = getGlobalRegionLaneType(globalRegion); const sameLaneSelectedRegionIds = globalSelectedRegionIds.filter(selectedId => { const selectedRegion = findProjectRegionById(selectedId); return selectedRegion instanceof KGGlobalRegion && getGlobalRegionLaneType(selectedRegion) === targetLaneType; }); const additiveSelection = isAdditiveSelection(options); let orderedSelection: string[]; if (additiveSelection) { orderedSelection = sameLaneSelectedRegionIds.includes(regionId) ? sameLaneSelectedRegionIds.filter(id => id !== regionId) : appendPrimarySelection([...sameLaneSelectedRegionIds, regionId], regionId); } else if (options.shiftKey) { const orderedLaneRegionIds = getOrderedGlobalRegionIds(targetLaneType); const laneRegionIdSet = new Set(orderedLaneRegionIds); const sameLaneSelectedIds = sameLaneSelectedRegionIds.filter(selectedId => laneRegionIdSet.has(selectedId)); const anchorRegionId = [...sameLaneSelectedIds].reverse().find(selectedId => selectedId !== regionId) ?? null; if (!anchorRegionId) { orderedSelection = [regionId]; } else { orderedSelection = buildSameLaneRangeSelection( sameLaneSelectedRegionIds, anchorRegionId, regionId, orderedLaneRegionIds ); } } else { orderedSelection = [regionId]; } applyRegionSelection(orderedSelection); }, [ appendPrimarySelection, applyRegionSelection, buildSameLaneRangeSelection, findProjectRegionById, getGlobalRegionLaneType, getOrderedGlobalRegionIds, isAdditiveSelection, isGlobalRegionId, selectedRegionIds, ]); const handleRegionLassoSelection = useCallback((regionIds: string[], options: RegionClickOptions = DEFAULT_REGION_CLICK_OPTIONS) => { const orderedRegionIds = regionIds.filter(regionId => regions.some(region => region.id === regionId)); const regularSelectedRegionIds = selectedRegionIds.filter(selectedId => !isGlobalRegionId(selectedId)); const additiveSelection = isAdditiveSelection(options); const orderedSelection = additiveSelection ? orderedRegionIds.reduce((nextSelection, regionId) => { if (nextSelection.includes(regionId)) { return nextSelection.filter(id => id !== regionId); } return appendPrimarySelection([...nextSelection, regionId], regionId); }, [...regularSelectedRegionIds]) : orderedRegionIds; applyRegionSelection(orderedSelection); }, [appendPrimarySelection, applyRegionSelection, isAdditiveSelection, isGlobalRegionId, regions, selectedRegionIds]); const handleEmptyMainContentClick = useCallback((event: React.MouseEvent) => { if (event.target !== event.currentTarget) { return; } if (preventEmptyMainContentDeselectRef.current) { preventEmptyMainContentDeselectRef.current = false; return; } handleRegionLassoSelection([], DEFAULT_REGION_CLICK_OPTIONS); }, [handleRegionLassoSelection]); const handleRegionLassoCommit = useCallback(() => { preventEmptyMainContentDeselectRef.current = true; }, []); useEffect(() => { const pendingRegionId = pendingAutoSelectionRegionIdRef.current; if (!pendingRegionId) { return; } const regionExists = regions.some(region => region.id === pendingRegionId); if (!regionExists) { return; } pendingAutoSelectionRegionIdRef.current = null; selectRegion(pendingRegionId, DEFAULT_REGION_CLICK_OPTIONS, regions); }, [regions, selectRegion]); const handleRegionCreated = useCallback((trackIndex: number, regionUI: RegionUI) => { const track = tracks[trackIndex]; updateTrack(track); setSelectedTrack(track.getId().toString()); pendingAutoSelectionRegionIdRef.current = regionUI.id; setRegions(previousRegions => [...previousRegions, regionUI]); }, [setSelectedTrack, tracks, updateTrack]); const handleExternalDropComplete = useCallback((trackIndex: number, regionUI: RegionUI) => { const track = tracks[trackIndex]; if (!track) { return; } updateTrack(track); setSelectedTrack(track.getId().toString()); const coreMaxBars = KGCore.instance().getCurrentProject().getMaxBars(); if (coreMaxBars > maxBars) { useProjectStore.setState({ maxBars: coreMaxBars }); document.documentElement.style.setProperty('--max-number-of-bars', coreMaxBars.toString()); } pendingAutoSelectionRegionIdRef.current = regionUI.id; setRegions(previousRegions => [...previousRegions, regionUI]); }, [maxBars, setSelectedTrack, tracks, updateTrack]); const handleRegionUpdated = useCallback(( regionId: string, updates: Partial, expectedModelUpdates?: { startBeat: number; length: number } ) => { if (DEBUG_MODE.MAIN_CONTENT) { console.log(`Updating region ${regionId} with:`, updates); } selectRegion(regionId); const updatedRegion = regions.find(region => region.id === regionId); if (updatedRegion) { const trackId = updates.trackId || updatedRegion.trackId; const track = tracks.find(candidate => candidate.getId().toString() === trackId); if (track) { setSelectedTrack(track.getId().toString()); } } setRegions(previousRegions => previousRegions.map(region => ( region.id === regionId ? { ...region, ...updates } : region ))); const region = regions.find(candidate => candidate.id === regionId); if (!region) { return; } if (updates.trackId && updates.trackId !== region.trackId) { if (DEBUG_MODE.MAIN_CONTENT) { console.log(`Region ${regionId} moved from track ${region.trackId} to track ${updates.trackId}`); } const originalTrack = tracks.find(candidate => candidate.getId().toString() === region.trackId); const targetTrack = tracks.find(candidate => candidate.getId().toString() === updates.trackId); if (originalTrack && targetTrack) { setSelectedTrack(targetTrack.getId().toString()); updateTrack(originalTrack); updateTrack(targetTrack); if (expectedModelUpdates) { const key = `${updates.trackId}-${regionId}-${Date.now()}`; pendingUpdates.current.set(key, { trackId: updates.trackId, regionId, startBeat: expectedModelUpdates.startBeat, length: expectedModelUpdates.length, }); } } } else { const track = tracks.find(candidate => candidate.getId().toString() === region.trackId); if (track) { const trackRegions = track.getRegions(); const midiRegion = trackRegions.find(candidate => candidate.getId() === regionId) as KGMidiRegion | undefined; if (midiRegion) { if (expectedModelUpdates) { if (DEBUG_MODE.MAIN_CONTENT) { console.log(`MainContent - Expected model updates: startBeat=${expectedModelUpdates.startBeat}, length=${expectedModelUpdates.length}`); } const key = `${track.getId()}-${regionId}-${Date.now()}`; pendingUpdates.current.set(key, { trackId: track.getId().toString(), regionId, startBeat: expectedModelUpdates.startBeat, length: expectedModelUpdates.length, }); } else { const startBeat = midiRegion.getStartFromBeat(); const length = midiRegion.getLength(); if (DEBUG_MODE.MAIN_CONTENT) { console.log(`MainContent - Region before store update: startBeat=${startBeat}, length=${length}`); } const key = `${track.getId()}-${regionId}-${Date.now()}`; pendingUpdates.current.set(key, { trackId: track.getId().toString(), regionId, startBeat, length, }); } } updateTrack(track); } } if (showPianoRoll && activeRegionId === regionId) { setActiveRegionId(regionId); if (DEBUG_MODE.MAIN_CONTENT) { console.log(`Updated region ${regionId} set as active region in piano roll`); } } }, [ activeRegionId, regions, selectRegion, setActiveRegionId, setSelectedTrack, showPianoRoll, tracks, updateTrack, ]); const handleRegionClick = useCallback((regionId: string, options: RegionClickOptions = DEFAULT_REGION_CLICK_OPTIONS) => { if (DEBUG_MODE.MAIN_CONTENT) { console.log(`Region clicked in MainContent (selection only): ${regionId}`); } selectRegion(regionId, options); const region = regions.find(candidate => candidate.id === regionId); if (!region) { return; } const track = tracks.find(candidate => candidate.getId().toString() === region.trackId); if (!track) { return; } setSelectedTrack(track.getId().toString()); }, [regions, selectRegion, setSelectedTrack, tracks]); const handleOpenPianoRoll = useCallback((regionId: string) => { const project = KGCore.instance().getCurrentProject(); for (const track of project.getTracks()) { const region = track.getRegions().find(candidate => candidate.getId() === regionId); if (region && region.getCurrentType() === 'KGAudioRegion') { return; } } if (DEBUG_MODE.MAIN_CONTENT) { console.log(`Open piano roll via pencil for region: ${regionId}`); } handleRegionClick(regionId, DEFAULT_REGION_CLICK_OPTIONS); openMidiPianoRoll(regionId); }, [handleRegionClick, openMidiPianoRoll]); const handleOpenSpectrogram = useCallback((regionId: string) => { handleRegionClick(regionId, DEFAULT_REGION_CLICK_OPTIONS); openSpectrogramViewer(regionId); }, [handleRegionClick, openSpectrogramViewer]); const handleOpenHybrid = useCallback((regionId: string) => { if (pianoRollMode === 'midi-edit' && activeRegionId) { openHybridMode(activeRegionId, regionId); } else if (pianoRollMode === 'spectrogram' && activeRegionId) { openHybridMode(regionId, activeRegionId); } }, [activeRegionId, openHybridMode, pianoRollMode]); return { regions, deleteSelectedRegions, selection: { selectedRegionId, findProjectRegionById, isGlobalRegionId, selectGlobalRegion, }, handleRegionCreated, handleExternalDropComplete, handleRegionUpdated, handleRegionClick, handleRegionLassoSelection, handleRegionLassoCommit, handleEmptyMainContentClick, handleOpenPianoRoll, handleOpenSpectrogram, handleOpenHybrid, }; }