diff --git a/CHANGELOG.md b/CHANGELOG.md index b3602f2b3..bc10802f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md ## [Unreleased] - Separate Scene controls and text presentation from project/playback operations; revoke replaced row listeners and suppress stale completion feedback. +- Preserve shot-label identity on selection so double-click rename can complete. - Split Cockpit camera/controller, instruments, briefing, signals and layout into focused modules with explicit application operations. - Give Display portal moves cancellable focus/scroll restoration and stop Cockpit work before asynchronous UI teardown. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 5ff2468c2..7bf3b3b00 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -5,7 +5,8 @@ Scene controls own creation/deletion prompts, panel listeners, shot rows, playback/recording presentation and keyboard cancellation. The director supplies project reads and explicit editing/playback actions while retaining persistence, camera and layer sequencing. -Replacing rows revokes their old listeners. Disposal stops controls immediately; +Shot selection updates the highlight without replacing the row, preserving +native double-click rename. Replacing rows revokes their old listeners. Disposal stops controls immediately; late file and failed-action completions cannot update removed presentation. ## Cockpit component ownership diff --git a/src/scenes/director.js b/src/scenes/director.js index d98929260..69af3429f 100644 --- a/src/scenes/director.js +++ b/src/scenes/director.js @@ -409,7 +409,7 @@ export class SceneDirector { this._selectedShotId = this._getSelectedScene()?.shots[0]?.id || null; this._renderShotList(); }, - selectShot: (id) => { this._selectedShotId = id; this._renderShotList(); }, + selectShot: (id) => { this._selectedShotId = id; }, renameShot: (sceneId, shotId, title) => { const { shot } = this._getShot(sceneId, shotId); if (!shot) return; diff --git a/src/ui/sceneControls.js b/src/ui/sceneControls.js index 781310436..ec1a99d3b 100644 --- a/src/ui/sceneControls.js +++ b/src/ui/sceneControls.js @@ -3,6 +3,7 @@ import { sceneElements, renderSceneOptions, renderSceneShots, + presentSceneSelection, presentSceneButtons, presentSceneProgress, presentSceneRuntime, @@ -112,7 +113,14 @@ export class SceneControls { renderSceneShots(this.elements.shots, this.read(), { listen: (element, type, callback) => this.listen(element, type, callback, this.rowRemovers), - select: (id) => this.run('selectShot', id), + select: (id) => { + this.run('selectShot', id); + if (!this.destroyed) + presentSceneSelection( + this.elements.shots, + this.read().selectedShotId, + ); + }, rename: (sceneId, shotId, title) => this.run('renameShot', sceneId, shotId, title), load: (sceneId, shotId) => this.run('load', sceneId, shotId), diff --git a/src/ui/sceneControls.test.mjs b/src/ui/sceneControls.test.mjs index 8194264ed..ca6974ded 100644 --- a/src/ui/sceneControls.test.mjs +++ b/src/ui/sceneControls.test.mjs @@ -1,6 +1,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { SceneControls } from './sceneControls.js'; +import { SceneDirector } from '../scenes/director.js'; function fixture() { class Element extends EventTarget { @@ -8,6 +9,7 @@ function fixture() { super(); this.children = []; this.style = {}; + this.dataset = {}; this.value = ''; this._text = ''; const classes = new Set(); @@ -304,3 +306,46 @@ test('creation and deletion prompts precede their model actions and remain inert f.restore(); } }); + +test('the real director preserves a selected shot label for the following double-click', async () => { + const f = fixture(); + const previousStorage = globalThis.localStorage; + let director; + try { + f.owner.destroy(); + const ids = { + 'scene-panel': 'panel', + 'scene-select': 'select', + 'scene-new-btn': 'new', + 'scene-delete-btn': 'delete', + 'scene-capture-btn': 'capture', + 'scene-update-shot-btn': 'update', + 'scene-shot-list': 'shots', + 'scene-start-btn': 'start', + 'scene-stop-btn': 'stop', + 'scene-next-btn': 'next', + 'scene-export-btn': 'export', + 'scene-import-btn': 'import', + 'scene-import-file': 'file', + 'scene-download-btn': 'download', + 'scene-status': 'status', + 'scene-progress-fill': 'progress', + 'scene-runtime': 'runtime', + }; + f.document.getElementById = (id) => f.elements[ids[id]] || null; + globalThis.localStorage = { + getItem: () => JSON.stringify({ version: 3, scenes: f.state.scenes }), + setItem() {}, + }; + director = new SceneDirector({ camera: { cancelFlight() {} } }, {}, {}); + const label = f.elements.shots.children[0].children[0].children[0]; + label.click(); + assert.equal(f.elements.shots.children[0].children[0].children[0], label); + label.dispatchEvent(new Event('dblclick')); + assert.equal(director._getSelectedScene().shots[0].title, 'Renamed'); + } finally { + await director?.destroy(); + globalThis.localStorage = previousStorage; + f.restore(); + } +}); diff --git a/src/ui/scenePresentation.js b/src/ui/scenePresentation.js index d0c0530be..700fc63dc 100644 --- a/src/ui/scenePresentation.js +++ b/src/ui/scenePresentation.js @@ -54,6 +54,7 @@ export function renderSceneShots( for (const shot of scene.shots) { const row = document.createElement('div'); row.className = 'scene-shot-row'; + row.dataset.sceneShotId = shot.id; row.classList.toggle('active', shot.id === state.selectedShotId); const top = document.createElement('div'); top.className = 'scene-shot-top'; @@ -90,6 +91,12 @@ export function renderSceneShots( } } +export function presentSceneSelection(element, selectedShotId) { + for (const row of element?.children || []) { + row.classList.toggle('active', row.dataset.sceneShotId === selectedShotId); + } +} + export function presentSceneButtons(elements, running, hasRun) { for (const name of [ 'start',