mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-29 04:36:47 -06:00
P: Page Numbering View (section/absolute) and Go to Page by name
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3609862c2e
commit
11212736f0
@@ -39,6 +39,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
||||
- Variable fonts: each named instance is a style (Font menu, shaping with feature variations, metrics, outlines) and embeds at its axis settings in PDF; free axis sliders still to come.
|
||||
- Object ▸ Select (first/next above, next/last below, container, content, previous/next in group), full Object ▸ Convert Shape (`object.convertShape`), Table ▸ Split Cell Horizontally/Vertically.
|
||||
- Attributes panel (`object.attributes`: Overprint Fill/Stroke/Gap, Nonprinting), with Overprint Gap in Overprint Preview and IDML.
|
||||
- Page Numbering View (Preferences: section or absolute): page labels everywhere follow it; Go to Page takes section names, numbers or `+n`.
|
||||
- Drag and drop text editing: drag selected text to move it (Alt copies) with its formatting (`text.release`).
|
||||
- Image Import Options: place any page of a multi-page PDF (`file.place {pdfPage}`; the app asks which page), shown on screen and embedded as that page in PDF export.
|
||||
- Define Lists: named numbered lists that continue across stories in page order (`list.define`, paragraph `listName` / `startAt`), round-tripped through IDML; `null` in `type.para` / `type.char` now removes an override.
|
||||
|
||||
@@ -682,3 +682,26 @@ mod spread_tests {
|
||||
d.check().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod page_numbering_view_tests {
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
|
||||
#[test]
|
||||
fn section_and_absolute_page_numbering_view() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"pages": 4})).unwrap();
|
||||
s.execute("layout.section", &json!({"page": 1, "startNumber": 1, "style": "lowerRoman"})).unwrap();
|
||||
s.execute("layout.section", &json!({"page": 3, "startNumber": 1})).unwrap();
|
||||
assert_eq!((0..4).map(|i| s.page_label(i)).collect::<Vec<_>>(), ["i", "ii", "1", "2"]);
|
||||
assert_eq!(s.resolve_page("ii"), Some(1));
|
||||
assert_eq!(s.resolve_page("1"), Some(2), "section name first");
|
||||
assert_eq!(s.resolve_page("+1"), Some(0), "+n is a position");
|
||||
s.execute("prefs.set", &json!({"absolutePageNumbers": true})).unwrap();
|
||||
assert_eq!((0..4).map(|i| s.page_label(i)).collect::<Vec<_>>(), ["1", "2", "3", "4"]);
|
||||
assert_eq!(s.resolve_page("1"), Some(0));
|
||||
assert_eq!(s.resolve_page("9"), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Preferences",
|
||||
[],
|
||||
None,
|
||||
"{showHiddenCharacters?, typographersQuotes?, polygonSides?, starInset?, scaleStrokes?, dimensionsIncludeStroke?, transformationsAreTotals?} → all application preferences",
|
||||
"{showHiddenCharacters?, typographersQuotes?, polygonSides?, starInset?, scaleStrokes?, dimensionsIncludeStroke?, transformationsAreTotals?, absolutePageNumbers?} → all application preferences",
|
||||
super::always,
|
||||
|s, p| {
|
||||
let cur = serde_json::to_value(&s.prefs).map_err(|e| bad("prefs.set", e.to_string()))?;
|
||||
|
||||
@@ -135,6 +135,9 @@ pub struct Prefs {
|
||||
/// objects (a graphic in a frame, objects in groups) are measured on the pasteboard rather
|
||||
/// than relative to their container.
|
||||
pub transformations_are_totals: bool,
|
||||
/// Preferences › General › Page Numbering View: Absolute (1, 2, 3… from the first page) instead
|
||||
/// of section numbering (the page names, e.g. "iv", "A-3").
|
||||
pub absolute_page_numbers: bool,
|
||||
/// Preferences › Type › Smart Text Reflow: pages follow the primary text frame's story
|
||||
/// (added while it oversets, empty ones at the end removed).
|
||||
pub smart_text_reflow: bool,
|
||||
@@ -150,6 +153,7 @@ impl Default for Prefs {
|
||||
scale_strokes: true,
|
||||
dimensions_include_stroke: true,
|
||||
transformations_are_totals: true,
|
||||
absolute_page_numbers: false,
|
||||
smart_text_reflow: true,
|
||||
}
|
||||
}
|
||||
@@ -212,6 +216,32 @@ impl Session {
|
||||
pub fn active_index(&self) -> Option<usize> {
|
||||
self.active
|
||||
}
|
||||
/// A page's label as the UI shows it (Page Numbering View).
|
||||
pub fn page_label(&self, abs: usize) -> String {
|
||||
match self.active() {
|
||||
Some(d) if !self.prefs.absolute_page_numbers => d.doc.page_name(abs),
|
||||
_ => (abs + 1).to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The page a typed page reference means: `+n` is always absolute; otherwise a section page
|
||||
/// name (section numbering) or a position (absolute numbering, or no page has that name).
|
||||
pub fn resolve_page(&self, s: &str) -> Option<usize> {
|
||||
let d = self.active()?;
|
||||
let n = d.doc.page_count();
|
||||
let s = s.trim();
|
||||
let pos = |t: &str| t.parse::<usize>().ok().filter(|k| (1..=n).contains(k)).map(|k| k - 1);
|
||||
if let Some(t) = s.strip_prefix('+') {
|
||||
return pos(t);
|
||||
}
|
||||
if !self.prefs.absolute_page_numbers
|
||||
&& let Some(i) = (0..n).find(|i| d.doc.page_name(*i).eq_ignore_ascii_case(s))
|
||||
{
|
||||
return Some(i);
|
||||
}
|
||||
pos(s)
|
||||
}
|
||||
|
||||
pub fn active(&self) -> Option<&DocState> {
|
||||
self.active.and_then(|i| self.docs.get(i))
|
||||
}
|
||||
|
||||
@@ -466,13 +466,13 @@ pub fn status_bar(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
if icons::button(ui, "prev", 14.0, false, "Previous Spread").clicked() {
|
||||
crate::canvas::go_to_page(app, cur.saturating_sub(1));
|
||||
}
|
||||
let name = app.session.active().map(|d| d.doc.page_name(cur)).unwrap_or_default();
|
||||
let name = app.session.page_label(cur);
|
||||
let (fr, fresp) = ui.allocate_exact_size(vec2(80.0, 14.0), Sense::click());
|
||||
ui.painter().rect(fr, 0.0, t.input, Stroke::new(1.0, t.field_border), egui::StrokeKind::Inside);
|
||||
ui.painter().text(fr.left_center() + vec2(5.0, 0.0), egui::Align2::LEFT_CENTER, &name, small.clone(), t.text);
|
||||
ui.painter().text(fr.right_center() - vec2(5.0, 0.0), egui::Align2::RIGHT_CENTER, "▾", small.clone(), t.text);
|
||||
egui::Popup::menu(&fresp).show(|ui| {
|
||||
let names: Vec<String> = app.session.active().map(|d| (0..n).map(|i| d.doc.page_name(i)).collect()).unwrap_or_default();
|
||||
let names: Vec<String> = (0..n).map(|i| app.session.page_label(i)).collect();
|
||||
egui::ScrollArea::vertical().max_height(300.0).show(ui, |ui| {
|
||||
for (i, nm) in names.iter().enumerate() {
|
||||
if ui.button(nm).clicked() {
|
||||
|
||||
@@ -25,7 +25,7 @@ impl Dialog {
|
||||
json!({"preset": "Letter", "width": "51p0", "height": "66p0", "pages": 1, "facingPages": true, "columns": 1, "gutter": "1p0",
|
||||
"marginTop": "3p0", "marginBottom": "3p0", "marginInside": "3p0", "marginOutside": "3p0", "bleed": "0p0", "primaryTextFrame": false})
|
||||
}
|
||||
"goToPage" => json!({"page": 1}),
|
||||
"goToPage" => json!({"page": "1"}),
|
||||
"insertTable" => json!({"bodyRows": 4, "columns": 4, "headerRows": 0, "footerRows": 0}),
|
||||
"insertXref" => json!({"linkTo": "paragraph", "style": "", "target": "", "format": ""}),
|
||||
"findChange" => json!({"find": "", "change": "", "grep": false, "caseSensitive": false, "wholeWord": false, "scope": "document"}),
|
||||
@@ -519,6 +519,9 @@ fn preferences(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
ui.label(egui::RichText::new("Transform").font(semibold(12.0)));
|
||||
check(ui, d, "dimensionsIncludeStroke", "Dimensions Include Stroke Weight");
|
||||
check(ui, d, "transformationsAreTotals", "Transformations are Totals");
|
||||
ui.add_space(6.0);
|
||||
ui.label(egui::RichText::new("Page Numbering").font(semibold(12.0)));
|
||||
check(ui, d, "absolutePageNumbers", "Absolute Numbering (instead of Section Numbering)");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -821,8 +824,19 @@ pub fn confirm(app: &mut DesignApp) -> Result<Value, String> {
|
||||
)
|
||||
}
|
||||
"goToPage" => {
|
||||
crate::canvas::go_to_page(app, (d.n("page").unwrap_or(1.0) as usize).saturating_sub(1));
|
||||
Ok(Value::Null)
|
||||
// A page name ("iv", "A-3"), a number, or "+n" for an absolute position.
|
||||
let typed = match d.fields.get("page") {
|
||||
Some(Value::String(s)) => s.clone(),
|
||||
Some(v) => v.to_string(),
|
||||
None => "1".into(),
|
||||
};
|
||||
match app.session.resolve_page(&typed) {
|
||||
Some(abs) => {
|
||||
crate::canvas::go_to_page(app, abs);
|
||||
Ok(Value::Null)
|
||||
}
|
||||
None => Err(format!("no page \"{typed}\"")),
|
||||
}
|
||||
}
|
||||
"insertTable" => app.run(
|
||||
"table.insert",
|
||||
@@ -882,7 +896,7 @@ pub fn confirm(app: &mut DesignApp) -> Result<Value, String> {
|
||||
"preferences" => {
|
||||
app.run(
|
||||
"prefs.set",
|
||||
json!({"scaleStrokes": d.b("scaleStrokes"), "dimensionsIncludeStroke": d.b("dimensionsIncludeStroke"), "transformationsAreTotals": d.b("transformationsAreTotals"), "typographersQuotes": d.b("typographersQuotes"), "smartTextReflow": d.b("smartTextReflow")}),
|
||||
json!({"scaleStrokes": d.b("scaleStrokes"), "dimensionsIncludeStroke": d.b("dimensionsIncludeStroke"), "transformationsAreTotals": d.b("transformationsAreTotals"), "absolutePageNumbers": d.b("absolutePageNumbers"), "typographersQuotes": d.b("typographersQuotes"), "smartTextReflow": d.b("smartTextReflow")}),
|
||||
)?;
|
||||
let view = match d.s("displayQuality").as_str() {
|
||||
"fast" => "view.fastDisplay",
|
||||
|
||||
@@ -56,7 +56,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
|
||||
("view.fastDisplay", "Fast Display", Some("Cmd+Alt+Shift+Z"), "{} — placed graphics as grey boxes, no effects"),
|
||||
("view.typicalDisplay", "Typical Display", Some("Cmd+Alt+Z"), "{} — low-resolution image proxies"),
|
||||
("view.highQualityDisplay", "High Quality Display", Some("Cmd+Alt+H"), "{} — full-resolution images"),
|
||||
("view.goToPage", "Go to Page…", Some("Cmd+J"), "{page}"),
|
||||
("view.goToPage", "Go to Page…", Some("Cmd+J"), "{page: number (position) | \"name\" (section page name, or \"+n\" for a position)}"),
|
||||
(
|
||||
"window.panel",
|
||||
"Show Panel",
|
||||
@@ -748,9 +748,13 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
|
||||
"view.textThreads" => flag(&mut app.ui.text_threads),
|
||||
"view.hiddenCharacters" => flag(&mut app.ui.hidden_characters),
|
||||
"view.goToPage" => {
|
||||
match p.get("page").and_then(Value::as_u64) {
|
||||
Some(n) => crate::canvas::go_to_page(app, (n as usize).saturating_sub(1)),
|
||||
None => app.ui.dialog = Some(crate::dialogs::Dialog::new("goToPage", json!({}))),
|
||||
match p.get("page") {
|
||||
Some(Value::Number(n)) => crate::canvas::go_to_page(app, (n.as_u64().unwrap_or(1) as usize).saturating_sub(1)),
|
||||
Some(Value::String(s)) => match app.session.resolve_page(s) {
|
||||
Some(abs) => crate::canvas::go_to_page(app, abs),
|
||||
None => return Some(Err(format!("no page \"{s}\""))),
|
||||
},
|
||||
_ => app.ui.dialog = Some(crate::dialogs::Dialog::new("goToPage", json!({}))),
|
||||
}
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
@@ -207,9 +207,14 @@ pub fn show(app: &mut DesignApp, ui: &mut Ui) {
|
||||
ui.spacing_mut().item_spacing.y = 4.0;
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(
|
||||
egui::RichText::new(format!("Page: {}, {} Layer{}", doc.page_name(cur), doc.layers.len(), if doc.layers.len() == 1 { "" } else { "s" }))
|
||||
.size(11.0)
|
||||
.color(t.text_dim),
|
||||
egui::RichText::new(format!(
|
||||
"Page: {}, {} Layer{}",
|
||||
app.session.page_label(cur),
|
||||
doc.layers.len(),
|
||||
if doc.layers.len() == 1 { "" } else { "s" }
|
||||
))
|
||||
.size(11.0)
|
||||
.color(t.text_dim),
|
||||
);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if icons::button(ui, "trash", 20.0, false, "Delete Layer").clicked() {
|
||||
|
||||
@@ -156,7 +156,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
// Drag and drop: pages reorder, parents apply.
|
||||
resp.dnd_set_drag_payload(PageDrag::Page(abs));
|
||||
if resp.dragged() {
|
||||
drag_ghost(ui, &doc.page_name(abs), &t);
|
||||
drag_ghost(ui, &app.session.page_label(abs), &t);
|
||||
}
|
||||
if dragging && resp.contains_pointer() {
|
||||
ui.painter().rect_stroke(pr.expand(1.5), 0.0, Stroke::new(2.0, BADGE), StrokeKind::Outside);
|
||||
@@ -173,7 +173,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
resp.context_menu(|ui| page_menu(app, ui, &doc, abs, si));
|
||||
// Page number under the page: white on a blue badge when selected.
|
||||
let name = doc.page_name(abs);
|
||||
let name = app.session.page_label(abs);
|
||||
let g = ui.painter().layout_no_wrap(name, crate::theme::semibold(11.0), Color32::WHITE);
|
||||
let c = egui::pos2(pr.center().x, pr.max.y + 11.0);
|
||||
if selected {
|
||||
|
||||
@@ -296,8 +296,10 @@ fn document_sections(app: &mut DesignApp, ui: &mut Ui) {
|
||||
// Page.
|
||||
section(ui, "Page", true);
|
||||
let b = block(ui, FIELD_H);
|
||||
let names: Vec<String> = (0..d.page_count()).map(|i| d.page_name(i)).collect();
|
||||
let picked = place(ui, sub(b, 0.0, 0.0, 93.0, FIELD_H), |ui| widgets::dropdown_list(ui, &d.page_name(cur), 93.0, &names, Some(cur)));
|
||||
let names: Vec<String> = (0..d.page_count()).map(|i| app.session.page_label(i)).collect();
|
||||
let picked = place(ui, sub(b, 0.0, 0.0, 93.0, FIELD_H), |ui| {
|
||||
widgets::dropdown_list(ui, &names[cur.min(names.len().saturating_sub(1))], 93.0, &names, Some(cur))
|
||||
});
|
||||
if let Some(pg) = picked {
|
||||
crate::canvas::go_to_page(app, pg);
|
||||
}
|
||||
@@ -1758,7 +1760,7 @@ pub fn preflight_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
});
|
||||
divider(ui);
|
||||
for i in issues {
|
||||
let page = i.page.and_then(|p| app.session.active().map(|d| d.doc.page_name(p)));
|
||||
let page = i.page.filter(|_| app.session.active().is_some()).map(|p| app.session.page_label(p));
|
||||
let text = format!("{} {}", if i.severity == "error" { "⛔" } else { "⚠" }, i.message);
|
||||
let resp = ui.add(egui::Button::new(egui::RichText::new(text).size(11.0)).frame(false));
|
||||
if let Some(pg) = &page {
|
||||
|
||||
+3
-3
@@ -10,13 +10,13 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
||||
| Priority | Features | Done | Partial | Missing | Parity |
|
||||
|---|---|---|---|---|---|
|
||||
| P0 | 102 | 99 | 3 | 0 | 99% |
|
||||
| P1 | 97 | 72 | 23 | 2 | 86% |
|
||||
| P1 | 97 | 73 | 22 | 2 | 87% |
|
||||
| P2 | 76 | 12 | 13 | 51 | 24% |
|
||||
|
||||
| Area | Parity |
|
||||
|---|---|
|
||||
| Application shell & workspace | 80% |
|
||||
| Documents, pages, spreads | 84% |
|
||||
| Documents, pages, spreads | 85% |
|
||||
| Layers | 95% |
|
||||
| Frames, shapes & paths | 91% |
|
||||
| Transform | 100% |
|
||||
@@ -69,7 +69,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t
|
||||
| Documents, pages, spreads | Spread rotation (view) | P2 | M |
|
||||
| Documents, pages, spreads | Pasteboard | P0 | D |
|
||||
| Documents, pages, spreads | Sections & numbering | P0 | D |
|
||||
| Documents, pages, spreads | Page numbering display | P1 | P |
|
||||
| Documents, pages, spreads | Page numbering display | P1 | D |
|
||||
| Documents, pages, spreads | Smart Text Reflow | P1 | D |
|
||||
| Documents, pages, spreads | Primary text frame | P1 | P |
|
||||
| Documents, pages, spreads | Parent pages | P0 | D |
|
||||
|
||||
Reference in New Issue
Block a user