M6: Organize pages ▸ Page properties sets the selected pages' tab order (#4516)

* M6: Organize pages ▸ Page properties sets the selected pages' tab order

Page properties in the Organize pages panel was listed as planned. In
the app, a page's tab order could only be set for every page at once,
with the form.tab_order commands, which offer no Unspecified. (The
form_tab_order tool already set chosen pages, Unspecified included.)

- Page properties (page.properties) opens a Tab Order dialog for the
  selected pages, with Use Row Order, Use Column Order, Use Document
  Structure and Unspecified. OK writes Edit::SetTabOrder for those pages
  as one undo step. The dialog opens on the pages' order, or with no
  choice made when they differ, and then OK is off until one is chosen.
  OK on the order the pages already have adds no undo step.
- Rows and columns run right to left in a document bound on the right,
  as Use Row Order and Use Column Order are described. PdfCraft reads a
  right-to-left binding from /ViewerPreferences /Direction /R2L
  (ISO 32000-1, 12.2, Table 150). Before, rank_tabs ranked them left to
  right.
- A page whose /Tabs is an indirect object now takes its row, column or
  structure order. Before, rank_tabs read only a direct name and ranked
  the page in annotation order.
- pdfcraft_forms::tab_orders and Document::tab_orders read each page's
  /Tabs. form_tab_order returns them as page_tabs, and reads without
  changing anything when it is given no order or field.
- set_option("dialog", "page-properties") opens the dialog.
- Parity: organize.page-tab-order is shipped, and its notes say the
  Actions tab of Page Properties (organize.page-actions) is still
  planned; the Organize pages grid notes list Page properties.

* Page properties: OK applies only to the document the dialog was opened on

The draft keeps the document's id. When another document is active by
OK, the dialog closes without changing either document.

* Menus: list Page properties… in the Pages menu

The menu bar now lists its commands explicitly, and every command with a menu group must appear in it.

* Page properties: apply the tab order only to an unchanged document

An edit, undo or redo made while the dialog is open can move the pages
at the stored indices. OK now closes the dialog without changing a page
when the document's edit generation differs from when it opened.

* Page properties: say when a document change cancels the tab order

