Fix keyboard-owned Pages sidebar select all (#2052)

This commit is contained in:
FredApps committed 2026-10-14 15:14:35 +02:00
1 parent 4e952e62e2
commit 971f6ea2b4
7 files changed
+89 -15

No files matched your search

+4 -1
View File
@@ -2,7 +2,7 @@
**Stage: alpha** · next: beta, ~26% (ready for real work 49% → 75%) and ~400–700 h away
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (M3 candidate: native opens retain chosen tools through password retries and defer startup intake while preparation is pending; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (M4/#2052 candidate: keyboard-owned Pages sidebar select-all; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (M13: CLI check visits each directory once through aliases and cycles; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (M4: Extract and Split preserve shared Type 3 glyph resources; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (M2: packed TIFF-predictor RGB rows retain their byte boundaries; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
@@ -149,6 +149,8 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
## Progress log
- **2026-10-14 (M4/#2052 candidate):** Ctrl/Cmd+A selects all thumbnails while the Pages sidebar owns the keyboard, preserves the current page, and keeps viewer/text-field shortcuts and document selection separate. Escape and existing permission checks are retained. Focus regression and scoped UI validation pass; estimates unchanged.
- **2026-10-13 (M3, native-opening candidate):** Chosen tools survive preparation, conversion and password retries; startup intake waits for pending preparation. Regression tests cover completion, cancellation and document identity. Estimates unchanged.
- **2026-10-12 (M10, Windows printing without PowerShell, #1107):** craft-print now prints on Windows in-process: printers from EnumPrinters/GetDefaultPrinter, jobs drawn as vectors by Windows.Data.Pdf into the XPS print API with a driver-validated PrintTicket (exact paper, orientation, no scaling), so printing works where App Control for Business or AppLocker run PowerShell in Constrained Language Mode. Portrait and landscape sheets still print as one job (turned with /Rotate); a sheet size the driver lacks is refused rather than shrunk. Pins craft-libs f03ec27 (craft-libs#28); deny.toml allows the storytold/winprint.rs fork and adds a webpki-root-certs licence exception and a RUSTSEC-2024-0370 ignore for winprint's build-dependencies. An earlier craft-print revision was checked in Windows Sandbox under enforced WDAC: printers listed, CLI and desktop jobs spooled, and no powershell.exe in the 4688 audit. The final pinned revision passed craft-libs Windows CI, including a real paused-queue job; that Sandbox run has not been repeated. Estimates unchanged.
- **2026-10-12 (M13, CLI robustness scans):** `pdfcraft-cli check` visits a directory once even when input roots overlap or directory symlinks lead back into the tree. PDFs reached through file symlinks remain accepted. Three real CLI regressions cover overlapping roots, a directory cycle and directory/file aliases. Estimates unchanged.
@@ -427,6 +429,7 @@ Newest first: the date, what moved, and the new overall estimate.
| Date | Change | Summary |
|---|---|---|
| 2026-10-14 | minor | M4/#2052 candidate: Pages sidebar select-all, focus ownership, Escape and document isolation validated; estimates unchanged. |
| 2026-10-12 | minor | M3/#1364 candidate: absolute local GoToR links require desktop consent or headless allow_absolute; existing relative navigation and validation retained. Estimates unchanged. |
| 2026-10-12 | minor | M8: redaction retires inherited `/Resources` so a parent `/Pages` can't keep a removed XObject reachable; only redacted pages that inherit are pinned (one shared reference), unredacted pages are untouched; estimates unchanged |
| 2026-10-12 | minor | M7/#1200 candidate: bounded local GoToR opening, root-confined automation, and action-preserving appearance edits; named destinations and window behavior remain partial; estimates unchanged. |
+11 -4
View File
@@ -433,6 +433,8 @@ pub struct DocView {
pub org_drag: Option<Vec<usize>>,
/// Pages panel: the pages being dragged to a new place.
pub panel_drag: Option<Vec<usize>>,
/// Transient keyboard owner for this document's Pages sidebar.
pub(crate) pages_focus_id: Option<egui::Id>,
/// Marquee Zoom / Snapshot: the rectangle being dragged (page, start), and a finished one.
pub marquee: Option<(usize, Pos2)>,
pub marquee_done: Option<crate::zoom_snap::Marquee>,
@@ -672,6 +674,7 @@ impl DocView {
sign: Default::default(),
org_drag: None,
panel_drag: None,
pages_focus_id: None,
marquee: None,
marquee_done: None,
fill_text: None,
@@ -1137,10 +1140,7 @@ impl DocView {
/// Select all: every page in Organize, or every word on the current page.
pub fn select_all(&mut self) -> bool {
if self.organize {
self.selected = (0..self.page_count).collect();
// Keep the current page as the anchor for the next Shift-click.
self.select_anchor = (self.page_count > 0).then_some(self.current);
return !self.selected.is_empty();
return self.select_all_pages();
}
let page = self.current;
let Some(t) = self.texts.get(&page) else { return false };
@@ -1151,6 +1151,13 @@ impl DocView {
true
}
/// Select every page without navigating; shared by Organize and the Pages sidebar.
pub fn select_all_pages(&mut self) -> bool {
self.selected = (0..self.page_count).collect();
self.select_anchor = (self.page_count > 0).then_some(self.current);
!self.selected.is_empty()
}
/// Rotate the view 90° clockwise or counter-clockwise (View ▸ Rotate View, ⇧⌘+ / ⇧⌘−).
pub fn rotate_view(&mut self, clockwise: bool) {
self.rotation = (self.rotation + if clockwise { 90 } else { 270 }) % 360;
+6 -2
View File
@@ -2650,12 +2650,16 @@ impl PdfCraftApp {
if let Some(i) = self.active {
// Select all belongs to the document or page grid, unless a text field or
// overlay owns the keyboard. Other canvas shortcuts keep their own handling.
let pages_owns_keyboard =
self.right == Some(RightPanel::Pages) && self.views[i].pages_focus_id.is_some_and(|id| ctx.memory(|m| m.has_focus(id)));
if self.dialog.is_none()
&& !self.palette_open
&& !ctx.egui_wants_keyboard_input()
&& (pages_owns_keyboard || !ctx.egui_wants_keyboard_input())
&& ctx.input_mut(|input| input.consume_shortcut(&egui::KeyboardShortcut::new(egui::Modifiers::COMMAND, Key::A)))
{
if self.is_preparing() {
if pages_owns_keyboard {
self.views[i].select_all_pages();
} else if self.is_preparing() {
if let Some(doc) = self.session.get(self.views[i].id) {
prepare::select_all(&mut self.views[i], &doc.form);
}
+11 -4
View File
@@ -1003,11 +1003,17 @@ fn outline_item(ui: &mut egui::Ui, t: &Tokens, info: &DocInfo, item: &OutlineIte
/// offers Extract, Cut, Copy and Paste on the selection, or on the page clicked; dragging
/// moves the selection, or the page grabbed, to the gap it is dropped on.
fn pages(ui: &mut egui::Ui, t: &Tokens, info: &DocInfo, view: &mut crate::DocView, modal: bool, editable: bool, nav: &mut Option<Nav>) {
let focus_id = ui.make_persistent_id(("pages-keyboard", view.id));
view.pages_focus_id = Some(focus_id);
let keyboard = ui.interact(ui.clip_rect(), focus_id, Sense::focusable_noninteractive());
let owns_keyboard = keyboard.has_focus();
// Escape belongs to this panel while it owns the keyboard, even after pointer movement.
ui.memory_mut(|m| m.set_focus_lock_filter(focus_id, egui::EventFilter { escape: true, ..Default::default() }));
// Escape drops the selection while the pointer is over the panel and nothing else wants the key.
if !modal
&& !view.selected.is_empty()
&& !ui.ctx().egui_wants_keyboard_input()
&& ui.rect_contains_pointer(ui.clip_rect())
&& (owns_keyboard || !ui.ctx().egui_wants_keyboard_input())
&& (owns_keyboard || ui.rect_contains_pointer(ui.clip_rect()))
&& ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape))
{
view.clear_page_selection(Some(view.current));
@@ -1015,8 +1021,8 @@ fn pages(ui: &mut egui::Ui, t: &Tokens, info: &DocInfo, view: &mut crate::DocVie
// 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())
&& (owns_keyboard || !ui.ctx().egui_wants_keyboard_input())
&& (owns_keyboard || 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());
@@ -1068,6 +1074,7 @@ fn pages(ui: &mut egui::Ui, t: &Tokens, info: &DocInfo, view: &mut crate::DocVie
ui.label(egui::RichText::new(&p.label).font(theme::medium(12.0)).color(if selected || picked { t.accent_text } else { t.text_muted }));
let resp = theme::hand(resp);
if resp.clicked() {
ui.memory_mut(|m| m.request_focus(focus_id));
let m = ui.input(|i| i.modifiers);
if m.shift || m.command {
view.click_page(i, m, true);
+52
View File
@@ -52,6 +52,58 @@ fn rect(h: &Harness<'static, PdfCraftApp>, page: usize) -> Option<egui::Rect> {
h.state().views[0].page_screen_rect(page)
}
fn sidebar_key(h: &mut Harness<'static, PdfCraftApp>, key: Key, modifiers: Modifiers) {
h.event(egui::Event::ModifiersChanged(modifiers));
for pressed in [true, false] {
h.event(egui::Event::Key { key, physical_key: None, pressed, repeat: false, modifiers });
}
h.run_steps(2);
h.event(egui::Event::ModifiersChanged(Modifiers::NONE));
h.run_steps(1);
}
#[test]
fn pages_sidebar_select_all_keeps_current_page_and_keyboard_scope() {
use egui_kittest::kittest::Queryable;
let mut h = harness(&[("panel", "pages")]);
h.get_by_label("Page 2").click();
h.run_steps(3);
assert_eq!(h.state().views[0].current, 1);
// Moving the pointer is not a keyboard-focus change.
h.hover_at(rect(&h, 1).unwrap().center());
h.run_steps(1);
for _ in 0..2 {
sidebar_key(&mut h, Key::A, Modifiers::COMMAND);
assert_eq!(h.state().views[0].target_pages(), vec![0, 1, 2]);
assert_eq!(h.state().views[0].current, 1, "selecting does not navigate");
}
sidebar_key(&mut h, Key::Escape, Modifiers::NONE);
assert!(h.state().views[0].selected.is_empty());
// A click on the viewer returns Ctrl+A to text selection, even with Pages open.
let pos = rect(&h, 1).unwrap().center();
for pressed in [true, false] {
h.event(egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Modifiers::NONE });
}
h.run_steps(3);
sidebar_key(&mut h, Key::A, Modifiers::COMMAND);
assert!(h.state().views[0].selected.is_empty(), "viewer shortcut must not select thumbnails");
// A different document does not inherit the previous sidebar's keyboard ownership.
h.get_by_label("Page 2").click();
h.run_steps(2);
h.state_mut().open_bytes("other.pdf", None, PAGES.to_vec()).unwrap();
h.run_steps(5);
sidebar_key(&mut h, Key::A, Modifiers::COMMAND);
let active = h.state().active.unwrap();
assert!(h.state().views[active].selected.is_empty());
h.get_by_label("Page 1").click();
h.run_steps(2);
sidebar_key(&mut h, Key::A, Modifiers::COMMAND);
assert_eq!(h.state().views[active].target_pages(), vec![0, 1, 2]);
assert!(h.state().views[0].selected.is_empty(), "selection remains document-local");
}
/// Two pages of different sizes: 300×400 then 600×300.
const MIXED: &[u8] = b"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
+3 -2
View File
@@ -1,6 +1,6 @@
# PdfCraft roadmap: milestones and what's next
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (M3 candidate: native opens retain chosen tools through password retries and defer startup intake while preparation is pending; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (M4/#2052 candidate: keyboard-owned Pages sidebar select-all; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (M13: CLI check visits each directory once through aliases and cycles; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (M4: Extract and Split preserve shared Type 3 glyph resources; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (M2: packed TIFF-predictor RGB rows retain their byte boundaries; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
@@ -95,7 +95,7 @@ total matches the parity estimate.
| M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. A custom text match that ends on a reconstructed separator stops before the next glyph (#1315). Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings |
| M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); Lab Bradford adaptation uses LMS white-point ratios, covered by independent neutral/chromatic and direct/Indexed render oracles; inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings |
| M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel<br>Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). |
| M4 | Engine, history, save, organize | 100–180 | 95% | 8–15 | Done:<br>- command registry (menus, shortcuts and palette all use it);<br>- undo/redo; incremental, atomic and encrypted saves;<br>- autosave and crash recovery;<br>- organize, combine, extract, split and insert-from-file, with identical fonts and images stored once;<br>- CLI `edit/combine/extract/split`.<br>Done since: Organize opens directly into the page overview, with checkbox selection, an editable page-range selection (#1092), and per-page rotate/delete controls; Combine files in its own tab (sortable, resizable columns; folders; early warnings; unlocking protected files), bookmark editing, page labels (Number pages), CLI `run`, Set Page Boxes, Crop tool, Duplicate pages, Ctrl+A / Cmd+A to select all pages in Organize, Replace pages, bookmarks from tagged headings. Extract and Split preserve shared Type 3 glyph resource graphs, retaining conservative pruning limits. Missing: page transitions, recovery on the web |
| M4 | Engine, history, save, organize | 100–180 | 95% | 8–15 | Done:<br>- command registry (menus, shortcuts and palette all use it);<br>- undo/redo; incremental, atomic and encrypted saves;<br>- autosave and crash recovery;<br>- organize, combine, extract, split and insert-from-file, with identical fonts and images stored once;<br>- CLI `edit/combine/extract/split`.<br>Done since: Organize opens directly into the page overview, with checkbox selection, an editable page-range selection (#1092), and per-page rotate/delete controls; Combine files in its own tab (sortable, resizable columns; folders; early warnings; unlocking protected files), bookmark editing, page labels (Number pages), CLI `run`, Set Page Boxes, Crop tool, Duplicate pages, Ctrl+A / Cmd+A to select all pages in Organize; #2052 candidate adds keyboard-owned Pages sidebar select-all without navigation, with focus and document-isolation regression coverage, Replace pages, bookmarks from tagged headings. Extract and Split preserve shared Type 3 glyph resource graphs, retaining conservative pruning limits. Missing: page transitions, recovery on the web |
| M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel<br>Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). Renderer-creation errors return to the existing OpenGL startup fallback; a real-GPU regression covers errors and device reuse. The underlying x86 DX12 shader compiler failure remains unresolved; see `windows-renderer-startup.md`. With Windows' Animation effects turned off, the wheel scrolls and zooms without easing and egui's animations take no time (`motion.rs`; macOS, Linux and the browser aren't asked yet); Preferences ▸ Animation effects (Use system setting, On, Off) decides it on every platform. |
| M4 | Engine, history, save, organize | 100–180 | 95% | 8–15 | Done:<br>- command registry (menus, shortcuts and palette all use it);<br>- undo/redo; incremental, atomic and encrypted saves;<br>- autosave and crash recovery;<br>- organize, combine, extract, split and insert-from-file, with identical fonts and images stored once;<br>- CLI `edit/combine/extract/split`.<br>Done since: Combine files in its own tab (sortable, resizable columns; folders; early warnings; unlocking protected files), bookmark editing, page labels (Number pages), CLI `run`, Set Page Boxes, Crop tool, Duplicate pages, Ctrl+A / Cmd+A to select all pages in Organize, Replace pages, bookmarks from tagged headings. Missing: page transitions, recovery on the web |
| M5 | Comments (all annotation types, XFDF) | 120–200 | 88% | 12–25 | Done: notes, highlight/underline/strikeout/squiggly, text boxes, ink, lines, arrows, rectangles, ovals, stamps (dynamic, Sign Here, Standard Business), with appearance streams; replies, status, checkmarks, locking, move/resize/restyle/delete; properties dialog; panel filter and sort; hide all; summaries (comments only); XFDF/FDF import and export (partial imports preserve existing reply and review-status links); flatten; Fill & Sign with adjustable text/date size; agent tools.<br>Done since: stamps placed on a rotated page read upright (#1398); polygons, connected lines, clouds, callouts, inserted text, colour and checkmark filters; image signature pointer previews, selection handles and live image movement/resizing with proportional corners and independent edge handles. Missing: custom stamps, replace-text proposals, summary layouts with the page |
@@ -162,6 +162,7 @@ M2 (renderer), M7 (content editing), M10 (OCR and Office), M11 (Preflight and st
| Date | Change | Summary |
|---|---|---|
| 2026-10-14 | minor | M4/#2052 candidate: Pages sidebar select-all, focus ownership, Escape and document isolation validated; estimates unchanged. |
| 2026-10-12 | minor | M13: CLI check directory identity, with overlap and symlink regressions; estimates unchanged |
| 2026-10-12 | minor | M4: shared Type 3 glyph resources survive extraction, with save/reopen and independent raster regressions; estimates unchanged |
| 2026-10-12 | minor | M2: packed TIFF-predictor RGB row boundaries, with decoding and page-render regressions; estimates unchanged |
+2 -2
View File
@@ -1250,8 +1250,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", "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."
evidence = ["crates/ui-egui/tests/view.rs::pages_sidebar_select_all_keeps_current_page_and_keyboard_scope", "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 = "Ctrl/Cmd+A selects every page when the Pages sidebar owns the keyboard, without navigation; Escape clears the selection. Viewer and text-field shortcuts retain their own scope. 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"