From b80e7e84b818a1d6985f609c84ddb27a5ba10214 Mon Sep 17 00:00:00 2001 From: Xiaohan-Tian <157918347+Xiaohan-Tian@users.noreply.github.com> Date: Thu, 22 Jan 2026 18:50:09 -0800 Subject: [PATCH] feat: implemented change loop setting command --- src/components/MainContent.tsx | 42 ++++- src/components/Toolbar.tsx | 14 +- src/core/commands/index.ts | 3 +- .../project/ChangeLoopSettingsCommand.ts | 163 ++++++++++++++++++ 4 files changed, 208 insertions(+), 14 deletions(-) create mode 100644 src/core/commands/project/ChangeLoopSettingsCommand.ts diff --git a/src/components/MainContent.tsx b/src/components/MainContent.tsx index c35d5a4..b905f6b 100644 --- a/src/components/MainContent.tsx +++ b/src/components/MainContent.tsx @@ -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 = ({ const isLoopDraggingRef = useRef(false); const loopDragStartBarRef = useRef(null); const loopDragStartXRef = useRef(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 = ({ 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 = ({ 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 = ({ isLoopDraggingRef.current = false; loopDragStartBarRef.current = null; loopDragStartXRef.current = null; + loopDragOriginalSettingsRef.current = null; } }; diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx index 2b09402..a07e3a2 100644 --- a/src/components/Toolbar.tsx +++ b/src/components/Toolbar.tsx @@ -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); diff --git a/src/core/commands/index.ts b/src/core/commands/index.ts index 60be091..bb1e8cd 100644 --- a/src/core/commands/index.ts +++ b/src/core/commands/index.ts @@ -28,4 +28,5 @@ export { MoveNotesCommand } from './note/MoveNotesCommand'; export { PasteNotesCommand } from './note/PasteNotesCommand'; // Project commands -export { ChangeProjectPropertyCommand, type ProjectUpdateProperties } from './project/ChangeProjectPropertyCommand'; \ No newline at end of file +export { ChangeProjectPropertyCommand, type ProjectUpdateProperties } from './project/ChangeProjectPropertyCommand'; +export { ChangeLoopSettingsCommand, type LoopSettings } from './project/ChangeLoopSettingsCommand'; \ No newline at end of file diff --git a/src/core/commands/project/ChangeLoopSettingsCommand.ts b/src/core/commands/project/ChangeLoopSettingsCommand.ts new file mode 100644 index 0000000..7b07a27 --- /dev/null +++ b/src/core/commands/project/ChangeLoopSettingsCommand.ts @@ -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 = 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 { + return new Set(this.changedSettings); + } +}