mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 21:10:40 +00:00
Tabs: setting the stops a paragraph already has records no undo step
text.tabs.set and text.tabs.clear return without an edit when every targeted paragraph already holds the stops. A Ctrl-drag that the 0 pt bound keeps from moving, or a drag released where it started, no longer adds an undo step that changes nothing. When one targeted paragraph holds other stops, the command still sets them all.
This commit is contained in:
@@ -69,6 +69,18 @@ fn apply(s: &mut Session, p: &Value, stops: Vec<TabStop>, label: &str) -> Result
|
||||
let ids = text_targets(s, p, "text.tabs.set")?;
|
||||
let range = TextRange::parse(p, "text.tabs.set")?;
|
||||
let n = stops.len();
|
||||
// Every targeted paragraph already holds these stops: no edit and no undo step.
|
||||
let doc = &s.doc()?.doc;
|
||||
let unchanged = ids.iter().all(|id| match doc.node(*id).map(|node| &node.kind) {
|
||||
Some(NodeKind::Text(t)) => match para_span(range, t) {
|
||||
None => t.para_styles().all(|pa| pa.tabs == stops),
|
||||
Some(mut r) => r.all(|i| t.para_at(i).tabs == stops),
|
||||
},
|
||||
_ => true,
|
||||
});
|
||||
if unchanged {
|
||||
return Ok(json!({ "stops": n }));
|
||||
}
|
||||
s.edit(label, |d, _| {
|
||||
for id in &ids {
|
||||
if let Some(NodeKind::Text(t)) = d.node_mut(*id).map(|n| &mut n.kind) {
|
||||
@@ -114,6 +126,33 @@ mod tests {
|
||||
assert_eq!(s.execute("text.tabs.get", &json!({})).unwrap()["stops"], json!([]));
|
||||
}
|
||||
|
||||
/// Setting the stops every targeted paragraph already holds records no undo step. When one
|
||||
/// targeted paragraph holds other stops, the command still sets them all.
|
||||
#[test]
|
||||
fn setting_the_same_stops_records_no_undo_step() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
s.execute("text.create", &json!({"x": 10, "y": 20, "text": "a\tb\nc\td"})).unwrap();
|
||||
let stops = json!({"stops": [{"position": 0}, {"position": 20, "leader": "."}]});
|
||||
s.execute("text.tabs.set", &stops).unwrap();
|
||||
let undo = |s: &Session| s.active().unwrap().history.undo.len();
|
||||
let before = undo(&s);
|
||||
s.execute("text.tabs.set", &stops).unwrap();
|
||||
s.execute("text.tabs.set", &json!({"stops": [{"position": 20, "leader": "."}, {"position": 0}]})).unwrap();
|
||||
assert_eq!(undo(&s), before);
|
||||
s.execute("text.tabs.clear", &json!({})).unwrap();
|
||||
s.execute("text.tabs.clear", &json!({})).unwrap();
|
||||
assert_eq!(undo(&s), before + 1);
|
||||
// The second paragraph (bytes 4 on) takes other stops; setting the first paragraph's stops
|
||||
// on both paragraphs changes the second.
|
||||
s.execute("text.tabs.set", &stops).unwrap();
|
||||
s.execute("text.tabs.set", &json!({"stops": [{"position": 50}], "start": 4, "end": 4})).unwrap();
|
||||
let before = undo(&s);
|
||||
s.execute("text.tabs.set", &stops).unwrap();
|
||||
assert_eq!(undo(&s), before + 1);
|
||||
assert_eq!(s.execute("text.tabs.get", &json!({"start": 4})).unwrap()["stops"][1]["position"], json!(20.0));
|
||||
}
|
||||
|
||||
/// #1877: a leader set the way the Tabs panel sets it fills the tab's gap with its dots.
|
||||
#[test]
|
||||
fn tab_leader_fills_the_gap_with_its_characters() {
|
||||
|
||||
@@ -690,6 +690,40 @@ fn command_drag_stops_at_zero_and_keeps_the_spacing() {
|
||||
assert!(moved_all(&[], 0, 10.0).is_none());
|
||||
}
|
||||
|
||||
/// A drag that leaves the stops where they were records no undo step: a Ctrl-drag (Command on
|
||||
/// macOS) to the left while the leftmost stop is at 0, and a plain drag that takes a stop away and
|
||||
/// releases it where it started. The dragged stop is still selected.
|
||||
#[test]
|
||||
fn a_drag_that_moves_no_stop_records_no_undo_step() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({})).unwrap();
|
||||
app.session.execute("text.create", &json!({"x": 10, "y": 20, "text": "a b", "area": {"width": 300, "height": 100}})).unwrap();
|
||||
app.session.execute("text.tabs.set", &json!({"stops": [{"position": 0}, {"position": 20}]})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
let positions = |app: &VectorcraftApp| current(app).unwrap().1.iter().map(|s| s.position).collect::<Vec<_>>();
|
||||
let before = app.session.active().unwrap().history.undo.len();
|
||||
command_drag_on_ruler(&ctx, &mut app, 20.0, 5.0);
|
||||
assert_eq!(positions(&app), [0.0, 20.0]);
|
||||
assert_eq!(app.session.active().unwrap().history.undo.len(), before, "Ctrl-drag");
|
||||
assert_eq!(pstate::<usize>(&ctx, "tabs-sel"), 1);
|
||||
set_pstate(&ctx, "tabs-sel", 0usize);
|
||||
let labels = ruler_frame(&ctx, &mut app, vec![], false);
|
||||
let [(zero, a), (next, b), ..] = &labels[..] else { panic!("ruler labels {labels:?}") };
|
||||
assert_eq!(zero, "0");
|
||||
let per = next.parse::<f64>().unwrap() * app.session.general_unit().points();
|
||||
let x = |pt: f64| egui::pos2(a.x - 2.0 + (pt / per) as f32 * (b.x - a.x), a.y + 15.0);
|
||||
let button = |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
|
||||
ruler_frame(&ctx, &mut app, vec![egui::Event::PointerMoved(x(20.0)), button(x(20.0), true)], false);
|
||||
for pt in [40.0, 60.0, 30.0, 20.0] {
|
||||
ruler_frame(&ctx, &mut app, vec![egui::Event::PointerMoved(x(pt))], false);
|
||||
}
|
||||
ruler_frame(&ctx, &mut app, vec![button(x(20.0), false)], false);
|
||||
ruler_frame(&ctx, &mut app, vec![], false);
|
||||
assert_eq!(positions(&app), [0.0, 20.0]);
|
||||
assert_eq!(app.session.active().unwrap().history.undo.len(), before, "plain drag");
|
||||
assert_eq!(pstate::<usize>(&ctx, "tabs-sel"), 1);
|
||||
}
|
||||
|
||||
/// A Ctrl-drag (Command on macOS) that would put a stop past 100,000 pt stops when the rightmost
|
||||
/// stop reaches 100,000 pt, so the stops move and keep their spacing.
|
||||
#[test]
|
||||
|
||||
+2
-1
@@ -5632,7 +5632,8 @@ spaceBefore, spaceAfter, hyphenate, mojikumi, burasagari, direction, leadingMode
|
||||
`start` / `end` byte offsets of the plain text (as `text.setRangeStyle` does): they change the paragraphs the range
|
||||
touches (a caret, `start == end`, touches its paragraph). Without a range they change every paragraph of the
|
||||
targeted objects, as before. With a range, the character attributes of `text.setStyle` and `text.setFormat` style
|
||||
that range of characters. `text.tabs.get {start?}` and `paraStyle.new` / `paraStyle.redefine {id, start?}` read the
|
||||
that range of characters. When every paragraph that `text.tabs.set` or `text.tabs.clear` targets already has those
|
||||
stops, the command changes nothing and records no undo step. `text.tabs.get {start?}` and `paraStyle.new` / `paraStyle.redefine {id, start?}` read the
|
||||
paragraph at `start`; `paraStyle.list` counts paragraphs as uses, and `charStyle.list` counts runs; both count the type in
|
||||
the layers, opacity masks, symbols, graph designs, pattern tiles and the art of brushes. `charStyle.unused` and
|
||||
`paraStyle.unused` (Select All Unused in the Character Styles and Paragraph Styles panel menus) list the styles with no
|
||||
|
||||
@@ -529,7 +529,7 @@ since 2026-10-01.
|
||||
- **The text style panel menus' Duplicate and Delete:** in the Character Styles and Paragraph Styles panel menus, Duplicate highlights the copy, as Create New Style highlights the new style, and Delete leaves no style highlighted, as the Delete button does, so Duplicate, Clear Overrides, Redefine and Style Options are disabled until another style is clicked. A highlighted style that is renamed in Style Options or deleted by a command is no longer highlighted either. The tests `panels::text_styles::tests::panel_menu_duplicate_and_delete_update_the_highlighted_style` and `a_highlighted_style_that_is_no_longer_listed_is_not_highlighted` cover this. Public reference: [Character and paragraph styles](https://web.archive.org/web/20261015081534/https://helpx.adobe.com/illustrator/using/character-paragraph-styles.html).
|
||||
- **Names of new styles and symbols:** `charStyle.new` and `paraStyle.new` trim the white space around the name they are given, as `charStyle.rename` and `paraStyle.rename` do, and refuse a name that is then empty; Duplicate names its copy from the trimmed name. `symbol.new` and `symbol.duplicate` trim the name as Symbol Options does, and `library.use` adds a library's text style under its trimmed name. Public reference: [Character and paragraph styles](https://web.archive.org/web/20260825113735/https://helpx.adobe.com/illustrator/using/character-paragraph-styles.html).
|
||||
- **Naming a new text style first:** Alt-click on New in the Character Styles or Paragraph Styles panel opens New Character Style or New Paragraph Style (`ui.newCharStyle`, `ui.newParaStyle`) with the name a plain click's style gets; OK makes the style from the text selected when the dialog opened, and Cancel adds none. Tests `panels::text_styles::tests::alt_clicking_new_character_style_asks_for_its_name` and `alt_clicking_new_paragraph_style_asks_for_its_name`. Public reference: [Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html), which lists Alt-click for opening panel options and names Actions, Brushes, Swatches and Symbols as exceptions.
|
||||
- **Moving all tab stops:** in the Tabs panel, a stop dragged on the ruler with Ctrl held (Command on macOS) moves every stop by the same distance, in one `text.tabs.set` and one undo step; the stops keep their order, alignment and leaders, the ruler shows them moving during the drag, and a drag that would take the leftmost stop below 0 leaves it at 0, or the rightmost past 100,000 pt leaves it at 100,000 pt, with the other stops at their spacing from it. Ctrl must be held when the drag starts. Tests `panels::tabs::tests::command_drag_moves_all_the_stops_together`, `command_drag_stops_at_zero_and_keeps_the_spacing` and `command_drag_stops_at_the_largest_position_and_keeps_the_spacing`. Public reference: [Manage tab stops](https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/manage-tab-stops.html), which says that a Ctrl-drag (Windows) or Command-drag (macOS) of a tab moves all the tab stops at the same time.
|
||||
- **Moving all tab stops:** in the Tabs panel, a stop dragged on the ruler with Ctrl held (Command on macOS) moves every stop by the same distance, in one `text.tabs.set` and one undo step; the stops keep their order, alignment and leaders, the ruler shows them moving during the drag, and a drag that would take the leftmost stop below 0 leaves it at 0, or the rightmost past 100,000 pt leaves it at 100,000 pt, with the other stops at their spacing from it. A drag that leaves every stop where it was records no undo step. Ctrl must be held when the drag starts. Tests `panels::tabs::tests::command_drag_moves_all_the_stops_together`, `command_drag_stops_at_zero_and_keeps_the_spacing`, `command_drag_stops_at_the_largest_position_and_keeps_the_spacing` and `a_drag_that_moves_no_stop_records_no_undo_step`. Public reference: [Manage tab stops](https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/manage-tab-stops.html), which says that a Ctrl-drag (Windows) or Command-drag (macOS) of a tab moves all the tab stops at the same time.
|
||||
- **Width Point Edit:** Shift-double-click enables Adjust Adjoining Width Points; plain double-click and direct opening retain the default. Tool dispatch and headless dialog tests cover neighboring widths, Undo/Redo and Cancel. Public reference: [Variable-width strokes](https://helpx.adobe.com/illustrator/using/stroke-object.html).
|
||||
Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a feature lands.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user