OK shows a notice when the document changed or another document became
active while the dialog was open, so the user knows to open it again.
This commit is contained in:
.CooCoo
2026-10-28 17:48:29 -04:00
committed by GitHub
parent 21f6c29d3e
commit 8f3a4f18bd
18 changed files with 365 additions and 21 deletions
+29 -7
View File
@@ -600,17 +600,23 @@ impl Automation {
};
let mut out = self.apply(a, Edit::MoveInTabOrder { name: field.to_owned(), earlier })?;
out["tab_order"] = self.tab_sequence(a)?;
out["page_tabs"] = self.page_tabs(a)?;
return Ok(out);
}
let order = match a.str("order")? {
"row" | "rows" => pdfcraft_engine::TabOrder::Row,
"column" | "columns" => pdfcraft_engine::TabOrder::Column,
"structure" => pdfcraft_engine::TabOrder::Structure,
"annotations" | "unspecified" => pdfcraft_engine::TabOrder::Annotations,
o => return Err(bad(format!("unknown order {o:?} (row, column, structure, annotations)"))),
let order = match a.opt_str("order")? {
None => None,
Some("row" | "rows") => Some(pdfcraft_engine::TabOrder::Row),
Some("column" | "columns") => Some(pdfcraft_engine::TabOrder::Column),
Some("structure") => Some(pdfcraft_engine::TabOrder::Structure),
Some("annotations" | "unspecified") => Some(pdfcraft_engine::TabOrder::Annotations),
Some(o) => return Err(bad(format!("unknown order {o:?} (row, column, structure, annotations)"))),
};
let mut out = match order {
Some(order) => self.apply(a, Edit::SetTabOrder { pages, order })?,
None => json!({}),
};
let mut out = self.apply(a, Edit::SetTabOrder { pages, order })?;
out["tab_order"] = self.tab_sequence(a)?;
out["page_tabs"] = self.page_tabs(a)?;
Ok(out)
}
@@ -671,6 +677,22 @@ impl Automation {
Ok(json!(seq.iter().map(|x| x.1).collect::<Vec<_>>()))
}
/// Each page's tab order (Page properties ▸ Tab order), in page order.
fn page_tabs(&self, a: &Args) -> Result<Value> {
let names: Vec<&str> = self
.doc(a)?
.tab_orders()
.into_iter()
.map(|o| match o {
pdfcraft_engine::TabOrder::Row => "row",
pdfcraft_engine::TabOrder::Column => "column",
pdfcraft_engine::TabOrder::Structure => "structure",
pdfcraft_engine::TabOrder::Annotations => "annotations",
})
.collect();
Ok(json!(names))
}
pub(crate) fn form_delete_field(&mut self, a: &Args) -> Result<Value> {
let name = a.str("field")?.to_owned();
if !self.doc(a)?.form.iter().any(|f| f.name == name) {
+1 -1
View File
@@ -464,7 +464,7 @@ pub fn tools() -> Vec<ToolDef> {
}),
&["doc"],
)),
t("form_tab_order", "Set the tab order", "Set the tab order of pages (default all): row (top to bottom, left to right), column, structure, or annotations (unspecified). Or order tabs manually: `field` with `move` earlier|later moves that field one place on its page. Returns the resulting order of fields. Undoable.")
t("form_tab_order", "Set the tab order", "Set the tab order of pages (default all): row (top to bottom, left to right; right to left in a document bound on the right), column, structure, or annotations (unspecified). Or order tabs manually: `field` with `move` earlier|later moves that field one place on its page. Without `order` or `field` it only reads. Returns the resulting order of fields (`tab_order`) and each page's tab order (`page_tabs`, as Page properties ▸ Tab Order shows it). Undoable.")
.with(schema(
json!({ "doc": doc(), "order": { "type": "string", "enum": ["row", "column", "structure", "annotations"] }, "pages": pages("to set (default: all)"), "field": { "type": "string" }, "move": { "type": "string", "enum": ["earlier", "later"] } }),
&["doc"],
+17
View File
@@ -4940,6 +4940,23 @@ fn the_calculation_order_is_read_fixed_and_set() {
assert_eq!((value(&mut a, "total"), value(&mut a, "tax")), (json!("0"), json!("0")), "undone with the values it recalculated");
}
#[test]
fn page_tab_orders_are_read_and_set_per_page_through_tools() {
let dir = workdir("pagetabs");
let mut a = auto(&dir);
let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap();
let r = ok(&mut a, "form_tab_order", json!({ "doc": doc }));
assert_eq!(r["page_tabs"], json!(["annotations", "annotations", "annotations"]));
assert!(r["undo"].is_null(), "reading changes nothing: {r}");
let r = ok(&mut a, "form_tab_order", json!({ "doc": doc, "order": "column", "pages": [1, 3] }));
assert_eq!(r["page_tabs"], json!(["column", "annotations", "column"]));
assert_eq!((r["undo"].as_str(), r["dirty"].as_bool()), (Some("Set tab order"), Some(true)));
let r = ok(&mut a, "form_tab_order", json!({ "doc": doc, "order": "structure", "pages": [2] }));
assert_eq!(r["page_tabs"], json!(["column", "structure", "column"]));
let r = ok(&mut a, "form_tab_order", json!({ "doc": doc, "order": "unspecified", "pages": [1] }));
assert_eq!(r["page_tabs"], json!(["annotations", "structure", "column"]));
}
#[test]
fn tab_order_and_field_appearance_through_tools() {
let dir = workdir("taborder");
+1
View File
@@ -392,6 +392,7 @@ pub const COMMANDS: &[CommandSpec] = &[
c("page.extract", "Extract pages…", PAGES, None, Assembly, "file-output"),
c("page.split", "Split document…", PAGES, None, Assembly, "scissors"),
c("page.number", "Number pages…", PAGES, None, Assembly, "hash"),
c("page.properties", "Page properties…", PAGES, None, Modification, "info"),
c("help.shortcuts", "Keyboard shortcuts", HELP, None, Nothing, "circle-help"),
c("help.discord", "Join the ArtCraft Discord", HELP, None, Nothing, "messages-square"),
c("help.app_page", "PdfCraft web page", HELP, None, Nothing, "globe"),
+5
View File
@@ -248,6 +248,11 @@ impl Document {
self.editor.as_ref().map(|e| pdfcraft_forms::calculation_order(&e.cos, &self.form)).unwrap_or_default()
}
/// Page properties ▸ Tab order: each page's tab order, in page order.
pub fn tab_orders(&self) -> Vec<TabOrder> {
self.editor.as_ref().map(|e| pdfcraft_forms::tab_orders(&e.cos)).unwrap_or_default()
}
/// Add alternate text: the figures, in document order.
pub fn figures(&self) -> Vec<a11y::Figure> {
self.editor.as_ref().map(|e| a11y::figures(&e.cos)).unwrap_or_default()
+1 -1
View File
@@ -46,7 +46,7 @@ pub use read::{Fields, acroform, adopted_page_fields, fields, fields_with_adopte
#[cfg(test)]
use read::{MAX_SCRIPT_BYTES, PageWalk, enumerate, form_reads};
use read::{script, script_budget};
pub use tab_order::{TabOrder, move_in_tab_order, set_tab_order};
pub use tab_order::{TabOrder, move_in_tab_order, set_tab_order, tab_orders};
#[cfg(test)]
use values::has_password_flag;
use values::write_value;
+30 -3
View File
@@ -4,9 +4,11 @@ use super::{Document, Field, FormError, ObjRef, Object, invalid};
/// A page's tab order (`/Tabs`).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TabOrder {
/// Rows, top to bottom, left to right within a row (`/R`).
/// Rows, top to bottom, left to right within a row (`/R`; right to left in a document bound
/// on the right).
Row,
/// Columns, left to right, top to bottom within a column (`/C`).
/// Columns, left to right, top to bottom within a column (`/C`; columns right to left in a
/// document bound on the right).
Column,
/// The document structure (`/S`; annotation order when the file has none).
Structure,
@@ -23,6 +25,12 @@ impl TabOrder {
_ => TabOrder::Annotations,
}
}
/// The tab order of the page at `r`, with an indirect `/Tabs` resolved.
fn of_page(doc: &Document, r: ObjRef) -> TabOrder {
let tabs = doc.get(r).as_dict().and_then(|d| d.get(b"Tabs")).map(|o| doc.resolve(o));
TabOrder::of(tabs.as_deref().and_then(Object::as_name))
}
}
/// A widget on a page for tab ordering: (page, field, widget, rect, annotation index).
@@ -38,13 +46,20 @@ pub(crate) fn rank_tabs(doc: &Document, pages: &[ObjRef], annot_index: &std::col
}
}
}
// Rows and columns run right to left in a document bound on the right: rank the mirrored
// rectangles there.
if right_to_left(doc) {
for i in &mut items {
i.3 = [-i.3[2], i.3[1], -i.3[0], i.3[3]];
}
}
let mut ordered: Vec<(usize, usize)> = Vec::with_capacity(items.len());
for (pi, p) in pages.iter().enumerate() {
let mut on: Vec<_> = items.iter().filter(|i| i.0 == pi).collect();
if on.is_empty() {
continue;
}
let tabs = TabOrder::of(doc.get(*p).as_dict().and_then(|d| d.name(b"Tabs")));
let tabs = TabOrder::of_page(doc, *p);
match tabs {
TabOrder::Row | TabOrder::Column => {
let row = tabs == TabOrder::Row;
@@ -87,6 +102,13 @@ pub(crate) fn rank_tabs(doc: &Document, pages: &[ObjRef], annot_index: &std::col
}
}
/// Whether the document is bound on the right (`/ViewerPreferences /Direction /R2L`).
fn right_to_left(doc: &Document) -> bool {
let Some(root) = doc.root() else { return false };
let Some(prefs) = doc.get(root).as_dict().and_then(|c| c.get(b"ViewerPreferences")).map(|o| doc.resolve(o)) else { return false };
prefs.as_dict().and_then(|p| p.get(b"Direction")).map(|o| doc.resolve(o)).is_some_and(|d| d.as_name() == Some(b"R2L"))
}
/// Set the tab order of pages (0-based).
pub fn set_tab_order(doc: &mut Document, pages: &[usize], order: TabOrder) -> Result<(), FormError> {
let refs = page_refs(doc);
@@ -104,6 +126,11 @@ pub fn set_tab_order(doc: &mut Document, pages: &[usize], order: TabOrder) -> Re
Ok(())
}
/// The tab order of each page, in page order (`/Tabs`; a page without one is unspecified).
pub fn tab_orders(doc: &Document) -> Vec<TabOrder> {
page_refs(doc).into_iter().map(|r| TabOrder::of_page(doc, r)).collect()
}
/// Order tabs manually (Acrobat: Fields ▸ Tab Order ▸ Order Tabs Manually, then drag a field):
/// move field `name` one place earlier or later in its page's tab order. The page switches to
/// annotation order (`/Tabs` removed) and its widgets are reordered in `/Annots`; other
+67
View File
@@ -1875,6 +1875,73 @@ fn tab_order_follows_the_page_setting() {
assert!(set_tab_order(&mut doc.clone(), &[3], TabOrder::Row).is_err());
}
#[test]
fn rows_and_columns_tab_right_to_left_in_a_document_bound_on_the_right() {
let mut doc = one_page();
let text = NewField::Text { multiline: false };
// A (50,700) B (300,700)
// C (50,600) D (300,600)
for (name, x, y) in [("C", 50.0, 600.0), ("A", 50.0, 700.0), ("D", 300.0, 600.0), ("B", 300.0, 700.0)] {
add_field(&mut doc, 0, [x, y, x + 200.0, y + 20.0], &text, Some(name)).unwrap();
}
let order = |doc: &Document| -> String {
let mut all: Vec<(usize, String)> = fields(doc).into_iter().map(|f| (f.widgets[0].tab, f.name)).collect();
all.sort();
all.into_iter().map(|x| x.1).collect()
};
let root = doc.root().unwrap();
let mut prefs = pdfcraft_cos::Dict::new();
prefs.set(b"Direction".to_vec(), Object::name("R2L"));
let prefs = doc.add(prefs);
doc.update_dict(root, |c| c.set(b"ViewerPreferences".to_vec(), Object::Ref(prefs))).unwrap();
set_tab_order(&mut doc, &[0], TabOrder::Row).unwrap();
assert_eq!(order(&doc), "BADC", "rows top to bottom, each right to left");
set_tab_order(&mut doc, &[0], TabOrder::Column).unwrap();
assert_eq!(order(&doc), "BDAC", "columns right to left, each top to bottom");
assert_eq!(order(&reopen(&doc)), "BDAC");
set_tab_order(&mut doc, &[0], TabOrder::Annotations).unwrap();
assert_eq!(order(&doc), "CADB", "unspecified keeps annotation order");
doc.update_dict(prefs, |p| p.set(b"Direction".to_vec(), Object::name("L2R"))).unwrap();
set_tab_order(&mut doc, &[0], TabOrder::Row).unwrap();
assert_eq!(order(&doc), "ABCD", "bound on the left");
}
#[test]
fn tab_orders_are_read_for_each_page() {
let mut doc = document(&[
"<< /Type /Catalog /Pages 2 0 R >>".into(),
"<< /Type /Pages /Kids [3 0 R 4 0 R 5 0 R 6 0 R 7 0 R] /Count 5 /MediaBox [0 0 600 800] >>".into(),
"<< /Type /Page /Parent 2 0 R /Tabs /C >>".into(),
"<< /Type /Page /Parent 2 0 R >>".into(),
"<< /Type /Page /Parent 2 0 R /Tabs /S >>".into(),
"<< /Type /Page /Parent 2 0 R /Tabs /W >>".into(),
"<< /Type /Page /Parent 2 0 R /Tabs 8 0 R >>".into(),
"/R".into(),
]);
use TabOrder::{Annotations, Column, Row, Structure};
assert_eq!(tab_orders(&doc), [Column, Annotations, Structure, Annotations, Row], "no /Tabs, or an unknown name: unspecified");
set_tab_order(&mut doc, &[1], Row).unwrap();
set_tab_order(&mut doc, &[0, 4], Annotations).unwrap();
let doc = reopen(&doc);
assert_eq!(tab_orders(&doc), [Annotations, Row, Structure, Annotations, Annotations]);
}
#[test]
fn an_indirect_tabs_value_orders_the_page() {
let mut doc = one_page();
let text = NewField::Text { multiline: false };
for (name, x, y) in [("C", 50.0, 600.0), ("A", 50.0, 700.0), ("D", 300.0, 600.0), ("B", 300.0, 700.0)] {
add_field(&mut doc, 0, [x, y, x + 200.0, y + 20.0], &text, Some(name)).unwrap();
}
let column = doc.add(Object::name("C"));
let page = page_refs(&doc)[0];
doc.update_dict(page, |p| p.set(b"Tabs".to_vec(), Object::Ref(column))).unwrap();
let mut all: Vec<(usize, String)> = fields(&doc).into_iter().map(|f| (f.widgets[0].tab, f.name)).collect();
all.sort();
assert_eq!(all.into_iter().map(|x| x.1).collect::<String>(), "ACBD", "columns, as a direct /C orders them");
assert_eq!(tab_orders(&doc), [TabOrder::Column]);
}
#[test]
fn options_tab_flags_alignment_and_defaults() {
let mut doc = one_page();
+1
View File
@@ -194,6 +194,7 @@ impl PdfCraftApp {
v_pressed: false,
attach_override: None,
duplicate_draft: None,
page_props_draft: None,
field_table_draft: None,
form_preview: false,
window_title: String::new(),
+5
View File
@@ -120,6 +120,11 @@ impl PdfCraftApp {
self.execute("page.number");
Some(Dialog::NumberPages)
}
"page-properties" => {
// Same path as the panel, so the selected pages and their order are seeded.
self.execute("page.properties");
self.dialog
}
"none" => None,
_ => Some(Dialog::About),
}
+14
View File
@@ -355,6 +355,20 @@ impl PdfCraftApp {
}
self.dialog = Some(Dialog::NumberPages);
}
"page.properties" => {
if let Some(i) = active
&& let Some(doc) = self.session.get(self.views[i].id)
{
let pages = self.views[i].target_pages();
let orders = doc.tab_orders();
let mut theirs = pages.iter().map(|p| orders.get(*p).copied());
let first = theirs.next().flatten();
let before = if theirs.all(|o| o == first) { first } else { None };
let generation = doc.edit_generation();
self.page_props_draft = Some(crate::PagePropsDraft { doc: self.views[i].id, generation, pages, before, tab_order: before });
self.dialog = Some(Dialog::PageProps);
}
}
"bookmark.add" => self.bookmark_action(crate::panels::BmAction::New),
"bookmark.from_structure" => self.bookmark_action(crate::panels::BmAction::FromStructure),
"page.organize" => {
+23
View File
@@ -52,6 +52,7 @@ struct Pending {
summarize_now: bool,
optimize_now: bool,
duplicate_now: Option<Edit>,
page_props_now: Option<(pdfcraft_engine::DocId, u64, Edit)>,
replace_now: bool,
open_revision: Option<usize>,
a11y_now: bool,
@@ -218,6 +219,19 @@ fn dialog_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, dialog: Dialog, req: &m
return false;
}
}
Dialog::PageProps => {
let Some(d) = app.page_props_draft.as_mut() else {
req.close = true;
return false;
};
let (ok, cancel) = crate::organize_panel::page_props_body(ui, d);
// An order every page already has adds no undo step.
if ok && let Some(order) = d.tab_order.filter(|o| d.before != Some(*o)) {
req.page_props_now = Some((d.doc, d.generation, Edit::SetTabOrder { pages: d.pages.clone(), order }));
}
req.close = ok || cancel;
return false;
}
Dialog::DuplicateField => {
if !dialog_duplicate_field(ui, app, req) {
return false;
@@ -1643,6 +1657,15 @@ fn finish_actions(app: &mut PdfCraftApp, req: &mut Pending) {
app.duplicate_draft = None;
app.apply_edit(e);
}
// Another document became active, or the document changed, while the dialog was open: the
// pages at those indices may not be the ones chosen.
if let Some((doc, generation, e)) = req.page_props_now.take() {
if app.active_ids().is_some_and(|(_, id)| id == doc) && app.session.get(doc).is_some_and(|d| d.edit_generation() == generation) {
app.apply_edit(e);
} else {
app.notify_tr("The document changed while the dialog was open, so nothing was changed. Open Page properties again to set the tab order.");
}
}
if req.extract_now {
app.extract_selection();
}
+4 -2
View File
@@ -127,8 +127,8 @@ use state::CopyPage;
#[cfg(not(target_arch = "wasm32"))]
use state::OpenRequest;
pub use state::{
CustomDraft, Dialog, DuplicateDraft, LeftPanel, LinkOrigin, Mode, NumberDraft, OpenOutcome, PageClip, PasswordPrompt, PendingLink,
PendingRemoteLink, PropsTab, QuickTool, RecentFile, RightPanel, UnlockPrompt,
CustomDraft, Dialog, DuplicateDraft, LeftPanel, LinkOrigin, Mode, NumberDraft, OpenOutcome, PageClip, PagePropsDraft, PasswordPrompt,
PendingLink, PendingRemoteLink, PropsTab, QuickTool, RecentFile, RightPanel, UnlockPrompt,
};
use state::{DocumentCopyJob, GridFile};
pub use tools::bulk_fields;
@@ -529,6 +529,8 @@ pub struct PdfCraftApp {
pub attach_override: Option<(String, Vec<u8>)>,
/// Duplicate Field: which field and onto which pages.
pub duplicate_draft: Option<DuplicateDraft>,
/// Page Properties: which pages, and the tab order chosen for them.
pub page_props_draft: Option<PagePropsDraft>,
/// Text field table: the table being set up, and the page it goes on.
pub field_table_draft: Option<field_table_ui::FieldTableDraft>,
/// Prepare a form ▸ Preview: fill the form instead of editing its fields.
+1
View File
@@ -190,6 +190,7 @@ const PAGES: &[&str] = &[
"page.crop",
"page.boxes",
"page.number",
"page.properties",
SEP,
"bookmark.add",
"bookmark.from_structure",
+27 -1
View File
@@ -159,7 +159,9 @@ pub(crate) fn panel(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens) {
if row(ui, t, "tag", tl!("Page labels"), Availability::Ready, false).clicked() {
act = Some(Act::Command("page.number"));
}
row(ui, t, "info", tl!("Page properties"), Availability::Later, false);
if row(ui, t, "info", tl!("Page properties"), Availability::Ready, false).clicked() {
act = Some(Act::Command("page.properties"));
}
});
if close {
@@ -182,6 +184,30 @@ pub(crate) fn panel(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens) {
}
}
/// Page Properties ▸ Tab Order for the pages it opened on: Use Row Order, Use Column Order, Use
/// Document Structure and Unspecified, then OK and Cancel. OK is off until a choice is made.
/// Returns (OK, Cancel).
pub(crate) fn page_props_body(ui: &mut egui::Ui, d: &mut crate::PagePropsDraft) -> (bool, bool) {
use pdfcraft_engine::TabOrder;
ui.label(egui::RichText::new(tl!("Page Properties")).font(theme::semibold(18.0)));
widgets::section_title(ui, tl!("Tab Order"));
for (order, label) in [
(TabOrder::Row, tl!("Use Row Order")),
(TabOrder::Column, tl!("Use Column Order")),
(TabOrder::Structure, tl!("Use Document Structure")),
(TabOrder::Annotations, tl!("Unspecified")),
] {
widgets::radio_value(ui, &mut d.tab_order, Some(order), label);
}
let (mut ok, mut cancel) = (false, false);
ui.add_space(12.0);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ok = ui.add_enabled_ui(d.tab_order.is_some(), |ui| widgets::pill_button(ui, tl!("OK"), true)).inner.clicked();
cancel = widgets::pill_button(ui, tl!("Cancel"), false).clicked();
});
(ok, cancel)
}
/// Whether a row works in this build.
#[derive(Clone, Copy, PartialEq, Eq)]
enum Availability {
+14
View File
@@ -109,6 +109,8 @@ pub enum Dialog {
Split,
/// Pages ▸ Number pages… (page labels).
NumberPages,
/// Organize pages ▸ Page properties (the tab order of the selected pages).
PageProps,
/// Protect Using Password.
Protect,
/// Set Page Boxes (crop, trim, bleed, art, media).
@@ -213,6 +215,18 @@ pub struct DuplicateDraft {
pub to: usize,
}
/// Page Properties: the document and pages it applies to (0-based), the document's edit
/// generation when the dialog opened, and the pages' tab order as they have it (`None` when they
/// differ) and as chosen (`None` until one is chosen).
#[derive(Clone, Debug, PartialEq)]
pub struct PagePropsDraft {
pub doc: DocId,
pub generation: u64,
pub pages: Vec<usize>,
pub before: Option<pdfcraft_engine::TabOrder>,
pub tab_order: Option<pdfcraft_engine::TabOrder>,
}
/// Copied pages: their document's name and bytes (as it was when copied) and the pages.
#[derive(Clone, Debug)]
pub struct PageClip {
+119
View File
@@ -6206,6 +6206,125 @@ fn organize_panel_comes_with_the_grid_and_closes_it() {
assert!(h.query_by_label("Page properties").is_none(), "the panel is gone");
}
/// Organize pages ▸ Page properties ▸ Tab Order sets the tab order of the selected pages only,
/// as one undo step, and opens on the order the pages have.
#[test]
fn page_properties_sets_the_tab_order_of_the_selected_pages() {
use pdfcraft_engine::TabOrder::{Annotations, Column, Row};
let tabs = |h: &Harness<'static, PdfCraftApp>| h.state().session.get(h.state().views[0].id).unwrap().tab_orders();
// The undo and redo labels: a new step replaces the redo step an undo left.
let undo = |h: &Harness<'static, PdfCraftApp>| {
let doc = h.state().session.get(h.state().views[0].id).unwrap();
(doc.can_undo().map(str::to_owned), doc.can_redo().map(str::to_owned))
};
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("Page properties").click();
h.run_steps(3);
assert_eq!(h.state().dialog, Some(Dialog::PageProps));
assert_eq!(h.state().page_props_draft.as_ref().unwrap().tab_order, Some(Annotations), "opens on Unspecified, as the pages have it");
h.get_by_label("Use Column Order").click();
h.run_steps(2);
h.get_by_label("OK").click();
h.run_steps(3);
assert_eq!(h.state().dialog, None);
assert_eq!(tabs(&h), [Column, Annotations, Column]);
assert_eq!(undo(&h).0.as_deref(), Some("Set tab order"));
h.key_press_modifiers(Modifiers::COMMAND, Key::Z);
h.run_steps(3);
assert_eq!(tabs(&h), [Annotations; 3], "one undo step");
// Pages with different orders open with no choice made, and OK is off until one is.
h.state_mut().apply_edit(pdfcraft_engine::Edit::SetTabOrder { pages: vec![0], order: Row });
h.run_steps(2);
h.state_mut().views[0].select_pages(&[0, 1]);
assert!(h.state_mut().execute("page.properties"));
h.run_steps(3);
assert_eq!(h.state().page_props_draft.as_ref().unwrap().tab_order, None);
assert!(h.get_by_label("OK").accesskit_node().is_disabled());
h.get_by_label("Use Row Order").click();
h.run_steps(2);
h.get_by_label("OK").click();
h.run_steps(3);
assert_eq!(tabs(&h), [Row, Row, Annotations]);
// OK on the order the pages already have adds no undo step, and Cancel changes nothing.
h.key_press_modifiers(Modifiers::COMMAND, Key::Z);
h.run_steps(3);
let before = (tabs(&h), undo(&h));
assert_eq!(before.1.1.as_deref(), Some("Set tab order"), "the undone step can be redone");
h.state_mut().views[0].select_pages(&[2]);
assert!(h.state_mut().execute("page.properties"));
h.run_steps(3);
h.get_by_label("OK").click();
h.run_steps(3);
assert_eq!((tabs(&h), undo(&h)), before);
assert!(h.state_mut().execute("page.properties"));
h.run_steps(3);
h.get_by_label("Use Document Structure").click();
h.run_steps(2);
h.get_by_label("Cancel").click();
h.run_steps(3);
assert_eq!(h.state().dialog, None);
assert_eq!((tabs(&h), undo(&h)), before);
}
/// Page properties sets the tab order of the document it was opened on: when another document
/// is active by OK, neither document changes.
#[test]
fn page_properties_does_not_apply_to_another_active_document() {
use pdfcraft_engine::TabOrder::Annotations;
let mut h = organize(3);
let first = h.state().views[0].id;
h.state_mut().views[0].select_pages(&[0]);
assert!(h.state_mut().execute("page.properties"));
h.run_steps(3);
h.get_by_label("Use Column Order").click();
h.run_steps(2);
h.state_mut().open_bytes("other.pdf", None, fixture(3)).unwrap();
h.run_steps(4);
let other = h.state().views[1].id;
assert_eq!(h.state().active_ids().map(|(_, id)| id), Some(other));
h.get_by_label("OK").click();
h.run_steps(3);
assert_eq!(h.state().dialog, None);
for id in [first, other] {
let doc = h.state().session.get(id).unwrap();
assert_eq!(doc.tab_orders(), [Annotations; 3]);
assert!(doc.can_undo().is_none());
}
}
/// Page properties sets the tab order of the pages it was opened on: when an edit changes the
/// document while the dialog is open, OK closes it without changing a page.
#[test]
fn page_properties_does_not_apply_after_the_document_changes() {
use pdfcraft_engine::TabOrder::Annotations;
let mut h = organize(3);
let id = h.state().views[0].id;
h.state_mut().views[0].select_pages(&[0]);
assert!(h.state_mut().execute("page.properties"));
h.run_steps(3);
h.get_by_label("Use Column Order").click();
h.run_steps(2);
// Page 1 becomes page 3, so index 0 is another page now.
assert!(h.state_mut().execute("page.reverse"));
h.run_steps(3);
h.get_by_label("OK").click();
h.run_steps(3);
assert_eq!(h.state().dialog, None);
let doc = h.state().session.get(id).unwrap();
assert_eq!(doc.tab_orders(), [Annotations; 3]);
assert_ne!(doc.can_undo(), Some("Set tab order"));
assert_eq!(
h.state().toast.as_ref().map(|t| t.0.as_str()),
Some("The document changed while the dialog was open, so nothing was changed. Open Page properties again to set the tab order.")
);
}
/// Leaving Organize (a page opened from the grid, or the grid's Close) goes back to the
/// workspace's panel: under the Edit tab, the Edit panel, not All tools.
#[test]
+6 -6
View File
@@ -2656,7 +2656,7 @@ status = "shipped"
commands = ["page.organize"]
tools = []
evidence = ["crates/ui-egui/tests/it/editing.rs::shift_click_selects_a_range_and_moves_it", "crates/ui-egui/tests/it/editing.rs::organize_tool_opens_the_overview_directly_and_can_be_reentered", "crates/ui-egui/tests/it/editing.rs::organize_opens_on_the_current_page", "crates/ui-egui/tests/it/control.rs::organize_overview_is_reachable_through_the_control_channel", "crates/ui-egui/tests/it/editing.rs::organize_panel_comes_with_the_grid_and_closes_it", "crates/ui-egui/tests/it/editing.rs::leaving_organize_goes_back_to_the_workspace_panel"]
notes = "Opening the Organize pages tool enters the overview directly, on the current page: selected and scrolled into view (#1910). Hovered, focused and selected pages expose individual rotate/delete actions; the toolbar acts on the selection. The left panel is laid out as Acrobat's Organize pages panel: Pages selected (typed ranges, or All, Even, Odd, Landscape or Portrait pages; empty, it shows the current page), rotate, delete, extract, Insert (a blank page, pages from a file, duplicates), Replace, Split, Bates numbering, Set page boxes, Crop pages, Print pages and Page labels. Closing it, by its Close, the grid's Close or opening a page, returns to the workspace's own panel (Edit, Convert, E-Sign or All tools). Page transitions, Page templates and Page properties are listed but planned."
notes = "Opening the Organize pages tool enters the overview directly, on the current page: selected and scrolled into view (#1910). Hovered, focused and selected pages expose individual rotate/delete actions; the toolbar acts on the selection. The left panel is laid out as Acrobat's Organize pages panel: Pages selected (typed ranges, or All, Even, Odd, Landscape or Portrait pages; empty, it shows the current page), rotate, delete, extract, Insert (a blank page, pages from a file, duplicates), Replace, Split, Bates numbering, Set page boxes, Crop pages, Print pages, Page labels and Page properties (the selected pages' tab order). Closing it, by its Close, the grid's Close or opening a page, returns to the workspace's own panel (Edit, Convert, E-Sign or All tools). Page transitions and Page templates are listed but planned."
[[feature]]
id = "organize.multi-select-pages"
@@ -3240,11 +3240,11 @@ title = "Page tab order property"
area = "D"
tier = "P1"
milestone = "M6"
status = "planned"
commands = []
tools = []
evidence = []
notes = ""
status = "shipped"
commands = ["page.properties"]
tools = ["form_tab_order"]
evidence = ["crates/ui-egui/tests/it/editing.rs::page_properties_sets_the_tab_order_of_the_selected_pages", "crates/forms/src/tests.rs::tab_orders_are_read_for_each_page", "crates/forms/src/tests.rs::rows_and_columns_tab_right_to_left_in_a_document_bound_on_the_right", "crates/forms/src/tests.rs::an_indirect_tabs_value_orders_the_page", "crates/automation/tests/it/automation.rs::page_tab_orders_are_read_and_set_per_page_through_tools"]
notes = "Organize pages ▸ Page properties ▸ Tab Order sets the selected pages' order: Use Row Order, Use Column Order, Use Document Structure or Unspecified, as one undo step. It opens on the pages' order, or with no choice when they differ. Rows and columns run right to left in a document bound on the right. form_tab_order reads each page's order (page_tabs) and sets it for chosen pages. Acrobat's Page Properties also has an Actions tab (organize.page-actions), still planned."
[[feature]]
id = "organize.page-actions"