Skip to content

Commit 9376d5d

Browse files
cale-bradburyclaude
andcommitted
Fix timeline track selection issues from Copilot review
- Coalesce selectedTimelineTrackId to null when loading a project file saved before this field existed, so it can't leak undefined into state. - Require both selectedTrackId and setSelectedTrackId before Timeline enters controlled mode, so passing only the setter can't strand selection at null. - Route [i]/[x] keydown handling through a shared stack so only the most recently mounted Timeline instance responds, preventing double-inserts/deletes when the global and per-input panels are both mounted. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 357dcf4 commit 9376d5d

2 files changed

Lines changed: 28 additions & 3 deletions

File tree

apps/desktop/src/renderer/handlers/fileHandlers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export const handleLoadProjectDialog = async (projectPath?: string) => {
7676
...state,
7777
currentSavePath: savePath,
7878
...projectData.app,
79+
selectedTimelineTrackId: projectData.app.selectedTimelineTrackId ?? null,
7980
}))
8081

8182
// Add/remove shots from sketches based on their current module configs, in case files were updated since last load

packages/timeline/src/components/Timeline/Timeline.tsx

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,30 @@ export interface TimelineProps {
2323
onKeyframeInsert?: (trackId: string, time: number) => void
2424
}
2525

26+
// Multiple Timeline instances can be mounted at once (e.g. the global timeline panel
27+
// plus a per-input panel). Only the most recently mounted one should act on [i]/[x],
28+
// otherwise both fire and e.g. double-insert the same keyframe.
29+
type KeydownHandler = (e: KeyboardEvent) => void
30+
const keydownHandlerStack: KeydownHandler[] = []
31+
let sharedKeydownListenerInstalled = false
32+
33+
function ensureSharedKeydownListener() {
34+
if (sharedKeydownListenerInstalled) return
35+
sharedKeydownListenerInstalled = true
36+
window.addEventListener('keydown', (e) => {
37+
keydownHandlerStack[keydownHandlerStack.length - 1]?.(e)
38+
})
39+
}
40+
41+
function registerKeydownHandler(handler: KeydownHandler) {
42+
ensureSharedKeydownListener()
43+
keydownHandlerStack.push(handler)
44+
return () => {
45+
const index = keydownHandlerStack.indexOf(handler)
46+
if (index !== -1) keydownHandlerStack.splice(index, 1)
47+
}
48+
}
49+
2650
export function Timeline({
2751
timeline,
2852
playheadPositionMs = 0,
@@ -37,7 +61,8 @@ export function Timeline({
3761
const [uncontrolledSelectedTrackId, setUncontrolledSelectedTrackId] = useState<string | null>(
3862
null,
3963
)
40-
const isControlled = controlledSetSelectedTrackId !== undefined
64+
const isControlled =
65+
controlledSelectedTrackId !== undefined && controlledSetSelectedTrackId !== undefined
4166
const selectedTrackId = isControlled
4267
? (controlledSelectedTrackId ?? null)
4368
: uncontrolledSelectedTrackId
@@ -82,8 +107,7 @@ export function Timeline({
82107
}
83108
}
84109
}
85-
window.addEventListener('keydown', handleKeyDown)
86-
return () => window.removeEventListener('keydown', handleKeyDown)
110+
return registerKeydownHandler(handleKeyDown)
87111
}, [
88112
selectedKeyframes,
89113
onKeyframeDelete,

0 commit comments

Comments
 (0)