diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index de0fa3f2..56fc4bb1 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -47,6 +47,8 @@ const JAPANESE: &[(&str, &str)] = &[ ("Preferences", "環境設定"), ("Preferences…", "環境設定…"), ("Interface language", "表示言語"), + ("Identity", "個人情報"), + ("Name on new comments", "新しい注釈の作成者名"), ("Open…", "開く…"), ("New blank PDF", "空白の PDF を作成"), ("Create PDF from file…", "ファイルから PDF を作成…"), diff --git a/crates/ui-egui/src/js_ui.rs b/crates/ui-egui/src/js_ui.rs index c89067b2..719df3e4 100644 --- a/crates/ui-egui/src/js_ui.rs +++ b/crates/ui-egui/src/js_ui.rs @@ -223,7 +223,7 @@ pub(crate) fn document_js_body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &T close } -/// Preferences (the JavaScript category for now). Returns `true` to close. +/// Preferences: interface language, identity and JavaScript. Returns `true` to close. pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> bool { ui.label(egui::RichText::new(app.language.tr("Preferences")).font(theme::semibold(18.0))); ui.horizontal(|ui| { @@ -235,6 +235,14 @@ pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &T }); }); ui.add_space(8.0); + // Identity: the author of new comments (Acrobat: Preferences ▸ Identity). + ui.label(egui::RichText::new(app.language.tr("Identity")).font(theme::semibold(13.0))); + ui.horizontal(|ui| { + let label = ui.label(app.language.tr("Name on new comments")); + ui.add(egui::TextEdit::singleline(&mut app.comment_prefs.author).desired_width(220.0).char_limit(crate::MAX_AUTHOR_CHARS)) + .labelled_by(label.id); + }); + ui.add_space(8.0); ui.label(egui::RichText::new("JavaScript").font(theme::semibold(13.0))); egui::Frame::new().fill(t.hover).corner_radius(egui::CornerRadius::same(6)).inner_margin(egui::Margin::same(10)).show(ui, |ui| { ui.set_width(ui.available_width()); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 1af06c19..14277be3 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -58,6 +58,9 @@ pub use print_ui::{Handling as PrintHandling, PrintDraft, Which as PrintWhich}; mod redact_ui; pub use redact_ui::{HiddenDraft, PagesDraft as RedactPagesDraft, RedactPrefs, SearchDraft as RedactSearchDraft}; pub mod i18n; + +/// The longest author name kept (Preferences ▸ Identity, restored settings). +pub(crate) const MAX_AUTHOR_CHARS: usize = 200; mod protect; mod recovery; pub mod theme; @@ -824,6 +827,7 @@ impl PrintCraftApp { "recent": self.recent, "theme": self.theme, "language": self.language, + "author": self.comment_prefs.author, // Drawn signatures keep their original form (older settings read the same). "signature": match &self.signature { Some(fill_sign::SavedSig::Drawn(s)) => Some(s), _ => None }, "signature_text": match &self.signature { Some(fill_sign::SavedSig::Typed(t)) => Some(t), _ => None }, @@ -853,6 +857,11 @@ impl PrintCraftApp { if let Ok(language) = serde_json::from_value::(v["language"].clone()) { self.language = language; } + // An empty or missing name keeps the login-name default; settings are untrusted, so the + // name is cut to a sane length. + if let Some(author) = v["author"].as_str().map(str::trim).filter(|a| !a.is_empty()) { + self.comment_prefs.author = author.chars().take(MAX_AUTHOR_CHARS).collect(); + } if let Ok(s) = serde_json::from_value::>>(v["signature"].clone()) && s.iter().all(|st| st.iter().all(|p| p.iter().all(|x| x.is_finite()))) { diff --git a/crates/ui-egui/tests/identity.rs b/crates/ui-egui/tests/identity.rs new file mode 100644 index 00000000..a09a6d5f --- /dev/null +++ b/crates/ui-egui/tests/identity.rs @@ -0,0 +1,53 @@ +//! Preferences ▸ Identity: the name new comments are signed with. + +use egui::accesskit::Role; +use egui::{Key, Modifiers}; +use egui_kittest::Harness; +use egui_kittest::kittest::Queryable; +use printcraft_ui_egui::PrintCraftApp; + +const PDF: &[u8] = b"%PDF-1.7 +1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj +2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj +3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 300 200] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >> endobj +4 0 obj << /Length 55 >> stream +BT /F1 14 Tf 20 150 Td (The quick brown fox) Tj ET +endstream endobj +5 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> endobj +trailer << /Root 1 0 R >> +%%EOF"; + +#[test] +fn the_identity_name_signs_new_comments_and_is_remembered() { + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { + let mut app = PrintCraftApp::new(); + app.open_bytes("doc.pdf", None, PDF.to_vec()).expect("opens"); + app.set_option("author", "Tester").unwrap(); + app.set_option("dialog", "preferences").unwrap(); + app + }); + h.run_steps(4); + h.get_by_label_contains("Name on new comments"); + // Replace the name in the field. + h.get_by_role_and_label(Role::TextInput, "Name on new comments").click(); + h.run_steps(1); + h.key_press_modifiers(Modifiers::COMMAND, Key::A); + h.get_by_role_and_label(Role::TextInput, "Name on new comments").type_text("Grace Hopper"); + h.run_steps(2); + assert_eq!(h.state().comment_prefs.author, "Grace Hopper"); + // New comments carry it. + h.state_mut().set_option("dialog", "none").unwrap(); + h.state_mut().views[0].select_text(0, 4, 8); + assert!(h.state_mut().execute("comment.highlight")); + h.run_steps(2); + let doc = h.state().session.get(h.state().views[0].id).unwrap(); + assert_eq!(doc.info.annotations.first().and_then(|a| a.author.as_deref()), Some("Grace Hopper")); + // It survives a restart; an empty or missing name keeps the default, and a huge one is cut. + let mut restored = PrintCraftApp::new(); + restored.restore(&h.state().persist()); + assert_eq!(restored.comment_prefs.author, "Grace Hopper"); + restored.restore(r#"{"author": " "}"#); + assert_eq!(restored.comment_prefs.author, "Grace Hopper"); + restored.restore(&format!(r#"{{"author": "{}"}}"#, "x".repeat(10_000))); + assert_eq!(restored.comment_prefs.author.chars().count(), 200); +} diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index d4875498..53ebd6b2 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -3803,11 +3803,11 @@ title = "Author name from identity preferences" area = "E" tier = "P1" milestone = "M5" -status = "partial" +status = "shipped" commands = [] tools = ["comment_add"] -evidence = ["crates/ui-egui/tests/comments.rs::dragging_over_text_with_the_highlighter_highlights_it"] -notes = "the login name, as Acrobat defaults to; no Identity preferences yet" +evidence = ["crates/ui-egui/tests/comments.rs::dragging_over_text_with_the_highlighter_highlights_it", "crates/ui-egui/tests/identity.rs::the_identity_name_signs_new_comments_and_is_remembered"] +notes = "Preferences ▸ Identity: the name on new comments (default the login name, as Acrobat), remembered across restarts" [[feature]] id = "comment.reply" @@ -9639,7 +9639,7 @@ status = "partial" commands = ["app.preferences"] tools = ["js_enabled"] evidence = ["crates/ui-egui/tests/javascript.rs::document_scripts_and_preferences"] -notes = "Preferences dialog with the JavaScript category; other categories still live in their tools." +notes = "Preferences dialog with Interface language, Identity and JavaScript; other categories still live in their tools, and it isn't generated from a schema yet." [[feature]] id = "misc.prefs-general"