Merge pr/689: Ungroup leaves frames with pasted-in content whole; name them by their shape (#689)

This commit is contained in:
aiongg
2026-10-13 12:27:49 +00:00
3 changed files with 48 additions and 3 deletions
+1
View File
@@ -128,6 +128,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
Newest first.
- **2026-10-13** · Ungroup only takes groups apart: a frame with pasted-in content stays whole (and selected), is named by its shape in the Layers and Properties panels, and offers Convert Shape rather than Ungroup. Non-groups stay selected. No readiness percentage change.
- **2026-10-13** · Copy with a placed graphic selected (Direct Selection) copies just the graphic, uncropped, where it sits on the page and without the frame's stroke, so Paste Into puts the whole image in place; Cut and Delete then remove only the graphic and leave its frame empty. Nested selections copy in their spread coordinates. No readiness percentage change.
- **2026-10-13** · Object effects (Basic Feather, Inner Glow, shadows, glows, bevel, satin, directional and gradient feathers) apply to frames with pasted-in content, on screen and in image exports: the frame, its clipped content and its stroke are one object to the effect. PDF export still has no soft effects for any object. No readiness percentage change.
- **2026-10-13** · Backspace between two consecutive tables, pasting a table next to a table anchor, and pasting several paragraphs right before one keep every table on its own anchor paragraph. Previously a table was dropped or left without its anchor, and the saved file failed to reopen. No readiness percentage change.
+2 -1
View File
@@ -1487,7 +1487,8 @@ impl Item {
/// The Layers-panel label: `<rectangle>`, the first words of a text frame, the placed file name…
pub fn default_label(&self) -> &'static str {
match (&self.content, self.shape) {
(Content::Group { .. }, _) => "<group>",
// A frame with items pasted into it is named by its shape, as in InDesign.
(Content::Group { .. }, Shape::Group) => "<group>",
(Content::Text(_), _) => "<text frame>",
(Content::Graphic(_), _) => "<image>",
(_, Shape::Rectangle) => "<rectangle>",
+45 -2
View File
@@ -1798,8 +1798,14 @@ fn ungroup(s: &mut Session, p: &Value) -> Result<Value> {
for id in &ids {
let Some(loc) = d.find(*id) else { continue };
let Some(g) = d.item(*id).cloned() else { continue };
let Content::Group { items } = &g.content else { continue };
if loc.path.len() != 1 {
// Only groups come apart: a frame holding pasted-into items stays whole, and objects
// that aren't groups stay selected (as in InDesign).
let Content::Group { items } = &g.content else {
out.push(*id);
continue;
};
if !g.is_group() || loc.path.len() != 1 {
out.push(*id);
continue;
}
let Some(sp) = d.spread_mut(loc.spread) else { continue };
@@ -4764,3 +4770,40 @@ mod effects_command_tests {
assert!(std::sync::Arc::ptr_eq(&before, &s.doc().unwrap().doc));
}
}
#[cfg(test)]
mod ungroup_tests {
use super::*;
#[test]
fn ungroup_leaves_frames_with_pasted_in_items_whole() {
let mut s = Session::new();
s.execute("file.new", &json!({})).unwrap();
let id = |r: Value| ItemId(r["id"].as_u64().unwrap());
let sq = id(s.execute("frame.create", &json!({"rect": [100, 100, 200, 200]})).unwrap());
let ell = id(s.execute("frame.create", &json!({"rect": [90, 90, 210, 210], "shape": "ellipse"})).unwrap());
s.execute("selection.set", &json!({"ids": [sq.0]})).unwrap();
s.execute("edit.cut", &json!({})).unwrap();
s.execute("selection.set", &json!({"ids": [ell.0]})).unwrap();
s.execute("edit.pasteInto", &json!({})).unwrap();
let d = s.doc().unwrap().doc.clone();
assert_eq!(d.item(ell).unwrap().default_label(), "<ellipse>", "named by its shape, not <group>");
// Ungroup: the frame keeps its content and stays selected.
s.execute("object.ungroup", &json!({})).unwrap();
let d = s.doc().unwrap().doc.clone();
assert!(d.item(ell).unwrap().has_nested_items());
assert_eq!(d.spreads[0].items.len(), 1);
assert_eq!(s.doc().unwrap().selection.items, vec![ell]);
// A real group with the frame in it still comes apart, the frame whole.
let other = id(s.execute("frame.create", &json!({"rect": [300, 100, 400, 200]})).unwrap());
s.execute("selection.set", &json!({"ids": [ell.0, other.0]})).unwrap();
let g = id(s.execute("object.group", &json!({})).unwrap());
assert_eq!(s.doc().unwrap().doc.item(g).unwrap().default_label(), "<group>");
s.execute("object.ungroup", &json!({})).unwrap();
let d = s.doc().unwrap().doc.clone();
assert!(d.item(g).is_none());
assert!(d.item(ell).unwrap().has_nested_items());
assert_eq!(s.doc().unwrap().selection.items.len(), 2);
d.check().unwrap();
}
}