M6: Field Properties ▸ Options for every field type: text alignment, default value, scroll long text, rich text, password, file selection, spell check, comb of characters (with its rules); choice defaults, sort items (the list is reordered), custom text, multiple selection, commit immediately; check box / radio checked by default and radios in unison. Reset form restores defaults. form_set_props takes align, default and flags
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
09c1404d95
commit
dfae15ec67
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -246,7 +246,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). 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<ToolDef> {
|
|||
"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)." },
|
||||
|
|
|
|||
|
|
@ -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(_))
|
||||
|
|
|
|||
|
|
@ -579,7 +579,7 @@ pub enum Edit {
|
|||
/// Field Properties ▸ General / Options.
|
||||
SetFieldProps {
|
||||
name: String,
|
||||
props: FieldProps,
|
||||
props: Box<FieldProps>,
|
||||
},
|
||||
/// Delete a field and all its widgets.
|
||||
DeleteField {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -76,6 +76,12 @@ pub struct FieldProps {
|
|||
pub format: Option<crate::af::Format>,
|
||||
pub validate: Option<crate::af::Validate>,
|
||||
pub calculate: Option<crate::af::Calculate>,
|
||||
/// 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<i64>,
|
||||
/// Default value (`/DV`): what Reset form restores.
|
||||
pub default_value: Option<Option<String>>,
|
||||
}
|
||||
|
||||
fn invalid<T>(m: impl Into<String>) -> Result<T, FormError> {
|
||||
|
|
@ -580,6 +586,26 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result<S
|
|||
if f.kind == FieldKind::Text {
|
||||
set_flag(flags::MULTILINE, props.multiline);
|
||||
}
|
||||
for (flag, on) in &props.flags {
|
||||
set_flag(*flag, Some(*on));
|
||||
}
|
||||
// A comb spreads a fixed number of characters across the field: it needs a limit and
|
||||
// excludes multi-line, password, file selection and scrolling (ISO 32000-2 Table 229).
|
||||
if f.kind == FieldKind::Text && ff & flags::COMB != 0 {
|
||||
let limit = match props.max_len {
|
||||
Some(m) => 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<S
|
|||
if let Some(opts) = &props.options {
|
||||
d.set(b"Opt".to_vec(), Object::Array(opts.iter().map(|o| Object::String(PdfString::text(o))).collect()));
|
||||
}
|
||||
// "Sort items": the list itself is kept in order (export values stay with their items).
|
||||
if ff & flags::SORT != 0 && matches!(f.kind, FieldKind::Combo | FieldKind::List) {
|
||||
let mut items: Vec<(String, String)> = 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()));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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<_>>(), 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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
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");
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue