mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-14 14:22:39 +00:00
Object styles: Text Frame General Options set only the General page
- An object style's text frame options set the columns, insets, vertical justification, paragraph spacing limit and Ignore Text Wrap. A frame keeps its auto-size, baseline, column rule and Type on a Path settings, and follows an edit of the style while its General options match the style's. - textFrame refuses keys outside the General options, takes one number for all four insets, and checks the values the call gives. - Applying a style that changes nothing records no undo step. - The dialog's Paragraph Spacing Limit reaches the style. - IDML import: a style's Text Frame General Options take the settings its TextFramePreference leaves out from the styles it is based on (by id or bare name), skipping styles that turn the category off. - i18n: Hungarian and Swedish rows for the object style dialog; the duplicate Brazilian Portuguese rows are removed.
This commit is contained in:
1 parent
630c2e2bba
commit
f47acd5dfd
11 files changed
+285
-30
No files matched your search
@@ -240,6 +240,49 @@ impl Default for TextFrameOptions {
|
||||
}
|
||||
}
|
||||
|
||||
impl TextFrameOptions {
|
||||
/// The JSON keys of the General options: columns, inset spacing, vertical justification and
|
||||
/// Ignore Text Wrap. An object style's Text Frame General Options category sets these only.
|
||||
pub const GENERAL_KEYS: [&str; 9] = [
|
||||
"columns",
|
||||
"columnsKind",
|
||||
"gutter",
|
||||
"columnWidth",
|
||||
"balanceColumns",
|
||||
"inset",
|
||||
"verticalJustification",
|
||||
"vjParagraphSpacingLimit",
|
||||
"ignoreWrap",
|
||||
];
|
||||
|
||||
/// Whether the General options (see [`Self::GENERAL_KEYS`]) of `self` and `other` are the same.
|
||||
pub fn general_eq(&self, other: &TextFrameOptions) -> bool {
|
||||
self.columns == other.columns
|
||||
&& self.columns_kind == other.columns_kind
|
||||
&& self.gutter == other.gutter
|
||||
&& self.column_width == other.column_width
|
||||
&& self.balance_columns == other.balance_columns
|
||||
&& self.inset == other.inset
|
||||
&& self.vertical_justification == other.vertical_justification
|
||||
&& self.vj_paragraph_spacing_limit == other.vj_paragraph_spacing_limit
|
||||
&& self.ignore_wrap == other.ignore_wrap
|
||||
}
|
||||
|
||||
/// Copies the General options of `from` (see [`Self::GENERAL_KEYS`]). Baseline, auto-size,
|
||||
/// column rule and Type on a Path settings stay as they are.
|
||||
pub fn set_general(&mut self, from: &TextFrameOptions) {
|
||||
self.columns = from.columns;
|
||||
self.columns_kind = from.columns_kind;
|
||||
self.gutter = from.gutter;
|
||||
self.column_width = from.column_width;
|
||||
self.balance_columns = from.balance_columns;
|
||||
self.inset = from.inset;
|
||||
self.vertical_justification = from.vertical_justification;
|
||||
self.vj_paragraph_spacing_limit = from.vj_paragraph_spacing_limit;
|
||||
self.ignore_wrap = from.ignore_wrap;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum WrapMode {
|
||||
|
||||
@@ -72,7 +72,8 @@ pub struct ObjectStyle {
|
||||
}
|
||||
|
||||
impl ObjectStyle {
|
||||
/// Gives `it` the fill, stroke, corners, effects and text frame options this style sets.
|
||||
/// Gives `it` the fill, stroke, corners, effects and text frame General options this style
|
||||
/// sets. A text frame keeps its baseline, auto-size, column rule and Type on a Path settings.
|
||||
pub fn apply_to(&self, it: &mut Item) {
|
||||
if let Some(c) = self.corners {
|
||||
it.corners = c.named_to_path(&it.path);
|
||||
@@ -87,7 +88,7 @@ impl ObjectStyle {
|
||||
it.effects = e.clone();
|
||||
}
|
||||
if let (Some(tf), Some(o)) = (it.text_frame_mut(), &self.text_frame) {
|
||||
tf.options = o.clone();
|
||||
tf.options.set_general(o);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -173,7 +173,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Apply Object Style",
|
||||
[],
|
||||
None,
|
||||
"{name, ids?} — sets the style's fill, stroke, effects and text frame options on the items",
|
||||
"{name, ids?} — sets the style's fill, stroke, corners, effects and text frame General options on the items, and its paragraph styles on their text; records nothing when the items already have them",
|
||||
super::has_selection,
|
||||
apply_object
|
||||
),
|
||||
@@ -182,7 +182,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"New Object Style…",
|
||||
[],
|
||||
None,
|
||||
"{name, fromSelection?: true (fill, stroke, corners, effects and text frame options of the first selected item), fill?: swatch | {swatch?, tint?}, stroke?: {swatch?, weight?, tint?, type?, …}, corners?: {shape?, size?} | {corners: [{shape, size}] ×4}, paragraphStyle?: a paragraph style (null: none; the text of a frame the style is applied to takes it), applyNextStyle?: bool (each paragraph after the first takes the Next Style of the one before), textFrame?: {columns?, columnsKind?, gutter?, columnWidth?, balanceColumns?, inset?: [top, left, bottom, right], verticalJustification?, ignoreWrap?, …} | null (the options of `document.inspect` text frames; null: the style sets none), effects?: {dropShadow?, feather?, innerShadow?, outerGlow?, innerGlow?, bevel?, satin?, gradientFeather?, directionalFeather?}, dropShadow?: {on?, color?, opacity? (0–1), angle?, globalLight?, distance?, size?, spread?}, feather?: width, innerShadow?, outerGlow?, innerGlow?, bevel?, satin?, gradientFeather?, directionalFeather?} → {name} (made unique)",
|
||||
"{name, fromSelection?: true (fill, stroke, corners, effects and text frame options of the first selected item), fill?: swatch | {swatch?, tint?}, stroke?: {swatch?, weight?, tint?, type?, …}, corners?: {shape?, size?} | {corners: [{shape, size}] ×4}, paragraphStyle?: a paragraph style (null: none; the text of a frame the style is applied to takes it), applyNextStyle?: bool (each paragraph after the first takes the Next Style of the one before), textFrame?: {columns? (1–40), columnsKind?, gutter?, columnWidth?, balanceColumns?, inset?: [top, left, bottom, right] | one number for all four, verticalJustification?: top|center|bottom|justify, vjParagraphSpacingLimit?, ignoreWrap?} | null (Text Frame General Options, named as `document.inspect` names text frame options; lengths 0–15552 pt; other keys are refused; a frame keeps its baseline, auto-size and column rule settings; null: the style sets none), effects?: {dropShadow?, feather?, innerShadow?, outerGlow?, innerGlow?, bevel?, satin?, gradientFeather?, directionalFeather?}, dropShadow?: {on?, color?, opacity? (0–1), angle?, globalLight?, distance?, size?, spread?}, feather?: width, innerShadow?, outerGlow?, innerGlow?, bevel?, satin?, gradientFeather?, directionalFeather?} → {name} (made unique)",
|
||||
has_doc,
|
||||
create_object
|
||||
),
|
||||
@@ -191,7 +191,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Object Style Options…",
|
||||
[],
|
||||
None,
|
||||
"{name, rename?: new name (every use follows), fill?: swatch | {swatch?, tint?} | null, stroke?: {swatch?, weight?, tint?, type?, …} | null, corners?: {shape?: none|rounded|inverseRounded|inset|bevel|fancy, size?} | {corners: [{shape, size}] ×4} | null (null: the style no longer sets it; items keep theirs), paragraphStyle?: a paragraph style (null: none; the text of a frame the style is applied to takes it), applyNextStyle?: bool (each paragraph after the first takes the Next Style of the one before), textFrame?: {columns?, columnsKind?, gutter?, columnWidth?, balanceColumns?, inset?: [top, left, bottom, right], verticalJustification?, ignoreWrap?, …} | null (the options of `document.inspect` text frames; null: the style sets none), effects?: {…} | null (the style no longer sets effects), dropShadow?, feather?, innerShadow?, outerGlow?, innerGlow?, bevel?, satin?, gradientFeather?, directionalFeather?} — effect objects merge field by field into the style's effects (fields as in `document.inspect` item effects); values that aren't finite numbers are refused, the others clamped (opacities 0–1, sizes and distances ≥ 0); items using the style whose effects (fill, stroke, corners) match the style's (an item's defaults, when the style set none) take the new ones, items with their own keep them → {name, updated}",
|
||||
"{name, rename?: new name (every use follows), fill?: swatch | {swatch?, tint?} | null, stroke?: {swatch?, weight?, tint?, type?, …} | null, corners?: {shape?: none|rounded|inverseRounded|inset|bevel|fancy, size?} | {corners: [{shape, size}] ×4} | null (null: the style no longer sets it; items keep theirs), paragraphStyle?: a paragraph style (null: none; the text of a frame the style is applied to takes it), applyNextStyle?: bool (each paragraph after the first takes the Next Style of the one before), textFrame?: {columns? (1–40), columnsKind?, gutter?, columnWidth?, balanceColumns?, inset?: [top, left, bottom, right] | one number for all four, verticalJustification?: top|center|bottom|justify, vjParagraphSpacingLimit?, ignoreWrap?} | null (Text Frame General Options, named as `document.inspect` names text frame options; lengths 0–15552 pt; other keys are refused; a frame keeps its baseline, auto-size and column rule settings; null: the style sets none), effects?: {…} | null (the style no longer sets effects), dropShadow?, feather?, innerShadow?, outerGlow?, innerGlow?, bevel?, satin?, gradientFeather?, directionalFeather?} — effect objects merge field by field into the style's effects (fields as in `document.inspect` item effects); values that aren't finite numbers are refused, the others clamped (opacities 0–1, sizes and distances ≥ 0); items using the style whose effects (fill, stroke, corners) match the style's (an item's defaults, when the style set none) take the new ones, items with their own keep them; the text of frames still in the old paragraph styles takes the new ones → {name, updated, restyled}",
|
||||
has_doc,
|
||||
edit_object
|
||||
),
|
||||
|
||||
@@ -4,6 +4,18 @@ pub(super) fn apply_object(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let name = str_param(p, "name").ok_or_else(|| bad("style.object.apply", "missing name"))?.to_string();
|
||||
let os = s.doc()?.doc.styles.object_style(&name).cloned().ok_or_else(|| bad("style.object.apply", format!("no object style `{name}`")))?;
|
||||
let ids = crate::cmd::targets(s, p)?;
|
||||
// Nothing to record when every target already has the style and its attributes, and its text
|
||||
// the style's paragraph styles.
|
||||
let d = &s.doc()?.doc;
|
||||
let unchanged = ids.iter().filter_map(|id| d.item(*id)).all(|it| {
|
||||
let mut next = it.clone();
|
||||
next.object_style = name.clone();
|
||||
os.apply_to(&mut next);
|
||||
next == *it && it.text_frame().and_then(|tf| d.story(tf.story)).is_none_or(|st| os.story_follows(&d.styles, st))
|
||||
});
|
||||
if unchanged {
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
s.edit(|d, _| {
|
||||
let mut stories = Vec::new();
|
||||
for id in &ids {
|
||||
@@ -170,19 +182,33 @@ fn paragraph_style_param(cmd: &str, s: &Session, p: &Value) -> Result<Option<Opt
|
||||
}
|
||||
}
|
||||
|
||||
/// `textFrame` (a text frame's options, as `document.inspect` shows them) merged over `base`, or
|
||||
/// null; refused when the columns aren't 1–40 or a gutter, width or inset isn't 0–15552 pt.
|
||||
/// `textFrame` (a text frame's General options, as `document.inspect` shows them) merged over
|
||||
/// `base`, or null; refused when it names another key, the columns aren't 1–40 or a gutter, width
|
||||
/// or inset isn't 0–15552 pt. `inset` is `[top, left, bottom, right]` or one number for all four.
|
||||
fn text_frame_param(
|
||||
cmd: &str,
|
||||
p: &Value,
|
||||
base: Option<&designcraft_doc::TextFrameOptions>,
|
||||
) -> Result<Option<Option<designcraft_doc::TextFrameOptions>>> {
|
||||
let tf = group_param(cmd, p, "textFrame", base.unwrap_or(&designcraft_doc::TextFrameOptions::default()))?;
|
||||
let general = designcraft_doc::TextFrameOptions::GENERAL_KEYS;
|
||||
let mut p = p.clone();
|
||||
if let Some(Value::Object(o)) = p.get_mut("textFrame") {
|
||||
if let Some(k) = o.keys().find(|k| !general.contains(&k.as_str())) {
|
||||
return Err(bad(cmd, format!("textFrame.{k} is not a General option ({})", general.join(", "))));
|
||||
}
|
||||
if let Some(n) = o.get("inset").filter(|v| v.is_number()).cloned() {
|
||||
o.insert("inset".into(), json!([n, n, n, n]));
|
||||
}
|
||||
}
|
||||
let tf = group_param(cmd, &p, "textFrame", base.unwrap_or(&designcraft_doc::TextFrameOptions::default()))?;
|
||||
if let Some(Some(o)) = &tf {
|
||||
if !(1..=40).contains(&o.columns) {
|
||||
// Only the values given in this call are checked.
|
||||
let given = |k: &str| p.get("textFrame").is_some_and(|t| t.get(k).is_some());
|
||||
if given("columns") && !(1..=40).contains(&o.columns) {
|
||||
return Err(bad(cmd, "text frame columns must be 1–40"));
|
||||
}
|
||||
let mut lengths = [o.gutter, o.column_width, o.vj_paragraph_spacing_limit, o.first_baseline_min].into_iter().chain(o.inset);
|
||||
let lengths = [("gutter", o.gutter), ("columnWidth", o.column_width), ("vjParagraphSpacingLimit", o.vj_paragraph_spacing_limit)];
|
||||
let mut lengths = lengths.into_iter().filter(|(k, _)| given(k)).map(|(_, v)| v).chain(o.inset.into_iter().filter(|_| given("inset")));
|
||||
if !lengths.all(|v| v.is_finite() && (0.0..=15552.0).contains(&v)) {
|
||||
return Err(bad(cmd, "a text frame's gutter, widths and insets must be 0–15552 pt"));
|
||||
}
|
||||
@@ -360,7 +386,7 @@ pub(super) fn edit_object(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|| fill.as_ref().is_some_and(|(o, _)| it.fill == *o)
|
||||
|| stroke.as_ref().is_some_and(|(o, _)| it.stroke == *o)
|
||||
|| corners.as_ref().is_some_and(|(o, _)| it.corners == o.named_to_path(&it.path))
|
||||
|| frame.as_ref().is_some_and(|(o, _)| it.text_frame().is_some_and(|t| t.options == *o))
|
||||
|| frame.as_ref().is_some_and(|(o, _)| it.text_frame().is_some_and(|t| t.options.general_eq(o)))
|
||||
};
|
||||
let updated = d.update_items(
|
||||
|it| it.object_style == new.name && follows(it),
|
||||
@@ -387,9 +413,9 @@ pub(super) fn edit_object(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
if let Some((o, n)) = &frame
|
||||
&& let Some(tf) = it.text_frame_mut()
|
||||
&& tf.options == *o
|
||||
&& tf.options.general_eq(o)
|
||||
{
|
||||
tf.options = n.clone();
|
||||
tf.options.set_general(n);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -882,6 +882,78 @@ fn object_styles_carry_a_paragraph_style_and_text_frame_options() {
|
||||
assert_eq!((styles_of(&s, a), options(&s, a).columns), (vec!["Box Body Text".to_string(); 3], 2));
|
||||
}
|
||||
|
||||
/// An object style's Text Frame General Options set the columns, insets, vertical justification
|
||||
/// and Ignore Text Wrap only: a frame keeps its auto-size and column rule, follows an edit of the
|
||||
/// style while its General options are the style's, and keeps the settings through save and open.
|
||||
/// Applying a style that changes nothing records no undo step, threaded frames share one Next
|
||||
/// Style chain, and keys outside the General options are refused.
|
||||
#[test]
|
||||
fn object_style_text_frame_general_options_leave_the_other_frame_settings() {
|
||||
use designcraft_doc::{AutoSize, ItemId, VerticalJustification};
|
||||
let mut s = session();
|
||||
s.execute("style.paragraph.create", &json!({"name": "Body"})).unwrap();
|
||||
s.execute("style.paragraph.create", &json!({"name": "Lead", "nextStyle": "Body"})).unwrap();
|
||||
let name = "Callout";
|
||||
s.execute(
|
||||
"style.object.create",
|
||||
&json!({"name": name, "fromSelection": false, "paragraphStyle": "Lead", "applyNextStyle": true,
|
||||
"textFrame": {"inset": 6, "verticalJustification": "center"}}),
|
||||
)
|
||||
.unwrap();
|
||||
let r = s.execute("frame.create", &json!({"rect": [36, 36, 156, 200], "content": "text", "text": "One\nTwo", "caret": false})).unwrap();
|
||||
let a = r["id"].as_u64().unwrap();
|
||||
let b = s.execute("frame.create", &json!({"rect": [200, 36, 320, 200], "content": "unassigned"})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("object.textFrameOptions", &json!({"ids": [a], "autoSize": "heightOnly", "columnRule": true})).unwrap();
|
||||
s.execute("style.object.apply", &json!({"name": name, "ids": [a]})).unwrap();
|
||||
let options = |s: &Session, id: u64| s.doc().unwrap().doc.item(ItemId(id)).unwrap().text_frame().unwrap().options.clone();
|
||||
let o = options(&s, a);
|
||||
assert_eq!((o.inset, o.vertical_justification), ([6.0; 4], VerticalJustification::Center));
|
||||
assert_eq!((o.auto_size, o.column_rule), (AutoSize::HeightOnly, true), "the frame's own auto-size and column rule stay");
|
||||
|
||||
// Applying it again changes nothing and records no undo step.
|
||||
let undo = |s: &Session| s.doc().unwrap().history.undo.len();
|
||||
let before = undo(&s);
|
||||
s.execute("style.object.apply", &json!({"name": name, "ids": [a]})).unwrap();
|
||||
assert_eq!(undo(&s), before, "no undo step when nothing changes");
|
||||
|
||||
// The frame differs from the style's options only in auto-size, so it follows the edit.
|
||||
let r = s.execute("style.object.edit", &json!({"name": name, "textFrame": {"inset": [1, 2, 3, 4], "ignoreWrap": true}})).unwrap();
|
||||
assert_eq!(r["updated"], 1);
|
||||
let o = options(&s, a);
|
||||
assert_eq!((o.inset, o.ignore_wrap, o.auto_size), ([1.0, 2.0, 3.0, 4.0], true, AutoSize::HeightOnly));
|
||||
|
||||
// Threaded frames: the story's paragraphs take one Next Style chain across both frames.
|
||||
s.execute("text.thread", &json!({"from": a, "to": b})).unwrap();
|
||||
s.execute("style.object.apply", &json!({"name": name, "ids": [a, b]})).unwrap();
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let sid = d.item(ItemId(a)).unwrap().text_frame().unwrap().story;
|
||||
assert_eq!(d.item(ItemId(b)).unwrap().text_frame().unwrap().story, sid);
|
||||
assert_eq!(d.story(sid).unwrap().paras.iter().map(|p| p.style.as_str()).collect::<Vec<_>>(), ["Lead", "Body"]);
|
||||
|
||||
// Save and open keep the style's General options and Apply Next Style.
|
||||
let bytes = s.execute("file.serialize", &json!({})).unwrap();
|
||||
let mut back = Session::new();
|
||||
back.execute("file.openBytes", &json!({"name": "copy", "base64": bytes["base64"]})).unwrap();
|
||||
let os = back.doc().unwrap().doc.styles.object_style(name).cloned().unwrap();
|
||||
assert!(os.apply_next_style);
|
||||
assert_eq!(os.text_frame.map(|t| (t.inset, t.ignore_wrap)), Some(([1.0, 2.0, 3.0, 4.0], true)));
|
||||
|
||||
// Keys outside the General options, a misspelled key and a word for the inset are refused.
|
||||
for bad in [json!({"autoSize": "heightOnly"}), json!({"insets": 4}), json!({"inset": "wide"}), json!({"verticalJustification": "middle"})] {
|
||||
let p = json!({"name": name, "textFrame": bad});
|
||||
assert!(s.execute("style.object.edit", &p).is_err(), "{bad}");
|
||||
}
|
||||
// A style that already holds 60 columns (as a file can give it) takes other edits, and 60
|
||||
// given again is refused.
|
||||
{
|
||||
let d = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
let st = d.styles_mut().object.iter_mut().find(|o| o.name == name).unwrap();
|
||||
st.text_frame.as_mut().unwrap().columns = 60;
|
||||
}
|
||||
s.execute("style.object.edit", &json!({"name": name, "textFrame": {"inset": 2}})).unwrap();
|
||||
assert!(s.execute("style.object.edit", &json!({"name": name, "textFrame": {"columns": 60}})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_style_effects_reach_grouped_and_anchored_items() {
|
||||
use designcraft_doc::ItemId;
|
||||
|
||||
@@ -197,8 +197,16 @@ impl<'r> Importer<'r> {
|
||||
let paragraph_style = enabled("EnableParagraphStyle", "AppliedParagraphStyle")
|
||||
.then(|| e.get("AppliedParagraphStyle").map(|r| self.para_style_ref(r)))
|
||||
.flatten();
|
||||
let levels = based_on_levels(e, &objects_by_self);
|
||||
// Text Frame General Options: each attribute from the nearest style whose
|
||||
// `TextFramePreference` sets it, among the styles that don't turn the category off.
|
||||
let text_frame = if e.get("EnableTextFrameGeneralOptions") == Some("true") {
|
||||
e.find("TextFramePreference").map(|t| self.frame_options(&[t]))
|
||||
let prefs: Vec<&El> = levels
|
||||
.iter()
|
||||
.filter(|l| l.get("EnableTextFrameGeneralOptions") != Some("false"))
|
||||
.filter_map(|l| l.find("TextFramePreference"))
|
||||
.collect();
|
||||
(!prefs.is_empty()).then(|| self.frame_options(&prefs))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -207,19 +215,7 @@ impl<'r> Importer<'r> {
|
||||
// that sets some corners takes the others from the styles it is based on, as the style
|
||||
// holds all four.
|
||||
let corners = (e.get("EnableStrokeAndCornerOptions") != Some("false") && corners(e, "").is_some())
|
||||
.then(|| {
|
||||
let mut levels = vec![e];
|
||||
let mut next = e.prop("BasedOn");
|
||||
while let Some(b) = next.take()
|
||||
&& levels.len() < 16
|
||||
&& let Some(s) = objects_by_self.get(b.trim()).copied()
|
||||
&& !levels.iter().any(|l| std::ptr::eq(*l, s))
|
||||
{
|
||||
levels.push(s);
|
||||
next = s.prop("BasedOn");
|
||||
}
|
||||
corners_in(&levels, "")
|
||||
})
|
||||
.then(|| corners_in(&levels, ""))
|
||||
.flatten()
|
||||
.map(|c| CornerOptions { corners: c });
|
||||
let apply_next_style = paragraph_style.is_some() && e.get("ApplyNextParagraphStyle") == Some("true");
|
||||
@@ -421,3 +417,23 @@ impl<'r> Importer<'r> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Object style `e`, then the styles it is based on, nearest first (cycle-safe, at most 16). A
|
||||
/// `BasedOn` may name a style by its `Self` id or by its bare name.
|
||||
fn based_on_levels<'e>(e: &'e El, objects_by_self: &HashMap<&str, &'e El>) -> Vec<&'e El> {
|
||||
let id = |b: String| {
|
||||
let b = b.trim();
|
||||
if b.starts_with("ObjectStyle/") { b.to_string() } else { format!("ObjectStyle/{}", names::escape_id(b)) }
|
||||
};
|
||||
let mut levels = vec![e];
|
||||
let mut next = e.prop("BasedOn").map(id);
|
||||
while let Some(b) = next.take()
|
||||
&& levels.len() < 16
|
||||
&& let Some(s) = objects_by_self.get(b.as_str()).copied()
|
||||
&& !levels.iter().any(|l| std::ptr::eq(*l, s))
|
||||
{
|
||||
levels.push(s);
|
||||
next = s.prop("BasedOn").map(id);
|
||||
}
|
||||
levels
|
||||
}
|
||||
@@ -177,6 +177,43 @@ fn object_style_apply_next_style_round_trips() {
|
||||
}
|
||||
}
|
||||
|
||||
/// A style's Text Frame General Options take each setting its own `TextFramePreference` leaves out
|
||||
/// from the style it is based on (named by its id or bare name, and only when that style doesn't
|
||||
/// turn the category off), and export and import keep the merged options.
|
||||
#[test]
|
||||
fn object_style_text_frame_options_inherit_from_the_based_on_style() {
|
||||
let styles = r#"<ObjectStyle Self="ObjectStyle/Box" Name="Box" EnableTextFrameGeneralOptions="true">
|
||||
<Properties><BasedOn type="string">$ID/[None]</BasedOn></Properties>
|
||||
<TextFramePreference TextColumnCount="2" VerticalJustification="CenterAlign"/>
|
||||
</ObjectStyle>
|
||||
<ObjectStyle Self="ObjectStyle/Inset" Name="Inset" EnableTextFrameGeneralOptions="true">
|
||||
<Properties><BasedOn type="object">ObjectStyle/Box</BasedOn></Properties>
|
||||
<TextFramePreference InsetSpacing="9"/>
|
||||
</ObjectStyle>
|
||||
<ObjectStyle Self="ObjectStyle/Bare" Name="Bare" EnableTextFrameGeneralOptions="true">
|
||||
<Properties><BasedOn type="string">Box</BasedOn></Properties>
|
||||
<TextFramePreference InsetSpacing="3"/>
|
||||
</ObjectStyle>
|
||||
<ObjectStyle Self="ObjectStyle/Off" Name="Off" EnableTextFrameGeneralOptions="false">
|
||||
<Properties><BasedOn type="string">$ID/[None]</BasedOn></Properties>
|
||||
<TextFramePreference TextColumnCount="5"/>
|
||||
</ObjectStyle>
|
||||
<ObjectStyle Self="ObjectStyle/OnOff" Name="OnOff" EnableTextFrameGeneralOptions="true">
|
||||
<Properties><BasedOn type="object">ObjectStyle/Off</BasedOn></Properties>
|
||||
<TextFramePreference InsetSpacing="1"/>
|
||||
</ObjectStyle>"#;
|
||||
let d = fixture(styles, &[]);
|
||||
let tf = d.styles.object_style("Inset").and_then(|s| s.text_frame.clone()).expect("Inset sets text frame options");
|
||||
assert_eq!((tf.inset, tf.columns, tf.vertical_justification), ([9.0; 4], 2, designcraft_doc::VerticalJustification::Center));
|
||||
let bare = d.styles.object_style("Bare").and_then(|s| s.text_frame.clone()).expect("Bare sets text frame options");
|
||||
assert_eq!((bare.inset, bare.columns), ([3.0; 4], 2), "a BasedOn given as a bare name");
|
||||
let on_off = d.styles.object_style("OnOff").and_then(|s| s.text_frame.clone()).expect("OnOff sets text frame options");
|
||||
assert_eq!((on_off.inset, on_off.columns), ([1.0; 4], 1), "a based-on style with the category off gives nothing");
|
||||
let back = import_idml(&export_idml(&d)).unwrap();
|
||||
let again = back.styles.object_style("Inset").and_then(|s| s.text_frame.clone()).expect("after a round trip");
|
||||
assert!(again.general_eq(&tf), "{again:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exported_frames_keep_their_own_values_over_their_object_style() {
|
||||
let mut d = small_doc();
|
||||
|
||||
@@ -1979,6 +1979,19 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Measure Tool", "Mérőeszköz"),
|
||||
("Hand Tool", "Kéz eszköz"),
|
||||
("Zoom Tool", "Nagyító eszköz"),
|
||||
("Apply Next Style", "Következő stílus alkalmazása"),
|
||||
("Text Frame General Options", "Szövegkeret általános beállításai"),
|
||||
("New Object Style", "Új objektumstílus"),
|
||||
("Object Style Options", "Objektumstílus beállításai"),
|
||||
("Object Style Options…", "Objektumstílus beállításai…"),
|
||||
("Basic Attributes", "Alapattribútumok"),
|
||||
("Stroke & Corner Options", "Körvonal- és sarokbeállítások"),
|
||||
("Effects for:", "Hatások ehhez:"),
|
||||
("Create New Style", "Új stílus létrehozása"),
|
||||
("Clear Overrides", "Felülírások törlése"),
|
||||
("Shape:", "Alakzat:"),
|
||||
("(no attributes)", "(nincsenek attribútumok)"),
|
||||
("Enter a style name.", "Adjon meg egy stílusnevet."),
|
||||
];
|
||||
|
||||
pub(super) const CONTEXT: &[(&str, &str, &str)] =
|
||||
|
||||
@@ -41,7 +41,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Text Frame General Options", "Opções gerais do quadro de texto"),
|
||||
("New Object Style", "Novo estilo de objeto"),
|
||||
("Object Style Options", "Opções de estilo de objeto"),
|
||||
("Object Style Options…", "Opções de estilo de objeto…"),
|
||||
("Basic Attributes", "Atributos básicos"),
|
||||
("Stroke & Corner Options", "Opções de traçado e canto"),
|
||||
("Effects for:", "Efeitos para:"),
|
||||
@@ -50,7 +49,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Shape:", "Forma:"),
|
||||
("(no attributes)", "(sem atributos)"),
|
||||
("Enter a style name.", "Digite um nome de estilo."),
|
||||
("Corner Options", "Opções de canto"),
|
||||
("Keep In Frame", "Manter no quadro"),
|
||||
("New Paragraph Style", "Novo estilo de parágrafo"),
|
||||
("New Character Style", "Novo estilo de caractere"),
|
||||
|
||||
@@ -2968,6 +2968,18 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
"Links and Images: {found} linked, {modified} changed, {missing} not found",
|
||||
"Länkar och bilder: {found} länkade, {modified} ändrade, {missing} hittades inte",
|
||||
),
|
||||
("Apply Next Style", "Använd nästa stil"),
|
||||
("Text Frame General Options", "Allmänna alternativ för textram"),
|
||||
("New Object Style", "Ny objektstil"),
|
||||
("Object Style Options", "Alternativ för objektstil"),
|
||||
("Basic Attributes", "Grundläggande attribut"),
|
||||
("Stroke & Corner Options", "Kontur- och hörnalternativ"),
|
||||
("Effects for:", "Effekter för:"),
|
||||
("Create New Style", "Skapa ny stil"),
|
||||
("Clear Overrides", "Rensa lokala ändringar"),
|
||||
("Shape:", "Form:"),
|
||||
("(no attributes)", "(inga attribut)"),
|
||||
("Enter a style name.", "Ange ett stilnamn."),
|
||||
];
|
||||
|
||||
pub(super) const CONTEXT: &[(&str, &str, &str)] = &[
|
||||
|
||||
@@ -153,10 +153,22 @@ fn frame_options(d: &Dialog) -> Result<Option<designcraft_doc::TextFrameOptions>
|
||||
*side = measure(key).ok_or_else(|| bad("an inset is not a measure"))?;
|
||||
}
|
||||
o.vertical_justification = serde_json::from_value(json!(d.s("verticalJustification"))).unwrap_or(o.vertical_justification);
|
||||
if o.vertical_justification == designcraft_doc::VerticalJustification::Justify {
|
||||
o.vj_paragraph_spacing_limit = measure("paragraphSpacingLimit").ok_or_else(|| bad("the paragraph spacing limit is not a measure"))?;
|
||||
}
|
||||
o.ignore_wrap = d.b("ignoreWrap");
|
||||
Ok(Some(o))
|
||||
}
|
||||
|
||||
/// The General options of `o` as the `textFrame` parameter of `style.object.create` and `edit`.
|
||||
fn general_json(o: &designcraft_doc::TextFrameOptions) -> Value {
|
||||
let mut v = serde_json::to_value(o).unwrap_or_default();
|
||||
if let Value::Object(m) = &mut v {
|
||||
m.retain(|k, _| designcraft_doc::TextFrameOptions::GENERAL_KEYS.contains(&k.as_str()));
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
/// OK: the new style (applied to the selection when asked), or the edited one.
|
||||
pub fn confirm(app: &mut DesignApp, d: &Dialog) -> Result<Value, String> {
|
||||
let name = d.s("name").trim().to_string();
|
||||
@@ -164,7 +176,7 @@ pub fn confirm(app: &mut DesignApp, d: &Dialog) -> Result<Value, String> {
|
||||
return Err(crate::i18n::tr(&app.ui.language, "Enter a style name.").to_string());
|
||||
}
|
||||
let mut p = attrs(d);
|
||||
p.insert("textFrame".into(), frame_options(d)?.map_or(Value::Null, |o| serde_json::to_value(o).unwrap_or_default()));
|
||||
p.insert("textFrame".into(), frame_options(d)?.map_or(Value::Null, |o| general_json(&o)));
|
||||
if d.b("new") {
|
||||
p.insert("name".into(), json!(name));
|
||||
p.insert("fromSelection".into(), json!(false));
|
||||
@@ -694,6 +706,31 @@ mod tests {
|
||||
assert_eq!(os.text_frame.map(|t| t.columns), Some(2), "2.5 columns changed nothing");
|
||||
}
|
||||
|
||||
/// Text Frame General Options › Vertical Justification: Justify with a Paragraph Spacing Limit
|
||||
/// sets both on the style and its frame, and the frame keeps its auto-size setting.
|
||||
#[test]
|
||||
fn the_general_options_page_sets_the_paragraph_spacing_limit() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let r = app.run("frame.create", json!({"rect": [36, 36, 336, 336], "content": "text", "caret": false})).unwrap();
|
||||
let id = r["id"].as_u64().unwrap();
|
||||
app.run("object.textFrameOptions", json!({"ids": [id], "autoSize": "heightOnly"})).unwrap();
|
||||
app.run("selection.set", json!({"ids": [id]})).unwrap();
|
||||
open_new(&mut app);
|
||||
if let Some(d) = app.ui.dialog.as_mut() {
|
||||
d.fields.insert("name".into(), json!("Justified"));
|
||||
d.fields.insert("verticalJustification".into(), json!("justify"));
|
||||
d.fields.insert("paragraphSpacingLimit".into(), json!("6 pt"));
|
||||
}
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
let doc = app.session.active().unwrap().doc.clone();
|
||||
let tf = doc.styles.object_style("Justified").and_then(|s| s.text_frame.clone()).unwrap();
|
||||
assert_eq!((tf.vertical_justification, tf.vj_paragraph_spacing_limit), (designcraft_doc::VerticalJustification::Justify, 6.0));
|
||||
app.run("style.object.apply", json!({"name": "Justified", "ids": [id]})).unwrap();
|
||||
let o = app.session.active().unwrap().doc.item(designcraft_doc::ItemId(id)).unwrap().text_frame().unwrap().options.clone();
|
||||
assert_eq!((o.vj_paragraph_spacing_limit, o.auto_size), (6.0, designcraft_doc::AutoSize::HeightOnly));
|
||||
}
|
||||
|
||||
/// `objectStyle.dialog` with a style the document lacks, or `[None]`, is an error and opens
|
||||
/// nothing; without a name it opens New Object Style.
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user