Forms: check box and radio button styles (#94)
Field Properties > Options > Check Box Style (Button Style for radio buttons): Check, Circle, Cross, Diamond, Square or Star, as Acrobat offers. The choice is stored in each widget's /MK /CA with Acrobat's ZapfDingbats codes (4 l 8 u n H), so other viewers draw the same mark, and PrintCraft draws it as paths, needing no symbol font. Styles set by other apps are read back too; the defaults stay a check for check boxes and a circle for radio buttons. - forms: CheckStyle, check_style(), FieldProps::check_style; set_props writes /MK /CA (resolving an indirect /MK so its colours survive) and redraws; text and choice fields refuse a style. - engine: Document::field_check_style. - automation: form_set_props takes check_style, form_fields reports it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
55c581760a
commit
cdb3f3d106
|
|
@ -201,6 +201,9 @@ impl Automation {
|
|||
if let Some(t) = &f.tooltip {
|
||||
o.insert("tooltip".into(), json!(t));
|
||||
}
|
||||
if let Some(s) = doc.field_check_style(&f.name) {
|
||||
o.insert("check_style".into(), json!(s.label().to_lowercase()));
|
||||
}
|
||||
if f.actions.format != Format::None {
|
||||
o.insert("format".into(), format_json(&f.actions.format));
|
||||
o.insert("display".into(), json!(f.value.first().map(|v| printcraft_engine::form_scripts::format_value(&f.actions.format, v))));
|
||||
|
|
@ -391,6 +394,15 @@ impl Automation {
|
|||
}
|
||||
},
|
||||
actions: None,
|
||||
check_style: match a.opt_str("check_style")? {
|
||||
None => None,
|
||||
Some(s) => Some(
|
||||
printcraft_engine::CheckStyle::ALL
|
||||
.into_iter()
|
||||
.find(|c| c.label().eq_ignore_ascii_case(s))
|
||||
.ok_or_else(|| bad(format!("unknown check style {s:?} (check, circle, cross, diamond, square, star)")))?,
|
||||
),
|
||||
},
|
||||
};
|
||||
if props == FieldProps::default() {
|
||||
return Err(ToolError::InvalidArgs("nothing to change".into()));
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ pub fn tools() -> Vec<ToolDef> {
|
|||
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). 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.",
|
||||
"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 }, check_style (check boxes and radio buttons: check, circle, cross, diamond, square, star). Only the given ones change. Undoable.",
|
||||
)
|
||||
.with(schema(
|
||||
json!({
|
||||
|
|
@ -290,6 +290,7 @@ pub fn tools() -> Vec<ToolDef> {
|
|||
"align": { "type": "string", "enum": ["left", "center", "right"] },
|
||||
"locked": { "type": "boolean", "description": "Lock the field's properties (only unlocking is accepted while locked)." },
|
||||
"default": { "type": "string" },
|
||||
"check_style": { "type": "string", "enum": ["check", "circle", "cross", "diamond", "square", "star"], "description": "Check boxes and radio buttons: the mark shown when on." },
|
||||
"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." },
|
||||
|
|
|
|||
|
|
@ -823,6 +823,12 @@ fn preparing_a_form_through_tools() {
|
|||
assert_eq!((f[0]["required"].as_bool(), f[0]["tooltip"].as_str()), (Some(true), Some("Your name")));
|
||||
assert_eq!(f[0]["rect"], json!([30.0, 20.0, 190.0, 42.0]), "moved; the rect round-trips in view coordinates");
|
||||
assert_eq!(f[1]["options"], json!(["S", "L"]));
|
||||
// #94: the mark a check box or radio button shows.
|
||||
assert_eq!(f[1]["check_style"], "circle", "radio buttons default to a circle");
|
||||
ok(&mut a, "form_set_props", json!({ "doc": doc, "field": "size", "check_style": "star" }));
|
||||
assert_eq!(ok(&mut a, "form_fields", json!({ "doc": doc }))["fields"][1]["check_style"], "star");
|
||||
assert!(matches!(a.call("form_set_props", &json!({ "doc": doc, "field": "size", "check_style": "heart" })), Err(ToolError::InvalidArgs(_))));
|
||||
assert!(matches!(a.call("form_set_props", &json!({ "doc": doc, "field": "full name", "check_style": "star" })), Err(ToolError::Failed(_))));
|
||||
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).
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ pub use printcraft_edit::{
|
|||
Added, AddedImage, AddedText, Align as TextAlign, Background, Content as AddedContent, Family as FontFamily, HeaderFooter, MarkKind, Watermark,
|
||||
};
|
||||
pub use printcraft_forms::{
|
||||
BorderStyle, Field as FormField, FieldAction, FieldFont, FieldKind as FormFieldKind, FieldProps, FieldValue, Look as FieldLook, NewField,
|
||||
TabOrder, Trigger as FieldTrigger, Widget as FormWidget, af as form_scripts, flags as field_flags,
|
||||
BorderStyle, CheckStyle, Field as FormField, FieldAction, FieldFont, FieldKind as FormFieldKind, FieldProps, FieldValue, Look as FieldLook,
|
||||
NewField, TabOrder, Trigger as FieldTrigger, Widget as FormWidget, af as form_scripts, flags as field_flags,
|
||||
};
|
||||
|
||||
pub use printcraft_a11y as a11y;
|
||||
|
|
@ -386,6 +386,13 @@ impl Document {
|
|||
Some(printcraft_forms::look(&e.cos, f))
|
||||
}
|
||||
|
||||
/// A check box's or radio button's mark (Field Properties ▸ Options), `None` for other fields.
|
||||
pub fn field_check_style(&self, name: &str) -> Option<CheckStyle> {
|
||||
let e = self.editor.as_ref()?;
|
||||
let f = self.form.iter().find(|f| f.name == name)?;
|
||||
matches!(f.kind, FormFieldKind::CheckBox | FormFieldKind::Radio).then(|| printcraft_forms::check_style(&e.cos, f))
|
||||
}
|
||||
|
||||
/// Remove Hidden Information: what each category would remove.
|
||||
pub fn hidden_info(&self) -> Vec<(Hidden, usize)> {
|
||||
self.editor.as_ref().map(|e| printcraft_redact::sanitize::scan(&e.cos)).unwrap_or_default()
|
||||
|
|
|
|||
|
|
@ -320,6 +320,7 @@ pub fn check_box_states(doc: &mut Document, w: &Widget, kind: FieldKind, on_name
|
|||
let wd = wobj.as_dict().cloned().unwrap_or_default();
|
||||
let (width, height) = ((w.rect[2] - w.rect[0]).max(1.0), (w.rect[3] - w.rect[1]).max(1.0));
|
||||
let (frame_c, _) = frame(doc, &wd, width, height);
|
||||
let style = crate::author::CheckStyle::of_widget(doc, &wd, kind);
|
||||
let mut form = |content: String| -> Object {
|
||||
let mut d = Dict::new();
|
||||
d.set(b"Type".to_vec(), Object::name("XObject"));
|
||||
|
|
@ -328,51 +329,7 @@ pub fn check_box_states(doc: &mut Document, w: &Widget, kind: FieldKind, on_name
|
|||
Object::Ref(doc.add(Object::Stream(Stream::flate(d, content.as_bytes()))))
|
||||
};
|
||||
let s = width.min(height);
|
||||
let (cx, cy) = (width / 2.0, height / 2.0);
|
||||
let mark = if kind == FieldKind::Radio {
|
||||
let r = s * 0.25;
|
||||
let k = 0.5523 * r;
|
||||
format!(
|
||||
"0 g\n{} {} m {} {} {} {} {} {} c {} {} {} {} {} {} c {} {} {} {} {} {} c {} {} {} {} {} {} c f\n",
|
||||
n(cx + r),
|
||||
n(cy),
|
||||
n(cx + r),
|
||||
n(cy + k),
|
||||
n(cx + k),
|
||||
n(cy + r),
|
||||
n(cx),
|
||||
n(cy + r),
|
||||
n(cx - k),
|
||||
n(cy + r),
|
||||
n(cx - r),
|
||||
n(cy + k),
|
||||
n(cx - r),
|
||||
n(cy),
|
||||
n(cx - r),
|
||||
n(cy - k),
|
||||
n(cx - k),
|
||||
n(cy - r),
|
||||
n(cx),
|
||||
n(cy - r),
|
||||
n(cx + k),
|
||||
n(cy - r),
|
||||
n(cx + r),
|
||||
n(cy - k),
|
||||
n(cx + r),
|
||||
n(cy)
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"0 G\n{} w 1 J 1 j\n{} {} m {} {} l {} {} l S\n",
|
||||
n((s * 0.1).max(1.0)),
|
||||
n(cx - s * 0.28),
|
||||
n(cy),
|
||||
n(cx - s * 0.08),
|
||||
n(cy - s * 0.22),
|
||||
n(cx + s * 0.3),
|
||||
n(cy + s * 0.25)
|
||||
)
|
||||
};
|
||||
let mark = check_mark(style, width / 2.0, height / 2.0, s);
|
||||
let mut nd = Dict::new();
|
||||
nd.set(on_name.as_bytes().to_vec(), form(format!("{frame_c}{mark}")));
|
||||
nd.set(b"Off".to_vec(), form(frame_c));
|
||||
|
|
@ -381,6 +338,100 @@ pub fn check_box_states(doc: &mut Document, w: &Widget, kind: FieldKind, on_name
|
|||
ap
|
||||
}
|
||||
|
||||
/// The on-state mark for `style` (#94), centred on (`cx`, `cy`) in a box whose smaller side is
|
||||
/// `s`, drawn in black.
|
||||
fn check_mark(style: crate::author::CheckStyle, cx: f64, cy: f64, s: f64) -> String {
|
||||
use crate::author::CheckStyle;
|
||||
// A filled polygon through `pts`.
|
||||
let polygon = |pts: &[(f64, f64)]| {
|
||||
let mut c = String::from("0 g\n");
|
||||
for (i, (x, y)) in pts.iter().enumerate() {
|
||||
c.push_str(&format!("{} {} {} ", n(*x), n(*y), if i == 0 { "m" } else { "l" }));
|
||||
}
|
||||
c.push_str("h f\n");
|
||||
c
|
||||
};
|
||||
let line = n((s * 0.1).max(1.0));
|
||||
match style {
|
||||
CheckStyle::Check => format!(
|
||||
"0 G\n{line} w 1 J 1 j\n{} {} m {} {} l {} {} l S\n",
|
||||
n(cx - s * 0.28),
|
||||
n(cy),
|
||||
n(cx - s * 0.08),
|
||||
n(cy - s * 0.22),
|
||||
n(cx + s * 0.3),
|
||||
n(cy + s * 0.25)
|
||||
),
|
||||
CheckStyle::Cross => {
|
||||
let d = s * 0.25;
|
||||
format!(
|
||||
"0 G\n{line} w 1 J\n{} {} m {} {} l S\n{} {} m {} {} l S\n",
|
||||
n(cx - d),
|
||||
n(cy - d),
|
||||
n(cx + d),
|
||||
n(cy + d),
|
||||
n(cx - d),
|
||||
n(cy + d),
|
||||
n(cx + d),
|
||||
n(cy - d)
|
||||
)
|
||||
}
|
||||
CheckStyle::Square => {
|
||||
let d = s * 0.22;
|
||||
format!("0 g\n{} {} {} {} re f\n", n(cx - d), n(cy - d), n(2.0 * d), n(2.0 * d))
|
||||
}
|
||||
CheckStyle::Diamond => {
|
||||
let d = s * 0.3;
|
||||
polygon(&[(cx, cy + d), (cx + d, cy), (cx, cy - d), (cx - d, cy)])
|
||||
}
|
||||
CheckStyle::Star => {
|
||||
// Five points: outer and inner radii alternate, the first point straight up.
|
||||
let (outer, inner) = (s * 0.32, s * 0.32 * 0.382);
|
||||
let pts: Vec<(f64, f64)> = (0..10)
|
||||
.map(|i| {
|
||||
let a = std::f64::consts::FRAC_PI_2 + i as f64 * std::f64::consts::PI / 5.0;
|
||||
let r = if i % 2 == 0 { outer } else { inner };
|
||||
(cx + r * a.cos(), cy + r * a.sin())
|
||||
})
|
||||
.collect();
|
||||
polygon(&pts)
|
||||
}
|
||||
CheckStyle::Circle => {
|
||||
let r = s * 0.25;
|
||||
let k = 0.5523 * r;
|
||||
format!(
|
||||
"0 g\n{} {} m {} {} {} {} {} {} c {} {} {} {} {} {} c {} {} {} {} {} {} c {} {} {} {} {} {} c f\n",
|
||||
n(cx + r),
|
||||
n(cy),
|
||||
n(cx + r),
|
||||
n(cy + k),
|
||||
n(cx + k),
|
||||
n(cy + r),
|
||||
n(cx),
|
||||
n(cy + r),
|
||||
n(cx - k),
|
||||
n(cy + r),
|
||||
n(cx - r),
|
||||
n(cy + k),
|
||||
n(cx - r),
|
||||
n(cy),
|
||||
n(cx - r),
|
||||
n(cy - k),
|
||||
n(cx - k),
|
||||
n(cy - r),
|
||||
n(cx),
|
||||
n(cy - r),
|
||||
n(cx + k),
|
||||
n(cy - r),
|
||||
n(cx + r),
|
||||
n(cy - k),
|
||||
n(cx + r),
|
||||
n(cy)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -89,6 +89,8 @@ pub struct FieldProps {
|
|||
pub locked: Option<bool>,
|
||||
/// Actions tab: every trigger's action (replacing the field's).
|
||||
pub actions: Option<Vec<(crate::Trigger, crate::FieldAction)>>,
|
||||
/// Options tab: the mark of a check box or radio button (every widget).
|
||||
pub check_style: Option<CheckStyle>,
|
||||
}
|
||||
|
||||
fn invalid<T>(m: impl Into<String>) -> Result<T, FormError> {
|
||||
|
|
@ -215,6 +217,63 @@ impl FieldFont {
|
|||
}
|
||||
}
|
||||
|
||||
/// The mark a check box or radio button shows when on (Field Properties ▸ Options ▸ style).
|
||||
/// Stored as `/MK /CA`, the ZapfDingbats character Acrobat uses for it; PrintCraft draws the
|
||||
/// mark as paths, so no symbol font is needed.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum CheckStyle {
|
||||
Check,
|
||||
Circle,
|
||||
Cross,
|
||||
Diamond,
|
||||
Square,
|
||||
Star,
|
||||
}
|
||||
|
||||
impl CheckStyle {
|
||||
pub const ALL: [CheckStyle; 6] =
|
||||
[CheckStyle::Check, CheckStyle::Circle, CheckStyle::Cross, CheckStyle::Diamond, CheckStyle::Square, CheckStyle::Star];
|
||||
|
||||
fn code(self) -> &'static str {
|
||||
match self {
|
||||
CheckStyle::Check => "4",
|
||||
CheckStyle::Circle => "l",
|
||||
CheckStyle::Cross => "8",
|
||||
CheckStyle::Diamond => "u",
|
||||
CheckStyle::Square => "n",
|
||||
CheckStyle::Star => "H",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
CheckStyle::Check => "Check",
|
||||
CheckStyle::Circle => "Circle",
|
||||
CheckStyle::Cross => "Cross",
|
||||
CheckStyle::Diamond => "Diamond",
|
||||
CheckStyle::Square => "Square",
|
||||
CheckStyle::Star => "Star",
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget's style: its `/MK /CA`, else a check for check boxes and a circle for radios.
|
||||
pub(crate) fn of_widget(doc: &Document, wd: &Dict, kind: FieldKind) -> Self {
|
||||
let mk = wd.get(b"MK").and_then(|m| doc.resolve(m).as_dict().cloned()).unwrap_or_default();
|
||||
let ca = mk.get(b"CA").and_then(|c| doc.resolve(c).as_string().map(|s| s.to_text()));
|
||||
CheckStyle::ALL.into_iter().find(|s| ca.as_deref() == Some(s.code())).unwrap_or(if kind == FieldKind::Radio {
|
||||
CheckStyle::Circle
|
||||
} else {
|
||||
CheckStyle::Check
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A check box's or radio button's style (from its first widget).
|
||||
pub fn check_style(doc: &Document, f: &Field) -> CheckStyle {
|
||||
let wd = f.widgets.first().and_then(|w| doc.get(w.obj).as_dict().cloned()).unwrap_or_default();
|
||||
CheckStyle::of_widget(doc, &wd, f.kind)
|
||||
}
|
||||
|
||||
fn rgb_of(doc: &Document, o: Option<&Object>) -> Option<[f64; 3]> {
|
||||
let v: Vec<f64> = doc.resolve(o?).as_array()?.iter().filter_map(Object::as_f64).collect();
|
||||
match v.len() {
|
||||
|
|
@ -702,6 +761,9 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result<S
|
|||
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");
|
||||
}
|
||||
if props.check_style.is_some() && !matches!(f.kind, FieldKind::CheckBox | FieldKind::Radio) {
|
||||
return invalid("only check boxes and radio buttons have a check style");
|
||||
}
|
||||
doc.update_dict(f.obj, |d| {
|
||||
d.set(b"Ff".to_vec(), Object::Int(ff as i64));
|
||||
if let Some(t) = &props.tooltip {
|
||||
|
|
@ -785,6 +847,14 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result<S
|
|||
})?;
|
||||
}
|
||||
}
|
||||
if let Some(style) = props.check_style {
|
||||
for w in &f.widgets {
|
||||
// Resolved first: an indirect /MK keeps its colours.
|
||||
let mut mk = doc.get(w.obj).as_dict().and_then(|d| d.get(b"MK").map(|m| doc.resolve(m).as_dict().cloned())).flatten().unwrap_or_default();
|
||||
mk.set(b"CA".to_vec(), PdfString::text(style.code()));
|
||||
doc.update_dict(w.obj, |d| d.set(b"MK".to_vec(), Object::Dict(mk)))?;
|
||||
}
|
||||
}
|
||||
if let Some((wi, r)) = props.rect {
|
||||
let w = f.widgets.get(wi).ok_or_else(|| FormError::Invalid(format!("{name} has no widget {}", wi + 1)))?;
|
||||
let r = [r[0].min(r[2]), r[1].min(r[3]), r[0].max(r[2]), r[1].max(r[3])];
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ pub mod detect;
|
|||
mod scripting;
|
||||
pub use actions::{FieldAction, Trigger, field_actions, set_field_actions};
|
||||
pub use author::{
|
||||
BorderStyle, FieldFont, FieldProps, Look, NewField, add_field, delete_field, duplicate_field, look, redraw_field, set_button_icon, set_props,
|
||||
BorderStyle, CheckStyle, FieldFont, FieldProps, Look, NewField, add_field, check_style, delete_field, duplicate_field, look, redraw_field,
|
||||
set_button_icon, set_props,
|
||||
};
|
||||
pub use scripting::{
|
||||
FieldChange, FieldEvent, NoScripts, ScriptResult, Scripts, apply_script_changes, document_scripts, document_scripts_named, set_document_script,
|
||||
|
|
|
|||
|
|
@ -523,6 +523,58 @@ fn options_tab_flags_alignment_and_defaults() {
|
|||
assert!(set_props(&mut doc, &text, &FieldProps { quadding: Some(5), ..FieldProps::default() }).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_box_styles_change_the_mark() {
|
||||
// #94: Field Properties ▸ Options ▸ Check Box Style (stored as /MK /CA).
|
||||
let mut doc = one_page();
|
||||
let check = add_field(&mut doc, 0, [50.0, 700.0, 70.0, 720.0], &NewField::CheckBox, None).unwrap();
|
||||
let radio = add_field(&mut doc, 0, [50.0, 600.0, 70.0, 620.0], &NewField::Radio { group: None, export: "A".into() }, None).unwrap();
|
||||
let text = add_field(&mut doc, 0, [50.0, 500.0, 250.0, 520.0], &NewField::Text { multiline: false }, None).unwrap();
|
||||
let style = |doc: &Document, name: &str| check_style(doc, field(&fields(doc), name));
|
||||
assert_eq!((style(&doc, &check), style(&doc, &radio)), (CheckStyle::Check, CheckStyle::Circle), "the defaults");
|
||||
// The "on" appearance: the widget's /AP /N entry for its on state.
|
||||
let on_ap = |doc: &Document, name: &str| {
|
||||
let all = fields(doc);
|
||||
let w = &field(&all, name).widgets[0];
|
||||
let on = w.on_state.clone().unwrap();
|
||||
let n = doc
|
||||
.get(w.obj)
|
||||
.as_dict()
|
||||
.unwrap()
|
||||
.get(b"AP")
|
||||
.unwrap()
|
||||
.as_dict()
|
||||
.unwrap()
|
||||
.get(b"N")
|
||||
.unwrap()
|
||||
.as_dict()
|
||||
.unwrap()
|
||||
.reference(on.as_bytes())
|
||||
.unwrap();
|
||||
let Object::Stream(s) = &*doc.get(n) else { panic!() };
|
||||
String::from_utf8_lossy(&s.decoded().unwrap()).into_owned()
|
||||
};
|
||||
let before = on_ap(&doc, &check);
|
||||
for (s, code) in
|
||||
[(CheckStyle::Cross, "8"), (CheckStyle::Square, "n"), (CheckStyle::Star, "H"), (CheckStyle::Diamond, "u"), (CheckStyle::Circle, "l")]
|
||||
{
|
||||
set_props(&mut doc, &check, &FieldProps { check_style: Some(s), ..FieldProps::default() }).unwrap();
|
||||
let doc2 = reopen(&doc);
|
||||
assert_eq!(style(&doc2, &check), s, "saved and read back");
|
||||
let mk = doc2.get(field(&fields(&doc2), &check).widgets[0].obj).as_dict().unwrap().get(b"MK").unwrap().as_dict().cloned().unwrap();
|
||||
assert_eq!(mk.get(b"CA").and_then(|c| c.as_string()).map(|c| c.to_text()).as_deref(), Some(code));
|
||||
assert!(mk.get(b"BC").is_some(), "the border colour is kept");
|
||||
assert_ne!(on_ap(&doc, &check), before, "{s:?} draws a different mark");
|
||||
}
|
||||
assert!(on_ap(&doc, &check).contains(" c f"), "a circle is a filled curve");
|
||||
set_props(&mut doc, &radio, &FieldProps { check_style: Some(CheckStyle::Square), ..FieldProps::default() }).unwrap();
|
||||
assert!(on_ap(&doc, &radio).contains(" re f"), "a square");
|
||||
assert!(
|
||||
set_props(&mut doc, &text, &FieldProps { check_style: Some(CheckStyle::Star), ..FieldProps::default() }).is_err(),
|
||||
"text fields have no style"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordering_tabs_manually() {
|
||||
let mut doc = one_page();
|
||||
|
|
|
|||
|
|
@ -541,8 +541,10 @@ impl crate::PrintCraftApp {
|
|||
let Some(f) = self.session.get(id).and_then(|d| d.form.iter().find(|f| f.name == name).cloned()) else { return };
|
||||
let mut d = FieldDraft::new(&f, widget);
|
||||
d.look = self.session.get(id).and_then(|doc| doc.field_look(name));
|
||||
d.check_style = self.session.get(id).and_then(|doc| doc.field_check_style(name));
|
||||
if let Some(o) = d.original.as_mut() {
|
||||
o.look = d.look;
|
||||
o.check_style = d.check_style;
|
||||
}
|
||||
let others: Vec<String> =
|
||||
self.session.get(id).map(|doc| doc.form.iter().filter(|x| x.name != name).map(|x| x.name.clone()).collect()).unwrap_or_default();
|
||||
|
|
@ -647,6 +649,8 @@ pub struct FieldDraft {
|
|||
/// Left, bottom, width, height in points.
|
||||
pub position: [f64; 4],
|
||||
pub look: Option<FieldLook>,
|
||||
/// Check boxes and radio buttons: the mark when on (Options tab).
|
||||
pub check_style: Option<printcraft_engine::CheckStyle>,
|
||||
pub format: Format,
|
||||
pub validate: Validate,
|
||||
pub calculate: Calculate,
|
||||
|
|
@ -688,6 +692,7 @@ impl FieldDraft {
|
|||
font_size: da_size(&f.da),
|
||||
position: [r[0], r[1], r[2] - r[0], r[3] - r[1]],
|
||||
look: None,
|
||||
check_style: None,
|
||||
format: f.actions.format.clone(),
|
||||
validate: f.actions.validate.clone(),
|
||||
calculate: f.actions.calculate.clone(),
|
||||
|
|
@ -742,6 +747,7 @@ impl FieldDraft {
|
|||
(self.widget, [x, y, x + w.max(4.0), y + h.max(4.0)])
|
||||
}),
|
||||
look: (self.look != o.look).then_some(self.look).flatten(),
|
||||
check_style: (self.check_style != o.check_style).then_some(self.check_style).flatten(),
|
||||
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()),
|
||||
|
|
@ -963,6 +969,20 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Toke
|
|||
if ui.checkbox(&mut checked, label).changed() {
|
||||
d.default = if checked { on } else { String::new() };
|
||||
}
|
||||
if let Some(style) = d.check_style.as_mut() {
|
||||
ui.horizontal(|ui| {
|
||||
let l = ui.label(if d.kind == FormFieldKind::CheckBox { "Check Box Style:" } else { "Button Style:" });
|
||||
egui::ComboBox::from_id_salt("check-style")
|
||||
.selected_text(style.label())
|
||||
.show_ui(ui, |ui| {
|
||||
for s in printcraft_engine::CheckStyle::ALL {
|
||||
ui.selectable_value(style, s, s.label());
|
||||
}
|
||||
})
|
||||
.response
|
||||
.labelled_by(l.id);
|
||||
});
|
||||
}
|
||||
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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -170,6 +170,30 @@ fn appearance_tab_restyles_the_field() {
|
|||
assert_eq!((l.style, l.font, l.fill), (BorderStyle::Dashed, FieldFont::Courier, Some([1.0, 1.0, 0.8])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn options_tab_sets_the_check_box_style() {
|
||||
// #94: Check Box Style on the Options tab.
|
||||
use printcraft_engine::CheckStyle;
|
||||
let mut h = harness();
|
||||
h.state_mut().apply_edit(printcraft_engine::Edit::AddField {
|
||||
page: 0,
|
||||
rect: [40.0, 300.0, 56.0, 316.0],
|
||||
kind: printcraft_engine::NewField::CheckBox,
|
||||
name: Some("agree".into()),
|
||||
});
|
||||
h.state_mut().open_field_props("agree", 0);
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Options").click();
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Check Box Style:");
|
||||
assert_eq!(h.state().field_props.as_ref().unwrap().check_style, Some(CheckStyle::Check));
|
||||
h.state_mut().field_props.as_mut().unwrap().check_style = Some(CheckStyle::Cross);
|
||||
h.get_by_label("OK").click();
|
||||
h.run_steps(3);
|
||||
let s = h.state();
|
||||
assert_eq!(s.session.get(s.views[0].id).unwrap().field_check_style("agree"), Some(CheckStyle::Cross));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn options_tab_sets_flags_alignment_and_defaults() {
|
||||
use printcraft_engine::field_flags as ff;
|
||||
|
|
|
|||
|
|
@ -4689,6 +4689,18 @@ tools = ["form_add_field"]
|
|||
evidence = ["crates/forms/src/tests.rs::every_field_type_can_be_added_named_and_drawn", "crates/automation/tests/automation.rs::preparing_a_form_through_tools"]
|
||||
notes = "new group Group1… or join the selected group with the next ChoiceN"
|
||||
|
||||
[[feature]]
|
||||
id = "form.check-box-style"
|
||||
title = "Check box and radio button styles"
|
||||
area = "F"
|
||||
tier = "P1"
|
||||
milestone = "M6"
|
||||
status = "shipped"
|
||||
commands = ["form.field.properties"]
|
||||
tools = ["form_set_props", "form_fields"]
|
||||
evidence = ["crates/forms/src/tests.rs::check_box_styles_change_the_mark", "crates/ui-egui/tests/prepare.rs::options_tab_sets_the_check_box_style", "crates/automation/tests/automation.rs::preparing_a_form_through_tools"]
|
||||
notes = "Field Properties ▸ Options: check, circle, cross, diamond, square, star (/MK /CA, Acrobat's codes), drawn as paths; defaults check (check boxes) and circle (radio buttons)"
|
||||
|
||||
[[feature]]
|
||||
id = "form.add-dropdown"
|
||||
title = "Add drop-down lists"
|
||||
|
|
|
|||
Loading…
Reference in New Issue