feat: added i18n support; added Simplified Chinese support
This commit is contained in:
@@ -44,6 +44,7 @@ import { isModifierKeyPressed } from '../../util/osUtil';
|
||||
import { parseChordSymbol } from '../../util/chordUtil';
|
||||
import { showAlert } from '../../util/dialogUtil';
|
||||
import { getSortedKeySignatureRegions, getSortedTempoRegions } from '../../util/globalTrackUtil';
|
||||
import { useI18n } from '../../i18n/useI18n';
|
||||
|
||||
interface GlobalEventListTabProps {
|
||||
globalTracks: KGGlobalTrack[];
|
||||
@@ -97,13 +98,6 @@ interface GlobalEditingCell {
|
||||
value: string;
|
||||
}
|
||||
|
||||
const ADD_GLOBAL_ITEM_OPTIONS: Array<{ label: string; value: AddGlobalItemType }> = [
|
||||
{ label: 'Marker', value: 'marker' },
|
||||
{ label: 'Tempo', value: 'tempo' },
|
||||
{ label: 'Key Signature', value: 'key-signature' },
|
||||
{ label: 'Chord', value: 'chord' },
|
||||
];
|
||||
|
||||
const GLOBAL_TYPE_ORDER: Record<GlobalRowData['type'], number> = {
|
||||
marker: 0,
|
||||
tempo: 1,
|
||||
@@ -113,16 +107,16 @@ const GLOBAL_TYPE_ORDER: Record<GlobalRowData['type'], number> = {
|
||||
|
||||
const CANONICAL_KEY_SIGNATURES = Object.keys(KEY_SIGNATURE_MAP) as KeySignature[];
|
||||
|
||||
const getRowStatus = (row: GlobalRowData): string => {
|
||||
const getRowStatus = (row: GlobalRowData, t: (key: string, params?: Record<string, string | number>) => string): string => {
|
||||
switch (row.type) {
|
||||
case 'marker':
|
||||
return 'Marker';
|
||||
return t('eventList.global.status.marker');
|
||||
case 'tempo':
|
||||
return 'Tempo';
|
||||
return t('eventList.global.status.tempo');
|
||||
case 'key-signature':
|
||||
return 'Key Signature';
|
||||
return t('eventList.global.status.keySignature');
|
||||
case 'chord':
|
||||
return 'Chord';
|
||||
return t('eventList.global.status.chord');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -147,16 +141,22 @@ const findRegionRowType = (region: KGGlobalRegion): GlobalRowData['type'] | null
|
||||
return null;
|
||||
};
|
||||
|
||||
const buildValueValidationMessage = (type: GlobalRowData['type']): string => {
|
||||
const buildValueValidationMessage = (
|
||||
type: GlobalRowData['type'],
|
||||
t: (key: string, params?: Record<string, string | number>) => string
|
||||
): string => {
|
||||
switch (type) {
|
||||
case 'marker':
|
||||
return 'Please enter a marker label. Expected a non-empty text label. Example: Intro';
|
||||
return t('eventList.global.validation.marker');
|
||||
case 'tempo':
|
||||
return `Please enter a BPM value using digits only. Expected a whole number between ${TIME_CONSTANTS.MIN_BPM + 1} and ${TIME_CONSTANTS.MAX_BPM - 1}. Example: 128`;
|
||||
return t('eventList.global.validation.tempo', {
|
||||
min: TIME_CONSTANTS.MIN_BPM + 1,
|
||||
max: TIME_CONSTANTS.MAX_BPM - 1,
|
||||
});
|
||||
case 'key-signature':
|
||||
return 'Please enter one exact key signature name. Expected a canonical value such as "C major" or "F# minor". Example: F# minor';
|
||||
return t('eventList.global.validation.keySignature');
|
||||
case 'chord':
|
||||
return 'Please enter a valid chord symbol. Expected a chord representation the app can parse. Example: Bm7b5';
|
||||
return t('eventList.global.validation.chord');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -167,6 +167,7 @@ const GlobalEventListTab: React.FC<GlobalEventListTabProps> = ({
|
||||
playheadPosition,
|
||||
refreshProjectState,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const [showMarkers, setShowMarkers] = useState(true);
|
||||
const [showTempo, setShowTempo] = useState(true);
|
||||
const [showKeySignature, setShowKeySignature] = useState(true);
|
||||
@@ -178,6 +179,13 @@ const GlobalEventListTab: React.FC<GlobalEventListTabProps> = ({
|
||||
const suppressBlurCommitRef = useRef(false);
|
||||
const pendingSingleClickSelectionRef = useRef<number | null>(null);
|
||||
|
||||
const addGlobalItemOptions = useMemo<Array<{ label: string; value: AddGlobalItemType }>>(() => ([
|
||||
{ label: t('eventList.global.addType.marker'), value: 'marker' },
|
||||
{ label: t('eventList.global.addType.tempo'), value: 'tempo' },
|
||||
{ label: t('eventList.global.addType.keySignature'), value: 'key-signature' },
|
||||
{ label: t('eventList.global.addType.chord'), value: 'chord' },
|
||||
]), [t]);
|
||||
|
||||
const markerTrack = globalTracks.find(track => track.getType() === GlobalTrackType.Marker) ?? null;
|
||||
const tempoTrack = globalTracks.find(track => track.getType() === GlobalTrackType.Tempo) ?? null;
|
||||
const signatureTrack = globalTracks.find(track => track.getType() === GlobalTrackType.Signature) ?? null;
|
||||
@@ -366,7 +374,7 @@ const GlobalEventListTab: React.FC<GlobalEventListTabProps> = ({
|
||||
try {
|
||||
if (editingCell.column === 'position') {
|
||||
if (isDeltaEdit) {
|
||||
const parsed = parseMidiEventPositionDelta(trimmedValue, timeSignature, MIDI_EVENT_TICKS_PER_BEAT);
|
||||
const parsed = parseMidiEventPositionDelta(trimmedValue, timeSignature, MIDI_EVENT_TICKS_PER_BEAT);
|
||||
if ('error' in parsed) {
|
||||
await showAlert(parsed.error);
|
||||
return;
|
||||
@@ -375,7 +383,7 @@ const GlobalEventListTab: React.FC<GlobalEventListTabProps> = ({
|
||||
for (const targetRow of targetRows) {
|
||||
const nextBeat = targetRow.absoluteStartBeat + parsed.deltaBeats;
|
||||
if (nextBeat < 0) {
|
||||
await showAlert('Please enter a position at or after the start of the project. Expected a non-negative location. Example: 1 1 0');
|
||||
await showAlert(t('eventList.global.validation.position'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -404,7 +412,7 @@ const GlobalEventListTab: React.FC<GlobalEventListTabProps> = ({
|
||||
return;
|
||||
}
|
||||
if (parsed.absoluteBeat < 0) {
|
||||
await showAlert('Please enter a position at or after the start of the project. Expected a non-negative location. Example: 1 1 0');
|
||||
await showAlert(t('eventList.global.validation.position'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -432,7 +440,7 @@ const GlobalEventListTab: React.FC<GlobalEventListTabProps> = ({
|
||||
if (row.type === 'marker') {
|
||||
const normalized = trimmedValue.replace(/\r?\n/g, ' ').trim();
|
||||
if (!normalized) {
|
||||
await showAlert(buildValueValidationMessage('marker'));
|
||||
await showAlert(buildValueValidationMessage('marker', t));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -443,7 +451,7 @@ const GlobalEventListTab: React.FC<GlobalEventListTabProps> = ({
|
||||
}
|
||||
} else if (row.type === 'tempo') {
|
||||
if (!/^\d+$/.test(trimmedValue)) {
|
||||
await showAlert(buildValueValidationMessage('tempo'));
|
||||
await showAlert(buildValueValidationMessage('tempo', t));
|
||||
return;
|
||||
}
|
||||
const nextBpm = parseInt(trimmedValue, 10);
|
||||
@@ -452,7 +460,7 @@ const GlobalEventListTab: React.FC<GlobalEventListTabProps> = ({
|
||||
|| nextBpm <= TIME_CONSTANTS.MIN_BPM
|
||||
|| nextBpm >= TIME_CONSTANTS.MAX_BPM
|
||||
) {
|
||||
await showAlert(buildValueValidationMessage('tempo'));
|
||||
await showAlert(buildValueValidationMessage('tempo', t));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -463,7 +471,7 @@ const GlobalEventListTab: React.FC<GlobalEventListTabProps> = ({
|
||||
}
|
||||
} else if (row.type === 'key-signature') {
|
||||
if (!CANONICAL_KEY_SIGNATURES.includes(trimmedValue as KeySignature)) {
|
||||
await showAlert(buildValueValidationMessage('key-signature'));
|
||||
await showAlert(buildValueValidationMessage('key-signature', t));
|
||||
return;
|
||||
}
|
||||
const keySignature = trimmedValue as KeySignature;
|
||||
@@ -473,7 +481,7 @@ const GlobalEventListTab: React.FC<GlobalEventListTabProps> = ({
|
||||
}
|
||||
}
|
||||
} else if (parseChordSymbol(trimmedValue) === null) {
|
||||
await showAlert(buildValueValidationMessage('chord'));
|
||||
await showAlert(buildValueValidationMessage('chord', t));
|
||||
return;
|
||||
} else {
|
||||
for (const targetRow of targetRows) {
|
||||
@@ -494,7 +502,7 @@ const GlobalEventListTab: React.FC<GlobalEventListTabProps> = ({
|
||||
|
||||
for (const targetRow of targetRows) {
|
||||
if (targetRow.durationBeats + parsed.deltaBeats <= 0) {
|
||||
await showAlert('Please enter a positive length. Expected a duration greater than zero. Example: 4 0');
|
||||
await showAlert(t('eventList.global.validation.length'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -527,7 +535,7 @@ const GlobalEventListTab: React.FC<GlobalEventListTabProps> = ({
|
||||
return;
|
||||
}
|
||||
if (parsed.duration <= 0) {
|
||||
await showAlert('Please enter a positive length. Expected a duration greater than zero. Example: 4 0');
|
||||
await showAlert(t('eventList.global.validation.length'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -752,11 +760,11 @@ const GlobalEventListTab: React.FC<GlobalEventListTabProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="event-list-tabs" role="tablist" aria-label="Global event filters">
|
||||
<button className={`event-list-tab${showMarkers ? ' active' : ''}`} aria-pressed={showMarkers} type="button" onClick={() => setShowMarkers(value => !value)}>Marker</button>
|
||||
<button className={`event-list-tab${showTempo ? ' active' : ''}`} aria-pressed={showTempo} type="button" onClick={() => setShowTempo(value => !value)}>Tempo</button>
|
||||
<button className={`event-list-tab${showKeySignature ? ' active' : ''}`} aria-pressed={showKeySignature} type="button" onClick={() => setShowKeySignature(value => !value)}>Key Sig.</button>
|
||||
<button className={`event-list-tab${showChords ? ' active' : ''}`} aria-pressed={showChords} type="button" onClick={() => setShowChords(value => !value)}>Chord</button>
|
||||
<div className="event-list-tabs" role="tablist" aria-label={t('eventList.global.filters')}>
|
||||
<button className={`event-list-tab${showMarkers ? ' active' : ''}`} aria-pressed={showMarkers} type="button" onClick={() => setShowMarkers(value => !value)}>{t('eventList.global.filter.marker')}</button>
|
||||
<button className={`event-list-tab${showTempo ? ' active' : ''}`} aria-pressed={showTempo} type="button" onClick={() => setShowTempo(value => !value)}>{t('eventList.global.filter.tempo')}</button>
|
||||
<button className={`event-list-tab${showKeySignature ? ' active' : ''}`} aria-pressed={showKeySignature} type="button" onClick={() => setShowKeySignature(value => !value)}>{t('eventList.global.filter.keySignature')}</button>
|
||||
<button className={`event-list-tab${showChords ? ' active' : ''}`} aria-pressed={showChords} type="button" onClick={() => setShowChords(value => !value)}>{t('eventList.global.filter.chord')}</button>
|
||||
</div>
|
||||
|
||||
<div className="event-list-toolbar">
|
||||
@@ -765,12 +773,12 @@ const GlobalEventListTab: React.FC<GlobalEventListTabProps> = ({
|
||||
className="event-list-add-button"
|
||||
title={
|
||||
addGlobalItemType === 'marker'
|
||||
? 'Add marker region at playhead'
|
||||
? t('eventList.global.add.markerTitle')
|
||||
: addGlobalItemType === 'tempo'
|
||||
? 'Add tempo region at playhead'
|
||||
? t('eventList.global.add.tempoTitle')
|
||||
: addGlobalItemType === 'key-signature'
|
||||
? 'Add key signature region at playhead'
|
||||
: 'Add chord region at playhead'
|
||||
? t('eventList.global.add.keySignatureTitle')
|
||||
: t('eventList.global.add.chordTitle')
|
||||
}
|
||||
type="button"
|
||||
onClick={handleAddGlobalItem}
|
||||
@@ -778,10 +786,10 @@ const GlobalEventListTab: React.FC<GlobalEventListTabProps> = ({
|
||||
<FaPlus />
|
||||
</button>
|
||||
<KGDropdown
|
||||
options={ADD_GLOBAL_ITEM_OPTIONS}
|
||||
options={addGlobalItemOptions}
|
||||
value={addGlobalItemType}
|
||||
onChange={(value) => setAddGlobalItemType(value as AddGlobalItemType)}
|
||||
label="Add"
|
||||
label={t('eventList.global.add.label')}
|
||||
buttonClassName="event-list-type-button"
|
||||
showValueAsLabel
|
||||
/>
|
||||
@@ -790,7 +798,7 @@ const GlobalEventListTab: React.FC<GlobalEventListTabProps> = ({
|
||||
<div className="event-list-toolbar-group event-list-toolbar-group-right">
|
||||
<button
|
||||
className="event-list-delete-button"
|
||||
title="Delete visible selected rows"
|
||||
title={t('eventList.deleteVisibleSelectedRows')}
|
||||
type="button"
|
||||
onClick={handleDeleteSelectedRows}
|
||||
disabled={visibleSelectedRows.length === 0}
|
||||
@@ -804,16 +812,16 @@ const GlobalEventListTab: React.FC<GlobalEventListTabProps> = ({
|
||||
<table className="event-list-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Position</th>
|
||||
<th>Status</th>
|
||||
<th>Val</th>
|
||||
<th>Length/Info</th>
|
||||
<th>{t('eventList.table.position')}</th>
|
||||
<th>{t('eventList.table.status')}</th>
|
||||
<th>{t('eventList.table.val')}</th>
|
||||
<th>{t('eventList.table.lengthInfo')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{globalRows.map((row, index) => {
|
||||
const positionText = formatMidiEventPosition(row.absoluteStartBeat, timeSignature, MIDI_EVENT_TICKS_PER_BEAT);
|
||||
const statusText = getRowStatus(row);
|
||||
const statusText = getRowStatus(row, t);
|
||||
const valText = getRowValue(row);
|
||||
const lengthText = formatMidiEventLength(row.durationBeats, MIDI_EVENT_TICKS_PER_BEAT);
|
||||
const isEditingPosition = editingCell?.rowId === row.id && editingCell.column === 'position';
|
||||
|
||||
@@ -35,6 +35,7 @@ import { UpdateControllerEventPropertiesCommand } from '../../core/commands/note
|
||||
import { UpdateNotePropertiesCommand } from '../../core/commands/note/UpdateNotePropertiesCommand';
|
||||
import { UpdatePitchBendPropertiesCommand } from '../../core/commands/note/UpdatePitchBendPropertiesCommand';
|
||||
import { showAlert } from '../../util/dialogUtil';
|
||||
import { useI18n } from '../../i18n/useI18n';
|
||||
|
||||
interface RegionEventListTabProps {
|
||||
activeMidiRegion: KGMidiRegion | null;
|
||||
@@ -74,12 +75,6 @@ interface EditingCell {
|
||||
value: string;
|
||||
}
|
||||
|
||||
const ADD_EVENT_TYPE_OPTIONS = [
|
||||
{ label: 'Note', value: 'note' },
|
||||
{ label: 'Pitch Bend', value: 'pitch-bend' },
|
||||
{ label: 'Controller', value: 'controller' },
|
||||
] as const;
|
||||
|
||||
const parseVelocityInput = (raw: string): { velocity: number } | { error: string } => {
|
||||
const trimmed = raw.trim();
|
||||
if (!/^\d+$/.test(trimmed)) {
|
||||
@@ -182,6 +177,7 @@ const parseControllerValueDeltaInput = (raw: string): { delta: number } | { erro
|
||||
};
|
||||
|
||||
const RegionEventListTab: React.FC<RegionEventListTabProps> = ({ activeMidiRegion, parentTrack }) => {
|
||||
const { t } = useI18n();
|
||||
const {
|
||||
selectedNoteIds,
|
||||
selectedPitchBendIds,
|
||||
@@ -253,6 +249,11 @@ const RegionEventListTab: React.FC<RegionEventListTabProps> = ({ activeMidiRegio
|
||||
const selectedControllerEventIdSet = new Set(selectedControllerEventIds);
|
||||
const selectedEventIdSet = new Set([...selectedNoteIds, ...selectedPitchBendIds, ...selectedControllerEventIds]);
|
||||
const visibleSelectedRows = eventRows.filter(row => selectedEventIdSet.has(row.id));
|
||||
const addEventTypeOptions = [
|
||||
{ label: t('eventList.region.addType.note'), value: 'note' },
|
||||
{ label: t('eventList.region.addType.pitchBend'), value: 'pitch-bend' },
|
||||
{ label: t('eventList.region.addType.controller'), value: 'controller' },
|
||||
] as const;
|
||||
|
||||
useEffect(() => {
|
||||
if (editingCell) {
|
||||
@@ -941,15 +942,15 @@ const RegionEventListTab: React.FC<RegionEventListTabProps> = ({ activeMidiRegio
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="event-list-tabs" role="tablist" aria-label="Region event filters">
|
||||
<button className={`event-list-tab${showNotes ? ' active' : ''}`} type="button" onClick={() => setShowNotes(value => !value)}>Notes</button>
|
||||
<button className={`event-list-tab${showPitchBends ? ' active' : ''}`} type="button" onClick={() => setShowPitchBends(value => !value)}>Pitch Bends</button>
|
||||
<button className={`event-list-tab${showControllers ? ' active' : ''}`} type="button" onClick={() => setShowControllers(value => !value)}>Controller</button>
|
||||
<div className="event-list-tabs" role="tablist" aria-label={t('eventList.region.filters')}>
|
||||
<button className={`event-list-tab${showNotes ? ' active' : ''}`} type="button" onClick={() => setShowNotes(value => !value)}>{t('eventList.region.filter.notes')}</button>
|
||||
<button className={`event-list-tab${showPitchBends ? ' active' : ''}`} type="button" onClick={() => setShowPitchBends(value => !value)}>{t('eventList.region.filter.pitchBends')}</button>
|
||||
<button className={`event-list-tab${showControllers ? ' active' : ''}`} type="button" onClick={() => setShowControllers(value => !value)}>{t('eventList.region.filter.controller')}</button>
|
||||
</div>
|
||||
|
||||
{!activeMidiRegion ? (
|
||||
<div className="event-list-empty-state">
|
||||
Please select a MIDI region, or open one in the Piano Roll, to view its event list.
|
||||
{t('eventList.region.empty')}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -957,17 +958,23 @@ const RegionEventListTab: React.FC<RegionEventListTabProps> = ({ activeMidiRegio
|
||||
<div className="event-list-toolbar-group">
|
||||
<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'
|
||||
? t('eventList.region.add.noteTitle')
|
||||
: addEventType === 'pitch-bend'
|
||||
? t('eventList.region.add.pitchBendTitle')
|
||||
: t('eventList.region.add.controllerTitle')
|
||||
}
|
||||
type="button"
|
||||
onClick={handleAddEvent}
|
||||
>
|
||||
<FaPlus />
|
||||
</button>
|
||||
<KGDropdown
|
||||
options={[...ADD_EVENT_TYPE_OPTIONS]}
|
||||
options={[...addEventTypeOptions]}
|
||||
value={addEventType}
|
||||
onChange={(value) => setAddEventType(value as AddEventType)}
|
||||
label="Note"
|
||||
label={t('eventList.region.add.label')}
|
||||
buttonClassName="event-list-type-button"
|
||||
showValueAsLabel
|
||||
/>
|
||||
@@ -975,28 +982,28 @@ const RegionEventListTab: React.FC<RegionEventListTabProps> = ({ activeMidiRegio
|
||||
|
||||
<div className="event-list-toolbar-group event-list-toolbar-group-right">
|
||||
<KGDropdown
|
||||
options={KGPianoRollState.QUANT_POS_OPTIONS}
|
||||
options={KGPianoRollState.QUANT_POS_OPTIONS.map(option => ({ label: t(option.labelKey), value: option.value }))}
|
||||
value={quantPosition}
|
||||
onChange={(value) => {
|
||||
setQuantPosition(value);
|
||||
quantizeSelectedNotes(value);
|
||||
}}
|
||||
label="Qua. Pos."
|
||||
label={t('pianoRoll.quantizePositionCompact')}
|
||||
buttonClassName="event-list-quant-button"
|
||||
/>
|
||||
<KGDropdown
|
||||
options={KGPianoRollState.QUANT_LEN_OPTIONS}
|
||||
options={KGPianoRollState.QUANT_LEN_OPTIONS.map(option => ({ label: t(option.labelKey), value: option.value }))}
|
||||
value={quantLength}
|
||||
onChange={(value) => {
|
||||
setQuantLength(value);
|
||||
quantizeSelectedNoteLengths(value);
|
||||
}}
|
||||
label="Qua. Len."
|
||||
label={t('pianoRoll.quantizeLengthCompact')}
|
||||
buttonClassName="event-list-quant-button"
|
||||
/>
|
||||
<button
|
||||
className="event-list-delete-button"
|
||||
title="Delete visible selected rows"
|
||||
title={t('eventList.deleteVisibleSelectedRows')}
|
||||
type="button"
|
||||
onClick={handleDeleteSelectedRows}
|
||||
disabled={visibleSelectedRows.length === 0}
|
||||
@@ -1010,18 +1017,22 @@ const RegionEventListTab: React.FC<RegionEventListTabProps> = ({ activeMidiRegio
|
||||
<table className="event-list-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Position</th>
|
||||
<th>Status</th>
|
||||
<th>Num</th>
|
||||
<th>Val</th>
|
||||
<th>Length/Info</th>
|
||||
<th>{t('eventList.table.position')}</th>
|
||||
<th>{t('eventList.table.status')}</th>
|
||||
<th>{t('eventList.table.num')}</th>
|
||||
<th>{t('eventList.table.val')}</th>
|
||||
<th>{t('eventList.table.lengthInfo')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{eventRows.map((row, index) => {
|
||||
const absoluteBeat = row.type === 'note' ? row.absoluteStartBeat : row.absoluteBeat;
|
||||
const positionText = formatMidiEventPosition(absoluteBeat, timeSignature, MIDI_EVENT_TICKS_PER_BEAT);
|
||||
const statusText = row.type === 'note' ? 'Note' : row.type === 'pitch-bend' ? 'Pitch Bend' : 'Controller';
|
||||
const statusText = row.type === 'note'
|
||||
? t('eventList.region.status.note')
|
||||
: row.type === 'pitch-bend'
|
||||
? t('eventList.region.status.pitchBend')
|
||||
: t('eventList.region.status.controller');
|
||||
const numText = row.type === 'note'
|
||||
? pitchToNoteNameString(row.note.getPitch())
|
||||
: row.type === 'controller'
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
import { isModifierKeyPressed } from '../../util/osUtil';
|
||||
import { showAlert } from '../../util/dialogUtil';
|
||||
import { AUDIO_INTERFACE_CONSTANTS } from '../../constants/coreConstants';
|
||||
import { useI18n } from '../../i18n/useI18n';
|
||||
|
||||
interface TrackEventListTabProps {
|
||||
selectedTrack: KGMidiTrack | KGAudioTrack | null;
|
||||
@@ -119,6 +120,7 @@ const findPreviousPanValue = (points: KGTrackAutomationPoint[], beat: number): n
|
||||
};
|
||||
|
||||
const TrackEventListTab: React.FC<TrackEventListTabProps> = ({ selectedTrack }) => {
|
||||
const { t } = useI18n();
|
||||
const {
|
||||
tracks,
|
||||
playheadPosition,
|
||||
@@ -164,12 +166,12 @@ const TrackEventListTab: React.FC<TrackEventListTabProps> = ({ selectedTrack })
|
||||
const availableAddOptions = useMemo(() => {
|
||||
const options: Array<{ label: string; value: AddTrackItemType }> = [];
|
||||
if (selectedTrack instanceof KGMidiTrack) {
|
||||
options.push({ label: 'MIDI Region', value: 'midi-region' });
|
||||
options.push({ label: t('eventList.track.addType.midiRegion'), value: 'midi-region' });
|
||||
}
|
||||
options.push({ label: 'Volume', value: 'volume' });
|
||||
options.push({ label: 'Pan', value: 'pan' });
|
||||
options.push({ label: t('eventList.track.filter.volume'), value: 'volume' });
|
||||
options.push({ label: t('eventList.track.filter.pan'), value: 'pan' });
|
||||
return options;
|
||||
}, [selectedTrack]);
|
||||
}, [selectedTrack, t]);
|
||||
|
||||
const liveSelectedTrack = useMemo(() => {
|
||||
if (!selectedTrack) {
|
||||
@@ -659,15 +661,15 @@ const TrackEventListTab: React.FC<TrackEventListTabProps> = ({ selectedTrack })
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="event-list-tabs" role="tablist" aria-label="Track list modes">
|
||||
<button className={`event-list-tab${showRegions ? ' active' : ''}`} aria-pressed={showRegions} type="button" onClick={() => setShowRegions(value => !value)}>Regions</button>
|
||||
<button className={`event-list-tab${showVolume ? ' active' : ''}`} aria-pressed={showVolume} type="button" onClick={() => setShowVolume(value => !value)}>Volume</button>
|
||||
<button className={`event-list-tab${showPan ? ' active' : ''}`} aria-pressed={showPan} type="button" onClick={() => setShowPan(value => !value)}>Pan</button>
|
||||
<div className="event-list-tabs" role="tablist" aria-label={t('eventList.track.filters')}>
|
||||
<button className={`event-list-tab${showRegions ? ' active' : ''}`} aria-pressed={showRegions} type="button" onClick={() => setShowRegions(value => !value)}>{t('eventList.track.filter.regions')}</button>
|
||||
<button className={`event-list-tab${showVolume ? ' active' : ''}`} aria-pressed={showVolume} type="button" onClick={() => setShowVolume(value => !value)}>{t('eventList.track.filter.volume')}</button>
|
||||
<button className={`event-list-tab${showPan ? ' active' : ''}`} aria-pressed={showPan} type="button" onClick={() => setShowPan(value => !value)}>{t('eventList.track.filter.pan')}</button>
|
||||
</div>
|
||||
|
||||
{!liveSelectedTrack ? (
|
||||
<div className="event-list-empty-state">
|
||||
Please select a track to view regions and track automation.
|
||||
{t('eventList.track.empty')}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -675,7 +677,13 @@ const TrackEventListTab: React.FC<TrackEventListTabProps> = ({ selectedTrack })
|
||||
<div className="event-list-toolbar-group">
|
||||
<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'
|
||||
? t('eventList.track.add.midiRegionTitle')
|
||||
: addTrackItemType === 'volume'
|
||||
? t('eventList.track.add.volumeTitle')
|
||||
: t('eventList.track.add.panTitle')
|
||||
}
|
||||
type="button"
|
||||
onClick={handleAddTrackItem}
|
||||
>
|
||||
@@ -685,7 +693,7 @@ const TrackEventListTab: React.FC<TrackEventListTabProps> = ({ selectedTrack })
|
||||
options={availableAddOptions}
|
||||
value={addTrackItemType}
|
||||
onChange={(value) => setAddTrackItemType(value as AddTrackItemType)}
|
||||
label="Add"
|
||||
label={t('eventList.track.add.label')}
|
||||
buttonClassName="event-list-type-button"
|
||||
showValueAsLabel
|
||||
/>
|
||||
@@ -694,7 +702,7 @@ const TrackEventListTab: React.FC<TrackEventListTabProps> = ({ selectedTrack })
|
||||
<div className="event-list-toolbar-group event-list-toolbar-group-right">
|
||||
<button
|
||||
className="event-list-delete-button"
|
||||
title="Delete visible selected rows"
|
||||
title={t('eventList.deleteVisibleSelectedRows')}
|
||||
type="button"
|
||||
onClick={handleDeleteSelectedRows}
|
||||
disabled={visibleSelectedRows.length === 0}
|
||||
@@ -708,16 +716,20 @@ const TrackEventListTab: React.FC<TrackEventListTabProps> = ({ selectedTrack })
|
||||
<table className="event-list-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Position</th>
|
||||
<th>Status</th>
|
||||
<th>Val</th>
|
||||
<th>Length/Info</th>
|
||||
<th>{t('eventList.table.position')}</th>
|
||||
<th>{t('eventList.table.status')}</th>
|
||||
<th>{t('eventList.table.val')}</th>
|
||||
<th>{t('eventList.table.lengthInfo')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{trackRows.map((row, index) => {
|
||||
const positionText = formatMidiEventPosition(row.type === 'region' ? row.absoluteStartBeat : row.absoluteBeat, timeSignature, MIDI_EVENT_TICKS_PER_BEAT);
|
||||
const statusText = row.type === 'region' ? row.statusLabel : row.automationType === 'volume' ? 'Volume' : 'Pan';
|
||||
const statusText = row.type === 'region'
|
||||
? (row.statusLabel === 'Audio' ? t('eventList.track.status.audio') : t('eventList.track.status.midi'))
|
||||
: row.automationType === 'volume'
|
||||
? t('eventList.track.status.volume')
|
||||
: t('eventList.track.status.pan');
|
||||
const valText = row.type === 'region' ? row.region.getName() : formatTrackAutomationValue(row.automationType, row.point.getValue());
|
||||
const infoText = row.type === 'region' ? formatMidiEventLength(row.durationBeats, MIDI_EVENT_TICKS_PER_BEAT) : formatTrackAutomationInfo(row.automationType, row.point.getValue());
|
||||
const isEditingPosition = editingCell?.rowId === row.id && editingCell.column === 'position';
|
||||
|
||||
Reference in New Issue
Block a user