diff --git a/public/config.json b/public/config.json index 64a7de2..60c1a47 100644 --- a/public/config.json +++ b/public/config.json @@ -34,6 +34,7 @@ "main": { "hold_to_create_region": "ctrl", "play": "space", + "loop": "c", "undo": "ctrl+z", "redo": "ctrl+shift+z", "select_all": "ctrl+a", diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx index a07e3a2..432c97c 100644 --- a/src/components/Toolbar.tsx +++ b/src/components/Toolbar.tsx @@ -14,7 +14,6 @@ import { FaCog } from 'react-icons/fa'; import { KGProject, type KeySignature } from '../core/KGProject'; -import { ChangeLoopSettingsCommand } from '../core/commands'; import { plainToInstance, instanceToPlain } from 'class-transformer'; import { FaPencil, FaCopy, FaPaste, FaTrash } from 'react-icons/fa6'; import { KGMainContentState } from '../core/state/KGMainContentState'; @@ -34,7 +33,7 @@ const Toolbar: React.FC = () => { isPlaying, startPlaying, stopPlaying, setPlayheadPosition, currentTime, setBpm, setTimeSignature, setKeySignature, maxBars, setMaxBars, - isLooping, loopingRange, + isLooping, toggleLoop, canUndo, canRedo, undoDescription, redoDescription, undo, redo, toggleChatBox, toggleSettings, cleanupProjectState, // Piano roll state/actions @@ -415,45 +414,9 @@ const Toolbar: React.FC = () => { }; const handleLoopToggle = () => { - const core = KGCore.instance(); - const newLoopingState = !isLooping; - let newLoopingRange = loopingRange; - - // When enabling loop, validate and set the loop range - if (newLoopingState) { - const currentRange = loopingRange; - const projectMaxBars = maxBars; - - // If range is [0, 0], set it to the entire song - if (currentRange[0] === 0 && currentRange[1] === 0) { - newLoopingRange = [0, projectMaxBars] as [number, number]; - if (DEBUG_MODE.TOOLBAR) { - console.log("Loop range auto-set to entire song:", newLoopingRange); - } - } else { - // Validate range is within [0, maxBars] - const validatedStart = Math.max(0, Math.min(currentRange[0], projectMaxBars)); - const validatedEnd = Math.max(0, Math.min(currentRange[1], projectMaxBars)); - - // If range changed, update it - if (validatedStart !== currentRange[0] || validatedEnd !== currentRange[1]) { - newLoopingRange = [validatedStart, validatedEnd] as [number, number]; - if (DEBUG_MODE.TOOLBAR) { - console.log("Loop range clamped to valid range:", newLoopingRange); - } - } - } - } - - // Execute command for undo/redo support - const command = new ChangeLoopSettingsCommand({ - isLooping: newLoopingState, - loopingRange: newLoopingRange - }); - core.executeCommand(command); - + toggleLoop(); if (DEBUG_MODE.TOOLBAR) { - console.log("Loop toggle clicked, isLooping:", newLoopingState, "range:", newLoopingRange); + console.log("Loop toggle clicked"); } }; diff --git a/src/core/config/ConfigManager.ts b/src/core/config/ConfigManager.ts index 994635c..6ac99ed 100644 --- a/src/core/config/ConfigManager.ts +++ b/src/core/config/ConfigManager.ts @@ -40,6 +40,7 @@ interface AppConfig { main: { hold_to_create_region: string; play: string; + loop: string; undo: string; redo: string; select_all: string; @@ -205,12 +206,13 @@ export class ConfigManager { main: { hold_to_create_region: 'ctrl', play: 'space', + loop: 'c', undo: 'ctrl+z', redo: 'ctrl+shift+z', select_all: 'ctrl+a', copy: 'ctrl+c', cut: 'ctrl+x', - paste: 'ctrl+v', + paste: 'ctrl+v', save: 'ctrl+s' }, piano_roll: { diff --git a/src/hooks/useGlobalKeyboardHandler.ts b/src/hooks/useGlobalKeyboardHandler.ts index ac38500..3ab7ae8 100644 --- a/src/hooks/useGlobalKeyboardHandler.ts +++ b/src/hooks/useGlobalKeyboardHandler.ts @@ -11,7 +11,7 @@ import { selectAllNotesInActiveRegion } from '../util/selectionUtil'; * Handles keyboard shortcuts defined in the configuration */ export const useGlobalKeyboardHandler = () => { - const { undo, redo, setStatus, isPlaying, startPlaying, stopPlaying, projectName } = useProjectStore(); + const { undo, redo, setStatus, isPlaying, startPlaying, stopPlaying, toggleLoop, projectName } = useProjectStore(); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { @@ -59,6 +59,7 @@ export const useGlobalKeyboardHandler = () => { 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; // Check for undo shortcut @@ -136,6 +137,19 @@ export const useGlobalKeyboardHandler = () => { 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 save shortcut if (saveShortcut && matchesKeyboardShortcut(event, saveShortcut)) { event.preventDefault(); @@ -156,5 +170,5 @@ export const useGlobalKeyboardHandler = () => { return () => { document.removeEventListener('keydown', handleKeyDown, { capture: true }); }; - }, [undo, redo, setStatus, isPlaying, startPlaying, stopPlaying, projectName]); // Include dependencies for store actions + }, [undo, redo, setStatus, isPlaying, startPlaying, stopPlaying, toggleLoop, projectName]); // Include dependencies for store actions }; \ No newline at end of file diff --git a/src/stores/projectStore.ts b/src/stores/projectStore.ts index a4e5df4..2f30d18 100644 --- a/src/stores/projectStore.ts +++ b/src/stores/projectStore.ts @@ -12,6 +12,7 @@ import { KGRegion } from '../core/region/KGRegion'; import { AddTrackCommand, RemoveTrackCommand, ReorderTracksCommand, UpdateTrackCommand, type TrackUpdateProperties, PasteRegionsCommand, PasteNotesCommand, ChangeProjectPropertyCommand } from '../core/commands'; import { ConfigManager } from '../core/config/ConfigManager'; import { upgradeProjectToLatest } from '../core/project-upgrader/KGProjectUpgrader'; +import { toggleLoop } from '../util/loopUtil'; /** * Update CSS custom property for time signature numerator @@ -86,6 +87,7 @@ interface ProjectState { setPlayheadPosition: (position: number) => void; startPlaying: () => Promise; stopPlaying: () => Promise; + toggleLoop: () => void; setBpm: (bpm: number) => void; setMaxBars: (maxBars: number) => void; setTimeSignature: (timeSignature: TimeSignature) => void; @@ -570,6 +572,11 @@ export const useProjectStore = create((set, get) => { set({ isPlaying: false }); }, + toggleLoop: () => { + const { isLooping, loopingRange, maxBars } = get(); + toggleLoop(isLooping, loopingRange, maxBars); + }, + setBpm: (bpm: number) => { try { // Create and execute the change project property command diff --git a/src/util/loopUtil.ts b/src/util/loopUtil.ts new file mode 100644 index 0000000..c152861 --- /dev/null +++ b/src/util/loopUtil.ts @@ -0,0 +1,49 @@ +import { KGCore } from '../core/KGCore'; +import { ChangeLoopSettingsCommand } from '../core/commands'; + +/** + * Toggle the loop mode on/off with proper validation. + * When enabling loop mode: + * - If loop range is [0, 0], sets it to the entire song [0, maxBars] + * - Validates that loop range is within [0, maxBars] + * Uses the command pattern for undo/redo support. + * + * @param currentIsLooping Current loop mode state + * @param currentLoopingRange Current loop range [startBar, endBar] + * @param maxBars Maximum number of bars in the project + */ +export const toggleLoop = ( + currentIsLooping: boolean, + currentLoopingRange: [number, number], + maxBars: number +): void => { + const newLoopingState = !currentIsLooping; + let newLoopingRange = currentLoopingRange; + + // When enabling loop, validate and set the loop range + if (newLoopingState) { + const currentRange = currentLoopingRange; + const projectMaxBars = maxBars; + + // If range is [0, 0], set it to the entire song + if (currentRange[0] === 0 && currentRange[1] === 0) { + newLoopingRange = [0, projectMaxBars] as [number, number]; + } else { + // Validate range is within [0, maxBars] + const validatedStart = Math.max(0, Math.min(currentRange[0], projectMaxBars)); + const validatedEnd = Math.max(0, Math.min(currentRange[1], projectMaxBars)); + + // If range changed, update it + if (validatedStart !== currentRange[0] || validatedEnd !== currentRange[1]) { + newLoopingRange = [validatedStart, validatedEnd] as [number, number]; + } + } + } + + // Execute command for undo/redo support + const command = new ChangeLoopSettingsCommand({ + isLooping: newLoopingState, + loopingRange: newLoopingRange + }); + KGCore.instance().executeCommand(command); +};