diff --git a/docs/CODE-BOUNDARIES.md b/docs/CODE-BOUNDARIES.md index cce4e8f20..2ba08e77b 100644 --- a/docs/CODE-BOUNDARIES.md +++ b/docs/CODE-BOUNDARIES.md @@ -323,7 +323,7 @@ original Display groups, independent scroll positions and current focus owner. ## Scene controls -`ui/scenes` owns Scene panel input, project/shot rows, playback button and runtime +`ui/scenes` owns Scene prompts, panel input, project/shot rows, playback button and runtime presentation. It receives project reads and explicit actions, with no imports of the director, source modules, camera engine or storage. Replacement and disposal release listeners; pending action feedback is limited to its current owner. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 5e267ab59..5ff2468c2 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -2,7 +2,7 @@ ## Scene control ownership -Scene controls own panel listeners, shot rows, playback/recording presentation +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; diff --git a/src/scenes/director.js b/src/scenes/director.js index 9160fcc4d..d98929260 100644 --- a/src/scenes/director.js +++ b/src/scenes/director.js @@ -417,7 +417,7 @@ export class SceneDirector { this._saveProject(); this._renderShotList(); }, - create: () => this._createScene(), + create: (name) => this._createScene(name), deleteScene: () => this._deleteSelectedScene(), capture: () => this.captureShot(), update: () => this.updateSelectedShot(), @@ -474,9 +474,8 @@ export class SceneDirector { return { scene, shot }; } - /** Prompt the user for a name and append a new empty scene to the project. */ - _createScene() { - const sceneName = window.prompt('New scene name', `Scene ${this._project.scenes.length + 1}`); + /** Append a named empty scene after the controls accept the creation prompt. */ + _createScene(sceneName) { if (!sceneName) return; const scene = { @@ -498,9 +497,6 @@ export class SceneDirector { const scene = this._getSelectedScene(); if (!scene) return; - const ok = window.confirm(`Delete scene "${scene.title}" and all shots?`); - if (!ok) return; - this._project.scenes = this._project.scenes.filter((item) => item.id !== scene.id); // Restore default recipes if the user deleted all scenes if (!this._project.scenes.length) { @@ -596,9 +592,6 @@ export class SceneDirector { const { scene, shot } = this._getShot(sceneId, shotId); if (!scene || !shot) return; - const ok = window.confirm(`Delete shot "${shot.title}"?`); - if (!ok) return; - scene.shots = scene.shots.filter((item) => item.id !== shot.id); this._selectedShotId = scene.shots[0]?.id || null; this._saveProject(); diff --git a/src/ui/sceneControls.js b/src/ui/sceneControls.js index 0ce6d61dd..781310436 100644 --- a/src/ui/sceneControls.js +++ b/src/ui/sceneControls.js @@ -25,8 +25,6 @@ export class SceneControls { this.run('selectScene', elements.select.value), ); for (const [element, action] of Object.entries({ - new: 'create', - delete: 'deleteScene', capture: 'capture', update: 'update', next: 'next', @@ -35,6 +33,8 @@ export class SceneControls { })) { this.listen(elements[element], 'click', () => this.run(action)); } + this.listen(elements.new, 'click', () => this.createScene()); + this.listen(elements.delete, 'click', () => this.deleteSelectedScene()); this.listen(elements.start, 'click', () => this.run('start', this.read().selectedSceneId), ); @@ -79,6 +79,33 @@ export class SceneControls { if (!this.destroyed) renderSceneOptions(this.elements.select, this.read()); } + createScene() { + if (this.destroyed) return; + const name = window.prompt( + 'New scene name', + `Scene ${this.read().scenes.length + 1}`, + ); + if (name) this.run('create', name); + } + + deleteSelectedScene() { + if (this.destroyed) return; + const state = this.read(); + const scene = state.scenes.find( + (item) => item.id === state.selectedSceneId, + ); + if (scene && window.confirm(`Delete scene "${scene.title}" and all shots?`)) + this.run('deleteScene'); + } + + deleteShot(sceneId, shotId) { + if (this.destroyed) return; + const scene = this.read().scenes.find((item) => item.id === sceneId); + const shot = scene?.shots.find((item) => item.id === shotId); + if (shot && window.confirm(`Delete shot "${shot.title}"?`)) + this.run('deleteShot', sceneId, shotId); + } + renderShotList() { if (this.destroyed) return; for (const remove of this.rowRemovers.splice(0)) remove(); @@ -89,7 +116,7 @@ export class SceneControls { rename: (sceneId, shotId, title) => this.run('renameShot', sceneId, shotId, title), load: (sceneId, shotId) => this.run('load', sceneId, shotId), - remove: (sceneId, shotId) => this.run('deleteShot', sceneId, shotId), + remove: (sceneId, shotId) => this.deleteShot(sceneId, shotId), }); } diff --git a/src/ui/sceneControls.test.mjs b/src/ui/sceneControls.test.mjs index 34b450f6d..8194264ed 100644 --- a/src/ui/sceneControls.test.mjs +++ b/src/ui/sceneControls.test.mjs @@ -262,3 +262,45 @@ test('Scene action failures are visible only while that action still owns presen f.restore(); } }); + +test('creation and deletion prompts precede their model actions and remain inert after disposal', () => { + const f = fixture(); + try { + let prompts = 0; + window.prompt = () => { + prompts++; + return null; + }; + window.confirm = () => { + prompts++; + return false; + }; + f.elements.new.click(); + f.elements.delete.click(); + f.owner.deleteShot('scene-a', 'shot-a'); + assert.deepEqual(f.calls, []); + window.prompt = () => { + prompts++; + return 'New title'; + }; + window.confirm = () => { + prompts++; + return true; + }; + f.elements.new.click(); + f.elements.delete.click(); + f.owner.deleteShot('scene-a', 'shot-a'); + assert.deepEqual(f.calls, [ + ['create', 'New title'], + ['deleteScene'], + ['deleteShot', 'scene-a', 'shot-a'], + ]); + f.owner.destroy(); + f.owner.createScene(); + f.owner.deleteSelectedScene(); + f.owner.deleteShot('scene-a', 'shot-a'); + assert.equal(prompts, 6); + } finally { + f.restore(); + } +});