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": {
"hold_to_create_region": "ctrl",
"play": "space",
"loop": "c",
"undo": "ctrl+z",
"redo": "ctrl+shift+z",
"select_all": "ctrl+a",
+3 -40
View File
@@ -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");
}
};
+3 -1
View File
@@ -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: {
+16 -2
View File
@@ -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
};
+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 { 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<void>;
stopPlaying: () => Promise<void>;
toggleLoop: () => void;
setBpm: (bpm: number) => void;
setMaxBars: (maxBars: number) => void;
setTimeSignature: (timeSignature: TimeSignature) => void;
@@ -570,6 +572,11 @@ export const useProjectStore = create<ProjectState>((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
+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);
};