diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 9dff12d3..ba8f1f18 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -482,6 +482,15 @@ pub fn invoke(app: &mut FilmcraftApp, ctx: &egui::Context, id: &str, mut params: crate::panels::color_dialogs::open_sequence(app); return Ok(json!({"dialog": "sequenceColor"})); } + // New Bin from menus, shortcuts and the Project panel asks for the name (inline rename); + // agents pass `name` to name the bin directly + "file.newBin" if params.get("name").is_none() => { + let r = crate::panels::project::new_bin(app, ¶ms); + if let Err(e) = &r { + app.ui.status = e.clone(); + } + return r; + } // From menus/shortcuts (no params) these ask first; agents pass params to act directly. "file.revert" if params.as_object().is_none_or(|m| m.is_empty()) && app.session.is_dirty() => { if app.session.path.is_none() { diff --git a/crates/ui-egui/src/panels/project.rs b/crates/ui-egui/src/panels/project.rs index 09b04069..2a18137a 100644 --- a/crates/ui-egui/src/panels/project.rs +++ b/crates/ui-egui/src/panels/project.rs @@ -607,7 +607,7 @@ fn footer(app: &mut FilmcraftApp, ui: &mut egui::Ui, bar: Rect, v: &View, action "find" => actions.push(("edit.find".into(), json!({}))), "file.newBin" => { let parent = (v.bin != app.session.project.root.id).then_some(v.bin.0); - actions.push(("file.newBin".into(), json!({"name": "New Bin", "parent": parent}))); + actions.push(("file.newBin".into(), json!({"parent": parent, "panel": pre}))); } "project.delete" if app.session.state.project_selection.is_empty() => app.ui.status = tl!("Select items to clear").into(), _ => actions.push((id.into(), json!({}))), @@ -700,7 +700,7 @@ pub fn panel_menu(app: &mut FilmcraftApp, ui: &mut egui::Ui) -> bool { ui.separator(); if item(ui, "newBin", tl!("New Bin"), true, s_bin.as_deref()) { let parent = (v.bin != app.session.project.root.id).then_some(v.bin.0); - actions.push(("file.newBin".into(), json!({"name": "New Bin", "parent": parent}))); + actions.push(("file.newBin".into(), json!({"parent": parent}))); } if item(ui, "newBinFromSelection", tl!("New Bin From Selection"), !app.session.state.project_selection.is_empty(), Some("Shift+B")) { actions.push(("file.newBinFromSelection".into(), json!({}))); @@ -939,6 +939,32 @@ fn start_rename(app: &mut FilmcraftApp, params: &Value) -> Result Ok(json!({"item": item, "bin": bin})) } +/// New Bin from a menu, a shortcut or the panel's buttons (no `name`): make a "New Bin" in `parent` +/// (left out: the bin the Project panel shows; null: the project's root) and start renaming it, so +/// its name can be typed right away (#456). `panel` names the panel instance that edits the name. +/// Agents pass `name` to `file.newBin` to name the bin directly. +pub fn new_bin(app: &mut FilmcraftApp, params: &Value) -> Result { + let shown = view_of(app, shown_inst(app)); + let parent = match params.get("parent") { + None => (shown.bin != app.session.project.root.id).then_some(shown.bin.0), + Some(Value::Null) => None, + Some(p) => Some(p.as_u64().ok_or("`parent` must be a bin id")?), + }; + let r = app.session.execute("file.newBin", json!({"name": tl!("New Bin"), "parent": parent})).map_err(|e| e.to_string())?; + let bin = r.get("bin").and_then(Value::as_u64).ok_or("the new bin has no id")?; + let text = app.session.project.root.find_bin(BinId(bin)).map(|b| b.name.clone()).ok_or("no such bin")?; + // a bin made inside a collapsed bin of the list opens it, so the name field is visible + if let Some(p) = parent + && !app.ui.expanded_bins.contains(&p) + { + app.ui.expanded_bins.push(p); + } + let panel = params.get("panel").and_then(Value::as_str).map_or(shown.prefix, str::to_string); + app.ui.project_panel.selected_bin = Some(bin); + app.ui.project_panel.rename = Some(Rename { item: None, bin: Some(bin), text, panel }); + Ok(r) +} + /// Commit the inline rename (Enter / focus lost). pub fn commit_rename(app: &mut FilmcraftApp) { let Some(r) = app.ui.project_panel.rename.take() else { return }; diff --git a/crates/ui-egui/src/panels/project_views.rs b/crates/ui-egui/src/panels/project_views.rs index 7a54afae..af7911fa 100644 --- a/crates/ui-egui/src/panels/project_views.rs +++ b/crates/ui-egui/src/panels/project_views.rs @@ -311,7 +311,7 @@ fn bin_menu(app: &mut FilmcraftApp, ui: &mut egui::Ui, v: &View, bin: u64, actio ui.close(); } if ui.button(tl!("New Bin")).clicked() { - actions.push(("file.newBin".into(), json!({"name": "New Bin", "parent": bin}))); + actions.push(("file.newBin".into(), json!({"parent": bin, "panel": v.prefix}))); ui.close(); } } @@ -517,7 +517,7 @@ fn background_menu(app: &mut FilmcraftApp, ui: &mut egui::Ui, v: &View, bin: Opt (tl!("Import…"), "file.import"), ] { if ui.button(label).clicked() { - let p = if cmd == "file.newBin" { json!({"name": tl!("New Bin"), "parent": bin}) } else { json!({}) }; + let p = if cmd == "file.newBin" { json!({"parent": bin, "panel": v.prefix}) } else { json!({}) }; actions.push((cmd.into(), p)); ui.close(); } diff --git a/crates/ui-egui/tests/project_panel_ui.rs b/crates/ui-egui/tests/project_panel_ui.rs index bb504f32..1b978cf2 100644 --- a/crates/ui-egui/tests/project_panel_ui.rs +++ b/crates/ui-egui/tests/project_panel_ui.rs @@ -477,6 +477,26 @@ fn footer_buttons_are_wired() { assert_eq!(d.app().session.prefs.project_panel.view.icon_sort.column, "Name"); } +#[test] +fn new_bin_asks_for_its_name() { + // #456: New Bin opens the name field of the new bin instead of leaving it "New Bin" + let mut d = project_driver(); + d.exec("project.view.set", json!({"view": "list"})); + d.click("project.button.file.newBin"); + d.frames(2); + let bin = d.app().ui.project_panel.rename.as_ref().and_then(|r| r.bin).expect("the new bin's name is being edited"); + assert!(d.has("project.rename"), "the name field is shown"); + d.key("Cmd+A"); + d.ok("ui.type", json!({"text": "Interviews"})); + d.key("Enter"); + let name = d.app().session.project.root.find_bin(filmcraft_project::BinId(bin)).unwrap().name.clone(); + assert_eq!(name, "Interviews"); + // with a name (agents) the bin is named directly and nothing is edited + let r = d.ok("ui.menu.invoke", json!({"id": "file.newBin", "params": {"name": "B-roll"}})); + assert!(r["bin"].as_u64().is_some(), "{r}"); + assert!(d.app().ui.project_panel.rename.is_none()); +} + // ------------------------------------------------------------------------------------ Media Browser fn media_dir(tag: &str) -> std::path::PathBuf {