feat: added track level event list tab; rename List Event to Event List.
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+22
-22
@@ -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;
|
||||||
+19
-19
@@ -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;
|
||||||
@@ -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
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user