diff --git a/src/components/Toolbar.css b/src/components/Toolbar.css index 6e7441c..e3a8b5c 100644 --- a/src/components/Toolbar.css +++ b/src/components/Toolbar.css @@ -153,3 +153,49 @@ width: 100px; left: 0; } + +/* Clickable zoom styling */ +.current-zoom { + font-family: 'Courier New', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-weight: normal; + letter-spacing: 0.5px; + cursor: pointer; + transition: background-color 0.2s ease; + padding: 2px 4px; + border-radius: 2px; +} + +.current-zoom:hover { + background-color: #4a4a4a; + color: #fff; +} + +.zoom-slider-popup { + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + background-color: #2d2d2d; + border: 1px solid #444; + border-radius: 3px; + padding: 8px 12px; + z-index: 10000; + display: flex; + align-items: center; + gap: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + white-space: nowrap; + margin-top: 2px; +} + +.zoom-slider-popup input[type="range"] { + width: 120px; +} + +.zoom-slider-label { + font-family: 'Courier New', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 12px; + color: #e0e0e0; + min-width: 20px; + text-align: center; +} diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx index 476c372..ab71b56 100644 --- a/src/components/Toolbar.tsx +++ b/src/components/Toolbar.tsx @@ -36,6 +36,7 @@ const Toolbar: React.FC = () => { isPlaying, startPlaying, stopPlaying, setPlayheadPosition, currentTime, setBpm, setTimeSignature, setKeySignature, maxBars, setMaxBars, + barWidthMultiplier, setBarWidthMultiplier, isLooping, toggleLoop, canUndo, canRedo, undoDescription, redoDescription, undo, redo, toggleChatBox, toggleSettings, cleanupProjectState, @@ -57,9 +58,25 @@ const Toolbar: React.FC = () => { // State for import modal const [showImportModal, setShowImportModal] = React.useState(false); + // State for zoom slider popup + const [showZoomSlider, setShowZoomSlider] = React.useState(false); + const zoomSliderRef = React.useRef(null); + // State for open project modal const [showOpenProject, setShowOpenProject] = React.useState(false); + // Close zoom slider on click outside + React.useEffect(() => { + if (!showZoomSlider) return; + const handleClickOutside = (e: MouseEvent) => { + if (zoomSliderRef.current && !zoomSliderRef.current.contains(e.target as Node)) { + setShowZoomSlider(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [showZoomSlider]); + // Key signature options const keySignatureOptions = Object.keys(KEY_SIGNATURE_MAP) as KeySignature[]; @@ -790,6 +807,28 @@ const Toolbar: React.FC = () => {
+
+ setShowZoomSlider(!showZoomSlider)} + style={{ cursor: 'pointer' }} + > + {barWidthMultiplier}x + + {showZoomSlider && ( +
+ setBarWidthMultiplier(parseInt(e.target.value))} + /> + {barWidthMultiplier}x +
+ )} +
{currentTime}
diff --git a/src/constants/uiConstants.ts b/src/constants/uiConstants.ts index 6eb9ded..32796dc 100644 --- a/src/constants/uiConstants.ts +++ b/src/constants/uiConstants.ts @@ -18,6 +18,7 @@ export const DEBUG_MODE = { // Toolbar related constants export const TOOLBAR_CONSTANTS = { + BASE_BAR_WIDTH: 40, // matches --track-grid-bar-width default in variables.css }; // Region related constants diff --git a/src/core/KGProject.ts b/src/core/KGProject.ts index 787f1c4..8c421a2 100644 --- a/src/core/KGProject.ts +++ b/src/core/KGProject.ts @@ -44,11 +44,15 @@ export class KGProject { @WithDefault([0, 0]) private loopingRange: [number, number] = [0, 0]; // [startBar, endBar] - bar indices (0-based) + @Expose() + @WithDefault(1) + private barWidthMultiplier: number = 1; + @Expose() @WithDefault(0) private projectStructureVersion: number = 0; - public static readonly CURRENT_PROJECT_STRUCTURE_VERSION: number = 4; + public static readonly CURRENT_PROJECT_STRUCTURE_VERSION: number = 5; @Expose() @Type(() => KGTrack, { @@ -64,7 +68,7 @@ export class KGProject { private tracks: KGTrack[] = []; // Constructor - constructor(name: string = "Untitled Project", maxBars: number = 32, currentBars: number = 0, bpm: number = 125, timeSignature: TimeSignature = { numerator: 4, denominator: 4 }, keySignature: KeySignature = "C major", selectedMode: string = "ionian", isLooping: boolean = false, loopingRange: [number, number] = [0, 0], tracks: KGTrack[] = [], projectStructureVersion: number = KGProject.CURRENT_PROJECT_STRUCTURE_VERSION) { + constructor(name: string = "Untitled Project", maxBars: number = 32, currentBars: number = 0, bpm: number = 125, timeSignature: TimeSignature = { numerator: 4, denominator: 4 }, keySignature: KeySignature = "C major", selectedMode: string = "ionian", isLooping: boolean = false, loopingRange: [number, number] = [0, 0], barWidthMultiplier: number = 1, tracks: KGTrack[] = [], projectStructureVersion: number = KGProject.CURRENT_PROJECT_STRUCTURE_VERSION) { this.name = name; this.maxBars = maxBars; this.currentBars = currentBars; @@ -74,6 +78,7 @@ export class KGProject { this.selectedMode = selectedMode; this.isLooping = isLooping; this.loopingRange = loopingRange; + this.barWidthMultiplier = barWidthMultiplier; this.tracks = tracks; this.projectStructureVersion = projectStructureVersion; } @@ -167,5 +172,13 @@ export class KGProject { public setLoopingRange(loopingRange: [number, number]): void { this.loopingRange = loopingRange; } + + public getBarWidthMultiplier(): number { + return this.barWidthMultiplier; + } + + public setBarWidthMultiplier(barWidthMultiplier: number): void { + this.barWidthMultiplier = barWidthMultiplier; + } } diff --git a/src/core/project-upgrader/KGProjectUpgrader.ts b/src/core/project-upgrader/KGProjectUpgrader.ts index 1bf5f6a..e7cd9dc 100644 --- a/src/core/project-upgrader/KGProjectUpgrader.ts +++ b/src/core/project-upgrader/KGProjectUpgrader.ts @@ -3,6 +3,7 @@ import { upgradeToV1 } from './upgradeToV1'; import { upgradeToV2 } from './upgradeToV2'; import { upgradeToV3 } from './upgradeToV3'; import { upgradeToV4 } from './upgradeToV4'; +import { upgradeToV5 } from './upgradeToV5'; /** * Upgrade the given project to the latest structure version, one version at a time. @@ -38,6 +39,10 @@ export function upgradeProjectToLatest(project: KGProject): KGProject { workingProject = upgradeToV4(workingProject); break; } + case 5: { + workingProject = upgradeToV5(workingProject); + break; + } default: { // If an upgrader is missing, throw to prevent loading incompatible structures throw new Error(`No upgrader found for project structure version ${nextVersion}`); diff --git a/src/core/project-upgrader/upgradeToV5.ts b/src/core/project-upgrader/upgradeToV5.ts new file mode 100644 index 0000000..49200e1 --- /dev/null +++ b/src/core/project-upgrader/upgradeToV5.ts @@ -0,0 +1,13 @@ +import { KGProject } from '../KGProject'; + +export function upgradeToV5(project: KGProject): KGProject { + try { + const current = project.getBarWidthMultiplier?.(); + if (current === undefined || current === null) { + project.setBarWidthMultiplier(1); + } + } finally { + project.setProjectStructureVersion(5); + } + return project; +} diff --git a/src/stores/projectStore.ts b/src/stores/projectStore.ts index 451a439..3175783 100644 --- a/src/stores/projectStore.ts +++ b/src/stores/projectStore.ts @@ -16,6 +16,7 @@ import { KGAudioFileStorage } from '../core/io/KGAudioFileStorage'; import { ConfigManager } from '../core/config/ConfigManager'; import { upgradeProjectToLatest } from '../core/project-upgrader/KGProjectUpgrader'; import { toggleLoop } from '../util/loopUtil'; +import { TOOLBAR_CONSTANTS } from '../constants/uiConstants'; import * as Tone from 'tone'; /** @@ -34,6 +35,16 @@ function updateMaxBarsCSS(maxBars: number): void { document.documentElement.style.setProperty('--max-number-of-bars', maxBars.toString()); } +/** + * Update CSS custom property for track grid bar width based on multiplier + */ +function updateBarWidthMultiplierCSS(multiplier: number): void { + document.documentElement.style.setProperty( + '--track-grid-bar-width', + `${TOOLBAR_CONSTANTS.BASE_BAR_WIDTH * multiplier}px` + ); +} + // Define the store state interface interface ProjectState { // State @@ -42,6 +53,7 @@ interface ProjectState { tracks: KGTrack[]; currentStatus: string; maxBars: number; + barWidthMultiplier: number; timeSignature: TimeSignature; bpm: number; keySignature: KeySignature; @@ -104,6 +116,7 @@ interface ProjectState { toggleLoop: () => void; setBpm: (bpm: number) => void; setMaxBars: (maxBars: number) => void; + setBarWidthMultiplier: (multiplier: number) => void; setTimeSignature: (timeSignature: TimeSignature) => void; setKeySignature: (keySignature: KeySignature) => void; setSelectedMode: (selectedMode: string) => void; @@ -157,6 +170,8 @@ export const useProjectStore = create((set, get) => { updateTimeSignatureCSS(currentProject.getTimeSignature()); // Initialize CSS variable for max bars on store creation updateMaxBarsCSS(currentProject.getMaxBars()); + // Initialize CSS variable for bar width multiplier on store creation + updateBarWidthMultiplierCSS(currentProject.getBarWidthMultiplier()); // Get initial ChatBox state from config const configManager = ConfigManager.instance(); @@ -240,6 +255,7 @@ export const useProjectStore = create((set, get) => { tracks: currentProject.getTracks() as KGTrack[], currentStatus: KGCore.instance().getStatus() || 'Unknown', maxBars: currentProject.getMaxBars(), + barWidthMultiplier: currentProject.getBarWidthMultiplier(), timeSignature: currentProject.getTimeSignature(), bpm: currentProject.getBpm(), keySignature: currentProject.getKeySignature(), @@ -669,11 +685,11 @@ export const useProjectStore = create((set, get) => { } } - // Update CSS variable for time signature numerator + // Update CSS variables updateTimeSignatureCSS(timeSignature); - // Update CSS variable for max bars updateMaxBarsCSS(maxBars); - + updateBarWidthMultiplierCSS(projectToLoad.getBarWidthMultiplier()); + // Log project loading info console.log(`Project max bars: ${maxBars}`); console.log(`Setup audio synths for ${tracks.length} tracks`); @@ -685,6 +701,7 @@ export const useProjectStore = create((set, get) => { savedProjectName: savedName ?? projectToLoad.getName(), tracks: [...tracks], maxBars, + barWidthMultiplier: projectToLoad.getBarWidthMultiplier(), timeSignature, bpm, keySignature, @@ -781,16 +798,23 @@ export const useProjectStore = create((set, get) => { } }, + setBarWidthMultiplier: (multiplier: number) => { + const project = KGCore.instance().getCurrentProject(); + project.setBarWidthMultiplier(multiplier); + set({ barWidthMultiplier: multiplier }); + updateBarWidthMultiplierCSS(multiplier); + }, + setTimeSignature: (timeSignature: TimeSignature) => { try { // Create and execute the change project property command const command = new ChangeProjectPropertyCommand({ timeSignature }); KGCore.instance().executeCommand(command); - + // Update the store state set({ timeSignature }); updateTimeSignatureCSS(timeSignature); - + console.log(`Set time signature to ${timeSignature.numerator}/${timeSignature.denominator}`); } catch (error) { console.error('Error setting time signature:', error); @@ -985,15 +1009,17 @@ export const useProjectStore = create((set, get) => { projectName: project.getName(), tracks: [...project.getTracks()] as KGTrack[], // Force new array reference - key for re-rendering! maxBars: project.getMaxBars(), + barWidthMultiplier: project.getBarWidthMultiplier(), timeSignature: project.getTimeSignature(), bpm: project.getBpm(), keySignature: project.getKeySignature(), selectedMode: project.getSelectedMode() }); - + // Sync CSS variables that affect layout updateTimeSignatureCSS(project.getTimeSignature()); updateMaxBarsCSS(project.getMaxBars()); + updateBarWidthMultiplierCSS(project.getBarWidthMultiplier()); // Sync all related state const actions = get(); diff --git a/src/test/utils/mock-data.ts b/src/test/utils/mock-data.ts index c7607e3..4b0adcc 100644 --- a/src/test/utils/mock-data.ts +++ b/src/test/utils/mock-data.ts @@ -123,8 +123,9 @@ export const createMockProject = (overrides: Partial<{ 'ionian', // selectedMode false, // isLooping [0, 0], // loopingRange + 1, // barWidthMultiplier defaults.tracks, // tracks - 3 // projectStructureVersion + 5 // projectStructureVersion ) return project