diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 1af2a6a4f..3b8ae81fd 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -665,6 +665,8 @@ pub enum ViewAction { Save, Extract, Split, + /// Split before each selected page: opens Split on that mode. + SplitBeforeSelected, /// Copy the selected pages (Cut also deletes them). CopyPages { cut: bool, @@ -5489,6 +5491,10 @@ fn organize_grid(view: &mut DocView, info: &DocInfo, editable: bool, dirty: bool view.pending_action = Some(ViewAction::Extract); ui.close(); } + if ui.button(tl!("Split document…")).clicked() { + view.pending_action = Some(ViewAction::SplitBeforeSelected); + ui.close(); + } if ui.add_enabled(editable, egui::Button::new(tl!("Replace pages…"))).clicked() { view.pending_action = Some(ViewAction::ReplacePages); ui.close(); diff --git a/crates/ui-egui/src/editing.rs b/crates/ui-egui/src/editing.rs index 2fdc2e9d0..7a311183c 100644 --- a/crates/ui-egui/src/editing.rs +++ b/crates/ui-egui/src/editing.rs @@ -265,6 +265,10 @@ impl PdfCraftApp { } Some(crate::canvas::ViewAction::Extract) => self.open_extract_dialog(), Some(crate::canvas::ViewAction::Split) => self.dialog = Some(crate::Dialog::Split), + Some(crate::canvas::ViewAction::SplitBeforeSelected) => { + self.split_draft.mode = crate::SplitMode::Selection; + self.dialog = Some(crate::Dialog::Split); + } Some(crate::canvas::ViewAction::CopyPages { cut }) => self.copy_pages(cut), Some(crate::canvas::ViewAction::PastePages) => self.paste_pages(), Some(crate::canvas::ViewAction::EnhanceCamera) => self.enhance_camera(i), diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index 7d16bc340..e5dd30a8c 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -1410,6 +1410,13 @@ 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.button(tl!("Split document…")).clicked() { + if view.selected.is_empty() { + view.select_pages(&[i]); + } + view.pending_action = Some(crate::canvas::ViewAction::SplitBeforeSelected); + ui.close(); + } if ui.add_enabled(editable, egui::Button::new(tl!("Replace pages…"))).clicked() { view.pending_action = Some(crate::canvas::ViewAction::ReplacePages); ui.close(); diff --git a/crates/ui-egui/tests/it/editing.rs b/crates/ui-egui/tests/it/editing.rs index 73464d33e..31c56ca79 100644 --- a/crates/ui-egui/tests/it/editing.rs +++ b/crates/ui-egui/tests/it/editing.rs @@ -2241,6 +2241,88 @@ fn split_before_selected_pages() { let _ = std::fs::remove_dir_all(dir); } +#[test] +fn split_from_organize_grid_context_menu() { + let dir = temp_path("split-org-ctx"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let mut h = organize(4); + h.state_mut().export_dir_override = Some(dir.to_string_lossy().into_owned()); + h.get_by_label("Page 3").click_button(egui::PointerButton::Secondary); + h.run_steps(2); + h.get_all_by_label("Split document…").last().unwrap().click(); + h.run_steps(3); + assert_eq!(h.state().dialog, Some(pdfcraft_ui_egui::Dialog::Split)); + assert_eq!(h.state().split_draft.mode, pdfcraft_ui_egui::SplitMode::Selection); + h.get_by_label_contains("Creates 2 files from 4 pages"); + h.get_all_by_label("Split").last().unwrap().click(); + h.run_steps(3); + let mut names: Vec = std::fs::read_dir(&dir).unwrap().map(|e| e.unwrap().file_name().to_string_lossy().into_owned()).collect(); + names.sort(); + assert_eq!(names, ["doc (pages 1-2).pdf", "doc (pages 3-4).pdf"]); + let _ = std::fs::remove_dir_all(dir); +} + +#[test] +fn split_from_pages_panel_context_menu() { + let mut h = harness(4, |app| app.right = Some(pdfcraft_ui_egui::RightPanel::Pages)); + let thumb = h.get_all_by_label("Page 3").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("Split document…").last().unwrap().click(); + h.run_steps(3); + assert_eq!(h.state().dialog, Some(pdfcraft_ui_egui::Dialog::Split)); + assert_eq!(h.state().split_draft.mode, pdfcraft_ui_egui::SplitMode::Selection); + h.get_by_label_contains("Creates 2 files from 4 pages"); +} + +#[test] +fn split_from_pages_panel_context_menu_on_current_page_without_selection() { + let mut h = harness(4, |app| app.right = Some(pdfcraft_ui_egui::RightPanel::Pages)); + // Navigate to Page 2 (0-indexed 1) so it is current, but keep selected empty + h.state_mut().views[0].go_to_page(1); + h.state_mut().views[0].selected.clear(); + h.run_steps(2); + assert_eq!(h.state().views[0].current, 1); + assert!(h.state().views[0].selected.is_empty()); + 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("Split document…").last().unwrap().click(); + h.run_steps(3); + assert_eq!(h.state().dialog, Some(pdfcraft_ui_egui::Dialog::Split)); + assert_eq!(h.state().split_draft.mode, pdfcraft_ui_egui::SplitMode::Selection); + h.get_by_label_contains("Creates 2 files from 4 pages"); +} + +#[test] +fn split_from_pages_panel_context_menu_on_page_one_falls_back_to_pages() { + let mut h = harness(4, |app| app.right = Some(pdfcraft_ui_egui::RightPanel::Pages)); + let thumb = h.get_all_by_label("Page 1").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("Split document…").last().unwrap().click(); + h.run_steps(3); + assert_eq!(h.state().dialog, Some(pdfcraft_ui_egui::Dialog::Split)); + assert_eq!(h.state().split_draft.mode, pdfcraft_ui_egui::SplitMode::Pages); +} + +#[test] +fn split_from_pages_panel_context_menu_with_multi_selection() { + let mut h = harness(4, |app| app.right = Some(pdfcraft_ui_egui::RightPanel::Pages)); + // Select pages 2 and 4 (indices 1 and 3) + h.state_mut().views[0].select_pages(&[1, 3]); + h.run_steps(2); + 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("Split document…").last().unwrap().click(); + h.run_steps(3); + assert_eq!(h.state().dialog, Some(pdfcraft_ui_egui::Dialog::Split)); + assert_eq!(h.state().split_draft.mode, pdfcraft_ui_egui::SplitMode::Selection); + h.get_by_label_contains("Creates 3 files from 4 pages"); +} + // ── Encrypted documents ─────────────────────────────────────────────────────────────────────── fn protected(user: &str, owner: &str, permissions: i32) -> Vec { diff --git a/crates/ui-egui/tests/it/viewer.rs b/crates/ui-egui/tests/it/viewer.rs index a3c86fc52..e4a18ba5e 100644 --- a/crates/ui-egui/tests/it/viewer.rs +++ b/crates/ui-egui/tests/it/viewer.rs @@ -1393,7 +1393,7 @@ fn pages_panel_thumbnails_have_a_context_menu() { h.run_steps(2); // The page right-clicked becomes the selection the menu acts on. assert_eq!(h.state().views[0].target_pages(), vec![2]); - for item in ["Extract pages", "Cut", "Paste after"] { + for item in ["Extract pages", "Split document…", "Cut", "Paste after"] { h.get_by_label(item); } h.get_by_label("Copy").click(); diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 09b84c00d..f2bd97cd6 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -1274,8 +1274,8 @@ milestone = "M4" status = "shipped" commands = [] tools = [] -evidence = ["crates/ui-egui/tests/it/editing.rs::dragging_pages_panel_thumbnails_reorders_pages", "crates/ui-egui/tests/it/viewer.rs::pages_panel_thumbnails_have_a_context_menu", "crates/ui-egui/tests/it/editing.rs::deleting_pages_from_the_pages_panel_menu", "crates/ui-egui/tests/it/editing.rs::the_delete_key_deletes_pages_picked_in_the_pages_panel_but_never_every_page"] -notes = "Drag the selection (or the page grabbed) to reorder, with a drop bar and edge scrolling; right-click offers Extract pages, Cut, Copy, Paste after and Delete pages (also the Delete key over the panel). Not yet: Rotate, Insert and Page labels in the menu, dragging pages between documents, or dropping files onto the panel." +evidence = ["crates/ui-egui/tests/it/editing.rs::dragging_pages_panel_thumbnails_reorders_pages", "crates/ui-egui/tests/it/viewer.rs::pages_panel_thumbnails_have_a_context_menu", "crates/ui-egui/tests/it/editing.rs::split_from_pages_panel_context_menu", "crates/ui-egui/tests/it/editing.rs::deleting_pages_from_the_pages_panel_menu", "crates/ui-egui/tests/it/editing.rs::the_delete_key_deletes_pages_picked_in_the_pages_panel_but_never_every_page"] +notes = "Drag the selection (or the page grabbed) to reorder, with a drop bar and edge scrolling; right-click offers Split document…, Extract pages, Cut, Copy, Paste after and Delete pages (also the Delete key over the panel). Not yet: Rotate, Insert and Page labels in the menu, dragging pages between documents, or dropping files onto the panel." [[feature]] id = "view.bookmarks-panel" @@ -2920,7 +2920,7 @@ milestone = "M4" status = "shipped" commands = ["page.split"] tools = ["doc_split"] -evidence = ["crates/ui-egui/tests/it/editing.rs::split_before_selected_pages"] +evidence = ["crates/ui-egui/tests/it/editing.rs::split_before_selected_pages", "crates/ui-egui/tests/it/editing.rs::split_from_organize_grid_context_menu"] notes = "" [[feature]]