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 LoadingOverlay from './components/common/LoadingOverlay';
|
||||
import KGOnePanel from './components/KGOnePanel';
|
||||
import ListEventPanel from './components/ListEventPanel';
|
||||
import { useEffect as useEffectReact, useState, useRef } from 'react';
|
||||
import { KGToneBuffersPool } from './core/audio-interface/KGToneBuffersPool';
|
||||
import { KGOfflineRenderer } from './core/audio-interface/KGOfflineRenderer';
|
||||
@@ -30,7 +31,7 @@ function App() {
|
||||
const {
|
||||
refreshStatus,
|
||||
loadProject, showChatBox, showSettings, setShowSettings, initializeFromConfig,
|
||||
showInstrumentSelection, showKGOnePanel
|
||||
showInstrumentSelection, showKGOnePanel, showListEventPanel
|
||||
} = useProjectStore();
|
||||
|
||||
// Track if app has been initialized to prevent multiple initializations
|
||||
@@ -168,6 +169,7 @@ function App() {
|
||||
</>
|
||||
)}
|
||||
<KGOnePanel isVisible={showKGOnePanel && !showSettings} />
|
||||
<ListEventPanel isVisible={showListEventPanel && !showSettings} />
|
||||
<ChatBox isVisible={showChatBox && !showSettings} />
|
||||
</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 { KGMidiRegion } from '../core/region/KGMidiRegion';
|
||||
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 { regionDeleteManager } from '../util/regionDeleteUtil';
|
||||
import { SplitRegionCommand } from '../core/commands/region/SplitRegionCommand';
|
||||
@@ -48,7 +48,7 @@ const Toolbar: React.FC = () => {
|
||||
barWidthMultiplier, setBarWidthMultiplier,
|
||||
isLooping, toggleLoop,
|
||||
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,
|
||||
// Piano roll state/actions
|
||||
showPianoRoll, setShowPianoRoll, activeRegionId, setActiveRegionId,
|
||||
@@ -939,6 +939,13 @@ const Toolbar: React.FC = () => {
|
||||
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
|
||||
const handlePianoButtonClick = async () => {
|
||||
if (DEBUG_MODE.TOOLBAR) {
|
||||
@@ -1058,6 +1065,7 @@ const Toolbar: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<button title="Import" onClick={handleImportProject}><FaUpload /></button>
|
||||
<button title="Settings" onClick={handleSettingsClick}><FaCog /></button>
|
||||
<div className="toolbar-separator"></div>
|
||||
<button title="Undo" onClick={handleUndoClick}><FaUndo /></button>
|
||||
<button title="Redo" onClick={handleRedoClick}><FaRedo /></button>
|
||||
@@ -1188,7 +1196,6 @@ const Toolbar: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button title="Settings" onClick={handleSettingsClick}><FaCog /></button>
|
||||
<button
|
||||
title={isKGOneEnabled ? 'K.G.One Music Generator' : 'K.G.One integration is disabled — enable it in Settings'}
|
||||
onClick={handleKGOneClick}
|
||||
@@ -1198,7 +1205,20 @@ const Toolbar: React.FC = () => {
|
||||
>
|
||||
<FaWandMagicSparkles />
|
||||
</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>
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
}) => {
|
||||
const isSpectrogram = mode === 'spectrogram';
|
||||
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
|
||||
const [activeTool, setActiveTool] = useState<'pointer' | 'pencil'>('pointer');
|
||||
@@ -140,7 +140,7 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
const instrumentPanelWidth = parseInt(instrumentPanelWidthStr, 10) || 300;
|
||||
|
||||
let availableWidth = window.innerWidth;
|
||||
if (showChatBox) availableWidth -= chatBoxWidth;
|
||||
if (showChatBox || showKGOnePanel || showListEventPanel) availableWidth -= chatBoxWidth;
|
||||
if (showInstrumentSelection) availableWidth -= instrumentPanelWidth;
|
||||
|
||||
// Ensure a sensible minimum starting width
|
||||
|
||||
@@ -87,6 +87,9 @@ interface ProjectState {
|
||||
// K.G.One panel state
|
||||
showKGOnePanel: boolean;
|
||||
|
||||
// List event panel state
|
||||
showListEventPanel: boolean;
|
||||
|
||||
// Instrument selection panel state
|
||||
showInstrumentSelection: boolean;
|
||||
// instrumentSelectionTrackId removed; panel now follows selectedTrackId
|
||||
@@ -169,6 +172,9 @@ interface ProjectState {
|
||||
// K.G.One panel actions
|
||||
toggleKGOnePanel: () => void;
|
||||
|
||||
// List event panel actions
|
||||
toggleListEventPanel: () => void;
|
||||
|
||||
// Instrument selection panel actions
|
||||
openInstrumentSelectionForTrack: () => void;
|
||||
toggleInstrumentSelectionForTrack: () => void;
|
||||
@@ -348,6 +354,9 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
||||
// Initial K.G.One panel state
|
||||
showKGOnePanel: false,
|
||||
|
||||
// Initial List Event panel state
|
||||
showListEventPanel: false,
|
||||
|
||||
// Initial Instrument Selection panel state
|
||||
showInstrumentSelection: initialShowInstrumentSelection,
|
||||
|
||||
@@ -1130,17 +1139,26 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
||||
|
||||
// ChatBox action implementations
|
||||
setShowChatBox: (show: boolean) => {
|
||||
set({ showChatBox: show });
|
||||
set({
|
||||
showChatBox: show,
|
||||
showKGOnePanel: show ? false : get().showKGOnePanel,
|
||||
showListEventPanel: show ? false : get().showListEventPanel
|
||||
});
|
||||
},
|
||||
|
||||
toggleChatBox: () => {
|
||||
const { showChatBox } = get();
|
||||
set({ showChatBox: !showChatBox, showKGOnePanel: false });
|
||||
set({ showChatBox: !showChatBox, showKGOnePanel: false, showListEventPanel: false });
|
||||
},
|
||||
|
||||
toggleKGOnePanel: () => {
|
||||
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
|
||||
@@ -1281,4 +1299,3 @@ export const useProjectStore = create<ProjectState>((set, get) => {
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user