feat: use embedded inputbox for track name editing and region name editing

This commit is contained in:
Xiaohan-Tian
2026-05-24 23:33:54 -07:00
parent 27948771de
commit b80066c2a4
5 changed files with 204 additions and 53 deletions
+22 -1
View File
@@ -316,11 +316,32 @@
font-weight: bold; font-weight: bold;
color: #e0e0e0; color: #e0e0e0;
/* text-transform: uppercase; */ /* text-transform: uppercase; */
cursor: pointer; cursor: text;
padding: 5px; padding: 5px;
border-radius: 3px; border-radius: 3px;
} }
.piano-roll-title-input {
flex: 1;
min-width: 0;
margin: 0;
padding: 5px 8px;
box-sizing: border-box;
border: 1px solid #5a5a5a;
border-radius: 6px;
outline: none;
background-color: #4a4a4a;
color: #e0e0e0;
font-size: 12px;
font-weight: bold;
text-align: center;
}
.piano-roll-title-input:focus {
border-color: #5a9fd4;
background-color: #4a4a4a;
}
.close-button { .close-button {
background: transparent; background: transparent;
border: none; border: none;
+48 -18
View File
@@ -17,7 +17,7 @@ import { ConfigManager } from '../../core/config/ConfigManager';
import { beatsToBar } from '../../util/midiUtil'; import { beatsToBar } from '../../util/midiUtil';
import { UpdateRegionCommand } from '../../core/commands'; import { UpdateRegionCommand } from '../../core/commands';
import { getSuitableChords, noteNameToPitchClass } from '../../util/scaleUtil'; import { getSuitableChords, noteNameToPitchClass } from '../../util/scaleUtil';
import { showAlert, showPrompt } from '../../util/dialogUtil'; import { showAlert } from '../../util/dialogUtil';
import { import {
normalizeSpectrogramHeightResolution, normalizeSpectrogramHeightResolution,
type SpectrogramHeightResolution, type SpectrogramHeightResolution,
@@ -103,6 +103,9 @@ const PianoRoll: React.FC<PianoRollProps> = ({
const [sheetMusicTrackScopeEnabled, setSheetMusicTrackScopeEnabled] = useState(false); const [sheetMusicTrackScopeEnabled, setSheetMusicTrackScopeEnabled] = useState(false);
const [sheetQuantization, setSheetQuantization] = useState('16,48'); const [sheetQuantization, setSheetQuantization] = useState('16,48');
const [sheetMeasureMetrics, setSheetMeasureMetrics] = useState<SheetMeasureMetric[]>([]); const [sheetMeasureMetrics, setSheetMeasureMetrics] = useState<SheetMeasureMetric[]>([]);
const [isEditingTitle, setIsEditingTitle] = useState(false);
const [titleInputValue, setTitleInputValue] = useState('');
const titleInputRef = useRef<HTMLInputElement>(null);
// Quantization state // Quantization state
const [quantPosition, setQuantPosition] = useState<string>('1/8'); const [quantPosition, setQuantPosition] = useState<string>('1/8');
@@ -419,43 +422,64 @@ const PianoRoll: React.FC<PianoRollProps> = ({
}; };
}, [isDragging, isResizing, dragOffset, position]); }, [isDragging, isResizing, dragOffset, position]);
// Handle title click to rename the region useEffect(() => {
const handleTitleClick = async () => { if (!isEditingTitle && activeRegion) {
// If we were just dragging, don't show the rename dialog setTitleInputValue(activeRegion.getName());
if (wasDraggingRef.current) { }
if (DEBUG_MODE.PIANO_ROLL) { }, [activeRegion, isEditingTitle]);
console.log("Skipping rename dialog because the window was just dragged");
} const cancelTitleEdit = () => {
setTitleInputValue(activeRegion?.getName() ?? '');
setIsEditingTitle(false);
};
const commitTitleEdit = async () => {
if (!activeRegion) {
setIsEditingTitle(false);
return; return;
} }
if (!activeRegion) return; const newName = titleInputValue.trim();
setIsEditingTitle(false);
setTitleInputValue(activeRegion.getName());
// Show a prompt to get the new name if (!newName || newName === activeRegion.getName()) return;
const newName = await showPrompt("Enter a new name for the region:", activeRegion.getName());
// If the user clicked Cancel or entered an empty string, do nothing
if (!newName || newName.trim() === '' || newName === activeRegion.getName()) return;
// Use command pattern to update the region name with undo support
try { try {
const command = new UpdateRegionCommand(activeRegion.getId(), { name: newName.trim() }); const command = new UpdateRegionCommand(activeRegion.getId(), { name: newName });
KGCore.instance().executeCommand(command); KGCore.instance().executeCommand(command);
if (DEBUG_MODE.PIANO_ROLL) { if (DEBUG_MODE.PIANO_ROLL) {
console.log(`Executed UpdateRegionCommand: renamed region ${activeRegion.getId()} to "${newName}" using command pattern`); console.log(`Executed UpdateRegionCommand: renamed region ${activeRegion.getId()} to "${newName}" using command pattern`);
} }
// Update the store to trigger re-render
const updatedTracks = [...tracks]; const updatedTracks = [...tracks];
useProjectStore.setState({ tracks: updatedTracks }); useProjectStore.setState({ tracks: updatedTracks });
} catch (error) { } catch (error) {
console.error('Error renaming region:', error); console.error('Error renaming region:', error);
await showAlert('Failed to rename region. Please try again.'); await showAlert('Failed to rename region. Please try again.');
} }
}; };
// Handle title click to rename the region
const handleTitleClick = () => {
// If we were just dragging, don't show the rename dialog
if (wasDraggingRef.current) {
if (DEBUG_MODE.PIANO_ROLL) {
console.log("Skipping inline rename because the window was just dragged");
}
return;
}
if (!activeRegion) return;
setTitleInputValue(activeRegion.getName());
setIsEditingTitle(true);
window.setTimeout(() => {
titleInputRef.current?.focus();
titleInputRef.current?.select();
}, 0);
};
// Handle tool selection // Handle tool selection
const handleToolSelect = (tool: 'pointer' | 'pencil') => { const handleToolSelect = (tool: 'pointer' | 'pencil') => {
setActiveTool(tool); setActiveTool(tool);
@@ -1274,8 +1298,14 @@ const PianoRoll: React.FC<PianoRollProps> = ({
<PianoRollHeader <PianoRollHeader
onClose={onClose} onClose={onClose}
title={getPianoRollTitle()} title={getPianoRollTitle()}
isEditingTitle={isEditingTitle}
titleInputValue={titleInputValue}
onTitleClick={handleTitleClick} onTitleClick={handleTitleClick}
onTitleInputChange={setTitleInputValue}
onTitleCommit={() => { void commitTitleEdit(); }}
onTitleCancel={cancelTitleEdit}
onMouseDown={(e) => handleMouseDown(e, 'drag')} onMouseDown={(e) => handleMouseDown(e, 'drag')}
titleInputRef={titleInputRef}
/> />
<PianoRollToolbar <PianoRollToolbar
+46 -9
View File
@@ -4,28 +4,65 @@ import { FaTimes } from 'react-icons/fa';
interface PianoRollHeaderProps { interface PianoRollHeaderProps {
onClose: () => void; onClose: () => void;
title: string; title: string;
isEditingTitle: boolean;
titleInputValue: string;
onTitleClick: () => void; onTitleClick: () => void;
onTitleInputChange: (value: string) => void;
onTitleCommit: () => void;
onTitleCancel: () => void;
onMouseDown: (e: React.MouseEvent) => void; onMouseDown: (e: React.MouseEvent) => void;
titleInputRef: React.RefObject<HTMLInputElement | null>;
} }
const PianoRollHeader: React.FC<PianoRollHeaderProps> = ({ const PianoRollHeader: React.FC<PianoRollHeaderProps> = ({
onClose, onClose,
title, title,
isEditingTitle,
titleInputValue,
onTitleClick, onTitleClick,
onMouseDown onTitleInputChange,
onTitleCommit,
onTitleCancel,
onMouseDown,
titleInputRef
}) => { }) => {
return ( return (
<div <div
className="piano-roll-header" className="piano-roll-header"
onMouseDown={onMouseDown} onMouseDown={onMouseDown}
> >
<div {isEditingTitle ? (
className="piano-roll-title" <input
onClick={onTitleClick} ref={titleInputRef}
title="Click to rename region" className="piano-roll-title-input"
> type="text"
{title} value={titleInputValue}
</div> onChange={(e) => onTitleInputChange(e.target.value.replace(/\r?\n/g, ' '))}
onBlur={onTitleCommit}
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
e.stopPropagation();
onTitleCommit();
} else if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
onTitleCancel();
}
}}
/>
) : (
<div
className="piano-roll-title"
onClick={onTitleClick}
title="Click to rename region"
>
{title}
</div>
)}
<button <button
className="close-button" className="close-button"
onClick={onClose} onClick={onClose}
@@ -36,4 +73,4 @@ const PianoRollHeader: React.FC<PianoRollHeaderProps> = ({
); );
}; };
export default PianoRollHeader; export default PianoRollHeader;
+23 -10
View File
@@ -94,24 +94,37 @@
/* Remove drag indicator since hover cursor is sufficient */ /* Remove drag indicator since hover cursor is sufficient */
.track-name { .track-name {
display: flex;
align-items: center;
min-height: 24px;
font-size: 12px; font-size: 12px;
cursor: pointer; cursor: text;
position: relative;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.track-name:hover { .track-name-input {
color: #5a9fd4; width: 100%;
min-height: 24px;
min-width: 0;
margin: 0;
padding: 4px 8px;
box-sizing: border-box;
border: 1px solid #5a5a5a;
border-radius: 6px;
outline: none;
background-color: #4a4a4a;
color: #e0e0e0;
font-family: inherit;
font-size: 12px;
font-weight: inherit;
line-height: 1.2;
} }
.track-name:hover::before { .track-name-input:focus {
content: "\270E"; border-color: #5a9fd4;
position: absolute; background-color: #4a4a4a;
right: 0px;
font-size: 16px;
opacity: 1;
} }
.track-controls { .track-controls {
+65 -15
View File
@@ -12,7 +12,7 @@ import { FLUIDR3_INSTRUMENT_MAP } from '../../constants/generalMidiConstants';
import { DEBUG_MODE } from '../../constants/uiConstants'; import { DEBUG_MODE } from '../../constants/uiConstants';
import { KGAudioInterface } from '../../core/audio-interface/KGAudioInterface'; import { KGAudioInterface } from '../../core/audio-interface/KGAudioInterface';
import { AUDIO_INTERFACE_CONSTANTS } from '../../constants/coreConstants'; import { AUDIO_INTERFACE_CONSTANTS } from '../../constants/coreConstants';
import { showAlert, showConfirm, showPrompt } from '../../util/dialogUtil'; import { showAlert, showConfirm } from '../../util/dialogUtil';
import type { TrackAutomationType } from '../../core/track/KGTrackAutomationPoint'; import type { TrackAutomationType } from '../../core/track/KGTrackAutomationPoint';
const UNITY_POS = 750; const UNITY_POS = 750;
@@ -94,6 +94,9 @@ const TrackInfoItem: React.FC<TrackInfoItemProps> = ({
const [isEditingVolume, setIsEditingVolume] = useState(false); const [isEditingVolume, setIsEditingVolume] = useState(false);
const [volumeInputText, setVolumeInputText] = useState(''); const [volumeInputText, setVolumeInputText] = useState('');
const volumeInputRef = useRef<HTMLInputElement>(null); const volumeInputRef = useRef<HTMLInputElement>(null);
const [isEditingTrackName, setIsEditingTrackName] = useState(false);
const [trackNameInput, setTrackNameInput] = useState(track.getName());
const trackNameInputRef = useRef<HTMLInputElement>(null);
// Local flag to track slider interaction; not used for rendering // Local flag to track slider interaction; not used for rendering
const isAdjustingVolumeRef = useRef(false); const isAdjustingVolumeRef = useRef(false);
const [muted, setMuted] = useState(track.getMuted()); const [muted, setMuted] = useState(track.getMuted());
@@ -135,21 +138,43 @@ const TrackInfoItem: React.FC<TrackInfoItemProps> = ({
setVolume(track.getVolume()); setVolume(track.getVolume());
}, [allTracks, track]); }, [allTracks, track]);
useEffect(() => {
if (!isEditingTrackName) {
setTrackNameInput(track.getName());
}
}, [isEditingTrackName, track, allTracks]);
// Sync mute/solo UI with the track model on track/project changes // Sync mute/solo UI with the track model on track/project changes
useEffect(() => { useEffect(() => {
setMuted(track.getMuted()); setMuted(track.getMuted());
setSolo(track.getSolo()); setSolo(track.getSolo());
}, [allTracks, track]); }, [allTracks, track]);
// Handle track name edit within the component const beginTrackNameEdit = (e: React.MouseEvent) => {
const handleTrackNameClick = async (e: React.MouseEvent) => { e.stopPropagation();
e.stopPropagation(); // Prevent opening piano roll when clicking track name setTrackNameInput(track.getName());
setIsEditingTrackName(true);
window.setTimeout(() => {
trackNameInputRef.current?.focus();
trackNameInputRef.current?.select();
}, 0);
};
const newName = await showPrompt("Enter track name:", track.getName()); const cancelTrackNameEdit = () => {
if (newName) { setTrackNameInput(track.getName());
// Call the parent handler with the new name setIsEditingTrackName(false);
onTrackNameEdit(track, newName); };
const commitTrackNameEdit = () => {
const trimmedName = trackNameInput.trim();
setIsEditingTrackName(false);
setTrackNameInput(track.getName());
if (!trimmedName || trimmedName === track.getName()) {
return;
} }
onTrackNameEdit(track, trimmedName);
}; };
// Prevent drag reordering when interacting with interactive controls // Prevent drag reordering when interacting with interactive controls
@@ -388,13 +413,38 @@ const TrackInfoItem: React.FC<TrackInfoItemProps> = ({
)} )}
</div> </div>
<div className="track-name-and-controls"> <div className="track-name-and-controls">
<div {isEditingTrackName ? (
className="track-name" <input
onClick={handleTrackNameClick} ref={trackNameInputRef}
title={track.getName()} className="track-name-input"
> type="text"
{track.getName()} value={trackNameInput}
</div> onChange={(e) => setTrackNameInput(e.target.value.replace(/\r?\n/g, ' '))}
onBlur={commitTrackNameEdit}
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
e.stopPropagation();
commitTrackNameEdit();
} else if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
cancelTrackNameEdit();
}
}}
/>
) : (
<div
className="track-name"
onClick={beginTrackNameEdit}
title={track.getName()}
>
{track.getName()}
</div>
)}
<div className="volume-slider"> <div className="volume-slider">
<input <input
type="range" type="range"