Merge pull request #5306 from storytold/fix/sec-place-queue-scope

Place: the place cursor reads its files within the roots it was loaded in
This commit is contained in:
BFlat
2026-11-03 16:29:13 +07:00
committed by GitHub
5 changed files with 76 additions and 6 deletions
+5
View File
@@ -163,6 +163,11 @@ pub struct Session {
/// While [`Session::place_queue_read`] runs `file.place.queue`: the files read for it, which
/// the command loads the place cursor with instead of reading them; `None` otherwise.
pub(crate) queue_hand_off: Option<cmd::place::QueueRead>,
/// The automation roots in force when the place cursor was last loaded (`None`: none were).
/// The cursor places its files later, on a click that may come from the person at the
/// keyboard: those files, and the files they link to, are read under these roots, so a queue
/// an agent loaded never reads beyond what it was given.
pub(crate) place_queue_roots: Option<std::sync::Arc<file_access::AutomationRoots>>,
/// While [`Session::import_image_read`] runs `clipboard.importImage`: the image read for it,
/// which the command loads instead of reading `dataBase64`; `None` otherwise.
pub(crate) image_hand_off: Option<cmd::clipboard::ImageRead>,
+1
View File
@@ -79,6 +79,7 @@ impl Session {
batch_steps: None,
place_hand_off: None,
queue_hand_off: None,
place_queue_roots: None,
image_hand_off: None,
trace_hand_off: None,
place_cursor_requests: false,
+33
View File
@@ -722,6 +722,39 @@ fn a_queue_of_3_with_click_drag_and_esc_places_2() {
assert_eq!(s.doc().unwrap().history.undo.len(), 2, "one undo step per placement");
}
/// A place cursor loaded within automation roots places its files within them, even on a later
/// click outside any roots (the person at the keyboard's): a queued file outside the roots, or
/// one it links to, is not read then. One loaded outside any roots places as before.
#[test]
fn the_place_cursor_places_within_the_roots_it_was_loaded_in() {
let base = std::env::temp_dir().join(format!("vectorcraft-place-roots-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(base.join("in")).unwrap();
let outside = base.join("outside.png");
std::fs::write(&outside, png(40, 20, [9, 9, 9, 255], None)).unwrap();
let entry = json!({"params": {"path": outside.to_string_lossy()}, "name": "outside.png", "width": 40, "height": 20});
let roots = crate::file_access::AutomationRoots::new(&[&base.join("in")], &[]).unwrap().unwrap();
let view = ViewInfo::default();
let click = |s: &mut Session| {
s.pointer(&PointerEvent::new(PointerKind::Down, 10.0, 10.0), view)?;
s.pointer(&PointerEvent::new(PointerKind::Up, 10.0, 10.0), view)
};
let mut s = session();
s.select_tool("place", view).unwrap();
// Loaded within the roots (as a tool option, past the check tool.setOption makes of paths).
crate::file_access::confine(Some(&roots), || s.set_tool_option("queue", &json!({"entries": [entry]})));
assert!(s.place_queue_roots().is_some());
let placed = crate::file_access::unconfined(|| click(&mut s));
assert!(top_children(&s).is_empty(), "{placed:?}");
// Loaded outside any roots: the click places it.
s.select_tool("place", view).unwrap();
crate::file_access::unconfined(|| s.set_tool_option("queue", &json!({"entries": [entry]})));
assert!(s.place_queue_roots().is_none());
crate::file_access::unconfined(|| click(&mut s)).unwrap();
assert_eq!(top_children(&s).len(), 1);
let _ = std::fs::remove_dir_all(base);
}
/// With [`Session::place_cursor_requests`] set, a click of the place cursor asks the frontend to
/// place its file ([`UiRequest::Place`]) and places nothing itself; the params it asks with place
/// the file as the click places it without the flag.
+26
View File
@@ -658,11 +658,27 @@ impl Session {
self.tool.options()
}
pub fn set_tool_option(&mut self, key: &str, v: &Value) {
if key == "queue" && self.tool.id() == "place" {
self.note_place_queue_roots();
}
self.tool.set_option(key, v);
self.tool_set_since_reset = true;
self.keep_tool_settings();
}
/// Keep the automation roots in force now as those the place cursor's queue, being loaded,
/// is placed under ([`Session::place_queue_roots`]).
fn note_place_queue_roots(&mut self) {
self.place_queue_roots = crate::file_access::current();
}
/// The automation roots the place cursor's files are placed under: those in force when it
/// was loaded. A frontend that places them itself ([`UiRequest::Place`]) reads them within
/// these roots ([`crate::file_access::confine`]).
pub fn place_queue_roots(&self) -> Option<std::sync::Arc<crate::file_access::AutomationRoots>> {
self.place_queue_roots.clone()
}
/// The options of tool `id`: the active tool's, or those it would have if chosen now.
pub fn tool_options_of(&self, id: &str) -> Value {
if id == self.tool.id() { self.tool.options() } else { self.make_tool(id).options() }
@@ -673,6 +689,9 @@ impl Session {
/// Liquify tools' Global Brush Dimensions, the Smooth tool's Fidelity for Pencil, Paintbrush
/// and Blob Brush Alt smoothing). Returns the tool's options.
pub fn set_tool_options(&mut self, id: Option<&str>, values: &Map<String, Value>) -> Value {
if values.contains_key("queue") && id.unwrap_or(self.tool.id()) == "place" {
self.note_place_queue_roots();
}
let active = id.is_none_or(|id| id == self.tool.id());
self.tool_set_since_reset |= active;
let mut other = (!active).then(|| self.make_tool(id.unwrap_or_default()));
@@ -842,6 +861,13 @@ impl Session {
}
// The place cursor's file, placed by the frontend that asked for it.
Action::Exec(cmd, p) if cmd == "file.place" && self.place_cursor_requests => ui.push(UiRequest::Place(p)),
// The place cursor's file, read within the roots of whoever loaded the cursor.
Action::Exec(cmd, p) if cmd == "file.place" => {
let roots = self.place_queue_roots.clone();
if let Some(w) = crate::file_access::confine(roots.as_ref(), || self.execute(&cmd, &p))?.get("warning").and_then(Value::as_str) {
ui.push(UiRequest::Status(w.to_string()));
}
}
Action::Exec(cmd, p) => {
// A command's warning (Crop Image's when the image couldn't be decoded) goes to the
// status bar too.
+11 -6
View File
@@ -376,13 +376,18 @@ fn queue_files(app: &mut VectorcraftApp, p: &Value, read: Read) -> Result<Value,
/// read off the UI thread where the files the user places are ([`crate::placing::start`]), else at
/// once. An error shows in the status bar.
pub(crate) fn place_from_cursor(app: &mut VectorcraftApp, p: &Value) {
if crate::placing::available(app) {
if let Err(e) = crate::placing::start(app, p, false) {
app.status(e);
// Read within the automation roots of whoever loaded the cursor, even on a click of the person
// at the keyboard: an agent's queue reaches no file it wasn't given.
let roots = app.session.place_queue_roots();
vectorcraft_engine::file_access::confine(roots.as_ref(), || {
if crate::placing::available(app) {
if let Err(e) = crate::placing::start(app, p, false) {
app.status(e);
}
} else {
let _ = engine(app, "file.place", p);
}
} else {
let _ = engine(app, "file.place", p);
}
});
}
/// The cursor's thumbnails from `file.place.queue`'s reply `r`.