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: {
+4 -4
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';
@@ -31,7 +31,7 @@ function App() {
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
@@ -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;
@@ -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;
+205 -205
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,
@@ -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
@@ -1047,218 +1047,218 @@ const Toolbar: React.FC = () => {
</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 <FileImportModal
isVisible={showImportModal} isVisible={showImportModal}
onClose={() => setShowImportModal(false)} onClose={() => setShowImportModal(false)}
onFileImport={handleFileImport} onFileImport={handleFileImport}
acceptedTypes={['.kgstudio', '.json', '.mid', '.midi']} acceptedTypes={['.kgstudio', '.json', '.mid', '.midi']}
title="Import Project" title="Import Project"
description="Drag and drop your project file here" description="Drag and drop your project file here"
/>
<LoadingOverlay
visible={isOpeningProject}
message="Opening project..."
/>
{showOpenProject && (
<OpenProjectModal
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;
+2 -2
View File
@@ -47,7 +47,7 @@ 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');
@@ -142,7 +142,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
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
+15 -15
View File
@@ -99,8 +99,8 @@ interface ProjectState {
// 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;
@@ -197,8 +197,8 @@ 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;
@@ -428,8 +428,8 @@ export const useProjectStore = create<ProjectState>((set, get) => {
// 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,
@@ -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);
@@ -1583,23 +1583,23 @@ export const useProjectStore = create<ProjectState>((set, get) => {
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