diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index 3f0515243..57944f400 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -7,7 +7,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Greek UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "312db664c98a25a4ed6b098b4849a95da2a29efe8d1a9e039ff47a92d27ccd0e" +sha256 = "8f2dc597b6f0b05c430260cccfa98e59905ae6badd920ea6982e6a48736e7602" # ATTRIBUTION.toml: one entry per asset (AGENTS.md §1). The source of truth for provenance and licences. # ATTRIBUTION.md is generated from this file: `cargo xtask assets --write`. Checked by `cargo xtask assets` (part of `xtask ci`). @@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "1928f3ee702ac4c5d24bd68d8ea3985920e3405347d46a5c8c636dad1a88f761" +sha256 = "747fb15896208d759e3c4d4ef243d39ea55f39e7d096a4f0c17f7b8276dd01f7" [[asset]] path = "crates/ui-egui/src/i18n/zh-hant.tsv" @@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "8b10d4e4bd7ce694afa0653d53148af767f5619dd50637b578e55a3c7e955361" +sha256 = "4430f20b3dd5e86471fb61d63529695f1b81d7af14cba028aa9e4a7709f2eb51" [[asset]] path = "crates/ui-egui/src/i18n/cs.tsv" @@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "f953308d54011abee040dd192f48a923216dd2a572f1d14dd550dd6ac8ba86df" +sha256 = "4aef3cd513d168abd15d1760d6e754278e7539e4c02f36770f9eed44dd6e88fd" [[asset]] path = "crates/ui-egui/src/i18n/pt-br.tsv" @@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "e01a0ec8da6dffc4dbafe4cc411a7a1ed9cba76bc75b10435818aae04dda0e4f" +sha256 = "707973b0c7f0e546c84c0ba07b4d828071d24a8318f1287ccbdf065b985ac326" [[asset]] path = "crates/ui-egui/src/i18n/es.tsv" @@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "d1414ea5a02fd634b635231e4439b5f279dcc3473869bbd534bc9a70dad6dbd8" +sha256 = "df205fdfc750a3f9de5a667710506f09fbf5604d632576b0085f8f0560cf1bfd" [[asset]] path = "crates/ui-egui/src/i18n/fr.tsv" @@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "234b7850d3067361eb13ec85170f45e5c9708e702b091b529a4c336e9f7b6560" +sha256 = "50fc9a57a9d301f46d38a812764b4e6e9791fff154297cdaa5240519d281c48b" [[asset]] path = "crates/ui-egui/src/i18n/de.tsv" @@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "0699859ac9c163d408bb6c8273f4d9d0be8e68f6338d2dd156d96fc5a2e411c8" +sha256 = "cbdd825e664c37a1dc2cf559a63a1ca6fb093a8c1036ccdbbf0738d684e61c8b" [[asset]] path = "crates/ui-egui/src/i18n/te.tsv" @@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "512ecfeb69f0ebc1caaa58ce30350c63ffa9a80e9222629b69c9d9df413ba499" +sha256 = "c119f82118264b45e46de6c31f4db3c062c86f35a661b8d2f12170ee11042ddf" [[asset]] path = "crates/ui-egui/src/i18n/it.tsv" @@ -118,7 +118,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "766362711c5980ba2fae81781f815621afb390db4c47fcfd8a3e77d5f1deb934" +sha256 = "24132323fb0ab81a96a18c1ed7a7dee719469b3733f0425346fce8f9a8ea8081" [[asset]] path = "crates/ui-egui/src/i18n/pl.tsv" @@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "ea9f02fc204f19e464b802bf32ba090dfac890283883a04eeb066b529cb4d3d1" +sha256 = "8146db6443af5cb41655aabc415d6bd738259c886dc47eddb4fd88b82f166347" [[asset]] path = "crates/ui-egui/src/i18n/ru.tsv" @@ -140,7 +140,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "e42b97aad738f199698d38ed475aa3eada7ab1765526ef1e8250af3a863768dd" +sha256 = "6e18893f0cad5d23b72bfcd5e8bbd697143811a4288a71efc87292e7bf1ef39b" [[asset]] path = "crates/ui-egui/src/i18n/hu.tsv" @@ -151,7 +151,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "848cc35acaf475c6a532e20cfcefc3f9d4be7570764882bc1becc6ad1d144abb" +sha256 = "8f4ddb7fec77362d652f4a08f65610ebbf83f1fce1c061dbc347b22bd92c9b23" [[asset]] path = "crates/ui-egui/src/i18n/bg.tsv" @@ -162,7 +162,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "9e65e6297aea35211aff2198700b5596a55d23c67ce8a6d83b7098f49e29b22a" +sha256 = "f1e021147716496129d887db04f576380f3ed75348dece15cf6446e5e57ef0e9" [[asset]] path = "crates/ui-egui/src/i18n/kk.tsv" @@ -173,7 +173,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "5c2e5a2b373e83506025ae2a065a74e5fc34639145e932d9abaf38be650a1b56" +sha256 = "842fe4d5047359db29f014b23eee4b7c1eb2cb76b0bf13077dcace2c763a17f4" [[asset]] path = "crates/ui-egui/src/i18n/ja.tsv" @@ -184,7 +184,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "44ac275334de6ccd1ac14c45bd51378c9d86d7fe6060430c16c581cdcf09715c" +sha256 = "ad4bb29a8c740bc5d9148f93d13e8b548509b107d9612a1a45e2a80decf1b0a8" [[asset]] path = "crates/ui-egui/src/i18n/uk.tsv" @@ -195,7 +195,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "03638838a52bb75bf9bd17d5e050cffdef42c2f4f2eafd61e04953feaf663ff6" +sha256 = "0fb190f6ee02103b7f06a94ee2ed7764c56ab8b905567d68485b8023b66e3e59" [[asset]] path = "crates/ui-egui/src/i18n/ar.tsv" @@ -206,7 +206,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "df1501a33b15029dac0740994a286809c5ec710195efb3f3c983bd363695cd3f" +sha256 = "15aa44d01688879b0321f820b0087cbcb9e3924a957cf1457ee9256e42626957" [[asset]] path = "crates/ui-egui/src/i18n/tr.tsv" @@ -217,7 +217,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "ae494a56449a5e574621219c18d1c45a817a803ccff8a3e8decdbdfe4f5cca21" +sha256 = "09cf93e500a6a0c461942ceb985731dbcee3ca00bb3160c1af60448a402b1862" [[asset]] path = "crates/ui-egui/src/i18n/lt.tsv" @@ -228,7 +228,7 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "e2f03a2c1832fabdc037a5b7d4d314c15d4d7b2b337aeec9b0e92b7584b4a49c" +sha256 = "10222eaf5a9dd16f5677554f586ed715f10f2c56ccfc8ffcf68f3f90d2e8c61a" [[asset]] @@ -3363,4 +3363,4 @@ licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "291a76520d622c7b5ef141e6f0bcaeb3e319974a1390b23179cb14dbcf03a28c" +sha256 = "dddc56fe8c320dd807c56a5d6450eeb5cdaecc81d9e517b6c1adfe986d688a41" diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 53925c008..6c812da70 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -2452,6 +2452,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { | QuickTool::Snapshot => false, }; let prefs = &app.comment_prefs; + let redact_mark = (app.redact_prefs.hover_fill, app.redact_prefs.opacity); let units = app.units; let allowed = doc.allows_annotation(); let comments_hidden = doc.comments_hidden(); @@ -2727,7 +2728,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { painter.rect_stroke(r, CornerRadius::ZERO, Stroke::new(0.5, t.border.gamma_multiply(0.8)), egui::StrokeKind::Outside); // Comments: tools, selection, moving and resizing come before text selection. - let pcx = comments::PageCx { page: i, xf: &xf, info, tool, prefs, allowed, hidden: comments_hidden }; + let pcx = comments::PageCx { page: i, xf: &xf, info, tool, prefs, allowed, hidden: comments_hidden, redact_mark }; if let QuickTool::Measure(measure_tool) = tool { crate::measure_ui::page_input(ui, &resp, doc, view, &pcx, measure_tool, units); } diff --git a/crates/ui-egui/src/commands.rs b/crates/ui-egui/src/commands.rs index 7f974e7d3..e887d1c91 100644 --- a/crates/ui-egui/src/commands.rs +++ b/crates/ui-egui/src/commands.rs @@ -454,7 +454,10 @@ impl PdfCraftApp { self.redact_search.found = None; self.dialog = Some(Dialog::RedactSearch); } - "redact.properties" => self.dialog = Some(Dialog::RedactProps), + "redact.properties" => { + self.redact_before = Some(self.redact_prefs.clone()); + self.dialog = Some(Dialog::RedactProps); + } "redact.apply" => { let marks = active.and_then(|i| self.session.get(self.views[i].id)).map_or(0, |d| d.redaction_marks()); if marks == 0 { diff --git a/crates/ui-egui/src/comments.rs b/crates/ui-egui/src/comments.rs index e0a6e23eb..08e2d89c5 100644 --- a/crates/ui-egui/src/comments.rs +++ b/crates/ui-egui/src/comments.rs @@ -511,6 +511,9 @@ pub(crate) struct PageCx<'a> { pub info: &'a DocInfo, pub tool: QuickTool, pub prefs: &'a CommentPrefs, + /// A redaction mark's fill while the pointer is over it, and its opacity (Redaction Tool + /// Properties). + pub redact_mark: ([f64; 3], f64), /// The document allows commenting. pub allowed: bool, /// Comments are hidden (Hide all comments): nothing to select. @@ -556,6 +559,23 @@ impl PageCx<'_> { } } + /// A redaction mark's areas: one per marked quad, or its rectangle. + fn redact_rects(&self, a: &Annotation) -> Vec { + if a.quads.is_empty() { + return vec![self.screen_rect(a)]; + } + a.quads + .iter() + .map(|q| { + let xs = [q[0], q[2], q[4], q[6]]; + let ys = [q[1], q[3], q[5], q[7]]; + let (x0, x1) = (xs.iter().copied().fold(f32::MAX, f32::min), xs.iter().copied().fold(f32::MIN, f32::max)); + let (y0, y1) = (ys.iter().copied().fold(f32::MAX, f32::min), ys.iter().copied().fold(f32::MIN, f32::max)); + self.xf.user_rect(self.info, self.page, [x0, y0, x1, y1]) + }) + .collect() + } + /// The topmost comment under `p`. fn hit(&self, p: Pos2) -> Option<&Annotation> { self.comments().filter(|a| self.screen_rects(a).iter().any(|r| r.expand(3.0).contains(p))).last() @@ -1071,6 +1091,18 @@ pub(crate) fn page_after_text(resp: &egui::Response, cx: &PageCx<'_>, view: &mut pub(crate) fn paint_page(ui: &egui::Ui, painter: &egui::Painter, cx: &PageCx<'_>, view: &DocView) { let cv = &view.comments; let pointer = ui.input(|i| i.pointer.hover_pos()); + // A redaction mark shows its fill while the pointer is over it, as Acrobat's does, so the + // marked area reads before anything is applied. + if cv.gesture.is_none() + && let Some(a) = pointer.and_then(|p| cx.hit(p)) + && a.subtype == "Redact" + { + let ([r, g, b], opacity) = cx.redact_mark; + let fill = Color32::from_rgba_unmultiplied((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8, (opacity.clamp(0.0, 1.0) * 255.0) as u8); + for rect in cx.redact_rects(a) { + painter.rect_filled(rect, CornerRadius::ZERO, fill); + } + } if cv.gesture.is_none() && let Some(a) = pointer.and_then(|p| cx.hit(p)) && (cx.tool == QuickTool::Select || matches!(cx.tool, QuickTool::Fill(_)) && a.fill_sign) @@ -1231,7 +1263,7 @@ pub(crate) fn composer(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, // Scrolled away: keep the draft until the page is back. return None; }; - let cx = PageCx { page, xf: &xf, info, tool: QuickTool::Select, prefs, allowed: true, hidden: false }; + let cx = PageCx { page, xf: &xf, info, tool: QuickTool::Select, prefs, allowed: true, hidden: false, redact_mark: ([0.89, 0.13, 0.13], 1.0) }; let anchor = cx.to_screen(c.at); let t = Tokens::get(ctx); let mut post = false; diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 5e091ee92..7d5f60c79 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -537,6 +537,15 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) { let mut prefs = app.redact_prefs.clone(); let (ok, cancel) = crate::redact_ui::props_body(ui, &mut prefs, &t); app.redact_prefs = prefs; + if ok && app.redact_prefs.remember { + app.redact_defaults = app.redact_prefs.clone(); + } + if cancel && let Some(before) = app.redact_before.take() { + app.redact_prefs = before; + } + if ok { + app.redact_before = None; + } close = ok || cancel; return; } diff --git a/crates/ui-egui/src/i18n/ar.tsv b/crates/ui-egui/src/i18n/ar.tsv index ccfcb5b56..0d597de4f 100644 --- a/crates/ui-egui/src/i18n/ar.tsv +++ b/crates/ui-egui/src/i18n/ar.tsv @@ -2415,3 +2415,9 @@ animation effects Off معطّلة Crop Image قص الصورة Remove Crop إزالة القص Trim the picture with its handles; Enter or Done applies قص الصورة بمقابضها؛ يطبّق Enter أو تم + Redaction mark appearance مظهر علامة الحجب + Outline colour: لون الحد الخارجي: + Fill colour: لون التعبئة: + Shown while the pointer is over a mark. يظهر أثناء وجود المؤشر فوق علامة. + Opacity: العتامة: + Make properties default جعل الخصائص افتراضية diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 523e3a6ac..4ea97a5aa 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -2357,3 +2357,9 @@ animation effects Off Изключени Crop Image Изрязване на изображението Remove Crop Премахване на изрязването Trim the picture with its handles; Enter or Done applies Изрежете картината чрез дръжките; Enter или Готово прилага + Redaction mark appearance Вид на маркировката за заличаване + Outline colour: Цвят на контура: + Fill colour: Цвят на запълването: + Shown while the pointer is over a mark. Показва се, докато показалецът е над маркировка. + Opacity: Непрозрачност: + Make properties default Задаване като свойства по подразбиране diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 33bd18b3b..97876c3b9 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -2468,3 +2468,9 @@ animation effects Off Vypnuto Crop Image Oříznout obrázek Remove Crop Odebrat oříznutí Trim the picture with its handles; Enter or Done applies Ořízněte obrázek úchyty; Enter nebo Hotovo použije + Redaction mark appearance Vzhled značky začernění + Outline colour: Barva obrysu: + Fill colour: Barva výplně: + Shown while the pointer is over a mark. Zobrazí se, když je ukazatel nad značkou. + Opacity: Neprůhlednost: + Make properties default Nastavit vlastnosti jako výchozí diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index bed9fdb06..e86d487fd 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -2395,3 +2395,9 @@ animation effects Off Aus Crop Image Bild zuschneiden Remove Crop Zuschnitt entfernen Trim the picture with its handles; Enter or Done applies Bild mit den Griffen zuschneiden; Eingabe oder Fertig übernimmt + Redaction mark appearance Aussehen der Schwärzungsmarkierung + Outline colour: Konturfarbe: + Fill colour: Füllfarbe: + Shown while the pointer is over a mark. Wird angezeigt, solange der Zeiger über einer Markierung ist. + Opacity: Deckkraft: + Make properties default Eigenschaften als Standard festlegen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index a8baf4269..f12a55334 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -2404,3 +2404,9 @@ animation effects Off Ανενεργά Crop Image Περικοπή εικόνας Remove Crop Κατάργηση περικοπής Trim the picture with its handles; Enter or Done applies Περικόψτε την εικόνα με τις λαβές της· Enter ή Τέλος για εφαρμογή + Redaction mark appearance Εμφάνιση σήμανσης απαλοιφής + Outline colour: Χρώμα περιγράμματος: + Fill colour: Χρώμα γεμίσματος: + Shown while the pointer is over a mark. Εμφανίζεται όσο ο δείκτης είναι πάνω από μια σήμανση. + Opacity: Αδιαφάνεια: + Make properties default Ορισμός ιδιοτήτων ως προεπιλογή diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index f3bd6af01..1176902b3 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -2524,3 +2524,9 @@ animation effects Off Desactivados Crop Image Recortar imagen Remove Crop Quitar recorte Trim the picture with its handles; Enter or Done applies Recorta la imagen con sus tiradores; Intro o Hecho lo aplica + Redaction mark appearance Aspecto de la marca de censura + Outline colour: Color del contorno: + Fill colour: Color de relleno: + Shown while the pointer is over a mark. Se muestra mientras el puntero está sobre una marca. + Opacity: Opacidad: + Make properties default Usar como propiedades predeterminadas diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index efcb04812..1ebdea307 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -2394,3 +2394,9 @@ animation effects Off Désactivés Crop Image Recadrer l'image Remove Crop Supprimer le recadrage Trim the picture with its handles; Enter or Done applies Recadrez l'image avec ses poignées ; Entrée ou Terminé applique + Redaction mark appearance Apparence de la marque de biffure + Outline colour: Couleur du contour : + Fill colour: Couleur de remplissage : + Shown while the pointer is over a mark. Affichée tant que le pointeur survole une marque. + Opacity: Opacité : + Make properties default Définir comme propriétés par défaut diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index f0f62078c..702d4e3dd 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -2393,3 +2393,9 @@ animation effects Off Ki Crop Image Kép körülvágása Remove Crop Körülvágás eltávolítása Trim the picture with its handles; Enter or Done applies Vágja körül a képet a fogantyúival; Enter vagy Kész alkalmazza + Redaction mark appearance Kitakarási jelölés megjelenése + Outline colour: Körvonal színe: + Fill colour: Kitöltés színe: + Shown while the pointer is over a mark. Akkor látszik, amikor a mutató egy jelölés fölött van. + Opacity: Átlátszatlanság: + Make properties default Tulajdonságok beállítása alapértelmezettként diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 46f67d6b9..85065c90b 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -2403,3 +2403,9 @@ animation effects Off Disattivati Crop Image Ritaglia immagine Remove Crop Rimuovi ritaglio Trim the picture with its handles; Enter or Done applies Ritaglia l'immagine con le maniglie; Invio o Fatto applica + Redaction mark appearance Aspetto del segno di redazione + Outline colour: Colore contorno: + Fill colour: Colore riempimento: + Shown while the pointer is over a mark. Visibile mentre il puntatore è su un segno. + Opacity: Opacità: + Make properties default Imposta proprietà come predefinite diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index b4ad39bf8..9db76cf4d 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -2394,3 +2394,9 @@ animation effects Off オフ Crop Image 画像を切り抜き Remove Crop 切り抜きを解除 Trim the picture with its handles; Enter or Done applies ハンドルで画像を切り抜きます。Enter または完了で適用 + Redaction mark appearance 墨消しマークの外観 + Outline colour: 枠線の色: + Fill colour: 塗りの色: + Shown while the pointer is over a mark. ポインターがマーク上にあるときに表示されます。 + Opacity: 不透明度: + Make properties default プロパティをデフォルトに設定 diff --git a/crates/ui-egui/src/i18n/kk.tsv b/crates/ui-egui/src/i18n/kk.tsv index 0058907f4..3c492d935 100644 --- a/crates/ui-egui/src/i18n/kk.tsv +++ b/crates/ui-egui/src/i18n/kk.tsv @@ -2392,3 +2392,9 @@ animation effects Off Өшірулі Crop Image Суретті қию Remove Crop Қиюды алып тастау Trim the picture with its handles; Enter or Done applies Суретті тұтқаларымен қиыңыз; Enter немесе Дайын қолданады + Redaction mark appearance Жасыру белгісінің көрінісі + Outline colour: Контур түсі: + Fill colour: Толтыру түсі: + Shown while the pointer is over a mark. Меңзер белгінің үстінде тұрғанда көрсетіледі. + Opacity: Мөлдірсіздік: + Make properties default Қасиеттерді әдепкі ету diff --git a/crates/ui-egui/src/i18n/lt.tsv b/crates/ui-egui/src/i18n/lt.tsv index 5b1d5e482..cde7b9914 100644 --- a/crates/ui-egui/src/i18n/lt.tsv +++ b/crates/ui-egui/src/i18n/lt.tsv @@ -2638,3 +2638,9 @@ key Space Tarpas Crop Image Apkarpyti vaizdą Remove Crop Pašalinti apkarpymą Trim the picture with its handles; Enter or Done applies Apkarpykite paveikslėlį rankenėlėmis; Enter arba Atlikta pritaiko + Redaction mark appearance Užtamsinimo žymės išvaizda + Outline colour: Kontūro spalva: + Fill colour: Užpildo spalva: + Shown while the pointer is over a mark. Rodoma, kol žymeklis yra virš žymės. + Opacity: Nepermatomumas: + Make properties default Nustatyti ypatybes kaip numatytąsias diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 204b61a64..daebec973 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -2374,3 +2374,9 @@ animation effects Off Wyłączone Crop Image Przytnij obraz Remove Crop Usuń przycięcie Trim the picture with its handles; Enter or Done applies Przytnij obraz uchwytami; Enter lub Gotowe stosuje + Redaction mark appearance Wygląd znacznika redakcji + Outline colour: Kolor obrysu: + Fill colour: Kolor wypełnienia: + Shown while the pointer is over a mark. Widoczny, gdy wskaźnik jest nad znacznikiem. + Opacity: Krycie: + Make properties default Ustaw właściwości jako domyślne diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 82c06d301..3b8d33d56 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2408,3 +2408,9 @@ animation effects Off Desativados Crop Image Cortar imagem Remove Crop Remover corte Trim the picture with its handles; Enter or Done applies Corte a imagem com as alças; Enter ou Concluído aplica + Redaction mark appearance Aparência da marca de redação + Outline colour: Cor do contorno: + Fill colour: Cor de preenchimento: + Shown while the pointer is over a mark. Mostrada enquanto o ponteiro está sobre uma marca. + Opacity: Opacidade: + Make properties default Tornar propriedades padrão diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 7aaddf0e6..386424958 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2307,3 +2307,9 @@ animation effects Off Выкл Crop Image Обрезать изображение Remove Crop Убрать обрезку Trim the picture with its handles; Enter or Done applies Обрежьте изображение за маркеры; Enter или «Готово» применяет + Redaction mark appearance Вид метки скрытия + Outline colour: Цвет контура: + Fill colour: Цвет заливки: + Shown while the pointer is over a mark. Показывается, пока указатель над меткой. + Opacity: Непрозрачность: + Make properties default Сделать свойства стандартными diff --git a/crates/ui-egui/src/i18n/sl.tsv b/crates/ui-egui/src/i18n/sl.tsv index 820186e6a..aca66b0db 100644 --- a/crates/ui-egui/src/i18n/sl.tsv +++ b/crates/ui-egui/src/i18n/sl.tsv @@ -2399,3 +2399,9 @@ animation effects Off Izklopljeni Crop Image Obreži sliko Remove Crop Odstrani obrezovanje Trim the picture with its handles; Enter or Done applies Obrežite sliko z ročicami; Enter ali Končano uporabi + Redaction mark appearance Videz oznake za zakritje + Outline colour: Barva obrobe: + Fill colour: Barva polnila: + Shown while the pointer is over a mark. Prikazano, ko je kazalec nad oznako. + Opacity: Prekrivnost: + Make properties default Nastavi lastnosti kot privzete diff --git a/crates/ui-egui/src/i18n/te.tsv b/crates/ui-egui/src/i18n/te.tsv index f8fc6f4a7..25f2a5ecd 100644 --- a/crates/ui-egui/src/i18n/te.tsv +++ b/crates/ui-egui/src/i18n/te.tsv @@ -2307,3 +2307,9 @@ animation effects Off ఆఫ్ Crop Image చిత్రాన్ని కత్తిరించు Remove Crop కత్తిరింపును తీసివేయి Trim the picture with its handles; Enter or Done applies హ్యాండిల్‌లతో చిత్రాన్ని కత్తిరించండి; Enter లేదా పూర్తయింది వర్తింపజేస్తుంది + Redaction mark appearance రెడాక్షన్ గుర్తు రూపం + Outline colour: అంచు రంగు: + Fill colour: నింపు రంగు: + Shown while the pointer is over a mark. పాయింటర్ గుర్తుపై ఉన్నప్పుడు కనిపిస్తుంది. + Opacity: అపారదర్శకత: + Make properties default లక్షణాలను డిఫాల్ట్‌గా చేయి diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 9d03c4371..6ee205caf 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -2390,3 +2390,9 @@ animation effects Off Kapalı Crop Image Resmi kırp Remove Crop Kırpmayı kaldır Trim the picture with its handles; Enter or Done applies Resmi tutamaçlarıyla kırpın; Enter veya Bitti uygular + Redaction mark appearance Karartma işareti görünümü + Outline colour: Kenar rengi: + Fill colour: Dolgu rengi: + Shown while the pointer is over a mark. İşaretçi bir işaretin üzerindeyken gösterilir. + Opacity: Opaklık: + Make properties default Özellikleri varsayılan yap diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 7ae401ef2..1af91509d 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -2416,3 +2416,9 @@ animation effects Off Вимкнено Crop Image Обрізати зображення Remove Crop Прибрати обрізання Trim the picture with its handles; Enter or Done applies Обріжте зображення за маркери; Enter або «Готово» застосовує + Redaction mark appearance Вигляд позначки приховування + Outline colour: Колір контуру: + Fill colour: Колір заливки: + Shown while the pointer is over a mark. Показується, поки вказівник над позначкою. + Opacity: Непрозорість: + Make properties default Зробити властивості типовими diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index f710c1fe4..8a70d693d 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2358,3 +2358,9 @@ animation effects Off 关 Crop Image 裁剪图像 Remove Crop 移除裁剪 Trim the picture with its handles; Enter or Done applies 用控制柄裁剪图片;按 Enter 或“完成”应用 + Redaction mark appearance 涂黑标记外观 + Outline colour: 轮廓颜色: + Fill colour: 填充颜色: + Shown while the pointer is over a mark. 指针位于标记上方时显示。 + Opacity: 不透明度: + Make properties default 将属性设为默认 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 607b1bbd7..40335829e 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2349,3 +2349,9 @@ animation effects Off 關 Crop Image 裁切影像 Remove Crop 移除裁切 Trim the picture with its handles; Enter or Done applies 用控點裁切圖片;按 Enter 或「完成」套用 + Redaction mark appearance 塗黑標記外觀 + Outline colour: 輪廓色彩: + Fill colour: 填滿色彩: + Shown while the pointer is over a mark. 指標位於標記上方時顯示。 + Opacity: 不透明度: + Make properties default 將內容設為預設 diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index d8ded6f6f..02ee22d4e 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -774,6 +774,10 @@ pub struct PdfCraftApp { pub field_props: Option, pub bulk_field_props: Option, pub redact_prefs: RedactPrefs, + /// The Redaction Tool Properties kept for the next run (what the last OK with "Make + /// properties default" ticked left), and the values before the dialog opened, for Cancel. + pub redact_defaults: RedactPrefs, + pub redact_before: Option, pub redact_pages_draft: RedactPagesDraft, pub redact_search: RedactSearchDraft, pub hidden_draft: HiddenDraft, @@ -1039,6 +1043,8 @@ impl PdfCraftApp { field_props: None, bulk_field_props: None, redact_prefs: RedactPrefs::default(), + redact_defaults: RedactPrefs::default(), + redact_before: None, redact_pages_draft: RedactPagesDraft::default(), redact_search: RedactSearchDraft::default(), hidden_draft: HiddenDraft::default(), @@ -2140,7 +2146,7 @@ impl PdfCraftApp { /// Serialize the user's persistent state (recent files, theme). Local only. pub fn persist(&self) -> String { let trusted: Vec = self.session.trusted_certificates().iter().map(pdfcraft_engine::sign::x509::to_pem).collect(); - serde_json::json!({ + let mut v = serde_json::json!({ "docking": self.docking, "docking_left_open": self.left_open, "docking_right": self.right, @@ -2199,8 +2205,10 @@ impl PdfCraftApp { "combine_zoom": self.combine_zoom, "comments_panel_closed": self.comments_panel_closed, "right_panel_closed": self.right_panel_closed, - }) - .to_string() + }); + // Set apart: one more key inside the macro above passes serde_json's recursion limit. + v["redact_prefs"] = self.redact_defaults.to_json(); + v.to_string() } /// Restore state written by `persist`. Unknown or malformed data is ignored. @@ -2278,6 +2286,10 @@ impl PdfCraftApp { if let Some(size) = v["fill_text_size"].as_f64().and_then(fill_sign::valid_text_size) { self.fill_text_size = size; } + if let Some(prefs) = RedactPrefs::from_json(&v["redact_prefs"]) { + self.redact_prefs = prefs.clone(); + self.redact_defaults = prefs; + } // Settings are untrusted: an unreadable value (a colour that isn't three bytes, say) // fails to deserialize and keeps Off. if let Ok(colors) = serde_json::from_value::(v["reading_colors"].clone()) { diff --git a/crates/ui-egui/src/redact_ui.rs b/crates/ui-egui/src/redact_ui.rs index 8a431a73d..412fc9599 100644 --- a/crates/ui-egui/src/redact_ui.rs +++ b/crates/ui-egui/src/redact_ui.rs @@ -26,6 +26,14 @@ pub struct RedactPrefs { pub codes: Vec<&'static str>, /// Overlay text font, size (0 = auto), colour, alignment and repetition. pub look: pdfcraft_engine::OverlayLook, + /// How an unapplied mark looks on the page: its outline colour (`/C`), the fill shown while + /// the pointer is over it (a preference, not written to the file) and its opacity (`/CA`). + pub outline: Rgb, + pub hover_fill: Rgb, + pub opacity: f64, + /// "Make properties default": OK also keeps these values for the next run. Off, they last + /// for this run only and the saved defaults stay what they were. + pub remember: bool, } impl Default for RedactPrefs { @@ -39,6 +47,10 @@ impl Default for RedactPrefs { code_set: REDACTION_CODE_SETS[0], codes: Vec::new(), look: Default::default(), + outline: [0.89, 0.13, 0.13], + hover_fill: [0.89, 0.13, 0.13], + opacity: 1.0, + remember: true, } } } @@ -57,8 +69,102 @@ impl RedactPrefs { let shape = Shape::Redact { quads, overlay: self.overlay_text(), look: self.look }; let mut style = Style::default_for(&shape); style.fill = self.fill; + style.color = self.outline; + style.opacity = self.opacity.clamp(0.0, 1.0); Edit::AddAnnotation(NewAnnotation { page, shape, style, contents: String::new(), author: author.to_string() }) } + + /// The settings as saved between runs (`"redact_prefs"` in the settings file): the values + /// the dialog's last OK with "Make properties default" ticked left. + pub fn to_json(&self) -> serde_json::Value { + serde_json::json!({ + "fill": self.fill, + "use_overlay": self.use_overlay, + "overlay": self.overlay, + "use_code": self.use_code, + "code_set": self.code_set.id, + "codes": self.codes, + "font": self.look.font.resource(), + "size": self.look.size, + "color": self.look.color, + "align": self.look.align, + "repeat": self.look.repeat, + "outline": self.outline, + "hover_fill": self.hover_fill, + "opacity": self.opacity, + }) + } + + /// Settings are untrusted: each value is taken only when it reads as what it should be; + /// anything else keeps the default. `None` when nothing was saved. + pub fn from_json(v: &serde_json::Value) -> Option { + if !v.is_object() { + return None; + } + let rgb = |x: &serde_json::Value| -> Option { + let a = x.as_array()?; + if a.len() != 3 { + return None; + } + let mut out = [0.0f64; 3]; + for (slot, n) in out.iter_mut().zip(a) { + *slot = n.as_f64().filter(|n| (0.0..=1.0).contains(n))?; + } + Some(out) + }; + let mut d = Self::default(); + // `"fill": null` is a saved "no colour"; a missing or malformed fill keeps the default. + match v.get("fill") { + Some(serde_json::Value::Null) => d.fill = None, + Some(x) => { + if let Some(c) = rgb(x) { + d.fill = Some(c); + } + } + None => {} + } + if let Some(b) = v["use_overlay"].as_bool() { + d.use_overlay = b; + } + if let Some(t) = v["overlay"].as_str() { + d.overlay = t.chars().take(1000).collect(); + } + if let Some(b) = v["use_code"].as_bool() { + d.use_code = b; + } + if let Some(set) = v["code_set"].as_str().and_then(RedactionCodeSet::from_id) { + d.code_set = set; + } + if let Some(codes) = v["codes"].as_array() { + // Only codes of the saved set, each once, in the set's order. + d.codes = d.code_set.codes.iter().copied().filter(|c| codes.iter().any(|x| x.as_str() == Some(c))).collect(); + } + if let Some(f) = v["font"].as_str() { + d.look.font = pdfcraft_engine::OverlayFont::from_resource(f); + } + if let Some(n) = v["size"].as_f64().filter(|n| (0.0..=400.0).contains(n)) { + d.look.size = n; + } + if let Some(c) = rgb(&v["color"]) { + d.look.color = c; + } + if let Some(a) = v["align"].as_u64().filter(|a| *a <= 2) { + d.look.align = a as u8; + } + if let Some(b) = v["repeat"].as_bool() { + d.look.repeat = b; + } + if let Some(c) = rgb(&v["outline"]) { + d.outline = c; + } + if let Some(c) = rgb(&v["hover_fill"]) { + d.hover_fill = c; + } + if let Some(o) = v["opacity"].as_f64().filter(|o| (0.0..=1.0).contains(o)) { + d.opacity = o; + } + Some(d) + } } /// Redact pages: which pages to mark. @@ -327,107 +433,146 @@ pub(crate) fn search_body(ui: &mut egui::Ui, d: &mut SearchDraft, t: &Tokens) -> /// Redaction Tool Properties. Returns (apply, cancel). pub(crate) fn props_body(ui: &mut egui::Ui, d: &mut RedactPrefs, _t: &Tokens) -> (bool, bool) { - ui.set_width(380.0); + ui.set_width(780.0); ui.label(egui::RichText::new(tl!("Redaction Tool Properties")).font(crate::theme::semibold(18.0))); ui.add_space(8.0); - egui::Grid::new("redact-props").num_columns(2).spacing([12.0, 10.0]).show(ui, |ui| { - ui.label(tl!("Redacted area fill colour:")); - ui.horizontal(|ui| { - let mut none = d.fill.is_none(); - if ui.add(widgets::checkbox(&mut none, tl!("No colour"))).changed() { - d.fill = if none { None } else { Some([0.0, 0.0, 0.0]) }; - } - }); - ui.end_row(); - ui.label(""); - if let Some(c) = crate::comments::swatch_grid(ui, d.fill) { - d.fill = Some(c); - } - ui.end_row(); - ui.label(""); - ui.add(widgets::checkbox(&mut d.use_overlay, tl!("Use overlay text"))); - ui.end_row(); - let on = d.use_overlay; - let l = ui.add_enabled(on, widgets::radio(!d.use_code, tl!("Custom text:"))); - if l.clicked() { - d.use_code = false; - } - ui.add_enabled(on && !d.use_code, crate::widgets::line(&mut d.overlay).desired_width(220.0)).labelled_by(l.id); - ui.end_row(); - if ui.add_enabled(on, widgets::radio(d.use_code, tl!("Redaction code:"))).clicked() { - d.use_code = true; - } - let codes_on = on && d.use_code; - ui.add_enabled_ui(codes_on, |ui| { - egui::ComboBox::from_id_salt("redaction-code-set").selected_text(d.code_set.name).show_ui(ui, |ui| { - for set in REDACTION_CODE_SETS { - if ui.selectable_label(d.code_set == set, set.name).clicked() && d.code_set != set { - d.code_set = set; - d.codes.clear(); - } - } - }); - }); - ui.end_row(); - for row in d.code_set.codes.chunks(4) { - ui.label(""); - ui.add_enabled_ui(codes_on, |ui| { + // Two columns, so three colour grids fit a short window: the redacted result and its + // overlay text on the left, the mark's own look (Acrobat's third group) on + // the right. + ui.horizontal_top(|ui| { + ui.vertical(|ui| { + ui.set_width(380.0); + egui::Grid::new("redact-props").num_columns(2).spacing([12.0, 10.0]).show(ui, |ui| { + ui.label(tl!("Redacted area fill colour:")); ui.horizontal(|ui| { - for &code in row { - let mut picked = d.codes.contains(&code); - if ui.add_sized([62.0, 18.0], widgets::checkbox(&mut picked, code)).changed() { - d.codes.retain(|c| *c != code); - if picked { - d.codes.push(code); - } - } + let mut none = d.fill.is_none(); + if ui.add(widgets::checkbox(&mut none, tl!("No colour"))).changed() { + d.fill = if none { None } else { Some([0.0, 0.0, 0.0]) }; } }); - }); - ui.end_row(); - } - let look = &mut d.look; - ui.label(tl!("Font:")); - ui.add_enabled_ui(on, |ui| { - egui::ComboBox::from_id_salt("overlay-font").selected_text(look.font.name()).show_ui(ui, |ui| { - for f in pdfcraft_engine::OverlayFont::ALL { - ui.selectable_value(&mut look.font, f, f.name()); + ui.end_row(); + ui.label(""); + if let Some(c) = crate::comments::swatch_grid(ui, d.fill) { + d.fill = Some(c); } + ui.end_row(); + ui.label(""); + ui.add(widgets::checkbox(&mut d.use_overlay, tl!("Use overlay text"))); + ui.end_row(); + let on = d.use_overlay; + let l = ui.add_enabled(on, widgets::radio(!d.use_code, tl!("Custom text:"))); + if l.clicked() { + d.use_code = false; + } + ui.add_enabled(on && !d.use_code, crate::widgets::line(&mut d.overlay).desired_width(220.0)).labelled_by(l.id); + ui.end_row(); + if ui.add_enabled(on, widgets::radio(d.use_code, tl!("Redaction code:"))).clicked() { + d.use_code = true; + } + let codes_on = on && d.use_code; + ui.add_enabled_ui(codes_on, |ui| { + egui::ComboBox::from_id_salt("redaction-code-set").selected_text(d.code_set.name).show_ui(ui, |ui| { + for set in REDACTION_CODE_SETS { + if ui.selectable_label(d.code_set == set, set.name).clicked() && d.code_set != set { + d.code_set = set; + d.codes.clear(); + } + } + }); + }); + ui.end_row(); + for row in d.code_set.codes.chunks(4) { + ui.label(""); + ui.add_enabled_ui(codes_on, |ui| { + ui.horizontal(|ui| { + for &code in row { + let mut picked = d.codes.contains(&code); + if ui.add_sized([62.0, 18.0], widgets::checkbox(&mut picked, code)).changed() { + d.codes.retain(|c| *c != code); + if picked { + d.codes.push(code); + } + } + } + }); + }); + ui.end_row(); + } + let look = &mut d.look; + ui.label(tl!("Font:")); + ui.add_enabled_ui(on, |ui| { + egui::ComboBox::from_id_salt("overlay-font").selected_text(look.font.name()).show_ui(ui, |ui| { + for f in pdfcraft_engine::OverlayFont::ALL { + ui.selectable_value(&mut look.font, f, f.name()); + } + }); + }); + ui.end_row(); + ui.label(tl!("Font size:")); + ui.add_enabled_ui(on, |ui| { + ui.horizontal(|ui| { + let mut auto = look.size <= 0.0; + if ui.add(widgets::checkbox(&mut auto, tl!("Auto-size text to fit redaction region"))).changed() { + look.size = if auto { 0.0 } else { 10.0 }; + } + if !auto { + ui.add(egui::DragValue::new(&mut look.size).range(2.0..=144.0).suffix(" pt")); + } + }); + }); + ui.end_row(); + ui.label(tl!("Font colour:")); + ui.add_enabled_ui(on, |ui| { + if let Some(c) = crate::comments::swatch_grid(ui, Some(look.color)) { + look.color = c; + } + }); + ui.end_row(); + ui.label(""); + ui.add_enabled(on, widgets::checkbox(&mut look.repeat, tl!("Repeat overlay text"))); + ui.end_row(); + ui.label(tl!("Text alignment:")); + ui.add_enabled_ui(on, |ui| { + ui.horizontal(|ui| { + for (a, label) in [(0u8, tl!("Left")), (1, tl!("Center")), (2, tl!("Right"))] { + widgets::radio_value(ui, &mut look.align, a, label); + } + }); + }); + ui.end_row(); }); }); - ui.end_row(); - ui.label(tl!("Font size:")); - ui.add_enabled_ui(on, |ui| { - ui.horizontal(|ui| { - let mut auto = look.size <= 0.0; - if ui.add(widgets::checkbox(&mut auto, tl!("Auto-size text to fit redaction region"))).changed() { - look.size = if auto { 0.0 } else { 10.0 }; + // No separator here: in a horizontal layout it would stretch to the window's bottom. + ui.add_space(28.0); + ui.vertical(|ui| { + ui.set_width(340.0); + ui.label(egui::RichText::new(tl!("Redaction mark appearance")).strong()); + ui.add_space(6.0); + egui::Grid::new("redact-mark").num_columns(2).spacing([12.0, 10.0]).show(ui, |ui| { + ui.label(tl!("Outline colour:")); + if let Some(c) = crate::comments::swatch_grid(ui, Some(d.outline)) { + d.outline = c; } - if !auto { - ui.add(egui::DragValue::new(&mut look.size).range(2.0..=144.0).suffix(" pt")); + ui.end_row(); + ui.label(tl!("Fill colour:")); + ui.vertical(|ui| { + if let Some(c) = crate::comments::swatch_grid(ui, Some(d.hover_fill)) { + d.hover_fill = c; + } + ui.label(egui::RichText::new(tl!("Shown while the pointer is over a mark.")).weak().size(11.5)); + }); + ui.end_row(); + ui.label(tl!("Opacity:")); + let mut pct = d.opacity * 100.0; + if ui.add(egui::Slider::new(&mut pct, 10.0..=100.0).suffix("%")).changed() { + d.opacity = (pct / 100.0).clamp(0.1, 1.0); } + ui.end_row(); + ui.label(""); + ui.add(widgets::checkbox(&mut d.remember, tl!("Make properties default"))); + ui.end_row(); }); }); - ui.end_row(); - ui.label(tl!("Font colour:")); - ui.add_enabled_ui(on, |ui| { - if let Some(c) = crate::comments::swatch_grid(ui, Some(look.color)) { - look.color = c; - } - }); - ui.end_row(); - ui.label(""); - ui.add_enabled(on, widgets::checkbox(&mut look.repeat, tl!("Repeat overlay text"))); - ui.end_row(); - ui.label(tl!("Text alignment:")); - ui.add_enabled_ui(on, |ui| { - ui.horizontal(|ui| { - for (a, label) in [(0u8, tl!("Left")), (1, tl!("Center")), (2, tl!("Right"))] { - widgets::radio_value(ui, &mut look.align, a, label); - } - }); - }); - ui.end_row(); }); ui.add_space(12.0); buttons(ui, "OK", true) diff --git a/crates/ui-egui/tests/ui_group/redact.rs b/crates/ui-egui/tests/ui_group/redact.rs index 76e075d08..c2d150e11 100644 --- a/crates/ui-egui/tests/ui_group/redact.rs +++ b/crates/ui-egui/tests/ui_group/redact.rs @@ -107,6 +107,95 @@ fn redaction_codes_become_the_overlay_text() { assert_eq!(h.state().redact_prefs.overlay_text(), "CONFIDENTIAL", "custom text is kept while codes are used"); } +/// The Redaction Tool Properties are the next run's too, like every other tool's defaults +/// (Acrobat's "Make Properties Default" without the box): fill, overlay text or codes, the +/// text's look, and the mark's outline colour and opacity all come back from the settings. +#[test] +fn redaction_properties_survive_a_restart() { + let mut h = harness(); + { + let p = &mut h.state_mut().redact_prefs; + p.fill = None; + p.use_overlay = true; + p.use_code = true; + p.code_set = pdfcraft_engine::REDACTION_CODE_SETS[0]; + p.codes = vec!["(b)(6)", "(b)(7)(C)"]; + p.overlay = "CONFIDENTIAL".into(); + p.look.font = pdfcraft_engine::OverlayFont::Courier; + p.look.size = 12.0; + p.look.color = [0.0, 0.0, 1.0]; + p.look.align = 2; + p.look.repeat = true; + p.outline = [0.0, 0.5, 0.0]; + p.hover_fill = [1.0, 1.0, 0.0]; + p.opacity = 0.4; + } + // The defaults are what OK with "Make properties default" ticked leaves: go through the + // dialog. + assert!(h.state_mut().execute("redact.properties")); + h.run_steps(6); + h.get_by_label("Make properties default"); + h.get_by_label("OK").click(); + h.run_steps(2); + let json = h.state().persist(); + let mut fresh = PdfCraftApp::new(); + fresh.restore(&json); + let p = &fresh.redact_prefs; + assert_eq!(p.fill, None, "no colour is a saved choice, not a missing one"); + assert_eq!(p.overlay_text(), "(b)(6), (b)(7)(C)"); + assert_eq!(p.overlay, "CONFIDENTIAL"); + assert_eq!(p.look.font, pdfcraft_engine::OverlayFont::Courier); + assert_eq!((p.look.size, p.look.color, p.look.align, p.look.repeat), (12.0, [0.0, 0.0, 1.0], 2, true)); + assert_eq!((p.outline, p.hover_fill, p.opacity), ([0.0, 0.5, 0.0], [1.0, 1.0, 0.0], 0.4)); + // The next mark takes the outline and opacity. + let pdfcraft_engine::Edit::AddAnnotation(a) = p.mark(0, vec![pdfcraft_engine::rect_quad([10.0, 10.0, 50.0, 30.0])], "T") else { + panic!("a mark adds an annotation") + }; + assert_eq!((a.style.color, a.style.opacity, a.style.fill), ([0.0, 0.5, 0.0], 0.4, None)); + // Settings are untrusted: a bad colour, an unknown code set and a code outside the set are + // dropped, the rest kept. + let mut odd = PdfCraftApp::new(); + odd.restore( + r#"{"redact_prefs":{"outline":[2,0,0],"opacity":0.7,"code_set":"nope","codes":["(b)(6)","bogus"],"use_overlay":true,"use_code":true}}"#, + ); + let p = &odd.redact_prefs; + assert_eq!((p.outline, p.opacity), ([0.89, 0.13, 0.13], 0.7)); + assert_eq!(p.code_set.id, pdfcraft_engine::REDACTION_CODE_SETS[0].id); + assert_eq!(p.overlay_text(), "(b)(6)"); +} + +/// "Make properties default" off: OK keeps the values for this run only, the saved defaults +/// stay what they were; Cancel puts back what the dialog opened with. +#[test] +fn redaction_properties_for_this_run_only_and_cancel() { + let mut h = harness(); + assert!(h.state_mut().execute("redact.properties")); + h.run_steps(6); + h.get_by_label("Redaction mark appearance"); + h.get_by_label("Outline colour:"); + h.get_by_label("Fill colour:"); + h.get_by_label("Opacity:"); + h.get_by_label("Make properties default").click(); + h.run_steps(3); + assert!(!h.state().redact_prefs.remember); + h.state_mut().redact_prefs.outline = [0.0, 0.0, 1.0]; + h.get_by_label("OK").click(); + h.run_steps(2); + assert_eq!(h.state().redact_prefs.outline, [0.0, 0.0, 1.0], "used for this run"); + assert_eq!(h.state().redact_defaults.outline, [0.89, 0.13, 0.13], "not made the default"); + let mut fresh = PdfCraftApp::new(); + fresh.restore(&h.state().persist()); + assert_eq!(fresh.redact_prefs.outline, [0.89, 0.13, 0.13], "the next run starts from the saved defaults"); + // Cancel discards the edits made while the dialog was open. + assert!(h.state_mut().execute("redact.properties")); + h.run_steps(2); + h.state_mut().redact_prefs.opacity = 0.2; + h.get_by_label("Cancel").click(); + h.run_steps(2); + assert_eq!(h.state().redact_prefs.opacity, 1.0, "cancelled"); + assert_eq!(h.state().redact_prefs.outline, [0.0, 0.0, 1.0], "the run's values stay"); +} + #[test] fn redacting_an_imported_word_list() { let memo = pdfcraft_engine::Session::new().create_from_text("memo", "Call Ada today\nAda is private\nPublic line").unwrap().to_vec(); diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 2df880e85..00bbaf401 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -5653,11 +5653,11 @@ title = "Redaction fill colour and overlay text" area = "G" tier = "P0" milestone = "M8" -status = "partial" +status = "shipped" commands = ["redact.properties"] tools = ["redact_mark"] -evidence = ["crates/ui-egui/tests/ui_group/redact.rs::marking_applying_and_clearing", "crates/redact/src/tests.rs::overlay_text_takes_its_font_size_colour_alignment_and_repeats"] -notes = "fill colour or none; overlay text with font, size or auto-size, colour, alignment and repeat, or redaction codes (protect.redaction-codes)" +evidence = ["crates/ui-egui/tests/ui_group/redact.rs::marking_applying_and_clearing", "crates/ui-egui/tests/ui_group/redact.rs::redaction_properties_survive_a_restart", "crates/ui-egui/tests/ui_group/redact.rs::redaction_properties_for_this_run_only_and_cancel", "crates/redact/src/tests.rs::overlay_text_takes_its_font_size_colour_alignment_and_repeats"] +notes = "Acrobat's three groups: redacted area fill colour or none; overlay text with font, size or auto-size, colour, alignment and repeat, or redaction codes (protect.redaction-codes); Redaction mark appearance: outline colour (/C), fill colour shown while the pointer is over a mark (a preference, not written), opacity (/CA). Make properties default: ticked, OK keeps the values for the next run (settings \"redact_prefs\"); unticked, this run only and the saved defaults stay; Cancel puts back what the dialog opened with." [[feature]] id = "protect.redaction-codes"