Preferences ▸ Identity: the name on new comments, remembered

Comments were signed with the login name (or set_option author), with no way
to change it in the app. Preferences gains an Identity section whose name
field (labelled for screen readers) sets the author of new comments; it is
saved with the settings and restored (an empty name keeps the login-name
default; restored names are capped at 200 characters). Japanese labels
added. Test: identity.rs. Parity comment.author-identity: shipped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-05 19:31:23 -04:00
1 parent b4e1d488de
commit b315eff24d
5 files changed
+77 -5

No files matched your search

+2
View File
@@ -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 を作成…"),
+9 -1
View File
@@ -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());
+9
View File
@@ -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::<i18n::Language>(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::<Vec<Vec<[f32; 2]>>>(v["signature"].clone())
&& s.iter().all(|st| st.iter().all(|p| p.iter().all(|x| x.is_finite())))
{
+53
View File
@@ -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);
}
+4 -4
View File
@@ -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"