mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 21:57:04 +00:00
Place: import options a cursor click asks for place within the cursor's roots
The OK of the import options a click of the place cursor asks for now places the file within the automation roots the cursor was loaded in, or within the roots of a caller that confirms with roots of its own. Options from a click before the cursor was loaded again are refused. A cursor loaded again (a queue set with tool.setOption, a script) no longer asks for import options or keeps the queued sizes of the cursor the Place dialog loaded. The engine counts cursor loads (Session::place_queue_loads) so the app can tell.
This commit is contained in:
@@ -244,6 +244,8 @@ pub struct Session {
|
||||
/// 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>>,
|
||||
/// How many times the place cursor has been loaded ([`Session::place_queue_loads`]).
|
||||
pub(crate) place_queue_loads: u64,
|
||||
/// 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>,
|
||||
|
||||
@@ -113,6 +113,7 @@ impl Session {
|
||||
place_hand_off: None,
|
||||
queue_hand_off: None,
|
||||
place_queue_roots: None,
|
||||
place_queue_loads: 0,
|
||||
image_hand_off: None,
|
||||
trace_hand_off: None,
|
||||
place_cursor_requests: false,
|
||||
|
||||
@@ -1256,6 +1256,14 @@ impl Session {
|
||||
/// is placed under ([`Session::place_queue_roots`]).
|
||||
fn note_place_queue_roots(&mut self) {
|
||||
self.place_queue_roots = crate::file_access::current();
|
||||
self.place_queue_loads = self.place_queue_loads.wrapping_add(1);
|
||||
}
|
||||
|
||||
/// How many times the place cursor has been loaded (`file.place.queue`, or a `queue` set on
|
||||
/// the Place tool): a frontend that keeps its own state about the loaded cursor compares it to
|
||||
/// tell a newer load.
|
||||
pub fn place_queue_loads(&self) -> u64 {
|
||||
self.place_queue_loads
|
||||
}
|
||||
|
||||
/// The automation roots the place cursor's files are placed under: those in force when it
|
||||
|
||||
@@ -74,8 +74,20 @@ pub struct PlaceState {
|
||||
queue_asks: bool,
|
||||
/// The loaded cursor's files by name, with the size a click places them at.
|
||||
queue_sizes: Vec<(String, f64, f64)>,
|
||||
/// The engine's count of cursor loads ([`vectorcraft_engine::Session::place_queue_loads`]) when
|
||||
/// `queue_asks` and `queue_sizes` were set: a later load (`tool.setOption`, a script) clears them.
|
||||
queue_load: u64,
|
||||
/// The import options dialog a click of the cursor opened (the token in its params'
|
||||
/// [`ASKED`]), with the automation roots the cursor was loaded in: its OK, pressed outside any
|
||||
/// roots, places the file within them. Cleared when the cursor is loaded again.
|
||||
asked: Option<(u64, std::sync::Arc<vectorcraft_engine::file_access::AutomationRoots>)>,
|
||||
}
|
||||
|
||||
/// The `file.place` param that ties an import options dialog's OK to the click of the place cursor
|
||||
/// that opened it ([`PlaceState::asked`]); hidden from the dialog's fields and removed before the
|
||||
/// file is placed.
|
||||
const ASKED: &str = "__placeCursorAsk";
|
||||
|
||||
/// A place cursor thumbnail: decoded when the cursor is loaded, uploaded when first drawn (and
|
||||
/// again in a new graphics context).
|
||||
struct Thumb {
|
||||
@@ -137,6 +149,20 @@ enum Read {
|
||||
}
|
||||
|
||||
fn place_file(app: &mut VectorcraftApp, p: &Value, read: Read) -> Result<Value, String> {
|
||||
// The OK of import options a click of the place cursor asked for places within the roots the
|
||||
// cursor was loaded in. A caller with roots of its own places within those.
|
||||
let Some(token) = p.get(ASKED) else { return place_file_in(app, p, read) };
|
||||
forget_older_queue(app);
|
||||
let Some(roots) = app.place.asked.as_ref().filter(|(t, _)| Some(*t) == token.as_u64()).map(|(_, r)| r.clone()) else {
|
||||
return Err("the place cursor that asked for these import options is no longer loaded".into());
|
||||
};
|
||||
if vectorcraft_engine::file_access::current().is_some() {
|
||||
return place_file_in(app, p, read);
|
||||
}
|
||||
vectorcraft_engine::file_access::confine(Some(&roots), || place_file_in(app, p, read))
|
||||
}
|
||||
|
||||
fn place_file_in(app: &mut VectorcraftApp, p: &Value, read: Read) -> Result<Value, String> {
|
||||
let show = match p.get("showImportOptions") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(Value::Bool(b)) => Some(*b),
|
||||
@@ -188,6 +214,9 @@ fn place_file(app: &mut VectorcraftApp, p: &Value, read: Read) -> Result<Value,
|
||||
{
|
||||
p["at"] = json!([v.center.x, v.center.y]);
|
||||
}
|
||||
if let Some(o) = p.as_object_mut() {
|
||||
o.remove(ASKED);
|
||||
}
|
||||
match read {
|
||||
Read::ForUser { dropped } if crate::placing::available(app) => {
|
||||
let r = crate::placing::start(app, &p, dropped);
|
||||
@@ -425,10 +454,19 @@ pub(crate) fn place_from_cursor(app: &mut VectorcraftApp, p: &Value) {
|
||||
// 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();
|
||||
forget_older_queue(app);
|
||||
vectorcraft_engine::file_access::confine(roots.as_ref(), || {
|
||||
if app.place.queue_asks {
|
||||
let mut p = clicked_at_full_size(app, p);
|
||||
// The import options it may ask for keep the token, and their OK places within the
|
||||
// same roots ([`place_file`]).
|
||||
app.place.asked = roots.clone().map(|r| {
|
||||
static TOKEN: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||
let token = TOKEN.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
p[ASKED] = json!(token);
|
||||
(token, r)
|
||||
});
|
||||
// Placing shows its own errors in the status bar.
|
||||
let p = clicked_at_full_size(app, p);
|
||||
let _ = place_file(app, &p, Read::ForUser { dropped: false });
|
||||
} else if crate::placing::available(app) {
|
||||
if let Err(e) = crate::placing::start(app, p, false) {
|
||||
@@ -440,6 +478,16 @@ pub(crate) fn place_from_cursor(app: &mut VectorcraftApp, p: &Value) {
|
||||
});
|
||||
}
|
||||
|
||||
/// The cursor was loaded again since [`show_thumbnails`] (`tool.setOption` with a `queue`, a
|
||||
/// script): its files no longer ask for their import options, and their queued sizes are unknown.
|
||||
fn forget_older_queue(app: &mut VectorcraftApp) {
|
||||
if app.place.queue_load != app.session.place_queue_loads() {
|
||||
app.place.queue_asks = false;
|
||||
app.place.queue_sizes.clear();
|
||||
app.place.asked = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// The params of a cursor click (`rect` at the file's queued size) on a PDF or DXF file, whose
|
||||
/// import options may change its size (another page, another scale): placed as its options place
|
||||
/// it at the click (`at`, the center of the box the click gave), as File › Place of that one file
|
||||
@@ -467,7 +515,9 @@ fn clicked_at_full_size(app: &VectorcraftApp, p: &Value) -> Value {
|
||||
/// The cursor's thumbnails and its files' sizes from `file.place.queue`'s reply `r` once the cursor
|
||||
/// is loaded; `asks`: its files ask for their import options when placed.
|
||||
pub(crate) fn show_thumbnails(app: &mut VectorcraftApp, r: &Value, asks: bool) {
|
||||
app.place.queue_load = app.session.place_queue_loads();
|
||||
app.place.queue_asks = asks;
|
||||
app.place.asked = None;
|
||||
app.place.queue_sizes = r["files"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
|
||||
@@ -1071,6 +1071,101 @@ fn the_place_cursors_files_ask_for_their_import_options_when_show_import_options
|
||||
assert_eq!(dialog_kind(&app), None);
|
||||
}
|
||||
|
||||
/// A cursor loaded again after the Place dialog loaded it with Show Import Options on (a queue set
|
||||
/// on the Place tool) places its files at once.
|
||||
#[test]
|
||||
fn a_place_cursor_loaded_again_no_longer_asks_for_import_options() {
|
||||
let mut app = app();
|
||||
let dir = Folder::new("ui-place-cursor-reloaded");
|
||||
let a = dir.file("a.png", &png(10, 10, 72.0));
|
||||
let notes = dir.file("notes.txt", NOTES);
|
||||
app.run("file.place", json!({"paths": [a, notes]})).unwrap();
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
let entries: Vec<Value> = [(&a, "a.png"), (¬es, "notes.txt")]
|
||||
.iter()
|
||||
.map(|(path, name)| json!({"params": {"path": path}, "name": name, "width": 190, "height": 90}))
|
||||
.collect();
|
||||
assert_eq!(app.session.tool_id(), "place");
|
||||
app.run("tool.setOption", json!({"values": {"queue": {"entries": entries}}})).unwrap();
|
||||
crate::place::place_from_cursor(&mut app, &json!({"path": notes, "rect": [10, 10, 190, 90]}));
|
||||
assert_eq!(dialog_kind(&app), None);
|
||||
assert!(selected_text(&app).contains("three"));
|
||||
}
|
||||
|
||||
/// The import options a click of a cursor loaded within automation roots asks for place within
|
||||
/// those roots when OK is pressed, by the person at the keyboard too: a file outside them is not
|
||||
/// read. One inside them is placed.
|
||||
#[test]
|
||||
fn the_import_options_a_place_cursor_asks_for_place_within_its_roots() {
|
||||
use vectorcraft_engine::file_access::{AutomationRoots, confine, unconfined};
|
||||
let mut app = app();
|
||||
let dir = Folder::new("ui-place-cursor-ask-roots");
|
||||
std::fs::create_dir_all(dir.join("in")).unwrap();
|
||||
let a = dir.file("in/a.png", &png(10, 10, 72.0));
|
||||
let notes = dir.file("in/notes.txt", NOTES);
|
||||
let pages = [PdfPage::new(100.0, 100.0, "0 g 0 0 10 10 re f"), PdfPage::new(300.0, 100.0, "1 0 0 rg 0 0 30 30 re f")];
|
||||
let two = dir.file("in/two.pdf", &pdf(&pages, None));
|
||||
let outside_notes = dir.file("outside.txt", NOTES);
|
||||
let outside_pdf = dir.file("outside.pdf", &pdf(&pages, None));
|
||||
let roots = AutomationRoots::new(&[&dir.join("in")], &[]).unwrap();
|
||||
confine(roots.as_ref(), || {
|
||||
app.run("file.place", json!({"paths": [a, notes, two]})).unwrap();
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("link".into(), json!(false));
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
});
|
||||
assert_eq!(app.session.tool_options()["count"], 3);
|
||||
let undo = |app: &VectorcraftApp| app.session.active().unwrap().history.undo.len();
|
||||
let before = undo(&app);
|
||||
// The text file's OK, its params naming a file outside the roots: nothing is placed.
|
||||
unconfined(|| crate::place::place_from_cursor(&mut app, &json!({"path": notes, "rect": [10, 10, 200, 100]})));
|
||||
assert_eq!(dialog_kind(&app), Some(crate::dialogs::text_import::KIND));
|
||||
app.ui.dialog.as_mut().unwrap().fields.get_mut("__params").unwrap()["path"] = json!(outside_notes);
|
||||
let _ = unconfined(|| crate::dialogs::confirm(&mut app));
|
||||
assert_eq!(undo(&app), before, "the text file outside the roots");
|
||||
// The PDF's OK, its file outside the roots: nothing is placed.
|
||||
unconfined(|| crate::place::place_from_cursor(&mut app, &json!({"path": two, "link": false, "rect": [10, 10, 100, 100]})));
|
||||
assert_eq!(dialog_kind(&app), Some("importPdf"));
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("path".into(), json!(outside_pdf));
|
||||
let _ = unconfined(|| crate::dialogs::confirm(&mut app));
|
||||
assert_eq!(undo(&app), before, "the PDF outside the roots");
|
||||
app.ui.dialog = None;
|
||||
// A caller with roots of its own confirms within those: the file in the cursor's roots but
|
||||
// outside the caller's is not placed.
|
||||
std::fs::create_dir_all(dir.join("b")).unwrap();
|
||||
let narrower = AutomationRoots::new(&[&dir.join("b")], &[]).unwrap();
|
||||
unconfined(|| crate::place::place_from_cursor(&mut app, &json!({"path": notes, "rect": [10, 10, 200, 100]})));
|
||||
let _ = confine(narrower.as_ref(), || crate::dialogs::confirm(&mut app));
|
||||
assert_eq!(undo(&app), before, "outside the caller's roots");
|
||||
// The text file inside the roots is placed.
|
||||
unconfined(|| crate::place::place_from_cursor(&mut app, &json!({"path": notes, "rect": [10, 10, 200, 100]})));
|
||||
assert_eq!(dialog_kind(&app), Some(crate::dialogs::text_import::KIND));
|
||||
unconfined(|| crate::dialogs::confirm(&mut app)).unwrap();
|
||||
assert!(selected_text(&app).contains("three"));
|
||||
// Once the cursor is loaded again, the earlier click's import options place nothing.
|
||||
let after = undo(&app);
|
||||
unconfined(|| crate::place::place_from_cursor(&mut app, &json!({"path": notes, "rect": [10, 10, 200, 100]})));
|
||||
assert_eq!(dialog_kind(&app), Some(crate::dialogs::text_import::KIND));
|
||||
app.run("file.place.queue", json!({"paths": [a]})).unwrap();
|
||||
assert!(unconfined(|| crate::dialogs::confirm(&mut app)).is_err());
|
||||
assert_eq!(undo(&app), after);
|
||||
// So do those of a click before a queue set on the Place tool.
|
||||
confine(roots.as_ref(), || {
|
||||
app.run("file.place", json!({"paths": [a, notes]})).unwrap();
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
});
|
||||
unconfined(|| crate::place::place_from_cursor(&mut app, &json!({"path": notes, "rect": [10, 10, 200, 100]})));
|
||||
assert_eq!(dialog_kind(&app), Some(crate::dialogs::text_import::KIND));
|
||||
let entry = json!({"params": {"path": a}, "name": "a.png", "width": 10, "height": 10});
|
||||
app.run("tool.setOption", json!({"values": {"queue": {"entries": [entry]}}})).unwrap();
|
||||
let saved = app.ui.dialog.clone();
|
||||
assert!(unconfined(|| crate::dialogs::confirm(&mut app)).is_err());
|
||||
assert_eq!(undo(&app), after);
|
||||
// Also when confirmed by a caller with roots that hold the file.
|
||||
app.ui.dialog = saved;
|
||||
assert!(confine(roots.as_ref(), || crate::dialogs::confirm(&mut app)).is_err());
|
||||
assert_eq!(undo(&app), after);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_placed_while_idle_follows_its_changes() {
|
||||
let mut app = app();
|
||||
|
||||
Reference in New Issue
Block a user