O: Select submenu, Convert Shape, split table cells
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
bddaaac887
commit
2cd4102469
|
|
@ -37,6 +37,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
|||
- Global Light (`object.globalLight`, `globalLight` on drop/inner shadows; Use Global Light in the Effects panel) and Generate Static Caption (`object.caption`: template over name, path, alt text, label, effective ppi, dimensions, format; any side, offset, style).
|
||||
- Control panel scale X/Y, rotation and shear fields (`transform.info`, absolute `transform.set {scaleX, scaleY, rotation, shear}` about the reference point) and Preferences › Transformations are Totals for nested objects and placed graphics.
|
||||
- Variable fonts: each named instance is a style (Font menu, shaping with feature variations, metrics, outlines) and embeds at its axis settings in PDF; free axis sliders still to come.
|
||||
- Object ▸ Select (first/next above, next/last below, container, content, previous/next in group), full Object ▸ Convert Shape (`object.convertShape`), Table ▸ Split Cell Horizontally/Vertically.
|
||||
- Drag and drop text editing: drag selected text to move it (Alt copies) with its formatting (`text.release`).
|
||||
- Image Import Options: place any page of a multi-page PDF (`file.place {pdfPage}`; the app asks which page), shown on screen and embedded as that page in PDF export.
|
||||
- Define Lists: named numbered lists that continue across stories in page order (`list.define`, paragraph `listName` / `startAt`), round-tripped through IDML; `null` in `type.para` / `type.char` now removes an override.
|
||||
|
|
|
|||
|
|
@ -602,6 +602,95 @@ impl Table {
|
|||
self.set_regions(®ions);
|
||||
}
|
||||
|
||||
/// Split the cell covering (r, c) in two: `horizontal` stacks the halves (Split Cell
|
||||
/// Horizontally), otherwise side by side. A merged cell splits its span; a single cell gets a
|
||||
/// new row (column) that the cells beside it span, the row height (column width) shared.
|
||||
pub fn split_cell(&mut self, r: usize, c: usize, horizontal: bool) {
|
||||
if r >= self.nrows() || c >= self.ncols() {
|
||||
return;
|
||||
}
|
||||
let (or, oc) = self.owner(r, c);
|
||||
let (rs, cs) = self.cell(or, oc).map_or((1, 1), |x| (x.row_span as usize, x.col_span as usize));
|
||||
let mut regions: Vec<_> = self.regions().into_iter().filter(|&(rr, cc, _, _)| (rr, cc) != (or, oc)).collect();
|
||||
let span = if horizontal { rs } else { cs };
|
||||
if span > 1 {
|
||||
let a = span.div_ceil(2);
|
||||
if horizontal {
|
||||
regions.push((or, oc, a, cs));
|
||||
regions.push((or + a, oc, rs - a, cs));
|
||||
} else {
|
||||
regions.push((or, oc, rs, a));
|
||||
regions.push((or, oc + a, rs, cs - a));
|
||||
}
|
||||
self.set_regions(®ions);
|
||||
return;
|
||||
}
|
||||
if horizontal {
|
||||
let h = self.rows[or].height;
|
||||
self.insert_rows(or + 1, 1);
|
||||
self.rows[or].height = h / 2.0;
|
||||
self.rows[or + 1].height = h / 2.0;
|
||||
// insert_rows doesn't grow regions that end at the split row; everything else on it
|
||||
// spans the new row now.
|
||||
let mut regions: Vec<_> = self.regions();
|
||||
let (nr, nc) = (self.nrows(), self.ncols());
|
||||
let mut covered = vec![false; nr * nc];
|
||||
for &(rr, cc, rrs, ccs) in ®ions {
|
||||
for y in rr..rr + rrs {
|
||||
for x in cc..cc + ccs {
|
||||
covered[y * nc + x] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for reg in regions.iter_mut() {
|
||||
let (rr, cc, rrs, ccs) = *reg;
|
||||
if rr + rrs == or + 1 && !(cc <= oc && oc < cc + ccs) {
|
||||
*reg = (rr, cc, rrs + 1, ccs);
|
||||
}
|
||||
}
|
||||
for x in 0..nc {
|
||||
if (oc..oc + cs).contains(&x) || covered[or * nc + x] {
|
||||
continue;
|
||||
}
|
||||
regions.push((or, x, 2, 1));
|
||||
}
|
||||
if cs > 1 {
|
||||
regions.push((or + 1, oc, 1, cs));
|
||||
}
|
||||
self.set_regions(®ions);
|
||||
} else {
|
||||
let w = self.columns[oc].width;
|
||||
self.insert_cols(oc + 1, 1, w / 2.0);
|
||||
self.columns[oc].width = w / 2.0;
|
||||
let mut regions: Vec<_> = self.regions();
|
||||
let (nr, nc) = (self.nrows(), self.ncols());
|
||||
let mut covered = vec![false; nr * nc];
|
||||
for &(rr, cc, rrs, ccs) in ®ions {
|
||||
for y in rr..rr + rrs {
|
||||
for x in cc..cc + ccs {
|
||||
covered[y * nc + x] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for reg in regions.iter_mut() {
|
||||
let (rr, cc, rrs, ccs) = *reg;
|
||||
if cc + ccs == oc + 1 && !(rr <= or && or < rr + rrs) {
|
||||
*reg = (rr, cc, rrs, ccs + 1);
|
||||
}
|
||||
}
|
||||
for y in 0..nr {
|
||||
if (or..or + rs).contains(&y) || covered[y * nc + oc] {
|
||||
continue;
|
||||
}
|
||||
regions.push((y, oc, 1, 2));
|
||||
}
|
||||
if rs > 1 {
|
||||
regions.push((or, oc + 1, rs, 1));
|
||||
}
|
||||
self.set_regions(®ions);
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark the first `header` rows as header rows and the last `footer` rows as footer rows.
|
||||
pub fn set_header_footer(&mut self, header: usize, footer: usize) {
|
||||
let n = self.nrows();
|
||||
|
|
@ -971,3 +1060,30 @@ mod tests {
|
|||
assert!(st.tables.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod split_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn split_single_and_merged_cells() {
|
||||
let mut t = Table::new(1, 2, 2, 0, 0, 100.0);
|
||||
t.split_cell(0, 0, true);
|
||||
assert_eq!((t.nrows(), t.ncols()), (3, 2));
|
||||
// The cell beside the split one spans both new rows.
|
||||
assert_eq!(t.cell(0, 1).unwrap().row_span, 2);
|
||||
assert_eq!(t.owner(1, 1), (0, 1));
|
||||
assert_eq!(t.owner(1, 0), (1, 0), "the new half is its own cell");
|
||||
t.split_cell(2, 1, false);
|
||||
assert_eq!(t.ncols(), 3);
|
||||
assert_eq!(t.cell(0, 0).unwrap().col_span, 1);
|
||||
assert_eq!(t.owner(2, 2), (2, 2));
|
||||
assert_eq!(t.owner(0, 2), (0, 1), "the merged cell above widens");
|
||||
// A merged cell splits its own span instead of adding rows.
|
||||
let mut m = Table::new(1, 4, 1, 0, 0, 100.0);
|
||||
m.merge(CellRange { r0: 0, c0: 0, r1: 3, c1: 0 }).unwrap();
|
||||
m.split_cell(0, 0, true);
|
||||
assert_eq!(m.nrows(), 4);
|
||||
assert_eq!((m.cell(0, 0).unwrap().row_span, m.cell(2, 0).unwrap().row_span), (2, 2));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ mod place_text;
|
|||
pub mod preflight;
|
||||
mod prefs;
|
||||
mod print;
|
||||
mod select;
|
||||
pub mod spelling;
|
||||
mod style;
|
||||
pub mod table;
|
||||
|
|
@ -136,6 +137,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
|
|||
v.extend(captions::specs());
|
||||
v.extend(lists::specs());
|
||||
v.extend(path_type::specs());
|
||||
v.extend(select::specs());
|
||||
v.extend(package::specs());
|
||||
v.extend(print::specs());
|
||||
v.extend(text::specs());
|
||||
|
|
|
|||
|
|
@ -0,0 +1,249 @@
|
|||
//! Object › Select (stacking order, container and content, objects in a group) and Object ›
|
||||
//! Convert Shape.
|
||||
|
||||
use designcraft_doc::{Content, Document, Item, ItemId, ItemLoc, Selection, Shape, SpreadRef};
|
||||
use designcraft_geom::corners::{CornerOptions, CornerShape};
|
||||
use designcraft_geom::{Point, Rect, shapes};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, bad, cmd, has_doc, has_selection, ok, str_param, targets};
|
||||
use crate::{Result, Session};
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(noundo "select.firstAbove", "First Object Above", ["Object", "Select"], Some("Cmd+Alt+Shift+]"), "{} — the frontmost object on the spread (layers included)", has_doc, |s, _| select_stacked(s, Step::Top)),
|
||||
cmd!(noundo "select.nextAbove", "Next Object Above", ["Object", "Select"], Some("Cmd+Alt+]"), "{}", has_doc, |s, _| select_stacked(s, Step::Up)),
|
||||
cmd!(noundo "select.nextBelow", "Next Object Below", ["Object", "Select"], Some("Cmd+Alt+["), "{}", has_doc, |s, _| select_stacked(s, Step::Down)),
|
||||
cmd!(noundo "select.lastBelow", "Last Object Below", ["Object", "Select"], Some("Cmd+Alt+Shift+["), "{} — the backmost object on the spread", has_doc, |s, _| select_stacked(s, Step::Bottom)),
|
||||
cmd!(noundo "select.container", "Container", ["Object", "Select"], None, "{} — the group or frame holding the selection", has_selection, |s, _| select_container(s)),
|
||||
cmd!(noundo "select.content", "Content", ["Object", "Select"], None, "{} — the graphic in a frame, or the first object in a group", has_selection, |s, _| select_content(s)),
|
||||
cmd!(noundo "select.nextInGroup", "Next Object in Group", ["Object", "Select"], None, "{}", has_selection, |s, _| select_sibling(s, 1)),
|
||||
cmd!(noundo "select.previousInGroup", "Previous Object in Group", ["Object", "Select"], None, "{}", has_selection, |s, _| select_sibling(s, -1)),
|
||||
cmd!(
|
||||
"object.convertShape",
|
||||
"Convert Shape",
|
||||
["Object", "Convert Shape"],
|
||||
None,
|
||||
"{to: rectangle|roundedRectangle|beveledRectangle|inverseRoundedRectangle|ellipse|triangle|polygon|line|orthogonalLine|openPath|closedPath, ids?}",
|
||||
has_selection,
|
||||
convert_shape
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Step {
|
||||
Top,
|
||||
Up,
|
||||
Down,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
/// Selectable top-level items of spread `r`, back to front.
|
||||
fn stacking(d: &Document, r: SpreadRef) -> Vec<ItemId> {
|
||||
let Some(sp) = d.spread(r) else { return vec![] };
|
||||
let mut v = Vec::new();
|
||||
for layer in d.layers.iter().rev() {
|
||||
if !layer.visible || layer.locked {
|
||||
continue;
|
||||
}
|
||||
v.extend(sp.items.iter().filter(|it| it.layer == layer.id && !it.hidden && !it.locked).map(|it| it.id));
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
fn select_stacked(s: &mut Session, how: Step) -> Result<Value> {
|
||||
let st = s.doc_mut()?;
|
||||
let cur = st.selection.items.first().and_then(|id| st.doc.find(*id).map(|l| (*id, l)));
|
||||
let r = cur.as_ref().map(|(_, l)| l.spread).or_else(|| st.doc.spread_refs().next()).ok_or_else(|| bad("select", "no spread"))?;
|
||||
let order = stacking(&st.doc, r);
|
||||
if order.is_empty() {
|
||||
return ok();
|
||||
}
|
||||
// Nested selections step from their top-level object.
|
||||
let at = cur
|
||||
.and_then(|(_, l)| st.doc.spread(l.spread).and_then(|sp| sp.items.get(l.top())).map(|it| it.id))
|
||||
.and_then(|id| order.iter().position(|x| *x == id));
|
||||
let n = order.len();
|
||||
let i = match (how, at) {
|
||||
(Step::Top, _) | (Step::Down, None) => n - 1,
|
||||
(Step::Bottom, _) | (Step::Up, None) => 0,
|
||||
(Step::Up, Some(i)) => (i + 1).min(n - 1),
|
||||
(Step::Down, Some(i)) => i.saturating_sub(1),
|
||||
};
|
||||
st.selection = Selection::items(vec![order[i]]);
|
||||
st.revision += 1;
|
||||
Ok(json!({"id": order[i].0}))
|
||||
}
|
||||
|
||||
fn parent_loc(l: &ItemLoc) -> Option<ItemLoc> {
|
||||
(l.path.len() > 1).then(|| ItemLoc { spread: l.spread, path: l.path[..l.path.len() - 1].to_vec() })
|
||||
}
|
||||
|
||||
fn select_container(s: &mut Session) -> Result<Value> {
|
||||
let st = s.doc_mut()?;
|
||||
let Some(id) = st.selection.items.first().copied() else { return ok() };
|
||||
if st.selection.content {
|
||||
st.selection = Selection::items(vec![id]);
|
||||
} else if let Some(p) = st.doc.find(id).and_then(|l| parent_loc(&l)).and_then(|l| st.doc.item_at(&l).map(|it| it.id)) {
|
||||
st.selection = Selection::items(vec![p]);
|
||||
} else {
|
||||
return ok();
|
||||
}
|
||||
st.revision += 1;
|
||||
Ok(json!({"id": st.selection.items[0].0}))
|
||||
}
|
||||
|
||||
fn select_content(s: &mut Session) -> Result<Value> {
|
||||
let st = s.doc_mut()?;
|
||||
let Some(id) = st.selection.items.first().copied() else { return ok() };
|
||||
let Some(it) = st.doc.item(id) else { return ok() };
|
||||
if let Some(c) = it.children().first() {
|
||||
st.selection = Selection::items(vec![c.id]);
|
||||
} else if matches!(it.content, Content::Graphic(_)) {
|
||||
st.selection = Selection { content: true, ..Selection::items(vec![id]) };
|
||||
} else {
|
||||
return ok();
|
||||
}
|
||||
st.revision += 1;
|
||||
Ok(json!({"id": st.selection.items[0].0}))
|
||||
}
|
||||
|
||||
fn select_sibling(s: &mut Session, dir: isize) -> Result<Value> {
|
||||
let st = s.doc_mut()?;
|
||||
let Some(id) = st.selection.items.first().copied() else { return ok() };
|
||||
let Some(l) = st.doc.find(id) else { return ok() };
|
||||
let Some(pl) = parent_loc(&l) else { return ok() };
|
||||
let Some(parent) = st.doc.item_at(&pl) else { return ok() };
|
||||
let kids = parent.children();
|
||||
let n = kids.len() as isize;
|
||||
let i = *l.path.last().expect("nested") as isize;
|
||||
// InDesign's "next" goes down the stack (towards the back), wrapping.
|
||||
let j = (i - dir).rem_euclid(n) as usize;
|
||||
let next = kids[j].id;
|
||||
st.selection = Selection::items(vec![next]);
|
||||
st.revision += 1;
|
||||
Ok(json!({"id": next.0}))
|
||||
}
|
||||
|
||||
fn convert_shape(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const ID: &str = "object.convertShape";
|
||||
let to = str_param(p, "to").ok_or_else(|| bad(ID, "`to` required"))?.to_string();
|
||||
let ids = targets(s, p)?;
|
||||
let sides = s.prefs.polygon_sides;
|
||||
let inset = s.prefs.star_inset;
|
||||
s.edit(|d, _| {
|
||||
let mut changed = 0;
|
||||
for id in &ids {
|
||||
let Some(it) = d.item_mut(*id) else { continue };
|
||||
if it.shape == Shape::Group {
|
||||
continue;
|
||||
}
|
||||
convert(it, &to, sides, inset).map_err(|e| bad(ID, e))?;
|
||||
changed += 1;
|
||||
}
|
||||
Ok(json!({"changed": changed}))
|
||||
})
|
||||
}
|
||||
|
||||
fn convert(it: &mut Item, to: &str, sides: u32, inset: f64) -> std::result::Result<(), String> {
|
||||
let r: Rect = it.path.bounds().unwrap_or(Rect::ZERO);
|
||||
let text = matches!(it.content, Content::Text(_));
|
||||
let corners = |shape: CornerShape| CornerOptions::uniform(shape, (r.width().min(r.height()) / 6.0).clamp(1.0, 12.0));
|
||||
let (path, shape, c) = match to {
|
||||
"rectangle" => (shapes::rectangle(r), Shape::Rectangle, CornerOptions::default()),
|
||||
"roundedRectangle" => (shapes::rectangle(r), Shape::Rectangle, corners(CornerShape::Rounded)),
|
||||
"beveledRectangle" => (shapes::rectangle(r), Shape::Rectangle, corners(CornerShape::Bevel)),
|
||||
"inverseRoundedRectangle" => (shapes::rectangle(r), Shape::Rectangle, corners(CornerShape::InverseRounded)),
|
||||
"ellipse" => (shapes::ellipse(r), Shape::Oval, CornerOptions::default()),
|
||||
"triangle" => (super::object::polygon_in(r, 3, 0.0), Shape::Polygon, CornerOptions::default()),
|
||||
"polygon" => (super::object::polygon_in(r, sides, inset), Shape::Polygon, CornerOptions::default()),
|
||||
"line" | "orthogonalLine" if text => return Err("a text frame can't become a line".into()),
|
||||
"line" => (shapes::line(Point::new(r.x0, r.y0), Point::new(r.x1, r.y1)), Shape::GraphicLine, CornerOptions::default()),
|
||||
"orthogonalLine" => {
|
||||
let c = r.center();
|
||||
let (a, b) =
|
||||
if r.width() >= r.height() { (Point::new(r.x0, c.y), Point::new(r.x1, c.y)) } else { (Point::new(c.x, r.y0), Point::new(c.x, r.y1)) };
|
||||
(shapes::line(a, b), Shape::GraphicLine, CornerOptions::default())
|
||||
}
|
||||
"openPath" | "closedPath" => {
|
||||
let closed = to == "closedPath";
|
||||
if !closed && text {
|
||||
return Err("a text frame needs a closed path".into());
|
||||
}
|
||||
let mut path = it.path.clone();
|
||||
for sp in &mut path.subpaths {
|
||||
sp.closed = closed;
|
||||
}
|
||||
(path, Shape::Path, it.corners)
|
||||
}
|
||||
o => return Err(format!("unknown shape `{o}`")),
|
||||
};
|
||||
it.path = path;
|
||||
it.shape = shape;
|
||||
it.corners = c;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
|
||||
fn ids(s: &Session) -> Vec<u64> {
|
||||
s.doc().unwrap().selection.items.iter().map(|i| i.0).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_by_stacking_order_and_groups() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let mk = |s: &mut Session, x: f64| s.execute("frame.create", &json!({"rect": [x, 100.0, x + 50.0, 150.0]})).unwrap()["id"].as_u64().unwrap();
|
||||
let (a, b, c) = (mk(&mut s, 0.0), mk(&mut s, 60.0), mk(&mut s, 120.0));
|
||||
s.execute("select.lastBelow", &json!({})).unwrap();
|
||||
assert_eq!(ids(&s), [a]);
|
||||
s.execute("select.nextAbove", &json!({})).unwrap();
|
||||
assert_eq!(ids(&s), [b]);
|
||||
s.execute("select.firstAbove", &json!({})).unwrap();
|
||||
assert_eq!(ids(&s), [c]);
|
||||
s.execute("select.nextAbove", &json!({})).unwrap();
|
||||
assert_eq!(ids(&s), [c], "already on top");
|
||||
s.execute("select.nextBelow", &json!({})).unwrap();
|
||||
assert_eq!(ids(&s), [b]);
|
||||
let g = s.execute("object.group", &json!({"ids": [a, b]})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("edit.deselectAll", &json!({})).unwrap();
|
||||
s.doc_mut().unwrap().selection = designcraft_doc::Selection::items(vec![designcraft_doc::ItemId(g)]);
|
||||
s.execute("select.content", &json!({})).unwrap();
|
||||
let first = ids(&s)[0];
|
||||
assert!(first == a || first == b);
|
||||
s.execute("select.nextInGroup", &json!({})).unwrap();
|
||||
assert_ne!(ids(&s)[0], first);
|
||||
s.execute("select.nextInGroup", &json!({})).unwrap();
|
||||
assert_eq!(ids(&s)[0], first, "wraps");
|
||||
s.execute("select.container", &json!({})).unwrap();
|
||||
assert_eq!(ids(&s), [g]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_shape_keeps_bounds() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let a = s.execute("frame.create", &json!({"rect": [100, 100, 200, 160]})).unwrap()["id"].as_u64().unwrap();
|
||||
let item = |s: &Session| s.doc().unwrap().doc.item(designcraft_doc::ItemId(a)).unwrap().clone();
|
||||
for to in ["ellipse", "triangle", "polygon", "roundedRectangle", "rectangle"] {
|
||||
s.execute("object.convertShape", &json!({"to": to})).unwrap();
|
||||
let b = item(&s).bounds();
|
||||
assert!((b.width() - 100.0).abs() < 1e-6 && (b.height() - 60.0).abs() < 1e-6, "{to}: {b:?}");
|
||||
}
|
||||
s.execute("object.convertShape", &json!({"to": "orthogonalLine"})).unwrap();
|
||||
let it = item(&s);
|
||||
assert_eq!(it.shape, designcraft_doc::Shape::GraphicLine);
|
||||
assert_eq!(it.bounds().height(), 0.0);
|
||||
s.execute("object.convertShape", &json!({"to": "closedPath"})).unwrap();
|
||||
assert!(item(&s).path.subpaths[0].closed);
|
||||
let t = s.execute("frame.create", &json!({"rect": [0, 0, 50, 50], "content": "text"})).unwrap()["id"].as_u64().unwrap();
|
||||
assert!(s.execute("object.convertShape", &json!({"to": "line", "ids": [t]})).is_err());
|
||||
assert!(s.execute("object.convertShape", &json!({"to": "blob", "ids": [a]})).is_err());
|
||||
}
|
||||
}
|
||||
|
|
@ -122,6 +122,24 @@ fn table_specs() -> Vec<CommandSpec> {
|
|||
sort_rows
|
||||
),
|
||||
cmd!("table.unmerge", "Unmerge Cells", ["Table"], None, "{}", in_table, unmerge),
|
||||
cmd!(
|
||||
"table.splitHorizontally",
|
||||
"Split Cell Horizontally",
|
||||
["Table"],
|
||||
None,
|
||||
"{} — the target cell becomes two, one above the other",
|
||||
in_table,
|
||||
|s, p| split(s, p, true)
|
||||
),
|
||||
cmd!(
|
||||
"table.splitVertically",
|
||||
"Split Cell Vertically",
|
||||
["Table"],
|
||||
None,
|
||||
"{} — the target cell becomes two, side by side",
|
||||
in_table,
|
||||
|s, p| split(s, p, false)
|
||||
),
|
||||
cmd!(
|
||||
"table.setCell",
|
||||
"Cell Options",
|
||||
|
|
@ -497,6 +515,14 @@ fn merge(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
ok()
|
||||
}
|
||||
|
||||
fn split(s: &mut Session, p: &Value, horizontal: bool) -> Result<Value> {
|
||||
let g = target(s, p, "table.split")?;
|
||||
edit_table(s, &g, "table.split", |t| {
|
||||
t.split_cell(g.range.r0, g.range.c0, horizontal);
|
||||
Ok(json!({"rows": t.nrows(), "cols": t.ncols()}))
|
||||
})
|
||||
}
|
||||
|
||||
fn unmerge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let g = target(s, p, "table.unmerge")?;
|
||||
edit_table(s, &g, "table.unmerge", |t| {
|
||||
|
|
@ -1128,5 +1154,8 @@ mod sort_tests {
|
|||
assert_eq!(col(&s, 0), ["Name", "Apple", "fig", "pear"], "header stays; case-insensitive");
|
||||
s.execute("table.sortRows", &json!({"column": 1, "descending": true})).unwrap();
|
||||
assert_eq!(col(&s, 1), ["Qty", "100", "10", "9"], "numbers by value");
|
||||
s.execute("table.splitVertically", &json!({"rows": [1, 1], "cols": [0, 0]})).unwrap();
|
||||
let r = s.execute("table.splitHorizontally", &json!({"rows": [0, 0], "cols": [1, 1]})).unwrap();
|
||||
assert_eq!((r["rows"].as_u64(), r["cols"].as_u64()), (Some(5), Some(3)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -338,6 +338,32 @@ pub const MENUS: &[(&str, &[&str])] = &[
|
|||
"cmd:object.outerGlow",
|
||||
"cmd:object.feather",
|
||||
"<",
|
||||
">Select",
|
||||
"cmd:select.firstAbove",
|
||||
"cmd:select.nextAbove",
|
||||
"cmd:select.nextBelow",
|
||||
"cmd:select.lastBelow",
|
||||
"-",
|
||||
"cmd:select.container",
|
||||
"cmd:select.content",
|
||||
"-",
|
||||
"cmd:select.previousInGroup",
|
||||
"cmd:select.nextInGroup",
|
||||
"<",
|
||||
">Convert Shape",
|
||||
"cmd:object.convertShape|Rectangle|{\"to\": \"rectangle\"}",
|
||||
"cmd:object.convertShape|Rounded Rectangle|{\"to\": \"roundedRectangle\"}",
|
||||
"cmd:object.convertShape|Beveled Rectangle|{\"to\": \"beveledRectangle\"}",
|
||||
"cmd:object.convertShape|Inverse Rounded Rectangle|{\"to\": \"inverseRoundedRectangle\"}",
|
||||
"cmd:object.convertShape|Ellipse|{\"to\": \"ellipse\"}",
|
||||
"cmd:object.convertShape|Triangle|{\"to\": \"triangle\"}",
|
||||
"cmd:object.convertShape|Polygon|{\"to\": \"polygon\"}",
|
||||
"cmd:object.convertShape|Line|{\"to\": \"line\"}",
|
||||
"cmd:object.convertShape|Orthogonal Line|{\"to\": \"orthogonalLine\"}",
|
||||
"-",
|
||||
"cmd:object.convertShape|Open Path|{\"to\": \"openPath\"}",
|
||||
"cmd:object.convertShape|Closed Path|{\"to\": \"closedPath\"}",
|
||||
"<",
|
||||
">Captions",
|
||||
"cmd:object.caption",
|
||||
"<",
|
||||
|
|
@ -385,6 +411,8 @@ pub const MENUS: &[(&str, &[&str])] = &[
|
|||
"-",
|
||||
"cmd:table.merge",
|
||||
"cmd:table.unmerge",
|
||||
"cmd:table.splitHorizontally",
|
||||
"cmd:table.splitVertically",
|
||||
"cmd:table.distributeColumns",
|
||||
"-",
|
||||
"ui:app.tablePanel",
|
||||
|
|
|
|||
|
|
@ -754,11 +754,21 @@ fn arrange_menu(app: &mut DesignApp, resp: &egui::Response) {
|
|||
|
||||
fn convert_shape_menu(app: &mut DesignApp, resp: &egui::Response) {
|
||||
egui::Popup::menu(resp).show(|ui| {
|
||||
for (label, shape) in
|
||||
[("Rectangle", "none"), ("Rounded Rectangle", "rounded"), ("Beveled Rectangle", "bevel"), ("Inverse Rounded Rectangle", "inverseRounded")]
|
||||
{
|
||||
for (label, to) in [
|
||||
("Rectangle", "rectangle"),
|
||||
("Rounded Rectangle", "roundedRectangle"),
|
||||
("Beveled Rectangle", "beveledRectangle"),
|
||||
("Inverse Rounded Rectangle", "inverseRoundedRectangle"),
|
||||
("Ellipse", "ellipse"),
|
||||
("Triangle", "triangle"),
|
||||
("Polygon", "polygon"),
|
||||
("Line", "line"),
|
||||
("Orthogonal Line", "orthogonalLine"),
|
||||
] {
|
||||
if ui.button(label).clicked() {
|
||||
let _ = app.run("object.cornerOptions", json!({"shape": shape, "size": 12.0}));
|
||||
if let Err(e) = app.run("object.convertShape", json!({"to": to})) {
|
||||
app.status(format!("Convert Shape: {e}"));
|
||||
}
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
|||
| Priority | Features | Done | Partial | Missing | Parity |
|
||||
|---|---|---|---|---|---|
|
||||
| P0 | 102 | 99 | 3 | 0 | 99% |
|
||||
| P1 | 97 | 68 | 27 | 2 | 84% |
|
||||
| P1 | 97 | 71 | 24 | 2 | 86% |
|
||||
| P2 | 76 | 12 | 13 | 51 | 24% |
|
||||
|
||||
| Area | Parity |
|
||||
|
|
@ -18,7 +18,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
|||
| Application shell & workspace | 80% |
|
||||
| Documents, pages, spreads | 84% |
|
||||
| Layers | 95% |
|
||||
| Frames, shapes & paths | 88% |
|
||||
| Frames, shapes & paths | 90% |
|
||||
| Transform | 100% |
|
||||
| Fill, stroke, colour | 86% |
|
||||
| Effects & transparency | 62% |
|
||||
|
|
@ -26,7 +26,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
|||
| Type & text frames | 88% |
|
||||
| Typography | 87% |
|
||||
| Styles | 92% |
|
||||
| Tables | 85% |
|
||||
| Tables | 88% |
|
||||
| Long documents | 79% |
|
||||
| Interactivity & digital | 35% |
|
||||
| Output & production | 70% |
|
||||
|
|
@ -102,7 +102,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t
|
|||
| Frames, shapes & paths | Pencil, Smooth, Erase | P1 | D |
|
||||
| Frames, shapes & paths | Scissors | P1 | D |
|
||||
| Frames, shapes & paths | Content type | P0 | D |
|
||||
| Frames, shapes & paths | Convert Shape | P1 | P |
|
||||
| Frames, shapes & paths | Convert Shape | P1 | D |
|
||||
| Frames, shapes & paths | Corner Options | P0 | D |
|
||||
| Frames, shapes & paths | Pathfinder | P1 | D |
|
||||
| Frames, shapes & paths | Compound paths | P1 | D |
|
||||
|
|
@ -122,7 +122,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t
|
|||
| Frames, shapes & paths | Group/Ungroup | P0 | D |
|
||||
| Frames, shapes & paths | Lock / Unlock All on Spread | P0 | D |
|
||||
| Frames, shapes & paths | Hide / Show All on Spread | P1 | D |
|
||||
| Frames, shapes & paths | Select submenu | P1 | P |
|
||||
| Frames, shapes & paths | Select submenu | P1 | D |
|
||||
| Frames, shapes & paths | Select All / Deselect All | P0 | D |
|
||||
| Frames, shapes & paths | Duplicate | P0 | D |
|
||||
| Frames, shapes & paths | Paste Into / Paste in Place | P0 | D |
|
||||
|
|
@ -248,7 +248,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t
|
|||
| Tables | Create / Insert Table | P1 | D |
|
||||
| Tables | Convert Text to Table / Table to Text | P1 | D |
|
||||
| Tables | Rows/columns insert/delete/resize/distribute | P1 | D |
|
||||
| Tables | Merge/Unmerge/Split cells | P1 | P |
|
||||
| Tables | Merge/Unmerge/Split cells | P1 | D |
|
||||
| Tables | Header/footer rows | P1 | D |
|
||||
| Tables | Table options | P1 | D |
|
||||
| Tables | Cell options | P1 | D |
|
||||
|
|
|
|||
Loading…
Reference in New Issue