mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-30 05:06:48 -06:00
feat(ui): Preview in the New Document window
A Preview checkbox beside Close and Create shows a document with the window's settings in the workspace and rebuilds it as they change: size, orientation, pages, facing pages, margins, columns, gutter, bleed and slug. It is off by default and kept for the session. Fields that don't hold a value yet, and sizes file.new refuses, keep the last preview. The engine keeps the preview document (Session::preview_new_document, end_new_document_preview): it replaces itself in its tab, takes no Untitled number and is never written as recovery data. Create removes it and makes the document with file.new, so it starts with a clean undo history. Close, Escape, ui.dialog.cancel, turning Preview off or another dialog replacing the window remove it and show the previously active document again, unchanged.
This commit is contained in:
@@ -129,6 +129,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
|
||||
|
||||
Newest first.
|
||||
|
||||
- **2026-10-13** · New Document has Preview (off by default, kept for the session): a document with the window's settings shows in the workspace and follows them as they change. Create keeps it with a clean undo history; Close removes it and shows the previous document again. The preview takes no Untitled number and writes no recovery data. No readiness percentage change.
|
||||
- **2026-10-12** · Window close and application/menu Quit ask about unsaved documents; later Cancel keeps every tab open, including earlier Don't Save choices. Approvals are rechecked after edits. Cancelable macOS Dock Quit remains open (#544). No readiness percentage change.
|
||||
- **2026-10-12** · Each running app keeps recovery data in its own locked folder; current folder and lock ownership are checked before writes and cleanup. Failed isolation disables recovery with a warning instead of sharing document IDs. No readiness percentage change.
|
||||
- **2026-10-12** · Native engine saves and exports replace files atomically; failed writes retain the original, private staging preserves Unix permissions, and valid long filenames work. Desktop Services and CLI raster exports remain separate follow-ups. No readiness percentage change.
|
||||
|
||||
@@ -145,13 +145,19 @@ fn find_preset(s: &Session, name: &str) -> Option<NewDocument> {
|
||||
NewDocument::from_preset(name).or_else(|| s.prefs.document_presets.iter().find(|p| p.title.eq_ignore_ascii_case(name.trim())).cloned())
|
||||
}
|
||||
|
||||
fn file_new(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
/// The settings `file.new` makes a document with for `p`: its preset, then the settings over it.
|
||||
fn new_document_settings(s: &Session, p: &Value) -> Result<NewDocument> {
|
||||
let mut nd = match str_param(p, "preset") {
|
||||
Some(name) => find_preset(s, name).ok_or_else(|| bad("file.new", format!("unknown preset `{name}`")))?,
|
||||
None => NewDocument::default(),
|
||||
};
|
||||
apply_settings(&mut nd, p);
|
||||
check_size("file.new", &nd)?;
|
||||
Ok(nd)
|
||||
}
|
||||
|
||||
fn file_new(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut nd = new_document_settings(s, p)?;
|
||||
s.untitled += 1;
|
||||
nd.title = str_param(p, "title").map(str::to_string).unwrap_or_else(|| format!("Untitled-{}", s.untitled));
|
||||
let d = Document::new(&nd);
|
||||
@@ -159,6 +165,64 @@ fn file_new(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({"index": i}))
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// New Document's Preview: a document made as `file.new` would make it with `p`, shown while
|
||||
/// the New Document window is open. It replaces the previous preview document in its tab and
|
||||
/// takes no Untitled number, no undo step and no recovery entry. Settings `file.new` refuses
|
||||
/// leave the previous preview as it was. Returns the preview document's uid.
|
||||
pub fn preview_new_document(&mut self, p: &Value) -> Result<u64> {
|
||||
let mut nd = new_document_settings(self, p)?;
|
||||
nd.title = str_param(p, "title").map(str::to_string).unwrap_or_else(|| self.next_untitled_title());
|
||||
let st = DocState::new(Document::new(&nd), None);
|
||||
let uid = st.uid;
|
||||
let shown = self.new_document_preview.and_then(|(old, _)| self.docs.iter().position(|d| d.uid == old));
|
||||
let previous = match self.new_document_preview {
|
||||
Some((_, previous)) => previous,
|
||||
None => self.active().map(|d| d.uid),
|
||||
};
|
||||
match shown.and_then(|i| self.docs.get_mut(i).map(|d| (i, d))) {
|
||||
Some((i, d)) => {
|
||||
let old = std::mem::replace(d, st);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(dir) = &self.recovery_dir {
|
||||
crate::recovery::discard(dir, old.uid);
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let _ = old;
|
||||
self.active = Some(i);
|
||||
}
|
||||
None => {
|
||||
// Pushed without `add_document`: the document that was active stays as it is.
|
||||
self.docs.push(st);
|
||||
self.active = Some(self.docs.len() - 1);
|
||||
}
|
||||
}
|
||||
self.new_document_preview = Some((uid, previous));
|
||||
Ok(uid)
|
||||
}
|
||||
|
||||
/// The uid of New Document's preview document, while one is shown.
|
||||
pub fn new_document_preview(&self) -> Option<u64> {
|
||||
self.new_document_preview.map(|(uid, _)| uid)
|
||||
}
|
||||
|
||||
/// Remove New Document's preview document and make the document that was active before it
|
||||
/// active again. Returns the preview's uid.
|
||||
pub fn end_new_document_preview(&mut self) -> Option<u64> {
|
||||
let (uid, previous) = self.new_document_preview.take()?;
|
||||
if let Some(i) = self.docs.iter().position(|d| d.uid == uid) {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(dir) = &self.recovery_dir {
|
||||
crate::recovery::discard(dir, uid);
|
||||
}
|
||||
self.docs.remove(i);
|
||||
}
|
||||
let back = previous.and_then(|p| self.docs.iter().position(|d| d.uid == p));
|
||||
self.active = back.or_else(|| self.docs.len().checked_sub(1));
|
||||
Some(uid)
|
||||
}
|
||||
}
|
||||
|
||||
fn save_preset(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let name = str_param(p, "name").map(str::trim).filter(|n| !n.is_empty()).ok_or_else(|| bad("file.savePreset", "missing `name`"))?.to_string();
|
||||
if name.chars().count() > MAX_PRESET_NAME {
|
||||
@@ -802,6 +866,31 @@ mod new_document_tests {
|
||||
assert_eq!(d.settings.bleed, [0.0; 4]);
|
||||
assert_eq!(d.settings.slug, [0.0, 1296.0, 0.0, 0.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_new_document_preview_replaces_itself_and_leaves_the_session_as_it_was() {
|
||||
let dir = std::env::temp_dir().join(format!("dc-new-doc-preview-{}", std::process::id()));
|
||||
let mut s = Session::new();
|
||||
s.recovery_dir = Some(dir.clone());
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let first = s.doc().unwrap().uid;
|
||||
let a = s.preview_new_document(&json!({"width": 300})).unwrap();
|
||||
assert_eq!((s.doc().unwrap().uid, s.doc().unwrap().doc.title.as_str()), (a, "Untitled-2"));
|
||||
let b = s.preview_new_document(&json!({"width": 400, "columns": 3})).unwrap();
|
||||
assert_eq!(s.documents().len(), 2, "the preview replaces itself");
|
||||
assert!(s.preview_new_document(&json!({"width": 0})).is_err());
|
||||
let d = s.doc().unwrap();
|
||||
assert_eq!((d.uid, d.doc.settings.page_width, d.doc.page(0).unwrap().columns.count), (b, 400.0, 3), "a refused size keeps it");
|
||||
// Even edited, it never reaches the recovery folder.
|
||||
s.execute("frame.create", &json!({"rect": [10, 10, 100, 100]})).unwrap();
|
||||
assert_eq!(crate::recovery::save(&s, &dir).unwrap(), 0);
|
||||
assert_eq!(s.end_new_document_preview(), Some(b));
|
||||
let d = s.doc().unwrap();
|
||||
assert_eq!((s.documents().len(), d.uid, d.is_dirty(), d.history.undo.len()), (1, first, false, 0));
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.title, "Untitled-2", "the preview took no Untitled number");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -278,6 +278,9 @@ pub struct Session {
|
||||
pub book: Option<cmd::book::Book>,
|
||||
/// Content Collector conveyor: collected objects as snippets (name, bytes).
|
||||
pub conveyor: Vec<(String, Vec<u8>)>,
|
||||
/// New Document's Preview: the preview document's uid and the uid of the document that was
|
||||
/// active before it (see [`Session::preview_new_document`]).
|
||||
pub(crate) new_document_preview: Option<(u64, Option<u64>)>,
|
||||
}
|
||||
|
||||
impl Default for Session {
|
||||
@@ -312,6 +315,7 @@ impl Session {
|
||||
conveyor: Vec::new(),
|
||||
book: None,
|
||||
untitled: 0,
|
||||
new_document_preview: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -350,7 +350,8 @@ pub fn save(s: &Session, dir: &Path) -> Result<usize> {
|
||||
let _ = created;
|
||||
let mut n = 0;
|
||||
for d in &s.docs {
|
||||
if !d.is_dirty() {
|
||||
// New Document's preview is not a document until Create makes it one.
|
||||
if !d.is_dirty() || s.new_document_preview() == Some(d.uid) {
|
||||
discard(dir, d.uid);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -302,6 +302,7 @@ pub fn handle(app: &mut DesignApp, ctx: &egui::Context, req: &ControlRequest) ->
|
||||
}
|
||||
"ui.dialog.confirm" => wrap(crate::dialogs::confirm(app)),
|
||||
"ui.dialog.cancel" => {
|
||||
crate::new_document::end_preview(app);
|
||||
app.cancel_pdf_import();
|
||||
app.ui.dialog = None;
|
||||
ok(Value::Null)
|
||||
|
||||
@@ -1657,6 +1657,10 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
if cancel || (!popup_open && ui.input(|i| i.key_pressed(egui::Key::Escape))) {
|
||||
result = Some(false);
|
||||
}
|
||||
if d.id == "newDocument" {
|
||||
ui.add_space(8.0);
|
||||
check(ui, &mut d, "preview", crate::i18n::tr(&app.ui.language, "Preview"));
|
||||
}
|
||||
if d.id == "closeDocument" && ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Don't Save"))).clicked() {
|
||||
d.fields.insert("discard".into(), json!(true));
|
||||
result = Some(true);
|
||||
@@ -1664,6 +1668,9 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
});
|
||||
});
|
||||
});
|
||||
if d.id == "newDocument" && result.is_none() {
|
||||
crate::new_document::preview(app, &mut d);
|
||||
}
|
||||
app.ui.dialog = Some(d);
|
||||
match result {
|
||||
Some(true) => {
|
||||
@@ -1671,6 +1678,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
}
|
||||
Some(false) => {
|
||||
app.ui.dialog = None;
|
||||
crate::new_document::end_preview(app);
|
||||
app.cancel_pdf_import();
|
||||
}
|
||||
None => {}
|
||||
@@ -1685,6 +1693,8 @@ pub fn confirm(app: &mut DesignApp) -> Result<Value, String> {
|
||||
}
|
||||
match d.id.as_str() {
|
||||
"newDocument" => {
|
||||
// The preview goes; Create makes the document afresh, with no undo history.
|
||||
crate::new_document::end_preview(app);
|
||||
let p = crate::new_document::params(&d);
|
||||
let r = app.run("file.new", p.clone());
|
||||
if r.is_ok() {
|
||||
|
||||
@@ -246,6 +246,9 @@ pub struct UiState {
|
||||
pub custom_workspaces: Vec<SavedWorkspace>,
|
||||
/// File › New › Document › Recent: the settings of the last documents made, newest first.
|
||||
pub recent_new_documents: Vec<designcraft_doc::build::NewDocument>,
|
||||
/// File › New › Document › Preview, kept for the session.
|
||||
#[serde(skip)]
|
||||
pub new_document_preview: bool,
|
||||
/// Transform reference point (0..8, row-major; 0 = top-left).
|
||||
pub ref_point: u8,
|
||||
/// Align To target (`selection`, `keyObject`, `margins`, `page`, `spread`).
|
||||
@@ -344,6 +347,7 @@ impl Default for UiState {
|
||||
workspace: "Essentials".into(),
|
||||
custom_workspaces: Vec::new(),
|
||||
recent_new_documents: Vec::new(),
|
||||
new_document_preview: false,
|
||||
ref_point: 0,
|
||||
align_to: "selection".into(),
|
||||
text_style_tab: 0,
|
||||
@@ -803,6 +807,7 @@ impl DesignApp {
|
||||
}
|
||||
self.last_time = now;
|
||||
self.drain_control(ctx);
|
||||
new_document::end_stale_preview(self);
|
||||
if self.fonts_ready {
|
||||
self.drain_inbox();
|
||||
}
|
||||
|
||||
@@ -139,6 +139,69 @@ pub(crate) fn remember(app: &mut DesignApp, p: &Value) {
|
||||
recent.truncate(MAX_RECENT);
|
||||
}
|
||||
|
||||
/// The `file.new` params of the document Preview shows (set while it is shown).
|
||||
const PREVIEWED: &str = "_previewed";
|
||||
|
||||
/// `file.new` params for Preview, or `None` while a field doesn't hold a value yet (empty, half
|
||||
/// typed, negative): the preview then stays as it was.
|
||||
fn preview_params(d: &Dialog) -> Option<Value> {
|
||||
let measures = measure_keys().into_iter().all(|k| !d.fields.contains_key(&k) || measure(d, &k).is_some_and(|v| v >= 0.0));
|
||||
let counts =
|
||||
["pages", "startPage", "columns"].into_iter().all(|k| !d.fields.contains_key(k) || d.n(k).is_some_and(|v| v.is_finite() && v >= 1.0));
|
||||
(measures && counts).then(|| params(d))
|
||||
}
|
||||
|
||||
/// Preview on: a document with the dialog's settings shows in the workspace, made again when a
|
||||
/// setting changes. Settings `file.new` refuses (a zero width) keep the last one. Off: it goes.
|
||||
pub(crate) fn preview(app: &mut DesignApp, d: &mut Dialog) {
|
||||
let on = match d.fields.get("preview").and_then(Value::as_bool) {
|
||||
Some(on) => on,
|
||||
None => {
|
||||
d.fields.insert("preview".into(), json!(app.ui.new_document_preview));
|
||||
app.ui.new_document_preview
|
||||
}
|
||||
};
|
||||
app.ui.new_document_preview = on;
|
||||
if !on {
|
||||
end_preview(app);
|
||||
d.fields.remove(PREVIEWED);
|
||||
return;
|
||||
}
|
||||
let Some(p) = preview_params(d) else { return };
|
||||
let shown = app.session.new_document_preview();
|
||||
if shown.is_some() && d.fields.get(PREVIEWED) == Some(&p) {
|
||||
return;
|
||||
}
|
||||
if app.session.preview_new_document(&p).is_ok() {
|
||||
forget_view(app, shown);
|
||||
}
|
||||
// Refused settings are not tried again until they change.
|
||||
d.fields.insert(PREVIEWED.into(), p);
|
||||
}
|
||||
|
||||
/// Remove the preview document, if one is shown, and show the document that was active before.
|
||||
pub(crate) fn end_preview(app: &mut DesignApp) {
|
||||
let uid = app.session.end_new_document_preview();
|
||||
forget_view(app, uid);
|
||||
}
|
||||
|
||||
/// End a preview whose New Document window has gone or no longer previews (closed over the
|
||||
/// control channel, or replaced by another dialog).
|
||||
pub(crate) fn end_stale_preview(app: &mut DesignApp) {
|
||||
let previewing = app.ui.dialog.as_ref().is_some_and(|d| d.id == "newDocument" && d.b("preview"));
|
||||
if !previewing && app.session.new_document_preview().is_some() {
|
||||
end_preview(app);
|
||||
}
|
||||
}
|
||||
|
||||
/// Drop the views (one per pane) of a preview document that has gone.
|
||||
fn forget_view(app: &mut DesignApp, uid: Option<u64>) {
|
||||
if let Some(uid) = uid {
|
||||
app.views.remove(&uid);
|
||||
app.views.remove(&(uid ^ (1 << 63)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Save the details as the document preset `name` (`file.savePreset`, which replaces one of that
|
||||
/// name) and show it on the Saved tab.
|
||||
fn save_preset(app: &mut DesignApp, d: &mut Dialog, name: &str) -> Result<(), String> {
|
||||
@@ -894,6 +957,78 @@ mod tests {
|
||||
assert!(h.state().app.ui.dialog.is_none());
|
||||
}
|
||||
|
||||
fn set(app: &mut DesignApp, ctx: &egui::Context, fields: &[(&str, Value)]) {
|
||||
let d = app.ui.dialog.as_mut().unwrap();
|
||||
for (k, v) in fields {
|
||||
d.fields.insert((*k).into(), v.clone());
|
||||
}
|
||||
frame(app, ctx, vec![]);
|
||||
}
|
||||
|
||||
fn shown(app: &DesignApp) -> (usize, f64, u32) {
|
||||
let doc = &app.session.active().unwrap().doc;
|
||||
(app.session.documents().len(), doc.settings.page_width, doc.page(0).unwrap().columns.count)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_shows_the_settings_live_and_close_restores_the_workspace() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({"title": "A"})).unwrap();
|
||||
app.run("file.new", json!({"title": "B"})).unwrap();
|
||||
app.session.set_active(0);
|
||||
let a = app.session.active().unwrap().clone();
|
||||
app.run("app.newDocumentDialog", json!({})).unwrap();
|
||||
let ctx = context();
|
||||
settle(&mut app, &ctx);
|
||||
assert_eq!(app.session.documents().len(), 2, "Preview starts off");
|
||||
set(&mut app, &ctx, &[("preview", json!(true))]);
|
||||
assert_eq!(shown(&app), (3, 612.0, 1), "a third tab shows the dialog's settings");
|
||||
set(&mut app, &ctx, &[("width", json!("300 pt")), ("columns", json!("3"))]);
|
||||
assert_eq!(shown(&app), (3, 300.0, 3), "and follows them");
|
||||
// Half-typed and refused values keep the last preview.
|
||||
for width in ["", "-5", "0"] {
|
||||
set(&mut app, &ctx, &[("width", json!(width))]);
|
||||
assert_eq!(shown(&app), (3, 300.0, 3), "{width:?}");
|
||||
}
|
||||
// Escape closes the window: the preview goes and A is shown again, untouched.
|
||||
frame(
|
||||
&mut app,
|
||||
&ctx,
|
||||
vec![egui::Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::NONE }],
|
||||
);
|
||||
assert!(app.ui.dialog.is_none());
|
||||
let back = app.session.active().unwrap();
|
||||
assert_eq!(app.session.documents().len(), 2);
|
||||
assert_eq!((back.uid, back.revision, back.history.undo.len(), back.is_dirty()), (a.uid, a.revision, 0, false));
|
||||
// Preview is remembered; turning it off removes the preview, as does closing the window
|
||||
// any other way.
|
||||
app.run("app.newDocumentDialog", json!({})).unwrap();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
assert_eq!(app.session.documents().len(), 3);
|
||||
set(&mut app, &ctx, &[("preview", json!(false))]);
|
||||
assert_eq!((app.session.documents().len(), app.session.active().unwrap().uid), (2, a.uid));
|
||||
set(&mut app, &ctx, &[("preview", json!(true))]);
|
||||
app.ui.dialog = None;
|
||||
end_stale_preview(&mut app);
|
||||
assert_eq!((app.session.documents().len(), app.session.active().unwrap().uid), (2, a.uid));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_keeps_the_previewed_document_with_the_final_settings() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({"title": "A"})).unwrap();
|
||||
app.run("app.newDocumentDialog", json!({})).unwrap();
|
||||
let ctx = context();
|
||||
set(&mut app, &ctx, &[("preview", json!(true)), ("width", json!("400 pt"))]);
|
||||
set(&mut app, &ctx, &[("pages", json!("4")), ("facingPages", json!(false))]);
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
let st = app.session.active().unwrap();
|
||||
assert_eq!((app.session.documents().len(), st.doc.settings.page_width, st.doc.page_count()), (2, 400.0, 4));
|
||||
assert!(!st.doc.settings.facing_pages);
|
||||
assert_eq!((st.doc.title.as_str(), st.history.undo.len(), st.is_dirty()), ("Untitled-2", 0, false));
|
||||
assert_eq!(app.session.new_document_preview(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn presets_are_saved_by_name_and_recent_documents_once() {
|
||||
let mut app = app();
|
||||
|
||||
@@ -420,7 +420,7 @@ It is evidence of breadth: each item exists. It is not a claim of InDesign-level
|
||||
- OpenType menu (Character panel): discretionary ligatures, fractions, ordinal, swash, titling, contextual alternates, slashed zero, the four figure styles and stylistic sets 1–20; `type.openType`; mapped to IDML's OTF attributes.
|
||||
- Add Anchor Point (=), Delete Anchor Point (-) and Convert Direction Point (⇧C) tools (click toggles smooth/corner, drag pulls out handles), with `path.addAnchor`, `path.deleteAnchor`, `path.convertAnchor`.
|
||||
- Gradients: the Gradient Swatch tool (G) drags a gradient's start and end across objects (Shift: 45°); the Gradient panel sets type, angle, reverse and edits stops on a ramp (drag, add, drag off to remove, location, colour). Edited gradients are unnamed swatches; `object.gradient`; the vector round-trips through IDML.
|
||||
- New Document (⌘N) as in InDesign: Recent, Saved, Print, Web and Mobile tabs of page cards (double-click creates) beside the preset's details: document name, size with arrows, units, orientation, pages, start page #, facing pages, primary text frame, columns, margins, bleed and slug per edge (linked or not); Save Document Preset (`file.savePreset`, `file.deletePreset`; saved presets are kept with the preferences, listed by `file.presets` and made by `file.new {preset: name}`); `file.new` takes `intent`, `units`, `startPage` and per-edge `bleed`/`slug`.
|
||||
- New Document (⌘N) as in InDesign: Recent, Saved, Print, Web and Mobile tabs of page cards (double-click creates) beside the preset's details: document name, size with arrows, units, orientation, pages, start page #, facing pages, primary text frame, columns, margins, bleed and slug per edge (linked or not); Save Document Preset (`file.savePreset`, `file.deletePreset`; saved presets are kept with the preferences, listed by `file.presets` and made by `file.new {preset: name}`); `file.new` takes `intent`, `units`, `startPage` and per-edge `bleed`/`slug`. Preview shows a document with the window's settings in the workspace while it is open; Create keeps it, Close removes it.
|
||||
- Document Setup (⌥⌘P): intent, number of pages, start page # (an even start begins with a left page), facing pages, size presets and orientation, bleed and slug per edge; `layout.documentSetup {}` reports the setup.
|
||||
- Stroke panel: cap, join, miter limit, alignment, type, Start/End arrowheads (12 kinds, drawn on screen and in PDF; the path is shortened under pointed heads) and gap colour under dashes and dots.
|
||||
- Preferences (⌘K: General, Type, Units & Increments, Grids, Guides & Pasteboard, Display Performance) backed by `prefs.set` (application) and `document.preferences` (units, increments, grids — undoable, saved with the document; right-click a ruler to pick its units, the horizontal and vertical rulers each have their own); scaling applies to content and scales stroke weights (Include Stroke Weight), and X/Y/W/H measure the stroke's outer edge (Dimensions Include Stroke Weight). Quick Apply (⌘Return) finds paragraph, character and object styles as well as commands.
|
||||
|
||||
Reference in New Issue
Block a user