mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
W: Scripts panel and script.run (one undo step)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8eca6305f8
commit
0d02222dab
@@ -116,6 +116,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
||||
- EPUB: cover image from the first page (`file.exportEpub {cover}`), and Fixed Layout export (`file.exportFixedEpub`, File ▸ Export EPUB (Fixed Layout)…): pre-paginated pages as 144 ppi images with their text kept for search and read-aloud.
|
||||
- Interactive PDF options (`file.exportPdf {fullScreen, bookmarksPanel, pageLayout, view, advanceSeconds}`, File ▸ Export PDF (Interactive)…): page mode, layout, opening view and page timing in the catalog and pages.
|
||||
- Object Export Options (`object.exportOptions`): tagged-PDF artifact, EPUB/HTML rasterize (the object as an image), alignment and page break before, alt text.
|
||||
- Scripts panel (Window ▸ Utilities ▸ Scripts, `script.run`): saved command scripts (with `$N.path` references to earlier results) run as one undo step; edit, add, load and delete them in the panel. Scripts use DesignCraft's command language, not ExtendScript.
|
||||
- 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.
|
||||
|
||||
@@ -93,6 +93,8 @@ fn services() -> Services {
|
||||
let d = rfd::FileDialog::new();
|
||||
let d = if purpose == "swatches" {
|
||||
d.add_filter("Swatch Exchange (ASE)", &["ase"])
|
||||
} else if purpose == "script" {
|
||||
d.add_filter("Script", &["dcscript", "txt", "json"])
|
||||
} else if purpose == "icc" {
|
||||
d.add_filter("ICC profile", &["icc", "icm"])
|
||||
} else if purpose == "book" {
|
||||
|
||||
@@ -42,6 +42,7 @@ pub mod preflight;
|
||||
mod prefs;
|
||||
mod print;
|
||||
mod qr;
|
||||
mod scripts;
|
||||
mod select;
|
||||
pub mod spelling;
|
||||
mod states;
|
||||
@@ -171,6 +172,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
|
||||
v.extend(liquid::specs());
|
||||
v.extend(xml::specs());
|
||||
v.extend(strokes::specs());
|
||||
v.extend(scripts::specs());
|
||||
v.extend(states::specs());
|
||||
v.extend(transitions::specs());
|
||||
v.extend(path_type::specs());
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
//! Window › Utilities › Scripts: run a command script (see [`crate::script`]) as one undo step.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, always, bad, cmd, str_param};
|
||||
use crate::Result;
|
||||
|
||||
fn run(s: &mut crate::Session, p: &Value) -> Result<Value> {
|
||||
let text = str_param(p, "text").ok_or_else(|| bad("script.run", "`text` required"))?;
|
||||
let steps = crate::script::parse(text).map_err(|e| bad("script.run", e))?;
|
||||
// The steps' own undo entries collapse into this command's single one.
|
||||
let uid = s.active().map(|d| d.uid);
|
||||
let before = s.active().map_or(0, |d| d.history.undo.len());
|
||||
let report = s.run_script(&steps);
|
||||
if let Some(st) = s.active_mut()
|
||||
&& Some(st.uid) == uid
|
||||
&& st.history.undo.len() > before
|
||||
{
|
||||
st.history.undo.truncate(before);
|
||||
}
|
||||
let out = json!({"results": report.results, "steps": steps.len()});
|
||||
match report.failed {
|
||||
Some((i, c, e)) => Err(bad("script.run", format!("step {i} ({c}): {e}"))),
|
||||
None => Ok(out),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![cmd!(
|
||||
"script.run",
|
||||
"Run Script",
|
||||
[],
|
||||
None,
|
||||
"{text} — command script lines (`command.id {json}`, JSON lines or a JSON array; `$N.path` refers to earlier results) → {results, steps}",
|
||||
always,
|
||||
run
|
||||
)]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
|
||||
#[test]
|
||||
fn scripts_run_as_one_undo_step() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let text = "frame.create {\"rect\": [72, 72, 200, 200]}\nobject.fill {\"swatch\": \"[Black]\", \"ids\": [\"$0.id\"]}\nframe.create {\"rect\": [300, 72, 400, 200]}";
|
||||
let r = s.execute("script.run", &json!({"text": text})).unwrap();
|
||||
assert_eq!(r["steps"], 3);
|
||||
let count = |s: &Session| s.doc().unwrap().doc.spreads[0].items.len();
|
||||
assert_eq!(count(&s), 2);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(count(&s), 0, "one undo removes the whole script's work");
|
||||
assert!(s.execute("script.run", &json!({"text": "nope.command"})).is_err());
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ pub const ICON_PANELS: &[(&str, &str, &str)] = &[
|
||||
("media", "Media", "panel-articles"),
|
||||
("transitions", "Page Transitions", "panel-pages"),
|
||||
("trackChanges", "Track Changes", "panel-articles"),
|
||||
("scripts", "Scripts", "panel-library"),
|
||||
];
|
||||
|
||||
pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
@@ -193,6 +194,7 @@ pub fn panel_body(app: &mut DesignApp, ui: &mut egui::Ui, id: &str) {
|
||||
"media" => panels::interactive::media(app, ui),
|
||||
"transitions" => panels::interactive::transitions(app, ui),
|
||||
"trackChanges" => panels::interactive::track_changes(app, ui),
|
||||
"scripts" => panels::library::scripts(app, ui),
|
||||
"table" => panels::table::show(app, ui),
|
||||
_ => panels::properties::info_panel(app, ui),
|
||||
}
|
||||
|
||||
@@ -103,6 +103,8 @@ pub struct UiState {
|
||||
/// Edit › Menus: hidden menu items (`menu/label`), and Show Full Menus.
|
||||
pub hidden_menu_items: Vec<String>,
|
||||
pub show_full_menus: bool,
|
||||
/// Scripts panel: saved scripts (name, text).
|
||||
pub scripts: Vec<(String, String)>,
|
||||
/// View › Structure › Show Tag Markers.
|
||||
pub tag_markers: bool,
|
||||
/// View › Flattener Preview: objects that involve transparency highlighted in red.
|
||||
@@ -191,6 +193,10 @@ impl Default for UiState {
|
||||
hidden_menu_items: Vec::new(),
|
||||
show_full_menus: false,
|
||||
story_editor_size: 14.0,
|
||||
scripts: vec![
|
||||
("Number the pages".into(), "# A page-number frame at the bottom of the current page.\nframe.create {\"rect\": [288, 740, 324, 760], \"content\": \"text\", \"text\": \"\"}\ntext.select {\"story\": \"$0.story\", \"anchor\": 0, \"focus\": 0}\ntext.insert {\"text\": \"\\ue000\", \"raw\": true}".into()),
|
||||
("Two-column grid".into(), "# Four text frames in a 2 × 2 grid.\nframe.grid {\"rect\": [36, 36, 576, 756], \"cols\": 2, \"rows\": 2, \"gutter\": 12}".into()),
|
||||
],
|
||||
tag_markers: false,
|
||||
flattener_preview: false,
|
||||
hidden_panels: 0,
|
||||
|
||||
@@ -619,6 +619,7 @@ pub const MENUS: &[(&str, &[&str])] = &[
|
||||
"ui:window.panel|Media|{\"panel\": \"media\"}",
|
||||
"ui:window.panel|Page Transitions|{\"panel\": \"transitions\"}",
|
||||
"ui:window.panel|Track Changes|{\"panel\": \"trackChanges\"}",
|
||||
"ui:window.panel|Scripts|{\"panel\": \"scripts\"}",
|
||||
"ui:window.panel|Liquid Layout|{\"panel\": \"liquid\"}",
|
||||
"ui:window.panel|Tags|{\"panel\": \"tags\"}",
|
||||
"ui:window.panel|Book|{\"panel\": \"book\"}",
|
||||
@@ -2105,6 +2106,16 @@ mod tests {
|
||||
assert!((app.view().unwrap().zoom - z0).abs() < 1e-9, "back at the zoom it started from");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sample_scripts_run() {
|
||||
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.session.execute("file.new", &json!({})).unwrap();
|
||||
for (name, text) in app.ui.scripts.clone() {
|
||||
let r = app.session.execute("script.run", &json!({"text": text}));
|
||||
assert!(r.is_ok(), "{name}: {r:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panels_float_and_dock() {
|
||||
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
|
||||
@@ -132,3 +132,63 @@ pub fn book(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Scripts panel: saved command scripts — run (one undo step), edit, add, delete, load.
|
||||
pub fn scripts(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let sel_id = egui::Id::new("scripts_sel");
|
||||
let mut sel: usize = ui.data(|d| d.get_temp(sel_id)).unwrap_or(0);
|
||||
if app.ui.scripts.is_empty() {
|
||||
ui.label(egui::RichText::new("No scripts. Add one below.").size(11.0).color(t.text_dim));
|
||||
}
|
||||
let mut run: Option<usize> = None;
|
||||
for (i, (name, _)) in app.ui.scripts.iter().enumerate() {
|
||||
let r = ui.selectable_label(i == sel, name);
|
||||
if r.clicked() {
|
||||
sel = i;
|
||||
}
|
||||
if r.double_clicked() {
|
||||
run = Some(i);
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
if ui.add_enabled(!app.ui.scripts.is_empty(), egui::Button::new("Run")).clicked() {
|
||||
run = Some(sel);
|
||||
}
|
||||
if ui.button("New").clicked() {
|
||||
app.ui.scripts.push((format!("Script {}", app.ui.scripts.len() + 1), "# command.id {json} per line\n".into()));
|
||||
sel = app.ui.scripts.len() - 1;
|
||||
}
|
||||
if ui.button("Load…").clicked()
|
||||
&& let Some(path) = app.services.pick_open.as_mut().and_then(|f| f("script"))
|
||||
{
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(text) => {
|
||||
let name = std::path::Path::new(&path).file_stem().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| "Script".into());
|
||||
app.ui.scripts.push((name, text));
|
||||
sel = app.ui.scripts.len() - 1;
|
||||
}
|
||||
Err(e) => app.status(format!("Scripts: {e}")),
|
||||
}
|
||||
}
|
||||
if ui.add_enabled(!app.ui.scripts.is_empty(), egui::Button::new("Delete")).clicked() && sel < app.ui.scripts.len() {
|
||||
app.ui.scripts.remove(sel);
|
||||
sel = sel.saturating_sub(1);
|
||||
}
|
||||
});
|
||||
if let Some((name, text)) = app.ui.scripts.get_mut(sel) {
|
||||
ui.add(egui::TextEdit::singleline(name).desired_width(f32::INFINITY));
|
||||
ui.add(egui::TextEdit::multiline(text).code_editor().desired_rows(8).desired_width(f32::INFINITY));
|
||||
}
|
||||
if let Some(i) = run
|
||||
&& let Some((name, text)) = app.ui.scripts.get(i).cloned()
|
||||
{
|
||||
match app.run("script.run", json!({"text": text})) {
|
||||
Ok(r) => app.status(format!("{name}: {} step(s) done", r["steps"])),
|
||||
Err(e) => app.status(format!("{name}: {e}")),
|
||||
}
|
||||
}
|
||||
ui.data_mut(|d| d.insert_temp(sel_id, sel));
|
||||
}
|
||||
|
||||
+3
-3
@@ -11,7 +11,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
||||
|---|---|---|---|---|---|
|
||||
| P0 | 102 | 100 | 2 | 0 | 99% |
|
||||
| P1 | 97 | 94 | 3 | 0 | 98% |
|
||||
| P2 | 76 | 63 | 13 | 0 | 91% |
|
||||
| P2 | 76 | 64 | 12 | 0 | 92% |
|
||||
|
||||
| Area | Parity |
|
||||
|---|---|
|
||||
@@ -30,7 +30,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
||||
| Long documents | 100% |
|
||||
| Interactivity & digital | 100% |
|
||||
| Output & production | 92% |
|
||||
| XML & automation | 89% |
|
||||
| XML & automation | 94% |
|
||||
| View & navigation | 98% |
|
||||
| Undo, history, saving | 100% |
|
||||
| Accessibility | 100% |
|
||||
@@ -291,7 +291,7 @@ Open P0 items: Vector/page formats (PDF/AI/SVG/EPS place), Export to PDF (print)
|
||||
| XML & automation | Tags panel, Structure | P2 | D |
|
||||
| XML & automation | Import/Export XML | P2 | P |
|
||||
| XML & automation | Data Merge | P2 | D |
|
||||
| XML & automation | Scripts panel | P2 | P |
|
||||
| XML & automation | Scripts panel | P2 | D |
|
||||
| XML & automation | Script Label | P2 | D |
|
||||
| XML & automation | Content Collector/Placer | P2 | D |
|
||||
| XML & automation | Snippets | P1 | D |
|
||||
|
||||
Reference in New Issue
Block a user