220 lines
8.0 KiB
TypeScript
220 lines
8.0 KiB
TypeScript
import { useEffect } from 'react';
|
|
import { matchesKeyboardShortcut } from '../util/osUtil';
|
|
import { handleCopyOperation, handlePasteOperation } from '../util/copyPasteUtil';
|
|
import { saveProject } from '../util/saveUtil';
|
|
import { ConfigManager } from '../core/config/ConfigManager';
|
|
import { useProjectStore } from '../stores/projectStore';
|
|
import { selectAllNotesInActiveRegion } from '../util/selectionUtil';
|
|
import { KGCore } from '../core/KGCore';
|
|
import { KGMidiInput } from '../core/midi-input/KGMidiInput';
|
|
import { KGMidiRegion } from '../core/region/KGMidiRegion';
|
|
|
|
/**
|
|
* Global keyboard handler for copy/paste, undo/redo, play/pause, and save operations
|
|
* Handles keyboard shortcuts defined in the configuration
|
|
*/
|
|
export const useGlobalKeyboardHandler = () => {
|
|
const { undo, redo, setStatus, isPlaying, startPlaying, stopPlaying, toggleLoop, projectName, savedProjectName, setSavedProjectName, setProjectName, isRecording, startRecording, stopRecording, activeRegionId, selectedRegionIds, setActiveRegionId, setShowPianoRoll } = useProjectStore();
|
|
|
|
useEffect(() => {
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
// Skip if user is typing in an input field
|
|
const target = event.target as HTMLElement;
|
|
if (target && (
|
|
target.tagName === 'INPUT' ||
|
|
target.tagName === 'TEXTAREA' ||
|
|
target.contentEditable === 'true' ||
|
|
target.hasAttribute('data-chatbox-input') ||
|
|
target.closest('.chatbox-input')
|
|
)) {
|
|
return;
|
|
}
|
|
|
|
// Enhanced prevention for system shortcuts like Cmd+S/Ctrl+S
|
|
const isCtrlOrCmd = event.ctrlKey || event.metaKey;
|
|
const isSKey = event.key.toLowerCase() === 's';
|
|
|
|
if (isCtrlOrCmd && isSKey) {
|
|
// Use multiple prevention methods for better browser compatibility
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
event.stopImmediatePropagation();
|
|
|
|
// Early return to handle save immediately
|
|
try {
|
|
saveProject(projectName, savedProjectName, setStatus, (finalName) => {
|
|
setSavedProjectName(finalName);
|
|
if (finalName !== projectName) setProjectName(finalName);
|
|
});
|
|
} catch (error) {
|
|
console.error('Save failed:', error);
|
|
setStatus('Save failed');
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Get keyboard shortcuts from config
|
|
const configManager = ConfigManager.instance();
|
|
if (!configManager.getIsInitialized()) {
|
|
return;
|
|
}
|
|
|
|
const undoShortcut = configManager.get('hotkeys.main.undo') as string;
|
|
const redoShortcut = configManager.get('hotkeys.main.redo') as string;
|
|
const copyShortcut = configManager.get('hotkeys.main.copy') as string;
|
|
const pasteShortcut = configManager.get('hotkeys.main.paste') as string;
|
|
const selectAllShortcut = configManager.get('hotkeys.main.select_all') as string;
|
|
const playShortcut = configManager.get('hotkeys.main.play') as string;
|
|
const loopShortcut = configManager.get('hotkeys.main.loop') as string;
|
|
const saveShortcut = configManager.get('hotkeys.main.save') as string;
|
|
const recordShortcut = configManager.get('hotkeys.main.record') as string;
|
|
|
|
// Check for undo shortcut
|
|
if (undoShortcut && matchesKeyboardShortcut(event, undoShortcut)) {
|
|
event.preventDefault();
|
|
try {
|
|
undo();
|
|
setStatus('Undo performed');
|
|
} catch (error) {
|
|
console.error('Undo failed:', error);
|
|
setStatus('Undo failed');
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Check for redo shortcut
|
|
if (redoShortcut && matchesKeyboardShortcut(event, redoShortcut)) {
|
|
event.preventDefault();
|
|
try {
|
|
redo();
|
|
setStatus('Redo performed');
|
|
} catch (error) {
|
|
console.error('Redo failed:', error);
|
|
setStatus('Redo failed');
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Check for copy shortcut
|
|
if (copyShortcut && matchesKeyboardShortcut(event, copyShortcut)) {
|
|
event.preventDefault();
|
|
const copied = handleCopyOperation();
|
|
if (copied) {
|
|
setStatus('Items copied to clipboard');
|
|
} else {
|
|
setStatus('No items selected to copy');
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Check for paste shortcut
|
|
if (pasteShortcut && matchesKeyboardShortcut(event, pasteShortcut)) {
|
|
event.preventDefault();
|
|
const pasted = handlePasteOperation();
|
|
if (pasted) {
|
|
setStatus('Items pasted from clipboard');
|
|
} else {
|
|
setStatus('Cannot paste - no valid clipboard content or context');
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Check for select-all-notes shortcut (only when piano roll is open)
|
|
if (selectAllShortcut && matchesKeyboardShortcut(event, selectAllShortcut)) {
|
|
event.preventDefault();
|
|
selectAllNotesInActiveRegion();
|
|
return;
|
|
}
|
|
|
|
// Check for play/pause shortcut
|
|
if (playShortcut && matchesKeyboardShortcut(event, playShortcut)) {
|
|
event.preventDefault();
|
|
try {
|
|
if (!isPlaying) {
|
|
startPlaying();
|
|
setStatus('Playback started');
|
|
} else {
|
|
stopPlaying();
|
|
setStatus('Playback stopped');
|
|
}
|
|
} catch (error) {
|
|
console.error('Play/pause failed:', error);
|
|
setStatus('Play/pause failed');
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Check for loop toggle shortcut
|
|
if (loopShortcut && matchesKeyboardShortcut(event, loopShortcut)) {
|
|
event.preventDefault();
|
|
try {
|
|
toggleLoop();
|
|
setStatus('Loop toggled');
|
|
} catch (error) {
|
|
console.error('Loop toggle failed:', error);
|
|
setStatus('Loop toggle failed');
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Check for record toggle shortcut
|
|
if (recordShortcut && matchesKeyboardShortcut(event, recordShortcut)) {
|
|
event.preventDefault();
|
|
if (isRecording) {
|
|
stopRecording();
|
|
setStatus('Recording stopped — notes committed');
|
|
return;
|
|
}
|
|
const candidateId = activeRegionId ?? (selectedRegionIds[0] ?? null);
|
|
if (!candidateId) {
|
|
setStatus('Select a MIDI region before recording');
|
|
return;
|
|
}
|
|
const tracks = KGCore.instance().getCurrentProject().getTracks();
|
|
let isMidi = false;
|
|
for (const track of tracks) {
|
|
const region = track.getRegions().find(r => r.getId() === candidateId);
|
|
if (region) { isMidi = region instanceof KGMidiRegion; break; }
|
|
}
|
|
if (!isMidi) {
|
|
setStatus('Select a MIDI region before recording');
|
|
return;
|
|
}
|
|
if (KGMidiInput.instance().getConnectedInputCount() === 0) {
|
|
setStatus('No MIDI device detected');
|
|
return;
|
|
}
|
|
if (!activeRegionId) {
|
|
setActiveRegionId(candidateId);
|
|
setShowPianoRoll(true);
|
|
}
|
|
startRecording();
|
|
setStatus('Recording started...');
|
|
return;
|
|
}
|
|
|
|
// Check for save shortcut
|
|
if (saveShortcut && matchesKeyboardShortcut(event, saveShortcut)) {
|
|
event.preventDefault();
|
|
try {
|
|
saveProject(projectName, savedProjectName, setStatus, (finalName) => {
|
|
setSavedProjectName(finalName);
|
|
if (finalName !== projectName) setProjectName(finalName);
|
|
});
|
|
} catch (error) {
|
|
console.error('Save failed:', error);
|
|
setStatus('Save failed');
|
|
}
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Add global event listener with capture for earlier interception
|
|
document.addEventListener('keydown', handleKeyDown, { capture: true });
|
|
|
|
// Cleanup on unmount
|
|
return () => {
|
|
document.removeEventListener('keydown', handleKeyDown, { capture: true });
|
|
};
|
|
}, [undo, redo, setStatus, isPlaying, startPlaying, stopPlaying, toggleLoop, projectName, isRecording, startRecording, stopRecording, activeRegionId, selectedRegionIds, setActiveRegionId, setShowPianoRoll]); // Include dependencies for store actions
|
|
}; |