mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-27 03:27:21 -06:00
* i18n: Czech interface language
Adds Czech (Čeština, code `cs`) next to English and Japanese:
Preferences ▸ Interface language, `ui.set language cs`, `ui.state`
and persisted preferences all accept it.
- i18n: `Language::Cs`; `tr()` picks the language's table with a match
(English has none), panic-free. The CZECH table (83 entries) covers
every label passed to `.tr("…")` (main menu, View zoom/theme/side-panel
entries, Preferences dialog), every registered File/Edit/Pages/View/Help
command shown by `registry_menu`, and every key of the Japanese table.
Written clean-room from the English meaning, not from any Acrobat
localisation.
- Tests: Czech covers every registered menu title and label, every
`.tr("…")` literal found by scanning the crate's sources, and the
Japanese keys; nothing is left identical except an explicit KEEP_AS_IS
list ("OK"); ellipses match the source; keys are unique per language;
`cs` round-trips through set_option, ui.set/ui.state and persistence.
- theme: the font setup is split into `font_definitions()`, and a test
checks with skrifa (dev-dependency, already in the tree via egui) that
Inter Regular/Medium/SemiBold and JetBrains Mono draw all Czech letters
and every Czech translation, so nothing falls back to Shippori Mincho.
- The `language` error lists `cs`; docs/localization.md, the parity note
for misc.prefs-language and the ROADMAP log mention Czech.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* M14: translation catalogs, same system as PhotoCraft
Move the interface translations to PhotoCraft's i18n system: one TSV
catalog per language (crates/ui-egui/src/i18n/ja.tsv), the tl! macro,
entries keyed by command id, context or plural forms, and catalog tests
(format, duplicates, placeholders, command ids).
The 49 existing Japanese strings move over unchanged, plus "Auto". The
language preference now defaults to "auto" and follows the system
language (English when it has no catalog); "en" and "ja" still persist
and unknown values are rejected.
Update docs/localization.md, ROADMAP.md and the misc.prefs-language
parity entry.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* M14: complete Japanese dialogs, panels, shortcuts and diagnostics
Expand Japanese from 49 to 1830 interface entries, expose language preferences in the Edit menu, preserve opaque template and history values, and localize known application diagnostics only at the UI boundary. Add control, persistence, template and desktop/web glyph regressions.
Co-authored-by: Codex <noreply@openai.com>
* i18n: add Brazilian Portuguese (pt-br) UI translation
Adds Language::PtBr (code pt-br, endonym Português (Brasil)) with serde rename so it persists as "pt-br", registers it in Language::ALL (Preferences ▸ Interface language and ui.set/ui.state) and translates the same 49 menu/command keys as the Japanese table. Extends the i18n tests (table parity, codes, persistence) and updates docs/localization.md.
* M14: strict catalog validation, one-pass fmt, translation assets
Fold in the infrastructure parts of #175:
- `fmt` fills placeholders in one pass, so inserted values (a file name
containing `{n}`) are never substituted again.
- Catalog validation rejects unknown escapes, unknown `@` contexts,
placeholder, ellipsis and plural-form mismatches and duplicates. The
bundled catalogs must have no errors (tests); at run time a bad line is
still only skipped and logged, so the rest of the catalog loads.
- `.tsv` files count as assets in `cargo xtask assets`, with a
`translation` kind that may never come from Adobe; ja.tsv gets its
ATTRIBUTION.toml entry.
- Control-channel test: switching the language keeps documents, their
dirty state and command ids.
Also make the drawing language thread-local, as in PhotoCraft, so
parallel kittest harnesses can't change each other's language.
Co-Authored-By: 才倾梦华V <185041488+CQMHV@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M14: Brazilian Portuguese as a TSV catalog
Move #165's Portuguese table into crates/ui-egui/src/i18n/pt-br.tsv
(same 49 strings, plus Auto) and register it in LANGUAGES with
PhotoCraft's Portuguese plural rule (0 and 1 singular). Any Portuguese
locale (pt_BR, pt_PT, pt) follows the Brazilian catalog, as in
PhotoCraft. The catalog is an attributed asset (ATTRIBUTION.toml), and
the docs and parity notes list the new code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M14: Czech as a TSV catalog
Move #68's Czech table into crates/ui-egui/src/i18n/cs.tsv (PrintCraft
labels renamed PdfCraft, plus Auto) and register it with PhotoCraft's
Czech plural rule (one / few / other). #68's tests become catalog
tests: Czech covers every registered menu title and menu command label,
keeps ellipses, uses no three-dot ellipsis or stray whitespace, and only
leaves "OK" untranslated; the code persists and `cz` is rejected. The
source-scanning `.tr("…")` test is dropped: strings now go through tl!
and the catalog tests check consistency.
The font-coverage test moves to tests/fonts.rs and covers both Latin
catalogs (cs, pt-br) against Inter and JetBrains Mono. It keeps skrifa
as a dev-dependency (the version pdfcraft-fonts already uses): egui's
has_glyphs can't answer it, since a family with only the primary face
also uses that face as its replacement face.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M14: route every UI string through the catalog; Japanese as ja.tsv
Port #141's wiring to the catalog API from land/106:
- `crate::i18n::tr(ui, s)` becomes `tl!(s)`, `tr_fmt`/`tr_template`
become `i18n::fmt(tl!(..), ..)`, and `Language::get`/`store` (the
egui temp-data copy of the language) are gone: the drawing language
is the per-thread `i18n::current()`, set from the preference at the
start of each frame. Worker threads (export, OCR, batch actions) take
the UI's language with them for their summaries.
- ja.rs (1,830 pairs) becomes ja.tsv, merged with the existing entries
and renamed PrintCraft -> PdfCraft. The 29 diagnostic templates of
errors.rs are dropped.
- `Language::diagnostic` and errors.rs are dropped: matching engine and
OS messages against English templates breaks silently whenever the
wording changes. Error details are shown as they are, inside the
translated frame ("{label} failed: {e}").
- History labels (`command_label`/`action_label`) move into i18n and keep
captured file and field names; registry menus prefer `@id` entries.
- Preferences: Auto plus every registered language, relabelled at once.
- Tests: Japanese covers every command and All tools entry; Japanese
labels have glyphs with the desktop faces and with the web subset
(with craft-fonts); history labels; #141's control tests, with the
error-detail expectation updated.
Co-Authored-By: shuiyue-cmyk <291597411+shuiyue-cmyk@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M14: Preferences in the Edit menu, Cmd/Ctrl-comma
Split out of #141: register app.preferences in the Edit menu with the
platform's usual shortcut (Command-comma on macOS, Ctrl-comma
elsewhere), so the interface language can be found and changed with no
document open. Shortcut keys may now be punctuation (egui's key name
","). #141's control test drives the menu item, the language selector
and the shortcut in Japanese and English.
Co-Authored-By: sakamoto824 <sakamoto824@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Petr Leo Compel <petrleocompel@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Simon Leblanc <contact@leblanc-simon.eu>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: RicSchonfelder <ricschonfelder@icloud.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: 才倾梦华V <185041488+CQMHV@users.noreply.github.com>
Co-authored-by: shuiyue-cmyk <291597411+shuiyue-cmyk@users.noreply.github.com>
404 lines
22 KiB
Rust
404 lines
22 KiB
Rust
//! The command registry: every user-facing action, by id (architecture §4, "everything is a
|
|
//! command").
|
|
//!
|
|
//! Menus, keyboard shortcuts, the ⌘K palette and automation (CLI `run`, the control channel and
|
|
//! MCP later) all go through this table. A frontend implements *what* each id does. The registry
|
|
//! says what it is called, where it appears, which key runs it, and when it is enabled, so that
|
|
//! every surface agrees.
|
|
//!
|
|
//! View-local keys (zoom, page navigation, find-next) stay with the document view. They act on
|
|
//! view state, not on the document.
|
|
|
|
use crate::{DocId, Session};
|
|
|
|
/// When a command can run.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum Needs {
|
|
/// Always.
|
|
Nothing,
|
|
/// A document is open.
|
|
Document,
|
|
/// The open document's security allows page changes (insert, delete, rotate, extract…).
|
|
Assembly,
|
|
/// The open document's security allows content and metadata changes.
|
|
Modification,
|
|
/// The open document's security allows adding and changing comments.
|
|
Annotate,
|
|
/// The document has form fields and its security allows filling them in.
|
|
FillForms,
|
|
/// The document has comments and allows changes.
|
|
HasComments,
|
|
/// The document has form fields and allows changes.
|
|
HasFields,
|
|
/// The document has redaction marks and allows changes.
|
|
HasRedactions,
|
|
/// The document has page marks of this kind and allows changes.
|
|
Marks(crate::MarkKind),
|
|
/// The document's security may be changed (owner, or nothing restricted).
|
|
Security,
|
|
/// The document is protected and its security may be removed.
|
|
ProtectedSecurity,
|
|
/// There is something to undo.
|
|
Undo,
|
|
/// There is something to redo.
|
|
Redo,
|
|
}
|
|
|
|
/// A keyboard shortcut. `command` is ⌘ on macOS and Ctrl elsewhere.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
pub struct Shortcut {
|
|
pub command: bool,
|
|
pub shift: bool,
|
|
/// ⌃ on macOS (in addition to ⌘); unused elsewhere.
|
|
pub mac_ctrl: bool,
|
|
/// Key name: a letter, a digit, punctuation such as `,`, or `Delete`.
|
|
pub key: &'static str,
|
|
}
|
|
|
|
impl Shortcut {
|
|
const fn cmd(key: &'static str) -> Self {
|
|
Self { command: true, shift: false, mac_ctrl: false, key }
|
|
}
|
|
|
|
const fn cmd_shift(key: &'static str) -> Self {
|
|
Self { command: true, shift: true, mac_ctrl: false, key }
|
|
}
|
|
|
|
/// How the shortcut is written in menus: `⇧⌘S` on macOS, `Ctrl+Shift+S` elsewhere.
|
|
pub fn label(&self, mac: bool) -> String {
|
|
if mac {
|
|
let mut s = String::new();
|
|
if self.mac_ctrl {
|
|
s.push('⌃');
|
|
}
|
|
if self.shift {
|
|
s.push('⇧');
|
|
}
|
|
if self.command {
|
|
s.push('⌘');
|
|
}
|
|
s.push_str(self.key);
|
|
s
|
|
} else {
|
|
let mut parts = Vec::new();
|
|
if self.command || self.mac_ctrl {
|
|
parts.push("Ctrl");
|
|
}
|
|
if self.shift {
|
|
parts.push("Shift");
|
|
}
|
|
parts.push(self.key);
|
|
parts.join("+")
|
|
}
|
|
}
|
|
|
|
/// Number of modifiers (more specific shortcuts are matched first).
|
|
pub fn modifier_count(&self) -> usize {
|
|
usize::from(self.command) + usize::from(self.shift) + usize::from(self.mac_ctrl)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct CommandSpec {
|
|
pub id: &'static str,
|
|
pub label: &'static str,
|
|
/// Top-level menu it appears in (`File`, `Edit`, `View`, `Pages`, `Help`), if any.
|
|
pub menu: Option<&'static str>,
|
|
pub shortcut: Option<Shortcut>,
|
|
pub needs: Needs,
|
|
/// The shortcut also works while a text field has focus.
|
|
pub in_text: bool,
|
|
/// Lucide icon name for palettes and toolbars.
|
|
pub icon: &'static str,
|
|
}
|
|
|
|
const fn c(
|
|
id: &'static str,
|
|
label: &'static str,
|
|
menu: Option<&'static str>,
|
|
shortcut: Option<Shortcut>,
|
|
needs: Needs,
|
|
icon: &'static str,
|
|
) -> CommandSpec {
|
|
CommandSpec { id, label, menu, shortcut, needs, in_text: true, icon }
|
|
}
|
|
|
|
/// Like `c`, but the shortcut is left to text fields while one has focus (⌘Z, ⌘A…).
|
|
const fn ct(
|
|
id: &'static str,
|
|
label: &'static str,
|
|
menu: Option<&'static str>,
|
|
shortcut: Option<Shortcut>,
|
|
needs: Needs,
|
|
icon: &'static str,
|
|
) -> CommandSpec {
|
|
CommandSpec { id, label, menu, shortcut, needs, in_text: false, icon }
|
|
}
|
|
|
|
use Needs::*;
|
|
|
|
const FILE: Option<&str> = Some("File");
|
|
const EDIT: Option<&str> = Some("Edit");
|
|
const VIEW: Option<&str> = Some("View");
|
|
const PAGES: Option<&str> = Some("Pages");
|
|
const HELP: Option<&str> = Some("Help");
|
|
|
|
/// Every command, in menu order.
|
|
pub const COMMANDS: &[CommandSpec] = &[
|
|
c("file.open", "Open…", FILE, Some(Shortcut::cmd("O")), Nothing, "folder-open"),
|
|
c("create.blank", "New blank PDF", FILE, None, Nothing, "file-plus-2"),
|
|
c("page.copy", "Copy pages", None, None, Document, "copy"),
|
|
c("page.cut", "Cut pages", None, None, Assembly, "scissors"),
|
|
c("page.paste", "Paste pages", None, None, Assembly, "clipboard-paste"),
|
|
c("create.file", "Create PDF from file…", FILE, None, Nothing, "file-input"),
|
|
c("create.images", "Create PDF from images…", FILE, None, Nothing, "image"),
|
|
c("create.clipboard", "Create PDF from clipboard", FILE, None, Nothing, "copy-plus"),
|
|
c("page.combine", "Combine files…", FILE, None, Nothing, "files"),
|
|
c("file.save", "Save", FILE, Some(Shortcut::cmd("S")), Document, "save"),
|
|
c("file.save_as", "Save as…", FILE, Some(Shortcut::cmd_shift("S")), Document, "save"),
|
|
c("file.close", "Close file", FILE, Some(Shortcut::cmd("W")), Document, "x"),
|
|
c("file.close_all", "Close all", FILE, Some(Shortcut::cmd_shift("W")), Document, "x"),
|
|
c("file.revert", "Revert", FILE, None, Undo, "rotate-ccw"),
|
|
c("print.dialog", "Print…", FILE, Some(Shortcut::cmd("P")), Document, "printer"),
|
|
c("file.properties", "Document properties…", FILE, Some(Shortcut::cmd("D")), Document, "info"),
|
|
ct("edit.undo", "Undo", EDIT, Some(Shortcut::cmd("Z")), Undo, "undo-2"),
|
|
ct("edit.redo", "Redo", EDIT, Some(Shortcut::cmd_shift("Z")), Redo, "redo-2"),
|
|
c("edit.find", "Find…", EDIT, Some(Shortcut::cmd("F")), Document, "search"),
|
|
c("edit.advanced_search", "Advanced search…", EDIT, Some(Shortcut::cmd_shift("F")), Document, "search"),
|
|
c("view.palette", "Find tools and commands…", VIEW, Some(Shortcut::cmd("K")), Nothing, "search"),
|
|
c("view.fit_visible", "Fit visible", VIEW, Some(Shortcut::cmd("3")), Document, "scan"),
|
|
c("view.marquee_zoom", "Marquee zoom", VIEW, None, Document, "zoom-in"),
|
|
c("edit.snapshot", "Take a snapshot", EDIT, None, Document, "camera"),
|
|
c("view.full_screen", "Full screen mode", VIEW, Some(Shortcut::cmd("L")), Document, "maximize"),
|
|
c("view.read_mode", "Read mode", VIEW, Some(Shortcut { command: true, shift: false, mac_ctrl: true, key: "H" }), Document, "book-open"),
|
|
c("view.theme", "Switch light / dark theme", VIEW, None, Nothing, "moon"),
|
|
c("comment.list", "Comments panel", VIEW, None, Document, "message-square-text"),
|
|
c("comment.note", "Add a sticky note", None, None, Annotate, "sticky-note"),
|
|
c("comment.freetext", "Add a text box", None, None, Annotate, "type"),
|
|
c("comment.highlight", "Highlight text", None, None, Annotate, "highlighter"),
|
|
c("comment.underline", "Underline text", None, None, Annotate, "underline"),
|
|
c("comment.strikeout", "Strikethrough text", None, None, Annotate, "strikethrough"),
|
|
c("comment.squiggly", "Squiggly underline text", None, None, Annotate, "spline"),
|
|
c("comment.ink", "Draw freehand", None, None, Annotate, "pencil"),
|
|
c("comment.line", "Draw a line", None, None, Annotate, "minus"),
|
|
c("comment.arrow", "Draw an arrow", None, None, Annotate, "move-right"),
|
|
c("comment.square", "Draw a rectangle", None, None, Annotate, "square"),
|
|
c("comment.circle", "Draw an oval", None, None, Annotate, "circle"),
|
|
c("comment.polygon", "Draw a polygon", None, None, Annotate, "pentagon"),
|
|
c("comment.polyline", "Draw connected lines", None, None, Annotate, "spline"),
|
|
c("comment.cloud", "Draw a cloud", None, None, Annotate, "cloud"),
|
|
c("comment.callout", "Add a callout", None, None, Annotate, "message-square-quote"),
|
|
c("comment.caret", "Insert text", None, None, Annotate, "text-cursor-input"),
|
|
c("comment.replace", "Replace text", None, None, Annotate, "replace"),
|
|
c("comment.attach", "Attach a file", None, None, Annotate, "paperclip"),
|
|
c("comment.eraser", "Erase drawings", None, None, Annotate, "eraser"),
|
|
c("form.fields", "Form fields panel", VIEW, None, Document, "list"),
|
|
c("form.clear", "Clear form", EDIT, None, FillForms, "eraser"),
|
|
c("comment.flatten", "Flatten comments", None, None, HasComments, "layers"),
|
|
c("comment.import", "Import comments…", None, None, Modification, "file-input"),
|
|
c("comment.stamp", "Add a stamp", None, None, Annotate, "stamp"),
|
|
c("comment.export", "Export all comments to data file…", None, None, HasComments, "file-output"),
|
|
c("comment.hide_all", "Hide all comments", None, None, Document, "eye-off"),
|
|
c("comment.summarize", "Summarize comments", None, None, HasComments, "file-text"),
|
|
c("form.import_data", "Import form data…", None, None, HasFields, "file-input"),
|
|
c("form.export_data", "Export form data…", None, None, HasFields, "file-output"),
|
|
c("standards.pdfa", "PDF/A…", None, None, Document, "file-check"),
|
|
c("actions.wizard", "Action Wizard…", None, None, Nothing, "list-checks"),
|
|
c("actions.distribution", "Prepare for distribution…", None, None, Nothing, "send"),
|
|
c("actions.optimize_scans", "Optimize scanned documents…", None, None, Nothing, "scan-text"),
|
|
c("doc.compare", "Compare files…", None, None, Document, "git-compare"),
|
|
c("form.detect", "Detect form fields", None, None, Modification, "scan"),
|
|
c("form.merge_data", "Merge data files into spreadsheet…", None, None, Nothing, "file-spreadsheet"),
|
|
c("form.flatten", "Flatten form fields", None, None, HasFields, "layers"),
|
|
c("form.prepare", "Prepare a form", None, None, Modification, "text-cursor-input"),
|
|
c("form.tab_order.row", "Tab order: by rows", None, None, HasFields, "rows-3"),
|
|
c("form.tab_order.column", "Tab order: by columns", None, None, HasFields, "columns-3"),
|
|
c("form.tab_order.structure", "Tab order: by document structure", None, None, HasFields, "list"),
|
|
c("edit.text", "Add text", None, None, Modification, "type"),
|
|
c("edit.edit_text", "Edit text & images", None, None, Modification, "text-cursor-input"),
|
|
c("edit.link", "Add or edit links", None, None, Modification, "link-2"),
|
|
c("edit.links_from_urls", "Create links from URLs", None, None, Modification, "link-2"),
|
|
c("edit.remove_links", "Remove all links", None, None, Modification, "trash-2"),
|
|
c("edit.bates", "Add Bates numbering…", None, None, Modification, "hash"),
|
|
c("edit.image", "Add image…", None, None, Modification, "image-plus"),
|
|
c("redact.mark", "Redact text and images", None, None, Modification, "rectangle-horizontal"),
|
|
c("redact.pages", "Redact pages…", None, None, Modification, "file-x"),
|
|
c("redact.search", "Find text and redact…", None, None, Modification, "file-search"),
|
|
c("redact.properties", "Redaction properties…", None, None, Document, "settings-2"),
|
|
c("redact.apply", "Apply redactions…", None, None, HasRedactions, "check"),
|
|
c("redact.clear", "Clear redaction marks", None, None, HasRedactions, "eraser"),
|
|
c("protect.remove_hidden", "Remove hidden information…", None, None, Modification, "eye-off"),
|
|
c("redact.sanitize", "Sanitize document…", None, None, Modification, "sparkles"),
|
|
c("form.field.properties", "Field properties…", None, None, HasFields, "settings-2"),
|
|
c("form.add.text", "Add a text field", None, None, Modification, "text-cursor-input"),
|
|
c("form.add.checkbox", "Add a checkbox", None, None, Modification, "check-circle-2"),
|
|
c("form.add.radio", "Add a radio button", None, None, Modification, "circle"),
|
|
c("form.add.combo", "Add a drop-down list", None, None, Modification, "chevron-down"),
|
|
c("form.add.list", "Add a list box", None, None, Modification, "list"),
|
|
c("form.add.button", "Add a button", None, None, Modification, "square"),
|
|
c("form.add.image", "Add an image field", None, None, Modification, "image"),
|
|
c("form.add.date", "Add a date field", None, None, Modification, "clock-3"),
|
|
c("form.add.signature", "Add a digital signature field", None, None, Modification, "signature"),
|
|
c("sign.digital", "Digitally sign", None, None, Document, "signature"),
|
|
c("sign.certify", "Certify (visible signature)", None, None, Document, "badge-check"),
|
|
c("sign.certify_invisible", "Certify (invisible signature)", None, None, Document, "badge-check"),
|
|
c("sign.validate", "Validate all signatures", None, None, Document, "badge-check"),
|
|
c("sign.panel", "Signatures panel", VIEW, None, Document, "signature"),
|
|
c("sign.fill.text", "Fill & Sign: add text", None, None, Annotate, "type"),
|
|
c("sign.fill.check", "Fill & Sign: checkmark", None, None, Annotate, "check"),
|
|
c("sign.fill.cross", "Fill & Sign: cross", None, None, Annotate, "x"),
|
|
c("sign.fill.dot", "Fill & Sign: dot", None, None, Annotate, "circle-dot"),
|
|
c("sign.fill.line", "Fill & Sign: line", None, None, Annotate, "minus"),
|
|
c("sign.fill.date", "Fill & Sign: date", None, None, Annotate, "clock-3"),
|
|
c("sign.fill.signature", "Fill & Sign: sign", None, None, Annotate, "signature"),
|
|
c("sign.fill.initials", "Fill & Sign: initials", None, None, Annotate, "signature"),
|
|
c("sign.fill.signature.change", "Fill & Sign: change signature", None, None, Nothing, "signature"),
|
|
c("sign.fill.signature.remove", "Fill & Sign: remove saved signature", None, None, Nothing, "x"),
|
|
c("sign.fill.initials.remove", "Fill & Sign: remove saved initials", None, None, Nothing, "x"),
|
|
c("sign.fill.initials.change", "Fill & Sign: change initials", None, None, Nothing, "signature"),
|
|
c("export.image", "Export to image…", FILE, None, Document, "image"),
|
|
c("optimize.reduce", "Reduce file size…", FILE, None, Document, "file-down"),
|
|
c("optimize.advanced", "Optimize PDF…", FILE, None, Document, "settings-2"),
|
|
c("export.text", "Export to text…", FILE, None, Document, "type"),
|
|
c("export.docx", "Export to Word…", FILE, None, Document, "file-text"),
|
|
c("export.html", "Export to HTML…", FILE, None, Document, "file-symlink"),
|
|
c("export.rtf", "Export to RTF…", FILE, None, Document, "file-text"),
|
|
c("app.preferences", "Preferences…", EDIT, Some(Shortcut::cmd(",")), Nothing, "settings"),
|
|
c("tools.js_console", "JavaScript console…", None, Some(Shortcut::cmd("J")), Document, "square-terminal"),
|
|
c("tools.document_js", "Document JavaScripts…", None, None, Modification, "file-code"),
|
|
c("ocr.recognize", "Recognize text…", None, None, Modification, "scan-text"),
|
|
c("ocr.recognize_batch", "Recognize text in multiple files…", None, None, Nothing, "files"),
|
|
c("a11y.check", "Check for accessibility…", None, None, Document, "accessibility"),
|
|
c("a11y.report", "Open accessibility report", None, None, Document, "file-text"),
|
|
c("a11y.reading_options", "Change reading options…", None, None, Document, "book-open"),
|
|
c("a11y.alt_text", "Add alternate text…", None, None, Modification, "image"),
|
|
c("export.all_images", "Export all images…", FILE, None, Document, "image"),
|
|
c("edit.header_footer", "Add header & footer…", None, None, Modification, "heading"),
|
|
c("edit.header_footer.update", "Update header & footer…", None, None, Marks(crate::MarkKind::HeaderFooter), "heading"),
|
|
c("edit.header_footer.remove", "Remove header & footer", None, None, Marks(crate::MarkKind::HeaderFooter), "heading"),
|
|
c("edit.watermark", "Add watermark…", None, None, Modification, "stamp"),
|
|
c("edit.watermark.update", "Update watermark…", None, None, Marks(crate::MarkKind::Watermark), "stamp"),
|
|
c("edit.watermark.remove", "Remove watermark", None, None, Marks(crate::MarkKind::Watermark), "stamp"),
|
|
c("edit.background", "Add background…", None, None, Modification, "palette"),
|
|
c("edit.background.update", "Update background…", None, None, Marks(crate::MarkKind::Background), "palette"),
|
|
c("edit.background.remove", "Remove background", None, None, Marks(crate::MarkKind::Background), "palette"),
|
|
c("protect.password", "Protect using password…", FILE, None, Security, "lock"),
|
|
c("protect.remove", "Remove security", FILE, None, ProtectedSecurity, "lock-open"),
|
|
c("protect.properties", "Security properties…", FILE, None, Document, "shield-check"),
|
|
c("page.organize", "Organize pages", PAGES, None, Document, "layout-grid"),
|
|
c("bookmark.add", "New bookmark", PAGES, Some(Shortcut::cmd("B")), Assembly, "bookmark-plus"),
|
|
c("page.rotate", "Rotate pages clockwise", PAGES, None, Assembly, "rotate-cw"),
|
|
c("page.rotate_ccw", "Rotate pages counterclockwise", PAGES, None, Assembly, "rotate-ccw"),
|
|
c("page.delete", "Delete pages", PAGES, None, Assembly, "trash-2"),
|
|
c("page.insert_blank", "Insert blank page", PAGES, None, Assembly, "file-plus"),
|
|
c("page.rotate_dialog", "Rotate pages…", PAGES, None, Assembly, "rotate-cw"),
|
|
c("page.duplicate", "Duplicate pages", PAGES, None, Assembly, "copy-plus"),
|
|
c("page.crop", "Crop pages", PAGES, None, Assembly, "crop"),
|
|
c("page.boxes", "Set page boxes…", PAGES, None, Assembly, "square-dashed-mouse-pointer"),
|
|
c("page.insert", "Insert pages from file…", PAGES, None, Assembly, "file-input"),
|
|
c("page.replace", "Replace pages…", PAGES, None, Assembly, "replace"),
|
|
c("page.extract", "Extract pages…", PAGES, None, Assembly, "file-output"),
|
|
c("page.split", "Split document…", PAGES, None, Assembly, "scissors"),
|
|
c("page.number", "Number pages…", PAGES, None, Assembly, "hash"),
|
|
c("help.shortcuts", "Keyboard shortcuts", HELP, None, Nothing, "circle-help"),
|
|
c("help.discord", "Join the ArtCraft Discord", HELP, None, Nothing, "messages-square"),
|
|
c("help.app_page", "PdfCraft web page", HELP, None, Nothing, "globe"),
|
|
c("help.github", "PdfCraft on GitHub", HELP, None, Nothing, "code-xml"),
|
|
c("help.website", "ArtCraft website", HELP, None, Nothing, "external-link"),
|
|
c("help.check_updates", "Check for updates…", HELP, None, Nothing, "cloud"),
|
|
c("help.about", "About PdfCraft", HELP, None, Nothing, "info"),
|
|
];
|
|
|
|
pub fn command(id: &str) -> Option<&'static CommandSpec> {
|
|
COMMANDS.iter().find(|c| c.id == id)
|
|
}
|
|
|
|
/// Commands that appear in `menu`, in order.
|
|
pub fn menu(menu: &str) -> impl Iterator<Item = &'static CommandSpec> + '_ {
|
|
COMMANDS.iter().filter(move |c| c.menu == Some(menu))
|
|
}
|
|
|
|
/// Whether `spec` can run now, for the document `active` (the focused tab).
|
|
pub fn is_enabled(spec: &CommandSpec, session: &Session, active: Option<DocId>) -> bool {
|
|
let doc = active.and_then(|id| session.get(id));
|
|
match spec.needs {
|
|
Nothing => true,
|
|
Document => doc.is_some(),
|
|
Assembly => doc.is_some_and(|d| d.allows_assembly()),
|
|
Modification => doc.is_some_and(|d| d.allows_modification()),
|
|
Annotate => doc.is_some_and(|d| d.allows_annotation()),
|
|
FillForms => doc.is_some_and(|d| d.allows_form_filling() && !d.form.is_empty()),
|
|
Marks(k) => doc.is_some_and(|d| d.allows_modification() && d.marks.contains(&k)),
|
|
HasComments => doc.is_some_and(|d| d.allows_modification() && !d.info.annotations.is_empty()),
|
|
HasFields => doc.is_some_and(|d| d.allows_modification() && !d.form.is_empty()),
|
|
HasRedactions => doc.is_some_and(|d| d.allows_modification() && d.redaction_marks() > 0),
|
|
Security => doc.is_some_and(|d| d.allows_security_change()),
|
|
ProtectedSecurity => doc.is_some_and(|d| d.allows_security_change() && d.security_summary().is_some()),
|
|
Undo => doc.is_some_and(|d| d.can_undo().is_some()),
|
|
Redo => doc.is_some_and(|d| d.can_redo().is_some()),
|
|
}
|
|
}
|
|
|
|
/// The label to show for `spec` now ("Undo Rotate page" rather than "Undo").
|
|
pub fn current_label(spec: &CommandSpec, session: &Session, active: Option<DocId>) -> String {
|
|
let doc = active.and_then(|id| session.get(id));
|
|
match (spec.id, doc) {
|
|
("edit.undo", Some(d)) => d.can_undo().map(|l| format!("Undo {l}")).unwrap_or_else(|| spec.label.into()),
|
|
("edit.redo", Some(d)) => d.can_redo().map(|l| format!("Redo {l}")).unwrap_or_else(|| spec.label.into()),
|
|
_ => spec.label.into(),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn ids_and_shortcuts_are_unique() {
|
|
let mut ids = std::collections::HashSet::new();
|
|
let mut keys = std::collections::HashSet::new();
|
|
for c in COMMANDS {
|
|
assert!(ids.insert(c.id), "duplicate id {}", c.id);
|
|
if let Some(s) = c.shortcut {
|
|
assert!(keys.insert(s), "duplicate shortcut {} ({})", s.label(true), c.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn every_ready_catalogue_item_is_a_registered_command() {
|
|
for g in crate::catalog::TOOL_GROUPS {
|
|
for s in g.sections {
|
|
for i in s.items {
|
|
if i.availability == crate::catalog::Availability::Ready {
|
|
assert!(command(i.command).is_some(), "catalogue item {} ({}) is Ready but not registered", i.label, i.command);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn shortcut_labels_follow_platform_conventions() {
|
|
let s = command("file.save_as").unwrap().shortcut.unwrap();
|
|
assert_eq!(s.label(true), "⇧⌘S");
|
|
assert_eq!(s.label(false), "Ctrl+Shift+S");
|
|
assert_eq!(command("view.read_mode").unwrap().shortcut.unwrap().label(true), "⌃⌘H");
|
|
}
|
|
|
|
#[test]
|
|
fn enablement_follows_the_document_state() {
|
|
let mut s = Session::new();
|
|
let undo = command("edit.undo").unwrap();
|
|
let save = command("file.save").unwrap();
|
|
assert!(!is_enabled(save, &s, None));
|
|
assert!(is_enabled(command("file.open").unwrap(), &s, None));
|
|
let id = s.open("a.pdf", None, std::sync::Arc::new(crate::tests::fixture(2)), None).unwrap();
|
|
assert!(is_enabled(save, &s, Some(id)));
|
|
assert!(!is_enabled(undo, &s, Some(id)));
|
|
s.apply(id, crate::Edit::RotatePages { pages: vec![0], degrees: 90 }).unwrap();
|
|
assert!(is_enabled(undo, &s, Some(id)));
|
|
assert_eq!(current_label(undo, &s, Some(id)), "Undo Rotate page");
|
|
}
|
|
}
|