feat: implemented change loop setting command

This commit is contained in:
Xiaohan-Tian
2026-01-22 18:50:09 -08:00
parent f367c84ee8
commit b80e7e84b8
4 changed files with 208 additions and 14 deletions
+36 -6
View File
@@ -11,6 +11,7 @@ import type { RegionUI } from './interfaces';
import { DEBUG_MODE, BAR_NUMBERS_CONSTANTS } from '../constants';
import { useRegionOperations } from '../hooks/useRegionOperations';
import { regionDeleteManager } from '../util/regionDeleteUtil';
import { ChangeLoopSettingsCommand } from '../core/commands';
interface MainContentProps {
onTrackClick?: () => void;
@@ -78,6 +79,7 @@ const MainContent: React.FC<MainContentProps> = ({
const isLoopDraggingRef = useRef(false);
const loopDragStartBarRef = useRef<number | null>(null);
const loopDragStartXRef = useRef<number | null>(null);
const loopDragOriginalSettingsRef = useRef<{ isLooping: boolean; loopingRange: [number, number] } | null>(null);
// Effect to verify track updates
useEffect(() => {
@@ -550,6 +552,12 @@ const MainContent: React.FC<MainContentProps> = ({
loopDragStartBarRef.current = startBarIndex;
loopDragStartXRef.current = e.clientX;
// Capture original loop settings for undo/redo
loopDragOriginalSettingsRef.current = {
isLooping,
loopingRange: [...loopingRange] as [number, number]
};
if (DEBUG_MODE.MAIN_CONTENT) {
console.log(`Bar numbers mouse down - Start bar: ${startBarIndex} (displayed as bar ${startBarIndex + 1})`);
}
@@ -599,15 +607,36 @@ const MainContent: React.FC<MainContentProps> = ({
if (loopDragStartXRef.current !== null) {
const distanceMoved = Math.abs(e.clientX - loopDragStartXRef.current);
// If dragged beyond threshold, enable looping
// If dragged beyond threshold, execute command for undo/redo support
if (distanceMoved >= BAR_NUMBERS_CONSTANTS.DRAG_THRESHOLD) {
const core = KGCore.instance();
const project = core.getCurrentProject();
project.setIsLooping(true);
useProjectStore.setState({ isLooping: true });
const currentIsLooping = core.getCurrentProject().getIsLooping();
const currentLoopingRange = core.getCurrentProject().getLoopingRange();
if (DEBUG_MODE.MAIN_CONTENT) {
console.log('Loop range drag ended - Looping auto-enabled');
// Only execute command if settings actually changed from original
if (loopDragOriginalSettingsRef.current) {
const originalSettings = loopDragOriginalSettingsRef.current;
const settingsChanged =
originalSettings.isLooping !== currentIsLooping ||
originalSettings.loopingRange[0] !== currentLoopingRange[0] ||
originalSettings.loopingRange[1] !== currentLoopingRange[1];
if (settingsChanged) {
// Revert to original state first (since we updated in real-time)
core.getCurrentProject().setIsLooping(originalSettings.isLooping);
core.getCurrentProject().setLoopingRange(originalSettings.loopingRange);
// Now execute command to apply new settings with undo support
const command = new ChangeLoopSettingsCommand({
isLooping: currentIsLooping,
loopingRange: currentLoopingRange
});
core.executeCommand(command);
if (DEBUG_MODE.MAIN_CONTENT) {
console.log('Loop range drag ended - Command executed for undo/redo');
}
}
}
} else {
// Single click (moved < threshold) - set playhead position
@@ -626,6 +655,7 @@ const MainContent: React.FC<MainContentProps> = ({
isLoopDraggingRef.current = false;
loopDragStartBarRef.current = null;
loopDragStartXRef.current = null;
loopDragOriginalSettingsRef.current = null;
}
};
+7 -7
View File
@@ -14,6 +14,7 @@ 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';
@@ -415,7 +416,6 @@ const Toolbar: React.FC = () => {
const handleLoopToggle = () => {
const core = KGCore.instance();
const project = core.getCurrentProject();
const newLoopingState = !isLooping;
let newLoopingRange = loopingRange;
@@ -445,12 +445,12 @@ const Toolbar: React.FC = () => {
}
}
// Update project model
project.setIsLooping(newLoopingState);
project.setLoopingRange(newLoopingRange);
// Update store to trigger UI re-render
useProjectStore.setState({ isLooping: newLoopingState, loopingRange: 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);
+2 -1
View File
@@ -28,4 +28,5 @@ export { MoveNotesCommand } from './note/MoveNotesCommand';
export { PasteNotesCommand } from './note/PasteNotesCommand';
// Project commands
export { ChangeProjectPropertyCommand, type ProjectUpdateProperties } from './project/ChangeProjectPropertyCommand';
export { ChangeProjectPropertyCommand, type ProjectUpdateProperties } from './project/ChangeProjectPropertyCommand';
export { ChangeLoopSettingsCommand, type LoopSettings } from './project/ChangeLoopSettingsCommand';
@@ -0,0 +1,163 @@
import { KGCommand } from '../KGCommand';
import { KGCore } from '../../KGCore';
import { KGProject } from '../../KGProject';
import { useProjectStore } from '../../../stores/projectStore';
/**
* Interface defining loop settings that can be updated
*/
export interface LoopSettings {
isLooping?: boolean;
loopingRange?: [number, number]; // [startBar, endBar] - bar indices (0-based)
}
/**
* Command to update loop settings (isLooping and loopingRange)
* Handles updating loop mode and range with undo support
*/
export class ChangeLoopSettingsCommand extends KGCommand {
private newSettings: LoopSettings;
private originalSettings: LoopSettings = {};
private targetProject: KGProject | null = null;
private changedSettings: Set<keyof LoopSettings> = new Set();
constructor(settings: LoopSettings) {
super();
this.newSettings = settings;
}
execute(): void {
const core = KGCore.instance();
this.targetProject = core.getCurrentProject();
// Store original settings for undo
this.originalSettings = {
isLooping: this.targetProject.getIsLooping(),
loopingRange: [...this.targetProject.getLoopingRange()] as [number, number], // Create a copy
};
// Apply updates and track what actually changes
const updatedSettings: string[] = [];
// Update isLooping
if (this.newSettings.isLooping !== undefined && this.newSettings.isLooping !== this.originalSettings.isLooping) {
this.targetProject.setIsLooping(this.newSettings.isLooping);
this.changedSettings.add('isLooping');
updatedSettings.push(`isLooping: ${this.originalSettings.isLooping}${this.newSettings.isLooping}`);
}
// Update loopingRange
if (this.newSettings.loopingRange !== undefined) {
const originalRange = this.originalSettings.loopingRange!;
const newRange = this.newSettings.loopingRange;
// Compare loop ranges
if (originalRange[0] !== newRange[0] || originalRange[1] !== newRange[1]) {
this.targetProject.setLoopingRange(newRange);
this.changedSettings.add('loopingRange');
updatedSettings.push(`loopingRange: [${originalRange[0]}, ${originalRange[1]}] → [${newRange[0]}, ${newRange[1]}]`);
}
}
// Update the store to trigger UI re-render
const storeUpdate: { isLooping?: boolean; loopingRange?: [number, number] } = {};
if (this.changedSettings.has('isLooping') && this.newSettings.isLooping !== undefined) {
storeUpdate.isLooping = this.newSettings.isLooping;
}
if (this.changedSettings.has('loopingRange') && this.newSettings.loopingRange !== undefined) {
storeUpdate.loopingRange = this.newSettings.loopingRange;
}
if (Object.keys(storeUpdate).length > 0) {
useProjectStore.setState(storeUpdate);
}
if (updatedSettings.length > 0) {
console.log(`Updated loop settings: ${updatedSettings.join(', ')}`);
} else {
console.log('No changes applied to loop settings');
}
}
undo(): void {
if (!this.targetProject) {
throw new Error('Cannot undo: no loop settings were updated');
}
// Only restore settings that were actually changed
const restoredSettings: string[] = [];
// Restore isLooping (only if it was changed)
if (this.changedSettings.has('isLooping') && this.originalSettings.isLooping !== undefined) {
this.targetProject.setIsLooping(this.originalSettings.isLooping);
restoredSettings.push(`isLooping: ${this.originalSettings.isLooping}`);
}
// Restore loopingRange (only if it was changed)
if (this.changedSettings.has('loopingRange') && this.originalSettings.loopingRange !== undefined) {
this.targetProject.setLoopingRange(this.originalSettings.loopingRange);
const range = this.originalSettings.loopingRange;
restoredSettings.push(`loopingRange: [${range[0]}, ${range[1]}]`);
}
// Update the store to trigger UI re-render
const storeUpdate: { isLooping?: boolean; loopingRange?: [number, number] } = {};
if (this.changedSettings.has('isLooping') && this.originalSettings.isLooping !== undefined) {
storeUpdate.isLooping = this.originalSettings.isLooping;
}
if (this.changedSettings.has('loopingRange') && this.originalSettings.loopingRange !== undefined) {
storeUpdate.loopingRange = this.originalSettings.loopingRange;
}
if (Object.keys(storeUpdate).length > 0) {
useProjectStore.setState(storeUpdate);
}
console.log(`Restored loop settings: ${restoredSettings.join(', ')}`);
}
getDescription(): string {
const updatedSettings: string[] = [];
if (this.newSettings.isLooping !== undefined) {
updatedSettings.push('loop mode');
}
if (this.newSettings.loopingRange !== undefined) {
updatedSettings.push('loop range');
}
if (updatedSettings.length === 1) {
return `Change ${updatedSettings[0]}`;
} else if (updatedSettings.length > 1) {
return `Change loop settings (${updatedSettings.join(', ')})`;
}
return `Change loop settings`;
}
/**
* Get the new settings being applied
*/
public getNewSettings(): LoopSettings {
return this.newSettings;
}
/**
* Get the original settings (only available after execute)
*/
public getOriginalSettings(): LoopSettings {
return this.originalSettings;
}
/**
* Get the target project instance (only available after execute)
*/
public getTargetProject(): KGProject | null {
return this.targetProject;
}
/**
* Get the settings that were actually changed (only available after execute)
*/
public getChangedSettings(): Set<keyof LoopSettings> {
return new Set(this.changedSettings);
}
}