feat: added a hotkey for loop button
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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) {
|
if (DEBUG_MODE.TOOLBAR) {
|
||||||
console.log("Loop range auto-set to entire song:", newLoopingRange);
|
console.log("Loop toggle clicked");
|
||||||
}
|
|
||||||
} 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) {
|
|
||||||
console.log("Loop toggle clicked, isLooping:", newLoopingState, "range:", newLoopingRange);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,6 +206,7 @@ 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',
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user