mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 14:41:51 +00:00
Redact: Undo past a saved Apply redactions makes Save ask where
Undo of an Apply redactions step that was already saved brought back the redacted content, and Save then wrote it in place over the _Redacted file. The view now counts the saved steps that Undo takes back, and Save asks where while any are undone. A new Apply redactions made while saved steps are undone clears Redo, so Save keeps asking until the next save or Revert.
This commit is contained in:
@@ -455,6 +455,13 @@ pub struct DocView {
|
||||
/// desktop save set it to 0; after a desktop save the document has the name it was saved
|
||||
/// under. A web download keeps it, so later downloads carry the `_Redacted` name.
|
||||
pub redactions_applied: usize,
|
||||
/// How many Apply redactions steps saved into the document's file are undone in this view.
|
||||
/// While it is above 0, Save asks where, so the restored content is not written over the
|
||||
/// redacted file. Redo takes one off. Revert, Reload and a desktop save set it to 0.
|
||||
pub redactions_undone_after_save: usize,
|
||||
/// Set when Apply redactions runs while saved Apply redactions steps are undone: Redo can no
|
||||
/// longer bring those steps back, so Save asks where until a desktop save or Revert clears it.
|
||||
pub saved_redactions_cut_off: bool,
|
||||
/// A crop rectangle being dragged (Crop tool).
|
||||
pub crop_drag: crate::crop::CropDrag,
|
||||
/// Enhance ▸ Camera image: the quadrilateral being placed.
|
||||
@@ -718,6 +725,8 @@ impl DocView {
|
||||
redact_drag: None,
|
||||
pending_redaction: None,
|
||||
redactions_applied: 0,
|
||||
redactions_undone_after_save: 0,
|
||||
saved_redactions_cut_off: false,
|
||||
crop_drag: None,
|
||||
camera: None,
|
||||
camera_done: None,
|
||||
|
||||
@@ -1685,6 +1685,11 @@ fn finish_redactions_and_fields(app: &mut PdfCraftApp, req: &mut Pending) {
|
||||
if app.apply_edit(Edit::ApplyRedactions { pages: None }) {
|
||||
if let Some(view) = app.active.and_then(|i| app.views.get_mut(i)) {
|
||||
view.redactions_applied = view.redactions_applied.saturating_add(1);
|
||||
// A new step clears the redo history, so the undone saved steps can't come back.
|
||||
if view.redactions_undone_after_save > 0 {
|
||||
view.redactions_undone_after_save = 0;
|
||||
view.saved_redactions_cut_off = true;
|
||||
}
|
||||
}
|
||||
if marks == 1 {
|
||||
app.notify_tr("Applied 1 redaction mark. Save to remove the content from the file.");
|
||||
|
||||
@@ -221,8 +221,15 @@ impl PdfCraftApp {
|
||||
}
|
||||
self.views[i].comments.selected = None;
|
||||
if label == (Edit::ApplyRedactions { pages: None }).label() {
|
||||
let applied = &mut self.views[i].redactions_applied;
|
||||
*applied = if undo { applied.saturating_sub(1) } else { applied.saturating_add(1) };
|
||||
let view = &mut self.views[i];
|
||||
let (applied, undone) = (view.redactions_applied, view.redactions_undone_after_save);
|
||||
// Undo past the last save counts as undone; Redo first takes those back.
|
||||
match (undo, applied, undone) {
|
||||
(true, 0, _) => view.redactions_undone_after_save = undone.saturating_add(1),
|
||||
(true, _, _) => view.redactions_applied = applied.saturating_sub(1),
|
||||
(false, _, 0) => view.redactions_applied = applied.saturating_add(1),
|
||||
(false, _, _) => view.redactions_undone_after_save = undone.saturating_sub(1),
|
||||
}
|
||||
}
|
||||
let label = crate::i18n::edit_label(&label);
|
||||
if undo {
|
||||
|
||||
@@ -56,6 +56,8 @@ impl PdfCraftApp {
|
||||
view.pending_edit = None;
|
||||
// The redactions applied since the last save are discarded too.
|
||||
view.redactions_applied = 0;
|
||||
view.redactions_undone_after_save = 0;
|
||||
view.saved_redactions_cut_off = false;
|
||||
}
|
||||
self.notify_tr("Reverted to the last saved version");
|
||||
}
|
||||
|
||||
@@ -51,7 +51,10 @@ impl PdfCraftApp {
|
||||
// After Apply redactions, Save asks where and proposes the name with `_Redacted`.
|
||||
let redacted = self.views.get(index).is_some_and(|v| v.redactions_applied > 0);
|
||||
let name = if redacted { pdfcraft_engine::redacted_file_name(&name) } else { name };
|
||||
let target = if redacted { SaveTarget::As } else { target };
|
||||
// Undo past a saved Apply redactions brings back content that the file on disk has
|
||||
// redacted: ask where instead of writing it over that file.
|
||||
let unredacted = self.views.get(index).is_some_and(|v| v.redactions_undone_after_save > 0 || v.saved_redactions_cut_off);
|
||||
let target = if redacted || unredacted { SaveTarget::As } else { target };
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let doc_path = path.clone();
|
||||
@@ -205,6 +208,8 @@ impl PdfCraftApp {
|
||||
// Saving over a version that changed on disk keeps this one: nothing to ask.
|
||||
view.disk_changed = false;
|
||||
view.redactions_applied = 0;
|
||||
view.redactions_undone_after_save = 0;
|
||||
view.saved_redactions_cut_off = false;
|
||||
}
|
||||
self.notify_fmt("Saved {name}", &[("name", &short_name(dest))]);
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
@@ -118,6 +118,117 @@ fn saving_after_applying_redactions_asks_where() {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Undo past an Apply redactions that is already saved in the `_Redacted` copy makes Save ask
|
||||
/// where, so the restored content is not written over the copy. Redo makes Save write in place.
|
||||
#[test]
|
||||
fn undoing_saved_redactions_does_not_save_over_the_redacted_copy() {
|
||||
let dir = std::env::temp_dir().join(format!("pdfcraft-redacted-saved-undo-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let original = dir.join("contract.pdf");
|
||||
std::fs::write(&original, include_bytes!("../data/form.pdf")).unwrap();
|
||||
let path = original.to_string_lossy().into_owned();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| {
|
||||
let mut app = PdfCraftApp::new();
|
||||
app.set_option("language", "en").unwrap();
|
||||
app.open_path(&path);
|
||||
app
|
||||
});
|
||||
crate::support::settle(&mut h);
|
||||
h.state_mut().execute("redact.pages");
|
||||
h.run_steps(2);
|
||||
h.get_by_label("OK").click();
|
||||
h.run_steps(3);
|
||||
assert!(h.state_mut().execute("redact.apply"));
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Apply").click();
|
||||
h.run_steps(4);
|
||||
let copy = dir.join("contract_Redacted.pdf");
|
||||
h.state_mut().pick_override = Some(vec![copy.to_string_lossy().into_owned()]);
|
||||
assert!(!h.state_mut().save_active(SaveTarget::InPlace));
|
||||
h.run_steps(3);
|
||||
h.state_mut().pick_override = None;
|
||||
assert!(copy.exists());
|
||||
// Undo and Redo leave the saved redactions in effect: Save writes the copy in place.
|
||||
h.state_mut().execute("edit.undo");
|
||||
h.run_steps(3);
|
||||
h.state_mut().execute("edit.redo");
|
||||
h.run_steps(3);
|
||||
assert_eq!((h.state().views[0].redactions_applied, h.state().views[0].redactions_undone_after_save), (0, 0));
|
||||
assert!(h.state_mut().save_active(SaveTarget::InPlace), "with the redactions back, Save writes the copy in place");
|
||||
let saved = std::fs::read(©).unwrap();
|
||||
h.state_mut().execute("edit.undo");
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().views[0].redactions_undone_after_save, 1);
|
||||
let elsewhere = dir.join("contract_restored.pdf");
|
||||
h.state_mut().pick_override = Some(vec![elsewhere.to_string_lossy().into_owned()]);
|
||||
assert!(!h.state_mut().save_active(SaveTarget::InPlace), "Save asks where instead of writing over the copy");
|
||||
h.run_steps(3);
|
||||
h.state_mut().pick_override = None;
|
||||
assert_eq!(std::fs::read(©).unwrap(), saved, "the redacted copy is left as it was");
|
||||
assert!(elsewhere.exists(), "the restored version is written where the user chose");
|
||||
assert_eq!(h.state().views[0].redactions_undone_after_save, 0);
|
||||
// The restored version is now the saved one: Redo applies the redactions to it again.
|
||||
h.state_mut().execute("edit.redo");
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().views[0].redactions_applied, 1);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// A new Apply redactions after Undo past a saved one clears Redo, so Undo and Redo of the new
|
||||
/// step do not bring Save in place back over the `_Redacted` copy.
|
||||
#[test]
|
||||
fn redoing_a_new_apply_after_undoing_saved_redactions_still_asks_where() {
|
||||
let dir = std::env::temp_dir().join(format!("pdfcraft-redacted-branch-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let original = dir.join("contract.pdf");
|
||||
std::fs::write(&original, include_bytes!("../data/form.pdf")).unwrap();
|
||||
let path = original.to_string_lossy().into_owned();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| {
|
||||
let mut app = PdfCraftApp::new();
|
||||
app.set_option("language", "en").unwrap();
|
||||
app.open_path(&path);
|
||||
app
|
||||
});
|
||||
crate::support::settle(&mut h);
|
||||
h.state_mut().execute("redact.pages");
|
||||
h.run_steps(2);
|
||||
h.get_by_label("OK").click();
|
||||
h.run_steps(3);
|
||||
assert!(h.state_mut().execute("redact.apply"));
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Apply").click();
|
||||
h.run_steps(4);
|
||||
let copy = dir.join("contract_Redacted.pdf");
|
||||
h.state_mut().pick_override = Some(vec![copy.to_string_lossy().into_owned()]);
|
||||
assert!(!h.state_mut().save_active(SaveTarget::InPlace));
|
||||
h.run_steps(3);
|
||||
h.state_mut().pick_override = None;
|
||||
let saved = std::fs::read(©).unwrap();
|
||||
h.state_mut().execute("edit.undo");
|
||||
h.run_steps(3);
|
||||
// Apply the marks again as a new step, then Undo and Redo it.
|
||||
assert!(h.state_mut().execute("redact.apply"));
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Apply").click();
|
||||
h.run_steps(4);
|
||||
h.state_mut().execute("edit.undo");
|
||||
h.run_steps(3);
|
||||
let view = &h.state().views[0];
|
||||
assert_eq!((view.redactions_applied, view.redactions_undone_after_save, view.saved_redactions_cut_off), (0, 0, true));
|
||||
h.state_mut().execute("edit.redo");
|
||||
h.run_steps(3);
|
||||
let elsewhere = dir.join("contract_again.pdf");
|
||||
h.state_mut().pick_override = Some(vec![elsewhere.to_string_lossy().into_owned()]);
|
||||
assert!(!h.state_mut().save_active(SaveTarget::InPlace), "Save asks where instead of writing over the copy");
|
||||
h.run_steps(3);
|
||||
h.state_mut().pick_override = None;
|
||||
assert_eq!(std::fs::read(©).unwrap(), saved, "the redacted copy is left as it was");
|
||||
assert!(!h.state().views[0].saved_redactions_cut_off);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Undo of Apply redactions brings back Save in place; Redo makes Save ask where again.
|
||||
#[test]
|
||||
fn undoing_apply_redactions_saves_in_place_again() {
|
||||
|
||||
Reference in New Issue
Block a user