mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-28 03:58:45 -06:00
* M6: Prepare a form ▸ Text field table: rows and columns of text fields as one edit
Drag an area with the Text field table tool and set it up in a movable
window: rows, columns, row and column spacing, the first row number,
column widths (draggable dividers, arrow keys once focused) and a name
pattern per column, with the table previewed on the page. Add fields adds
every cell as one undoable edit ("Add text field table").
- forms: `table` lays the table out in view space and names its cells. It
refuses the whole table for a cell under MIN_FIELD_SIDE (now one
constant) or a name with a period, used twice or already taken, and
free_base picks the first free TableK for default names.
- engine: `field_table::edit` maps the cells onto the page, /Rotate and
/UserUnit included, and checks their size again there.
- automation: `form_add_field_table`, with an end-to-end test.
- ui-egui: the window belongs to one page of one document and closes when
that stops being the one prepared; Enter commits a number instead of
adding the table; dividers between many columns no longer panic in
f64::clamp. Strings in all 22 catalogs, and the Spanish engine-wording
tests cover the table's mistakes.
- parity: form.multiple-copies is partial; copying an existing field into
a grid is still missing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M6: text field table: refuse corners past the page's number range, run it by its command
Review follow-up on #3479.
- engine: a corner finite as an f64 but past f32's range came back from
the page's f32 mapping infinite. With one such corner a side was
infinite rather than NaN, so the cell passed and add_field refused it
only once the edit was applied ("the field is too small"). The edit now
refuses any corner that isn't finite on the page with the table's own
reason, before anything is added.
- automation: form.add.text_table runs form_add_field_table through
command_run, as comment.stamp runs stamp_custom.
- ui-egui: doc comments for the window's three remaining helpers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M6: text field table: close on document edits, lay out once per change, keep Escape for fields
Review follow-up on #3479 (benandez).
- The window belongs to the document as it was when it opened: it closes
when the edit generation moves (a page rotated, moved or deleted, an edit
undone), so a table set up for one page never lands on another or off it.
- The table is laid out and checked once per change, not several times a
frame: the window and the page preview share one `Laid` (cells and the
edit), and the preview draws only the cells in view, naming only those
with room for a name.
- Escape in a field of the window leaves the field and keeps the setup; a
second Escape closes the window.
- A table placed with a click is moved in from the page's edges, so all of
it is on the page.
- `free_base` takes one pass over the names; `{seq}` is documented as
counting from the first row number; `form_add_field_table` takes its
names from the batch instead of laying the table out again; the
unreachable `Dialog::FieldTable` arm says why it is there.
Each new test fails with its fix reverted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: text field table: drawn with its panel closed, the window opens Prepare a form instead of closing
With the tool picked and the Prepare a form panel closed, the form is
still prepared and a table can be drawn. Drawing sends the tool back to
Select, though, so nothing was prepared on the next frame, and the table
window, which lasts only while the form is prepared, closed itself at
once: the table couldn't be set up. Opening the window now opens the
Prepare a form panel it belongs to. Closing the panel while the window
is open still closes the window.
Found by CodeRabbit, which saw the preview stop drawing; the window
closing first was the larger effect. prepare.rs
a_table_drawn_with_the_panel_closed_opens_it_again fails without the fix
("the window stays open": (None, false)).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* Drop the second generation binding the main merge left in document_area
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
612 lines
24 KiB
Rust
612 lines
24 KiB
Rust
//! The tool catalogue: Acrobat's "All tools" information architecture, as data.
|
|
//!
|
|
//! Every UI (the egui shell today, anything tomorrow) builds its tool panel, palette and menus
|
|
//! from this table; nothing is hard-coded in the toolkit. Each item names the command it will
|
|
//! dispatch and the milestone (plan/execution-plan.md) in which it ships, so the UI can be honest
|
|
//! about what works today.
|
|
//!
|
|
//! Source of the inventory: plan/acrobat/02-ui-ux.md §2 (observed in Acrobat Pro 26.002).
|
|
|
|
/// Where a tool or item stands.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum Availability {
|
|
/// Works in this build.
|
|
Ready,
|
|
/// Planned; ships in the named milestone.
|
|
Planned(&'static str),
|
|
/// Cloud-only in Acrobat; PdfCraft offers an optional pluggable provider instead.
|
|
Provider,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct ToolItem {
|
|
pub label: &'static str,
|
|
pub icon: &'static str,
|
|
pub command: &'static str,
|
|
pub availability: Availability,
|
|
/// Further commands of the same tool, shown as a drop-down beside the entry: Acrobat's
|
|
/// Watermark control offers Add, Update and Remove. The first is `command` itself. A UI
|
|
/// enables each one by its own command's needs, so Update and Remove stay greyed out until
|
|
/// the document has that kind of mark. Empty for a plain entry.
|
|
pub actions: &'static [&'static str],
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct ToolSection {
|
|
pub title: &'static str,
|
|
pub items: &'static [ToolItem],
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct ToolGroup {
|
|
pub id: &'static str,
|
|
pub label: &'static str,
|
|
pub icon: &'static str,
|
|
/// Icon tint, as Acrobat colour-codes its tool families.
|
|
pub hue: [u8; 3],
|
|
pub badge: Option<&'static str>,
|
|
pub availability: Availability,
|
|
pub sections: &'static [ToolSection],
|
|
}
|
|
|
|
const fn item(label: &'static str, icon: &'static str, command: &'static str, availability: Availability) -> ToolItem {
|
|
ToolItem { label, icon, command, availability, actions: &[] }
|
|
}
|
|
|
|
/// An entry that opens a menu of `actions` (the first one is its `command`).
|
|
const fn menu_item(label: &'static str, icon: &'static str, actions: &'static [&'static str]) -> ToolItem {
|
|
let command = match actions {
|
|
[first, ..] => *first,
|
|
[] => "",
|
|
};
|
|
ToolItem { label, icon, command, availability: Ready, actions }
|
|
}
|
|
|
|
use Availability::{Planned, Provider, Ready};
|
|
|
|
const RED: [u8; 3] = [0xE0, 0x3E, 0x3E];
|
|
const PURPLE: [u8; 3] = [0x8E, 0x4E, 0xE6];
|
|
const BLUE: [u8; 3] = [0x3A, 0x6F, 0xE8];
|
|
const GREEN: [u8; 3] = [0x2D, 0x9D, 0x5B];
|
|
const ORANGE: [u8; 3] = [0xE8, 0x8A, 0x1A];
|
|
const TEAL: [u8; 3] = [0x14, 0x9C, 0xA8];
|
|
const PINK: [u8; 3] = [0xD6, 0x3B, 0x8F];
|
|
|
|
pub static TOOL_GROUPS: &[ToolGroup] = &[
|
|
ToolGroup {
|
|
id: "export",
|
|
label: "Export a PDF",
|
|
icon: "file-output",
|
|
hue: BLUE,
|
|
badge: None,
|
|
availability: Ready,
|
|
sections: &[ToolSection {
|
|
title: "Export to",
|
|
items: &[
|
|
item("Microsoft Word (.docx)", "file-text", "export.docx", Ready),
|
|
item("Spreadsheet (.xlsx)", "grid-3x3", "export.xlsx", Planned("M10")),
|
|
item("Presentation (.pptx)", "presentation", "export.pptx", Ready),
|
|
item("Image (PNG)", "image", "export.image", Ready),
|
|
item("Export all images", "image", "export.all_images", Ready),
|
|
item("HTML web page", "file-symlink", "export.html", Ready),
|
|
item("Rich Text Format (.rtf)", "file-text", "export.rtf", Ready),
|
|
item("Text (plain)", "type", "export.text", Ready),
|
|
item("PostScript / EPS", "file-down", "export.ps", Planned("M10")),
|
|
],
|
|
}],
|
|
},
|
|
ToolGroup {
|
|
id: "fill_sign",
|
|
label: "Fill & Sign",
|
|
icon: "pen-line",
|
|
hue: PURPLE,
|
|
badge: None,
|
|
availability: Ready,
|
|
sections: &[ToolSection {
|
|
title: "Fill",
|
|
items: &[
|
|
item("Add text", "type", "sign.fill.text", Ready),
|
|
item("Checkmark", "check", "sign.fill.check", Ready),
|
|
item("Cross", "x", "sign.fill.cross", Ready),
|
|
item("Dot", "circle-dot", "sign.fill.dot", Ready),
|
|
item("Line", "minus", "sign.fill.line", Ready),
|
|
item("Date", "clock-3", "sign.fill.date", Ready),
|
|
item("Signature", "signature", "sign.fill.signature", Ready),
|
|
item("Initials", "signature", "sign.fill.initials", Ready),
|
|
item("Change signature", "signature", "sign.fill.signature.change", Ready),
|
|
item("Change initials", "signature", "sign.fill.initials.change", Ready),
|
|
],
|
|
}],
|
|
},
|
|
ToolGroup {
|
|
id: "edit",
|
|
label: "Edit a PDF",
|
|
icon: "file-pen-line",
|
|
hue: PINK,
|
|
badge: None,
|
|
availability: Planned("M7"),
|
|
sections: &[
|
|
ToolSection {
|
|
title: "Modify page",
|
|
items: &[
|
|
item("Edit text & images", "text-cursor-input", "edit.edit_text", Ready),
|
|
item("Rotate pages", "rotate-cw", "page.rotate", Ready),
|
|
item("Insert pages", "file-plus-2", "page.insert", Ready),
|
|
item("Delete pages", "trash-2", "page.delete", Ready),
|
|
item("Extract pages", "file-output", "page.extract", Ready),
|
|
item("Organize pages", "layout-grid", "page.organize", Ready),
|
|
],
|
|
},
|
|
ToolSection {
|
|
title: "Add content",
|
|
items: &[
|
|
item("Text", "type", "edit.text", Ready),
|
|
item("Image", "image-plus", "edit.image", Ready),
|
|
menu_item("Header and footer", "heading", &["edit.header_footer", "edit.header_footer.update", "edit.header_footer.remove"]),
|
|
menu_item("Watermark", "stamp", &["edit.watermark", "edit.watermark.update", "edit.watermark.remove"]),
|
|
item("Link", "link-2", "edit.link", Ready),
|
|
item("Bates numbering", "hash", "edit.bates", Ready),
|
|
menu_item("Background", "palette", &["edit.background", "edit.background.update", "edit.background.remove"]),
|
|
item("Attach file", "paperclip", "edit.attach", Planned("M12")),
|
|
],
|
|
},
|
|
],
|
|
},
|
|
ToolGroup {
|
|
id: "create",
|
|
label: "Create a PDF",
|
|
icon: "file-plus-2",
|
|
hue: RED,
|
|
badge: None,
|
|
availability: Ready,
|
|
sections: &[ToolSection {
|
|
title: "Create from",
|
|
items: &[
|
|
item("Single file", "file-input", "create.file", Ready),
|
|
item("Multiple files", "files", "create.multiple", Ready),
|
|
item("Separate PDFs", "files", "create.multiple_separate", Ready),
|
|
item("Images", "image", "create.images", Ready),
|
|
item("Clipboard", "copy-plus", "create.clipboard", Ready),
|
|
item("Blank page", "file-plus-2", "create.blank", Ready),
|
|
],
|
|
}],
|
|
},
|
|
ToolGroup {
|
|
id: "combine",
|
|
label: "Combine files",
|
|
icon: "files",
|
|
hue: BLUE,
|
|
badge: None,
|
|
availability: Ready,
|
|
sections: &[ToolSection { title: "Combine", items: &[item("Add files to combine", "files", "page.combine", Ready)] }],
|
|
},
|
|
ToolGroup {
|
|
id: "organize",
|
|
label: "Organize pages",
|
|
icon: "layout-grid",
|
|
hue: GREEN,
|
|
badge: None,
|
|
availability: Ready,
|
|
sections: &[
|
|
ToolSection {
|
|
title: "Page options",
|
|
items: &[
|
|
item("Page grid", "layout-grid", "page.organize", Ready),
|
|
item("Rotate", "rotate-cw", "page.rotate", Ready),
|
|
item("Delete", "trash-2", "page.delete", Ready),
|
|
item("Extract", "file-output", "page.extract", Ready),
|
|
item("Insert", "file-plus-2", "page.insert", Ready),
|
|
item("Replace", "replace", "page.replace", Ready),
|
|
item("Split", "scissors", "page.split", Ready),
|
|
],
|
|
},
|
|
ToolSection {
|
|
title: "More",
|
|
items: &[
|
|
item("Set page boxes", "square-dashed-mouse-pointer", "page.boxes", Ready),
|
|
item("Crop pages", "crop", "page.crop", Ready),
|
|
item("Duplicate pages", "copy-plus", "page.duplicate", Ready),
|
|
item("Reverse pages", "arrow-up-down", "page.reverse", Ready),
|
|
item("Page labels", "tag", "page.number", Ready),
|
|
item("Page transitions", "presentation", "page.transitions", Planned("M4")),
|
|
],
|
|
},
|
|
],
|
|
},
|
|
ToolGroup {
|
|
id: "comment",
|
|
label: "Add comments",
|
|
icon: "message-square-text",
|
|
hue: ORANGE,
|
|
badge: None,
|
|
availability: Ready,
|
|
sections: &[
|
|
ToolSection {
|
|
title: "Markup",
|
|
items: &[
|
|
item("Sticky note", "sticky-note", "comment.note", Ready),
|
|
item("Highlight text", "highlighter", "comment.highlight", Ready),
|
|
item("Underline", "underline", "comment.underline", Ready),
|
|
item("Strikethrough", "strikethrough", "comment.strikeout", Ready),
|
|
item("Squiggly underline", "spline", "comment.squiggly", Ready),
|
|
item("Text box", "type", "comment.freetext", Ready),
|
|
],
|
|
},
|
|
ToolSection {
|
|
title: "Drawing",
|
|
items: &[
|
|
item("Draw freehand", "pencil", "comment.ink", Ready),
|
|
item("Rectangle", "square", "comment.square", Ready),
|
|
item("Oval", "circle", "comment.circle", Ready),
|
|
item("Line", "minus", "comment.line", Ready),
|
|
item("Arrow", "move-right", "comment.arrow", Ready),
|
|
item("Cloud", "cloud", "comment.cloud", Planned("M5")),
|
|
item("Stamp", "stamp", "comment.stamp", Planned("M5")),
|
|
],
|
|
},
|
|
ToolSection {
|
|
title: "Review",
|
|
items: &[
|
|
item("Comment list", "message-square-text", "comment.list", Ready),
|
|
item("Flatten comments", "layers", "comment.flatten", Ready),
|
|
],
|
|
},
|
|
],
|
|
},
|
|
ToolGroup {
|
|
id: "scan",
|
|
label: "Scan & OCR",
|
|
icon: "scan-text",
|
|
hue: GREEN,
|
|
badge: None,
|
|
availability: Ready,
|
|
sections: &[ToolSection {
|
|
title: "Recognize text",
|
|
items: &[
|
|
item("In this file", "scan-text", "ocr.recognize", Ready),
|
|
item("In multiple files", "files", "ocr.recognize_batch", Ready),
|
|
item("Enhance scanned file", "sparkles", "ocr.enhance", Planned("M10")),
|
|
item("Enhance camera image", "camera", "ocr.enhance_camera", Ready),
|
|
item("Correct recognized text", "text-select", "ocr.correct", Planned("M10")),
|
|
],
|
|
}],
|
|
},
|
|
ToolGroup {
|
|
id: "protect",
|
|
label: "Protect a PDF",
|
|
icon: "shield-check",
|
|
hue: BLUE,
|
|
badge: None,
|
|
availability: Planned("M8"),
|
|
sections: &[
|
|
ToolSection {
|
|
title: "Protect",
|
|
items: &[
|
|
item("Protect with password", "lock", "protect.password", Ready),
|
|
item("Remove hidden information", "eye-off", "protect.remove_hidden", Ready),
|
|
],
|
|
},
|
|
ToolSection {
|
|
title: "Advanced options",
|
|
items: &[
|
|
item("Encrypt with certificate", "file-lock-2", "protect.certificate", Planned("M8")),
|
|
item("Security properties", "shield-check", "protect.properties", Ready),
|
|
item("Remove security", "lock-open", "protect.remove", Ready),
|
|
],
|
|
},
|
|
],
|
|
},
|
|
ToolGroup {
|
|
id: "redact",
|
|
label: "Redact a PDF",
|
|
icon: "rectangle-horizontal",
|
|
hue: RED,
|
|
badge: None,
|
|
availability: Ready,
|
|
sections: &[ToolSection {
|
|
title: "Redact",
|
|
items: &[
|
|
item("Redact text and images", "rectangle-horizontal", "redact.mark", Ready),
|
|
item("Redact pages", "file-x", "redact.pages", Ready),
|
|
item("Find text and redact", "file-search", "redact.search", Ready),
|
|
item("Set properties", "settings-2", "redact.properties", Ready),
|
|
item("Sanitize document", "sparkles", "redact.sanitize", Ready),
|
|
],
|
|
}],
|
|
},
|
|
ToolGroup {
|
|
id: "compress",
|
|
label: "Compress a PDF",
|
|
icon: "file-down",
|
|
hue: RED,
|
|
badge: None,
|
|
availability: Ready,
|
|
sections: &[ToolSection {
|
|
title: "Optimize",
|
|
items: &[
|
|
item("Reduce file size", "file-down", "optimize.reduce", Ready),
|
|
item("Advanced optimization", "settings-2", "optimize.advanced", Ready),
|
|
item("Audit space usage", "gauge", "optimize.audit", Planned("M11")),
|
|
],
|
|
}],
|
|
},
|
|
ToolGroup {
|
|
id: "form",
|
|
label: "Prepare a form",
|
|
icon: "text-cursor-input",
|
|
hue: PURPLE,
|
|
badge: None,
|
|
availability: Ready,
|
|
sections: &[
|
|
ToolSection {
|
|
title: "Fields",
|
|
items: &[
|
|
item("Field list", "list", "form.fields", Ready),
|
|
item("Detect form fields", "scan", "form.detect", Ready),
|
|
item("Clear form", "eraser", "form.clear", Ready),
|
|
item("Field properties", "settings-2", "form.field.properties", Ready),
|
|
item("Tab order by rows", "rows-3", "form.tab_order.row", Ready),
|
|
item("Tab order by columns", "columns-3", "form.tab_order.column", Ready),
|
|
item("Calculation order", "list-ordered", "form.calculation_order", Ready),
|
|
item("Flatten form fields", "layers", "form.flatten", Ready),
|
|
item("Import data", "file-input", "form.import_data", Ready),
|
|
item("Export data", "file-output", "form.export_data", Ready),
|
|
],
|
|
},
|
|
ToolSection {
|
|
title: "Add form components",
|
|
items: &[
|
|
item("Text field", "text-cursor-input", "form.add.text", Ready),
|
|
item("Text field table", "grid-3x3", "form.add.text_table", Ready),
|
|
item("Checkbox", "check-circle-2", "form.add.checkbox", Ready),
|
|
item("Radio button", "circle", "form.add.radio", Ready),
|
|
item("Drop-down list", "chevron-down", "form.add.combo", Ready),
|
|
item("List box", "list", "form.add.list", Ready),
|
|
item("Button", "square", "form.add.button", Ready),
|
|
item("Image field", "image", "form.add.image", Ready),
|
|
item("Date field", "clock-3", "form.add.date", Ready),
|
|
item("Digital signature", "signature", "form.add.signature", Ready),
|
|
],
|
|
},
|
|
],
|
|
},
|
|
ToolGroup {
|
|
id: "stamp",
|
|
label: "Add a stamp",
|
|
icon: "stamp",
|
|
hue: PURPLE,
|
|
badge: None,
|
|
availability: Ready,
|
|
sections: &[ToolSection { title: "Stamps", items: &[item("Stamp palette", "stamp", "comment.stamp", Ready)] }],
|
|
},
|
|
ToolGroup {
|
|
id: "certificate",
|
|
label: "Use a certificate",
|
|
icon: "badge-check",
|
|
hue: TEAL,
|
|
badge: None,
|
|
availability: Ready,
|
|
sections: &[ToolSection {
|
|
title: "Certificates",
|
|
items: &[
|
|
item("Digitally sign", "signature", "sign.digital", Ready),
|
|
item("Timestamp", "clock-3", "sign.timestamp", Planned("M9")),
|
|
item("Validate all signatures", "badge-check", "sign.validate", Ready),
|
|
item("Certify (visible signature)", "badge-check", "sign.certify", Ready),
|
|
item("Certify (invisible signature)", "badge-check", "sign.certify_invisible", Ready),
|
|
],
|
|
}],
|
|
},
|
|
ToolGroup {
|
|
id: "print_production",
|
|
label: "Use print production",
|
|
icon: "printer",
|
|
hue: PURPLE,
|
|
badge: None,
|
|
availability: Planned("M11"),
|
|
sections: &[ToolSection {
|
|
title: "Print production",
|
|
items: &[
|
|
item("Output preview", "layers", "prepress.output_preview", Planned("M11")),
|
|
item("Preflight", "file-check", "preflight.run", Planned("M11")),
|
|
item("Convert colors", "palette", "prepress.convert_colors", Planned("M11")),
|
|
item("Set page boxes", "square-dashed-mouse-pointer", "page.boxes", Ready),
|
|
item("Add printer marks", "ruler", "prepress.marks", Planned("M11")),
|
|
item("Fix hairlines", "minus", "prepress.hairlines", Planned("M11")),
|
|
],
|
|
}],
|
|
},
|
|
ToolGroup {
|
|
id: "measure",
|
|
label: "Measure objects",
|
|
icon: "ruler",
|
|
hue: PINK,
|
|
badge: None,
|
|
availability: Ready,
|
|
sections: &[ToolSection {
|
|
title: "Measure",
|
|
items: &[
|
|
item("Distance", "ruler", "measure.distance", Ready),
|
|
item("Perimeter", "ruler", "measure.perimeter", Ready),
|
|
item("Area", "ruler", "measure.area", Ready),
|
|
item("Drawing scale", "ruler", "measure.scale", Ready),
|
|
item("Measurement information", "ruler", "measure.info", Ready),
|
|
item("Snapping", "ruler", "measure.snap", Ready),
|
|
item("Export measurements", "file-output", "measure.export", Ready),
|
|
item("Geospatial location", "compass", "measure.geo", Planned("M12")),
|
|
],
|
|
}],
|
|
},
|
|
ToolGroup {
|
|
id: "compare",
|
|
label: "Compare files",
|
|
icon: "git-compare",
|
|
hue: PINK,
|
|
badge: None,
|
|
availability: Ready,
|
|
sections: &[ToolSection { title: "Compare", items: &[item("Select files to compare", "git-compare", "doc.compare", Ready)] }],
|
|
},
|
|
ToolGroup {
|
|
id: "actions",
|
|
label: "Use guided actions",
|
|
icon: "list-checks",
|
|
hue: PURPLE,
|
|
badge: None,
|
|
availability: Ready,
|
|
sections: &[ToolSection {
|
|
title: "Actions list",
|
|
items: &[
|
|
item("Action Wizard", "list-checks", "actions.wizard", Ready),
|
|
item("Make accessible", "accessibility", "actions.make_accessible", Planned("M13")),
|
|
item("Prepare for distribution", "send", "actions.distribution", Ready),
|
|
item("Optimize scanned documents", "scan-text", "actions.optimize_scans", Ready),
|
|
item("Archive documents", "file-check", "actions.archive", Planned("M13")),
|
|
],
|
|
}],
|
|
},
|
|
ToolGroup {
|
|
id: "accessibility",
|
|
label: "Prepare for accessibility",
|
|
icon: "accessibility",
|
|
hue: PURPLE,
|
|
badge: None,
|
|
availability: Ready,
|
|
sections: &[ToolSection {
|
|
title: "Accessibility",
|
|
items: &[
|
|
item("Automatically tag PDF", "tag", "a11y.autotag", Ready),
|
|
item("Tag form fields", "tag", "a11y.autotag_fields", Ready),
|
|
item("Change reading options", "book-open", "a11y.reading_options", Ready),
|
|
item("Check for accessibility", "accessibility", "a11y.check", Ready),
|
|
item("Find untagged content", "search", "a11y.find_untagged", Ready),
|
|
item("Open accessibility report", "file-text", "a11y.report", Ready),
|
|
item("Add alternate text", "image", "a11y.alt_text", Ready),
|
|
item("Tags panel", "tag", "a11y.tags", Ready),
|
|
item("Fix reading order", "list-ordered", "a11y.reading_order", Planned("M12")),
|
|
],
|
|
}],
|
|
},
|
|
ToolGroup {
|
|
id: "standards",
|
|
label: "Apply PDF standards",
|
|
icon: "file-check",
|
|
hue: RED,
|
|
badge: None,
|
|
availability: Ready,
|
|
sections: &[ToolSection {
|
|
title: "Standards",
|
|
items: &[
|
|
item("Save as PDF/A", "file-check", "standards.pdfa", Ready),
|
|
item("Save as PDF/X", "file-check", "standards.pdfx", Planned("M11")),
|
|
item("Save as PDF/UA", "accessibility", "standards.pdfua", Planned("M11")),
|
|
item("Preflight", "file-check", "preflight.run", Planned("M11")),
|
|
],
|
|
}],
|
|
},
|
|
ToolGroup {
|
|
id: "search_index",
|
|
label: "Add search index",
|
|
icon: "file-search",
|
|
hue: GREEN,
|
|
badge: None,
|
|
availability: Planned("M12"),
|
|
sections: &[ToolSection { title: "Index", items: &[item("Manage embedded index", "file-search", "search.index", Planned("M12"))] }],
|
|
},
|
|
ToolGroup {
|
|
id: "javascript",
|
|
label: "Use JavaScript",
|
|
icon: "braces",
|
|
hue: BLUE,
|
|
badge: None,
|
|
availability: Planned("M6"),
|
|
sections: &[ToolSection {
|
|
title: "JavaScript",
|
|
items: &[
|
|
item("Console", "braces", "js.console", Planned("M6")),
|
|
item("Document JavaScripts", "file-text", "js.document", Planned("M6")),
|
|
item("Document actions", "list-checks", "js.actions", Planned("M6")),
|
|
],
|
|
}],
|
|
},
|
|
ToolGroup {
|
|
id: "ai",
|
|
label: "AI assistant",
|
|
icon: "sparkles",
|
|
hue: PINK,
|
|
badge: Some("Optional"),
|
|
availability: Provider,
|
|
sections: &[ToolSection {
|
|
title: "Bring your own model (off by default)",
|
|
items: &[
|
|
item("Summarize", "sparkles", "ai.summary", Provider),
|
|
item("Ask about this document", "message-circle-reply", "ai.ask", Provider),
|
|
item("Translate", "languages", "ai.translate", Provider),
|
|
],
|
|
}],
|
|
},
|
|
];
|
|
|
|
pub fn group(id: &str) -> Option<&'static ToolGroup> {
|
|
TOOL_GROUPS.iter().find(|g| g.id == id)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::collections::HashSet;
|
|
|
|
#[test]
|
|
fn ids_are_unique_and_every_group_has_items() {
|
|
let mut seen = HashSet::new();
|
|
for g in TOOL_GROUPS {
|
|
assert!(seen.insert(g.id), "duplicate group id {}", g.id);
|
|
assert!(g.sections.iter().any(|s| !s.items.is_empty()), "{} has no items", g.id);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn commands_are_namespaced() {
|
|
for g in TOOL_GROUPS {
|
|
for s in g.sections {
|
|
for i in s.items {
|
|
assert!(i.command.contains('.'), "{} lacks a namespace", i.command);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn menu_entries_list_registered_commands_starting_with_their_own() {
|
|
let mut menus = 0;
|
|
for g in TOOL_GROUPS {
|
|
for s in g.sections {
|
|
for i in s.items {
|
|
if i.actions.is_empty() {
|
|
continue;
|
|
}
|
|
menus += 1;
|
|
assert_eq!(i.actions[0], i.command, "{}: the first action is the entry's command", i.label);
|
|
for id in i.actions {
|
|
assert!(crate::commands::command(id).is_some(), "{id} is not a registered command");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Watermark, Header and footer, Background: Add / Update / Remove each (Acrobat's menu).
|
|
assert!(menus >= 3);
|
|
}
|
|
|
|
#[test]
|
|
fn every_update_and_remove_command_is_reachable_from_a_tool_entry() {
|
|
let reachable: HashSet<&str> =
|
|
TOOL_GROUPS.iter().flat_map(|g| g.sections.iter().flat_map(|s| s.items.iter())).flat_map(|i| i.actions.iter().copied()).collect();
|
|
for id in crate::commands::COMMANDS
|
|
.iter()
|
|
.map(|c| c.id)
|
|
.filter(|id| id.starts_with("edit.") && (id.ends_with(".update") || id.ends_with(".remove")))
|
|
{
|
|
assert!(reachable.contains(id), "{id} is in no tool menu");
|
|
}
|
|
}
|
|
}
|