diff --git a/crates/automation/src/forms.rs b/crates/automation/src/forms.rs index c01b438f..acc5839e 100644 --- a/crates/automation/src/forms.rs +++ b/crates/automation/src/forms.rs @@ -347,12 +347,49 @@ impl Automation { format: a.get("format").map(format_arg).transpose()?, validate: a.get("validate").map(validate_arg).transpose()?, calculate: a.get("calculate").map(calculate_arg).transpose()?, + quadding: match a.opt_str("align")? { + None => None, + Some("left") => Some(0), + Some("center" | "centre") => Some(1), + Some("right") => Some(2), + Some(o) => return Err(bad(format!("unknown alignment {o:?} (left, center, right)"))), + }, + default_value: a.opt_str("default")?.map(|d| (!d.is_empty()).then(|| d.to_owned())), + flags: match a.get("flags") { + None => Vec::new(), + Some(v) => { + use printcraft_engine::field_flags as ff; + let obj = v.as_object().ok_or_else(|| bad("flags must be an object of booleans"))?; + let mut out = Vec::new(); + for (k, on) in obj { + let on = on.as_bool().ok_or_else(|| bad(format!("flags.{k} must be true or false")))?; + // Acrobat's wording: "Scroll long text" and "Check spelling" are the + // inverse of their bits. + let (bit, inverted) = match k.as_str() { + "scroll" => (ff::DO_NOT_SCROLL, true), + "rich_text" => (ff::RICH_TEXT, false), + "password" => (ff::PASSWORD, false), + "file_select" => (ff::FILE_SELECT, false), + "spell_check" => (ff::DO_NOT_SPELL_CHECK, true), + "comb" => (ff::COMB, false), + "sort" => (ff::SORT, false), + "editable" => (ff::EDIT, false), + "multi_select" => (ff::MULTI_SELECT, false), + "commit_immediately" => (ff::COMMIT_ON_SEL_CHANGE, false), + "radios_in_unison" => (ff::RADIOS_IN_UNISON, false), + other => return Err(bad(format!("unknown flag {other:?}"))), + }; + out.push((bit, on != inverted)); + } + out + } + }, }; if props == FieldProps::default() { return Err(ToolError::InvalidArgs("nothing to change".into())); } let new_name = props.name.clone().unwrap_or(name.clone()); - let mut out = self.apply(a, Edit::SetFieldProps { name, props })?; + let mut out = self.apply(a, Edit::SetFieldProps { name, props: Box::new(props) })?; out["field"] = json!(new_name); Ok(out) } diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index 6d741f62..b449d3ff 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -246,7 +246,7 @@ pub fn tools() -> Vec { t( "form_set_props", "Field properties", - "Change a field's properties (General and Options tabs): name (renames it), tooltip, read_only, required, multiline, max_length (0 = no limit), options (combo/list items), font_size (0 = auto), rect (position), format, validate, calculate (Acrobat's Format/Validate/Calculate tabs, run natively: values are checked, formatted and recalculated as in Acrobat). Only the given ones change. Undoable.", + "Change a field's properties (General and Options tabs): name (renames it), tooltip, read_only, required, multiline, max_length (0 = no limit), options (combo/list items), font_size (0 = auto), rect (position), format, validate, calculate (Acrobat's Format/Validate/Calculate tabs, run natively: values are checked, formatted and recalculated as in Acrobat). Options tab: align (left, center, right), default (the value Reset form restores; for check boxes and radio buttons their on state, \"\" for off), flags { scroll, rich_text, password, file_select, spell_check, comb (needs max_length), sort, editable, multi_select, commit_immediately }. Only the given ones change. Undoable.", ) .with(schema( json!({ @@ -259,6 +259,9 @@ pub fn tools() -> Vec { "multiline": { "type": "boolean" }, "max_length": { "type": "integer", "minimum": 0 }, "options": { "type": "array", "items": { "type": "string" } }, + "align": { "type": "string", "enum": ["left", "center", "right"] }, + "default": { "type": "string" }, + "flags": { "type": "object", "additionalProperties": { "type": "boolean" } }, "font_size": { "type": "number", "minimum": 0 }, "rect": { "type": "array", "items": { "type": "number" }, "minItems": 4, "maxItems": 4, "description": "Move/resize: points from the top-left of the displayed page." }, "appearance": { "type": "object", "description": "Appearance tab: border, fill, text_color (#RRGGBB, a name or \"none\"), width (1 thin, 2 medium, 3 thick), style (solid, dashed, beveled, inset, underline), font (helvetica, times, courier)." }, diff --git a/crates/automation/tests/automation.rs b/crates/automation/tests/automation.rs index a021e07c..9f2435ef 100644 --- a/crates/automation/tests/automation.rs +++ b/crates/automation/tests/automation.rs @@ -616,6 +616,17 @@ fn preparing_a_form_through_tools() { assert_eq!(f[1]["options"], json!(["S", "L"])); ok(&mut a, "form_fill", json!({ "doc": doc, "values": { "full name": "Ada", "size": "L" } })); assert!(page_text(&mut a, doc)[0].contains("Ada")); + // Options tab: alignment, default, flags (comb needs a limit). + assert!(matches!(a.call("form_set_props", &json!({ "doc": doc, "field": "full name", "flags": { "comb": true } })), Err(ToolError::Failed(_)))); + ok( + &mut a, + "form_set_props", + json!({ "doc": doc, "field": "full name", "align": "center", "default": "Anon", "max_length": 8, "flags": { "comb": true, "spell_check": false } }), + ); + assert!(matches!( + a.call("form_set_props", &json!({ "doc": doc, "field": "full name", "flags": { "sparkles": true } })), + Err(ToolError::InvalidArgs(_)) + )); assert!(matches!( a.call("form_add_field", &json!({ "doc": doc, "page": 1, "type": "slider", "rect": [0, 0, 9, 9] })), Err(ToolError::InvalidArgs(_)) diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index f25b9175..c5d54924 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -579,7 +579,7 @@ pub enum Edit { /// Field Properties ▸ General / Options. SetFieldProps { name: String, - props: FieldProps, + props: Box, }, /// Delete a field and all its widgets. DeleteField { diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index a1a09dfd..5777bc6e 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -677,7 +677,7 @@ fn preparing_a_form_adds_renames_and_deletes_fields() { assert_eq!(names(&s), ["Text1", "Check Box1", "Text2"]); assert_eq!(s.get(id).unwrap().can_undo(), Some("Add field")); let props = FieldProps { name: Some("email".into()), tooltip: Some("Your e-mail".into()), required: Some(true), ..Default::default() }; - s.apply(id, Edit::SetFieldProps { name: "Text2".into(), props }).unwrap(); + s.apply(id, Edit::SetFieldProps { name: "Text2".into(), props: Box::new(props) }).unwrap(); s.apply(id, Edit::DeleteField { name: "Check Box1".into() }).unwrap(); assert_eq!(names(&s), ["Text1", "email"]); s.apply(id, Edit::SetFieldValue { name: "email".into(), value: FieldValue::Text("ada@example.org".into()) }).unwrap(); diff --git a/crates/forms/src/author.rs b/crates/forms/src/author.rs index 3b680a54..bff3f1c4 100644 --- a/crates/forms/src/author.rs +++ b/crates/forms/src/author.rs @@ -76,6 +76,12 @@ pub struct FieldProps { pub format: Option, pub validate: Option, pub calculate: Option, + /// Options-tab flags (`/Ff` bits from [`crate::flags`]) to set or clear. + pub flags: Vec<(u32, bool)>, + /// Alignment (`/Q`): 0 left, 1 centre, 2 right. + pub quadding: Option, + /// Default value (`/DV`): what Reset form restores. + pub default_value: Option>, } fn invalid(m: impl Into) -> Result { @@ -580,6 +586,26 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result m, + None => f.max_len, + }; + if limit.is_none_or(|m| m == 0) { + return invalid("a comb of characters needs a character limit"); + } + ff &= !(flags::MULTILINE | flags::PASSWORD | flags::FILE_SELECT); + } + if let Some(q) = props.quadding + && !(0..=2).contains(&q) + { + return invalid("alignment is 0 (left), 1 (centre) or 2 (right)"); + } if props.options.as_ref().is_some_and(|o| o.is_empty()) && matches!(f.kind, FieldKind::Combo | FieldKind::List) { return invalid("a list needs at least one option"); } @@ -603,6 +629,37 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result = match &props.options { + Some(o) => o.iter().map(|x| (x.clone(), x.clone())).collect(), + None => f.options.clone(), + }; + items.sort_by_key(|(_, display)| display.to_lowercase()); + let opt = items + .iter() + .map(|(e, disp)| { + if e == disp { + Object::String(PdfString::text(disp)) + } else { + Object::Array(vec![Object::String(PdfString::text(e)), Object::String(PdfString::text(disp))]) + } + }) + .collect(); + d.set(b"Opt".to_vec(), Object::Array(opt)); + } + if let Some(q) = props.quadding { + d.set(b"Q".to_vec(), Object::Int(q)); + } + match &props.default_value { + // Buttons name their default state; text and choices hold a string. + Some(Some(v)) if matches!(f.kind, FieldKind::CheckBox | FieldKind::Radio) => d.set(b"DV".to_vec(), Object::name(v)), + Some(Some(v)) => d.set(b"DV".to_vec(), PdfString::text(v)), + Some(None) => { + d.remove(b"DV"); + } + None => {} + } if props.font_size.is_some() || props.look.is_some() { d.set(b"DA".to_vec(), PdfString::literal(new_da.clone().into_bytes())); } diff --git a/crates/forms/src/lib.rs b/crates/forms/src/lib.rs index 19e56792..0123fcac 100644 --- a/crates/forms/src/lib.rs +++ b/crates/forms/src/lib.rs @@ -57,10 +57,16 @@ pub mod flags { pub const PUSH_BUTTON: u32 = 1 << 16; pub const COMBO: u32 = 1 << 17; pub const EDIT: u32 = 1 << 18; + pub const SORT: u32 = 1 << 19; + pub const FILE_SELECT: u32 = 1 << 20; pub const MULTI_SELECT: u32 = 1 << 21; + pub const DO_NOT_SPELL_CHECK: u32 = 1 << 22; pub const DO_NOT_SCROLL: u32 = 1 << 23; pub const COMB: u32 = 1 << 24; + /// Buttons: radios in unison. Text fields use the same bit for rich text. pub const RADIOS_IN_UNISON: u32 = 1 << 25; + pub const RICH_TEXT: u32 = 1 << 25; + pub const COMMIT_ON_SEL_CHANGE: u32 = 1 << 26; } /// One widget (the field's appearance on a page). diff --git a/crates/forms/src/tests.rs b/crates/forms/src/tests.rs index 7eed9279..bf5f294c 100644 --- a/crates/forms/src/tests.rs +++ b/crates/forms/src/tests.rs @@ -474,3 +474,51 @@ fn tab_order_follows_the_page_setting() { assert_eq!(order(&doc), "ACBD", "saved as /Tabs"); assert!(set_tab_order(&mut doc.clone(), &[3], TabOrder::Row).is_err()); } + +#[test] +fn options_tab_flags_alignment_and_defaults() { + let mut doc = one_page(); + let text = add_field(&mut doc, 0, [50.0, 700.0, 250.0, 720.0], &NewField::Text { multiline: false }, None).unwrap(); + let combo = add_field( + &mut doc, + 0, + [50.0, 600.0, 250.0, 620.0], + &NewField::Combo { options: vec!["Pear".into(), "apple".into(), "Fig".into()], editable: false }, + None, + ) + .unwrap(); + let check = add_field(&mut doc, 0, [50.0, 500.0, 64.0, 514.0], &NewField::CheckBox, None).unwrap(); + // A comb needs a limit. + let comb = FieldProps { flags: vec![(flags::COMB, true)], ..FieldProps::default() }; + assert!(matches!(set_props(&mut doc, &text, &comb), Err(FormError::Invalid(_)))); + set_props( + &mut doc, + &text, + &FieldProps { + flags: vec![(flags::COMB, true), (flags::DO_NOT_SPELL_CHECK, true)], + max_len: Some(Some(6)), + quadding: Some(1), + default_value: Some(Some("ABC123".into())), + ..FieldProps::default() + }, + ) + .unwrap(); + let f = fields(&doc).into_iter().find(|f| f.name == text).unwrap(); + assert!(f.has(flags::COMB) && f.has(flags::DO_NOT_SPELL_CHECK) && !f.has(flags::MULTILINE)); + assert_eq!((f.max_len, f.quadding, f.default.clone()), (Some(6), 1, vec!["ABC123".to_string()])); + // Reset form restores the default. + set_value(&mut doc, &text, &FieldValue::Text("XYZ".into())).unwrap(); + reset(&mut doc, None).unwrap(); + assert_eq!(fields(&doc).into_iter().find(|f| f.name == text).unwrap().value, vec!["ABC123".to_string()]); + // Sort items orders the list; custom text allowed. + set_props(&mut doc, &combo, &FieldProps { flags: vec![(flags::SORT, true), (flags::EDIT, true)], ..FieldProps::default() }).unwrap(); + let c = fields(&doc).into_iter().find(|f| f.name == combo).unwrap(); + assert_eq!(c.options.iter().map(|(_, d)| d.as_str()).collect::>(), vec!["apple", "Fig", "Pear"]); + assert!(c.has(flags::EDIT)); + // Checked by default: the on state is the default. + let on = fields(&doc).into_iter().find(|f| f.name == check).unwrap().widgets[0].on_state.clone().unwrap(); + set_props(&mut doc, &check, &FieldProps { default_value: Some(Some(on.clone())), ..FieldProps::default() }).unwrap(); + reset(&mut doc, None).unwrap(); + assert_eq!(fields(&doc).into_iter().find(|f| f.name == check).unwrap().value, vec![on]); + assert!(set_props(&mut doc, &text, &FieldProps { quadding: Some(5), ..FieldProps::default() }).is_err()); +} diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 52e7ee8f..51a3ee48 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -858,7 +858,7 @@ pub fn show(app: &mut PrintCraftApp, ctx: &egui::Context) { if field_props_now && let Some(d) = app.field_props.take() && let Some(props) = d.props() - && app.apply_edit(Edit::SetFieldProps { name: d.field.clone(), props }) + && app.apply_edit(Edit::SetFieldProps { name: d.field.clone(), props: Box::new(props) }) && let Some(i) = app.active { // Keep the (possibly renamed) field selected. diff --git a/crates/ui-egui/src/prepare.rs b/crates/ui-egui/src/prepare.rs index f5c8fa11..6c2533be 100644 --- a/crates/ui-egui/src/prepare.rs +++ b/crates/ui-egui/src/prepare.rs @@ -227,7 +227,8 @@ pub(crate) fn page_input( if delta.length() >= 1.0 { let r = dragged(screen_rect(xf, info, page, w.rect), grab, delta); let rect = user_rect(xf, info, page, r); - view.pending_edit = Some(Edit::SetFieldProps { name, props: FieldProps { rect: Some((wi, rect)), ..Default::default() } }); + view.pending_edit = + Some(Edit::SetFieldProps { name, props: Box::new(FieldProps { rect: Some((wi, rect)), ..Default::default() }) }); } } } @@ -441,6 +442,14 @@ pub struct FieldDraft { pub max_len: usize, pub options: Vec, pub new_option: String, + /// `/Ff` as edited on the Options tab. + pub flags: u32, + /// `/Q`: 0 left, 1 centre, 2 right. + pub quadding: i64, + /// `/DV` (text and choices), or the default state (buttons: empty = off). + pub default: String, + /// Check boxes and radio buttons: this widget's "on" state (export value). + pub on_state: String, /// 0 = auto. pub font_size: f64, /// Left, bottom, width, height in points. @@ -476,6 +485,10 @@ impl FieldDraft { max_len: f.max_len.unwrap_or(0), options: f.options.iter().map(|(_, d)| d.clone()).collect(), new_option: String::new(), + flags: f.flags, + quadding: f.quadding, + default: f.default.first().cloned().unwrap_or_default(), + on_state: f.widgets.get(widget).and_then(|w| w.on_state.clone()).unwrap_or_default(), font_size: da_size(&f.da), position: [r[0], r[1], r[2] - r[0], r[3] - r[1]], look: None, @@ -503,7 +516,7 @@ impl FieldDraft { fn tabs(&self) -> Vec<(FieldTab, &'static str)> { let mut t = vec![(FieldTab::General, "General"), (FieldTab::Appearance, "Appearance"), (FieldTab::Position, "Position")]; - if matches!(self.kind, FormFieldKind::Text | FormFieldKind::Combo | FormFieldKind::List) { + if !matches!(self.kind, FormFieldKind::PushButton | FormFieldKind::Signature) { t.push((FieldTab::Options, "Options")); } if matches!(self.kind, FormFieldKind::Text | FormFieldKind::Combo) { @@ -533,11 +546,43 @@ impl FieldDraft { format: (self.format != o.format).then(|| self.format.clone()), validate: (self.validate != o.validate).then(|| self.validate.clone()), calculate: (self.calculate != o.calculate).then(|| self.calculate.clone()), + flags: OPTION_FLAGS.iter().filter(|b| (self.flags ^ o.flags) & **b != 0).map(|b| (*b, self.flags & b != 0)).collect(), + quadding: (self.quadding != o.quadding).then_some(self.quadding), + default_value: (self.default != o.default).then(|| (!self.default.is_empty()).then(|| self.default.clone())), }; (p != FieldProps::default()).then_some(p) } } +/// The `/Ff` bits the Options tab edits. +const OPTION_FLAGS: [u32; 10] = { + use printcraft_engine::field_flags as ff; + [ + ff::DO_NOT_SCROLL, + ff::RICH_TEXT, + ff::PASSWORD, + ff::FILE_SELECT, + ff::DO_NOT_SPELL_CHECK, + ff::COMB, + ff::SORT, + ff::EDIT, + ff::MULTI_SELECT, + ff::COMMIT_ON_SEL_CHANGE, + ] +}; + +/// A check box for a `/Ff` bit (`inverted`: checked when the bit is clear). +fn flag_box(ui: &mut egui::Ui, flags: &mut u32, bit: u32, inverted: bool, label: &str) { + let mut on = (*flags & bit != 0) != inverted; + if ui.checkbox(&mut on, label).changed() { + if on != inverted { + *flags |= bit; + } else { + *flags &= !bit; + } + } +} + /// Draw Field Properties; returns (apply, cancel). pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens) -> (bool, bool) { use crate::widgets; @@ -656,12 +701,60 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Toke FieldTab::Calculate => calculate_tab(ui, d, t), FieldTab::Options => match d.kind { FormFieldKind::Text => { + use printcraft_engine::field_flags as ff; + egui::Grid::new("text-options").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| { + ui.label("Alignment:"); + egui::ComboBox::from_id_salt("quadding").selected_text(["Left", "Center", "Right"][d.quadding.clamp(0, 2) as usize]).show_ui( + ui, + |ui| { + for (q, l) in [(0, "Left"), (1, "Center"), (2, "Right")] { + ui.selectable_value(&mut d.quadding, q, l); + } + }, + ); + ui.end_row(); + let l = ui.label("Default Value:"); + ui.add(egui::TextEdit::singleline(&mut d.default).desired_width(260.0)).labelled_by(l.id); + ui.end_row(); + }); + ui.add_space(6.0); ui.checkbox(&mut d.multiline, "Multi-line"); + flag_box(ui, &mut d.flags, ff::DO_NOT_SCROLL, true, "Scroll long text"); + flag_box(ui, &mut d.flags, ff::RICH_TEXT, false, "Allow Rich Text Formatting"); ui.horizontal(|ui| { ui.checkbox(&mut d.limit, "Limit of"); ui.add_enabled(d.limit, egui::DragValue::new(&mut d.max_len).range(0..=10_000)); ui.label("characters"); }); + flag_box(ui, &mut d.flags, ff::PASSWORD, false, "Password"); + flag_box(ui, &mut d.flags, ff::FILE_SELECT, false, "Field is used for file selection"); + flag_box(ui, &mut d.flags, ff::DO_NOT_SPELL_CHECK, true, "Check spelling"); + ui.horizontal(|ui| { + flag_box(ui, &mut d.flags, ff::COMB, false, "Comb of"); + if d.flags & ff::COMB != 0 { + d.limit = true; + d.multiline = false; + d.flags &= !(ff::PASSWORD | ff::FILE_SELECT); + if d.max_len == 0 { + d.max_len = 10; + } + } + ui.add_enabled(d.flags & ff::COMB != 0, egui::DragValue::new(&mut d.max_len).range(1..=500)); + ui.label("characters"); + }); + } + FormFieldKind::CheckBox | FormFieldKind::Radio => { + use printcraft_engine::field_flags as ff; + let on = d.on_state.clone(); + ui.label(format!("Export Value: {on}")); + let mut checked = !d.default.is_empty() && d.default == on; + let label = if d.kind == FormFieldKind::CheckBox { "Check box is checked by default" } else { "Button is checked by default" }; + if ui.checkbox(&mut checked, label).changed() { + d.default = if checked { on } else { String::new() }; + } + if d.kind == FormFieldKind::Radio { + flag_box(ui, &mut d.flags, ff::RADIOS_IN_UNISON, false, "Buttons with the same name and value are selected in unison"); + } } _ => { ui.horizontal(|ui| { @@ -701,6 +794,26 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Toke if d.options.is_empty() { ui.label(egui::RichText::new("Add the choices people pick from.").color(t.text_muted)); } + use printcraft_engine::field_flags as ff; + ui.add_space(6.0); + ui.horizontal(|ui| { + ui.label("Default:"); + let shown = if d.default.is_empty() { "(none)".to_string() } else { d.default.clone() }; + egui::ComboBox::from_id_salt("choice-default").selected_text(shown).show_ui(ui, |ui| { + ui.selectable_value(&mut d.default, String::new(), "(none)"); + for o in d.options.clone() { + ui.selectable_value(&mut d.default, o.clone(), o); + } + }); + }); + flag_box(ui, &mut d.flags, ff::SORT, false, "Sort items"); + if d.kind == FormFieldKind::Combo { + flag_box(ui, &mut d.flags, ff::EDIT, false, "Allow user to enter custom text"); + flag_box(ui, &mut d.flags, ff::DO_NOT_SPELL_CHECK, true, "Check spelling"); + } else { + flag_box(ui, &mut d.flags, ff::MULTI_SELECT, false, "Multiple selection"); + } + flag_box(ui, &mut d.flags, ff::COMMIT_ON_SEL_CHANGE, false, "Commit selected value immediately"); } }, } diff --git a/crates/ui-egui/tests/prepare.rs b/crates/ui-egui/tests/prepare.rs index 87761c09..328856d8 100644 --- a/crates/ui-egui/tests/prepare.rs +++ b/crates/ui-egui/tests/prepare.rs @@ -169,3 +169,28 @@ fn appearance_tab_restyles_the_field() { let l = s.session.get(s.views[0].id).unwrap().field_look("city").unwrap(); assert_eq!((l.style, l.font, l.fill), (BorderStyle::Dashed, FieldFont::Courier, Some([1.0, 1.0, 0.8]))); } + +#[test] +fn options_tab_sets_flags_alignment_and_defaults() { + use printcraft_engine::field_flags as ff; + let mut h = harness(); + h.state_mut().open_field_props("city", 0); + h.run_steps(2); + h.get_by_label("Options").click(); + h.run_steps(2); + h.get_by_label("Check spelling").click(); + h.get_by_label("Password").click(); + h.run_steps(1); + { + let d = h.state_mut().field_props.as_mut().unwrap(); + d.quadding = 2; + d.default = "Paris".into(); + } + h.get_by_label("OK").click(); + h.run_steps(3); + let s = h.state(); + let f = s.session.get(s.views[0].id).unwrap().form.iter().find(|f| f.name == "city").cloned().unwrap(); + assert!(f.has(ff::PASSWORD) && f.has(ff::DO_NOT_SPELL_CHECK), "{:b}", f.flags); + assert_eq!((f.quadding, f.default.clone()), (2, vec!["Paris".to_string()])); + assert_eq!(s.session.get(s.views[0].id).unwrap().can_undo(), Some("Change field properties")); +} diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index e1033a27..52d404a7 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -4791,11 +4791,11 @@ title = "Field properties: options per field type" area = "F" tier = "P0" milestone = "M6" -status = "partial" +status = "shipped" commands = ["form.field.properties"] tools = ["form_set_props"] -evidence = ["crates/forms/src/tests.rs::properties_rename_and_delete", "crates/automation/tests/automation.rs::preparing_a_form_through_tools"] -notes = "text: multi-line, character limit; choices: item list add/delete/reorder" +evidence = ["crates/forms/src/tests.rs::properties_rename_and_delete", "crates/automation/tests/automation.rs::preparing_a_form_through_tools", "crates/forms/src/tests.rs::options_tab_flags_alignment_and_defaults", "crates/ui-egui/tests/prepare.rs::options_tab_sets_flags_alignment_and_defaults"] +notes = "text: alignment, default, multi-line, scroll, rich text flag, limit, password, file selection, spell check, comb; choices: items, default, sort, custom text, multiple selection, commit immediately; check boxes and radios: checked by default, radios in unison" [[feature]] id = "form.props-actions"