From 079580257a7c335f220dce830b59adcc189d2983 Mon Sep 17 00:00:00 2001 From: Katt <238968166+KattKnight@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:25:56 +0530 Subject: [PATCH] Links: Update Links applies while a document is open Linked files another app changes while their document is open now follow Preferences > File Handling > Update Links, as they do when it opens: Automatically reads them again (an Update Links undo step), Ask When Modified offers to (a question worded for files changed on disk while working), Manually leaves them modified in the Links panel. The desktop app stamps the active document's linked files (size + modification time) on a worker thread every two seconds and when its window comes back to the front; no look starts while a dialog is open. Stamps are kept per document; a change is acted on once; a file still being written is tried again. links.updateChanged reads changed files again regardless of the preference (CLI, MCP). New strings translated in all ten catalogs; ROADMAP updated. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/engine/src/cmd/links.rs | 14 ++ crates/engine/src/lib.rs | 10 ++ crates/engine/src/link_watch.rs | 150 ++++++++++++++++++++ crates/engine/src/tests_linkwatch.rs | 88 ++++++++++++ crates/ui-egui/src/dialogs/missing_links.rs | 15 +- crates/ui-egui/src/i18n/cs.tsv | 3 + crates/ui-egui/src/i18n/es.tsv | 3 + crates/ui-egui/src/i18n/fr.tsv | 3 + crates/ui-egui/src/i18n/it.tsv | 3 + crates/ui-egui/src/i18n/ja.tsv | 3 + crates/ui-egui/src/i18n/pt-br.tsv | 3 + crates/ui-egui/src/i18n/ru.tsv | 3 + crates/ui-egui/src/i18n/uk.tsv | 3 + crates/ui-egui/src/i18n/zh-hans.tsv | 3 + crates/ui-egui/src/i18n/zh-hant.tsv | 3 + crates/ui-egui/src/lib.rs | 43 ++++++ 17 files changed, 347 insertions(+), 4 deletions(-) create mode 100644 crates/engine/src/link_watch.rs create mode 100644 crates/engine/src/tests_linkwatch.rs diff --git a/ROADMAP.md b/ROADMAP.md index 3450000c..76f7dff6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -179,6 +179,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist - Affinity documents in (AF.1–AF.4): `.af` (Affinity 3) and `.afdesign`/`.afphoto`/`.afpub` (Affinity 1 and 2, container versions 8–12) open and place as editable art: layers and sublayers, groups (pass-through or isolated), artboards (with their names and backgrounds) and Publisher pages side by side, curves with live corners, parametric shapes (rectangles with corner radii, ellipses, polygons, stars, pies, triangles, trapezoids), compound shapes, solid/linear/elliptical gradient fills and strokes (weight, dashes, alignment, scale-with-object), clipping by nesting, vector and pixel masks, artistic and frame text with fonts, sizes, tracking, leading and alignment, placed images (their original JPEG/PNG) and pixel layers (8/16-bit RGB, CMYK), opacity, visibility, locking and blend modes, at the document's resolution in points. Effects, adjustments, brushes and pressure profiles, transparency gradients, master pages, symbol instances (imported as their cached picture), special shapes (cloud, heart, cog…, imported as ellipses) and Affinity-only blend modes are named in the import warning. A file whose native data can't be read opens as its embedded preview with the reason, and that preview can't be placed. Save never writes back over the Affinity file and there is no Affinity export. AF.4 adds Affinity 3 artboard recognition and actual-outline clipping, source-backed JPEG/PNG raster tiles, Unicode text-run recovery, stored fallback/exact installed fonts, OpenType feature flags and compound-geometry limits. 51 public CC0/MIT/Apache-2.0 files (33 current `.af`), pinned and sha256-verified (`cargo xtask corpus --affinity`), test native retention and every-board native/SVG/PDF/PSD output for current files; selected files compare their saved thumbnails. No Affinity reopen or independent paired PSD oracle was available; [source audit and complex-font gaps](docs/affinity-validation.md); [scope, provenance and limits](crates/affinity/README.md). - Placed documents: File › Place of a `.vectorcraft` file links to it by default (`file.place`'s `link`): one locked object showing an artboard of the document (or its art's bounds), read the way placing it without Link reads it, its symbols, patterns and images kept apart from the document's; without Link (or from bytes, not a file) it is an editable copy as before. It stays vectors in every output (SVG, PDF, EPS, EMF, DXF), and so do placed documents inside it, read from their own files up to 8 levels deep (documents that place each other stop there). On screen they draw from bitmaps cached per file and zoom step, shared by every copy and made in the background (a placeholder until ready), so a sheet of them redraws in about a millisecond; exports and prints draw them exactly. Like linked images, a save keeps only a preview of each (JPEG when opaque) unless Include Linked Files; output, and zooming past the preview, read the file again, falling back to the preview with a warning when it is gone or changed. Saves for older versions write them as plain art. Links check, update, relink, Update Links on open, Link Info (the artboard's size), Placement Options, Break Link (also Embed, Object › Expand: the editable copy placing without Link gives, its resources joining the document), Edit Original (the document in a new tab) and the Links panel cover them; saving a document updates the open documents that place it. The native format (v4) keeps data of 8 KB or more after the JSON instead of base64 in it. - Place and the Links panel, Package (placed `.vectorcraft` documents packaged with their own linked files and fonts, relinked to the copies: #777), File Info, Print with print presets, PostScript output and print tiling, slices and Save for Web, Export for Screens and Asset Export, CSS Properties, PNG/PDF/SVG/text clipboard flavours; files copied in a file manager paste as art (SVG, PDF, EMF/WMF, bitmaps) on every desktop platform, and Linux/Wayland reads the compositor's clipboard (#398); on Linux the Paste menu checks the clipboard off the UI thread, so an X11 owner that never answers can't freeze the window (#510); screenshots and other copied pictures paste as embedded images at the view centre everywhere, the web included (#457). + - Linked files another app changes while their document is open follow Preferences › File Handling › Update Links, as they do when it opens: Automatically reads them again (an Update Links undo step), Ask When Modified offers to, Manually leaves them modified in the Links panel. The desktop app compares the active document’s linked files by size and modification time every two seconds and when its window comes back to the front, on a worker thread; a file still being written is tried again (`links.updateChanged`). - **Performance:** 20k shapes + 1k texts render in 27 ms per full-retina frame (7.8 ms zoomed), 7× faster than the first version. The UI thread never blocks. The web build is 7.1 MB gzipped. - **Graphics processor:** the canvas is rasterized on the CPU and the GPU only composites it, so any GPU that can show the window will do. Preferences › Performance › Graphics Processor (`gpuPreference`) is Automatic by default: the power-saving GPU on Windows and macOS (the integrated one on hybrid-graphics laptops, where presenting from the discrete GPU made the window flicker, #306), and the GPU the desktop runs on under Linux, where KWin on an NVIDIA GPU refused frames from a Ryzen's integrated GPU and 0.5.0 crashed at startup (#502). Power Saving and High Performance pick a kind of GPU; `WGPU_POWER_PREF`, `WGPU_ADAPTER_NAME` and `WGPU_BACKEND` override them. The app picks among the adapters that can present to the window (native backends before OpenGL, hardware before software, and on Windows a GPU's DX12 adapter before its Vulkan one: Intel's Vulkan driver made the whole window flicker black, #545), and if the window fails on one while starting up it starts again without it instead of crashing; Help › About, `ui.inspect` and the log name the adapter in use. When the browser drops the page's WebGPU device or WebGL context, the web build writes recovery copies, starts again on new graphics with the same app and uploads its textures again; if it can't, it removes the stale canvas and says to reload, which offers the copies back. The desktop app writes the copies and says to restart (#369). On Windows and Linux the tools' cursors are OS cursors (bitmaps rasterized once per display scale), which the system moves itself: painted into the window, they trailed the pointer by the frames waiting to be shown, and the window now queues one frame instead of two (#444). macOS and the web still paint them (macOS sizes cursor bitmaps in points, eframe has no bitmap cursors on the web). Pens and touch drive the tools as the mouse does, and a tap pressed and lifted within one frame clicks where it tapped (#491); pen pressure reaches the Liquify tools' Use Pressure Pen on Windows, while winit 0.30 reports none on macOS or Linux and egui none on the web. On Linux, see `docs/development.md` for pens under Wayland. - **Tests and robustness:** ~2,840 automated tests: model-based property tests, a junk-parameter sweep over every command, import fuzzing (SVG, PDF, libraries), golden renders, and MCP end-to-end tests over stdio. Shipped code never panics (workspace lints and a rollback safety net; see `docs/development.md`). diff --git a/crates/engine/src/cmd/links.rs b/crates/engine/src/cmd/links.rs index 59915ff3..779b780e 100644 --- a/crates/engine/src/cmd/links.rs +++ b/crates/engine/src/cmd/links.rs @@ -52,6 +52,15 @@ pub fn specs() -> Vec { has_doc, update ), + cmd!( + "links.updateChanged", + "Update Changed Links", + [], + None, + "{} read again the active document's linked files that changed on disk since they were last seen (size or modification time), whatever Preferences → File Handling → Update Links says; the desktop app looks every two seconds and follows that preference. Files seen for the first time are only remembered. One undo step → {updated: [ids], ask: [], modified: []}", + has_doc, + crate::link_watch::update_changed + ), cmd!( "links.relink", "Relink", @@ -160,6 +169,11 @@ 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() +} + 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/engine/src/lib.rs b/crates/engine/src/lib.rs index e8d389d2..ec72f1ee 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -10,6 +10,7 @@ pub mod cmd; pub mod file_access; pub mod guard; pub mod inspect; +pub mod link_watch; pub mod steps; mod tooling; pub mod units; @@ -873,6 +874,11 @@ pub struct Session { /// The walker threads a folder search starts; `None`: [`cmd::findfiles::threads`]. Tests set /// it. pub search_threads: Option, + /// Each open document's linked files' size and modification time when last seen (linked + /// files changing while the document is open, [`link_watch`]). + pub link_stamps: std::collections::HashMap<(u64, String), link_watch::Stamp>, + /// The stamps being taken on a worker thread ([`Session::start_link_scan`]). + pub link_scan: Option, } impl Default for Session { @@ -928,6 +934,8 @@ impl Session { font_search: None, search_rules: None, search_threads: None, + link_stamps: Default::default(), + link_scan: None, } } @@ -1506,6 +1514,8 @@ mod tests_links; #[cfg(test)] mod tests_linkspanel; #[cfg(test)] +mod tests_linkwatch; +#[cfg(test)] mod tests_liquify; #[cfg(test)] mod tests_live; diff --git a/crates/engine/src/link_watch.rs b/crates/engine/src/link_watch.rs new file mode 100644 index 00000000..c30f8b58 --- /dev/null +++ b/crates/engine/src/link_watch.rs @@ -0,0 +1,150 @@ +//! Linked files that change while their document is open (another app saved them: PhotoCraft, +//! LightCraft, Photoshop, a placed VectorCraft document): Preferences › File Handling › Update +//! Links decides, as it does when a document opens. Automatically reads them again +//! (`links.update`, an undo step), Ask When Modified offers to, Manually leaves them modified in +//! the Links panel. +//! +//! The active document's linked files are compared by size and modification time with what they +//! were when last seen. The desktop app takes the stamps every two seconds and when its window +//! comes back to the front, on a worker thread so a file on a slow network share never holds up +//! the interface ([`Session::start_link_scan`], [`Session::poll_link_scan`]); +//! `links.updateChanged` takes them on the spot and reads the changed files again (CLI, agents). +//! +//! Stamps are kept per document: one in the background is looked at when it comes to the front. +//! A file seen for the first time is only remembered (one modified while its document was closed +//! is handled when the document opens); one that can't be read yet (another app is still writing +//! it) keeps its old stamp, so the next look tries again. A change is acted on once: undoing an +//! update, or answering No, leaves the link modified until the file changes again. + +use std::collections::{BTreeSet, HashMap, HashSet}; +use std::sync::{Arc, Mutex, PoisonError}; + +use serde_json::{Value, json}; + +use crate::{Result, Session}; + +/// A file's size and modification time (nanoseconds since 1970). +pub type Stamp = (u64, u128); + +/// Stamps of a document's linked files taken on a worker thread: (document uid, [(file, +/// object ids, stamp)]). +pub type LinkScan = Arc, Option)>)>>>; + +#[cfg(not(target_arch = "wasm32"))] +fn stamp(path: &str) -> Option { + let meta = std::fs::metadata(path).ok()?; + let modified = meta.modified().ok()?.duration_since(std::time::UNIX_EPOCH).ok()?; + Some((meta.len(), modified.as_nanos())) +} + +#[cfg(target_arch = "wasm32")] +fn stamp(_: &str) -> Option { + None +} + +/// The linked files of `d` with the ids of the objects showing them. +pub fn linked_files(d: &vectorcraft_doc::Document) -> Vec<(String, Vec)> { + crate::cmd::links::linked_groups(d) +} + +/// `links.updateChanged`: stamp the active document's linked files now and read the changed +/// ones again, whatever Update Links says. +pub(crate) fn update_changed(s: &mut Session, _: &Value) -> Result { + let st = s.doc()?; + let (uid, files) = (st.uid, crate::cmd::links::linked_groups(&st.doc)); + let now = files.into_iter().map(|(p, ids)| (p.clone(), ids, stamp(&p))).collect(); + s.apply_link_stamps(uid, now, true) +} + +impl Session { + /// Start stamping the active document's linked files on a worker thread, unless a look is + /// still running (or the build has no files to look at: the web app). + pub fn start_link_scan(&mut self) { + if self.link_scan.is_some() || cfg!(target_arch = "wasm32") { + return; + } + let Some(st) = self.active() else { return }; + let (uid, files) = (st.uid, crate::cmd::links::linked_groups(&st.doc)); + if files.is_empty() { + return; + } + let slot: LinkScan = Arc::default(); + let out = slot.clone(); + let spawned = std::thread::Builder::new().name("link-watch".into()).spawn(move || { + let stamps = files.into_iter().map(|(p, ids)| (p.clone(), ids, stamp(&p))).collect(); + *out.lock().unwrap_or_else(PoisonError::into_inner) = Some((uid, stamps)); + }); + if spawned.is_ok() { + self.link_scan = Some(slot); + } + } + + /// Apply a finished look (`None` while there is none, or it is still running): see + /// [`Session::apply_link_stamps`]. + pub fn poll_link_scan(&mut self) -> Option> { + let (uid, stamps) = self.link_scan.as_ref()?.lock().unwrap_or_else(PoisonError::into_inner).take()?; + self.link_scan = None; + Some(self.apply_link_stamps(uid, stamps, false)) + } + + /// Compare the stamps of document `uid`'s linked files with the ones last seen and act on + /// the changed ones as Update Links says (`update`: read them again regardless). Returns + /// `{updated: [ids], ask: [ids], modified: [ids]}`: read again, to offer (Ask When Modified), + /// or left modified (Manually). + pub fn apply_link_stamps(&mut self, uid: u64, now: Vec<(String, Vec, Option)>, update: bool) -> Result { + let none = || json!({"updated": [], "ask": [], "modified": []}); + // The document went to the back meanwhile: it's looked at when it's in front again. + if self.active().map(|d| d.uid) != Some(uid) { + return Ok(none()); + } + // This document's last stamps; those of closed documents are dropped. + let open: HashSet = self.documents().iter().map(|d| d.uid).collect(); + let mut before: HashMap = HashMap::new(); + self.link_stamps.retain(|(u, p), s| { + if *u == uid { + before.insert(p.clone(), *s); + false + } else { + open.contains(u) + } + }); + let mut changed: Vec<(String, Vec)> = Vec::new(); + for (path, ids, stamp) in now { + match (before.get(&path), stamp) { + // Not there right now (an app saving by delete and rename): keep the last stamp. + (Some(old), None) => { + self.link_stamps.insert((uid, path), *old); + } + (old, Some(new)) => { + if old.is_some_and(|o| *o != new) { + changed.push((path.clone(), ids)); + } + self.link_stamps.insert((uid, path), new); + } + (None, None) => {} + } + } + if changed.is_empty() { + return Ok(none()); + } + let ids: Vec = changed.iter().flat_map(|(_, ids)| ids.iter().copied()).collect(); + let mode = if update { "automatically" } else { self.prefs.update_links.as_str() }; + match mode { + "automatically" => { + let r = self.execute("links.update", &json!({"ids": ids}))?; + // Not readable yet (still being written): the next look tries again. + let unread: BTreeSet = r["missing"].as_array().map(|a| a.iter().filter_map(Value::as_u64).collect()).unwrap_or_default(); + for (path, ids) in &changed { + if ids.iter().any(|i| unread.contains(i)) + && let Some(old) = before.get(path) + { + self.link_stamps.insert((uid, path.clone()), *old); + } + } + Ok(json!({"updated": r["updated"], "ask": [], "modified": []})) + } + "askWhenModified" => Ok(json!({"updated": [], "ask": ids, "modified": []})), + _ => Ok(json!({"updated": [], "ask": [], "modified": ids})), + } + } +} diff --git a/crates/engine/src/tests_linkwatch.rs b/crates/engine/src/tests_linkwatch.rs new file mode 100644 index 00000000..6d2d0b87 --- /dev/null +++ b/crates/engine/src/tests_linkwatch.rs @@ -0,0 +1,88 @@ +//! Linked files changing while their document is open (`link_watch`): `links.updateChanged`, +//! and the background look following Preferences → File Handling → Update Links. + +use serde_json::{Value, json}; + +use super::*; +use crate::tests_links::{BLUE, Folder, RED, centre_colour, place, png, session}; + +/// Write `bytes` to `path` so its modification time moves even on coarse file systems. +fn save(path: &str, bytes: &[u8]) { + std::thread::sleep(std::time::Duration::from_millis(30)); + std::fs::write(path, bytes).unwrap(); +} + +fn colour(s: &Session) -> [u8; 3] { + centre_colour(&s.doc().unwrap().doc) +} + +/// One background look, waited for. +fn look(s: &mut Session) -> Value { + s.start_link_scan(); + let t0 = std::time::Instant::now(); + loop { + if let Some(r) = s.poll_link_scan() { + return r.unwrap(); + } + assert!(t0.elapsed().as_secs() < 10, "the look never finished"); + std::thread::yield_now(); + } +} + +#[test] +fn update_changed_reads_changed_files_again_once() { + let dir = Folder::new("watch-update"); + let path = dir.file("art.png"); + std::fs::write(&path, png(40, 40, RED)).unwrap(); + let mut s = session(); + let id = place(&mut s, &path); + // First look: remembered, nothing to read. + assert_eq!(s.execute("links.updateChanged", &json!({})).unwrap()["updated"], json!([])); + save(&path, &png(40, 40, BLUE)); + assert_eq!(s.execute("links.updateChanged", &json!({})).unwrap()["updated"], json!([id.0])); + assert_eq!(colour(&s), BLUE); + // Undoing it is respected: the link stays modified until the file changes again. + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(colour(&s), RED); + assert_eq!(s.execute("links.updateChanged", &json!({})).unwrap()["updated"], json!([])); + assert_eq!(s.execute("links.check", &json!({})).unwrap()["modified"], 1); +} + +#[test] +fn a_background_look_follows_update_links() { + for (mode, key) in [("automatically", "updated"), ("askWhenModified", "ask"), ("manually", "modified")] { + let dir = Folder::new(&format!("watch-{mode}")); + let path = dir.file("art.png"); + std::fs::write(&path, png(40, 40, RED)).unwrap(); + let mut s = session(); + s.prefs.update_links = mode.into(); + let id = place(&mut s, &path); + assert_eq!(look(&mut s), json!({"updated": [], "ask": [], "modified": []}), "{mode}"); + save(&path, &png(40, 40, BLUE)); + let r = look(&mut s); + assert_eq!(r[key], json!([id.0]), "{mode}: {r}"); + assert_eq!(colour(&s), if mode == "automatically" { BLUE } else { RED }, "{mode}"); + // Acted on once: the next look finds nothing new. + assert_eq!(look(&mut s), json!({"updated": [], "ask": [], "modified": []}), "{mode}"); + assert!(s.link_scan.is_none()); + } +} + +#[test] +fn a_file_still_being_written_is_tried_again() { + let dir = Folder::new("watch-partial"); + let path = dir.file("art.png"); + std::fs::write(&path, png(40, 40, RED)).unwrap(); + let mut s = session(); + s.prefs.update_links = "automatically".into(); + let id = place(&mut s, &path); + look(&mut s); + // Another app has only written the start of the file so far. + let full = png(40, 40, BLUE); + save(&path, &full[..16]); + assert_eq!(look(&mut s)["updated"], json!([])); + assert_eq!(colour(&s), RED, "the last good pixels stay"); + save(&path, &full); + assert_eq!(look(&mut s)["updated"], json!([id.0])); + assert_eq!(colour(&s), BLUE); +} diff --git a/crates/ui-egui/src/dialogs/missing_links.rs b/crates/ui-egui/src/dialogs/missing_links.rs index e2bd4b45..ecddaaed 100644 --- a/crates/ui-egui/src/dialogs/missing_links.rs +++ b/crates/ui-egui/src/dialogs/missing_links.rs @@ -61,6 +61,16 @@ fn open(app: &mut VectorcraftApp, missing: Vec, modified: Vec) { /// With Update Links: Ask When Modified, offer to read the modified linked files `ids` again. fn ask_update(app: &mut VectorcraftApp, ids: Vec) { + ask(app, ids, tl!("{count} linked image(s) changed since this document was saved. Show the new versions?")); +} + +/// The same offer for linked files another app changed while the document is open +/// ([`vectorcraft_engine::link_watch`]). +pub(crate) fn ask_update_changed(app: &mut VectorcraftApp, ids: Vec) { + ask(app, ids, tl!("{count} linked image(s) changed on disk. Show the new versions?")); +} + +fn ask(app: &mut VectorcraftApp, ids: Vec, question: &str) { if ids.is_empty() { return; } @@ -68,10 +78,7 @@ fn ask_update(app: &mut VectorcraftApp, ids: Vec) { app.status(format!("{} linked image(s) changed on disk: Update Links shows the new versions", ids.len())); return; } - let detail = crate::i18n::fmt( - tl!("{count} linked image(s) changed since this document was saved. Show the new versions?"), - &[("count", &ids.len().to_string())], - ); + let detail = crate::i18n::fmt(question, &[("count", &ids.len().to_string())]); super::confirm::ask(app, tl!("Update Modified Links"), &detail, "links.update", json!({ "ids": ids })); } diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 2b42f7c6..06aa8084 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -516,6 +516,9 @@ Layers Vrstvy Libraries Knihovny Links Propojení + {count} linked image(s) changed on disk. Show the new versions? Na disku se změnilo {count} propojených obrázků. Zobrazit nové verze? + Update Changed Links Aktualizovat změněná propojení +@msg Updated {_1} linked file(s) changed on disk Aktualizováno propojených souborů změněných na disku: {_1} Magic Wand Kouzelná hůlka Navigator Navigátor Pattern Options Možnosti vzorku diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 6c631752..9ea4e4ab 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -3003,6 +3003,7 @@ Untitled Sin título Update Link Actualizar vínculo Update Links Actualizar vínculos + Update Changed Links Actualizar vínculos cambiados Update Modified Links Actualizar vínculos modificados Updated {} image(s) Se actualizaron {} imagen(es) UPPERCASE MAYÚSCULAS @@ -3138,6 +3139,7 @@ {count} linked file(s) not found No se encontraron {count} archivo(s) vinculado(s) fonts not copied: {fonts} fuentes no copiadas: {fonts} {count} linked image(s) changed since this document was saved. Show the new versions? {count} imagen(es) vinculada(s) cambiaron desde que se guardó este documento. ¿Mostrar las versiones nuevas? + {count} linked image(s) changed on disk. Show the new versions? {count} imagen(es) vinculada(s) cambiaron en el disco. ¿Mostrar las versiones nuevas? {count} more rules: Export writes every one. {count} reglas más: Exportar las escribe todas. {count} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. Se omitieron {count} objetos sin nombre: ponles nombre o activa Objetos sin nombre en el menú del panel. {effect} is already applied {effect} ya está aplicado @@ -3788,6 +3790,7 @@ axis Both Ambos @msg {_1} is already used by {_2} {_1} ya lo usa {_2} @msg {_1} linked file(s) can't be read here: their previews show aquí no se pueden leer {_1} archivo(s) vinculado(s): se muestran sus previsualizaciones @msg {_1} linked image(s) changed on disk: Update Links shows the new versions {_1} imagen(es) vinculada(s) cambiaron en el disco: Actualizar vínculos muestra las versiones nuevas +@msg Updated {_1} linked file(s) changed on disk Se actualizaron {_1} archivo(s) vinculado(s) que cambiaron en el disco @msg {_1}: not saved, so not closed {_1}: no se guardó, así que no se cerró @msg {bpp}-bit bitmaps can't be read no se pueden leer mapas de bits de {bpp} bits @msg {command} didn't create an object {command} no creó ningún objeto diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 90f9551d..42e9469a 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -3000,6 +3000,7 @@ Untitled Sans titre Update Link Mettre à jour le lien Update Links Mettre à jour les liens + Update Changed Links Mettre à jour les liens modifiés Update Modified Links Mettre à jour les liens modifiés Updated {} image(s) {} image(s) mise(s) à jour UPPERCASE MAJUSCULES @@ -3135,6 +3136,7 @@ {count} linked file(s) not found {count} fichier(s) lié(s) introuvable(s) fonts not copied: {fonts} polices non copiées : {fonts} {count} linked image(s) changed since this document was saved. Show the new versions? {count} image(s) liée(s) modifiée(s) depuis l’enregistrement de ce document. Afficher les nouvelles versions ? + {count} linked image(s) changed on disk. Show the new versions? {count} image(s) liée(s) modifiée(s) sur le disque. Afficher les nouvelles versions ? {count} more rules: Export writes every one. {count} règles de plus : Exporter les écrit toutes. {count} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. {count} objets sans nom omis : nommez-les ou activez Objets sans nom dans le menu du panneau. {effect} is already applied {effect} est déjà appliqué @@ -3785,6 +3787,7 @@ axis Both Les deux @msg {_1} is already used by {_2} {_1} est déjà utilisé par {_2} @msg {_1} linked file(s) can't be read here: their previews show {_1} fichier(s) lié(s) illisible(s) ici : leurs aperçus sont affichés @msg {_1} linked image(s) changed on disk: Update Links shows the new versions {_1} image(s) liée(s) modifiée(s) sur le disque : Mettre à jour les liens affiche les nouvelles versions +@msg Updated {_1} linked file(s) changed on disk {_1} fichier(s) lié(s) modifié(s) sur le disque mis à jour @msg {_1}: not saved, so not closed {_1} : non enregistré, donc non fermé @msg {bpp}-bit bitmaps can't be read impossible de lire les images bitmap en {bpp} bits @msg {command} didn't create an object {command} n’a créé aucun objet diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index be58523b..a4effecc 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -3003,6 +3003,7 @@ Untitled Senza titolo Update Link Aggiorna collegamento Update Links Aggiorna collegamenti + Update Changed Links Aggiorna collegamenti modificati Update Modified Links Aggiorna collegamenti modificati Updated {} image(s) Immagini aggiornate: {} UPPERCASE MAIUSCOLO @@ -3138,6 +3139,7 @@ {count} linked file(s) not found File collegati non trovati: {count} fonts not copied: {fonts} font non copiati: {fonts} {count} linked image(s) changed since this document was saved. Show the new versions? Immagini collegate modificate dopo il salvataggio di questo documento: {count}. Vuoi mostrare le nuove versioni? + {count} linked image(s) changed on disk. Show the new versions? Immagini collegate modificate su disco: {count}. Vuoi mostrare le nuove versioni? {count} more rules: Export writes every one. Altre {count} regole: Esporta le scrive tutte. {count} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. {count} oggetti senza nome esclusi: assegna loro un nome o attiva Oggetti senza nome nel menu del pannello. {effect} is already applied L'effetto {effect} è già applicato @@ -3788,6 +3790,7 @@ axis Both Entrambi @msg {_1} is already used by {_2} {_1} è già usato da {_2} @msg {_1} linked file(s) can't be read here: their previews show file collegati non leggibili qui: {_1}; vengono mostrate le loro anteprime @msg {_1} linked image(s) changed on disk: Update Links shows the new versions immagini collegate modificate su disco: {_1}; Aggiorna collegamenti mostra le nuove versioni +@msg Updated {_1} linked file(s) changed on disk File collegati modificati su disco aggiornati: {_1} @msg {_1}: not saved, so not closed {_1}: non salvato, quindi non chiuso @msg {bpp}-bit bitmaps can't be read impossibile leggere bitmap a {bpp} bit @msg {command} didn't create an object {command} non ha creato alcun oggetto diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 0b1502fa..4230940f 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -2668,6 +2668,7 @@ Unshift シフトを戻す Update Link リンクを更新 Update Links リンクを更新 + Update Changed Links 変更されたリンクを更新 Update Modified Links 変更されたリンクを更新 Use メニュー Use Artboards アートボードを使用 @@ -2777,6 +2778,7 @@ {count} linked file(s) not found リンクファイルが {count} 個見つかりません fonts not copied: {fonts} コピーされなかったフォント:{fonts} {count} linked image(s) changed since this document was saved. Show the new versions? 保存後に {count} 個のリンク画像が変更されています。新しいバージョンを表示しますか? + {count} linked image(s) changed on disk. Show the new versions? {count} 個のリンク画像がディスク上で変更されました。新しいバージョンを表示しますか? {count} more rules: Export writes every one. ほかに {count} 個のルールがあります。書き出しにはすべて含まれます。 {count} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. 名前のないオブジェクト {count} 個を除外しました。名前を付けるか、パネルメニューで「名前のないオブジェクト」をオンにしてください。 {effect} is already applied {effect} はすでに適用されています @@ -3719,6 +3721,7 @@ theme Appearance 外観 @msg `{tool}` isn't a Liquify tool `{tool}` はゆがみツールではありません @msg {_1} linked file(s) can't be read here: their previews show この環境では {_1} 個のリンクファイルを読み込めません: 代わりにプレビューを表示しています @msg {_1} linked image(s) changed on disk: Update Links shows the new versions {_1} 個のリンク画像がディスク上で変更されました: 「リンクを更新」で新しいバージョンを表示できます +@msg Updated {_1} linked file(s) changed on disk ディスク上で変更された {_1} 個のリンクファイルを更新しました @msg pick a colour, gradient or pattern to save as a swatch スウォッチとして保存するカラー、グラデーション、またはパターンを選択してください @msg Packaged {n} file(s) in {folder}{note} {n} 個のファイルを {folder} にパッケージしました{note} @msg Packaged {n} file(s){note} {n} 個のファイルをパッケージしました{note} diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index e1b986e3..6b1ef85a 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2483,6 +2483,9 @@ Unshift Desfazer Mudança de Cor Update Link Atualizar Link Update Links Atualizar Links + Update Changed Links Atualizar Links alterados + {count} linked image(s) changed on disk. Show the new versions? {count} imagem(ns) vinculada(s) alterada(s) no disco. Mostrar as novas versões? +@msg Updated {_1} linked file(s) changed on disk {_1} arquivo(s) vinculado(s) alterado(s) no disco atualizado(s) Update Modified Links Atualizar Links Modificados Use Use Use Artboards Usar Mesas de Trabalho diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 393759e7..d8825945 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2999,6 +2999,7 @@ Untitled Без названия Update Link Обновить связь Update Links Обновить связи + Update Changed Links Обновить измененные связи Update Modified Links Обновить изменённые связи Updated {} image(s) Обновлено изображений: {} UPPERCASE ПРОПИСНЫЕ @@ -3134,6 +3135,7 @@ {count} linked file(s) not found Не найдено связанных файлов: {count} fonts not copied: {fonts} шрифты не скопированы: {fonts} {count} linked image(s) changed since this document was saved. Show the new versions? Связанных изображений ({count}) изменились с момента сохранения этого документа. Показать новые версии? + {count} linked image(s) changed on disk. Show the new versions? Связанных изображений ({count}) изменились на диске. Показать новые версии? {count} more rules: Export writes every one. Ещё правил: {count}: экспорт записывает все. {count} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. Пропущено объектов без имени: {count}: назовите их или включите «Объекты без имени» в меню панели. {effect} is already applied {effect} уже применён @@ -3784,6 +3786,7 @@ axis Both Оба @msg {_1} is already used by {_2} {_1} уже используется {_2} @msg {_1} linked file(s) can't be read here: their previews show здесь невозможно прочитать связанных файлов: {_1}: отображаются их предварительные просмотры @msg {_1} linked image(s) changed on disk: Update Links shows the new versions связанных изображений ({_1}) изменились на диске: «Обновить связи» покажет новые версии +@msg Updated {_1} linked file(s) changed on disk Обновлено связанных файлов, измененных на диске: {_1} @msg {_1}: not saved, so not closed {_1}: не сохранён, поэтому не закрыт @msg {bpp}-bit bitmaps can't be read растровые изображения с глубиной {bpp} бит не читаются @msg {command} didn't create an object {command} не создал объект diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 2ff7ca51..d6bedbd6 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -2995,6 +2995,7 @@ Untitled Без назви Update Link Оновити зв’язок Update Links Оновити зв’язки + Update Changed Links Оновити змінені зв’язки Update Modified Links Оновити змінені зв’язки Updated {} image(s) Оновлено зображень: {} UPPERCASE ВЕЛИКІ ЛІТЕРИ @@ -3130,6 +3131,7 @@ {count} linked file(s) not found не знайдено пов’язаних файлів: {count} fonts not copied: {fonts} шрифти не скопійовано: {fonts} {count} linked image(s) changed since this document was saved. Show the new versions? пов’язані зображення ({count}) змінилися з моменту збереження цього документа. Показати нові версії? + {count} linked image(s) changed on disk. Show the new versions? пов’язані зображення ({count}) змінилися на диску. Показати нові версії? {count} more rules: Export writes every one. ще правил: {count}: експорт записує все. {count} unnamed objects left out: name them, or turn on Unnamed Objects in the panel menu. пропущено об’єкти без імені: {count}: назвіть їх або увімкніть «Об’єкти без імені» у меню панелі. {effect} is already applied {effect} вже застосований @@ -3771,6 +3773,7 @@ axis Both Обидві @msg {_1} is already used by {_2} {_1} вже використовується {_2} @msg {_1} linked file(s) can't be read here: their previews show тут неможливо прочитати пов’язані файли: {_1}: відображаються їх попередні перегляди @msg {_1} linked image(s) changed on disk: Update Links shows the new versions пов’язаних зображень ({_1}) змінилися на диску: «Оновити зв’язки» покаже нові версії +@msg Updated {_1} linked file(s) changed on disk Оновлено пов’язаних файлів, змінених на диску: {_1} @msg {_1}: not saved, so not closed {_1}: не збережено, тому не закрито @msg {bpp}-bit bitmaps can't be read растрові зображення з глибиною {bpp} біт не читаються @msg {command} didn't create an object {command} не створив об’єкт diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index aa8f5cf6..9ed9c7fb 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2893,6 +2893,9 @@ Untitled 未命名 Update Link 更新链接 Update Links 更新链接 + Update Changed Links 更新已更改的链接 + {count} linked image(s) changed on disk. Show the new versions? 有 {count} 个链接图像已在磁盘上更改。要显示新版本吗? +@msg Updated {_1} linked file(s) changed on disk 已更新 {_1} 个在磁盘上更改的链接文件 Update Modified Links 更新已修改的链接 Updated {} image(s) 已更新 {} 张图像 UPPERCASE 大写 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index f9b7bdc7..24a61a41 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2984,6 +2984,9 @@ Untitled 未命名 Update Link 更新連結 Update Links 更新連結 + Update Changed Links 更新已變更的連結 + {count} linked image(s) changed on disk. Show the new versions? 有 {count} 個連結影像已在磁碟上變更。要顯示新版本嗎? +@msg Updated {_1} linked file(s) changed on disk 已更新 {_1} 個在磁碟上變更的連結檔案 Update Modified Links 更新已修改的連結 Updated {} image(s) 已更新 {} 張影像 UPPERCASE 全部大寫 diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 02546253..b899c376 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -758,6 +758,47 @@ impl VectorcraftApp { } /// 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 + /// says ([`vectorcraft_engine::link_watch`]). No look starts while a dialog is open, so an + /// Ask When Modified question never goes unseen. + #[cfg(not(target_arch = "wasm32"))] + fn tick_link_updates(&mut self, ctx: &egui::Context, now: f64) { + if let Some(r) = self.session.poll_link_scan() { + match r { + Ok(r) => { + let n = |k: &str| r[k].as_array().map_or(0, Vec::len); + if n("updated") > 0 { + self.status(format!("Updated {} linked file(s) changed on disk", n("updated"))); + } + if n("ask") > 0 { + dialogs::missing_links::ask_update_changed(self, r["ask"].as_array().cloned().unwrap_or_default()); + } + if n("modified") > 0 { + self.status(format!("{} linked image(s) changed on disk: Update Links shows the new versions", n("modified"))); + } + } + Err(e) => self.status(e.to_string()), + } + } + let (last_key, focus_key) = (egui::Id::new("linkWatch.lastScan"), egui::Id::new("linkWatch.focused")); + 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 { + 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) { + ctx.data_mut(|d| d.insert_temp(last_key, now)); + self.session.start_link_scan(); + } + let wait = if self.session.link_scan.is_some() { 100 } else { 2000 }; + ctx.request_repaint_after(std::time::Duration::from_millis(wait)); + } + pub fn status(&mut self, s: impl Into) { self.ui.status = s.into(); } @@ -894,6 +935,8 @@ impl VectorcraftApp { } picks::poll(self, ctx); background::poll(self); + #[cfg(not(target_arch = "wasm32"))] + self.tick_link_updates(ctx, now); if !self.background.jobs.is_empty() { // Keep the status bar's progress moving and pick the result up when it arrives. ctx.request_repaint_after(std::time::Duration::from_millis(100));