feat: added track level event list tab; rename List Event to Event List.

This commit is contained in:
Xiaohan-Tian
2026-05-09 17:10:03 -07:00
parent 48a51f12ac
commit 894c3b116f
11 changed files with 601 additions and 601 deletions
+2 -2
View File
@@ -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.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**.
<div align="center"> <div align="center">
<img src="./public/snapshots/2026-05-08-automations.png" alt="K.G.Studio Logo" width="640" /> <img src="./public/snapshots/2026-05-08-automations.png" alt="K.G.Studio Logo" width="640" />
</div> </div>
@@ -325,7 +325,7 @@ Feature priorities might change.
- [X] Support MIDI control events (e.g. CC, pitch bend, etc.) - [X] Support MIDI control events (e.g. CC, pitch bend, etc.)
- [X] Support WAV audio tracks - [X] Support WAV audio tracks
- [X] Recording - [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`) - [X] Add support for OpenAI's open source models (`gpt-oss-20b` and `gpt-oss-120b`)
### Post 1.0 ### Post 1.0
+1 -1
View File
@@ -19,7 +19,7 @@ vi.mock('./components/MainContent', () => ({ default: () => null }));
vi.mock('./components/InstrumentSelection', () => ({ default: () => null })); vi.mock('./components/InstrumentSelection', () => ({ default: () => null }));
vi.mock('./components/ChatBox', () => ({ default: () => null })); vi.mock('./components/ChatBox', () => ({ default: () => null }));
vi.mock('./components/KGOnePanel', () => ({ 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('./components/settings', () => ({ SettingsPanel: () => null }));
vi.mock('./core/audio-interface/KGToneBuffersPool', () => ({ vi.mock('./core/audio-interface/KGToneBuffersPool', () => ({
KGToneBuffersPool: { KGToneBuffersPool: {
+7 -7
View File
@@ -11,7 +11,7 @@ import ChatBox from './components/ChatBox';
import { SettingsPanel } from './components/settings'; import { SettingsPanel } from './components/settings';
import LoadingOverlay from './components/common/LoadingOverlay'; import LoadingOverlay from './components/common/LoadingOverlay';
import KGOnePanel from './components/KGOnePanel'; import KGOnePanel from './components/KGOnePanel';
import ListEventPanel from './components/ListEventPanel'; import EventListPanel from './components/EventListPanel';
import { useEffect as useEffectReact, useState, useRef } from 'react'; import { useEffect as useEffectReact, useState, useRef } from 'react';
import { KGToneBuffersPool } from './core/audio-interface/KGToneBuffersPool'; import { KGToneBuffersPool } from './core/audio-interface/KGToneBuffersPool';
import { KGOfflineRenderer } from './core/audio-interface/KGOfflineRenderer'; import { KGOfflineRenderer } from './core/audio-interface/KGOfflineRenderer';
@@ -26,12 +26,12 @@ import { RESERVED_PROJECT_NAME } from './util/projectNameUtil';
function App() { function App() {
// Enable global keyboard handler for copy/paste and undo/redo // Enable global keyboard handler for copy/paste and undo/redo
useGlobalKeyboardHandler(); useGlobalKeyboardHandler();
// Use project store instead of local state for project name and tracks // Use project store instead of local state for project name and tracks
const { const {
refreshStatus, refreshStatus,
loadProject, showChatBox, showSettings, setShowSettings, initializeFromConfig, loadProject, showChatBox, showSettings, setShowSettings, initializeFromConfig,
showInstrumentSelection, showKGOnePanel, showListEventPanel showInstrumentSelection, showKGOnePanel, showEventListPanel
} = useProjectStore(); } = useProjectStore();
// Track if app has been initialized to prevent multiple initializations // Track if app has been initialized to prevent multiple initializations
@@ -144,12 +144,12 @@ function App() {
useEffect(() => { useEffect(() => {
// Initial refresh // Initial refresh
refreshStatus(); refreshStatus();
// Set up interval to refresh status every second // Set up interval to refresh status every second
const intervalId = setInterval(() => { const intervalId = setInterval(() => {
refreshStatus(); refreshStatus();
}, 1000); }, 1000);
// Clean up interval on unmount // Clean up interval on unmount
return () => clearInterval(intervalId); return () => clearInterval(intervalId);
}, [refreshStatus]); }, [refreshStatus]);
@@ -169,7 +169,7 @@ function App() {
</> </>
)} )}
<KGOnePanel isVisible={showKGOnePanel && !showSettings} /> <KGOnePanel isVisible={showKGOnePanel && !showSettings} />
<ListEventPanel isVisible={showListEventPanel && !showSettings} /> <EventListPanel isVisible={showEventListPanel && !showSettings} />
<ChatBox isVisible={showChatBox && !showSettings} /> <ChatBox isVisible={showChatBox && !showSettings} />
</div> </div>
@@ -264,7 +264,7 @@ const MigrationOverlayContainer: React.FC = () => {
useEffectReact(() => { useEffectReact(() => {
KGCore.instance().setMigrationStateChangeCallback(setIsMigrating); KGCore.instance().setMigrationStateChangeCallback(setIsMigrating);
return () => { return () => {
KGCore.instance().setMigrationStateChangeCallback(() => {}); KGCore.instance().setMigrationStateChangeCallback(() => { });
}; };
}, []); }, []);
@@ -1,4 +1,4 @@
.list-event-panel { .event-list-panel {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: var(--chat-box-width); width: var(--chat-box-width);
@@ -8,11 +8,11 @@
overflow: hidden; overflow: hidden;
} }
.list-event-panel.is-hidden { .event-list-panel.is-hidden {
display: none; display: none;
} }
.list-event-panel-header { .event-list-panel-header {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -23,14 +23,14 @@
flex-shrink: 0; flex-shrink: 0;
} }
.list-event-panel-header h3 { .event-list-panel-header h3 {
color: #e0e0e0; color: #e0e0e0;
font-size: 12px; font-size: 12px;
font-weight: bold; font-weight: bold;
margin: 0; margin: 0;
} }
.list-event-panel-body { .event-list-panel-body {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -39,20 +39,20 @@
gap: 12px; gap: 12px;
} }
.list-event-tabs { .event-list-tabs {
display: flex; display: flex;
gap: 4px; gap: 4px;
flex-shrink: 0; flex-shrink: 0;
} }
.list-event-scope-tabs { .event-list-scope-tabs {
display: flex; display: flex;
background-color: #2d2d2d; background-color: #2d2d2d;
border-bottom: 1px solid #3a3a3a; border-bottom: 1px solid #3a3a3a;
flex-shrink: 0; flex-shrink: 0;
} }
.list-event-scope-tab { .event-list-scope-tab {
flex: 1; flex: 1;
background: transparent; background: transparent;
color: #999; color: #999;
@@ -65,16 +65,16 @@
transition: color 0.15s, border-color 0.15s; transition: color 0.15s, border-color 0.15s;
} }
.list-event-scope-tab:hover { .event-list-scope-tab:hover {
color: #ccc; color: #ccc;
} }
.list-event-scope-tab.active { .event-list-scope-tab.active {
color: #e0e0e0; color: #e0e0e0;
border-bottom-color: #5a9fd4; border-bottom-color: #5a9fd4;
} }
.list-event-tab { .event-list-tab {
flex: 1; flex: 1;
background-color: #1e1e1e; background-color: #1e1e1e;
color: #999; color: #999;
@@ -88,16 +88,16 @@
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.list-event-tab:hover { .event-list-tab:hover {
color: #e0e0e0; color: #e0e0e0;
} }
.list-event-tab.active { .event-list-tab.active {
background-color: #5a9fd4; background-color: #5a9fd4;
color: #fff; color: #fff;
} }
.list-event-empty-state { .event-list-empty-state {
color: #888; color: #888;
font-size: 11px; font-size: 11px;
line-height: 1.5; line-height: 1.5;
@@ -107,7 +107,7 @@
border-radius: 6px; border-radius: 6px;
} }
.list-event-toolbar { .event-list-toolbar {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -115,23 +115,23 @@
flex-shrink: 0; flex-shrink: 0;
} }
.list-event-toolbar-group { .event-list-toolbar-group {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
min-width: 0; min-width: 0;
} }
.list-event-toolbar-group:first-child { .event-list-toolbar-group:first-child {
gap: 0; gap: 0;
} }
.list-event-toolbar-group-right { .event-list-toolbar-group-right {
margin-left: auto; margin-left: auto;
gap: 8px; gap: 8px;
} }
.list-event-add-button { .event-list-add-button {
width: 22px; width: 22px;
height: 22px; height: 22px;
border: 1px solid #444; border: 1px solid #444;
@@ -153,34 +153,34 @@
font-size: 11px; font-size: 11px;
} }
.list-event-add-button:hover { .event-list-add-button:hover {
background-color: #3b3b3b; background-color: #3b3b3b;
border-right: 0; border-right: 0;
} }
.list-event-dropdown-button, .event-list-dropdown-button,
.list-event-quant-button, .event-list-quant-button,
.list-event-type-button { .event-list-type-button {
font-size: 11px; font-size: 11px;
} }
.list-event-dropdown-button { .event-list-dropdown-button {
margin-left: 0; margin-left: 0;
} }
.list-event-quant-button { .event-list-quant-button {
min-width: 78px; min-width: 78px;
padding: 3px 5px; padding: 3px 5px;
} }
.list-event-type-button { .event-list-type-button {
min-width: 88px; min-width: 88px;
margin-left: 0; margin-left: 0;
border-top-left-radius: 0; border-top-left-radius: 0;
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
} }
.list-event-delete-button { .event-list-delete-button {
width: 22px; width: 22px;
height: 22px; height: 22px;
border: 1px solid #444; border: 1px solid #444;
@@ -197,17 +197,17 @@
font-size: 11px; font-size: 11px;
} }
.list-event-delete-button:hover:not(:disabled) { .event-list-delete-button:hover:not(:disabled) {
background-color: #464646; background-color: #464646;
border-color: #5a5a5a; border-color: #5a5a5a;
} }
.list-event-delete-button:disabled { .event-list-delete-button:disabled {
opacity: 0.45; opacity: 0.45;
cursor: default; cursor: default;
} }
.list-event-table-shell { .event-list-table-shell {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
overflow: auto; overflow: auto;
@@ -216,13 +216,13 @@
border-radius: 6px; border-radius: 6px;
} }
.list-event-table { .event-list-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
table-layout: fixed; table-layout: fixed;
} }
.list-event-table thead th { .event-list-table thead th {
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 1; z-index: 1;
@@ -238,25 +238,25 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.list-event-table tbody tr { .event-list-table tbody tr {
color: #e0e0e0; color: #e0e0e0;
cursor: default; cursor: default;
} }
.list-event-table tbody tr:nth-child(odd) { .event-list-table tbody tr:nth-child(odd) {
background-color: #282828; background-color: #282828;
} }
.list-event-table tbody tr:nth-child(even) { .event-list-table tbody tr:nth-child(even) {
background-color: #303030; background-color: #303030;
} }
.list-event-table tbody tr.selected { .event-list-table tbody tr.selected {
background-color: #5a9fd4; background-color: #5a9fd4;
color: #fff; color: #fff;
} }
.list-event-table td { .event-list-table td {
height: 20px; height: 20px;
padding: 2px 12px; padding: 2px 12px;
font-size: 11px; font-size: 11px;
@@ -266,7 +266,7 @@
max-width: 0; max-width: 0;
} }
.list-event-cell-input { .event-list-cell-input {
width: calc(100% + 8px); width: calc(100% + 8px);
height: 16px; height: 16px;
margin: 0 -4px; margin: 0 -4px;
@@ -278,4 +278,4 @@
font-size: 11px; font-size: 11px;
line-height: 16px; line-height: 16px;
outline: none; outline: none;
} }
@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest';
import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import ListEventPanel from './ListEventPanel'; import EventListPanel from './EventListPanel';
import { KGMidiControllerEvent } from '../core/midi/KGMidiControllerEvent'; import { KGMidiControllerEvent } from '../core/midi/KGMidiControllerEvent';
import { KGMidiNote } from '../core/midi/KGMidiNote'; import { KGMidiNote } from '../core/midi/KGMidiNote';
import { KGMidiPitchBend } from '../core/midi/KGMidiPitchBend'; import { KGMidiPitchBend } from '../core/midi/KGMidiPitchBend';
@@ -158,7 +158,7 @@ vi.mock('../core/KGCore', () => ({
}, },
})); }));
describe('ListEventPanel', () => { describe('EventListPanel', () => {
beforeEach(() => { beforeEach(() => {
selectedItems = [midiRegion]; selectedItems = [midiRegion];
midiRegion.select(); midiRegion.select();
@@ -198,7 +198,7 @@ describe('ListEventPanel', () => {
}); });
it('defaults to Region tab and preserves existing event rows', () => { it('defaults to Region tab and preserves existing event rows', () => {
render(<ListEventPanel isVisible={true} />); render(<EventListPanel isVisible={true} />);
expect(screen.getByRole('button', { name: 'Region' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Region' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Track' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Track' })).toBeInTheDocument();
@@ -207,7 +207,7 @@ describe('ListEventPanel', () => {
}); });
it('switches to Track tab and lists selected track regions', () => { it('switches to Track tab and lists selected track regions', () => {
render(<ListEventPanel isVisible={true} />); render(<EventListPanel isVisible={true} />);
fireEvent.click(screen.getByRole('button', { name: 'Track' })); fireEvent.click(screen.getByRole('button', { name: 'Track' }));
@@ -219,7 +219,7 @@ describe('ListEventPanel', () => {
}); });
it('toggles track filters independently', () => { it('toggles track filters independently', () => {
render(<ListEventPanel isVisible={true} />); render(<EventListPanel isVisible={true} />);
fireEvent.click(screen.getByRole('button', { name: 'Track' })); fireEvent.click(screen.getByRole('button', { name: 'Track' }));
fireEvent.click(screen.getByRole('button', { name: 'Regions' })); fireEvent.click(screen.getByRole('button', { name: 'Regions' }));
@@ -237,40 +237,40 @@ describe('ListEventPanel', () => {
it('shows track empty state when no track is selected', () => { it('shows track empty state when no track is selected', () => {
storeState.selectedTrackId = null; storeState.selectedTrackId = null;
render(<ListEventPanel isVisible={true} />); render(<EventListPanel isVisible={true} />);
fireEvent.click(screen.getByRole('button', { name: 'Track' })); fireEvent.click(screen.getByRole('button', { name: 'Track' }));
expect(screen.getByText('Please select a track to view regions and track automation.')).toBeInTheDocument(); expect(screen.getByText('Please select a track to view regions and track automation.')).toBeInTheDocument();
}); });
it('syncs Track Regions row selection to selectedRegionIds', () => { it('syncs Track Regions row selection to selectedRegionIds', () => {
const { rerender } = render(<ListEventPanel isVisible={true} />); const { rerender } = render(<EventListPanel isVisible={true} />);
fireEvent.click(screen.getByRole('button', { name: 'Track' })); fireEvent.click(screen.getByRole('button', { name: 'Track' }));
fireEvent.click(screen.getByText('Second Region').closest('tr')!); fireEvent.click(screen.getByText('Second Region').closest('tr')!);
rerender(<ListEventPanel isVisible={true} />); rerender(<EventListPanel isVisible={true} />);
expect(storeState.selectedRegionIds).toEqual(['region-2']); expect(storeState.selectedRegionIds).toEqual(['region-2']);
expect(screen.getByText('Second Region').closest('tr')).toHaveClass('selected'); expect(screen.getByText('Second Region').closest('tr')).toHaveClass('selected');
}); });
it('syncs Track Volume row selection to selectedTrackAutomationPointIds', () => { it('syncs Track Volume row selection to selectedTrackAutomationPointIds', () => {
const { rerender } = render(<ListEventPanel isVisible={true} />); const { rerender } = render(<EventListPanel isVisible={true} />);
fireEvent.click(screen.getByRole('button', { name: 'Track' })); fireEvent.click(screen.getByRole('button', { name: 'Track' }));
fireEvent.click(screen.getByText('-6.0dB').closest('tr')!); fireEvent.click(screen.getByText('-6.0dB').closest('tr')!);
rerender(<ListEventPanel isVisible={true} />); rerender(<EventListPanel isVisible={true} />);
expect(storeState.selectedTrackAutomationPointIds).toEqual(['vol-1']); expect(storeState.selectedTrackAutomationPointIds).toEqual(['vol-1']);
expect(screen.getByText('-6.0dB').closest('tr')).toHaveClass('selected'); expect(screen.getByText('-6.0dB').closest('tr')).toHaveClass('selected');
}); });
it('syncs Track Pan row selection to selectedTrackAutomationPointIds', () => { it('syncs Track Pan row selection to selectedTrackAutomationPointIds', () => {
const { rerender } = render(<ListEventPanel isVisible={true} />); const { rerender } = render(<EventListPanel isVisible={true} />);
fireEvent.click(screen.getByRole('button', { name: 'Track' })); fireEvent.click(screen.getByRole('button', { name: 'Track' }));
fireEvent.click(screen.getByText('-32').closest('tr')!); fireEvent.click(screen.getByText('-32').closest('tr')!);
rerender(<ListEventPanel isVisible={true} />); rerender(<EventListPanel isVisible={true} />);
expect(storeState.selectedTrackAutomationPointIds).toEqual(['pan-1']); expect(storeState.selectedTrackAutomationPointIds).toEqual(['pan-1']);
}); });
@@ -278,7 +278,7 @@ describe('ListEventPanel', () => {
it('hides MIDI Region add option for audio tracks', () => { it('hides MIDI Region add option for audio tracks', () => {
storeState.selectedTrackId = '2'; storeState.selectedTrackId = '2';
render(<ListEventPanel isVisible={true} />); render(<EventListPanel isVisible={true} />);
fireEvent.click(screen.getByRole('button', { name: 'Track' })); fireEvent.click(screen.getByRole('button', { name: 'Track' }));
fireEvent.click(screen.getAllByRole('button', { name: 'Volume' })[1]); 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 () => { it('creates a 1-bar MIDI region at the playhead from the Track tab', async () => {
render(<ListEventPanel isVisible={true} />); render(<EventListPanel isVisible={true} />);
fireEvent.click(screen.getByRole('button', { name: 'Track' })); fireEvent.click(screen.getByRole('button', { name: 'Track' }));
fireEvent.click(screen.getByTitle('Add MIDI region at playhead')); 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 () => { it('creates a volume automation point using the track base volume', async () => {
render(<ListEventPanel isVisible={true} />); render(<EventListPanel isVisible={true} />);
fireEvent.click(screen.getByRole('button', { name: 'Track' })); fireEvent.click(screen.getByRole('button', { name: 'Track' }));
fireEvent.click(screen.getByTitle('Add MIDI region at playhead')); fireEvent.click(screen.getByTitle('Add MIDI region at playhead'));
fireEvent.click(screen.getByRole('button', { name: 'MIDI Region' })); 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 () => { it('creates a pan automation point using the nearest earlier pan value or zero', async () => {
render(<ListEventPanel isVisible={true} />); render(<EventListPanel isVisible={true} />);
fireEvent.click(screen.getByRole('button', { name: 'Track' })); fireEvent.click(screen.getByRole('button', { name: 'Track' }));
fireEvent.click(screen.getByTitle('Add MIDI region at playhead')); fireEvent.click(screen.getByTitle('Add MIDI region at playhead'));
fireEvent.click(screen.getByRole('button', { name: 'MIDI Region' })); 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 () => { it('deletes selected track automation points from the Track tab', async () => {
const { rerender } = render(<ListEventPanel isVisible={true} />); const { rerender } = render(<EventListPanel isVisible={true} />);
fireEvent.click(screen.getByRole('button', { name: 'Track' })); fireEvent.click(screen.getByRole('button', { name: 'Track' }));
fireEvent.click(screen.getByText('-6.0dB').closest('tr')!); fireEvent.click(screen.getByText('-6.0dB').closest('tr')!);
rerender(<ListEventPanel isVisible={true} />); rerender(<EventListPanel isVisible={true} />);
fireEvent.click(screen.getByTitle('Delete visible selected rows')); fireEvent.click(screen.getByTitle('Delete visible selected rows'));
await waitFor(() => expect(midiTrack.getVolumeAutomation()).toHaveLength(0)); await waitFor(() => expect(midiTrack.getVolumeAutomation()).toHaveLength(0));
}); });
it('edits track region position and length inline', async () => { it('edits track region position and length inline', async () => {
render(<ListEventPanel isVisible={true} />); render(<EventListPanel isVisible={true} />);
fireEvent.click(screen.getByRole('button', { name: 'Track' })); fireEvent.click(screen.getByRole('button', { name: 'Track' }));
fireEvent.doubleClick(screen.getByText('4 1 0')); fireEvent.doubleClick(screen.getByText('4 1 0'));
@@ -359,7 +359,7 @@ describe('ListEventPanel', () => {
}); });
it('edits track automation position and value inline', async () => { it('edits track automation position and value inline', async () => {
render(<ListEventPanel isVisible={true} />); render(<EventListPanel isVisible={true} />);
fireEvent.click(screen.getByRole('button', { name: 'Track' })); fireEvent.click(screen.getByRole('button', { name: 'Track' }));
fireEvent.doubleClick(screen.getByText('1 3 0')); fireEvent.doubleClick(screen.getByText('1 3 0'));
@@ -1,19 +1,19 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import './ListEventPanel.css'; import './EventListPanel.css';
import { useProjectStore } from '../stores/projectStore'; import { useProjectStore } from '../stores/projectStore';
import { KGMidiRegion } from '../core/region/KGMidiRegion'; import { KGMidiRegion } from '../core/region/KGMidiRegion';
import { KGMidiTrack } from '../core/track/KGMidiTrack'; import { KGMidiTrack } from '../core/track/KGMidiTrack';
import { KGAudioTrack } from '../core/track/KGAudioTrack'; import { KGAudioTrack } from '../core/track/KGAudioTrack';
import RegionListEventTab from './list-event-panel/RegionListEventTab'; import RegionEventListTab from './event-list-panel/RegionEventListTab';
import TrackListEventTab from './list-event-panel/TrackListEventTab'; import TrackEventListTab from './event-list-panel/TrackEventListTab';
interface ListEventPanelProps { interface EventListPanelProps {
isVisible: boolean; isVisible: boolean;
} }
type ScopeTab = 'region' | 'track'; type ScopeTab = 'region' | 'track';
const ListEventPanel: React.FC<ListEventPanelProps> = ({ isVisible }) => { const EventListPanel: React.FC<EventListPanelProps> = ({ isVisible }) => {
const { tracks, activeRegionId, selectedRegionIds, selectedTrackId } = useProjectStore(); const { tracks, activeRegionId, selectedRegionIds, selectedTrackId } = useProjectStore();
const [scopeTab, setScopeTab] = useState<ScopeTab>('region'); const [scopeTab, setScopeTab] = useState<ScopeTab>('region');
@@ -43,21 +43,21 @@ const ListEventPanel: React.FC<ListEventPanelProps> = ({ isVisible }) => {
} }
return ( return (
<div className={`list-event-panel${isVisible ? '' : ' is-hidden'}`}> <div className={`event-list-panel${isVisible ? '' : ' is-hidden'}`}>
<div className="list-event-panel-header"> <div className="event-list-panel-header">
<h3>List Event</h3> <h3>Event List</h3>
</div> </div>
<div className="list-event-scope-tabs" role="tablist" aria-label="List event scopes"> <div className="event-list-scope-tabs" role="tablist" aria-label="Event list scopes">
<button <button
className={`list-event-scope-tab${scopeTab === 'region' ? ' active' : ''}`} className={`event-list-scope-tab${scopeTab === 'region' ? ' active' : ''}`}
type="button" type="button"
onClick={() => setScopeTab('region')} onClick={() => setScopeTab('region')}
> >
Region Region
</button> </button>
<button <button
className={`list-event-scope-tab${scopeTab === 'track' ? ' active' : ''}`} className={`event-list-scope-tab${scopeTab === 'track' ? ' active' : ''}`}
type="button" type="button"
onClick={() => setScopeTab('track')} onClick={() => setScopeTab('track')}
> >
@@ -65,15 +65,15 @@ const ListEventPanel: React.FC<ListEventPanelProps> = ({ isVisible }) => {
</button> </button>
</div> </div>
<div className="list-event-panel-body"> <div className="event-list-panel-body">
{scopeTab === 'region' ? ( {scopeTab === 'region' ? (
<RegionListEventTab activeMidiRegion={activeMidiRegion} parentTrack={parentTrack} /> <RegionEventListTab activeMidiRegion={activeMidiRegion} parentTrack={parentTrack} />
) : ( ) : (
<TrackListEventTab selectedTrack={selectedTrack} /> <TrackEventListTab selectedTrack={selectedTrack} />
)} )}
</div> </div>
</div> </div>
); );
}; };
export default ListEventPanel; export default EventListPanel;
+257 -257
View File
@@ -49,7 +49,7 @@ const Toolbar: React.FC = () => {
barWidthMultiplier, setBarWidthMultiplier, barWidthMultiplier, setBarWidthMultiplier,
isLooping, toggleLoop, isLooping, toggleLoop,
canUndo, canRedo, undoDescription, redoDescription, undo, redo, 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, isRecording, startRecording, stopRecording,
// Piano roll state/actions // Piano roll state/actions
showPianoRoll, setShowPianoRoll, activeRegionId, setActiveRegionId, showPianoRoll, setShowPianoRoll, activeRegionId, setActiveRegionId,
@@ -66,10 +66,10 @@ const Toolbar: React.FC = () => {
// State for key signature dropdown // State for key signature dropdown
const [showKeySignatureDropdown, setShowKeySignatureDropdown] = React.useState(false); const [showKeySignatureDropdown, setShowKeySignatureDropdown] = React.useState(false);
// State for export dropdown // State for export dropdown
const [showExportDropdown, setShowExportDropdown] = React.useState(false); const [showExportDropdown, setShowExportDropdown] = React.useState(false);
// State for import modal // State for import modal
const [showImportModal, setShowImportModal] = React.useState(false); const [showImportModal, setShowImportModal] = React.useState(false);
@@ -80,7 +80,7 @@ const Toolbar: React.FC = () => {
// State for open project modal // State for open project modal
const [showOpenProject, setShowOpenProject] = React.useState(false); const [showOpenProject, setShowOpenProject] = React.useState(false);
const [isOpeningProject, setIsOpeningProject] = React.useState(false); const [isOpeningProject, setIsOpeningProject] = React.useState(false);
// Close zoom slider on click outside // Close zoom slider on click outside
React.useEffect(() => { React.useEffect(() => {
if (!showZoomSlider) return; if (!showZoomSlider) return;
@@ -95,7 +95,7 @@ const Toolbar: React.FC = () => {
// Key signature options // Key signature options
const keySignatureOptions = Object.keys(KEY_SIGNATURE_MAP) as KeySignature[]; const keySignatureOptions = Object.keys(KEY_SIGNATURE_MAP) as KeySignature[];
// Export options // Export options
const exportOptions = ["Export to KGStudio file", "Export to MIDI file", "Export to WAV", "Export to MP3"]; const exportOptions = ["Export to KGStudio file", "Export to MIDI file", "Export to WAV", "Export to MP3"];
const lastSelectedRegionId = selectedRegionIds[selectedRegionIds.length - 1] ?? null; const lastSelectedRegionId = selectedRegionIds[selectedRegionIds.length - 1] ?? null;
@@ -252,7 +252,7 @@ const Toolbar: React.FC = () => {
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log("user selected export option:", exportType); console.log("user selected export option:", exportType);
} }
if (exportType === "Export to KGStudio file") { if (exportType === "Export to KGStudio file") {
handleExportKGStudio(); handleExportKGStudio();
} else if (exportType === "Export to MIDI file") { } else if (exportType === "Export to MIDI file") {
@@ -262,7 +262,7 @@ const Toolbar: React.FC = () => {
} else if (exportType === "Export to MP3") { } else if (exportType === "Export to MP3") {
handleBounceToMp3(); handleBounceToMp3();
} }
setShowExportDropdown(false); setShowExportDropdown(false);
}; };
@@ -306,37 +306,37 @@ const Toolbar: React.FC = () => {
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log("exporting to MIDI file"); console.log("exporting to MIDI file");
} }
try { try {
// Get the current project from KGCore // Get the current project from KGCore
const currentProject = KGCore.instance().getCurrentProject(); const currentProject = KGCore.instance().getCurrentProject();
// Convert project to MIDI format // Convert project to MIDI format
const midiData = convertProjectToMidi(currentProject); const midiData = convertProjectToMidi(currentProject);
// Create a downloadable blob // Create a downloadable blob
const blob = new Blob([midiData.buffer as ArrayBuffer], { type: 'audio/midi' }); const blob = new Blob([midiData.buffer as ArrayBuffer], { type: 'audio/midi' });
// Create a temporary download link // Create a temporary download link
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const link = document.createElement('a'); const link = document.createElement('a');
link.href = url; link.href = url;
link.download = `${projectName}.mid`; link.download = `${projectName}.mid`;
// Trigger download // Trigger download
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
// Cleanup // Cleanup
document.body.removeChild(link); document.body.removeChild(link);
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
setStatus(`Project "${projectName}" exported as MIDI file`); setStatus(`Project "${projectName}" exported as MIDI file`);
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log("MIDI export completed successfully"); console.log("MIDI export completed successfully");
} }
} catch (error) { } catch (error) {
console.error("Error exporting MIDI:", error); console.error("Error exporting MIDI:", error);
setStatus(`Error exporting MIDI: ${error}`); setStatus(`Error exporting MIDI: ${error}`);
@@ -387,10 +387,10 @@ const Toolbar: React.FC = () => {
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log("file selected for import:", file.name); console.log("file selected for import:", file.name);
} }
// Get file extension // Get file extension
const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase(); const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
try { try {
if (fileExtension === '.kgstudio') { if (fileExtension === '.kgstudio') {
// Handle KGStudio bundle import // Handle KGStudio bundle import
@@ -404,7 +404,7 @@ const Toolbar: React.FC = () => {
} else { } else {
throw new Error(`Unsupported file type: ${fileExtension}`); throw new Error(`Unsupported file type: ${fileExtension}`);
} }
} catch (error) { } catch (error) {
console.error("Error importing file:", error); console.error("Error importing file:", error);
setStatus(`Failed to import file: ${error}`); setStatus(`Failed to import file: ${error}`);
@@ -480,35 +480,35 @@ const Toolbar: React.FC = () => {
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log("Starting MIDI file import:", file.name); console.log("Starting MIDI file import:", file.name);
} }
// Show loading status // Show loading status
setStatus(`Importing MIDI file "${file.name}"...`); setStatus(`Importing MIDI file "${file.name}"...`);
// Read the MIDI file as binary data // Read the MIDI file as binary data
const arrayBuffer = await file.arrayBuffer(); const arrayBuffer = await file.arrayBuffer();
const midiData = new Uint8Array(arrayBuffer); const midiData = new Uint8Array(arrayBuffer);
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log("MIDI file read successfully, size:", midiData.length, "bytes"); console.log("MIDI file read successfully, size:", midiData.length, "bytes");
} }
// Get current project to append MIDI tracks to it // Get current project to append MIDI tracks to it
const currentProject = KGCore.instance().getCurrentProject(); const currentProject = KGCore.instance().getCurrentProject();
// Convert MIDI data and append to current project // Convert MIDI data and append to current project
const updatedProject = convertMidiToProject(midiData, currentProject); const updatedProject = convertMidiToProject(midiData, currentProject);
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log("MIDI conversion successful, tracks added to existing project"); console.log("MIDI conversion successful, tracks added to existing project");
} }
// Load the updated project using common loading logic // Load the updated project using common loading logic
await loadProjectFromData(updatedProject, `MIDI file "${file.name}"`); await loadProjectFromData(updatedProject, `MIDI file "${file.name}"`);
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log("MIDI file imported successfully:", file.name); console.log("MIDI file imported successfully:", file.name);
} }
} catch (error) { } catch (error) {
console.error("Error importing MIDI file:", error); console.error("Error importing MIDI file:", error);
const errorMessage = error instanceof Error ? error.message : String(error); const errorMessage = error instanceof Error ? error.message : String(error);
@@ -585,33 +585,33 @@ const Toolbar: React.FC = () => {
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log("BPM clicked, current BPM:", bpm); console.log("BPM clicked, current BPM:", bpm);
} }
const newBpmStr = await showPrompt(`Enter new BPM (${TIME_CONSTANTS.MIN_BPM}-${TIME_CONSTANTS.MAX_BPM}):`, bpm.toString()); const newBpmStr = await showPrompt(`Enter new BPM (${TIME_CONSTANTS.MIN_BPM}-${TIME_CONSTANTS.MAX_BPM}):`, bpm.toString());
// Check if user cancelled // Check if user cancelled
if (newBpmStr === null) { if (newBpmStr === null) {
return; return;
} }
// Validate input // Validate input
const newBpm = parseInt(newBpmStr.trim()); const newBpm = parseInt(newBpmStr.trim());
// Check if it's a valid number // Check if it's a valid number
if (isNaN(newBpm)) { if (isNaN(newBpm)) {
await showAlert("Invalid input. Please enter a valid number."); await showAlert("Invalid input. Please enter a valid number.");
return; return;
} }
// Check if it's within valid range // Check if it's within valid range
if (newBpm <= TIME_CONSTANTS.MIN_BPM || newBpm >= TIME_CONSTANTS.MAX_BPM) { 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}.`); await showAlert(`Invalid BPM. Please enter a value between ${TIME_CONSTANTS.MIN_BPM} and ${TIME_CONSTANTS.MAX_BPM}.`);
return; return;
} }
// Update BPM // Update BPM
setBpm(newBpm); setBpm(newBpm);
setStatus(`BPM changed to ${newBpm}`); setStatus(`BPM changed to ${newBpm}`);
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log(`BPM updated from ${bpm} to ${newBpm}`); console.log(`BPM updated from ${bpm} to ${newBpm}`);
} }
@@ -643,7 +643,7 @@ const Toolbar: React.FC = () => {
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log("Key signature changed from", keySignature, "to", newKeySignature); console.log("Key signature changed from", keySignature, "to", newKeySignature);
} }
setKeySignature(newKeySignature as KeySignature); setKeySignature(newKeySignature as KeySignature);
setStatus(`Key signature changed to ${newKeySignature}`); setStatus(`Key signature changed to ${newKeySignature}`);
setShowKeySignatureDropdown(false); setShowKeySignatureDropdown(false);
@@ -673,9 +673,9 @@ const Toolbar: React.FC = () => {
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log("Copy button clicked"); console.log("Copy button clicked");
} }
const copied = handleCopyOperation(); const copied = handleCopyOperation();
if (copied) { if (copied) {
setStatus("Items copied to clipboard"); setStatus("Items copied to clipboard");
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
@@ -694,9 +694,9 @@ const Toolbar: React.FC = () => {
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log("Paste button clicked"); console.log("Paste button clicked");
} }
const pasted = handlePasteOperation(); const pasted = handlePasteOperation();
if (pasted) { if (pasted) {
setStatus("Items pasted from clipboard"); setStatus("Items pasted from clipboard");
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
@@ -715,9 +715,9 @@ const Toolbar: React.FC = () => {
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log("Delete button clicked"); console.log("Delete button clicked");
} }
const deleted = regionDeleteManager.deleteSelectedRegions(); const deleted = regionDeleteManager.deleteSelectedRegions();
if (deleted) { if (deleted) {
setStatus("Selected regions deleted"); setStatus("Selected regions deleted");
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
@@ -875,16 +875,16 @@ const Toolbar: React.FC = () => {
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log("Undo button clicked"); console.log("Undo button clicked");
} }
if (!canUndo) { if (!canUndo) {
await showAlert("Nothing to undo"); await showAlert("Nothing to undo");
return; return;
} }
undo(); undo();
const description = undoDescription || "action"; const description = undoDescription || "action";
setStatus(`Undid: ${description}`); setStatus(`Undid: ${description}`);
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log(`Undo successful: ${description}`); console.log(`Undo successful: ${description}`);
} }
@@ -895,16 +895,16 @@ const Toolbar: React.FC = () => {
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log("Redo button clicked"); console.log("Redo button clicked");
} }
if (!canRedo) { if (!canRedo) {
await showAlert("Nothing to redo"); await showAlert("Nothing to redo");
return; return;
} }
redo(); redo();
const description = redoDescription || "action"; const description = redoDescription || "action";
setStatus(`Redid: ${description}`); setStatus(`Redid: ${description}`);
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log(`Redo successful: ${description}`); console.log(`Redo successful: ${description}`);
} }
@@ -915,7 +915,7 @@ const Toolbar: React.FC = () => {
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log("Chat button clicked"); console.log("Chat button clicked");
} }
toggleChatBox(); toggleChatBox();
setStatus("Chat toggled"); setStatus("Chat toggled");
}; };
@@ -925,7 +925,7 @@ const Toolbar: React.FC = () => {
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log("Settings button clicked"); console.log("Settings button clicked");
} }
toggleSettings(); toggleSettings();
setStatus("Settings toggled"); setStatus("Settings toggled");
}; };
@@ -940,11 +940,11 @@ const Toolbar: React.FC = () => {
toggleKGOnePanel(); toggleKGOnePanel();
}; };
const handleListEventClick = () => { const handleEventListClick = () => {
if (DEBUG_MODE.TOOLBAR) { 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 // Handle Piano button click: open piano roll if closed, targeting active or selected region
@@ -1039,226 +1039,226 @@ const Toolbar: React.FC = () => {
<div className="logo-container"> <div className="logo-container">
<img src={`${import.meta.env.BASE_URL}logo.png`} alt="DAW Logo" className="logo" /> <img src={`${import.meta.env.BASE_URL}logo.png`} alt="DAW Logo" className="logo" />
</div> </div>
<div <div
className="project-name" className="project-name"
onClick={handleProjectNameClick} onClick={handleProjectNameClick}
> >
{projectName} {projectName}
</div> </div>
</div> </div>
<div className="toolbar-center"> <div className="toolbar-center">
<button title="New" onClick={handleNewProject}><FaPlus /></button> <button title="New" onClick={handleNewProject}><FaPlus /></button>
<button title="Load" onClick={handleLoadProject}><FaFolderOpen /></button> <button title="Load" onClick={handleLoadProject}><FaFolderOpen /></button>
<button title="Save" onClick={handleSaveProject}><FaSave /></button> <button title="Save" onClick={handleSaveProject}><FaSave /></button>
<div style={{ position: 'relative', display: 'inline-block' }}> <div style={{ position: 'relative', display: 'inline-block' }}>
<button <button
title="Export" title="Export"
onClick={() => setShowExportDropdown(!showExportDropdown)} onClick={() => setShowExportDropdown(!showExportDropdown)}
style={{ display: 'flex', alignItems: 'center', gap: '4px' }} style={{ display: 'flex', alignItems: 'center', gap: '4px' }}
>
<FaDownload />
</button>
<div style={{ position: 'absolute', top: '100%', left: 0, zIndex: 10000 }}>
<KGDropdown
options={exportOptions}
value={exportOptions[0]}
onChange={handleExportProject}
label="Export"
hideButton={true}
isOpen={showExportDropdown}
onToggle={setShowExportDropdown}
className="export-dropdown"
/>
</div>
</div>
<button title="Import" onClick={handleImportProject}><FaUpload /></button>
<button
title="Settings"
className={`tool-button ${showSettings ? 'active' : ''}`}
onClick={handleSettingsClick}
>
<FaCog />
</button>
<div className="toolbar-separator"></div>
<button title="Undo" onClick={handleUndoClick}><FaUndo /></button>
<button title="Redo" onClick={handleRedoClick}><FaRedo /></button>
<div className="toolbar-separator"></div>
<button
title="Select"
className={`tool-button ${activeMainTool === 'pointer' ? 'active' : ''}`}
onClick={() => handleMainToolSelect('pointer')}
>
<FaMousePointer />
</button>
<button
title="Pencil"
className={`tool-button ${activeMainTool === 'pencil' ? 'active' : ''}`}
onClick={() => handleMainToolSelect('pencil')}
>
<FaPencil />
</button>
<button
title="Split Region at Playhead"
onClick={handleSplitClick}
>
<FaCut />
</button>
<button
title="Merge Selected MIDI Regions"
onClick={handleMergeClick}
>
<FaCompress />
</button>
<button
title="Snap to Grid"
className={`tool-button ${isSnapping ? 'active' : ''}`}
onClick={handleSnappingToggle}
>
<FaMagnet />
</button>
<div className="toolbar-separator"></div>
<button title="Copy" onClick={handleCopyClick}><FaCopy /></button>
<button title="Paste" onClick={handlePasteClick}><FaPaste /></button>
<button title="Delete" onClick={handleDeleteClick}><FaTrash /></button>
<div className="toolbar-separator"></div>
<button title="Back to beginning" className="button-back-to-beginning" onClick={handleBackToBeginningClick}><FaStepBackward /></button>
{!isPlaying ? (
<button title="Play" className="button-play" onClick={handlePlayClick} disabled={isPreparingPlayback}><FaPlay /></button>
) : (
<button title="Pause" className="button-pause" onClick={handlePauseClick}><FaPause /></button>
)}
<button
title={isRecording ? "Stop Recording" : "Record"}
className={`tool-button record-button ${isRecording ? 'active' : ''}`}
onClick={handleRecordClick}
>
<FaCircle />
</button>
<button
title="Loop"
className={`tool-button ${isLooping ? 'active' : ''}`}
onClick={handleLoopToggle}
>
<FaSync />
</button>
<div className="toolbar-separator"></div>
<button
title="Metronome"
className={`tool-button ${isMetronomeEnabled ? 'active' : ''}`}
onClick={handleMetronomeToggle}
>
<MetronomeIcon />
</button>
<button title="Piano" onClick={handlePianoButtonClick}><PianoIcon /></button>
{/* <button title="Record"><FaCircle className="record-btn" /></button>
<button title="Metronome">🎵</button> */}
</div>
<div className="toolbar-right">
<div className="transport-control">
<div className="transport-item" style={{ position: 'relative' }} ref={zoomSliderRef}>
<span
className='current-zoom'
onClick={() => setShowZoomSlider(!showZoomSlider)}
style={{ cursor: 'pointer' }}
> >
{barWidthMultiplier}x <FaDownload />
</span> </button>
{showZoomSlider && (
<div className="zoom-slider-popup">
<input
type="range"
min="1"
max="8"
step="1"
value={barWidthMultiplier}
onChange={(e) => setBarWidthMultiplier(parseInt(e.target.value))}
/>
<span className="zoom-slider-label">{barWidthMultiplier}x</span>
</div>
)}
</div>
<div className="transport-item">
<span className='current-time' onClick={handleCurrentTimeClick} style={{ cursor: 'pointer' }}>{currentTime}</span>
</div>
<div className="transport-item">
<span className='current-bpm' onClick={handleBpmClick} style={{ cursor: 'pointer' }}>{bpm}</span>
</div>
<div className="transport-item">
<span className='current-time-signature' onClick={handleTimeSignatureClick} style={{ cursor: 'pointer' }}>{timeSignature.numerator + "/" + timeSignature.denominator}</span>
</div>
<div className="transport-item" style={{ position: 'relative' }}>
<span
className='current-key-signature'
onClick={() => setShowKeySignatureDropdown(!showKeySignatureDropdown)}
style={{ cursor: 'pointer' }}
>
{keySignature}
</span>
<div style={{ position: 'absolute', top: '100%', left: 0, zIndex: 10000 }}> <div style={{ position: 'absolute', top: '100%', left: 0, zIndex: 10000 }}>
<KGDropdown <KGDropdown
options={keySignatureOptions} options={exportOptions}
value={keySignature} value={exportOptions[0]}
onChange={handleKeySignatureChange} onChange={handleExportProject}
label="Key Signature" label="Export"
hideButton={true} hideButton={true}
isOpen={showKeySignatureDropdown} isOpen={showExportDropdown}
onToggle={setShowKeySignatureDropdown} onToggle={setShowExportDropdown}
className="key-signature-dropdown" className="export-dropdown"
/> />
</div> </div>
</div> </div>
<button title="Import" onClick={handleImportProject}><FaUpload /></button>
<button
title="Settings"
className={`tool-button ${showSettings ? 'active' : ''}`}
onClick={handleSettingsClick}
>
<FaCog />
</button>
<div className="toolbar-separator"></div>
<button title="Undo" onClick={handleUndoClick}><FaUndo /></button>
<button title="Redo" onClick={handleRedoClick}><FaRedo /></button>
<div className="toolbar-separator"></div>
<button
title="Select"
className={`tool-button ${activeMainTool === 'pointer' ? 'active' : ''}`}
onClick={() => handleMainToolSelect('pointer')}
>
<FaMousePointer />
</button>
<button
title="Pencil"
className={`tool-button ${activeMainTool === 'pencil' ? 'active' : ''}`}
onClick={() => handleMainToolSelect('pencil')}
>
<FaPencil />
</button>
<button
title="Split Region at Playhead"
onClick={handleSplitClick}
>
<FaCut />
</button>
<button
title="Merge Selected MIDI Regions"
onClick={handleMergeClick}
>
<FaCompress />
</button>
<button
title="Snap to Grid"
className={`tool-button ${isSnapping ? 'active' : ''}`}
onClick={handleSnappingToggle}
>
<FaMagnet />
</button>
<div className="toolbar-separator"></div>
<button title="Copy" onClick={handleCopyClick}><FaCopy /></button>
<button title="Paste" onClick={handlePasteClick}><FaPaste /></button>
<button title="Delete" onClick={handleDeleteClick}><FaTrash /></button>
<div className="toolbar-separator"></div>
<button title="Back to beginning" className="button-back-to-beginning" onClick={handleBackToBeginningClick}><FaStepBackward /></button>
{!isPlaying ? (
<button title="Play" className="button-play" onClick={handlePlayClick} disabled={isPreparingPlayback}><FaPlay /></button>
) : (
<button title="Pause" className="button-pause" onClick={handlePauseClick}><FaPause /></button>
)}
<button
title={isRecording ? "Stop Recording" : "Record"}
className={`tool-button record-button ${isRecording ? 'active' : ''}`}
onClick={handleRecordClick}
>
<FaCircle />
</button>
<button
title="Loop"
className={`tool-button ${isLooping ? 'active' : ''}`}
onClick={handleLoopToggle}
>
<FaSync />
</button>
<div className="toolbar-separator"></div>
<button
title="Metronome"
className={`tool-button ${isMetronomeEnabled ? 'active' : ''}`}
onClick={handleMetronomeToggle}
>
<MetronomeIcon />
</button>
<button title="Piano" onClick={handlePianoButtonClick}><PianoIcon /></button>
{/* <button title="Record"><FaCircle className="record-btn" /></button>
<button title="Metronome">🎵</button> */}
</div>
<div className="toolbar-right">
<div className="transport-control">
<div className="transport-item" style={{ position: 'relative' }} ref={zoomSliderRef}>
<span
className='current-zoom'
onClick={() => setShowZoomSlider(!showZoomSlider)}
style={{ cursor: 'pointer' }}
>
{barWidthMultiplier}x
</span>
{showZoomSlider && (
<div className="zoom-slider-popup">
<input
type="range"
min="1"
max="8"
step="1"
value={barWidthMultiplier}
onChange={(e) => setBarWidthMultiplier(parseInt(e.target.value))}
/>
<span className="zoom-slider-label">{barWidthMultiplier}x</span>
</div>
)}
</div>
<div className="transport-item">
<span className='current-time' onClick={handleCurrentTimeClick} style={{ cursor: 'pointer' }}>{currentTime}</span>
</div>
<div className="transport-item">
<span className='current-bpm' onClick={handleBpmClick} style={{ cursor: 'pointer' }}>{bpm}</span>
</div>
<div className="transport-item">
<span className='current-time-signature' onClick={handleTimeSignatureClick} style={{ cursor: 'pointer' }}>{timeSignature.numerator + "/" + timeSignature.denominator}</span>
</div>
<div className="transport-item" style={{ position: 'relative' }}>
<span
className='current-key-signature'
onClick={() => setShowKeySignatureDropdown(!showKeySignatureDropdown)}
style={{ cursor: 'pointer' }}
>
{keySignature}
</span>
<div style={{ position: 'absolute', top: '100%', left: 0, zIndex: 10000 }}>
<KGDropdown
options={keySignatureOptions}
value={keySignature}
onChange={handleKeySignatureChange}
label="Key Signature"
hideButton={true}
isOpen={showKeySignatureDropdown}
onToggle={setShowKeySignatureDropdown}
className="key-signature-dropdown"
/>
</div>
</div>
</div>
<button
title={isKGOneEnabled ? 'K.G.One Music Generator' : 'K.G.One integration is disabled — enable it in Settings'}
onClick={handleKGOneClick}
disabled={!isKGOneEnabled}
className={showKGOnePanel ? 'active' : ''}
style={!isKGOneEnabled ? { opacity: 0.4, cursor: 'not-allowed' } : undefined}
>
<FaWandMagicSparkles />
</button>
<button
title="Chat"
onClick={handleChatClick}
className={showChatBox ? 'active' : ''}
>
<FaComments />
</button>
<button
title="Event List Editor"
onClick={handleEventListClick}
className={showEventListPanel ? 'active' : ''}
>
<FaListUl />
</button>
</div> </div>
<button
title={isKGOneEnabled ? 'K.G.One Music Generator' : 'K.G.One integration is disabled — enable it in Settings'}
onClick={handleKGOneClick}
disabled={!isKGOneEnabled}
className={showKGOnePanel ? 'active' : ''}
style={!isKGOneEnabled ? { opacity: 0.4, cursor: 'not-allowed' } : undefined}
>
<FaWandMagicSparkles />
</button>
<button
title="Chat"
onClick={handleChatClick}
className={showChatBox ? 'active' : ''}
>
<FaComments />
</button>
<button
title="List Event Editor"
onClick={handleListEventClick}
className={showListEventPanel ? 'active' : ''}
>
<FaListUl />
</button>
</div> </div>
</div>
<FileImportModal
isVisible={showImportModal}
onClose={() => setShowImportModal(false)}
onFileImport={handleFileImport}
acceptedTypes={['.kgstudio', '.json', '.mid', '.midi']}
title="Import Project"
description="Drag and drop your project file here"
/>
<LoadingOverlay <FileImportModal
visible={isOpeningProject} isVisible={showImportModal}
message="Opening project..." onClose={() => setShowImportModal(false)}
/> onFileImport={handleFileImport}
acceptedTypes={['.kgstudio', '.json', '.mid', '.midi']}
{showOpenProject && ( title="Import Project"
<OpenProjectModal description="Drag and drop your project file here"
onClose={() => setShowOpenProject(false)}
onConfirmOpenProject={handleConfirmOpenProject}
onOpenProject={handleOpenProjectSelect}
currentProjectName={savedProjectName}
onCreateNewProject={createNewProject}
/> />
)}
<LoadingOverlay
visible={isOpeningProject}
message="Opening project..."
/>
{showOpenProject && (
<OpenProjectModal
onClose={() => setShowOpenProject(false)}
onConfirmOpenProject={handleConfirmOpenProject}
onOpenProject={handleOpenProjectSelect}
currentProjectName={savedProjectName}
onCreateNewProject={createNewProject}
/>
)}
</> </>
); );
}; };
@@ -36,7 +36,7 @@ import { UpdateNotePropertiesCommand } from '../../core/commands/note/UpdateNote
import { UpdatePitchBendPropertiesCommand } from '../../core/commands/note/UpdatePitchBendPropertiesCommand'; import { UpdatePitchBendPropertiesCommand } from '../../core/commands/note/UpdatePitchBendPropertiesCommand';
import { showAlert } from '../../util/dialogUtil'; import { showAlert } from '../../util/dialogUtil';
interface RegionListEventTabProps { interface RegionEventListTabProps {
activeMidiRegion: KGMidiRegion | null; activeMidiRegion: KGMidiRegion | null;
parentTrack: KGMidiTrack | null; parentTrack: KGMidiTrack | null;
} }
@@ -181,7 +181,7 @@ const parseControllerValueDeltaInput = (raw: string): { delta: number } | { erro
return { delta: parseInt(trimmed, 10) }; return { delta: parseInt(trimmed, 10) };
}; };
const RegionListEventTab: React.FC<RegionListEventTabProps> = ({ activeMidiRegion, parentTrack }) => { const RegionEventListTab: React.FC<RegionEventListTabProps> = ({ activeMidiRegion, parentTrack }) => {
const { const {
selectedNoteIds, selectedNoteIds,
selectedPitchBendIds, selectedPitchBendIds,
@@ -941,22 +941,22 @@ const RegionListEventTab: React.FC<RegionListEventTabProps> = ({ activeMidiRegio
return ( return (
<> <>
<div className="list-event-tabs" role="tablist" aria-label="Region event filters"> <div className="event-list-tabs" role="tablist" aria-label="Region event filters">
<button className={`list-event-tab${showNotes ? ' active' : ''}`} type="button" onClick={() => setShowNotes(value => !value)}>Notes</button> <button className={`event-list-tab${showNotes ? ' active' : ''}`} type="button" onClick={() => setShowNotes(value => !value)}>Notes</button>
<button className={`list-event-tab${showPitchBends ? ' active' : ''}`} type="button" onClick={() => setShowPitchBends(value => !value)}>Pitch Bends</button> <button className={`event-list-tab${showPitchBends ? ' active' : ''}`} type="button" onClick={() => setShowPitchBends(value => !value)}>Pitch Bends</button>
<button className={`list-event-tab${showControllers ? ' active' : ''}`} type="button" onClick={() => setShowControllers(value => !value)}>Controller</button> <button className={`event-list-tab${showControllers ? ' active' : ''}`} type="button" onClick={() => setShowControllers(value => !value)}>Controller</button>
</div> </div>
{!activeMidiRegion ? ( {!activeMidiRegion ? (
<div className="list-event-empty-state"> <div className="event-list-empty-state">
Please select a MIDI region, or open one in the Piano Roll, to view its event list. Please select a MIDI region, or open one in the Piano Roll, to view its event list.
</div> </div>
) : ( ) : (
<> <>
<div className="list-event-toolbar"> <div className="event-list-toolbar">
<div className="list-event-toolbar-group"> <div className="event-list-toolbar-group">
<button <button
className="list-event-add-button" className="event-list-add-button"
title={addEventType === 'note' ? 'Add note at playhead' : addEventType === 'pitch-bend' ? 'Add pitch bend at playhead' : 'Add controller event at playhead'} title={addEventType === 'note' ? 'Add note at playhead' : addEventType === 'pitch-bend' ? 'Add pitch bend at playhead' : 'Add controller event at playhead'}
type="button" type="button"
onClick={handleAddEvent} onClick={handleAddEvent}
@@ -968,12 +968,12 @@ const RegionListEventTab: React.FC<RegionListEventTabProps> = ({ activeMidiRegio
value={addEventType} value={addEventType}
onChange={(value) => setAddEventType(value as AddEventType)} onChange={(value) => setAddEventType(value as AddEventType)}
label="Note" label="Note"
buttonClassName="list-event-type-button" buttonClassName="event-list-type-button"
showValueAsLabel showValueAsLabel
/> />
</div> </div>
<div className="list-event-toolbar-group list-event-toolbar-group-right"> <div className="event-list-toolbar-group event-list-toolbar-group-right">
<KGDropdown <KGDropdown
options={KGPianoRollState.QUANT_POS_OPTIONS} options={KGPianoRollState.QUANT_POS_OPTIONS}
value={quantPosition} value={quantPosition}
@@ -982,7 +982,7 @@ const RegionListEventTab: React.FC<RegionListEventTabProps> = ({ activeMidiRegio
quantizeSelectedNotes(value); quantizeSelectedNotes(value);
}} }}
label="Qua. Pos." label="Qua. Pos."
buttonClassName="list-event-quant-button" buttonClassName="event-list-quant-button"
/> />
<KGDropdown <KGDropdown
options={KGPianoRollState.QUANT_LEN_OPTIONS} options={KGPianoRollState.QUANT_LEN_OPTIONS}
@@ -992,10 +992,10 @@ const RegionListEventTab: React.FC<RegionListEventTabProps> = ({ activeMidiRegio
quantizeSelectedNoteLengths(value); quantizeSelectedNoteLengths(value);
}} }}
label="Qua. Len." label="Qua. Len."
buttonClassName="list-event-quant-button" buttonClassName="event-list-quant-button"
/> />
<button <button
className="list-event-delete-button" className="event-list-delete-button"
title="Delete visible selected rows" title="Delete visible selected rows"
type="button" type="button"
onClick={handleDeleteSelectedRows} onClick={handleDeleteSelectedRows}
@@ -1006,8 +1006,8 @@ const RegionListEventTab: React.FC<RegionListEventTabProps> = ({ activeMidiRegio
</div> </div>
</div> </div>
<div className="list-event-table-shell" onMouseDown={handleTableBackgroundMouseDown}> <div className="event-list-table-shell" onMouseDown={handleTableBackgroundMouseDown}>
<table className="list-event-table"> <table className="event-list-table">
<thead> <thead>
<tr> <tr>
<th>Position</th> <th>Position</th>
@@ -1056,7 +1056,7 @@ const RegionListEventTab: React.FC<RegionListEventTabProps> = ({ activeMidiRegio
{isEditingPosition ? ( {isEditingPosition ? (
<input <input
ref={editInputRef} ref={editInputRef}
className="list-event-cell-input" className="event-list-cell-input"
value={editingCell.value} value={editingCell.value}
onChange={(event) => setEditingCell({ ...editingCell, value: event.target.value })} onChange={(event) => setEditingCell({ ...editingCell, value: event.target.value })}
onBlur={handleEditInputBlur} onBlur={handleEditInputBlur}
@@ -1075,7 +1075,7 @@ const RegionListEventTab: React.FC<RegionListEventTabProps> = ({ activeMidiRegio
{isEditingNum ? ( {isEditingNum ? (
<input <input
ref={editInputRef} ref={editInputRef}
className="list-event-cell-input" className="event-list-cell-input"
value={editingCell.value} value={editingCell.value}
onChange={(event) => setEditingCell({ ...editingCell, value: event.target.value })} onChange={(event) => setEditingCell({ ...editingCell, value: event.target.value })}
onBlur={handleEditInputBlur} onBlur={handleEditInputBlur}
@@ -1092,7 +1092,7 @@ const RegionListEventTab: React.FC<RegionListEventTabProps> = ({ activeMidiRegio
{isEditingVal ? ( {isEditingVal ? (
<input <input
ref={editInputRef} ref={editInputRef}
className="list-event-cell-input" className="event-list-cell-input"
value={editingCell.value} value={editingCell.value}
onChange={(event) => setEditingCell({ ...editingCell, value: event.target.value })} onChange={(event) => setEditingCell({ ...editingCell, value: event.target.value })}
onBlur={handleEditInputBlur} onBlur={handleEditInputBlur}
@@ -1110,7 +1110,7 @@ const RegionListEventTab: React.FC<RegionListEventTabProps> = ({ activeMidiRegio
{isEditingLength ? ( {isEditingLength ? (
<input <input
ref={editInputRef} ref={editInputRef}
className="list-event-cell-input" className="event-list-cell-input"
value={editingCell.value} value={editingCell.value}
onChange={(event) => setEditingCell({ ...editingCell, value: event.target.value })} onChange={(event) => setEditingCell({ ...editingCell, value: event.target.value })}
onBlur={handleEditInputBlur} onBlur={handleEditInputBlur}
@@ -1132,4 +1132,4 @@ const RegionListEventTab: React.FC<RegionListEventTabProps> = ({ activeMidiRegio
); );
}; };
export default RegionListEventTab; export default RegionEventListTab;
@@ -30,7 +30,7 @@ import { isModifierKeyPressed } from '../../util/osUtil';
import { showAlert } from '../../util/dialogUtil'; import { showAlert } from '../../util/dialogUtil';
import { AUDIO_INTERFACE_CONSTANTS } from '../../constants/coreConstants'; import { AUDIO_INTERFACE_CONSTANTS } from '../../constants/coreConstants';
interface TrackListEventTabProps { interface TrackEventListTabProps {
selectedTrack: KGMidiTrack | KGAudioTrack | null; selectedTrack: KGMidiTrack | KGAudioTrack | null;
} }
@@ -118,7 +118,7 @@ const findPreviousPanValue = (points: KGTrackAutomationPoint[], beat: number): n
return previousPoint?.getValue() ?? 0; return previousPoint?.getValue() ?? 0;
}; };
const TrackListEventTab: React.FC<TrackListEventTabProps> = ({ selectedTrack }) => { const TrackEventListTab: React.FC<TrackEventListTabProps> = ({ selectedTrack }) => {
const { const {
tracks, tracks,
playheadPosition, playheadPosition,
@@ -659,22 +659,22 @@ const TrackListEventTab: React.FC<TrackListEventTabProps> = ({ selectedTrack })
return ( return (
<> <>
<div className="list-event-tabs" role="tablist" aria-label="Track list modes"> <div className="event-list-tabs" role="tablist" aria-label="Track list modes">
<button className={`list-event-tab${showRegions ? ' active' : ''}`} aria-pressed={showRegions} type="button" onClick={() => setShowRegions(value => !value)}>Regions</button> <button className={`event-list-tab${showRegions ? ' active' : ''}`} aria-pressed={showRegions} type="button" onClick={() => setShowRegions(value => !value)}>Regions</button>
<button className={`list-event-tab${showVolume ? ' active' : ''}`} aria-pressed={showVolume} type="button" onClick={() => setShowVolume(value => !value)}>Volume</button> <button className={`event-list-tab${showVolume ? ' active' : ''}`} aria-pressed={showVolume} type="button" onClick={() => setShowVolume(value => !value)}>Volume</button>
<button className={`list-event-tab${showPan ? ' active' : ''}`} aria-pressed={showPan} type="button" onClick={() => setShowPan(value => !value)}>Pan</button> <button className={`event-list-tab${showPan ? ' active' : ''}`} aria-pressed={showPan} type="button" onClick={() => setShowPan(value => !value)}>Pan</button>
</div> </div>
{!liveSelectedTrack ? ( {!liveSelectedTrack ? (
<div className="list-event-empty-state"> <div className="event-list-empty-state">
Please select a track to view regions and track automation. Please select a track to view regions and track automation.
</div> </div>
) : ( ) : (
<> <>
<div className="list-event-toolbar"> <div className="event-list-toolbar">
<div className="list-event-toolbar-group"> <div className="event-list-toolbar-group">
<button <button
className="list-event-add-button" className="event-list-add-button"
title={addTrackItemType === 'midi-region' ? 'Add MIDI region at playhead' : addTrackItemType === 'volume' ? 'Add volume automation point at playhead' : 'Add pan automation point at playhead'} title={addTrackItemType === 'midi-region' ? 'Add MIDI region at playhead' : addTrackItemType === 'volume' ? 'Add volume automation point at playhead' : 'Add pan automation point at playhead'}
type="button" type="button"
onClick={handleAddTrackItem} onClick={handleAddTrackItem}
@@ -686,14 +686,14 @@ const TrackListEventTab: React.FC<TrackListEventTabProps> = ({ selectedTrack })
value={addTrackItemType} value={addTrackItemType}
onChange={(value) => setAddTrackItemType(value as AddTrackItemType)} onChange={(value) => setAddTrackItemType(value as AddTrackItemType)}
label="Add" label="Add"
buttonClassName="list-event-type-button" buttonClassName="event-list-type-button"
showValueAsLabel showValueAsLabel
/> />
</div> </div>
<div className="list-event-toolbar-group list-event-toolbar-group-right"> <div className="event-list-toolbar-group event-list-toolbar-group-right">
<button <button
className="list-event-delete-button" className="event-list-delete-button"
title="Delete visible selected rows" title="Delete visible selected rows"
type="button" type="button"
onClick={handleDeleteSelectedRows} onClick={handleDeleteSelectedRows}
@@ -704,8 +704,8 @@ const TrackListEventTab: React.FC<TrackListEventTabProps> = ({ selectedTrack })
</div> </div>
</div> </div>
<div className="list-event-table-shell" onMouseDown={handleTableBackgroundMouseDown}> <div className="event-list-table-shell" onMouseDown={handleTableBackgroundMouseDown}>
<table className="list-event-table"> <table className="event-list-table">
<thead> <thead>
<tr> <tr>
<th>Position</th> <th>Position</th>
@@ -738,7 +738,7 @@ const TrackListEventTab: React.FC<TrackListEventTabProps> = ({ selectedTrack })
{isEditingPosition ? ( {isEditingPosition ? (
<input <input
ref={editInputRef} ref={editInputRef}
className="list-event-cell-input" className="event-list-cell-input"
value={editingCell.value} value={editingCell.value}
onChange={(event) => setEditingCell({ ...editingCell, value: event.target.value })} onChange={(event) => setEditingCell({ ...editingCell, value: event.target.value })}
onBlur={handleEditInputBlur} onBlur={handleEditInputBlur}
@@ -757,7 +757,7 @@ const TrackListEventTab: React.FC<TrackListEventTabProps> = ({ selectedTrack })
{isEditingVal ? ( {isEditingVal ? (
<input <input
ref={editInputRef} ref={editInputRef}
className="list-event-cell-input" className="event-list-cell-input"
value={editingCell.value} value={editingCell.value}
onChange={(event) => setEditingCell({ ...editingCell, value: event.target.value })} onChange={(event) => setEditingCell({ ...editingCell, value: event.target.value })}
onBlur={handleEditInputBlur} onBlur={handleEditInputBlur}
@@ -775,7 +775,7 @@ const TrackListEventTab: React.FC<TrackListEventTabProps> = ({ selectedTrack })
{isEditingLength ? ( {isEditingLength ? (
<input <input
ref={editInputRef} ref={editInputRef}
className="list-event-cell-input" className="event-list-cell-input"
value={editingCell.value} value={editingCell.value}
onChange={(event) => setEditingCell({ ...editingCell, value: event.target.value })} onChange={(event) => setEditingCell({ ...editingCell, value: event.target.value })}
onBlur={handleEditInputBlur} onBlur={handleEditInputBlur}
@@ -797,4 +797,4 @@ const TrackListEventTab: React.FC<TrackListEventTabProps> = ({ selectedTrack })
); );
}; };
export default TrackListEventTab; export default TrackEventListTab;
+112 -112
View File
@@ -47,8 +47,8 @@ const PianoRoll: React.FC<PianoRollProps> = ({
}) => { }) => {
const isSpectrogram = mode === 'spectrogram'; const isSpectrogram = mode === 'spectrogram';
const isHybrid = mode === 'hybrid'; 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 // Tool state for piano roll
const [activeTool, setActiveTool] = useState<'pointer' | 'pencil'>('pointer'); const [activeTool, setActiveTool] = useState<'pointer' | 'pencil'>('pointer');
@@ -62,7 +62,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
const [pianoRollZoom, setPianoRollZoom] = useState<number>(1); const [pianoRollZoom, setPianoRollZoom] = useState<number>(1);
const [automationEnabled, setAutomationEnabled] = useState(false); const [automationEnabled, setAutomationEnabled] = useState(false);
const [automationType, setAutomationType] = useState<PianoRollAutomationType>('pitch-bend'); const [automationType, setAutomationType] = useState<PianoRollAutomationType>('pitch-bend');
// Quantization state // Quantization state
const [quantPosition, setQuantPosition] = useState<string>('1/8'); const [quantPosition, setQuantPosition] = useState<string>('1/8');
const [quantLength, setQuantLength] = useState<string>('1/8'); const [quantLength, setQuantLength] = useState<string>('1/8');
@@ -75,7 +75,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
// Piano roll state with temporary initial values // Piano roll state with temporary initial values
const [position, setPosition] = useState(initialPosition || { x: 0, y: 0 }); const [position, setPosition] = useState(initialPosition || { x: 0, y: 0 });
// Blink effect state for toolbar button feedback // Blink effect state for toolbar button feedback
const [blinkButton, setBlinkButton] = useState<string | null>(null); const [blinkButton, setBlinkButton] = useState<string | null>(null);
const [size, setSize] = useState(initialSize || { width: 800, height: PIANO_ROLL_CONSTANTS.PIANO_ROLL_HEIGHT }); const [size, setSize] = useState(initialSize || { width: 800, height: PIANO_ROLL_CONSTANTS.PIANO_ROLL_HEIGHT });
@@ -102,7 +102,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
// Ref for storing the setNoteUpdateCounter function // Ref for storing the setNoteUpdateCounter function
const triggerNoteUpdateRef = useRef<React.Dispatch<React.SetStateAction<number>> | null>(null); const triggerNoteUpdateRef = useRef<React.Dispatch<React.SetStateAction<number>> | null>(null);
// Ref for storing the deleteSelectedNotes function // Ref for storing the deleteSelectedNotes function
const deleteSelectedNotesRef = useRef<(() => boolean) | null>(null); const deleteSelectedNotesRef = useRef<(() => boolean) | null>(null);
@@ -133,32 +133,32 @@ const PianoRoll: React.FC<PianoRollProps> = ({
y: window.innerHeight - statusBarHeight - pianoRollHeight y: window.innerHeight - statusBarHeight - pianoRollHeight
}; };
}; };
const calculateInitialSize = () => { const calculateInitialSize = () => {
const rootStyles = getComputedStyle(document.documentElement); const rootStyles = getComputedStyle(document.documentElement);
const chatBoxWidthStr = rootStyles.getPropertyValue('--chat-box-width') || '350px'; const chatBoxWidthStr = rootStyles.getPropertyValue('--chat-box-width') || '350px';
const instrumentPanelWidthStr = rootStyles.getPropertyValue('--instrument-selection-width') || '300px'; const instrumentPanelWidthStr = rootStyles.getPropertyValue('--instrument-selection-width') || '300px';
const chatBoxWidth = parseInt(chatBoxWidthStr, 10) || 350; const chatBoxWidth = parseInt(chatBoxWidthStr, 10) || 350;
const instrumentPanelWidth = parseInt(instrumentPanelWidthStr, 10) || 300; const instrumentPanelWidth = parseInt(instrumentPanelWidthStr, 10) || 300;
let availableWidth = window.innerWidth; let availableWidth = window.innerWidth;
if (showChatBox || showKGOnePanel || showListEventPanel) availableWidth -= chatBoxWidth; if (showChatBox || showKGOnePanel || showEventListPanel) availableWidth -= chatBoxWidth;
if (showInstrumentSelection) availableWidth -= instrumentPanelWidth; if (showInstrumentSelection) availableWidth -= instrumentPanelWidth;
// Ensure a sensible minimum starting width // Ensure a sensible minimum starting width
const clampedWidth = Math.max(400, availableWidth); const clampedWidth = Math.max(400, availableWidth);
return { return {
width: clampedWidth, width: clampedWidth,
height: PIANO_ROLL_CONSTANTS.PIANO_ROLL_HEIGHT height: PIANO_ROLL_CONSTANTS.PIANO_ROLL_HEIGHT
}; };
}; };
// Set position and size only if not provided as props // Set position and size only if not provided as props
if (!initialPosition) { if (!initialPosition) {
setPosition(calculateInitialPosition()); setPosition(calculateInitialPosition());
} }
if (!initialSize) { if (!initialSize) {
setSize(calculateInitialSize()); setSize(calculateInitialSize());
} }
@@ -195,17 +195,17 @@ const PianoRoll: React.FC<PianoRollProps> = ({
// Sync local state with KGPianoRollState on mount // Sync local state with KGPianoRollState on mount
useEffect(() => { useEffect(() => {
const pianoRollState = KGPianoRollState.instance(); const pianoRollState = KGPianoRollState.instance();
// Sync snapping state // Sync snapping state
const currentSnap = pianoRollState.getCurrentSnap(); const currentSnap = pianoRollState.getCurrentSnap();
setSnapping(currentSnap); setSnapping(currentSnap);
// Sync tool state // Sync tool state
const currentTool = pianoRollState.getActiveTool() as 'pointer' | 'pencil'; const currentTool = pianoRollState.getActiveTool() as 'pointer' | 'pencil';
setActiveTool(currentTool); setActiveTool(currentTool);
setAutomationEnabled(pianoRollState.getAutomationViewEnabled()); setAutomationEnabled(pianoRollState.getAutomationViewEnabled());
setAutomationType(pianoRollState.getCurrentAutomationType() as PianoRollAutomationType); setAutomationType(pianoRollState.getCurrentAutomationType() as PianoRollAutomationType);
if (DEBUG_MODE.PIANO_ROLL) { if (DEBUG_MODE.PIANO_ROLL) {
console.log(`Synced piano roll state on mount - snap: ${currentSnap}, tool: ${currentTool}`); console.log(`Synced piano roll state on mount - snap: ${currentSnap}, tool: ${currentTool}`);
} }
@@ -257,10 +257,10 @@ const PianoRoll: React.FC<PianoRollProps> = ({
onClose(); onClose();
} }
}; };
// Add event listener // Add event listener
window.addEventListener('keydown', handleKeyDown); window.addEventListener('keydown', handleKeyDown);
// Remove event listener on cleanup // Remove event listener on cleanup
return () => { return () => {
window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('keydown', handleKeyDown);
@@ -284,13 +284,13 @@ const PianoRoll: React.FC<PianoRollProps> = ({
e.preventDefault(); e.preventDefault();
} }
}; };
useEffect(() => { useEffect(() => {
const handleMouseMove = (e: MouseEvent) => { const handleMouseMove = (e: MouseEvent) => {
if (isDragging) { if (isDragging) {
// Set the flag to true as soon as any movement happens // Set the flag to true as soon as any movement happens
wasDraggingRef.current = true; wasDraggingRef.current = true;
setPosition({ setPosition({
x: e.clientX - dragOffset.x, x: e.clientX - dragOffset.x,
y: e.clientY - dragOffset.y y: e.clientY - dragOffset.y
@@ -302,25 +302,25 @@ const PianoRoll: React.FC<PianoRollProps> = ({
}); });
} }
}; };
const handleMouseUp = () => { const handleMouseUp = () => {
setIsDragging(false); setIsDragging(false);
setIsResizing(false); setIsResizing(false);
// We keep wasDraggingRef.current as is - it will be used in handleTitleClick // We keep wasDraggingRef.current as is - it will be used in handleTitleClick
// and reset on the next mousedown // and reset on the next mousedown
}; };
if (isDragging || isResizing) { if (isDragging || isResizing) {
document.addEventListener('mousemove', handleMouseMove as unknown as EventListener); document.addEventListener('mousemove', handleMouseMove as unknown as EventListener);
document.addEventListener('mouseup', handleMouseUp); document.addEventListener('mouseup', handleMouseUp);
} }
return () => { return () => {
document.removeEventListener('mousemove', handleMouseMove as unknown as EventListener); document.removeEventListener('mousemove', handleMouseMove as unknown as EventListener);
document.removeEventListener('mouseup', handleMouseUp); document.removeEventListener('mouseup', handleMouseUp);
}; };
}, [isDragging, isResizing, dragOffset, position]); }, [isDragging, isResizing, dragOffset, position]);
// Handle title click to rename the region // Handle title click to rename the region
const handleTitleClick = async () => { const handleTitleClick = async () => {
// If we were just dragging, don't show the rename dialog // If we were just dragging, don't show the rename dialog
@@ -330,28 +330,28 @@ const PianoRoll: React.FC<PianoRollProps> = ({
} }
return; return;
} }
if (!activeRegion) return; if (!activeRegion) return;
// Show a prompt to get the new name // Show a prompt to get the new name
const newName = await showPrompt("Enter a new name for the region:", activeRegion.getName()); 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 the user clicked Cancel or entered an empty string, do nothing
if (!newName || newName.trim() === '' || newName === activeRegion.getName()) return; if (!newName || newName.trim() === '' || newName === activeRegion.getName()) return;
// Use command pattern to update the region name with undo support // Use command pattern to update the region name with undo support
try { try {
const command = new UpdateRegionCommand(activeRegion.getId(), { name: newName.trim() }); const command = new UpdateRegionCommand(activeRegion.getId(), { name: newName.trim() });
KGCore.instance().executeCommand(command); KGCore.instance().executeCommand(command);
if (DEBUG_MODE.PIANO_ROLL) { if (DEBUG_MODE.PIANO_ROLL) {
console.log(`Executed UpdateRegionCommand: renamed region ${activeRegion.getId()} to "${newName}" using command pattern`); console.log(`Executed UpdateRegionCommand: renamed region ${activeRegion.getId()} to "${newName}" using command pattern`);
} }
// Update the store to trigger re-render // Update the store to trigger re-render
const updatedTracks = [...tracks]; const updatedTracks = [...tracks];
useProjectStore.setState({ tracks: updatedTracks }); useProjectStore.setState({ tracks: updatedTracks });
} catch (error) { } catch (error) {
console.error('Error renaming region:', error); console.error('Error renaming region:', error);
await showAlert('Failed to rename region. Please try again.'); await showAlert('Failed to rename region. Please try again.');
@@ -366,7 +366,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
console.log(`Selected tool: ${tool}`); console.log(`Selected tool: ${tool}`);
} }
}; };
// Handle snapping selection // Handle snapping selection
const handleSnappingSelect = useCallback((value: string) => { const handleSnappingSelect = useCallback((value: string) => {
setSnapping(value); setSnapping(value);
@@ -448,166 +448,166 @@ const PianoRoll: React.FC<PianoRollProps> = ({
const handleSetDeleteNotesTrigger = (deleteFn: () => boolean) => { const handleSetDeleteNotesTrigger = (deleteFn: () => boolean) => {
deleteSelectedNotesRef.current = deleteFn; deleteSelectedNotesRef.current = deleteFn;
}; };
// Quantize selected notes based on the selected quantization value // Quantize selected notes based on the selected quantization value
const quantizeSelectedNotes = useCallback((quantValue: string) => { const quantizeSelectedNotes = useCallback((quantValue: string) => {
if (!activeRegion) return; if (!activeRegion) return;
// Get the KGCore instance // Get the KGCore instance
const core = KGCore.instance(); const core = KGCore.instance();
// Get all selected notes // Get all selected notes
const selectedItems = core.getSelectedItems(); const selectedItems = core.getSelectedItems();
const selectedNotes = selectedItems.filter(item => const selectedNotes = selectedItems.filter(item =>
item instanceof KGMidiNote && item instanceof KGMidiNote &&
activeRegion.getNotes().some(note => note.getId() === item.getId()) activeRegion.getNotes().some(note => note.getId() === item.getId())
) as KGMidiNote[]; ) as KGMidiNote[];
if (selectedNotes.length === 0) { if (selectedNotes.length === 0) {
if (DEBUG_MODE.PIANO_ROLL) { if (DEBUG_MODE.PIANO_ROLL) {
console.log('No notes selected for quantization'); console.log('No notes selected for quantization');
} }
return; return;
} }
if (DEBUG_MODE.PIANO_ROLL) { if (DEBUG_MODE.PIANO_ROLL) {
console.log(`Quantizing ${selectedNotes.length} selected notes with value: ${quantValue}`); console.log(`Quantizing ${selectedNotes.length} selected notes with value: ${quantValue}`);
} }
// Parse the quantization value (e.g., "1/4", "1/8", "1/16", "1/32") // Parse the quantization value (e.g., "1/4", "1/8", "1/16", "1/32")
const denominator = parseInt(quantValue.split('/')[1]); const denominator = parseInt(quantValue.split('/')[1]);
if (isNaN(denominator)) { if (isNaN(denominator)) {
console.error(`Invalid quantization value: ${quantValue}`); console.error(`Invalid quantization value: ${quantValue}`);
return; return;
} }
// Calculate the quantization step in beats // Calculate the quantization step in beats
// In a 4/4 time signature, a quarter note (1/4) is 1 beat // 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 // In a 6/8 time signature, an eighth note (1/8) is 1 beat
const { numerator, denominator: timeSigDenominator } = timeSignature; const { numerator, denominator: timeSigDenominator } = timeSignature;
// Calculate beats per whole note based on time signature // Calculate beats per whole note based on time signature
// In 4/4, a whole note is 4 beats // 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) // In 6/8, a whole note is 6 beats (because each beat is an eighth note)
const beatsPerWholeNote = numerator * (4 / timeSigDenominator); const beatsPerWholeNote = numerator * (4 / timeSigDenominator);
// Calculate the quantization step in beats // Calculate the quantization step in beats
// quantizationStep should ALWAYS be 4 / denominator regardless of time signature // quantizationStep should ALWAYS be 4 / denominator regardless of time signature
const quantizationStep = 4 / denominator; const quantizationStep = 4 / denominator;
if (DEBUG_MODE.PIANO_ROLL) { if (DEBUG_MODE.PIANO_ROLL) {
console.log(`Time signature: ${numerator}/${timeSigDenominator}`); console.log(`Time signature: ${numerator}/${timeSigDenominator}`);
console.log(`Beats per whole note: ${beatsPerWholeNote}`); console.log(`Beats per whole note: ${beatsPerWholeNote}`);
console.log(`Quantization step: ${quantizationStep} beats`); console.log(`Quantization step: ${quantizationStep} beats`);
} }
// Apply quantization to each selected note // Apply quantization to each selected note
selectedNotes.forEach(note => { selectedNotes.forEach(note => {
// Get the current start beat // Get the current start beat
const currentStartBeat = note.getStartBeat(); const currentStartBeat = note.getStartBeat();
// Calculate the quantized start beat // Calculate the quantized start beat
const quantizedStartBeat = Math.round(currentStartBeat / quantizationStep) * quantizationStep; const quantizedStartBeat = Math.round(currentStartBeat / quantizationStep) * quantizationStep;
// Calculate the duration of the note // Calculate the duration of the note
const duration = note.getEndBeat() - currentStartBeat; const duration = note.getEndBeat() - currentStartBeat;
// Set the new start beat and maintain the duration // Set the new start beat and maintain the duration
note.setStartBeat(quantizedStartBeat); note.setStartBeat(quantizedStartBeat);
note.setEndBeat(quantizedStartBeat + duration); note.setEndBeat(quantizedStartBeat + duration);
if (DEBUG_MODE.PIANO_ROLL) { if (DEBUG_MODE.PIANO_ROLL) {
console.log(`Quantized note ${note.getId()}: ${currentStartBeat} -> ${quantizedStartBeat}`); console.log(`Quantized note ${note.getId()}: ${currentStartBeat} -> ${quantizedStartBeat}`);
} }
}); });
// Find the track that contains this region and update it // Find the track that contains this region and update it
const track = tracks.find(t => t.getId().toString() === activeRegion.getTrackId()); const track = tracks.find(t => t.getId().toString() === activeRegion.getTrackId());
if (track) { if (track) {
updateTrack(track); updateTrack(track);
} }
// Trigger a re-render by incrementing the note update counter // Trigger a re-render by incrementing the note update counter
if (triggerNoteUpdateRef.current) { if (triggerNoteUpdateRef.current) {
triggerNoteUpdateRef.current(prev => prev + 1); triggerNoteUpdateRef.current(prev => prev + 1);
if (DEBUG_MODE.PIANO_ROLL) { if (DEBUG_MODE.PIANO_ROLL) {
console.log('Triggered note update to re-render quantized notes'); console.log('Triggered note update to re-render quantized notes');
} }
} }
}, [activeRegion, timeSignature, updateTrack, tracks]); }, [activeRegion, timeSignature, updateTrack, tracks]);
// Quantize selected notes length based on the selected quantization value // Quantize selected notes length based on the selected quantization value
const quantizeNoteLength = useCallback((quantValue: string) => { const quantizeNoteLength = useCallback((quantValue: string) => {
if (!activeRegion) return; if (!activeRegion) return;
// Get the KGCore instance // Get the KGCore instance
const core = KGCore.instance(); const core = KGCore.instance();
// Get all selected notes // Get all selected notes
const selectedItems = core.getSelectedItems(); const selectedItems = core.getSelectedItems();
const selectedNotes = selectedItems.filter(item => const selectedNotes = selectedItems.filter(item =>
item instanceof KGMidiNote && item instanceof KGMidiNote &&
activeRegion.getNotes().some(note => note.getId() === item.getId()) activeRegion.getNotes().some(note => note.getId() === item.getId())
) as KGMidiNote[]; ) as KGMidiNote[];
if (selectedNotes.length === 0) { if (selectedNotes.length === 0) {
if (DEBUG_MODE.PIANO_ROLL) { if (DEBUG_MODE.PIANO_ROLL) {
console.log('No notes selected for length quantization'); console.log('No notes selected for length quantization');
} }
return; return;
} }
if (DEBUG_MODE.PIANO_ROLL) { if (DEBUG_MODE.PIANO_ROLL) {
console.log(`Quantizing length of ${selectedNotes.length} selected notes with value: ${quantValue}`); 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") // Parse the quantization value (e.g., "1/1", "1/2", "1/4", "1/8", "1/16", "1/32")
const denominator = parseInt(quantValue.split('/')[1]); const denominator = parseInt(quantValue.split('/')[1]);
if (isNaN(denominator)) { if (isNaN(denominator)) {
console.error(`Invalid quantization value: ${quantValue}`); console.error(`Invalid quantization value: ${quantValue}`);
return; return;
} }
// Calculate the quantization step in beats // Calculate the quantization step in beats
// In a 4/4 time signature, a quarter note (1/4) is 1 beat // 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 // In a 6/8 time signature, an eighth note (1/8) is 1 beat
const { numerator, denominator: timeSigDenominator } = timeSignature; const { numerator, denominator: timeSigDenominator } = timeSignature;
// Calculate beats per whole note based on time signature // Calculate beats per whole note based on time signature
// In 4/4, a whole note is 4 beats // 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) // In 6/8, a whole note is 6 beats (because each beat is an eighth note)
const beatsPerWholeNote = numerator * (4 / timeSigDenominator); const beatsPerWholeNote = numerator * (4 / timeSigDenominator);
// Calculate the quantization step in beats // Calculate the quantization step in beats
// quantizationStep should ALWAYS be 4 / denominator regardless of time signature // quantizationStep should ALWAYS be 4 / denominator regardless of time signature
const quantizationStep = 4 / denominator; const quantizationStep = 4 / denominator;
if (DEBUG_MODE.PIANO_ROLL) { if (DEBUG_MODE.PIANO_ROLL) {
console.log(`Time signature: ${numerator}/${timeSigDenominator}`); console.log(`Time signature: ${numerator}/${timeSigDenominator}`);
console.log(`Beats per whole note: ${beatsPerWholeNote}`); console.log(`Beats per whole note: ${beatsPerWholeNote}`);
console.log(`Length quantization step: ${quantizationStep} beats`); console.log(`Length quantization step: ${quantizationStep} beats`);
} }
// Apply quantization to each selected note // Apply quantization to each selected note
selectedNotes.forEach(note => { selectedNotes.forEach(note => {
// Get the current start and end beats // Get the current start and end beats
const startBeat = note.getStartBeat(); const startBeat = note.getStartBeat();
const currentEndBeat = note.getEndBeat(); const currentEndBeat = note.getEndBeat();
// Calculate the current duration // Calculate the current duration
const currentDuration = currentEndBeat - startBeat; const currentDuration = currentEndBeat - startBeat;
// Calculate the quantized duration // Calculate the quantized duration
// If the current duration is less than the quantization step, // If the current duration is less than the quantization step,
// extend it to match the quantization step exactly // extend it to match the quantization step exactly
// Otherwise, round to the nearest multiple of quantizationStep // Otherwise, round to the nearest multiple of quantizationStep
let quantizedDuration; let quantizedDuration;
if (currentDuration < quantizationStep) { if (currentDuration < quantizationStep) {
// For notes shorter than the quantization step, extend to exactly one step // For notes shorter than the quantization step, extend to exactly one step
quantizedDuration = quantizationStep; quantizedDuration = quantizationStep;
if (DEBUG_MODE.PIANO_ROLL) { if (DEBUG_MODE.PIANO_ROLL) {
console.log(`Extending short note ${note.getId()} from ${currentDuration} to ${quantizedDuration}`); console.log(`Extending short note ${note.getId()} from ${currentDuration} to ${quantizedDuration}`);
} }
@@ -615,28 +615,28 @@ const PianoRoll: React.FC<PianoRollProps> = ({
// For longer notes, round to nearest multiple of quantizationStep // For longer notes, round to nearest multiple of quantizationStep
quantizedDuration = Math.round(currentDuration / quantizationStep) * quantizationStep; quantizedDuration = Math.round(currentDuration / quantizationStep) * quantizationStep;
} }
// Ensure minimum note length // Ensure minimum note length
quantizedDuration = Math.max(PIANO_ROLL_CONSTANTS.MIN_NOTE_LENGTH, quantizedDuration); quantizedDuration = Math.max(PIANO_ROLL_CONSTANTS.MIN_NOTE_LENGTH, quantizedDuration);
// Set the new end beat while maintaining the start beat // Set the new end beat while maintaining the start beat
note.setEndBeat(startBeat + quantizedDuration); note.setEndBeat(startBeat + quantizedDuration);
if (DEBUG_MODE.PIANO_ROLL) { if (DEBUG_MODE.PIANO_ROLL) {
console.log(`Quantized note length ${note.getId()}: ${currentDuration} -> ${quantizedDuration}`); console.log(`Quantized note length ${note.getId()}: ${currentDuration} -> ${quantizedDuration}`);
} }
}); });
// Find the track that contains this region and update it // Find the track that contains this region and update it
const track = tracks.find(t => t.getId().toString() === activeRegion.getTrackId()); const track = tracks.find(t => t.getId().toString() === activeRegion.getTrackId());
if (track) { if (track) {
updateTrack(track); updateTrack(track);
} }
// Trigger a re-render by incrementing the note update counter // Trigger a re-render by incrementing the note update counter
if (triggerNoteUpdateRef.current) { if (triggerNoteUpdateRef.current) {
triggerNoteUpdateRef.current(prev => prev + 1); triggerNoteUpdateRef.current(prev => prev + 1);
if (DEBUG_MODE.PIANO_ROLL) { if (DEBUG_MODE.PIANO_ROLL) {
console.log('Triggered note update to re-render quantized note lengths'); console.log('Triggered note update to re-render quantized note lengths');
} }
@@ -647,19 +647,19 @@ const PianoRoll: React.FC<PianoRollProps> = ({
const handleQuantSelect = useCallback((type: 'position' | 'length', value: string) => { const handleQuantSelect = useCallback((type: 'position' | 'length', value: string) => {
if (type === 'position') { if (type === 'position') {
setQuantPosition(value); setQuantPosition(value);
// Apply quantization immediately when position quantization is changed // Apply quantization immediately when position quantization is changed
quantizeSelectedNotes(value); quantizeSelectedNotes(value);
if (DEBUG_MODE.PIANO_ROLL) { if (DEBUG_MODE.PIANO_ROLL) {
console.log(`quant-position selected: ${value}`); console.log(`quant-position selected: ${value}`);
} }
} else { } else {
setQuantLength(value); setQuantLength(value);
// Apply length quantization immediately when length quantization is changed // Apply length quantization immediately when length quantization is changed
quantizeNoteLength(value); quantizeNoteLength(value);
if (DEBUG_MODE.PIANO_ROLL) { if (DEBUG_MODE.PIANO_ROLL) {
console.log(`quant-length selected: ${value}`); console.log(`quant-length selected: ${value}`);
} }
@@ -710,24 +710,24 @@ const PianoRoll: React.FC<PianoRollProps> = ({
// We have 8 octaves (0-7), and C4 is in the middle // We have 8 octaves (0-7), and C4 is in the middle
// Each octave has 12 notes, each note is piano key height // 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 // C4 is in octave 4, and C is the first note in each octave
// Calculate from the bottom: // Calculate from the bottom:
// - Octaves 0-3 = 4 octaves = 4 * 12 * piano key height // - Octaves 0-3 = 4 octaves = 4 * 12 * piano key height
// - Within octave 4, C is the first note (from bottom), so 0px additional // - 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 keyHeight = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--region-piano-key-height')) || 20;
const c4Position = 4 * 12 * keyHeight; // pixels from bottom const c4Position = 4 * 12 * keyHeight; // pixels from bottom
// Total height of all notes (8 octaves * 12 notes * piano key height) // Total height of all notes (8 octaves * 12 notes * piano key height)
const totalHeight = 8 * 12 * keyHeight; const totalHeight = 8 * 12 * keyHeight;
// Get the viewport height of the piano roll content // Get the viewport height of the piano roll content
const viewportHeight = pianoRollNoteScrollRef.current.clientHeight; const viewportHeight = pianoRollNoteScrollRef.current.clientHeight;
// Calculate scroll position to center C4 // Calculate scroll position to center C4
// We need to scroll from the top, so we calculate: // We need to scroll from the top, so we calculate:
// (total height - C4 position) - (viewport height / 2) // (total height - C4 position) - (viewport height / 2)
const scrollPosition = (totalHeight - c4Position) - (viewportHeight / 2); const scrollPosition = (totalHeight - c4Position) - (viewportHeight / 2);
// Scroll to the calculated position // Scroll to the calculated position
pianoRollNoteScrollRef.current.scrollTop = Math.max(0, scrollPosition); pianoRollNoteScrollRef.current.scrollTop = Math.max(0, scrollPosition);
} }
@@ -846,23 +846,23 @@ const PianoRoll: React.FC<PianoRollProps> = ({
if (pianoRollNoteScrollRef.current && activeRegion) { if (pianoRollNoteScrollRef.current && activeRegion) {
// Get the starting beat of the region // Get the starting beat of the region
const startBeat = activeRegion.getStartFromBeat(); const startBeat = activeRegion.getStartFromBeat();
// Get the time signature to calculate beats per bar // Get the time signature to calculate beats per bar
const beatsPerBar = timeSignature.numerator; const beatsPerBar = timeSignature.numerator;
// Calculate the bar number (0-indexed) // Calculate the bar number (0-indexed)
const barNumber = Math.floor(startBeat / beatsPerBar); const barNumber = Math.floor(startBeat / beatsPerBar);
if (DEBUG_MODE.PIANO_ROLL) { if (DEBUG_MODE.PIANO_ROLL) {
console.log(`Scrolling to region's starting bar: ${barNumber + 1} (startBeat: ${startBeat}, beatsPerBar: ${beatsPerBar})`); 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) // 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; const barWidth = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--region-grid-bar-width')) || 160;
// Calculate the scroll position to scroll to the starting bar // Calculate the scroll position to scroll to the starting bar
const scrollPosition = barNumber * barWidth; const scrollPosition = barNumber * barWidth;
// Scroll to the calculated position // Scroll to the calculated position
pianoRollNoteScrollRef.current.scrollLeft = Math.max(0, scrollPosition); pianoRollNoteScrollRef.current.scrollLeft = Math.max(0, scrollPosition);
} }
@@ -874,8 +874,8 @@ const PianoRoll: React.FC<PianoRollProps> = ({
// Skip if user is typing in an input field (including ChatBox) // Skip if user is typing in an input field (including ChatBox)
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
if (target && ( if (target && (
target.tagName === 'INPUT' || target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' || target.tagName === 'TEXTAREA' ||
target.contentEditable === 'true' || target.contentEditable === 'true' ||
target.hasAttribute('data-chatbox-input') || target.hasAttribute('data-chatbox-input') ||
target.closest('.chatbox-input') target.closest('.chatbox-input')
@@ -894,7 +894,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
} }
return; return;
} }
// Handle piano roll hotkeys // Handle piano roll hotkeys
const configManager = ConfigManager.instance(); const configManager = ConfigManager.instance();
if (configManager.getIsInitialized()) { if (configManager.getIsInitialized()) {
@@ -916,21 +916,21 @@ const PianoRoll: React.FC<PianoRollProps> = ({
const snap_1_4_key = configManager.get('hotkeys.piano_roll.snap_1_4') as string; 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_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; const snap_1_16_key = configManager.get('hotkeys.piano_roll.snap_1_16') as string;
// Quantize position hotkeys // Quantize position hotkeys
const qua_pos_1_4_key = configManager.get('hotkeys.piano_roll.qua_pos_1_4') as string; 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_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; const qua_pos_1_16_key = configManager.get('hotkeys.piano_roll.qua_pos_1_16') as string;
// Quantize length hotkeys // Quantize length hotkeys
const qua_len_1_4_key = configManager.get('hotkeys.piano_roll.qua_len_1_4') as string; 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_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; const qua_len_1_16_key = configManager.get('hotkeys.piano_roll.qua_len_1_16') as string;
let actionType: 'snap' | 'quantize' | null = null; let actionType: 'snap' | 'quantize' | null = null;
let actionValue: string | null = null; let actionValue: string | null = null;
let quantType: 'position' | 'length' | null = null; let quantType: 'position' | 'length' | null = null;
// Check snapping hotkeys // Check snapping hotkeys
if (event.key === snap_none_key) { if (event.key === snap_none_key) {
actionType = 'snap'; actionType = 'snap';
@@ -973,21 +973,21 @@ const PianoRoll: React.FC<PianoRollProps> = ({
actionValue = '1/16'; actionValue = '1/16';
quantType = 'length'; quantType = 'length';
} }
if (actionType && actionValue) { if (actionType && actionValue) {
// Prevent default behavior // Prevent default behavior
event.preventDefault(); event.preventDefault();
if (actionType === 'snap') { if (actionType === 'snap') {
// Validate the snap value exists in snap options // Validate the snap value exists in snap options
if (KGPianoRollState.SNAP_OPTIONS.includes(actionValue)) { if (KGPianoRollState.SNAP_OPTIONS.includes(actionValue)) {
// Change snapping value // Change snapping value
handleSnappingSelect(actionValue); handleSnappingSelect(actionValue);
// Trigger blink effect for visual feedback // Trigger blink effect for visual feedback
setBlinkButton('snapping'); setBlinkButton('snapping');
setTimeout(() => setBlinkButton(null), 200); setTimeout(() => setBlinkButton(null), 200);
if (DEBUG_MODE.PIANO_ROLL) { if (DEBUG_MODE.PIANO_ROLL) {
console.log(`Snap hotkey triggered: ${event.key}${actionValue}`); console.log(`Snap hotkey triggered: ${event.key}${actionValue}`);
} }
@@ -995,16 +995,16 @@ const PianoRoll: React.FC<PianoRollProps> = ({
} else if (actionType === 'quantize' && quantType) { } else if (actionType === 'quantize' && quantType) {
// Validate the quantValue exists in the appropriate options // Validate the quantValue exists in the appropriate options
const validOptions = quantType === 'length' ? KGPianoRollState.QUANT_LEN_OPTIONS : KGPianoRollState.QUANT_POS_OPTIONS; const validOptions = quantType === 'length' ? KGPianoRollState.QUANT_LEN_OPTIONS : KGPianoRollState.QUANT_POS_OPTIONS;
if (validOptions.includes(actionValue)) { if (validOptions.includes(actionValue)) {
// Apply quantization // Apply quantization
handleQuantSelect(quantType, actionValue); handleQuantSelect(quantType, actionValue);
// Trigger blink effect for visual feedback // Trigger blink effect for visual feedback
const buttonName = quantType === 'length' ? 'quant-length' : 'quant-position'; const buttonName = quantType === 'length' ? 'quant-length' : 'quant-position';
setBlinkButton(buttonName); setBlinkButton(buttonName);
setTimeout(() => setBlinkButton(null), 200); setTimeout(() => setBlinkButton(null), 200);
if (DEBUG_MODE.PIANO_ROLL) { if (DEBUG_MODE.PIANO_ROLL) {
console.log(`Quantize ${quantType} hotkey triggered: ${event.key}${actionValue}`); console.log(`Quantize ${quantType} hotkey triggered: ${event.key}${actionValue}`);
} }
@@ -1013,10 +1013,10 @@ const PianoRoll: React.FC<PianoRollProps> = ({
} }
} }
}; };
// Add event listener // Add event listener
window.addEventListener('keydown', handlePianoRollKeyDown); window.addEventListener('keydown', handlePianoRollKeyDown);
// Remove event listener on cleanup // Remove event listener on cleanup
return () => { return () => {
window.removeEventListener('keydown', handlePianoRollKeyDown); window.removeEventListener('keydown', handlePianoRollKeyDown);
@@ -1032,20 +1032,20 @@ const PianoRoll: React.FC<PianoRollProps> = ({
return `${midiName} + ${audioName}`; return `${midiName} + ${audioName}`;
} }
if (!activeRegion) return "EDIT NOTE CLIP"; if (!activeRegion) return "EDIT NOTE CLIP";
// Calculate the bar and beat position of the region // Calculate the bar and beat position of the region
const startBeat = activeRegion.getStartFromBeat(); const startBeat = activeRegion.getStartFromBeat();
const { bar, beatInBar } = beatsToBar(startBeat, timeSignature); const { bar, beatInBar } = beatsToBar(startBeat, timeSignature);
// Format as 1-indexed bar and beat (bar + 1, beatInBar + 1) // Format as 1-indexed bar and beat (bar + 1, beatInBar + 1)
const barNumber = bar + 1; const barNumber = bar + 1;
const beatNumber = beatInBar + 1; const beatNumber = beatInBar + 1;
return `${activeRegion.getName()} (at ${barNumber}:${beatNumber})`; return `${activeRegion.getName()} (at ${barNumber}:${beatNumber})`;
}; };
return ( return (
<div <div
className="piano-roll-panel" className="piano-roll-panel"
style={{ style={{
position: 'fixed', position: 'fixed',
@@ -1063,7 +1063,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
onTitleClick={handleTitleClick} onTitleClick={handleTitleClick}
onMouseDown={(e) => handleMouseDown(e, 'drag')} onMouseDown={(e) => handleMouseDown(e, 'drag')}
/> />
<PianoRollToolbar <PianoRollToolbar
activeTool={activeTool} activeTool={activeTool}
onToolSelect={handleToolSelect} onToolSelect={handleToolSelect}
@@ -1120,8 +1120,8 @@ const PianoRoll: React.FC<PianoRollProps> = ({
automationType={automationType} automationType={automationType}
automationRedrawVersion={automationRedrawVersion} automationRedrawVersion={automationRedrawVersion}
/> />
<div <div
className="resize-handle" className="resize-handle"
onMouseDown={(e) => handleMouseDown(e, 'resize')} onMouseDown={(e) => handleMouseDown(e, 'resize')}
> >
+107 -107
View File
@@ -74,7 +74,7 @@ interface ProjectState {
isPreparingPlayback: boolean; isPreparingPlayback: boolean;
autoScrollEnabled: boolean; autoScrollEnabled: boolean;
currentTime: string; // formatted time string currentTime: string; // formatted time string
// Selection state for UI reactivity // Selection state for UI reactivity
selectedNoteIds: string[]; selectedNoteIds: string[];
selectedPitchBendIds: string[]; selectedPitchBendIds: string[];
@@ -82,7 +82,7 @@ interface ProjectState {
selectedTrackAutomationPointIds: string[]; selectedTrackAutomationPointIds: string[];
selectedRegionIds: string[]; selectedRegionIds: string[];
selectedTrackId: string | null; selectedTrackId: string | null;
// Piano roll state // Piano roll state
showPianoRoll: boolean; showPianoRoll: boolean;
activeRegionId: string | null; activeRegionId: string | null;
@@ -92,20 +92,20 @@ interface ProjectState {
activeTrackAutomationTrackId: string | null; activeTrackAutomationTrackId: string | null;
activeTrackAutomationType: TrackAutomationType | null; activeTrackAutomationType: TrackAutomationType | null;
trackAutomationRedrawVersion: number; trackAutomationRedrawVersion: number;
// ChatBox state // ChatBox state
showChatBox: boolean; showChatBox: boolean;
// K.G.One panel state // K.G.One panel state
showKGOnePanel: boolean; showKGOnePanel: boolean;
// List event panel state // Event list panel state
showListEventPanel: boolean; showEventListPanel: boolean;
// Instrument selection panel state // Instrument selection panel state
showInstrumentSelection: boolean; showInstrumentSelection: boolean;
// instrumentSelectionTrackId removed; panel now follows selectedTrackId // instrumentSelectionTrackId removed; panel now follows selectedTrackId
// Audio import modal state // Audio import modal state
showAudioImportModal: boolean; showAudioImportModal: boolean;
audioImportTargetTrackId: string | null; audioImportTargetTrackId: string | null;
@@ -140,7 +140,7 @@ interface ProjectState {
requestPianoRollScroll: (beatPosition: number) => void; requestPianoRollScroll: (beatPosition: number) => void;
mainContentScrollRequest: number | null; mainContentScrollRequest: number | null;
pianoRollScrollRequest: number | null; pianoRollScrollRequest: number | null;
// Actions // Actions
setProjectName: (name: string) => void; setProjectName: (name: string) => void;
setSavedProjectName: (name: string) => void; setSavedProjectName: (name: string) => void;
@@ -177,7 +177,7 @@ interface ProjectState {
clearAllSelections: () => void; clearAllSelections: () => void;
setSelectedTrack: (trackId: string | null) => void; setSelectedTrack: (trackId: string | null) => void;
setTrackAutomationView: (trackId: string | null, automationType: TrackAutomationType | null) => void; setTrackAutomationView: (trackId: string | null, automationType: TrackAutomationType | null) => void;
// Piano roll actions // Piano roll actions
setShowPianoRoll: (show: boolean) => void; setShowPianoRoll: (show: boolean) => void;
setActiveRegionId: (regionId: string | null) => void; setActiveRegionId: (regionId: string | null) => void;
@@ -186,10 +186,10 @@ interface ProjectState {
openHybridMode: (midiRegionId: string, audioRegionId: string) => void; openHybridMode: (midiRegionId: string, audioRegionId: string) => void;
bumpAutomationRedrawVersion: () => void; bumpAutomationRedrawVersion: () => void;
bumpTrackAutomationRedrawVersion: () => void; bumpTrackAutomationRedrawVersion: () => void;
// Project state cleanup // Project state cleanup
cleanupProjectState: () => void; cleanupProjectState: () => void;
// ChatBox actions // ChatBox actions
setShowChatBox: (show: boolean) => void; setShowChatBox: (show: boolean) => void;
toggleChatBox: () => void; toggleChatBox: () => void;
@@ -197,27 +197,27 @@ interface ProjectState {
// K.G.One panel actions // K.G.One panel actions
toggleKGOnePanel: () => void; toggleKGOnePanel: () => void;
// List event panel actions // Event List panel actions
toggleListEventPanel: () => void; toggleEventListPanel: () => void;
// Instrument selection panel actions // Instrument selection panel actions
openInstrumentSelectionForTrack: () => void; openInstrumentSelectionForTrack: () => void;
toggleInstrumentSelectionForTrack: () => void; toggleInstrumentSelectionForTrack: () => void;
closeInstrumentSelection: () => void; closeInstrumentSelection: () => void;
// Settings actions // Settings actions
setShowSettings: (show: boolean) => void; setShowSettings: (show: boolean) => void;
toggleSettings: () => void; toggleSettings: () => void;
// Copy/paste actions // Copy/paste actions
pasteRegionsAtTrack: (trackId: string, position: number) => void; pasteRegionsAtTrack: (trackId: string, position: number) => void;
pasteNotesToActiveRegion: (regionId: string, position: number) => void; pasteNotesToActiveRegion: (regionId: string, position: number) => void;
// Undo/redo actions // Undo/redo actions
undo: () => void; undo: () => void;
redo: () => void; redo: () => void;
syncUndoRedoState: () => void; syncUndoRedoState: () => void;
// Project state refresh actions // Project state refresh actions
refreshProjectState: () => void; refreshProjectState: () => void;
@@ -282,20 +282,20 @@ function getAudioRecordingExtension(mimeType: string): string {
// Create the store // Create the store
export const useProjectStore = create<ProjectState>((set, get) => { export const useProjectStore = create<ProjectState>((set, get) => {
const currentProject = KGCore.instance().getCurrentProject(); const currentProject = KGCore.instance().getCurrentProject();
// Initialize CSS variable for time signature on store creation // Initialize CSS variable for time signature on store creation
updateTimeSignatureCSS(currentProject.getTimeSignature()); updateTimeSignatureCSS(currentProject.getTimeSignature());
// Initialize CSS variable for max bars on store creation // Initialize CSS variable for max bars on store creation
updateMaxBarsCSS(currentProject.getMaxBars()); updateMaxBarsCSS(currentProject.getMaxBars());
// Initialize CSS variable for bar width multiplier on store creation // Initialize CSS variable for bar width multiplier on store creation
updateBarWidthMultiplierCSS(currentProject.getBarWidthMultiplier()); updateBarWidthMultiplierCSS(currentProject.getBarWidthMultiplier());
// Get initial ChatBox state from config // Get initial ChatBox state from config
const configManager = ConfigManager.instance(); const configManager = ConfigManager.instance();
const initialChatBoxState = configManager.getIsInitialized() const initialChatBoxState = configManager.getIsInitialized()
? (configManager.get('chatbox.default_open') as boolean) ?? false ? (configManager.get('chatbox.default_open') as boolean) ?? false
: false; : false;
// Set up playhead update callback to keep store in sync during playback // Set up playhead update callback to keep store in sync during playback
KGCore.instance().setPlayheadUpdateCallback((position: number) => { KGCore.instance().setPlayheadUpdateCallback((position: number) => {
const { bpm, timeSignature } = get(); const { bpm, timeSignature } = get();
@@ -317,7 +317,7 @@ export const useProjectStore = create<ProjectState>((set, get) => {
const syncSelectionFromCore = () => { const syncSelectionFromCore = () => {
const core = KGCore.instance(); const core = KGCore.instance();
const selectedItems = core.getSelectedItems(); const selectedItems = core.getSelectedItems();
const noteIds = selectedItems const noteIds = selectedItems
.filter(item => item instanceof KGMidiNote) .filter(item => item instanceof KGMidiNote)
.map(item => item.getId()); .map(item => item.getId());
@@ -333,7 +333,7 @@ export const useProjectStore = create<ProjectState>((set, get) => {
const regionIds = selectedItems const regionIds = selectedItems
.filter(item => item instanceof KGRegion) .filter(item => item instanceof KGRegion)
.map(item => item.getId()); .map(item => item.getId());
set({ set({
selectedNoteIds: noteIds, selectedNoteIds: noteIds,
selectedPitchBendIds: pitchBendIds, selectedPitchBendIds: pitchBendIds,
@@ -345,10 +345,10 @@ export const useProjectStore = create<ProjectState>((set, get) => {
// Register the sync callback with KGCore // Register the sync callback with KGCore
KGCore.instance().onSelectionChanged(syncSelectionFromCore); KGCore.instance().onSelectionChanged(syncSelectionFromCore);
// Initial selection sync // Initial selection sync
syncSelectionFromCore(); syncSelectionFromCore();
// Set up command history sync callback // Set up command history sync callback
const syncUndoRedoState = () => { const syncUndoRedoState = () => {
const core = KGCore.instance(); const core = KGCore.instance();
@@ -362,10 +362,10 @@ export const useProjectStore = create<ProjectState>((set, get) => {
// Register the undo/redo sync callback with KGCore // Register the undo/redo sync callback with KGCore
KGCore.instance().setOnCommandHistoryChanged(syncUndoRedoState); KGCore.instance().setOnCommandHistoryChanged(syncUndoRedoState);
// Initial undo/redo state sync // Initial undo/redo state sync
syncUndoRedoState(); syncUndoRedoState();
// Ensure a default track exists on initial app start // Ensure a default track exists on initial app start
// Also auto-select it and open instrument selection panel // Also auto-select it and open instrument selection panel
let initialSelectedTrackId: string | null = null; let initialSelectedTrackId: string | null = null;
@@ -403,7 +403,7 @@ export const useProjectStore = create<ProjectState>((set, get) => {
isPreparingPlayback: false, isPreparingPlayback: false,
autoScrollEnabled: true, autoScrollEnabled: true,
currentTime: beatsToTimeString(KGCore.instance().getPlayheadPosition(), currentProject.getBpm(), currentProject.getTimeSignature()), currentTime: beatsToTimeString(KGCore.instance().getPlayheadPosition(), currentProject.getBpm(), currentProject.getTimeSignature()),
// Initial selection state // Initial selection state
selectedNoteIds: [], selectedNoteIds: [],
selectedPitchBendIds: [], selectedPitchBendIds: [],
@@ -411,7 +411,7 @@ export const useProjectStore = create<ProjectState>((set, get) => {
selectedTrackAutomationPointIds: [], selectedTrackAutomationPointIds: [],
selectedRegionIds: [], selectedRegionIds: [],
selectedTrackId: initialSelectedTrackId, selectedTrackId: initialSelectedTrackId,
// Initial piano roll state // Initial piano roll state
showPianoRoll: false, showPianoRoll: false,
activeRegionId: null, activeRegionId: null,
@@ -421,26 +421,26 @@ export const useProjectStore = create<ProjectState>((set, get) => {
activeTrackAutomationTrackId: null, activeTrackAutomationTrackId: null,
activeTrackAutomationType: null, activeTrackAutomationType: null,
trackAutomationRedrawVersion: 0, trackAutomationRedrawVersion: 0,
// Initial ChatBox state // Initial ChatBox state
showChatBox: initialChatBoxState, showChatBox: initialChatBoxState,
// Initial K.G.One panel state // Initial K.G.One panel state
showKGOnePanel: false, showKGOnePanel: false,
// Initial List Event panel state // Initial Event List panel state
showListEventPanel: false, showEventListPanel: false,
// Initial Instrument Selection panel state // Initial Instrument Selection panel state
showInstrumentSelection: initialShowInstrumentSelection, showInstrumentSelection: initialShowInstrumentSelection,
// Initial audio import modal state // Initial audio import modal state
showAudioImportModal: false, showAudioImportModal: false,
audioImportTargetTrackId: null, audioImportTargetTrackId: null,
// Initial Settings state // Initial Settings state
showSettings: false, showSettings: false,
// Initial undo/redo state // Initial undo/redo state
canUndo: false, canUndo: false,
canRedo: false, canRedo: false,
@@ -493,18 +493,18 @@ export const useProjectStore = create<ProjectState>((set, get) => {
// Create and execute the add track command // Create and execute the add track command
const command = new AddTrackCommand(); const command = new AddTrackCommand();
KGCore.instance().executeCommand(command); KGCore.instance().executeCommand(command);
// Update the store state with a new array reference to trigger re-render // Update the store state with a new array reference to trigger re-render
const project = KGCore.instance().getCurrentProject(); const project = KGCore.instance().getCurrentProject();
set({ tracks: [...project.getTracks()] as KGTrack[] }); set({ tracks: [...project.getTracks()] as KGTrack[] });
// Auto-select the newly created track and open instrument selection panel // Auto-select the newly created track and open instrument selection panel
const newTrackId = command.getTrackId().toString(); const newTrackId = command.getTrackId().toString();
set({ set({
selectedTrackId: newTrackId, selectedTrackId: newTrackId,
showInstrumentSelection: true, showInstrumentSelection: true,
}); });
console.log(`Added track ${command.getTrackId()}`); console.log(`Added track ${command.getTrackId()}`);
} catch (error) { } catch (error) {
console.error('Error adding track:', error); console.error('Error adding track:', error);
@@ -630,23 +630,23 @@ export const useProjectStore = create<ProjectState>((set, get) => {
const deletedTrackIndex = currentTracks.findIndex(track => track.getId() === id); const deletedTrackIndex = currentTracks.findIndex(track => track.getId() === id);
const { selectedTrackId } = get(); const { selectedTrackId } = get();
const isCurrentTrackSelected = selectedTrackId === id.toString(); const isCurrentTrackSelected = selectedTrackId === id.toString();
// Create and execute the remove track command // Create and execute the remove track command
const command = new RemoveTrackCommand(id); const command = new RemoveTrackCommand(id);
KGCore.instance().executeCommand(command); KGCore.instance().executeCommand(command);
// Update the store state with a new array reference to trigger re-render // Update the store state with a new array reference to trigger re-render
const project = KGCore.instance().getCurrentProject(); const project = KGCore.instance().getCurrentProject();
const remainingTracks = [...project.getTracks()] as KGTrack[]; const remainingTracks = [...project.getTracks()] as KGTrack[];
set({ tracks: remainingTracks }); set({ tracks: remainingTracks });
// Auto-select another track if any remain // Auto-select another track if any remain
if (remainingTracks.length > 0) { if (remainingTracks.length > 0) {
// Prefer previous track, fallback to next track // Prefer previous track, fallback to next track
const newSelectedIndex = deletedTrackIndex > 0 const newSelectedIndex = deletedTrackIndex > 0
? deletedTrackIndex - 1 // Select previous track ? deletedTrackIndex - 1 // Select previous track
: 0; // Select first remaining track (was next) : 0; // Select first remaining track (was next)
const newSelectedTrack = remainingTracks[newSelectedIndex]; const newSelectedTrack = remainingTracks[newSelectedIndex];
const newSelectedTrackId = newSelectedTrack.getId().toString(); const newSelectedTrackId = newSelectedTrack.getId().toString();
@@ -662,7 +662,7 @@ export const useProjectStore = create<ProjectState>((set, get) => {
showInstrumentSelection: false, showInstrumentSelection: false,
}); });
} }
console.log(`Removed track ${id}`); console.log(`Removed track ${id}`);
} catch (error) { } catch (error) {
console.error('Error removing track:', error); console.error('Error removing track:', error);
@@ -672,23 +672,23 @@ export const useProjectStore = create<ProjectState>((set, get) => {
updateTrack: async (updatedTrack: KGTrack) => { updateTrack: async (updatedTrack: KGTrack) => {
const { tracks } = get(); const { tracks } = get();
try { try {
// Find the old track to compare instruments // Find the old track to compare instruments
const oldTrack = tracks.find(track => track.getId() === updatedTrack.getId()); const oldTrack = tracks.find(track => track.getId() === updatedTrack.getId());
// Type guard to check if track is KGMidiTrack // Type guard to check if track is KGMidiTrack
const isMidiTrack = (track: KGTrack): track is KGMidiTrack => { const isMidiTrack = (track: KGTrack): track is KGMidiTrack => {
return track.getCurrentType() === 'KGMidiTrack' && 'getInstrument' in track; return track.getCurrentType() === 'KGMidiTrack' && 'getInstrument' in track;
}; };
// Check if instrument changed (only for MIDI tracks) // Check if instrument changed (only for MIDI tracks)
const shouldUpdateInstrument = const shouldUpdateInstrument =
isMidiTrack(updatedTrack) && isMidiTrack(updatedTrack) &&
oldTrack && oldTrack &&
isMidiTrack(oldTrack) && isMidiTrack(oldTrack) &&
updatedTrack.getInstrument() !== oldTrack.getInstrument(); updatedTrack.getInstrument() !== oldTrack.getInstrument();
// Update instrument in audio interface if changed // Update instrument in audio interface if changed
if (shouldUpdateInstrument && isMidiTrack(updatedTrack)) { if (shouldUpdateInstrument && isMidiTrack(updatedTrack)) {
const audioInterface = KGAudioInterface.instance(); const audioInterface = KGAudioInterface.instance();
@@ -696,18 +696,18 @@ export const useProjectStore = create<ProjectState>((set, get) => {
audioInterface.setTrackInstrument(updatedTrack.getId().toString(), newInstrument); audioInterface.setTrackInstrument(updatedTrack.getId().toString(), newInstrument);
console.log(`Updated track ${updatedTrack.getId()} instrument to ${newInstrument}`); console.log(`Updated track ${updatedTrack.getId()} instrument to ${newInstrument}`);
} }
// Find and update the track // Find and update the track
const updatedTracks = tracks.map(track => const updatedTracks = tracks.map(track =>
track.getId() === updatedTrack.getId() ? updatedTrack : track track.getId() === updatedTrack.getId() ? updatedTrack : track
); );
// Update the core model // Update the core model
KGCore.instance().getCurrentProject().setTracks(updatedTracks); KGCore.instance().getCurrentProject().setTracks(updatedTracks);
// Update the store // Update the store
set({ tracks: updatedTracks }); set({ tracks: updatedTracks });
console.log(`Updated track ${updatedTrack.getId()}`); console.log(`Updated track ${updatedTrack.getId()}`);
} catch (error) { } catch (error) {
console.error('Error updating track:', error); console.error('Error updating track:', error);
@@ -724,7 +724,7 @@ export const useProjectStore = create<ProjectState>((set, get) => {
// Update the store state with the current project state // Update the store state with the current project state
const project = KGCore.instance().getCurrentProject(); const project = KGCore.instance().getCurrentProject();
set({ tracks: [...project.getTracks()] as KGTrack[] }); set({ tracks: [...project.getTracks()] as KGTrack[] });
console.log(`Updated track ${trackId} properties`); console.log(`Updated track ${trackId} properties`);
} catch (error) { } catch (error) {
console.error('Error updating track properties:', error); console.error('Error updating track properties:', error);
@@ -736,14 +736,14 @@ export const useProjectStore = create<ProjectState>((set, get) => {
try { try {
// Use the new updateTrackProperties method with command pattern // Use the new updateTrackProperties method with command pattern
await get().updateTrackProperties(trackId, { instrument }); await get().updateTrackProperties(trackId, { instrument });
console.log(`Set track ${trackId} instrument to ${instrument}`); console.log(`Set track ${trackId} instrument to ${instrument}`);
} catch (error) { } catch (error) {
console.error('Error setting track instrument:', error); console.error('Error setting track instrument:', error);
get().setStatus('Failed to change instrument'); get().setStatus('Failed to change instrument');
} }
}, },
reorderTracks: (sourceIndex: number, destinationIndex: number) => { reorderTracks: (sourceIndex: number, destinationIndex: number) => {
try { try {
// Create and execute the reorder tracks command // Create and execute the reorder tracks command
@@ -753,7 +753,7 @@ export const useProjectStore = create<ProjectState>((set, get) => {
// Update the store state with the current project state // Update the store state with the current project state
const project = KGCore.instance().getCurrentProject(); const project = KGCore.instance().getCurrentProject();
set({ tracks: [...project.getTracks()] as KGTrack[] }); set({ tracks: [...project.getTracks()] as KGTrack[] });
console.log(`Reordered track from index ${sourceIndex} to ${destinationIndex}`); console.log(`Reordered track from index ${sourceIndex} to ${destinationIndex}`);
} catch (error) { } catch (error) {
console.error('Error reordering tracks:', error); console.error('Error reordering tracks:', error);
@@ -774,11 +774,11 @@ export const useProjectStore = create<ProjectState>((set, get) => {
refreshStatus: () => { refreshStatus: () => {
set({ currentStatus: KGCore.instance().getStatus() || 'Unknown' }); set({ currentStatus: KGCore.instance().getStatus() || 'Unknown' });
}, },
loadProject: async (project: KGProject | null = null, savedName?: string) => { loadProject: async (project: KGProject | null = null, savedName?: string) => {
try { try {
const { setPlayheadPosition } = get(); const { setPlayheadPosition } = get();
// Upgrade incoming project data to latest structure version (only when provided explicitly) // Upgrade incoming project data to latest structure version (only when provided explicitly)
if (project) { if (project) {
project = upgradeProjectToLatest(project); project = upgradeProjectToLatest(project);
@@ -786,12 +786,12 @@ export const useProjectStore = create<ProjectState>((set, get) => {
// Get project from KGCore if null // Get project from KGCore if null
const projectToLoad = project || KGCore.instance().getCurrentProject(); const projectToLoad = project || KGCore.instance().getCurrentProject();
// Set the project in KGCore if one was provided // Set the project in KGCore if one was provided
if (project) { if (project) {
KGCore.instance().setCurrentProject(projectToLoad); KGCore.instance().setCurrentProject(projectToLoad);
} }
// Ensure a default "Melody" track exists for empty projects // Ensure a default "Melody" track exists for empty projects
if (projectToLoad.getTracks().length === 0) { if (projectToLoad.getTracks().length === 0) {
const addDefaultTrackCommand = new AddTrackCommand(undefined, 'Melody'); const addDefaultTrackCommand = new AddTrackCommand(undefined, 'Melody');
@@ -800,17 +800,17 @@ export const useProjectStore = create<ProjectState>((set, get) => {
// Reset playhead to 0 when loading a project // Reset playhead to 0 when loading a project
setPlayheadPosition(0); setPlayheadPosition(0);
// Get project properties // Get project properties
const maxBars = projectToLoad.getMaxBars(); const maxBars = projectToLoad.getMaxBars();
const timeSignature = projectToLoad.getTimeSignature(); const timeSignature = projectToLoad.getTimeSignature();
const bpm = projectToLoad.getBpm(); const bpm = projectToLoad.getBpm();
const keySignature = projectToLoad.getKeySignature(); const keySignature = projectToLoad.getKeySignature();
const tracks = projectToLoad.getTracks(); const tracks = projectToLoad.getTracks();
// Setup audio synths for all tracks // Setup audio synths for all tracks
const audioInterface = KGAudioInterface.instance(); const audioInterface = KGAudioInterface.instance();
// Clear any existing synths/buses first // Clear any existing synths/buses first
tracks.forEach(track => { tracks.forEach(track => {
const trackId = track.getId().toString(); const trackId = track.getId().toString();
@@ -857,7 +857,7 @@ export const useProjectStore = create<ProjectState>((set, get) => {
audioInterface.setTrackVolume(trackId, track.getVolume()); audioInterface.setTrackVolume(trackId, track.getVolume());
} }
} }
// Update CSS variables // Update CSS variables
updateTimeSignatureCSS(timeSignature); updateTimeSignatureCSS(timeSignature);
updateMaxBarsCSS(maxBars); updateMaxBarsCSS(maxBars);
@@ -866,7 +866,7 @@ export const useProjectStore = create<ProjectState>((set, get) => {
// Log project loading info // Log project loading info
console.log(`Project max bars: ${maxBars}`); console.log(`Project max bars: ${maxBars}`);
console.log(`Setup audio synths for ${tracks.length} tracks`); console.log(`Setup audio synths for ${tracks.length} tracks`);
// Update the store state to reflect the loaded project // Update the store state to reflect the loaded project
// Force a new array reference for tracks to trigger React/Zustand re-render // Force a new array reference for tracks to trigger React/Zustand re-render
set({ set({
@@ -914,7 +914,7 @@ export const useProjectStore = create<ProjectState>((set, get) => {
// Reset piano roll state for new/loaded project // Reset piano roll state for new/loaded project
KGPianoRollState.instance().setLastEditedNoteLength(1); KGPianoRollState.instance().setLastEditedNoteLength(1);
// Add a status message // Add a status message
KGCore.instance().setStatus(`Project "${projectToLoad.getName()}" loaded with audio setup`); KGCore.instance().setStatus(`Project "${projectToLoad.getName()}" loaded with audio setup`);
set({ currentStatus: KGCore.instance().getStatus() || 'Unknown' }); set({ currentStatus: KGCore.instance().getStatus() || 'Unknown' });
@@ -927,7 +927,7 @@ export const useProjectStore = create<ProjectState>((set, get) => {
setPlayheadPosition: (position: number) => { setPlayheadPosition: (position: number) => {
const { bpm, timeSignature } = get(); const { bpm, timeSignature } = get();
KGCore.instance().setPlayheadPosition(position); KGCore.instance().setPlayheadPosition(position);
set({ set({
playheadPosition: position, playheadPosition: position,
currentTime: beatsToTimeString(position, bpm, timeSignature) currentTime: beatsToTimeString(position, bpm, timeSignature)
}); });
@@ -1024,9 +1024,9 @@ export const useProjectStore = create<ProjectState>((set, get) => {
KGCore.instance().setLoopBoundaryReachedCallback(projectLooping KGCore.instance().setLoopBoundaryReachedCallback(projectLooping
? (loopEndBeat: number) => { ? (loopEndBeat: number) => {
_audioRecordingForcedStopBeatAbsolute = loopEndBeat; _audioRecordingForcedStopBeatAbsolute = loopEndBeat;
void get().stopRecording(); void get().stopRecording();
} }
: null); : null);
setPlayheadPosition(recordingStartBeatAbsolute); setPlayheadPosition(recordingStartBeatAbsolute);
@@ -1405,10 +1405,10 @@ export const useProjectStore = create<ProjectState>((set, get) => {
// Create and execute the change project property command // Create and execute the change project property command
const command = new ChangeProjectPropertyCommand({ bpm }); const command = new ChangeProjectPropertyCommand({ bpm });
KGCore.instance().executeCommand(command); KGCore.instance().executeCommand(command);
// Update the store state // Update the store state
set({ bpm }); set({ bpm });
console.log(`Set BPM to ${bpm}`); console.log(`Set BPM to ${bpm}`);
} catch (error) { } catch (error) {
console.error('Error setting BPM:', error); console.error('Error setting BPM:', error);
@@ -1421,12 +1421,12 @@ export const useProjectStore = create<ProjectState>((set, get) => {
// Create and execute the change project property command // Create and execute the change project property command
const command = new ChangeProjectPropertyCommand({ maxBars }); const command = new ChangeProjectPropertyCommand({ maxBars });
KGCore.instance().executeCommand(command); KGCore.instance().executeCommand(command);
// Update the store state // Update the store state
set({ maxBars }); set({ maxBars });
// Sync CSS var so layout adjusts immediately // Sync CSS var so layout adjusts immediately
updateMaxBarsCSS(maxBars); updateMaxBarsCSS(maxBars);
console.log(`Set max bars to ${maxBars}`); console.log(`Set max bars to ${maxBars}`);
} catch (error) { } catch (error) {
console.error('Error setting max bars:', error); console.error('Error setting max bars:', error);
@@ -1492,7 +1492,7 @@ export const useProjectStore = create<ProjectState>((set, get) => {
// Selection actions // Selection actions
syncSelectionFromCore, syncSelectionFromCore,
clearAllSelections: () => { clearAllSelections: () => {
KGCore.instance().clearSelectedItems(); KGCore.instance().clearSelectedItems();
// Note: syncSelectionFromCore will be called automatically via callback // Note: syncSelectionFromCore will be called automatically via callback
@@ -1526,7 +1526,7 @@ export const useProjectStore = create<ProjectState>((set, get) => {
selectedTrackId: trackId, selectedTrackId: trackId,
}); });
}, },
// Piano roll actions // Piano roll actions
setShowPianoRoll: (show: boolean) => { setShowPianoRoll: (show: boolean) => {
set({ showPianoRoll: show }); set({ showPianoRoll: show });
@@ -1553,7 +1553,7 @@ export const useProjectStore = create<ProjectState>((set, get) => {
bumpTrackAutomationRedrawVersion: () => { bumpTrackAutomationRedrawVersion: () => {
set(state => ({ trackAutomationRedrawVersion: state.trackAutomationRedrawVersion + 1 })); set(state => ({ trackAutomationRedrawVersion: state.trackAutomationRedrawVersion + 1 }));
}, },
// Project state cleanup - used when starting new/loading projects // Project state cleanup - used when starting new/loading projects
cleanupProjectState: () => { cleanupProjectState: () => {
// Close piano roll if it's visible // Close piano roll if it's visible
@@ -1570,36 +1570,36 @@ export const useProjectStore = create<ProjectState>((set, get) => {
recordingAudioPreviewPeaks: [], recordingAudioPreviewPeaks: [],
recordingAudioPreviewCurrentBeat: 0, recordingAudioPreviewCurrentBeat: 0,
}); });
// Clear any selected items // Clear any selected items
KGCore.instance().clearSelectedItems(); KGCore.instance().clearSelectedItems();
// Note: syncSelectionFromCore will be called automatically via callback // Note: syncSelectionFromCore will be called automatically via callback
console.log("Cleaned up project state: closed piano roll, cleared active region, cleared selections"); console.log("Cleaned up project state: closed piano roll, cleared active region, cleared selections");
}, },
// ChatBox action implementations // ChatBox action implementations
setShowChatBox: (show: boolean) => { setShowChatBox: (show: boolean) => {
set({ set({
showChatBox: show, showChatBox: show,
showKGOnePanel: show ? false : get().showKGOnePanel, showKGOnePanel: show ? false : get().showKGOnePanel,
showListEventPanel: show ? false : get().showListEventPanel showEventListPanel: show ? false : get().showEventListPanel
}); });
}, },
toggleChatBox: () => { toggleChatBox: () => {
const { showChatBox } = get(); const { showChatBox } = get();
set({ showChatBox: !showChatBox, showKGOnePanel: false, showListEventPanel: false }); set({ showChatBox: !showChatBox, showKGOnePanel: false, showEventListPanel: false });
}, },
toggleKGOnePanel: () => { toggleKGOnePanel: () => {
const { showKGOnePanel } = get(); const { showKGOnePanel } = get();
set({ showKGOnePanel: !showKGOnePanel, showChatBox: false, showListEventPanel: false }); set({ showKGOnePanel: !showKGOnePanel, showChatBox: false, showEventListPanel: false });
}, },
toggleListEventPanel: () => { toggleEventListPanel: () => {
const { showListEventPanel } = get(); const { showEventListPanel } = get();
set({ showListEventPanel: !showListEventPanel, showChatBox: false, showKGOnePanel: false }); set({ showEventListPanel: !showEventListPanel, showChatBox: false, showKGOnePanel: false });
}, },
// Instrument selection panel actions // Instrument selection panel actions
@@ -1612,64 +1612,64 @@ export const useProjectStore = create<ProjectState>((set, get) => {
closeInstrumentSelection: () => { closeInstrumentSelection: () => {
set({ showInstrumentSelection: false }); set({ showInstrumentSelection: false });
}, },
// Settings action implementations // Settings action implementations
setShowSettings: (show: boolean) => { setShowSettings: (show: boolean) => {
set({ showSettings: show }); set({ showSettings: show });
}, },
toggleSettings: () => { toggleSettings: () => {
const { showSettings } = get(); const { showSettings } = get();
set({ showSettings: !showSettings }); set({ showSettings: !showSettings });
}, },
// Copy/paste actions // Copy/paste actions
pasteRegionsAtTrack: (trackId: string, position: number) => { pasteRegionsAtTrack: (trackId: string, position: number) => {
// Use command pattern for region pasting with undo support // Use command pattern for region pasting with undo support
const command = PasteRegionsCommand.fromClipboard(trackId, position); const command = PasteRegionsCommand.fromClipboard(trackId, position);
if (!command) { if (!command) {
console.log('No regions to paste'); console.log('No regions to paste');
return; return;
} }
try { try {
KGCore.instance().executeCommand(command); KGCore.instance().executeCommand(command);
// Update the store to trigger re-render // Update the store to trigger re-render
const { tracks } = get(); const { tracks } = get();
const updatedTracks = [...tracks]; const updatedTracks = [...tracks];
set({ tracks: updatedTracks }); set({ tracks: updatedTracks });
console.log(`Executed PasteRegionsCommand: pasted regions to track ${trackId} using command pattern`); console.log(`Executed PasteRegionsCommand: pasted regions to track ${trackId} using command pattern`);
} catch (error) { } catch (error) {
console.error('Error pasting regions:', error); console.error('Error pasting regions:', error);
} }
}, },
pasteNotesToActiveRegion: (regionId: string, position: number) => { pasteNotesToActiveRegion: (regionId: string, position: number) => {
// Use command pattern for note pasting with undo support // Use command pattern for note pasting with undo support
const command = PasteNotesCommand.fromClipboard(regionId, position); const command = PasteNotesCommand.fromClipboard(regionId, position);
if (!command) { if (!command) {
console.log('No notes to paste'); console.log('No notes to paste');
return; return;
} }
try { try {
KGCore.instance().executeCommand(command); KGCore.instance().executeCommand(command);
// Update the store to trigger re-render // Update the store to trigger re-render
const { tracks } = get(); const { tracks } = get();
const updatedTracks = [...tracks]; const updatedTracks = [...tracks];
set({ tracks: updatedTracks }); set({ tracks: updatedTracks });
console.log(`Executed PasteNotesCommand: pasted notes to region ${regionId} using command pattern`); console.log(`Executed PasteNotesCommand: pasted notes to region ${regionId} using command pattern`);
} catch (error) { } catch (error) {
console.error('Error pasting notes:', error); console.error('Error pasting notes:', error);
} }
}, },
// Undo/redo actions // Undo/redo actions
undo: () => { undo: () => {
const core = KGCore.instance(); const core = KGCore.instance();
@@ -1680,7 +1680,7 @@ export const useProjectStore = create<ProjectState>((set, get) => {
console.log('Undo completed'); console.log('Undo completed');
} }
}, },
redo: () => { redo: () => {
const core = KGCore.instance(); const core = KGCore.instance();
if (core.redo()) { if (core.redo()) {
@@ -1690,7 +1690,7 @@ export const useProjectStore = create<ProjectState>((set, get) => {
console.log('Redo completed'); console.log('Redo completed');
} }
}, },
syncUndoRedoState: () => { syncUndoRedoState: () => {
const core = KGCore.instance(); const core = KGCore.instance();
set({ set({
@@ -1700,13 +1700,13 @@ export const useProjectStore = create<ProjectState>((set, get) => {
redoDescription: core.getRedoDescription() redoDescription: core.getRedoDescription()
}); });
}, },
// Centralized project state refresh method // Centralized project state refresh method
// Used by undo/redo and external operations (like XML tools) to sync UI with core model // Used by undo/redo and external operations (like XML tools) to sync UI with core model
refreshProjectState: () => { refreshProjectState: () => {
const core = KGCore.instance(); const core = KGCore.instance();
const project = core.getCurrentProject(); const project = core.getCurrentProject();
// Force new array reference to trigger React re-renders // Force new array reference to trigger React re-renders
set({ set({
projectName: project.getName(), projectName: project.getName(),
@@ -1729,7 +1729,7 @@ export const useProjectStore = create<ProjectState>((set, get) => {
actions.syncUndoRedoState(); actions.syncUndoRedoState();
actions.syncSelectionFromCore(); actions.syncSelectionFromCore();
}, },
// Initialize store with configuration values // Initialize store with configuration values
initializeFromConfig: async () => { initializeFromConfig: async () => {
const configManager = ConfigManager.instance(); const configManager = ConfigManager.instance();