feat: use embedded inputbox for track name editing and region name editing
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user