M4: open Organize Pages as a selectable thumbnail overview (#1004)

* M4: document the directly accessible page overview

Part of the organize-page-overview change.

Co-authored-by: Codex <[email protected]>

* M4: open Organize in the page grid with selection and per-page actions

Show the overview from the tool catalogue, Home, palette and control channel. Add checkbox selection and local rotate/delete actions; preserve thumbnail aspect ratios.

Co-authored-by: Codex <[email protected]>

* M4: translate page selection and local actions

Co-authored-by: Codex <[email protected]>

* M4: cover overview entry, selection and page actions

Co-authored-by: Codex <[email protected]>

* M4: record the organizer workflow improvement

Co-authored-by: Codex <[email protected]>

* M4: link page overview regression evidence

Co-authored-by: Codex <[email protected]>

---------

Co-authored-by: Codex <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-11 13:58:58 -04:00
1 parent 1651069e7a
commit 134b98b0a8
27 files changed
+293 -41

No files matched your search

+16 -16
View File
@@ -19,7 +19,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "07a12136c12f2ffab3f958991399de877c6c306b70235b9d9b6ea35af70a3038"
sha256 = "a562bce9a9ba1be6bc464ef74104bbf53f88291a5ec522d18ba3f607e8490cf6"
[[asset]]
path = "crates/ui-egui/src/i18n/zh-hant.tsv"
@@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "4774f2e2ba768dc7dd102a250e107afd2b778bc1f0ff0769ea4e4dcbe0095068"
sha256 = "3d9dde3978c43dea4ad461288c859d466520888aa4fce0ea43514f8206bc084f"
[[asset]]
path = "crates/ui-egui/src/i18n/cs.tsv"
@@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "abd8d0c409be846fcf48cbc1338def7f77f2d301bd84d863e4ad66434e5f821f"
sha256 = "a84c17c7a8f297bef07a613e63116f9a8bb05aa02fd8442a684648df3a768224"
[[asset]]
path = "crates/ui-egui/src/i18n/pt-br.tsv"
@@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "050d4a70bd2c64bb1104098b17a5d58cc80fe1e3404194193454cdd7d7afa0a3"
sha256 = "59fbeb2f5c912f7b77c1f117e0f68b8806bb015e0d187721bc77b6708cb3e088"
[[asset]]
path = "crates/ui-egui/src/i18n/es.tsv"
@@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "2f6259d83cc64b50267ea9f4d29524d8b0c24706d0014991d0eb11cf3366de75"
sha256 = "47a294326663323e42e92b4286d68b42741d0f6c8bb5df2023ed6c5697595332"
[[asset]]
path = "crates/ui-egui/src/i18n/fr.tsv"
@@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "921eabe6a997a428cb55f588caf556fa2f7105a1b55f095f7ddbbb4c104a6e4c"
sha256 = "15aada11b884a55869b2080f79a6902134a4cdeea1c2c011484b22b5c5f53747"
[[asset]]
path = "crates/ui-egui/src/i18n/de.tsv"
@@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "5916589c59d74474823c2b5007cf3ab38c286b756866ef39548d30a66fb89382"
sha256 = "6945b0d3b6b6bcd8cad936b8b440484903fa71ce439a5da6fddf595ffa9f0233"
[[asset]]
path = "crates/ui-egui/src/i18n/te.tsv"
@@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "6ec9c44295c74966f3786f3f5e994ca1940205e7d440266c48a4dc0022fa1f59"
sha256 = "8ee5d4a658dd29e41e171103343eb223cc360956fb533788cec8676dcdf1c6ec"
[[asset]]
path = "crates/ui-egui/src/i18n/it.tsv"
@@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "7d975aa37094e268293bf1e76338e0e14d435648557c812f7866f29ab4bd501c"
sha256 = "884adac1fca5f8a4bb14d947e9feabdee2ca18e86c3a1dbc2e7a042512c37ec2"
[[asset]]
path = "crates/ui-egui/src/i18n/pl.tsv"
@@ -118,7 +118,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "961c713b921e0b38ff41cca714cea252e88ffa4d9b3eb1e8610f67d532ab9d7d"
sha256 = "5e794e7ab841956a509ecdf1701d3e61ced122d10e962d20ce14068cdd8d4df4"
[[asset]]
path = "crates/ui-egui/src/i18n/ru.tsv"
@@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "20933c2d09f1e0cd764014521c8dffbf5b6899fb5244f3256357d501d332322f"
sha256 = "f83db805a29b5ac05bea6c89b7542d7afdb8fab8edf578a8d976aca7e18cd348"
[[asset]]
path = "crates/ui-egui/src/i18n/hu.tsv"
@@ -140,7 +140,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "823d127d847950c545befd1ea8e94f99a34473bd9be7aebee93b72dca6b79b2f"
sha256 = "ad66bf99115990740d7a65ced3495d3dc954cd7c229ac35e3f136c20c439420b"
[[asset]]
path = "crates/ui-egui/src/i18n/bg.tsv"
@@ -151,7 +151,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "c3b4fa31e77735baf25e06e16c9ae1e999cba7a1a7c3b765d6c7bdc84526a48f"
sha256 = "f0f5451f8602e2d9ac27386e3fabde563bcc69d357c235db7babfeee8cac6cb6"
[[asset]]
path = "crates/ui-egui/src/i18n/ja.tsv"
@@ -162,7 +162,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "f228a767496a912e9d93d5bcd4323c8ffe406f9be93663efd6adf51151f43b8e"
sha256 = "ca0f9ea68fa5df68c468dd535baf85aa11e5e7bcd704149f7d2df44c284a0cff"
[[asset]]
path = "crates/ui-egui/src/i18n/uk.tsv"
@@ -173,7 +173,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "d5e5c58a63e1cde6224ccb1438064138f86de69d559c99f1a101f8d5e1daa7ed"
sha256 = "d5eb9db7c07041b0dca02dbe5ea27ca10de9ce2d3a519d7b8e2dc8dedf5078c4"
[[asset]]
path = "crates/ui-egui/src/i18n/ar.tsv"
@@ -184,7 +184,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "2960b61dcb97358a6c304b4b41d9ef8021d1e9cb86d313959d442fd226aed3bd"
sha256 = "df5deac92b0ff5b7b40251d3eee21c6d3c345fa62a6b726001a730c16fabbfb8"
[[asset]]
path = "assets/icons/accessibility.svg"
+2
View File
@@ -135,6 +135,8 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
Newest first: the date, what moved, and the new overall estimate.
- **2026-10-11 (M4, Organize overview):** Opening Organize pages from All tools, Home, the tool palette or `ui.set tool=organize` opens the thumbnail grid directly. Checkboxes toggle individual pages without modifier keys; page controls rotate either way or delete one page, while the toolbar acts on the selection. Portrait and landscape previews retain their aspect ratio. UI and control regressions cover entry, selection, single-page versus batch actions, undo, permissions and retaining the last page. Milestone and effort estimates unchanged.
- **2026-10-10 (M9, signature graphics):** A visible digital signature can show a graphic where the signer's name is large, as Acrobat's Configure Signature Appearance does: the saved Fill & Sign signature (drawn strokes smoothed as ink, typed outlines in the script font, or an image) or an imported PNG/JPEG. Sign as previews it and remembers the choice for the next signing; `sign_document` takes `graphic` (`image`, `text` or `strokes`). Graphics are checked and capped before drawing (non-finite or unbounded points, mismatched pixel buffers, oversized images are refused with an error the caller can act on); images keep their alpha as a soft mask. `sign.signature-appearance` stays partial (no named saved appearances or PDF-page graphics). Overall estimates unchanged.
- **2026-10-11 (M14, absolute interface sizes):** A chosen Interface size is now absolute, as PhotoCraft's UI Scale is: "100%" is one screen pixel per point whatever scale the display reports, so on a 1080p screen whose system reports 200 % it draws at a true 100 % (#456, reported after the first change). Auto still keeps the display's scale and adds GNOME's text scaling, and its menu entry shows the size it draws at. The menu offers PhotoCraft's sizes (75–300 %).
- **2026-10-11 (automation, #971):** `text_find` reads pages in growing batches (8, doubling to 128) and stops once `limit` is reached, so a bounded search on a long document no longer extracts and caches every page first. Results say `complete`, and a capped one gives `next` (`start_page`, `skip`) to continue. Still open from #971: a byte budget for the automation text cache and per-page warnings for pages whose text can't be extracted.
+47 -11
View File
@@ -3415,10 +3415,10 @@ fn organize_toolbar(view: &mut DocView, info: &DocInfo, editable: bool, dirty: b
// Fixed width so the buttons never shift as the selection text changes.
ui.add_sized([150.0, 30.0], egui::Label::new(egui::RichText::new(label).font(theme::medium(13.0)).color(t.text_muted)).truncate());
ui.add_enabled_ui(editable, |ui| {
if icons::button(ui, "rotate-ccw", 30.0, false, "Rotate counterclockwise").clicked() {
if icons::button(ui, "rotate-ccw", 30.0, false, tl!("Rotate counterclockwise")).clicked() {
view.pending_edit = Some(Edit::RotatePages { pages: targets.clone(), degrees: -90 });
}
if icons::button(ui, "rotate-cw", 30.0, false, "Rotate clockwise").clicked() {
if icons::button(ui, "rotate-cw", 30.0, false, tl!("Rotate clockwise")).clicked() {
view.pending_edit = Some(Edit::RotatePages { pages: targets.clone(), degrees: 90 });
}
let can_delete = targets.len() < n;
@@ -3562,7 +3562,7 @@ fn gap_button(ui: &mut egui::Ui, gap: usize, at: Pos2, height: f32, label: Strin
fn organize_grid(view: &mut DocView, info: &DocInfo, editable: bool, dirty: bool, unobstructed: bool, ui: &mut egui::Ui, t: &Tokens) {
let ppp = ui.ctx().pixels_per_point();
// The page image scales with the zoom; the padding and the page number don't.
let cell = vec2(146.0 * view.grid_zoom + 44.0, 194.0 * view.grid_zoom + 56.0);
let cell = vec2(146.0 * view.grid_zoom + 44.0, 194.0 * view.grid_zoom + 88.0);
let anchor = view.grid_anchor.take();
let mut open_page = None;
organize_toolbar(view, info, editable, dirty, unobstructed, ui, t);
@@ -3620,10 +3620,10 @@ fn organize_grid(view: &mut DocView, info: &DocInfo, editable: bool, dirty: bool
if resp.drag_stopped() {
drop = true;
}
let info = crate::i18n::fmt(tl!("Page {label}"), &[("label", &p.label)]);
resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, true, view.selected.contains(&i), info.clone()));
let s = (cell.x - 44.0) / p.width.max(1.0);
let size = vec2(p.width * s, p.height * s).min(vec2(cell.x - 44.0, cell.y - 56.0));
let page_label = crate::i18n::fmt(tl!("Page {label}"), &[("label", &p.label)]);
resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, true, view.selected.contains(&i), page_label.clone()));
let s = ((cell.x - 60.0) / p.width.max(1.0)).min((cell.y - 88.0) / p.height.max(1.0));
let size = vec2(p.width * s, p.height * s);
let pr = Rect::from_center_size(pos2(c.center().x, c.top() + 16.0 + size.y / 2.0), size);
let selected = view.selected.contains(&i) || (view.selected.is_empty() && i == view.current);
if selected || resp.hovered() {
@@ -3650,7 +3650,43 @@ fn organize_grid(view: &mut DocView, info: &DocInfo, editable: bool, dirty: bool
ui.painter().image(tex.id(), pr, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
}
ui.painter().rect_stroke(pr, CornerRadius::ZERO, Stroke::new(1.0, t.border), egui::StrokeKind::Outside);
ui.painter().text(pos2(c.center().x, pr.bottom() + 16.0), Align2::CENTER_CENTER, &p.label, theme::medium(12.0), t.text_muted);
ui.painter().text(pos2(c.center().x, c.bottom() - 48.0), Align2::CENTER_CENTER, &p.label, theme::medium(12.0), t.text_muted);
// The checkbox toggles just this page without requiring a modifier. Keep it
// outside the page image, including on very tall or wide pages.
let mut checked = view.selected.contains(&i);
let check_rect = Rect::from_min_size(c.min + vec2(8.0, 8.0), Vec2::splat(20.0));
let check = ui.push_id(("org-check", i), |ui| ui.put(check_rect, egui::Checkbox::without_text(&mut checked))).inner;
let label = crate::i18n::fmt(tl!("Select page {label}"), &[("label", &p.label)]);
check.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Checkbox, true, checked, label.clone()));
if check.on_hover_text(label).changed() {
view.click_page(i, egui::Modifiers::COMMAND, false);
view.current = i;
}
// Local actions always target the labelled page. The toolbar above the grid
// remains the place for actions on the whole selection.
if selected || resp.contains_pointer() || resp.has_focus() {
let actions = Rect::from_center_size(pos2(c.center().x, c.bottom() - 24.0), vec2(84.0, 26.0));
ui.scope_builder(
egui::UiBuilder::new().id_salt(("org-actions", i)).max_rect(actions).layout(egui::Layout::left_to_right(egui::Align::Center)),
|ui| {
ui.spacing_mut().item_spacing.x = 3.0;
ui.add_enabled_ui(editable, |ui| {
for (icon, degrees, key) in
[("rotate-ccw", -90, "Rotate page {label} counterclockwise"), ("rotate-cw", 90, "Rotate page {label} clockwise")]
{
let label = crate::i18n::fmt(tl!(key), &[("label", &p.label)]);
if icons::button(ui, icon, 26.0, false, &label).clicked() {
view.pending_edit = Some(Edit::RotatePages { pages: vec![i], degrees });
}
}
let label = crate::i18n::fmt(tl!("Delete page {label}"), &[("label", &p.label)]);
if ui.add_enabled_ui(info.pages.len() > 1, |ui| icons::button(ui, "trash-2", 26.0, false, &label)).inner.clicked() {
view.pending_edit = Some(Edit::DeletePages { pages: vec![i] });
}
});
},
);
}
if resp.clicked() {
let m = ui.input(|i| i.modifiers);
view.click_page(i, m, false);
@@ -3682,20 +3718,20 @@ fn organize_grid(view: &mut DocView, info: &DocInfo, editable: bool, dirty: bool
}
// Between pages (and at both ends): insert files right there.
if editable && !middle_gesture && view.org_drag.is_none() {
let mid = |r: &Rect| r.top() + 16.0 + (cell.y - 56.0) / 2.0;
let mid = |r: &Rect| r.top() + 16.0 + (cell.y - 88.0) / 2.0;
for (i, c) in &cells {
let label = info
.pages
.get(*i)
.map(|p| crate::i18n::fmt(tl!("Insert a file before page {label}"), &[("label", &p.label)]))
.unwrap_or_default();
if gap_button(ui, *i, pos2(c.left(), mid(c)), cell.y - 56.0, label, t) {
if gap_button(ui, *i, pos2(c.left(), mid(c)), cell.y - 88.0, label, t) {
view.pending_action = Some(ViewAction::InsertFromFileAt(*i));
}
}
if let Some((i, c)) = cells.last()
&& i + 1 == info.pages.len()
&& gap_button(ui, i + 1, pos2(c.right(), mid(c)), cell.y - 56.0, tl!("Insert a file at the end").to_string(), t)
&& gap_button(ui, i + 1, pos2(c.right(), mid(c)), cell.y - 88.0, tl!("Insert a file at the end").to_string(), t)
{
view.pending_action = Some(ViewAction::InsertFromFileAt(i + 1));
}
+2 -3
View File
@@ -5,7 +5,7 @@ use egui::{Align2, CornerRadius, Rect, Sense, Stroke, vec2};
use pdfcraft_engine::catalog;
use crate::theme::{self, Tokens};
use crate::{LeftPanel, PdfCraftApp, icons, panels::human_size, widgets};
use crate::{PdfCraftApp, icons, panels::human_size, widgets};
const RECOMMENDED: [&str; 5] = ["organize", "comment", "form", "edit", "protect"];
@@ -86,8 +86,7 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
);
let resp = if title_elided || blurb_elided { resp.on_hover_text(format!("{}\n{blurb}", tl!(g.label))) } else { resp };
if resp.clicked() {
app.left = LeftPanel::Tool(g.id);
app.left_open = true;
app.select_tool(g.id);
}
}
let (rect, resp) = ui.allocate_exact_size(vec2(170.0, 104.0), Sense::click());
+4
View File
@@ -2232,6 +2232,10 @@ key Space مسافة
Panels اللوحات
Resize panels تغيير حجم اللوحات
Resize panel window تغيير حجم نافذة اللوحة
Select page {label} تحديد الصفحة {label}
Rotate page {label} counterclockwise تدوير الصفحة {label} عكس اتجاه عقارب الساعة
Rotate page {label} clockwise تدوير الصفحة {label} باتجاه عقارب الساعة
Delete page {label} حذف الصفحة {label}
Mouse wheel scroll speed سرعة التمرير بعجلة الماوس
scroll speed Slow بطيئة
scroll speed Normal عادية
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -2174,6 +2174,10 @@ key Space Интервал
Resize panel window Преоразмери прозореца на панела
Tools Инструменти
Inspector Инспектор
Select page {label} Избиране на страница {label}
Rotate page {label} counterclockwise Завъртане на страница {label} обратно на часовниковата стрелка
Rotate page {label} clockwise Завъртане на страница {label} по часовниковата стрелка
Delete page {label} Изтриване на страница {label}
Mouse wheel scroll speed Скорост на превъртане с колелцето на мишката
scroll speed Slow Ниска
scroll speed Normal Нормална
Can't render this file because it contains an unexpected character in line 8 and column 16.
+4
View File
@@ -182,3 +182,7 @@ key Space Mezerník
Resize panel window Změnit velikost okna panelu
Tools Nástroje
Inspector Inspektor
Select page {label} Vybrat stránku {label}
Rotate page {label} counterclockwise Otočit stránku {label} proti směru hodinových ručiček
Rotate page {label} clockwise Otočit stránku {label} po směru hodinových ručiček
Delete page {label} Odstranit stránku {label}
Can't render this file because it contains an unexpected character in line 7 and column 81.
+4
View File
@@ -682,6 +682,10 @@
Edit image Bild bearbeiten
Rotate counterclockwise Gegen den Uhrzeigersinn drehen
Rotate clockwise Im Uhrzeigersinn drehen
Select page {label} Seite {label} auswählen
Rotate page {label} counterclockwise Seite {label} gegen den Uhrzeigersinn drehen
Rotate page {label} clockwise Seite {label} im Uhrzeigersinn drehen
Delete page {label} Seite {label} löschen
Flip horizontal Horizontal spiegeln
Flip vertical Vertikal spiegeln
Replace image Bild ersetzen
Can't render this file because it contains an unexpected character in line 6 and column 51.
+4
View File
@@ -2127,6 +2127,10 @@ key Space Espacio
Resize panel window Cambiar tamaño de la ventana del panel
Tools Herramientas
Inspector Inspector
Select page {label} Seleccionar la página {label}
Rotate page {label} counterclockwise Girar la página {label} en sentido antihorario
Rotate page {label} clockwise Girar la página {label} en sentido horario
Delete page {label} Eliminar la página {label}
Mouse wheel scroll speed Velocidad de desplazamiento de la rueda del ratón
scroll speed Slow Lenta
scroll speed Normal Normal
Can't render this file because it contains an unexpected character in line 6 and column 60.
+4
View File
@@ -2212,6 +2212,10 @@ key Space Espace
Resize panel window Redimensionner la fenêtre du panneau
Tools Outils
Inspector Inspecteur
Select page {label} Sélectionner la page {label}
Rotate page {label} counterclockwise Faire pivoter la page {label} dans le sens antihoraire
Rotate page {label} clockwise Faire pivoter la page {label} dans le sens horaire
Delete page {label} Supprimer la page {label}
Mouse wheel scroll speed Vitesse de défilement de la molette
scroll speed Slow Lente
scroll speed Normal Normale
Can't render this file because it contains an unexpected character in line 6 and column 39.
+4
View File
@@ -2210,6 +2210,10 @@ key Space Szóköz
Resize panel window Panelablak átméretezése
Tools Eszközök
Inspector Tulajdonságvizsgáló
Select page {label} {label}. oldal kijelölése
Rotate page {label} counterclockwise {label}. oldal elforgatása az óramutató járásával ellentétesen
Rotate page {label} clockwise {label}. oldal elforgatása az óramutató járásával megegyezően
Delete page {label} {label}. oldal törlése
Mouse wheel scroll speed Egérgörgő görgetési sebessége
scroll speed Slow Lassú
scroll speed Normal Normál
Can't render this file because it contains an unexpected character in line 7 and column 44.
+4
View File
@@ -2219,6 +2219,10 @@ key Space Spazio
Panels Pannelli
Resize panels Ridimensiona pannelli
Resize panel window Ridimensiona finestra pannello
Select page {label} Seleziona pagina {label}
Rotate page {label} counterclockwise Ruota pagina {label} in senso antiorario
Rotate page {label} clockwise Ruota pagina {label} in senso orario
Delete page {label} Elimina pagina {label}
Mouse wheel scroll speed Velocità di scorrimento della rotellina del mouse
scroll speed Slow Lenta
scroll speed Normal Normale
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -2212,6 +2212,10 @@ key Space スペース
Resize panel window パネルウィンドウのサイズを変更
Tools ツール
Inspector インスペクター
Select page {label} ページ {label} を選択
Rotate page {label} counterclockwise ページ {label} を反時計回りに回転
Rotate page {label} clockwise ページ {label} を時計回りに回転
Delete page {label} ページ {label} を削除
Mouse wheel scroll speed マウスホイールのスクロール速度
scroll speed Slow 遅い
scroll speed Normal 標準
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -2205,6 +2205,10 @@ key Space Spacja
Text used as a clipping path cannot be moved with this selection. Tekstu używanego jako ścieżka przycinania nie można przenieść razem z tym zaznaczeniem.
These objects could not be moved. Select them again and try a smaller move. Nie udało się przenieść tych obiektów. Zaznacz je ponownie i spróbuj mniejszego przesunięcia.
This page contains objects that cannot be selected together. Ta strona zawiera obiekty, których nie można zaznaczyć razem.
Select page {label} Wybierz stronę {label}
Rotate page {label} counterclockwise Obróć stronę {label} w lewo
Rotate page {label} clockwise Obróć stronę {label} w prawo
Delete page {label} Usuń stronę {label}
Mouse wheel scroll speed Szybkość przewijania kółkiem myszy
scroll speed Slow Wolna
scroll speed Normal Normalna
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -2213,6 +2213,10 @@ key Space Espaço
Resize panel window Redimensionar janela do painel
Tools Ferramentas
Inspector Inspetor
Select page {label} Selecionar página {label}
Rotate page {label} counterclockwise Girar página {label} no sentido anti-horário
Rotate page {label} clockwise Girar página {label} no sentido horário
Delete page {label} Excluir página {label}
calendar date January janeiro
calendar date February fevereiro
calendar date March março
Can't render this file because it contains an unexpected character in line 9 and column 16.
+4
View File
@@ -2125,6 +2125,10 @@ key Space Пробел
Resize panel window Изменить размер окна панели
Tools Инструменты
Inspector Инспектор
Select page {label} Выбрать страницу {label}
Rotate page {label} counterclockwise Повернуть страницу {label} против часовой стрелки
Rotate page {label} clockwise Повернуть страницу {label} по часовой стрелке
Delete page {label} Удалить страницу {label}
Mouse wheel scroll speed Скорость прокрутки колесом мыши
scroll speed Slow Низкая
scroll speed Normal Обычная
Can't render this file because it contains an unexpected character in line 9 and column 16.
+4
View File
@@ -2141,6 +2141,10 @@ key Space స్పేస్
Resize panel window ప్యానెల్ విండో పరిమాణం మార్చు
Tools సాధనాలు
Inspector పరిశీలకుడు
Select page {label} {label} పేజీని ఎంచుకోండి
Rotate page {label} counterclockwise {label} పేజీని అపసవ్య దిశలో తిప్పండి
Rotate page {label} clockwise {label} పేజీని సవ్య దిశలో తిప్పండి
Delete page {label} {label} పేజీని తొలగించండి
Mouse wheel scroll speed మౌస్ వీల్ స్క్రోల్ వేగం
scroll speed Slow నెమ్మదిగా
scroll speed Normal సాధారణం
Can't render this file because it contains an unexpected character in line 13 and column 16.
+4
View File
@@ -2233,6 +2233,10 @@ key Space Пробіл
Panels Панелі
Resize panels Змінити розмір панелей
Resize panel window Змінити розмір вікна панелі
Select page {label} Вибрати сторінку {label}
Rotate page {label} counterclockwise Повернути сторінку {label} проти годинникової стрілки
Rotate page {label} clockwise Повернути сторінку {label} за годинниковою стрілкою
Delete page {label} Видалити сторінку {label}
Mouse wheel scroll speed Швидкість прокручування коліщатком миші
scroll speed Slow Повільна
scroll speed Normal Звичайна
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -2176,6 +2176,10 @@ key Space 空格
Resize panel window 调整面板窗口大小
Tools 工具
Inspector 检查器
Select page {label} 选择页面 {label}
Rotate page {label} counterclockwise 逆时针旋转页面 {label}
Rotate page {label} clockwise 顺时针旋转页面 {label}
Delete page {label} 删除页面 {label}
Mouse wheel scroll speed 鼠标滚轮滚动速度
scroll speed Slow 慢
scroll speed Normal 正常
Can't render this file because it contains an unexpected character in line 9 and column 16.
+4
View File
@@ -2097,6 +2097,10 @@ key Space 空格
Resize panel window 調整面板視窗大小
Tools 工具
Inspector 檢查器
Select page {label} 選取頁面 {label}
Rotate page {label} counterclockwise 逆時針旋轉頁面 {label}
Rotate page {label} clockwise 順時針旋轉頁面 {label}
Delete page {label} 刪除頁面 {label}
Mouse wheel scroll speed 滑鼠滾輪捲動速度
scroll speed Slow 慢
scroll speed Normal 正常
Can't render this file because it contains an unexpected character in line 9 and column 16.
+21 -2
View File
@@ -1000,6 +1000,13 @@ impl PdfCraftApp {
self.active = Some(self.views.len() - 1);
}
self.apply_initial_view(self.views.len() - 1, &initial);
if activate
&& self.left_open
&& self.left == LeftPanel::Tool("organize")
&& let Some(view) = self.views.last_mut()
{
view.organize = true;
}
if activate && let Some(mode) = self.mode_override {
// Explicit mode options do not reset independent --tool / --left choices.
self.mode = mode;
@@ -1440,10 +1447,23 @@ impl PdfCraftApp {
self.pending_panel_reveals.push(docking::Panel::Tools);
}
/// Enter a tool from the catalogue, Home, palette or control channel. Organize is a
/// workspace, so opening its panel also opens the page grid (without toggling it off).
pub(crate) fn select_tool(&mut self, id: &'static str) {
self.left = LeftPanel::Tool(id);
self.reveal_tools();
if let Some(view) = self.active.and_then(|i| self.views.get_mut(i)) {
view.organize = id == "organize";
}
}
/// Select the workspace and its matching tool panel, just like the mode bar.
pub(crate) fn select_mode(&mut self, mode: Mode) {
self.mode = mode;
self.reveal_tools();
if let Some(view) = self.active.and_then(|i| self.views.get_mut(i)) {
view.organize = false;
}
self.left = match mode {
Mode::Edit => LeftPanel::Tool("edit"),
Mode::Convert => LeftPanel::Tool("export"),
@@ -1739,8 +1759,7 @@ impl PdfCraftApp {
("date-language", _) => self.session.set_date_language(Some(value).filter(|v| *v != "auto"))?,
("tool", _) => {
let g = pdfcraft_engine::catalog::group(value).ok_or_else(|| format!("unknown tool {value}"))?;
self.left = LeftPanel::Tool(g.id);
self.reveal_tools();
self.select_tool(g.id);
}
("left", _) => self.left_open = value != "closed",
("home", _) => self.active = None,
+2 -3
View File
@@ -5,7 +5,7 @@ use egui::{Align2, CornerRadius, Rect, Sense, Stroke, vec2};
use pdfcraft_engine::catalog::{Availability, TOOL_GROUPS};
use crate::theme::{self, Tokens};
use crate::{LeftPanel, PdfCraftApp, icons};
use crate::{PdfCraftApp, icons};
struct Hit {
/// The tool panel to open (tools and catalogue items); `None` for plain commands.
@@ -185,8 +185,7 @@ pub fn show(app: &mut PdfCraftApp, ctx: &egui::Context) {
app.palette_open = false;
app.palette_query.clear();
if let Some(g) = group {
app.left_open = true;
app.left = LeftPanel::Tool(g);
app.select_tool(g);
}
if let Some(c) = command {
app.run_command(c);
+1 -1
View File
@@ -55,7 +55,7 @@ fn all_tools(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens) {
let shown = if app.all_tools_expanded { TOOL_GROUPS.len() } else { COLLAPSED_TOOLS.min(TOOL_GROUPS.len()) };
for g in &TOOL_GROUPS[..shown] {
if tool_row(ui, t, g).clicked() {
app.left = LeftPanel::Tool(g.id);
app.select_tool(g.id);
}
}
ui.add_space(4.0);
+16
View File
@@ -992,6 +992,22 @@ fn state_reports_the_selected_pages() {
assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["active"]["selected_pages"], json!([2, 4]));
}
#[test]
fn organize_overview_is_reachable_through_the_control_channel() {
let (mut h, c) = harness();
ok(&mut h, &c, "ui.set", json!({ "key": "tool", "value": "organize" }));
assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["active"]["organize"], true);
h.run_steps(3);
for label in ["Select page 1", "Select page 3", "Rotate page 1 clockwise"] {
ok(&mut h, &c, "ui.click", json!({ "label": label }));
h.run_steps(3);
}
assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["active"]["selected_pages"], json!([1, 3]));
let app = h.state();
let rotations: Vec<_> = app.session.get(app.views[0].id).unwrap().info.pages.iter().map(|p| p.rotation).collect();
assert_eq!(rotations, [90, 0, 0, 0, 0]);
}
#[test]
fn saved_signature_can_be_changed_through_the_control_channel() {
let (mut h, c) = harness();
+114 -1
View File
@@ -50,6 +50,117 @@ fn organize(pages: usize) -> Harness<'static, PdfCraftApp> {
harness(pages, |app| app.set_option("organize", "on").unwrap())
}
#[test]
fn organize_tool_opens_the_overview_directly_and_can_be_reentered() {
let mut h = harness(12, |_| {});
assert!(!h.state().views[0].organize);
h.get_by_label("Organize pages").click();
h.run_steps(3);
assert!(h.state().views[0].organize);
h.get_by_label("Page 1");
h.get_by_label("Page 5");
h.get_by_label("Select page 2").click();
h.run_steps(2);
h.get_by_label("Close").click();
h.run_steps(2);
assert!(!h.state().views[0].organize);
h.state_mut().set_option("tool", "organize").unwrap();
h.state_mut().set_option("tool", "organize").unwrap();
h.run_steps(2);
assert!(h.state().views[0].organize, "entering the tool is idempotent");
assert_eq!(h.state().views[0].target_pages(), [1]);
h.state_mut().set_option("tool", "edit").unwrap();
h.run_steps(2);
assert!(!h.state().views[0].organize, "another tool returns to the document");
}
#[test]
fn organize_tool_chosen_before_opening_a_document_shows_its_pages() {
let mut app = PdfCraftApp::new();
app.set_option("tool", "organize").unwrap();
app.open_bytes("doc.pdf", None, fixture(3)).unwrap();
assert!(app.views[0].organize);
}
#[test]
fn organize_checkboxes_toggle_multiple_pages_without_modifiers() {
let mut h = organize(4);
h.get_by_label("Select page 1").click();
h.run_steps(2);
h.get_by_label("Select page 3").click();
h.run_steps(2);
assert_eq!(h.state().views[0].target_pages(), [0, 2]);
h.get_by_label("Select page 1").click();
h.run_steps(2);
assert_eq!(h.state().views[0].target_pages(), [2]);
h.get_by_label("Page 4").click_modifiers(Modifiers::SHIFT);
h.run_steps(2);
assert_eq!(h.state().views[0].target_pages(), [0, 1, 2, 3], "checkbox toggles update the range anchor");
}
#[test]
fn organize_page_actions_rotate_only_the_named_page_and_undo() {
let mut h = organize(3);
h.get_by_label("Select page 1").click();
h.run_steps(2);
h.get_by_label("Select page 3").click();
h.run_steps(2);
h.get_by_label("Rotate page 1 clockwise").click();
h.run_steps(3);
let rotations = |h: &Harness<'static, PdfCraftApp>| {
h.state().session.get(h.state().views[0].id).unwrap().info.pages.iter().map(|p| p.rotation).collect::<Vec<_>>()
};
assert_eq!(rotations(&h), [90, 0, 0]);
assert_eq!(h.state().views[0].target_pages(), [0, 2], "a local action preserves the multi-selection");
h.get_by_label("Rotate counterclockwise").click();
h.run_steps(3);
assert_eq!(rotations(&h), [0, 0, 270], "toolbar actions apply to the selection");
h.key_press_modifiers(Modifiers::COMMAND, Key::Z);
h.run_steps(3);
assert_eq!(rotations(&h), [90, 0, 0]);
h.get_by_label("Rotate page 1 counterclockwise").click();
h.run_steps(3);
assert_eq!(rotations(&h), [0, 0, 0]);
}
#[test]
fn organize_hover_actions_delete_only_the_named_page_and_undo() {
let mut h = organize(3);
h.get_by_label("Page 2").hover();
h.run_steps(2);
h.get_by_label("Delete page 2").click();
h.run_steps(3);
assert_eq!(page_texts(h.state()), ["Page 1", "Page 3"]);
h.key_press_modifiers(Modifiers::COMMAND, Key::Z);
h.run_steps(3);
assert_eq!(page_texts(h.state()), ["Page 1", "Page 2", "Page 3"]);
let single = organize(1);
assert!(single.get_by_label("Delete page 1").accesskit_node().is_disabled());
}
#[test]
fn organize_local_actions_respect_document_permissions() {
let mut h = harness(3, |app| {
app.apply_edit(pdfcraft_engine::Edit::Protect(pdfcraft_engine::Protection {
permissions_password: Some("owner".into()),
changes: pdfcraft_engine::Changes::None,
..Default::default()
}));
let bytes = app.session.save_bytes(app.views[0].id).unwrap();
app.open_bytes("restricted.pdf", None, bytes.as_ref().clone()).unwrap();
app.set_option("tool", "organize").unwrap();
});
h.get_by_label("Select page 2").click();
h.run_steps(2);
assert!(h.get_by_label("Rotate page 2 clockwise").accesskit_node().is_disabled());
assert!(h.get_by_label("Rotate page 2 counterclockwise").accesskit_node().is_disabled());
assert!(h.get_by_label("Delete page 2").accesskit_node().is_disabled());
let app = h.state();
let view = &app.views[app.active.unwrap()];
assert_eq!(view.target_pages(), [1]);
assert!(!app.session.get(view.id).unwrap().dirty);
}
/// Page labels of the active document, read back from its current bytes.
fn page_texts(app: &PdfCraftApp) -> Vec<String> {
let doc = app.session.get(app.views[0].id).unwrap();
@@ -1401,7 +1512,9 @@ fn zoomed_pages_in_view_are_rendered_at_the_size_drawn() {
app.set_option("organize", "on").unwrap();
app.set_option("grid-zoom", "300").unwrap();
});
let drawn = 146.0 * 3.0 * h.ctx.pixels_per_point();
// The 200 × 300 fixture fits by height; keep its aspect ratio instead of stretching
// it to the full cell width. Request enough pixels for that actual preview width.
let drawn = 194.0 * 3.0 * (200.0 / 300.0) * h.ctx.pixels_per_point();
let sharp = |h: &Harness<'static, PdfCraftApp>, page| h.state().views[0].grid_page_pixels(page);
for _ in 0..200 {
if sharp(&h, 0).is_some_and(|w| w as f32 >= drawn * 0.9) {
+4
View File
@@ -1,5 +1,6 @@
# PdfCraft roadmap: milestones and what's next
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (direct Organize overview and page actions; localized shortcut labels, #496; alpha gate added; convert-and-share re-judged with the cross-app rule → alpha) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (closed issues synced: #774, #756, #817–#821, #809, #819, #816, #844; earlier: localized shortcut labels, #496; alpha gate added; convert-and-share re-judged with the cross-app rule → alpha) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (M6: respect existing form-font encodings; retain source fonts on appearance fallback; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (M6: keep accepted form values when Tab reopens the same field before Save; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
@@ -71,6 +72,8 @@ total matches the parity estimate.
| M0 | Skeleton: workspace, xtask gates, CI | 15–30 | 98% | 1–2 | GitHub workflow, `deny.toml` (licence audit of every dependency), parity checklist (826 features, `xtask parity`) done. Missing: remaining crate stubs, testkit/oracle crates |
| M1 | COS: filters, crypt, parser, xref, writer | 120–200 | 80% | 20–40 | Done:<br>- filters and crypt: every standard-security revision R2–R6 (RC4, AES-128/256), SASLprep, permissions, creating encryption;<br>- cos: parse and repair (including invalid xref object-number ranges), decoded-stream limits including unfiltered data, decrypt on load, re-encrypt on save, incremental and full writing.<br>Corpus: open/edit/save passes on 958 files, and all 7 password-protected files open.<br>Full saves now pack objects into compressed object streams. Fuzzing runs nightly (`xtask fuzz`). Missing: ≥ 250 tests, own image codecs |
| 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. 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; 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 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. 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; 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`. |
| 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; flatten; Fill & Sign; agent tools.<br>Done since: 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 |
@@ -131,6 +134,7 @@ M2 (renderer), M7 (content editing), M10 (OCR and Office), M11 (Preflight and st
| Date | Change | Summary |
|---|---|---|
| 2026-10-11 | minor | M4: direct Organize overview, checkbox selection and individual page actions. Estimates unchanged. |
| 2026-10-11 | minor | Synced with closed issues: encrypted output opens in Acrobat (#774, #836), so gap 1 left rank 0 and convert and share (32–55 h, the gap's 8–15 h removed); organize, XFDF and multi-object move fixes (#817, #821, #809, #819, #844); the #816 panic and the #799–#821 batch fixed; rank 1 names web printing (Windows shipped, #756). Stage and readiness numbers not re-measured |
| 2026-10-11 | minor | M6: respect existing form-font encodings; retain source fonts on appearance fallback. Estimates unchanged. |
| 2026-10-11 | minor | M6: keep accepted form values when Tab reopens the same field before Save. Estimates unchanged. |
+4 -4
View File
@@ -2566,8 +2566,8 @@ milestone = "M4"
status = "shipped"
commands = ["page.organize"]
tools = []
evidence = ["crates/ui-egui/tests/editing.rs::shift_click_selects_a_range_and_moves_it"]
notes = ""
evidence = ["crates/ui-egui/tests/editing.rs::shift_click_selects_a_range_and_moves_it", "crates/ui-egui/tests/editing.rs::organize_tool_opens_the_overview_directly_and_can_be_reentered", "crates/ui-egui/tests/control.rs::organize_overview_is_reachable_through_the_control_channel"]
notes = "Opening the Organize pages tool enters the overview directly. Hovered, focused and selected pages expose individual rotate/delete actions; the toolbar acts on the selection."
[[feature]]
id = "organize.multi-select-pages"
@@ -2579,7 +2579,7 @@ status = "shipped"
commands = ["page.organize"]
tools = []
evidence = ["crates/ui-egui/tests/editing.rs::command_click_toggles_and_rotation_applies_to_selection", "crates/ui-egui/tests/editing.rs::shift_click_selects_a_range_and_moves_it", "crates/ui-egui/tests/editing.rs::organize_select_all_shortcut_preserves_current_page_and_sets_range_anchor", "crates/ui-egui/tests/control.rs::select_all_key_selects_every_page_in_organize"]
notes = "Ctrl+A / ⌘A selects every page in the Organize grid, preserving the current page; text inputs and dialogs keep their own keyboard input."
notes = "Ctrl+A / ⌘A selects every page in the Organize grid, preserving the current page; text inputs and dialogs keep their own keyboard input. Page checkboxes also toggle selection without modifiers (organize_checkboxes_toggle_multiple_pages_without_modifiers)."
[[feature]]
id = "organize.rotate-pages"
@@ -2590,7 +2590,7 @@ milestone = "M4"
status = "shipped"
commands = ["page.rotate", "page.rotate_ccw"]
tools = ["page_rotate"]
evidence = ["crates/organize/src/tests.rs::rotate_uses_inherited_rotation_and_round_trips", "crates/ui-egui/tests/editing.rs::command_click_toggles_and_rotation_applies_to_selection"]
evidence = ["crates/organize/src/tests.rs::rotate_uses_inherited_rotation_and_round_trips", "crates/ui-egui/tests/editing.rs::command_click_toggles_and_rotation_applies_to_selection", "crates/ui-egui/tests/editing.rs::organize_page_actions_rotate_only_the_named_page_and_undo", "crates/ui-egui/tests/editing.rs::organize_local_actions_respect_document_permissions"]
notes = ""
[[feature]]