mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 07:42:42 +00:00
Type: Justification, OpenType, text styles and Convert to Text change every frame of a selected story
With the Selection tool, the Justification dialog, the OpenType panel, a Character or Paragraph Styles row (Alt-click and Clear Overrides included) and Type > Bullets and Numbering > Convert to Text now change every frame of each selected threaded story, locked and hidden frames included, in one undo step. Before, they changed only the selected frames. - The Justification dialog keeps the frames of the stories selected when it opened for its preview and OK. - The OpenType panel's Figure menu shows Mixed and its checkboxes show a dash when the characters differ; a click on a dash turns the feature on everywhere. - The style panels highlight a style only when all of the text has it, and show "+" when any of it overrides the style. - Convert to Text takes the same stories as Apply Bullets and Apply Numbers.
This commit is contained in:
@@ -260,7 +260,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"type.convertListToText",
|
||||
"Convert to Text",
|
||||
"",
|
||||
"{} Type › Bullets and Numbering › Convert to Text: the bullets and numbers of the selected type's list paragraphs (or of those the Type tool's selection touches) become characters of the text, followed by a tab, and the lists are turned off (text.convertListToText); the Type tool's anchor and caret move past the characters inserted at or before them, into the frame the caret's character moved to in threaded type",
|
||||
"{} Type › Bullets and Numbering › Convert to Text: the bullets and numbers of the selected type's list paragraphs, in every frame of a threaded story (or of those the Type tool's selection touches), become characters of the text, followed by a tab, and the lists are turned off (text.convertListToText); the Type tool's anchor and caret move past the characters inserted at or before them, into the frame the caret's character moved to in threaded type",
|
||||
),
|
||||
(
|
||||
"align.horizontalLeft",
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
//! Fields: `wordMin`, `wordDesired`, `wordMax` (0..1000), `letterMin`, `letterDesired`,
|
||||
//! `letterMax` (-100..500), `glyphMin`, `glyphDesired`, `glyphMax` (50..200), `autoLeading`
|
||||
//! (0..500), `singleWord` (`fullJustify`, `alignLeft`, `alignCenter`, `alignRight`) and `preview`,
|
||||
//! with `__initial` (the values it opened with), `__targets` (the type and range it acts on) and,
|
||||
//! after Select All in threaded text, `__story` (the edited frame, whose story it acts on).
|
||||
//! with `__initial` (the values it opened with), `__targets` (the type and range it acts on; with
|
||||
//! the Selection tool, every frame of each story selected when the dialog opened, locked and hidden
|
||||
//! frames included) and, after Select All in threaded text, `__story` (the edited frame, whose
|
||||
//! story it acts on).
|
||||
//!
|
||||
//! Behaviour reference (public documentation):
|
||||
//! <https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/adjust-word-and-letterspacing-in-justified-text.html>.
|
||||
@@ -95,6 +97,9 @@ pub fn open(app: &mut VectorcraftApp) -> Result<Value, String> {
|
||||
targets = json!({"ids": [id.0], "start": a, "end": b});
|
||||
// After Select All in threaded text: every paragraph of every frame ([`params`]).
|
||||
story = crate::panels::character::text_story(app).map(|_| id.0);
|
||||
} else {
|
||||
// With the Selection tool: every frame of each selected story.
|
||||
crate::panels::character::target_selected_stories(app, &mut targets);
|
||||
}
|
||||
let mut values = Map::new();
|
||||
for (_, prefix, _) in ROWS {
|
||||
@@ -423,6 +428,74 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// A threaded story of three frames, its second frame locked and its third hidden, a type object
|
||||
/// of its own, and the first frame selected with the Selection tool → the app, the frames and
|
||||
/// the other type object.
|
||||
fn story_selected_in_its_first_frame() -> (VectorcraftApp, [u64; 3], u64) {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 900, "height": 300})).unwrap();
|
||||
let story = "The quick brown fox jumps over the lazy dog.\n".repeat(12);
|
||||
let area = json!({"x": 10, "y": 10, "text": story, "size": 12, "area": {"width": 150, "height": 60}});
|
||||
let a = app.run("text.create", area).unwrap()["id"].as_u64().unwrap();
|
||||
let b = app.run("text.thread.add", json!({"from": a, "rect": [200, 10, 150, 60]})).unwrap()["id"].as_u64().unwrap();
|
||||
let c = app.run("text.thread.add", json!({"from": b, "rect": [400, 10, 150, 200]})).unwrap()["id"].as_u64().unwrap();
|
||||
let other = app.run("text.create", json!({"x": 10, "y": 250, "text": "Other", "size": 12})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("select.set", json!({"ids": [b]})).unwrap();
|
||||
app.run("object.lock", json!({})).unwrap();
|
||||
app.run("select.set", json!({"ids": [c]})).unwrap();
|
||||
app.run("object.hide", json!({})).unwrap();
|
||||
app.run("select.set", json!({"ids": [a]})).unwrap();
|
||||
(app, [a, b, c], other)
|
||||
}
|
||||
|
||||
/// Every paragraph's Auto Leading in `id`.
|
||||
fn auto_leadings(app: &VectorcraftApp, id: u64) -> Vec<f64> {
|
||||
match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind {
|
||||
NodeKind::Text(t) => (0..t.paragraph_count()).map(|i| t.para_at(i).justification.auto_leading).collect(),
|
||||
_ => panic!("not text"),
|
||||
}
|
||||
}
|
||||
|
||||
/// With the Selection tool a selected frame of threaded text stands for its whole story: the
|
||||
/// dialog opens blank where a locked or hidden frame differs, previews on every frame, Cancel
|
||||
/// restores them, and OK changes every frame of the story as it was when the dialog opened, in
|
||||
/// one undo step, leaving other type and a selection made later alone.
|
||||
#[test]
|
||||
fn selection_tool_justification_reaches_every_frame_of_the_story() {
|
||||
let (mut app, frames, other) = story_selected_in_its_first_frame();
|
||||
let [a, _, c] = frames;
|
||||
for id in frames {
|
||||
assert!(!auto_leadings(&app, id).is_empty(), "frame {id} holds text");
|
||||
}
|
||||
app.run("text.setFormat", json!({"ids": [c], "autoLeading": 150})).unwrap();
|
||||
app.run("select.set", json!({"ids": [a]})).unwrap();
|
||||
app.run("ui.justification", json!({})).unwrap();
|
||||
assert!(!app.ui.dialog.as_ref().unwrap().fields.contains_key("autoLeading"), "the hidden frame differs");
|
||||
set(&mut app, "autoLeading", json!(200));
|
||||
set(&mut app, "preview", json!(true));
|
||||
frame(&mut app);
|
||||
for id in frames {
|
||||
assert!(auto_leadings(&app, id).iter().all(|l| *l == 200.0), "the preview shows on frame {id}");
|
||||
}
|
||||
super::super::cancel(&mut app);
|
||||
assert!(auto_leadings(&app, a).iter().all(|l| *l == 120.0));
|
||||
assert!(auto_leadings(&app, c).iter().all(|l| *l == 150.0), "Cancel restores the hidden frame");
|
||||
|
||||
app.run("ui.justification", json!({})).unwrap();
|
||||
// A selection made while the dialog is open does not change what it acts on.
|
||||
app.run("select.set", json!({"ids": [other]})).unwrap();
|
||||
set(&mut app, "autoLeading", json!(140));
|
||||
let undo = app.session.active().unwrap().history.undo.len();
|
||||
super::super::confirm(&mut app).unwrap();
|
||||
assert_eq!(app.session.active().unwrap().history.undo.len(), undo + 1, "one undo step");
|
||||
for id in frames {
|
||||
assert!(auto_leadings(&app, id).iter().all(|l| *l == 140.0), "frame {id} takes the change");
|
||||
}
|
||||
assert!(auto_leadings(&app, other).iter().all(|l| *l == 120.0), "other type keeps its paragraphs");
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert!(auto_leadings(&app, c).iter().all(|l| *l == 150.0), "undo restores the hidden frame");
|
||||
}
|
||||
|
||||
/// When paragraphs in scope disagree, only shared justification values are filled in.
|
||||
#[test]
|
||||
fn justification_dialog_mixed_paragraph_fields_show_blank() {
|
||||
|
||||
@@ -261,7 +261,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"type.convertListToText",
|
||||
"Convert to Text",
|
||||
"",
|
||||
"{} Type › Bullets and Numbering › Convert to Text: the bullets and numbers of the selected type's list paragraphs (or of those the Type tool's selection touches) become characters of the text, followed by a tab, and the lists are turned off (text.convertListToText); the Type tool's anchor and caret move past the characters inserted at or before them, into the frame the caret's character moved to in threaded type",
|
||||
"{} Type › Bullets and Numbering › Convert to Text: the bullets and numbers of the selected type's list paragraphs, in every frame of a threaded story (or of those the Type tool's selection touches), become characters of the text, followed by a tab, and the lists are turned off (text.convertListToText); the Type tool's anchor and caret move past the characters inserted at or before them, into the frame the caret's character moved to in threaded type",
|
||||
),
|
||||
(
|
||||
"align.horizontalLeft",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! OpenType panel: per-character OpenType features (ligatures, alternates, swashes, small caps,
|
||||
//! ordinals, fractions, figure style, position) for the selected text or the Type tool's selected
|
||||
//! characters.
|
||||
//! ordinals, fractions, figure style, position) for the selected text (every frame of each
|
||||
//! selected threaded story) or the Type tool's selected characters.
|
||||
|
||||
use egui::Ui;
|
||||
use serde_json::json;
|
||||
@@ -112,6 +112,22 @@ fn position_label(shown: Option<usize>) -> &'static str {
|
||||
shown.and_then(|i| POSITIONS.get(i).copied()).unwrap_or("Mixed")
|
||||
}
|
||||
|
||||
/// The Figure menu entry for the characters the panel acts on, or `None` when they differ.
|
||||
fn shown_figure(app: &VectorcraftApp) -> Option<usize> {
|
||||
shared(app, |s| figure(&OtFeatures::from_tags(s.features.iter().map(String::as_str))))
|
||||
}
|
||||
|
||||
/// Whether `flag` is on for every character the panel acts on (`Some(true)`), for none of them
|
||||
/// (`Some(false)`), or only for some (`None`).
|
||||
fn shown_flag(app: &VectorcraftApp, flag: Flag) -> Option<bool> {
|
||||
shared(app, |s| flag.get(&OtFeatures::from_tags(s.features.iter().map(String::as_str))))
|
||||
}
|
||||
|
||||
/// The Figure menu's text for entry `shown`: Mixed when the characters differ.
|
||||
fn figure_label(shown: Option<usize>) -> &'static str {
|
||||
shown.and_then(|i| FIGURES.get(i).copied()).unwrap_or("Mixed")
|
||||
}
|
||||
|
||||
/// A change one of the panel's controls makes to the features of each character it acts on.
|
||||
#[derive(Clone, Copy)]
|
||||
enum Change {
|
||||
@@ -161,7 +177,8 @@ fn set(app: &mut VectorcraftApp, f: OtFeatures) {
|
||||
}
|
||||
|
||||
/// Send `tags` as the `key` param (`features` or `featureChanges`) for the Type tool's range or
|
||||
/// caret, or else for the selected type.
|
||||
/// caret, or else for every frame of the selected stories
|
||||
/// ([`super::character::selected_stories`]).
|
||||
fn run_features(app: &mut VectorcraftApp, key: &str, tags: Vec<String>) {
|
||||
// After Select All in threaded text: every frame of the story.
|
||||
let story = super::character::text_story(app).and_then(|story| story.first().copied());
|
||||
@@ -169,7 +186,11 @@ fn run_features(app: &mut VectorcraftApp, key: &str, tags: Vec<String>) {
|
||||
(Some(_), Some(first)) => ("text.setRangeStyle", json!({ "id": first.0, "story": true })),
|
||||
(Some((id, a, b)), None) if b > a => ("text.setRangeStyle", json!({ "id": id.0, "start": a, "end": b })),
|
||||
(Some((id, _, _)), None) => ("text.setRangeStyle", json!({ "id": id.0 })),
|
||||
(None, _) => ("text.setStyle", json!({})),
|
||||
(None, _) => {
|
||||
let mut p = json!({});
|
||||
super::character::target_selected_stories(app, &mut p);
|
||||
("text.setStyle", p)
|
||||
}
|
||||
};
|
||||
p[key] = json!(tags);
|
||||
if let Err(e) = app.run(cmd, p) {
|
||||
@@ -184,10 +205,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
};
|
||||
let cur = OtFeatures::from_tags(st.features.iter().map(String::as_str));
|
||||
let mut change = None;
|
||||
let fig = shown_figure(app);
|
||||
ui.horizontal(|ui| {
|
||||
widgets::dim_label(ui, tl!("Figure:"));
|
||||
let fig = figure(&cur);
|
||||
if let Some(i) = widgets::dropdown(ui, "ot-figure", FIGURES[fig], &FIGURES, 170.0) {
|
||||
if let Some(i) = widgets::dropdown(ui, "ot-figure", figure_label(fig), &FIGURES, 170.0) {
|
||||
change = Some(Change::Figure(i));
|
||||
}
|
||||
});
|
||||
@@ -199,26 +220,30 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
});
|
||||
widgets::divider(ui);
|
||||
let mut next = cur;
|
||||
if let Some(c) = change {
|
||||
c.apply(&mut next);
|
||||
}
|
||||
// A row whose characters differ shows a dash, and a click turns the feature on for all of them.
|
||||
for (label, flag) in rows() {
|
||||
let on = flag.get(&next);
|
||||
if widgets::check(ui, label, on, true) {
|
||||
change = Some(Change::Toggle(flag, !on));
|
||||
let shown = shown_flag(app, flag);
|
||||
if widgets::check3(ui, label, shown, true) {
|
||||
change = Some(Change::Toggle(flag, shown != Some(true)));
|
||||
}
|
||||
}
|
||||
// The font, not the text, decides which numbered sets can be offered. Each set is an
|
||||
// independent feature, so the menu permits several at once. A font's own label wins over
|
||||
// the numbered fallback. See the public Stylistic Sets help page.
|
||||
let sets = offered_sets(&st);
|
||||
// the numbered fallback. See the public Stylistic Sets help page. A set is checked when every
|
||||
// character has it.
|
||||
let sets: Vec<(StylisticSet, Flag, bool)> = offered_sets(&st)
|
||||
.into_iter()
|
||||
.map(|set| {
|
||||
let flag = Flag::Extra(EXTRA_FEATURES[usize::from(set.number - 1)]);
|
||||
let on = shown_flag(app, flag) == Some(true);
|
||||
(set, flag, on)
|
||||
})
|
||||
.collect();
|
||||
ui.add_enabled_ui(!sets.is_empty(), |ui| {
|
||||
ui.menu_button(tl!("Stylistic Sets"), |ui| {
|
||||
widgets::menu_scroll(ui, |ui| {
|
||||
for set in &sets {
|
||||
let flag = Flag::Extra(EXTRA_FEATURES[usize::from(set.number - 1)]);
|
||||
let on = flag.get(&next);
|
||||
for (set, flag, on) in &sets {
|
||||
let (flag, on) = (*flag, *on);
|
||||
if widgets::check_name(ui, &set_label(set), on) {
|
||||
change = Some(Change::Toggle(flag, !on));
|
||||
}
|
||||
@@ -481,6 +506,90 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The features of every run of `id`, each as its tags.
|
||||
fn frame_features(app: &VectorcraftApp, id: u64) -> Vec<Vec<String>> {
|
||||
match &app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().kind {
|
||||
NodeKind::Text(t) => t.runs.iter().map(|r| r.style.features.clone()).collect(),
|
||||
_ => panic!("not text"),
|
||||
}
|
||||
}
|
||||
|
||||
/// With the Selection tool a selected frame of threaded text stands for its whole story: the
|
||||
/// Figure and Position menus show Mixed when a locked or hidden frame differs, and a choice
|
||||
/// reaches every frame in one undo step, each keeping its other features, while other type keeps
|
||||
/// its own. A story selected through two frames and a group is changed once.
|
||||
#[test]
|
||||
fn selection_tool_features_reach_every_frame_of_the_story() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 900, "height": 300})).unwrap();
|
||||
let story = "The quick brown fox jumps over the lazy dog 1234.\n".repeat(12);
|
||||
let area = json!({"x": 10, "y": 10, "text": story, "size": 12, "area": {"width": 150, "height": 60}});
|
||||
let a = app.run("text.create", area).unwrap()["id"].as_u64().unwrap();
|
||||
let b = app.run("text.thread.add", json!({"from": a, "rect": [200, 10, 150, 60]})).unwrap()["id"].as_u64().unwrap();
|
||||
let c = app.run("text.thread.add", json!({"from": b, "rect": [400, 10, 150, 200]})).unwrap()["id"].as_u64().unwrap();
|
||||
let other = app.run("text.create", json!({"x": 10, "y": 250, "text": "Other 12", "size": 12})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("select.set", json!({"ids": [b]})).unwrap();
|
||||
app.run("object.lock", json!({})).unwrap();
|
||||
app.run("select.set", json!({"ids": [c]})).unwrap();
|
||||
app.run("object.hide", json!({})).unwrap();
|
||||
app.run("text.setStyle", json!({"ids": [c], "features": ["onum", "smcp", "sups", "-liga"]})).unwrap();
|
||||
app.run("select.set", json!({"ids": [a]})).unwrap();
|
||||
for id in [a, b, c] {
|
||||
assert!(!frame_features(&app, id).is_empty(), "frame {id} holds text");
|
||||
}
|
||||
assert_eq!(shown_figure(&app), None, "the hidden frame's figures differ");
|
||||
assert_eq!(figure_label(shown_figure(&app)), "Mixed");
|
||||
assert_eq!(shown_position(&app), None, "the hidden frame's position differs");
|
||||
let hidden = frame_features(&app, c);
|
||||
let has = |app: &VectorcraftApp, id: u64, tag: &str| frame_features(app, id).iter().all(|f| f.iter().any(|t| t == tag));
|
||||
|
||||
let undo = undo_steps(&app);
|
||||
edit(&mut app, Change::Figure(1));
|
||||
assert_eq!(undo_steps(&app), undo + 1, "one undo step");
|
||||
assert_eq!(shown_figure(&app), Some(1));
|
||||
for id in [a, b] {
|
||||
assert!(frame_features(&app, id).iter().all(|f| *f == ["tnum"]), "frame {id}: {:?}", frame_features(&app, id));
|
||||
}
|
||||
assert!(
|
||||
["smcp", "sups", "-liga", "tnum"].iter().all(|t| has(&app, c, t))
|
||||
&& frame_features(&app, c).iter().all(|f| !f.iter().any(|t| t == "onum")),
|
||||
"{:?}",
|
||||
frame_features(&app, c)
|
||||
);
|
||||
assert!(frame_features(&app, other).iter().all(Vec::is_empty), "other type keeps its features");
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(frame_features(&app, c), hidden, "undo restores the hidden frame");
|
||||
|
||||
// Standard Ligatures is off in the hidden frame only: its row shows a dash, and a click on
|
||||
// it turns ligatures on in every frame.
|
||||
let liga = rows().into_iter().find(|(l, _)| *l == "Standard Ligatures").map(|(_, f)| f);
|
||||
let Some(liga) = liga else { panic!("no Standard Ligatures row") };
|
||||
assert_eq!(shown_flag(&app, liga), None);
|
||||
let ctx = egui::Context::default();
|
||||
let draw = |app: &mut VectorcraftApp, ui: &mut Ui| show(app, ui);
|
||||
super::super::tests_appearance::frame_events(&ctx, &mut app, vec![], draw);
|
||||
let texts = super::super::tests_appearance::frame_events(&ctx, &mut app, vec![], draw);
|
||||
let at = super::super::tests_appearance::text_rect(&texts, "Standard Ligatures").center();
|
||||
let undo = undo_steps(&app);
|
||||
super::super::tests_appearance::click(&ctx, &mut app, at, draw);
|
||||
assert_eq!(undo_steps(&app), undo + 1, "one undo step");
|
||||
assert_eq!(shown_flag(&app, liga), Some(true));
|
||||
for id in [a, b, c] {
|
||||
assert!(frame_features(&app, id).iter().all(|f| !f.iter().any(|t| t == "-liga")), "frame {id} has ligatures");
|
||||
}
|
||||
assert!(has(&app, c, "smcp"), "the hidden frame keeps its other features");
|
||||
|
||||
// Two frames of the story and the other type in a group: each story once.
|
||||
app.run("object.unlockAll", json!({})).unwrap();
|
||||
app.run("select.set", json!({"ids": [a, other]})).unwrap();
|
||||
let group = app.run("object.group", json!({})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("select.set", json!({"ids": [group, b]})).unwrap();
|
||||
edit(&mut app, Change::Position(1));
|
||||
for id in [a, b, c, other] {
|
||||
assert!(frame_features(&app, id).iter().all(|f| f.iter().filter(|t| *t == "sups").count() == 1), "frame {id} has sups once");
|
||||
}
|
||||
}
|
||||
|
||||
/// Type with two positions turned on shows the Position menu as mixed, and the entry of one
|
||||
/// of them turns the other off.
|
||||
#[test]
|
||||
|
||||
@@ -79,13 +79,7 @@ fn para_cmd(app: &mut VectorcraftApp, cmd: &str, p: Value) {
|
||||
/// Run `cmd` on the paragraphs the Paragraph panel acts on ([`format`]) and return its result. With
|
||||
/// the Selection tool these are the paragraphs of every frame of the selected stories
|
||||
/// ([`super::character::selected_stories`]).
|
||||
pub(crate) fn run_on_paragraphs(app: &mut VectorcraftApp, cmd: &str, p: Value) -> Result<Value, String> {
|
||||
run_on(app, cmd, p, true)
|
||||
}
|
||||
|
||||
/// [`run_on_paragraphs`], with the Selection tool on the selected type objects alone unless
|
||||
/// `stories`.
|
||||
fn run_on(app: &mut VectorcraftApp, cmd: &str, mut p: Value, stories: bool) -> Result<Value, String> {
|
||||
pub(crate) fn run_on_paragraphs(app: &mut VectorcraftApp, cmd: &str, mut p: Value) -> Result<Value, String> {
|
||||
if let Some((id, a, b)) = super::character::text_editing(app) {
|
||||
super::character::end_typing(app);
|
||||
match super::character::text_story(app) {
|
||||
@@ -97,7 +91,7 @@ fn run_on(app: &mut VectorcraftApp, cmd: &str, mut p: Value, stories: bool) -> R
|
||||
p["end"] = json!(b);
|
||||
}
|
||||
}
|
||||
} else if stories {
|
||||
} else {
|
||||
super::character::target_selected_stories(app, &mut p);
|
||||
}
|
||||
app.run(cmd, p)
|
||||
@@ -260,7 +254,7 @@ pub(crate) fn shared_single_word_justify(app: &VectorcraftApp) -> Option<SingleW
|
||||
}
|
||||
|
||||
/// A paragraph attribute shared by every paragraph in scope, or `None` when they differ.
|
||||
pub(crate) fn shared_para<T: PartialEq + Copy>(app: &VectorcraftApp, field: impl Fn(&ParaStyle) -> T) -> Option<T> {
|
||||
pub(crate) fn shared_para<T: PartialEq>(app: &VectorcraftApp, field: impl Fn(&ParaStyle) -> T) -> Option<T> {
|
||||
let mut acc = Shared::None;
|
||||
let mut any = false;
|
||||
visit_paragraphs(app, |p| {
|
||||
@@ -474,12 +468,13 @@ pub(crate) fn number_sample(style: vectorcraft_doc::text::NumberStyle, n: u32) -
|
||||
}
|
||||
|
||||
/// Type › Bullets and Numbering › Convert to Text (`type.convertListToText`): the markers of the
|
||||
/// list paragraphs of the selected type, or of those the Type tool's selection touches, become
|
||||
/// characters of the text and the lists are turned off (`text.convertListToText`, which also moves
|
||||
/// the Type tool's anchor and caret past the characters inserted at or before them).
|
||||
/// list paragraphs the Paragraph panel acts on ([`run_on_paragraphs`]: with the Selection tool,
|
||||
/// every frame of the selected stories) become characters of the text and the lists are turned
|
||||
/// off (`text.convertListToText`, which also moves the Type tool's anchor and caret past the
|
||||
/// characters inserted at or before them).
|
||||
pub(crate) fn convert_list_to_text(app: &mut VectorcraftApp) -> Result<Value, String> {
|
||||
text_style(app).ok_or("select some type first")?;
|
||||
run_on(app, "text.convertListToText", json!({}), false)
|
||||
run_on_paragraphs(app, "text.convertListToText", json!({}))
|
||||
}
|
||||
|
||||
/// The Bullets button and the arrow beside it (Paragraph panel). The button makes the paragraphs
|
||||
|
||||
@@ -678,6 +678,50 @@ fn convert_to_text_keeps_the_caret_by_its_character_in_threaded_type() {
|
||||
assert_eq!(super::super::character::text_editing(&app).map(|(id, _, _)| id), Some(NodeId(a)));
|
||||
}
|
||||
|
||||
/// With the Selection tool, Convert to Text takes the same story as Apply Numbers: the numbers
|
||||
/// of every frame of the selected story, its locked and hidden frames included, become characters
|
||||
/// of the text in one undo step, and other type keeps its list.
|
||||
#[test]
|
||||
fn selection_tool_convert_to_text_takes_every_frame_of_the_story() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 900, "height": 300})).unwrap();
|
||||
let story = "The quick brown fox jumps over the lazy dog.\n".repeat(12);
|
||||
let area = json!({"x": 10, "y": 10, "text": story, "size": 12, "area": {"width": 150, "height": 60}});
|
||||
let a = app.run("text.create", area).unwrap()["id"].as_u64().unwrap();
|
||||
let b = app.run("text.thread.add", json!({"from": a, "rect": [200, 10, 150, 60]})).unwrap()["id"].as_u64().unwrap();
|
||||
let c = app.run("text.thread.add", json!({"from": b, "rect": [400, 10, 150, 200]})).unwrap()["id"].as_u64().unwrap();
|
||||
let other = app.run("text.create", json!({"x": 10, "y": 250, "text": "Other", "size": 12})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("text.setFormat", json!({"ids": [other], "list": "bullet"})).unwrap();
|
||||
app.run("select.set", json!({"ids": [b]})).unwrap();
|
||||
app.run("object.lock", json!({})).unwrap();
|
||||
app.run("select.set", json!({"ids": [c]})).unwrap();
|
||||
app.run("object.hide", json!({})).unwrap();
|
||||
app.run("select.set", json!({"ids": [a]})).unwrap();
|
||||
let frame = |app: &VectorcraftApp, id: u64| match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind {
|
||||
NodeKind::Text(t) => (t.plain_text(), (0..t.paragraph_count()).map(|i| t.para_at(i).list.is_some()).collect::<Vec<_>>()),
|
||||
_ => panic!("not text"),
|
||||
};
|
||||
let undo_steps = |app: &VectorcraftApp| app.session.active().unwrap().history.undo.len();
|
||||
app.run("type.applyNumbers", json!({})).unwrap();
|
||||
for id in [a, b, c] {
|
||||
let (_, lists) = frame(&app, id);
|
||||
assert!(!lists.is_empty() && lists.iter().all(|l| *l), "Apply Numbers numbers frame {id}");
|
||||
}
|
||||
let before = frame(&app, c);
|
||||
|
||||
let steps = undo_steps(&app);
|
||||
app.run("type.convertListToText", json!({})).unwrap();
|
||||
assert_eq!(undo_steps(&app), steps + 1, "one undo step");
|
||||
for id in [a, b, c] {
|
||||
let (plain, lists) = frame(&app, id);
|
||||
assert!(lists.iter().all(|l| !*l), "frame {id} leaves the list");
|
||||
assert!(plain.contains(".\tThe quick"), "frame {id} holds its numbers as text: {plain:?}");
|
||||
}
|
||||
assert_eq!(frame(&app, other), ("Other".to_string(), vec![true]), "other type keeps its list");
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(frame(&app, c), before, "undo restores the hidden frame");
|
||||
}
|
||||
|
||||
/// Reset Panel turns Hyphenate and Roman Hanging Punctuation off and sets the Hyphenation and
|
||||
/// Justification options back to their defaults in every paragraph it acts on, so the paragraphs
|
||||
/// save neither.
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
//! Character Styles and Paragraph Styles panels (one implementation, two kinds).
|
||||
//!
|
||||
//! Click a style to apply it to the selected text (or the Type tool's selected characters);
|
||||
//! Alt-click applies it and clears overrides. Double-clicking a character style's row opens its
|
||||
//! Character Style Options (`ui.charStyleOptions`), as the panel menu does; a paragraph style's row
|
||||
//! opens its Paragraph Style Options (`ui.paraStyleOptions`). The style of the
|
||||
//! selection is highlighted, with a "+" when the text overrides some of its attributes. New makes
|
||||
//! Click a style to apply it to the selected text (every frame of each selected threaded story) or
|
||||
//! the Type tool's selected characters; Alt-click applies it and clears overrides. Double-clicking a
|
||||
//! character style's row opens its Character Style Options (`ui.charStyleOptions`), as the panel
|
||||
//! menu does; a paragraph style's row opens its Paragraph Style Options (`ui.paraStyleOptions`).
|
||||
//! The style all of that text has is highlighted, with a "+" when some of the text overrides some
|
||||
//! of its attributes; text in several styles highlights none. New makes
|
||||
//! a style from the selected text; Alt-click on New first asks for its name (`ui.newCharStyle`,
|
||||
//! `ui.newParaStyle`).
|
||||
|
||||
@@ -15,8 +16,9 @@ use serde_json::{Map, Value, json};
|
||||
use vectorcraft_doc::Document;
|
||||
use vectorcraft_engine::cmd::{fileio, textstyles};
|
||||
|
||||
use super::character::{end_typing, text_editing, text_style};
|
||||
use super::character::{end_typing, shared, text_editing, text_style};
|
||||
use super::memo::Memo;
|
||||
use super::paragraph::shared_para;
|
||||
use super::{alt_held, pstate, selection_len, set_pstate};
|
||||
use crate::VectorcraftApp;
|
||||
use crate::theme::Tokens;
|
||||
@@ -172,14 +174,25 @@ thread_local! {
|
||||
static LISTED: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
/// The selection's style name (Normal when none) and every one of its attributes, written as a
|
||||
/// style holds them, if text is selected.
|
||||
fn current(app: &VectorcraftApp, kind: Kind) -> Option<(Option<String>, Map<String, Value>)> {
|
||||
let (c, p) = text_style(app)?;
|
||||
Some(match kind {
|
||||
Kind::Char => (c.style_name.clone(), textstyles::char_attrs(&c)),
|
||||
Kind::Para => (p.style_name.clone(), textstyles::para_attrs(&p)),
|
||||
})
|
||||
/// The style name (`None`: the Normal style) that every character the panel acts on has (paragraph
|
||||
/// styles: every paragraph), with the Selection tool in every frame of the selected stories;
|
||||
/// `None` when they differ or no type is selected.
|
||||
fn current(app: &VectorcraftApp, kind: Kind) -> Option<Option<String>> {
|
||||
match kind {
|
||||
Kind::Char => shared(app, |s| s.style_name.clone()),
|
||||
Kind::Para => shared_para(app, |p| p.style_name.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether any character (paragraph styles: paragraph) the panel acts on differs from the style
|
||||
/// attributes `attrs` in one of them.
|
||||
fn has_overrides(app: &VectorcraftApp, kind: Kind, attrs: &Map<String, Value>) -> bool {
|
||||
let differs = |m: Map<String, Value>| attrs.iter().any(|(k, v)| m.get(k) != Some(v));
|
||||
let same = match kind {
|
||||
Kind::Char => shared(app, |s| differs(textstyles::char_attrs(s))),
|
||||
Kind::Para => shared_para(app, |p| differs(textstyles::para_attrs(p))),
|
||||
};
|
||||
same != Some(false)
|
||||
}
|
||||
|
||||
/// Parameters targeting the selection: the Type tool's selected characters (paragraph styles: the
|
||||
@@ -196,14 +209,19 @@ fn target(app: &VectorcraftApp, kind: Kind, mut p: Value) -> Value {
|
||||
}
|
||||
|
||||
/// [`target`] for applying a style: after Select All in threaded text, all the text of every
|
||||
/// frame of the story.
|
||||
/// frame of the story; with the Selection tool, every frame of the selected stories
|
||||
/// ([`super::character::selected_stories`]).
|
||||
fn apply_target(app: &VectorcraftApp, kind: Kind, mut p: Value) -> Value {
|
||||
if text_editing(app).is_none() {
|
||||
super::character::target_selected_stories(app, &mut p);
|
||||
return p;
|
||||
}
|
||||
match super::character::text_story(app) {
|
||||
Some(story) if text_editing(app).is_some() => {
|
||||
Some(story) => {
|
||||
p["ids"] = json!(story.iter().map(|f| f.0).collect::<Vec<_>>());
|
||||
p
|
||||
}
|
||||
_ => target(app, kind, p),
|
||||
None => target(app, kind, p),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,9 +260,8 @@ fn open_style_options(app: &mut VectorcraftApp, kind: Kind, name: &str) {
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let all = styles(app, kind);
|
||||
let cur = current(app, kind);
|
||||
let normal = all.first().map(|s| s.0.clone()).unwrap_or_default();
|
||||
let cur_name = cur.as_ref().map(|(n, _)| n.clone().unwrap_or_else(|| normal.clone()));
|
||||
let cur_name = current(app, kind).map(|n| n.unwrap_or_else(|| normal.clone()));
|
||||
let mut clicked: Option<(String, egui::Modifiers)> = None;
|
||||
// A double-clicked style's row, whose options open after the list is laid out.
|
||||
let mut options: Option<String> = None;
|
||||
@@ -270,8 +287,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) {
|
||||
widgets::list_box(ui, |ui| {
|
||||
for (name, attrs, _) in &all {
|
||||
let is_cur = cur_name.as_deref() == Some(name.as_str());
|
||||
// Overrides: an attribute the style sets that the selection doesn't match.
|
||||
let overridden = is_cur && cur.as_ref().is_some_and(|(_, m)| attrs.iter().any(|(k, v)| m.get(k) != Some(v)));
|
||||
// Overrides: an attribute the style sets that some of the text doesn't match.
|
||||
let overridden = is_cur && has_overrides(app, kind, attrs);
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 22.0), Sense::click_and_drag());
|
||||
if resp.drag_started_by(egui::PointerButton::Primary) {
|
||||
egui::DragAndDrop::set_payload(ui.ctx(), RowDrag { kind, name: name.clone() });
|
||||
@@ -310,7 +327,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) {
|
||||
if scroll_to.is_some() {
|
||||
set_pstate::<Option<String>>(ui.ctx(), kind.scroll_key(), None);
|
||||
}
|
||||
let has_text = cur.is_some();
|
||||
let has_text = text_style(app).is_some();
|
||||
if let Some((name, m)) = clicked {
|
||||
sel = super::swatches::click_selection(ui, kind.anchor_key(), sel, &listed, &name, m);
|
||||
// Shift and Cmd/Ctrl clicks only highlight, as in the Graphic Styles panel. The second
|
||||
@@ -381,7 +398,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) {
|
||||
let normal = if kind == Kind::Char { "[Normal Character Style]" } else { "[Normal Paragraph Style]" };
|
||||
let user = sel.as_ref().is_some_and(|s| s != normal);
|
||||
let doomed: Vec<&String> = many.iter().filter(|s| *s != normal).collect();
|
||||
let has_text = current(app, kind).is_some() || selection_len(app) > 0;
|
||||
let has_text = text_style(app).is_some() || selection_len(app) > 0;
|
||||
let p = kind.prefix();
|
||||
if menu_item(ui, &crate::i18n::fmt(tl!("New {kind} Style…"), &[("kind", kind.what_label())]), true, false) {
|
||||
// A later typing preview must not restore the document from before this capture.
|
||||
|
||||
@@ -29,8 +29,8 @@ fn panels_draw_and_apply_headless() {
|
||||
out.textures_delta.clear();
|
||||
}
|
||||
app.run("charStyle.apply", target(&app, Kind::Char, json!({"name": "Big"}))).unwrap();
|
||||
let (name, attrs) = current(&app, Kind::Char).unwrap();
|
||||
assert_eq!((name.as_deref(), attrs["size"].as_f64()), (Some("Big"), Some(30.0)));
|
||||
let (style, _) = text_style(&app).unwrap();
|
||||
assert_eq!((current(&app, Kind::Char), style.size), (Some(Some("Big".to_string())), 30.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -818,6 +818,74 @@ fn the_styles_are_listed_once_per_document_version() {
|
||||
}
|
||||
}
|
||||
|
||||
/// The style names of `id`'s runs (`Kind::Para`: of its paragraphs).
|
||||
fn frame_style_names(app: &VectorcraftApp, kind: Kind, id: u64) -> Vec<Option<String>> {
|
||||
match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind {
|
||||
NodeKind::Text(t) if kind == Kind::Char => t.runs.iter().map(|r| r.style.style_name.clone()).collect(),
|
||||
NodeKind::Text(t) => (0..t.paragraph_count()).map(|i| t.para_at(i).style_name.clone()).collect(),
|
||||
_ => panic!("not text"),
|
||||
}
|
||||
}
|
||||
|
||||
/// With the Selection tool a selected frame of threaded text stands for its whole story, in both
|
||||
/// panels: no row shows as the current style while a locked or hidden frame has another style, a
|
||||
/// row click applies the style to every frame in one undo step and leaves other type alone, the
|
||||
/// row shows "+" while a hidden frame overrides the style, and an Alt-click clears that override.
|
||||
#[test]
|
||||
fn selection_tool_styles_reach_every_frame_of_the_story() {
|
||||
use egui::Modifiers;
|
||||
for kind in [Kind::Char, Kind::Para] {
|
||||
let what = kind.what();
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 900, "height": 300})).unwrap();
|
||||
let story = "The quick brown fox jumps over the lazy dog.\n".repeat(12);
|
||||
let area = json!({"x": 10, "y": 10, "text": story, "size": 12, "area": {"width": 150, "height": 60}});
|
||||
let a = app.run("text.create", area).unwrap()["id"].as_u64().unwrap();
|
||||
let b = app.run("text.thread.add", json!({"from": a, "rect": [200, 10, 150, 60]})).unwrap()["id"].as_u64().unwrap();
|
||||
let c = app.run("text.thread.add", json!({"from": b, "rect": [400, 10, 150, 200]})).unwrap()["id"].as_u64().unwrap();
|
||||
let other = app.run("text.create", json!({"x": 10, "y": 250, "text": "Other", "size": 12})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("select.set", json!({"ids": [b]})).unwrap();
|
||||
app.run("object.lock", json!({})).unwrap();
|
||||
app.run("select.set", json!({"ids": [c]})).unwrap();
|
||||
app.run("object.hide", json!({})).unwrap();
|
||||
app.run("select.set", json!({"ids": [a]})).unwrap();
|
||||
app.run(&format!("{}.new", kind.prefix()), json!({"name": "Plain"})).unwrap();
|
||||
app.run(&format!("{}.new", kind.prefix()), json!({"name": "Second", "attrs": {}})).unwrap();
|
||||
app.run(&format!("{}.apply", kind.prefix()), json!({"name": "Second", "ids": [c]})).unwrap();
|
||||
app.run("select.set", json!({"ids": [a]})).unwrap();
|
||||
for id in [a, b, c] {
|
||||
assert!(!frame_style_names(&app, kind, id).is_empty(), "frame {id} holds text");
|
||||
}
|
||||
assert_eq!(current(&app, kind), None, "{what} Styles: the hidden frame has another style");
|
||||
|
||||
let ctx = egui::Context::default();
|
||||
let rows = |app: &mut VectorcraftApp, ui: &mut egui::Ui| {
|
||||
ui.set_width(280.0);
|
||||
show(app, ui, kind);
|
||||
};
|
||||
let undo = app.session.doc().unwrap().history.undo.len();
|
||||
click_text_held(&ctx, &mut app, "Plain", Modifiers::NONE, rows);
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 1, "{what} Styles: one undo step");
|
||||
for id in [a, b, c] {
|
||||
assert!(frame_style_names(&app, kind, id).iter().all(|n| n.as_deref() == Some("Plain")), "{what} Styles: frame {id}");
|
||||
}
|
||||
assert!(frame_style_names(&app, kind, other).iter().all(Option::is_none), "{what} Styles: other type keeps its style");
|
||||
assert_eq!(current(&app, kind), Some(Some("Plain".to_string())));
|
||||
|
||||
app.run("text.setFormat", json!({"ids": [c], "kerning": 40, "composer": "singleLine"})).unwrap();
|
||||
app.run("select.set", json!({"ids": [a]})).unwrap();
|
||||
let shown = crate::tests_labels::painted_text(&mut app, |app, ui| show(app, ui, kind));
|
||||
assert!(shown.lines().any(|line| line == "Plain+"), "{what} Styles: the hidden frame's override shows: {shown}");
|
||||
// A new context, so that this click does not count as the second click of a double-click.
|
||||
click_text_held(&egui::Context::default(), &mut app, "Plain+", Modifiers::ALT, rows);
|
||||
let shown = crate::tests_labels::painted_text(&mut app, |app, ui| show(app, ui, kind));
|
||||
assert!(shown.lines().any(|line| line == "Plain"), "{what} Styles: Alt-click clears the override: {shown}");
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
let shown = crate::tests_labels::painted_text(&mut app, |app, ui| show(app, ui, kind));
|
||||
assert!(shown.lines().any(|line| line == "Plain+"), "{what} Styles: undo brings the override back: {shown}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A row dragged from the highlighted styles carries all of them: dropped on Create New Style it
|
||||
/// duplicates each in one undo step and highlights the copies, and dropped on Delete it deletes each
|
||||
/// but the Normal style in one undo step and clears the highlight. A row from outside the highlight
|
||||
@@ -1201,7 +1269,7 @@ fn alt_clicking_new_paragraph_style_asks_for_its_name() {
|
||||
assert!(app.session.doc().unwrap().doc.para_styles.is_empty(), "no style is added before OK");
|
||||
set_name(&mut app, "Body Copy");
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
let (_, attrs) = current(&app, Kind::Para).unwrap();
|
||||
let attrs = textstyles::para_attrs(&text_style(&app).unwrap().1);
|
||||
let doc = &app.session.doc().unwrap().doc;
|
||||
assert_eq!(doc.para_styles.iter().map(|s| (s.name.as_str(), &s.attrs)).collect::<Vec<_>>(), [("Body Copy", &attrs)]);
|
||||
|
||||
|
||||
+3
-1
@@ -8946,7 +8946,9 @@ send an Alignment or Level change for paragraphs that stay in no list. With no t
|
||||
with an unknown `tab`, `ui.bulletsAndNumbering` is an error.
|
||||
|
||||
Type › Bullets and Numbering › Convert to Text runs `type.convertListToText` on the selected type's paragraphs, or on
|
||||
those the Type tool's selection touches, and that runs `text.convertListToText {ids?, start?, end?}`. The marker of
|
||||
those the Type tool's selection touches, and that runs `text.convertListToText {ids?, start?, end?}`. With the
|
||||
Selection tool, a selected frame of threaded text names every frame of its story in `ids`, locked and hidden frames
|
||||
included, as Apply Bullets and Apply Numbers do. The marker of
|
||||
each list paragraph becomes characters at the start of the paragraph: the bullet, or the number and its period as the
|
||||
paragraph showed it, then a tab. They are set in the marker's font, size and color: the style of the paragraph's first
|
||||
character without the attributes the marker leaves out. The paragraph leaves its list. A paragraph at a list level
|
||||
|
||||
@@ -997,6 +997,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- With the whole story of threaded text selected by Select All, the Character, Paragraph, OpenType, Tabs and Text Styles panels, the Justification dialog and the font menu change the frames that are in the edited frame's thread at the time of the change, locked and hidden frames included. A frame that has left the thread is not changed, and a frame added to the thread is (VectorCraft's own behavior; `panels::character::tests::story_changes_follow_the_edited_frames_thread_after_the_first_saved_frame_leaves`, `story_changes_leave_out_a_frame_that_left_and_take_in_a_frame_that_joined`, `story_changes_stay_in_the_edited_frame_once_its_thread_is_removed`, `story_changes_take_in_locked_and_hidden_frames`, `panels::tabs::tests::indent_markers_after_select_all_keep_each_story_paragraphs_indents`, `dialogs::justification::tests::justification_dialog_follows_the_story_thread_at_ok`).
|
||||
- With the whole story of threaded text selected by Select All, the Character, Paragraph, OpenType, Tabs and Text Styles panels, the Justification dialog and the font menu change the frames that are in the edited frame's thread at the time of the change, locked and hidden frames included. A frame that has left the thread is not changed, and a frame added to the thread is (VectorCraft's own behavior; `panels::character::tests::story_changes_follow_the_edited_frames_thread_after_the_first_saved_frame_leaves`, `story_changes_leave_out_a_frame_that_left_and_take_in_a_frame_that_joined`, `story_changes_stay_in_the_edited_frame_once_its_thread_is_removed`, `story_changes_take_in_locked_and_hidden_frames`).
|
||||
- With the Selection tool, a Character or Paragraph panel change to a selected frame of threaded text changes every frame of its story, locked and hidden frames included, as one undo step. The anti-aliasing method, Variable Font axes, the Tabs panel's tab stops and both panels' Reset Panel change the whole story too, and the panels show a value only when every frame of the story has it. A story selected through several of its frames or through a group is changed once, and other type keeps its formatting (VectorCraft's own behavior; `panels::character::tests::selection_tool_changes_reach_every_frame_of_the_story`, `selection_tool_fields_read_every_frame_of_the_story`, `selected_stories_list_each_frame_once`, `panels::paragraph::tests::selection_tool_paragraph_changes_reach_every_frame_of_the_story`, `panels::tabs::tests::selection_tool_tab_stops_reach_every_frame_of_the_story`).
|
||||
- With the Selection tool, the Justification dialog, the OpenType panel, a Character or Paragraph Styles row and Type › Bullets and Numbering › Convert to Text also change every frame of a selected story, locked and hidden frames included, as one undo step. The Justification dialog acts on the story's frames as they were when it opened. When the frames differ, the OpenType panel's Figure menu shows Mixed, its checkboxes show a dash (a click turns the feature on everywhere), and the style panels highlight no row; a row shows "+" when any frame overrides its style (VectorCraft's own behavior; `dialogs::justification::tests::selection_tool_justification_reaches_every_frame_of_the_story`, `panels::opentype::tests::selection_tool_features_reach_every_frame_of_the_story`, `panels::text_styles::tests::selection_tool_styles_reach_every_frame_of_the_story`, `panels::paragraph::tests::selection_tool_convert_to_text_takes_every_frame_of_the_story`).
|
||||
- Switching from a Type tool to the Eyedropper keeps a whole story selected by Select All: a pick applies to the text of every frame as one undo step, and switching back restores the story selection (VectorCraft's own behavior; `textedit::switching_to_the_eyedropper_keeps_the_whole_story_selected`).
|
||||
- While the Type tool edits text, a character typed with AltGr is typed and runs no command. Firefox on Windows reports AltGr as Ctrl and Alt held, so in the web build AltGr+L (ł on a Polish layout) does not also open Layer Options for a new layer, and AltGr+Q (@ on a German layout) does not also run Reset Tracking and Kerning. A Ctrl+Alt chord that types no character runs its command.
|
||||
- On a layout without Latin letters, a one-key shortcut goes by the position of the key that typed the character, in the web build too, where the character arrives before its key press: on a Russian layout the P key (`з`) chooses the Pen, and two keys pressed in one frame each run their own shortcut.
|
||||
|
||||
Reference in New Issue
Block a user