mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-02 22:50:27 -06:00
Tables: Shift+Tab goes back a cell and Esc toggles text and cell selection
As in InDesign (Learn Skills Daily / Adobe "How to make a table" lesson): - With the Type tool in a table cell, Shift+Tab goes to the previous cell. It went on to the next one: the tool sent a plain Tab whatever the modifiers. - Esc in a cell toggles between the text and the cell: the insertion point or text in a cell selects that cell, and selected cells select the text of the first one. Esc left the table and selected the frame. New engine command table.toggleSelection does the toggle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2e36dcf909
commit
3886ad2fd1
@@ -349,6 +349,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
|
||||
Newest first.
|
||||
|
||||
- **2026-10-16** · Edit › Keyboard Shortcuts imports a set saved by InDesign's Show Set (.txt, Windows or Mac key names, UTF-8 or UTF-16): matched commands take its Default shortcuts or lose theirs, other commands give up keys it takes, and a report lists what it changed and the items it could not match (`window.importShortcuts`, #1180). Tool keys stay fixed. No readiness percentage change.
|
||||
- **2026-10-23** · Tables: with the Type tool in a cell, Shift+Tab goes back a cell (it went on to the next one) and Esc toggles between the text and the cell, as in InDesign: the insertion point selects its cell, and selected cells select the text of the first one (`table.toggleSelection`). Esc left the table for the frame. No readiness percentage change.
|
||||
- **2026-10-22** · A ruler guide dragged with the Selection tools, out of a ruler, on its own or with other selected guides, snaps along its axis as objects do (#1221): to other ruler guides, margins, columns, bleed, page and object edges and centers (with the smart-guide mark) and the document grid, within the snap zone and following Snap to Guides and Smart Guides. Shift snaps to the ruler's ticks and Cmd suspends snapping. The preview line, the position readout and the drop use the same snapped position. No readiness percentage change.
|
||||
- **2026-10-22** · macOS: the app menu has a Language submenu after Settings… with each interface language in its own name and the current one checked, as in the suite's other apps; Preferences › General › Interface Language stays. No readiness percentage change.
|
||||
- **2026-10-22** · macOS menu bar: the menus keep working after the interface language or Show CJK Features changes (a rebuilt menu bar lost every click, Quit included), and its key equivalents are the shortcuts in effect, so a shortcut moved in the Keyboard Shortcuts editor leaves its old menu item. No readiness percentage change.
|
||||
|
||||
@@ -403,6 +403,15 @@ fn table_specs() -> Vec<CommandSpec> {
|
||||
}),
|
||||
cmd!(noundo "table.nextCell", "Next Cell", [], None, "{}", in_cell, |s, _| step_cell(s, true)),
|
||||
cmd!(noundo "table.prevCell", "Previous Cell", [], None, "{}", in_cell, |s, _| step_cell(s, false)),
|
||||
cmd!(
|
||||
noundo "table.toggleSelection",
|
||||
"Toggle Text and Cell Selection",
|
||||
[],
|
||||
None,
|
||||
"{} — Esc in a table, as InDesign: the insertion point or text in a cell selects that cell; selected cells select the text of the first one",
|
||||
in_cell,
|
||||
|s, _| toggle_selection(s)
|
||||
),
|
||||
cmd!(query "table.get", "Get Table", [], None, "{story?, table?, rows?: [a,b] | row?, cols?: [a,b] | col?} → rows, columns, cells (text, spans, fill), options", in_table_or_ids, get),
|
||||
]
|
||||
}
|
||||
@@ -1537,6 +1546,33 @@ pub(crate) fn step_cell(s: &mut Session, forward: bool) -> Result<Value> {
|
||||
Ok(json!({"row": row, "col": col}))
|
||||
}
|
||||
|
||||
/// Esc in a table: text in a cell becomes that cell's selection, and selected cells the text of
|
||||
/// the first one (InDesign toggles between the two).
|
||||
fn toggle_selection(s: &mut Session) -> Result<Value> {
|
||||
const ID: &str = "table.toggleSelection";
|
||||
let st = s.doc_mut()?;
|
||||
let t = st.selection.text.ok_or_else(|| bad(ID, "no caret"))?;
|
||||
let c = t.cell.ok_or_else(|| bad(ID, "not in a cell"))?;
|
||||
let cells = match st.selection.cells {
|
||||
Some(cs) => {
|
||||
let cell = CellAddr { table: cs.table, row: cs.range.r0, col: cs.range.c0 };
|
||||
let len = st.doc.text_story(cs.story, Some(cell)).map_or(0, |x| x.len());
|
||||
st.selection = Selection::text(TextSel { story: cs.story, anchor: 0, focus: len, cell: Some(cell), ..t });
|
||||
false
|
||||
}
|
||||
None => {
|
||||
st.selection = Selection {
|
||||
cells: Some(TableSel { story: t.story, table: c.table, range: CellRange::cell(c.row, c.col) }),
|
||||
text: Some(TextSel { anchor: 0, focus: 0, ..t }),
|
||||
..Default::default()
|
||||
};
|
||||
true
|
||||
}
|
||||
};
|
||||
st.bump_revision();
|
||||
Ok(json!({"cells": cells}))
|
||||
}
|
||||
|
||||
fn get(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let g = target(s, p, "table.get")?;
|
||||
let st = s.doc()?;
|
||||
|
||||
@@ -127,6 +127,32 @@ fn table_edit_after_anchor_removal_floors_parent_caret() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Esc in a table toggles between the text and the cell (InDesign's Esc): the caret in a cell
|
||||
/// selects that cell, and selected cells select the first one's text; Shift+Tab goes back a cell.
|
||||
#[test]
|
||||
fn escape_toggles_text_and_cell_selection() {
|
||||
let (mut s, sid, _) = session_with_frame();
|
||||
let tid = s.execute("table.insert", &json!({"rows": 2, "cols": 2})).unwrap()["table"].as_u64().unwrap();
|
||||
s.execute("table.nextCell", &json!({})).unwrap();
|
||||
s.execute("text.insert", &json!({"text": "Phone"})).unwrap();
|
||||
let cell = designcraft_doc::CellAddr { table: tid, row: 0, col: 1 };
|
||||
assert_eq!(s.execute("table.toggleSelection", &json!({})).unwrap(), json!({"cells": true}));
|
||||
let sel = s.doc().unwrap().selection.clone();
|
||||
let cells = sel.cells.unwrap();
|
||||
assert_eq!((cells.story.0, cells.table, cells.range), (sid, tid, designcraft_doc::CellRange::cell(0, 1)));
|
||||
assert_eq!(sel.text.unwrap().cell, Some(cell));
|
||||
assert_eq!(s.execute("table.toggleSelection", &json!({})).unwrap(), json!({"cells": false}));
|
||||
let sel = s.doc().unwrap().selection.clone();
|
||||
assert!(sel.cells.is_none());
|
||||
let t = sel.text.unwrap();
|
||||
assert_eq!((t.cell, t.range()), (Some(cell), 0..5), "the cell's text is selected");
|
||||
s.execute("table.prevCell", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().selection.text.unwrap().cell, Some(designcraft_doc::CellAddr { col: 0, ..cell }));
|
||||
// Outside a table there is nothing to toggle.
|
||||
s.execute("text.select", &json!({"story": sid, "anchor": 0, "focus": 0})).unwrap();
|
||||
assert!(s.execute("table.toggleSelection", &json!({})).is_err());
|
||||
}
|
||||
|
||||
fn selected_merged_table(column: bool) -> (Session, u64, u64, u64) {
|
||||
let (mut s, sid, fid) = session_with_frame();
|
||||
let tid = s.execute("table.insert", &json!({"rows": 2, "cols": 2})).unwrap()["table"].as_u64().unwrap();
|
||||
|
||||
@@ -1452,6 +1452,29 @@ fn table_frame(degrees: f64) -> (Document, ItemId) {
|
||||
(d, id)
|
||||
}
|
||||
|
||||
/// In a table cell Shift+Tab goes back a cell and Esc toggles cell selection, as InDesign; out of
|
||||
/// a table Tab types a tab and Esc leaves the text.
|
||||
#[test]
|
||||
fn type_tool_keys_in_a_table_cell() {
|
||||
let (d, id) = table_frame(0.0);
|
||||
let sid = d.item(id).unwrap().text_frame().unwrap().story;
|
||||
let table = *d.story(sid).unwrap().tables.keys().next().unwrap();
|
||||
let (c, l) = (Cache::new(), CanvasLayout::new(&d, false));
|
||||
let cell = designcraft_doc::CellAddr { table, row: 1, col: 1 };
|
||||
let s = Selection::text(designcraft_doc::TextSel { cell: Some(cell), ..designcraft_doc::TextSel::caret(sid, 0) });
|
||||
let cx = ctx(&d, &s, &c, &l);
|
||||
let mut t = create("type");
|
||||
let shift = Mods { shift: true, ..Default::default() };
|
||||
let exec = |c: &str, p: serde_json::Value| vec![Action::Exec(c.into(), p)];
|
||||
assert_eq!(t.key(&cx, ToolKey::Tab, shift), exec("table.prevCell", serde_json::json!({})));
|
||||
assert_eq!(t.key(&cx, ToolKey::Tab, Mods::default()), exec("text.insert", serde_json::json!({"text": "\t"})));
|
||||
assert_eq!(t.key(&cx, ToolKey::Escape, Mods::default()), exec("table.toggleSelection", serde_json::json!({})));
|
||||
let s = Selection::text(designcraft_doc::TextSel::caret(sid, 0));
|
||||
let cx = ctx(&d, &s, &c, &l);
|
||||
assert_eq!(t.key(&cx, ToolKey::Tab, shift), exec("text.insert", serde_json::json!({"text": "\t"})));
|
||||
assert!(matches!(t.key(&cx, ToolKey::Escape, Mods::default()).first(), Some(Action::SwitchTool(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_tool_drags_a_table_column_border() {
|
||||
for degrees in [0.0, 90.0] {
|
||||
|
||||
@@ -359,6 +359,7 @@ impl Tool for TypeTool {
|
||||
let steps = if mods.cmd || mods.ctrl { 5.0 } else { 1.0 };
|
||||
vec![Action::Exec("type.kernStep".into(), json!({"steps": sign * steps}))]
|
||||
};
|
||||
let in_cell = cx.selection.text.is_some_and(|t| t.cell.is_some());
|
||||
match key {
|
||||
ToolKey::Left if mods.alt && !mods.shift => kern(-1.0),
|
||||
ToolKey::Right if mods.alt && !mods.shift => kern(1.0),
|
||||
@@ -371,7 +372,11 @@ impl Tool for TypeTool {
|
||||
ToolKey::Backspace => vec![Action::Exec("text.delete".into(), json!({"forward": false, "word": mods.alt}))],
|
||||
ToolKey::Delete => vec![Action::Exec("text.delete".into(), json!({"forward": true, "word": mods.alt}))],
|
||||
ToolKey::Enter => vec![Action::Exec("text.insert".into(), json!({"text": if mods.shift { "\u{2028}" } else { "\n" }}))],
|
||||
// In a table cell Shift+Tab goes back a cell (Tab goes on through `text.insert`).
|
||||
ToolKey::Tab if mods.shift && in_cell => vec![Action::Exec("table.prevCell".into(), json!({}))],
|
||||
ToolKey::Tab => vec![Action::Exec("text.insert".into(), json!({"text": "\t"}))],
|
||||
// In a table Esc toggles between the cell's text and the cell, as InDesign.
|
||||
ToolKey::Escape if in_cell => vec![Action::Exec("table.toggleSelection".into(), json!({}))],
|
||||
ToolKey::Escape => vec![Action::SwitchTool("selection".into()), Action::Exec("text.exitToFrame".into(), json!({}))],
|
||||
_ => vec![],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user