/* * Copyright (c) 2025 Robin E. R. Davies * All rights reserved. * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ import React, { useState } from 'react'; import DialogEx from './DialogEx'; import DialogTitle from '@mui/material/DialogTitle'; import DialogContent from '@mui/material/DialogContent'; import Button from '@mui/material/Button'; import Select from '@mui/material/Select'; import MenuItem from '@mui/material/MenuItem'; import TextField, { StandardTextFieldProps } from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; import Box from '@mui/material/Box'; import Toolbar from '@mui/material/Toolbar'; import IconButtonEx from './IconButtonEx'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import Timebase, { TimebaseUnits } from './Timebase'; interface TimebaseSelectorDialogProps { timebase: Timebase; onTimebaseChange: (timebase: Timebase) => void; } interface NumericEditProps extends StandardTextFieldProps { value: number; onValueChange: (value: number) => void; parse: (value: string) => number; min: number; max: number; step?: number; style?: React.CSSProperties; } function NumericEdit(props: NumericEditProps) { let { value, min, max, parse, step, onValueChange,style, ...extras } = props; let [text, setText] = React.useState(value.toString()); let [error, setError] = React.useState(false); let [focus, setFocus] = React.useState(false); React.useEffect(() => { if (!focus) { setText(value.toString()); setError(false); } }, [value]); const handleChange = (event: React.ChangeEvent) => { let newValue = parse(event.target.value); if (!isNaN(newValue) && newValue >= min && newValue <= max) { setText(event.target.value); onValueChange(newValue); setError(false); } else { setText(event.target.value); setError(true); } }; return ( { setFocus(true); e.target.select(); }} onBlur={() => { setFocus(false); let newValue = parse(text); if (isNaN(newValue)) { setText(value.toString()); } else if (newValue < min) { newValue = min; setText(value.toString()); onValueChange(newValue); } else if (newValue > max) { newValue = max; setText(newValue.toString()); onValueChange(value) } else { setText(newValue.toString()); onValueChange(newValue); } setError(false); }} /> ); } export default function TimebaseSelectorDialog(props: TimebaseSelectorDialogProps) { const { timebase, onTimebaseChange } = props; const [open, setOpen] = useState(false); const [editingTimebase, setEditingTimebase] = useState({ ...timebase }); const handleOpen = () => { setEditingTimebase({ ...timebase }); setOpen(true); }; const handleClose = () => { setOpen(false); }; const enableBeatControls = editingTimebase.units === TimebaseUnits.Beats; const handleChange = (timebase: Timebase) => { onTimebaseChange(timebase); }; const handleTimebaseTypeChange = (event: any) => { let value = { ...editingTimebase, units: event.target.value as TimebaseUnits }; setEditingTimebase(value); handleChange(value); }; const handleTempoChange = (tempo: number) => { let value = { ...editingTimebase, tempo: tempo }; setEditingTimebase(value); handleChange(value); }; const handleTimeSignatureChange = (field: 'numerator' | 'denominator', numVal: number) => { let value = { ...editingTimebase, timeSignature: { ...editingTimebase.timeSignature, [field]: numVal } }; setEditingTimebase(value); handleChange(value); }; const getTimebaseTypeLabel = (timebase: Timebase): string => { switch (timebase.units) { case TimebaseUnits.Seconds: return 'Seconds'; case TimebaseUnits.Samples: return 'Samples'; case TimebaseUnits.Beats: { return timebase.tempo.toString() + " bpm (" + timebase.timeSignature.numerator.toString() + "/" + timebase.timeSignature.denominator.toString() + ")"; } default: return 'Unknown'; } }; return ( <> { }} > { handleClose(); }} > Timebase Settings
Units
Tempo (BPM) parseFloat(value)} min={10} max={400} step={1} onValueChange={handleTempoChange} value={editingTimebase.tempo} disabled={!enableBeatControls} variant="standard" type="number" style={{ width: 120, }} /> Time Signature
parseInt(value)} min={1} max={32} step={1} onValueChange={(value) => handleTimeSignatureChange('numerator', value)} value={editingTimebase.timeSignature.numerator} style={{ width: 80 }} variant="standard" type="number" disabled={!enableBeatControls} />  / 
); }