diff --git a/crates/engine/src/cmd/links.rs b/crates/engine/src/cmd/links.rs index 779b780e..cd527216 100644 --- a/crates/engine/src/cmd/links.rs +++ b/crates/engine/src/cmd/links.rs @@ -167,13 +167,13 @@ fn linked(n: &Node) -> Option<(&LinkInfo, &str, Option)> { } } -/// The linked images and placed documents of `d` by file and content: those in the layers (only `ids` when -/// given), then, without `ids`, the ones only symbols and patterns show. /// Every linked file of `d` with the ids of the objects showing it (for [`crate::link_watch`]). pub(crate) fn linked_groups(d: &Document) -> Vec<(String, Vec)> { groups(d, None).into_iter().filter(|g| !g.ids.is_empty()).map(|g| (g.link.path, ids_json(&g.ids))).collect() } +/// The linked images and placed documents of `d` by file and content: those in the layers (only `ids` when +/// given), then, without `ids`, the ones only symbols and patterns show. fn groups(d: &Document, ids: Option<&[NodeId]>) -> Vec { let mut out: Vec = vec![]; let mut index: BTreeMap<(String, String, bool), usize> = BTreeMap::new(); diff --git a/crates/ui-egui/src/dialogs/missing_links.rs b/crates/ui-egui/src/dialogs/missing_links.rs index ecddaaed..43f8e9d5 100644 --- a/crates/ui-egui/src/dialogs/missing_links.rs +++ b/crates/ui-egui/src/dialogs/missing_links.rs @@ -2,8 +2,8 @@ //! (`links.relink`), and with Apply to All the other missing files are looked for by name in its //! folder; Ignore keeps the images showing their saved preview, and with Apply to All ignores the //! rest. Afterwards, with Preferences › Update Links: Ask When Modified, the modified links are -//! offered for update (`links.update`). Files changed while the app was in the background are -//! checked when its window comes back to the front ([`after_focus`]). +//! offered for update (`links.update`). Files another app changes while the document is open are +//! offered the same way ([`ask_update_changed`], from [`vectorcraft_engine::link_watch`]). //! //! Fields: `missing` (`[{name, path, ids}]`, the one asked about first), `modified` (the ids of //! images whose file was modified), `applyToAll`, `path` (the replacement file; picked when empty) @@ -82,39 +82,6 @@ fn ask(app: &mut VectorcraftApp, ids: Vec, question: &str) { super::confirm::ask(app, tl!("Update Modified Links"), &detail, "links.update", json!({ "ids": ids })); } -/// A linked file as last seen: its path, and its size and modification time when it exists. -pub(crate) type FileStamp = (String, Option<(u64, Option)>); - -/// The window came back to the front (#925): linked files of the active document changed -/// meanwhile are read again with Update Links: Automatically, offered for update with Ask When -/// Modified (once per change of a file), and left to the Links panel with Manually. Nothing is -/// asked while a dialog is open. -pub fn after_focus(app: &mut VectorcraftApp) { - if cfg!(target_arch = "wasm32") || app.session.prefs.update_links == "manually" || app.ui.dialog.is_some() || app.session.active().is_none() { - return; - } - let Ok(r) = app.session.execute("links.check", &json!({})) else { return }; - let rows: Vec = r["links"].as_array().into_iter().flatten().filter(|l| l["status"] == "modified").cloned().collect(); - if rows.is_empty() { - return; - } - if app.session.prefs.update_links == "automatically" { - // A failure shows in the status bar, as the Links panel's Update Link does. - let _ = app.run("links.update", json!({ "ids": ids(&rows) })); - return; - } - // The file each link reads (one found elsewhere after a move), its size and when it was last - // changed (two changes can share a time on file systems that keep it coarsely). - let stamp = |l: &Value| -> FileStamp { - let path = l["found"].as_str().or(l["path"].as_str()).unwrap_or_default().to_string(); - let meta = std::fs::metadata(&path).ok(); - let at = meta.as_ref().map(|m| (m.len(), m.modified().ok())); - (path, at) - }; - let new: Vec = rows.into_iter().filter(|l| app.links_asked.insert(stamp(l))).collect(); - ask_update(app, ids(&new)); -} - fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let t = Tokens::get(ui.ctx()); let missing = d.fields.get("missing").and_then(Value::as_array).map_or(&[][..], Vec::as_slice); @@ -303,58 +270,4 @@ mod tests { assert!(app.ui.dialog.is_none()); let _ = std::fs::remove_dir_all(dir); } - - /// #925: a linked file changed while the app was in the background follows Update Links when - /// the window comes back to the front. - #[test] - fn a_link_changed_in_the_background_follows_update_links_on_focus() { - let dir = std::env::temp_dir().join(format!("vectorcraft-focus-links-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); - let mut app = VectorcraftApp::new(Session::new(), Default::default()); - app.services.read = Some(Box::new(|p: &str| std::fs::read(p).map_err(|e| e.to_string()))); - app.run("file.new", json!({"width": 400, "height": 300})).unwrap(); - let pic = dir.join("a.png"); - // Each version a different size, so no two share a length and modification time. - let mut version = 0; - let mut change = || { - version += 1; - let mut out = vec![]; - image::RgbaImage::from_pixel(100 + version, 50, image::Rgba([40 * version as u8, 0, 0, 255])) - .write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png) - .unwrap(); - std::fs::write(&pic, out).unwrap(); - }; - change(); - app.run("file.place", json!({"path": pic.to_string_lossy()})).unwrap(); - let modified = |app: &mut VectorcraftApp| app.run("links.check", json!({})).unwrap()["modified"].as_u64().unwrap(); - // The window coming to the front asks for the check. - let mut raw = egui::RawInput { events: vec![egui::Event::WindowFocused(true)], ..Default::default() }; - app.raw_input_hook(&mut raw); - assert!(app.links_check); - // Ask When Modified (the default): asked once per change. - change(); - after_focus(&mut app); - let d = app.ui.dialog.take().expect("asks"); - assert_eq!(d.str("__command"), "links.update"); - after_focus(&mut app); - assert!(app.ui.dialog.is_none(), "not twice for the same change"); - change(); - after_focus(&mut app); - assert!(app.ui.dialog.is_some(), "a new change asks again"); - super::super::confirm(&mut app).unwrap(); - assert_eq!(modified(&mut app), 0); - // Automatically: read again without asking. - app.run("prefs.set", json!({"key": "updateLinks", "value": "automatically"})).unwrap(); - change(); - after_focus(&mut app); - assert!(app.ui.dialog.is_none()); - assert_eq!(modified(&mut app), 0); - // Manually: left to the Links panel. - app.run("prefs.set", json!({"key": "updateLinks", "value": "manually"})).unwrap(); - change(); - after_focus(&mut app); - assert!(app.ui.dialog.is_none()); - assert_eq!(modified(&mut app), 1); - let _ = std::fs::remove_dir_all(dir); - } } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index b899c376..cd41b96f 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -371,12 +371,6 @@ pub struct VectorcraftApp { /// The look for fonts installed or removed while the app was in the background, running /// ([`Self::refresh_installed_fonts`]): whether they were. font_check: Option>, - /// The window came back to the front: check the active document's linked files next frame - /// ([`dialogs::missing_links::after_focus`]). - pub(crate) links_check: bool, - /// The changed linked files already offered for update (path, size and modification time), so - /// coming back to the window doesn't ask about the same change twice. - pub(crate) links_asked: std::collections::HashSet, /// Keyboard pastes of something other than text (see [`shortcuts::PasteChord`]). pub(crate) paste_chord: shortcuts::PasteChord, /// Saves and exports running in the background (Preferences → File Handling). @@ -474,8 +468,6 @@ impl VectorcraftApp { clipboard_probe: None, picks: picks::Picks::default(), font_check: None, - links_check: false, - links_asked: Default::default(), paste_chord: Default::default(), background: Default::default(), recovery: Default::default(), @@ -757,7 +749,6 @@ impl VectorcraftApp { std::mem::take(&mut self.ime_discard) } - /// Show a transient status message. /// Linked files another app changed while their document is open: every two seconds, and as /// soon as the window comes back to the front, stamp the active document's linked files on a /// worker thread and act on the changed ones as Preferences › File Handling › Update Links @@ -786,19 +777,27 @@ impl VectorcraftApp { let focused = ctx.input(|i| i.focused); let was_focused: bool = ctx.data(|d| d.get_temp(focus_key)).unwrap_or(focused); ctx.data_mut(|d| d.insert_temp(focus_key, focused)); - let has_links = self.session.active().is_some_and(|d| !vectorcraft_engine::link_watch::linked_files(&d.doc).is_empty()); - if self.ui.dialog.is_some() || !has_links { + if self.ui.dialog.is_some() || self.session.active().is_none() { return; } let last: f64 = ctx.data(|d| d.get_temp(last_key)).unwrap_or(f64::NEG_INFINITY); if now - last >= 2.0 || (focused && !was_focused) { + // Walking the document's links costs a pass over it: only when a look is due. + // `start_link_scan` starts none for a document without links, which then doesn't + // wake the app for looks of its own (any frame still looks once one is due). ctx.data_mut(|d| d.insert_temp(last_key, now)); self.session.start_link_scan(); + let links = self.session.link_scan.is_some(); + ctx.data_mut(|d| d.insert_temp(egui::Id::new("linkWatch.links"), links)); + } + if self.session.link_scan.is_some() { + ctx.request_repaint_after(std::time::Duration::from_millis(100)); + } else if ctx.data(|d| d.get_temp::(egui::Id::new("linkWatch.links"))).unwrap_or(false) { + ctx.request_repaint_after(std::time::Duration::from_millis(2000)); } - let wait = if self.session.link_scan.is_some() { 100 } else { 2000 }; - ctx.request_repaint_after(std::time::Duration::from_millis(wait)); } + /// Show a transient status message. pub fn status(&mut self, s: impl Into) { self.ui.status = s.into(); } @@ -930,9 +929,6 @@ impl VectorcraftApp { self.system_paste = wanted && self.system_clipboard_pasteable(); } self.poll_font_check(ctx); - if std::mem::take(&mut self.links_check) { - dialogs::missing_links::after_focus(self); - } picks::poll(self, ctx); background::poll(self); #[cfg(not(target_arch = "wasm32"))] @@ -1068,10 +1064,7 @@ impl VectorcraftApp { match e { egui::Event::ModifiersChanged(m) => self.host_modifiers = *m, egui::Event::WindowFocused(false) => self.host_modifiers = egui::Modifiers::NONE, - egui::Event::WindowFocused(true) => { - self.refresh_installed_fonts(); - self.links_check = true; - } + egui::Event::WindowFocused(true) => self.refresh_installed_fonts(), _ => {} } }