From 98ce1b35be50d2f6ee6a4ad2dbedf883d171b6f8 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Sat, 24 Oct 2026 22:52:52 +0200 Subject: [PATCH] Pasted shapes get the next automatic name instead of keeping the original's (fixes #1290) --- crates/engine/src/cmd/edit.rs | 10 +++++++++- crates/engine/src/cmd/insert.rs | 35 +++++++++++++++++++-------------- crates/engine/src/tests.rs | 23 ++++++++++++++++++++++ 3 files changed, 52 insertions(+), 16 deletions(-) diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index fa13825f..cdbb2356 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -358,7 +358,15 @@ fn paste(s: &mut Session, p: &Value) -> Result { let mut shapes = clip.shapes.clone(); reid("edit.paste", doc, &mut shapes)?; remap_media(&mut shapes, &media); - for sh in &mut shapes { + let taken = super::current_shapes(doc, sel).map_or(0, Vec::len); + for (k, sh) in shapes.iter_mut().enumerate() { + // An automatic name ("Rectangle 3") counts on from this slide, as a new shape would; + // a name the user typed stays. + let base = super::insert::auto_name_base(sh); + let numbered = sh.name.strip_prefix(base.as_str()).and_then(|r| r.strip_prefix(' ')); + if numbered.is_some_and(|d| !d.is_empty() && d.bytes().all(|b| b.is_ascii_digit())) { + sh.name = format!("{base} {}", taken + k + 1); + } if let Some(x) = sh.xfrm.as_mut() { x.x += off; x.y += off; diff --git a/crates/engine/src/cmd/insert.rs b/crates/engine/src/cmd/insert.rs index a9ab3644..0acf9676 100644 --- a/crates/engine/src/cmd/insert.rs +++ b/crates/engine/src/cmd/insert.rs @@ -149,25 +149,30 @@ fn default_rect(s: &Session, w: f64, h: f64) -> Xfrm { Xfrm::new((size.width - w) / 2.0, (size.height - h) / 2.0, w, h) } +/// The word an automatic shape name starts with ("Rectangle" in "Rectangle 3"). +pub(crate) fn auto_name_base(shape: &Shape) -> String { + match &shape.kind { + ShapeKind::Picture { .. } => "Picture".to_string(), + ShapeKind::Table(_) => "Table".to_string(), + ShapeKind::Chart(_) => "Chart".to_string(), + ShapeKind::Media(m) if m.video => "Video".to_string(), + ShapeKind::Media(_) => "Audio".to_string(), + ShapeKind::Model3d { .. } => "3D Model".to_string(), + _ if shape.text_box => "TextBox".to_string(), + _ => shape + .geom + .preset_name() + .and_then(deckcraft_geom::preset::info) + .map(|i| i.label.split(':').next().unwrap_or(i.label).trim().to_string()) + .unwrap_or_else(|| "Shape".into()), + } +} + pub(crate) fn add_shape(s: &mut Session, cmd: &str, mut shape: Shape, select: bool) -> Result { s.edit(|doc, sel| { shape.id = ShapeId(doc.alloc_id().ok_or_else(|| bad(cmd, NO_ID_SPACE))?); if shape.name.is_empty() { - let base = match &shape.kind { - ShapeKind::Picture { .. } => "Picture".to_string(), - ShapeKind::Table(_) => "Table".to_string(), - ShapeKind::Chart(_) => "Chart".to_string(), - ShapeKind::Media(m) if m.video => "Video".to_string(), - ShapeKind::Media(_) => "Audio".to_string(), - ShapeKind::Model3d { .. } => "3D Model".to_string(), - _ if shape.text_box => "TextBox".to_string(), - _ => shape - .geom - .preset_name() - .and_then(deckcraft_geom::preset::info) - .map(|i| i.label.split(':').next().unwrap_or(i.label).trim().to_string()) - .unwrap_or_else(|| "Shape".into()), - }; + let base = auto_name_base(&shape); let n = super::current_shapes(doc, sel).map(|v| v.len()).unwrap_or(0) + 1; shape.name = format!("{base} {n}"); } diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index fc5cd2c9..478ef496 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -210,6 +210,29 @@ fn clipboard_copy_paste_shapes_and_slides() { assert!(s.doc().unwrap().doc.validate().is_empty()); } +#[test] +fn pasted_shapes_get_the_next_automatic_name() { + let mut s = session(); + let names = |s: &Session| s.doc().unwrap().current_slide().unwrap().shapes.iter().map(|x| x.name.clone()).collect::>(); + let before = names(&s).len(); + s.execute("shape.insert", &json!({"preset": "rect", "rect": [10, 10, 50, 50]})).unwrap(); + let auto = names(&s)[before].clone(); + s.execute("edit.copy", &json!({})).unwrap(); + s.execute("edit.paste", &json!({})).unwrap(); + s.execute("edit.paste", &json!({})).unwrap(); + let all = names(&s); + let base = auto.rsplit_once(' ').unwrap().0; + assert_eq!(all[before + 1], format!("{base} {}", before + 2)); + assert_eq!(all[before + 2], format!("{base} {}", before + 3)); + + // A name the user chose is not an automatic one and stays on the copy. + let id = s.doc().unwrap().selection.shapes[0].0; + s.execute("shape.rename", &json!({"id": id, "name": "Hero"})).unwrap(); + s.execute("edit.copy", &json!({})).unwrap(); + s.execute("edit.paste", &json!({})).unwrap(); + assert_eq!(names(&s).last().unwrap(), "Hero"); +} + #[test] fn bold_copy_is_html_and_pages_html_pastes_bold() { let mut s = session();