Merge pull request #68 from punkhop/fix/clear-deletes-a-bin

project: Clear deletes a bin it is given, with everything in it
This commit is contained in:
Brandon Thomas 2026-10-07 05:56:48 -04:00 committed by GitHub
commit b8cc3925c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 74 additions and 2 deletions

View File

@ -2219,12 +2219,33 @@ fn build() -> Vec<CommandSpec> {
Ok(Value::Null)
}),
cmd!("project.delete", "Clear", [], Some("Delete"), r#"{"items":[id]?}"#, has_project_selection, |s, p| {
let items: Vec<ItemId> = p
let asked: Vec<ItemId> = p
.get("items")
.and_then(Value::as_array)
.map(|a| a.iter().filter_map(|v| v.as_u64().map(ItemId)).collect())
.unwrap_or_else(|| s.state.project_selection.clone());
// A bin named here goes with everything in it, as in the Project panel of any editor.
let root = s.project.root.id;
let named: Vec<filmcraft_project::BinId> =
asked.iter().map(|i| filmcraft_project::BinId(i.0)).filter(|b| *b != root && s.project.root.find_bin(*b).is_some()).collect();
// a bin inside another named bin goes with that one
let bins: Vec<filmcraft_project::BinId> = named
.iter()
.copied()
.filter(|b| !named.iter().any(|o| o != b && s.project.root.find_bin(*o).is_some_and(|o| o.find_bin(*b).is_some())))
.collect();
let mut items: Vec<ItemId> = asked.iter().copied().filter(|i| !named.contains(&filmcraft_project::BinId(i.0))).collect();
for b in &bins {
if let Some(bin) = s.project.root.find_bin(*b) {
bin.all_items(&mut items);
}
}
items.sort_by_key(|i| i.0);
items.dedup();
s.edit("Clear", |pr, st| {
for b in &bins {
pr.root.remove_bin(*b);
}
for i in &items {
pr.items.remove(i);
pr.root.remove_item(*i);
@ -2246,7 +2267,7 @@ fn build() -> Vec<CommandSpec> {
s.media.remove(*i);
}
s.fix_state();
Ok(Value::Null)
Ok(json!({"items": items.len(), "bins": bins.len()}))
}),
cmd!("project.moveToBin", "Move to Bin", [], None, r#"{"items":[id]?,"bin":binId|null}"#, always, |s, p| {
let items: Vec<ItemId> = match p.get("items").and_then(Value::as_array) {

View File

@ -401,3 +401,45 @@ fn system_report_lists_the_build() {
assert!(r["decoders"].as_array().unwrap().iter().any(|d| d.as_str().unwrap().contains("H.264")));
assert_eq!(r["exportFormats"].as_array().unwrap().len(), filmcraft_export::Format::ALL.len());
}
#[test]
fn clear_deletes_a_bin_with_everything_in_it() {
let mut s = demo();
let before = s.project.items.len();
let outer = s.execute("file.newBin", json!({"name": "Old footage"})).unwrap()["bin"].as_u64().unwrap();
let inner = s.execute("file.newBin", json!({"name": "Inside", "parent": outer})).unwrap()["bin"].as_u64().unwrap();
let empty = s.execute("file.newBin", json!({"name": "Empty"})).unwrap()["bin"].as_u64().unwrap();
let ocean = item_named(&s, "Ocean_Sunset.mp4");
s.execute("project.moveToBin", json!({"items": [ocean.0], "bin": inner})).unwrap();
let dunes = item_named(&s, "Desert_Dunes.mp4");
let select = |s: &mut Session| s.execute("project.select", json!({"items": [dunes.0]})).unwrap();
select(&mut s);
// an empty bin, by its id: gone, nothing else touched
let r = s.execute("project.delete", json!({"items": [empty]})).unwrap();
assert_eq!(r, json!({"items": 0, "bins": 1}));
assert!(s.project.root.find_bin(filmcraft_project::BinId(empty)).is_none());
assert_eq!(s.project.items.len(), before);
// a bin holding a sub-bin holding a clip: all three go
select(&mut s);
let r = s.execute("project.delete", json!({"items": [outer]})).unwrap();
assert_eq!(r, json!({"items": 1, "bins": 1}));
assert!(s.project.root.find_bin(filmcraft_project::BinId(outer)).is_none());
assert!(s.project.root.find_bin(filmcraft_project::BinId(inner)).is_none());
assert!(s.project.item(ocean).is_none());
assert_eq!(s.project.items.len(), before - 1);
// undo puts the bins and the clip back where they were
s.execute("edit.undo", json!({})).unwrap();
assert!(s.project.root.find_bin(filmcraft_project::BinId(inner)).is_some());
assert_eq!(s.project.root.parent_of(ocean), Some(filmcraft_project::BinId(inner)));
// the clip, its bin and the bin around it all named: each counted once
select(&mut s);
let r = s.execute("project.delete", json!({"items": [ocean.0, inner, outer]})).unwrap();
assert_eq!(r, json!({"items": 1, "bins": 1}));
assert_eq!(s.project.items.len(), before - 1);
s.execute("edit.undo", json!({})).unwrap();
// the project's own top bin is never cleared
let root = s.project.root.id.0;
select(&mut s);
s.execute("project.delete", json!({"items": [root]})).unwrap();
assert_eq!(s.project.items.len(), before);
}

View File

@ -418,6 +418,15 @@ impl Bin {
}
self.children.iter_mut().any(|c| if let BinEntry::Bin(b) = c { b.remove_item(id) } else { false })
}
/// Take a sub-bin, and everything in it, out of the tree. The bin itself is never removed from itself.
pub fn remove_bin(&mut self, id: BinId) -> Option<Bin> {
if let Some(at) = self.children.iter().position(|c| matches!(c, BinEntry::Bin(b) if b.id == id))
&& let BinEntry::Bin(b) = self.children.remove(at)
{
return Some(b);
}
self.children.iter_mut().find_map(|c| if let BinEntry::Bin(b) = c { b.remove_bin(id) } else { None })
}
/// All items in this bin and sub-bins.
pub fn all_items(&self, out: &mut Vec<ItemId>) {
for c in &self.children {