mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-14 19:27:04 -04:00
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:
27 files changed
+293
-41
No files matched your search
+16
-16
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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]]
|
||||
|
||||
Reference in new issue
Block a user