mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-15 19:56:55 -04:00
Links: one watch for changed linked files
The link watch checks the active document's linked files every two seconds and as the window comes back to the front, so the separate check on focus that #925 added (missing_links::after_focus and its asked-files set) goes: with both, a change was acted on twice. The watch also walks the document's links only when a look is due, not every frame, and wakes the app every two seconds only while the document has links. The doc comments of status and links::groups are back on their functions. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
079580257a
commit
068a934f6a
3 files changed
+17
-111
No files matched your search
@@ -167,13 +167,13 @@ fn linked(n: &Node) -> Option<(&LinkInfo, &str, Option<bool>)> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<u64>)> {
|
||||
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<Group> {
|
||||
let mut out: Vec<Group> = vec![];
|
||||
let mut index: BTreeMap<(String, String, bool), usize> = BTreeMap::new();
|
||||
|
||||
@@ -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<Value>, 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<std::time::SystemTime>)>);
|
||||
|
||||
/// 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<Value> = 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<Value> = 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);
|
||||
}
|
||||
}
|
||||
+13
-20
@@ -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<std::sync::mpsc::Receiver<bool>>,
|
||||
/// 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<dialogs::missing_links::FileStamp>,
|
||||
/// 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::<bool>(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<String>) {
|
||||
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(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user