mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
Add 'Replace pages…' action to the Organize Pages toolbar, organize grid thumbnail context menu, and Pages panel context menu to replace selected pages. - Add ViewAction::ReplacePages triggering replace_pages_dialog() - Expose 'Replace pages…' button with 'replace' icon on the organize toolbar - Expose 'Replace pages…' on the organize grid and Pages panel thumbnail context menus - Update parity evidence for organize.replace-pages - Add integration tests for toolbar and context menu replace actions in crates/ui-egui/tests/editing.rs Co-authored-by: KrzysztofC <krzysztofc@localhost-live.lan>
This commit is contained in:
co-authored by
KrzysztofC
parent
036e00f2a1
commit
38df401687
@@ -64,13 +64,12 @@ impl Session {
|
||||
let (mut ra, mut rb) = (renderer(a), renderer(b));
|
||||
let scale = dpi.clamp(18.0, 150.0) / 72.0;
|
||||
let mut out = Vec::new();
|
||||
for page in 0..a.info.pages.len().min(b.info.pages.len()) {
|
||||
for (page, info) in b.info.pages.iter().enumerate().take(a.info.pages.len()) {
|
||||
let req = pdfcraft_render::RenderRequest { page, scale, ..Default::default() };
|
||||
let (x, y) = (ra.render(req), rb.render(req));
|
||||
if x.error.is_some() || y.error.is_some() {
|
||||
continue;
|
||||
}
|
||||
let info = &b.info.pages[page];
|
||||
let s = y.width as f32 / info.width.max(1e-3);
|
||||
for r in pdfcraft_compare::visual_regions((&y.rgba, y.width, y.height), (&x.rgba, x.width, x.height), 24) {
|
||||
let p = info.view_to_user(r[0] as f32 / s, r[1] as f32 / s);
|
||||
|
||||
@@ -499,6 +499,8 @@ pub enum ViewAction {
|
||||
InsertFromFile,
|
||||
/// Insert files at a gap of the page grid (0 = before the first page).
|
||||
InsertFromFileAt(usize),
|
||||
/// Replace the selected pages with pages from another file.
|
||||
ReplacePages,
|
||||
/// Save the document as the grid shows it.
|
||||
Save,
|
||||
Extract,
|
||||
@@ -3805,6 +3807,9 @@ fn organize_toolbar(view: &mut DocView, info: &DocInfo, editable: bool, dirty: b
|
||||
if icons::button(ui, "file-input", 30.0, false, tl!("Insert pages from a file…")).clicked() {
|
||||
view.pending_action = Some(ViewAction::InsertFromFile);
|
||||
}
|
||||
if icons::button(ui, "replace", 30.0, false, tl!("Replace pages…")).clicked() {
|
||||
view.pending_action = Some(ViewAction::ReplacePages);
|
||||
}
|
||||
if icons::button(ui, "file-output", 30.0, false, tl!("Extract pages to a new document")).clicked() {
|
||||
view.pending_action = Some(ViewAction::Extract);
|
||||
}
|
||||
@@ -4085,12 +4090,21 @@ fn organize_grid(view: &mut DocView, info: &DocInfo, editable: bool, dirty: bool
|
||||
if resp.double_clicked() {
|
||||
open_page = Some(i);
|
||||
}
|
||||
// Right-click: Cut, Copy, Paste (on the selection, or this page).
|
||||
// Right-click: Extract, Replace, Cut, Copy, Paste (on the selection, or this page).
|
||||
resp.context_menu(|ui| {
|
||||
if !view.selected.contains(&i) {
|
||||
view.selected = [i].into();
|
||||
view.current = i;
|
||||
}
|
||||
if ui.button(tl!("Extract pages")).clicked() {
|
||||
view.pending_action = Some(ViewAction::Extract);
|
||||
ui.close();
|
||||
}
|
||||
if ui.add_enabled(editable, egui::Button::new(tl!("Replace pages…"))).clicked() {
|
||||
view.pending_action = Some(ViewAction::ReplacePages);
|
||||
ui.close();
|
||||
}
|
||||
ui.separator();
|
||||
if ui.add_enabled(editable, egui::Button::new(tl!("Cut"))).clicked() {
|
||||
view.pending_action = Some(ViewAction::CopyPages { cut: true });
|
||||
ui.close();
|
||||
|
||||
@@ -204,6 +204,7 @@ impl PdfCraftApp {
|
||||
match self.views.get_mut(i).and_then(|v| v.pending_action.take()) {
|
||||
Some(crate::canvas::ViewAction::InsertFromFile) => self.insert_from_file_dialog(),
|
||||
Some(crate::canvas::ViewAction::InsertFromFileAt(at)) => self.insert_from_file_at(Some(at)),
|
||||
Some(crate::canvas::ViewAction::ReplacePages) => self.replace_pages_dialog(),
|
||||
Some(crate::canvas::ViewAction::Save) => {
|
||||
self.save_active(SaveTarget::InPlace);
|
||||
}
|
||||
|
||||
@@ -1045,6 +1045,10 @@ fn pages(ui: &mut egui::Ui, t: &Tokens, info: &DocInfo, view: &mut crate::DocVie
|
||||
view.pending_action = Some(crate::canvas::ViewAction::Extract);
|
||||
ui.close();
|
||||
}
|
||||
if ui.add_enabled(editable, egui::Button::new(tl!("Replace pages…"))).clicked() {
|
||||
view.pending_action = Some(crate::canvas::ViewAction::ReplacePages);
|
||||
ui.close();
|
||||
}
|
||||
ui.separator();
|
||||
if ui.add_enabled(editable, egui::Button::new(tl!("Cut"))).clicked() {
|
||||
view.pending_action = Some(crate::canvas::ViewAction::CopyPages { cut: true });
|
||||
|
||||
@@ -196,7 +196,7 @@ impl PdfCraftApp {
|
||||
let mut r = pdfcraft_render::PageRenderer::new(doc.bytes.clone(), config);
|
||||
let mut edits = Vec::new();
|
||||
let author = self.comment_prefs.author.clone();
|
||||
for page in 0..doc.info.pages.len() {
|
||||
for (page, page_info) in doc.info.pages.iter().enumerate() {
|
||||
let out = r.render(pdfcraft_render::RenderRequest { page, kind: pdfcraft_render::RequestKind::Text, scale: 1.0, ..Default::default() });
|
||||
let Some(text) = out.text else { continue };
|
||||
let hits = match d.mode {
|
||||
@@ -204,7 +204,7 @@ impl PdfCraftApp {
|
||||
_ => needles.iter().flat_map(|w| text.find(w)).collect(),
|
||||
};
|
||||
for h in hits {
|
||||
let quads: Vec<[f64; 8]> = text.line_rects(h).into_iter().map(|r| doc.info.pages[page].view_rect_to_quad(r)).collect();
|
||||
let quads: Vec<[f64; 8]> = text.line_rects(h).into_iter().map(|r| page_info.view_rect_to_quad(r)).collect();
|
||||
if !quads.is_empty() {
|
||||
edits.push(self.redact_prefs.mark(page, quads, &author));
|
||||
}
|
||||
|
||||
@@ -1324,6 +1324,84 @@ fn extract_button_copies_selected_pages_to_a_new_tab() {
|
||||
assert!(!app.session.get(app.views[0].id).unwrap().dirty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_button_replaces_selected_pages_in_organize() {
|
||||
let dir = temp_path("replace-btn");
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let other = dir.join("other.pdf");
|
||||
std::fs::write(&other, fixture(5)).unwrap();
|
||||
|
||||
let mut h = organize(3);
|
||||
h.get_by_label("Page 2").click();
|
||||
h.run_steps(1);
|
||||
h.get_by_label("Page 3").click_modifiers(Modifiers::COMMAND);
|
||||
h.run_steps(2);
|
||||
h.state_mut().pick_override = Some(vec![other.to_string_lossy().into_owned()]);
|
||||
h.get_by_label("Replace pages…").click();
|
||||
h.run_steps(3);
|
||||
egui_kittest::kittest::Queryable::get_by_label(&h, "Replace Pages");
|
||||
assert_eq!(h.state().replace_draft.as_ref().unwrap().from, 2);
|
||||
assert_eq!(h.state().replace_draft.as_ref().unwrap().to, 3);
|
||||
egui_kittest::kittest::Queryable::get_by_label(&h, "OK").click();
|
||||
h.run_steps(3);
|
||||
let s = h.state();
|
||||
let doc = s.session.get(s.views[0].id).unwrap();
|
||||
assert_eq!(doc.info.pages.len(), 3);
|
||||
assert_eq!(doc.can_undo(), Some("Replace pages"));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_pages_from_thumbnail_context_menu() {
|
||||
let dir = temp_path("replace-ctx");
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let other = dir.join("other.pdf");
|
||||
std::fs::write(&other, fixture(5)).unwrap();
|
||||
|
||||
let mut h = organize(3);
|
||||
h.state_mut().pick_override = Some(vec![other.to_string_lossy().into_owned()]);
|
||||
h.get_by_label("Page 2").click_button(egui::PointerButton::Secondary);
|
||||
h.run_steps(2);
|
||||
h.get_all_by_label("Replace pages…").last().unwrap().click();
|
||||
h.run_steps(3);
|
||||
egui_kittest::kittest::Queryable::get_by_label(&h, "Replace Pages");
|
||||
assert_eq!(h.state().replace_draft.as_ref().unwrap().from, 2);
|
||||
assert_eq!(h.state().replace_draft.as_ref().unwrap().to, 2);
|
||||
egui_kittest::kittest::Queryable::get_by_label(&h, "OK").click();
|
||||
h.run_steps(3);
|
||||
let s = h.state();
|
||||
let doc = s.session.get(s.views[0].id).unwrap();
|
||||
assert_eq!(doc.info.pages.len(), 3);
|
||||
assert_eq!(doc.can_undo(), Some("Replace page"));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_pages_from_pages_panel_context_menu() {
|
||||
let dir = temp_path("replace-panel-ctx");
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let other = dir.join("other.pdf");
|
||||
std::fs::write(&other, fixture(5)).unwrap();
|
||||
|
||||
let mut h = harness(4, |app| app.right = Some(pdfcraft_ui_egui::RightPanel::Pages));
|
||||
h.state_mut().pick_override = Some(vec![other.to_string_lossy().into_owned()]);
|
||||
let thumb = h.get_all_by_label("Page 2").max_by(|a, b| a.rect().left().total_cmp(&b.rect().left())).unwrap();
|
||||
thumb.click_button(egui::PointerButton::Secondary);
|
||||
h.run_steps(2);
|
||||
h.get_all_by_label("Replace pages…").last().unwrap().click();
|
||||
h.run_steps(3);
|
||||
egui_kittest::kittest::Queryable::get_by_label(&h, "Replace Pages");
|
||||
assert_eq!(h.state().replace_draft.as_ref().unwrap().from, 2);
|
||||
assert_eq!(h.state().replace_draft.as_ref().unwrap().to, 2);
|
||||
egui_kittest::kittest::Queryable::get_by_label(&h, "OK").click();
|
||||
h.run_steps(3);
|
||||
let s = h.state();
|
||||
let doc = s.session.get(s.views[0].id).unwrap();
|
||||
assert_eq!(doc.info.pages.len(), 4);
|
||||
assert_eq!(doc.can_undo(), Some("Replace page"));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inserting_a_file_goes_after_the_selection_and_undoes() {
|
||||
let mut h = organize(2);
|
||||
|
||||
@@ -2770,7 +2770,7 @@ milestone = "M4"
|
||||
status = "shipped"
|
||||
commands = ["page.replace"]
|
||||
tools = ["page_replace"]
|
||||
evidence = ["crates/organize/src/tests.rs::replace_pages_swaps_content_but_keeps_annotations", "crates/ui-egui/tests/editing.rs::replace_pages_dialog_swaps_page_content", "crates/automation/tests/automation.rs::replacing_pages_through_tools"]
|
||||
evidence = ["crates/organize/src/tests.rs::replace_pages_swaps_content_but_keeps_annotations", "crates/ui-egui/tests/editing.rs::replace_pages_dialog_swaps_page_content", "crates/ui-egui/tests/editing.rs::replace_button_replaces_selected_pages_in_organize", "crates/automation/tests/automation.rs::replacing_pages_through_tools"]
|
||||
notes = "content, resources and boxes replaced; links, comments, fields and bookmarks kept"
|
||||
|
||||
[[feature]]
|
||||
|
||||
Reference in New Issue
Block a user