Merge pull request #749 from vangel76/fix-456-new-bin-name

#456: a new bin asks for its name
This commit is contained in:
Brandon Thomas 2026-10-10 15:52:55 -04:00 committed by GitHub
commit a270f5cf34
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 59 additions and 4 deletions

View File

@ -483,6 +483,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, &params);
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() {

View File

@ -622,7 +622,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" => match crate::panels::project_views::clear_params(&app.session.state.project_selection, app.ui.project_panel.selected_bin) {
Some(p) => actions.push((id.into(), p)),
@ -719,7 +719,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!({})));
@ -958,6 +958,32 @@ fn start_rename(app: &mut FilmcraftApp, params: &Value) -> Result<Value, String>
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<Value, String> {
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 };

View File

@ -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();
}
ui.separator();
@ -599,7 +599,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();
}

View File

@ -515,6 +515,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 {