mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-01 06:06:49 -06:00
Opening an InDesign document says to export IDML (#21)
.indd and .indt files (recognised by their published 16-byte signature) failed to open or place with a confusing format error. They now explain that the format is closed and how to export IDML from InDesign instead. File › Open offers .indd and .indt files (desktop and web), and dropping one on the window opens it like a .designcraft or .idml file. A file that can't be opened or placed from File › Open, Place, a drop, the command line or the control channel's app.open shows an alert with the reason, as well as the status line.
This commit is contained in:
@@ -4,7 +4,7 @@ use designcraft_engine::Session;
|
||||
use designcraft_ui_egui::{DesignApp, Inbox, Services};
|
||||
use wasm_bindgen::JsCast as _;
|
||||
|
||||
const DOC_EXTS: &[&str] = &["designcraft", "idml"];
|
||||
const DOC_EXTS: &[&str] = &["designcraft", "idml", "indd", "indt"];
|
||||
const IMAGE_EXTS: &[&str] = &[
|
||||
"png",
|
||||
"jpg",
|
||||
@@ -123,7 +123,7 @@ fn services(inbox: Inbox, ctx: egui::Context) -> Services {
|
||||
} else if purpose == "place" {
|
||||
rfd::AsyncFileDialog::new().add_filter("Graphics", IMAGE_EXTS)
|
||||
} else {
|
||||
rfd::AsyncFileDialog::new().add_filter("DesignCraft", DOC_EXTS)
|
||||
rfd::AsyncFileDialog::new().add_filter("DesignCraft, IDML or InDesign", DOC_EXTS)
|
||||
};
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let Some(file) = dialog.pick_file().await else {
|
||||
|
||||
@@ -143,9 +143,10 @@ fn services() -> Services {
|
||||
.add_filter("Text (Word, RTF, plain, Excel)", &["docx", "rtf", "txt", "md", "xlsx"])
|
||||
.add_filter("Video and sound", &["mp4", "m4v", "mov", "webm", "mp3", "m4a", "wav", "ogg"])
|
||||
} else {
|
||||
d.add_filter("DesignCraft or IDML", &["designcraft", "idml"])
|
||||
d.add_filter("DesignCraft, IDML or InDesign", &["designcraft", "idml", "indd", "indt"])
|
||||
.add_filter("DesignCraft", &["designcraft"])
|
||||
.add_filter("InDesign Markup (IDML)", &["idml"])
|
||||
.add_filter("InDesign Document or Template (INDD, INDT)", &["indd", "indt"])
|
||||
};
|
||||
d.pick_file().map(|p| p.to_string_lossy().to_string())
|
||||
})),
|
||||
@@ -232,7 +233,7 @@ fn main() -> eframe::Result {
|
||||
let _ = app.run("file.newSample", serde_json::json!({}));
|
||||
}
|
||||
for f in files {
|
||||
if let Err(e) = app.run("file.open", serde_json::json!({"path": f})) {
|
||||
if let Err(e) = app.open_file("file.open", serde_json::json!({"path": f})) {
|
||||
eprintln!("designcraft: {f}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,9 +107,23 @@ pub fn to_bytes(d: &Document) -> Vec<u8> {
|
||||
}
|
||||
|
||||
pub fn from_bytes(b: &[u8]) -> Result<Document> {
|
||||
if is_indd(b) {
|
||||
return Err(EngineError::Other(INDD_UNREADABLE.into()));
|
||||
}
|
||||
designcraft_format::load(b).map_err(|e| EngineError::Other(e.to_string()))
|
||||
}
|
||||
|
||||
/// InDesign documents and templates (.indd, .indt) begin with this 16-byte class id.
|
||||
const INDD_SIGNATURE: [u8; 16] = [0x06, 0x06, 0xED, 0xF5, 0xD8, 0x1D, 0x46, 0xE5, 0xBD, 0x31, 0xEF, 0xE7, 0xFE, 0x74, 0xB7, 0x1D];
|
||||
|
||||
const INDD_UNREADABLE: &str = "This is an InDesign document (.indd). Its format is closed and can't be read: in InDesign, \
|
||||
choose File › Export… › InDesign Markup (IDML), then open the .idml file here.";
|
||||
|
||||
/// An InDesign document or template, recognised by its signature.
|
||||
pub fn is_indd(b: &[u8]) -> bool {
|
||||
b.starts_with(&INDD_SIGNATURE)
|
||||
}
|
||||
|
||||
fn file_new(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut nd = match str_param(p, "preset") {
|
||||
Some(name) => NewDocument::from_preset(name).ok_or_else(|| bad("file.new", format!("unknown preset `{name}`")))?,
|
||||
@@ -217,6 +231,9 @@ fn file_place(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
s.ui_requests.push(crate::UiRequest::Pick { purpose: "place".into(), params: json!({}) });
|
||||
return ok();
|
||||
};
|
||||
if is_indd(&bytes) {
|
||||
return Err(EngineError::Other(INDD_UNREADABLE.into()));
|
||||
}
|
||||
// A page of another layout (IDML or DesignCraft): its objects, as one group.
|
||||
let lname = name.to_lowercase();
|
||||
if lname.ends_with(".idml") || lname.ends_with(".designcraft") {
|
||||
@@ -656,3 +673,38 @@ mod layout_place_tests {
|
||||
assert!(b.execute("file.place", &json!({"base64": b64, "name": "src.idml", "layoutPage": 2})).is_err(), "page 2 is empty");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod indd_tests {
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
|
||||
/// The start of an InDesign document: its signature, then the `DOCUMENT` class name.
|
||||
fn indd() -> Vec<u8> {
|
||||
let mut b = super::INDD_SIGNATURE.to_vec();
|
||||
b.extend_from_slice(b"DOCUMENT");
|
||||
b.resize(4096, 0);
|
||||
b
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indd_files_say_to_export_idml() {
|
||||
let b64 = super::base64_encode(&indd());
|
||||
let mut s = Session::new();
|
||||
let open = s.execute("file.openBytes", &json!({"base64": b64, "name": "Brochure"})).unwrap_err().to_string();
|
||||
assert!(open.contains("InDesign document") && open.contains("IDML"), "{open}");
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let place = s.execute("file.place", &json!({"base64": b64, "name": "Brochure.indd"})).unwrap_err().to_string();
|
||||
assert!(place.contains("InDesign document") && place.contains("IDML"), "{place}");
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let path = std::env::temp_dir().join(format!("designcraft-indd-{}.indd", std::process::id()));
|
||||
std::fs::write(&path, indd()).unwrap();
|
||||
let open = s.execute("file.open", &json!({"path": path.to_string_lossy()})).unwrap_err().to_string();
|
||||
let _ = std::fs::remove_file(&path);
|
||||
assert!(open.contains("InDesign document"), "{open}");
|
||||
}
|
||||
assert_eq!(s.documents().len(), 1, "nothing was opened");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,7 +333,7 @@ pub fn handle(app: &mut DesignApp, ctx: &egui::Context, req: &ControlRequest) ->
|
||||
None => ok(json!({"width": img.width, "height": img.height, "pngBase64": designcraft_engine::cmd::base64_encode(&png)})),
|
||||
}
|
||||
}
|
||||
"app.open" => wrap(app.run("file.open", json!({"path": s("path")}))),
|
||||
"app.open" => wrap(app.open_file("file.open", json!({"path": s("path")}))),
|
||||
"app.save" => wrap(app.run("file.save", json!({"path": s("path")}))),
|
||||
"app.export" => wrap(app.run("app.exportPng", json!({"path": s("path"), "page": p.get("page"), "scale": p.get("scale")}))),
|
||||
"app.quit" => {
|
||||
|
||||
@@ -797,7 +797,9 @@ fn check(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str) {
|
||||
pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
let Some(mut d) = app.ui.dialog.clone() else { return };
|
||||
let mut result: Option<bool> = None;
|
||||
let alert_title = d.s("title");
|
||||
let title = match d.id.as_str() {
|
||||
"alert" => crate::i18n::tr(&app.ui.language, &alert_title),
|
||||
"ruby" => crate::i18n::tr(&app.ui.language, "Ruby"),
|
||||
"newDocument" => crate::i18n::tr(&app.ui.language, "New Document"),
|
||||
"frameSize" => crate::i18n::tr(&app.ui.language, "Rectangle"),
|
||||
@@ -831,7 +833,11 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
};
|
||||
egui::Modal::new(egui::Id::new("dialog")).show(ctx, |ui| {
|
||||
ui.set_min_width(380.0);
|
||||
ui.set_max_width(if d.id == "newDocument" && crate::i18n::is_rtl(&app.ui.language) { 380.0 } else { 640.0 });
|
||||
ui.set_max_width(match d.id.as_str() {
|
||||
"alert" => 440.0,
|
||||
"newDocument" if crate::i18n::is_rtl(&app.ui.language) => 380.0,
|
||||
_ => 640.0,
|
||||
});
|
||||
if crate::i18n::is_rtl(&app.ui.language) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
@@ -1360,6 +1366,14 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
});
|
||||
}
|
||||
"documentSetup" => document_setup(app, ui, &mut d),
|
||||
"alert" => {
|
||||
let file = d.s("file");
|
||||
if !file.is_empty() {
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::rtl::isolate(&file)).font(semibold(13.0)));
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
crate::rtl::label(ui, d.s("message"));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
@@ -1375,7 +1389,9 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
{
|
||||
result = Some(true);
|
||||
}
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Cancel"))).clicked() || ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
// An alert only has OK.
|
||||
let cancel = d.id != "alert" && ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Cancel"))).clicked();
|
||||
if cancel || ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
result = Some(false);
|
||||
}
|
||||
});
|
||||
@@ -1492,7 +1508,7 @@ pub fn confirm(app: &mut DesignApp) -> Result<Value, String> {
|
||||
}
|
||||
"pdfImport" => {
|
||||
let crop = d.s("crop");
|
||||
app.run("file.place", json!({"path": d.s("path"), "pdfPage": d.n("page").unwrap_or(1.0).max(1.0) as u64, "pdfCrop": if crop.is_empty() { "crop".to_string() } else { crop }}))
|
||||
app.open_file("file.place", json!({"path": d.s("path"), "pdfPage": d.n("page").unwrap_or(1.0).max(1.0) as u64, "pdfCrop": if crop.is_empty() { "crop".to_string() } else { crop }}))
|
||||
}
|
||||
"print" => {
|
||||
let pages = if d.s("range") == "all" { Value::Null } else { json!(d.s("pages")) };
|
||||
@@ -1594,7 +1610,7 @@ pub fn confirm(app: &mut DesignApp) -> Result<Value, String> {
|
||||
app.run("document.preferences", doc)
|
||||
}
|
||||
"newWorkspace" => app.run("window.newWorkspace", json!({"name": d.s("name")})),
|
||||
"importOptions" => app.run(
|
||||
"importOptions" => app.open_file(
|
||||
"file.place",
|
||||
json!({"path": d.s("path"), "removeStyles": d.b("removeStyles"), "styleConflicts": d.s("styleConflicts"), "styleMap": d.fields.get("map").cloned().unwrap_or(json!({}))}),
|
||||
),
|
||||
@@ -1662,6 +1678,7 @@ pub fn confirm(app: &mut DesignApp) -> Result<Value, String> {
|
||||
}
|
||||
app.run("footnote.options", p)
|
||||
}
|
||||
"alert" => Ok(Value::Null),
|
||||
"findChange" if d.b("objectMode") => Ok(Value::Null),
|
||||
"findChange" => app.run("find.change", json!({"find": d.s("find"), "change": d.s("change"), "grep": d.b("grep"), "caseSensitive": d.b("caseSensitive"), "wholeWord": d.b("wholeWord"), "scope": d.s("scope")})),
|
||||
id if id.starts_with("cmd:") => {
|
||||
|
||||
@@ -219,6 +219,26 @@ const TABLE: &[(&str, [&str; 5])] = &[
|
||||
("Business Card", ["Business Card", "Business Card", "Business Card", "Business Card", "名片"]),
|
||||
("CMYK", ["CMYK", "CMYK", "CMYK", "CMYK", "CMYK"]),
|
||||
("Cancel", ["Cancel", "Cancel", "Cancel", "Cancel", "取消"]),
|
||||
(
|
||||
"Can't Open the File",
|
||||
[
|
||||
"Datei kann nicht geöffnet werden",
|
||||
"Impossible d'ouvrir le fichier",
|
||||
"No se puede abrir el archivo",
|
||||
"ファイルを開けません",
|
||||
"无法打开文件",
|
||||
],
|
||||
),
|
||||
(
|
||||
"Can't Place the File",
|
||||
[
|
||||
"Datei kann nicht platziert werden",
|
||||
"Impossible d'importer le fichier",
|
||||
"No se puede colocar el archivo",
|
||||
"ファイルを配置できません",
|
||||
"无法置入文件",
|
||||
],
|
||||
),
|
||||
("Center Content", ["Center Content", "Center Content", "Center Content", "Center Content", "内容居中"]),
|
||||
("Change All", ["Change All", "Change All", "Change All", "Change All", "全部更改"]),
|
||||
("Change to:", ["Change to:", "Change to:", "Change to:", "Change to:", "更改为:"]),
|
||||
|
||||
@@ -166,6 +166,8 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Dock panel", "إرساء اللوحة"),
|
||||
("OK", "موافق"),
|
||||
("Cancel", "إلغاء"),
|
||||
("Can't Open the File", "تعذّر فتح الملف"),
|
||||
("Can't Place the File", "تعذّر إدراج الملف"),
|
||||
("Apply", "تطبيق"),
|
||||
("Done", "تم"),
|
||||
("Reset", "إعادة ضبط"),
|
||||
|
||||
+109
-15
@@ -57,8 +57,8 @@ pub struct Services {
|
||||
/// Hand bytes to the user as a named file (browser download). When set, Save uses it
|
||||
/// instead of writing to a path.
|
||||
pub download: Option<DownloadFn>,
|
||||
/// Files delivered asynchronously, drained every frame: `.designcraft` → `file.openBytes`,
|
||||
/// anything else → `file.place`.
|
||||
/// Files delivered asynchronously, drained every frame: documents ([`opens_as_document`]) →
|
||||
/// `file.openBytes`, `.ase` → `swatch.load`, anything else → `file.place`.
|
||||
pub inbox: Option<Inbox>,
|
||||
}
|
||||
|
||||
@@ -534,10 +534,10 @@ impl DesignApp {
|
||||
self.ui.dialog = Some(dialogs::Dialog::new("pdfImport", json!({"path": path, "page": "1", "pages": pages})));
|
||||
Ok(Value::Null)
|
||||
} else {
|
||||
self.run(cmd, json!({"path": path}))
|
||||
self.open_file(cmd, json!({"path": path}))
|
||||
}
|
||||
}
|
||||
Some(path) => self.run(cmd, json!({"path": path})),
|
||||
Some(path) => self.open_file(cmd, json!({"path": path})),
|
||||
None => Ok(Value::Null),
|
||||
};
|
||||
}
|
||||
@@ -556,11 +556,11 @@ impl DesignApp {
|
||||
let lower = name.to_ascii_lowercase();
|
||||
let r = if lower.ends_with(".ase") {
|
||||
self.run("swatch.load", json!({"base64": b64}))
|
||||
} else if lower.ends_with(".designcraft") || lower.ends_with(".idml") {
|
||||
} else if opens_as_document(&name) {
|
||||
let title = name.rsplit_once('.').map_or(name.as_str(), |(stem, _)| stem);
|
||||
self.run("file.openBytes", json!({"name": title, "base64": b64}))
|
||||
self.open_file("file.openBytes", json!({"name": title, "base64": b64}))
|
||||
} else {
|
||||
self.run("file.place", json!({"name": name, "base64": b64}))
|
||||
self.open_file("file.place", json!({"name": name, "base64": b64}))
|
||||
};
|
||||
if let Err(e) = r {
|
||||
self.status(format!("{name}: {e}"));
|
||||
@@ -631,18 +631,34 @@ impl DesignApp {
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
for f in ctx.input(|i| i.raw.dropped_files.clone()) {
|
||||
{
|
||||
let p = f.path().to_string_lossy().to_string();
|
||||
if p.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let lp = p.to_ascii_lowercase();
|
||||
let cmd = if lp.ends_with(".designcraft") || lp.ends_with(".idml") { "file.open" } else { "file.place" };
|
||||
let _ = self.run(cmd, json!({"path": p}));
|
||||
let p = f.path().to_string_lossy().to_string();
|
||||
if !p.is_empty() {
|
||||
let _ = self.open_dropped(&p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A file dropped on the window: documents open, anything else is placed.
|
||||
pub fn open_dropped(&mut self, path: &str) -> Result<Value, String> {
|
||||
let cmd = if opens_as_document(path) { "file.open" } else { "file.place" };
|
||||
self.open_file(cmd, json!({"path": path}))
|
||||
}
|
||||
|
||||
/// Open (`file.open`, `file.openBytes`) or place (`file.place`) a file the user chose. A file
|
||||
/// that can't be opened or placed gets an alert with the reason, as well as the status line.
|
||||
pub fn open_file(&mut self, cmd: &str, params: Value) -> Result<Value, String> {
|
||||
let file = match params.get("path").and_then(Value::as_str) {
|
||||
Some(path) => std::path::Path::new(path).file_name().map_or_else(|| path.to_string(), |n| n.to_string_lossy().to_string()),
|
||||
None => params.get("name").and_then(Value::as_str).unwrap_or_default().to_string(),
|
||||
};
|
||||
let r = self.run(cmd, params);
|
||||
if let Err(e) = &r {
|
||||
let title = if cmd == "file.place" { "Can't Place the File" } else { "Can't Open the File" };
|
||||
self.ui.dialog = Some(dialogs::Dialog::new("alert", json!({"title": title, "file": file, "message": e})));
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
/// Inject synthetic events (one pointer event per frame).
|
||||
pub fn raw_input_hook(&mut self, raw: &mut egui::RawInput) {
|
||||
if self.synthetic.is_empty() {
|
||||
@@ -851,6 +867,13 @@ impl DesignApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// Files that open as documents (when dropped or picked) rather than being placed: DesignCraft,
|
||||
/// IDML, and InDesign documents and templates (which explain how to export IDML).
|
||||
pub fn opens_as_document(name: &str) -> bool {
|
||||
let n = name.to_ascii_lowercase();
|
||||
[".designcraft", ".idml", ".indd", ".indt"].iter().any(|ext| n.ends_with(ext))
|
||||
}
|
||||
|
||||
pub fn now_ms() -> f64 {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
@@ -878,4 +901,75 @@ mod tests {
|
||||
assert!(!ui.snap_view().snap_to_guides);
|
||||
assert_eq!(ui.snap_view().zone_px, 0.0);
|
||||
}
|
||||
|
||||
/// A temporary file that starts like an InDesign document (its 16-byte signature).
|
||||
fn indd_file(tag: &str) -> std::path::PathBuf {
|
||||
let path = std::env::temp_dir().join(format!("designcraft-ui-{tag}-{}.indd", std::process::id()));
|
||||
let mut b = vec![0x06, 0x06, 0xED, 0xF5, 0xD8, 0x1D, 0x46, 0xE5, 0xBD, 0x31, 0xEF, 0xE7, 0xFE, 0x74, 0xB7, 0x1D];
|
||||
b.resize(4096, 0);
|
||||
std::fs::write(&path, b).unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
/// The open alert dialog: (title, message).
|
||||
fn alert(app: &DesignApp) -> (String, String) {
|
||||
let d = app.ui.dialog.as_ref().expect("an alert is open");
|
||||
assert_eq!(d.id, "alert");
|
||||
(d.fields["title"].as_str().unwrap().to_string(), d.fields["message"].as_str().unwrap().to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dropped_documents_open_and_the_rest_is_placed() {
|
||||
for name in ["a.designcraft", "B.IDML", "c.indd", "D.INDT"] {
|
||||
assert!(opens_as_document(name), "{name}");
|
||||
}
|
||||
for name in ["a.png", "b.pdf", "indd.txt", "c.indd.zip"] {
|
||||
assert!(!opens_as_document(name), "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dropping_an_indesign_document_opens_it_and_alerts() {
|
||||
let path = indd_file("drop");
|
||||
let mut app = DesignApp::new(Session::new(), Services::default());
|
||||
let r = app.open_dropped(&path.to_string_lossy());
|
||||
let _ = std::fs::remove_file(&path);
|
||||
assert!(r.is_err());
|
||||
let (title, message) = alert(&app);
|
||||
assert_eq!(title, "Can't Open the File", "an .indd opens, it isn't placed");
|
||||
assert!(message.contains("InDesign document") && message.contains("IDML"), "{message}");
|
||||
assert!(app.ui.status.contains("InDesign document"), "the status line says it too");
|
||||
assert!(app.session.documents().is_empty());
|
||||
dialogs::confirm(&mut app).unwrap();
|
||||
assert!(app.ui.dialog.is_none(), "OK closes the alert");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_open_failures_show_an_alert() {
|
||||
let path = indd_file("open");
|
||||
let picked = path.to_string_lossy().to_string();
|
||||
let services = Services { pick_open: Some(Box::new(move |_| Some(picked.clone()))), ..Default::default() };
|
||||
let mut app = DesignApp::new(Session::new(), services);
|
||||
let r = app.run("app.openDialog", json!({}));
|
||||
assert!(r.is_err());
|
||||
let (title, message) = alert(&app);
|
||||
assert_eq!(title, "Can't Open the File");
|
||||
assert!(message.contains("InDesign document"), "{message}");
|
||||
// Placing one says so too.
|
||||
app.ui.dialog = None;
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
assert!(app.run("app.placeDialog", json!({})).is_err());
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let (title, message) = alert(&app);
|
||||
assert_eq!(title, "Can't Place the File");
|
||||
assert!(message.contains("InDesign document"), "{message}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn other_command_errors_stay_in_the_status_line() {
|
||||
let mut app = DesignApp::new(Session::new(), Services::default());
|
||||
assert!(app.run("file.open", json!({"path": "/nonexistent/x.designcraft"})).is_err());
|
||||
assert!(app.ui.dialog.is_none(), "a scripted file.open doesn't raise an alert");
|
||||
assert!(!app.ui.status.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -698,7 +698,7 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
|
||||
let Some(path) = app.services.pick_open.as_mut().and_then(|f| f("place")) else { return Some(Ok(Value::Null)) };
|
||||
let l = path.to_lowercase();
|
||||
if !(l.ends_with(".docx") || l.ends_with(".rtf")) {
|
||||
return Some(app.run("file.place", json!({"path": path})));
|
||||
return Some(app.open_file("file.place", json!({"path": path})));
|
||||
}
|
||||
let info = match app.run("place.styles", json!({"path": path})) {
|
||||
Ok(v) => v,
|
||||
|
||||
Reference in New Issue
Block a user