feat: added shortcut N for sheet music view

This commit is contained in:
Xiaohan-Tian
2026-05-18 19:12:08 -07:00
parent 4cf474be7e
commit 97dad696b2
7 changed files with 253 additions and 8 deletions
+99 -2
View File
@@ -2,12 +2,27 @@ import React from 'react';
import { fireEvent, render, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useGlobalKeyboardHandler } from './useGlobalKeyboardHandler';
import { showAlert } from '../util/dialogUtil';
const regionEditUtilMocks = vi.hoisted(() => ({
splitSelectedRegionAtPlayhead: vi.fn(),
mergeSelectedMidiRegions: vi.fn(),
}));
const MockMidiRegion = vi.hoisted(() => class {
private readonly id: string;
constructor(id: string) {
this.id = id;
}
getId() {
return this.id;
}
});
let mockTracks: Array<{ getRegions: () => Array<{ getId: () => string }> }> = [];
const storeState = {
undo: vi.fn(),
redo: vi.fn(),
@@ -30,6 +45,7 @@ const storeState = {
setShowPianoRoll: vi.fn(),
showPianoRoll: false,
openMidiPianoRoll: vi.fn(),
openMidiPianoRollWithSheetMusicView: vi.fn(),
openSpectrogramViewer: vi.fn(),
playheadPosition: 12,
refreshProjectState: vi.fn(),
@@ -101,7 +117,7 @@ vi.mock('../core/KGCore', () => ({
KGCore: {
instance: () => ({
getCurrentProject: () => ({
getTracks: () => [],
getTracks: () => mockTracks,
}),
}),
},
@@ -116,7 +132,7 @@ vi.mock('../core/midi-input/KGMidiInput', () => ({
}));
vi.mock('../core/region/KGMidiRegion', () => ({
KGMidiRegion: class {},
KGMidiRegion: MockMidiRegion,
}));
vi.mock('../core/track/KGAudioTrack', () => ({
@@ -139,9 +155,18 @@ const HookHarness = () => {
describe('useGlobalKeyboardHandler region shortcuts', () => {
beforeEach(() => {
mockTracks = [];
regionEditUtilMocks.splitSelectedRegionAtPlayhead.mockReset();
regionEditUtilMocks.mergeSelectedMidiRegions.mockReset();
storeState.setStatus.mockClear();
storeState.setShowPianoRoll.mockClear();
storeState.showPianoRoll = false;
storeState.activeRegionId = null;
storeState.selectedRegionIds = ['region-a', 'region-b'];
storeState.openMidiPianoRoll.mockClear();
storeState.openMidiPianoRollWithSheetMusicView.mockClear();
storeState.openSpectrogramViewer.mockClear();
vi.mocked(showAlert).mockClear();
});
it('triggers split on Ctrl+T', async () => {
@@ -180,4 +205,76 @@ describe('useGlobalKeyboardHandler region shortcuts', () => {
expect(storeState.setStatus).toHaveBeenCalledWith('Merged 2 MIDI regions');
});
});
it('opens a selected MIDI region in piano roll view on E', () => {
mockTracks = [
{
getRegions: () => [new MockMidiRegion('region-b')],
},
];
render(<HookHarness />);
fireEvent.keyDown(document.body, { key: 'e' });
expect(storeState.openMidiPianoRollWithSheetMusicView).toHaveBeenCalledWith('region-b', false);
expect(storeState.openSpectrogramViewer).not.toHaveBeenCalled();
});
it('opens a selected MIDI region in sheet music view on N', () => {
mockTracks = [
{
getRegions: () => [new MockMidiRegion('region-b')],
},
];
render(<HookHarness />);
fireEvent.keyDown(document.body, { key: 'n' });
expect(storeState.openMidiPianoRollWithSheetMusicView).toHaveBeenCalledWith('region-b', true);
expect(storeState.setShowPianoRoll).not.toHaveBeenCalled();
});
it('shows the editor alert on N when no region is selected', () => {
storeState.selectedRegionIds = [];
render(<HookHarness />);
fireEvent.keyDown(document.body, { key: 'n' });
expect(showAlert).toHaveBeenCalledWith('Please select a region to open the editor.');
expect(storeState.openMidiPianoRollWithSheetMusicView).not.toHaveBeenCalled();
});
it('does not open spectrogram on N for an audio region', () => {
mockTracks = [
{
getRegions: () => [
{
getId: () => 'region-b',
},
],
},
];
render(<HookHarness />);
fireEvent.keyDown(document.body, { key: 'n' });
expect(showAlert).toHaveBeenCalledWith('Sheet music view is only available for MIDI regions.');
expect(storeState.openSpectrogramViewer).not.toHaveBeenCalled();
expect(storeState.openMidiPianoRollWithSheetMusicView).not.toHaveBeenCalled();
});
it('does not close the editor when N is pressed while piano roll is already open', () => {
storeState.showPianoRoll = true;
mockTracks = [
{
getRegions: () => [new MockMidiRegion('region-b')],
},
];
render(<HookHarness />);
fireEvent.keyDown(document.body, { key: 'n' });
expect(storeState.setShowPianoRoll).not.toHaveBeenCalled();
expect(storeState.openMidiPianoRollWithSheetMusicView).toHaveBeenCalledWith('region-b', true);
});
});
+30 -3
View File
@@ -17,7 +17,7 @@ import { showAlert } from '../util/dialogUtil';
* Handles keyboard shortcuts defined in the configuration
*/
export const useGlobalKeyboardHandler = () => {
const { undo, redo, setStatus, isPlaying, startPlaying, stopTransport, toggleLoop, projectName, savedProjectName, setSavedProjectName, setProjectName, isRecording, startRecording, stopRecording, activeRegionId, selectedRegionIds, setActiveRegionId, setShowPianoRoll, showPianoRoll, openMidiPianoRoll, openSpectrogramViewer, playheadPosition, refreshProjectState } = useProjectStore();
const { undo, redo, setStatus, isPlaying, startPlaying, stopTransport, toggleLoop, projectName, savedProjectName, setSavedProjectName, setProjectName, isRecording, startRecording, stopRecording, activeRegionId, selectedRegionIds, setActiveRegionId, setShowPianoRoll, showPianoRoll, openMidiPianoRollWithSheetMusicView, openSpectrogramViewer, playheadPosition, refreshProjectState } = useProjectStore();
const lastSelectedRegionId = selectedRegionIds[selectedRegionIds.length - 1] ?? null;
useEffect(() => {
@@ -256,13 +256,40 @@ export const useGlobalKeyboardHandler = () => {
}
}
if (foundMidi) {
openMidiPianoRoll(candidateId);
openMidiPianoRollWithSheetMusicView(candidateId, false);
} else if (foundAudio) {
openSpectrogramViewer(candidateId);
}
return;
}
// Check for sheet music shortcut (N) — open piano roll for MIDI in sheet music view
if (event.key.toLowerCase() === 'n' && !event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
event.preventDefault();
const candidateId = activeRegionId ?? lastSelectedRegionId;
if (!candidateId) {
void showAlert('Please select a region to open the editor.');
return;
}
const tracks = KGCore.instance().getCurrentProject().getTracks();
let foundMidi = false;
let foundAudio = false;
for (const track of tracks) {
const region = track.getRegions().find(r => r.getId() === candidateId);
if (region) {
if (region instanceof KGMidiRegion) foundMidi = true;
else foundAudio = true;
break;
}
}
if (foundMidi) {
openMidiPianoRollWithSheetMusicView(candidateId, true);
} else if (foundAudio) {
void showAlert('Sheet music view is only available for MIDI regions.');
}
return;
}
// Check for save shortcut
if (saveShortcut && matchesKeyboardShortcut(event, saveShortcut)) {
event.preventDefault();
@@ -307,7 +334,7 @@ export const useGlobalKeyboardHandler = () => {
setActiveRegionId,
setShowPianoRoll,
showPianoRoll,
openMidiPianoRoll,
openMidiPianoRollWithSheetMusicView,
openSpectrogramViewer,
playheadPosition,
refreshProjectState,