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:
@@ -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 {
|
||||||
@@ -596,29 +596,21 @@ const Toolbar: React.FC = () => {
|
|||||||
if (DEBUG_MODE.TOOLBAR) {
|
if (DEBUG_MODE.TOOLBAR) {
|
||||||
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
|
const newTimeSignature = parseTimeSignature(`${result.numerator}/${result.denominator}`);
|
||||||
if (newTimeSignatureStr === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse and validate time signature
|
|
||||||
const newTimeSignature = parseTimeSignature(newTimeSignatureStr);
|
|
||||||
|
|
||||||
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}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -34,21 +40,35 @@ export function showConfirm(message: string, options?: ConfirmOptions): Promise<
|
|||||||
|
|
||||||
export function showPrompt(message: string, defaultValue?: string, options?: PromptOptions): Promise<string | null> {
|
export function showPrompt(message: string, defaultValue?: string, options?: PromptOptions): Promise<string | null> {
|
||||||
if (!_showPromptFn) {
|
if (!_showPromptFn) {
|
||||||
return Promise.resolve(window.prompt(message, defaultValue) );
|
return Promise.resolve(window.prompt(message, defaultValue));
|
||||||
}
|
}
|
||||||
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>
|
||||||
|
|||||||
@@ -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';
|
||||||
Reference in New Issue
Block a user