feat: added a hotkey for loop button

This commit is contained in:
Xiaohan-Tian
2026-01-22 19:05:22 -08:00
parent b80e7e84b8
commit 78b798e138
6 changed files with 79 additions and 43 deletions
+1
View File
@@ -34,6 +34,7 @@
"main": { "main": {
"hold_to_create_region": "ctrl", "hold_to_create_region": "ctrl",
"play": "space", "play": "space",
"loop": "c",
"undo": "ctrl+z", "undo": "ctrl+z",
"redo": "ctrl+shift+z", "redo": "ctrl+shift+z",
"select_all": "ctrl+a", "select_all": "ctrl+a",
+3 -40
View File
@@ -14,7 +14,6 @@ import {
FaCog FaCog
} from 'react-icons/fa'; } from 'react-icons/fa';
import { KGProject, type KeySignature } from '../core/KGProject'; import { KGProject, type KeySignature } from '../core/KGProject';
import { ChangeLoopSettingsCommand } from '../core/commands';
import { plainToInstance, instanceToPlain } from 'class-transformer'; import { plainToInstance, instanceToPlain } from 'class-transformer';
import { FaPencil, FaCopy, FaPaste, FaTrash } from 'react-icons/fa6'; import { FaPencil, FaCopy, FaPaste, FaTrash } from 'react-icons/fa6';
import { KGMainContentState } from '../core/state/KGMainContentState'; import { KGMainContentState } from '../core/state/KGMainContentState';
@@ -34,7 +33,7 @@ const Toolbar: React.FC = () => {
isPlaying, startPlaying, stopPlaying, setPlayheadPosition, isPlaying, startPlaying, stopPlaying, setPlayheadPosition,
currentTime, setBpm, setTimeSignature, setKeySignature, currentTime, setBpm, setTimeSignature, setKeySignature,
maxBars, setMaxBars, maxBars, setMaxBars,
isLooping, loopingRange, isLooping, toggleLoop,
canUndo, canRedo, undoDescription, redoDescription, undo, redo, canUndo, canRedo, undoDescription, redoDescription, undo, redo,
toggleChatBox, toggleSettings, cleanupProjectState, toggleChatBox, toggleSettings, cleanupProjectState,
// Piano roll state/actions // Piano roll state/actions
@@ -415,45 +414,9 @@ const Toolbar: React.FC = () => {
}; };
const handleLoopToggle = () => { const handleLoopToggle = () => {
const core = KGCore.instance(); toggleLoop();
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);
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log("Loop toggle clicked, isLooping:", newLoopingState, "range:", newLoopingRange); console.log("Loop toggle clicked");
} }
}; };
+3 -1
View File
@@ -40,6 +40,7 @@ interface AppConfig {
main: { main: {
hold_to_create_region: string; hold_to_create_region: string;
play: string; play: string;
loop: string;
undo: string; undo: string;
redo: string; redo: string;
select_all: string; select_all: string;
@@ -205,12 +206,13 @@ export class ConfigManager {
main: { main: {
hold_to_create_region: 'ctrl', hold_to_create_region: 'ctrl',
play: 'space', play: 'space',
loop: 'c',
undo: 'ctrl+z', undo: 'ctrl+z',
redo: 'ctrl+shift+z', redo: 'ctrl+shift+z',
select_all: 'ctrl+a', select_all: 'ctrl+a',
copy: 'ctrl+c', copy: 'ctrl+c',
cut: 'ctrl+x', cut: 'ctrl+x',
paste: 'ctrl+v', paste: 'ctrl+v',
save: 'ctrl+s' save: 'ctrl+s'
}, },
piano_roll: { piano_roll: {
+16 -2
View File
@@ -11,7 +11,7 @@ import { selectAllNotesInActiveRegion } from '../util/selectionUtil';
* Handles keyboard shortcuts defined in the configuration * Handles keyboard shortcuts defined in the configuration
*/ */
export const useGlobalKeyboardHandler = () => { export const useGlobalKeyboardHandler = () => {
const { undo, redo, setStatus, isPlaying, startPlaying, stopPlaying, projectName } = useProjectStore(); const { undo, redo, setStatus, isPlaying, startPlaying, stopPlaying, toggleLoop, projectName } = useProjectStore();
useEffect(() => { useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
@@ -59,6 +59,7 @@ export const useGlobalKeyboardHandler = () => {
const pasteShortcut = configManager.get('hotkeys.main.paste') as string; const pasteShortcut = configManager.get('hotkeys.main.paste') as string;
const selectAllShortcut = configManager.get('hotkeys.main.select_all') as string; const selectAllShortcut = configManager.get('hotkeys.main.select_all') as string;
const playShortcut = configManager.get('hotkeys.main.play') 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 saveShortcut = configManager.get('hotkeys.main.save') as string;
// Check for undo shortcut // Check for undo shortcut
@@ -136,6 +137,19 @@ export const useGlobalKeyboardHandler = () => {
return; 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 // Check for save shortcut
if (saveShortcut && matchesKeyboardShortcut(event, saveShortcut)) { if (saveShortcut && matchesKeyboardShortcut(event, saveShortcut)) {
event.preventDefault(); event.preventDefault();
@@ -156,5 +170,5 @@ export const useGlobalKeyboardHandler = () => {
return () => { return () => {
document.removeEventListener('keydown', handleKeyDown, { capture: true }); 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
}; };
+7
View File
@@ -12,6 +12,7 @@ import { KGRegion } from '../core/region/KGRegion';
import { AddTrackCommand, RemoveTrackCommand, ReorderTracksCommand, UpdateTrackCommand, type TrackUpdateProperties, PasteRegionsCommand, PasteNotesCommand, ChangeProjectPropertyCommand } from '../core/commands'; import { AddTrackCommand, RemoveTrackCommand, ReorderTracksCommand, UpdateTrackCommand, type TrackUpdateProperties, PasteRegionsCommand, PasteNotesCommand, ChangeProjectPropertyCommand } from '../core/commands';
import { ConfigManager } from '../core/config/ConfigManager'; import { ConfigManager } from '../core/config/ConfigManager';
import { upgradeProjectToLatest } from '../core/project-upgrader/KGProjectUpgrader'; import { upgradeProjectToLatest } from '../core/project-upgrader/KGProjectUpgrader';
import { toggleLoop } from '../util/loopUtil';
/** /**
* Update CSS custom property for time signature numerator * Update CSS custom property for time signature numerator
@@ -86,6 +87,7 @@ interface ProjectState {
setPlayheadPosition: (position: number) => void; setPlayheadPosition: (position: number) => void;
startPlaying: () => Promise<void>; startPlaying: () => Promise<void>;
stopPlaying: () => Promise<void>; stopPlaying: () => Promise<void>;
toggleLoop: () => void;
setBpm: (bpm: number) => void; setBpm: (bpm: number) => void;
setMaxBars: (maxBars: number) => void; setMaxBars: (maxBars: number) => void;
setTimeSignature: (timeSignature: TimeSignature) => void; setTimeSignature: (timeSignature: TimeSignature) => void;
@@ -570,6 +572,11 @@ export const useProjectStore = create<ProjectState>((set, get) => {
set({ isPlaying: false }); set({ isPlaying: false });
}, },
toggleLoop: () => {
const { isLooping, loopingRange, maxBars } = get();
toggleLoop(isLooping, loopingRange, maxBars);
},
setBpm: (bpm: number) => { setBpm: (bpm: number) => {
try { try {
// Create and execute the change project property command // Create and execute the change project property command
+49
View File
@@ -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);
};