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:
dotCooCoo committed 2026-11-11 20:54:37 -08:00
1 parent 630c2e2bba
commit f47acd5dfd
11 files changed
+285 -30

No files matched your search

+43
View File
@@ -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 {
+3 -2
View File
@@ -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);
}
}
+3 -3
View File
@@ -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
),
+34 -8
View File
@@ -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);
}
},
);
+72
View File
@@ -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;
+30 -14
View File
@@ -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
}
+37
View File
@@ -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();
+13
View File
@@ -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)] =
-2
View File
@@ -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"),
+12
View File
@@ -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)] = &[
+38 -1
View File
@@ -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]