feat: added v1 read-only version list event panel
This commit is contained in:
+3
-1
@@ -11,6 +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 { 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';
|
||||||
@@ -30,7 +31,7 @@ function App() {
|
|||||||
const {
|
const {
|
||||||
refreshStatus,
|
refreshStatus,
|
||||||
loadProject, showChatBox, showSettings, setShowSettings, initializeFromConfig,
|
loadProject, showChatBox, showSettings, setShowSettings, initializeFromConfig,
|
||||||
showInstrumentSelection, showKGOnePanel
|
showInstrumentSelection, showKGOnePanel, showListEventPanel
|
||||||
} = useProjectStore();
|
} = useProjectStore();
|
||||||
|
|
||||||
// Track if app has been initialized to prevent multiple initializations
|
// Track if app has been initialized to prevent multiple initializations
|
||||||
@@ -168,6 +169,7 @@ function App() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<KGOnePanel isVisible={showKGOnePanel && !showSettings} />
|
<KGOnePanel isVisible={showKGOnePanel && !showSettings} />
|
||||||
|
<ListEventPanel isVisible={showListEventPanel && !showSettings} />
|
||||||
<ChatBox isVisible={showChatBox && !showSettings} />
|
<ChatBox isVisible={showChatBox && !showSettings} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
.list-event-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: var(--chat-box-width);
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
border-left: 1px solid #3a3a3a;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-event-panel.is-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-event-panel-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background-color: #3a3a3a;
|
||||||
|
height: 40px;
|
||||||
|
border-bottom: 1px solid #4a4a4a;
|
||||||
|
padding: 0 15px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-event-panel-header h3 {
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-event-panel-body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 12px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-event-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-event-tab {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
color: #999;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
min-height: 20px;
|
||||||
|
padding: 5px 6px;
|
||||||
|
cursor: default;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-event-tab:hover {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-event-tab.active {
|
||||||
|
background-color: #5a9fd4;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-event-empty-state {
|
||||||
|
color: #888;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.5;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px 8px;
|
||||||
|
background-color: #252525;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-event-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-event-toolbar-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-event-toolbar-group-right {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-event-add-button {
|
||||||
|
width: 22px;
|
||||||
|
height: 20px;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: #3a3a3a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: background-color 0.18s ease, border-color 0.18s ease;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-event-add-button:hover {
|
||||||
|
background-color: #464646;
|
||||||
|
border-color: #5a5a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-event-dropdown-button,
|
||||||
|
.list-event-quant-button {
|
||||||
|
min-width: 92px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-event-dropdown-button {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-event-table-shell {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: #232323;
|
||||||
|
border: 1px solid #373737;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-event-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-event-table thead th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
background-color: #343434;
|
||||||
|
color: #bfc6d2;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: left;
|
||||||
|
height: 20px;
|
||||||
|
padding: 2px 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-event-table tbody tr {
|
||||||
|
color: #e0e0e0;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-event-table tbody tr:nth-child(odd) {
|
||||||
|
background-color: #282828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-event-table tbody tr:nth-child(even) {
|
||||||
|
background-color: #303030;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-event-table tbody tr.selected {
|
||||||
|
background-color: #5a9fd4;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-event-table td {
|
||||||
|
height: 20px;
|
||||||
|
padding: 2px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,345 @@
|
|||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
import './ListEventPanel.css';
|
||||||
|
import { FaPlus } from 'react-icons/fa';
|
||||||
|
import KGDropdown from './common/KGDropdown';
|
||||||
|
import { useProjectStore } from '../stores/projectStore';
|
||||||
|
import { KGCore } from '../core/KGCore';
|
||||||
|
import { KGMidiRegion } from '../core/region/KGMidiRegion';
|
||||||
|
import { KGMidiNote } from '../core/midi/KGMidiNote';
|
||||||
|
import { KGPianoRollState } from '../core/state/KGPianoRollState';
|
||||||
|
import { beatsToBar, pitchToNoteNameString } from '../util/midiUtil';
|
||||||
|
import { isModifierKeyPressed } from '../util/osUtil';
|
||||||
|
import { PIANO_ROLL_CONSTANTS } from '../constants';
|
||||||
|
|
||||||
|
interface ListEventPanelProps {
|
||||||
|
isVisible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NoteRowData {
|
||||||
|
id: string;
|
||||||
|
note: KGMidiNote;
|
||||||
|
absoluteStartBeat: number;
|
||||||
|
durationBeats: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EVENT_TYPE_OPTIONS = [{ label: 'Notes', value: 'notes' }];
|
||||||
|
const EVENT_POSITION_TICKS_PER_BEAT = 480;
|
||||||
|
|
||||||
|
const formatEventPosition = (
|
||||||
|
beats: number,
|
||||||
|
timeSignature: { numerator: number; denominator: number }
|
||||||
|
): string => {
|
||||||
|
const { bar, beatInBar } = beatsToBar(beats, timeSignature);
|
||||||
|
const beatInteger = Math.floor(beatInBar);
|
||||||
|
let tick = Math.round((beatInBar - beatInteger) * EVENT_POSITION_TICKS_PER_BEAT);
|
||||||
|
let normalizedBeat = beatInteger;
|
||||||
|
let normalizedBar = bar;
|
||||||
|
|
||||||
|
if (tick >= EVENT_POSITION_TICKS_PER_BEAT) {
|
||||||
|
tick = 0;
|
||||||
|
normalizedBeat += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedBeat >= timeSignature.numerator) {
|
||||||
|
normalizedBeat = 0;
|
||||||
|
normalizedBar += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${normalizedBar + 1} ${normalizedBeat + 1} ${tick}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatEventLength = (
|
||||||
|
beats: number
|
||||||
|
): string => {
|
||||||
|
const fullBeats = Math.floor(beats);
|
||||||
|
let tick = Math.round((beats - fullBeats) * EVENT_POSITION_TICKS_PER_BEAT);
|
||||||
|
let normalizedBeats = fullBeats;
|
||||||
|
|
||||||
|
if (tick >= EVENT_POSITION_TICKS_PER_BEAT) {
|
||||||
|
tick = 0;
|
||||||
|
normalizedBeats += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${normalizedBeats} ${tick}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ListEventPanel: React.FC<ListEventPanelProps> = ({ isVisible }) => {
|
||||||
|
const {
|
||||||
|
tracks,
|
||||||
|
activeRegionId,
|
||||||
|
selectedRegionIds,
|
||||||
|
timeSignature,
|
||||||
|
selectedNoteIds,
|
||||||
|
updateTrack,
|
||||||
|
refreshProjectState
|
||||||
|
} = useProjectStore();
|
||||||
|
|
||||||
|
const [eventType, setEventType] = useState('notes');
|
||||||
|
const [quantPosition, setQuantPosition] = useState<string>('1/8');
|
||||||
|
const [quantLength, setQuantLength] = useState<string>('1/8');
|
||||||
|
const rangeAnchorNoteIdRef = useRef<string | null>(null);
|
||||||
|
|
||||||
|
const resolvedRegionId = selectedRegionIds.length > 1
|
||||||
|
? activeRegionId
|
||||||
|
: selectedRegionIds.length === 1
|
||||||
|
? selectedRegionIds[0]
|
||||||
|
: activeRegionId;
|
||||||
|
|
||||||
|
let activeMidiRegion: KGMidiRegion | null = null;
|
||||||
|
let parentTrack = null as typeof tracks[number] | null;
|
||||||
|
|
||||||
|
if (resolvedRegionId) {
|
||||||
|
for (const track of tracks) {
|
||||||
|
const region = track.getRegions().find(candidate => candidate.getId() === resolvedRegionId);
|
||||||
|
if (region instanceof KGMidiRegion) {
|
||||||
|
activeMidiRegion = region;
|
||||||
|
parentTrack = track;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const noteRows: NoteRowData[] = activeMidiRegion
|
||||||
|
? [...activeMidiRegion.getNotes()]
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.getStartBeat() !== b.getStartBeat()) return a.getStartBeat() - b.getStartBeat();
|
||||||
|
if (a.getPitch() !== b.getPitch()) return a.getPitch() - b.getPitch();
|
||||||
|
return a.getId().localeCompare(b.getId());
|
||||||
|
})
|
||||||
|
.map(note => ({
|
||||||
|
id: note.getId(),
|
||||||
|
note,
|
||||||
|
absoluteStartBeat: activeMidiRegion!.getStartFromBeat() + note.getStartBeat(),
|
||||||
|
durationBeats: note.getEndBeat() - note.getStartBeat()
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const selectedNoteIdSet = new Set(selectedNoteIds);
|
||||||
|
|
||||||
|
const commitSelection = (nextSelectedIds: Set<string>) => {
|
||||||
|
if (!activeMidiRegion || !parentTrack) return;
|
||||||
|
|
||||||
|
const selectedNotes = activeMidiRegion.getNotes().filter(note => nextSelectedIds.has(note.getId()));
|
||||||
|
const core = KGCore.instance();
|
||||||
|
|
||||||
|
activeMidiRegion.getNotes().forEach(note => {
|
||||||
|
if (nextSelectedIds.has(note.getId())) {
|
||||||
|
note.select();
|
||||||
|
} else {
|
||||||
|
note.deselect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
core.clearSelectedItems();
|
||||||
|
if (selectedNotes.length > 0) {
|
||||||
|
core.addSelectedItems(selectedNotes);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateTrack(parentTrack);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRowClick = (noteId: string, rowIndex: number, event: React.MouseEvent<HTMLTableRowElement>) => {
|
||||||
|
if (!activeMidiRegion) return;
|
||||||
|
|
||||||
|
const isModifierPressed = isModifierKeyPressed(event);
|
||||||
|
const nextSelectedIds = new Set(selectedNoteIdSet);
|
||||||
|
|
||||||
|
if (event.shiftKey) {
|
||||||
|
const anchorIndex = noteRows.findIndex(row => row.id === rangeAnchorNoteIdRef.current);
|
||||||
|
const rangeStartIndex = anchorIndex >= 0 ? Math.min(anchorIndex, rowIndex) : rowIndex;
|
||||||
|
const rangeEndIndex = anchorIndex >= 0 ? Math.max(anchorIndex, rowIndex) : rowIndex;
|
||||||
|
|
||||||
|
if (!isModifierPressed) {
|
||||||
|
nextSelectedIds.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = rangeStartIndex; index <= rangeEndIndex; index += 1) {
|
||||||
|
nextSelectedIds.add(noteRows[index].id);
|
||||||
|
}
|
||||||
|
} else if (isModifierPressed) {
|
||||||
|
if (nextSelectedIds.has(noteId)) {
|
||||||
|
nextSelectedIds.delete(noteId);
|
||||||
|
} else {
|
||||||
|
nextSelectedIds.add(noteId);
|
||||||
|
}
|
||||||
|
rangeAnchorNoteIdRef.current = noteId;
|
||||||
|
} else {
|
||||||
|
nextSelectedIds.clear();
|
||||||
|
nextSelectedIds.add(noteId);
|
||||||
|
rangeAnchorNoteIdRef.current = noteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.shiftKey && rangeAnchorNoteIdRef.current === null) {
|
||||||
|
rangeAnchorNoteIdRef.current = noteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
commitSelection(nextSelectedIds);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTableBackgroundMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (event.target !== event.currentTarget) return;
|
||||||
|
rangeAnchorNoteIdRef.current = null;
|
||||||
|
commitSelection(new Set());
|
||||||
|
};
|
||||||
|
|
||||||
|
const quantizeSelectedNotes = (quantValue: string) => {
|
||||||
|
if (!activeMidiRegion || !parentTrack) return;
|
||||||
|
|
||||||
|
const denominator = parseInt(quantValue.split('/')[1], 10);
|
||||||
|
if (Number.isNaN(denominator)) return;
|
||||||
|
|
||||||
|
const selectedNotes = activeMidiRegion
|
||||||
|
.getNotes()
|
||||||
|
.filter(note => selectedNoteIdSet.has(note.getId()));
|
||||||
|
|
||||||
|
if (selectedNotes.length === 0) return;
|
||||||
|
|
||||||
|
const quantizationStep = 4 / denominator;
|
||||||
|
selectedNotes.forEach(note => {
|
||||||
|
const currentStartBeat = note.getStartBeat();
|
||||||
|
const duration = note.getEndBeat() - currentStartBeat;
|
||||||
|
const quantizedStartBeat = Math.round(currentStartBeat / quantizationStep) * quantizationStep;
|
||||||
|
note.setStartBeat(quantizedStartBeat);
|
||||||
|
note.setEndBeat(quantizedStartBeat + duration);
|
||||||
|
});
|
||||||
|
|
||||||
|
void updateTrack(parentTrack);
|
||||||
|
refreshProjectState();
|
||||||
|
};
|
||||||
|
|
||||||
|
const quantizeSelectedNoteLengths = (quantValue: string) => {
|
||||||
|
if (!activeMidiRegion || !parentTrack) return;
|
||||||
|
|
||||||
|
const denominator = parseInt(quantValue.split('/')[1], 10);
|
||||||
|
if (Number.isNaN(denominator)) return;
|
||||||
|
|
||||||
|
const selectedNotes = activeMidiRegion
|
||||||
|
.getNotes()
|
||||||
|
.filter(note => selectedNoteIdSet.has(note.getId()));
|
||||||
|
|
||||||
|
if (selectedNotes.length === 0) return;
|
||||||
|
|
||||||
|
const quantizationStep = 4 / denominator;
|
||||||
|
selectedNotes.forEach(note => {
|
||||||
|
const startBeat = note.getStartBeat();
|
||||||
|
const currentDuration = note.getEndBeat() - startBeat;
|
||||||
|
let quantizedDuration = currentDuration < quantizationStep
|
||||||
|
? quantizationStep
|
||||||
|
: Math.round(currentDuration / quantizationStep) * quantizationStep;
|
||||||
|
|
||||||
|
quantizedDuration = Math.max(PIANO_ROLL_CONSTANTS.MIN_NOTE_LENGTH, quantizedDuration);
|
||||||
|
note.setEndBeat(startBeat + quantizedDuration);
|
||||||
|
});
|
||||||
|
|
||||||
|
void updateTrack(parentTrack);
|
||||||
|
refreshProjectState();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`list-event-panel${isVisible ? '' : ' is-hidden'}`}>
|
||||||
|
<div className="list-event-panel-header">
|
||||||
|
<h3>List Event</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="list-event-panel-body">
|
||||||
|
<div className="list-event-tabs" role="tablist" aria-label="Event types">
|
||||||
|
<button className="list-event-tab active" aria-pressed="true">Notes</button>
|
||||||
|
<button className="list-event-tab" aria-pressed="false">Pitch Bends</button>
|
||||||
|
<button className="list-event-tab" aria-pressed="false">Controller</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!activeMidiRegion ? (
|
||||||
|
<div className="list-event-empty-state">
|
||||||
|
Please select a MIDI region, or open one in the Piano Roll, to view its event list.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="list-event-toolbar">
|
||||||
|
<div className="list-event-toolbar-group">
|
||||||
|
<button
|
||||||
|
className="list-event-add-button"
|
||||||
|
title="Add note UI only for now"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<FaPlus />
|
||||||
|
</button>
|
||||||
|
<KGDropdown
|
||||||
|
options={EVENT_TYPE_OPTIONS}
|
||||||
|
value={eventType}
|
||||||
|
onChange={setEventType}
|
||||||
|
label="Event Type"
|
||||||
|
buttonClassName="list-event-dropdown-button"
|
||||||
|
showValueAsLabel={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="list-event-toolbar-group list-event-toolbar-group-right">
|
||||||
|
<KGDropdown
|
||||||
|
options={KGPianoRollState.QUANT_POS_OPTIONS}
|
||||||
|
value={quantPosition}
|
||||||
|
onChange={(value) => {
|
||||||
|
setQuantPosition(value);
|
||||||
|
quantizeSelectedNotes(value);
|
||||||
|
}}
|
||||||
|
label="Qua. Pos."
|
||||||
|
buttonClassName="list-event-quant-button"
|
||||||
|
/>
|
||||||
|
<KGDropdown
|
||||||
|
options={KGPianoRollState.QUANT_LEN_OPTIONS}
|
||||||
|
value={quantLength}
|
||||||
|
onChange={(value) => {
|
||||||
|
setQuantLength(value);
|
||||||
|
quantizeSelectedNoteLengths(value);
|
||||||
|
}}
|
||||||
|
label="Qua. Len."
|
||||||
|
buttonClassName="list-event-quant-button"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="list-event-table-shell" onMouseDown={handleTableBackgroundMouseDown}>
|
||||||
|
<table className="list-event-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Position</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Num</th>
|
||||||
|
<th>Val</th>
|
||||||
|
<th>Length/Info</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{noteRows.map((row, index) => (
|
||||||
|
(() => {
|
||||||
|
const positionText = formatEventPosition(row.absoluteStartBeat, timeSignature);
|
||||||
|
const statusText = 'Note';
|
||||||
|
const noteText = pitchToNoteNameString(row.note.getPitch());
|
||||||
|
const velocityText = String(row.note.getVelocity());
|
||||||
|
const lengthText = formatEventLength(row.durationBeats);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={row.id}
|
||||||
|
className={selectedNoteIdSet.has(row.id) ? 'selected' : ''}
|
||||||
|
onClick={(event) => handleRowClick(row.id, index, event)}
|
||||||
|
>
|
||||||
|
<td title={positionText}>{positionText}</td>
|
||||||
|
<td title={statusText}>{statusText}</td>
|
||||||
|
<td title={noteText}>{noteText}</td>
|
||||||
|
<td title={velocityText}>{velocityText}</td>
|
||||||
|
<td title={lengthText}>{lengthText}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})()
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ListEventPanel;
|
||||||
@@ -18,7 +18,7 @@ import { KGProject, type KeySignature } from '../core/KGProject';
|
|||||||
import { KGMidiInput } from '../core/midi-input/KGMidiInput';
|
import { KGMidiInput } from '../core/midi-input/KGMidiInput';
|
||||||
import { KGMidiRegion } from '../core/region/KGMidiRegion';
|
import { KGMidiRegion } from '../core/region/KGMidiRegion';
|
||||||
import { plainToInstance } from 'class-transformer';
|
import { plainToInstance } from 'class-transformer';
|
||||||
import { FaPencil, FaCopy, FaPaste, FaTrash, FaWandMagicSparkles } from 'react-icons/fa6';
|
import { FaPencil, FaCopy, FaPaste, FaTrash, FaWandMagicSparkles, FaListUl } from 'react-icons/fa6';
|
||||||
import { KGMainContentState } from '../core/state/KGMainContentState';
|
import { KGMainContentState } from '../core/state/KGMainContentState';
|
||||||
import { regionDeleteManager } from '../util/regionDeleteUtil';
|
import { regionDeleteManager } from '../util/regionDeleteUtil';
|
||||||
import { SplitRegionCommand } from '../core/commands/region/SplitRegionCommand';
|
import { SplitRegionCommand } from '../core/commands/region/SplitRegionCommand';
|
||||||
@@ -48,7 +48,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, showKGOnePanel, cleanupProjectState, toggleMetronome, isMetronomeEnabled,
|
toggleChatBox, toggleSettings, toggleKGOnePanel, toggleListEventPanel, showKGOnePanel, showListEventPanel, showChatBox, 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,
|
||||||
@@ -939,6 +939,13 @@ const Toolbar: React.FC = () => {
|
|||||||
toggleKGOnePanel();
|
toggleKGOnePanel();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleListEventClick = () => {
|
||||||
|
if (DEBUG_MODE.TOOLBAR) {
|
||||||
|
console.log("List Event button clicked");
|
||||||
|
}
|
||||||
|
toggleListEventPanel();
|
||||||
|
};
|
||||||
|
|
||||||
// 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
|
||||||
const handlePianoButtonClick = async () => {
|
const handlePianoButtonClick = async () => {
|
||||||
if (DEBUG_MODE.TOOLBAR) {
|
if (DEBUG_MODE.TOOLBAR) {
|
||||||
@@ -1058,6 +1065,7 @@ const Toolbar: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button title="Import" onClick={handleImportProject}><FaUpload /></button>
|
<button title="Import" onClick={handleImportProject}><FaUpload /></button>
|
||||||
|
<button title="Settings" onClick={handleSettingsClick}><FaCog /></button>
|
||||||
<div className="toolbar-separator"></div>
|
<div className="toolbar-separator"></div>
|
||||||
<button title="Undo" onClick={handleUndoClick}><FaUndo /></button>
|
<button title="Undo" onClick={handleUndoClick}><FaUndo /></button>
|
||||||
<button title="Redo" onClick={handleRedoClick}><FaRedo /></button>
|
<button title="Redo" onClick={handleRedoClick}><FaRedo /></button>
|
||||||
@@ -1188,7 +1196,6 @@ const Toolbar: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button title="Settings" onClick={handleSettingsClick}><FaCog /></button>
|
|
||||||
<button
|
<button
|
||||||
title={isKGOneEnabled ? 'K.G.One Music Generator' : 'K.G.One integration is disabled — enable it in Settings'}
|
title={isKGOneEnabled ? 'K.G.One Music Generator' : 'K.G.One integration is disabled — enable it in Settings'}
|
||||||
onClick={handleKGOneClick}
|
onClick={handleKGOneClick}
|
||||||
@@ -1198,7 +1205,20 @@ const Toolbar: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<FaWandMagicSparkles />
|
<FaWandMagicSparkles />
|
||||||
</button>
|
</button>
|
||||||
<button title="Chat" onClick={handleChatClick}><FaComments /></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>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const isSpectrogram = mode === 'spectrogram';
|
const isSpectrogram = mode === 'spectrogram';
|
||||||
const isHybrid = mode === 'hybrid';
|
const isHybrid = mode === 'hybrid';
|
||||||
const { maxBars, tracks, updateTrack, timeSignature, showChatBox, showInstrumentSelection, keySignature, selectedMode, setSelectedMode, playheadPosition, isPlaying, autoScrollEnabled, bpm, pianoRollScrollRequest, selectedNoteIds } = useProjectStore();
|
const { maxBars, tracks, updateTrack, timeSignature, showChatBox, showKGOnePanel, showListEventPanel, showInstrumentSelection, keySignature, selectedMode, setSelectedMode, playheadPosition, isPlaying, autoScrollEnabled, bpm, pianoRollScrollRequest, selectedNoteIds } = 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');
|
||||||
@@ -140,7 +140,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
|||||||
const instrumentPanelWidth = parseInt(instrumentPanelWidthStr, 10) || 300;
|
const instrumentPanelWidth = parseInt(instrumentPanelWidthStr, 10) || 300;
|
||||||
|
|
||||||
let availableWidth = window.innerWidth;
|
let availableWidth = window.innerWidth;
|
||||||
if (showChatBox) availableWidth -= chatBoxWidth;
|
if (showChatBox || showKGOnePanel || showListEventPanel) availableWidth -= chatBoxWidth;
|
||||||
if (showInstrumentSelection) availableWidth -= instrumentPanelWidth;
|
if (showInstrumentSelection) availableWidth -= instrumentPanelWidth;
|
||||||
|
|
||||||
// Ensure a sensible minimum starting width
|
// Ensure a sensible minimum starting width
|
||||||
|
|||||||
@@ -87,6 +87,9 @@ interface ProjectState {
|
|||||||
// K.G.One panel state
|
// K.G.One panel state
|
||||||
showKGOnePanel: boolean;
|
showKGOnePanel: boolean;
|
||||||
|
|
||||||
|
// List event panel state
|
||||||
|
showListEventPanel: 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
|
||||||
@@ -169,6 +172,9 @@ interface ProjectState {
|
|||||||
// K.G.One panel actions
|
// K.G.One panel actions
|
||||||
toggleKGOnePanel: () => void;
|
toggleKGOnePanel: () => void;
|
||||||
|
|
||||||
|
// List event panel actions
|
||||||
|
toggleListEventPanel: () => void;
|
||||||
|
|
||||||
// Instrument selection panel actions
|
// Instrument selection panel actions
|
||||||
openInstrumentSelectionForTrack: () => void;
|
openInstrumentSelectionForTrack: () => void;
|
||||||
toggleInstrumentSelectionForTrack: () => void;
|
toggleInstrumentSelectionForTrack: () => void;
|
||||||
@@ -348,6 +354,9 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
|||||||
// Initial K.G.One panel state
|
// Initial K.G.One panel state
|
||||||
showKGOnePanel: false,
|
showKGOnePanel: false,
|
||||||
|
|
||||||
|
// Initial List Event panel state
|
||||||
|
showListEventPanel: false,
|
||||||
|
|
||||||
// Initial Instrument Selection panel state
|
// Initial Instrument Selection panel state
|
||||||
showInstrumentSelection: initialShowInstrumentSelection,
|
showInstrumentSelection: initialShowInstrumentSelection,
|
||||||
|
|
||||||
@@ -1130,17 +1139,26 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
|||||||
|
|
||||||
// ChatBox action implementations
|
// ChatBox action implementations
|
||||||
setShowChatBox: (show: boolean) => {
|
setShowChatBox: (show: boolean) => {
|
||||||
set({ showChatBox: show });
|
set({
|
||||||
|
showChatBox: show,
|
||||||
|
showKGOnePanel: show ? false : get().showKGOnePanel,
|
||||||
|
showListEventPanel: show ? false : get().showListEventPanel
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleChatBox: () => {
|
toggleChatBox: () => {
|
||||||
const { showChatBox } = get();
|
const { showChatBox } = get();
|
||||||
set({ showChatBox: !showChatBox, showKGOnePanel: false });
|
set({ showChatBox: !showChatBox, showKGOnePanel: false, showListEventPanel: false });
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleKGOnePanel: () => {
|
toggleKGOnePanel: () => {
|
||||||
const { showKGOnePanel } = get();
|
const { showKGOnePanel } = get();
|
||||||
set({ showKGOnePanel: !showKGOnePanel, showChatBox: false });
|
set({ showKGOnePanel: !showKGOnePanel, showChatBox: false, showListEventPanel: false });
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleListEventPanel: () => {
|
||||||
|
const { showListEventPanel } = get();
|
||||||
|
set({ showListEventPanel: !showListEventPanel, showChatBox: false, showKGOnePanel: false });
|
||||||
},
|
},
|
||||||
|
|
||||||
// Instrument selection panel actions
|
// Instrument selection panel actions
|
||||||
@@ -1281,4 +1299,3 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user