diff --git a/README.md b/README.md index a702683..240b0c5 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ K.G.Studio is a lightweight, modern DAW that runs entirely in the browser with * - **2026.05.09**: Added **audio recording** — record directly from your microphone into an audio track. A live waveform preview grows in real time as you record, and the region is committed to the timeline as a standard audio region when you stop. Added **audio I/O device selection** in Settings so you can choose your preferred microphone input and audio output device. -- **2026.05.08**: Added **MIDI automation** — draw and edit pitch bend and MIDI CC curves (CC1 Modulation, CC2 Breath, CC7 Volume, CC11 Expression, CC64 Sustain) in an editable automation lane below the piano grid. Added **track-level automation**: each track now has a dedicated automation panel where you can view and edit the same curves directly on the timeline. Real-time MIDI controller input (pitch wheel, CC pedals) is recorded and played back with per-lane interpolation. Added the **List Event Panel** — a tabbed sidebar (Notes / Pitch Bend / Controller) for inspecting and inline-editing all events in the active MIDI region. Added **region multi-select** with lasso and bulk move/resize, and **merge MIDI regions**. +- **2026.05.08**: Added **MIDI automation** — draw and edit pitch bend and MIDI CC curves (CC1 Modulation, CC2 Breath, CC7 Volume, CC11 Expression, CC64 Sustain) in an editable automation lane below the piano grid. Added **track-level automation**: each track now has a dedicated automation panel where you can view and edit the same curves directly on the timeline. Real-time MIDI controller input (pitch wheel, CC pedals) is recorded and played back with per-lane interpolation. Added the **Event List Panel** — a tabbed sidebar (Notes / Pitch Bend / Controller) for inspecting and inline-editing all events in the active MIDI region. Added **region multi-select** with lasso and bulk move/resize, and **merge MIDI regions**.
K.G.Studio Logo
@@ -325,7 +325,7 @@ Feature priorities might change. - [X] Support MIDI control events (e.g. CC, pitch bend, etc.) - [X] Support WAV audio tracks - [X] Recording -- [ ] List Event + List Region +- [X] Event List - [X] Add support for OpenAI's open source models (`gpt-oss-20b` and `gpt-oss-120b`) ### Post 1.0 diff --git a/src/App.test.tsx b/src/App.test.tsx index a0f4d40..4bd3d52 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -19,7 +19,7 @@ vi.mock('./components/MainContent', () => ({ default: () => null })); vi.mock('./components/InstrumentSelection', () => ({ default: () => null })); vi.mock('./components/ChatBox', () => ({ default: () => null })); vi.mock('./components/KGOnePanel', () => ({ default: () => null })); -vi.mock('./components/ListEventPanel', () => ({ default: () => null })); +vi.mock('./components/EventListPanel', () => ({ default: () => null })); vi.mock('./components/settings', () => ({ SettingsPanel: () => null })); vi.mock('./core/audio-interface/KGToneBuffersPool', () => ({ KGToneBuffersPool: { diff --git a/src/App.tsx b/src/App.tsx index c066b6d..c31919b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,7 +11,7 @@ import ChatBox from './components/ChatBox'; import { SettingsPanel } from './components/settings'; import LoadingOverlay from './components/common/LoadingOverlay'; import KGOnePanel from './components/KGOnePanel'; -import ListEventPanel from './components/ListEventPanel'; +import EventListPanel from './components/EventListPanel'; import { useEffect as useEffectReact, useState, useRef } from 'react'; import { KGToneBuffersPool } from './core/audio-interface/KGToneBuffersPool'; import { KGOfflineRenderer } from './core/audio-interface/KGOfflineRenderer'; @@ -26,12 +26,12 @@ import { RESERVED_PROJECT_NAME } from './util/projectNameUtil'; function App() { // Enable global keyboard handler for copy/paste and undo/redo useGlobalKeyboardHandler(); - + // Use project store instead of local state for project name and tracks const { refreshStatus, loadProject, showChatBox, showSettings, setShowSettings, initializeFromConfig, - showInstrumentSelection, showKGOnePanel, showListEventPanel + showInstrumentSelection, showKGOnePanel, showEventListPanel } = useProjectStore(); // Track if app has been initialized to prevent multiple initializations @@ -144,12 +144,12 @@ function App() { useEffect(() => { // Initial refresh refreshStatus(); - + // Set up interval to refresh status every second const intervalId = setInterval(() => { refreshStatus(); }, 1000); - + // Clean up interval on unmount return () => clearInterval(intervalId); }, [refreshStatus]); @@ -169,7 +169,7 @@ function App() { )} - + @@ -264,7 +264,7 @@ const MigrationOverlayContainer: React.FC = () => { useEffectReact(() => { KGCore.instance().setMigrationStateChangeCallback(setIsMigrating); return () => { - KGCore.instance().setMigrationStateChangeCallback(() => {}); + KGCore.instance().setMigrationStateChangeCallback(() => { }); }; }, []); diff --git a/src/components/ListEventPanel.css b/src/components/EventListPanel.css similarity index 77% rename from src/components/ListEventPanel.css rename to src/components/EventListPanel.css index eff384e..bf4ef49 100644 --- a/src/components/ListEventPanel.css +++ b/src/components/EventListPanel.css @@ -1,4 +1,4 @@ -.list-event-panel { +.event-list-panel { display: flex; flex-direction: column; width: var(--chat-box-width); @@ -8,11 +8,11 @@ overflow: hidden; } -.list-event-panel.is-hidden { +.event-list-panel.is-hidden { display: none; } -.list-event-panel-header { +.event-list-panel-header { display: flex; align-items: center; justify-content: space-between; @@ -23,14 +23,14 @@ flex-shrink: 0; } -.list-event-panel-header h3 { +.event-list-panel-header h3 { color: #e0e0e0; font-size: 12px; font-weight: bold; margin: 0; } -.list-event-panel-body { +.event-list-panel-body { flex: 1; display: flex; flex-direction: column; @@ -39,20 +39,20 @@ gap: 12px; } -.list-event-tabs { +.event-list-tabs { display: flex; gap: 4px; flex-shrink: 0; } -.list-event-scope-tabs { +.event-list-scope-tabs { display: flex; background-color: #2d2d2d; border-bottom: 1px solid #3a3a3a; flex-shrink: 0; } -.list-event-scope-tab { +.event-list-scope-tab { flex: 1; background: transparent; color: #999; @@ -65,16 +65,16 @@ transition: color 0.15s, border-color 0.15s; } -.list-event-scope-tab:hover { +.event-list-scope-tab:hover { color: #ccc; } -.list-event-scope-tab.active { +.event-list-scope-tab.active { color: #e0e0e0; border-bottom-color: #5a9fd4; } -.list-event-tab { +.event-list-tab { flex: 1; background-color: #1e1e1e; color: #999; @@ -88,16 +88,16 @@ transition: all 0.2s ease; } -.list-event-tab:hover { +.event-list-tab:hover { color: #e0e0e0; } -.list-event-tab.active { +.event-list-tab.active { background-color: #5a9fd4; color: #fff; } -.list-event-empty-state { +.event-list-empty-state { color: #888; font-size: 11px; line-height: 1.5; @@ -107,7 +107,7 @@ border-radius: 6px; } -.list-event-toolbar { +.event-list-toolbar { display: flex; align-items: center; justify-content: space-between; @@ -115,23 +115,23 @@ flex-shrink: 0; } -.list-event-toolbar-group { +.event-list-toolbar-group { display: flex; align-items: center; gap: 8px; min-width: 0; } -.list-event-toolbar-group:first-child { +.event-list-toolbar-group:first-child { gap: 0; } -.list-event-toolbar-group-right { +.event-list-toolbar-group-right { margin-left: auto; gap: 8px; } -.list-event-add-button { +.event-list-add-button { width: 22px; height: 22px; border: 1px solid #444; @@ -153,34 +153,34 @@ font-size: 11px; } -.list-event-add-button:hover { +.event-list-add-button:hover { background-color: #3b3b3b; border-right: 0; } -.list-event-dropdown-button, -.list-event-quant-button, -.list-event-type-button { +.event-list-dropdown-button, +.event-list-quant-button, +.event-list-type-button { font-size: 11px; } -.list-event-dropdown-button { +.event-list-dropdown-button { margin-left: 0; } -.list-event-quant-button { +.event-list-quant-button { min-width: 78px; padding: 3px 5px; } -.list-event-type-button { +.event-list-type-button { min-width: 88px; margin-left: 0; border-top-left-radius: 0; border-bottom-left-radius: 0; } -.list-event-delete-button { +.event-list-delete-button { width: 22px; height: 22px; border: 1px solid #444; @@ -197,17 +197,17 @@ font-size: 11px; } -.list-event-delete-button:hover:not(:disabled) { +.event-list-delete-button:hover:not(:disabled) { background-color: #464646; border-color: #5a5a5a; } -.list-event-delete-button:disabled { +.event-list-delete-button:disabled { opacity: 0.45; cursor: default; } -.list-event-table-shell { +.event-list-table-shell { flex: 1; min-height: 0; overflow: auto; @@ -216,13 +216,13 @@ border-radius: 6px; } -.list-event-table { +.event-list-table { width: 100%; border-collapse: collapse; table-layout: fixed; } -.list-event-table thead th { +.event-list-table thead th { position: sticky; top: 0; z-index: 1; @@ -238,25 +238,25 @@ text-overflow: ellipsis; } -.list-event-table tbody tr { +.event-list-table tbody tr { color: #e0e0e0; cursor: default; } -.list-event-table tbody tr:nth-child(odd) { +.event-list-table tbody tr:nth-child(odd) { background-color: #282828; } -.list-event-table tbody tr:nth-child(even) { +.event-list-table tbody tr:nth-child(even) { background-color: #303030; } -.list-event-table tbody tr.selected { +.event-list-table tbody tr.selected { background-color: #5a9fd4; color: #fff; } -.list-event-table td { +.event-list-table td { height: 20px; padding: 2px 12px; font-size: 11px; @@ -266,7 +266,7 @@ max-width: 0; } -.list-event-cell-input { +.event-list-cell-input { width: calc(100% + 8px); height: 16px; margin: 0 -4px; @@ -278,4 +278,4 @@ font-size: 11px; line-height: 16px; outline: none; -} +} \ No newline at end of file diff --git a/src/components/ListEventPanel.test.tsx b/src/components/EventListPanel.test.tsx similarity index 93% rename from src/components/ListEventPanel.test.tsx rename to src/components/EventListPanel.test.tsx index abd8212..584cc26 100644 --- a/src/components/ListEventPanel.test.tsx +++ b/src/components/EventListPanel.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import ListEventPanel from './ListEventPanel'; +import EventListPanel from './EventListPanel'; import { KGMidiControllerEvent } from '../core/midi/KGMidiControllerEvent'; import { KGMidiNote } from '../core/midi/KGMidiNote'; import { KGMidiPitchBend } from '../core/midi/KGMidiPitchBend'; @@ -158,7 +158,7 @@ vi.mock('../core/KGCore', () => ({ }, })); -describe('ListEventPanel', () => { +describe('EventListPanel', () => { beforeEach(() => { selectedItems = [midiRegion]; midiRegion.select(); @@ -198,7 +198,7 @@ describe('ListEventPanel', () => { }); it('defaults to Region tab and preserves existing event rows', () => { - render(); + render(); expect(screen.getByRole('button', { name: 'Region' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Track' })).toBeInTheDocument(); @@ -207,7 +207,7 @@ describe('ListEventPanel', () => { }); it('switches to Track tab and lists selected track regions', () => { - render(); + render(); fireEvent.click(screen.getByRole('button', { name: 'Track' })); @@ -219,7 +219,7 @@ describe('ListEventPanel', () => { }); it('toggles track filters independently', () => { - render(); + render(); fireEvent.click(screen.getByRole('button', { name: 'Track' })); fireEvent.click(screen.getByRole('button', { name: 'Regions' })); @@ -237,40 +237,40 @@ describe('ListEventPanel', () => { it('shows track empty state when no track is selected', () => { storeState.selectedTrackId = null; - render(); + render(); fireEvent.click(screen.getByRole('button', { name: 'Track' })); expect(screen.getByText('Please select a track to view regions and track automation.')).toBeInTheDocument(); }); it('syncs Track Regions row selection to selectedRegionIds', () => { - const { rerender } = render(); + const { rerender } = render(); fireEvent.click(screen.getByRole('button', { name: 'Track' })); fireEvent.click(screen.getByText('Second Region').closest('tr')!); - rerender(); + rerender(); expect(storeState.selectedRegionIds).toEqual(['region-2']); expect(screen.getByText('Second Region').closest('tr')).toHaveClass('selected'); }); it('syncs Track Volume row selection to selectedTrackAutomationPointIds', () => { - const { rerender } = render(); + const { rerender } = render(); fireEvent.click(screen.getByRole('button', { name: 'Track' })); fireEvent.click(screen.getByText('-6.0dB').closest('tr')!); - rerender(); + rerender(); expect(storeState.selectedTrackAutomationPointIds).toEqual(['vol-1']); expect(screen.getByText('-6.0dB').closest('tr')).toHaveClass('selected'); }); it('syncs Track Pan row selection to selectedTrackAutomationPointIds', () => { - const { rerender } = render(); + const { rerender } = render(); fireEvent.click(screen.getByRole('button', { name: 'Track' })); fireEvent.click(screen.getByText('-32').closest('tr')!); - rerender(); + rerender(); expect(storeState.selectedTrackAutomationPointIds).toEqual(['pan-1']); }); @@ -278,7 +278,7 @@ describe('ListEventPanel', () => { it('hides MIDI Region add option for audio tracks', () => { storeState.selectedTrackId = '2'; - render(); + render(); fireEvent.click(screen.getByRole('button', { name: 'Track' })); fireEvent.click(screen.getAllByRole('button', { name: 'Volume' })[1]); @@ -289,7 +289,7 @@ describe('ListEventPanel', () => { }); it('creates a 1-bar MIDI region at the playhead from the Track tab', async () => { - render(); + render(); fireEvent.click(screen.getByRole('button', { name: 'Track' })); fireEvent.click(screen.getByTitle('Add MIDI region at playhead')); @@ -301,7 +301,7 @@ describe('ListEventPanel', () => { }); it('creates a volume automation point using the track base volume', async () => { - render(); + render(); fireEvent.click(screen.getByRole('button', { name: 'Track' })); fireEvent.click(screen.getByTitle('Add MIDI region at playhead')); fireEvent.click(screen.getByRole('button', { name: 'MIDI Region' })); @@ -315,7 +315,7 @@ describe('ListEventPanel', () => { }); it('creates a pan automation point using the nearest earlier pan value or zero', async () => { - render(); + render(); fireEvent.click(screen.getByRole('button', { name: 'Track' })); fireEvent.click(screen.getByTitle('Add MIDI region at playhead')); fireEvent.click(screen.getByRole('button', { name: 'MIDI Region' })); @@ -329,18 +329,18 @@ describe('ListEventPanel', () => { }); it('deletes selected track automation points from the Track tab', async () => { - const { rerender } = render(); + const { rerender } = render(); fireEvent.click(screen.getByRole('button', { name: 'Track' })); fireEvent.click(screen.getByText('-6.0dB').closest('tr')!); - rerender(); + rerender(); fireEvent.click(screen.getByTitle('Delete visible selected rows')); await waitFor(() => expect(midiTrack.getVolumeAutomation()).toHaveLength(0)); }); it('edits track region position and length inline', async () => { - render(); + render(); fireEvent.click(screen.getByRole('button', { name: 'Track' })); fireEvent.doubleClick(screen.getByText('4 1 0')); @@ -359,7 +359,7 @@ describe('ListEventPanel', () => { }); it('edits track automation position and value inline', async () => { - render(); + render(); fireEvent.click(screen.getByRole('button', { name: 'Track' })); fireEvent.doubleClick(screen.getByText('1 3 0')); diff --git a/src/components/ListEventPanel.tsx b/src/components/EventListPanel.tsx similarity index 69% rename from src/components/ListEventPanel.tsx rename to src/components/EventListPanel.tsx index 8f0fac1..67d10bf 100644 --- a/src/components/ListEventPanel.tsx +++ b/src/components/EventListPanel.tsx @@ -1,19 +1,19 @@ import React, { useMemo, useState } from 'react'; -import './ListEventPanel.css'; +import './EventListPanel.css'; import { useProjectStore } from '../stores/projectStore'; import { KGMidiRegion } from '../core/region/KGMidiRegion'; import { KGMidiTrack } from '../core/track/KGMidiTrack'; import { KGAudioTrack } from '../core/track/KGAudioTrack'; -import RegionListEventTab from './list-event-panel/RegionListEventTab'; -import TrackListEventTab from './list-event-panel/TrackListEventTab'; +import RegionEventListTab from './event-list-panel/RegionEventListTab'; +import TrackEventListTab from './event-list-panel/TrackEventListTab'; -interface ListEventPanelProps { +interface EventListPanelProps { isVisible: boolean; } type ScopeTab = 'region' | 'track'; -const ListEventPanel: React.FC = ({ isVisible }) => { +const EventListPanel: React.FC = ({ isVisible }) => { const { tracks, activeRegionId, selectedRegionIds, selectedTrackId } = useProjectStore(); const [scopeTab, setScopeTab] = useState('region'); @@ -43,21 +43,21 @@ const ListEventPanel: React.FC = ({ isVisible }) => { } return ( -
-
-

List Event

+
+
+

Event List

-
+
-
+
{scopeTab === 'region' ? ( - + ) : ( - + )}
); }; -export default ListEventPanel; +export default EventListPanel; diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx index 1abddae..34f83be 100644 --- a/src/components/Toolbar.tsx +++ b/src/components/Toolbar.tsx @@ -49,7 +49,7 @@ const Toolbar: React.FC = () => { barWidthMultiplier, setBarWidthMultiplier, isLooping, toggleLoop, canUndo, canRedo, undoDescription, redoDescription, undo, redo, - toggleChatBox, toggleSettings, toggleKGOnePanel, toggleListEventPanel, showKGOnePanel, showListEventPanel, showChatBox, showSettings, cleanupProjectState, toggleMetronome, isMetronomeEnabled, + toggleChatBox, toggleSettings, toggleKGOnePanel, toggleEventListPanel, showKGOnePanel, showEventListPanel, showChatBox, showSettings, cleanupProjectState, toggleMetronome, isMetronomeEnabled, isRecording, startRecording, stopRecording, // Piano roll state/actions showPianoRoll, setShowPianoRoll, activeRegionId, setActiveRegionId, @@ -66,10 +66,10 @@ const Toolbar: React.FC = () => { // State for key signature dropdown const [showKeySignatureDropdown, setShowKeySignatureDropdown] = React.useState(false); - + // State for export dropdown const [showExportDropdown, setShowExportDropdown] = React.useState(false); - + // State for import modal const [showImportModal, setShowImportModal] = React.useState(false); @@ -80,7 +80,7 @@ const Toolbar: React.FC = () => { // State for open project modal const [showOpenProject, setShowOpenProject] = React.useState(false); const [isOpeningProject, setIsOpeningProject] = React.useState(false); - + // Close zoom slider on click outside React.useEffect(() => { if (!showZoomSlider) return; @@ -95,7 +95,7 @@ const Toolbar: React.FC = () => { // Key signature options const keySignatureOptions = Object.keys(KEY_SIGNATURE_MAP) as KeySignature[]; - + // Export options const exportOptions = ["Export to KGStudio file", "Export to MIDI file", "Export to WAV", "Export to MP3"]; const lastSelectedRegionId = selectedRegionIds[selectedRegionIds.length - 1] ?? null; @@ -252,7 +252,7 @@ const Toolbar: React.FC = () => { if (DEBUG_MODE.TOOLBAR) { console.log("user selected export option:", exportType); } - + if (exportType === "Export to KGStudio file") { handleExportKGStudio(); } else if (exportType === "Export to MIDI file") { @@ -262,7 +262,7 @@ const Toolbar: React.FC = () => { } else if (exportType === "Export to MP3") { handleBounceToMp3(); } - + setShowExportDropdown(false); }; @@ -306,37 +306,37 @@ const Toolbar: React.FC = () => { if (DEBUG_MODE.TOOLBAR) { console.log("exporting to MIDI file"); } - + try { // Get the current project from KGCore const currentProject = KGCore.instance().getCurrentProject(); - + // Convert project to MIDI format const midiData = convertProjectToMidi(currentProject); - + // Create a downloadable blob const blob = new Blob([midiData.buffer as ArrayBuffer], { type: 'audio/midi' }); - + // Create a temporary download link const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `${projectName}.mid`; - + // Trigger download document.body.appendChild(link); link.click(); - + // Cleanup document.body.removeChild(link); URL.revokeObjectURL(url); - + setStatus(`Project "${projectName}" exported as MIDI file`); - + if (DEBUG_MODE.TOOLBAR) { console.log("MIDI export completed successfully"); } - + } catch (error) { console.error("Error exporting MIDI:", error); setStatus(`Error exporting MIDI: ${error}`); @@ -387,10 +387,10 @@ const Toolbar: React.FC = () => { if (DEBUG_MODE.TOOLBAR) { console.log("file selected for import:", file.name); } - + // Get file extension const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase(); - + try { if (fileExtension === '.kgstudio') { // Handle KGStudio bundle import @@ -404,7 +404,7 @@ const Toolbar: React.FC = () => { } else { throw new Error(`Unsupported file type: ${fileExtension}`); } - + } catch (error) { console.error("Error importing file:", error); setStatus(`Failed to import file: ${error}`); @@ -480,35 +480,35 @@ const Toolbar: React.FC = () => { if (DEBUG_MODE.TOOLBAR) { console.log("Starting MIDI file import:", file.name); } - + // Show loading status setStatus(`Importing MIDI file "${file.name}"...`); - + // Read the MIDI file as binary data const arrayBuffer = await file.arrayBuffer(); const midiData = new Uint8Array(arrayBuffer); - + if (DEBUG_MODE.TOOLBAR) { console.log("MIDI file read successfully, size:", midiData.length, "bytes"); } - + // Get current project to append MIDI tracks to it const currentProject = KGCore.instance().getCurrentProject(); - + // Convert MIDI data and append to current project const updatedProject = convertMidiToProject(midiData, currentProject); - + if (DEBUG_MODE.TOOLBAR) { console.log("MIDI conversion successful, tracks added to existing project"); } - + // Load the updated project using common loading logic await loadProjectFromData(updatedProject, `MIDI file "${file.name}"`); - + if (DEBUG_MODE.TOOLBAR) { console.log("MIDI file imported successfully:", file.name); } - + } catch (error) { console.error("Error importing MIDI file:", error); const errorMessage = error instanceof Error ? error.message : String(error); @@ -585,33 +585,33 @@ const Toolbar: React.FC = () => { if (DEBUG_MODE.TOOLBAR) { console.log("BPM clicked, current BPM:", bpm); } - + const newBpmStr = await showPrompt(`Enter new BPM (${TIME_CONSTANTS.MIN_BPM}-${TIME_CONSTANTS.MAX_BPM}):`, bpm.toString()); - + // Check if user cancelled if (newBpmStr === null) { return; } - + // Validate input const newBpm = parseInt(newBpmStr.trim()); - + // Check if it's a valid number if (isNaN(newBpm)) { await showAlert("Invalid input. Please enter a valid number."); return; } - + // Check if it's within valid range if (newBpm <= TIME_CONSTANTS.MIN_BPM || newBpm >= TIME_CONSTANTS.MAX_BPM) { await showAlert(`Invalid BPM. Please enter a value between ${TIME_CONSTANTS.MIN_BPM} and ${TIME_CONSTANTS.MAX_BPM}.`); return; } - + // Update BPM setBpm(newBpm); setStatus(`BPM changed to ${newBpm}`); - + if (DEBUG_MODE.TOOLBAR) { console.log(`BPM updated from ${bpm} to ${newBpm}`); } @@ -643,7 +643,7 @@ const Toolbar: React.FC = () => { if (DEBUG_MODE.TOOLBAR) { console.log("Key signature changed from", keySignature, "to", newKeySignature); } - + setKeySignature(newKeySignature as KeySignature); setStatus(`Key signature changed to ${newKeySignature}`); setShowKeySignatureDropdown(false); @@ -673,9 +673,9 @@ const Toolbar: React.FC = () => { if (DEBUG_MODE.TOOLBAR) { console.log("Copy button clicked"); } - + const copied = handleCopyOperation(); - + if (copied) { setStatus("Items copied to clipboard"); if (DEBUG_MODE.TOOLBAR) { @@ -694,9 +694,9 @@ const Toolbar: React.FC = () => { if (DEBUG_MODE.TOOLBAR) { console.log("Paste button clicked"); } - + const pasted = handlePasteOperation(); - + if (pasted) { setStatus("Items pasted from clipboard"); if (DEBUG_MODE.TOOLBAR) { @@ -715,9 +715,9 @@ const Toolbar: React.FC = () => { if (DEBUG_MODE.TOOLBAR) { console.log("Delete button clicked"); } - + const deleted = regionDeleteManager.deleteSelectedRegions(); - + if (deleted) { setStatus("Selected regions deleted"); if (DEBUG_MODE.TOOLBAR) { @@ -875,16 +875,16 @@ const Toolbar: React.FC = () => { if (DEBUG_MODE.TOOLBAR) { console.log("Undo button clicked"); } - + if (!canUndo) { await showAlert("Nothing to undo"); return; } - + undo(); const description = undoDescription || "action"; setStatus(`Undid: ${description}`); - + if (DEBUG_MODE.TOOLBAR) { console.log(`Undo successful: ${description}`); } @@ -895,16 +895,16 @@ const Toolbar: React.FC = () => { if (DEBUG_MODE.TOOLBAR) { console.log("Redo button clicked"); } - + if (!canRedo) { await showAlert("Nothing to redo"); return; } - + redo(); const description = redoDescription || "action"; setStatus(`Redid: ${description}`); - + if (DEBUG_MODE.TOOLBAR) { console.log(`Redo successful: ${description}`); } @@ -915,7 +915,7 @@ const Toolbar: React.FC = () => { if (DEBUG_MODE.TOOLBAR) { console.log("Chat button clicked"); } - + toggleChatBox(); setStatus("Chat toggled"); }; @@ -925,7 +925,7 @@ const Toolbar: React.FC = () => { if (DEBUG_MODE.TOOLBAR) { console.log("Settings button clicked"); } - + toggleSettings(); setStatus("Settings toggled"); }; @@ -940,11 +940,11 @@ const Toolbar: React.FC = () => { toggleKGOnePanel(); }; - const handleListEventClick = () => { + const handleEventListClick = () => { if (DEBUG_MODE.TOOLBAR) { - console.log("List Event button clicked"); + console.log("Event List button clicked"); } - toggleListEventPanel(); + toggleEventListPanel(); }; // Handle Piano button click: open piano roll if closed, targeting active or selected region @@ -1039,226 +1039,226 @@ const Toolbar: React.FC = () => {
DAW Logo
-
{projectName}
- -
- - - -
- -
- -
-
- - -
- - -
- - - - - -
- - - -
- - {!isPlaying ? ( - - ) : ( - - )} - - -
- - - {/* - */} -
- -
-
-
- setShowZoomSlider(!showZoomSlider)} - style={{ cursor: 'pointer' }} + +
+ + + +
+
-
- {currentTime} -
-
- {bpm} -
-
- {timeSignature.numerator + "/" + timeSignature.denominator} -
-
- setShowKeySignatureDropdown(!showKeySignatureDropdown)} - style={{ cursor: 'pointer' }} - > - {keySignature} - + +
+ + +
+ + +
+ + + + + +
+ + + +
+ + {!isPlaying ? ( + + ) : ( + + )} + + +
+ + + {/* + */} +
+ +
+
+
+ setShowZoomSlider(!showZoomSlider)} + style={{ cursor: 'pointer' }} + > + {barWidthMultiplier}x + + {showZoomSlider && ( +
+ setBarWidthMultiplier(parseInt(e.target.value))} + /> + {barWidthMultiplier}x +
+ )} +
+
+ {currentTime} +
+
+ {bpm} +
+
+ {timeSignature.numerator + "/" + timeSignature.denominator} +
+
+ setShowKeySignatureDropdown(!showKeySignatureDropdown)} + style={{ cursor: 'pointer' }} + > + {keySignature} + +
+ +
+
+
+ + +
- - -
-
- - setShowImportModal(false)} - onFileImport={handleFileImport} - acceptedTypes={['.kgstudio', '.json', '.mid', '.midi']} - title="Import Project" - description="Drag and drop your project file here" - /> - - - {showOpenProject && ( - setShowOpenProject(false)} - onConfirmOpenProject={handleConfirmOpenProject} - onOpenProject={handleOpenProjectSelect} - currentProjectName={savedProjectName} - onCreateNewProject={createNewProject} + setShowImportModal(false)} + onFileImport={handleFileImport} + acceptedTypes={['.kgstudio', '.json', '.mid', '.midi']} + title="Import Project" + description="Drag and drop your project file here" /> - )} + + + + {showOpenProject && ( + setShowOpenProject(false)} + onConfirmOpenProject={handleConfirmOpenProject} + onOpenProject={handleOpenProjectSelect} + currentProjectName={savedProjectName} + onCreateNewProject={createNewProject} + /> + )} ); }; diff --git a/src/components/list-event-panel/RegionListEventTab.tsx b/src/components/event-list-panel/RegionEventListTab.tsx similarity index 97% rename from src/components/list-event-panel/RegionListEventTab.tsx rename to src/components/event-list-panel/RegionEventListTab.tsx index 11d8387..d202dd0 100644 --- a/src/components/list-event-panel/RegionListEventTab.tsx +++ b/src/components/event-list-panel/RegionEventListTab.tsx @@ -36,7 +36,7 @@ import { UpdateNotePropertiesCommand } from '../../core/commands/note/UpdateNote import { UpdatePitchBendPropertiesCommand } from '../../core/commands/note/UpdatePitchBendPropertiesCommand'; import { showAlert } from '../../util/dialogUtil'; -interface RegionListEventTabProps { +interface RegionEventListTabProps { activeMidiRegion: KGMidiRegion | null; parentTrack: KGMidiTrack | null; } @@ -181,7 +181,7 @@ const parseControllerValueDeltaInput = (raw: string): { delta: number } | { erro return { delta: parseInt(trimmed, 10) }; }; -const RegionListEventTab: React.FC = ({ activeMidiRegion, parentTrack }) => { +const RegionEventListTab: React.FC = ({ activeMidiRegion, parentTrack }) => { const { selectedNoteIds, selectedPitchBendIds, @@ -941,22 +941,22 @@ const RegionListEventTab: React.FC = ({ activeMidiRegio return ( <> -
- - - +
+ + +
{!activeMidiRegion ? ( -
+
Please select a MIDI region, or open one in the Piano Roll, to view its event list.
) : ( <> -
-
+
+
-
+
= ({ activeMidiRegio quantizeSelectedNotes(value); }} label="Qua. Pos." - buttonClassName="list-event-quant-button" + buttonClassName="event-list-quant-button" /> = ({ activeMidiRegio quantizeSelectedNoteLengths(value); }} label="Qua. Len." - buttonClassName="list-event-quant-button" + buttonClassName="event-list-quant-button" />
-
- +
+
@@ -1056,7 +1056,7 @@ const RegionListEventTab: React.FC = ({ activeMidiRegio {isEditingPosition ? ( setEditingCell({ ...editingCell, value: event.target.value })} onBlur={handleEditInputBlur} @@ -1075,7 +1075,7 @@ const RegionListEventTab: React.FC = ({ activeMidiRegio {isEditingNum ? ( setEditingCell({ ...editingCell, value: event.target.value })} onBlur={handleEditInputBlur} @@ -1092,7 +1092,7 @@ const RegionListEventTab: React.FC = ({ activeMidiRegio {isEditingVal ? ( setEditingCell({ ...editingCell, value: event.target.value })} onBlur={handleEditInputBlur} @@ -1110,7 +1110,7 @@ const RegionListEventTab: React.FC = ({ activeMidiRegio {isEditingLength ? ( setEditingCell({ ...editingCell, value: event.target.value })} onBlur={handleEditInputBlur} @@ -1132,4 +1132,4 @@ const RegionListEventTab: React.FC = ({ activeMidiRegio ); }; -export default RegionListEventTab; +export default RegionEventListTab; diff --git a/src/components/list-event-panel/TrackListEventTab.tsx b/src/components/event-list-panel/TrackEventListTab.tsx similarity index 96% rename from src/components/list-event-panel/TrackListEventTab.tsx rename to src/components/event-list-panel/TrackEventListTab.tsx index 71818c7..c55a5a1 100644 --- a/src/components/list-event-panel/TrackListEventTab.tsx +++ b/src/components/event-list-panel/TrackEventListTab.tsx @@ -30,7 +30,7 @@ import { isModifierKeyPressed } from '../../util/osUtil'; import { showAlert } from '../../util/dialogUtil'; import { AUDIO_INTERFACE_CONSTANTS } from '../../constants/coreConstants'; -interface TrackListEventTabProps { +interface TrackEventListTabProps { selectedTrack: KGMidiTrack | KGAudioTrack | null; } @@ -118,7 +118,7 @@ const findPreviousPanValue = (points: KGTrackAutomationPoint[], beat: number): n return previousPoint?.getValue() ?? 0; }; -const TrackListEventTab: React.FC = ({ selectedTrack }) => { +const TrackEventListTab: React.FC = ({ selectedTrack }) => { const { tracks, playheadPosition, @@ -659,22 +659,22 @@ const TrackListEventTab: React.FC = ({ selectedTrack }) return ( <> -
- - - +
+ + +
{!liveSelectedTrack ? ( -
+
Please select a track to view regions and track automation.
) : ( <> -
-
+
+
-
+
-
-
Position
+
+
@@ -738,7 +738,7 @@ const TrackListEventTab: React.FC = ({ selectedTrack }) {isEditingPosition ? ( setEditingCell({ ...editingCell, value: event.target.value })} onBlur={handleEditInputBlur} @@ -757,7 +757,7 @@ const TrackListEventTab: React.FC = ({ selectedTrack }) {isEditingVal ? ( setEditingCell({ ...editingCell, value: event.target.value })} onBlur={handleEditInputBlur} @@ -775,7 +775,7 @@ const TrackListEventTab: React.FC = ({ selectedTrack }) {isEditingLength ? ( setEditingCell({ ...editingCell, value: event.target.value })} onBlur={handleEditInputBlur} @@ -797,4 +797,4 @@ const TrackListEventTab: React.FC = ({ selectedTrack }) ); }; -export default TrackListEventTab; +export default TrackEventListTab; diff --git a/src/components/piano-roll/PianoRoll.tsx b/src/components/piano-roll/PianoRoll.tsx index ebf625f..7c45e52 100644 --- a/src/components/piano-roll/PianoRoll.tsx +++ b/src/components/piano-roll/PianoRoll.tsx @@ -47,8 +47,8 @@ const PianoRoll: React.FC = ({ }) => { const isSpectrogram = mode === 'spectrogram'; const isHybrid = mode === 'hybrid'; - const { maxBars, tracks, updateTrack, timeSignature, showChatBox, showKGOnePanel, showListEventPanel, showInstrumentSelection, keySignature, selectedMode, setSelectedMode, playheadPosition, isPlaying, autoScrollEnabled, bpm, pianoRollScrollRequest, selectedNoteIds, automationRedrawVersion } = useProjectStore(); - + const { maxBars, tracks, updateTrack, timeSignature, showChatBox, showKGOnePanel, showEventListPanel, showInstrumentSelection, keySignature, selectedMode, setSelectedMode, playheadPosition, isPlaying, autoScrollEnabled, bpm, pianoRollScrollRequest, selectedNoteIds, automationRedrawVersion } = useProjectStore(); + // Tool state for piano roll const [activeTool, setActiveTool] = useState<'pointer' | 'pencil'>('pointer'); @@ -62,7 +62,7 @@ const PianoRoll: React.FC = ({ const [pianoRollZoom, setPianoRollZoom] = useState(1); const [automationEnabled, setAutomationEnabled] = useState(false); const [automationType, setAutomationType] = useState('pitch-bend'); - + // Quantization state const [quantPosition, setQuantPosition] = useState('1/8'); const [quantLength, setQuantLength] = useState('1/8'); @@ -75,7 +75,7 @@ const PianoRoll: React.FC = ({ // Piano roll state with temporary initial values const [position, setPosition] = useState(initialPosition || { x: 0, y: 0 }); - + // Blink effect state for toolbar button feedback const [blinkButton, setBlinkButton] = useState(null); const [size, setSize] = useState(initialSize || { width: 800, height: PIANO_ROLL_CONSTANTS.PIANO_ROLL_HEIGHT }); @@ -102,7 +102,7 @@ const PianoRoll: React.FC = ({ // Ref for storing the setNoteUpdateCounter function const triggerNoteUpdateRef = useRef> | null>(null); - + // Ref for storing the deleteSelectedNotes function const deleteSelectedNotesRef = useRef<(() => boolean) | null>(null); @@ -133,32 +133,32 @@ const PianoRoll: React.FC = ({ y: window.innerHeight - statusBarHeight - pianoRollHeight }; }; - + const calculateInitialSize = () => { const rootStyles = getComputedStyle(document.documentElement); const chatBoxWidthStr = rootStyles.getPropertyValue('--chat-box-width') || '350px'; const instrumentPanelWidthStr = rootStyles.getPropertyValue('--instrument-selection-width') || '300px'; const chatBoxWidth = parseInt(chatBoxWidthStr, 10) || 350; const instrumentPanelWidth = parseInt(instrumentPanelWidthStr, 10) || 300; - + let availableWidth = window.innerWidth; - if (showChatBox || showKGOnePanel || showListEventPanel) availableWidth -= chatBoxWidth; + if (showChatBox || showKGOnePanel || showEventListPanel) availableWidth -= chatBoxWidth; if (showInstrumentSelection) availableWidth -= instrumentPanelWidth; - + // Ensure a sensible minimum starting width const clampedWidth = Math.max(400, availableWidth); - + return { width: clampedWidth, height: PIANO_ROLL_CONSTANTS.PIANO_ROLL_HEIGHT }; }; - + // Set position and size only if not provided as props if (!initialPosition) { setPosition(calculateInitialPosition()); } - + if (!initialSize) { setSize(calculateInitialSize()); } @@ -195,17 +195,17 @@ const PianoRoll: React.FC = ({ // Sync local state with KGPianoRollState on mount useEffect(() => { const pianoRollState = KGPianoRollState.instance(); - + // Sync snapping state const currentSnap = pianoRollState.getCurrentSnap(); setSnapping(currentSnap); - + // Sync tool state const currentTool = pianoRollState.getActiveTool() as 'pointer' | 'pencil'; setActiveTool(currentTool); setAutomationEnabled(pianoRollState.getAutomationViewEnabled()); setAutomationType(pianoRollState.getCurrentAutomationType() as PianoRollAutomationType); - + if (DEBUG_MODE.PIANO_ROLL) { console.log(`Synced piano roll state on mount - snap: ${currentSnap}, tool: ${currentTool}`); } @@ -257,10 +257,10 @@ const PianoRoll: React.FC = ({ onClose(); } }; - + // Add event listener window.addEventListener('keydown', handleKeyDown); - + // Remove event listener on cleanup return () => { window.removeEventListener('keydown', handleKeyDown); @@ -284,13 +284,13 @@ const PianoRoll: React.FC = ({ e.preventDefault(); } }; - + useEffect(() => { const handleMouseMove = (e: MouseEvent) => { if (isDragging) { // Set the flag to true as soon as any movement happens wasDraggingRef.current = true; - + setPosition({ x: e.clientX - dragOffset.x, y: e.clientY - dragOffset.y @@ -302,25 +302,25 @@ const PianoRoll: React.FC = ({ }); } }; - + const handleMouseUp = () => { setIsDragging(false); setIsResizing(false); // We keep wasDraggingRef.current as is - it will be used in handleTitleClick // and reset on the next mousedown }; - + if (isDragging || isResizing) { document.addEventListener('mousemove', handleMouseMove as unknown as EventListener); document.addEventListener('mouseup', handleMouseUp); } - + return () => { document.removeEventListener('mousemove', handleMouseMove as unknown as EventListener); document.removeEventListener('mouseup', handleMouseUp); }; }, [isDragging, isResizing, dragOffset, position]); - + // Handle title click to rename the region const handleTitleClick = async () => { // If we were just dragging, don't show the rename dialog @@ -330,28 +330,28 @@ const PianoRoll: React.FC = ({ } return; } - + if (!activeRegion) return; - + // Show a prompt to get the new name const newName = await showPrompt("Enter a new name for the region:", activeRegion.getName()); - + // If the user clicked Cancel or entered an empty string, do nothing if (!newName || newName.trim() === '' || newName === activeRegion.getName()) return; - + // Use command pattern to update the region name with undo support try { const command = new UpdateRegionCommand(activeRegion.getId(), { name: newName.trim() }); KGCore.instance().executeCommand(command); - + if (DEBUG_MODE.PIANO_ROLL) { console.log(`Executed UpdateRegionCommand: renamed region ${activeRegion.getId()} to "${newName}" using command pattern`); } - + // Update the store to trigger re-render const updatedTracks = [...tracks]; useProjectStore.setState({ tracks: updatedTracks }); - + } catch (error) { console.error('Error renaming region:', error); await showAlert('Failed to rename region. Please try again.'); @@ -366,7 +366,7 @@ const PianoRoll: React.FC = ({ console.log(`Selected tool: ${tool}`); } }; - + // Handle snapping selection const handleSnappingSelect = useCallback((value: string) => { setSnapping(value); @@ -448,166 +448,166 @@ const PianoRoll: React.FC = ({ const handleSetDeleteNotesTrigger = (deleteFn: () => boolean) => { deleteSelectedNotesRef.current = deleteFn; }; - + // Quantize selected notes based on the selected quantization value const quantizeSelectedNotes = useCallback((quantValue: string) => { if (!activeRegion) return; - + // Get the KGCore instance const core = KGCore.instance(); - + // Get all selected notes const selectedItems = core.getSelectedItems(); - const selectedNotes = selectedItems.filter(item => - item instanceof KGMidiNote && + const selectedNotes = selectedItems.filter(item => + item instanceof KGMidiNote && activeRegion.getNotes().some(note => note.getId() === item.getId()) ) as KGMidiNote[]; - + if (selectedNotes.length === 0) { if (DEBUG_MODE.PIANO_ROLL) { console.log('No notes selected for quantization'); } return; } - + if (DEBUG_MODE.PIANO_ROLL) { console.log(`Quantizing ${selectedNotes.length} selected notes with value: ${quantValue}`); } - + // Parse the quantization value (e.g., "1/4", "1/8", "1/16", "1/32") const denominator = parseInt(quantValue.split('/')[1]); if (isNaN(denominator)) { console.error(`Invalid quantization value: ${quantValue}`); return; } - + // Calculate the quantization step in beats // In a 4/4 time signature, a quarter note (1/4) is 1 beat // In a 6/8 time signature, an eighth note (1/8) is 1 beat const { numerator, denominator: timeSigDenominator } = timeSignature; - + // Calculate beats per whole note based on time signature // In 4/4, a whole note is 4 beats // In 6/8, a whole note is 6 beats (because each beat is an eighth note) const beatsPerWholeNote = numerator * (4 / timeSigDenominator); - + // Calculate the quantization step in beats // quantizationStep should ALWAYS be 4 / denominator regardless of time signature const quantizationStep = 4 / denominator; - + if (DEBUG_MODE.PIANO_ROLL) { console.log(`Time signature: ${numerator}/${timeSigDenominator}`); console.log(`Beats per whole note: ${beatsPerWholeNote}`); console.log(`Quantization step: ${quantizationStep} beats`); } - + // Apply quantization to each selected note selectedNotes.forEach(note => { // Get the current start beat const currentStartBeat = note.getStartBeat(); - + // Calculate the quantized start beat const quantizedStartBeat = Math.round(currentStartBeat / quantizationStep) * quantizationStep; - + // Calculate the duration of the note const duration = note.getEndBeat() - currentStartBeat; - + // Set the new start beat and maintain the duration note.setStartBeat(quantizedStartBeat); note.setEndBeat(quantizedStartBeat + duration); - + if (DEBUG_MODE.PIANO_ROLL) { console.log(`Quantized note ${note.getId()}: ${currentStartBeat} -> ${quantizedStartBeat}`); } }); - + // Find the track that contains this region and update it const track = tracks.find(t => t.getId().toString() === activeRegion.getTrackId()); if (track) { updateTrack(track); } - + // Trigger a re-render by incrementing the note update counter if (triggerNoteUpdateRef.current) { triggerNoteUpdateRef.current(prev => prev + 1); - + if (DEBUG_MODE.PIANO_ROLL) { console.log('Triggered note update to re-render quantized notes'); } } }, [activeRegion, timeSignature, updateTrack, tracks]); - + // Quantize selected notes length based on the selected quantization value const quantizeNoteLength = useCallback((quantValue: string) => { if (!activeRegion) return; - + // Get the KGCore instance const core = KGCore.instance(); - + // Get all selected notes const selectedItems = core.getSelectedItems(); - const selectedNotes = selectedItems.filter(item => - item instanceof KGMidiNote && + const selectedNotes = selectedItems.filter(item => + item instanceof KGMidiNote && activeRegion.getNotes().some(note => note.getId() === item.getId()) ) as KGMidiNote[]; - + if (selectedNotes.length === 0) { if (DEBUG_MODE.PIANO_ROLL) { console.log('No notes selected for length quantization'); } return; } - + if (DEBUG_MODE.PIANO_ROLL) { console.log(`Quantizing length of ${selectedNotes.length} selected notes with value: ${quantValue}`); } - + // Parse the quantization value (e.g., "1/1", "1/2", "1/4", "1/8", "1/16", "1/32") const denominator = parseInt(quantValue.split('/')[1]); if (isNaN(denominator)) { console.error(`Invalid quantization value: ${quantValue}`); return; } - + // Calculate the quantization step in beats // In a 4/4 time signature, a quarter note (1/4) is 1 beat // In a 6/8 time signature, an eighth note (1/8) is 1 beat const { numerator, denominator: timeSigDenominator } = timeSignature; - + // Calculate beats per whole note based on time signature // In 4/4, a whole note is 4 beats // In 6/8, a whole note is 6 beats (because each beat is an eighth note) const beatsPerWholeNote = numerator * (4 / timeSigDenominator); - + // Calculate the quantization step in beats // quantizationStep should ALWAYS be 4 / denominator regardless of time signature const quantizationStep = 4 / denominator; - + if (DEBUG_MODE.PIANO_ROLL) { console.log(`Time signature: ${numerator}/${timeSigDenominator}`); console.log(`Beats per whole note: ${beatsPerWholeNote}`); console.log(`Length quantization step: ${quantizationStep} beats`); } - + // Apply quantization to each selected note selectedNotes.forEach(note => { // Get the current start and end beats const startBeat = note.getStartBeat(); const currentEndBeat = note.getEndBeat(); - + // Calculate the current duration const currentDuration = currentEndBeat - startBeat; - + // Calculate the quantized duration // If the current duration is less than the quantization step, // extend it to match the quantization step exactly // Otherwise, round to the nearest multiple of quantizationStep let quantizedDuration; - + if (currentDuration < quantizationStep) { // For notes shorter than the quantization step, extend to exactly one step quantizedDuration = quantizationStep; - + if (DEBUG_MODE.PIANO_ROLL) { console.log(`Extending short note ${note.getId()} from ${currentDuration} to ${quantizedDuration}`); } @@ -615,28 +615,28 @@ const PianoRoll: React.FC = ({ // For longer notes, round to nearest multiple of quantizationStep quantizedDuration = Math.round(currentDuration / quantizationStep) * quantizationStep; } - + // Ensure minimum note length quantizedDuration = Math.max(PIANO_ROLL_CONSTANTS.MIN_NOTE_LENGTH, quantizedDuration); - + // Set the new end beat while maintaining the start beat note.setEndBeat(startBeat + quantizedDuration); - + if (DEBUG_MODE.PIANO_ROLL) { console.log(`Quantized note length ${note.getId()}: ${currentDuration} -> ${quantizedDuration}`); } }); - + // Find the track that contains this region and update it const track = tracks.find(t => t.getId().toString() === activeRegion.getTrackId()); if (track) { updateTrack(track); } - + // Trigger a re-render by incrementing the note update counter if (triggerNoteUpdateRef.current) { triggerNoteUpdateRef.current(prev => prev + 1); - + if (DEBUG_MODE.PIANO_ROLL) { console.log('Triggered note update to re-render quantized note lengths'); } @@ -647,19 +647,19 @@ const PianoRoll: React.FC = ({ const handleQuantSelect = useCallback((type: 'position' | 'length', value: string) => { if (type === 'position') { setQuantPosition(value); - + // Apply quantization immediately when position quantization is changed quantizeSelectedNotes(value); - + if (DEBUG_MODE.PIANO_ROLL) { console.log(`quant-position selected: ${value}`); } } else { setQuantLength(value); - + // Apply length quantization immediately when length quantization is changed quantizeNoteLength(value); - + if (DEBUG_MODE.PIANO_ROLL) { console.log(`quant-length selected: ${value}`); } @@ -710,24 +710,24 @@ const PianoRoll: React.FC = ({ // We have 8 octaves (0-7), and C4 is in the middle // Each octave has 12 notes, each note is piano key height // C4 is in octave 4, and C is the first note in each octave - + // Calculate from the bottom: // - Octaves 0-3 = 4 octaves = 4 * 12 * piano key height // - Within octave 4, C is the first note (from bottom), so 0px additional const keyHeight = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--region-piano-key-height')) || 20; const c4Position = 4 * 12 * keyHeight; // pixels from bottom - + // Total height of all notes (8 octaves * 12 notes * piano key height) const totalHeight = 8 * 12 * keyHeight; - + // Get the viewport height of the piano roll content const viewportHeight = pianoRollNoteScrollRef.current.clientHeight; - + // Calculate scroll position to center C4 // We need to scroll from the top, so we calculate: // (total height - C4 position) - (viewport height / 2) const scrollPosition = (totalHeight - c4Position) - (viewportHeight / 2); - + // Scroll to the calculated position pianoRollNoteScrollRef.current.scrollTop = Math.max(0, scrollPosition); } @@ -846,23 +846,23 @@ const PianoRoll: React.FC = ({ if (pianoRollNoteScrollRef.current && activeRegion) { // Get the starting beat of the region const startBeat = activeRegion.getStartFromBeat(); - + // Get the time signature to calculate beats per bar const beatsPerBar = timeSignature.numerator; - + // Calculate the bar number (0-indexed) const barNumber = Math.floor(startBeat / beatsPerBar); - + if (DEBUG_MODE.PIANO_ROLL) { console.log(`Scrolling to region's starting bar: ${barNumber + 1} (startBeat: ${startBeat}, beatsPerBar: ${beatsPerBar})`); } - + // Calculate the pixel position (each bar is --region-grid-bar-width wide, which is 160px by default) const barWidth = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--region-grid-bar-width')) || 160; - + // Calculate the scroll position to scroll to the starting bar const scrollPosition = barNumber * barWidth; - + // Scroll to the calculated position pianoRollNoteScrollRef.current.scrollLeft = Math.max(0, scrollPosition); } @@ -874,8 +874,8 @@ const PianoRoll: React.FC = ({ // Skip if user is typing in an input field (including ChatBox) const target = event.target as HTMLElement; if (target && ( - target.tagName === 'INPUT' || - target.tagName === 'TEXTAREA' || + target.tagName === 'INPUT' || + target.tagName === 'TEXTAREA' || target.contentEditable === 'true' || target.hasAttribute('data-chatbox-input') || target.closest('.chatbox-input') @@ -894,7 +894,7 @@ const PianoRoll: React.FC = ({ } return; } - + // Handle piano roll hotkeys const configManager = ConfigManager.instance(); if (configManager.getIsInitialized()) { @@ -916,21 +916,21 @@ const PianoRoll: React.FC = ({ const snap_1_4_key = configManager.get('hotkeys.piano_roll.snap_1_4') as string; const snap_1_8_key = configManager.get('hotkeys.piano_roll.snap_1_8') as string; const snap_1_16_key = configManager.get('hotkeys.piano_roll.snap_1_16') as string; - + // Quantize position hotkeys const qua_pos_1_4_key = configManager.get('hotkeys.piano_roll.qua_pos_1_4') as string; const qua_pos_1_8_key = configManager.get('hotkeys.piano_roll.qua_pos_1_8') as string; const qua_pos_1_16_key = configManager.get('hotkeys.piano_roll.qua_pos_1_16') as string; - + // Quantize length hotkeys const qua_len_1_4_key = configManager.get('hotkeys.piano_roll.qua_len_1_4') as string; const qua_len_1_8_key = configManager.get('hotkeys.piano_roll.qua_len_1_8') as string; const qua_len_1_16_key = configManager.get('hotkeys.piano_roll.qua_len_1_16') as string; - + let actionType: 'snap' | 'quantize' | null = null; let actionValue: string | null = null; let quantType: 'position' | 'length' | null = null; - + // Check snapping hotkeys if (event.key === snap_none_key) { actionType = 'snap'; @@ -973,21 +973,21 @@ const PianoRoll: React.FC = ({ actionValue = '1/16'; quantType = 'length'; } - + if (actionType && actionValue) { // Prevent default behavior event.preventDefault(); - + if (actionType === 'snap') { // Validate the snap value exists in snap options if (KGPianoRollState.SNAP_OPTIONS.includes(actionValue)) { // Change snapping value handleSnappingSelect(actionValue); - + // Trigger blink effect for visual feedback setBlinkButton('snapping'); setTimeout(() => setBlinkButton(null), 200); - + if (DEBUG_MODE.PIANO_ROLL) { console.log(`Snap hotkey triggered: ${event.key} → ${actionValue}`); } @@ -995,16 +995,16 @@ const PianoRoll: React.FC = ({ } else if (actionType === 'quantize' && quantType) { // Validate the quantValue exists in the appropriate options const validOptions = quantType === 'length' ? KGPianoRollState.QUANT_LEN_OPTIONS : KGPianoRollState.QUANT_POS_OPTIONS; - + if (validOptions.includes(actionValue)) { // Apply quantization handleQuantSelect(quantType, actionValue); - + // Trigger blink effect for visual feedback const buttonName = quantType === 'length' ? 'quant-length' : 'quant-position'; setBlinkButton(buttonName); setTimeout(() => setBlinkButton(null), 200); - + if (DEBUG_MODE.PIANO_ROLL) { console.log(`Quantize ${quantType} hotkey triggered: ${event.key} → ${actionValue}`); } @@ -1013,10 +1013,10 @@ const PianoRoll: React.FC = ({ } } }; - + // Add event listener window.addEventListener('keydown', handlePianoRollKeyDown); - + // Remove event listener on cleanup return () => { window.removeEventListener('keydown', handlePianoRollKeyDown); @@ -1032,20 +1032,20 @@ const PianoRoll: React.FC = ({ return `${midiName} + ${audioName}`; } if (!activeRegion) return "EDIT NOTE CLIP"; - + // Calculate the bar and beat position of the region const startBeat = activeRegion.getStartFromBeat(); const { bar, beatInBar } = beatsToBar(startBeat, timeSignature); - + // Format as 1-indexed bar and beat (bar + 1, beatInBar + 1) const barNumber = bar + 1; const beatNumber = beatInBar + 1; - + return `${activeRegion.getName()} (at ${barNumber}:${beatNumber})`; }; return ( -
= ({ onTitleClick={handleTitleClick} onMouseDown={(e) => handleMouseDown(e, 'drag')} /> - + = ({ automationType={automationType} automationRedrawVersion={automationRedrawVersion} /> - -
handleMouseDown(e, 'resize')} > diff --git a/src/stores/projectStore.ts b/src/stores/projectStore.ts index 9540cdc..32f34cf 100644 --- a/src/stores/projectStore.ts +++ b/src/stores/projectStore.ts @@ -74,7 +74,7 @@ interface ProjectState { isPreparingPlayback: boolean; autoScrollEnabled: boolean; currentTime: string; // formatted time string - + // Selection state for UI reactivity selectedNoteIds: string[]; selectedPitchBendIds: string[]; @@ -82,7 +82,7 @@ interface ProjectState { selectedTrackAutomationPointIds: string[]; selectedRegionIds: string[]; selectedTrackId: string | null; - + // Piano roll state showPianoRoll: boolean; activeRegionId: string | null; @@ -92,20 +92,20 @@ interface ProjectState { activeTrackAutomationTrackId: string | null; activeTrackAutomationType: TrackAutomationType | null; trackAutomationRedrawVersion: number; - + // ChatBox state showChatBox: boolean; // K.G.One panel state showKGOnePanel: boolean; - // List event panel state - showListEventPanel: boolean; + // Event list panel state + showEventListPanel: boolean; // Instrument selection panel state showInstrumentSelection: boolean; // instrumentSelectionTrackId removed; panel now follows selectedTrackId - + // Audio import modal state showAudioImportModal: boolean; audioImportTargetTrackId: string | null; @@ -140,7 +140,7 @@ interface ProjectState { requestPianoRollScroll: (beatPosition: number) => void; mainContentScrollRequest: number | null; pianoRollScrollRequest: number | null; - + // Actions setProjectName: (name: string) => void; setSavedProjectName: (name: string) => void; @@ -177,7 +177,7 @@ interface ProjectState { clearAllSelections: () => void; setSelectedTrack: (trackId: string | null) => void; setTrackAutomationView: (trackId: string | null, automationType: TrackAutomationType | null) => void; - + // Piano roll actions setShowPianoRoll: (show: boolean) => void; setActiveRegionId: (regionId: string | null) => void; @@ -186,10 +186,10 @@ interface ProjectState { openHybridMode: (midiRegionId: string, audioRegionId: string) => void; bumpAutomationRedrawVersion: () => void; bumpTrackAutomationRedrawVersion: () => void; - + // Project state cleanup cleanupProjectState: () => void; - + // ChatBox actions setShowChatBox: (show: boolean) => void; toggleChatBox: () => void; @@ -197,27 +197,27 @@ interface ProjectState { // K.G.One panel actions toggleKGOnePanel: () => void; - // List event panel actions - toggleListEventPanel: () => void; + // Event List panel actions + toggleEventListPanel: () => void; // Instrument selection panel actions openInstrumentSelectionForTrack: () => void; toggleInstrumentSelectionForTrack: () => void; closeInstrumentSelection: () => void; - + // Settings actions setShowSettings: (show: boolean) => void; toggleSettings: () => void; - + // Copy/paste actions pasteRegionsAtTrack: (trackId: string, position: number) => void; pasteNotesToActiveRegion: (regionId: string, position: number) => void; - + // Undo/redo actions undo: () => void; redo: () => void; syncUndoRedoState: () => void; - + // Project state refresh actions refreshProjectState: () => void; @@ -282,20 +282,20 @@ function getAudioRecordingExtension(mimeType: string): string { // Create the store export const useProjectStore = create((set, get) => { const currentProject = KGCore.instance().getCurrentProject(); - + // Initialize CSS variable for time signature on store creation 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(); - const initialChatBoxState = configManager.getIsInitialized() + const initialChatBoxState = configManager.getIsInitialized() ? (configManager.get('chatbox.default_open') as boolean) ?? false : false; - + // Set up playhead update callback to keep store in sync during playback KGCore.instance().setPlayheadUpdateCallback((position: number) => { const { bpm, timeSignature } = get(); @@ -317,7 +317,7 @@ export const useProjectStore = create((set, get) => { const syncSelectionFromCore = () => { const core = KGCore.instance(); const selectedItems = core.getSelectedItems(); - + const noteIds = selectedItems .filter(item => item instanceof KGMidiNote) .map(item => item.getId()); @@ -333,7 +333,7 @@ export const useProjectStore = create((set, get) => { const regionIds = selectedItems .filter(item => item instanceof KGRegion) .map(item => item.getId()); - + set({ selectedNoteIds: noteIds, selectedPitchBendIds: pitchBendIds, @@ -345,10 +345,10 @@ export const useProjectStore = create((set, get) => { // Register the sync callback with KGCore KGCore.instance().onSelectionChanged(syncSelectionFromCore); - + // Initial selection sync syncSelectionFromCore(); - + // Set up command history sync callback const syncUndoRedoState = () => { const core = KGCore.instance(); @@ -362,10 +362,10 @@ export const useProjectStore = create((set, get) => { // Register the undo/redo sync callback with KGCore KGCore.instance().setOnCommandHistoryChanged(syncUndoRedoState); - + // Initial undo/redo state sync syncUndoRedoState(); - + // Ensure a default track exists on initial app start // Also auto-select it and open instrument selection panel let initialSelectedTrackId: string | null = null; @@ -403,7 +403,7 @@ export const useProjectStore = create((set, get) => { isPreparingPlayback: false, autoScrollEnabled: true, currentTime: beatsToTimeString(KGCore.instance().getPlayheadPosition(), currentProject.getBpm(), currentProject.getTimeSignature()), - + // Initial selection state selectedNoteIds: [], selectedPitchBendIds: [], @@ -411,7 +411,7 @@ export const useProjectStore = create((set, get) => { selectedTrackAutomationPointIds: [], selectedRegionIds: [], selectedTrackId: initialSelectedTrackId, - + // Initial piano roll state showPianoRoll: false, activeRegionId: null, @@ -421,26 +421,26 @@ export const useProjectStore = create((set, get) => { activeTrackAutomationTrackId: null, activeTrackAutomationType: null, trackAutomationRedrawVersion: 0, - + // Initial ChatBox state showChatBox: initialChatBoxState, // Initial K.G.One panel state showKGOnePanel: false, - // Initial List Event panel state - showListEventPanel: false, + // Initial Event List panel state + showEventListPanel: false, // Initial Instrument Selection panel state showInstrumentSelection: initialShowInstrumentSelection, - + // Initial audio import modal state showAudioImportModal: false, audioImportTargetTrackId: null, // Initial Settings state showSettings: false, - + // Initial undo/redo state canUndo: false, canRedo: false, @@ -493,18 +493,18 @@ export const useProjectStore = create((set, get) => { // Create and execute the add track command const command = new AddTrackCommand(); KGCore.instance().executeCommand(command); - + // Update the store state with a new array reference to trigger re-render const project = KGCore.instance().getCurrentProject(); set({ tracks: [...project.getTracks()] as KGTrack[] }); - + // Auto-select the newly created track and open instrument selection panel const newTrackId = command.getTrackId().toString(); set({ selectedTrackId: newTrackId, showInstrumentSelection: true, }); - + console.log(`Added track ${command.getTrackId()}`); } catch (error) { console.error('Error adding track:', error); @@ -630,23 +630,23 @@ export const useProjectStore = create((set, get) => { const deletedTrackIndex = currentTracks.findIndex(track => track.getId() === id); const { selectedTrackId } = get(); const isCurrentTrackSelected = selectedTrackId === id.toString(); - + // Create and execute the remove track command const command = new RemoveTrackCommand(id); KGCore.instance().executeCommand(command); - + // Update the store state with a new array reference to trigger re-render const project = KGCore.instance().getCurrentProject(); const remainingTracks = [...project.getTracks()] as KGTrack[]; set({ tracks: remainingTracks }); - + // Auto-select another track if any remain if (remainingTracks.length > 0) { // Prefer previous track, fallback to next track - const newSelectedIndex = deletedTrackIndex > 0 + const newSelectedIndex = deletedTrackIndex > 0 ? deletedTrackIndex - 1 // Select previous track : 0; // Select first remaining track (was next) - + const newSelectedTrack = remainingTracks[newSelectedIndex]; const newSelectedTrackId = newSelectedTrack.getId().toString(); @@ -662,7 +662,7 @@ export const useProjectStore = create((set, get) => { showInstrumentSelection: false, }); } - + console.log(`Removed track ${id}`); } catch (error) { console.error('Error removing track:', error); @@ -672,23 +672,23 @@ export const useProjectStore = create((set, get) => { updateTrack: async (updatedTrack: KGTrack) => { const { tracks } = get(); - + try { // Find the old track to compare instruments const oldTrack = tracks.find(track => track.getId() === updatedTrack.getId()); - + // Type guard to check if track is KGMidiTrack const isMidiTrack = (track: KGTrack): track is KGMidiTrack => { return track.getCurrentType() === 'KGMidiTrack' && 'getInstrument' in track; }; - + // Check if instrument changed (only for MIDI tracks) - const shouldUpdateInstrument = - isMidiTrack(updatedTrack) && - oldTrack && + const shouldUpdateInstrument = + isMidiTrack(updatedTrack) && + oldTrack && isMidiTrack(oldTrack) && updatedTrack.getInstrument() !== oldTrack.getInstrument(); - + // Update instrument in audio interface if changed if (shouldUpdateInstrument && isMidiTrack(updatedTrack)) { const audioInterface = KGAudioInterface.instance(); @@ -696,18 +696,18 @@ export const useProjectStore = create((set, get) => { audioInterface.setTrackInstrument(updatedTrack.getId().toString(), newInstrument); console.log(`Updated track ${updatedTrack.getId()} instrument to ${newInstrument}`); } - + // Find and update the track - const updatedTracks = tracks.map(track => + const updatedTracks = tracks.map(track => track.getId() === updatedTrack.getId() ? updatedTrack : track ); - + // Update the core model KGCore.instance().getCurrentProject().setTracks(updatedTracks); - + // Update the store set({ tracks: updatedTracks }); - + console.log(`Updated track ${updatedTrack.getId()}`); } catch (error) { console.error('Error updating track:', error); @@ -724,7 +724,7 @@ export const useProjectStore = create((set, get) => { // Update the store state with the current project state const project = KGCore.instance().getCurrentProject(); set({ tracks: [...project.getTracks()] as KGTrack[] }); - + console.log(`Updated track ${trackId} properties`); } catch (error) { console.error('Error updating track properties:', error); @@ -736,14 +736,14 @@ export const useProjectStore = create((set, get) => { try { // Use the new updateTrackProperties method with command pattern await get().updateTrackProperties(trackId, { instrument }); - + console.log(`Set track ${trackId} instrument to ${instrument}`); } catch (error) { console.error('Error setting track instrument:', error); get().setStatus('Failed to change instrument'); } }, - + reorderTracks: (sourceIndex: number, destinationIndex: number) => { try { // Create and execute the reorder tracks command @@ -753,7 +753,7 @@ export const useProjectStore = create((set, get) => { // Update the store state with the current project state const project = KGCore.instance().getCurrentProject(); set({ tracks: [...project.getTracks()] as KGTrack[] }); - + console.log(`Reordered track from index ${sourceIndex} to ${destinationIndex}`); } catch (error) { console.error('Error reordering tracks:', error); @@ -774,11 +774,11 @@ export const useProjectStore = create((set, get) => { refreshStatus: () => { set({ currentStatus: KGCore.instance().getStatus() || 'Unknown' }); }, - + loadProject: async (project: KGProject | null = null, savedName?: string) => { try { const { setPlayheadPosition } = get(); - + // Upgrade incoming project data to latest structure version (only when provided explicitly) if (project) { project = upgradeProjectToLatest(project); @@ -786,12 +786,12 @@ export const useProjectStore = create((set, get) => { // Get project from KGCore if null const projectToLoad = project || KGCore.instance().getCurrentProject(); - + // Set the project in KGCore if one was provided if (project) { KGCore.instance().setCurrentProject(projectToLoad); } - + // Ensure a default "Melody" track exists for empty projects if (projectToLoad.getTracks().length === 0) { const addDefaultTrackCommand = new AddTrackCommand(undefined, 'Melody'); @@ -800,17 +800,17 @@ export const useProjectStore = create((set, get) => { // Reset playhead to 0 when loading a project setPlayheadPosition(0); - + // Get project properties const maxBars = projectToLoad.getMaxBars(); const timeSignature = projectToLoad.getTimeSignature(); const bpm = projectToLoad.getBpm(); const keySignature = projectToLoad.getKeySignature(); const tracks = projectToLoad.getTracks(); - + // Setup audio synths for all tracks const audioInterface = KGAudioInterface.instance(); - + // Clear any existing synths/buses first tracks.forEach(track => { const trackId = track.getId().toString(); @@ -857,7 +857,7 @@ export const useProjectStore = create((set, get) => { audioInterface.setTrackVolume(trackId, track.getVolume()); } } - + // Update CSS variables updateTimeSignatureCSS(timeSignature); updateMaxBarsCSS(maxBars); @@ -866,7 +866,7 @@ export const useProjectStore = create((set, get) => { // Log project loading info console.log(`Project max bars: ${maxBars}`); console.log(`Setup audio synths for ${tracks.length} tracks`); - + // Update the store state to reflect the loaded project // Force a new array reference for tracks to trigger React/Zustand re-render set({ @@ -914,7 +914,7 @@ export const useProjectStore = create((set, get) => { // Reset piano roll state for new/loaded project KGPianoRollState.instance().setLastEditedNoteLength(1); - + // Add a status message KGCore.instance().setStatus(`Project "${projectToLoad.getName()}" loaded with audio setup`); set({ currentStatus: KGCore.instance().getStatus() || 'Unknown' }); @@ -927,7 +927,7 @@ export const useProjectStore = create((set, get) => { setPlayheadPosition: (position: number) => { const { bpm, timeSignature } = get(); KGCore.instance().setPlayheadPosition(position); - set({ + set({ playheadPosition: position, currentTime: beatsToTimeString(position, bpm, timeSignature) }); @@ -1024,9 +1024,9 @@ export const useProjectStore = create((set, get) => { KGCore.instance().setLoopBoundaryReachedCallback(projectLooping ? (loopEndBeat: number) => { - _audioRecordingForcedStopBeatAbsolute = loopEndBeat; - void get().stopRecording(); - } + _audioRecordingForcedStopBeatAbsolute = loopEndBeat; + void get().stopRecording(); + } : null); setPlayheadPosition(recordingStartBeatAbsolute); @@ -1405,10 +1405,10 @@ export const useProjectStore = create((set, get) => { // Create and execute the change project property command const command = new ChangeProjectPropertyCommand({ bpm }); KGCore.instance().executeCommand(command); - + // Update the store state set({ bpm }); - + console.log(`Set BPM to ${bpm}`); } catch (error) { console.error('Error setting BPM:', error); @@ -1421,12 +1421,12 @@ export const useProjectStore = create((set, get) => { // Create and execute the change project property command const command = new ChangeProjectPropertyCommand({ maxBars }); KGCore.instance().executeCommand(command); - + // Update the store state set({ maxBars }); // Sync CSS var so layout adjusts immediately updateMaxBarsCSS(maxBars); - + console.log(`Set max bars to ${maxBars}`); } catch (error) { console.error('Error setting max bars:', error); @@ -1492,7 +1492,7 @@ export const useProjectStore = create((set, get) => { // Selection actions syncSelectionFromCore, - + clearAllSelections: () => { KGCore.instance().clearSelectedItems(); // Note: syncSelectionFromCore will be called automatically via callback @@ -1526,7 +1526,7 @@ export const useProjectStore = create((set, get) => { selectedTrackId: trackId, }); }, - + // Piano roll actions setShowPianoRoll: (show: boolean) => { set({ showPianoRoll: show }); @@ -1553,7 +1553,7 @@ export const useProjectStore = create((set, get) => { bumpTrackAutomationRedrawVersion: () => { set(state => ({ trackAutomationRedrawVersion: state.trackAutomationRedrawVersion + 1 })); }, - + // Project state cleanup - used when starting new/loading projects cleanupProjectState: () => { // Close piano roll if it's visible @@ -1570,36 +1570,36 @@ export const useProjectStore = create((set, get) => { recordingAudioPreviewPeaks: [], recordingAudioPreviewCurrentBeat: 0, }); - + // Clear any selected items KGCore.instance().clearSelectedItems(); // Note: syncSelectionFromCore will be called automatically via callback - + console.log("Cleaned up project state: closed piano roll, cleared active region, cleared selections"); }, - + // ChatBox action implementations setShowChatBox: (show: boolean) => { set({ showChatBox: show, showKGOnePanel: show ? false : get().showKGOnePanel, - showListEventPanel: show ? false : get().showListEventPanel + showEventListPanel: show ? false : get().showEventListPanel }); }, - + toggleChatBox: () => { const { showChatBox } = get(); - set({ showChatBox: !showChatBox, showKGOnePanel: false, showListEventPanel: false }); + set({ showChatBox: !showChatBox, showKGOnePanel: false, showEventListPanel: false }); }, toggleKGOnePanel: () => { const { showKGOnePanel } = get(); - set({ showKGOnePanel: !showKGOnePanel, showChatBox: false, showListEventPanel: false }); + set({ showKGOnePanel: !showKGOnePanel, showChatBox: false, showEventListPanel: false }); }, - toggleListEventPanel: () => { - const { showListEventPanel } = get(); - set({ showListEventPanel: !showListEventPanel, showChatBox: false, showKGOnePanel: false }); + toggleEventListPanel: () => { + const { showEventListPanel } = get(); + set({ showEventListPanel: !showEventListPanel, showChatBox: false, showKGOnePanel: false }); }, // Instrument selection panel actions @@ -1612,64 +1612,64 @@ export const useProjectStore = create((set, get) => { closeInstrumentSelection: () => { set({ showInstrumentSelection: false }); }, - + // Settings action implementations setShowSettings: (show: boolean) => { set({ showSettings: show }); }, - + toggleSettings: () => { const { showSettings } = get(); set({ showSettings: !showSettings }); }, - + // Copy/paste actions pasteRegionsAtTrack: (trackId: string, position: number) => { // Use command pattern for region pasting with undo support const command = PasteRegionsCommand.fromClipboard(trackId, position); - + if (!command) { console.log('No regions to paste'); return; } - + try { KGCore.instance().executeCommand(command); - + // Update the store to trigger re-render const { tracks } = get(); const updatedTracks = [...tracks]; set({ tracks: updatedTracks }); - + console.log(`Executed PasteRegionsCommand: pasted regions to track ${trackId} using command pattern`); } catch (error) { console.error('Error pasting regions:', error); } }, - + pasteNotesToActiveRegion: (regionId: string, position: number) => { // Use command pattern for note pasting with undo support const command = PasteNotesCommand.fromClipboard(regionId, position); - + if (!command) { console.log('No notes to paste'); return; } - + try { KGCore.instance().executeCommand(command); - + // Update the store to trigger re-render const { tracks } = get(); const updatedTracks = [...tracks]; set({ tracks: updatedTracks }); - + console.log(`Executed PasteNotesCommand: pasted notes to region ${regionId} using command pattern`); } catch (error) { console.error('Error pasting notes:', error); } }, - + // Undo/redo actions undo: () => { const core = KGCore.instance(); @@ -1680,7 +1680,7 @@ export const useProjectStore = create((set, get) => { console.log('Undo completed'); } }, - + redo: () => { const core = KGCore.instance(); if (core.redo()) { @@ -1690,7 +1690,7 @@ export const useProjectStore = create((set, get) => { console.log('Redo completed'); } }, - + syncUndoRedoState: () => { const core = KGCore.instance(); set({ @@ -1700,13 +1700,13 @@ export const useProjectStore = create((set, get) => { redoDescription: core.getRedoDescription() }); }, - + // Centralized project state refresh method // Used by undo/redo and external operations (like XML tools) to sync UI with core model refreshProjectState: () => { const core = KGCore.instance(); const project = core.getCurrentProject(); - + // Force new array reference to trigger React re-renders set({ projectName: project.getName(), @@ -1729,7 +1729,7 @@ export const useProjectStore = create((set, get) => { actions.syncUndoRedoState(); actions.syncSelectionFromCore(); }, - + // Initialize store with configuration values initializeFromConfig: async () => { const configManager = ConfigManager.instance();
Position