fix: added a special treatment for time signature input; fixed text selection in pop-up and release the mouse outside the pop-up will close the pop-up issue.

This commit is contained in:
Xiaohan-Tian
2026-04-20 00:02:47 -07:00
parent 1828dbbc63
commit 778c0236b6
4 changed files with 117 additions and 29 deletions
+5 -13
View File
@@ -31,7 +31,7 @@ import { clearChatHistoryAndUI } from '../util/chatUtil';
import PianoIcon from './common/icons/PianoIcon'; import PianoIcon from './common/icons/PianoIcon';
import MetronomeIcon from './common/icons/MetronomeIcon'; import MetronomeIcon from './common/icons/MetronomeIcon';
import { ConfigManager } from '../core/config/ConfigManager'; import { ConfigManager } from '../core/config/ConfigManager';
import { showAlert, showConfirm, showPrompt } from './common/DialogProvider'; import { showAlert, showConfirm, showPrompt, showTimeSigPrompt } from './common/DialogProvider';
const Toolbar: React.FC = () => { const Toolbar: React.FC = () => {
const { const {
@@ -597,28 +597,20 @@ const Toolbar: React.FC = () => {
console.log("Time signature clicked, current:", `${timeSignature.numerator}/${timeSignature.denominator}`); console.log("Time signature clicked, current:", `${timeSignature.numerator}/${timeSignature.denominator}`);
} }
const currentTimeSignatureStr = `${timeSignature.numerator}/${timeSignature.denominator}`; const result = await showTimeSigPrompt('Set the time signature:', timeSignature);
const newTimeSignatureStr = await showPrompt(`Enter new time signature (numerator/denominator):`, currentTimeSignatureStr); if (result === null) return;
// Check if user cancelled
if (newTimeSignatureStr === null) {
return;
}
// Parse and validate time signature
const newTimeSignature = parseTimeSignature(newTimeSignatureStr);
const newTimeSignature = parseTimeSignature(`${result.numerator}/${result.denominator}`);
if (newTimeSignature === null) { if (newTimeSignature === null) {
await showAlert(getTimeSignatureErrorMessage()); await showAlert(getTimeSignatureErrorMessage());
return; return;
} }
// Update time signature
setTimeSignature(newTimeSignature); setTimeSignature(newTimeSignature);
setStatus(`Time signature changed to ${newTimeSignature.numerator}/${newTimeSignature.denominator}`); setStatus(`Time signature changed to ${newTimeSignature.numerator}/${newTimeSignature.denominator}`);
if (DEBUG_MODE.TOOLBAR) { if (DEBUG_MODE.TOOLBAR) {
console.log(`Time signature updated from ${currentTimeSignatureStr} to ${newTimeSignature.numerator}/${newTimeSignature.denominator}`); console.log(`Time signature updated to ${newTimeSignature.numerator}/${newTimeSignature.denominator}`);
} }
}; };
+20
View File
@@ -101,6 +101,26 @@
border-color: #5a9fd4; border-color: #5a9fd4;
} }
.dialog-timesig-row {
display: flex;
align-items: center;
gap: 10px;
margin-top: 12px;
}
.dialog-timesig-input {
width: 72px;
margin-top: 0;
text-align: center;
}
.dialog-timesig-sep {
color: #b0b0b0;
font-size: 20px;
font-weight: 300;
line-height: 1;
}
.dialog-footer { .dialog-footer {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
+85 -9
View File
@@ -13,9 +13,15 @@ export interface PromptOptions {
placeholder?: string; placeholder?: string;
} }
export interface TimeSigResult {
numerator: number;
denominator: number;
}
let _showAlertFn: ((message: string) => Promise<void>) | null = null; let _showAlertFn: ((message: string) => Promise<void>) | null = null;
let _showConfirmFn: ((message: string, options?: ConfirmOptions) => Promise<boolean>) | null = null; let _showConfirmFn: ((message: string, options?: ConfirmOptions) => Promise<boolean>) | null = null;
let _showPromptFn: ((message: string, defaultValue?: string, options?: PromptOptions) => Promise<string | null>) | null = null; let _showPromptFn: ((message: string, defaultValue?: string, options?: PromptOptions) => Promise<string | null>) | null = null;
let _showTimeSigFn: ((message: string, defaultValue?: TimeSigResult) => Promise<TimeSigResult | null>) | null = null;
export function showAlert(message: string): Promise<void> { export function showAlert(message: string): Promise<void> {
if (!_showAlertFn) { if (!_showAlertFn) {
@@ -39,16 +45,30 @@ export function showPrompt(message: string, defaultValue?: string, options?: Pro
return _showPromptFn(message, defaultValue, options); return _showPromptFn(message, defaultValue, options);
} }
export function showTimeSigPrompt(message: string, defaultValue?: TimeSigResult): Promise<TimeSigResult | null> {
if (!_showTimeSigFn) {
const raw = window.prompt(message, defaultValue ? `${defaultValue.numerator}/${defaultValue.denominator}` : '4/4');
if (!raw) return Promise.resolve(null);
const [n, d] = raw.split('/').map(Number);
if (!n || !d) return Promise.resolve(null);
return Promise.resolve({ numerator: n, denominator: d });
}
return _showTimeSigFn(message, defaultValue);
}
interface DialogInfo { interface DialogInfo {
type: 'alert' | 'confirm' | 'prompt'; type: 'alert' | 'confirm' | 'prompt' | 'timesig';
message: string; message: string;
options?: ConfirmOptions | PromptOptions; options?: ConfirmOptions | PromptOptions;
defaultValue?: string; defaultValue?: string;
defaultTimeSig?: TimeSigResult;
} }
const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [dialog, setDialog] = useState<DialogInfo | null>(null); const [dialog, setDialog] = useState<DialogInfo | null>(null);
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
const [timeSigNumerator, setTimeSigNumerator] = useState('');
const [timeSigDenominator, setTimeSigDenominator] = useState('');
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const resolveRef = useRef<((value: any) => void) | null>(null); const resolveRef = useRef<((value: any) => void) | null>(null);
@@ -74,21 +94,35 @@ const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) =
}); });
}, []); }, []);
const openTimeSig = useCallback((message: string, defaultValue?: TimeSigResult): Promise<TimeSigResult | null> => {
return new Promise<TimeSigResult | null>((resolve) => {
resolveRef.current = resolve;
setTimeSigNumerator(String(defaultValue?.numerator ?? 4));
setTimeSigDenominator(String(defaultValue?.denominator ?? 4));
setDialog({ type: 'timesig', message, defaultTimeSig: defaultValue });
});
}, []);
const close = useCallback((value: unknown) => { const close = useCallback((value: unknown) => {
setDialog(null); setDialog(null);
setInputValue(''); setInputValue('');
setTimeSigNumerator('');
setTimeSigDenominator('');
if (resolveRef.current) { if (resolveRef.current) {
resolveRef.current(value); resolveRef.current(value);
resolveRef.current = null; resolveRef.current = null;
} }
}, []); }, []);
const mouseDownOnOverlay = useRef(false);
const registered = useRef(false); const registered = useRef(false);
if (!registered.current) { if (!registered.current) {
registered.current = true; registered.current = true;
_showAlertFn = openAlert; _showAlertFn = openAlert;
_showConfirmFn = openConfirm; _showConfirmFn = openConfirm;
_showPromptFn = openPrompt; _showPromptFn = openPrompt;
_showTimeSigFn = openTimeSig;
} }
if (!dialog) { if (!dialog) {
@@ -97,23 +131,37 @@ const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) =
const isAlert = dialog.type === 'alert'; const isAlert = dialog.type === 'alert';
const isPrompt = dialog.type === 'prompt'; const isPrompt = dialog.type === 'prompt';
const isTimeSig = dialog.type === 'timesig';
const promptOptions = isPrompt ? (dialog.options as PromptOptions | undefined) : undefined; const promptOptions = isPrompt ? (dialog.options as PromptOptions | undefined) : undefined;
const title = isAlert ? 'Notice' : isPrompt ? 'Input' : 'Confirm'; const title = isAlert ? 'Notice' : isTimeSig ? 'Time Signature' : isPrompt ? 'Input' : 'Confirm';
const handleOverlayMouseDown = (e: React.MouseEvent) => {
mouseDownOnOverlay.current = e.target === e.currentTarget;
};
const handleOverlayClick = (e: React.MouseEvent) => { const handleOverlayClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) { if (e.target === e.currentTarget && mouseDownOnOverlay.current) {
close(isAlert ? undefined : isPrompt ? null : false); close(isAlert ? undefined : (isPrompt || isTimeSig) ? null : false);
} }
}; };
const handleCancel = () => close(isAlert ? undefined : isPrompt ? null : false); const handleCancel = () => close(isAlert ? undefined : (isPrompt || isTimeSig) ? null : false);
const handleConfirm = () => close(isAlert ? undefined : isPrompt ? inputValue : true);
const handleConfirm = () => {
if (isAlert) { close(undefined); return; }
if (isPrompt) { close(inputValue); return; }
if (isTimeSig) {
close({ numerator: Number(timeSigNumerator), denominator: Number(timeSigDenominator) });
return;
}
close(true);
};
return ( return (
<> <>
{children} {children}
<div className="dialog-overlay" onClick={handleOverlayClick}> <div className="dialog-overlay" onMouseDown={handleOverlayMouseDown} onClick={handleOverlayClick}>
<div className="dialog-modal"> <div className="dialog-modal">
<div className="dialog-header"> <div className="dialog-header">
<h3 className="dialog-title">{title}</h3> <h3 className="dialog-title">{title}</h3>
@@ -141,6 +189,34 @@ const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) =
autoFocus autoFocus
/> />
)} )}
{isTimeSig && (
<div className="dialog-timesig-row">
<input
className="dialog-input dialog-timesig-input"
type="number"
min={1}
value={timeSigNumerator}
onChange={(e) => setTimeSigNumerator(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleConfirm();
if (e.key === 'Escape') handleCancel();
}}
autoFocus
/>
<span className="dialog-timesig-sep">/</span>
<input
className="dialog-input dialog-timesig-input"
type="number"
min={1}
value={timeSigDenominator}
onChange={(e) => setTimeSigDenominator(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleConfirm();
if (e.key === 'Escape') handleCancel();
}}
/>
</div>
)}
</div> </div>
<div className="dialog-footer"> <div className="dialog-footer">
{!isAlert && ( {!isAlert && (
@@ -154,9 +230,9 @@ const DialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) =
<button <button
className="dialog-btn dialog-btn-primary" className="dialog-btn dialog-btn-primary"
onClick={handleConfirm} onClick={handleConfirm}
autoFocus={!isPrompt} autoFocus={!isPrompt && !isTimeSig}
> >
{isAlert ? 'OK' : ((dialog.options as ConfirmOptions | PromptOptions | undefined)?.confirmLabel ?? (isPrompt ? 'OK' : 'Yes'))} {isAlert ? 'OK' : ((dialog.options as ConfirmOptions | PromptOptions | undefined)?.confirmLabel ?? (isPrompt || isTimeSig ? 'OK' : 'Yes'))}
</button> </button>
</div> </div>
</div> </div>
+2 -2
View File
@@ -3,5 +3,5 @@ export { default as Playhead } from './Playhead';
export { default as FileImportModal } from './FileImportModal'; export { default as FileImportModal } from './FileImportModal';
export { default as LoadingOverlay } from './LoadingOverlay'; export { default as LoadingOverlay } from './LoadingOverlay';
export { default as OpenProjectModal } from './OpenProjectModal'; export { default as OpenProjectModal } from './OpenProjectModal';
export { default as DialogProvider, showAlert, showConfirm, showPrompt } from './DialogProvider'; export { default as DialogProvider, showAlert, showConfirm, showPrompt, showTimeSigPrompt } from './DialogProvider';
export type { ConfirmOptions, PromptOptions } from './DialogProvider'; export type { ConfirmOptions, PromptOptions, TimeSigResult } from './DialogProvider';