diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs index aacc58c0..e8354e03 100644 --- a/crates/ui-egui/src/panels.rs +++ b/crates/ui-egui/src/panels.rs @@ -908,7 +908,17 @@ fn pages(ui: &mut egui::Ui, t: &Tokens, info: &DocInfo, view: &mut crate::DocVie { view.clear_page_selection(Some(view.current)); } + // Delete (or Backspace) deletes the selection, or the current page, likewise. + if !modal + && editable + && !ui.ctx().egui_wants_keyboard_input() + && ui.rect_contains_pointer(ui.clip_rect()) + && ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Delete) || i.consume_key(egui::Modifiers::NONE, egui::Key::Backspace)) + { + delete_pages(view, info.pages.len()); + } let w = (ui.available_width() - 40.0).min(150.0); + let count = info.pages.len(); let mut rows: Vec = Vec::with_capacity(info.pages.len()); let mut dropped = false; for (i, p) in info.pages.iter().enumerate() { @@ -986,6 +996,13 @@ fn pages(ui: &mut egui::Ui, t: &Tokens, info: &DocInfo, view: &mut crate::DocVie view.pending_action = Some(crate::canvas::ViewAction::PastePages); ui.close(); } + ui.separator(); + let some_left = view.target_pages().len() < count; + // Never every page: a document keeps at least one. + if ui.add_enabled(editable && some_left, egui::Button::new(tl!("Delete pages"))).clicked() { + delete_pages(view, count); + ui.close(); + } }); }); ui.add_space(4.0); @@ -993,6 +1010,24 @@ fn pages(ui: &mut egui::Ui, t: &Tokens, info: &DocInfo, view: &mut crate::DocVie page_drag(ui, t, view, &rows, dropped); } +/// Queue deleting the selection (or the current page) of a document of `n` pages, unless that +/// would leave no page. The selection goes; the current page stays on the page it showed, or on +/// the page that follows the deleted ones. +fn delete_pages(view: &mut crate::DocView, n: usize) { + let mut pages = view.target_pages(); + pages.sort_unstable(); + pages.dedup(); + if pages.is_empty() || pages.len() >= n { + return; + } + // Deleted pages before it shift it up; if it was deleted itself, the page after the deleted + // run lands where it was (or the last page, when the deletion ran to the end). + let before = pages.iter().filter(|p| **p < view.current).count(); + view.current = view.current.saturating_sub(before).min(n.saturating_sub(pages.len() + 1)); + view.clear_page_selection(None); + view.pending_edit = Some(pdfcraft_engine::Edit::DeletePages { pages }); +} + /// The gap (0 = before the first page, n = after the last) the pointer points at in the Pages /// panel, whose thumbnails are `rows`, top to bottom. fn panel_gap(rows: &[Rect], y: f32) -> Option { diff --git a/crates/ui-egui/tests/editing.rs b/crates/ui-egui/tests/editing.rs index 61410be2..b7d96d1d 100644 --- a/crates/ui-egui/tests/editing.rs +++ b/crates/ui-egui/tests/editing.rs @@ -2077,3 +2077,61 @@ fn a_thumbnail_drag_that_ends_off_the_panel_or_outlives_the_pages_is_dropped() { h.state_mut().views[0].document_changed(&info); assert!(h.state().views[0].panel_drag.is_none()); } + +/// A Pages panel thumbnail (the rightmost node with that label). +fn panel_thumb(h: &Harness<'static, PdfCraftApp>, label: &str) -> egui::Pos2 { + h.get_all_by_label(label).map(|n| n.rect()).max_by(|a, b| a.left().total_cmp(&b.left())).expect("the thumbnail").center() +} + +fn right_click_at(h: &mut Harness<'static, PdfCraftApp>, at: egui::Pos2) { + h.hover_at(at); + h.run_steps(1); + h.event(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Secondary, pressed: true, modifiers: Default::default() }); + h.event(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Secondary, pressed: false, modifiers: Default::default() }); + h.run_steps(2); +} + +#[test] +fn deleting_pages_from_the_pages_panel_menu() { + let mut h = pages_panel(4); + let at = panel_thumb(&h, "Page 2"); + right_click_at(&mut h, at); + h.get_by_label("Delete pages").click(); + h.run_steps(4); + assert_eq!(page_texts(h.state()), ["Page 1", "Page 3", "Page 4"]); + assert_eq!(h.state().session.get(h.state().views[0].id).unwrap().can_undo(), Some("Delete page")); + assert!(h.state().views[0].selected.is_empty(), "the deleted pages leave the selection"); + h.key_press_modifiers(Modifiers::COMMAND, Key::Z); + h.run_steps(3); + assert_eq!(page_texts(h.state()), ["Page 1", "Page 2", "Page 3", "Page 4"]); +} + +#[test] +fn the_delete_key_deletes_pages_picked_in_the_pages_panel_but_never_every_page() { + let mut h = pages_panel(3); + // ⌘-click picks pages 2 and 3 (and the current page, 1: the first ⌘-click keeps it). + h.get_by_label("Page 2").click_modifiers(Modifiers::COMMAND); + h.run_steps(2); + h.get_by_label("Page 3").click_modifiers(Modifiers::COMMAND); + h.run_steps(2); + let (p1, p3) = (panel_thumb(&h, "Page 1"), panel_thumb(&h, "Page 3")); + h.hover_at(p3); + h.run_steps(1); + assert_eq!(h.state().views[0].selected.iter().copied().collect::>(), vec![0, 1, 2]); + // Every page picked: Delete does nothing, and the menu item is off. + h.key_press(Key::Delete); + h.run_steps(3); + assert_eq!(page_texts(h.state()).len(), 3, "a document keeps at least one page"); + right_click_at(&mut h, p3); + assert!(h.query_by(|n| n.label().as_deref() == Some("Delete pages") && n.is_disabled()).is_some()); + h.key_press(Key::Escape); + h.run_steps(2); + // Pages 2 and 3 picked (page 2 current): Delete deletes them, and page 1 becomes current. + h.state_mut().views[0].select_pages(&[1, 2]); + h.hover_at(p1); + h.run_steps(1); + h.key_press(Key::Delete); + h.run_steps(4); + assert_eq!(page_texts(h.state()), ["Page 1"]); + assert_eq!(h.state().views[0].current, 0); +} diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index bbe7b097..2a172ae9 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -1238,8 +1238,8 @@ milestone = "M4" status = "shipped" commands = [] tools = [] -evidence = ["crates/ui-egui/tests/editing.rs::dragging_pages_panel_thumbnails_reorders_pages", "crates/ui-egui/tests/viewer.rs::pages_panel_thumbnails_have_a_context_menu"] -notes = "Drag the selection (or the page grabbed) to reorder, with a drop bar and edge scrolling; right-click offers Extract pages, Cut, Copy and Paste after. Not yet: Rotate, Delete, Insert and Page labels in the menu, dragging pages between documents, or dropping files onto the panel." +evidence = ["crates/ui-egui/tests/editing.rs::dragging_pages_panel_thumbnails_reorders_pages", "crates/ui-egui/tests/viewer.rs::pages_panel_thumbnails_have_a_context_menu", "crates/ui-egui/tests/editing.rs::deleting_pages_from_the_pages_panel_menu", "crates/ui-egui/tests/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." [[feature]] id = "view.bookmarks-panel"