diff --git a/crates/ui-egui/src/dialogs/psd_import.rs b/crates/ui-egui/src/dialogs/psd_import.rs index 108495e2cc..75fd0e0f11 100644 --- a/crates/ui-egui/src/dialogs/psd_import.rs +++ b/crates/ui-egui/src/dialogs/psd_import.rs @@ -96,6 +96,28 @@ pub fn queue_options(app: &VectorcraftApp, q: &mut Value) { q["importSlices"] = json!(slices); } +/// `file.place` params `p` placed without asking (Show Import Options off) that embed a PSD or PSB +/// file and give no `layers` take the PSD Import Options chosen last; `hiddenLayers` and +/// `importSlices` that the params give are kept. +pub fn last_options(app: &VectorcraftApp, p: &mut Value) { + let embedded = p.get("path").is_none() || p.get("link").and_then(Value::as_bool) == Some(false); + let name = p.get("path").or_else(|| p.get("name")).and_then(Value::as_str).unwrap_or_default(); + if embedded && p.get("layers").is_none() && matches!(fileio::extension(name).as_str(), "psd" | "pdd" | "psb") { + let (layers, hidden, slices) = last_choices(app); + p["layers"] = json!(layers); + if layers && hidden && p.get("hiddenLayers").is_none() { + p["hiddenLayers"] = json!(true); + } + if p.get("importSlices").is_none() { + p["importSlices"] = json!(slices); + } + if p.get("path").is_none() { + // A file from bytes has no file to link to; it may carry the Place dialog's Link. + p["link"] = json!(false); + } + } +} + /// OK: open or place the file with the choices. fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { let file = app.ui.dialog_file.clone().ok_or("the file is no longer loaded")?; @@ -451,6 +473,102 @@ mod tests { assert_eq!(q["importSlices"], json!(false)); } + #[test] + fn placing_with_show_import_options_off_takes_the_last_choices() { + let b64 = vectorcraft_format::base64_encode(&sliced(&layered())); + let mut app = app(); + app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap(); + // The file's bytes carry the Place dialog's Link, which they have no file for. + let place = |app: &mut VectorcraftApp, slices: Option| { + let mut p = json!({"name": "poster.psd", "dataBase64": b64, "link": true, "showImportOptions": false}); + if let Some(s) = slices { + p["importSlices"] = json!(s); + } + crate::place::run_for_user(app, &p).unwrap(); + assert!(app.ui.dialog.is_none(), "placed without asking"); + let st = app.session.doc().unwrap(); + let [id] = st.selection.objects[..] else { panic!("one object is selected") }; + (st.doc.node(id).unwrap().kind.clone(), st.doc.slices.len()) + }; + // At first, Convert Layers to Objects with Import Slices. + let (kind, slices) = place(&mut app, None); + assert!(matches!(kind, NodeKind::Group { .. })); + assert_eq!(slices, 1); + // An `importSlices` the params give is kept. + let (kind, slices) = place(&mut app, Some(false)); + assert!(matches!(kind, NodeKind::Group { .. })); + assert_eq!(slices, 1, "no new slice"); + // Then the choices made last. + app.ui.psd_import = json!({"layers": false, "importSlices": false}); + let (kind, slices) = place(&mut app, None); + assert!(matches!(kind, NodeKind::Image(_))); + assert_eq!(slices, 1, "no new slice"); + let (_, slices) = place(&mut app, Some(true)); + assert_eq!(slices, 2, "the given importSlices brings the slice in"); + } + + #[test] + fn files_from_bytes_placed_with_link_take_the_last_choices() { + let files: Vec = ["a.psd", "b.psd"].iter().map(|n| json!({ "name": n })).collect(); + let mut app = app(); + for n in ["a.psd", "b.psd"] { + app.place.picked.push((n.to_string(), sliced(&layered()))); + } + app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap(); + // The dialog's Link is on, but files from bytes have no file to link to. + let d = Dialog::new(crate::dialogs::place::KIND, json!({"files": files, "link": true, "template": false, "showImportOptions": false})); + crate::place::confirm(&mut app, &d).unwrap(); + assert!(app.ui.dialog.is_none(), "the cursor loads without asking"); + let view = vectorcraft_engine::ViewInfo::default(); + for kind in [vectorcraft_tools::PointerKind::Down, vectorcraft_tools::PointerKind::Up] { + for r in app.session.pointer(&vectorcraft_tools::PointerEvent::new(kind, 10.0, 10.0), view).unwrap() { + if let vectorcraft_engine::UiRequest::Place(p) = r { + crate::place::place_from_cursor(&mut app, &p); + } + } + } + let st = app.session.doc().unwrap(); + let [id] = st.selection.objects[..] else { panic!("one object is placed") }; + // At first, Convert Layers to Objects with Import Slices. + assert!(matches!(st.doc.node(id).unwrap().kind, NodeKind::Group { .. })); + assert_eq!(st.doc.slices.len(), 1); + } + + #[test] + fn a_cursor_loaded_with_show_import_options_on_asks_for_each_psd_file() { + let dir = std::env::temp_dir().join(format!("vc-psd-import-asks-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let paths: Vec = ["a.psd", "b.psd"] + .iter() + .map(|n| { + let p = dir.join(n); + std::fs::write(&p, layered()).unwrap(); + p.to_string_lossy().into_owned() + }) + .collect(); + let files: Vec = paths.iter().map(|p| json!({ "path": p })).collect(); + let services = crate::Services { read: Some(Box::new(|p: &str| std::fs::read(p).map_err(|e| e.to_string()))), ..Default::default() }; + let mut app = VectorcraftApp::new(Session::new(), services); + app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap(); + // The choices made last would place the layers without asking with Show Import Options off. + app.ui.psd_import = json!({"layers": true, "importSlices": true}); + let d = Dialog::new(crate::dialogs::place::KIND, json!({"files": files, "link": false, "template": false, "showImportOptions": true})); + crate::place::confirm(&mut app, &d).unwrap(); + assert!(app.ui.dialog.is_none(), "the cursor loads without asking"); + assert_eq!(app.session.tool_options()["count"], 2); + // A click on the canvas: the file the cursor's request names asks. + let view = vectorcraft_engine::ViewInfo::default(); + for kind in [vectorcraft_tools::PointerKind::Down, vectorcraft_tools::PointerKind::Up] { + for r in app.session.pointer(&vectorcraft_tools::PointerEvent::new(kind, 10.0, 10.0), view).unwrap() { + if let vectorcraft_engine::UiRequest::Place(p) = r { + crate::place::place_from_cursor(&mut app, &p); + } + } + } + assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(KIND), "the clicked file asks"); + std::fs::remove_dir_all(&dir).ok(); + } + #[test] fn scripted_places_and_places_that_give_layers_dont_ask() { let b64 = vectorcraft_format::base64_encode(&layered()); diff --git a/crates/ui-egui/src/place.rs b/crates/ui-egui/src/place.rs index 662c0c0266..01979b4537 100644 --- a/crates/ui-egui/src/place.rs +++ b/crates/ui-egui/src/place.rs @@ -61,7 +61,7 @@ pub fn is_document(name: &str) -> bool { #[derive(Default)] pub struct PlaceState { /// Web: the bytes of the files picked for the Place dialog, by name. - picked: Vec<(String, Vec)>, + pub(crate) picked: Vec<(String, Vec)>, /// The loaded cursor's thumbnails by file name. thumbs: Vec<(String, Thumb)>, /// The Control bar's `image.info`, for (document uid, revision, image id). @@ -113,10 +113,11 @@ fn engine(app: &mut VectorcraftApp, id: &str, p: &Value) -> Result Result { place_file(app, p, Read::AtOnce) } @@ -155,6 +156,7 @@ fn place_file(app: &mut VectorcraftApp, p: &Value, read: Read) -> Result Result