feat: use embedded inputbox for track name editing and region name editing
This commit is contained in:
@@ -316,11 +316,32 @@
|
||||
font-weight: bold;
|
||||
color: #e0e0e0;
|
||||
/* text-transform: uppercase; */
|
||||
cursor: pointer;
|
||||
cursor: text;
|
||||
padding: 5px;
|
||||
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 {
|
||||
background: transparent;
|
||||
border: none;
|
||||
|
||||
@@ -17,7 +17,7 @@ import { ConfigManager } from '../../core/config/ConfigManager';
|
||||
import { beatsToBar } from '../../util/midiUtil';
|
||||
import { UpdateRegionCommand } from '../../core/commands';
|
||||
import { getSuitableChords, noteNameToPitchClass } from '../../util/scaleUtil';
|
||||
import { showAlert, showPrompt } from '../../util/dialogUtil';
|
||||
import { showAlert } from '../../util/dialogUtil';
|
||||
import {
|
||||
normalizeSpectrogramHeightResolution,
|
||||
type SpectrogramHeightResolution,
|
||||
@@ -103,6 +103,9 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
const [sheetMusicTrackScopeEnabled, setSheetMusicTrackScopeEnabled] = useState(false);
|
||||
const [sheetQuantization, setSheetQuantization] = useState('16,48');
|
||||
const [sheetMeasureMetrics, setSheetMeasureMetrics] = useState<SheetMeasureMetric[]>([]);
|
||||
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
||||
const [titleInputValue, setTitleInputValue] = useState('');
|
||||
const titleInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Quantization state
|
||||
const [quantPosition, setQuantPosition] = useState<string>('1/8');
|
||||
@@ -419,43 +422,64 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
};
|
||||
}, [isDragging, isResizing, dragOffset, position]);
|
||||
|
||||
// Handle title click to rename the region
|
||||
const handleTitleClick = async () => {
|
||||
// If we were just dragging, don't show the rename dialog
|
||||
if (wasDraggingRef.current) {
|
||||
if (DEBUG_MODE.PIANO_ROLL) {
|
||||
console.log("Skipping rename dialog because the window was just dragged");
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!isEditingTitle && activeRegion) {
|
||||
setTitleInputValue(activeRegion.getName());
|
||||
}
|
||||
}, [activeRegion, isEditingTitle]);
|
||||
|
||||
const cancelTitleEdit = () => {
|
||||
setTitleInputValue(activeRegion?.getName() ?? '');
|
||||
setIsEditingTitle(false);
|
||||
};
|
||||
|
||||
const commitTitleEdit = async () => {
|
||||
if (!activeRegion) {
|
||||
setIsEditingTitle(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!activeRegion) return;
|
||||
const newName = titleInputValue.trim();
|
||||
setIsEditingTitle(false);
|
||||
setTitleInputValue(activeRegion.getName());
|
||||
|
||||
// Show a prompt to get the new name
|
||||
const newName = await showPrompt("Enter a new name for the region:", activeRegion.getName());
|
||||
if (!newName || newName === activeRegion.getName()) return;
|
||||
|
||||
// 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 {
|
||||
const command = new UpdateRegionCommand(activeRegion.getId(), { name: newName.trim() });
|
||||
const command = new UpdateRegionCommand(activeRegion.getId(), { name: newName });
|
||||
KGCore.instance().executeCommand(command);
|
||||
|
||||
if (DEBUG_MODE.PIANO_ROLL) {
|
||||
console.log(`Executed UpdateRegionCommand: renamed region ${activeRegion.getId()} to "${newName}" using command pattern`);
|
||||
}
|
||||
|
||||
// Update the store to trigger re-render
|
||||
const updatedTracks = [...tracks];
|
||||
useProjectStore.setState({ tracks: updatedTracks });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error renaming region:', error);
|
||||
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
|
||||
const handleToolSelect = (tool: 'pointer' | 'pencil') => {
|
||||
setActiveTool(tool);
|
||||
@@ -1274,8 +1298,14 @@ const PianoRoll: React.FC<PianoRollProps> = ({
|
||||
<PianoRollHeader
|
||||
onClose={onClose}
|
||||
title={getPianoRollTitle()}
|
||||
isEditingTitle={isEditingTitle}
|
||||
titleInputValue={titleInputValue}
|
||||
onTitleClick={handleTitleClick}
|
||||
onTitleInputChange={setTitleInputValue}
|
||||
onTitleCommit={() => { void commitTitleEdit(); }}
|
||||
onTitleCancel={cancelTitleEdit}
|
||||
onMouseDown={(e) => handleMouseDown(e, 'drag')}
|
||||
titleInputRef={titleInputRef}
|
||||
/>
|
||||
|
||||
<PianoRollToolbar
|
||||
|
||||
@@ -4,28 +4,65 @@ import { FaTimes } from 'react-icons/fa';
|
||||
interface PianoRollHeaderProps {
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
isEditingTitle: boolean;
|
||||
titleInputValue: string;
|
||||
onTitleClick: () => void;
|
||||
onTitleInputChange: (value: string) => void;
|
||||
onTitleCommit: () => void;
|
||||
onTitleCancel: () => void;
|
||||
onMouseDown: (e: React.MouseEvent) => void;
|
||||
titleInputRef: React.RefObject<HTMLInputElement | null>;
|
||||
}
|
||||
|
||||
const PianoRollHeader: React.FC<PianoRollHeaderProps> = ({
|
||||
onClose,
|
||||
title,
|
||||
isEditingTitle,
|
||||
titleInputValue,
|
||||
onTitleClick,
|
||||
onMouseDown
|
||||
onTitleInputChange,
|
||||
onTitleCommit,
|
||||
onTitleCancel,
|
||||
onMouseDown,
|
||||
titleInputRef
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className="piano-roll-header"
|
||||
onMouseDown={onMouseDown}
|
||||
>
|
||||
<div
|
||||
className="piano-roll-title"
|
||||
onClick={onTitleClick}
|
||||
title="Click to rename region"
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
{isEditingTitle ? (
|
||||
<input
|
||||
ref={titleInputRef}
|
||||
className="piano-roll-title-input"
|
||||
type="text"
|
||||
value={titleInputValue}
|
||||
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
|
||||
className="close-button"
|
||||
onClick={onClose}
|
||||
@@ -36,4 +73,4 @@ const PianoRollHeader: React.FC<PianoRollHeaderProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default PianoRollHeader;
|
||||
export default PianoRollHeader;
|
||||
|
||||
@@ -94,24 +94,37 @@
|
||||
/* Remove drag indicator since hover cursor is sufficient */
|
||||
|
||||
.track-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
cursor: text;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.track-name:hover {
|
||||
color: #5a9fd4;
|
||||
.track-name-input {
|
||||
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 {
|
||||
content: "\270E";
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
font-size: 16px;
|
||||
opacity: 1;
|
||||
.track-name-input:focus {
|
||||
border-color: #5a9fd4;
|
||||
background-color: #4a4a4a;
|
||||
}
|
||||
|
||||
.track-controls {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { FLUIDR3_INSTRUMENT_MAP } from '../../constants/generalMidiConstants';
|
||||
import { DEBUG_MODE } from '../../constants/uiConstants';
|
||||
import { KGAudioInterface } from '../../core/audio-interface/KGAudioInterface';
|
||||
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';
|
||||
|
||||
const UNITY_POS = 750;
|
||||
@@ -94,6 +94,9 @@ const TrackInfoItem: React.FC<TrackInfoItemProps> = ({
|
||||
const [isEditingVolume, setIsEditingVolume] = useState(false);
|
||||
const [volumeInputText, setVolumeInputText] = useState('');
|
||||
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
|
||||
const isAdjustingVolumeRef = useRef(false);
|
||||
const [muted, setMuted] = useState(track.getMuted());
|
||||
@@ -135,21 +138,43 @@ const TrackInfoItem: React.FC<TrackInfoItemProps> = ({
|
||||
setVolume(track.getVolume());
|
||||
}, [allTracks, track]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditingTrackName) {
|
||||
setTrackNameInput(track.getName());
|
||||
}
|
||||
}, [isEditingTrackName, track, allTracks]);
|
||||
|
||||
// Sync mute/solo UI with the track model on track/project changes
|
||||
useEffect(() => {
|
||||
setMuted(track.getMuted());
|
||||
setSolo(track.getSolo());
|
||||
}, [allTracks, track]);
|
||||
|
||||
// Handle track name edit within the component
|
||||
const handleTrackNameClick = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation(); // Prevent opening piano roll when clicking track name
|
||||
const beginTrackNameEdit = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setTrackNameInput(track.getName());
|
||||
setIsEditingTrackName(true);
|
||||
window.setTimeout(() => {
|
||||
trackNameInputRef.current?.focus();
|
||||
trackNameInputRef.current?.select();
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const newName = await showPrompt("Enter track name:", track.getName());
|
||||
if (newName) {
|
||||
// Call the parent handler with the new name
|
||||
onTrackNameEdit(track, newName);
|
||||
const cancelTrackNameEdit = () => {
|
||||
setTrackNameInput(track.getName());
|
||||
setIsEditingTrackName(false);
|
||||
};
|
||||
|
||||
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
|
||||
@@ -388,13 +413,38 @@ const TrackInfoItem: React.FC<TrackInfoItemProps> = ({
|
||||
)}
|
||||
</div>
|
||||
<div className="track-name-and-controls">
|
||||
<div
|
||||
className="track-name"
|
||||
onClick={handleTrackNameClick}
|
||||
title={track.getName()}
|
||||
>
|
||||
{track.getName()}
|
||||
</div>
|
||||
{isEditingTrackName ? (
|
||||
<input
|
||||
ref={trackNameInputRef}
|
||||
className="track-name-input"
|
||||
type="text"
|
||||
value={trackNameInput}
|
||||
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">
|
||||
<input
|
||||
type="range"
|
||||
|
||||
Reference in New Issue
Block a user