M4: delete pages from the Pages panel (#607)
Right-clicking a thumbnail offers Delete pages, and Delete or Backspace with the pointer over the panel deletes too, as in the organize grid: the selection, or the current page. A document keeps at least one page, so with every page picked the item is off and the keys do nothing. The selection goes and the current page moves to the page after the deleted ones; one undoable step. Parity: view.thumbnail-context-ops notes and evidence include Delete pages. Co-authored-by: William Max & Co. <info@williammax.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
parent
6e13a5fc54
commit
c110c579a2
|
|
@ -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<Rect> = 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<usize> {
|
||||
|
|
|
|||
|
|
@ -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<_>>(), 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue