diff --git a/ROADMAP.md b/ROADMAP.md index ee0f4e022..c06cfceaa 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -400,6 +400,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m - **2026-11-12** · Type shortcuts: Justify All Lines ⇧⌘F, Insert White Space › Em Space ⇧⌘M and En Space ⇧⌘N (shown in the menu, enabled at an insertion point), Paragraph Rules… ⌥⌘J (shown in the Paragraph panel menu). On the Mac a menu-bar insert (Em/En Space, Current Page Number) no longer types into the document while a text field has focus, a dialog or Quick Apply is open, or an input method is composing. An alignment the paragraphs already have records no undo step (#1138). No readiness percentage change. - **2026-11-12** · A placed JPEG drawn at a half, a quarter or an eighth of its pixel size or less is decoded at that size straight from its compressed blocks, so the canvas, page thumbnails and loaded-graphic thumbnails no longer decode and keep the full-size picture for it. CMYK JPEGs and the full size still decode in full. No readiness percentage change. - **2026-11-12** · A decoded placed image becomes a screen pixmap in its own buffer, premultiplied in place, and opaque pixels are left as they are: 120 ms becomes 7 ms for a 24-megapixel opaque photo, with the same pixels. No readiness percentage change. +- **2026-11-12** · With the insertion point in footnote text, the table commands (Insert Row, Delete Column, Merge Cells, Table Options, Select Table, table styles) are disabled, as they are outside a table, instead of failing with "no table 18446744073709551615". A call that names a table still runs. No readiness percentage change. - **2026-11-12** · The status bar's preflight count and the Preflight panel take their issues from a check on a background worker, which checks the newest state of the document and shows the issues last found meanwhile. Moving or resizing a frame and typing no longer wait for a check of the whole document on every drag step and keystroke. No readiness percentage change. - **2026-11-12** · Object › Text Frame Options… (Cmd+B) opens with the Type tool in a frame, so inset spacing and vertical justification can be changed while typing; it changes the frame that shows the insertion point, or every frame that holds part of the selected text (#2396). No readiness percentage change. diff --git a/crates/engine/src/cmd/table/target.rs b/crates/engine/src/cmd/table/target.rs index 0240aff23..a939a09dd 100644 --- a/crates/engine/src/cmd/table/target.rs +++ b/crates/engine/src/cmd/table/target.rs @@ -8,10 +8,12 @@ use super::{ pub(super) fn in_table(s: &Session) -> std::result::Result<(), String> { has_doc(s)?; let st = s.active().ok_or("no document open")?; - if st.selection.cells.is_some() || st.selection.text.is_some_and(|t| t.cell.is_some()) { + // Footnote text is addressed as a cell too, but isn't in a table. + if st.selection.cells.is_some() || st.selection.text.is_some_and(|t| t.cell.is_some_and(|c| c.footnote_id().is_none())) { return Ok(()); } if let Some(t) = st.selection.text + && t.cell.is_none() && st.doc.story(t.story).is_some_and(|x| x.table_at(t.focus).is_some()) { return Ok(()); @@ -81,6 +83,7 @@ pub(super) fn target(s: &Session, p: &Value, cmd: &str) -> Result { (c.story, c.table, Some(c.range)) } else if let Some(t) = st.selection.text { match t.cell { + Some(c) if c.footnote_id().is_some() => return Err(bad(cmd, "place the insertion point in a table")), Some(c) => (t.story, c.table, Some(CellRange::cell(c.row, c.col))), None => { let tid = doc.story(t.story).and_then(|x| x.table_at(t.focus)).ok_or_else(|| bad(cmd, "the insertion point is not in a table"))?; diff --git a/crates/engine/src/tests/table.rs b/crates/engine/src/tests/table.rs index 7626005f4..fd5323ffb 100644 --- a/crates/engine/src/tests/table.rs +++ b/crates/engine/src/tests/table.rs @@ -643,6 +643,51 @@ fn a_named_table_edit_keeps_a_footnote_caret() { check(&s); } +/// A caret in footnote text is not in a table: the table commands that need one are disabled +/// there, also when the caret's offset in the footnote falls on a table's paragraph in the main +/// story, and a call that names a table still runs. +#[test] +fn table_commands_are_disabled_in_footnote_text() { + let in_table = ["table.insertRow", "table.deleteColumn", "table.merge", "table.options", "table.selectTable", "style.table.apply"]; + let info = |s: &Session, id: &str| crate::cmd::find_command(id).unwrap().info(s); + for on_table in [false, true] { + let (mut s, sid, _) = session_with_frame(); + let tid = if on_table { + s.execute("story.setText", &json!({"story": sid, "text": "Hello"})).unwrap(); + s.execute("text.select", &json!({"story": sid, "anchor": 0, "focus": 0})).unwrap(); + let tid = s.execute("table.insert", &json!({"rows": 2, "cols": 2})).unwrap()["table"].as_u64().unwrap(); + let end = s.doc().unwrap().doc.story(designcraft_doc::StoryId(sid)).unwrap().text.len(); + s.execute("text.select", &json!({"story": sid, "anchor": end, "focus": end})).unwrap(); + s.execute("footnote.insert", &json!({})).unwrap(); + tid + } else { + let tid = s.execute("table.insert", &json!({"rows": 2, "cols": 2})).unwrap()["table"].as_u64().unwrap(); + s.execute("text.select", &json!({"story": sid, "anchor": 0, "focus": 0})).unwrap(); + s.execute("footnote.insert", &json!({"text": "Note"})).unwrap(); + tid + }; + let before = s.doc().unwrap().selection.clone(); + let caret = before.text.unwrap(); + assert!(caret.cell.and_then(|c| c.footnote_id()).is_some(), "the caret is in the footnote"); + let story = s.doc().unwrap().doc.story(designcraft_doc::StoryId(sid)).unwrap(); + assert_eq!(story.table_at(caret.focus).is_some(), on_table, "focus {}", caret.focus); + for id in in_table { + let i = info(&s, id); + assert!(!i.enabled, "{id} {on_table}"); + assert_eq!(i.disabled_reason.as_deref(), Some("place the insertion point in a table"), "{id} {on_table}"); + assert!(matches!(s.execute(id, &json!({})), Err(crate::EngineError::Disabled(..))), "{id} {on_table}"); + } + // A query that takes any selection and names no table is told where to put the caret. + let error = s.execute("table.get", &json!({})).unwrap_err().to_string(); + assert!(error.contains("place the insertion point in a table"), "{on_table}: {error}"); + assert_eq!(s.doc().unwrap().selection, before); + s.execute("table.insertRow", &json!({"table": tid})).unwrap(); + assert_eq!(s.execute("table.get", &json!({"table": tid})).unwrap()["rows"].as_array().unwrap().len(), 3); + assert_eq!(s.doc().unwrap().selection, before); + check(&s); + } +} + /// With nothing selected, the table commands stay disabled for menus and for calls that name no /// table. #[test]