mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-28 12:08:46 -06:00
refactor(ui-egui): split prepare.rs into prepare/
New modules: draft, properties, tabs.
This commit is contained in:
+8
-1007
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,297 @@
|
||||
use super::*;
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────── Field Properties
|
||||
|
||||
impl crate::PdfCraftApp {
|
||||
/// Open Field Properties for a field of the active document.
|
||||
pub fn open_field_props(&mut self, name: &str, widget: usize) {
|
||||
let Some((index, id)) = self.active_ids() else { return };
|
||||
let names = self.views[index].prepare.names();
|
||||
if names.len() > 1 && names.iter().any(|n| n == name) {
|
||||
let draft = self.session.get(id).map_or(Err("The document is no longer open."), |doc| crate::bulk_fields::Draft::new(doc, names));
|
||||
match draft {
|
||||
Ok(draft) => {
|
||||
self.field_props = None;
|
||||
self.bulk_field_props = Some(draft);
|
||||
self.dialog = Some(crate::Dialog::BulkFieldProps);
|
||||
}
|
||||
Err(why) => self.notify_tr(why),
|
||||
}
|
||||
return;
|
||||
}
|
||||
self.bulk_field_props = None;
|
||||
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();
|
||||
d.others = others.clone();
|
||||
d.actions = self.session.get(id).map(|doc| doc.field_actions(name)).unwrap_or_default();
|
||||
d.chained = self.session.get(id).map(|doc| doc.field_action_chains(name)).unwrap_or_default();
|
||||
if let Some(o) = d.original.as_mut() {
|
||||
o.others = others;
|
||||
o.actions = d.actions.clone();
|
||||
}
|
||||
self.field_props = Some(d);
|
||||
self.dialog = Some(crate::Dialog::FieldProps);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum FieldTab {
|
||||
General,
|
||||
Appearance,
|
||||
Position,
|
||||
Options,
|
||||
Format,
|
||||
Validate,
|
||||
Calculate,
|
||||
Actions,
|
||||
}
|
||||
|
||||
/// Actions tab: the "Select Action" choices.
|
||||
pub const ACTION_KINDS: [&str; 8] = [
|
||||
"Run a JavaScript",
|
||||
"Open a web link",
|
||||
"Reset a form",
|
||||
"Execute a menu item",
|
||||
"Go to a page view",
|
||||
"Show a field",
|
||||
"Hide a field",
|
||||
"Submit a form",
|
||||
];
|
||||
|
||||
/// Actions tab: the action being added.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ActionDraft {
|
||||
pub trigger: pdfcraft_engine::FieldTrigger,
|
||||
pub kind: usize,
|
||||
/// The script, URL, field names (comma-separated), menu item or page number.
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
impl Default for ActionDraft {
|
||||
fn default() -> Self {
|
||||
ActionDraft { trigger: pdfcraft_engine::FieldTrigger::MouseUp, kind: 0, text: String::new() }
|
||||
}
|
||||
}
|
||||
|
||||
impl ActionDraft {
|
||||
pub fn action(&self) -> Result<pdfcraft_engine::FieldAction, String> {
|
||||
use pdfcraft_engine::FieldAction as A;
|
||||
let t = self.text.trim();
|
||||
let names = || t.split(',').map(str::trim).filter(|s| !s.is_empty()).map(str::to_string).collect::<Vec<_>>();
|
||||
Ok(match self.kind {
|
||||
0 => A::JavaScript(self.text.clone()),
|
||||
1 if !t.is_empty() => A::Uri(t.to_string()),
|
||||
2 => A::Reset(names()),
|
||||
3 => A::Named(if t.is_empty() { "Print".into() } else { t.to_string() }),
|
||||
4 => A::GoTo(t.parse::<usize>().ok().filter(|p| *p >= 1).ok_or("Enter a page number")? - 1),
|
||||
5 if !t.is_empty() => A::ShowHide { fields: names(), hide: false },
|
||||
6 if !t.is_empty() => A::ShowHide { fields: names(), hide: true },
|
||||
7 if !t.is_empty() => A::Submit(t.to_string()),
|
||||
1 | 7 => return Err("Enter a URL".into()),
|
||||
_ => return Err("Enter the field names".into()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The Field Properties dialog's working copy.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct FieldDraft {
|
||||
pub field: String,
|
||||
pub kind: FormFieldKind,
|
||||
pub widget: usize,
|
||||
pub tab: FieldTab,
|
||||
pub name: String,
|
||||
pub tooltip: String,
|
||||
pub caption: String,
|
||||
pub read_only: bool,
|
||||
pub required: bool,
|
||||
pub multiline: bool,
|
||||
pub limit: bool,
|
||||
pub max_len: usize,
|
||||
pub options: Vec<String>,
|
||||
pub new_option: String,
|
||||
/// `/Ff` as edited on the Options tab.
|
||||
pub flags: u32,
|
||||
/// Field Properties ▸ General ▸ Locked.
|
||||
pub locked: bool,
|
||||
/// `/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.
|
||||
pub position: [f64; 4],
|
||||
/// `/MK /R`: 0, 90, 180 or 270 degrees counterclockwise.
|
||||
pub rotation: i64,
|
||||
pub look: Option<FieldLook>,
|
||||
/// Check boxes and radio buttons: the mark when on (Options tab).
|
||||
pub check_style: Option<pdfcraft_engine::CheckStyle>,
|
||||
pub format: Format,
|
||||
pub validate: Validate,
|
||||
pub calculate: Calculate,
|
||||
/// Every other field's name (the Calculate tab picks from them).
|
||||
pub others: Vec<String>,
|
||||
/// Actions tab: each trigger's action, and the one being added.
|
||||
pub actions: Vec<(pdfcraft_engine::FieldTrigger, pdfcraft_engine::FieldAction)>,
|
||||
pub new_action: ActionDraft,
|
||||
/// Actions tab: the trigger whose script `new_action` is editing (Edit), if any.
|
||||
pub editing: Option<pdfcraft_engine::FieldTrigger>,
|
||||
/// Actions tab: how many actions the document chains after each trigger's first one. They are
|
||||
/// kept when that first action is replaced.
|
||||
pub chained: Vec<(pdfcraft_engine::FieldTrigger, usize)>,
|
||||
pub(super) original: Box<Option<FieldDraft>>,
|
||||
}
|
||||
|
||||
pub(crate) fn da_size(da: &str) -> f64 {
|
||||
let mut previous = "";
|
||||
let mut size = 0.0;
|
||||
for token in da.split_whitespace() {
|
||||
if token == "Tf" {
|
||||
size = previous.parse::<f64>().ok().filter(|v| v.is_finite() && *v >= 0.0).unwrap_or(0.0);
|
||||
}
|
||||
previous = token;
|
||||
}
|
||||
size
|
||||
}
|
||||
|
||||
pub(crate) fn da_font(da: &str) -> &str {
|
||||
let (mut two_back, mut previous, mut font) = ("", "", "Helv");
|
||||
for token in da.split_whitespace() {
|
||||
if token == "Tf" {
|
||||
font = two_back.trim_start_matches('/');
|
||||
}
|
||||
two_back = previous;
|
||||
previous = token;
|
||||
}
|
||||
font
|
||||
}
|
||||
|
||||
impl FieldDraft {
|
||||
pub fn new(f: &FormField, widget: usize) -> Self {
|
||||
let r = f.widgets.get(widget).map(|w| w.rect).unwrap_or_default();
|
||||
let mut d = FieldDraft {
|
||||
field: f.name.clone(),
|
||||
kind: f.kind,
|
||||
widget,
|
||||
tab: FieldTab::General,
|
||||
// The full name: a dotted name moves the field under those parent fields (#2696).
|
||||
name: f.name.clone(),
|
||||
tooltip: f.tooltip.clone().unwrap_or_default(),
|
||||
caption: f.widgets.get(widget).and_then(|w| w.caption.clone()).unwrap_or_default(),
|
||||
read_only: f.read_only(),
|
||||
required: f.has(pdfcraft_engine::field_flags::REQUIRED),
|
||||
multiline: f.has(pdfcraft_engine::field_flags::MULTILINE),
|
||||
limit: f.max_len.is_some(),
|
||||
max_len: f.max_len.unwrap_or(0),
|
||||
options: f.options.iter().map(|(_, d)| d.clone()).collect(),
|
||||
new_option: String::new(),
|
||||
flags: f.flags,
|
||||
locked: f.locked(),
|
||||
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]],
|
||||
rotation: f.widgets.get(widget).map(|w| w.rotation).unwrap_or(0),
|
||||
look: None,
|
||||
check_style: None,
|
||||
format: f.actions.format.clone(),
|
||||
validate: f.actions.validate.clone(),
|
||||
calculate: f.actions.calculate.clone(),
|
||||
others: Vec::new(),
|
||||
actions: Vec::new(),
|
||||
new_action: ActionDraft::default(),
|
||||
editing: None,
|
||||
chained: Vec::new(),
|
||||
original: Box::new(None),
|
||||
};
|
||||
d.original = Box::new(Some(d.clone()));
|
||||
d
|
||||
}
|
||||
|
||||
pub fn title(&self) -> &'static str {
|
||||
match self.kind {
|
||||
FormFieldKind::Text => "Text Field Properties",
|
||||
FormFieldKind::CheckBox => "Check Box Properties",
|
||||
FormFieldKind::Radio => "Radio Button Properties",
|
||||
FormFieldKind::Combo => "Dropdown Properties",
|
||||
FormFieldKind::List => "List Box Properties",
|
||||
FormFieldKind::PushButton => "Button Properties",
|
||||
FormFieldKind::Signature => "Digital Signature Properties",
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn tabs(&self) -> Vec<(FieldTab, &'static str)> {
|
||||
let mut t = vec![(FieldTab::General, "General"), (FieldTab::Appearance, "Appearance"), (FieldTab::Position, "Position")];
|
||||
if self.kind != FormFieldKind::Signature {
|
||||
t.push((FieldTab::Options, "Options"));
|
||||
}
|
||||
if matches!(self.kind, FormFieldKind::Text | FormFieldKind::Combo) {
|
||||
t.extend([(FieldTab::Format, "Format"), (FieldTab::Validate, "Validate"), (FieldTab::Calculate, "Calculate")]);
|
||||
}
|
||||
t.push((FieldTab::Actions, "Actions"));
|
||||
t
|
||||
}
|
||||
|
||||
/// The properties that changed (`None` when nothing did).
|
||||
pub fn props(&self) -> Option<FieldProps> {
|
||||
let o = self.original.as_ref().as_ref()?;
|
||||
let ch = |a: bool, b: bool| (a != b).then_some(a);
|
||||
let p = FieldProps {
|
||||
name: (self.name.trim() != o.name).then(|| self.name.trim().to_string()),
|
||||
tooltip: (self.tooltip != o.tooltip).then(|| self.tooltip.clone()),
|
||||
caption: (self.caption != o.caption).then(|| (self.widget, self.caption.clone())),
|
||||
read_only: ch(self.read_only, o.read_only),
|
||||
required: ch(self.required, o.required),
|
||||
multiline: ch(self.multiline, o.multiline),
|
||||
max_len: ((self.limit, self.max_len) != (o.limit, o.max_len)).then_some((self.limit && self.max_len > 0).then_some(self.max_len)),
|
||||
options: (self.options != o.options).then(|| self.options.clone()),
|
||||
font_size: ((self.font_size - o.font_size).abs() > 1e-6).then_some(self.font_size),
|
||||
rect: (self.position != o.position).then(|| {
|
||||
let [x, y, w, h] = self.position;
|
||||
(self.widget, [x, y, x + w.max(4.0), y + h.max(4.0)])
|
||||
}),
|
||||
rotation: (self.rotation != o.rotation).then_some((self.widget, self.rotation)),
|
||||
look: (self.look != o.look).then_some(self.look).flatten(),
|
||||
appearance: None,
|
||||
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()),
|
||||
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),
|
||||
locked: ch(self.locked, o.locked),
|
||||
actions: (self.actions != o.actions).then(|| self.actions.clone()),
|
||||
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 pdfcraft_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,
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,332 @@
|
||||
use super::draft::{FieldDraft, FieldTab};
|
||||
use super::tabs::{actions_tab, calculate_tab, format_tab, validate_tab};
|
||||
use super::*;
|
||||
|
||||
/// 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.add(widgets::checkbox(&mut on, label)).changed() {
|
||||
if on != inverted {
|
||||
*flags |= bit;
|
||||
} else {
|
||||
*flags &= !bit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared PDF `/Q` picker for text and choice-field Options.
|
||||
fn alignment(ui: &mut egui::Ui, quadding: &mut i64) {
|
||||
ui.label(tl!("Alignment:"));
|
||||
egui::ComboBox::from_id_salt("quadding")
|
||||
.selected_text(tl!(match (*quadding).clamp(0, 2) {
|
||||
1 => "Center",
|
||||
2 => "Right",
|
||||
_ => "Left",
|
||||
}))
|
||||
.show_ui(ui, |ui| {
|
||||
for (q, label) in [(0, tl!("Left")), (1, tl!("Center")), (2, tl!("Right"))] {
|
||||
ui.selectable_value(quadding, q, label);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 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;
|
||||
ui.set_width(600.0);
|
||||
ui.label(egui::RichText::new(tl!(d.title())).font(theme::semibold(18.0)));
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for (tab, label) in d.tabs() {
|
||||
if widgets::mode_tab(ui, tl!(label), d.tab == tab).clicked() {
|
||||
d.tab = tab;
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
ui.add_space(6.0);
|
||||
let mut enter = false;
|
||||
// A locked field keeps its properties until it is unlocked (Acrobat greys them out).
|
||||
let was_locked = d.original.as_ref().as_ref().is_some_and(|o| o.locked) && d.locked;
|
||||
if was_locked {
|
||||
ui.label(egui::RichText::new(tl!("This field is locked. Unlock it to change its properties.")).small().color(t.text_muted));
|
||||
}
|
||||
ui.scope(|ui| {
|
||||
if was_locked {
|
||||
ui.disable();
|
||||
}
|
||||
match d.tab {
|
||||
FieldTab::General => {
|
||||
egui::Grid::new("field-general").num_columns(2).spacing([12.0, 10.0]).show(ui, |ui| {
|
||||
let l = ui.label(tl!("Name:"));
|
||||
let r = ui.add(crate::widgets::line(&mut d.name).desired_width(340.0)).labelled_by(l.id);
|
||||
enter |= r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
|
||||
ui.end_row();
|
||||
let l = ui.label(tl!("Tooltip:"));
|
||||
ui.add(crate::widgets::line(&mut d.tooltip).desired_width(340.0)).labelled_by(l.id);
|
||||
ui.end_row();
|
||||
});
|
||||
ui.add_space(12.0);
|
||||
widgets::section_title(ui, tl!("Common Properties"));
|
||||
ui.add(widgets::checkbox(&mut d.read_only, tl!("Read Only")));
|
||||
ui.add(widgets::checkbox(&mut d.required, tl!("Required")));
|
||||
}
|
||||
FieldTab::Appearance => {
|
||||
egui::Grid::new("field-appearance").num_columns(2).spacing([12.0, 10.0]).show(ui, |ui| {
|
||||
ui.label(tl!("Font Size:"));
|
||||
let mut auto = d.font_size == 0.0;
|
||||
ui.horizontal(|ui| {
|
||||
if ui.add(widgets::checkbox(&mut auto, tl!("Auto"))).changed() {
|
||||
d.font_size = if auto { 0.0 } else { 12.0 };
|
||||
}
|
||||
ui.add_enabled(
|
||||
!auto,
|
||||
egui::DragValue::new(&mut d.font_size).range(2.0..=100.0).clamp_existing_to_range(false).speed(0.25).suffix(" pt"),
|
||||
);
|
||||
});
|
||||
ui.end_row();
|
||||
if let Some(l) = d.look.as_mut() {
|
||||
ui.label(tl!("Font:"));
|
||||
egui::ComboBox::from_id_salt("field-font").selected_text(l.font.label()).show_ui(ui, |ui| {
|
||||
for f in FieldFont::ALL {
|
||||
ui.selectable_value(&mut l.font, f, f.label());
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label(tl!("Text Color:"));
|
||||
if let Some(c) = crate::comments::swatch_grid(ui, Some(l.text)) {
|
||||
l.text = c;
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
});
|
||||
if let Some(l) = d.look.as_mut() {
|
||||
widgets::section_title(ui, tl!("Borders and Colors"));
|
||||
egui::Grid::new("field-borders").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
|
||||
for (index, (label, slot)) in
|
||||
[(tl!("Border Color:"), &mut l.border), (tl!("Fill Color:"), &mut l.fill)].into_iter().enumerate()
|
||||
{
|
||||
ui.label(label);
|
||||
ui.push_id(("field-color", index), |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
let mut none = slot.is_none();
|
||||
if ui.add(widgets::checkbox(&mut none, tl!("No color"))).changed() {
|
||||
*slot = if none { None } else { Some([0.0; 3]) };
|
||||
}
|
||||
if let Some(c) = crate::comments::swatch_grid(ui, *slot) {
|
||||
*slot = Some(c);
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.end_row();
|
||||
}
|
||||
ui.label(tl!("Line Thickness:"));
|
||||
let thick = |w: f64| {
|
||||
if w <= 1.0 {
|
||||
tl!("Thin")
|
||||
} else if w <= 2.0 {
|
||||
tl!("Medium")
|
||||
} else {
|
||||
tl!("Thick")
|
||||
}
|
||||
};
|
||||
egui::ComboBox::from_id_salt("field-width").selected_text(thick(l.width)).show_ui(ui, |ui| {
|
||||
for (w, label) in [(1.0, tl!("Thin")), (2.0, tl!("Medium")), (3.0, tl!("Thick"))] {
|
||||
ui.selectable_value(&mut l.width, w, label);
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label(tl!("Line Style:"));
|
||||
egui::ComboBox::from_id_salt("field-style").selected_text(tl!(l.style.label())).show_ui(ui, |ui| {
|
||||
for s in BorderStyle::ALL {
|
||||
ui.selectable_value(&mut l.style, s, tl!(s.label()));
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
});
|
||||
}
|
||||
}
|
||||
FieldTab::Position => {
|
||||
egui::Grid::new("field-position").num_columns(4).spacing([12.0, 10.0]).show(ui, |ui| {
|
||||
for (i, label) in [tl!("Left:"), tl!("Bottom:"), tl!("Width:"), tl!("Height:")].into_iter().enumerate() {
|
||||
ui.label(label);
|
||||
let range = if i < 2 { -14_400.0..=14_400.0 } else { 4.0..=14_400.0 };
|
||||
ui.add(egui::DragValue::new(&mut d.position[i]).range(range).speed(0.5).suffix(" pt"));
|
||||
if i % 2 == 1 {
|
||||
ui.end_row();
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.label(egui::RichText::new(tl!("Points from the page's bottom-left corner.")).small().color(t.text_faint));
|
||||
ui.add_space(8.0);
|
||||
ui.horizontal(|ui| {
|
||||
let l = ui.label(tl!("Rotation:"));
|
||||
let before = d.rotation;
|
||||
egui::ComboBox::from_id_salt("field-rotation")
|
||||
.selected_text(format!("{}°", d.rotation))
|
||||
.show_ui(ui, |ui| {
|
||||
for r in [0, 90, 180, 270] {
|
||||
if ui.selectable_value(&mut d.rotation, r, format!("{r}°")).changed() && (before % 180 == 0) != (r % 180 == 0) {
|
||||
// Keep the same center. set_props skips its own swap when the rect changed too.
|
||||
let [x, y, w, h] = d.position;
|
||||
let (cx, cy) = (x + w / 2.0, y + h / 2.0);
|
||||
d.position = [cx - h / 2.0, cy - w / 2.0, h, w];
|
||||
}
|
||||
}
|
||||
})
|
||||
.response
|
||||
.labelled_by(l.id);
|
||||
});
|
||||
}
|
||||
FieldTab::Format => format_tab(ui, d, t),
|
||||
FieldTab::Validate => validate_tab(ui, d),
|
||||
FieldTab::Calculate => calculate_tab(ui, d, t),
|
||||
FieldTab::Actions => actions_tab(ui, d, t),
|
||||
FieldTab::Options => match d.kind {
|
||||
FormFieldKind::PushButton => {
|
||||
ui.horizontal(|ui| {
|
||||
let l = ui.label(tl!("Label"));
|
||||
ui.add(crate::widgets::line(&mut d.caption).desired_width(340.0)).labelled_by(l.id);
|
||||
});
|
||||
}
|
||||
FormFieldKind::Text => {
|
||||
use pdfcraft_engine::field_flags as ff;
|
||||
egui::Grid::new("text-options").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
|
||||
alignment(ui, &mut d.quadding);
|
||||
ui.end_row();
|
||||
let l = ui.label(tl!("Default Value:"));
|
||||
ui.add(crate::widgets::line(&mut d.default).desired_width(260.0)).labelled_by(l.id);
|
||||
ui.end_row();
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
ui.add(widgets::checkbox(&mut d.multiline, tl!("Multi-line")));
|
||||
flag_box(ui, &mut d.flags, ff::DO_NOT_SCROLL, true, tl!("Scroll long text"));
|
||||
flag_box(ui, &mut d.flags, ff::RICH_TEXT, false, tl!("Allow Rich Text Formatting"));
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(widgets::checkbox(&mut d.limit, tl!("Limit of")));
|
||||
ui.add_enabled(d.limit, egui::DragValue::new(&mut d.max_len).range(0..=10_000));
|
||||
ui.label(tl!("characters"));
|
||||
});
|
||||
flag_box(ui, &mut d.flags, ff::PASSWORD, false, tl!("Password"));
|
||||
flag_box(ui, &mut d.flags, ff::FILE_SELECT, false, tl!("Field is used for file selection"));
|
||||
flag_box(ui, &mut d.flags, ff::DO_NOT_SPELL_CHECK, true, tl!("Check spelling"));
|
||||
ui.horizontal(|ui| {
|
||||
flag_box(ui, &mut d.flags, ff::COMB, false, tl!("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(tl!("characters"));
|
||||
});
|
||||
}
|
||||
FormFieldKind::CheckBox | FormFieldKind::Radio => {
|
||||
use pdfcraft_engine::field_flags as ff;
|
||||
let on = d.on_state.clone();
|
||||
ui.label(crate::i18n::fmt(tl!("Export Value: {on}"), &[("on", &on)]));
|
||||
let mut checked = !d.default.is_empty() && d.default == on;
|
||||
let label =
|
||||
if d.kind == FormFieldKind::CheckBox { tl!("Check box is checked by default") } else { tl!("Button is checked by default") };
|
||||
if ui.add(widgets::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(tl!(if d.kind == FormFieldKind::CheckBox { "Check Box Style:" } else { "Button Style:" }));
|
||||
egui::ComboBox::from_id_salt("check-style")
|
||||
.selected_text(tl!(style.label()))
|
||||
.show_ui(ui, |ui| {
|
||||
for s in pdfcraft_engine::CheckStyle::ALL {
|
||||
ui.selectable_value(style, s, tl!(s.label()));
|
||||
}
|
||||
})
|
||||
.response
|
||||
.labelled_by(l.id);
|
||||
});
|
||||
}
|
||||
if d.kind == FormFieldKind::Radio {
|
||||
flag_box(ui, &mut d.flags, ff::RADIOS_IN_UNISON, false, tl!("Buttons with the same name and value are selected in unison"));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
ui.horizontal(|ui| alignment(ui, &mut d.quadding));
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
let l = ui.label(tl!("Item:"));
|
||||
let r = ui.add(crate::widgets::line(&mut d.new_option).desired_width(240.0)).labelled_by(l.id);
|
||||
let typed = r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
|
||||
if (widgets::pill_button(ui, tl!("Add"), false).clicked() || typed) && !d.new_option.trim().is_empty() {
|
||||
d.options.push(d.new_option.trim().to_string());
|
||||
d.new_option.clear();
|
||||
}
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
widgets::section_title(ui, tl!("Item List"));
|
||||
let mut remove = None;
|
||||
let mut up = None;
|
||||
egui::ScrollArea::vertical().max_height(140.0).show(ui, |ui| {
|
||||
for (i, o) in d.options.iter().enumerate() {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(o);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if ui.small_button(tl!("Delete")).on_hover_text(crate::i18n::fmt(tl!("Delete {o}"), &[("o", o)])).clicked() {
|
||||
remove = Some(i);
|
||||
}
|
||||
if i > 0 && ui.small_button(tl!("Up")).clicked() {
|
||||
up = Some(i);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
if let Some(i) = remove {
|
||||
d.options.remove(i);
|
||||
}
|
||||
if let Some(i) = up {
|
||||
d.options.swap(i - 1, i);
|
||||
}
|
||||
if d.options.is_empty() {
|
||||
ui.label(egui::RichText::new(tl!("Add the choices people pick from.")).color(t.text_muted));
|
||||
}
|
||||
use pdfcraft_engine::field_flags as ff;
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tl!("Default:"));
|
||||
let shown = if d.default.is_empty() { tl!("(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(), tl!("(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, tl!("Sort items"));
|
||||
if d.kind == FormFieldKind::Combo {
|
||||
flag_box(ui, &mut d.flags, ff::EDIT, false, tl!("Allow user to enter custom text"));
|
||||
flag_box(ui, &mut d.flags, ff::DO_NOT_SPELL_CHECK, true, tl!("Check spelling"));
|
||||
} else {
|
||||
flag_box(ui, &mut d.flags, ff::MULTI_SELECT, false, tl!("Multiple selection"));
|
||||
}
|
||||
flag_box(ui, &mut d.flags, ff::COMMIT_ON_SEL_CHANGE, false, tl!("Commit selected value immediately"));
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
ui.add(widgets::checkbox(&mut d.locked, tl!("Locked")));
|
||||
ui.add_space(6.0);
|
||||
let (mut apply, mut cancel) = (enter, false);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if widgets::pill_button(ui, tl!("OK"), true).clicked() {
|
||||
apply = true;
|
||||
}
|
||||
if widgets::pill_button(ui, tl!("Cancel"), false).clicked() {
|
||||
cancel = true;
|
||||
}
|
||||
});
|
||||
(apply, cancel)
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
use super::draft::{ACTION_KINDS, ActionDraft, FieldDraft};
|
||||
use super::*;
|
||||
|
||||
/// The categories of the Format tab.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Category {
|
||||
None,
|
||||
Number,
|
||||
Percent,
|
||||
Date,
|
||||
Time,
|
||||
Special,
|
||||
}
|
||||
|
||||
fn category(f: &Format) -> Category {
|
||||
match f {
|
||||
Format::None => Category::None,
|
||||
Format::Number { .. } => Category::Number,
|
||||
Format::Percent { .. } => Category::Percent,
|
||||
Format::Date(_) => Category::Date,
|
||||
Format::Time(_) => Category::Time,
|
||||
Format::Special(_) | Format::Mask(_) => Category::Special,
|
||||
}
|
||||
}
|
||||
|
||||
const SEPARATORS: [&str; 5] = ["1,234.56", "1234.56", "1.234,56", "1234,56", "1'234.56"];
|
||||
|
||||
const NEGATIVES: [&str; 4] = ["-1,234.01", "1,234.01 (red)", "(1,234.01)", "(1,234.01) (red)"];
|
||||
|
||||
const SPECIALS: [&str; 5] = ["Zip Code", "Zip Code + 4", "Phone Number", "Social Security Number", "Arbitrary Mask"];
|
||||
|
||||
/// Draw the field-format editor for the selected category and its options.
|
||||
pub(super) fn format_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens) {
|
||||
let cat = category(&d.format);
|
||||
let mut picked = cat;
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tl!("Select format category:"));
|
||||
egui::ComboBox::from_id_salt("format-cat")
|
||||
.selected_text(tl!(match cat {
|
||||
Category::None => "None",
|
||||
Category::Number => "Number",
|
||||
Category::Percent => "Percentage",
|
||||
Category::Date => "Date",
|
||||
Category::Time => "Time",
|
||||
Category::Special => "Special",
|
||||
}))
|
||||
.show_ui(ui, |ui| {
|
||||
for (c, l) in [
|
||||
(Category::None, tl!("None")),
|
||||
(Category::Number, tl!("Number")),
|
||||
(Category::Percent, tl!("Percentage")),
|
||||
(Category::Date, tl!("Date")),
|
||||
(Category::Time, tl!("Time")),
|
||||
(Category::Special, tl!("Special")),
|
||||
] {
|
||||
ui.selectable_value(&mut picked, c, l);
|
||||
}
|
||||
});
|
||||
});
|
||||
if picked != cat {
|
||||
d.format = match picked {
|
||||
Category::None => Format::None,
|
||||
Category::Number => Format::Number { decimals: 2, sep: 0, neg: 0, currency: String::new(), prepend: true },
|
||||
Category::Percent => Format::Percent { decimals: 2, sep: 0 },
|
||||
Category::Date => Format::Date("mm/dd/yyyy".into()),
|
||||
Category::Time => Format::Time("HH:MM".into()),
|
||||
Category::Special => Format::Special(0),
|
||||
};
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
let sample = match &d.format {
|
||||
Format::Number { .. } | Format::Percent { .. } => "-1234.5",
|
||||
Format::Date(_) => "10/1/2026",
|
||||
Format::Time(_) => "14:05",
|
||||
Format::Special(2) => "5551234567",
|
||||
Format::Special(3) => "123456789",
|
||||
Format::Special(_) => "123456789",
|
||||
_ => "",
|
||||
};
|
||||
match &mut d.format {
|
||||
Format::None => {
|
||||
ui.label(egui::RichText::new(tl!("The value is shown as typed.")).color(t.text_muted));
|
||||
}
|
||||
Format::Number { decimals, sep, neg, currency, prepend } => {
|
||||
egui::Grid::new("fmt-number").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
|
||||
ui.label(tl!("Decimal places:"));
|
||||
ui.add(egui::DragValue::new(decimals).range(0..=10));
|
||||
ui.end_row();
|
||||
ui.label(tl!("Separator style:"));
|
||||
egui::ComboBox::from_id_salt("fmt-sep").selected_text(SEPARATORS[(*sep).min(4) as usize]).show_ui(ui, |ui| {
|
||||
for (i, s) in SEPARATORS.iter().enumerate() {
|
||||
ui.selectable_value(sep, i as u8, *s);
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label(tl!("Currency symbol:"));
|
||||
ui.horizontal(|ui| {
|
||||
egui::ComboBox::from_id_salt("fmt-cur")
|
||||
.selected_text(if currency.is_empty() { tl!("None") } else { currency.as_str() })
|
||||
.width(70.0)
|
||||
.show_ui(ui, |ui| {
|
||||
for c in ["", "$", "€", "£", "¥", "CHF "] {
|
||||
ui.selectable_value(currency, c.to_string(), if c.is_empty() { tl!("None") } else { c });
|
||||
}
|
||||
});
|
||||
ui.add(widgets::checkbox(prepend, tl!("Before the number")));
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label(tl!("Negative number style:"));
|
||||
egui::ComboBox::from_id_salt("fmt-neg").selected_text(NEGATIVES[(*neg).min(3) as usize]).show_ui(ui, |ui| {
|
||||
for (i, s) in NEGATIVES.iter().enumerate() {
|
||||
ui.selectable_value(neg, i as u8, *s);
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
});
|
||||
}
|
||||
Format::Percent { decimals, sep } => {
|
||||
egui::Grid::new("fmt-pct").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
|
||||
ui.label(tl!("Decimal places:"));
|
||||
ui.add(egui::DragValue::new(decimals).range(0..=10));
|
||||
ui.end_row();
|
||||
ui.label(tl!("Separator style:"));
|
||||
egui::ComboBox::from_id_salt("fmt-psep").selected_text(SEPARATORS[(*sep).min(4) as usize]).show_ui(ui, |ui| {
|
||||
for (i, s) in SEPARATORS.iter().enumerate() {
|
||||
ui.selectable_value(sep, i as u8, *s);
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
});
|
||||
}
|
||||
Format::Date(p) | Format::Time(p) => {
|
||||
let presets: Vec<&str> = if matches!(cat, Category::Time) || picked == Category::Time {
|
||||
TIME_PRESETS.to_vec()
|
||||
} else {
|
||||
DATE_PRESETS.iter().copied().chain(["m/d/yyyy", "mm/dd/yyyy", "yyyy-mm-dd", "dd/mm/yyyy"]).collect()
|
||||
};
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tl!("Format:"));
|
||||
egui::ComboBox::from_id_salt("fmt-date").selected_text(p.as_str()).width(170.0).show_ui(ui, |ui| {
|
||||
for pr in presets {
|
||||
ui.selectable_value(p, pr.to_string(), pr);
|
||||
}
|
||||
});
|
||||
let custom = ui.label(tl!("Custom:"));
|
||||
ui.add(crate::widgets::line(p).desired_width(120.0)).labelled_by(custom.id);
|
||||
});
|
||||
}
|
||||
Format::Special(n) => {
|
||||
let mut idx = *n as usize;
|
||||
egui::ComboBox::from_id_salt("fmt-special").selected_text(tl!(SPECIALS[idx.min(4)])).show_ui(ui, |ui| {
|
||||
for (i, s) in SPECIALS.iter().enumerate() {
|
||||
ui.selectable_value(&mut idx, i, tl!(s));
|
||||
}
|
||||
});
|
||||
if idx == 4 {
|
||||
d.format = Format::Mask("999-999".into());
|
||||
} else {
|
||||
*n = idx as u8;
|
||||
}
|
||||
}
|
||||
Format::Mask(m) => {
|
||||
let mut idx = 4usize;
|
||||
egui::ComboBox::from_id_salt("fmt-special").selected_text(tl!(SPECIALS[4])).show_ui(ui, |ui| {
|
||||
for (i, s) in SPECIALS.iter().enumerate() {
|
||||
ui.selectable_value(&mut idx, i, tl!(s));
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
let mask = ui.label(tl!("Mask:"));
|
||||
ui.add(crate::widgets::line(m).desired_width(160.0)).labelled_by(mask.id);
|
||||
});
|
||||
ui.label(egui::RichText::new(tl!("9 digit, A letter, O letter or digit, X any character")).small().color(t.text_faint));
|
||||
if idx < 4 {
|
||||
d.format = Format::Special(idx as u8);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !sample.is_empty() && d.format != Format::None {
|
||||
ui.add_space(6.0);
|
||||
ui.label(
|
||||
egui::RichText::new(crate::i18n::fmt(tl!("Example: {sample}"), &[("sample", &format_value(&d.format, sample))])).color(t.text_muted),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn validate_tab(ui: &mut egui::Ui, d: &mut FieldDraft) {
|
||||
let mut ranged = matches!(d.validate, Validate::Range { .. });
|
||||
widgets::radio_value(ui, &mut ranged, false, tl!("Field value is not validated"));
|
||||
widgets::radio_value(ui, &mut ranged, true, tl!("Field value is in range:"));
|
||||
if !ranged {
|
||||
d.validate = Validate::None;
|
||||
return;
|
||||
}
|
||||
if d.validate == Validate::None {
|
||||
d.validate = Validate::Range { min: Some(0.0), max: Some(100.0) };
|
||||
}
|
||||
if let Validate::Range { min, max } = &mut d.validate {
|
||||
ui.horizontal(|ui| {
|
||||
let mut has = min.is_some();
|
||||
ui.add(widgets::checkbox(&mut has, tl!("From:")));
|
||||
let mut v = min.unwrap_or(0.0);
|
||||
ui.add_enabled(has, egui::DragValue::new(&mut v).speed(1.0));
|
||||
*min = has.then_some(v);
|
||||
let mut has = max.is_some();
|
||||
ui.add(widgets::checkbox(&mut has, tl!("To:")));
|
||||
let mut v = max.unwrap_or(100.0);
|
||||
ui.add_enabled(has, egui::DragValue::new(&mut v).speed(1.0));
|
||||
*max = has.then_some(v);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn actions_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens) {
|
||||
use pdfcraft_engine::FieldTrigger as T;
|
||||
let editing = d.editing.is_some();
|
||||
ui.label(egui::RichText::new(if editing { tl!("Edit Action") } else { tl!("Add an Action") }).font(theme::semibold(13.0)));
|
||||
let a = &mut d.new_action;
|
||||
egui::Grid::new("field-actions-add").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
|
||||
ui.label(tl!("Select Trigger:"));
|
||||
// Editing changes the script of one trigger's action: its trigger and kind stay.
|
||||
ui.add_enabled_ui(!editing, |ui| {
|
||||
egui::ComboBox::from_id_salt("action-trigger").selected_text(tl!(a.trigger.label())).width(220.0).show_ui(ui, |ui| {
|
||||
for tr in T::ALL {
|
||||
ui.selectable_value(&mut a.trigger, tr, tl!(tr.label()));
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label(tl!("Select Action:"));
|
||||
ui.add_enabled_ui(!editing, |ui| {
|
||||
egui::ComboBox::from_id_salt("action-kind").selected_text(tl!(ACTION_KINDS[a.kind])).width(220.0).show_ui(ui, |ui| {
|
||||
for (i, k) in ACTION_KINDS.iter().enumerate() {
|
||||
ui.selectable_value(&mut a.kind, i, tl!(k));
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.end_row();
|
||||
let hint = match a.kind {
|
||||
0 => "JavaScript",
|
||||
1 => "https://…",
|
||||
2 => tl!("Fields to reset (comma-separated; empty: all)"),
|
||||
3 => tl!("Print, NextPage, PrevPage, FirstPage or LastPage"),
|
||||
4 => tl!("Page number"),
|
||||
5 | 6 => tl!("Field names (comma-separated)"),
|
||||
_ => tl!("URL to submit to"),
|
||||
};
|
||||
ui.label(if a.kind == 0 { tl!("Script:") } else { tl!("Value:") });
|
||||
if a.kind == 0 {
|
||||
ui.add(crate::widgets::block(&mut a.text).code_editor().desired_rows(3).desired_width(340.0).hint_text(hint).id_salt("action-text"));
|
||||
} else {
|
||||
ui.add(crate::widgets::line(&mut a.text).desired_width(340.0).hint_text(hint).id_salt("action-text"));
|
||||
}
|
||||
ui.end_row();
|
||||
});
|
||||
let mut error = None;
|
||||
let (mut commit, mut discard) = (false, false);
|
||||
ui.horizontal(|ui| {
|
||||
commit = ui.button(if editing { tl!("Update") } else { tl!("Add") }).clicked();
|
||||
discard = editing && ui.button(tl!("Discard")).clicked();
|
||||
});
|
||||
if commit {
|
||||
match d.new_action.action() {
|
||||
Ok(act) => {
|
||||
let tr = d.new_action.trigger;
|
||||
d.actions.retain(|(x, _)| *x != tr);
|
||||
d.actions.push((tr, act));
|
||||
d.actions.sort_by_key(|(x, _)| T::ALL.iter().position(|y| y == x));
|
||||
d.new_action.text.clear();
|
||||
d.editing = None;
|
||||
}
|
||||
Err(e) => error = Some(e),
|
||||
}
|
||||
}
|
||||
if discard {
|
||||
d.new_action = ActionDraft::default();
|
||||
d.editing = None;
|
||||
}
|
||||
if let Some(e) = error {
|
||||
ui.label(egui::RichText::new(tl!(&e)).small().color(t.text_muted));
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
ui.label(egui::RichText::new(tl!("Actions")).font(theme::semibold(13.0)));
|
||||
egui::Frame::new().fill(t.hover).corner_radius(egui::CornerRadius::same(6)).inner_margin(egui::Margin::same(8)).show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
if d.actions.is_empty() {
|
||||
ui.label(egui::RichText::new(tl!("No actions")).color(t.text_muted));
|
||||
}
|
||||
let (mut remove, mut edit) = (None, None);
|
||||
for (i, (tr, act)) in d.actions.iter().enumerate() {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new(tl!(tr.label())).strong());
|
||||
let more = d.chained.iter().find(|(x, _)| x == tr).map_or(0, |(_, n)| *n);
|
||||
if more == 0 {
|
||||
ui.label(act.describe());
|
||||
} else {
|
||||
let then = crate::i18n::trn(crate::i18n::current(), more as u64, "then {n} more action", "then {n} more actions");
|
||||
ui.label(format!("{}, {then}", act.describe()));
|
||||
}
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.push_id(i, |ui| {
|
||||
if ui.button(tl!("Delete")).clicked() {
|
||||
remove = Some(i);
|
||||
}
|
||||
if let pdfcraft_engine::FieldAction::JavaScript(js) = act
|
||||
&& ui.add_enabled(d.editing.is_none(), egui::Button::new(tl!("Edit"))).clicked()
|
||||
{
|
||||
edit = Some((*tr, js.clone()));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
if let Some((trigger, text)) = edit {
|
||||
d.new_action = ActionDraft { trigger, kind: 0, text };
|
||||
d.editing = Some(trigger);
|
||||
}
|
||||
if let Some(i) = remove {
|
||||
if d.actions.get(i).map(|(x, _)| Some(*x)) == Some(d.editing) {
|
||||
d.new_action = ActionDraft::default();
|
||||
d.editing = None;
|
||||
}
|
||||
d.actions.remove(i);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn calculate_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Tokens) {
|
||||
#[derive(PartialEq, Clone, Copy)]
|
||||
enum Mode {
|
||||
None,
|
||||
Simple,
|
||||
Notation,
|
||||
}
|
||||
let mode = match d.calculate {
|
||||
Calculate::None => Mode::None,
|
||||
Calculate::Simple { .. } => Mode::Simple,
|
||||
Calculate::Notation(_) => Mode::Notation,
|
||||
};
|
||||
let mut m = mode;
|
||||
widgets::radio_value(ui, &mut m, Mode::None, tl!("Value is not calculated"));
|
||||
widgets::radio_value(ui, &mut m, Mode::Simple, tl!("Value is the"));
|
||||
if m != mode {
|
||||
d.calculate = match m {
|
||||
Mode::None => Calculate::None,
|
||||
Mode::Simple => Calculate::Simple { op: CalcOp::Sum, fields: Vec::new() },
|
||||
Mode::Notation => Calculate::Notation(String::new()),
|
||||
};
|
||||
}
|
||||
if let Calculate::Simple { op, fields } = &mut d.calculate {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(24.0);
|
||||
egui::ComboBox::from_id_salt("calc-op").selected_text(tl!(op.label())).show_ui(ui, |ui| {
|
||||
for o in CalcOp::ALL {
|
||||
ui.selectable_value(op, o, tl!(o.label()));
|
||||
}
|
||||
});
|
||||
ui.label(tl!("of the following fields:"));
|
||||
});
|
||||
egui::ScrollArea::vertical().max_height(110.0).show(ui, |ui| {
|
||||
for name in &d.others {
|
||||
let mut on = fields.contains(name);
|
||||
if ui.add(widgets::checkbox(&mut on, name)).changed() {
|
||||
if on {
|
||||
fields.push(name.clone());
|
||||
} else {
|
||||
fields.retain(|f| f != name);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
let mut m2 = match d.calculate {
|
||||
Calculate::Notation(_) => Mode::Notation,
|
||||
_ => m,
|
||||
};
|
||||
if widgets::radio_value(ui, &mut m2, Mode::Notation, tl!("Simplified field notation:")).clicked()
|
||||
&& !matches!(d.calculate, Calculate::Notation(_))
|
||||
{
|
||||
d.calculate = Calculate::Notation(String::new());
|
||||
}
|
||||
if let Calculate::Notation(expr) = &mut d.calculate {
|
||||
ui.add(crate::widgets::block(expr).hint_text("Price * Quantity").desired_rows(2).desired_width(420.0));
|
||||
ui.label(egui::RichText::new(tl!("Field names with + - * / and parentheses; put \\ before spaces in names.")).small().color(t.text_faint));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user